Agent Server Integration
Integrate Chat UI with @cognipeer/agent-server.
Overview
Chat UI is designed to work seamlessly with @cognipeer/agent-server. The API endpoints match exactly; the requests the UI makes are listed in Server routes.
New in 0.4.0
Approvals and questions, runs and reattach, usage limits, raw uploads, per-agent settings, and the portal routes all use agent-server 0.3.0+ features. The server-side guides: Approvals & Questions, Runs, Cancel & Reattach, Usage & Budgets, Chat experience and Embedding & limits.
Architecture
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ Chat UI │ ──────▶ │ Agent Server │
│ (React) │ SSE │ (Node.js) │
│ │ ◀────── │ │
└─────────────────┘ └─────────────────┘Setup
1. Set Up Agent Server
// server.ts
import express from "express";
import {
createAgentServer,
createPostgresProvider,
createExpressMiddleware,
} from "@cognipeer/agent-server";
import { createSmartAgent, fromLangchainModel } from "@cognipeer/agent-sdk";
import { ChatOpenAI } from "@langchain/openai";
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
const agentServer = createAgentServer({
basePath: "/api/agents",
storage,
swagger: { enabled: true },
cors: {
enabled: true,
origins: ["http://localhost:5173"], // Your frontend URL
},
});
// Register your agent
const model = fromLangchainModel(new ChatOpenAI());
const assistant = createSmartAgent({
name: "Assistant",
model,
});
agentServer.registerSDKAgent("assistant", assistant);
const app = express();
app.use(express.json());
await storage.connect();
app.use(createExpressMiddleware(agentServer));
app.listen(3000);2. Set Up Chat UI
// App.tsx
import { Chat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";
function App() {
return (
<div className="h-screen">
<Chat
baseUrl="http://localhost:3000/api/agents"
agentId="assistant"
/>
</div>
);
}With Authentication
Server
import { createTokenAuthProvider } from "@cognipeer/agent-server";
const authProvider = createTokenAuthProvider({
tokens: {
[process.env.API_KEY]: "user-1",
},
});
const agentServer = createAgentServer({
basePath: "/api/agents",
storage,
auth: {
enabled: true,
provider: authProvider,
},
});Client
<Chat
baseUrl="http://localhost:3000/api/agents"
agentId="assistant"
authorization="Bearer your-api-key"
/>With JWT Authentication
Server
import { createJWTAuthProvider } from "@cognipeer/agent-server";
const authProvider = createJWTAuthProvider({
secret: process.env.JWT_SECRET,
extractUserId: (payload) => payload.sub,
});
const agentServer = createAgentServer({
basePath: "/api/agents",
storage,
auth: {
enabled: true,
provider: authProvider,
},
});Client
import { useAuth } from "./auth-context";
function ChatPage() {
const { token } = useAuth();
return (
<Chat
baseUrl="/api/agents"
agentId="assistant"
authorization={`Bearer ${token}`}
/>
);
}Next.js Full Stack
API Route
// app/api/agents/[...path]/route.ts
import {
createAgentServer,
createPostgresProvider,
createNextRouteHandlers,
} from "@cognipeer/agent-server";
const storage = createPostgresProvider({
connectionString: process.env.DATABASE_URL,
});
const agentServer = createAgentServer({
basePath: "/api/agents",
storage,
});
// Register agents...
await storage.connect();
export const { GET, POST, PATCH, DELETE, OPTIONS } = createNextRouteHandlers(agentServer);Client Page
// app/chat/page.tsx
"use client";
import { Chat } from "@cognipeer/chat-ui";
import "@cognipeer/chat-ui/styles.css";
export default function ChatPage() {
return (
<div className="h-screen">
<Chat
baseUrl="/api/agents"
agentId="assistant"
/>
</div>
);
}Multiple Agents
Register multiple agents on the server:
agentServer.registerSDKAgent("assistant", assistantAgent, {
description: "General purpose assistant",
});
agentServer.registerSDKAgent("code-helper", codeAgent, {
description: "Helps with coding tasks",
});
agentServer.registerCustomAgent("simple-bot", {
processMessage: async ({ message }) => ({
content: `You said: ${message}`,
}),
});Use different agents in the UI:
function AgentSelector() {
const [agentId, setAgentId] = useState("assistant");
return (
<div>
<select value={agentId} onChange={(e) => setAgentId(e.target.value)}>
<option value="assistant">Assistant</option>
<option value="code-helper">Code Helper</option>
<option value="simple-bot">Simple Bot</option>
</select>
<Chat
key={agentId} // Reset on agent change
baseUrl="/api/agents"
agentId={agentId}
/>
</div>
);
}Custom Headers
<Chat
baseUrl="/api/agents"
agentId="assistant"
headers={{
"X-Custom-Header": "value",
"X-Tenant-ID": "tenant-123",
}}
/>Approvals and questions
With @cognipeer/agent-server 0.3.0 or later, an agent can pause for an approval or a question. Chat shows the card and sends the answer to POST /conversations/:conversationId/actions/:actionId (body { resolution, stream? }); GET /conversations/:id lists the actions still waiting. See Approvals and Questions.
Runs, Stop and reattach
With agent-server 0.3.0 or later a turn runs independently of the request that started it. Chat uses that:
- A page that reloads, or opens a conversation, while a turn is still running follows it (
GET /conversations/:id/events/stream?after=<runtime.lastEventSeq>) and shows the text and tool calls as they happen, then the stored answer. - Stop calls
POST /conversations/:id/cancel, so the run really stops, and keeps what was written as an answer marked "Stopped". - A dropped connection is not an error: the page looks at the conversation again and follows or shows what finished.
runtimeonGET /conversationsandGET /conversations/:iddrives the status badges in the history, andstopReasonon the messages the notice under an answer.- A conversation with a usage limit shows a meter, and
PATCH /conversations/:id { budget }andPOST /conversations/:id/continueraise it and carry on, where the host allows.
See Running turns, Stop and reattach. A server older than 0.3.0 has none of these routes and everything stays as it was.
Files
GET /agents tells the chat what each agent takes (capabilities.attachments), POST /files takes the file as itself (raw body with progress), and GET /conversations/:id/files is the count the chat shows. See File uploads.
Portal experience routes
Regenerate, edit, feedback, rename, pin, bulk delete and export use the routes PATCH /conversations/:id, POST /conversations/bulk-delete, POST /conversations/:id/messages/:messageId/{regenerate,edit,feedback} and GET /conversations/:id/export. The host switches each feature on with a prop and each agent can take it away through metadata.experience; the server refuses a disallowed action with 403 action_disabled. See Portal Experience and Chat experience.
Per-agent settings
GET /agents can say what each agent takes, and the chat adapts: capabilities.attachments (limits, accepted types, whether images are read, retention), capabilities.approvalArgsEdit, capabilities.budget and metadata (pendingActionComposerMode, starters, welcome, experience). The agent's value wins over the matching prop. See Running Turns.
Error codes
Failed requests carry the server's code, and Chat shows a localized message for the well-known ones (conversation_busy, action_pending, action_expired, rate_limited, message_too_long, file_too_large, ...). A send refused because a card is waiting or a turn is running makes the page fetch the conversation again, so the card or the latest answer shows. See Approvals and Questions.
Direct API Client Usage
For custom implementations:
import { AgentServerClient } from "@cognipeer/chat-ui";
const client = new AgentServerClient({
baseUrl: "/api/agents",
authorization: "Bearer token",
});
// List conversations
const { conversations } = await client.getConversations();
// Create conversation (the agent is named here)
const conversation = await client.createConversation({ agentId: "assistant", title: "New Chat" });
// Send message
const { response } = await client.sendMessage(conversation.id, { message: "Hello!" });
// Stream response
await client.sendMessageStream(
conversation.id,
{ message: "Hello!" },
{ onText: (chunk, fullText) => console.log(fullText) }
);
// Follow a conversation's event log (replay, then live) until aborted
const controller = new AbortController();
await client.followEvents(conversation.id, {
after: 0,
signal: controller.signal,
onEvent: (event) => console.log(event.seq, event.type, event.data),
});
// Stop the run on the server
await client.cancel(conversation.id);
