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): document the head-start persistence and handover contract
Adds a persistence section to the fast starts page covering the stable
assistant messageId across the handover, onTurnComplete as the canonical
persistence point, reasoning parts surviving into durable history, and
how Head Start composes with hydrateMessages. Switches the hydrate hook
examples to upsert their conversation row, since head-start first turns
run without a preload to create it.
Copy file name to clipboardExpand all lines: docs/ai-chat/fast-starts.mdx
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -524,8 +524,27 @@ The handler keeps the SSE response open until the agent run signals turn-complet
524
524
| Tool execution runs in | Trigger.dev agent run | Trigger.dev agent run |
525
525
| Step 2+ LLM call runs in | Trigger.dev agent run | Trigger.dev agent run |
526
526
|`onChatStart` / `onTurnStart` fire | After handover signal arrives | Normally |
527
+
|`hydrateMessages` fires (if registered) | After handover, with the first-turn history as `incomingMessages`| Normally |
527
528
|`onTurnComplete` fires | After turn finishes (handover) or skipped (handover-skip) | Normally |
528
529
530
+
### Persistence and the handover contract
531
+
532
+
A head-start turn persists exactly like a normal turn — the handover machinery is invisible to your hooks. The guarantees:
533
+
534
+
-**One stable assistant `messageId` across the whole turn.** The route handler generates the id, the handover signal carries it to the agent, and the agent's step 2+ stream reuses it — so the browser merges step 1 and step 2+ into a single assistant message, and you can merge-by-id when persisting.
535
+
-**`onTurnComplete` is the canonical persistence point**, same as any turn. It carries the full assistant message under that one id: step-1 text, reasoning, and tool calls plus step-2+ tool results and text. The [database persistence](/ai-chat/patterns/database-persistence) patterns apply unchanged.
536
+
-**Reasoning parts survive the handover.** When step 1 runs on an extended-thinking model, the reasoning streamed by your route handler lands in the durable session history (and `onTurnComplete`) under the same `messageId`, with provider metadata intact — Anthropic thinking signatures survive a replay back to the model. Step-2 reasoning appends to the same message rather than replacing it.
537
+
538
+
#### With `hydrateMessages`
539
+
540
+
Head Start composes with [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages). On the first turn, the hook receives the route handler's first-turn history as `incomingMessages` — the canonical upsert-and-return pattern persists the user message exactly as it would on a direct-trigger turn. The runtime splices the warm handler's partial assistant onto your hydrated chain after the hook returns, deduplicated by the assistant `messageId`, so your hook never needs to include the in-flight partial.
541
+
542
+
<Warning>
543
+
**Hydrate hooks must upsert their conversation row, not update it.** Head-start turns skip preload entirely, so row-creating hooks (`onPreload`, or an `onChatStart` create) have not run when `hydrateMessages` first fires. A bare `UPDATE` against a missing row throws and errors the turn.
544
+
</Warning>
545
+
546
+
Your hydrate hook shapes **model context**, not the transcript — dropping reasoning-only entries or unresolved tool rows from the returned chain is fine and does not affect what `onTurnComplete` persists or what the UI renders.
Copy file name to clipboardExpand all lines: docs/ai-chat/lifecycle-hooks.mdx
+6-3Lines changed: 6 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -273,7 +273,7 @@ Use this when the backend should be the source of truth for message history: abu
273
273
|`chatId`|`string`| Chat session ID |
274
274
|`turn`|`number`| Turn number (0-indexed) |
275
275
|`trigger`|`"submit-message" \| "regenerate-message" \| "action"`| The trigger type for this turn |
276
-
|`incomingMessages`|`UIMessage[]`| Validated wire messages from the frontend — 0-or-1-length (empty for actions, regenerates, and continuations; one element for normal `submit-message` and tool-approval responses) |
276
+
|`incomingMessages`|`UIMessage[]`| Validated wire messages from the frontend — 0-or-1-length (empty for actions, regenerates, and continuations; one element for normal `submit-message` and tool-approval responses). On a [Head Start](/ai-chat/fast-starts#with-hydratemessages) first turn, carries the route handler's first-turn history|
277
277
|`previousMessages`|`UIMessage[]`| Accumulated UI messages before this turn (`[]` on turn 0) |
278
278
|`clientData`| Typed by `clientDataSchema`| Custom data from the frontend |
279
279
|`continuation`|`boolean`| Whether this run is continuing an existing chat |
This replaces the `onTurnStart` persistence pattern — the hook handles both loading and persisting the new message in one place.
219
225
226
+
Hydration composes with [Head Start](/ai-chat/fast-starts#with-hydratemessages): on a head-start first turn the route handler's history arrives as `incomingMessages`, and the write path must be an upsert because no preload ran to create the row.
227
+
220
228
## Design notes
221
229
222
230
-**`chatId`** is stable for the life of a thread and is the only identifier the transport persists. Runs come and go (idle continuation, upgrade, cancel/restart) but the chat keeps its identity.
0 commit comments