UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

139 lines (112 loc) • 6.02 kB
--- title: Queueing User Messages description: Inject messages during an agent's turn, before tool calls complete or while the model is reasoning. type: guide summary: Inject user messages mid-turn using the `prepareStep` callback to influence the agent's next step. prerequisites: - /docs/ai related: - /docs/ai/chat-session-modeling - /docs/api-reference/workflow/define-hook --- When using [multi-turn workflows](/docs/ai/chat-session-modeling#multi-turn-workflows), messages typically arrive between agent turns. The workflow waits at a hook, receives a message, then starts a new turn. But sometimes you need to inject messages *during* an agent's turn, before tool calls complete or while the model is reasoning. `WorkflowAgent`'s `prepareStep` callback enables this by running before each step in the agent loop, giving you a chance to inject queued messages into the conversation. `prepareStep` also allows you to modify the model choice and existing messages mid-turn, see AI SDK's [prepareStep callback](https://ai-sdk.dev/docs/agents/loop-control#prepare-step) for more details. ## When to use this Message queueing is useful when: - Users send follow-up messages while the agent is still searching for flights or processing bookings - External systems need to inject context mid-turn (e.g., a flight status webhook fires during processing) - You want messages to influence the agent's next step rather than waiting for the current turn to complete <Callout type="info"> If you need basic multi-turn conversations where messages arrive between turns, see [Chat Session Modeling](/docs/ai/chat-session-modeling). This guide covers the more advanced case of injecting messages *during* turns. </Callout> ## The `prepareStep` callback The `prepareStep` callback runs before each step in the agent loop. Use WorkflowAgent's exported types rather than redeclaring its normalized provider-prompt contract: ```typescript lineNumbers import type { PrepareStepInfo, PrepareStepResult, } from "@ai-sdk/workflow"; const prepareStep = ( { messages }: PrepareStepInfo ): PrepareStepResult => ({ messages }); ``` `PrepareStepInfo.messages` is a normalized `LanguageModelV4Prompt`, not the application-level `ModelMessage[]` accepted by `WorkflowAgent.stream()`. ## Queueing messages during and between turns Use one async Hook consumer and one FIFO. `prepareStep` atomically drains messages that arrived during a model turn; messages that arrive after the final model step become input to the next turn. Each Hook payload therefore has exactly one ownership path. ```typescript title="workflows/chat/index.ts" lineNumbers import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow"; import { getWritable, getWorkflowMetadata } from "workflow"; import { chatMessageHook } from "./hooks/chat-message"; import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools"; import type { ModelMessage } from "ai"; export async function chat(initialMessages: ModelMessage[]) { "use workflow"; const { workflowRunId: runId } = getWorkflowMetadata(); const writable = getWritable<ModelCallStreamPart>(); let messages: ModelMessage[] = [...initialMessages]; const messageQueue: Array<{ role: "user"; content: string }> = []; // [!code highlight] let stopped = false; let notifyMessage: (() => void) | undefined; const agent = new WorkflowAgent({ model: "spacexai/grok-4.6", instructions: FLIGHT_ASSISTANT_PROMPT, tools: flightBookingTools, }); const hook = chatMessageHook.create({ token: runId }); // [!code highlight] // This is the only code path that consumes Hook payloads. // [!code highlight] const consumeMessages = (async () => { // [!code highlight] for await (const { message } of hook) { // [!code highlight] if (message === "/done") { // [!code highlight] stopped = true; // [!code highlight] notifyMessage?.(); // [!code highlight] break; // [!code highlight] } // [!code highlight] messageQueue.push({ role: "user", content: message }); // [!code highlight] notifyMessage?.(); // [!code highlight] notifyMessage = undefined; // [!code highlight] } // [!code highlight] })(); // [!code highlight] const waitForMessage = async () => { while (messageQueue.length === 0 && !stopped) { await new Promise<void>((resolve) => { notifyMessage = resolve; }); } }; while (!stopped) { const result = await agent.stream({ messages, writable, preventClose: true, sendFinish: false, prepareStep: ({ messages: currentMessages }) => { const queued = messageQueue.splice(0); // Atomic drain // [!code highlight] if (queued.length === 0) return {}; return { messages: [ ...currentMessages, ...queued.map(({ role, content }) => ({ role, content: [{ type: "text" as const, text: content }], })), ], }; }, }); messages = result.messages; if (stopped) break; await waitForMessage(); // [!code highlight] if (stopped) break; // Anything not consumed by prepareStep arrived after the final model step. messages = [...messages, ...messageQueue.splice(0)]; // [!code highlight] } await consumeMessages; return { messages }; } ``` Messages sent via `chatMessageHook.resume()` accumulate until either `prepareStep` or the between-turn branch drains the FIFO. Send `/done` to stop the consumer and let the workflow return. ## Related documentation - [Chat Session Modeling](/docs/ai/chat-session-modeling) - Single-turn vs multi-turn patterns - [Building Durable AI Agents](/docs/ai) - Complete guide to creating durable agents - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) - AI SDK API for durable, resumable agents - [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options