Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4e81681
Add DurableAgent compat tests, e2e agent tests, and migrate to AI SDK v6
pranaygp Mar 12, 2026
870b4a5
Address PR review feedback
pranaygp Mar 12, 2026
e4da8aa
Remove streamTextIterator mock from compat tests, use it.fails for gaps
pranaygp Mar 12, 2026
9ef5174
Implement Tier 1+2 gaps, add @workflow/ai/test mock provider, wire e2…
pranaygp Mar 12, 2026
e0148e0
Remove e2e agent tests — mock models can't serialize across step boun…
pranaygp Mar 12, 2026
876c822
Add working e2e agent tests with mock model step factories
pranaygp Mar 13, 2026
2fdeb6d
Use @workflow/ai/test package imports for e2e mock models
pranaygp Mar 13, 2026
09cbbf6
Simplify mock provider, add comprehensive e2e tests for all features …
pranaygp Mar 13, 2026
b0f9732
Add tool approval (needsApproval) gap tests, fix SWC closure var binding
pranaygp Mar 13, 2026
087da88
Add default args for agent e2e workflows in UI definitions
pranaygp Mar 13, 2026
6b848dd
Add DurableAgent chat UI with tools, update docs for AI SDK v6
pranaygp Mar 13, 2026
2ae6460
Fix tool rendering, add reasoning support, model picker, observabilit…
pranaygp Mar 13, 2026
dbaca20
Fix tool output rendering: use input/output props on ToolInput/ToolOu…
pranaygp Mar 13, 2026
cbad07d
Fix: Documentation for `PrepareStepInfo` and `PrepareStepResult` inte…
vercel[bot] Mar 13, 2026
d7fbcce
Fix instructions tests: flip from it.fails to it, update snapshots
pranaygp Mar 13, 2026
08bddb0
Fix getReadable call: pass startIndex as options object
pranaygp Mar 13, 2026
923d1e5
Fix docs type errors and turbopack build
pranaygp Mar 13, 2026
b3b15bc
Fix pnpm-workspace.yaml: use double quotes for catalog entries
pranaygp Mar 13, 2026
c62e4e4
Fix CI build failures, add changeset
pranaygp Mar 13, 2026
43aa31d
Sync webpack workbench with turbopack: add chat UI deps and symlinks
pranaygp Mar 13, 2026
312ce54
Fix circular symlink: restore chat-client.tsx as real file in turbopack
pranaygp Mar 13, 2026
7a255d8
Add missing deps to webpack: use-stick-to-bottom, radix-ui, @vercel/blob
pranaygp Mar 13, 2026
2551e0f
Fix model picker: merge body params via prepareSendMessagesRequest
pranaygp Mar 13, 2026
346a60b
Fix WorkflowChatTransport: forward body/headers from ChatRequestOptions
pranaygp Mar 13, 2026
a87a6a3
Fix model IDs: use real AI Gateway model names
pranaygp Mar 13, 2026
f592ba8
Use correct AI Gateway model IDs: Opus 4.5, GPT-5.2, GPT-5.3
pranaygp Mar 13, 2026
ac985ca
Enable reasoning for all model providers
pranaygp Mar 13, 2026
17c1a15
Fix OpenAI reasoning: use 'medium' effort (GPT-5.3 doesn't support 'h…
pranaygp Mar 13, 2026
1f5caf5
Merge remote-tracking branch 'origin/main' into pgp/durable-agent-tes…
pranaygp Mar 13, 2026
2876a1a
Address all PR review comments
pranaygp Mar 13, 2026
bdcb25b
Remove accidentally created empty mock2.ts
pranaygp Mar 13, 2026
2a09c01
Merge remote-tracking branch 'origin/main' into pgp/durable-agent-tes…
pranaygp Mar 13, 2026
11738e6
Change changeset back to minor for breaking AI SDK v6 migration
pranaygp Mar 13, 2026
192ec41
Fix agent e2e tests: add Vercel world setup for CI
pranaygp Mar 13, 2026
37d18a4
Deduplicate e2e test utilities: extract shared code to utils.ts
pranaygp Mar 13, 2026
6d5aaa2
Fix missing deploymentUrl args in e2e.test.ts after utils refactor
pranaygp Mar 13, 2026
2ce818d
Add @workflow/ai dep to all workbenches for agent e2e tests
pranaygp Mar 13, 2026
92c51e1
Merge remote-tracking branch 'origin/main' into pgp/durable-agent-tes…
pranaygp Mar 13, 2026
d09f5a4
Fix missing imports in e2e.test.ts: add fetchManifest and sleep
pranaygp Mar 13, 2026
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
17 changes: 17 additions & 0 deletions .changeset/durable-agent-ai-sdk-v6.md
Comment thread
VaguelySerious marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@workflow/ai": minor
---

**BREAKING CHANGE**: Migrate to AI SDK v6. Drop AI SDK v5 support.

- Migrate all types from V2 to V3 (`LanguageModelV2*` → `LanguageModelV3*`)
- Update peer dependency: `ai` `^5 || ^6` → `^6`, `@ai-sdk/provider` `^2 || ^3` → `^3`
- Simplify `CompatibleLanguageModel` from V2|V3 union to `LanguageModelV3`
- Remove `providerExecuted` guard on tool-result stream parts (V3: all tool-results are provider-executed)
- Add `instructions` constructor option (replaces deprecated `system`)
- Add `onStepFinish` and `onFinish` on constructor (merged with stream callbacks)
- Add `timeout` stream option
- Enrich `onFinish` event with `text`, `finishReason`, `totalUsage`
- Add `@workflow/ai/test` export with `mockTextModel` and `mockSequenceModel` for workflow e2e testing
- Update `OutputSpecification` to match AI SDK v6 Output interface
- Fix `WorkflowChatTransport` to forward `body` and `headers` from `ChatRequestOptions` to `prepareSendMessagesRequest` and the default request body
8 changes: 4 additions & 4 deletions docs/content/docs/ai/chat-session-modeling.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ export async function chat(messages: UIMessage[]) {

const agent = new DurableAgent({
model: "bedrock/claude-haiku-4-5-20251001-v1",
system: FLIGHT_ASSISTANT_PROMPT,
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});

await agent.stream({
messages: convertToModelMessages(messages), // [!code highlight] Full history from client
messages: await convertToModelMessages(messages), // [!code highlight] Full history from client
writable,
});
}
Expand Down Expand Up @@ -171,7 +171,7 @@ export async function chat(initialMessages: UIMessage[]) {

const { workflowRunId: runId } = getWorkflowMetadata();
const writable = getWritable<UIMessageChunk>();
const messages: ModelMessage[] = convertToModelMessages(initialMessages);
const messages: ModelMessage[] = await convertToModelMessages(initialMessages);

// Write markers for initial user messages (for replay) // [!code highlight]
for (const msg of initialMessages) { // [!code highlight]
Expand All @@ -183,7 +183,7 @@ export async function chat(initialMessages: UIMessage[]) {

const agent = new DurableAgent({
model: "bedrock/claude-haiku-4-5-20251001-v1",
system: FLIGHT_ASSISTANT_PROMPT,
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/ai/defining-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ When you tool needs access to the full message history, you can access it via th
```typescript title="tools.ts" lineNumbers
async function getWeather(
{ city }: { city: string },
{ messages, toolCallId }: { messages: LanguageModelV2Prompt, toolCallId: string }) { // [!code highlight]
{ messages, toolCallId }: { messages: ModelMessage[], toolCallId: string }) { // [!code highlight]
"use step";
return `Weather in ${city} is sunny`;
}
Expand Down
10 changes: 5 additions & 5 deletions docs/content/docs/ai/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const agent = new Agent({ // [!code highlight]
model: gateway("bedrock/claude-4-5-haiku-20251001-v1"),
system: FLIGHT_ASSISTANT_PROMPT,
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});
const modelMessages = convertToModelMessages(messages);
const stream = agent.stream({ messages: modelMessages }); // [!code highlight]
const modelMessages = await convertToModelMessages(messages);
const stream = await agent.stream({ messages: modelMessages }); // [!code highlight]
return createUIMessageStreamResponse({
stream: stream.toUIMessageStream(),
});
Expand Down Expand Up @@ -273,7 +273,7 @@ export async function chatWorkflow(messages: ModelMessage[]) {
// ELSE if using a custom provider, pass the provider call as an argument:
model: openai("gpt-5.1"), // [!code highlight]

system: FLIGHT_ASSISTANT_PROMPT,
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});

Expand Down Expand Up @@ -304,7 +304,7 @@ import { chatWorkflow } from "@/workflows/chat/workflow";

export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const modelMessages = convertToModelMessages(messages);
const modelMessages = await convertToModelMessages(messages);

const run = await start(chatWorkflow, [modelMessages]); // [!code highlight]

Expand Down
12 changes: 6 additions & 6 deletions docs/content/docs/ai/message-queueing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ The `prepareStep` callback runs before each step in the agent loop. It receives

```typescript lineNumbers
interface PrepareStepInfo {
model: string | (() => Promise<LanguageModelV2>); // Current model
model: string | (() => Promise<LanguageModel>); // Current model
stepNumber: number; // 0-indexed step count
steps: StepResult[]; // Previous step results
messages: LanguageModelV2Prompt; // Messages to be sent
messages: ModelMessage[]; // Messages to be sent
}

interface PrepareStepResult {
model?: string | (() => Promise<LanguageModelV2>); // Override model
messages?: LanguageModelV2Prompt; // Override messages
model?: string | (() => Promise<LanguageModel>); // Override model
messages?: ModelMessage[]; // Override messages
}
```

Expand All @@ -65,7 +65,7 @@ export async function chat(initialMessages: ModelMessage[]) {

const agent = new DurableAgent({
model: "bedrock/claude-haiku-4-5-20251001-v1",
system: FLIGHT_ASSISTANT_PROMPT,
instructions: FLIGHT_ASSISTANT_PROMPT,
tools: flightBookingTools,
});

Expand Down Expand Up @@ -101,7 +101,7 @@ export async function chat(initialMessages: ModelMessage[]) {
Messages sent via `chatMessageHook.resume()` accumulate in the queue and get injected before the next step, whether that's a tool call or another LLM request.

<Callout type="info">
The `prepareStep` callback receives messages in `LanguageModelV2Prompt` format (with content arrays), which is the internal format used by the AI SDK.
The `prepareStep` callback receives messages in `ModelMessage[]` format (with content arrays), which is the internal format used by the AI SDK.
</Callout>

## Combining with Multi-Turn Sessions
Expand Down
10 changes: 5 additions & 5 deletions docs/content/docs/api-reference/workflow-ai/durable-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function myAgent() {

const agent = new DurableAgent({
model: "anthropic/claude-haiku-4.5",
system: "You are a helpful weather assistant.",
instructions: "You are a helpful weather assistant.",
temperature: 0.7,
tools: {
getWeather: {
Expand Down Expand Up @@ -249,7 +249,7 @@ async function weatherAgentWorkflow(userQuery: string) {
execute: getWeather,
},
},
system: "You are a helpful weather assistant. Always provide accurate weather information.",
instructions: "You are a helpful weather assistant. Always provide accurate weather information.",
});

await agent.stream({
Expand Down Expand Up @@ -446,7 +446,7 @@ async function agentWithPrepareStep(userMessage: string) {

const agent = new DurableAgent({
model: "openai/gpt-4.1-mini", // Default model
system: "You are a helpful assistant.",
instructions: "You are a helpful assistant.",
});

await agent.stream({
Expand Down Expand Up @@ -500,7 +500,7 @@ async function agentWithMessageQueue(initialMessage: string) {

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

await agent.stream({
Expand Down Expand Up @@ -812,7 +812,7 @@ async function agentWithUIMessages(userMessage: string) {

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

const result = await agent.stream({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"test": "turbo test",
"clean": "turbo clean",
"typecheck": "turbo typecheck",
"test:e2e": "vitest run packages/core/e2e/e2e.test.ts",
"test:e2e": "vitest run packages/core/e2e/e2e.test.ts packages/core/e2e/e2e-agent.test.ts",
"test:e2e:nextjs-webpack:staged": "node scripts/test-staged-nextjs-webpack.mjs",
"test:docs": "pnpm --filter @workflow/docs-typecheck test:docs",
"bench": "vitest bench packages/core/e2e/bench.bench.ts",
Expand Down
19 changes: 12 additions & 7 deletions packages/ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"./xai": {
"types": "./dist/providers/xai.d.ts",
"default": "./dist/providers/xai.js"
},
"./test": {
"types": "./dist/providers/mock.d.ts",
"default": "./dist/providers/mock.js"
}
},
"scripts": {
Expand All @@ -68,18 +72,19 @@
"workflow": "workspace:*"
},
"peerDependencies": {
"ai": "^5 || ^6",
"ai": "^6",
"workflow": "workspace:^"
},
"dependencies": {
"@ai-sdk/provider": "^2.0.0 || ^3.0.0",
"@ai-sdk/provider": "^3.0.0",
"@workflow/serde": "workspace:^",
"zod": "catalog:"
},
"optionalDependencies": {
"@ai-sdk/anthropic": "^2.0.0 || ^3.0.0",
"@ai-sdk/gateway": "^2.0.0 || ^3.0.0",
"@ai-sdk/google": "^2.0.0 || ^3.0.0",
"@ai-sdk/openai": "^2.0.0 || ^3.0.0",
"@ai-sdk/xai": "^2.0.0 || ^3.0.0"
"@ai-sdk/anthropic": "^3.0.0",
"@ai-sdk/gateway": "^3.0.0",
"@ai-sdk/google": "^3.0.0",
"@ai-sdk/openai": "^3.0.0",
"@ai-sdk/xai": "^3.0.0"
}
}
40 changes: 18 additions & 22 deletions packages/ai/src/agent/do-stream-step.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ describe('normalizeFinishReason', () => {
it('should pass through "other"', () => {
expect(normalizeFinishReason('other')).toBe('other');
});

it('should pass through "unknown"', () => {
expect(normalizeFinishReason('unknown')).toBe('unknown');
});
});

describe('object finish reasons', () => {
Expand Down Expand Up @@ -59,20 +55,20 @@ describe('normalizeFinishReason', () => {
expect(normalizeFinishReason({ type: 'other' })).toBe('other');
});

it('should extract "unknown" from object', () => {
expect(normalizeFinishReason({ type: 'unknown' })).toBe('unknown');
it('should return "other" for object with unrecognized type', () => {
expect(normalizeFinishReason({ type: 'unknown' })).toBe('other');
});

it('should return "unknown" for object without type property', () => {
expect(normalizeFinishReason({})).toBe('unknown');
it('should return "other" for object without type property', () => {
expect(normalizeFinishReason({})).toBe('other');
});

it('should return "unknown" for object with null type', () => {
expect(normalizeFinishReason({ type: null })).toBe('unknown');
it('should return "other" for object with null type', () => {
expect(normalizeFinishReason({ type: null })).toBe('other');
});

it('should return "unknown" for object with undefined type', () => {
expect(normalizeFinishReason({ type: undefined })).toBe('unknown');
it('should return "other" for object with undefined type', () => {
expect(normalizeFinishReason({ type: undefined })).toBe('other');
});

it('should handle object with additional properties', () => {
Expand All @@ -87,24 +83,24 @@ describe('normalizeFinishReason', () => {
});

describe('edge cases', () => {
it('should return "unknown" for undefined', () => {
expect(normalizeFinishReason(undefined)).toBe('unknown');
it('should return "other" for undefined', () => {
expect(normalizeFinishReason(undefined)).toBe('other');
});

it('should return "unknown" for null', () => {
expect(normalizeFinishReason(null)).toBe('unknown');
it('should return "other" for null', () => {
expect(normalizeFinishReason(null)).toBe('other');
});

it('should return "unknown" for number', () => {
expect(normalizeFinishReason(42)).toBe('unknown');
it('should return "other" for number', () => {
expect(normalizeFinishReason(42)).toBe('other');
});

it('should return "unknown" for boolean', () => {
expect(normalizeFinishReason(true)).toBe('unknown');
it('should return "other" for boolean', () => {
expect(normalizeFinishReason(true)).toBe('other');
});

it('should return "unknown" for array', () => {
expect(normalizeFinishReason(['stop'])).toBe('unknown');
it('should return "other" for array', () => {
expect(normalizeFinishReason(['stop'])).toBe('other');
});

it('should handle empty string', () => {
Expand Down
Loading
Loading