Cogitator
Integrations

Next.js

First-class Next.js integration with streaming API route handlers and React hooks for building chat interfaces.

Overview

The @cogitator-ai/next package provides everything you need to build AI-powered Next.js applications: server-side route handlers that stream responses via SSE, and client-side React hooks that consume them with full tool call support.

pnpm add @cogitator-ai/next @cogitator-ai/core

Server: Chat Handler

createChatHandler turns a Cogitator agent into a streaming API route handler. It accepts a standard Request, runs the agent with token streaming enabled, and returns an SSE stream of typed events (start, text-*, reasoning-*, tool-call-*, tool-result, approval-required, finish, error) ending with data: [DONE]. The events are the server adapters' streaming protocol, with the threadId added to finish — consume them with useCogitatorChat. finish carries the run's usage, the same object createAgentHandler answers: inputTokens, outputTokens, totalTokens, and reasoningTokens, cachedInputTokens and cacheWriteTokens when the model reported them. The handlers are plain (req: Request) => Promise<Response> functions, so they also work in other fetch-based runtimes.

The default body is { messages, threadId?, metadata? }.

  • The last user message is the run input; history lives in the thread (configure memory on Cogitator). The finish event carries the threadId, and useCogitatorChat adopts it automatically.
  • Request metadata is passed to the run as context.
  • The run is aborted when the client disconnects.
  • An agent with reasoning: { summary: true } streams its summary as reasoning-start / reasoning-delta / reasoning-end; a reasoning or text block is closed before the other kind, a tool call or finish starts.
  • Bodies over 1 MB get 413; malformed bodies or a request without a user message get 400.
  • A failed run ends the stream with an error event — see Errors.
app/api/chat/route.ts
import { Cogitator, Agent } from '@cogitator-ai/core';
import { createChatHandler } from '@cogitator-ai/next';

const cog = new Cogitator({
  llm: {
    defaultProvider: 'openai',
    providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } },
  },
});

const agent = new Agent({
  name: 'assistant',
  model: 'openai/gpt-6.1-sol',
  instructions: 'You are a helpful assistant.',
});

const handler = createChatHandler(cog, agent);

export const POST = handler;

ChatHandlerOptions

You can pass options to customize parsing, authentication, and post-processing:

const handler = createChatHandler(cog, agent, {
  beforeRun: async (req, input) => {
    const token = req.headers.get('authorization');
    if (!token) throw new Error('Unauthorized');
    const user = await verifyToken(token);
    return { userId: user.id };
  },

  afterRun: async (result) => {
    await db.saveChatMessage(result.threadId, result.output);
  },
});
OptionTypeDescription
parseInput(req: Request) => Promise<ChatInput>Custom request body parser
beforeRun(req: Request, input: ChatInput) => Promise<object | void>Auth hook; the returned object is merged into the run options (userId, threadId, ...) but cannot replace stream, signal or the streaming callbacks. Throw an error with a status property to reject with that status (default 401)
afterRun(result: RunResult) => Promise<void>Post-processing hook (save to DB, log, etc.). Runs before finish; a failure is reported as an error event

Server: Agent Handler

For non-streaming use cases, createAgentHandler returns a JSON response with the full result including tool calls, traces, and usage. The default parser requires a non-blank string input (plus optional object context and string threadId) and answers 400 otherwise:

app/api/agent/route.ts
import { createAgentHandler } from '@cogitator-ai/next';

const handler = createAgentHandler(cog, agent, {
  beforeRun: async (req, input) => {
    return { userId: 'user-123' };
  },
});

export const POST = handler;

The response shape:

interface AgentResponse {
  output: string;
  threadId: string;
  usage: {
    inputTokens: number;
    outputTokens: number;
    totalTokens: number;
    reasoningTokens?: number;
    cachedInputTokens?: number;
    cacheWriteTokens?: number;
  };
  toolCalls: ToolCall[];
  trace?: { traceId: string; spans: unknown[] };
  reasoning?: string;
  status?: 'completed' | 'paused';
  pendingApprovals?: PendingApproval[];
}

Errors

Every handler answers request problems before running the agent, as JSON { "error": "<message>" }:

  • 413 for bodies over 1 MB, 400 for invalid JSON, a body that fails the default parser, or an error thrown by parseInput.
  • An error thrown by beforeRun uses its status property (400–599) and defaults to 401, with the error's message.

A failed run answers { "error": "<message>", "code": "<code>" } — or, in a stream, an error event with the same message and code. A CogitatorError keeps its message, code and status (for example 403 THREAD_ACCESS_DENIED, 429 LLM_RATE_LIMITED); any other error is logged on the server and reported as 500 with Internal server error and code INTERNAL_ERROR, so its text never reaches the client.

Approvals

A tool with requiresApproval pauses the run before it executes. The runtime keeps the checkpoint on the server, per thread; clients only see the calls waiting for a decision (toolCallId, toolName, arguments, description, sideEffects). createAgentHandler answers with status: 'paused' and pendingApprovals, and createChatHandler streams { type: 'approval-required', threadId, approvals } right before finish.

createResumeHandler continues the run. Its body is { threadId, decisions?, defaultDecision? }, a decision being { approved: true } or { approved: false, reason? } by tool call id. Approved calls run, declined ones answer the model with the reason, and undecided ones pause the run again:

app/api/chat/resume/route.ts
import { createResumeHandler } from '@cogitator-ai/next';

export const POST = createResumeHandler(cog, agent, {
  stream: true,
  beforeRun: async (req) => ({ userId: await getUserId(req) }),
});

It answers like createAgentHandler, or streams like createChatHandler with stream: true. Return the caller's userId from beforeRun: only the user the run belongs to may resume it (403 THREAD_ACCESS_DENIED), and a thread without a paused run answers 409 RUN_NOT_PAUSED.

components/chat.tsx
const { pendingApprovals, approve, deny, resume } = useCogitatorChat({
  api: '/api/chat',
  resumeApi: '/api/chat/resume',
  onApprovalRequired: (approvals) => console.log('Waiting for', approvals),
});

return (
  <>
    {pendingApprovals.map((call) => (
      <p key={call.toolCallId}>
        {call.toolName}: {JSON.stringify(call.arguments)}
        <button onClick={() => resume({ decisions: { [call.toolCallId]: { approved: true } } })}>
          Approve this one
        </button>
      </p>
    ))}
    <button onClick={() => approve()}>Approve all</button>
    <button onClick={() => deny('Over the refund limit')}>Deny all</button>
  </>
);

The rest of the run streams into a new assistant message (a JSON resumeApi is appended as one message). pendingApprovals clears once the resume is accepted, when the server has nothing paused anymore (409), or when the user sends a new message, which declines the waiting calls. useCogitatorAgent({ api, resumeApi }) exposes the pendingApprovals of its last result and resume(decisions).

Client: useCogitatorChat

The useCogitatorChat hook manages the entire chat lifecycle -- sending messages, processing the SSE stream, handling tool calls, and tracking loading state.

components/chat.tsx
'use client';

import { useCogitatorChat } from '@cogitator-ai/next/client';

export function Chat() {
  const { messages, input, setInput, send, isLoading, stop, reload, error } = useCogitatorChat({
    api: '/api/chat',
    onToolCall: (toolCall) => {
      console.log(`Tool invoked: ${toolCall.name}`, toolCall.arguments);
    },
    onFinish: (message) => {
      console.log('Assistant replied:', message.content);
    },
    retry: { maxRetries: 2, backoff: 'exponential' },
  });

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong> {msg.content}
        </div>
      ))}

      {error && <p>Error: {error.message}</p>}

      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && send()}
        disabled={isLoading}
      />

      {isLoading && <button onClick={stop}>Stop</button>}
    </div>
  );
}

UseChatOptions

OptionTypeDescription
apistringAPI endpoint URL (required)
threadIdstringInitial thread ID for conversation context
initialMessagesChatMessage[]Pre-populate the message list
headersRecord<string, string>Custom headers sent with each request
onError(error: Error) => voidError callback
onFinish(message: ChatMessage) => voidCalled when assistant message is complete (not after an error event)
onToolCall(toolCall: ToolCall) => voidCalled when a tool is invoked
onToolResult(result: ToolResultEvent) => voidCalled when a tool returns a result
onReasoning(delta: string) => voidCalled with each piece of the reasoning summary; the pieces collect in message.reasoning
onApprovalRequired(approvals: PendingApproval[]) => voidCalled when the run pauses for approvals
resumeApistringEndpoint of a createResumeHandler, for resume, approve and deny
retryRetryConfigRetry on network failures and 408/429/502/503/504

Return Values

FieldTypeDescription
messagesChatMessage[]All messages including live streaming
inputstringCurrent input value
setInput(value: string) => voidUpdate input
send(input?, metadata?) => Promise<void>Send a message (input defaults to the input field)
isLoadingbooleanWhether agent is running
errorError | nullLast error
stop() => voidAbort the current stream (the partial reply is kept)
reload() => Promise<void>Re-send the last user message
threadIdstring | undefinedCurrent thread ID
setThreadId(id: string) => voidSwitch conversation thread
appendMessage(msg: ChatMessage) => voidAdd a message to the list
clearMessages() => voidReset conversation
setMessages(msgs: ChatMessage[]) => voidReplace message list
pendingApprovalsPendingApproval[]Tool calls the paused run waits on
resume(decisions: ResumeDecisions) => Promise<void>Continue the paused run with these decisions
approve() => Promise<void>Approve every pending call
deny(reason?: string) => Promise<void>Decline every pending call

HTTP failures surface as HttpError (exported from @cogitator-ai/next/client) with a status field and a message such as Request failed: 400 - No user message provided.

Client: useCogitatorAgent

For non-streaming agent calls (task execution, one-shot questions), use useCogitatorAgent:

'use client';

import { useCogitatorAgent } from '@cogitator-ai/next/client';

export function AgentRunner() {
  const { run, result, isLoading, error, reset } = useCogitatorAgent({
    api: '/api/agent',
    onSuccess: (res) => console.log('Done:', res.output),
  });

  return (
    <div>
      <button onClick={() => run({ input: "Summarize today's news" })} disabled={isLoading}>
        Run Agent
      </button>
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  );
}
OptionTypeDescription
apistringEndpoint of a createAgentHandler (required)
resumeApistringEndpoint of a createResumeHandler, for resume
headersRecord<string, string>Custom headers sent with each request
onSuccess(result: AgentResponse) => voidCalled with each successful result
onError(error: Error) => voidError callback
retryRetryConfigRetry on network failures and 408/429/502/503/504

It returns run(input), result, reasoning, pendingApprovals, resume(decisions), isLoading, error and reset().

createAgentHandler returns the run's reasoning summary as reasoning (also exposed by the hook as reasoning), and usage carries reasoningTokens, cachedInputTokens and cacheWriteTokens when the provider reports them. Starting a new run() aborts the previous request, and reset() cancels the in-flight one.

Server Actions

You can also use Cogitator directly inside Next.js Server Actions without the HTTP layer:

app/actions.ts
'use server';

import { Cogitator, Agent } from '@cogitator-ai/core';

const cog = new Cogitator({/* ... */});

const agent = new Agent({
  name: 'summarizer',
  model: 'openai/gpt-6-luna',
  instructions: 'Summarize the given text concisely.',
});

export async function summarize(text: string) {
  const result = await cog.run(agent, { input: text });
  return { summary: result.output, usage: result.usage };
}

On this page