Skip to content

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 ​

typescript
// 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 ​

tsx
// 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 ​

typescript
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 ​

tsx
<Chat
  baseUrl="http://localhost:3000/api/agents"
  agentId="assistant"
  authorization="Bearer your-api-key"
/>

With JWT Authentication ​

Server ​

typescript
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 ​

tsx
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 ​

typescript
// 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 ​

tsx
// 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:

typescript
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:

tsx
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 ​

tsx
<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.
  • runtime on GET /conversations and GET /conversations/:id drives the status badges in the history, and stopReason on the messages the notice under an answer.
  • A conversation with a usage limit shows a meter, and PATCH /conversations/:id { budget } and POST /conversations/:id/continue raise 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:

tsx
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);

Next Steps ​

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub