mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
docs(ai): clean up WorkflowAgent docs and examples (#3891)
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@workflow/rollup": patch
|
||||
---
|
||||
|
||||
Disable implicit input source map loading during workflow transforms to avoid false missing-map build errors for dependencies.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"workflow": patch
|
||||
"@workflow/core": patch
|
||||
"@workflow/ai": patch
|
||||
---
|
||||
|
||||
Route Workflow AI examples through AI Gateway and recommend WorkflowAgent for Workflow 5.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@workflow/swc-plugin': patch
|
||||
---
|
||||
|
||||
Fix a crash ("Cannot redefine property: classId") when a bundler pipeline re-runs the transform over its own output for a dependency that ships custom serialization methods, such as `@ai-sdk/gateway`.
|
||||
@@ -42,7 +42,7 @@ export async function chat(messages: UIMessage[]) {
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
@@ -185,7 +185,7 @@ export async function chat(initialMessages: UIMessage[]) {
|
||||
} // [!code highlight]
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
@@ -57,56 +57,16 @@ cd workflow-examples/flight-booking-app
|
||||
|
||||
<Step>
|
||||
|
||||
### Set up API keys
|
||||
### Configure AI Gateway
|
||||
|
||||
To connect to an LLM, set up an API key. You can use Vercel Gateway, which works with all providers at zero markup, or configure a custom provider.
|
||||
<Tabs items={['Gateway', 'Custom Provider']}>
|
||||
AI SDK uses [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) as its default global provider, so plain `"provider/model"` strings need no provider-specific package. Vercel deployments authenticate with OIDC automatically. For local development, link the project and pull a short-lived OIDC token:
|
||||
|
||||
<Tab value="Gateway">
|
||||
|
||||
Get a Gateway API key from the [Vercel Gateway](https://vercel.com/docs/ai-gateway/authentication) page.
|
||||
|
||||
Then add it to your `.env.local` file:
|
||||
|
||||
```bash title=".env.local" lineNumbers
|
||||
GATEWAY_API_KEY=...
|
||||
```bash
|
||||
vercel link
|
||||
vercel env pull .env.local
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Custom Provider">
|
||||
|
||||
This is an example of how to use the OpenAI provider for AI SDK. For details on other providers and more details, see the [AI SDK provider guide](https://ai-sdk.dev/providers/ai-sdk-providers).
|
||||
|
||||
```package-install
|
||||
npm i @ai-sdk/openai
|
||||
```
|
||||
|
||||
Set your OpenAI API key in your environment variables:
|
||||
|
||||
```bash title=".env.local" lineNumbers
|
||||
OPENAI_API_KEY=...
|
||||
```
|
||||
|
||||
Then modify your API endpoint to use the OpenAI provider:
|
||||
|
||||
{/* @skip-typecheck: incomplete code sample */}
|
||||
```typescript title="app/api/chat/route.ts" lineNumbers
|
||||
// ...
|
||||
import { openai } from "@ai-sdk/openai"; // [!code highlight]
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// ...
|
||||
const agent = new Agent({
|
||||
// This uses the OPENAI_API_KEY environment variable by default, but you
|
||||
// can also pass { apiKey: string } as an option.
|
||||
model: openai("gpt-5.1"), // [!code highlight]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
You can alternatively set `AI_GATEWAY_API_KEY` from the [AI Gateway authentication](https://vercel.com/docs/ai-gateway/authentication) page.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
@@ -131,7 +91,7 @@ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
const agent = new ToolLoopAgent({ // [!code highlight]
|
||||
model: "bedrock/claude-4-5-haiku-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
@@ -257,12 +217,10 @@ export default withWorkflow(nextConfig);
|
||||
|
||||
Move the agent logic into a separate function, which will serve as our workflow definition.
|
||||
|
||||
{/* @skip-typecheck: Shows two mutually exclusive model options */}
|
||||
```typescript title="workflows/chat/workflow.ts" lineNumbers
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow"; // [!code highlight]
|
||||
import { getWritable } from "workflow"; // [!code highlight]
|
||||
import { tools } from "@/ai/tools";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "@/ai/tools";
|
||||
import { convertToModelMessages, type UIMessage } from "ai";
|
||||
|
||||
export async function chatWorkflow(messages: UIMessage[]) {
|
||||
@@ -271,13 +229,8 @@ export async function chatWorkflow(messages: UIMessage[]) {
|
||||
const writable = getWritable<ModelCallStreamPart>(); // [!code highlight]
|
||||
|
||||
const agent = new WorkflowAgent({ // [!code highlight]
|
||||
|
||||
// If using AI Gateway, specify the model name as a string:
|
||||
model: "bedrock/claude-4-5-haiku-20251001-v1", // [!code highlight]
|
||||
|
||||
// ELSE if using a custom provider, pass the provider call as an argument:
|
||||
model: openai("gpt-5.1"), // [!code highlight]
|
||||
|
||||
// Plain model strings use Vercel AI Gateway.
|
||||
model: "spacexai/grok-4.6", // [!code highlight]
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
@@ -29,27 +29,24 @@ If you need basic multi-turn conversations where messages arrive between turns,
|
||||
|
||||
## The `prepareStep` callback
|
||||
|
||||
The `prepareStep` callback runs before each step in the agent loop. It receives the current state and can modify the messages sent to the model:
|
||||
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 { ModelMessage, LanguageModel } from "ai";
|
||||
import type {
|
||||
PrepareStepInfo,
|
||||
PrepareStepResult,
|
||||
} from "@ai-sdk/workflow";
|
||||
|
||||
interface PrepareStepInfo {
|
||||
model: string | (() => Promise<LanguageModel>); // Current model
|
||||
stepNumber: number; // 0-indexed step count
|
||||
steps: StepResult[]; // Previous step results
|
||||
messages: ModelMessage[]; // Messages to be sent
|
||||
}
|
||||
|
||||
interface PrepareStepResult {
|
||||
model?: string | (() => Promise<LanguageModel>); // Override model
|
||||
messages?: ModelMessage[]; // Override messages
|
||||
}
|
||||
const prepareStep = (
|
||||
{ messages }: PrepareStepInfo
|
||||
): PrepareStepResult => ({ messages });
|
||||
```
|
||||
|
||||
## Injecting queued messages
|
||||
`PrepareStepInfo.messages` is a normalized `LanguageModelV4Prompt`, not the application-level `ModelMessage[]` accepted by `WorkflowAgent.stream()`.
|
||||
|
||||
Once you have a [multi-turn workflow](/docs/ai/chat-session-modeling#multi-turn-workflows), you can combine a message queue with `prepareStep` to inject messages that arrive during processing:
|
||||
## 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";
|
||||
@@ -63,112 +60,77 @@ export async function chat(initialMessages: ModelMessage[]) {
|
||||
|
||||
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: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
// Listen for messages in background (non-blocking) // [!code highlight]
|
||||
const hook = chatMessageHook.create({ token: runId }); // [!code highlight]
|
||||
hook.then(({ message }) => { // [!code highlight]
|
||||
messageQueue.push({ role: "user", content: message }); // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
await agent.stream({
|
||||
messages: initialMessages,
|
||||
writable,
|
||||
prepareStep: ({ messages: currentMessages }) => { // [!code highlight]
|
||||
// Inject any queued messages before the next LLM call // [!code highlight]
|
||||
if (messageQueue.length > 0) { // [!code highlight]
|
||||
const newMessages = messageQueue.splice(0); // Drain queue // [!code highlight]
|
||||
return { // [!code highlight]
|
||||
messages: [ // [!code highlight]
|
||||
...currentMessages, // [!code highlight]
|
||||
...newMessages.map((m) => ({ // [!code highlight]
|
||||
role: m.role, // [!code highlight]
|
||||
content: [{ type: "text" as const, text: m.content }], // [!code highlight]
|
||||
})), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
}; // [!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]
|
||||
return {}; // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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 `ModelMessage[]` format (with content arrays), which is the internal format used by the AI SDK.
|
||||
</Callout>
|
||||
|
||||
## Combining with multi-turn sessions
|
||||
|
||||
You can also combine message queueing with the standard multi-turn pattern:
|
||||
|
||||
```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 type { ModelMessage } from "ai";
|
||||
|
||||
export async function chat(initialMessages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const { workflowRunId: runId } = getWorkflowMetadata();
|
||||
const writable = getWritable<ModelCallStreamPart>();
|
||||
const messages: ModelMessage[] = [...initialMessages];
|
||||
const messageQueue: Array<{ role: "user"; content: string }> = [];
|
||||
|
||||
const agent = new WorkflowAgent({ /* ... */ });
|
||||
const hook = chatMessageHook.create({ token: runId });
|
||||
|
||||
while (true) {
|
||||
// Set up non-blocking listener for mid-turn messages // [!code highlight]
|
||||
let pendingMessage: string | null = null; // [!code highlight]
|
||||
hook.then(({ message }) => { // [!code highlight]
|
||||
if (message === "/done") return; // [!code highlight]
|
||||
messageQueue.push({ role: "user", content: message }); // [!code highlight]
|
||||
pendingMessage = message; // [!code highlight]
|
||||
}); // [!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 }) => {
|
||||
// Inject queued messages during turn // [!code highlight]
|
||||
if (messageQueue.length > 0) {
|
||||
const newMessages = messageQueue.splice(0);
|
||||
return {
|
||||
messages: [
|
||||
...currentMessages,
|
||||
...newMessages.map((m) => ({
|
||||
role: m.role,
|
||||
content: [{ type: "text" as const, text: m.content }],
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
const queued = messageQueue.splice(0); // Atomic drain // [!code highlight]
|
||||
if (queued.length === 0) return {};
|
||||
return { // [!code highlight]
|
||||
messages: [ // [!code highlight]
|
||||
...currentMessages, // [!code highlight]
|
||||
...queued.map(({ role, content }) => ({ // [!code highlight]
|
||||
role, // [!code highlight]
|
||||
content: [{ type: "text" as const, text: content }], // [!code highlight]
|
||||
})), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
}; // [!code highlight]
|
||||
},
|
||||
});
|
||||
messages = result.messages;
|
||||
|
||||
messages.push(...result.messages.slice(messages.length));
|
||||
if (stopped) break;
|
||||
await waitForMessage(); // [!code highlight]
|
||||
if (stopped) break;
|
||||
|
||||
// Wait for next message (either queued during turn or new) // [!code highlight]
|
||||
const { message: followUp } = pendingMessage ? { message: pendingMessage } : await hook; // [!code highlight]
|
||||
if (followUp === "/done") break;
|
||||
|
||||
messages.push({ role: "user", content: followUp });
|
||||
// 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
|
||||
|
||||
@@ -199,7 +199,7 @@ async function weatherAgentWorkflow(userQuery: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get current weather for a location",
|
||||
@@ -244,7 +244,7 @@ async function multiToolAgentWorkflow(userQuery: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get weather for a location",
|
||||
@@ -289,7 +289,7 @@ async function multiTurnAgentWorkflow() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
searchProducts: {
|
||||
description: "Search for products",
|
||||
@@ -368,7 +368,7 @@ async function agentWithLibraryFeaturesWorkflow(userRequest: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
scheduleTask: {
|
||||
description: "Pause the workflow for the specified number of seconds",
|
||||
@@ -405,7 +405,7 @@ async function agentWithPrepareStep(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "openai/gpt-4.1-mini", // Default model
|
||||
model: "spacexai/grok-4.6", // Default model
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -459,7 +459,7 @@ async function agentWithMessageQueue(initialMessage: string) {
|
||||
});
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -500,7 +500,7 @@ async function agentWithGenerationSettings() {
|
||||
|
||||
// Set default generation settings in constructor
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 2000,
|
||||
topP: 0.9,
|
||||
@@ -548,7 +548,7 @@ async function multiStepAgent() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
searchWeb: {
|
||||
description: "Search the web for information",
|
||||
@@ -588,7 +588,7 @@ async function agentWithCallbacks() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
@@ -630,7 +630,7 @@ async function agentWithStructuredOutput() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
const result = await agent.stream({
|
||||
@@ -665,7 +665,7 @@ async function agentWithToolChoice() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
calculator: {
|
||||
description: "Perform calculations",
|
||||
@@ -733,7 +733,7 @@ async function agentWithContext(userId: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getUserData: {
|
||||
description: "Get user data",
|
||||
@@ -771,7 +771,7 @@ async function agentWithUIMessages(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -819,7 +819,7 @@ async function agentWithToolInspection(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
checkOrderStatus: {
|
||||
description: "Check order status",
|
||||
@@ -874,7 +874,7 @@ async function agentWithTimeout(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Serializable Steps
|
||||
description: Wrap non-serializable third-party objects (like AI model providers) inside step factory functions so they can cross the workflow boundary.
|
||||
description: Wrap non-serializable third-party objects, including AI provider models and cloud clients, inside step factory functions.
|
||||
type: guide
|
||||
summary: Return a callback from a step to defer construction of a non-owned class (AI SDK models, cloud SDK clients) until execution time, making them usable inside durable workflows.
|
||||
summary: Defer construction of non-owned AI provider models and cloud SDK clients until step execution so they remain usable in durable workflows.
|
||||
related:
|
||||
- /docs/foundations/serialization
|
||||
- /docs/foundations/serialization#custom-class-serialization
|
||||
@@ -10,7 +10,7 @@ related:
|
||||
---
|
||||
|
||||
<CopyPrompt
|
||||
text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing the object (AI SDK model, cloud SDK client) into the workflow, export a factory that returns an async callback marked with "use step" which constructs and returns the object at execution time, for example `export function openai(...args) { return async () => { "use step"; return openaiProvider(...args); }; }`. Pass the factory across the workflow boundary (the compiler serializes the function reference, not the instance) and invoke it inside steps where full Node.js access is available. Keep the factory's constructor arguments serializable. Verify the workflow builds, replays deterministically, and the dependency is only instantiated during step execution."
|
||||
text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing an AI provider model, cloud SDK client, or other class instance into the workflow, export a factory that returns an async callback marked with "use step". Capture only serializable constructor options, construct the provider or client inside the step, and keep the instance inside that step's execution. Verify the workflow builds, replays deterministically, and never serializes the live dependency."
|
||||
/>
|
||||
|
||||
<Callout>
|
||||
@@ -22,97 +22,68 @@ This is an advanced guide. It dives into workflow internals and is not required
|
||||
Workflow functions run inside a sandboxed VM where every value that crosses a function boundary must be serializable. There are two ways to get a non-serializable object across that boundary, depending on whether you own the class:
|
||||
|
||||
- **You own the class**: implement the [`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE` protocol](/docs/foundations/serialization#custom-class-serialization). The instance becomes a first-class serializable value: you can pass it as a workflow input, return it from a step, and call `"use step"` instance methods on it directly. This is the right tool when the class is yours to modify.
|
||||
- **You don't own the class**: you can't add methods to `openai("gpt-4o")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction in a `"use step"` factory function and pass the factory across the boundary. That's what this page covers.
|
||||
- **You don't own the class**: you can't add serialization methods to `openai("gpt-5.6-sol")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction and use in a `"use step"` factory function. That's what this page covers.
|
||||
|
||||
## The problem
|
||||
|
||||
AI SDK model providers (`openai("gpt-4o")`, `anthropic("claude-sonnet-4-20250514")`, etc.) return complex objects with methods, closures, and internal state. Passing one directly into a step causes a serialization error, and you can't bolt `WORKFLOW_SERIALIZE` onto a third-party class.
|
||||
AI SDK provider models and cloud SDK clients often contain methods, closures, sockets, and internal state. Passing one across a workflow boundary causes a serialization error, and you can't add `WORKFLOW_SERIALIZE` to a class you don't own.
|
||||
|
||||
```typescript lineNumbers
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { getWritable } from "workflow";
|
||||
import type { UIMessageChunk } from "ai";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
export async function brokenAgent(prompt: string) {
|
||||
async function uploadFile(client: S3Client, key: string) {
|
||||
"use step";
|
||||
// ... upload with client ...
|
||||
}
|
||||
|
||||
export async function brokenUpload(region: string, key: string) {
|
||||
"use workflow";
|
||||
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
const agent = new DurableAgent({
|
||||
// This fails: the model object is not serializable
|
||||
model: openai("gpt-4o"),
|
||||
});
|
||||
|
||||
await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
|
||||
const client = new S3Client({ region });
|
||||
await uploadFile(client, key); // Fails: S3Client is not serializable
|
||||
}
|
||||
```
|
||||
|
||||
## The solution: step-as-factory
|
||||
|
||||
Instead of passing the model object, pass a **callback function** that returns the model. Marking that callback with `"use step"` tells the compiler to serialize the *function reference* (which is a string identifier) rather than its return value. The provider is only instantiated at execution time, inside the step's full Node.js runtime.
|
||||
Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
|
||||
|
||||
```typescript lineNumbers
|
||||
import { openai as openaiProvider } from "@ai-sdk/openai";
|
||||
|
||||
// Returns a step function, not a model object
|
||||
export function openai(...args: Parameters<typeof openaiProvider>) {
|
||||
return async () => {
|
||||
"use step";
|
||||
return openaiProvider(...args); // [!code highlight]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead of a model object. When the agent needs to call the large language model (LLM), it invokes the factory inside a step where the real provider can be constructed with full Node.js access.
|
||||
|
||||
## How `@workflow/ai` uses this
|
||||
|
||||
<Callout type="warn">
|
||||
`@workflow/ai`'s pre-wrapped providers and `DurableAgent` are deprecated. AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) resolves models from AI Gateway model strings (for example, `"openai/gpt-4o"`), which usually removes the need for a model factory; see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The serialization pattern on this page still applies to any non-serializable dependency you own (for example, cloud SDK clients).
|
||||
<Callout type="info">
|
||||
A plain Vercel AI Gateway model string such as `"spacexai/grok-4.6"` is already serializable and does not need a factory.
|
||||
</Callout>
|
||||
|
||||
The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:
|
||||
### AI provider example
|
||||
|
||||
When using an AI SDK provider package, construct and use its model inside the step. The outer factory captures only the serializable model ID:
|
||||
|
||||
```typescript lineNumbers
|
||||
// packages/ai/src/providers/anthropic.ts
|
||||
import { anthropic as anthropicProvider } from "@ai-sdk/anthropic";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { generateText } from "ai";
|
||||
|
||||
export function anthropic(...args: Parameters<typeof anthropicProvider>) {
|
||||
return async () => {
|
||||
export function createOpenAIGenerator(modelId: string) {
|
||||
return async (prompt: string) => {
|
||||
"use step";
|
||||
return anthropicProvider(...args); // [!code highlight]
|
||||
const { text } = await generateText({ model: openai(modelId), prompt });
|
||||
return text;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This means you import from `@workflow/ai` instead of `@ai-sdk/*` directly:
|
||||
|
||||
```typescript lineNumbers
|
||||
import { anthropic } from "@workflow/ai/anthropic";
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { getWritable } from "workflow";
|
||||
import type { UIMessageChunk } from "ai";
|
||||
|
||||
export async function chatAgent(prompt: string) {
|
||||
export async function summarize(prompt: string) {
|
||||
"use workflow";
|
||||
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
const agent = new DurableAgent({
|
||||
model: anthropic("claude-sonnet-4-20250514"), // [!code highlight]
|
||||
});
|
||||
|
||||
await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
|
||||
const generate = createOpenAIGenerator("gpt-5.6-sol");
|
||||
return generate(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
## Writing your own serializable wrapper
|
||||
The same structure works with provider packages such as `@ai-sdk/anthropic` and `@ai-sdk/google`: capture serializable configuration in the outer function and keep the provider object inside the step.
|
||||
|
||||
Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
|
||||
### Cloud client example
|
||||
|
||||
```typescript lineNumbers
|
||||
import type { S3Client as S3ClientType } from "@aws-sdk/client-s3";
|
||||
|
||||
// The arguments (region, bucket) are plain strings, which are serializable
|
||||
// The region is a plain string, which is serializable
|
||||
export function createS3Client(region: string) {
|
||||
return async (): Promise<S3ClientType> => {
|
||||
"use step";
|
||||
@@ -151,5 +122,5 @@ async function uploadFile(
|
||||
|
||||
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Marks a function for extraction and serialization.
|
||||
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent, resolves models through AI Gateway strings, and replaces `DurableAgent`.
|
||||
- [AI SDK providers](https://ai-sdk.dev/providers/ai-sdk-providers): Lists direct provider packages and configuration.
|
||||
- [Custom class serialization](/docs/foundations/serialization#custom-class-serialization): Provides the companion pattern for classes you own (`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`).
|
||||
|
||||
@@ -113,7 +113,7 @@ export async function stoppableAgent(messages: ModelMessage[]) {
|
||||
const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a research assistant. Search and analyze data as needed.",
|
||||
tools: {
|
||||
searchWeb: {
|
||||
|
||||
@@ -148,7 +148,7 @@ export async function bookingAgent(messages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You help book flights. Always request approval before booking.",
|
||||
tools: {
|
||||
searchFlights: {
|
||||
|
||||
@@ -91,7 +91,7 @@ async function runTurn(messages: ModelMessage[]) {
|
||||
"use step";
|
||||
|
||||
const result = streamText({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
system: "You are a customer support agent.",
|
||||
messages,
|
||||
tools: TOOLS,
|
||||
|
||||
@@ -35,14 +35,13 @@ Import the `fetch` step function from the `workflow` package and assign it to `g
|
||||
|
||||
```typescript lineNumbers title="workflows/ai.ts"
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function chatWorkflow(prompt: string) {
|
||||
"use workflow";
|
||||
|
||||
// Error - generateText() calls fetch() under the hood
|
||||
const result = await generateText({ // [!code highlight]
|
||||
model: openai("gpt-4"), // [!code highlight]
|
||||
model: "spacexai/grok-4.6", // [!code highlight]
|
||||
prompt, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
@@ -54,7 +53,6 @@ export async function chatWorkflow(prompt: string) {
|
||||
|
||||
```typescript lineNumbers title="workflows/ai.ts"
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { fetch } from "workflow"; // [!code highlight]
|
||||
|
||||
export async function chatWorkflow(prompt: string) {
|
||||
@@ -64,7 +62,7 @@ export async function chatWorkflow(prompt: string) {
|
||||
|
||||
// Now generateText() can make HTTP requests via the fetch step
|
||||
const result = await generateText({
|
||||
model: openai("gpt-4"),
|
||||
model: "spacexai/grok-4.6",
|
||||
prompt,
|
||||
});
|
||||
|
||||
@@ -80,7 +78,6 @@ This is the most common scenario - using AI SDK functions that make HTTP request
|
||||
|
||||
```typescript lineNumbers
|
||||
import { generateText, streamText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { fetch } from "workflow"; // [!code highlight]
|
||||
|
||||
export async function aiWorkflow(userMessage: string) {
|
||||
@@ -88,9 +85,9 @@ export async function aiWorkflow(userMessage: string) {
|
||||
|
||||
globalThis.fetch = fetch; // [!code highlight]
|
||||
|
||||
// generateText makes HTTP requests to OpenAI
|
||||
// Plain model strings route through Vercel AI Gateway
|
||||
const response = await generateText({
|
||||
model: openai("gpt-4"),
|
||||
model: "spacexai/grok-4.6",
|
||||
prompt: userMessage,
|
||||
});
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ export async function aiAssistantWorkflow(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new WorkflowAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful flight assistant.",
|
||||
tools: {
|
||||
searchFlights: tool({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Chat Session Modeling
|
||||
description: Model chat sessions at different architectural layers to control state ownership and handle interruptions.
|
||||
description: Model WorkflowAgent chat sessions at different architectural layers to control state ownership and handle interruptions.
|
||||
type: guide
|
||||
summary: Choose between single-turn and multi-turn workflow patterns for managing chat session state.
|
||||
summary: Choose between single-turn and multi-turn WorkflowAgent patterns for managing chat session state.
|
||||
prerequisites:
|
||||
- /docs/ai
|
||||
- /docs/foundations/workflows-and-steps
|
||||
@@ -10,528 +10,278 @@ related:
|
||||
- /docs/ai/message-queueing
|
||||
- /docs/ai/resumable-streams
|
||||
- /docs/foundations/hooks
|
||||
- /docs/api-reference/workflow-ai/durable-agent
|
||||
- /docs/api-reference/workflow/define-hook
|
||||
---
|
||||
|
||||
<Callout type="warn">
|
||||
The examples below use the deprecated `DurableAgent` API. For new agents, use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and follow the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The session-modeling patterns here (single- vs multi-turn, hooks, stream reconnection) apply to either API.
|
||||
</Callout>
|
||||
Chat sessions can be modeled at different layers of your architecture. The choice determines who owns message history, how long a workflow run stays active, and how clients reconnect after an interruption.
|
||||
|
||||
Chat sessions in AI agents can be modeled at different layers of your architecture. The choice affects state ownership and how you handle interruptions and reconnections.
|
||||
|
||||
While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.
|
||||
Workflow 5 applications should use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for both patterns.
|
||||
|
||||
## Single-turn workflows
|
||||
|
||||
Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.
|
||||
Each user turn starts a new workflow run. The client or API owns conversation history and sends the complete `UIMessage[]` array with every request.
|
||||
|
||||
<Tabs items={['Workflow', 'API Route', 'Client']}>
|
||||
### Workflow
|
||||
|
||||
<Tab value="Workflow">
|
||||
Convert UI messages to model messages inside the workflow. `WorkflowAgent` writes durable `ModelCallStreamPart` values to the run stream.
|
||||
|
||||
```typescript title="workflows/chat/index.ts" lineNumbers
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
```typescript title="workflows/chat.ts" lineNumbers
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import { convertToModelMessages, type UIMessage } from "ai";
|
||||
import { getWritable } from "workflow";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
|
||||
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
|
||||
|
||||
export async function chat(messages: UIMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
messages: await convertToModelMessages(messages), // [!code highlight] Full history from client
|
||||
writable,
|
||||
return agent.stream({
|
||||
messages: await convertToModelMessages(messages),
|
||||
writable: getWritable<ModelCallStreamPart>(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
### API route
|
||||
|
||||
<Tab value="API Route">
|
||||
Convert the durable model-call stream to AI SDK UI chunks at the response boundary:
|
||||
|
||||
```typescript title="app/api/chat/route.ts" lineNumbers
|
||||
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
||||
import { createUIMessageStreamResponse, type UIMessage } from "ai";
|
||||
import { start } from "workflow/api";
|
||||
import { chat } from "@/workflows/chat";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
const run = await start(chat, [messages]); // [!code highlight]
|
||||
export async function POST(request: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await request.json();
|
||||
const run = await start(chat, [messages]);
|
||||
|
||||
return createUIMessageStreamResponse({
|
||||
stream: run.readable,
|
||||
headers: {
|
||||
"x-workflow-run-id": run.runId, // [!code highlight] For stream reconnection
|
||||
},
|
||||
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
|
||||
headers: { "x-workflow-run-id": run.runId },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
### Client
|
||||
|
||||
<Tab value="Client">
|
||||
`WorkflowChatTransport` reconnects when the HTTP connection ends before the workflow stream finishes:
|
||||
|
||||
Chat messages need to be stored somewhere, typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
|
||||
|
||||
```typescript title="app/chats/[id]/page.tsx" lineNumbers
|
||||
```tsx title="app/chat.tsx" lineNumbers
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { WorkflowChatTransport } from "@ai-sdk/workflow"; // [!code highlight]
|
||||
import { useParams } from "next/navigation";
|
||||
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
||||
import { useMemo } from "react";
|
||||
|
||||
// Fetch existing messages from your backend
|
||||
async function getMessages(sessionId: string) { // [!code highlight]
|
||||
const res = await fetch(`/api/chats/${sessionId}/messages`); // [!code highlight]
|
||||
return res.json(); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
|
||||
export function Chat({ initialMessages }) {
|
||||
const { id: sessionId } = useParams<{ id: string }>();
|
||||
|
||||
const transport = useMemo( // [!code highlight]
|
||||
() => // [!code highlight]
|
||||
new WorkflowChatTransport({ // [!code highlight]
|
||||
api: "/api/chat", // [!code highlight]
|
||||
onChatEnd: async () => { // [!code highlight]
|
||||
// Persist the updated messages to the chat session // [!code highlight]
|
||||
await fetch(`/api/chats/${sessionId}/messages`, { // [!code highlight]
|
||||
method: "PUT", // [!code highlight]
|
||||
headers: { "Content-Type": "application/json" }, // [!code highlight]
|
||||
body: JSON.stringify({ messages }), // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
}), // [!code highlight]
|
||||
[sessionId] // [!code highlight]
|
||||
); // [!code highlight]
|
||||
|
||||
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
||||
initialMessages, // [!code highlight] Loaded via getMessages(sessionId)
|
||||
transport, // [!code highlight]
|
||||
const transport = useMemo(
|
||||
() => new WorkflowChatTransport({ api: "/api/chat" }),
|
||||
[]
|
||||
);
|
||||
const { messages, sendMessage } = useChat({
|
||||
messages: initialMessages,
|
||||
transport,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* ... render messages ... */}
|
||||
<input value={input} onChange={handleInputChange} />
|
||||
</form>
|
||||
);
|
||||
// Render messages and call sendMessage({ text }) from your form.
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
Persist `UIMessage[]` in your application database. `WorkflowAgent.stream()` returns `ModelMessage[]`, but there is no general conversion from model messages back to UI messages with all UI metadata intact.
|
||||
|
||||
</Tabs>
|
||||
Use the single-turn pattern when:
|
||||
|
||||
This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.
|
||||
|
||||
In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.
|
||||
|
||||
Persist the turn through one of these methods:
|
||||
|
||||
- Run a workflow step after `agent.stream()` that takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`).
|
||||
- Use a `useChat` client hook that calls an API to persist state, such as on every new message or in `onFinish`.
|
||||
- Use the resumable stream attached to the workflow (see [Resumable streams](/docs/ai/resumable-streams)). User messages are not persisted to the stream by default, so persist them separately.
|
||||
- Your application already owns chat history
|
||||
- Each turn should run on the latest deployment
|
||||
- You want a simple request-to-run mapping
|
||||
- Approval responses or client-side tool results arrive as another message turn
|
||||
|
||||
## Multi-turn workflows
|
||||
|
||||
A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.
|
||||
A single workflow run can own the model-message history for the whole session. It waits on a Hook between turns, and external callers resume the Hook with the next message. The workflow run ID becomes the session identifier.
|
||||
|
||||
For a full example of an agent using multi-turn workflows, check out the Flight Booking App example in the [Workflow Examples](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app) repository.
|
||||
```typescript title="workflows/chat-session.ts" lineNumbers
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import { type ModelMessage } from "ai";
|
||||
import { defineHook, getWorkflowMetadata, getWritable } from "workflow";
|
||||
import { z } from "zod";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
|
||||
|
||||
A key challenge in multi-turn workflows is ensuring user messages appear in the correct order when replaying the stream (e.g., after a page refresh). Since the stream primarily contains AI responses, user messages must be explicitly marked in the stream so the client can reconstruct the full conversation.
|
||||
export const chatMessageHook = defineHook({
|
||||
schema: z.object({ message: z.string() }),
|
||||
});
|
||||
|
||||
<Tabs items={['Workflow', 'API Routes', 'Hook Definition', 'Client Hook']}>
|
||||
|
||||
<Tab value="Workflow">
|
||||
|
||||
```typescript title="workflows/chat/index.ts" lineNumbers
|
||||
import {
|
||||
convertToModelMessages,
|
||||
type UIMessageChunk,
|
||||
type UIMessage,
|
||||
type ModelMessage,
|
||||
} from "ai";
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { getWritable, getWorkflowMetadata } from "workflow";
|
||||
import { chatMessageHook } from "./hooks/chat-message";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
|
||||
import { writeUserMessageMarker, writeStreamClose } from "./steps/writer"; // [!code highlight]
|
||||
|
||||
export async function chat(initialMessages: UIMessage[]) {
|
||||
export async function chatSession(initialMessages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const { workflowRunId: runId } = getWorkflowMetadata();
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
const messages: ModelMessage[] = await convertToModelMessages(initialMessages);
|
||||
const { workflowRunId } = getWorkflowMetadata();
|
||||
const hook = chatMessageHook.create({ token: workflowRunId });
|
||||
const writable = getWritable<ModelCallStreamPart>();
|
||||
let messages = [...initialMessages];
|
||||
|
||||
// Write markers for initial user messages (for replay) // [!code highlight]
|
||||
for (const msg of initialMessages) { // [!code highlight]
|
||||
if (msg.role === "user") { // [!code highlight]
|
||||
const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
|
||||
if (text) await writeUserMessageMarker(writable, text, msg.id); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
} // [!code highlight]
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
// Use run ID as the hook token for resumption
|
||||
const hook = chatMessageHook.create({ token: runId });
|
||||
let turnNumber = 0;
|
||||
|
||||
while (true) {
|
||||
turnNumber++;
|
||||
const maxTurns = 100;
|
||||
for (let turn = 0; turn < maxTurns; turn++) {
|
||||
const result = await agent.stream({
|
||||
messages,
|
||||
writable,
|
||||
preventClose: true, // [!code highlight] Keep stream open for follow-ups
|
||||
sendStart: turnNumber === 1,
|
||||
preventClose: true,
|
||||
sendFinish: false,
|
||||
});
|
||||
messages.push(...result.messages.slice(messages.length));
|
||||
messages = result.messages;
|
||||
|
||||
// Wait for next user message via hook
|
||||
const { message: followUp } = await hook;
|
||||
if (followUp === "/done") break;
|
||||
// Do not accept a follow-up that this run has no remaining turn to process.
|
||||
if (turn === maxTurns - 1) break;
|
||||
|
||||
// Write marker and add to messages // [!code highlight]
|
||||
const followUpId = `user-${runId}-${turnNumber}`; // [!code highlight]
|
||||
await writeUserMessageMarker(writable, followUp, followUpId); // [!code highlight]
|
||||
messages.push({ role: "user", content: followUp });
|
||||
const { message } = await hook;
|
||||
if (message === "/done") break;
|
||||
messages = [...messages, { role: "user", content: message }];
|
||||
}
|
||||
|
||||
await writeStreamClose(writable); // [!code highlight]
|
||||
return { messages };
|
||||
}
|
||||
```
|
||||
|
||||
The `writeUserMessageMarker` helper writes a `data-workflow` chunk to mark user turns:
|
||||
Create the Hook once, outside the loop. Recreating the same token on every turn causes a Hook conflict. Intermediate turns use `preventClose: true` with `sendFinish: false`; the workflow closes the durable stream and emits the final UI `finish` when the run returns.
|
||||
|
||||
```typescript title="workflows/chat/steps/writer.ts" lineNumbers
|
||||
import type { UIMessageChunk } from "ai";
|
||||
### Start and resume the session
|
||||
|
||||
export async function writeUserMessageMarker( // [!code highlight]
|
||||
writable: WritableStream<UIMessageChunk>,
|
||||
content: string,
|
||||
messageId: string
|
||||
) {
|
||||
"use step"; // [!code highlight]
|
||||
const writer = writable.getWriter();
|
||||
try {
|
||||
await writer.write({
|
||||
type: "data-workflow", // [!code highlight]
|
||||
data: { type: "user-message", id: messageId, content, timestamp: Date.now() }, // [!code highlight]
|
||||
} as UIMessageChunk);
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeStreamClose(writable: WritableStream<UIMessageChunk>) {
|
||||
const writer = writable.getWriter();
|
||||
await writer.write({ type: "finish" });
|
||||
await writer.close();
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="API Routes">
|
||||
|
||||
Use three endpoints to start a session, send follow-up messages, and reconnect to the stream.
|
||||
Use one route to create the run and a second route to deliver follow-up messages:
|
||||
|
||||
```typescript title="app/api/chat/route.ts" lineNumbers
|
||||
import { createUIMessageStreamResponse, type UIMessage } from "ai";
|
||||
import { convertToModelMessages, type UIMessage } from "ai";
|
||||
import { start } from "workflow/api";
|
||||
import { chat } from "@/workflows/chat";
|
||||
import { chatSession } from "@/workflows/chat-session";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { initialMessage }: { initialMessage: UIMessage } = await req.json();
|
||||
|
||||
const run = await start(chat, [[initialMessage]]); // [!code highlight]
|
||||
|
||||
return createUIMessageStreamResponse({
|
||||
stream: run.readable,
|
||||
headers: {
|
||||
"x-workflow-run-id": run.runId, // [!code highlight] For follow-ups and reconnection
|
||||
},
|
||||
});
|
||||
export async function POST(request: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await request.json();
|
||||
const run = await start(chatSession, [await convertToModelMessages(messages)]);
|
||||
return Response.json({ runId: run.runId });
|
||||
}
|
||||
```
|
||||
|
||||
```typescript title="app/api/chat/[id]/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
|
||||
```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat-session";
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ runId: string }> }
|
||||
) {
|
||||
const { id: runId } = await params;
|
||||
const { message } = await req.json();
|
||||
|
||||
// Resume the hook using the workflow run ID // [!code highlight]
|
||||
await chatMessageHook.resume(runId, { message }); // [!code highlight]
|
||||
|
||||
const { runId } = await params;
|
||||
const { message }: { message: string } = await request.json();
|
||||
await chatMessageHook.resume(runId, { message });
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
```typescript title="app/api/chat/[id]/stream/route.ts" lineNumbers
|
||||
### Reconnect to the stream
|
||||
|
||||
`WorkflowChatTransport` counts transformed UI chunks, while the durable stream stores raw `ModelCallStreamPart` values. Always replay the raw stream from index `0` and apply the UI cursor in `createModelCallToUIChunkTransform()`:
|
||||
|
||||
{/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
|
||||
```typescript title="app/api/chat/[runId]/stream/route.ts" lineNumbers
|
||||
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
||||
import { createUIMessageStreamResponse } from "ai";
|
||||
import { getRun } from "workflow/api";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
{ params }: { params: Promise<{ runId: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const startIndex = searchParams.get("startIndex");
|
||||
|
||||
const run = getRun(id); // [!code highlight]
|
||||
const stream = run.getReadable({ // [!code highlight]
|
||||
startIndex: startIndex ? parseInt(startIndex, 10) : undefined, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
return createUIMessageStreamResponse({ stream });
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Hook Definition">
|
||||
|
||||
```typescript title="workflows/chat/hooks/chat-message.ts" lineNumbers
|
||||
import { defineHook } from "workflow";
|
||||
import { z } from "zod";
|
||||
|
||||
export const chatMessageHook = defineHook({
|
||||
schema: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Client Hook">
|
||||
|
||||
A custom hook wraps `useChat` to manage the multi-turn session. It handles:
|
||||
|
||||
- Routing between the initial message endpoint and follow-up endpoint
|
||||
- Reconstructing user messages from stream markers for correct ordering on replay
|
||||
|
||||
```typescript title="hooks/use-multi-turn-chat.ts" lineNumbers
|
||||
"use client";
|
||||
|
||||
import type { UIMessage, UIDataTypes, ChatStatus } from "ai";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
||||
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
||||
|
||||
const STORAGE_KEY = "workflow-run-id";
|
||||
|
||||
interface UserMessageData {
|
||||
type: "user-message";
|
||||
id: string;
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function useMultiTurnChat() {
|
||||
const [runId, setRunId] = useState<string | null>(null);
|
||||
const [shouldResume, setShouldResume] = useState(false);
|
||||
const userMessagesRef = useRef<Map<string, UIMessage>>(new Map());
|
||||
|
||||
// Check for existing session on mount // [!code highlight]
|
||||
useEffect(() => {
|
||||
const storedRunId = localStorage.getItem(STORAGE_KEY);
|
||||
if (storedRunId) {
|
||||
setRunId(storedRunId);
|
||||
setShouldResume(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const transport = useMemo(
|
||||
() =>
|
||||
new WorkflowChatTransport({
|
||||
api: "/api/chat",
|
||||
onChatSendMessage: (response) => {
|
||||
const workflowRunId = response.headers.get("x-workflow-run-id");
|
||||
if (workflowRunId) {
|
||||
setRunId(workflowRunId);
|
||||
localStorage.setItem(STORAGE_KEY, workflowRunId);
|
||||
}
|
||||
},
|
||||
onChatEnd: () => {
|
||||
setRunId(null);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
userMessagesRef.current.clear();
|
||||
},
|
||||
prepareReconnectToStreamRequest: ({ api, ...rest }) => {
|
||||
const storedRunId = localStorage.getItem(STORAGE_KEY);
|
||||
if (!storedRunId) throw new Error("No active session");
|
||||
return { ...rest, api: `/api/chat/${storedRunId}/stream` };
|
||||
},
|
||||
}),
|
||||
[]
|
||||
const { runId } = await params;
|
||||
const startIndex = Number(
|
||||
new URL(request.url).searchParams.get("startIndex") ?? "0"
|
||||
);
|
||||
|
||||
const { messages: rawMessages, sendMessage: baseSendMessage, status, stop, setMessages } =
|
||||
useChat({ resume: shouldResume, transport });
|
||||
|
||||
// Reconstruct conversation order from stream markers // [!code highlight]
|
||||
const messages = useMemo(() => { // [!code highlight]
|
||||
const result: UIMessage[] = []; // [!code highlight]
|
||||
const seenContent = new Set<string>(); // [!code highlight]
|
||||
// [!code highlight]
|
||||
// Collect content from optimistic user messages // [!code highlight]
|
||||
for (const msg of rawMessages) { // [!code highlight]
|
||||
if (msg.role === "user") { // [!code highlight]
|
||||
const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
|
||||
if (text) seenContent.add(text); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
} // [!code highlight]
|
||||
// [!code highlight]
|
||||
for (const msg of rawMessages) { // [!code highlight]
|
||||
if (msg.role === "user") { // [!code highlight]
|
||||
result.push(msg); // [!code highlight]
|
||||
continue; // [!code highlight]
|
||||
} // [!code highlight]
|
||||
// [!code highlight]
|
||||
if (msg.role === "assistant") { // [!code highlight]
|
||||
// Process parts in order, splitting on user-message markers // [!code highlight]
|
||||
let currentParts: typeof msg.parts = []; // [!code highlight]
|
||||
let partIndex = 0; // [!code highlight]
|
||||
// [!code highlight]
|
||||
for (const part of msg.parts) { // [!code highlight]
|
||||
if (part.type === "data-workflow" && "data" in part) { // [!code highlight]
|
||||
const data = part.data as UserMessageData; // [!code highlight]
|
||||
if (data?.type === "user-message") { // [!code highlight]
|
||||
// Flush accumulated assistant parts // [!code highlight]
|
||||
if (currentParts.length > 0) { // [!code highlight]
|
||||
result.push({ ...msg, id: `${msg.id}-${partIndex++}`, parts: currentParts }); // [!code highlight]
|
||||
currentParts = []; // [!code highlight]
|
||||
} // [!code highlight]
|
||||
// Add user message if not duplicate // [!code highlight]
|
||||
if (!seenContent.has(data.content)) { // [!code highlight]
|
||||
seenContent.add(data.content); // [!code highlight]
|
||||
result.push({ id: data.id, role: "user", parts: [{ type: "text", text: data.content }] }); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
continue; // [!code highlight]
|
||||
} // [!code highlight]
|
||||
} // [!code highlight]
|
||||
currentParts.push(part); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
// [!code highlight]
|
||||
if (currentParts.length > 0) { // [!code highlight]
|
||||
result.push({ ...msg, id: partIndex > 0 ? `${msg.id}-${partIndex}` : msg.id, parts: currentParts }); // [!code highlight]
|
||||
} // [!code highlight]
|
||||
} // [!code highlight]
|
||||
} // [!code highlight]
|
||||
return result; // [!code highlight]
|
||||
}, [rawMessages]); // [!code highlight]
|
||||
|
||||
// Route messages to appropriate endpoint
|
||||
const sendMessage = useCallback(
|
||||
async (text: string) => {
|
||||
if (runId) {
|
||||
// Follow-up: send via hook resumption // [!code highlight]
|
||||
await fetch(`/api/chat/${runId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
} else {
|
||||
// First message: start new workflow
|
||||
await baseSendMessage({ text, metadata: { createdAt: Date.now() } });
|
||||
}
|
||||
},
|
||||
[runId, baseSendMessage]
|
||||
if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
|
||||
return Response.json(
|
||||
{ error: "startIndex must be a non-negative safe integer" },
|
||||
{ status: 400 }
|
||||
);
|
||||
|
||||
const endSession = useCallback(async () => {
|
||||
if (runId) {
|
||||
await fetch(`/api/chat/${runId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: "/done" }),
|
||||
});
|
||||
}
|
||||
setRunId(null);
|
||||
setShouldResume(false);
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
userMessagesRef.current.clear();
|
||||
setMessages([]);
|
||||
}, [runId, setMessages]);
|
||||
|
||||
return { messages, status, runId, sendMessage, endSession, stop };
|
||||
const run = getRun(runId);
|
||||
const stream = run
|
||||
.getReadable({ startIndex: 0 })
|
||||
.pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex: startIndex }));
|
||||
|
||||
return createUIMessageStreamResponse({
|
||||
stream,
|
||||
headers: { "x-workflow-run-id": runId },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
Persist `UIMessage[]` separately if the client must reconstruct user messages and UI metadata after a refresh. The workflow-owned `ModelMessage[]` history is the model's durable context, not a replacement for an application chat table.
|
||||
|
||||
</Tabs>
|
||||
### Persist and display the full session
|
||||
|
||||
In this pattern, the workflow owns the entire conversation session. All messages are persisted in the workflow, and follow-up messages are injected via hooks. The workflow writes **user message markers** to the stream using `data-workflow` chunks, which allows the client to reconstruct the full conversation in the correct order when replaying the stream (e.g., after a page refresh).
|
||||
A multi-turn chat has three related records with different responsibilities:
|
||||
|
||||
The client hook processes these markers by:
|
||||
1. **Workflow model history**: `ModelMessage[]` is the durable context sent back to the model on each turn.
|
||||
2. **Workflow run stream**: `ModelCallStreamPart` values contain durable model and tool output for live delivery and reconnection.
|
||||
3. **Application chat history**: `UIMessage[]` preserves user messages, display metadata, attachments, and application-specific parts.
|
||||
|
||||
1. Iterate through message parts in order.
|
||||
2. When a `user-message` marker is found, flush any accumulated assistant content and insert the user message.
|
||||
3. Deduplicate against optimistic sends from the initial message.
|
||||
When the user sends the first message, start the run, store its run ID with the application chat record, and connect to the run stream. For each follow-up, optimistically add and persist the user `UIMessage`, then resume `chatMessageHook` with the corresponding text. After a refresh, load the persisted UI messages and reconnect through the stream route above using the last persisted UI chunk cursor. If you persist only user messages, replay model output from the beginning and merge by stable message IDs.
|
||||
|
||||
This ensures the conversation displays as User → AI → User → AI regardless of whether viewing live or replaying from the stream.
|
||||
Do not treat the run stream as the only chat database. `WorkflowAgent` deliberately stores raw model-call parts, and user messages resumed through a Hook are not automatically written to that output stream. Keeping the application history separate avoids synthetic stream markers and preserves UI information that cannot be reconstructed from `ModelMessage[]`.
|
||||
|
||||
<Callout type="info">
|
||||
The reconnect route replays raw `ModelCallStreamPart` values from index `0` because raw parts and transformed UI chunks do not have matching indexes. `uiStartIndex` prevents already-delivered UI chunks from being sent to the client again, but the server still transforms the earlier raw history. For very long streams, split conversations into bounded runs until WorkflowAgent exposes a persisted raw-to-UI cursor mapping.
|
||||
</Callout>
|
||||
|
||||
Use the multi-turn pattern when:
|
||||
|
||||
- One workflow should own the session's model context
|
||||
- Backend events or other users need to inject messages through Hooks
|
||||
- Full-session tracing is more important than running every turn on the newest deployment
|
||||
- The application is prepared to manage a long-lived run and stream cursor
|
||||
|
||||
## Choosing a pattern
|
||||
|
||||
| Consideration | Single-Turn | Multi-Turn |
|
||||
| Consideration | Single-turn | Multi-turn |
|
||||
|--------------|-------------|------------|
|
||||
| State ownership | Client or API route | Workflow |
|
||||
| Message injection from backend | Requires stitching together runs | Native via hooks |
|
||||
| State ownership | Client or application database | Workflow for model context; application database for UI history |
|
||||
| Deployment version | Latest deployment per turn | Deployment that started the run |
|
||||
| Message injection | Start another run | Resume a Hook |
|
||||
| Workflow complexity | Lower | Higher |
|
||||
| Workflow time horizon | Minutes | Hours to indefinitely |
|
||||
| Observability scope | Per-turn traces | Full session traces |
|
||||
| Workflow time horizon | One model turn | Hours or longer |
|
||||
| Observability scope | Per turn | Full session |
|
||||
|
||||
**Multi-turn is recommended for most production use cases.** For new applications, use multi-turn workflows. The workflow's built-in persistence maintains the chat history and supports native message injection and full-session observability.
|
||||
**Multi-turn works well for new durable sessions.** The workflow owns model context, accepts messages from users and backend systems through the same Hook, and provides one full-session trace.
|
||||
|
||||
**Single-turn works well when adapting existing architectures.** If you already have a system for managing message state and want to adopt durable agents incrementally, single-turn workflows require fewer changes. Each turn maps to an independent workflow run.
|
||||
**Single-turn works well when adapting an existing architecture.** If the application already manages message state and you want to adopt durable agents incrementally, one workflow run per turn requires fewer lifecycle changes and always uses the latest deployment.
|
||||
|
||||
## Multiplayer chat sessions
|
||||
|
||||
The multi-turn pattern also enables multiplayer chat sessions. Messages can come from system events, external services, and other users. A `hook` can inject messages into a workflow at any point, while clients reconnect to one stream containing the entire history.
|
||||
The multi-turn pattern also supports messages from system events, external services, and multiple users. Every source resumes the same Hook; the workflow queues those messages and processes them between model turns.
|
||||
|
||||
<Tabs items={['System Event', 'External Service', 'Multiple Users']}>
|
||||
<Tabs items={['System event', 'External service', 'Multiple users']}>
|
||||
|
||||
<Tab value="System Event">
|
||||
<Tab value="System event">
|
||||
|
||||
Internal system events like scheduled tasks, background jobs, or database triggers can inject updates into an active conversation.
|
||||
Scheduled tasks, background jobs, or database triggers can inject updates into an active conversation:
|
||||
|
||||
```typescript title="app/api/internal/flight-update/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
|
||||
import { chatMessageHook } from "@/workflows/chat-session";
|
||||
|
||||
// Called by your flight status monitoring system
|
||||
export async function POST(req: Request) {
|
||||
const { runId, flightNumber, newStatus } = await req.json();
|
||||
export async function POST(request: Request) {
|
||||
const { runId, flightNumber, newStatus } = await request.json();
|
||||
|
||||
await chatMessageHook.resume(runId, { // [!code highlight]
|
||||
message: `[System] Flight ${flightNumber} status updated: ${newStatus}`, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
await chatMessageHook.resume(runId, {
|
||||
message: `[System] Flight ${flightNumber} status updated: ${newStatus}`,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
@@ -539,20 +289,20 @@ export async function POST(req: Request) {
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="External Service">
|
||||
<Tab value="External service">
|
||||
|
||||
External webhooks from third-party services, such as Stripe and Twilio, can notify the conversation of events.
|
||||
A third-party webhook can notify the conversation about an external event:
|
||||
|
||||
```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
|
||||
import { chatMessageHook } from "@/workflows/chat-session";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { runId, paymentStatus, amount } = await req.json();
|
||||
export async function POST(request: Request) {
|
||||
const { runId, paymentStatus, amount } = await request.json();
|
||||
|
||||
if (paymentStatus === "succeeded") {
|
||||
await chatMessageHook.resume(runId, { // [!code highlight]
|
||||
message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed!`, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
await chatMessageHook.resume(runId, {
|
||||
message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed.`,
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({ received: true });
|
||||
@@ -561,38 +311,39 @@ export async function POST(req: Request) {
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Multiple Users">
|
||||
<Tab value="Multiple users">
|
||||
|
||||
Multiple human users can participate in the same conversation. Each user's client connects to the same workflow stream.
|
||||
Multiple authenticated users can participate in the same workflow-owned session. Include attribution when resuming the Hook:
|
||||
|
||||
```typescript title="app/api/chat/[id]/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
|
||||
```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
|
||||
import { chatMessageHook } from "@/workflows/chat-session";
|
||||
import { getUser } from "@/lib/auth";
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ runId: string }> }
|
||||
) {
|
||||
const { id: runId } = await params;
|
||||
const { message } = await req.json();
|
||||
const user = await getUser(req); // [!code highlight]
|
||||
const { runId } = await params;
|
||||
const { message } = await request.json();
|
||||
const user = await getUser(request);
|
||||
|
||||
// Inject message with user attribution // [!code highlight]
|
||||
await chatMessageHook.resume(runId, { // [!code highlight]
|
||||
message: `[${user.name}] ${message}`, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
await chatMessageHook.resume(runId, {
|
||||
message: `[${user.name}] ${message}`,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
To preserve structured attribution across refreshes, persist the corresponding `UIMessage` using the application-history approach described above.
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Building Durable AI Agents](/docs/ai): Foundation guide for durable agents
|
||||
- [Message Queueing](/docs/ai/message-queueing): Queueing messages during tool execution
|
||||
- [`defineHook()` API reference](/docs/api-reference/workflow/define-hook): Hook configuration options
|
||||
- [Building Durable AI Agents](/docs/ai): Foundation guide for WorkflowAgent
|
||||
- [Message Queueing](/docs/ai/message-queueing): Inject messages between model-call steps
|
||||
- [Resumable Streams](/docs/ai/resumable-streams): Reconnect to durable output
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
|
||||
|
||||
@@ -57,55 +57,49 @@ cd workflow-examples/flight-booking-app
|
||||
|
||||
<Step>
|
||||
|
||||
### Set up API keys
|
||||
### Configure model access
|
||||
|
||||
To connect to an LLM, set up an API key. You can use Vercel Gateway, which works with all providers at zero markup, or configure a custom provider.
|
||||
<Tabs items={['Gateway', 'Custom Provider']}>
|
||||
<Tabs items={['AI Gateway', 'Provider package']}>
|
||||
|
||||
<Tab value="Gateway">
|
||||
<Tab value="AI Gateway">
|
||||
|
||||
Get a Gateway API key from the [Vercel Gateway](https://vercel.com/docs/ai-gateway/authentication) page.
|
||||
AI SDK uses [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) as its default global provider, so plain `"provider/model"` strings need no provider-specific package. Vercel deployments authenticate with OIDC automatically. For local development, link the project and pull a short-lived OIDC token:
|
||||
|
||||
Then add it to your `.env.local` file:
|
||||
|
||||
```bash title=".env.local" lineNumbers
|
||||
GATEWAY_API_KEY=...
|
||||
```bash
|
||||
vercel link
|
||||
vercel env pull .env.local
|
||||
```
|
||||
|
||||
You can alternatively set `AI_GATEWAY_API_KEY` from the [AI Gateway authentication](https://vercel.com/docs/ai-gateway/authentication) page.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Custom Provider">
|
||||
<Tab value="Provider package">
|
||||
|
||||
This is an example of how to use the OpenAI provider for AI SDK. For details on other providers and more details, see the [AI SDK provider guide](https://ai-sdk.dev/providers/ai-sdk-providers).
|
||||
`WorkflowAgent` accepts any AI SDK provider. To use OpenAI, install its provider package:
|
||||
|
||||
```package-install
|
||||
npm i @ai-sdk/openai
|
||||
```
|
||||
|
||||
Set your OpenAI API key in your environment variables:
|
||||
Set the provider's API key:
|
||||
|
||||
```bash title=".env.local" lineNumbers
|
||||
OPENAI_API_KEY=...
|
||||
```
|
||||
|
||||
Then modify your API endpoint to use the OpenAI provider:
|
||||
Then construct the model with the provider package:
|
||||
|
||||
{/* @skip-typecheck: incomplete code sample */}
|
||||
```typescript title="app/api/chat/route.ts" lineNumbers
|
||||
// ...
|
||||
import { openai } from "@ai-sdk/openai"; // [!code highlight]
|
||||
```typescript
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// ...
|
||||
const agent = new Agent({
|
||||
// This uses the OPENAI_API_KEY environment variable by default, but you
|
||||
// can also pass { apiKey: string } as an option.
|
||||
model: openai("gpt-5.1"), // [!code highlight]
|
||||
// ...
|
||||
});
|
||||
const model = openai("gpt-5.6-sol");
|
||||
```
|
||||
|
||||
See the [AI SDK provider guide](https://ai-sdk.dev/providers/ai-sdk-providers) for Anthropic, Google, Amazon Bedrock, and other providers.
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
</Step>
|
||||
|
||||
@@ -131,7 +125,7 @@ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
const agent = new ToolLoopAgent({ // [!code highlight]
|
||||
model: "bedrock/claude-4-5-haiku-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
@@ -257,12 +251,10 @@ export default withWorkflow(nextConfig);
|
||||
|
||||
Move the agent logic into a separate function, which will serve as our workflow definition.
|
||||
|
||||
{/* @skip-typecheck: Shows two mutually exclusive model options */}
|
||||
```typescript title="workflows/chat/workflow.ts" lineNumbers
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow"; // [!code highlight]
|
||||
import { getWritable } from "workflow"; // [!code highlight]
|
||||
import { tools } from "@/ai/tools";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "@/ai/tools";
|
||||
import { convertToModelMessages, type UIMessage } from "ai";
|
||||
|
||||
export async function chatWorkflow(messages: UIMessage[]) {
|
||||
@@ -271,13 +263,8 @@ export async function chatWorkflow(messages: UIMessage[]) {
|
||||
const writable = getWritable<ModelCallStreamPart>(); // [!code highlight]
|
||||
|
||||
const agent = new WorkflowAgent({ // [!code highlight]
|
||||
|
||||
// If using AI Gateway, specify the model name as a string:
|
||||
model: "bedrock/claude-4-5-haiku-20251001-v1", // [!code highlight]
|
||||
|
||||
// ELSE if using a custom provider, pass the provider call as an argument:
|
||||
model: openai("gpt-5.1"), // [!code highlight]
|
||||
|
||||
// Plain model strings use Vercel AI Gateway.
|
||||
model: "spacexai/grok-4.6", // [!code highlight]
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
@@ -291,6 +278,10 @@ export async function chatWorkflow(messages: UIMessage[]) {
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
`WorkflowAgent` accepts any AI SDK provider. Import the provider and pass its model instance, for example `model: openai("gpt-5.6-sol")` from `@ai-sdk/openai`. The rest of the integration is unchanged.
|
||||
</Callout>
|
||||
|
||||
Key changes:
|
||||
|
||||
- Add the `"use workflow"` directive to mark our Agent as a workflow function
|
||||
|
||||
@@ -7,7 +7,6 @@ prerequisites:
|
||||
- /docs/ai
|
||||
related:
|
||||
- /docs/ai/chat-session-modeling
|
||||
- /docs/api-reference/workflow-ai/durable-agent
|
||||
- /docs/api-reference/workflow/define-hook
|
||||
---
|
||||
|
||||
@@ -29,27 +28,24 @@ If you need basic multi-turn conversations where messages arrive between turns,
|
||||
|
||||
## The `prepareStep` callback
|
||||
|
||||
The `prepareStep` callback runs before each step in the agent loop. It receives the current state and can modify the messages sent to the model:
|
||||
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 { ModelMessage, LanguageModel } from "ai";
|
||||
import type {
|
||||
PrepareStepInfo,
|
||||
PrepareStepResult,
|
||||
} from "@ai-sdk/workflow";
|
||||
|
||||
interface PrepareStepInfo {
|
||||
model: string | (() => Promise<LanguageModel>); // Current model
|
||||
stepNumber: number; // 0-indexed step count
|
||||
steps: StepResult[]; // Previous step results
|
||||
messages: ModelMessage[]; // Messages to be sent
|
||||
}
|
||||
|
||||
interface PrepareStepResult {
|
||||
model?: string | (() => Promise<LanguageModel>); // Override model
|
||||
messages?: ModelMessage[]; // Override messages
|
||||
}
|
||||
const prepareStep = (
|
||||
{ messages }: PrepareStepInfo
|
||||
): PrepareStepResult => ({ messages });
|
||||
```
|
||||
|
||||
## Injecting queued messages
|
||||
`PrepareStepInfo.messages` is a normalized `LanguageModelV4Prompt`, not the application-level `ModelMessage[]` accepted by `WorkflowAgent.stream()`.
|
||||
|
||||
Once you have a [multi-turn workflow](/docs/ai/chat-session-modeling#multi-turn-workflows), you can combine a message queue with `prepareStep` to inject messages that arrive during processing:
|
||||
## 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";
|
||||
@@ -63,112 +59,77 @@ export async function chat(initialMessages: ModelMessage[]) {
|
||||
|
||||
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: "bedrock/claude-haiku-4-5-20251001-v1",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: FLIGHT_ASSISTANT_PROMPT,
|
||||
tools: flightBookingTools,
|
||||
});
|
||||
|
||||
// Listen for messages in background (non-blocking) // [!code highlight]
|
||||
const hook = chatMessageHook.create({ token: runId }); // [!code highlight]
|
||||
hook.then(({ message }) => { // [!code highlight]
|
||||
messageQueue.push({ role: "user", content: message }); // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
await agent.stream({
|
||||
messages: initialMessages,
|
||||
writable,
|
||||
prepareStep: ({ messages: currentMessages }) => { // [!code highlight]
|
||||
// Inject any queued messages before the next LLM call // [!code highlight]
|
||||
if (messageQueue.length > 0) { // [!code highlight]
|
||||
const newMessages = messageQueue.splice(0); // Drain queue // [!code highlight]
|
||||
return { // [!code highlight]
|
||||
messages: [ // [!code highlight]
|
||||
...currentMessages, // [!code highlight]
|
||||
...newMessages.map((m) => ({ // [!code highlight]
|
||||
role: m.role, // [!code highlight]
|
||||
content: [{ type: "text" as const, text: m.content }], // [!code highlight]
|
||||
})), // [!code highlight]
|
||||
], // [!code highlight]
|
||||
}; // [!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]
|
||||
return {}; // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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 `ModelMessage[]` format (with content arrays), which is the internal format used by the AI SDK.
|
||||
</Callout>
|
||||
|
||||
## Combining with multi-turn sessions
|
||||
|
||||
You can also combine message queueing with the standard multi-turn pattern:
|
||||
|
||||
```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 type { ModelMessage } from "ai";
|
||||
|
||||
export async function chat(initialMessages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const { workflowRunId: runId } = getWorkflowMetadata();
|
||||
const writable = getWritable<ModelCallStreamPart>();
|
||||
const messages: ModelMessage[] = [...initialMessages];
|
||||
const messageQueue: Array<{ role: "user"; content: string }> = [];
|
||||
|
||||
const agent = new WorkflowAgent({ /* ... */ });
|
||||
const hook = chatMessageHook.create({ token: runId });
|
||||
|
||||
while (true) {
|
||||
// Set up non-blocking listener for mid-turn messages // [!code highlight]
|
||||
let pendingMessage: string | null = null; // [!code highlight]
|
||||
hook.then(({ message }) => { // [!code highlight]
|
||||
if (message === "/done") return; // [!code highlight]
|
||||
messageQueue.push({ role: "user", content: message }); // [!code highlight]
|
||||
pendingMessage = message; // [!code highlight]
|
||||
}); // [!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 }) => {
|
||||
// Inject queued messages during turn // [!code highlight]
|
||||
if (messageQueue.length > 0) {
|
||||
const newMessages = messageQueue.splice(0);
|
||||
const queued = messageQueue.splice(0); // Atomic drain // [!code highlight]
|
||||
if (queued.length === 0) return {};
|
||||
return {
|
||||
messages: [
|
||||
...currentMessages,
|
||||
...newMessages.map((m) => ({
|
||||
role: m.role,
|
||||
content: [{ type: "text" as const, text: m.content }],
|
||||
...queued.map(({ role, content }) => ({
|
||||
role,
|
||||
content: [{ type: "text" as const, text: content }],
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
});
|
||||
messages = result.messages;
|
||||
|
||||
messages.push(...result.messages.slice(messages.length));
|
||||
if (stopped) break;
|
||||
await waitForMessage(); // [!code highlight]
|
||||
if (stopped) break;
|
||||
|
||||
// Wait for next message (either queued during turn or new) // [!code highlight]
|
||||
const { message: followUp } = pendingMessage ? { message: pendingMessage } : await hook; // [!code highlight]
|
||||
if (followUp === "/done") break;
|
||||
|
||||
messages.push({ role: "user", content: followUp });
|
||||
// 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
|
||||
|
||||
@@ -51,7 +51,7 @@ All the functions and primitives that come with Workflow SDK by package.
|
||||
Serialization symbols for custom class serialization in workflows.
|
||||
</Card>
|
||||
<Card title="@workflow/ai" href="/docs/api-reference/workflow-ai">
|
||||
Helpers for integrating AI SDK for building AI-powered workflows.
|
||||
Deprecated AI integration APIs kept for existing applications. Use `@ai-sdk/workflow` for new agents.
|
||||
</Card>
|
||||
<Card title="@workflow/vitest" href="/docs/api-reference/vitest">
|
||||
Vitest plugin and test helpers for integration testing workflows in-process.
|
||||
|
||||
@@ -199,7 +199,7 @@ async function weatherAgentWorkflow(userQuery: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get current weather for a location",
|
||||
@@ -244,7 +244,7 @@ async function multiToolAgentWorkflow(userQuery: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getWeather: {
|
||||
description: "Get weather for a location",
|
||||
@@ -289,7 +289,7 @@ async function multiTurnAgentWorkflow() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
searchProducts: {
|
||||
description: "Search for products",
|
||||
@@ -368,7 +368,7 @@ async function agentWithLibraryFeaturesWorkflow(userRequest: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
scheduleTask: {
|
||||
description: "Pause the workflow for the specified number of seconds",
|
||||
@@ -405,7 +405,7 @@ async function agentWithPrepareStep(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "openai/gpt-4.1-mini", // Default model
|
||||
model: "spacexai/grok-4.6", // Default model
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -459,7 +459,7 @@ async function agentWithMessageQueue(initialMessage: string) {
|
||||
});
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -500,7 +500,7 @@ async function agentWithGenerationSettings() {
|
||||
|
||||
// Set default generation settings in constructor
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 2000,
|
||||
topP: 0.9,
|
||||
@@ -548,7 +548,7 @@ async function multiStepAgent() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
searchWeb: {
|
||||
description: "Search the web for information",
|
||||
@@ -588,7 +588,7 @@ async function agentWithCallbacks() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
@@ -630,7 +630,7 @@ async function agentWithStructuredOutput() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
const result = await agent.stream({
|
||||
@@ -665,7 +665,7 @@ async function agentWithToolChoice() {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
calculator: {
|
||||
description: "Perform calculations",
|
||||
@@ -733,7 +733,7 @@ async function agentWithContext(userId: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
getUserData: {
|
||||
description: "Get user data",
|
||||
@@ -771,7 +771,7 @@ async function agentWithUIMessages(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
@@ -819,7 +819,7 @@ async function agentWithToolInspection(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
tools: {
|
||||
checkOrderStatus: {
|
||||
description: "Check order status",
|
||||
@@ -874,7 +874,7 @@ async function agentWithTimeout(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "@workflow/ai"
|
||||
description: Helpers for building AI-powered workflows with the AI SDK.
|
||||
description: Deprecated AI integration APIs kept for existing Workflow applications.
|
||||
type: overview
|
||||
summary: Explore helpers for integrating AI SDK to build durable AI-powered workflows.
|
||||
summary: Migrate legacy @workflow/ai APIs to AI SDK's WorkflowAgent and WorkflowChatTransport.
|
||||
related:
|
||||
- /docs/ai
|
||||
---
|
||||
|
||||
The `@workflow/ai` package provides helpers for integrating AI SDK into AI-powered workflows.
|
||||
The `@workflow/ai` package is deprecated in Workflow 5 and remains documented for existing applications. Build new agents with [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and [`WorkflowChatTransport`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport) from `@ai-sdk/workflow`.
|
||||
|
||||
## Classes
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
title: Serializable Steps
|
||||
description: Wrap non-serializable third-party objects (like AI model providers) inside step factory functions so they can cross the workflow boundary.
|
||||
description: Wrap non-serializable third-party objects, including AI provider models and cloud clients, inside step factory functions.
|
||||
type: guide
|
||||
summary: Return a callback from a step to defer construction of a non-owned class (AI SDK models, cloud SDK clients) until execution time, making them usable inside durable workflows.
|
||||
summary: Defer construction of non-owned AI provider models and cloud SDK clients until step execution so they remain usable in durable workflows.
|
||||
related:
|
||||
- /docs/foundations/serialization
|
||||
- /docs/foundations/serialization#custom-class-serialization
|
||||
@@ -10,7 +10,7 @@ related:
|
||||
---
|
||||
|
||||
<CopyPrompt
|
||||
text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing the object (AI SDK model, cloud SDK client) into the workflow, export a factory that returns an async callback marked with "use step" which constructs and returns the object at execution time, for example `export function openai(...args) { return async () => { "use step"; return openaiProvider(...args); }; }`. Pass the factory across the workflow boundary (the compiler serializes the function reference, not the instance) and invoke it inside steps where full Node.js access is available. Keep the factory's constructor arguments serializable. Verify the workflow builds, replays deterministically, and the dependency is only instantiated during step execution."
|
||||
text="Make this non-serializable dependency usable inside a durable workflow with the step-as-factory pattern. Instead of passing an AI provider model, cloud SDK client, or other class instance into the workflow, export a factory that returns an async callback marked with "use step". Capture only serializable constructor options, construct the provider or client inside the step, and keep the instance inside that step's execution. Verify the workflow builds, replays deterministically, and never serializes the live dependency."
|
||||
/>
|
||||
|
||||
<Callout>
|
||||
@@ -22,97 +22,68 @@ This is an advanced guide. It dives into workflow internals and is not required
|
||||
Workflow functions run inside a sandboxed VM where every value that crosses a function boundary must be serializable. There are two ways to get a non-serializable object across that boundary, depending on whether you own the class:
|
||||
|
||||
- **You own the class**: implement the [`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE` protocol](/docs/foundations/serialization#custom-class-serialization). The instance becomes a first-class serializable value: you can pass it as a workflow input, return it from a step, and call `"use step"` instance methods on it directly. This is the right tool when the class is yours to modify.
|
||||
- **You don't own the class**: you can't add methods to `openai("gpt-4o")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction in a `"use step"` factory function and pass the factory across the boundary. That's what this page covers.
|
||||
- **You don't own the class**: you can't add serialization methods to `openai("gpt-5.6-sol")` from `@ai-sdk/openai` or `new S3Client({...})` from `@aws-sdk/client-s3`. Instead, wrap construction and use in a `"use step"` factory function. That's what this page covers.
|
||||
|
||||
## The problem
|
||||
|
||||
AI SDK model providers (`openai("gpt-4o")`, `anthropic("claude-sonnet-4-20250514")`, etc.) return complex objects with methods, closures, and internal state. Passing one directly into a step causes a serialization error, and you can't bolt `WORKFLOW_SERIALIZE` onto a third-party class.
|
||||
AI SDK provider models and cloud SDK clients often contain methods, closures, sockets, and internal state. Passing one across a workflow boundary causes a serialization error, and you can't add `WORKFLOW_SERIALIZE` to a class you don't own.
|
||||
|
||||
```typescript lineNumbers
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { getWritable } from "workflow";
|
||||
import type { UIMessageChunk } from "ai";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
export async function brokenAgent(prompt: string) {
|
||||
async function uploadFile(client: S3Client, key: string) {
|
||||
"use step";
|
||||
// ... upload with client ...
|
||||
}
|
||||
|
||||
export async function brokenUpload(region: string, key: string) {
|
||||
"use workflow";
|
||||
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
const agent = new DurableAgent({
|
||||
// This fails: the model object is not serializable
|
||||
model: openai("gpt-4o"),
|
||||
});
|
||||
|
||||
await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
|
||||
const client = new S3Client({ region });
|
||||
await uploadFile(client, key); // Fails: S3Client is not serializable
|
||||
}
|
||||
```
|
||||
|
||||
## The solution: step-as-factory
|
||||
|
||||
Instead of passing the model object, pass a **callback function** that returns the model. Marking that callback with `"use step"` tells the compiler to serialize the *function reference* (which is a string identifier) rather than its return value. The provider is only instantiated at execution time, inside the step's full Node.js runtime.
|
||||
Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
|
||||
|
||||
```typescript lineNumbers
|
||||
import { openai as openaiProvider } from "@ai-sdk/openai";
|
||||
|
||||
// Returns a step function, not a model object
|
||||
export function openai(...args: Parameters<typeof openaiProvider>) {
|
||||
return async () => {
|
||||
"use step";
|
||||
return openaiProvider(...args); // [!code highlight]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead of a model object. When the agent needs to call the large language model (LLM), it invokes the factory inside a step where the real provider can be constructed with full Node.js access.
|
||||
|
||||
## How `@workflow/ai` uses this
|
||||
|
||||
<Callout type="warn">
|
||||
`@workflow/ai`'s pre-wrapped providers and `DurableAgent` are deprecated. AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) resolves models from AI Gateway model strings (for example, `"openai/gpt-4o"`), which usually removes the need for a model factory; see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The serialization pattern on this page still applies to any non-serializable dependency you own (for example, cloud SDK clients).
|
||||
<Callout type="info">
|
||||
A plain Vercel AI Gateway model string such as `"spacexai/grok-4.6"` is already serializable and does not need a factory.
|
||||
</Callout>
|
||||
|
||||
The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:
|
||||
### AI provider example
|
||||
|
||||
When using an AI SDK provider package, construct and use its model inside the step. The outer factory captures only the serializable model ID:
|
||||
|
||||
```typescript lineNumbers
|
||||
// packages/ai/src/providers/anthropic.ts
|
||||
import { anthropic as anthropicProvider } from "@ai-sdk/anthropic";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { generateText } from "ai";
|
||||
|
||||
export function anthropic(...args: Parameters<typeof anthropicProvider>) {
|
||||
return async () => {
|
||||
export function createOpenAIGenerator(modelId: string) {
|
||||
return async (prompt: string) => {
|
||||
"use step";
|
||||
return anthropicProvider(...args); // [!code highlight]
|
||||
const { text } = await generateText({ model: openai(modelId), prompt });
|
||||
return text;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This means you import from `@workflow/ai` instead of `@ai-sdk/*` directly:
|
||||
|
||||
```typescript lineNumbers
|
||||
import { anthropic } from "@workflow/ai/anthropic";
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { getWritable } from "workflow";
|
||||
import type { UIMessageChunk } from "ai";
|
||||
|
||||
export async function chatAgent(prompt: string) {
|
||||
export async function summarize(prompt: string) {
|
||||
"use workflow";
|
||||
|
||||
const writable = getWritable<UIMessageChunk>();
|
||||
const agent = new DurableAgent({
|
||||
model: anthropic("claude-sonnet-4-20250514"), // [!code highlight]
|
||||
});
|
||||
|
||||
await agent.stream({ messages: [{ role: "user", content: prompt }], writable });
|
||||
const generate = createOpenAIGenerator("gpt-5.6-sol");
|
||||
return generate(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
## Writing your own serializable wrapper
|
||||
The same structure works with provider packages such as `@ai-sdk/anthropic` and `@ai-sdk/google`: capture serializable configuration in the outer function and keep the provider object inside the step. For durable agent loops, continue to use `WorkflowAgent`; the factory pattern is for lower-level AI SDK calls and other third-party dependencies.
|
||||
|
||||
Apply the same pattern to any non-serializable dependency. The key rule: **the outer function captures serializable arguments, and the inner `"use step"` function constructs the real object at runtime**.
|
||||
### Cloud client example
|
||||
|
||||
```typescript lineNumbers
|
||||
import type { S3Client as S3ClientType } from "@aws-sdk/client-s3";
|
||||
|
||||
// The arguments (region, bucket) are plain strings, which are serializable
|
||||
// The region is a plain string, which is serializable
|
||||
export function createS3Client(region: string) {
|
||||
return async (): Promise<S3ClientType> => {
|
||||
"use step";
|
||||
@@ -152,4 +123,5 @@ async function uploadFile(
|
||||
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Marks a function for extraction and serialization.
|
||||
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent, resolves models through AI Gateway strings, and replaces `DurableAgent`.
|
||||
- [AI SDK providers](https://ai-sdk.dev/providers/ai-sdk-providers): Lists direct provider packages and configuration.
|
||||
- [Custom class serialization](/docs/foundations/serialization#custom-class-serialization): Provides the companion pattern for classes you own (`WORKFLOW_SERIALIZE` and `WORKFLOW_DESERIALIZE`).
|
||||
|
||||
@@ -1,54 +1,59 @@
|
||||
---
|
||||
title: Agent Cancellation
|
||||
description: Cancel a running agent from the outside using AbortSignal. A hook fires the abort, the agent step bails out of the model stream, and the client gets a clean stop notification.
|
||||
description: Cancel a running WorkflowAgent from the outside using AbortSignal and a durable stop hook.
|
||||
type: guide
|
||||
summary: Cancel a running agent cooperatively with AbortController. A stop hook fires controller.abort(), the signal propagates into the agent step to cancel the model stream, and a data-stopped part is emitted to streaming clients before the workflow returns.
|
||||
summary: Cancel a running WorkflowAgent cooperatively with AbortController so the model stream stops and the workflow can return a clean status.
|
||||
---
|
||||
|
||||
<CopyPrompt
|
||||
text="Add cancellation to this durable AI agent. For hard cancellation, expose a server route that receives `runId` and calls `getRun(runId).cancel()` from `workflow/api`. For graceful stop, define `stopHook` with `defineHook()` from `workflow`, create it with a stable token such as the workflow run ID, and race the agent loop against the stop hook using `Promise.race`. Use `getWritable<UIMessageChunk>()` to emit a final stopped/canceled message before returning. Wire the UI Stop button to the route that resumes the hook or falls back to `getRun(runId).cancel()`. Verify active model/tool work stops, cleanup runs for graceful stop, and stale run IDs are handled."
|
||||
text="Add cancellation to this AI SDK WorkflowAgent. For hard cancellation, expose a server route that receives `runId` and calls `getRun(runId).cancel()` from `workflow/api`. For graceful cancellation, define `stopHook` with `defineHook()` from `workflow`, create it with a stable token such as the workflow run ID, and race `WorkflowAgent.stream()` against the stop hook using `Promise.race`. Pass an `AbortController` signal to the agent, forward the tool execution signal to cancellable I/O, and wait for the agent branch to settle after aborting before returning a stopped status. Wire the UI Stop button to the route that resumes the hook or falls back to `getRun(runId).cancel()`, and verify stale run IDs are handled."
|
||||
/>
|
||||
|
||||
Cancel a running agent from the outside through a **Stop** button in a chat user interface (UI), an admin cancellation endpoint, or a timeout fallback.
|
||||
|
||||
<Callout type="warn">
|
||||
This recipe uses the deprecated `DurableAgent` API. For new agents, use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and follow the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The cancellation patterns here (`run.cancel()`, stop-signal hook + `Promise.race`, `AbortController`) apply to either API.
|
||||
</Callout>
|
||||
|
||||
## Pattern
|
||||
|
||||
Create an `AbortController` in the workflow and race the agent (passing its signal) against a stop hook. When the hook fires, `controller.abort()` is called: the signal propagates into the agent step and cancels the underlying model stream. Before returning, a `data-stopped` part is written to the stream so any streaming clients can render a clean end state.
|
||||
Create an `AbortController` in the workflow and race the agent (passing its signal) against a stop hook. When the hook fires, call `controller.abort()`, then wait for the agent branch to observe the signal and settle before the workflow returns. The signal cancels the underlying model stream and is passed to tool execution; each tool must forward it to cancellable I/O. The workflow return value records whether it completed or stopped; read that value through the run API or persist it in application state.
|
||||
|
||||
{/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
|
||||
```typescript lineNumbers
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import {
|
||||
isStepCount,
|
||||
tool,
|
||||
type ModelMessage,
|
||||
type ToolExecutionOptions,
|
||||
} from "ai";
|
||||
import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
|
||||
import { z } from "zod";
|
||||
import type { ModelMessage, UIMessageChunk } from "ai";
|
||||
|
||||
export const stopHook = defineHook({
|
||||
schema: z.object({ reason: z.string().optional() }),
|
||||
});
|
||||
|
||||
async function searchWeb({ query }: { query: string }) {
|
||||
async function searchWeb(
|
||||
{ query }: { query: string },
|
||||
{ abortSignal }: ToolExecutionOptions<unknown>,
|
||||
) {
|
||||
"use step";
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
return { results: [{ title: `${query} - Wikipedia`, snippet: `Overview of ${query}...` }] };
|
||||
const response = await fetch(
|
||||
`https://api.example.com/search?q=${encodeURIComponent(query)}`,
|
||||
{ signal: abortSignal }, // [!code highlight]
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function analyzeData({ topic }: { topic: string }) {
|
||||
async function analyzeData(
|
||||
{ topic }: { topic: string },
|
||||
{ abortSignal }: ToolExecutionOptions<unknown>,
|
||||
) {
|
||||
"use step";
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
return { summary: `Analysis of ${topic}: significant developments found.`, confidence: 0.85 };
|
||||
}
|
||||
|
||||
async function emitStopSignal(details: { reason?: string }) {
|
||||
"use step";
|
||||
const writer = getWritable<UIMessageChunk>().getWriter();
|
||||
try {
|
||||
await writer.write({ type: "data-stopped", id: "stop-signal", data: details } as UIMessageChunk);
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
const response = await fetch("https://api.example.com/analyze", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ topic }),
|
||||
signal: abortSignal, // [!code highlight]
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function stoppableAgent(messages: ModelMessage[]) {
|
||||
@@ -58,43 +63,48 @@ export async function stoppableAgent(messages: ModelMessage[]) {
|
||||
const controller = new AbortController(); // [!code highlight]
|
||||
const hook = stopHook.create({ token: `stop:${workflowRunId}` });
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a research assistant. Search and analyze data as needed.",
|
||||
tools: {
|
||||
searchWeb: {
|
||||
searchWeb: tool({
|
||||
description: "Search the web for information",
|
||||
inputSchema: z.object({ query: z.string() }),
|
||||
execute: searchWeb,
|
||||
},
|
||||
analyzeData: {
|
||||
}),
|
||||
analyzeData: tool({
|
||||
description: "Analyze a piece of data",
|
||||
inputSchema: z.object({ topic: z.string() }),
|
||||
execute: analyzeData,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await Promise.race([
|
||||
agent
|
||||
.stream({
|
||||
const agentPromise = agent.stream({
|
||||
messages,
|
||||
writable: getWritable<UIMessageChunk>(),
|
||||
writable: getWritable<ModelCallStreamPart>(),
|
||||
abortSignal: controller.signal, // [!code highlight]
|
||||
maxSteps: 15,
|
||||
})
|
||||
.then((r) => ({ type: "complete" as const, messages: r.messages })),
|
||||
hook.then(({ reason }) => {
|
||||
controller.abort(reason); // [!code highlight]
|
||||
return { type: "stopped" as const, reason };
|
||||
}),
|
||||
stopWhen: isStepCount(15),
|
||||
});
|
||||
|
||||
const outcome = await Promise.race([
|
||||
agentPromise.then((result) => ({
|
||||
type: "complete" as const,
|
||||
messages: result.messages,
|
||||
})),
|
||||
hook.then(({ reason }) => ({
|
||||
type: "stop-requested" as const,
|
||||
reason,
|
||||
})),
|
||||
]);
|
||||
|
||||
if (result.type === "stopped") {
|
||||
await emitStopSignal({ reason: result.reason });
|
||||
if (outcome.type === "stop-requested") {
|
||||
controller.abort(outcome.reason); // [!code highlight]
|
||||
await agentPromise; // Wait until the losing branch has stopped. // [!code highlight]
|
||||
return { type: "stopped" as const, reason: outcome.reason };
|
||||
}
|
||||
|
||||
return result;
|
||||
return outcome;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -145,20 +155,20 @@ export function StopButton({ runId }: { runId: string }) {
|
||||
1. The workflow creates an `AbortController` when it starts.
|
||||
2. The workflow creates a hook with the token `stop:${workflowRunId}`.
|
||||
3. `Promise.race` runs the agent stream and the stop hook concurrently.
|
||||
4. The agent receives `controller.signal`. When aborted, the signal cancels the underlying model stream.
|
||||
5. When the stop API resumes the hook, the workflow calls `controller.abort()`, resolves the race, and exits.
|
||||
6. `emitStopSignal` writes a `data-stopped` part to the stream so the client renders a clean stop state.
|
||||
4. The agent receives `controller.signal`. When aborted, the signal cancels the active model stream, propagates to tool execution, and prevents another model step from starting. Tool implementations must forward it to operations such as `fetch` that support cancellation.
|
||||
5. When the stop API resumes the hook, the race reports a stop request. The workflow aborts the controller and awaits `agentPromise`, so it does not return while that branch is still running.
|
||||
6. After the agent branch settles, the workflow returns a `stopped` result. That return value is not written to the model-call stream automatically; the application can read `run.returnValue` or persist the status separately.
|
||||
|
||||
## Adapting this
|
||||
|
||||
- **Add a timeout**: Race a third `sleep()` promise to stop automatically after a deadline.
|
||||
- **Audit logging**: Include a `reason` field in the stop schema to record who stopped the agent and why.
|
||||
- **Cross-process**: The hook token is deterministic, so any process can call `stopHook.resume()` with the run ID.
|
||||
- **Step limits**: Combine the pattern with `maxSteps` on the agent to cap execution without a manual stop.
|
||||
- **Step limits**: Combine the pattern with `stopWhen: isStepCount(...)` to cap execution without a manual stop.
|
||||
|
||||
## Key APIs
|
||||
|
||||
- [`defineHook()`](/docs/api-reference/workflow/define-hook): Defines a type-safe hook for the stop signal.
|
||||
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata): Provides the run ID for deterministic hook tokens.
|
||||
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Streams output and the stop notification to the client.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK's durable agent that respects the abort signal and replaces `DurableAgent`.
|
||||
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call output from the agent.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK's durable agent that applies the abort signal to model calls and replaces `DurableAgent`.
|
||||
|
||||
@@ -11,12 +11,32 @@ summary: Build durable, resumable AI agents with AI SDK v7's WorkflowAgent.
|
||||
|
||||
## WorkflowAgent from AI SDK v7
|
||||
|
||||
Use AI SDK v7's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for new durable agent work. It replaces `DurableAgent` and keeps the current agent pattern in the AI SDK package.
|
||||
Use AI SDK v7's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for durable agents in Workflow 5. It replaces `DurableAgent` and keeps the current agent pattern in the AI SDK package.
|
||||
|
||||
- Import `WorkflowAgent` from `@ai-sdk/workflow` and run it inside a `"use workflow"` function.
|
||||
- Pass `"spacexai/grok-4.6"` as a plain model string so AI SDK routes requests through Vercel AI Gateway.
|
||||
- Stream `ModelCallStreamPart` chunks with `getWritable()`, then convert the run stream to UI message chunks with `createModelCallToUIChunkTransform()` in your route.
|
||||
- Mark tool `execute` functions with `"use step"` when they should run as durable workflow steps with retry and observability behavior.
|
||||
|
||||
```typescript
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import { getWritable } from "workflow";
|
||||
|
||||
export async function agentWorkflow(prompt: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
});
|
||||
|
||||
return agent.stream({
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
writable: getWritable<ModelCallStreamPart>(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
`DurableAgent` is deprecated and remains documented for existing code only. See the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent), or the [`DurableAgent` API reference](/docs/api-reference/workflow-ai/durable-agent) while migrating.
|
||||
</Callout>
|
||||
|
||||
@@ -1,46 +1,37 @@
|
||||
---
|
||||
title: Human-in-the-Loop
|
||||
description: Pause an AI agent to wait for human approval, then resume based on the decision.
|
||||
description: Pause a WorkflowAgent for human approval before a consequential tool executes.
|
||||
type: guide
|
||||
summary: Use defineHook with the tool call ID to suspend an agent for human approval, with an optional timeout.
|
||||
summary: Use WorkflowAgent's needsApproval option and AI SDK approval responses to build durable human approval flows.
|
||||
---
|
||||
|
||||
<CopyPrompt
|
||||
text="Add a human approval gate to this AI workflow. Define a typed hook with `defineHook()` from `workflow` for approval payloads. At the approval point, create the hook once with a stable token, await it inside the `"use workflow"` function, and branch on approved/rejected input. Add a server route that receives the human decision and calls `resumeHook(token, payload)` from `workflow/api`. If the approval should expire, race the hook against `sleep()` from `workflow`. Update the UI to show the pending approval and call the resume route. Verify approve, reject, timeout, and duplicate resume behavior."
|
||||
text="Add a human approval gate to this AI SDK WorkflowAgent. Define the consequential action with AI SDK's `tool()` helper, set `needsApproval: true` (or an input-dependent function), and keep the tool's `execute` function as a durable `"use step"` function. Configure `experimental_toolApprovalSecret` with the name of a high-entropy secret environment variable so client-supplied approvals are signed and verified. Stream `ModelCallStreamPart` values through `getWritable()` and convert them with `createModelCallToUIChunkTransform()` in the API route. In the client, render tool parts whose state is `approval-requested`, call `addToolApprovalResponse()` with the approval ID and decision, and use `lastAssistantMessageIsCompleteWithApprovalResponses` to continue automatically. Verify approve and reject paths, invalid or missing signatures, duplicate responses, and that the side effect never runs before approval."
|
||||
/>
|
||||
|
||||
<Callout type="warn">
|
||||
This recipe uses the deprecated `DurableAgent` API. For new agents, use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) and follow the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The human-in-the-loop pattern here (hooks, `Promise.race`, approval gating) applies to either API.
|
||||
</Callout>
|
||||
|
||||
Use this pattern when an AI agent needs human confirmation before performing a consequential action like booking, purchasing, or publishing. The workflow suspends without consuming resources until the human responds through a user interface (UI) or API.
|
||||
Use this pattern when an AI agent needs confirmation before performing an action such as booking, purchasing, publishing, or deleting data. `WorkflowAgent` makes approval a first-class part of the durable agent loop: it emits an approval request, pauses before the tool executes, and resumes after the user responds.
|
||||
|
||||
## When to use this
|
||||
|
||||
- Booking confirmations where users must approve before charges are made
|
||||
- Content publishing gates where an editor must sign off
|
||||
- Agent actions where the cost of an error justifies a human check
|
||||
- Actions with side effects that are difficult to reverse
|
||||
- Agent actions where the cost of an error justifies human review
|
||||
- Side effects that are difficult to reverse
|
||||
|
||||
## Pattern
|
||||
## Define an approval-gated tool
|
||||
|
||||
Create a typed hook using `defineHook()`. When the agent calls the approval tool, the tool emits a custom data part to the stream so the client can render approval controls, then creates a hook and suspends. An API route resumes the hook with the decision.
|
||||
Set `needsApproval` on the tool. Keep the action itself in a step so it receives Workflow retries and observability only after approval succeeds.
|
||||
|
||||
### Workflow
|
||||
|
||||
```typescript
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { defineHook, sleep, getWritable } from "workflow";
|
||||
```typescript title="workflows/booking-agent.ts" lineNumbers
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import {
|
||||
tool,
|
||||
type InferUITools,
|
||||
type ModelMessage,
|
||||
type UIMessage,
|
||||
} from "ai";
|
||||
import { getWritable } from "workflow";
|
||||
import { z } from "zod";
|
||||
import type { ModelMessage, UIMessageChunk } from "ai";
|
||||
|
||||
// Exported so the approval API route can call .resume()
|
||||
export const bookingApprovalHook = defineHook({ // [!code highlight]
|
||||
schema: z.object({
|
||||
approved: z.boolean(),
|
||||
comment: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
async function searchFlights({ from, to, date }: {
|
||||
from: string;
|
||||
@@ -48,110 +39,29 @@ async function searchFlights({ from, to, date }: {
|
||||
date: string;
|
||||
}) {
|
||||
"use step";
|
||||
const res = await fetch(
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
|
||||
);
|
||||
return res.json();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function confirmBooking({ flightId, passenger }: {
|
||||
async function confirmBooking({ flightId, passenger, price }: {
|
||||
flightId: string;
|
||||
passenger: string;
|
||||
price: number;
|
||||
}) {
|
||||
"use step";
|
||||
const res = await fetch("https://api.example.com/bookings", {
|
||||
|
||||
const response = await fetch("https://api.example.com/bookings", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ flightId, passenger }),
|
||||
body: JSON.stringify({ flightId, passenger, price }),
|
||||
});
|
||||
return res.json();
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Stream a custom data part so the client can render the approval UI.
|
||||
// This MUST run before the hook suspends the workflow, otherwise
|
||||
// the tool-invocation won't appear in the stream until the tool returns,
|
||||
// and the client would have no way to show approval buttons.
|
||||
async function emitApprovalRequest(details: {
|
||||
flightId: string;
|
||||
passenger: string;
|
||||
price: number;
|
||||
toolCallId: string;
|
||||
}) {
|
||||
"use step";
|
||||
const writer = getWritable<UIMessageChunk>().getWriter();
|
||||
try {
|
||||
await writer.write({
|
||||
type: "data-approval-needed", // [!code highlight]
|
||||
id: details.toolCallId,
|
||||
data: details,
|
||||
} as UIMessageChunk);
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the resolution so the client can update the approval card.
|
||||
async function emitApprovalResolved(details: {
|
||||
toolCallId: string;
|
||||
result: string;
|
||||
}) {
|
||||
"use step";
|
||||
const writer = getWritable<UIMessageChunk>().getWriter();
|
||||
try {
|
||||
await writer.write({
|
||||
type: "data-approval-resolved", // [!code highlight]
|
||||
id: details.toolCallId,
|
||||
data: details,
|
||||
} as UIMessageChunk);
|
||||
} finally {
|
||||
writer.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// No "use step": hooks are workflow-level primitives
|
||||
async function requestBookingApproval(
|
||||
{ flightId, passenger, price }: {
|
||||
flightId: string;
|
||||
passenger: string;
|
||||
price: number;
|
||||
},
|
||||
{ toolCallId }: { toolCallId: string }
|
||||
) {
|
||||
// Emit to the stream before suspending so the UI can show buttons
|
||||
await emitApprovalRequest({ flightId, passenger, price, toolCallId }); // [!code highlight]
|
||||
|
||||
const hook = bookingApprovalHook.create({ token: toolCallId });
|
||||
|
||||
// Race: human decision vs. timeout
|
||||
const result = await Promise.race([
|
||||
hook.then((payload) => ({ type: "decision" as const, ...payload })),
|
||||
sleep("24h").then(() => ({ type: "timeout" as const, approved: false as const })),
|
||||
]);
|
||||
|
||||
if (result.type === "timeout") {
|
||||
const msg = "Booking request expired.";
|
||||
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
||||
return msg;
|
||||
}
|
||||
if (!result.approved) {
|
||||
const msg = `Rejected: ${result.comment || "No reason given"}`;
|
||||
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
||||
return msg;
|
||||
}
|
||||
|
||||
const booking = await confirmBooking({ flightId, passenger });
|
||||
const msg = `Booked! Confirmation: ${booking.confirmationId}`;
|
||||
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
||||
return msg;
|
||||
}
|
||||
|
||||
export async function bookingAgent(messages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
instructions: "You help book flights. Always request approval before booking.",
|
||||
tools: {
|
||||
searchFlights: {
|
||||
export const bookingTools = {
|
||||
searchFlights: tool({
|
||||
description: "Search for available flights",
|
||||
inputSchema: z.object({
|
||||
from: z.string().describe("Departure airport code"),
|
||||
@@ -159,105 +69,172 @@ export async function bookingAgent(messages: ModelMessage[]) {
|
||||
date: z.string().describe("Travel date (YYYY-MM-DD)"),
|
||||
}),
|
||||
execute: searchFlights,
|
||||
},
|
||||
requestBookingApproval: {
|
||||
description: "Request human approval before booking a flight",
|
||||
inputSchema: z.object({
|
||||
flightId: z.string().describe("Flight ID to book"),
|
||||
passenger: z.string().describe("Passenger name"),
|
||||
price: z.number().describe("Total price"),
|
||||
}),
|
||||
execute: requestBookingApproval,
|
||||
},
|
||||
},
|
||||
confirmBooking: tool({
|
||||
description: "Book a selected flight for a passenger",
|
||||
inputSchema: z.object({
|
||||
flightId: z.string(),
|
||||
passenger: z.string(),
|
||||
price: z.number(),
|
||||
}),
|
||||
needsApproval: true, // [!code highlight]
|
||||
execute: confirmBooking,
|
||||
}),
|
||||
};
|
||||
|
||||
export type BookingAgentUIMessage = UIMessage<
|
||||
unknown,
|
||||
never,
|
||||
InferUITools<typeof bookingTools>
|
||||
>;
|
||||
|
||||
export async function bookingAgent(messages: ModelMessage[]) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "Help the user find and book flights.",
|
||||
tools: bookingTools,
|
||||
experimental_toolApprovalSecret: { // [!code highlight]
|
||||
environmentVariable: "WORKFLOW_TOOL_APPROVAL_SECRET", // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
});
|
||||
|
||||
await agent.stream({
|
||||
return agent.stream({
|
||||
messages,
|
||||
writable: getWritable<UIMessageChunk>(),
|
||||
writable: getWritable<ModelCallStreamPart>(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Approval API route
|
||||
## Sign approval requests
|
||||
|
||||
The approval route imports the hook definition and calls `.resume()` with the tool call ID as the token:
|
||||
When approval responses come from client-supplied message history, configure `experimental_toolApprovalSecret` as shown above. `WorkflowAgent` signs the approval ID, tool-call ID, tool name, and validated input when it emits the approval request, then verifies that signature before an approved tool can execute. Missing or invalid signatures prevent the action from running.
|
||||
|
||||
Set `WORKFLOW_TOOL_APPROVAL_SECRET` to a high-entropy secret in every environment that can execute the workflow. For example, generate one with `openssl rand -base64 32`, then store it in your deployment's secret environment variables. Only the environment variable name crosses the workflow boundary; the secret is read inside signing and verification steps and is never serialized into workflow history.
|
||||
|
||||
Signed approvals require `@ai-sdk/workflow` 2.0.16 or later.
|
||||
|
||||
`needsApproval` can also decide from the parsed tool input. For example, require approval only when a booking costs more than a threshold:
|
||||
|
||||
{/* @skip-typecheck: property excerpt */}
|
||||
```typescript
|
||||
import { bookingApprovalHook } from "@/app/workflows/booking-agent";
|
||||
needsApproval: async ({ price }) => price > 500,
|
||||
```
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { toolCallId, approved, comment } = await req.json();
|
||||
## Start the workflow and transform its stream
|
||||
|
||||
await bookingApprovalHook.resume(toolCallId, { approved, comment }); // [!code highlight]
|
||||
`WorkflowAgent` stores `ModelCallStreamPart` values. Convert those durable parts into AI SDK UI chunks at the HTTP boundary:
|
||||
|
||||
return Response.json({ success: true });
|
||||
```typescript title="app/api/chat/route.ts" lineNumbers
|
||||
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
||||
import {
|
||||
convertToModelMessages,
|
||||
createUIMessageStreamResponse,
|
||||
} from "ai";
|
||||
import { start } from "workflow/api";
|
||||
import {
|
||||
bookingAgent,
|
||||
type BookingAgentUIMessage,
|
||||
} from "@/workflows/booking-agent";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { messages }: { messages: BookingAgentUIMessage[] } =
|
||||
await request.json();
|
||||
const modelMessages = await convertToModelMessages(messages);
|
||||
const run = await start(bookingAgent, [modelMessages]);
|
||||
|
||||
return createUIMessageStreamResponse({
|
||||
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
|
||||
headers: { "x-workflow-run-id": run.runId },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Client rendering
|
||||
## Render and answer approval requests
|
||||
|
||||
Listen for `data-approval-needed` and `data-approval-resolved` custom data parts in the message stream. The approval tool invocation itself won't appear until the tool returns, so the custom data parts are the mechanism for showing and updating the approval UI.
|
||||
Approval requests arrive as typed tool parts with `state: "approval-requested"`. Call `addToolApprovalResponse()` with the approval ID. The AI SDK then sends the updated message history back to the route and `WorkflowAgent` continues the durable tool flow.
|
||||
|
||||
```tsx
|
||||
// Scan all messages for the resolution
|
||||
const approvalResult = messages
|
||||
.flatMap((m) => m.parts)
|
||||
.find((p) => p.type === "data-approval-resolved")
|
||||
?.data?.result;
|
||||
```tsx title="app/chat.tsx" lineNumbers
|
||||
"use client";
|
||||
|
||||
// In your message parts loop:
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === "data-approval-needed") { // [!code highlight]
|
||||
const { flightId, passenger, price, toolCallId } = part.data;
|
||||
if (approvalResult) {
|
||||
return <div key={i}>Result: {approvalResult}</div>;
|
||||
}
|
||||
return (
|
||||
<div key={i} className="rounded-lg border p-4 space-y-3">
|
||||
<div className="text-sm">
|
||||
<div>Flight: {flightId}</div>
|
||||
<div>Passenger: {passenger}</div>
|
||||
<div>Price: ${price}</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => approve(toolCallId)}>Approve</button> {/* [!code highlight] */}
|
||||
<button onClick={() => reject(toolCallId)}>Reject</button> {/* [!code highlight] */}
|
||||
</div>
|
||||
</div>
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
||||
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
||||
import { useMemo } from "react";
|
||||
import type { BookingAgentUIMessage } from "@/workflows/booking-agent";
|
||||
|
||||
export function Chat() {
|
||||
const transport = useMemo(
|
||||
() => new WorkflowChatTransport({ api: "/api/chat" }),
|
||||
[]
|
||||
);
|
||||
}
|
||||
// Hide the requestBookingApproval tool-invocation part
|
||||
if (part.type === "tool-invocation" &&
|
||||
part.toolInvocation.toolName === "requestBookingApproval") {
|
||||
const { messages, addToolApprovalResponse } = useChat<BookingAgentUIMessage>({
|
||||
transport,
|
||||
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
||||
});
|
||||
|
||||
return messages.map((message) =>
|
||||
message.parts.map((part) => {
|
||||
if (
|
||||
part.type !== "tool-confirmBooking" ||
|
||||
part.state !== "approval-requested" ||
|
||||
part.approval.isAutomatic
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// ... other part types
|
||||
})}
|
||||
|
||||
return (
|
||||
<div key={part.toolCallId}>
|
||||
<p>
|
||||
Book flight {part.input.flightId} for {part.input.passenger} at
|
||||
${part.input.price}?
|
||||
</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
addToolApprovalResponse({
|
||||
id: part.approval.id,
|
||||
approved: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
addToolApprovalResponse({
|
||||
id: part.approval.id,
|
||||
approved: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
1. **`defineHook()` with schema**: Creates a typed hook with Zod validation. The approval payload is validated before the workflow receives it.
|
||||
2. **`toolCallId` as token**: Uses the tool call ID as the hook token, linking the hook to the specific tool invocation.
|
||||
3. **`emitApprovalRequest` step**: Writes a `data-approval-needed` custom data part to the stream *before* the hook suspends. Without this step, the client wouldn't see the approval controls because tool invocations don't stream until the tool returns.
|
||||
4. **No `"use step"` on the approval tool**: Runs the tool at the workflow level because `defineHook().create()` is a workflow primitive. The tool calls step functions (`emitApprovalRequest`, `emitApprovalResolved`, and `confirmBooking`) for I/O.
|
||||
5. **`Promise.race` with sleep**: Races the approval against a durable timeout. If nobody responds, the workflow continues with an expiration message.
|
||||
6. **`emitApprovalResolved` step**: Writes the outcome to the stream so the client can update the card immediately without waiting for the tool-invocation result.
|
||||
1. The model calls `confirmBooking` with validated input.
|
||||
2. `needsApproval` prevents the tool's `execute` function from running and emits an approval request.
|
||||
3. The durable stream preserves the request across disconnects and process restarts.
|
||||
4. The client adds an approval response to the conversation.
|
||||
5. If approved, `confirmBooking` runs as a durable step. If rejected, the model receives the denial and can respond without performing the side effect.
|
||||
|
||||
## Adapting to your use case
|
||||
## Adapting the pattern
|
||||
|
||||
- **Change the approval schema**: Add fields such as `reason`, `amount`, and `reviewerEmail` to match your domain.
|
||||
- **Multiple approval gates**: Apply the pattern to any number of tools. Each tool creates its own hook with its own `toolCallId`.
|
||||
- **Escalation**: If the first approver doesn't respond, use `sleep()` and another hook to escalate to a backup reviewer.
|
||||
- **Adjust the timeout**: Use `"24h"` for production and shorter durations for demos.
|
||||
- **Workflow-level versus step tools**: Tools that use `sleep()`, `defineHook()`, or other workflow primitives must not use `"use step"`. Tools with only I/O, such as API calls and database queries, should use `"use step"` for retries.
|
||||
- **Conditional approval**: Return a boolean from `needsApproval` based on amount, tenant policy, or risk.
|
||||
- **Timeouts and escalation**: Combine the surrounding workflow with `sleep()` and hooks when an approval must expire or escalate.
|
||||
- **Audit context**: Include durable user and tenant identifiers in the workflow input, then record the approver in your application database.
|
||||
- **Multiple gates**: Set `needsApproval` on every consequential tool independently.
|
||||
|
||||
## Key APIs
|
||||
|
||||
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions): Declares the orchestrator function.
|
||||
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): Declares step functions with retries.
|
||||
- [`defineHook()`](/docs/api-reference/workflow/define-hook): Defines a type-safe hook with schema validation.
|
||||
- [`sleep()`](/docs/api-reference/workflow/sleep): Provides a durable timeout for approval expiration.
|
||||
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Streams custom data parts from steps.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent and replaces `DurableAgent`.
|
||||
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Durable AI SDK agent with first-class tool approvals
|
||||
- [`tool()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool): Defines a typed tool and its approval policy
|
||||
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call stream parts
|
||||
- [`WorkflowChatTransport`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport): Reconnects interrupted chat streams
|
||||
|
||||
@@ -91,7 +91,7 @@ async function runTurn(messages: ModelMessage[]) {
|
||||
"use step";
|
||||
|
||||
const result = streamText({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
system: "You are a customer support agent.",
|
||||
messages,
|
||||
tools: TOOLS,
|
||||
@@ -373,7 +373,7 @@ This example stores the `runId` after the first response. For strict one-session
|
||||
| **Tool call durability** | Not individually durable: re-executes with the parent turn | Per tool: mark `"use step"` for a durable, retryable step, or keep at workflow level for `sleep()` / hooks |
|
||||
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
||||
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
||||
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
|
||||
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepEnd`, `onEnd`, `onError`, `onAbort` (`onChunk` not available) |
|
||||
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
||||
|
||||
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
||||
|
||||
@@ -35,14 +35,13 @@ Import the `fetch` step function from the `workflow` package and assign it to `g
|
||||
|
||||
```typescript lineNumbers title="workflows/ai.ts"
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
export async function chatWorkflow(prompt: string) {
|
||||
"use workflow";
|
||||
|
||||
// Error - generateText() calls fetch() under the hood
|
||||
const result = await generateText({ // [!code highlight]
|
||||
model: openai("gpt-4"), // [!code highlight]
|
||||
model: "spacexai/grok-4.6", // [!code highlight]
|
||||
prompt, // [!code highlight]
|
||||
}); // [!code highlight]
|
||||
|
||||
@@ -54,7 +53,6 @@ export async function chatWorkflow(prompt: string) {
|
||||
|
||||
```typescript lineNumbers title="workflows/ai.ts"
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { fetch } from "workflow"; // [!code highlight]
|
||||
|
||||
export async function chatWorkflow(prompt: string) {
|
||||
@@ -64,7 +62,7 @@ export async function chatWorkflow(prompt: string) {
|
||||
|
||||
// Now generateText() can make HTTP requests via the fetch step
|
||||
const result = await generateText({
|
||||
model: openai("gpt-4"),
|
||||
model: "spacexai/grok-4.6",
|
||||
prompt,
|
||||
});
|
||||
|
||||
@@ -80,7 +78,6 @@ This is the most common scenario - using AI SDK functions that make HTTP request
|
||||
|
||||
```typescript lineNumbers
|
||||
import { generateText, streamText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { fetch } from "workflow"; // [!code highlight]
|
||||
|
||||
export async function aiWorkflow(userMessage: string) {
|
||||
@@ -88,9 +85,9 @@ export async function aiWorkflow(userMessage: string) {
|
||||
|
||||
globalThis.fetch = fetch; // [!code highlight]
|
||||
|
||||
// generateText makes HTTP requests to OpenAI
|
||||
// generateText makes an HTTP request under the hood
|
||||
const response = await generateText({
|
||||
model: openai("gpt-4"),
|
||||
model: "spacexai/grok-4.6",
|
||||
prompt: userMessage,
|
||||
});
|
||||
|
||||
|
||||
@@ -419,7 +419,7 @@ export async function aiAssistantWorkflow(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new WorkflowAgent({
|
||||
model: "anthropic/claude-haiku-4.5",
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful flight assistant.",
|
||||
tools: {
|
||||
searchFlights: tool({
|
||||
|
||||
@@ -43,12 +43,16 @@ export { Output };
|
||||
|
||||
/**
|
||||
* Infer the type of the tools of a durable agent.
|
||||
*
|
||||
* @deprecated Use `InferWorkflowAgentTools` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export type InferDurableAgentTools<DURABLE_AGENT> =
|
||||
DURABLE_AGENT extends DurableAgent<infer TOOLS> ? TOOLS : never;
|
||||
|
||||
/**
|
||||
* Infer the UI message type of a durable agent.
|
||||
*
|
||||
* @deprecated Use `InferWorkflowAgentUIMessage` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export type InferDurableAgentUIMessage<
|
||||
DURABLE_AGENT,
|
||||
@@ -331,13 +335,15 @@ export type PrepareStepCallback<TTools extends ToolSet = ToolSet> = (
|
||||
|
||||
/**
|
||||
* Configuration options for creating a {@link DurableAgent} instance.
|
||||
*
|
||||
* @deprecated Use `WorkflowAgentOptions` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export interface DurableAgentOptions<TTools extends ToolSet = ToolSet>
|
||||
extends GenerationSettings {
|
||||
/**
|
||||
* The model provider to use for the agent.
|
||||
*
|
||||
* This should be a string compatible with the Vercel AI Gateway (e.g., 'anthropic/claude-opus'),
|
||||
* This should be a string compatible with the Vercel AI Gateway (e.g., 'spacexai/grok-4.6'),
|
||||
* or a step function that returns a LanguageModelV3 instance.
|
||||
*/
|
||||
model: string | (() => Promise<CompatibleLanguageModel>);
|
||||
@@ -464,6 +470,8 @@ export type StreamTextOnAbortCallback<TTools extends ToolSet = ToolSet> =
|
||||
|
||||
/**
|
||||
* Options for the {@link DurableAgent.stream} method.
|
||||
*
|
||||
* @deprecated Use `WorkflowAgentStreamOptions` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export interface DurableAgentStreamOptions<
|
||||
TTools extends ToolSet = ToolSet,
|
||||
@@ -693,6 +701,8 @@ export interface ToolResult {
|
||||
|
||||
/**
|
||||
* Result of the DurableAgent.stream method.
|
||||
*
|
||||
* @deprecated Use `WorkflowAgentStreamResult` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export interface DurableAgentStreamResult<
|
||||
TTools extends ToolSet = ToolSet,
|
||||
@@ -769,7 +779,7 @@ export interface DurableAgentStreamResult<
|
||||
* @example
|
||||
* ```typescript
|
||||
* const agent = new DurableAgent({
|
||||
* model: 'anthropic/claude-opus',
|
||||
* model: 'spacexai/grok-4.6',
|
||||
* tools: {
|
||||
* getWeather: {
|
||||
* description: 'Get weather for a location',
|
||||
@@ -785,6 +795,8 @@ export interface DurableAgentStreamResult<
|
||||
* writable: getWritable<UIMessageChunk>(),
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @deprecated Use `WorkflowAgent` from `@ai-sdk/workflow` for Workflow 5 applications. `DurableAgent` remains supported for Workflow 4 maintenance applications.
|
||||
*/
|
||||
export class DurableAgent<TBaseTools extends ToolSet = ToolSet> {
|
||||
private model: string | (() => Promise<CompatibleLanguageModel>);
|
||||
|
||||
@@ -140,6 +140,7 @@ type OnChatEnd = ({
|
||||
*
|
||||
* @template UI_MESSAGE - The type of UI messages being sent and received,
|
||||
* must extend the UIMessage interface from the AI SDK.
|
||||
* @deprecated Use `WorkflowChatTransportOptions` from `@ai-sdk/workflow` for Workflow 5 applications.
|
||||
*/
|
||||
export interface WorkflowChatTransportOptions<UI_MESSAGE extends UIMessage> {
|
||||
/**
|
||||
@@ -216,6 +217,7 @@ export interface WorkflowChatTransportOptions<UI_MESSAGE extends UIMessage> {
|
||||
* must extend the UIMessage interface from the AI SDK.
|
||||
*
|
||||
* @implements {ChatTransport<UI_MESSAGE>}
|
||||
* @deprecated Use `WorkflowChatTransport` from `@ai-sdk/workflow` for Workflow 5 applications. This transport remains supported for Workflow 4 maintenance applications.
|
||||
*/
|
||||
export class WorkflowChatTransport<UI_MESSAGE extends UIMessage>
|
||||
implements ChatTransport<UI_MESSAGE>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* E2E tests for DurableAgent workflows.
|
||||
* E2E tests for WorkflowAgent workflows.
|
||||
*
|
||||
* Tests exercise DurableAgent through the full workflow runtime using mock
|
||||
* providers from @workflow/ai/test. Tests marked it.fails() correspond to
|
||||
* known API gaps that need implementation.
|
||||
* Tests exercise AI SDK 7's WorkflowAgent through the full workflow runtime
|
||||
* using a serializable mock provider.
|
||||
*
|
||||
* Run locally:
|
||||
* 1. cd workbench/nextjs-turbopack && pnpm dev
|
||||
@@ -35,14 +34,9 @@ afterAll(() => {
|
||||
writeInfraSidecar();
|
||||
});
|
||||
|
||||
// Next.js canary builds (16.2.0-canary.100+) have a regression where
|
||||
// @workflow/ai step files are missing from the step bundle, causing
|
||||
// "doStreamStep not found" errors. Skip agent tests on canary until fixed.
|
||||
const isCanary = process.env.NEXT_CANARY === '1';
|
||||
|
||||
// DurableAgent tests are only supported on Next.js and SvelteKit deployments.
|
||||
// WorkflowAgent tests are only supported on Next.js and SvelteKit deployments.
|
||||
// Nitro-based BOA deployments use the V2 combined handler which needs
|
||||
// additional work for DurableAgent support on these frameworks.
|
||||
// additional work for WorkflowAgent support on these frameworks.
|
||||
const supportedApps = new Set([
|
||||
'nextjs-turbopack',
|
||||
'nextjs-webpack',
|
||||
@@ -71,8 +65,8 @@ beforeEach((ctx) => {
|
||||
// Core agent tests
|
||||
// ============================================================================
|
||||
|
||||
describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
'DurableAgent e2e',
|
||||
describe.skipIf(isUnsupportedApp)(
|
||||
'WorkflowAgent e2e',
|
||||
{ timeout: 120_000 },
|
||||
() => {
|
||||
describe('core', () => {
|
||||
@@ -114,12 +108,12 @@ describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// onStepFinish callback tests
|
||||
// onStepEnd callback tests
|
||||
// ==========================================================================
|
||||
|
||||
describe('onStepFinish', () => {
|
||||
describe('onStepEnd', () => {
|
||||
it('fires constructor + stream callbacks in order with step data', async () => {
|
||||
const run = await start(await agentE2e('agentOnStepFinishE2e'), []);
|
||||
const run = await start(await agentE2e('agentOnStepEndE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
|
||||
// Constructor callback fires first, then stream callback
|
||||
@@ -136,12 +130,12 @@ describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// onFinish callback tests
|
||||
// onEnd callback tests
|
||||
// ==========================================================================
|
||||
|
||||
describe('onFinish', () => {
|
||||
describe('onEnd', () => {
|
||||
it('fires constructor + stream callbacks in order with event data', async () => {
|
||||
const run = await start(await agentE2e('agentOnFinishE2e'), []);
|
||||
const run = await start(await agentE2e('agentOnEndE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
|
||||
expect(rv.callSources).toEqual(['constructor', 'method']);
|
||||
@@ -212,52 +206,58 @@ describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// GAP tests — these fail until the feature is implemented
|
||||
// Additional WorkflowAgent callbacks
|
||||
// ==========================================================================
|
||||
|
||||
describe('experimental_onStart (GAP)', () => {
|
||||
it('completes but callbacks are not called (GAP)', async () => {
|
||||
describe('experimental_onStart', () => {
|
||||
it('fires constructor + stream callbacks in order', async () => {
|
||||
const run = await start(await agentE2e('agentOnStartE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
// GAP: when implemented, should be ['constructor', 'method']
|
||||
expect(rv.callSources).toEqual([]);
|
||||
expect(rv.callSources).toEqual(['constructor', 'method']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('experimental_onStepStart (GAP)', () => {
|
||||
it('completes but callbacks are not called (GAP)', async () => {
|
||||
describe('experimental_onStepStart', () => {
|
||||
it('fires constructor + stream callbacks in order', async () => {
|
||||
const run = await start(await agentE2e('agentOnStepStartE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
// GAP: when implemented, should be ['constructor', 'method']
|
||||
expect(rv.callSources).toEqual([]);
|
||||
expect(rv.callSources).toEqual(['constructor', 'method']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('experimental_onToolCallStart (GAP)', () => {
|
||||
it('completes but callbacks are not called (GAP)', async () => {
|
||||
const run = await start(await agentE2e('agentOnToolCallStartE2e'), []);
|
||||
describe('onToolExecutionStart', () => {
|
||||
it('fires constructor + stream callbacks in order', async () => {
|
||||
const run = await start(
|
||||
await agentE2e('agentOnToolExecutionStartE2e'),
|
||||
[]
|
||||
);
|
||||
const rv = await run.returnValue;
|
||||
// GAP: when implemented, should be ['constructor', 'method']
|
||||
expect(rv.calls).toEqual([]);
|
||||
expect(rv.calls).toEqual(['constructor', 'method']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('experimental_onToolCallFinish (GAP)', () => {
|
||||
it('completes but callbacks are not called (GAP)', async () => {
|
||||
const run = await start(await agentE2e('agentOnToolCallFinishE2e'), []);
|
||||
describe('onToolExecutionEnd', () => {
|
||||
it('fires constructor + stream callbacks with the tool result', async () => {
|
||||
const run = await start(
|
||||
await agentE2e('agentOnToolExecutionEndE2e'),
|
||||
[]
|
||||
);
|
||||
const rv = await run.returnValue;
|
||||
// GAP: when implemented, should be ['constructor', 'method']
|
||||
expect(rv.calls).toEqual([]);
|
||||
// GAP: capturedEvent should have tool result data
|
||||
expect(rv.capturedEvent).toBeNull();
|
||||
expect(rv.calls).toEqual(['constructor', 'method']);
|
||||
expect(rv.capturedEvent).toMatchObject({
|
||||
toolName: 'addNumbers',
|
||||
success: true,
|
||||
output: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareCall (GAP)', () => {
|
||||
it('completes but prepareCall is not applied (GAP)', async () => {
|
||||
describe('prepareCall', () => {
|
||||
it('applies prepareCall before streaming', async () => {
|
||||
const run = await start(await agentE2e('agentPrepareCallE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
expect(rv.stepCount).toBe(1);
|
||||
expect(rv.prepareCallCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,7 +294,7 @@ describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
// ==========================================================================
|
||||
|
||||
describe('multimodal tool results', () => {
|
||||
it('passes through LanguageModelV3ToolResultOutput from tools', async () => {
|
||||
it('passes through LanguageModelV4ToolResultOutput from tools', async () => {
|
||||
const run = await start(
|
||||
await agentE2e('agentMultimodalToolResultE2e'),
|
||||
[]
|
||||
@@ -306,22 +306,17 @@ describe.skipIf(isCanary || isUnsupportedApp)(
|
||||
});
|
||||
|
||||
// ==========================================================================
|
||||
// GAP tests
|
||||
// Tool approval
|
||||
// ==========================================================================
|
||||
|
||||
describe('tool approval (GAP)', () => {
|
||||
it('completes but needsApproval is not checked (GAP)', async () => {
|
||||
describe('tool approval', () => {
|
||||
it('pauses before executing a tool that needs approval', async () => {
|
||||
const run = await start(await agentE2e('agentToolApprovalE2e'), []);
|
||||
const rv = await run.returnValue;
|
||||
// GAP: when tool approval is implemented, the agent should pause
|
||||
// with toolCallsCount=1 and toolResultsCount=0 (awaiting approval).
|
||||
// Currently needsApproval is ignored, so the tool executes immediately.
|
||||
// The workflow completes with both tool call and result.
|
||||
expect(rv.stepCount).toBe(2);
|
||||
// When implemented, these should be:
|
||||
// expect(rv.toolCallsCount).toBe(1);
|
||||
// expect(rv.toolResultsCount).toBe(0);
|
||||
// expect(rv.firstToolCallName).toBe('riskyTool');
|
||||
expect(rv.stepCount).toBe(1);
|
||||
expect(rv.toolCallsCount).toBe(1);
|
||||
expect(rv.toolResultsCount).toBe(0);
|
||||
expect(rv.firstToolCallName).toBe('riskyTool');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@ import {
|
||||
WORKFLOW_OPTIONAL_WS_NATIVE_MODULES,
|
||||
} from '@workflow/builders';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { transformMock } = vi.hoisted(() => ({
|
||||
transformMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@swc/core', () => ({
|
||||
transform: transformMock,
|
||||
}));
|
||||
|
||||
import { workflowTransformPlugin } from './index.js';
|
||||
|
||||
/**
|
||||
@@ -132,3 +141,29 @@ describe('workflowTransformPlugin resolveId — ws optional native accelerators'
|
||||
await expect(resolveId('ws')).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflowTransformPlugin transform', () => {
|
||||
it('does not implicitly load input source maps', async () => {
|
||||
transformMock.mockResolvedValueOnce({ code: 'compiled', map: null });
|
||||
|
||||
const plugin = workflowTransformPlugin();
|
||||
const transform = plugin.transform;
|
||||
if (typeof transform !== 'function') {
|
||||
throw new Error('expected transform to be a function');
|
||||
}
|
||||
|
||||
const source = `export async function example() {
|
||||
"use step";
|
||||
}`;
|
||||
await transform.call({} as never, source, '/project/workflows/example.ts');
|
||||
|
||||
expect(transformMock).toHaveBeenCalledWith(
|
||||
source,
|
||||
expect.objectContaining({
|
||||
inputSourceMap: false,
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,6 +169,9 @@ export function workflowTransformPlugin(
|
||||
},
|
||||
},
|
||||
minify: false,
|
||||
// Rollup composes transform maps itself. Do not let SWC separately
|
||||
// resolve sourceMappingURL comments against this normalized module id.
|
||||
inputSourceMap: false,
|
||||
sourceMaps: true,
|
||||
inlineSourcesContent: true,
|
||||
});
|
||||
|
||||
@@ -390,11 +390,11 @@ export class Counter {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
})(Counter, "class//./input//Counter");
|
||||
```
|
||||
|
||||
Note: Instance methods use `#` in the step ID (e.g., `Counter#add`) and are registered via `ClassName.prototype["methodName"]`.
|
||||
Note: Instance methods use `#` in the step ID (e.g., `Counter#add`) and are registered via `ClassName.prototype["methodName"]`. The `hasOwnProperty` guard before `classId` is defined is explained under [Custom serialization](#custom-serialization).
|
||||
|
||||
### Module-level directive
|
||||
|
||||
@@ -568,7 +568,7 @@ export class MyService {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
})(MyService, "class//./input//MyService");
|
||||
```
|
||||
|
||||
@@ -581,7 +581,7 @@ MyService.process = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
})(MyService, "class//./input//MyService");
|
||||
```
|
||||
|
||||
@@ -651,12 +651,14 @@ export class Point {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false });
|
||||
})(Point, "class//./input//Point");
|
||||
```
|
||||
|
||||
The registration is **inlined as a self-contained IIFE** that uses `Symbol.for("workflow-class-registry")` on `globalThis`. This approach works for third-party packages that don't depend on the `workflow` package directly and requires no module imports.
|
||||
|
||||
The `Object.defineProperty` call is guarded by `hasOwnProperty`, making it idempotent: `classId` is defined non-configurable, so visiting the same class a second time would otherwise throw `Cannot redefine property: classId` and crash the bundle at module load. This is not merely defensive: some bundler pipelines legitimately re-run this transform over its own output for the same module. Observed case: a Vite/Nitro SSR build reaching a dependency (`@ai-sdk/gateway`, which ships its own `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE` methods) through more than one build stage, where the second stage's input is the first stage's already-registered output. On a named class expression (`var Foo = class _Foo {}`) the second pass also can't recover the binding name `Foo` — the class is no longer a bare initializer, it is now the argument of the first pass's registration call — so it falls back to the class expression's own inner name `_Foo` (see [Class names for IDs](#class-names-for-ids)) and would otherwise nest a second, differently-named registration around the first. The registry `.set()` call stays unconditional: a class visited under two different names is registered under both, and either resolves it.
|
||||
|
||||
You can also use imported symbols from `@workflow/serde`:
|
||||
|
||||
```javascript
|
||||
@@ -740,7 +742,7 @@ var FileSystem = function(__wf_cls) {
|
||||
Object.defineProperty(__wf_fn, "name", { value: "readFile", configurable: true });
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//FileSystem", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { value: "class//./input//FileSystem", writable: false, enumerable: false, configurable: false });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { value: "class//./input//FileSystem", writable: false, enumerable: false, configurable: false });
|
||||
return __wf_cls;
|
||||
}(class FileSystem {
|
||||
constructor(sandbox) { this.sandbox = sandbox; }
|
||||
@@ -755,7 +757,7 @@ var FileSystem = function(__wf_cls) {
|
||||
__wf_cls.prototype["readFile"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//FileSystem#readFile");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//FileSystem", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", { /* ... */ });
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", { /* ... */ });
|
||||
return __wf_cls;
|
||||
}(class FileSystem {
|
||||
constructor(sandbox) { this.sandbox = sandbox; }
|
||||
|
||||
@@ -3662,13 +3662,55 @@ impl StepTransform {
|
||||
///
|
||||
/// ```js
|
||||
/// __wf_reg.set(<class_id>, <cls>);
|
||||
/// if (!Object.prototype.hasOwnProperty.call(<cls>, "classId")) {
|
||||
/// Object.defineProperty(<cls>, "classId", { value: <class_id>, writable: false, enumerable: false, configurable: false });
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `hasOwnProperty` guard makes the `defineProperty` call idempotent.
|
||||
/// Some multi-pass bundler pipelines (observed with a Vite/Nitro SSR
|
||||
/// build re-running this transform over its own already-registered
|
||||
/// output for a dependency reached through more than one build stage)
|
||||
/// visit the same class object twice; since `defineProperty` marks
|
||||
/// `classId` non-configurable, a second, unguarded call would throw
|
||||
/// "Cannot redefine property: classId" and crash the bundle at module
|
||||
/// load. The registry `.set()` above stays unconditional: it is what
|
||||
/// makes {@link aliasSerializationClass}-style dual lookups work, and a
|
||||
/// repeated `.set()` under the same or a different id is harmless.
|
||||
fn class_registration_stmts(reg_var: &str, cls_ref: &Expr, class_id: &Expr) -> Vec<Stmt> {
|
||||
let boxed = |expr: &Expr| Box::new(expr.clone());
|
||||
let has_own_class_id = Expr::Call(CallExpr {
|
||||
span: DUMMY_SP,
|
||||
ctxt: SyntaxContext::empty(),
|
||||
callee: Callee::Expr(Self::member(
|
||||
Self::member(
|
||||
Self::member(Self::ident_expr("Object"), "prototype"),
|
||||
"hasOwnProperty",
|
||||
),
|
||||
"call",
|
||||
)),
|
||||
args: vec![
|
||||
ExprOrSpread {
|
||||
spread: None,
|
||||
expr: boxed(cls_ref),
|
||||
},
|
||||
ExprOrSpread {
|
||||
spread: None,
|
||||
expr: Self::str_lit("classId"),
|
||||
},
|
||||
],
|
||||
type_args: None,
|
||||
});
|
||||
vec![
|
||||
Self::registry_set_stmt(reg_var, boxed(class_id), boxed(cls_ref)),
|
||||
Self::define_property_stmt(
|
||||
Stmt::If(IfStmt {
|
||||
span: DUMMY_SP,
|
||||
test: Box::new(Expr::Unary(UnaryExpr {
|
||||
span: DUMMY_SP,
|
||||
op: UnaryOp::Bang,
|
||||
arg: Box::new(has_own_class_id),
|
||||
})),
|
||||
cons: Box::new(Self::define_property_stmt(
|
||||
boxed(cls_ref),
|
||||
"classId",
|
||||
vec![
|
||||
@@ -3677,7 +3719,9 @@ impl StepTransform {
|
||||
("enumerable", Self::bool_lit(false)),
|
||||
("configurable", Self::bool_lit(false)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
alt: None,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -105,3 +105,97 @@ fn inferred_class_names_step_mode() {
|
||||
fn inferred_class_names_workflow_mode() {
|
||||
assert_class_names(TransformMode::Workflow);
|
||||
}
|
||||
|
||||
/// Running the transform a second time over its own output must not crash,
|
||||
/// even though `classId` is defined non-configurable.
|
||||
///
|
||||
/// Regression test for a real npm package (`@ai-sdk/gateway`'s
|
||||
/// `GatewayLanguageModel`) that pairs a named class expression with a
|
||||
/// self-reference through its *inner* name:
|
||||
/// `var Foo = class _Foo { static [WORKFLOW_DESERIALIZE]() { return new
|
||||
/// _Foo(); } }`. A bundler pipeline that re-runs this transform over its own
|
||||
/// output for the same module (observed with a Vite/Nitro SSR build, where a
|
||||
/// dependency is reached through more than one build stage) transforms an
|
||||
/// already-wrapped class a second time. The first pass resolves the class's
|
||||
/// name from its *binding* (`Foo`), matching `class-expression-binding-name`;
|
||||
/// on the second pass the class is no longer a bare initializer (it is now
|
||||
/// wrapped in the first pass's registration IIFE), so the binding name is
|
||||
/// unavailable and the second pass falls back to the class expression's own
|
||||
/// inner name (`_Foo`) instead, nesting a second `Object.defineProperty`
|
||||
/// call for `classId` inside the first. Before the fix in
|
||||
/// `class_registration_stmts`, that second, unguarded call threw "Cannot
|
||||
/// redefine property: classId" at module load, crashing the bundle.
|
||||
#[test]
|
||||
fn repeated_transform_of_named_class_expression_does_not_crash() {
|
||||
let source = r#"
|
||||
var GatewayLanguageModel = class _GatewayLanguageModel {
|
||||
constructor(modelId) {
|
||||
this.modelId = modelId;
|
||||
}
|
||||
static [Symbol.for('workflow-serialize')](model) {
|
||||
return { modelId: model.modelId };
|
||||
}
|
||||
static [Symbol.for('workflow-deserialize')](options) {
|
||||
return new _GatewayLanguageModel(options.modelId);
|
||||
}
|
||||
};
|
||||
|
||||
export { GatewayLanguageModel };
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
let transform_once = |source: &str| {
|
||||
Tester::run(|tester| {
|
||||
let program = tester.apply_transform(
|
||||
visit_mut_pass(StepTransform::new(
|
||||
TransformMode::Step,
|
||||
"input.js".into(),
|
||||
None,
|
||||
)),
|
||||
"input.js",
|
||||
Default::default(),
|
||||
Some(true),
|
||||
source,
|
||||
)?;
|
||||
Ok(tester.print(&program, &tester.comments.clone()))
|
||||
})
|
||||
};
|
||||
|
||||
let pass1 = transform_once(&source);
|
||||
// The bug only manifests on a second pass over already-transformed code:
|
||||
// confirm pass 1 alone is unaffected before layering pass 2 on top of it.
|
||||
let pass1_check = format!(
|
||||
r#"{pass1}
|
||||
import assert from 'node:assert/strict';
|
||||
assert.equal(typeof GatewayLanguageModel.classId, 'string');
|
||||
"#
|
||||
);
|
||||
let output = Command::new("node")
|
||||
.args(["--input-type=module", "--eval", &pass1_check])
|
||||
.output()
|
||||
.expect("Node.js is required for class-name runtime tests");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"pass 1 alone: {}\n{pass1_check}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let pass2 = transform_once(&pass1);
|
||||
let pass2_check = format!(
|
||||
r#"{pass2}
|
||||
import assert from 'node:assert/strict';
|
||||
const registry = globalThis[Symbol.for('workflow-class-registry')];
|
||||
assert.equal(typeof GatewayLanguageModel.classId, 'string');
|
||||
assert.equal(registry.get(GatewayLanguageModel.classId), GatewayLanguageModel);
|
||||
"#
|
||||
);
|
||||
let output = Command::new("node")
|
||||
.args(["--input-type=module", "--eval", &pass2_check])
|
||||
.output()
|
||||
.expect("Node.js is required for class-name runtime tests");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"pass 2 (re-transforming pass 1's output): {}\n{pass2_check}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class TestClass {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Object.defineProperty(TestClass.prototype, "value", {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
@@ -35,7 +35,7 @@ export class TestClass {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ TestClass.prototype["instanceMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export class Top {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ Top.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./inpu
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+6
-6
@@ -24,7 +24,7 @@ registerPlugin(function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass1", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass1",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -49,7 +49,7 @@ export const handlers = [
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass2", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass2",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -74,7 +74,7 @@ export const Worker = process.env.FAST ? function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass3", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass3",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -93,7 +93,7 @@ const registry = new Map([
|
||||
function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass4", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass4",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -134,7 +134,7 @@ useModel(function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass6$1", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass6$1",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -158,7 +158,7 @@ registerPlugin(function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//NamedPlugin",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+6
-6
@@ -17,7 +17,7 @@ registerPlugin(function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass1#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass1", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass1",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -32,7 +32,7 @@ export const handlers = [
|
||||
__wf_cls.execute = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass2.execute");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass2", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass2",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -54,7 +54,7 @@ export const Worker = process.env.FAST ? function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass3", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass3",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -70,7 +70,7 @@ const registry = new Map([
|
||||
function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass4", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass4",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -105,7 +105,7 @@ useModel(function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//AnonymousClass6$1#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//AnonymousClass6$1", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//AnonymousClass6$1",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -119,7 +119,7 @@ registerPlugin(function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//NamedPlugin#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//NamedPlugin", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//NamedPlugin",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ var LanguageModel = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//LanguageModel", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//LanguageModel",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ var LanguageModel = function(__wf_cls) {
|
||||
__wf_cls.prototype["doStream"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//LanguageModel#doStream");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//LanguageModel", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//LanguageModel",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
|
||||
var Bash = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Bash", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Bash",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -30,7 +30,7 @@ var Bash = function(__wf_cls) {
|
||||
var Shell = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Shell", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Shell",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
|
||||
var Bash = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Bash", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Bash",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -30,7 +30,7 @@ var Bash = function(__wf_cls) {
|
||||
var Shell = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Shell", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Shell",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+11
-11
@@ -17,7 +17,7 @@ var FileSystem = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//FileSystem", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//FileSystem",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -44,7 +44,7 @@ var Alpha = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Alpha", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Alpha",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -66,7 +66,7 @@ var Alpha = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Beta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Beta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -91,7 +91,7 @@ Gamma = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Gamma", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Gamma",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -115,7 +115,7 @@ var Delta = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Delta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Delta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -139,7 +139,7 @@ var Epsilon = exports.Epsilon = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Epsilon", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Epsilon",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -169,7 +169,7 @@ exports.Zeta = function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Zeta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Zeta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -198,7 +198,7 @@ export const handlers = {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Job", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Job",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -225,7 +225,7 @@ export const handlers = {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//kebab-job", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//kebab-job",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -243,7 +243,7 @@ export const handlers = {
|
||||
const Unreferenced = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Unreferenced", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Unreferenced",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -274,7 +274,7 @@ registerPlugin(function(__wf_cls) {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Plugin", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Plugin",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+11
-11
@@ -10,7 +10,7 @@ var FileSystem = function(__wf_cls) {
|
||||
__wf_cls.prototype["readFile"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//FileSystem#readFile");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//FileSystem", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//FileSystem",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -27,7 +27,7 @@ var Alpha = function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Alpha#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Alpha", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Alpha",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -39,7 +39,7 @@ var Alpha = function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Beta#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Beta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Beta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -54,7 +54,7 @@ Gamma = function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Gamma#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Gamma", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Gamma",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -68,7 +68,7 @@ var Delta = function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Delta#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Delta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Delta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -82,7 +82,7 @@ var Epsilon = exports.Epsilon = function(__wf_cls) {
|
||||
__wf_cls.make = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Epsilon.make");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Epsilon", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Epsilon",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -99,7 +99,7 @@ exports.Zeta = function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Zeta#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Zeta", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Zeta",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -118,7 +118,7 @@ export const handlers = {
|
||||
__wf_cls.execute = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Job.execute");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Job", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Job",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -142,7 +142,7 @@ export const handlers = {
|
||||
});
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//kebab-job", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//kebab-job",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -157,7 +157,7 @@ export const handlers = {
|
||||
const Unreferenced = function(__wf_cls) {
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Unreferenced", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Unreferenced",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -181,7 +181,7 @@ registerPlugin(function(__wf_cls) {
|
||||
__wf_cls.prototype["run"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//Plugin#run");
|
||||
var __wf_cls_sym = Symbol.for("workflow-class-registry"), __wf_cls_reg = globalThis[__wf_cls_sym] || (globalThis[__wf_cls_sym] = new Map());
|
||||
__wf_cls_reg.set("class//./input//Plugin", __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: "class//./input//Plugin",
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ export class Color {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -51,7 +51,7 @@ export class Color {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ export class Color {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -51,7 +51,7 @@ export class Color {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -67,7 +67,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -77,7 +77,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+3
-3
@@ -57,7 +57,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -67,7 +67,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
@@ -77,7 +77,7 @@ export class Triangle {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ exports.Sandbox = Sandbox;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ exports.Sandbox = Sandbox;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ exports.Sandbox = Sandbox;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ exports.Sandbox = Sandbox;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class OnlySerialize {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class OnlySerialize {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export default __DefaultClass;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ __DefaultClass.prototype["validate"] = globalThis[Symbol.for("WORKFLOW_USE_STEP"
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export default __DefaultClass;
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ __DefaultClass.prototype["process"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export default class MyService {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ MyService.prototype["handle"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("ste
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export class DataProcessor {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ Object.defineProperty(DataProcessor.prototype, "result", {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ export class Service {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Service.prototype["process"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export class Calculator {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ Calculator.prototype["add"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step/
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ export class Edge {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export class Edge {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export class Counter {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ export class Counter {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export class ReadFileTool {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export class ReadFileTool {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ export class Run {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ Object.defineProperty(Run.prototype, "value", {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export class Run {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ Object.defineProperty(Run.prototype, "value", {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ export class Config {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Object.defineProperty(Config, "timeout", {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export class MyService {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ MyService.transform = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ class TestClass extends BaseClass {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ TestClass.prototype["stepMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")](
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
@@ -65,7 +65,7 @@ export let syncFnExprLet = function transform(input) {
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Service.fetchData = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//./input//
|
||||
(function(__wf_cls, __wf_id) {
|
||||
var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map());
|
||||
__wf_reg.set(__wf_id, __wf_cls);
|
||||
Object.defineProperty(__wf_cls, "classId", {
|
||||
if (!Object.prototype.hasOwnProperty.call(__wf_cls, "classId")) Object.defineProperty(__wf_cls, "classId", {
|
||||
value: __wf_id,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
|
||||
Generated
+202
-143
@@ -5,6 +5,16 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
catalogs:
|
||||
ai-sdk-v7:
|
||||
'@ai-sdk/react':
|
||||
specifier: 4.0.88
|
||||
version: 4.0.88
|
||||
'@ai-sdk/workflow':
|
||||
specifier: 2.0.15
|
||||
version: 2.0.15
|
||||
ai:
|
||||
specifier: 7.0.85
|
||||
version: 7.0.85
|
||||
default:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.4.4
|
||||
@@ -1762,21 +1772,21 @@ importers:
|
||||
|
||||
workbench/astro:
|
||||
dependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@astrojs/node':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2(astro@7.0.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.8.0(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.20.0))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.20.0)(postgres@3.4.8)))(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(rollup@4.62.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))(supports-color@8.1.1)
|
||||
'@astrojs/vercel':
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.2(68d422e2e77ccaf95f993b3f9644ee1e)
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
astro:
|
||||
specifier: ^7.0.6
|
||||
version: 7.0.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@netlify/blobs@9.1.2)(@types/node@24.6.2)(@vercel/blob@2.0.0)(@vercel/functions@3.8.0(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.20.0))(db0@0.3.4(better-sqlite3@11.10.0)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.20.0)(postgres@3.4.8)))(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(rollup@4.62.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0)
|
||||
@@ -1795,6 +1805,9 @@ importers:
|
||||
|
||||
workbench/example:
|
||||
dependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@opentelemetry/api':
|
||||
specifier: 1.9.1
|
||||
version: 1.9.1
|
||||
@@ -1804,12 +1817,9 @@ importers:
|
||||
'@vercel/otel':
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0(@opentelemetry/api-logs@0.57.2)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -1851,18 +1861,18 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.260610-beta(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.8.0(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.20.0))(@vercel/queue@0.3.1)(better-sqlite3@11.10.0)(chokidar@5.0.0)(dotenv@17.3.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.20.0)(postgres@3.4.8))(giget@3.2.0)(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(lru-cache@11.5.1)(rollup@4.62.2)(vite@8.1.3(@types/node@24.6.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))
|
||||
devDependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@types/express':
|
||||
specifier: ^5.0.6
|
||||
version: 5.0.6
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -1885,18 +1895,18 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.260610-beta(@netlify/blobs@9.1.2)(@vercel/blob@2.0.0)(@vercel/functions@3.8.0(@aws-sdk/credential-provider-web-identity@3.972.49)(ws@8.20.0))(@vercel/queue@0.3.1)(better-sqlite3@11.10.0)(chokidar@5.0.0)(dotenv@17.3.1)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(better-sqlite3@11.10.0)(pg@8.20.0)(postgres@3.4.8))(giget@3.2.0)(ioredis@5.10.1(supports-color@10.2.2))(jiti@2.7.0)(lru-cache@11.5.1)(rollup@4.62.2)(vite@8.1.3(@types/node@22.19.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))
|
||||
devDependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 22.19.0
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -1915,15 +1925,15 @@ importers:
|
||||
|
||||
workbench/hono:
|
||||
devDependencies:
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
hono:
|
||||
specifier: ^4.12.27
|
||||
version: 4.12.28
|
||||
@@ -1945,6 +1955,9 @@ importers:
|
||||
|
||||
workbench/nest:
|
||||
dependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@nestjs/common':
|
||||
specifier: ^11.1.27
|
||||
version: 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)
|
||||
@@ -1954,9 +1967,6 @@ importers:
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^11.1.27
|
||||
version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.1.27)(supports-color@10.2.2)
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/nest':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/nest
|
||||
@@ -1964,8 +1974,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
express:
|
||||
specifier: ^5.2.1
|
||||
version: 5.2.1(supports-color@10.2.2)
|
||||
@@ -2010,8 +2020,11 @@ importers:
|
||||
workbench/nextjs-turbopack:
|
||||
dependencies:
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.76
|
||||
version: 2.0.76(react@19.2.7)(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 4.0.88(react@19.2.7)(zod@4.5.4)
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@node-rs/xxhash':
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
@@ -2039,12 +2052,9 @@ importers:
|
||||
'@vercel/otel':
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0(@opentelemetry/api-logs@0.57.2)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -2134,8 +2144,11 @@ importers:
|
||||
workbench/nextjs-webpack:
|
||||
dependencies:
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.76
|
||||
version: 2.0.76(react@19.2.7)(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 4.0.88(react@19.2.7)(zod@4.5.4)
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@node-rs/xxhash':
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
@@ -2163,12 +2176,9 @@ importers:
|
||||
'@vercel/otel':
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0(@opentelemetry/api-logs@0.57.2)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -2257,6 +2267,9 @@ importers:
|
||||
|
||||
workbench/nitro-v2:
|
||||
devDependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 22.19.0
|
||||
@@ -2264,8 +2277,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
h3:
|
||||
specifier: ^1.15.5
|
||||
version: 1.15.11
|
||||
@@ -2291,15 +2304,15 @@ importers:
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
devDependencies:
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -2325,18 +2338,18 @@ importers:
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
devDependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 22.19.0
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
h3:
|
||||
specifier: ^1.15.5
|
||||
version: 1.15.11
|
||||
@@ -2385,9 +2398,9 @@ importers:
|
||||
|
||||
workbench/sveltekit:
|
||||
dependencies:
|
||||
'@ai-sdk/react':
|
||||
specifier: 2.0.76
|
||||
version: 2.0.76(react@19.2.7)(zod@4.5.4)
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@node-rs/xxhash':
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
@@ -2406,12 +2419,9 @@ importers:
|
||||
'@vercel/otel':
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0(@opentelemetry/api-logs@0.57.2)(@opentelemetry/api@1.9.1)(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)(supports-color@10.2.2))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
exsolve:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7
|
||||
@@ -2556,6 +2566,9 @@ importers:
|
||||
specifier: 19.2.7
|
||||
version: 19.2.7(react@19.2.7)
|
||||
devDependencies:
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@types/react':
|
||||
specifier: 19.1.13
|
||||
version: 19.1.13
|
||||
@@ -2565,15 +2578,12 @@ importers:
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^5.1.2
|
||||
version: 5.2.0(supports-color@10.2.2)(vite@7.3.6(@types/node@24.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.9.0))
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -2599,15 +2609,15 @@ importers:
|
||||
specifier: 1.7.6
|
||||
version: 1.7.6
|
||||
devDependencies:
|
||||
'@workflow/ai':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ai
|
||||
'@ai-sdk/workflow':
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 2.0.15(workflow@packages+workflow)(zod@4.5.4)
|
||||
'@workflow/world-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/world-postgres
|
||||
ai:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.116(zod@4.5.4)
|
||||
specifier: catalog:ai-sdk-v7
|
||||
version: 7.0.85(zod@4.5.4)
|
||||
lodash.chunk:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -2656,12 +2666,6 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@2.0.0':
|
||||
resolution: {integrity: sha512-Gj0PuawK7NkZuyYgO/h5kDK/l6hFOjhLdTq3/Lli1FTl47iGmwhH1IZQpAL3Z09BeFYWakcwUmn02ovIm2wy9g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@3.0.143':
|
||||
resolution: {integrity: sha512-RCH60KsUaNiZkI/fBuyau4yvYrVBIEgAcN+Ain94QpL1kVm28GduQzFKfGffAiJU2We0ZrmN4BHkoCZzACK96Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2674,12 +2678,24 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/gateway@4.0.69':
|
||||
resolution: {integrity: sha512-W5MMdyqsaziQy/A4kxlK74iEQ+NuO6OaszH32cEQpUgBW0o15S2fAdP0aYSH2/5lrVZSMXLQLCzuMkRGHBua3A==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/google@3.0.43':
|
||||
resolution: {integrity: sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/mcp@2.0.41':
|
||||
resolution: {integrity: sha512-1u3HJYmgehXl1cz7ipi3NO8HrT6SdozZ7DNEh9MXdQE7knAnq70yhD8DIqfRdk/iZ3Net7QRmECUC+R5ISG0SQ==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/openai-compatible@2.0.35':
|
||||
resolution: {integrity: sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2692,12 +2708,6 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.12':
|
||||
resolution: {integrity: sha512-ZtbdvYxdMoria+2SlNarEk6Hlgyf+zzcznlD55EAl+7VZvJaSg2sqPvwArY7L6TfDEDJsnCq0fdhBSkYo0Xqdg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.19':
|
||||
resolution: {integrity: sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2710,9 +2720,11 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
|
||||
engines: {node: '>=18'}
|
||||
'@ai-sdk/provider-utils@5.0.34':
|
||||
resolution: {integrity: sha512-tRBdgRcys/4d8wyQdOdyYScq1AxfMdMd0hIlwolxJKVIbBwXUgClZuQT0VIsz4e7pylY8FE6utYCCZ494UAMJQ==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/provider@3.0.13':
|
||||
resolution: {integrity: sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw==}
|
||||
@@ -2722,15 +2734,9 @@ packages:
|
||||
resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@2.0.76':
|
||||
resolution: {integrity: sha512-ggAPzyaKJTqUWigpxMzI5DuC0Y3iEpDUPCgz6/6CpnKZY/iok+x5xiZhDemeaP0ILw5IQekV0kdgBR8JPgI8zQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
'@ai-sdk/provider@4.0.9':
|
||||
resolution: {integrity: sha512-XnGXPWiBIfqjsVEud5pOaVneRByJQOu2sYNwlSVJTPCvakdCDkVuYKKfNuStkIpMUYl7JIkBZGBx+B5YfNeVjA==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
'@ai-sdk/react@3.0.221':
|
||||
resolution: {integrity: sha512-5Zd+rF0YbjOqre0NEzdNE+FvNPrEuqfHm4KGxw9ovbAHv0ut/7sgYIn2kzr6ekvXHNqWz0HEmi+zYOXOZCJxJw==}
|
||||
@@ -2738,6 +2744,19 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
|
||||
|
||||
'@ai-sdk/react@4.0.88':
|
||||
resolution: {integrity: sha512-lDE1hHAVWiOSLsZRzyMicKR2DziET0dQJfROS/By39w5yQXU9EPYN7+HIbx24gaDTPSFzkw8ax3O4/cVIc++cA==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
|
||||
|
||||
'@ai-sdk/workflow@2.0.15':
|
||||
resolution: {integrity: sha512-Ge/BASKp1UxdX7c7aqLbRRutT25mtHTddhvBiiEn7vWYdxnrX0z1fDJDl2r8Np6Ub3G+RqlBcAGhUpeNX24aig==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
workflow: ^5.0.0-beta.42
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
'@ai-sdk/xai@3.0.67':
|
||||
resolution: {integrity: sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10242,10 +10261,6 @@ packages:
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
'@vercel/oidc@3.0.3':
|
||||
resolution: {integrity: sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@vercel/oidc@3.1.0':
|
||||
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -10547,6 +10562,9 @@ packages:
|
||||
resolution: {integrity: sha512-ueFCcIPaMgtuYDS9u0qlUoEvj6GiSsKrwnOLPp9SshqjtcRaR1IEHRjoReq3sXNydsF5i0ZnmuYgXq9dV53t0g==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@workflow/serde@4.1.0':
|
||||
resolution: {integrity: sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==}
|
||||
|
||||
'@xhmikosr/archive-type@7.1.0':
|
||||
resolution: {integrity: sha512-xZEpnGplg1sNPyEgFh0zbHxqlw5dtYg6viplmWSxUj12+QjU9SKu3U/2G73a15pEjLaOqTefNSZ1fOPUOT4Xgg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10687,12 +10705,6 @@ packages:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ai@5.0.76:
|
||||
resolution: {integrity: sha512-ZCxi1vrpyCUnDbtYrO/W8GLvyacV9689f00yshTIQ3mFFphbD7eIv40a2AOZBv3GGRA7SSRYIDnr56wcS/gyQg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ai@6.0.116:
|
||||
resolution: {integrity: sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -10705,6 +10717,12 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ai@7.0.85:
|
||||
resolution: {integrity: sha512-HVtPz0qLbTUad+QBnWWReIUmwk+U4PcRENMx+9PsHGVoinoc5CLDiVjNR+VBXTKOSaNgce8kSU/Rtbb4kjZsSw==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
zod: ^3.25.76 || ^4.1.8
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -10737,6 +10755,9 @@ packages:
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
ajv@8.20.0:
|
||||
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
|
||||
|
||||
ajv@8.6.3:
|
||||
resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==}
|
||||
|
||||
@@ -15001,6 +15022,10 @@ packages:
|
||||
piscina@4.9.2:
|
||||
resolution: {integrity: sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==}
|
||||
|
||||
pkce-challenge@5.0.1:
|
||||
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@@ -16518,6 +16543,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
swr@2.5.1:
|
||||
resolution: {integrity: sha512-BRw55e8r0B7SpDN20CAzoQAHl7y1yP7/Zt7oqUjMv0vSt2u2Xnkm88Ws+VypbV9BXHQVuSuyVq7zMjO16wSExw==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
symbol-observable@4.0.0:
|
||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -17941,13 +17971,6 @@ snapshots:
|
||||
zod: 4.5.4
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/gateway@2.0.0(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@4.5.4)
|
||||
'@vercel/oidc': 3.0.3
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/gateway@3.0.143(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.13
|
||||
@@ -17962,6 +17985,13 @@ snapshots:
|
||||
'@vercel/oidc': 3.1.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/gateway@4.0.69(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
|
||||
'@vercel/oidc': 3.2.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/google@3.0.43(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
@@ -17969,6 +17999,14 @@ snapshots:
|
||||
zod: 4.5.4
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/mcp@2.0.41(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
|
||||
cross-spawn: 7.0.6
|
||||
pkce-challenge: 5.0.1
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/openai-compatible@2.0.35(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
@@ -17983,13 +18021,6 @@ snapshots:
|
||||
zod: 4.5.4
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider-utils@3.0.12(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@standard-schema/spec': 1.0.0
|
||||
eventsource-parser: 3.0.6
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider-utils@4.0.19(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 3.0.8
|
||||
@@ -18004,9 +18035,14 @@ snapshots:
|
||||
eventsource-parser: 3.1.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider@2.0.0':
|
||||
'@ai-sdk/provider-utils@5.0.34(zod@4.5.4)':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@workflow/serde': 4.1.0
|
||||
eventsource-parser: 3.1.0
|
||||
undici: 7.29.0
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/provider@3.0.13':
|
||||
dependencies:
|
||||
@@ -18016,15 +18052,9 @@ snapshots:
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@2.0.76(react@19.2.7)(zod@4.5.4)':
|
||||
'@ai-sdk/provider@4.0.9':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@4.5.4)
|
||||
ai: 5.0.76(zod@4.5.4)
|
||||
react: 19.2.7
|
||||
swr: 2.3.6(react@19.2.7)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 4.5.4
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@3.0.221(react@19.2.4)(zod@4.5.4)':
|
||||
dependencies:
|
||||
@@ -18036,6 +18066,27 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@ai-sdk/react@4.0.88(react@19.2.7)(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/mcp': 2.0.41(zod@4.5.4)
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
|
||||
ai: 7.0.85(zod@4.5.4)
|
||||
react: 19.2.7
|
||||
swr: 2.5.1(react@19.2.7)
|
||||
throttleit: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@ai-sdk/workflow@2.0.15(workflow@packages+workflow)(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
|
||||
ai: 7.0.85(zod@4.5.4)
|
||||
ajv: 8.20.0
|
||||
workflow: link:packages/workflow
|
||||
zod: 4.5.4
|
||||
|
||||
'@ai-sdk/xai@3.0.67(zod@4.5.4)':
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible': 2.0.35(zod@4.5.4)
|
||||
@@ -27023,8 +27074,6 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@vercel/oidc@3.0.3': {}
|
||||
|
||||
'@vercel/oidc@3.1.0': {}
|
||||
|
||||
'@vercel/oidc@3.2.0': {}
|
||||
@@ -27527,6 +27576,8 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@workflow/serde@4.1.0': {}
|
||||
|
||||
'@xhmikosr/archive-type@7.1.0(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
file-type: 20.5.0(supports-color@10.2.2)
|
||||
@@ -27778,14 +27829,6 @@ snapshots:
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ai@5.0.76(zod@4.5.4):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 2.0.0(zod@4.5.4)
|
||||
'@ai-sdk/provider': 2.0.0
|
||||
'@ai-sdk/provider-utils': 3.0.12(zod@4.5.4)
|
||||
'@opentelemetry/api': 1.9.1
|
||||
zod: 4.5.4
|
||||
|
||||
ai@6.0.116(zod@4.5.4):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 3.0.66(zod@4.5.4)
|
||||
@@ -27802,6 +27845,13 @@ snapshots:
|
||||
'@opentelemetry/api': 1.9.1
|
||||
zod: 4.5.4
|
||||
|
||||
ai@7.0.85(zod@4.5.4):
|
||||
dependencies:
|
||||
'@ai-sdk/gateway': 4.0.69(zod@4.5.4)
|
||||
'@ai-sdk/provider': 4.0.9
|
||||
'@ai-sdk/provider-utils': 5.0.34(zod@4.5.4)
|
||||
zod: 4.5.4
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.18.0):
|
||||
optionalDependencies:
|
||||
ajv: 8.18.0
|
||||
@@ -27833,6 +27883,13 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ajv@8.20.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ajv@8.6.3:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -33622,6 +33679,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@napi-rs/nice': 1.1.1
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@@ -35992,7 +36051,7 @@ snapshots:
|
||||
react: 19.2.4
|
||||
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||
|
||||
swr@2.3.6(react@19.2.7):
|
||||
swr@2.5.1(react@19.2.7):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 19.2.7
|
||||
|
||||
@@ -26,6 +26,12 @@ catalog:
|
||||
vitest: ^4.1.10
|
||||
zod: ~4.5.4
|
||||
|
||||
catalogs:
|
||||
ai-sdk-v7:
|
||||
"@ai-sdk/react": 4.0.88
|
||||
"@ai-sdk/workflow": 2.0.15
|
||||
ai: 7.0.85
|
||||
|
||||
overrides:
|
||||
# `@vercel/queue` accepts any `@vercel/oidc` in the 3.x line, and 3.3+ pulls in
|
||||
# `@vercel/cli-config`, whose `xdg-app-paths` dependency instantiates itself at
|
||||
|
||||
@@ -78,41 +78,61 @@ function toTarballFilename(packageName, version) {
|
||||
return `${normalized}-${version}.tgz`;
|
||||
}
|
||||
|
||||
function parseCatalogEntries(yamlPath) {
|
||||
const catalog = {};
|
||||
function parseCatalogMapping(lines, startIndex, indentation) {
|
||||
const entries = {};
|
||||
const prefix = ' '.repeat(indentation);
|
||||
|
||||
for (const line of lines.slice(startIndex)) {
|
||||
if (!line.trim() || line.trimStart().startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
if (!line.startsWith(prefix) || line.startsWith(`${prefix} `)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const match = line
|
||||
.slice(indentation)
|
||||
.match(/^(?:"([^"]+)"|(\S[^:]*)):\s*(.+)\s*$/u);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
entries[match[1] ?? match[2]] = match[3].trim();
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function parseCatalogEntries(yamlPath) {
|
||||
const lines = fs.readFileSync(yamlPath, 'utf8').split(/\r?\n/u);
|
||||
let inCatalog = false;
|
||||
const defaultCatalogIndex = lines.indexOf('catalog:');
|
||||
const catalogsIndex = lines.indexOf('catalogs:');
|
||||
const catalogs = {
|
||||
default:
|
||||
defaultCatalogIndex === -1
|
||||
? {}
|
||||
: parseCatalogMapping(lines, defaultCatalogIndex + 1, 2),
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
if (!inCatalog) {
|
||||
if (line.trim() === 'catalog:') {
|
||||
inCatalog = true;
|
||||
if (catalogsIndex === -1) {
|
||||
return catalogs;
|
||||
}
|
||||
|
||||
for (let index = catalogsIndex + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (!line.trim() || line.trimStart().startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!line.startsWith(' ')) {
|
||||
break;
|
||||
}
|
||||
|
||||
const match = line.match(/^\s{2}("?[^"]+"?|[^:]+):\s*(.+)\s*$/u);
|
||||
if (!match) {
|
||||
continue;
|
||||
const match = line.match(/^ {2}(?:"([^"]+)"|(\S[^:]*)):\s*$/u);
|
||||
if (match) {
|
||||
catalogs[match[1] ?? match[2]] = parseCatalogMapping(lines, index + 1, 4);
|
||||
}
|
||||
}
|
||||
|
||||
let key = match[1].trim();
|
||||
if (key.startsWith('"') && key.endsWith('"')) {
|
||||
key = key.slice(1, -1);
|
||||
}
|
||||
const value = match[2].trim();
|
||||
catalog[key] = value;
|
||||
}
|
||||
|
||||
return catalog;
|
||||
return catalogs;
|
||||
}
|
||||
|
||||
function collectMonorepoPackages() {
|
||||
@@ -185,10 +205,34 @@ function copyRepoLib(destinationRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function rewriteDependencySpecs(
|
||||
function classifyDependencySpec(
|
||||
dependencyName,
|
||||
spec,
|
||||
tarballPathByPackageName,
|
||||
catalogs
|
||||
) {
|
||||
const tarballPath = tarballPathByPackageName.get(dependencyName);
|
||||
if (tarballPath) {
|
||||
return { kind: 'tarball', spec: `file:${tarballPath}` };
|
||||
}
|
||||
if (typeof spec === 'string' && spec.startsWith('workspace:')) {
|
||||
return { kind: 'unresolved-workspace' };
|
||||
}
|
||||
if (typeof spec !== 'string' || !spec.startsWith('catalog:')) {
|
||||
return { kind: 'unchanged' };
|
||||
}
|
||||
|
||||
const catalogName = spec.slice('catalog:'.length) || 'default';
|
||||
const resolvedVersion = catalogs[catalogName]?.[dependencyName];
|
||||
return resolvedVersion
|
||||
? { kind: 'catalog', spec: resolvedVersion }
|
||||
: { kind: 'unresolved-catalog' };
|
||||
}
|
||||
|
||||
export function rewriteDependencySpecs(
|
||||
packageJsonPath,
|
||||
tarballPathByPackageName,
|
||||
catalog
|
||||
catalogs
|
||||
) {
|
||||
const packageJson = readJson(packageJsonPath);
|
||||
const replacedWithTarballs = [];
|
||||
@@ -203,31 +247,27 @@ function rewriteDependencySpecs(
|
||||
}
|
||||
|
||||
for (const [dependencyName, spec] of Object.entries(dependencies)) {
|
||||
const tarballPath = tarballPathByPackageName.get(dependencyName);
|
||||
if (tarballPath) {
|
||||
dependencies[dependencyName] = `file:${tarballPath}`;
|
||||
const classification = classifyDependencySpec(
|
||||
dependencyName,
|
||||
spec,
|
||||
tarballPathByPackageName,
|
||||
catalogs
|
||||
);
|
||||
switch (classification.kind) {
|
||||
case 'tarball':
|
||||
dependencies[dependencyName] = classification.spec;
|
||||
replacedWithTarballs.push(`${field}.${dependencyName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof spec === 'string' && spec.startsWith('workspace:')) {
|
||||
unresolvedWorkspaceSpecs.push(`${field}.${dependencyName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (spec === 'catalog:') {
|
||||
const resolvedVersion = catalog[dependencyName];
|
||||
if (resolvedVersion) {
|
||||
dependencies[dependencyName] = resolvedVersion;
|
||||
break;
|
||||
case 'catalog':
|
||||
dependencies[dependencyName] = classification.spec;
|
||||
replacedCatalogEntries.push(`${field}.${dependencyName}`);
|
||||
} else {
|
||||
unresolvedCatalogSpecs.push(`${field}.${dependencyName}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof spec === 'string' && spec.startsWith('catalog:')) {
|
||||
break;
|
||||
case 'unresolved-workspace':
|
||||
unresolvedWorkspaceSpecs.push(`${field}.${dependencyName}`);
|
||||
break;
|
||||
case 'unresolved-catalog':
|
||||
unresolvedCatalogSpecs.push(`${field}.${dependencyName}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,13 +394,13 @@ function main() {
|
||||
);
|
||||
}
|
||||
|
||||
const catalog = parseCatalogEntries(workspaceYamlPath);
|
||||
const catalogs = parseCatalogEntries(workspaceYamlPath);
|
||||
const stagedPackageJsonPath = path.join(stagedWorkbenchDir, 'package.json');
|
||||
const { replacedWithTarballs, replacedCatalogEntries } =
|
||||
rewriteDependencySpecs(
|
||||
stagedPackageJsonPath,
|
||||
tarballPathByPackageName,
|
||||
catalog
|
||||
catalogs
|
||||
);
|
||||
const overridesApplied = writeStagedWorkspaceConfig(
|
||||
stagedWorkbenchDir,
|
||||
@@ -384,4 +424,6 @@ function main() {
|
||||
console.log(`Tarballs: ${tarballDir}`);
|
||||
}
|
||||
|
||||
main();
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
|
||||
main();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
parseCatalogEntries,
|
||||
rewriteDependencySpecs,
|
||||
} from './stage-workbench-with-tarballs.mjs';
|
||||
|
||||
const temporaryDirectories = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('staged workbench catalog dependencies', () => {
|
||||
it('resolves default and named catalogs before leaving the workspace', () => {
|
||||
const directory = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'workflow-catalog-test-')
|
||||
);
|
||||
temporaryDirectories.push(directory);
|
||||
|
||||
const workspacePath = path.join(directory, 'pnpm-workspace.yaml');
|
||||
fs.writeFileSync(
|
||||
workspacePath,
|
||||
`packages:
|
||||
- .
|
||||
|
||||
catalog:
|
||||
zod: 4.3.6
|
||||
|
||||
catalogs:
|
||||
ai-sdk-v7:
|
||||
"@ai-sdk/workflow": 2.0.15
|
||||
ai: 7.0.85
|
||||
|
||||
overrides: {}
|
||||
`
|
||||
);
|
||||
|
||||
const packageJsonPath = path.join(directory, 'package.json');
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
dependencies: {
|
||||
'@ai-sdk/workflow': 'catalog:ai-sdk-v7',
|
||||
ai: 'catalog:ai-sdk-v7',
|
||||
zod: 'catalog:',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
);
|
||||
|
||||
const catalogs = parseCatalogEntries(workspacePath);
|
||||
const result = rewriteDependencySpecs(packageJsonPath, new Map(), catalogs);
|
||||
|
||||
expect(JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))).toEqual({
|
||||
dependencies: {
|
||||
'@ai-sdk/workflow': '2.0.15',
|
||||
ai: '7.0.85',
|
||||
zod: '4.3.6',
|
||||
},
|
||||
});
|
||||
expect(result.replacedCatalogEntries).toEqual([
|
||||
'dependencies.@ai-sdk/workflow',
|
||||
'dependencies.ai',
|
||||
'dependencies.zod',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+27
-31
@@ -3,7 +3,7 @@ name: workflow
|
||||
description: Creates durable, resumable workflows using Vercel's Workflow SDK. Use when building workflows that need to survive restarts, pause for external events, retry on failure, or coordinate multi-step operations over time. Triggers on mentions of "workflow", "durable functions", "resumable", "workflow sdk", "queue", "event", "push", "subscribe", or step-based orchestration.
|
||||
metadata:
|
||||
author: Vercel Inc.
|
||||
version: '1.12'
|
||||
version: '1.16'
|
||||
---
|
||||
|
||||
## *Critical*: Always use correct `workflow` documentation
|
||||
@@ -31,7 +31,8 @@ Documentation structure in `node_modules/workflow/docs/`:
|
||||
|
||||
Related packages also include bundled docs:
|
||||
|
||||
- `@workflow/ai`: `node_modules/@workflow/ai/docs/` - DurableAgent and AI integration
|
||||
- `@ai-sdk/workflow`: `node_modules/ai/docs/` - WorkflowAgent and AI SDK integration
|
||||
- `@workflow/ai`: `node_modules/@workflow/ai/docs/` - deprecated DurableAgent APIs for existing applications
|
||||
- `@workflow/core`: `node_modules/@workflow/core/docs/` - Core runtime (foundations, how-it-works)
|
||||
- `@workflow/next`: `node_modules/@workflow/next/docs/` - Next.js integration
|
||||
|
||||
@@ -71,8 +72,8 @@ import { workflow } from "workflow/vite";
|
||||
import { workflow } from "workflow/astro";
|
||||
// Or use modules: ["workflow/nitro"] for Nitro/Nuxt
|
||||
|
||||
// AI agent
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
// AI agent (Workflow 5)
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
```
|
||||
|
||||
## Prefer step functions to avoid sandbox errors
|
||||
@@ -91,7 +92,7 @@ async function processWithAI(data: any) {
|
||||
"use step";
|
||||
// AI SDK works in steps without workarounds
|
||||
return await generateText({
|
||||
model: openai("gpt-4"),
|
||||
model: "spacexai/grok-4.6",
|
||||
prompt: `Process: ${JSON.stringify(data)}`,
|
||||
});
|
||||
}
|
||||
@@ -129,17 +130,17 @@ export async function myWorkflow() {
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `DurableAgent` from `@workflow/ai` handles the fetch assignment automatically.
|
||||
**Note:** Plain `"provider/model"` strings use Vercel AI Gateway. Do not construct a direct provider instance unless the user explicitly needs a provider-only feature.
|
||||
|
||||
## DurableAgent: AI agents in workflows
|
||||
## WorkflowAgent: AI agents in Workflow 5
|
||||
|
||||
Use `DurableAgent` to build AI agents that maintain state and survive interruptions. It handles the workflow sandbox automatically (no manual `globalThis.fetch` needed).
|
||||
Use AI SDK's `WorkflowAgent` for durable agents on Workflow 5. It replaces the deprecated `DurableAgent` API from `@workflow/ai` and checkpoints model calls and step-backed tools.
|
||||
|
||||
```typescript
|
||||
import { DurableAgent } from "@workflow/ai/agent";
|
||||
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
||||
import { isStepCount, tool } from "ai";
|
||||
import { getWritable } from "workflow";
|
||||
import { z } from "zod";
|
||||
import type { UIMessageChunk } from "ai";
|
||||
|
||||
async function lookupData({ query }: { query: string }) {
|
||||
"use step";
|
||||
@@ -150,22 +151,22 @@ async function lookupData({ query }: { query: string }) {
|
||||
export async function myAgentWorkflow(userMessage: string) {
|
||||
"use workflow";
|
||||
|
||||
const agent = new DurableAgent({
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
system: "You are a helpful assistant.",
|
||||
const agent = new WorkflowAgent({
|
||||
model: "spacexai/grok-4.6",
|
||||
instructions: "You are a helpful assistant.",
|
||||
tools: {
|
||||
lookupData: {
|
||||
lookupData: tool({
|
||||
description: "Search for information",
|
||||
inputSchema: z.object({ query: z.string() }),
|
||||
execute: lookupData,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await agent.stream({
|
||||
messages: [{ role: "user", content: userMessage }],
|
||||
writable: getWritable<UIMessageChunk>(),
|
||||
maxSteps: 10,
|
||||
writable: getWritable<ModelCallStreamPart>(),
|
||||
stopWhen: isStepCount(10),
|
||||
});
|
||||
|
||||
return result.messages;
|
||||
@@ -173,17 +174,18 @@ export async function myAgentWorkflow(userMessage: string) {
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `getWritable<UIMessageChunk>()` streams output to the workflow run's default stream
|
||||
- A plain `"provider/model"` string routes through Vercel AI Gateway; `spacexai/grok-4.6` is the default model in Workflow examples
|
||||
- `getWritable<ModelCallStreamPart>()` streams durable model-call output; convert it with `createModelCallToUIChunkTransform()` in an HTTP route
|
||||
- Tool `execute` functions that need Node.js/npm access should use `"use step"`
|
||||
- Tool `execute` functions that use workflow primitives (`sleep()`, `createHook()`) should **NOT** use `"use step"` because they run at the workflow level
|
||||
- `maxSteps` limits the number of LLM calls (default is unlimited)
|
||||
- `stopWhen` limits the number of model calls; the default is to stop when the model stops calling tools
|
||||
- Multi-turn: pass `result.messages` plus new user messages to subsequent `agent.stream()` calls
|
||||
|
||||
**For more details on `DurableAgent`, check the AI docs in `node_modules/@workflow/ai/docs/`.**
|
||||
**For more details, check the WorkflowAgent docs in the installed AI SDK package or at https://ai-sdk.dev/v7/docs/agents/workflow-agent.**
|
||||
|
||||
## Starting workflows & child workflows
|
||||
|
||||
Use `start()` to launch workflows from API routes. **`start()` cannot be called directly in workflow context**, so wrap it in a step function.
|
||||
Use `start()` to launch workflows from API routes. In Workflow 5, `start()` can also be called directly from a workflow function to spawn a child run; it is step-backed and records a deterministic boundary in the parent's event log.
|
||||
|
||||
```typescript
|
||||
import { start } from "workflow/api";
|
||||
@@ -198,26 +200,20 @@ export async function POST() {
|
||||
const run = await start(noArgWorkflow);
|
||||
```
|
||||
|
||||
**Starting child workflows from inside a workflow requires a step:**
|
||||
**Starting child workflows from inside a Workflow 5 workflow:**
|
||||
|
||||
```typescript
|
||||
import { start } from "workflow/api";
|
||||
|
||||
// Wrap start() in a step function
|
||||
async function triggerChild(data: string) {
|
||||
"use step";
|
||||
const run = await start(childWorkflow, [data]);
|
||||
return run.runId;
|
||||
}
|
||||
|
||||
export async function parentWorkflow() {
|
||||
"use workflow";
|
||||
const childRunId = await triggerChild("some data"); // Fire-and-forget via step
|
||||
const childRun = await start(childWorkflow, ["some data"]);
|
||||
await sleep("1h");
|
||||
return { childRunId: childRun.runId };
|
||||
}
|
||||
```
|
||||
|
||||
`start()` returns immediately and doesn't wait for the workflow to complete. Use `run.returnValue` to await completion.
|
||||
`start()` returns after creating the child run and doesn't wait for it to complete. Use `childRun.returnValue` only when the parent should wait for the child; each `Run` property access or method call inside a workflow is a step.
|
||||
|
||||
## Hooks: pause & resume with external events
|
||||
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
"start": "node scripts/start-with-pg.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/workflow": "catalog:ai-sdk-v7",
|
||||
"@astrojs/node": "11.0.2",
|
||||
"@astrojs/vercel": "^11.0.2",
|
||||
"ai": "catalog:",
|
||||
"@workflow/world-postgres": "workspace:*",
|
||||
"ai": "catalog:ai-sdk-v7",
|
||||
"astro": "^7.0.6",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"openai": "6.9.0",
|
||||
"workflow": "workspace:*",
|
||||
"@workflow/ai": "workspace:*",
|
||||
"@workflow/world-postgres": "workspace:*",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
"esbuild": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/workflow": "catalog:ai-sdk-v7",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@vercel/functions": "catalog:",
|
||||
"@vercel/otel": "^1.13.0",
|
||||
"workflow": "workspace:*",
|
||||
"@workflow/ai": "workspace:*",
|
||||
"ai": "catalog:",
|
||||
"ai": "catalog:ai-sdk-v7",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mixpart": "^0.0.4",
|
||||
"openai": "^6",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* E2E test workflows for DurableAgent using @workflow/ai/test mock providers.
|
||||
* E2E test workflows for AI SDK 7's WorkflowAgent.
|
||||
*/
|
||||
import { DurableAgent } from '@workflow/ai/agent';
|
||||
import { mockSequenceModel, mockTextModel } from '@workflow/ai/test';
|
||||
import { WorkflowAgent } from '@ai-sdk/workflow';
|
||||
import type { Tool } from 'ai';
|
||||
import { FatalError, getWritable } from 'workflow';
|
||||
import z from 'zod/v4';
|
||||
import { mockSequenceModel, mockTextModel } from './ai-sdk-test-provider.js';
|
||||
|
||||
// ============================================================================
|
||||
// Tool step functions
|
||||
@@ -25,13 +26,29 @@ async function throwingStep(): Promise<string> {
|
||||
throw new FatalError('Tool execution failed fatally');
|
||||
}
|
||||
|
||||
async function riskyStep(input: { action: string }): Promise<string> {
|
||||
'use step';
|
||||
return input.action;
|
||||
}
|
||||
|
||||
function mockWebSearchTool(args: { maxUses?: number } = {}): Tool {
|
||||
return {
|
||||
type: 'provider',
|
||||
isProviderExecuted: true,
|
||||
id: 'anthropic.web_search',
|
||||
args,
|
||||
inputSchema: z.object({ query: z.string() }),
|
||||
outputSchema: z.unknown(),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Core agent tests
|
||||
// ============================================================================
|
||||
|
||||
export async function agentBasicE2e(prompt: string) {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel(`Echo: ${prompt}`),
|
||||
instructions: 'You are a helpful assistant.',
|
||||
});
|
||||
@@ -47,7 +64,7 @@ export async function agentBasicE2e(prompt: string) {
|
||||
|
||||
export async function agentToolCallE2e(a: number, b: number) {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'tool-call',
|
||||
@@ -78,7 +95,7 @@ export async function agentToolCallE2e(a: number, b: number) {
|
||||
|
||||
export async function agentMultiStepE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'tool-call',
|
||||
@@ -117,7 +134,7 @@ export async function agentMultiStepE2e() {
|
||||
|
||||
export async function agentErrorToolE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{ type: 'tool-call', toolName: 'throwingTool', input: '{}' },
|
||||
{ type: 'text', text: 'Tool failed but I recovered.' },
|
||||
@@ -151,7 +168,7 @@ export async function agentErrorToolE2e() {
|
||||
*/
|
||||
export async function agentProviderToolE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'provider-tool-call',
|
||||
@@ -162,11 +179,7 @@ export async function agentProviderToolE2e() {
|
||||
{ type: 'text', text: 'I found a result for you.' },
|
||||
]),
|
||||
tools: {
|
||||
webSearch: {
|
||||
type: 'provider',
|
||||
id: 'anthropic.web_search',
|
||||
args: { maxUses: 5 },
|
||||
} as any,
|
||||
webSearch: mockWebSearchTool({ maxUses: 5 }),
|
||||
},
|
||||
});
|
||||
const result = await agent.stream({
|
||||
@@ -185,7 +198,7 @@ export async function agentProviderToolE2e() {
|
||||
*/
|
||||
export async function agentMixedToolsE2e(a: number, b: number) {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'provider-tool-call',
|
||||
@@ -201,11 +214,7 @@ export async function agentMixedToolsE2e(a: number, b: number) {
|
||||
{ type: 'text', text: `The answer is ${a + b}` },
|
||||
]),
|
||||
tools: {
|
||||
webSearch: {
|
||||
type: 'provider',
|
||||
id: 'anthropic.web_search',
|
||||
args: {},
|
||||
} as any,
|
||||
webSearch: mockWebSearchTool(),
|
||||
addNumbers: {
|
||||
description: 'Add two numbers',
|
||||
inputSchema: z.object({ a: z.number(), b: z.number() }),
|
||||
@@ -224,28 +233,27 @@ export async function agentMixedToolsE2e(a: number, b: number) {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callback tests — onStepFinish
|
||||
// Callback tests — onStepEnd
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnStepFinishE2e() {
|
||||
export async function agentOnStepEndE2e() {
|
||||
'use workflow';
|
||||
const callSources: string[] = [];
|
||||
let capturedStepResult: any = null;
|
||||
const agent = new DurableAgent({
|
||||
let capturedStepResult = null;
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('hello'),
|
||||
onStepFinish: async () => {
|
||||
onStepEnd: async () => {
|
||||
callSources.push('constructor');
|
||||
},
|
||||
});
|
||||
const result = await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
onStepFinish: async (stepResult) => {
|
||||
onStepEnd: async (stepResult) => {
|
||||
callSources.push('method');
|
||||
capturedStepResult = {
|
||||
text: stepResult.text,
|
||||
finishReason: stepResult.finishReason,
|
||||
stepNumber: (stepResult as any).stepNumber,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -253,30 +261,30 @@ export async function agentOnStepFinishE2e() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callback tests — onFinish
|
||||
// Callback tests — onEnd
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnFinishE2e() {
|
||||
export async function agentOnEndE2e() {
|
||||
'use workflow';
|
||||
const callSources: string[] = [];
|
||||
let capturedEvent: any = null;
|
||||
const agent = new DurableAgent({
|
||||
let capturedEvent = null;
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('hello from finish'),
|
||||
onFinish: async () => {
|
||||
onEnd: async () => {
|
||||
callSources.push('constructor');
|
||||
},
|
||||
});
|
||||
const result = await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
onFinish: async (event) => {
|
||||
onEnd: async (event) => {
|
||||
callSources.push('method');
|
||||
capturedEvent = {
|
||||
text: (event as any).text,
|
||||
finishReason: (event as any).finishReason,
|
||||
text: event.text,
|
||||
finishReason: event.finishReason,
|
||||
stepsLength: event.steps.length,
|
||||
hasMessages: event.messages.length > 0,
|
||||
hasTotalUsage: (event as any).totalUsage != null,
|
||||
hasTotalUsage: event.totalUsage != null,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -289,7 +297,7 @@ export async function agentOnFinishE2e() {
|
||||
|
||||
export async function agentInstructionsStringE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('ok'),
|
||||
instructions: 'You are a pirate.',
|
||||
});
|
||||
@@ -309,7 +317,7 @@ export async function agentInstructionsStringE2e() {
|
||||
|
||||
export async function agentTimeoutE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('fast response'),
|
||||
});
|
||||
const result = await agent.stream({
|
||||
@@ -324,59 +332,59 @@ export async function agentTimeoutE2e() {
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — experimental_onStart
|
||||
// Callback tests — experimental_onStart
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnStartE2e() {
|
||||
'use workflow';
|
||||
const callSources: string[] = [];
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('hello'),
|
||||
experimental_onStart: async () => {
|
||||
callSources.push('constructor');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
experimental_onStart: async () => {
|
||||
callSources.push('method');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
return { callSources };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — experimental_onStepStart
|
||||
// Callback tests — experimental_onStepStart
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnStepStartE2e() {
|
||||
'use workflow';
|
||||
const callSources: string[] = [];
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('hello'),
|
||||
experimental_onStepStart: async () => {
|
||||
callSources.push('constructor');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
experimental_onStepStart: async () => {
|
||||
callSources.push('method');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
return { callSources };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — experimental_onToolCallStart
|
||||
// Callback tests — onToolExecutionStart
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnToolCallStartE2e() {
|
||||
export async function agentOnToolExecutionStartE2e() {
|
||||
'use workflow';
|
||||
const calls: string[] = [];
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'tool-call',
|
||||
@@ -392,29 +400,29 @@ export async function agentOnToolCallStartE2e() {
|
||||
execute: echoStep,
|
||||
},
|
||||
},
|
||||
experimental_onToolCallStart: async () => {
|
||||
onToolExecutionStart: async () => {
|
||||
calls.push('constructor');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
experimental_onToolCallStart: async () => {
|
||||
onToolExecutionStart: async () => {
|
||||
calls.push('method');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
return { calls };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — experimental_onToolCallFinish
|
||||
// Callback tests — onToolExecutionEnd
|
||||
// ============================================================================
|
||||
|
||||
export async function agentOnToolCallFinishE2e() {
|
||||
export async function agentOnToolExecutionEndE2e() {
|
||||
'use workflow';
|
||||
const calls: string[] = [];
|
||||
let capturedEvent: any = null;
|
||||
const agent = new DurableAgent({
|
||||
let capturedEvent = null;
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'tool-call',
|
||||
@@ -430,14 +438,14 @@ export async function agentOnToolCallFinishE2e() {
|
||||
execute: addNumbers,
|
||||
},
|
||||
},
|
||||
experimental_onToolCallFinish: async () => {
|
||||
onToolExecutionEnd: async () => {
|
||||
calls.push('constructor');
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
experimental_onToolCallFinish: async (event: any) => {
|
||||
onToolExecutionEnd: async (event) => {
|
||||
calls.push('method');
|
||||
capturedEvent = {
|
||||
toolName: event?.toolCall?.toolName,
|
||||
@@ -445,23 +453,26 @@ export async function agentOnToolCallFinishE2e() {
|
||||
output: event?.output,
|
||||
};
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
return { calls, capturedEvent };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — prepareCall
|
||||
// prepareCall
|
||||
// ============================================================================
|
||||
|
||||
export async function agentPrepareCallE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
let prepareCallCount = 0;
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('ok'),
|
||||
prepareCall: ({ options, ...rest }: any) => ({
|
||||
...rest,
|
||||
providerOptions: { test: { value: options?.value } },
|
||||
}),
|
||||
} as any);
|
||||
prepareCall: () => {
|
||||
prepareCallCount++;
|
||||
return {
|
||||
providerOptions: { test: { value: 'prepared' } },
|
||||
};
|
||||
},
|
||||
});
|
||||
const result = await agent.stream({
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
writable: getWritable(),
|
||||
@@ -469,17 +480,18 @@ export async function agentPrepareCallE2e() {
|
||||
return {
|
||||
stepCount: result.steps.length,
|
||||
lastStepText: result.steps[result.steps.length - 1]?.text,
|
||||
prepareCallCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GAP tests — tool approval (needsApproval)
|
||||
// Tool approval (needsApproval)
|
||||
// ============================================================================
|
||||
|
||||
/** Tool with needsApproval: true should pause the agent. */
|
||||
export async function agentToolApprovalE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{
|
||||
type: 'tool-call',
|
||||
@@ -492,9 +504,9 @@ export async function agentToolApprovalE2e() {
|
||||
riskyTool: {
|
||||
description: 'A dangerous tool that needs approval',
|
||||
inputSchema: z.object({ action: z.string() }),
|
||||
execute: echoStep as any,
|
||||
execute: riskyStep,
|
||||
needsApproval: true,
|
||||
} as any,
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await agent.stream({
|
||||
@@ -524,8 +536,7 @@ async function prepareStepStep(input: { n: number }): Promise<string> {
|
||||
export async function agentConstructorPrepareStepE2e() {
|
||||
'use workflow';
|
||||
const stepNumbers: number[] = [];
|
||||
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{ type: 'tool-call', toolName: 'greet', input: JSON.stringify({ n: 1 }) },
|
||||
{ type: 'text', text: 'done' },
|
||||
@@ -537,8 +548,8 @@ export async function agentConstructorPrepareStepE2e() {
|
||||
execute: prepareStepStep,
|
||||
},
|
||||
},
|
||||
prepareStep: ({ stepNumber }) => {
|
||||
stepNumbers.push(stepNumber);
|
||||
prepareStep: (options) => {
|
||||
stepNumbers.push(options.stepNumber);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
@@ -561,7 +572,7 @@ export async function agentStreamPrepareStepOverrideE2e() {
|
||||
'use workflow';
|
||||
const source: string[] = [];
|
||||
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockTextModel('ok'),
|
||||
prepareStep: () => {
|
||||
source.push('constructor');
|
||||
@@ -607,10 +618,10 @@ async function multimodalToolStep(): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
/** Tools returning LanguageModelV3ToolResultOutput should pass through. */
|
||||
/** Tools returning LanguageModelV4ToolResultOutput should pass through. */
|
||||
export async function agentMultimodalToolResultE2e() {
|
||||
'use workflow';
|
||||
const agent = new DurableAgent({
|
||||
const agent = new WorkflowAgent({
|
||||
model: mockSequenceModel([
|
||||
{ type: 'tool-call', toolName: 'vision', input: '{}' },
|
||||
{ type: 'text', text: 'I see the image' },
|
||||
|
||||
@@ -2,6 +2,8 @@ import { generateText, stepCountIs } from 'ai';
|
||||
import { FatalError } from 'workflow';
|
||||
import z from 'zod/v4';
|
||||
|
||||
const DEFAULT_AI_MODEL = 'spacexai/grok-4.6';
|
||||
|
||||
async function getWeatherInformation({ city }: { city: string }) {
|
||||
'use step';
|
||||
|
||||
@@ -32,7 +34,7 @@ export async function ai(prompt: string) {
|
||||
// AI SDK's `generateText` just works natively in a workflow thanks to
|
||||
// workflow's automatic fetch hoisting functionality
|
||||
const { text } = await generateText({
|
||||
model: 'openai/o3',
|
||||
model: DEFAULT_AI_MODEL,
|
||||
prompt,
|
||||
});
|
||||
|
||||
@@ -49,7 +51,7 @@ export async function agent(prompt: string) {
|
||||
// You can also provide tools, and if those tools are `steps` - voila, you have yourself
|
||||
// a durable agent with fetches and steps being offloaded
|
||||
const { text } = await generateText({
|
||||
model: 'anthropic/claude-4-opus-20250514',
|
||||
model: DEFAULT_AI_MODEL,
|
||||
prompt,
|
||||
tools: {
|
||||
getWeatherInformation: {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { convertArrayToReadableStream, MockLanguageModelV4 } from 'ai/test';
|
||||
|
||||
type JsonValue =
|
||||
| null
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
export type MockResponseDescriptor =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'tool-call'; toolName: string; input: string }
|
||||
| {
|
||||
type: 'provider-tool-call';
|
||||
toolName: string;
|
||||
input: string;
|
||||
result: Exclude<JsonValue, null>;
|
||||
};
|
||||
|
||||
type MockStreamOptions = Parameters<MockLanguageModelV4['doStream']>[0];
|
||||
type MockStreamResult = Awaited<ReturnType<MockLanguageModelV4['doStream']>>;
|
||||
type MockStreamPart = MockStreamResult extends {
|
||||
stream: ReadableStream<infer Part>;
|
||||
}
|
||||
? Part
|
||||
: never;
|
||||
|
||||
const usage = {
|
||||
inputTokens: {
|
||||
total: 5,
|
||||
noCache: 5,
|
||||
cacheRead: undefined,
|
||||
cacheWrite: undefined,
|
||||
},
|
||||
outputTokens: { total: 10, text: 10, reasoning: undefined },
|
||||
};
|
||||
|
||||
class SerializableMockLanguageModel extends MockLanguageModelV4 {
|
||||
static [Symbol.for('workflow-serialize')](
|
||||
model: SerializableMockLanguageModel
|
||||
) {
|
||||
return { responses: model.responses };
|
||||
}
|
||||
|
||||
static [Symbol.for('workflow-deserialize')](options: {
|
||||
responses: MockResponseDescriptor[];
|
||||
}) {
|
||||
return new SerializableMockLanguageModel(options.responses);
|
||||
}
|
||||
|
||||
constructor(private readonly responses: MockResponseDescriptor[]) {
|
||||
super({
|
||||
provider: 'workflow-test',
|
||||
modelId: 'workflow-test-model',
|
||||
doStream: async (options: MockStreamOptions) => {
|
||||
const responseIndex = Math.min(
|
||||
options.prompt.filter((message) => message.role === 'assistant')
|
||||
.length,
|
||||
responses.length - 1
|
||||
);
|
||||
const response = responses[responseIndex];
|
||||
|
||||
const toolCallId = `call-${responseIndex + 1}`;
|
||||
const prefix: MockStreamPart[] = [
|
||||
{ type: 'stream-start', warnings: [] },
|
||||
{
|
||||
type: 'response-metadata',
|
||||
id: `response-${responseIndex}`,
|
||||
modelId: 'workflow-test-model',
|
||||
timestamp: new Date('2026-08-31T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
const streamParts: MockStreamPart[] =
|
||||
response.type === 'text'
|
||||
? [
|
||||
...prefix,
|
||||
{ type: 'text-start', id: `text-${responseIndex}` },
|
||||
{
|
||||
type: 'text-delta',
|
||||
id: `text-${responseIndex}`,
|
||||
delta: response.text,
|
||||
},
|
||||
{ type: 'text-end', id: `text-${responseIndex}` },
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'stop', raw: 'stop' },
|
||||
usage,
|
||||
},
|
||||
]
|
||||
: response.type === 'provider-tool-call'
|
||||
? [
|
||||
...prefix,
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolCallId,
|
||||
toolName: response.toolName,
|
||||
input: response.input,
|
||||
providerExecuted: true,
|
||||
},
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolCallId,
|
||||
toolName: response.toolName,
|
||||
result: response.result,
|
||||
},
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'tool-calls', raw: undefined },
|
||||
usage,
|
||||
},
|
||||
]
|
||||
: [
|
||||
...prefix,
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolCallId,
|
||||
toolName: response.toolName,
|
||||
input: response.input,
|
||||
},
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'tool-calls', raw: undefined },
|
||||
usage,
|
||||
},
|
||||
];
|
||||
|
||||
return { stream: convertArrayToReadableStream(streamParts) };
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function mockTextModel(text: string): MockLanguageModelV4 {
|
||||
return mockSequenceModel([{ type: 'text', text }]);
|
||||
}
|
||||
|
||||
export function mockSequenceModel(
|
||||
responses: MockResponseDescriptor[]
|
||||
): MockLanguageModelV4 {
|
||||
if (responses.length === 0) {
|
||||
throw new Error('At least one mock response is required');
|
||||
}
|
||||
|
||||
return new SerializableMockLanguageModel(responses);
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
"nitro": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/workflow": "catalog:ai-sdk-v7",
|
||||
"@types/express": "^5.0.6",
|
||||
"@workflow/world-postgres": "workspace:*",
|
||||
"ai": "catalog:",
|
||||
"ai": "catalog:ai-sdk-v7",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"openai": "^6.1.0",
|
||||
"workflow": "workspace:*",
|
||||
"@workflow/ai": "workspace:*",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
"nitro": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/workflow": "catalog:ai-sdk-v7",
|
||||
"@types/node": "catalog:",
|
||||
"@workflow/world-postgres": "workspace:*",
|
||||
"ai": "catalog:",
|
||||
"ai": "catalog:ai-sdk-v7",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"openai": "^6.6.0",
|
||||
"typescript": "catalog:",
|
||||
"workflow": "workspace:*",
|
||||
"@workflow/ai": "workspace:*",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user