Skip to content

Commit c222b92

Browse files
committed
docs(ai-chat): split lifecycle hooks, actions, and fast starts into pages
Extract the lifecycle hooks reference (~360 lines) from backend.mdx into ai-chat/lifecycle-hooks.mdx. Extract Actions into ai-chat/actions.mdx. Merge Preload and Head Start into a unified ai-chat/fast-starts.mdx with a comparison table for picking the right approach. Move the full three-file persistence example out of backend.mdx into the patterns/database-persistence.mdx page where it fits with the conceptual breakdown. Document the new chat.history read primitives (getPendingToolCalls, getResolvedToolCalls, extractNewToolResults, getChain, findMessage) in backend.mdx and the human-in-the-loop pattern. Update navigation and rewrite all internal cross-links to the new pages.
1 parent 4cddac0 commit c222b92

18 files changed

Lines changed: 1346 additions & 729 deletions

docs/ai-chat/actions.mdx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
title: "Actions"
3+
sidebarTitle: "Actions"
4+
description: "Custom commands sent from the frontend that mutate chat state without consuming a turn — undo, rollback, edit, regenerate."
5+
---
6+
7+
## Overview
8+
9+
Custom actions let the frontend send structured commands (undo, rollback, edit, regenerate) that modify the conversation state. **Actions are not turns**: they fire `hydrateMessages` (if set) and `onAction` only. No turn lifecycle hooks (`onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete`), no `run()`, no turn-counter increment. The trace span is named `chat action`.
10+
11+
Actions wake the agent from suspension the same way a new message does, run their handler against the latest accumulator state, and emit a `trigger:turn-complete` chunk so the frontend's `useChat` knows the action has been applied.
12+
13+
## Defining an action handler
14+
15+
Define an `actionSchema` for validation and an `onAction` handler that uses [`chat.history`](/ai-chat/backend#chat-history) to modify state:
16+
17+
```ts
18+
import { z } from "zod";
19+
20+
export const myChat = chat.agent({
21+
id: "my-chat",
22+
actionSchema: z.discriminatedUnion("type", [
23+
z.object({ type: z.literal("undo") }),
24+
z.object({ type: z.literal("rollback"), targetMessageId: z.string() }),
25+
z.object({ type: z.literal("edit"), messageId: z.string(), text: z.string() }),
26+
]),
27+
28+
onAction: async ({ action }) => {
29+
switch (action.type) {
30+
case "undo":
31+
chat.history.slice(0, -2); // Remove last user + assistant exchange
32+
break;
33+
case "rollback":
34+
chat.history.rollbackTo(action.targetMessageId);
35+
break;
36+
case "edit":
37+
chat.history.replace(action.messageId, {
38+
id: action.messageId,
39+
role: "user",
40+
parts: [{ type: "text", text: action.text }],
41+
});
42+
break;
43+
}
44+
// returning void → side-effect-only, no model call
45+
},
46+
47+
run: async ({ messages, signal }) => {
48+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
49+
},
50+
});
51+
```
52+
53+
**Lifecycle flow:** Wake → parse action against `actionSchema``hydrateMessages` (if set) → **`onAction`** → apply `chat.history` mutations → emit `trigger:turn-complete` → wait for next message.
54+
55+
## Returning a model response from an action
56+
57+
`onAction` can return a `StreamTextResult`, `string`, or `UIMessage` to produce a response. The returned stream is auto-piped to the frontend just like a normal turn, but the rest of the turn machinery (`onTurnStart`, `onTurnComplete`, etc.) still does not fire.
58+
59+
```ts
60+
onAction: async ({ action, messages }) => {
61+
if (action.type === "regenerate") {
62+
chat.history.slice(0, -1); // drop the last assistant
63+
return streamText({
64+
model: openai("gpt-4o"),
65+
messages,
66+
});
67+
}
68+
// other actions return void → side-effect only
69+
}
70+
```
71+
72+
This is useful for actions that both mutate state and want a fresh model response (regenerate-from-here, retry-with-different-style). Persistence is your responsibility inside `onAction` itself; you have access to the streamed response object.
73+
74+
## Gating actions on HITL state
75+
76+
If you have a [human-in-the-loop](/ai-chat/patterns/human-in-the-loop) tool waiting on `addToolOutput`, you usually want to refuse competing actions like `regenerate` until the answer arrives. [`chat.history.getPendingToolCalls()`](/ai-chat/backend#chat-history) gives you exactly that signal:
77+
78+
```ts
79+
onAction: async ({ action, messages, signal }) => {
80+
if (action.type === "regenerate") {
81+
if (chat.history.getPendingToolCalls().length > 0) return; // gated
82+
chat.history.slice(0, -1);
83+
return streamText({ model: openai("gpt-4o"), messages, abortSignal: signal });
84+
}
85+
},
86+
```
87+
88+
## Sending actions from the frontend
89+
90+
```ts
91+
// Browser — TriggerChatTransport
92+
const stream = await transport.sendAction(chatId, { type: "undo" });
93+
94+
// Server — AgentChat
95+
const stream = await agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" });
96+
```
97+
98+
The action payload is validated against `actionSchema` on the backend; invalid actions throw and surface as a stream error. The `action` parameter in `onAction` is fully typed from the schema.
99+
100+
<Note>
101+
For silent state changes that should never appear as a turn (e.g. injecting background context), use [`chat.inject()`](/ai-chat/background-injection) instead. Actions are explicit user-driven mutations; injections are agent-side context updates.
102+
</Note>
103+
104+
## See also
105+
106+
- [`chat.history`](/ai-chat/backend#chat-history) — the imperative API actions use to mutate state
107+
- [Sending actions from the frontend](/ai-chat/frontend#sending-actions)`transport.sendAction` ergonomics
108+
- [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — fires before `onAction` when set
109+
- [Branching conversations](/ai-chat/patterns/branching-conversations) — pairs action handlers with backend-controlled history
110+
- [Human-in-the-loop](/ai-chat/patterns/human-in-the-loop) — gating fresh actions while a tool is waiting

0 commit comments

Comments
 (0)