You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(ai-chat): custom agents page, backend decision table, and a building-agents anatomy entry (#3921)
## Summary
Documents the two lower-level chat backend APIs and restructures the
Building agents section so it has a sane reading order.
**Custom agents page.** `chat.customAgent()` was effectively
undocumented (one passing mention) and `chat.createSession()` was buried
at the bottom of the Backend page, prompted by a customer asking whether
dropping down a level was supported at all. Both now live on one
dedicated page framed as a composition: register with `customAgent`,
then drive turns with the managed `createSession` iterator or a
hand-rolled primitives loop. The page covers the patterns the managed
lifecycle otherwise handles for you, each verified against a running
agent: seeding history on continuation runs (and why the seed must go
through the turn-0 `addIncoming`, which replaces the accumulator),
persisting the user message before streaming so a mid-stream reload
keeps it, racing `totalUsage` after a stop so the loop cannot wedge, and
the single-message wire shape.
**Backend page.** Now leads with a decision table across the three
abstraction levels and focuses on `chat.agent()`, routing to the new
page. Stale examples that read a plural `messages` field off the wire
payload are fixed (copy-pasting them broke turn accumulation), and the
ChatSessionOptions / ChatTurn reference tables gain their missing rows
(`compaction`, `pendingMessages`, usage fields, `setMessages`,
`prepareStep`).
**Anatomy page + reorder.** The Building agents group opened with the
long How it works mechanics page, a wall right after the Quick Start. A
short Anatomy page now leads the group: the three moving parts, one
annotated example where each region names the page that covers it, and a
routing table. How it works moves to the end of the group as the depth
payoff, matching where peer docs put their internals pages.
All pages visually verified against a local Mintlify build; cross-links
and anchors updated across the section.
**A chat agent is three parts: a long-lived agent task that runs the turn loop, a durable Session carrying messages in and the response stream out, and a frontend transport that plugs the session into `useChat`.** The pages in this section each own one part of that picture. This page is the map — if you'd rather read mechanics end to end, skip to [How it works](/ai-chat/how-it-works).
// Tools declared on the config survive history re-conversion
32
+
// across turns — see Tools.
33
+
tools: { searchDocs },
34
+
35
+
// Hooks fire around each turn: validation, persistence,
36
+
// post-turn work — see Lifecycle hooks.
37
+
onTurnComplete: async ({ responseMessage }) => {
38
+
awaitdb.messages.save(responseMessage);
39
+
},
40
+
41
+
// The turn loop. Messages arrive accumulated; you stream back.
42
+
// Options, levels, and alternatives — see Backend.
43
+
run: async ({ messages, tools, signal }) =>
44
+
streamText({
45
+
...chat.toStreamTextOptions({ tools }),
46
+
model: anthropic("claude-sonnet-4-5"),
47
+
messages,
48
+
abortSignal: signal,
49
+
stopWhen: stepCountIs(15),
50
+
}),
51
+
});
52
+
```
53
+
54
+
The frontend side is one hook — `useTriggerChatTransport` connects `useChat` to the agent's session, no API routes ([Frontend](/ai-chat/frontend)). Underneath, the conversation lives on a [Session](/ai-chat/sessions): a pair of durable streams keyed on your `chatId` that survives refreshes, deploys, and run boundaries.
@@ -8,6 +8,22 @@ import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8
8
9
9
<RcBanner />
10
10
11
+
There are three abstraction levels for a chat backend. All three speak the same wire protocol, so the [frontend transport](/ai-chat/frontend) works unchanged whichever you pick.
12
+
13
+
| Capability |`chat.agent()`|`chat.createSession()`| Raw primitives |
The raw-primitives column assumes [`chat.customAgent()`](/ai-chat/custom-agents) as the wrapper, which is what makes the task visible to the agent dashboard.
24
+
25
+
Start with `chat.agent()`. Drop to `chat.createSession()` when you want to own the per-turn code (model routing, persistence, custom telemetry) without rebuilding the turn loop. Drop to raw primitives only when you need full control over stream conversion or a custom protocol.
26
+
11
27
## chat.agent()
12
28
13
29
The highest-level approach. Handles message accumulation, stop signals, turn lifecycle, and auto-piping automatically.
@@ -119,7 +135,7 @@ writer.write({
119
135
</Info>
120
136
121
137
<Note>
122
-
`chat.response` and the `writer` accumulation behavior work with `chat.agent` and `chat.createSession`. If you're using [`chat.customAgent`](#raw-task-with-primitives), you own the accumulator — see the raw-task example for the manual pattern.
138
+
`chat.response` and the `writer` accumulation behavior work with `chat.agent` and `chat.createSession`. If you're using [`chat.customAgent`](/ai-chat/custom-agents), you own the accumulator — see the raw-task example for the manual pattern.
123
139
</Note>
124
140
125
141
### Raw streaming with `chat.stream`
@@ -750,7 +766,7 @@ See [ChatUIMessageStreamOptions](/ai-chat/reference#chatuimessagestreamoptions)
750
766
<Note>
751
767
`onFinish` is managed internally for response capture and cannot be overridden here. Use
752
768
`streamText`'s `onFinish` callback for custom finish handling, or use [raw task
753
-
mode](#raw-task-with-primitives) for full control over `toUIMessageStream()`.
769
+
mode](/ai-chat/custom-agents) for full control over `toUIMessageStream()`.
A middle ground between `chat.agent()` and raw primitives. You get an async iterator that yields `ChatTurn` objects — each turn handles stop signals, message accumulation, and turn-complete signaling automatically. You control initialization, model/tool selection, persistence, and any custom per-turn logic.
793
-
794
-
Use `chat.createSession()` inside a standard `task()`:
|`turn.complete(source)`| Pipe stream, capture response, accumulate, and signal turn-complete |
864
-
|`turn.done()`| Just signal turn-complete (when you've piped manually) |
865
-
|`turn.addResponse(response)`| Add a response to the accumulator manually |
866
-
867
-
### turn.complete() vs manual control
868
-
869
-
`turn.complete(result)` is the easy path — it handles piping, capturing the response, accumulating messages, cleaning up aborted parts, and writing the turn-complete chunk.
For full control, use a standard `task()` with the composable primitives from the `chat` namespace. You manage everything: the turn loop, stop signals, message accumulation, and turn-complete signaling.
903
-
904
-
Raw task mode also lets you call `.toUIMessageStream()` yourself with any options — including `onFinish` and `originalMessages`. This is the right choice when you need complete control over the stream conversion beyond what `chat.setUIMessageStreamOptions()` provides.
{/* Anchor stubs for inbound deep links to the sections that moved to /ai-chat/custom-agents. */}
807
+
<aid="chat-createsession" />
808
+
<aid="chat-customagent" />
809
+
<aid="raw-task-with-primitives" />
982
810
983
-
// Persist, analytics, etc.
984
-
awaitdb.chat.update({
985
-
where: { id: currentPayload.chatId },
986
-
data: { messages: conversation.uiMessages },
987
-
});
811
+
## Custom agents
988
812
989
-
awaitchat.writeTurnComplete();
813
+
Both lower levels — `chat.createSession()` (managed turn iterator, your turn body) and `chat.customAgent()` with raw primitives (hand-rolled loop, full stream-conversion control) — are covered together on the Custom agents page, including the `ChatTurn` surface, the continuation-seeding pattern, and the hand-rolled-loop checklist:
990
814
991
-
// Wait for the next message
992
-
const next =awaitchat.messages.waitWithIdleTimeout({
993
-
idleTimeoutInSeconds: 60,
994
-
timeout: "1h",
995
-
spanName: "waiting for next message",
996
-
});
997
-
if (!next.ok) break;
998
-
currentPayload=next.output;
999
-
}
1000
-
1001
-
stop.cleanup();
1002
-
},
1003
-
});
1004
-
```
1005
-
1006
-
### MessageAccumulator
1007
-
1008
-
The `MessageAccumulator` handles the transport protocol automatically:
1009
-
1010
-
- Turn 0: replaces messages (full history from frontend)
1011
-
- Subsequent turns: appends new messages (frontend only sends the new user message)
1012
-
- Regenerate: replaces messages (full history minus last assistant message)
1013
-
1014
-
```ts
1015
-
const conversation =newchat.MessageAccumulator();
1016
-
1017
-
// Returns full accumulated ModelMessage[] for streamText
0 commit comments