|
| 1 | +--- |
| 2 | +title: "Server-Side Chat" |
| 3 | +sidebarTitle: "Server-Side Chat" |
| 4 | +description: "Use AgentChat to interact with chat agents from server-side code — tasks, webhooks, scripts, or other agents." |
| 5 | +--- |
| 6 | + |
| 7 | +`AgentChat` lets you chat with agents from server-side code. It works inside tasks (agent-to-agent), request handlers, webhook processors, and scripts. |
| 8 | + |
| 9 | +```ts |
| 10 | +import { AgentChat } from "@trigger.dev/sdk/chat"; |
| 11 | + |
| 12 | +const chat = new AgentChat({ agent: "my-agent" }); |
| 13 | +const stream = await chat.sendMessage("Hello!"); |
| 14 | +const text = await stream.text(); |
| 15 | +await chat.close(); |
| 16 | +``` |
| 17 | + |
| 18 | +## Type-safe client data |
| 19 | + |
| 20 | +Pass `typeof yourAgent` as a type parameter and `clientData` is automatically typed from the agent's `withClientData` schema: |
| 21 | + |
| 22 | +```ts |
| 23 | +import { AgentChat } from "@trigger.dev/sdk/chat"; |
| 24 | +import type { myAgent } from "./trigger/my-agent"; |
| 25 | + |
| 26 | +const chat = new AgentChat<typeof myAgent>({ |
| 27 | + agent: "my-agent", |
| 28 | + clientData: { userId: "user_123" }, // ← typed from agent definition |
| 29 | +}); |
| 30 | +``` |
| 31 | + |
| 32 | +## Conversation lifecycle |
| 33 | + |
| 34 | +Each `AgentChat` instance represents one conversation. The conversation ID is auto-generated or can be set explicitly: |
| 35 | + |
| 36 | +```ts |
| 37 | +// Auto-generated ID |
| 38 | +const chat = new AgentChat({ agent: "my-agent" }); |
| 39 | + |
| 40 | +// Explicit ID — useful for persistence or finding the run later |
| 41 | +const chat = new AgentChat({ agent: "my-agent", id: `review-${prNumber}` }); |
| 42 | +``` |
| 43 | + |
| 44 | +### Sending messages |
| 45 | + |
| 46 | +`sendMessage()` triggers a new run on the first call, then reuses the same run for subsequent messages via input streams: |
| 47 | + |
| 48 | +```ts |
| 49 | +// First message — triggers a new run |
| 50 | +const stream1 = await chat.sendMessage("Review PR #42"); |
| 51 | +const review = await stream1.text(); |
| 52 | + |
| 53 | +// Follow-up — same run, agent has full context |
| 54 | +const stream2 = await chat.sendMessage("Can you fix the main bug?"); |
| 55 | +const fix = await stream2.text(); |
| 56 | +``` |
| 57 | + |
| 58 | +### Preloading (optional) |
| 59 | + |
| 60 | +If you want the agent to initialize before the first message (e.g., load data, authenticate), call `preload()`. This is optional — `sendMessage()` triggers the run automatically if needed. |
| 61 | + |
| 62 | +```ts |
| 63 | +await chat.preload(); |
| 64 | +// Agent's onPreload hook fires now, before user types anything |
| 65 | +const stream = await chat.sendMessage("Hello"); |
| 66 | +``` |
| 67 | + |
| 68 | +### Closing |
| 69 | + |
| 70 | +Signal the agent to exit its loop gracefully: |
| 71 | + |
| 72 | +```ts |
| 73 | +await chat.close(); |
| 74 | +``` |
| 75 | + |
| 76 | +Without `close()`, the agent exits on its own when its idle/suspend timeout expires. |
| 77 | + |
| 78 | +## Reading responses |
| 79 | + |
| 80 | +`sendMessage()` returns a `ChatStream` — a typed wrapper around the response. |
| 81 | + |
| 82 | +### Get the full text |
| 83 | + |
| 84 | +```ts |
| 85 | +const stream = await chat.sendMessage("What is Trigger.dev?"); |
| 86 | +const text = await stream.text(); |
| 87 | +``` |
| 88 | + |
| 89 | +### Get structured results |
| 90 | + |
| 91 | +```ts |
| 92 | +const stream = await chat.sendMessage("Research this topic"); |
| 93 | +const { text, toolCalls, toolResults } = await stream.result(); |
| 94 | + |
| 95 | +for (const tc of toolCalls) { |
| 96 | + console.log(`Tool: ${tc.toolName}, Input: ${JSON.stringify(tc.input)}`); |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### Stream chunks in real-time |
| 101 | + |
| 102 | +```ts |
| 103 | +const stream = await chat.sendMessage("Write a report"); |
| 104 | + |
| 105 | +for await (const chunk of stream) { |
| 106 | + if (chunk.type === "text-delta") { |
| 107 | + process.stdout.write(chunk.delta); |
| 108 | + } |
| 109 | + if (chunk.type === "tool-input-available") { |
| 110 | + console.log(`Using tool: ${chunk.toolName}`); |
| 111 | + } |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +## Stateless request handlers |
| 116 | + |
| 117 | +In a stateless environment (HTTP handler, serverless function), you need to persist and restore the session across requests. |
| 118 | + |
| 119 | +`AgentChat` provides a `session` option and two callbacks for this: |
| 120 | + |
| 121 | +```ts |
| 122 | +import { AgentChat } from "@trigger.dev/sdk/chat"; |
| 123 | + |
| 124 | +export async function POST(req: Request) { |
| 125 | + const { chatId, message, runId, lastEventId } = await req.json(); |
| 126 | + |
| 127 | + const chat = new AgentChat({ |
| 128 | + agent: "my-agent", |
| 129 | + id: chatId, |
| 130 | + // Restore from previous request |
| 131 | + session: runId ? { runId, lastEventId } : undefined, |
| 132 | + // Persist when a new run starts |
| 133 | + onTriggered: async ({ runId, chatId }) => { |
| 134 | + await db.sessions.upsert({ chatId, runId }); |
| 135 | + }, |
| 136 | + // Persist after each turn for stream resumption |
| 137 | + onTurnComplete: async ({ lastEventId, chatId }) => { |
| 138 | + await db.sessions.update({ chatId, lastEventId }); |
| 139 | + }, |
| 140 | + }); |
| 141 | + |
| 142 | + const stream = await chat.sendMessage(message); |
| 143 | + const text = await stream.text(); |
| 144 | + |
| 145 | + return Response.json({ text, runId: chat.run?.runId }); |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +## Sub-agent tool pattern |
| 150 | + |
| 151 | +`AgentChat` can be used inside an AI SDK tool to delegate work to a durable sub-agent. The sub-agent's response streams as preliminary tool results: |
| 152 | + |
| 153 | +```ts |
| 154 | +import { tool } from "ai"; |
| 155 | +import { AgentChat } from "@trigger.dev/sdk/chat"; |
| 156 | +import { z } from "zod"; |
| 157 | + |
| 158 | +const researchTool = tool({ |
| 159 | + description: "Delegate research to a specialist agent.", |
| 160 | + inputSchema: z.object({ topic: z.string() }), |
| 161 | + execute: async function* ({ topic }, { abortSignal }) { |
| 162 | + const chat = new AgentChat({ agent: "research-agent" }); |
| 163 | + const stream = await chat.sendMessage(topic, { abortSignal }); |
| 164 | + yield* stream.messages(); |
| 165 | + await chat.close(); |
| 166 | + }, |
| 167 | + toModelOutput: ({ output: message }) => { |
| 168 | + const lastText = message?.parts?.findLast( |
| 169 | + (p: { type: string }) => p.type === "text" |
| 170 | + ) as { text?: string } | undefined; |
| 171 | + return { type: "text", value: lastText?.text ?? "Done." }; |
| 172 | + }, |
| 173 | +}); |
| 174 | +``` |
| 175 | + |
| 176 | +This supports single-turn delegation, multi-turn LLM-driven conversations with persistent sub-agents, and cross-turn state that survives snapshot/restore. |
| 177 | + |
| 178 | +See the [Sub-Agents guide](/ai-chat/sub-agents) for the full pattern including multi-turn conversations, cleanup, and what the frontend sees. |
| 179 | + |
| 180 | +## Additional methods |
| 181 | + |
| 182 | +### Steering |
| 183 | + |
| 184 | +Send a message during an active stream without interrupting it: |
| 185 | + |
| 186 | +```ts |
| 187 | +await chat.steer("Focus on security issues specifically"); |
| 188 | +``` |
| 189 | + |
| 190 | +### Stop generation |
| 191 | + |
| 192 | +Abort the current `streamText` call without ending the run: |
| 193 | + |
| 194 | +```ts |
| 195 | +await chat.stop(); |
| 196 | +``` |
| 197 | + |
| 198 | +### Raw messages |
| 199 | + |
| 200 | +For full control over the UIMessage shape: |
| 201 | + |
| 202 | +```ts |
| 203 | +const rawStream = await chat.sendRaw([ |
| 204 | + { |
| 205 | + id: "msg-1", |
| 206 | + role: "user", |
| 207 | + parts: [ |
| 208 | + { type: "text", text: "Hello" }, |
| 209 | + { type: "file", url: "https://...", mediaType: "image/png" }, |
| 210 | + ], |
| 211 | + }, |
| 212 | +]); |
| 213 | +``` |
| 214 | + |
| 215 | +### Reconnect |
| 216 | + |
| 217 | +Resume a stream subscription after a disconnect: |
| 218 | + |
| 219 | +```ts |
| 220 | +const stream = await chat.reconnect(); |
| 221 | +``` |
| 222 | + |
| 223 | +## AgentChat options |
| 224 | + |
| 225 | +| Option | Type | Default | Description | |
| 226 | +|---|---|---|---| |
| 227 | +| `agent` | `string` | required | The agent task ID to trigger | |
| 228 | +| `id` | `string` | `crypto.randomUUID()` | Conversation ID for tagging and correlation | |
| 229 | +| `clientData` | typed from agent | `undefined` | Client data included in every request | |
| 230 | +| `session` | `{ runId: string; lastEventId?: string }` | `undefined` | Restore a previous session | |
| 231 | +| `onTriggered` | `(event) => void` | `undefined` | Called when a new run is created | |
| 232 | +| `onTurnComplete` | `(event) => void` | `undefined` | Called when a turn's stream ends | |
| 233 | +| `streamKey` | `string` | `"chat"` | Output stream key | |
| 234 | +| `streamTimeoutSeconds` | `number` | `120` | SSE timeout in seconds | |
| 235 | +| `triggerOptions` | `object` | `undefined` | Tags, queue, machine, priority | |
| 236 | + |
| 237 | +## ChatStream methods |
| 238 | + |
| 239 | +| Method | Returns | Description | |
| 240 | +|---|---|---| |
| 241 | +| `text()` | `Promise<string>` | Consume stream, return accumulated text | |
| 242 | +| `result()` | `Promise<ChatStreamResult>` | Consume stream, return `{ text, toolCalls, toolResults }` | |
| 243 | +| `messages()` | `AsyncGenerator<UIMessage>` | Yield accumulated UIMessage snapshots (sub-agent pattern) | |
| 244 | +| `[Symbol.asyncIterator]` | `UIMessageChunk` | Iterate over typed stream chunks | |
| 245 | +| `.stream` | `ReadableStream<UIMessageChunk>` | Raw stream for AI SDK utilities | |
0 commit comments