diff --git a/packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md b/packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md index 052411df61d..fc3dcca31de 100644 --- a/packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md +++ b/packages/trigger-sdk/skills/trigger-chat-agent-advanced/SKILL.md @@ -185,37 +185,48 @@ compaction: { }, ``` -### 5. Actions: mutate state without a turn +### 5. Actions: edit state, and optionally answer -`actionSchema` validates; `onAction` mutates via `chat.history` (`slice`, `replace`, `rollbackTo`, -`remove`, `getPendingToolCalls`, `extractNewToolResults`). Actions fire `hydrateMessages` and -`onAction` only, never `run()` or the turn hooks. Return a `StreamTextResult`, string, or `UIMessage` -to also emit a model response, built with the `streamText` from `onAction`'s own argument so it -carries the agent's prompt and tools like any other turn. +`actionSchema` validates; `onAction` edits via `chat.history` (`slice`, `replace`, `rollbackTo`, +`remove`, `getPendingToolCalls`, `extractNewToolResults`). An action fires `hydrateMessages` and +`onAction` only. Return nothing for an edit-only action: no model call, and the turn counter does not +advance. Return `chat.turn()` to answer after the edit: a turn runs on the edited history with +everything a turn has (the agent's system prompt and tools, steering, compaction, injected +instructions, `onTurnStart` and `onTurnComplete`, persistence), and `run()` receives it with +`trigger: "action-turn"`. `onAction` has no `streamText` argument, and returning a +`StreamTextResult`, string or `UIMessage` from it throws. Persistence splits by model. Without `hydrateMessages` the runtime snapshots the conversation after -an action that changed it, so a rollback or a returned response survives the run ending. With +an action that changed it, before any turn starts, so a rollback survives the run ending. With `hydrateMessages` your store is the source of truth and the runtime does not write, so mirror every -mutation yourself: a regenerate is a delete and an insert, and `chat.pipeAndCapture` hands back the -same assistant message the runtime would have captured. +mutation yourself: a regenerate is a delete in `onAction` and an insert that arrives in +`onTurnComplete` like any turn's answer. ```ts export const myChat = chat.agent({ id: "my-chat", actionSchema: z.discriminatedUnion("type", [ z.object({ type: z.literal("undo") }), + z.object({ type: z.literal("regenerate") }), z.object({ type: z.literal("rollback"), targetMessageId: z.string() }), ]), onAction: async ({ action }) => { - if (action.type === "undo") chat.history.slice(0, -2); + if (action.type === "undo") chat.history.slice(0, -2); // edit only if (action.type === "rollback") chat.history.rollbackTo(action.targetMessageId); + if (action.type === "regenerate") { + chat.history.slice(0, -1); + return chat.turn(); // answer the edited history + } }, run: async ({ messages, signal, streamText }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal }), }); ``` -Send from the browser with `transport.sendAction(chatId, { type: "undo" })`, or server-side with +Send from the browser through `useChat`, so the answer a turn produces renders like any turn: +`sendMessage(undefined, { body: { action: { type: "regenerate" } } })`, `regenerate({ body: { action } })`, +or `useChatActions({ sendMessage })` from `@trigger.dev/sdk/chat/react`. `transport.sendAction(chatId, action)` +returns a raw stream the caller must read itself. Server-side, use `agentChat.sendAction({ type: "rollback", targetMessageId: "msg-3" })`. ### 6. Fast starts: Head Start