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
`onValidateMessages` fires **before**`onTurnStart` and message accumulation. If you need to validate messages loaded from a database, do the loading in `onChatStart` or `onPreload` and let `onValidateMessages` validate the full incoming set each turn.
182
182
</Note>
183
183
184
+
#### hydrateMessages
185
+
186
+
Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state — the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
187
+
188
+
Use this when the backend should be the source of truth for message history — abuse prevention, branching conversations (DAGs), or rollback/undo support.
After the hook returns, any incoming wire message whose ID matches a hydrated message is auto-merged — this makes [tool approvals](/ai-chat/frontend#tool-approvals) work transparently with hydration.
229
+
230
+
<Note>
231
+
`hydrateMessages` also fires for [action](#actions) turns (`trigger: "action"`) with empty `incomingMessages`. This lets the action handler work with the latest DB state.
232
+
</Note>
233
+
184
234
#### onTurnStart
185
235
186
236
Fires at the start of every turn, after message accumulation and `onChatStart` (turn 0), but **before**`run()` executes. Use it to persist messages before streaming begins — so a mid-stream page refresh still shows the user's message.
example, and how it differs from pending messages.
786
836
</Tip>
787
837
838
+
### Actions
839
+
840
+
Custom actions let the frontend send structured commands (undo, rollback, edit) that modify the conversation state before the LLM responds. Actions use the same input stream as messages, so they wake the agent from suspension and trigger a full turn.
841
+
842
+
Define an `actionSchema` for validation and an `onAction` handler that uses `chat.history` to modify state:
The action payload is validated against `actionSchema` on the backend — invalid actions throw and abort the turn. The `action` parameter in `onAction` is fully typed from the schema.
892
+
893
+
<Note>
894
+
Actions always trigger `run()` — the LLM responds to the modified state. For silent state changes that don't need a response (e.g. injecting background context), use [`chat.inject()`](/ai-chat/background-injection) instead.
895
+
</Note>
896
+
897
+
### chat.history {#chat-history}
898
+
899
+
Imperative API for modifying the accumulated message history. Works from any hook (`onAction`, `onTurnStart`, `onBeforeTurnComplete`, `onTurnComplete`) or from `run()` and AI SDK tools.
900
+
901
+
| Method | Description |
902
+
|--------|-------------|
903
+
|`chat.history.all()`| Read the current accumulated UI messages (returns a copy) |
904
+
|`chat.history.set(messages)`| Replace all messages (same as `chat.setMessages()`) |
905
+
|`chat.history.remove(messageId)`| Remove a specific message by ID |
906
+
|`chat.history.rollbackTo(messageId)`| Keep messages up to and including the given ID (undo) |
907
+
|`chat.history.replace(messageId, message)`| Replace a specific message by ID (edit) |
908
+
|`chat.history.slice(start, end?)`| Keep only messages in the given range |
909
+
910
+
```ts
911
+
// Undo the last exchange in onAction
912
+
onAction: async ({ action }) => {
913
+
if (action.type==="undo") {
914
+
chat.history.slice(0, -2);
915
+
}
916
+
},
917
+
918
+
// Trim history in onTurnComplete
919
+
onTurnComplete: async ({ uiMessages }) => {
920
+
if (uiMessages.length>50) {
921
+
chat.history.slice(-20);
922
+
}
923
+
},
924
+
```
925
+
926
+
Mutations use the same deferred mechanism as `chat.setMessages()` — they are applied at lifecycle checkpoints (after hooks return). Multiple mutations in the same hook compose correctly.
927
+
788
928
### prepareMessages
789
929
790
930
Transform model messages before they're used anywhere — in `run()`, in compaction rebuilds, and in compaction results. Define once, applied everywhere.
## `hydrateMessages` — backend-controlled message history
10
+
11
+
Load message history from your database on every turn instead of trusting the frontend accumulator. The hook replaces the built-in linear accumulation entirely — the backend is the source of truth.
Tool approval updates are auto-merged after hydration — no extra handling needed.
28
+
29
+
See [hydrateMessages](/ai-chat/backend#hydratemessages).
30
+
31
+
## `chat.history` — imperative message mutations
32
+
33
+
Modify the accumulated message history from any hook or `run()`:
34
+
35
+
```ts
36
+
chat.history.rollbackTo(messageId); // Undo — keep up to this message
37
+
chat.history.remove(messageId); // Remove one message
38
+
chat.history.replace(id, newMsg); // Edit a message
39
+
chat.history.slice(0, -2); // Remove last 2 messages
40
+
chat.history.all(); // Read current state
41
+
```
42
+
43
+
See [chat.history](/ai-chat/backend#chat-history).
44
+
45
+
## Custom actions — `actionSchema` + `onAction`
46
+
47
+
Send typed actions (undo, rollback, edit) from the frontend via `transport.sendAction()`. Actions wake the agent, fire `onAction`, then trigger a normal `run()` turn.
Copy file name to clipboardExpand all lines: docs/ai-chat/client-protocol.mdx
+28Lines changed: 28 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -290,6 +290,34 @@ The agent matches the incoming message by its `id` against the accumulated conve
290
290
The message `id` must match the one the agent assigned during streaming. If you're using `TriggerChatTransport`, IDs are kept in sync automatically. Custom transports should use the `messageId` from the stream's `start` chunk.
291
291
</Note>
292
292
293
+
## Custom actions
294
+
295
+
Send a custom action (undo, rollback, edit) to the agent using the same `chat-messages` input stream. Actions use `trigger: "action"` and carry a custom payload in the `action` field:
296
+
297
+
```bash
298
+
POST /realtime/v1/streams/{runId}/input/chat-messages
299
+
Authorization: Bearer <publicAccessToken>
300
+
Content-Type: application/json
301
+
302
+
{
303
+
"data": {
304
+
"messages": [],
305
+
"chatId": "conversation-123",
306
+
"trigger": "action",
307
+
"action": { "type": "undo" },
308
+
"metadata": { "userId": "user-456" }
309
+
}
310
+
}
311
+
```
312
+
313
+
Actions wake the agent from suspension (same as messages), fire the `onAction` hook, then trigger a normal `run()` turn. The `action` payload is validated against the agent's `actionSchema`.
314
+
315
+
After sending, subscribe to the output stream to receive the agent's response — the same flow as [Step 2](#step-2-subscribe-to-the-output-stream).
316
+
317
+
<Note>
318
+
`messages` is empty for actions. The agent's `onAction` handler modifies the conversation state via `chat.history.*`, and the LLM responds to the updated state. See [Actions](/ai-chat/backend#actions) for backend setup.
319
+
</Note>
320
+
293
321
## Pending and steering messages
294
322
295
323
You can send messages to the agent **while it's still streaming a response**. These are called pending messages — the agent receives them mid-turn and can inject them between tool-call steps.
Copy file name to clipboardExpand all lines: docs/ai-chat/frontend.mdx
+36Lines changed: 36 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -370,6 +370,42 @@ function Chat({ chatId, transport }) {
370
370
for tool approval updates.
371
371
</Info>
372
372
373
+
## Sending actions
374
+
375
+
Send custom actions (undo, rollback, edit) to the agent via `transport.sendAction()`. Actions wake the agent, fire the `onAction` hook, and trigger a normal response — the LLM responds to the modified state.
376
+
377
+
```tsx
378
+
function ChatControls({ chatId }: { chatId:string }) {
379
+
const transport =useTriggerChatTransport({ task: "my-chat", accessToken });
The action payload is validated against the agent's `actionSchema` on the backend — invalid actions are rejected. See [Actions](/ai-chat/backend#actions) for the backend setup.
395
+
396
+
<Note>
397
+
`sendAction` returns a `ReadableStream<UIMessageChunk>` — the agent's response to the modified state. If you're using `useChat`, the response is handled automatically through the transport.
398
+
</Note>
399
+
400
+
For server-to-server usage, `AgentChat` has the same method:
Copy file name to clipboardExpand all lines: docs/ai-chat/patterns/database-persistence.mdx
+32Lines changed: 32 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -113,6 +113,38 @@ chat.agent({
113
113
});
114
114
```
115
115
116
+
## Alternative: `hydrateMessages`
117
+
118
+
For apps that need the backend to be the single source of truth for message history — abuse prevention, branching conversations, or rollback support — use [`hydrateMessages`](/ai-chat/backend#hydratemessages) instead of relying on the frontend's accumulated state.
119
+
120
+
With hydration, the hook loads messages from your database on every turn. The frontend's messages are ignored (except for the new user message, which arrives in `incomingMessages`):
returnstreamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
142
+
},
143
+
});
144
+
```
145
+
146
+
This replaces the `onTurnStart` persistence pattern — the hook handles both loading and persisting the new message in one place.
147
+
116
148
## Design notes
117
149
118
150
-**`chatId`** is stable for the life of a thread; **`runId`** changes when the user starts a **new** run (timeout, cancel, explicit new chat). Session rows must always reflect the **current** run.
0 commit comments