@@ -185,37 +185,48 @@ compaction: {
185185},
186186```
187187
188- ### 5. Actions: mutate state without a turn
188+ ### 5. Actions: edit state, and optionally answer
189189
190- ` actionSchema ` validates; ` onAction ` mutates via ` chat.history ` (` slice ` , ` replace ` , ` rollbackTo ` ,
191- ` remove ` , ` getPendingToolCalls ` , ` extractNewToolResults ` ). Actions fire ` hydrateMessages ` and
192- ` onAction ` only, never ` run() ` or the turn hooks. Return a ` StreamTextResult ` , string, or ` UIMessage `
193- to also emit a model response, built with the ` streamText ` from ` onAction ` 's own argument so it
194- carries the agent's prompt and tools like any other turn.
190+ ` actionSchema ` validates; ` onAction ` edits via ` chat.history ` (` slice ` , ` replace ` , ` rollbackTo ` ,
191+ ` remove ` , ` getPendingToolCalls ` , ` extractNewToolResults ` ). An action fires ` hydrateMessages ` and
192+ ` onAction ` only. Return nothing for an edit-only action: no model call, and the turn counter does not
193+ advance. Return ` chat.turn() ` to answer after the edit: a turn runs on the edited history with
194+ everything a turn has (the agent's system prompt and tools, steering, compaction, injected
195+ instructions, ` onTurnStart ` and ` onTurnComplete ` , persistence), and ` run() ` receives it with
196+ ` trigger: "action-turn" ` . ` onAction ` has no ` streamText ` argument, and returning a
197+ ` StreamTextResult ` , string or ` UIMessage ` from it throws.
195198
196199Persistence splits by model. Without ` hydrateMessages ` the runtime snapshots the conversation after
197- an action that changed it, so a rollback or a returned response survives the run ending. With
200+ an action that changed it, before any turn starts, so a rollback survives the run ending. With
198201` hydrateMessages ` your store is the source of truth and the runtime does not write, so mirror every
199- mutation yourself: a regenerate is a delete and an insert, and ` chat.pipeAndCapture ` hands back the
200- same assistant message the runtime would have captured .
202+ mutation yourself: a regenerate is a delete in ` onAction ` and an insert that arrives in
203+ ` onTurnComplete ` like any turn's answer .
201204
202205``` ts
203206export const myChat = chat .agent ({
204207 id: " my-chat" ,
205208 actionSchema: z .discriminatedUnion (" type" , [
206209 z .object ({ type: z .literal (" undo" ) }),
210+ z .object ({ type: z .literal (" regenerate" ) }),
207211 z .object ({ type: z .literal (" rollback" ), targetMessageId: z .string () }),
208212 ]),
209213 onAction : async ({ action }) => {
210- if (action .type === " undo" ) chat .history .slice (0 , - 2 );
214+ if (action .type === " undo" ) chat .history .slice (0 , - 2 ); // edit only
211215 if (action .type === " rollback" ) chat .history .rollbackTo (action .targetMessageId );
216+ if (action .type === " regenerate" ) {
217+ chat .history .slice (0 , - 1 );
218+ return chat .turn (); // answer the edited history
219+ }
212220 },
213221 run : async ({ messages , signal , streamText }) =>
214222 streamText ({ model: anthropic (" claude-sonnet-4-5" ), messages , abortSignal: signal }),
215223});
216224```
217225
218- Send from the browser with ` transport.sendAction(chatId, { type: "undo" }) ` , or server-side with
226+ Send from the browser through ` useChat ` , so the answer a turn produces renders like any turn:
227+ ` sendMessage(undefined, { body: { action: { type: "regenerate" } } }) ` , ` regenerate({ body: { action } }) ` ,
228+ or ` useChatActions({ sendMessage }) ` from ` @trigger.dev/sdk/chat/react ` . ` transport.sendAction(chatId, action) `
229+ returns a raw stream the caller must read itself. Server-side, use
219230` agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" }) ` .
220231
221232### 6. Fast starts: Head Start
0 commit comments