workflow 5.0.0-beta.50 → 5.0.0-beta.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai/chat-session-modeling.mdx +170 -419
- package/docs/ai/index.mdx +27 -36
- package/docs/ai/message-queueing.mdx +61 -100
- package/docs/api-reference/index.mdx +1 -1
- package/docs/api-reference/workflow-ai/durable-agent.mdx +15 -15
- package/docs/api-reference/workflow-ai/index.mdx +3 -3
- package/docs/changelog/batched-event-writes.mdx +1 -1
- package/docs/configuration/runtime-tuning.mdx +5 -0
- package/docs/configuration/worlds.mdx +1 -1
- package/docs/cookbook/advanced/serializable-steps.mdx +33 -61
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +64 -54
- package/docs/cookbook/agent-patterns/durable-agent.mdx +21 -1
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +177 -200
- package/docs/cookbook/integrations/ai-sdk.mdx +2 -2
- package/docs/errors/corrupted-event-log.mdx +11 -1
- package/docs/errors/fetch-in-workflow.mdx +4 -7
- package/docs/errors/run-expired.mdx +6 -6
- package/docs/foundations/streaming.mdx +1 -1
- package/docs/getting-started/python.mdx +69 -15
- package/docs/how-it-works/event-sourcing.mdx +27 -0
- package/docs/observability/retention.mdx +8 -6
- package/package.json +10 -10
|
@@ -1,46 +1,37 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Human-in-the-Loop
|
|
3
|
-
description: Pause
|
|
3
|
+
description: Pause a WorkflowAgent for human approval before a consequential tool executes.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Use
|
|
5
|
+
summary: Use WorkflowAgent's needsApproval option and AI SDK approval responses to build durable human approval flows.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<CopyPrompt
|
|
9
|
-
text="Add a human approval gate to this AI
|
|
9
|
+
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."
|
|
10
10
|
/>
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
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.
|
|
14
|
-
</Callout>
|
|
15
|
-
|
|
16
|
-
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.
|
|
12
|
+
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.
|
|
17
13
|
|
|
18
14
|
## When to use this
|
|
19
15
|
|
|
20
16
|
- Booking confirmations where users must approve before charges are made
|
|
21
17
|
- Content publishing gates where an editor must sign off
|
|
22
|
-
- Agent actions where the cost of an error justifies
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
- Agent actions where the cost of an error justifies human review
|
|
19
|
+
- Side effects that are difficult to reverse
|
|
20
|
+
|
|
21
|
+
## Define an approval-gated tool
|
|
22
|
+
|
|
23
|
+
Set `needsApproval` on the tool. Keep the action itself in a step so it receives Workflow retries and observability only after approval succeeds.
|
|
24
|
+
|
|
25
|
+
```typescript title="workflows/booking-agent.ts" lineNumbers
|
|
26
|
+
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
|
27
|
+
import {
|
|
28
|
+
tool,
|
|
29
|
+
type InferUITools,
|
|
30
|
+
type ModelMessage,
|
|
31
|
+
type UIMessage,
|
|
32
|
+
} from "ai";
|
|
33
|
+
import { getWritable } from "workflow";
|
|
34
34
|
import { z } from "zod";
|
|
35
|
-
import type { ModelMessage, UIMessageChunk } from "ai";
|
|
36
|
-
|
|
37
|
-
// Exported so the approval API route can call .resume()
|
|
38
|
-
export const bookingApprovalHook = defineHook({ // [!code highlight]
|
|
39
|
-
schema: z.object({
|
|
40
|
-
approved: z.boolean(),
|
|
41
|
-
comment: z.string().optional(),
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
35
|
|
|
45
36
|
async function searchFlights({ from, to, date }: {
|
|
46
37
|
from: string;
|
|
@@ -48,216 +39,202 @@ async function searchFlights({ from, to, date }: {
|
|
|
48
39
|
date: string;
|
|
49
40
|
}) {
|
|
50
41
|
"use step";
|
|
51
|
-
|
|
42
|
+
|
|
43
|
+
const response = await fetch(
|
|
52
44
|
`https://api.example.com/flights?from=${from}&to=${to}&date=${date}`
|
|
53
45
|
);
|
|
54
|
-
return
|
|
46
|
+
return response.json();
|
|
55
47
|
}
|
|
56
48
|
|
|
57
|
-
async function confirmBooking({ flightId, passenger }: {
|
|
49
|
+
async function confirmBooking({ flightId, passenger, price }: {
|
|
58
50
|
flightId: string;
|
|
59
51
|
passenger: string;
|
|
52
|
+
price: number;
|
|
60
53
|
}) {
|
|
61
54
|
"use step";
|
|
62
|
-
|
|
55
|
+
|
|
56
|
+
const response = await fetch("https://api.example.com/bookings", {
|
|
63
57
|
method: "POST",
|
|
64
|
-
body: JSON.stringify({ flightId, passenger }),
|
|
58
|
+
body: JSON.stringify({ flightId, passenger, price }),
|
|
65
59
|
});
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Stream a custom data part so the client can render the approval UI.
|
|
70
|
-
// This MUST run before the hook suspends the workflow, otherwise
|
|
71
|
-
// the tool-invocation won't appear in the stream until the tool returns,
|
|
72
|
-
// and the client would have no way to show approval buttons.
|
|
73
|
-
async function emitApprovalRequest(details: {
|
|
74
|
-
flightId: string;
|
|
75
|
-
passenger: string;
|
|
76
|
-
price: number;
|
|
77
|
-
toolCallId: string;
|
|
78
|
-
}) {
|
|
79
|
-
"use step";
|
|
80
|
-
const writer = getWritable<UIMessageChunk>().getWriter();
|
|
81
|
-
try {
|
|
82
|
-
await writer.write({
|
|
83
|
-
type: "data-approval-needed", // [!code highlight]
|
|
84
|
-
id: details.toolCallId,
|
|
85
|
-
data: details,
|
|
86
|
-
} as UIMessageChunk);
|
|
87
|
-
} finally {
|
|
88
|
-
writer.releaseLock();
|
|
89
|
-
}
|
|
60
|
+
return response.json();
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
63
|
+
export const bookingTools = {
|
|
64
|
+
searchFlights: tool({
|
|
65
|
+
description: "Search for available flights",
|
|
66
|
+
inputSchema: z.object({
|
|
67
|
+
from: z.string().describe("Departure airport code"),
|
|
68
|
+
to: z.string().describe("Arrival airport code"),
|
|
69
|
+
date: z.string().describe("Travel date (YYYY-MM-DD)"),
|
|
70
|
+
}),
|
|
71
|
+
execute: searchFlights,
|
|
72
|
+
}),
|
|
73
|
+
confirmBooking: tool({
|
|
74
|
+
description: "Book a selected flight for a passenger",
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
flightId: z.string(),
|
|
77
|
+
passenger: z.string(),
|
|
78
|
+
price: z.number(),
|
|
79
|
+
}),
|
|
80
|
+
needsApproval: true, // [!code highlight]
|
|
81
|
+
execute: confirmBooking,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
109
84
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
price: number;
|
|
116
|
-
},
|
|
117
|
-
{ toolCallId }: { toolCallId: string }
|
|
118
|
-
) {
|
|
119
|
-
// Emit to the stream before suspending so the UI can show buttons
|
|
120
|
-
await emitApprovalRequest({ flightId, passenger, price, toolCallId }); // [!code highlight]
|
|
121
|
-
|
|
122
|
-
const hook = bookingApprovalHook.create({ token: toolCallId });
|
|
123
|
-
|
|
124
|
-
// Race: human decision vs. timeout
|
|
125
|
-
const result = await Promise.race([
|
|
126
|
-
hook.then((payload) => ({ type: "decision" as const, ...payload })),
|
|
127
|
-
sleep("24h").then(() => ({ type: "timeout" as const, approved: false as const })),
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
if (result.type === "timeout") {
|
|
131
|
-
const msg = "Booking request expired.";
|
|
132
|
-
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
|
133
|
-
return msg;
|
|
134
|
-
}
|
|
135
|
-
if (!result.approved) {
|
|
136
|
-
const msg = `Rejected: ${result.comment || "No reason given"}`;
|
|
137
|
-
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
|
138
|
-
return msg;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const booking = await confirmBooking({ flightId, passenger });
|
|
142
|
-
const msg = `Booked! Confirmation: ${booking.confirmationId}`;
|
|
143
|
-
await emitApprovalResolved({ toolCallId, result: msg }); // [!code highlight]
|
|
144
|
-
return msg;
|
|
145
|
-
}
|
|
85
|
+
export type BookingAgentUIMessage = UIMessage<
|
|
86
|
+
unknown,
|
|
87
|
+
never,
|
|
88
|
+
InferUITools<typeof bookingTools>
|
|
89
|
+
>;
|
|
146
90
|
|
|
147
91
|
export async function bookingAgent(messages: ModelMessage[]) {
|
|
148
92
|
"use workflow";
|
|
149
93
|
|
|
150
|
-
const agent = new
|
|
151
|
-
model: "
|
|
152
|
-
instructions: "
|
|
153
|
-
tools:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
from: z.string().describe("Departure airport code"),
|
|
158
|
-
to: z.string().describe("Arrival airport code"),
|
|
159
|
-
date: z.string().describe("Travel date (YYYY-MM-DD)"),
|
|
160
|
-
}),
|
|
161
|
-
execute: searchFlights,
|
|
162
|
-
},
|
|
163
|
-
requestBookingApproval: {
|
|
164
|
-
description: "Request human approval before booking a flight",
|
|
165
|
-
inputSchema: z.object({
|
|
166
|
-
flightId: z.string().describe("Flight ID to book"),
|
|
167
|
-
passenger: z.string().describe("Passenger name"),
|
|
168
|
-
price: z.number().describe("Total price"),
|
|
169
|
-
}),
|
|
170
|
-
execute: requestBookingApproval,
|
|
171
|
-
},
|
|
172
|
-
},
|
|
94
|
+
const agent = new WorkflowAgent({
|
|
95
|
+
model: "spacexai/grok-4.6",
|
|
96
|
+
instructions: "Help the user find and book flights.",
|
|
97
|
+
tools: bookingTools,
|
|
98
|
+
experimental_toolApprovalSecret: { // [!code highlight]
|
|
99
|
+
environmentVariable: "WORKFLOW_TOOL_APPROVAL_SECRET", // [!code highlight]
|
|
100
|
+
}, // [!code highlight]
|
|
173
101
|
});
|
|
174
102
|
|
|
175
|
-
|
|
103
|
+
return agent.stream({
|
|
176
104
|
messages,
|
|
177
|
-
writable: getWritable<
|
|
105
|
+
writable: getWritable<ModelCallStreamPart>(),
|
|
178
106
|
});
|
|
179
107
|
}
|
|
180
108
|
```
|
|
181
109
|
|
|
182
|
-
|
|
110
|
+
## Sign approval requests
|
|
183
111
|
|
|
184
|
-
|
|
112
|
+
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.
|
|
185
113
|
|
|
186
|
-
|
|
187
|
-
import { bookingApprovalHook } from "@/app/workflows/booking-agent";
|
|
114
|
+
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.
|
|
188
115
|
|
|
189
|
-
|
|
190
|
-
const { toolCallId, approved, comment } = await req.json();
|
|
116
|
+
Signed approvals require `@ai-sdk/workflow` 2.0.16 or later.
|
|
191
117
|
|
|
192
|
-
|
|
118
|
+
`needsApproval` can also decide from the parsed tool input. For example, require approval only when a booking costs more than a threshold:
|
|
193
119
|
|
|
194
|
-
|
|
120
|
+
{/* @skip-typecheck: property excerpt */}
|
|
121
|
+
```typescript
|
|
122
|
+
needsApproval: async ({ price }) => price > 500,
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Start the workflow and transform its stream
|
|
126
|
+
|
|
127
|
+
`WorkflowAgent` stores `ModelCallStreamPart` values. Convert those durable parts into AI SDK UI chunks at the HTTP boundary:
|
|
128
|
+
|
|
129
|
+
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
130
|
+
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
|
131
|
+
import {
|
|
132
|
+
convertToModelMessages,
|
|
133
|
+
createUIMessageStreamResponse,
|
|
134
|
+
} from "ai";
|
|
135
|
+
import { start } from "workflow/api";
|
|
136
|
+
import {
|
|
137
|
+
bookingAgent,
|
|
138
|
+
type BookingAgentUIMessage,
|
|
139
|
+
} from "@/workflows/booking-agent";
|
|
140
|
+
|
|
141
|
+
export async function POST(request: Request) {
|
|
142
|
+
const { messages }: { messages: BookingAgentUIMessage[] } =
|
|
143
|
+
await request.json();
|
|
144
|
+
const modelMessages = await convertToModelMessages(messages);
|
|
145
|
+
const run = await start(bookingAgent, [modelMessages]);
|
|
146
|
+
|
|
147
|
+
return createUIMessageStreamResponse({
|
|
148
|
+
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
|
|
149
|
+
headers: { "x-workflow-run-id": run.runId },
|
|
150
|
+
});
|
|
195
151
|
}
|
|
196
152
|
```
|
|
197
153
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```tsx
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
154
|
+
## Render and answer approval requests
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
```tsx title="app/chat.tsx" lineNumbers
|
|
159
|
+
"use client";
|
|
160
|
+
|
|
161
|
+
import { useChat } from "@ai-sdk/react";
|
|
162
|
+
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
|
163
|
+
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
|
164
|
+
import { useMemo } from "react";
|
|
165
|
+
import type { BookingAgentUIMessage } from "@/workflows/booking-agent";
|
|
166
|
+
|
|
167
|
+
export function Chat() {
|
|
168
|
+
const transport = useMemo(
|
|
169
|
+
() => new WorkflowChatTransport({ api: "/api/chat" }),
|
|
170
|
+
[]
|
|
171
|
+
);
|
|
172
|
+
const { messages, addToolApprovalResponse } = useChat<BookingAgentUIMessage>({
|
|
173
|
+
transport,
|
|
174
|
+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return messages.map((message) =>
|
|
178
|
+
message.parts.map((part) => {
|
|
179
|
+
if (
|
|
180
|
+
part.type !== "tool-confirmBooking" ||
|
|
181
|
+
part.state !== "approval-requested" ||
|
|
182
|
+
part.approval.isAutomatic
|
|
183
|
+
) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div key={part.toolCallId}>
|
|
189
|
+
<p>
|
|
190
|
+
Book flight {part.input.flightId} for {part.input.passenger} at
|
|
191
|
+
${part.input.price}?
|
|
192
|
+
</p>
|
|
193
|
+
<button
|
|
194
|
+
onClick={() =>
|
|
195
|
+
addToolApprovalResponse({
|
|
196
|
+
id: part.approval.id,
|
|
197
|
+
approved: true,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
Approve
|
|
202
|
+
</button>
|
|
203
|
+
<button
|
|
204
|
+
onClick={() =>
|
|
205
|
+
addToolApprovalResponse({
|
|
206
|
+
id: part.approval.id,
|
|
207
|
+
approved: false,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
>
|
|
211
|
+
Reject
|
|
212
|
+
</button>
|
|
226
213
|
</div>
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (part.type === "tool-invocation" &&
|
|
232
|
-
part.toolInvocation.toolName === "requestBookingApproval") {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
// ... other part types
|
|
236
|
-
})}
|
|
214
|
+
);
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
}
|
|
237
218
|
```
|
|
238
219
|
|
|
239
220
|
## How it works
|
|
240
221
|
|
|
241
|
-
1.
|
|
242
|
-
2.
|
|
243
|
-
3.
|
|
244
|
-
4.
|
|
245
|
-
5.
|
|
246
|
-
6. **`emitApprovalResolved` step**: Writes the outcome to the stream so the client can update the card immediately without waiting for the tool-invocation result.
|
|
222
|
+
1. The model calls `confirmBooking` with validated input.
|
|
223
|
+
2. `needsApproval` prevents the tool's `execute` function from running and emits an approval request.
|
|
224
|
+
3. The durable stream preserves the request across disconnects and process restarts.
|
|
225
|
+
4. The client adds an approval response to the conversation.
|
|
226
|
+
5. If approved, `confirmBooking` runs as a durable step. If rejected, the model receives the denial and can respond without performing the side effect.
|
|
247
227
|
|
|
248
|
-
## Adapting
|
|
228
|
+
## Adapting the pattern
|
|
249
229
|
|
|
250
|
-
- **
|
|
251
|
-
- **
|
|
252
|
-
- **
|
|
253
|
-
- **
|
|
254
|
-
- **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.
|
|
230
|
+
- **Conditional approval**: Return a boolean from `needsApproval` based on amount, tenant policy, or risk.
|
|
231
|
+
- **Timeouts and escalation**: Combine the surrounding workflow with `sleep()` and hooks when an approval must expire or escalate.
|
|
232
|
+
- **Audit context**: Include durable user and tenant identifiers in the workflow input, then record the approver in your application database.
|
|
233
|
+
- **Multiple gates**: Set `needsApproval` on every consequential tool independently.
|
|
255
234
|
|
|
256
235
|
## Key APIs
|
|
257
236
|
|
|
258
|
-
- [`
|
|
259
|
-
- [`
|
|
260
|
-
- [`
|
|
261
|
-
- [`
|
|
262
|
-
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Streams custom data parts from steps.
|
|
263
|
-
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Provides AI SDK's durable agent and replaces `DurableAgent`.
|
|
237
|
+
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Durable AI SDK agent with first-class tool approvals
|
|
238
|
+
- [`tool()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool): Defines a typed tool and its approval policy
|
|
239
|
+
- [`getWritable()`](/docs/api-reference/workflow/get-writable): Stores durable model-call stream parts
|
|
240
|
+
- [`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[]) {
|
|
|
91
91
|
"use step";
|
|
92
92
|
|
|
93
93
|
const result = streamText({
|
|
94
|
-
model: "
|
|
94
|
+
model: "spacexai/grok-4.6",
|
|
95
95
|
system: "You are a customer support agent.",
|
|
96
96
|
messages,
|
|
97
97
|
tools: TOOLS,
|
|
@@ -373,7 +373,7 @@ This example stores the `runId` after the first response. For strict one-session
|
|
|
373
373
|
| **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 |
|
|
374
374
|
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
|
375
375
|
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
|
376
|
-
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `
|
|
376
|
+
| **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepEnd`, `onEnd`, `onError`, `onAbort` (`onChunk` not available) |
|
|
377
377
|
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
|
378
378
|
|
|
379
379
|
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
|
@@ -18,9 +18,19 @@ This is a **workflow-level fatal error**. It cannot be caught or handled inside
|
|
|
18
18
|
For replay divergence:
|
|
19
19
|
|
|
20
20
|
```text
|
|
21
|
-
Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId
|
|
21
|
+
Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>; divergent event ids: <eventId>, <eventId>, ... Last divergence: <details>
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
The `divergent event ids` list has one entry per divergence in the recovery chain, oldest first. Every recovery replay diverging at the same event points at a fixed disagreement between the log and the code; ids that wander point at a race with another writer.
|
|
25
|
+
|
|
26
|
+
`<details>` is the last divergence's own message. When the replay could not place an event, it names the invocation that was pending under that event's position at the time, and where the replay's walk over the log stood:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Replay could not consume event: eventType=wait_created, correlationId=wait_<id>, eventId=<eventId>. pending at this id: step <stepName> (step_<id>). consumer: index=<n>, length=<n>, parked=<n>, lastConsumed=<eventId>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Steps, sleeps and hooks draw their ids from one sequence, so `pending at this id` reports the entity the replay put at that position, whatever its kind. In the example, the log recorded a sleep where this replay reached a step named `<stepName>`.
|
|
33
|
+
|
|
24
34
|
For an unreadable stored payload:
|
|
25
35
|
|
|
26
36
|
```text
|
|
@@ -35,14 +35,13 @@ Import the `fetch` step function from the `workflow` package and assign it to `g
|
|
|
35
35
|
|
|
36
36
|
```typescript lineNumbers title="workflows/ai.ts"
|
|
37
37
|
import { generateText } from "ai";
|
|
38
|
-
import { openai } from "@ai-sdk/openai";
|
|
39
38
|
|
|
40
39
|
export async function chatWorkflow(prompt: string) {
|
|
41
40
|
"use workflow";
|
|
42
41
|
|
|
43
42
|
// Error - generateText() calls fetch() under the hood
|
|
44
43
|
const result = await generateText({ // [!code highlight]
|
|
45
|
-
model:
|
|
44
|
+
model: "spacexai/grok-4.6", // [!code highlight]
|
|
46
45
|
prompt, // [!code highlight]
|
|
47
46
|
}); // [!code highlight]
|
|
48
47
|
|
|
@@ -54,7 +53,6 @@ export async function chatWorkflow(prompt: string) {
|
|
|
54
53
|
|
|
55
54
|
```typescript lineNumbers title="workflows/ai.ts"
|
|
56
55
|
import { generateText } from "ai";
|
|
57
|
-
import { openai } from "@ai-sdk/openai";
|
|
58
56
|
import { fetch } from "workflow"; // [!code highlight]
|
|
59
57
|
|
|
60
58
|
export async function chatWorkflow(prompt: string) {
|
|
@@ -64,7 +62,7 @@ export async function chatWorkflow(prompt: string) {
|
|
|
64
62
|
|
|
65
63
|
// Now generateText() can make HTTP requests via the fetch step
|
|
66
64
|
const result = await generateText({
|
|
67
|
-
model:
|
|
65
|
+
model: "spacexai/grok-4.6",
|
|
68
66
|
prompt,
|
|
69
67
|
});
|
|
70
68
|
|
|
@@ -80,7 +78,6 @@ This is the most common scenario - using AI SDK functions that make HTTP request
|
|
|
80
78
|
|
|
81
79
|
```typescript lineNumbers
|
|
82
80
|
import { generateText, streamText } from "ai";
|
|
83
|
-
import { openai } from "@ai-sdk/openai";
|
|
84
81
|
import { fetch } from "workflow"; // [!code highlight]
|
|
85
82
|
|
|
86
83
|
export async function aiWorkflow(userMessage: string) {
|
|
@@ -88,9 +85,9 @@ export async function aiWorkflow(userMessage: string) {
|
|
|
88
85
|
|
|
89
86
|
globalThis.fetch = fetch; // [!code highlight]
|
|
90
87
|
|
|
91
|
-
// generateText makes HTTP
|
|
88
|
+
// generateText makes an HTTP request under the hood
|
|
92
89
|
const response = await generateText({
|
|
93
|
-
model:
|
|
90
|
+
model: "spacexai/grok-4.6",
|
|
94
91
|
prompt: userMessage,
|
|
95
92
|
});
|
|
96
93
|
|
|
@@ -42,8 +42,8 @@ Two ways to reach it:
|
|
|
42
42
|
|
|
43
43
|
## How to respond
|
|
44
44
|
|
|
45
|
-
`RunExpiredError` is terminal. Retrying will not bring the data back
|
|
46
|
-
|
|
45
|
+
`RunExpiredError` is terminal. Retrying will not bring the data back. Catch it
|
|
46
|
+
and use the run's metadata to decide what you want to do.
|
|
47
47
|
|
|
48
48
|
```typescript lineNumbers
|
|
49
49
|
import { getRun } from "workflow/api"
|
|
@@ -73,10 +73,10 @@ them.
|
|
|
73
73
|
### If you need the result of a zero-retention run
|
|
74
74
|
|
|
75
75
|
Do not read it back off the run. Send it somewhere you control while the run
|
|
76
|
-
is still executing — a step that writes it to your own store. That is the
|
|
77
|
-
for `experimental_retention: 0`: the point of the option is
|
|
78
|
-
does not keep your data, so the platform cannot also be where
|
|
79
|
-
from afterwards.
|
|
76
|
+
is still executing — a step that writes it to your own store. That is the
|
|
77
|
+
intended pattern for `experimental_retention: 0`: the point of the option is
|
|
78
|
+
that the platform does not keep your data, so the platform cannot also be where
|
|
79
|
+
you fetch it from afterwards.
|
|
80
80
|
|
|
81
81
|
## Related
|
|
82
82
|
|
|
@@ -419,7 +419,7 @@ export async function aiAssistantWorkflow(userMessage: string) {
|
|
|
419
419
|
"use workflow";
|
|
420
420
|
|
|
421
421
|
const agent = new WorkflowAgent({
|
|
422
|
-
model: "
|
|
422
|
+
model: "spacexai/grok-4.6",
|
|
423
423
|
instructions: "You are a helpful flight assistant.",
|
|
424
424
|
tools: {
|
|
425
425
|
searchFlights: tool({
|