diff --git a/docs/content/docs/v4/api-reference/workflow-ai/durable-agent.mdx b/docs/content/docs/v4/api-reference/workflow-ai/durable-agent.mdx index edd95a8985..a54cdd8d94 100644 --- a/docs/content/docs/v4/api-reference/workflow-ai/durable-agent.mdx +++ b/docs/content/docs/v4/api-reference/workflow-ai/durable-agent.mdx @@ -11,6 +11,10 @@ related: The `DurableAgent` class enables you to create AI-powered agents that can maintain state across workflow steps, call tools, and gracefully handle interruptions and resumptions. + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. See [Migrating from DurableAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent) in the AI SDK docs. + + Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as [`sleep()`](/docs/api-reference/workflow/sleep) and [Hooks](/docs/foundations/hooks). ```typescript lineNumbers diff --git a/docs/content/docs/v4/cookbook/agent-patterns/durable-agent.mdx b/docs/content/docs/v4/cookbook/agent-patterns/durable-agent.mdx index 3ef5dca9f3..86be220197 100644 --- a/docs/content/docs/v4/cookbook/agent-patterns/durable-agent.mdx +++ b/docs/content/docs/v4/cookbook/agent-patterns/durable-agent.mdx @@ -7,6 +7,10 @@ summary: Convert an AI SDK Agent into a DurableAgent backed by a workflow, with Use this pattern to make any AI SDK agent durable. The agent becomes a workflow, tools become steps, and the framework handles retries, streaming, and state persistence automatically. + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. See [Migrating from DurableAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent) in the AI SDK docs. + + ## When to use this - Any AI agent with tool calls that should survive crashes and restarts diff --git a/docs/content/docs/v4/cookbook/index.mdx b/docs/content/docs/v4/cookbook/index.mdx index ea1ecbb724..570ed0b90c 100644 --- a/docs/content/docs/v4/cookbook/index.mdx +++ b/docs/content/docs/v4/cookbook/index.mdx @@ -8,7 +8,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f ## Agent Patterns -- [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Replace a stateless AI agent with one that survives crashes and retries tool calls +- [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Legacy durable agent pattern (deprecated soon — prefer [WorkflowAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent)) - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision - [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation) — Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race` diff --git a/docs/content/docs/v4/cookbook/integrations/ai-sdk.mdx b/docs/content/docs/v4/cookbook/integrations/ai-sdk.mdx index a5676e0b92..4cb1df56c8 100644 --- a/docs/content/docs/v4/cookbook/integrations/ai-sdk.mdx +++ b/docs/content/docs/v4/cookbook/integrations/ai-sdk.mdx @@ -2,7 +2,7 @@ title: AI SDK description: Use AI SDK's streamText directly inside durable workflows for lower-level control over model calls and tool execution. type: guide -summary: Use streamText() inside a workflow for full control over model options, stop conditions, and output schemas — while tools remain durable steps. +summary: Use streamText() inside a workflow for full control over model options, stop conditions, and output schemas. The per-turn step is durable; individual tool calls inside it are not. related: - /docs/ai - /docs/ai/chat-session-modeling @@ -11,17 +11,17 @@ related: - /docs/api-reference/workflow-ai/durable-agent --- -[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making those calls durable: the model request, the tool loop, and the multi-turn conversation all survive restarts and timeouts. +[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making the multi-turn loop durable: the conversation state, hooks, and per-turn responses survive restarts and timeouts. Note that in this pattern the durability boundary is the entire turn — individual tool calls inside a turn are **not** durable on their own (see [Pitfalls](#tools-are-not-individually-durable) below). For the full AI SDK reference (providers, `streamText`, `generateObject`, `useChat`, tool calling, etc.) see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points. -For most agent use cases, prefer [`DurableAgent`](/cookbook/agent-patterns/durable-agent) which wraps [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) and manages the tool loop automatically. This page covers using `streamText()` directly when you need lower-level control. +For most agent use cases, prefer [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow`, which manages the tool loop automatically and runs tools as durable workflow steps. This page covers using `streamText()` directly when you need AI SDK features that require lower-level control — accepting that tools inside a turn are no longer individually durable unless you wrap them yourself. ## When to use streamText directly -Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `DurableAgent` when you need: +Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) or the legacy `DurableAgent` when you need: * **Custom stop conditions** — [`stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions), [`prepareStep`](https://ai-sdk.dev/docs/ai-sdk-core/agents#prepare-step), or [`onStepFinish`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#on-step-finish) callbacks * **Structured output** — [`Output.object()`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) or `Output.array()` alongside tool calling @@ -52,14 +52,16 @@ export const turnHook = defineHook({ // [!code highlight] schema: z.object({ message: z.string() }), }); +// Tools are plain async functions in this pattern. `streamText` calls them +// from inside `runTurn` (a step), and the `"use step"` directive is a no-op +// when called from another step — see the "Tools are not individually durable" +// pitfall below. Make side-effectful tools idempotent. async function lookupOrder({ orderId }: { orderId: string }) { - "use step"; const res = await fetch(`https://api.store.com/orders/${orderId}`); return res.json(); } async function processRefund({ orderId, reason }: { orderId: string; reason: string }) { - "use step"; const res = await fetch("https://api.store.com/refunds", { method: "POST", body: JSON.stringify({ orderId, reason }), @@ -294,16 +296,32 @@ export function SupportChat() { ## How it works 1. **One workflow = one conversation.** The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns. -2. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`. -3. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it. -4. **`sliceUntilFinish`** in the API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released — not cancelled — so the workflow stream keeps flowing. -5. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns. -6. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming". +2. **`runTurn` is the durability boundary.** Each turn is one step. The model request and all tool calls inside it run as plain inline functions within that step. If anything throws mid-turn, the whole `runTurn` retries — individual tool calls are not separately durable. See [Pitfalls](#tools-are-not-individually-durable). +3. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`. +4. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it. +5. **`sliceUntilFinish`** in the API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released — not cancelled — so the workflow stream keeps flowing. +6. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns. +7. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming". ## Pitfalls Non-obvious correctness details worth knowing before adapting this pattern. +### Tools are not individually durable + +`streamText()` is invoked from inside `runTurn` (a `"use step"` function), and the AI SDK calls each tool by directly invoking its `execute` function in that same step. Even if a tool body has its own `"use step"` directive, that directive is a [no-op when called from another step](/docs/foundations/workflows-and-steps#step-functions) — the function just runs inline. + +The consequences: + +- The atomic retry unit is the entire `runTurn`, not the individual tool call. +- If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again. +- Tool calls do not appear as separate entries in the event log or observability dashboard. + +**Mitigations:** + +- Make side-effectful tool implementations idempotent — dedupe server-side on a stable key (e.g. `orderId`, an `Idempotency-Key` header, etc.). +- Or use [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent), which runs tools at workflow scope — each tool can be marked `"use step"` to become its own durable, retryable step, or stay at workflow level to use primitives like `sleep()` and hooks. + ### Snapshot `tailIndex` *before* resuming the hook {/* @skip-typecheck - fragment referencing variables from the surrounding multi-turn pattern */} @@ -332,32 +350,33 @@ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather Clients can send a `runId` from a long-gone workflow (localStorage, back button, server restart). Wrap the follow-up path in a try/catch for `not found` / `expired` and fall through to the first-turn code path to start a fresh workflow. -## streamText vs DurableAgent +## streamText vs WorkflowAgent -| | `streamText()` | `DurableAgent` | +| | `streamText()` | [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) | |---|---|---| -| **Tool loop** | AI SDK handles via `stopWhen` | DurableAgent handles internally | +| **Tool loop** | AI SDK handles via `stopWhen` | WorkflowAgent handles internally | | **LLM call durability** | Re-executes on replay | Each LLM call is a durable step | -| **Stop conditions** | `stopWhen`, `prepareStep` | `prepareStep` only | -| **Structured output** | `Output.object()`, `Output.array()` | Not available | -| **Step callbacks** | `onStepFinish`, `onChunk` | Not available | -| **Setup** | Manual stream piping | Automatic | +| **Tool call durability** | Not individually durable — re-executes with the parent turn | Per tool — mark `"use step"` for a durable, retryable step | +| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` | +| **Structured output** | `Output.object()`, `Output.array()` | `Output.object()` via `stream()` | +| **Step callbacks** | `onStepFinish`, `onChunk` | Lifecycle callbacks on the agent | +| **Setup** | Manual stream piping | `getWritable()` + `createModelCallToUIChunkTransform()` | -Use `DurableAgent` for most agent use cases. Use `streamText` when you need the additional control. +Use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for most agent use cases. Use `streamText` when you need the additional control. The legacy [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) docs remain available but will be deprecated soon. ## Key APIs **AI SDK** ([docs](https://ai-sdk.dev/docs)) * [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) — core streaming function; `toUIMessageStream()` pipes into the durable writable -* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools wrap `"use step"` functions so each tool call is replayed from the log, not re-executed +* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools are plain async functions invoked by `streamText` inside the turn step; they are **not** individually durable in this pattern (see [Pitfalls](#tools-are-not-individually-durable)) * [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn * [`convertToModelMessages()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/convert-to-model-messages) / [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response) — UI ↔ model message conversion at the API boundary * [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) — React hook that consumes the UI message stream on the client **Workflow SDK** -* [`"use step"`](/docs/api-reference/workflow/use-step) — makes tool executions durable +* [`"use step"`](/docs/api-reference/workflow/use-step) — applied to `runTurn` to make each turn a durable, retryable unit * [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages * [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output * [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams diff --git a/docs/content/docs/v4/foundations/streaming.mdx b/docs/content/docs/v4/foundations/streaming.mdx index b21a0ea01c..6d33c71092 100644 --- a/docs/content/docs/v4/foundations/streaming.mdx +++ b/docs/content/docs/v4/foundations/streaming.mdx @@ -347,6 +347,10 @@ export async function batchProcessingWorkflow(items: string[]) { ### Streaming AI Responses with `DurableAgent` + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. + + Stream AI-generated content using [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) from `@workflow/ai`. Tools can also emit progress updates to the same stream using [data chunks](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) with the [`UIMessageChunk`](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) type from the AI SDK: ```typescript title="workflows/ai-assistant.ts" lineNumbers diff --git a/docs/content/docs/v5/api-reference/workflow-ai/durable-agent.mdx b/docs/content/docs/v5/api-reference/workflow-ai/durable-agent.mdx index edd95a8985..a54cdd8d94 100644 --- a/docs/content/docs/v5/api-reference/workflow-ai/durable-agent.mdx +++ b/docs/content/docs/v5/api-reference/workflow-ai/durable-agent.mdx @@ -11,6 +11,10 @@ related: The `DurableAgent` class enables you to create AI-powered agents that can maintain state across workflow steps, call tools, and gracefully handle interruptions and resumptions. + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. See [Migrating from DurableAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent) in the AI SDK docs. + + Tool calls can be implemented as workflow steps for automatic retries, or as regular workflow-level logic utilizing core library features such as [`sleep()`](/docs/api-reference/workflow/sleep) and [Hooks](/docs/foundations/hooks). ```typescript lineNumbers diff --git a/docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx b/docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx index 3ef5dca9f3..86be220197 100644 --- a/docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx +++ b/docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx @@ -7,6 +7,10 @@ summary: Convert an AI SDK Agent into a DurableAgent backed by a workflow, with Use this pattern to make any AI SDK agent durable. The agent becomes a workflow, tools become steps, and the framework handles retries, streaming, and state persistence automatically. + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. See [Migrating from DurableAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent) in the AI SDK docs. + + ## When to use this - Any AI agent with tool calls that should survive crashes and restarts diff --git a/docs/content/docs/v5/cookbook/index.mdx b/docs/content/docs/v5/cookbook/index.mdx index 309f087c21..93d01db06b 100644 --- a/docs/content/docs/v5/cookbook/index.mdx +++ b/docs/content/docs/v5/cookbook/index.mdx @@ -8,7 +8,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f ## Agent Patterns -- [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Replace a stateless AI agent with one that survives crashes and retries tool calls +- [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Legacy durable agent pattern (deprecated soon — prefer [WorkflowAgent](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent)) - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision - [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation) — Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race` diff --git a/docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx b/docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx index a5676e0b92..4cb1df56c8 100644 --- a/docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx +++ b/docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx @@ -2,7 +2,7 @@ title: AI SDK description: Use AI SDK's streamText directly inside durable workflows for lower-level control over model calls and tool execution. type: guide -summary: Use streamText() inside a workflow for full control over model options, stop conditions, and output schemas — while tools remain durable steps. +summary: Use streamText() inside a workflow for full control over model options, stop conditions, and output schemas. The per-turn step is durable; individual tool calls inside it are not. related: - /docs/ai - /docs/ai/chat-session-modeling @@ -11,17 +11,17 @@ related: - /docs/api-reference/workflow-ai/durable-agent --- -[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making those calls durable: the model request, the tool loop, and the multi-turn conversation all survive restarts and timeouts. +[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making the multi-turn loop durable: the conversation state, hooks, and per-turn responses survive restarts and timeouts. Note that in this pattern the durability boundary is the entire turn — individual tool calls inside a turn are **not** durable on their own (see [Pitfalls](#tools-are-not-individually-durable) below). For the full AI SDK reference (providers, `streamText`, `generateObject`, `useChat`, tool calling, etc.) see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points. -For most agent use cases, prefer [`DurableAgent`](/cookbook/agent-patterns/durable-agent) which wraps [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) and manages the tool loop automatically. This page covers using `streamText()` directly when you need lower-level control. +For most agent use cases, prefer [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow`, which manages the tool loop automatically and runs tools as durable workflow steps. This page covers using `streamText()` directly when you need AI SDK features that require lower-level control — accepting that tools inside a turn are no longer individually durable unless you wrap them yourself. ## When to use streamText directly -Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `DurableAgent` when you need: +Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) or the legacy `DurableAgent` when you need: * **Custom stop conditions** — [`stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions), [`prepareStep`](https://ai-sdk.dev/docs/ai-sdk-core/agents#prepare-step), or [`onStepFinish`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#on-step-finish) callbacks * **Structured output** — [`Output.object()`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) or `Output.array()` alongside tool calling @@ -52,14 +52,16 @@ export const turnHook = defineHook({ // [!code highlight] schema: z.object({ message: z.string() }), }); +// Tools are plain async functions in this pattern. `streamText` calls them +// from inside `runTurn` (a step), and the `"use step"` directive is a no-op +// when called from another step — see the "Tools are not individually durable" +// pitfall below. Make side-effectful tools idempotent. async function lookupOrder({ orderId }: { orderId: string }) { - "use step"; const res = await fetch(`https://api.store.com/orders/${orderId}`); return res.json(); } async function processRefund({ orderId, reason }: { orderId: string; reason: string }) { - "use step"; const res = await fetch("https://api.store.com/refunds", { method: "POST", body: JSON.stringify({ orderId, reason }), @@ -294,16 +296,32 @@ export function SupportChat() { ## How it works 1. **One workflow = one conversation.** The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns. -2. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`. -3. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it. -4. **`sliceUntilFinish`** in the API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released — not cancelled — so the workflow stream keeps flowing. -5. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns. -6. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming". +2. **`runTurn` is the durability boundary.** Each turn is one step. The model request and all tool calls inside it run as plain inline functions within that step. If anything throws mid-turn, the whole `runTurn` retries — individual tool calls are not separately durable. See [Pitfalls](#tools-are-not-individually-durable). +3. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`. +4. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it. +5. **`sliceUntilFinish`** in the API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released — not cancelled — so the workflow stream keeps flowing. +6. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns. +7. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming". ## Pitfalls Non-obvious correctness details worth knowing before adapting this pattern. +### Tools are not individually durable + +`streamText()` is invoked from inside `runTurn` (a `"use step"` function), and the AI SDK calls each tool by directly invoking its `execute` function in that same step. Even if a tool body has its own `"use step"` directive, that directive is a [no-op when called from another step](/docs/foundations/workflows-and-steps#step-functions) — the function just runs inline. + +The consequences: + +- The atomic retry unit is the entire `runTurn`, not the individual tool call. +- If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again. +- Tool calls do not appear as separate entries in the event log or observability dashboard. + +**Mitigations:** + +- Make side-effectful tool implementations idempotent — dedupe server-side on a stable key (e.g. `orderId`, an `Idempotency-Key` header, etc.). +- Or use [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent), which runs tools at workflow scope — each tool can be marked `"use step"` to become its own durable, retryable step, or stay at workflow level to use primitives like `sleep()` and hooks. + ### Snapshot `tailIndex` *before* resuming the hook {/* @skip-typecheck - fragment referencing variables from the surrounding multi-turn pattern */} @@ -332,32 +350,33 @@ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather Clients can send a `runId` from a long-gone workflow (localStorage, back button, server restart). Wrap the follow-up path in a try/catch for `not found` / `expired` and fall through to the first-turn code path to start a fresh workflow. -## streamText vs DurableAgent +## streamText vs WorkflowAgent -| | `streamText()` | `DurableAgent` | +| | `streamText()` | [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) | |---|---|---| -| **Tool loop** | AI SDK handles via `stopWhen` | DurableAgent handles internally | +| **Tool loop** | AI SDK handles via `stopWhen` | WorkflowAgent handles internally | | **LLM call durability** | Re-executes on replay | Each LLM call is a durable step | -| **Stop conditions** | `stopWhen`, `prepareStep` | `prepareStep` only | -| **Structured output** | `Output.object()`, `Output.array()` | Not available | -| **Step callbacks** | `onStepFinish`, `onChunk` | Not available | -| **Setup** | Manual stream piping | Automatic | +| **Tool call durability** | Not individually durable — re-executes with the parent turn | Per tool — mark `"use step"` for a durable, retryable step | +| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` | +| **Structured output** | `Output.object()`, `Output.array()` | `Output.object()` via `stream()` | +| **Step callbacks** | `onStepFinish`, `onChunk` | Lifecycle callbacks on the agent | +| **Setup** | Manual stream piping | `getWritable()` + `createModelCallToUIChunkTransform()` | -Use `DurableAgent` for most agent use cases. Use `streamText` when you need the additional control. +Use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for most agent use cases. Use `streamText` when you need the additional control. The legacy [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) docs remain available but will be deprecated soon. ## Key APIs **AI SDK** ([docs](https://ai-sdk.dev/docs)) * [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) — core streaming function; `toUIMessageStream()` pipes into the durable writable -* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools wrap `"use step"` functions so each tool call is replayed from the log, not re-executed +* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools are plain async functions invoked by `streamText` inside the turn step; they are **not** individually durable in this pattern (see [Pitfalls](#tools-are-not-individually-durable)) * [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn * [`convertToModelMessages()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/convert-to-model-messages) / [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response) — UI ↔ model message conversion at the API boundary * [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) — React hook that consumes the UI message stream on the client **Workflow SDK** -* [`"use step"`](/docs/api-reference/workflow/use-step) — makes tool executions durable +* [`"use step"`](/docs/api-reference/workflow/use-step) — applied to `runTurn` to make each turn a durable, retryable unit * [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages * [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output * [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams diff --git a/docs/content/docs/v5/foundations/streaming.mdx b/docs/content/docs/v5/foundations/streaming.mdx index b21a0ea01c..6d33c71092 100644 --- a/docs/content/docs/v5/foundations/streaming.mdx +++ b/docs/content/docs/v5/foundations/streaming.mdx @@ -347,6 +347,10 @@ export async function batchProcessingWorkflow(items: string[]) { ### Streaming AI Responses with `DurableAgent` + +**Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. + + Stream AI-generated content using [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) from `@workflow/ai`. Tools can also emit progress updates to the same stream using [data chunks](https://ai-sdk.dev/docs/ai-sdk-ui/streaming-data#streaming-custom-data) with the [`UIMessageChunk`](https://ai-sdk.dev/docs/ai-sdk-ui/stream-protocol) type from the AI SDK: ```typescript title="workflows/ai-assistant.ts" lineNumbers diff --git a/docs/lib/cookbook-tree.ts b/docs/lib/cookbook-tree.ts index 25e6748bd5..437ae1becd 100644 --- a/docs/lib/cookbook-tree.ts +++ b/docs/lib/cookbook-tree.ts @@ -133,7 +133,7 @@ export const recipes: Record = { slug: 'durable-agent', title: 'Durable Agent', description: - 'Replace a stateless AI agent with a durable one that survives crashes, retries tool calls, and streams output.', + 'Legacy durable agent pattern (deprecated soon — prefer WorkflowAgent from @ai-sdk/workflow). Survives crashes, retries tool calls, and streams output.', category: 'agent-patterns', }, 'human-in-the-loop': { @@ -179,7 +179,7 @@ export const recipes: Record = { slug: 'child-workflows', title: 'Child Workflows', description: - 'Spawn and orchestrate child workflows from a parent, polling for completion and handling partial failures.', + 'Spawn and orchestrate child workflows from a parent, waiting for completion via hook resume and handling partial failures.', category: 'advanced', }, 'distributed-abort-controller': { diff --git a/packages/ai/src/agent/durable-agent.ts b/packages/ai/src/agent/durable-agent.ts index b3fbfe6e3c..83f7fc9862 100644 --- a/packages/ai/src/agent/durable-agent.ts +++ b/packages/ai/src/agent/durable-agent.ts @@ -751,6 +751,10 @@ export interface DurableAgentStreamResult< /** * A class for building durable AI agents within workflows. * + * @deprecated `DurableAgent` will be deprecated soon. For new durable agent + * workflows, use `WorkflowAgent` from `@ai-sdk/workflow` instead. + * https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent + * * DurableAgent enables you to create AI-powered agents that can maintain state * across workflow steps, call tools, and gracefully handle interruptions and resumptions. * It integrates seamlessly with the AI SDK and the Workflow SDK for diff --git a/skills/workflow/SKILL.md b/skills/workflow/SKILL.md index 40d9993fad..6f8e096b2e 100644 --- a/skills/workflow/SKILL.md +++ b/skills/workflow/SKILL.md @@ -132,6 +132,8 @@ export async function myWorkflow() { ## DurableAgent — AI Agents in Workflows +> **Deprecation notice:** `DurableAgent` will be deprecated soon. For new durable agent workflows, use [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) from `@ai-sdk/workflow` instead. + Use `DurableAgent` to build AI agents that maintain state and survive interruptions. It handles the workflow sandbox automatically (no manual `globalThis.fetch` needed). ```typescript