Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/durable-agent-v6-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/ai': patch
---

Add AI SDK v6 compatibility to DurableAgent: accept model objects (V2/V3) with auto-conversion to string IDs, normalize V3 finish reason and usage formats, pass through typed ToolResultOutput without re-wrapping, and add `instructions` alias for `system`
123 changes: 123 additions & 0 deletions docs/content/docs/api-reference/workflow-ai/durable-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,72 @@ export default OutputSpecification;`}
- **Flexible Tool Implementation**: Tools can be implemented as workflow steps for automatic retries, or as regular workflow-level logic
- **Stream Processing**: Handles streaming responses and tool calls in a structured way
- **Workflow Native**: Fully integrated with Workflow DevKit for production-grade reliability
- **AI SDK v5 and v6**: Accepts `LanguageModelV2` and `LanguageModelV3` model objects, with runtime normalization of V3 stream differences
- **AI SDK Parity**: Supports the same options as AI SDK's `streamText` including generation settings, callbacks, and structured output

## Model Configuration

The `model` field accepts three forms:

| Form | Example | Recommended |
|------|---------|-------------|
| String ID | `"anthropic/claude-sonnet-4.5"` | Yes |
| Model object | `anthropic("claude-sonnet-4.5")` | Accepted |
| Factory function | `() => Promise<model>` | Deprecated |

**String IDs** are the recommended approach. The string is resolved via the AI SDK gateway inside the workflow step, so it crosses the step boundary cleanly.

**Model objects** from both AI SDK v5 (`LanguageModelV2`) and AI SDK v6 (`LanguageModelV3`) are accepted. They are automatically converted to string IDs using the object's `provider` and `modelId` fields. Middleware and wrappers applied to the model object are **not preserved** across the step boundary — use `providerOptions` to configure provider-specific settings instead.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would this approach work? you can't serialize the doStream etc. functions inside LanguageModelV3 can't be serialized which is why we do the factory function version that allows us to hack around it


**Factory functions** are deprecated and will not work in workflow mode. Use a string model ID instead.

<Callout type="info">
Because workflow steps serialize all arguments across the step boundary, model objects (which contain methods) cannot be passed directly to the underlying `streamText` call. DurableAgent extracts the `provider/modelId` string and resolves it via the gateway inside the step.
</Callout>

### Instructions Alias

The `instructions` field is an alias for `system`, available on both the constructor and `stream()` options. When both are provided, `instructions` takes precedence:

```typescript
const agent = new DurableAgent({
model: "anthropic/claude-sonnet-4.5",
instructions: "You are a helpful coding assistant.",
});

// Override per-stream call
await agent.stream({
messages,
writable,
instructions: "You are a helpful writing assistant.",
});
```

### Tool Result Types

Tool `execute` functions can return typed `ToolResultOutput` values that pass through to the model without re-wrapping. Supported output types include `text`, `json`, `content` (for multimodal responses), `error-text`, and `error-json`:

```typescript
tools: {
analyzeImage: {
description: "Analyze an image",
inputSchema: z.object({ url: z.string() }),
execute: async ({ url }) => {
// Return typed content — passes through to the model as-is
return {
type: "content" as const,
value: [
{ type: "text", text: "Analysis complete" },
{ type: "image", data: imageBuffer, mimeType: "image/png" },
],
};
},
},
},
```

If a tool returns a plain string or object without a recognized `type` field, DurableAgent wraps it as `text` or `json` automatically (the existing behavior).

## Good to Know

- Tools can be implemented as workflow steps (using `"use step"` for automatic retries), or as regular workflow-level logic
Expand All @@ -213,6 +277,7 @@ export default OutputSpecification;`}
- Generation settings (temperature, maxOutputTokens, etc.) can be set on the constructor and overridden per-stream call
- Use `activeTools` to limit which tools are available for a specific stream call
- The `onFinish` callback is called when all steps complete; `onAbort` is called if aborted
- The `instructions` field is an alias for `system` — use whichever name you prefer

## Examples

Expand Down Expand Up @@ -834,6 +899,64 @@ async function saveConversation(messages: UIMessage[]) {
The `uiMessages` property is only available when `collectUIMessages` is set to `true`. When disabled, `uiMessages` is `undefined`.
</Callout>

## Migrating from Wrapped Models

If you previously used `wrapLanguageModel` with middleware (e.g., `defaultSettingsMiddleware` for gateway routing or thinking configuration), those settings are lost at the step boundary. Replace them with `providerOptions`, which are serialized as plain objects and cross the boundary correctly.

### Before

```typescript
import { wrapLanguageModel, defaultSettingsMiddleware } from "ai";

// Middleware carries settings — these are lost at the step boundary
const model = wrapLanguageModel({
model: gateway("anthropic/claude-sonnet-4.5"),
middleware: defaultSettingsMiddleware({
settings: {
providerOptions: {
gateway: { only: ["anthropic"], order: ["anthropic"] },
anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } },
},
},
}),
});

const agent = new DurableAgent({ model });
```

### After

```typescript
// Settings passed directly — these cross the step boundary as plain objects
const agent = new DurableAgent({
model: "anthropic/claude-sonnet-4.5",
providerOptions: {
gateway: { only: ["anthropic"], order: ["anthropic"] },
anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } },
},
});
```

### Dynamic Per-Step Configuration

Use `prepareStep` to adjust `providerOptions` dynamically based on step context:

```typescript
const agent = new DurableAgent({
model: "anthropic/claude-sonnet-4.5",
});

await agent.stream({
messages,
writable,
prepareStep: ({ stepNumber }) => ({
providerOptions: stepNumber > 3
? { anthropic: { thinking: { type: "enabled", budgetTokens: 20000 } } }
: undefined,
}),
});
```

## See Also

- [Building Durable AI Agents](/docs/ai) - Complete guide to creating durable agents
Expand Down
128 changes: 0 additions & 128 deletions packages/ai/src/agent/do-stream-step.test.ts

This file was deleted.

Loading