# Next.js

URL: https://cogitator.app/docs/integrations/nextjs

> 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.

```bash
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](/docs/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](#errors).

```typescript title="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:

```typescript
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);
  },
});
```

| Option       | Type                                                          | Description                                                                                                                                                                                                                                    |
| ------------ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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:

```typescript title="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:

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

```typescript title="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`.

```tsx title="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.

```tsx title="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

| Option               | Type                                     | Description                                                                                |
| -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------ |
| `api`                | `string`                                 | API endpoint URL (required)                                                                |
| `threadId`           | `string`                                 | Initial thread ID for conversation context                                                 |
| `initialMessages`    | `ChatMessage[]`                          | Pre-populate the message list                                                              |
| `headers`            | `Record<string, string>`                 | Custom headers sent with each request                                                      |
| `onError`            | `(error: Error) => void`                 | Error callback                                                                             |
| `onFinish`           | `(message: ChatMessage) => void`         | Called when assistant message is complete (not after an `error` event)                     |
| `onToolCall`         | `(toolCall: ToolCall) => void`           | Called when a tool is invoked                                                              |
| `onToolResult`       | `(result: ToolResultEvent) => void`      | Called when a tool returns a result                                                        |
| `onReasoning`        | `(delta: string) => void`                | Called with each piece of the reasoning summary; the pieces collect in `message.reasoning` |
| `onApprovalRequired` | `(approvals: PendingApproval[]) => void` | Called when the run pauses for [approvals](#approvals)                                     |
| `resumeApi`          | `string`                                 | Endpoint of a `createResumeHandler`, for `resume`, `approve` and `deny`                    |
| `retry`              | `RetryConfig`                            | Retry on network failures and 408/429/502/503/504                                          |

### Return Values

| Field              | Type                                            | Description                                          |
| ------------------ | ----------------------------------------------- | ---------------------------------------------------- |
| `messages`         | `ChatMessage[]`                                 | All messages including live streaming                |
| `input`            | `string`                                        | Current input value                                  |
| `setInput`         | `(value: string) => void`                       | Update input                                         |
| `send`             | `(input?, metadata?) => Promise<void>`          | Send a message (`input` defaults to the input field) |
| `isLoading`        | `boolean`                                       | Whether agent is running                             |
| `error`            | `Error \| null`                                 | Last error                                           |
| `stop`             | `() => void`                                    | Abort the current stream (the partial reply is kept) |
| `reload`           | `() => Promise<void>`                           | Re-send the last user message                        |
| `threadId`         | `string \| undefined`                           | Current thread ID                                    |
| `setThreadId`      | `(id: string) => void`                          | Switch conversation thread                           |
| `appendMessage`    | `(msg: ChatMessage) => void`                    | Add a message to the list                            |
| `clearMessages`    | `() => void`                                    | Reset conversation                                   |
| `setMessages`      | `(msgs: ChatMessage[]) => void`                 | Replace message list                                 |
| `pendingApprovals` | `PendingApproval[]`                             | 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`:

```tsx
'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>
  );
}
```

| Option      | Type                              | Description                                       |
| ----------- | --------------------------------- | ------------------------------------------------- |
| `api`       | `string`                          | Endpoint of a `createAgentHandler` (required)     |
| `resumeApi` | `string`                          | Endpoint of a `createResumeHandler`, for `resume` |
| `headers`   | `Record<string, string>`          | Custom headers sent with each request             |
| `onSuccess` | `(result: AgentResponse) => void` | Called with each successful result                |
| `onError`   | `(error: Error) => void`          | Error callback                                    |
| `retry`     | `RetryConfig`                     | Retry 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:

```typescript title="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 };
}
```
