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
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
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
+
returnstreamText({ 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
+
returnstreamText({ 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.
Copy file name to clipboardExpand all lines: docs/ai-chat/frontend.mdx
+15-4Lines changed: 15 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -428,7 +428,9 @@ function Chat({ chatId, transport }) {
428
428
429
429
## Sending actions
430
430
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:
432
434
433
435
```tsx
434
436
function ChatControls({ chatId }: { chatId:string }) {
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.
456
467
457
468
<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.
459
470
</Note>
460
471
461
472
For server-to-server usage, `AgentChat` has the same method:
Copy file name to clipboardExpand all lines: docs/ai-chat/reference.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,7 +18,7 @@ Options for `chat.agent()`.
18
18
|`onValidateMessages`|`(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>`| — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/backend#onvalidatemessages)|
19
19
|`hydrateMessages`|`(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>`| — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/backend#hydratemessages)|
20
20
|`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)|
22
22
|`onTurnStart`|`(event: TurnStartEvent) => Promise<void> \| void`| — | Fires every turn before `run()`|
23
23
|`onBeforeTurnComplete`|`(event: BeforeTurnCompleteEvent) => Promise<void> \| void`| — | Fires after response but before stream closes. Includes `writer`. |
24
24
|`onTurnComplete`|`(event: TurnCompleteEvent) => Promise<void> \| void`| — | Fires after each turn completes (stream closed) |
Copy file name to clipboardExpand all lines: docs/ai-chat/testing.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -201,7 +201,7 @@ Equivalent to the frontend's `useChat().regenerate()` — replays a turn with th
201
201
202
202
### sendAction
203
203
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`):
0 commit comments