Skip to content

Commit f08c15f

Browse files
committed
docs(ai-chat): document actions-not-turns semantics
Update the customer-facing docs for TRI-9118: - changelog.mdx: new Update entry with the breaking-change writeup and a before/after migration snippet - backend.mdx (already shipped): action lifecycle no longer mentions run() / turn hooks; documents void vs StreamTextResult returns - frontend.mdx: rewrite sendAction section with optimistic setMessages example for void actions - testing.mdx: update harness.sendAction reference to reflect the new lifecycle - reference.mdx: widen onAction return type in the hooks table - upgrade-guide.mdx: drop the misleading 'onAction unchanged' claim from the Sessions migration's 'what didn't change' list
1 parent 12795d6 commit f08c15f

5 files changed

Lines changed: 65 additions & 8 deletions

File tree

docs/ai-chat/changelog.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,48 @@ sidebarTitle: "Changelog"
44
description: "Pre-release updates for AI chat agents."
55
---
66

7+
<Update label="May 6, 2026" tags={["SDK", "Breaking"]}>
8+
9+
## `chat.agent` actions are no longer turns
10+
11+
Submitting an action via `transport.sendAction()` previously fell through to the regular turn machinery, calling `onTurnStart`, `run()`, `onTurnComplete`, etc. — meaning every action fired an LLM call by default. The workaround was a `chat.store`-based `skipModelCall` flag in `run()`.
12+
13+
Actions now fire `hydrateMessages` and `onAction` only. No `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete`, no `run()` invocation, no turn-counter increment. The trace span is named `chat action` instead of `chat turn N`.
14+
15+
`onAction`'s return type widens: returning `void` is side-effect-only (default); returning a `StreamTextResult`, `string`, or `UIMessage` produces a model response that's auto-piped back to the frontend.
16+
17+
### Migration
18+
19+
If you had `run()` branching on `payload.trigger === "action"` for a model response, return your `streamText(...)` from `onAction` instead. If you persisted in `onTurnComplete`, do that work inside `onAction`. For state-only actions, just remove the skip-the-model workaround.
20+
21+
```ts
22+
// before
23+
onAction: async ({ action }) => {
24+
if (action.type === "regenerate") {
25+
chat.store.set({ skipModelCall: false });
26+
chat.history.slice(0, -1);
27+
}
28+
},
29+
run: async ({ messages, signal }) => {
30+
if (chat.store.get()?.skipModelCall) return;
31+
return streamText({ model, messages, abortSignal: signal });
32+
},
33+
34+
// after
35+
onAction: async ({ action, messages, signal }) => {
36+
if (action.type === "regenerate") {
37+
chat.history.slice(0, -1);
38+
return streamText({ model, messages, abortSignal: signal });
39+
}
40+
},
41+
run: async ({ messages, signal }) =>
42+
streamText({ model, messages, abortSignal: signal }),
43+
```
44+
45+
Actions arriving when no `onAction` handler is configured now `console.warn` once and are ignored — previously they silently fell through to `run()` with an empty wire payload.
46+
47+
</Update>
48+
749
<Update label="May 5, 2026" description="0.0.0-chat-prerelease-20260505140031" tags={["SDK"]}>
850

951
## Fix: duplicate turn after `chat.agent` idle-suspends

docs/ai-chat/frontend.mdx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,9 @@ function Chat({ chatId, transport }) {
428428

429429
## Sending actions
430430

431-
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.
431+
Send custom actions (undo, rollback, edit) to the agent via `transport.sendAction()`. Actions wake the agent and fire only `hydrateMessages` (if configured) and `onAction` — they're not turns, so `onTurnStart` / `prepareMessages` / `onBeforeTurnComplete` / `onTurnComplete` and `run()` do not fire.
432+
433+
For optimistic UI, mirror the action's effect on the `useChat` state via `setMessages` while the request is in flight:
432434

433435
```tsx
434436
function ChatControls({ chatId }: { chatId: string }) {
@@ -439,12 +441,21 @@ function ChatControls({ chatId }: { chatId: string }) {
439441
startChatSession({ chatId, taskId, clientData }),
440442
});
441443

444+
const { setMessages } = useChat({ transport });
445+
442446
return (
443447
<div>
444-
<button onClick={() => transport.sendAction(chatId, { type: "undo" })}>
448+
<button
449+
onClick={() => {
450+
void transport.sendAction(chatId, { type: "undo" });
451+
setMessages((prev) => prev.slice(0, -2));
452+
}}
453+
>
445454
Undo last exchange
446455
</button>
447-
<button onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}>
456+
<button
457+
onClick={() => transport.sendAction(chatId, { type: "rollback", targetMessageId: "msg-5" })}
458+
>
448459
Rollback to message
449460
</button>
450461
</div>
@@ -455,7 +466,7 @@ function ChatControls({ chatId }: { chatId: string }) {
455466
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.
456467

457468
<Note>
458-
`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.
469+
`sendAction` returns a `ReadableStream<UIMessageChunk>`. For side-effect-only actions (where `onAction` returns `void`), the stream completes immediately with `trigger:turn-complete`. For actions where `onAction` returns a `StreamTextResult`, the stream carries the assistant chunks the same way `sendMessages` does — `useChat` consumes them automatically.
459470
</Note>
460471

461472
For server-to-server usage, `AgentChat` has the same method:

docs/ai-chat/reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Options for `chat.agent()`.
1818
| `onValidateMessages` | `(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` || Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/backend#onvalidatemessages) |
1919
| `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` || Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/backend#hydratemessages) |
2020
| `actionSchema` | `TaskSchema` || Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/backend#actions) |
21-
| `onAction` | `(event: ActionEvent) => Promise<void> \| void` || Handle custom actions. Fires after hydration, before `onTurnStart`. See [Actions](/ai-chat/backend#actions) |
21+
| `onAction` | `(event: ActionEvent) => Promise<unknown> \| unknown` || Handle custom actions. Actions are not turns — only `hydrateMessages` + `onAction` fire. Return a `StreamTextResult` (or `string` / `UIMessage`) for a model response; return `void` for side-effect-only. See [Actions](/ai-chat/backend#actions) |
2222
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` || Fires every turn before `run()` |
2323
| `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` || Fires after response but before stream closes. Includes `writer`. |
2424
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes (stream closed) |

docs/ai-chat/testing.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Equivalent to the frontend's `useChat().regenerate()` — replays a turn with th
201201

202202
### sendAction
203203

204-
Routes a payload through `actionSchema` + `onAction`:
204+
Routes a payload through `actionSchema` + `onAction`. Actions are not turns: only `hydrateMessages` and `onAction` fire on the agent side — no turn lifecycle hooks, no `run()`. The returned `turn.rawChunks` contains whatever `onAction` produced (a streamed model response if it returned a `StreamTextResult`, otherwise just `trigger:turn-complete`):
205205

206206
```ts
207207
const turn = await harness.sendAction({ type: "undo" });

docs/ai-chat/upgrade-guide.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,12 @@ and direct API consumers.
301301
- `chat.agent({...})` definition — `id`, `idleTimeoutInSeconds`,
302302
`clientDataSchema`, `actionSchema`, `hydrateMessages`, `onPreload`,
303303
`onChatStart`, `onValidateMessages`, `onTurnStart`, `onTurnComplete`,
304-
`onChatSuspend`, `onAction`, `run`. All callbacks have the same
305-
signature and fire at the same lifecycle points.
304+
`onChatSuspend`, `run`. All callbacks have the same signature and
305+
fire at the same lifecycle points.
306+
- `onAction` is still defined the same way, but its semantics changed
307+
in the [May 6 prerelease](/ai-chat/changelog) — actions are no longer
308+
turns, and `onAction` returning a `StreamTextResult` produces a model
309+
response.
306310
- `chat.customAgent({...})` and the `chat.createSession(payload, ...)`
307311
helper for building a session loop manually inside a custom agent.
308312
- `chat.store` (snapshot store), `chat.defer` (deferred work), and

0 commit comments

Comments
 (0)