workflow 5.0.0-beta.0 → 5.0.0-beta.2
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/README.md +4 -4
- package/dist/api-workflow.d.ts +1 -3
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -6
- package/dist/api.js +1 -1
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.js +1 -1
- package/dist/internal/class-serialization.js +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/nest.js +1 -1
- package/dist/next.cjs +4 -2
- package/dist/next.d.cts +1 -1
- package/dist/next.d.cts.map +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.js +1 -1
- package/dist/stdlib.js +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/vite.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +61 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
- package/docs/api-reference/workflow-ai/index.mdx +0 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
- package/docs/api-reference/workflow-api/get-world.mdx +6 -6
- package/docs/api-reference/workflow-api/index.mdx +1 -1
- package/docs/api-reference/workflow-api/world/index.mdx +2 -2
- package/docs/api-reference/workflow-api/world/observability.mdx +1 -1
- package/docs/api-reference/workflow-api/world/queue.mdx +1 -1
- package/docs/api-reference/workflow-api/world/storage.mdx +8 -8
- package/docs/api-reference/workflow-api/world/streams.mdx +38 -36
- package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
- package/docs/cookbook/advanced/durable-objects.mdx +148 -0
- package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
- package/docs/cookbook/advanced/meta.json +10 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
- package/docs/cookbook/agent-patterns/meta.json +10 -0
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
- package/docs/cookbook/common-patterns/batching.mdx +179 -0
- package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
- package/docs/cookbook/common-patterns/content-router.mdx +207 -0
- package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
- package/docs/cookbook/common-patterns/meta.json +15 -0
- package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
- package/docs/cookbook/common-patterns/saga.mdx +152 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/index.mdx +41 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +128 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +45 -43
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +10 -5
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/next.mdx +24 -0
- package/docs/getting-started/python.mdx +165 -0
- package/docs/how-it-works/code-transform.mdx +6 -5
- package/docs/meta.json +1 -0
- package/docs/migration-guides/index.mdx +34 -0
- package/docs/migration-guides/meta.json +9 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
- package/package.json +13 -14
- package/dist/internal/private.d.ts +0 -6
- package/dist/internal/private.d.ts.map +0 -1
- package/dist/internal/private.js +0 -6
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Human-in-the-Loop
|
|
3
|
+
description: Pause an AI agent to wait for human approval, then resume based on the decision.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Use defineHook with the tool call ID to suspend an agent for human approval, with an optional timeout.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## Pattern
|
|
11
|
+
|
|
12
|
+
Create a typed hook using `defineHook()`. When the agent calls the approval tool, the tool creates a hook instance using the tool call ID as the token, then awaits it. The UI renders approval controls, and an API route resumes the hook with the decision.
|
|
13
|
+
|
|
14
|
+
### Simplified
|
|
15
|
+
|
|
16
|
+
```typescript lineNumbers
|
|
17
|
+
import { DurableAgent } from "@workflow/ai/agent";
|
|
18
|
+
import { defineHook, sleep, getWritable } from "workflow";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import type { ModelMessage, UIMessageChunk } from "ai";
|
|
21
|
+
|
|
22
|
+
export const bookingApprovalHook = defineHook({
|
|
23
|
+
schema: z.object({
|
|
24
|
+
approved: z.boolean(),
|
|
25
|
+
comment: z.string().optional(),
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
declare function confirmBooking(args: { flightId: string; passenger: string }): Promise<{ confirmationId: string }>; // @setup
|
|
30
|
+
|
|
31
|
+
// This tool runs at the workflow level (no "use step") because hooks are workflow primitives
|
|
32
|
+
async function requestBookingApproval(
|
|
33
|
+
{ flightId, passenger, price }: { flightId: string; passenger: string; price: number },
|
|
34
|
+
{ toolCallId }: { toolCallId: string }
|
|
35
|
+
) {
|
|
36
|
+
const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
|
|
37
|
+
|
|
38
|
+
const result = await Promise.race([ // [!code highlight]
|
|
39
|
+
hook.then((payload) => ({ type: "decision" as const, ...payload })),
|
|
40
|
+
sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
if (result.type === "timeout") return "Booking request expired after 24 hours.";
|
|
44
|
+
if (!result.approved) return `Booking rejected: ${result.comment || "No reason given"}`;
|
|
45
|
+
|
|
46
|
+
const booking = await confirmBooking({ flightId, passenger });
|
|
47
|
+
return `Booked! Confirmation: ${booking.confirmationId}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function bookingAgent(messages: ModelMessage[]) {
|
|
51
|
+
"use workflow";
|
|
52
|
+
|
|
53
|
+
const agent = new DurableAgent({
|
|
54
|
+
model: "anthropic/claude-haiku-4.5",
|
|
55
|
+
instructions: "You help book flights. Always request approval before booking.",
|
|
56
|
+
tools: {
|
|
57
|
+
requestBookingApproval: {
|
|
58
|
+
description: "Request human approval before booking a flight",
|
|
59
|
+
inputSchema: z.object({
|
|
60
|
+
flightId: z.string(),
|
|
61
|
+
passenger: z.string(),
|
|
62
|
+
price: z.number(),
|
|
63
|
+
}),
|
|
64
|
+
execute: requestBookingApproval,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await agent.stream({ // [!code highlight]
|
|
70
|
+
messages,
|
|
71
|
+
writable: getWritable<UIMessageChunk>(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Full Implementation
|
|
77
|
+
|
|
78
|
+
```typescript lineNumbers
|
|
79
|
+
import { DurableAgent } from "@workflow/ai/agent";
|
|
80
|
+
import { defineHook, sleep, getWritable } from "workflow";
|
|
81
|
+
import { z } from "zod";
|
|
82
|
+
import type { ModelMessage, UIMessageChunk } from "ai";
|
|
83
|
+
|
|
84
|
+
// Define the approval hook with schema validation
|
|
85
|
+
export const bookingApprovalHook = defineHook({
|
|
86
|
+
schema: z.object({
|
|
87
|
+
approved: z.boolean(),
|
|
88
|
+
comment: z.string().optional(),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Step: Search for flights (full Node.js access, automatic retries)
|
|
93
|
+
async function searchFlights({
|
|
94
|
+
from,
|
|
95
|
+
to,
|
|
96
|
+
date,
|
|
97
|
+
}: {
|
|
98
|
+
from: string;
|
|
99
|
+
to: string;
|
|
100
|
+
date: string;
|
|
101
|
+
}) {
|
|
102
|
+
"use step";
|
|
103
|
+
|
|
104
|
+
// Your real flight search API call here
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
106
|
+
return {
|
|
107
|
+
flights: [
|
|
108
|
+
{ id: "FL-100", airline: "Example Air", price: 299, from, to, date },
|
|
109
|
+
{ id: "FL-200", airline: "Demo Airlines", price: 349, from, to, date },
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Step: Confirm the booking after approval
|
|
115
|
+
async function confirmBooking({
|
|
116
|
+
flightId,
|
|
117
|
+
passenger,
|
|
118
|
+
}: {
|
|
119
|
+
flightId: string;
|
|
120
|
+
passenger: string;
|
|
121
|
+
}) {
|
|
122
|
+
"use step";
|
|
123
|
+
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
125
|
+
return { confirmationId: `CONF-${flightId}-${Date.now().toString(36)}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Workflow-level tool: hooks must be created in workflow context, not inside steps
|
|
129
|
+
async function requestBookingApproval(
|
|
130
|
+
{
|
|
131
|
+
flightId,
|
|
132
|
+
passenger,
|
|
133
|
+
price,
|
|
134
|
+
}: { flightId: string; passenger: string; price: number },
|
|
135
|
+
{ toolCallId }: { toolCallId: string }
|
|
136
|
+
) {
|
|
137
|
+
// No "use step" — hooks are workflow-level primitives
|
|
138
|
+
|
|
139
|
+
const hook = bookingApprovalHook.create({ token: toolCallId }); // [!code highlight]
|
|
140
|
+
|
|
141
|
+
// Race: human approval vs. 24-hour timeout
|
|
142
|
+
const result = await Promise.race([ // [!code highlight]
|
|
143
|
+
hook.then((payload) => ({ type: "decision" as const, ...payload })),
|
|
144
|
+
sleep("24h").then(() => ({ type: "timeout" as const, approved: false })),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
if (result.type === "timeout") {
|
|
148
|
+
return "Booking request expired after 24 hours.";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!result.approved) {
|
|
152
|
+
return `Booking rejected: ${result.comment || "No reason given"}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Approved — proceed with booking
|
|
156
|
+
const booking = await confirmBooking({ flightId, passenger });
|
|
157
|
+
return `Flight ${flightId} booked for ${passenger}. Confirmation: ${booking.confirmationId}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function bookingAgent(messages: ModelMessage[]) {
|
|
161
|
+
"use workflow";
|
|
162
|
+
|
|
163
|
+
const writable = getWritable<UIMessageChunk>();
|
|
164
|
+
|
|
165
|
+
const agent = new DurableAgent({
|
|
166
|
+
model: "anthropic/claude-haiku-4.5",
|
|
167
|
+
instructions:
|
|
168
|
+
"You are a flight booking assistant. Search for flights, then request approval before booking.",
|
|
169
|
+
tools: {
|
|
170
|
+
searchFlights: {
|
|
171
|
+
description: "Search for available flights",
|
|
172
|
+
inputSchema: z.object({
|
|
173
|
+
from: z.string().describe("Departure airport code"),
|
|
174
|
+
to: z.string().describe("Arrival airport code"),
|
|
175
|
+
date: z.string().describe("Travel date (YYYY-MM-DD)"),
|
|
176
|
+
}),
|
|
177
|
+
execute: searchFlights,
|
|
178
|
+
},
|
|
179
|
+
requestBookingApproval: {
|
|
180
|
+
description: "Request human approval before booking a flight",
|
|
181
|
+
inputSchema: z.object({
|
|
182
|
+
flightId: z.string().describe("Flight ID to book"),
|
|
183
|
+
passenger: z.string().describe("Passenger name"),
|
|
184
|
+
price: z.number().describe("Total price"),
|
|
185
|
+
}),
|
|
186
|
+
execute: requestBookingApproval,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await agent.stream({ messages, writable }); // [!code highlight]
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### API Route for Approvals
|
|
196
|
+
|
|
197
|
+
```typescript lineNumbers
|
|
198
|
+
import { bookingApprovalHook } from "@/workflows/booking-agent";
|
|
199
|
+
|
|
200
|
+
export async function POST(request: Request) {
|
|
201
|
+
const { toolCallId, approved, comment } = await request.json();
|
|
202
|
+
|
|
203
|
+
// Schema validation happens automatically via defineHook
|
|
204
|
+
await bookingApprovalHook.resume(toolCallId, { approved, comment }); // [!code highlight]
|
|
205
|
+
|
|
206
|
+
return Response.json({ success: true });
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Approval Component
|
|
211
|
+
|
|
212
|
+
```tsx lineNumbers
|
|
213
|
+
"use client";
|
|
214
|
+
|
|
215
|
+
import { useState } from "react";
|
|
216
|
+
|
|
217
|
+
export function BookingApproval({
|
|
218
|
+
toolCallId,
|
|
219
|
+
input,
|
|
220
|
+
output,
|
|
221
|
+
}: {
|
|
222
|
+
toolCallId: string;
|
|
223
|
+
input?: { flightId: string; passenger: string; price: number };
|
|
224
|
+
output?: string;
|
|
225
|
+
}) {
|
|
226
|
+
const [comment, setComment] = useState("");
|
|
227
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
228
|
+
|
|
229
|
+
if (output) {
|
|
230
|
+
return <p className="text-sm text-muted-foreground">{output}</p>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const handleSubmit = async (approved: boolean) => {
|
|
234
|
+
setIsSubmitting(true);
|
|
235
|
+
await fetch("/api/hooks/approval", {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "Content-Type": "application/json" },
|
|
238
|
+
body: JSON.stringify({ toolCallId, approved, comment }),
|
|
239
|
+
});
|
|
240
|
+
setIsSubmitting(false);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div className="border rounded-lg p-4 space-y-3">
|
|
245
|
+
{input && (
|
|
246
|
+
<div className="text-sm space-y-1">
|
|
247
|
+
<div>Flight: {input.flightId}</div>
|
|
248
|
+
<div>Passenger: {input.passenger}</div>
|
|
249
|
+
<div>Price: ${input.price}</div>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
<textarea
|
|
253
|
+
value={comment}
|
|
254
|
+
onChange={(e) => setComment(e.target.value)}
|
|
255
|
+
placeholder="Add a comment (optional)..."
|
|
256
|
+
className="w-full border rounded p-2 text-sm"
|
|
257
|
+
rows={2}
|
|
258
|
+
/>
|
|
259
|
+
<div className="flex gap-2">
|
|
260
|
+
<button type="button" onClick={() => handleSubmit(true)} disabled={isSubmitting}>
|
|
261
|
+
Approve
|
|
262
|
+
</button>
|
|
263
|
+
<button type="button" onClick={() => handleSubmit(false)} disabled={isSubmitting}>
|
|
264
|
+
Reject
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Key APIs
|
|
273
|
+
|
|
274
|
+
- [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
|
|
275
|
+
- [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
|
|
276
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook with schema validation
|
|
277
|
+
- [`sleep()`](/docs/api-reference/workflow/sleep) — durable timeout for approval expiry
|
|
278
|
+
- [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — durable agent with tool definitions
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Stop Workflow
|
|
3
|
+
description: Gracefully cancel a running agent workflow using a hook signal.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Use a hook as a stop signal to break out of an agent loop and close the stream cleanly.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Use this pattern when you need to gracefully stop a running agent from the outside — for example, a "Stop" button in a chat UI or an admin cancellation endpoint. The workflow listens for a stop signal via a hook while the agent runs, and breaks out of the loop when the signal arrives.
|
|
9
|
+
|
|
10
|
+
## Pattern
|
|
11
|
+
|
|
12
|
+
Create a hook with a known token (the run ID). Listen for a stop signal in a non-blocking `.then()`. In the `prepareStep` callback, check the flag and return `{ toolChoice: "none" }` to prevent further tool calls, causing the agent to generate a final response and exit the loop cleanly.
|
|
13
|
+
|
|
14
|
+
### Simplified
|
|
15
|
+
|
|
16
|
+
```typescript lineNumbers
|
|
17
|
+
import { DurableAgent } from "@workflow/ai/agent";
|
|
18
|
+
import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import type { ModelMessage, UIMessageChunk } from "ai";
|
|
21
|
+
|
|
22
|
+
export const stopHook = defineHook({
|
|
23
|
+
schema: z.object({ reason: z.string().optional() }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
declare function processQuery(args: { query: string }): Promise<string>; // @setup
|
|
27
|
+
|
|
28
|
+
export async function stoppableAgent(messages: ModelMessage[]) {
|
|
29
|
+
"use workflow";
|
|
30
|
+
|
|
31
|
+
const { workflowRunId } = getWorkflowMetadata();
|
|
32
|
+
let stopRequested = false;
|
|
33
|
+
|
|
34
|
+
const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
|
|
35
|
+
hook.then(() => { stopRequested = true; }); // [!code highlight]
|
|
36
|
+
|
|
37
|
+
const agent = new DurableAgent({
|
|
38
|
+
model: "anthropic/claude-haiku-4.5",
|
|
39
|
+
tools: {
|
|
40
|
+
processQuery: {
|
|
41
|
+
description: "Process a query",
|
|
42
|
+
inputSchema: z.object({ query: z.string() }),
|
|
43
|
+
execute: processQuery,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await agent.stream({
|
|
49
|
+
messages,
|
|
50
|
+
writable: getWritable<UIMessageChunk>(),
|
|
51
|
+
prepareStep: () => { // [!code highlight]
|
|
52
|
+
if (stopRequested) return { toolChoice: "none" }; // [!code highlight]
|
|
53
|
+
return {};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { messages: result.messages, stopped: stopRequested };
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Full Implementation
|
|
62
|
+
|
|
63
|
+
```typescript lineNumbers
|
|
64
|
+
import { DurableAgent } from "@workflow/ai/agent";
|
|
65
|
+
import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
|
|
66
|
+
import { z } from "zod";
|
|
67
|
+
import type { ModelMessage, UIMessageChunk } from "ai";
|
|
68
|
+
|
|
69
|
+
// Hook to signal the workflow to stop
|
|
70
|
+
export const stopHook = defineHook({
|
|
71
|
+
schema: z.object({
|
|
72
|
+
reason: z.string().optional(),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Step: Search the web
|
|
77
|
+
async function searchWeb({ query }: { query: string }) {
|
|
78
|
+
"use step";
|
|
79
|
+
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
81
|
+
return { results: [`Result for "${query}"`] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step: Analyze data
|
|
85
|
+
async function analyzeData({ data }: { data: string }) {
|
|
86
|
+
"use step";
|
|
87
|
+
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
89
|
+
return { analysis: `Analysis of: ${data}` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step: Write the final close marker to the stream
|
|
93
|
+
async function closeStream() {
|
|
94
|
+
"use step";
|
|
95
|
+
|
|
96
|
+
const writable = getWritable<UIMessageChunk>();
|
|
97
|
+
const writer = writable.getWriter();
|
|
98
|
+
try {
|
|
99
|
+
await writer.write({ type: "finish" } as UIMessageChunk);
|
|
100
|
+
} finally {
|
|
101
|
+
writer.releaseLock();
|
|
102
|
+
}
|
|
103
|
+
await writable.close();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function stoppableAgent(messages: ModelMessage[]) {
|
|
107
|
+
"use workflow";
|
|
108
|
+
|
|
109
|
+
const { workflowRunId } = getWorkflowMetadata();
|
|
110
|
+
const writable = getWritable<UIMessageChunk>();
|
|
111
|
+
|
|
112
|
+
// Listen for stop signal using a non-blocking hook
|
|
113
|
+
let stopRequested = false;
|
|
114
|
+
let stopReason: string | undefined;
|
|
115
|
+
|
|
116
|
+
const hook = stopHook.create({ token: `stop:${workflowRunId}` }); // [!code highlight]
|
|
117
|
+
hook.then(({ reason }) => { // [!code highlight]
|
|
118
|
+
stopRequested = true;
|
|
119
|
+
stopReason = reason;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const agent = new DurableAgent({
|
|
123
|
+
model: "anthropic/claude-haiku-4.5",
|
|
124
|
+
instructions: "You are a research assistant. Search and analyze data as needed.",
|
|
125
|
+
tools: {
|
|
126
|
+
searchWeb: {
|
|
127
|
+
description: "Search the web for information",
|
|
128
|
+
inputSchema: z.object({ query: z.string() }),
|
|
129
|
+
execute: searchWeb,
|
|
130
|
+
},
|
|
131
|
+
analyzeData: {
|
|
132
|
+
description: "Analyze a piece of data",
|
|
133
|
+
inputSchema: z.object({ data: z.string() }),
|
|
134
|
+
execute: analyzeData,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await agent.stream({
|
|
140
|
+
messages,
|
|
141
|
+
writable,
|
|
142
|
+
preventClose: true,
|
|
143
|
+
maxSteps: 20,
|
|
144
|
+
prepareStep: ({ stepNumber }) => { // [!code highlight]
|
|
145
|
+
// Check stop flag before each agent step.
|
|
146
|
+
// Setting toolChoice to "none" prevents tool calls,
|
|
147
|
+
// causing the agent to generate a final response and exit.
|
|
148
|
+
if (stopRequested) {
|
|
149
|
+
return { toolChoice: "none" }; // [!code highlight]
|
|
150
|
+
}
|
|
151
|
+
return {};
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Clean up: close the stream
|
|
156
|
+
await closeStream();
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
messages: result.messages,
|
|
160
|
+
stopped: stopRequested,
|
|
161
|
+
stopReason,
|
|
162
|
+
stepsCompleted: result.steps.length,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### API Route to Trigger Stop
|
|
168
|
+
|
|
169
|
+
```typescript lineNumbers
|
|
170
|
+
import { stopHook } from "@/workflows/stoppable-agent";
|
|
171
|
+
|
|
172
|
+
export async function POST(
|
|
173
|
+
request: Request,
|
|
174
|
+
{ params }: { params: Promise<{ runId: string }> }
|
|
175
|
+
) {
|
|
176
|
+
const { runId } = await params;
|
|
177
|
+
const { reason } = await request.json();
|
|
178
|
+
|
|
179
|
+
await stopHook.resume(`stop:${runId}`, { // [!code highlight]
|
|
180
|
+
reason: reason || "User requested stop",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return Response.json({ success: true });
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Client Stop Button
|
|
188
|
+
|
|
189
|
+
```tsx lineNumbers
|
|
190
|
+
"use client";
|
|
191
|
+
|
|
192
|
+
export function StopButton({ runId }: { runId: string }) {
|
|
193
|
+
const handleStop = async () => {
|
|
194
|
+
await fetch(`/api/chat/${runId}/stop`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({ reason: "User clicked stop" }),
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<button type="button" onClick={handleStop}>
|
|
203
|
+
Stop Agent
|
|
204
|
+
</button>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Key APIs
|
|
210
|
+
|
|
211
|
+
- [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
|
|
212
|
+
- [`"use step"`](/docs/api-reference/workflow/use-step) — declares step functions with retries
|
|
213
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — type-safe hook for the stop signal
|
|
214
|
+
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) — access the run ID for deterministic hook tokens
|
|
215
|
+
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — stream output and close cleanly on stop
|
|
216
|
+
- [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — `prepareStep` callback to check stop flag before each step
|