workflow 5.0.0-beta.5 → 5.0.0-beta.51
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 +68 -23
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/internal/builtins.d.ts +20 -3
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +68 -4
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/dist/nest-builder.d.ts +2 -0
- package/dist/nest-builder.d.ts.map +1 -0
- package/dist/nest-builder.js +2 -0
- package/dist/nest-vercel-builder.d.ts +2 -0
- package/dist/nest-vercel-builder.d.ts.map +1 -0
- package/dist/nest-vercel-builder.js +2 -0
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +4 -1
- package/docs/ai/chat-session-modeling.mdx +176 -422
- package/docs/ai/defining-tools.mdx +6 -7
- package/docs/ai/human-in-the-loop.mdx +11 -11
- package/docs/ai/index.mdx +71 -75
- package/docs/ai/message-queueing.mdx +71 -110
- package/docs/ai/meta.json +1 -0
- package/docs/ai/resumable-streams.mdx +40 -28
- package/docs/ai/sleep-and-delays.mdx +10 -10
- package/docs/ai/streaming-updates-from-tools.mdx +6 -6
- package/docs/api-reference/index.mdx +25 -1
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +9 -15
- package/docs/api-reference/workflow/create-hook.mdx +89 -10
- package/docs/api-reference/workflow/create-webhook.mdx +16 -15
- package/docs/api-reference/workflow/define-hook.mdx +35 -33
- package/docs/api-reference/workflow/fatal-error.mdx +30 -8
- package/docs/api-reference/workflow/fetch.mdx +14 -10
- package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
- package/docs/api-reference/workflow/get-writable.mdx +7 -7
- package/docs/api-reference/workflow/index.mdx +4 -1
- package/docs/api-reference/workflow/retryable-error.mdx +1 -1
- package/docs/api-reference/workflow/set-attributes.mdx +61 -0
- package/docs/api-reference/workflow/sleep.mdx +4 -4
- package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
- package/docs/api-reference/workflow-ai/index.mdx +5 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +26 -12
- package/docs/api-reference/workflow-api/get-run.mdx +43 -8
- package/docs/api-reference/workflow-api/index.mdx +6 -10
- package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
- package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
- package/docs/api-reference/workflow-api/start.mdx +60 -13
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
- package/docs/api-reference/workflow-errors/index.mdx +88 -0
- package/docs/api-reference/workflow-errors/meta.json +6 -0
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
- package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
- package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
- package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
- package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +6 -6
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +5 -5
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
- package/docs/api-reference/workflow-globals.mdx +14 -10
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
- package/docs/api-reference/workflow-nitro/index.mdx +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +62 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
- package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
- package/docs/api-reference/workflow-runtime/index.mdx +41 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
- package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +98 -34
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
- package/docs/api-reference/workflow-serde/index.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +380 -0
- package/docs/changelog/batched-event-writes.mdx +79 -0
- package/docs/changelog/eager-processing.mdx +110 -436
- package/docs/changelog/index.mdx +4 -2
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/lazy-hook-resume.mdx +78 -0
- package/docs/changelog/meta.json +11 -1
- package/docs/changelog/resilient-resume.mdx +32 -0
- package/docs/changelog/resilient-start.mdx +33 -285
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/comparisons/index.mdx +66 -0
- package/docs/comparisons/meta.json +11 -0
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
- package/docs/configuration/build-and-diagnostics.mdx +70 -0
- package/docs/configuration/cli-and-web-ui.mdx +241 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +381 -0
- package/docs/configuration/worlds.mdx +313 -0
- package/docs/cookbook/advanced/child-workflows.mdx +211 -264
- package/docs/cookbook/advanced/meta.json +6 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
- package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
- package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
- package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -131
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
- package/docs/cookbook/common-patterns/batching.mdx +18 -14
- package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
- package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
- package/docs/cookbook/common-patterns/saga.mdx +23 -19
- package/docs/cookbook/common-patterns/scheduling.mdx +34 -22
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
- package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
- package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
- package/docs/cookbook/common-patterns/workflow-composition.mdx +30 -27
- package/docs/cookbook/index.mdx +22 -21
- package/docs/cookbook/integrations/ai-sdk.mdx +86 -48
- package/docs/cookbook/integrations/chat-sdk.mdx +50 -33
- package/docs/cookbook/integrations/sandbox.mdx +62 -45
- package/docs/deploying.mdx +95 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
- package/docs/errors/corrupted-event-log.mdx +39 -18
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +15 -14
- package/docs/errors/hook-conflict.mdx +69 -13
- package/docs/errors/index.mdx +2 -36
- package/docs/errors/node-js-module-in-workflow.mdx +9 -5
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/run-expired.mdx +85 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/serialization-failed.mdx +44 -12
- package/docs/errors/start-invalid-workflow-function.mdx +9 -5
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +6 -6
- package/docs/errors/timeout-in-workflow.mdx +12 -8
- package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
- package/docs/errors/webhook-response-not-sent.mdx +20 -16
- package/docs/errors/workflow-not-registered.mdx +5 -5
- package/docs/foundations/cancellation.mdx +31 -32
- package/docs/foundations/errors-and-retries.mdx +42 -11
- package/docs/foundations/hooks.mdx +64 -35
- package/docs/foundations/idempotency.mdx +244 -12
- package/docs/foundations/index.mdx +1 -23
- package/docs/foundations/meta.json +2 -1
- package/docs/foundations/serialization.mdx +21 -22
- package/docs/foundations/starting-workflows.mdx +106 -30
- package/docs/foundations/streaming.mdx +108 -60
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/foundations/workflows-and-steps.mdx +9 -9
- package/docs/getting-started/astro.mdx +22 -18
- package/docs/getting-started/express.mdx +15 -11
- package/docs/getting-started/fastify.mdx +15 -11
- package/docs/getting-started/hono.mdx +15 -11
- package/docs/getting-started/index.mdx +10 -3
- package/docs/getting-started/meta.json +3 -1
- package/docs/getting-started/nestjs.mdx +87 -20
- package/docs/getting-started/next.mdx +22 -16
- package/docs/getting-started/nitro.mdx +22 -18
- package/docs/getting-started/nuxt.mdx +15 -11
- package/docs/getting-started/python.mdx +190 -41
- package/docs/getting-started/react-router/index.mdx +33 -0
- package/docs/getting-started/react-router/meta.json +5 -0
- package/docs/getting-started/react-router/v7.mdx +237 -0
- package/docs/getting-started/react-router/v8.mdx +232 -0
- package/docs/getting-started/sveltekit.mdx +20 -16
- package/docs/getting-started/tanstack-start.mdx +17 -13
- package/docs/getting-started/vite.mdx +15 -11
- package/docs/how-it-works/cancellation.mdx +63 -63
- package/docs/how-it-works/code-transform.mdx +83 -67
- package/docs/how-it-works/encryption.mdx +30 -26
- package/docs/how-it-works/event-sourcing.mdx +125 -34
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- package/docs/how-it-works/understanding-directives.mdx +22 -22
- package/docs/internal/index.mdx +6 -4
- package/docs/internal/meta.json +6 -1
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +7 -7
- package/docs/meta.json +3 -2
- package/docs/observability/attributes.mdx +134 -0
- package/docs/observability/index.mdx +32 -10
- package/docs/observability/meta.json +1 -1
- package/docs/observability/retention.mdx +95 -0
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +36 -36
- package/docs/testing/server-based.mdx +10 -10
- package/docs/whats-new.mdx +186 -0
- package/package.json +17 -14
- package/docs/api-reference/workflow-api/world/index.mdx +0 -58
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/index.mdx +0 -95
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -84
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -224
- package/docs/deploying/world/vercel-world.mdx +0 -179
- package/docs/migration-guides/index.mdx +0 -34
- package/docs/migration-guides/meta.json +0 -9
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Chat Session Modeling
|
|
3
|
-
description: Model chat sessions at different architectural layers to control state ownership and handle interruptions.
|
|
3
|
+
description: Model WorkflowAgent chat sessions at different architectural layers to control state ownership and handle interruptions.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Choose between single-turn and multi-turn
|
|
5
|
+
summary: Choose between single-turn and multi-turn WorkflowAgent patterns for managing chat session state.
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/ai
|
|
8
8
|
- /docs/foundations/workflows-and-steps
|
|
@@ -10,525 +10,278 @@ related:
|
|
|
10
10
|
- /docs/ai/message-queueing
|
|
11
11
|
- /docs/ai/resumable-streams
|
|
12
12
|
- /docs/foundations/hooks
|
|
13
|
-
- /docs/api-reference/workflow-ai/durable-agent
|
|
14
13
|
- /docs/api-reference/workflow/define-hook
|
|
15
14
|
---
|
|
16
15
|
|
|
17
|
-
Chat sessions
|
|
16
|
+
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.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
Workflow 5 applications should use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for both patterns.
|
|
20
19
|
|
|
21
|
-
## Single-
|
|
20
|
+
## Single-turn workflows
|
|
22
21
|
|
|
23
|
-
Each user
|
|
22
|
+
Each user turn starts a new workflow run. The client or API owns conversation history and sends the complete `UIMessage[]` array with every request.
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
### Workflow
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
Convert UI messages to model messages inside the workflow. `WorkflowAgent` writes durable `ModelCallStreamPart` values to the run stream.
|
|
28
27
|
|
|
29
|
-
```typescript title="workflows/chat
|
|
30
|
-
import {
|
|
28
|
+
```typescript title="workflows/chat.ts" lineNumbers
|
|
29
|
+
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
|
30
|
+
import { convertToModelMessages, type UIMessage } from "ai";
|
|
31
31
|
import { getWritable } from "workflow";
|
|
32
|
-
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./
|
|
33
|
-
import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
|
|
32
|
+
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
|
|
34
33
|
|
|
35
34
|
export async function chat(messages: UIMessage[]) {
|
|
36
35
|
"use workflow";
|
|
37
36
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const agent = new DurableAgent({
|
|
41
|
-
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
|
37
|
+
const agent = new WorkflowAgent({
|
|
38
|
+
model: "spacexai/grok-4.6",
|
|
42
39
|
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
43
40
|
tools: flightBookingTools,
|
|
44
41
|
});
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
messages: await convertToModelMessages(messages),
|
|
48
|
-
writable,
|
|
43
|
+
return agent.stream({
|
|
44
|
+
messages: await convertToModelMessages(messages),
|
|
45
|
+
writable: getWritable<ModelCallStreamPart>(),
|
|
49
46
|
});
|
|
50
47
|
}
|
|
51
48
|
```
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
### API route
|
|
54
51
|
|
|
55
|
-
|
|
52
|
+
Convert the durable model-call stream to AI SDK UI chunks at the response boundary:
|
|
56
53
|
|
|
57
54
|
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
55
|
+
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
|
58
56
|
import { createUIMessageStreamResponse, type UIMessage } from "ai";
|
|
59
57
|
import { start } from "workflow/api";
|
|
60
58
|
import { chat } from "@/workflows/chat";
|
|
61
59
|
|
|
62
|
-
export async function POST(
|
|
63
|
-
const { messages }: { messages: UIMessage[] } = await
|
|
64
|
-
|
|
65
|
-
const run = await start(chat, [messages]); // [!code highlight]
|
|
60
|
+
export async function POST(request: Request) {
|
|
61
|
+
const { messages }: { messages: UIMessage[] } = await request.json();
|
|
62
|
+
const run = await start(chat, [messages]);
|
|
66
63
|
|
|
67
64
|
return createUIMessageStreamResponse({
|
|
68
|
-
stream: run.readable,
|
|
69
|
-
headers: {
|
|
70
|
-
"x-workflow-run-id": run.runId, // [!code highlight] For stream reconnection
|
|
71
|
-
},
|
|
65
|
+
stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
|
|
66
|
+
headers: { "x-workflow-run-id": run.runId },
|
|
72
67
|
});
|
|
73
68
|
}
|
|
74
69
|
```
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<Tab value="Client">
|
|
71
|
+
### Client
|
|
79
72
|
|
|
80
|
-
|
|
73
|
+
`WorkflowChatTransport` reconnects when the HTTP connection ends before the workflow stream finishes:
|
|
81
74
|
|
|
82
|
-
```
|
|
75
|
+
```tsx title="app/chat.tsx" lineNumbers
|
|
83
76
|
"use client";
|
|
84
77
|
|
|
85
78
|
import { useChat } from "@ai-sdk/react";
|
|
86
|
-
import { WorkflowChatTransport } from "@workflow
|
|
87
|
-
import { useParams } from "next/navigation";
|
|
79
|
+
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
|
88
80
|
import { useMemo } from "react";
|
|
89
81
|
|
|
90
|
-
// Fetch existing messages from your backend
|
|
91
|
-
async function getMessages(sessionId: string) { // [!code highlight]
|
|
92
|
-
const res = await fetch(`/api/chats/${sessionId}/messages`); // [!code highlight]
|
|
93
|
-
return res.json(); // [!code highlight]
|
|
94
|
-
} // [!code highlight]
|
|
95
|
-
|
|
96
82
|
export function Chat({ initialMessages }) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Persist the updated messages to the chat session // [!code highlight]
|
|
105
|
-
await fetch(`/api/chats/${sessionId}/messages`, { // [!code highlight]
|
|
106
|
-
method: "PUT", // [!code highlight]
|
|
107
|
-
headers: { "Content-Type": "application/json" }, // [!code highlight]
|
|
108
|
-
body: JSON.stringify({ messages }), // [!code highlight]
|
|
109
|
-
}); // [!code highlight]
|
|
110
|
-
}, // [!code highlight]
|
|
111
|
-
}), // [!code highlight]
|
|
112
|
-
[sessionId] // [!code highlight]
|
|
113
|
-
); // [!code highlight]
|
|
114
|
-
|
|
115
|
-
const { messages, input, handleInputChange, handleSubmit } = useChat({
|
|
116
|
-
initialMessages, // [!code highlight] Loaded via getMessages(sessionId)
|
|
117
|
-
transport, // [!code highlight]
|
|
83
|
+
const transport = useMemo(
|
|
84
|
+
() => new WorkflowChatTransport({ api: "/api/chat" }),
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
87
|
+
const { messages, sendMessage } = useChat({
|
|
88
|
+
messages: initialMessages,
|
|
89
|
+
transport,
|
|
118
90
|
});
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
<form onSubmit={handleSubmit}>
|
|
122
|
-
{/* ... render messages ... */}
|
|
123
|
-
<input value={input} onChange={handleInputChange} />
|
|
124
|
-
</form>
|
|
125
|
-
);
|
|
92
|
+
// Render messages and call sendMessage({ text }) from your form.
|
|
126
93
|
}
|
|
127
94
|
```
|
|
128
95
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</Tabs>
|
|
132
|
-
|
|
133
|
-
This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.
|
|
134
|
-
|
|
135
|
-
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.
|
|
136
|
-
|
|
137
|
-
Persisting the turn is usually done through either:
|
|
138
|
-
|
|
139
|
-
- A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
|
|
140
|
-
- A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
|
|
141
|
-
- The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
|
|
142
|
-
- Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
|
|
96
|
+
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.
|
|
143
97
|
|
|
144
|
-
|
|
98
|
+
Use the single-turn pattern when:
|
|
145
99
|
|
|
146
|
-
|
|
100
|
+
- Your application already owns chat history
|
|
101
|
+
- Each turn should run on the latest deployment
|
|
102
|
+
- You want a simple request-to-run mapping
|
|
103
|
+
- Approval responses or client-side tool results arrive as another message turn
|
|
147
104
|
|
|
148
|
-
|
|
105
|
+
## Multi-turn workflows
|
|
149
106
|
|
|
150
|
-
A
|
|
107
|
+
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.
|
|
151
108
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
109
|
+
```typescript title="workflows/chat-session.ts" lineNumbers
|
|
110
|
+
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
|
|
111
|
+
import { type ModelMessage } from "ai";
|
|
112
|
+
import { defineHook, getWorkflowMetadata, getWritable } from "workflow";
|
|
113
|
+
import { z } from "zod";
|
|
114
|
+
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
|
|
155
115
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
type UIMessageChunk,
|
|
160
|
-
type UIMessage,
|
|
161
|
-
type ModelMessage,
|
|
162
|
-
} from "ai";
|
|
163
|
-
import { DurableAgent } from "@workflow/ai/agent";
|
|
164
|
-
import { getWritable, getWorkflowMetadata } from "workflow";
|
|
165
|
-
import { chatMessageHook } from "./hooks/chat-message";
|
|
166
|
-
import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
|
|
167
|
-
import { writeUserMessageMarker, writeStreamClose } from "./steps/writer"; // [!code highlight]
|
|
116
|
+
export const chatMessageHook = defineHook({
|
|
117
|
+
schema: z.object({ message: z.string() }),
|
|
118
|
+
});
|
|
168
119
|
|
|
169
|
-
export async function
|
|
120
|
+
export async function chatSession(initialMessages: ModelMessage[]) {
|
|
170
121
|
"use workflow";
|
|
171
122
|
|
|
172
|
-
const { workflowRunId
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
// Write markers for initial user messages (for replay) // [!code highlight]
|
|
177
|
-
for (const msg of initialMessages) { // [!code highlight]
|
|
178
|
-
if (msg.role === "user") { // [!code highlight]
|
|
179
|
-
const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
|
|
180
|
-
if (text) await writeUserMessageMarker(writable, text, msg.id); // [!code highlight]
|
|
181
|
-
} // [!code highlight]
|
|
182
|
-
} // [!code highlight]
|
|
123
|
+
const { workflowRunId } = getWorkflowMetadata();
|
|
124
|
+
const hook = chatMessageHook.create({ token: workflowRunId });
|
|
125
|
+
const writable = getWritable<ModelCallStreamPart>();
|
|
126
|
+
let messages = [...initialMessages];
|
|
183
127
|
|
|
184
|
-
const agent = new
|
|
185
|
-
model: "
|
|
128
|
+
const agent = new WorkflowAgent({
|
|
129
|
+
model: "spacexai/grok-4.6",
|
|
186
130
|
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
187
131
|
tools: flightBookingTools,
|
|
188
132
|
});
|
|
189
133
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let turnNumber = 0;
|
|
193
|
-
|
|
194
|
-
while (true) {
|
|
195
|
-
turnNumber++;
|
|
134
|
+
const maxTurns = 100;
|
|
135
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
196
136
|
const result = await agent.stream({
|
|
197
137
|
messages,
|
|
198
138
|
writable,
|
|
199
|
-
preventClose: true,
|
|
200
|
-
sendStart: turnNumber === 1,
|
|
139
|
+
preventClose: true,
|
|
201
140
|
sendFinish: false,
|
|
202
141
|
});
|
|
203
|
-
messages
|
|
142
|
+
messages = result.messages;
|
|
204
143
|
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
if (followUp === "/done") break;
|
|
144
|
+
// Do not accept a follow-up that this run has no remaining turn to process.
|
|
145
|
+
if (turn === maxTurns - 1) break;
|
|
208
146
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
messages.push({ role: "user", content: followUp });
|
|
147
|
+
const { message } = await hook;
|
|
148
|
+
if (message === "/done") break;
|
|
149
|
+
messages = [...messages, { role: "user", content: message }];
|
|
213
150
|
}
|
|
214
151
|
|
|
215
|
-
await writeStreamClose(writable); // [!code highlight]
|
|
216
152
|
return { messages };
|
|
217
153
|
}
|
|
218
154
|
```
|
|
219
155
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
```typescript title="workflows/chat/steps/writer.ts" lineNumbers
|
|
223
|
-
import type { UIMessageChunk } from "ai";
|
|
224
|
-
|
|
225
|
-
export async function writeUserMessageMarker( // [!code highlight]
|
|
226
|
-
writable: WritableStream<UIMessageChunk>,
|
|
227
|
-
content: string,
|
|
228
|
-
messageId: string
|
|
229
|
-
) {
|
|
230
|
-
"use step"; // [!code highlight]
|
|
231
|
-
const writer = writable.getWriter();
|
|
232
|
-
try {
|
|
233
|
-
await writer.write({
|
|
234
|
-
type: "data-workflow", // [!code highlight]
|
|
235
|
-
data: { type: "user-message", id: messageId, content, timestamp: Date.now() }, // [!code highlight]
|
|
236
|
-
} as UIMessageChunk);
|
|
237
|
-
} finally {
|
|
238
|
-
writer.releaseLock();
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export async function writeStreamClose(writable: WritableStream<UIMessageChunk>) {
|
|
243
|
-
const writer = writable.getWriter();
|
|
244
|
-
await writer.write({ type: "finish" });
|
|
245
|
-
await writer.close();
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
</Tab>
|
|
156
|
+
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.
|
|
250
157
|
|
|
251
|
-
|
|
158
|
+
### Start and resume the session
|
|
252
159
|
|
|
253
|
-
|
|
160
|
+
Use one route to create the run and a second route to deliver follow-up messages:
|
|
254
161
|
|
|
255
162
|
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
256
|
-
import {
|
|
163
|
+
import { convertToModelMessages, type UIMessage } from "ai";
|
|
257
164
|
import { start } from "workflow/api";
|
|
258
|
-
import {
|
|
259
|
-
|
|
260
|
-
export async function POST(req: Request) {
|
|
261
|
-
const { initialMessage }: { initialMessage: UIMessage } = await req.json();
|
|
165
|
+
import { chatSession } from "@/workflows/chat-session";
|
|
262
166
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
headers: {
|
|
268
|
-
"x-workflow-run-id": run.runId, // [!code highlight] For follow-ups and reconnection
|
|
269
|
-
},
|
|
270
|
-
});
|
|
167
|
+
export async function POST(request: Request) {
|
|
168
|
+
const { messages }: { messages: UIMessage[] } = await request.json();
|
|
169
|
+
const run = await start(chatSession, [await convertToModelMessages(messages)]);
|
|
170
|
+
return Response.json({ runId: run.runId });
|
|
271
171
|
}
|
|
272
172
|
```
|
|
273
173
|
|
|
274
|
-
```typescript title="app/api/chat/[
|
|
275
|
-
import { chatMessageHook } from "@/workflows/chat
|
|
174
|
+
```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
|
|
175
|
+
import { chatMessageHook } from "@/workflows/chat-session";
|
|
276
176
|
|
|
277
177
|
export async function POST(
|
|
278
|
-
|
|
279
|
-
{ params }: { params: Promise<{
|
|
178
|
+
request: Request,
|
|
179
|
+
{ params }: { params: Promise<{ runId: string }> }
|
|
280
180
|
) {
|
|
281
|
-
const {
|
|
282
|
-
const { message } = await
|
|
283
|
-
|
|
284
|
-
// Resume the hook using the workflow run ID // [!code highlight]
|
|
285
|
-
await chatMessageHook.resume(runId, { message }); // [!code highlight]
|
|
286
|
-
|
|
181
|
+
const { runId } = await params;
|
|
182
|
+
const { message }: { message: string } = await request.json();
|
|
183
|
+
await chatMessageHook.resume(runId, { message });
|
|
287
184
|
return Response.json({ success: true });
|
|
288
185
|
}
|
|
289
186
|
```
|
|
290
187
|
|
|
291
|
-
|
|
188
|
+
### Reconnect to the stream
|
|
189
|
+
|
|
190
|
+
`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()`:
|
|
191
|
+
|
|
192
|
+
{/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
|
|
193
|
+
```typescript title="app/api/chat/[runId]/stream/route.ts" lineNumbers
|
|
194
|
+
import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
|
|
292
195
|
import { createUIMessageStreamResponse } from "ai";
|
|
293
196
|
import { getRun } from "workflow/api";
|
|
294
197
|
|
|
295
198
|
export async function GET(
|
|
296
199
|
request: Request,
|
|
297
|
-
{ params }: { params: Promise<{
|
|
200
|
+
{ params }: { params: Promise<{ runId: string }> }
|
|
298
201
|
) {
|
|
299
|
-
const {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const run = getRun(id); // [!code highlight]
|
|
304
|
-
const stream = run.getReadable({ // [!code highlight]
|
|
305
|
-
startIndex: startIndex ? parseInt(startIndex, 10) : undefined, // [!code highlight]
|
|
306
|
-
}); // [!code highlight]
|
|
307
|
-
|
|
308
|
-
return createUIMessageStreamResponse({ stream });
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
</Tab>
|
|
313
|
-
|
|
314
|
-
<Tab value="Hook Definition">
|
|
315
|
-
|
|
316
|
-
```typescript title="workflows/chat/hooks/chat-message.ts" lineNumbers
|
|
317
|
-
import { defineHook } from "workflow";
|
|
318
|
-
import { z } from "zod";
|
|
319
|
-
|
|
320
|
-
export const chatMessageHook = defineHook({
|
|
321
|
-
schema: z.object({
|
|
322
|
-
message: z.string(),
|
|
323
|
-
}),
|
|
324
|
-
});
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
</Tab>
|
|
328
|
-
|
|
329
|
-
<Tab value="Client Hook">
|
|
330
|
-
|
|
331
|
-
A custom hook wraps `useChat` to manage the multi-turn session. It handles:
|
|
332
|
-
|
|
333
|
-
- Routing between the initial message endpoint and follow-up endpoint
|
|
334
|
-
- Reconstructing user messages from stream markers for correct ordering on replay
|
|
335
|
-
|
|
336
|
-
```typescript title="hooks/use-multi-turn-chat.ts" lineNumbers
|
|
337
|
-
"use client";
|
|
202
|
+
const { runId } = await params;
|
|
203
|
+
const startIndex = Number(
|
|
204
|
+
new URL(request.url).searchParams.get("startIndex") ?? "0"
|
|
205
|
+
);
|
|
338
206
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
207
|
+
if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
|
|
208
|
+
return Response.json(
|
|
209
|
+
{ error: "startIndex must be a non-negative safe integer" },
|
|
210
|
+
{ status: 400 }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
343
213
|
|
|
344
|
-
const
|
|
214
|
+
const run = getRun(runId);
|
|
215
|
+
const stream = run
|
|
216
|
+
.getReadable({ startIndex: 0 })
|
|
217
|
+
.pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex: startIndex }));
|
|
345
218
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
timestamp: number;
|
|
219
|
+
return createUIMessageStreamResponse({
|
|
220
|
+
stream,
|
|
221
|
+
headers: { "x-workflow-run-id": runId },
|
|
222
|
+
});
|
|
351
223
|
}
|
|
224
|
+
```
|
|
352
225
|
|
|
353
|
-
|
|
354
|
-
const [runId, setRunId] = useState<string | null>(null);
|
|
355
|
-
const [shouldResume, setShouldResume] = useState(false);
|
|
356
|
-
const userMessagesRef = useRef<Map<string, UIMessage>>(new Map());
|
|
226
|
+
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.
|
|
357
227
|
|
|
358
|
-
|
|
359
|
-
useEffect(() => {
|
|
360
|
-
const storedRunId = localStorage.getItem(STORAGE_KEY);
|
|
361
|
-
if (storedRunId) {
|
|
362
|
-
setRunId(storedRunId);
|
|
363
|
-
setShouldResume(true);
|
|
364
|
-
}
|
|
365
|
-
}, []);
|
|
228
|
+
### Persist and display the full session
|
|
366
229
|
|
|
367
|
-
|
|
368
|
-
() =>
|
|
369
|
-
new WorkflowChatTransport({
|
|
370
|
-
api: "/api/chat",
|
|
371
|
-
onChatSendMessage: (response) => {
|
|
372
|
-
const workflowRunId = response.headers.get("x-workflow-run-id");
|
|
373
|
-
if (workflowRunId) {
|
|
374
|
-
setRunId(workflowRunId);
|
|
375
|
-
localStorage.setItem(STORAGE_KEY, workflowRunId);
|
|
376
|
-
}
|
|
377
|
-
},
|
|
378
|
-
onChatEnd: () => {
|
|
379
|
-
setRunId(null);
|
|
380
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
381
|
-
userMessagesRef.current.clear();
|
|
382
|
-
},
|
|
383
|
-
prepareReconnectToStreamRequest: ({ api, ...rest }) => {
|
|
384
|
-
const storedRunId = localStorage.getItem(STORAGE_KEY);
|
|
385
|
-
if (!storedRunId) throw new Error("No active session");
|
|
386
|
-
return { ...rest, api: `/api/chat/${storedRunId}/stream` };
|
|
387
|
-
},
|
|
388
|
-
}),
|
|
389
|
-
[]
|
|
390
|
-
);
|
|
230
|
+
A multi-turn chat has three related records with different responsibilities:
|
|
391
231
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Reconstruct conversation order from stream markers // [!code highlight]
|
|
396
|
-
const messages = useMemo(() => { // [!code highlight]
|
|
397
|
-
const result: UIMessage[] = []; // [!code highlight]
|
|
398
|
-
const seenContent = new Set<string>(); // [!code highlight]
|
|
399
|
-
// [!code highlight]
|
|
400
|
-
// Collect content from optimistic user messages // [!code highlight]
|
|
401
|
-
for (const msg of rawMessages) { // [!code highlight]
|
|
402
|
-
if (msg.role === "user") { // [!code highlight]
|
|
403
|
-
const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
|
|
404
|
-
if (text) seenContent.add(text); // [!code highlight]
|
|
405
|
-
} // [!code highlight]
|
|
406
|
-
} // [!code highlight]
|
|
407
|
-
// [!code highlight]
|
|
408
|
-
for (const msg of rawMessages) { // [!code highlight]
|
|
409
|
-
if (msg.role === "user") { // [!code highlight]
|
|
410
|
-
result.push(msg); // [!code highlight]
|
|
411
|
-
continue; // [!code highlight]
|
|
412
|
-
} // [!code highlight]
|
|
413
|
-
// [!code highlight]
|
|
414
|
-
if (msg.role === "assistant") { // [!code highlight]
|
|
415
|
-
// Process parts in order, splitting on user-message markers // [!code highlight]
|
|
416
|
-
let currentParts: typeof msg.parts = []; // [!code highlight]
|
|
417
|
-
let partIndex = 0; // [!code highlight]
|
|
418
|
-
// [!code highlight]
|
|
419
|
-
for (const part of msg.parts) { // [!code highlight]
|
|
420
|
-
if (part.type === "data-workflow" && "data" in part) { // [!code highlight]
|
|
421
|
-
const data = part.data as UserMessageData; // [!code highlight]
|
|
422
|
-
if (data?.type === "user-message") { // [!code highlight]
|
|
423
|
-
// Flush accumulated assistant parts // [!code highlight]
|
|
424
|
-
if (currentParts.length > 0) { // [!code highlight]
|
|
425
|
-
result.push({ ...msg, id: `${msg.id}-${partIndex++}`, parts: currentParts }); // [!code highlight]
|
|
426
|
-
currentParts = []; // [!code highlight]
|
|
427
|
-
} // [!code highlight]
|
|
428
|
-
// Add user message if not duplicate // [!code highlight]
|
|
429
|
-
if (!seenContent.has(data.content)) { // [!code highlight]
|
|
430
|
-
seenContent.add(data.content); // [!code highlight]
|
|
431
|
-
result.push({ id: data.id, role: "user", parts: [{ type: "text", text: data.content }] }); // [!code highlight]
|
|
432
|
-
} // [!code highlight]
|
|
433
|
-
continue; // [!code highlight]
|
|
434
|
-
} // [!code highlight]
|
|
435
|
-
} // [!code highlight]
|
|
436
|
-
currentParts.push(part); // [!code highlight]
|
|
437
|
-
} // [!code highlight]
|
|
438
|
-
// [!code highlight]
|
|
439
|
-
if (currentParts.length > 0) { // [!code highlight]
|
|
440
|
-
result.push({ ...msg, id: partIndex > 0 ? `${msg.id}-${partIndex}` : msg.id, parts: currentParts }); // [!code highlight]
|
|
441
|
-
} // [!code highlight]
|
|
442
|
-
} // [!code highlight]
|
|
443
|
-
} // [!code highlight]
|
|
444
|
-
return result; // [!code highlight]
|
|
445
|
-
}, [rawMessages]); // [!code highlight]
|
|
446
|
-
|
|
447
|
-
// Route messages to appropriate endpoint
|
|
448
|
-
const sendMessage = useCallback(
|
|
449
|
-
async (text: string) => {
|
|
450
|
-
if (runId) {
|
|
451
|
-
// Follow-up: send via hook resumption // [!code highlight]
|
|
452
|
-
await fetch(`/api/chat/${runId}`, {
|
|
453
|
-
method: "POST",
|
|
454
|
-
headers: { "Content-Type": "application/json" },
|
|
455
|
-
body: JSON.stringify({ message: text }),
|
|
456
|
-
});
|
|
457
|
-
} else {
|
|
458
|
-
// First message: start new workflow
|
|
459
|
-
await baseSendMessage({ text, metadata: { createdAt: Date.now() } });
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
[runId, baseSendMessage]
|
|
463
|
-
);
|
|
232
|
+
1. **Workflow model history**: `ModelMessage[]` is the durable context sent back to the model on each turn.
|
|
233
|
+
2. **Workflow run stream**: `ModelCallStreamPart` values contain durable model and tool output for live delivery and reconnection.
|
|
234
|
+
3. **Application chat history**: `UIMessage[]` preserves user messages, display metadata, attachments, and application-specific parts.
|
|
464
235
|
|
|
465
|
-
|
|
466
|
-
if (runId) {
|
|
467
|
-
await fetch(`/api/chat/${runId}`, {
|
|
468
|
-
method: "POST",
|
|
469
|
-
headers: { "Content-Type": "application/json" },
|
|
470
|
-
body: JSON.stringify({ message: "/done" }),
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
setRunId(null);
|
|
474
|
-
setShouldResume(false);
|
|
475
|
-
localStorage.removeItem(STORAGE_KEY);
|
|
476
|
-
userMessagesRef.current.clear();
|
|
477
|
-
setMessages([]);
|
|
478
|
-
}, [runId, setMessages]);
|
|
479
|
-
|
|
480
|
-
return { messages, status, runId, sendMessage, endSession, stop };
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
</Tab>
|
|
485
|
-
|
|
486
|
-
</Tabs>
|
|
236
|
+
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.
|
|
487
237
|
|
|
488
|
-
|
|
238
|
+
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[]`.
|
|
489
239
|
|
|
490
|
-
|
|
240
|
+
<Callout type="info">
|
|
241
|
+
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.
|
|
242
|
+
</Callout>
|
|
491
243
|
|
|
492
|
-
|
|
493
|
-
2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
|
|
494
|
-
3. Deduplicating against optimistic sends from the initial message
|
|
244
|
+
Use the multi-turn pattern when:
|
|
495
245
|
|
|
496
|
-
|
|
246
|
+
- One workflow should own the session's model context
|
|
247
|
+
- Backend events or other users need to inject messages through Hooks
|
|
248
|
+
- Full-session tracing is more important than running every turn on the newest deployment
|
|
249
|
+
- The application is prepared to manage a long-lived run and stream cursor
|
|
497
250
|
|
|
498
|
-
## Choosing a
|
|
251
|
+
## Choosing a pattern
|
|
499
252
|
|
|
500
|
-
| Consideration | Single-
|
|
253
|
+
| Consideration | Single-turn | Multi-turn |
|
|
501
254
|
|--------------|-------------|------------|
|
|
502
|
-
| State ownership | Client or
|
|
503
|
-
|
|
|
255
|
+
| State ownership | Client or application database | Workflow for model context; application database for UI history |
|
|
256
|
+
| Deployment version | Latest deployment per turn | Deployment that started the run |
|
|
257
|
+
| Message injection | Start another run | Resume a Hook |
|
|
504
258
|
| Workflow complexity | Lower | Higher |
|
|
505
|
-
| Workflow time horizon |
|
|
506
|
-
| Observability scope | Per
|
|
259
|
+
| Workflow time horizon | One model turn | Hours or longer |
|
|
260
|
+
| Observability scope | Per turn | Full session |
|
|
507
261
|
|
|
508
|
-
**Multi-turn
|
|
262
|
+
**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.
|
|
509
263
|
|
|
510
|
-
**Single-turn works well when adapting existing
|
|
264
|
+
**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.
|
|
511
265
|
|
|
512
|
-
## Multiplayer
|
|
266
|
+
## Multiplayer chat sessions
|
|
513
267
|
|
|
514
|
-
The multi-turn pattern also
|
|
268
|
+
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.
|
|
515
269
|
|
|
516
|
-
<Tabs items={['System
|
|
270
|
+
<Tabs items={['System event', 'External service', 'Multiple users']}>
|
|
517
271
|
|
|
518
|
-
<Tab value="System
|
|
272
|
+
<Tab value="System event">
|
|
519
273
|
|
|
520
|
-
|
|
274
|
+
Scheduled tasks, background jobs, or database triggers can inject updates into an active conversation:
|
|
521
275
|
|
|
522
276
|
```typescript title="app/api/internal/flight-update/route.ts" lineNumbers
|
|
523
|
-
import { chatMessageHook } from "@/workflows/chat
|
|
277
|
+
import { chatMessageHook } from "@/workflows/chat-session";
|
|
524
278
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const { runId, flightNumber, newStatus } = await req.json();
|
|
279
|
+
export async function POST(request: Request) {
|
|
280
|
+
const { runId, flightNumber, newStatus } = await request.json();
|
|
528
281
|
|
|
529
|
-
await chatMessageHook.resume(runId, {
|
|
530
|
-
message: `[System] Flight ${flightNumber} status updated: ${newStatus}`,
|
|
531
|
-
});
|
|
282
|
+
await chatMessageHook.resume(runId, {
|
|
283
|
+
message: `[System] Flight ${flightNumber} status updated: ${newStatus}`,
|
|
284
|
+
});
|
|
532
285
|
|
|
533
286
|
return Response.json({ success: true });
|
|
534
287
|
}
|
|
@@ -536,20 +289,20 @@ export async function POST(req: Request) {
|
|
|
536
289
|
|
|
537
290
|
</Tab>
|
|
538
291
|
|
|
539
|
-
<Tab value="External
|
|
292
|
+
<Tab value="External service">
|
|
540
293
|
|
|
541
|
-
|
|
294
|
+
A third-party webhook can notify the conversation about an external event:
|
|
542
295
|
|
|
543
296
|
```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
|
|
544
|
-
import { chatMessageHook } from "@/workflows/chat
|
|
297
|
+
import { chatMessageHook } from "@/workflows/chat-session";
|
|
545
298
|
|
|
546
|
-
export async function POST(
|
|
547
|
-
const { runId, paymentStatus, amount } = await
|
|
299
|
+
export async function POST(request: Request) {
|
|
300
|
+
const { runId, paymentStatus, amount } = await request.json();
|
|
548
301
|
|
|
549
302
|
if (paymentStatus === "succeeded") {
|
|
550
|
-
await chatMessageHook.resume(runId, {
|
|
551
|
-
message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed
|
|
552
|
-
});
|
|
303
|
+
await chatMessageHook.resume(runId, {
|
|
304
|
+
message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed.`,
|
|
305
|
+
});
|
|
553
306
|
}
|
|
554
307
|
|
|
555
308
|
return Response.json({ received: true });
|
|
@@ -558,38 +311,39 @@ export async function POST(req: Request) {
|
|
|
558
311
|
|
|
559
312
|
</Tab>
|
|
560
313
|
|
|
561
|
-
<Tab value="Multiple
|
|
314
|
+
<Tab value="Multiple users">
|
|
562
315
|
|
|
563
|
-
Multiple
|
|
316
|
+
Multiple authenticated users can participate in the same workflow-owned session. Include attribution when resuming the Hook:
|
|
564
317
|
|
|
565
|
-
```typescript title="app/api/chat/[
|
|
566
|
-
import { chatMessageHook } from "@/workflows/chat
|
|
318
|
+
```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
|
|
319
|
+
import { chatMessageHook } from "@/workflows/chat-session";
|
|
567
320
|
import { getUser } from "@/lib/auth";
|
|
568
321
|
|
|
569
322
|
export async function POST(
|
|
570
|
-
|
|
571
|
-
{ params }: { params: Promise<{
|
|
323
|
+
request: Request,
|
|
324
|
+
{ params }: { params: Promise<{ runId: string }> }
|
|
572
325
|
) {
|
|
573
|
-
const {
|
|
574
|
-
const { message } = await
|
|
575
|
-
const user = await getUser(
|
|
326
|
+
const { runId } = await params;
|
|
327
|
+
const { message } = await request.json();
|
|
328
|
+
const user = await getUser(request);
|
|
576
329
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}); // [!code highlight]
|
|
330
|
+
await chatMessageHook.resume(runId, {
|
|
331
|
+
message: `[${user.name}] ${message}`,
|
|
332
|
+
});
|
|
581
333
|
|
|
582
334
|
return Response.json({ success: true });
|
|
583
335
|
}
|
|
584
336
|
```
|
|
585
337
|
|
|
338
|
+
To preserve structured attribution across refreshes, persist the corresponding `UIMessage` using the application-history approach described above.
|
|
339
|
+
|
|
586
340
|
</Tab>
|
|
587
341
|
|
|
588
342
|
</Tabs>
|
|
589
343
|
|
|
590
|
-
## Related
|
|
344
|
+
## Related documentation
|
|
591
345
|
|
|
592
|
-
- [Building Durable AI Agents](/docs/ai)
|
|
593
|
-
- [Message Queueing](/docs/ai/message-queueing)
|
|
594
|
-
- [
|
|
595
|
-
- [`
|
|
346
|
+
- [Building Durable AI Agents](/docs/ai): Foundation guide for WorkflowAgent
|
|
347
|
+
- [Message Queueing](/docs/ai/message-queueing): Inject messages between model-call steps
|
|
348
|
+
- [Resumable Streams](/docs/ai/resumable-streams): Reconnect to durable output
|
|
349
|
+
- [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents
|