workflow 4.2.0-beta.70 → 4.2.0-beta.72
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/dist/api.d.ts +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -1
- package/dist/internal/builtins.d.ts +3 -3
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +7 -7
- 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/observability.d.ts +20 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +20 -0
- package/docs/ai/chat-session-modeling.mdx +4 -4
- package/docs/ai/defining-tools.mdx +7 -1
- package/docs/ai/index.mdx +8 -5
- package/docs/ai/message-queueing.mdx +8 -6
- package/docs/ai/resumable-streams.mdx +37 -4
- package/docs/ai/sleep-and-delays.mdx +2 -0
- package/docs/api-reference/index.mdx +3 -0
- package/docs/api-reference/meta.json +1 -1
- package/docs/api-reference/workflow/define-hook.mdx +2 -0
- package/docs/api-reference/workflow/get-writable.mdx +1 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +2 -0
- package/docs/api-reference/workflow-api/get-run.mdx +14 -0
- package/docs/api-reference/workflow-api/get-world.mdx +105 -0
- package/docs/api-reference/workflow-api/start.mdx +24 -0
- package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +90 -0
- package/docs/api-reference/workflow-errors/meta.json +16 -0
- package/docs/api-reference/workflow-errors/run-expired-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/throttle-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/too-early-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +57 -0
- package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +62 -0
- package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +56 -0
- package/docs/api-reference/workflow-errors/workflow-world-error.mdx +79 -0
- package/docs/api-reference/workflow-serde/index.mdx +52 -0
- package/docs/api-reference/workflow-serde/meta.json +3 -0
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +70 -0
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +75 -0
- package/docs/changelog/index.mdx +15 -0
- package/docs/changelog/meta.json +5 -0
- package/docs/deploying/building-a-world.mdx +20 -0
- package/docs/deploying/world/vercel-world.mdx +2 -1
- package/docs/errors/hook-conflict.mdx +9 -3
- package/docs/errors/index.mdx +6 -0
- package/docs/errors/step-not-registered.mdx +66 -0
- package/docs/errors/webhook-invalid-respond-with-value.mdx +10 -0
- package/docs/errors/webhook-response-not-sent.mdx +8 -0
- package/docs/errors/workflow-not-registered.mdx +64 -0
- package/docs/foundations/common-patterns.mdx +4 -0
- package/docs/foundations/errors-and-retries.mdx +29 -0
- package/docs/foundations/serialization.mdx +211 -0
- package/docs/foundations/streaming.mdx +22 -0
- package/docs/getting-started/index.mdx +3 -3
- package/docs/getting-started/meta.json +15 -0
- package/docs/getting-started/nestjs.mdx +4 -8
- package/docs/how-it-works/encryption.mdx +93 -0
- package/docs/how-it-works/meta.json +2 -1
- package/docs/observability/index.mdx +3 -0
- package/package.json +18 -12
package/dist/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
|
|
1
|
+
export { type Event, getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, type StartOptions, start, type StopSleepOptions, type StopSleepResult, type WorkflowReadableStream, type WorkflowReadableStreamOptions, type WorkflowRun, } from '@workflow/core/runtime';
|
|
2
2
|
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,KAAK,EACV,cAAc,EACd,MAAM,EACN,GAAG,EACH,UAAU,EACV,aAAa,EACb,OAAO,EACP,KAAK,YAAY,EACjB,KAAK,EACL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAClC,KAAK,WAAW,GACjB,MAAM,wBAAwB,CAAC"}
|
package/dist/api.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { getHookByToken, getRun, Run, resumeHook, resumeWebhook, runStep, start, } from '@workflow/core/runtime';
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXBpLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2FwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBRUwsY0FBYyxFQUNkLE1BQU0sRUFDTixHQUFHLEVBQ0gsVUFBVSxFQUNWLGFBQWEsRUFDYixPQUFPLEVBRVAsS0FBSyxHQU1OLE1BQU0sd0JBQXdCLENBQUMifQ==
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* similar to "stdlib" except that are not meant to be imported by users, but are instead "just available"
|
|
4
4
|
* alongside user defined steps. They are used internally by the runtime
|
|
5
5
|
*/
|
|
6
|
-
export declare function __builtin_response_array_buffer(
|
|
7
|
-
export declare function __builtin_response_json(
|
|
8
|
-
export declare function __builtin_response_text(
|
|
6
|
+
export declare function __builtin_response_array_buffer(this: Request | Response): Promise<ArrayBuffer>;
|
|
7
|
+
export declare function __builtin_response_json(this: Request | Response): Promise<unknown>;
|
|
8
|
+
export declare function __builtin_response_text(this: Request | Response): Promise<string>;
|
|
9
9
|
//# sourceMappingURL=builtins.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/internal/builtins.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAsB,+BAA+B,
|
|
1
|
+
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../../src/internal/builtins.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,OAAO,GAAG,QAAQ,wBAIzB;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,oBAGrE;AAED,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,mBAGrE"}
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* similar to "stdlib" except that are not meant to be imported by users, but are instead "just available"
|
|
4
4
|
* alongside user defined steps. They are used internally by the runtime
|
|
5
5
|
*/
|
|
6
|
-
export async function __builtin_response_array_buffer(
|
|
6
|
+
export async function __builtin_response_array_buffer() {
|
|
7
7
|
'use step';
|
|
8
|
-
return
|
|
8
|
+
return this.arrayBuffer();
|
|
9
9
|
}
|
|
10
|
-
export async function __builtin_response_json(
|
|
10
|
+
export async function __builtin_response_json() {
|
|
11
11
|
'use step';
|
|
12
|
-
return
|
|
12
|
+
return this.json();
|
|
13
13
|
}
|
|
14
|
-
export async function __builtin_response_text(
|
|
14
|
+
export async function __builtin_response_text() {
|
|
15
15
|
'use step';
|
|
16
|
-
return
|
|
16
|
+
return this.text();
|
|
17
17
|
}
|
|
18
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
18
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVpbHRpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW50ZXJuYWwvYnVpbHRpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7R0FJRztBQUVILE1BQU0sQ0FBQyxLQUFLLFVBQVUsK0JBQStCO0lBR25ELFVBQVUsQ0FBQztJQUNYLE9BQU8sSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO0FBQzVCLENBQUM7QUFFRCxNQUFNLENBQUMsS0FBSyxVQUFVLHVCQUF1QjtJQUMzQyxVQUFVLENBQUM7SUFDWCxPQUFPLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUNyQixDQUFDO0FBRUQsTUFBTSxDQUFDLEtBQUssVUFBVSx1QkFBdUI7SUFDM0MsVUFBVSxDQUFDO0lBQ1gsT0FBTyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7QUFDckIsQ0FBQyJ9
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { EntityConflictError, HookConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
2
2
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/internal/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,yBAAyB,EACzB,sBAAsB,EACtB,4BAA4B,EAC5B,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,kBAAkB,CAAC"}
|
package/dist/internal/errors.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
1
|
+
export { EntityConflictError, HookConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, StepNotRegisteredError, ThrottleError, TooEarlyError, WorkflowError, WorkflowNotRegisteredError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotCompletedError, WorkflowRunNotFoundError, WorkflowRuntimeError, WorkflowWorldError, } from '@workflow/errors';
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ludGVybmFsL2Vycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQ0wsbUJBQW1CLEVBQ25CLGlCQUFpQixFQUNqQixpQkFBaUIsRUFDakIsZUFBZSxFQUNmLG9CQUFvQixFQUNwQixzQkFBc0IsRUFDdEIsYUFBYSxFQUNiLGFBQWEsRUFDYixhQUFhLEVBQ2IsMEJBQTBCLEVBQzFCLHlCQUF5QixFQUN6QixzQkFBc0IsRUFDdEIsNEJBQTRCLEVBQzVCLHdCQUF3QixFQUN4QixvQkFBb0IsRUFDcEIsa0JBQWtCLEdBQ25CLE1BQU0sa0JBQWtCLENBQUMifQ==
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability utilities for hydrating serialized workflow data.
|
|
3
|
+
*
|
|
4
|
+
* Use these when inspecting workflow step I/O, run inputs/outputs,
|
|
5
|
+
* or event data from the Workflow SDK's world APIs.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getWorld } from 'workflow/api';
|
|
10
|
+
* import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
|
|
11
|
+
*
|
|
12
|
+
* const world = getWorld();
|
|
13
|
+
* const step = await world.steps.get(runId, stepId, { resolveData: 'all' });
|
|
14
|
+
* const hydrated = hydrateResourceIO(step, observabilityRevivers);
|
|
15
|
+
* // hydrated.input and hydrated.output are now plain JS objects
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export { hydrateData, hydrateResourceIO, observabilityRevivers, type Revivers, } from '@workflow/core/serialization-format';
|
|
19
|
+
export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
|
|
20
|
+
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,QAAQ,GACd,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,cAAc,EACd,aAAa,EACb,iBAAiB,GAClB,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability utilities for hydrating serialized workflow data.
|
|
3
|
+
*
|
|
4
|
+
* Use these when inspecting workflow step I/O, run inputs/outputs,
|
|
5
|
+
* or event data from the Workflow SDK's world APIs.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { getWorld } from 'workflow/api';
|
|
10
|
+
* import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
|
|
11
|
+
*
|
|
12
|
+
* const world = getWorld();
|
|
13
|
+
* const step = await world.steps.get(runId, stepId, { resolveData: 'all' });
|
|
14
|
+
* const hydrated = hydrateResourceIO(step, observabilityRevivers);
|
|
15
|
+
* // hydrated.input and hydrated.output are now plain JS objects
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export { hydrateData, hydrateResourceIO, observabilityRevivers, } from '@workflow/core/serialization-format';
|
|
19
|
+
export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
|
|
20
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2YWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vYnNlcnZhYmlsaXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsT0FBTyxFQUNMLFdBQVcsRUFDWCxpQkFBaUIsRUFDakIscUJBQXFCLEdBRXRCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUNMLGNBQWMsRUFDZCxhQUFhLEVBQ2IsaUJBQWlCLEdBQ2xCLE1BQU0saUJBQWlCLENBQUMifQ==
|
|
@@ -39,12 +39,12 @@ export async function chat(messages: UIMessage[]) {
|
|
|
39
39
|
|
|
40
40
|
const agent = new DurableAgent({
|
|
41
41
|
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
|
42
|
-
|
|
42
|
+
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
43
43
|
tools: flightBookingTools,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
await agent.stream({
|
|
47
|
-
messages: convertToModelMessages(messages), // [!code highlight] Full history from client
|
|
47
|
+
messages: await convertToModelMessages(messages), // [!code highlight] Full history from client
|
|
48
48
|
writable,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
@@ -171,7 +171,7 @@ export async function chat(initialMessages: UIMessage[]) {
|
|
|
171
171
|
|
|
172
172
|
const { workflowRunId: runId } = getWorkflowMetadata();
|
|
173
173
|
const writable = getWritable<UIMessageChunk>();
|
|
174
|
-
const messages: ModelMessage[] = convertToModelMessages(initialMessages);
|
|
174
|
+
const messages: ModelMessage[] = await convertToModelMessages(initialMessages);
|
|
175
175
|
|
|
176
176
|
// Write markers for initial user messages (for replay) // [!code highlight]
|
|
177
177
|
for (const msg of initialMessages) { // [!code highlight]
|
|
@@ -183,7 +183,7 @@ export async function chat(initialMessages: UIMessage[]) {
|
|
|
183
183
|
|
|
184
184
|
const agent = new DurableAgent({
|
|
185
185
|
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
|
186
|
-
|
|
186
|
+
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
187
187
|
tools: flightBookingTools,
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -23,9 +23,12 @@ Just like in regular AI SDK tool definitions, tool in DurableAgent are called wi
|
|
|
23
23
|
When you tool needs access to the full message history, you can access it via the `messages` property of the tool call context:
|
|
24
24
|
|
|
25
25
|
```typescript title="tools.ts" lineNumbers
|
|
26
|
+
import { Experimental_Agent as Agent } from "ai";
|
|
27
|
+
import type { ModelMessage } from "ai";
|
|
28
|
+
|
|
26
29
|
async function getWeather(
|
|
27
30
|
{ city }: { city: string },
|
|
28
|
-
{ messages, toolCallId }: { messages:
|
|
31
|
+
{ messages, toolCallId }: { messages: ModelMessage[], toolCallId: string }) { // [!code highlight]
|
|
29
32
|
"use step";
|
|
30
33
|
return `Weather in ${city} is sunny`;
|
|
31
34
|
}
|
|
@@ -65,6 +68,9 @@ Tools can be implemented either at the step level or the workflow level, with di
|
|
|
65
68
|
Tools can also combine both by starting out on the workflow level, and calling into steps for I/O operations, like so:
|
|
66
69
|
|
|
67
70
|
```typescript title="tools.ts" lineNumbers
|
|
71
|
+
import { sleep } from "workflow";
|
|
72
|
+
import type { LanguageModel, ModelMessage } from "ai";
|
|
73
|
+
|
|
68
74
|
// Step: handles I/O with retries
|
|
69
75
|
async function performFetch(url: string) {
|
|
70
76
|
"use step";
|
package/docs/ai/index.mdx
CHANGED
|
@@ -123,15 +123,18 @@ The core code that makes all of this happen is quite simple. Here's a breakdown
|
|
|
123
123
|
Our API route makes a simple call to [AI SDK's `Agent` class](https://ai-sdk.dev/docs/agents/overview), which is a simple wrapper around [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
|
|
124
124
|
|
|
125
125
|
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
126
|
+
import { Experimental_Agent as Agent } from "ai";
|
|
127
|
+
import type { LanguageModel } from "ai";
|
|
128
|
+
|
|
126
129
|
export async function POST(req: Request) {
|
|
127
130
|
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
128
131
|
const agent = new Agent({ // [!code highlight]
|
|
129
132
|
model: gateway("bedrock/claude-4-5-haiku-20251001-v1"),
|
|
130
|
-
|
|
133
|
+
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
131
134
|
tools: flightBookingTools,
|
|
132
135
|
});
|
|
133
|
-
const modelMessages = convertToModelMessages(messages);
|
|
134
|
-
const stream = agent.stream({ messages: modelMessages }); // [!code highlight]
|
|
136
|
+
const modelMessages = await convertToModelMessages(messages);
|
|
137
|
+
const stream = await agent.stream({ messages: modelMessages }); // [!code highlight]
|
|
135
138
|
return createUIMessageStreamResponse({
|
|
136
139
|
stream: stream.toUIMessageStream(),
|
|
137
140
|
});
|
|
@@ -273,7 +276,7 @@ export async function chatWorkflow(messages: ModelMessage[]) {
|
|
|
273
276
|
// ELSE if using a custom provider, pass the provider call as an argument:
|
|
274
277
|
model: openai("gpt-5.1"), // [!code highlight]
|
|
275
278
|
|
|
276
|
-
|
|
279
|
+
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
277
280
|
tools: flightBookingTools,
|
|
278
281
|
});
|
|
279
282
|
|
|
@@ -304,7 +307,7 @@ import { chatWorkflow } from "@/workflows/chat/workflow";
|
|
|
304
307
|
|
|
305
308
|
export async function POST(req: Request) {
|
|
306
309
|
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
307
|
-
const modelMessages = convertToModelMessages(messages);
|
|
310
|
+
const modelMessages = await convertToModelMessages(messages);
|
|
308
311
|
|
|
309
312
|
const run = await start(chatWorkflow, [modelMessages]); // [!code highlight]
|
|
310
313
|
|
|
@@ -32,16 +32,18 @@ If you just need basic multi-turn conversations where messages arrive between tu
|
|
|
32
32
|
The `prepareStep` callback runs before each step in the agent loop. It receives the current state and can modify the messages sent to the model:
|
|
33
33
|
|
|
34
34
|
```typescript lineNumbers
|
|
35
|
+
import type { ModelMessage, LanguageModel } from "ai";
|
|
36
|
+
|
|
35
37
|
interface PrepareStepInfo {
|
|
36
|
-
model: string | (() => Promise<
|
|
38
|
+
model: string | (() => Promise<LanguageModel>); // Current model
|
|
37
39
|
stepNumber: number; // 0-indexed step count
|
|
38
40
|
steps: StepResult[]; // Previous step results
|
|
39
|
-
messages:
|
|
41
|
+
messages: ModelMessage[]; // Messages to be sent
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
interface PrepareStepResult {
|
|
43
|
-
model?: string | (() => Promise<
|
|
44
|
-
messages?:
|
|
45
|
+
model?: string | (() => Promise<LanguageModel>); // Override model
|
|
46
|
+
messages?: ModelMessage[]; // Override messages
|
|
45
47
|
}
|
|
46
48
|
```
|
|
47
49
|
|
|
@@ -65,7 +67,7 @@ export async function chat(initialMessages: ModelMessage[]) {
|
|
|
65
67
|
|
|
66
68
|
const agent = new DurableAgent({
|
|
67
69
|
model: "bedrock/claude-haiku-4-5-20251001-v1",
|
|
68
|
-
|
|
70
|
+
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
69
71
|
tools: flightBookingTools,
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -101,7 +103,7 @@ export async function chat(initialMessages: ModelMessage[]) {
|
|
|
101
103
|
Messages sent via `chatMessageHook.resume()` accumulate in the queue and get injected before the next step, whether that's a tool call or another LLM request.
|
|
102
104
|
|
|
103
105
|
<Callout type="info">
|
|
104
|
-
The `prepareStep` callback receives messages in `
|
|
106
|
+
The `prepareStep` callback receives messages in `ModelMessage[]` format (with content arrays), which is the internal format used by the AI SDK.
|
|
105
107
|
</Callout>
|
|
106
108
|
|
|
107
109
|
## Combining with Multi-Turn Sessions
|
|
@@ -30,7 +30,8 @@ Let's add stream resumption to our Flight Booking Agent that we build in the [Bu
|
|
|
30
30
|
|
|
31
31
|
Modify your chat endpoint to include the workflow run ID in a response header. The Run ID uniquely identifies the run's stream, so it allows the client to know which stream to reconnect to.
|
|
32
32
|
|
|
33
|
-
{
|
|
33
|
+
{/*@skip-typecheck: incomplete code sample*/}
|
|
34
|
+
|
|
34
35
|
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
35
36
|
// ... imports ...
|
|
36
37
|
|
|
@@ -76,14 +77,24 @@ export async function GET(
|
|
|
76
77
|
|
|
77
78
|
// Instead of starting a new run, we fetch an existing run.
|
|
78
79
|
const run = getRun(id); // [!code highlight]
|
|
79
|
-
const
|
|
80
|
+
const readable = run.getReadable({ startIndex }); // [!code highlight]
|
|
81
|
+
|
|
82
|
+
// Provide the stream's tail index so the transport can resolve
|
|
83
|
+
// negative startIndex values into absolute positions for retries.
|
|
84
|
+
const tailIndex = await readable.getTailIndex(); // [!code highlight]
|
|
80
85
|
|
|
81
|
-
return createUIMessageStreamResponse({
|
|
86
|
+
return createUIMessageStreamResponse({
|
|
87
|
+
stream: readable, // [!code highlight]
|
|
88
|
+
headers: { // [!code highlight]
|
|
89
|
+
"x-workflow-stream-tail-index": String(tailIndex), // [!code highlight]
|
|
90
|
+
}, // [!code highlight]
|
|
91
|
+
});
|
|
82
92
|
}
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
The `startIndex` parameter ensures the client can choose where to resume the stream from. For instance, if the function times out during streaming, the chat transport will use `startIndex` to resume the stream exactly from the last token it received.
|
|
95
|
+
The `startIndex` parameter ensures the client can choose where to resume the stream from. For instance, if the function times out during streaming, the chat transport will use `startIndex` to resume the stream exactly from the last token it received. Negative values are also supported (e.g. `-5` starts 5 chunks before the end), which is useful for custom stream consumers (such as a dashboard showing recent output) that want to show the most recent output without replaying the full stream.
|
|
86
96
|
|
|
97
|
+
When using a negative `startIndex`, your stream endpoint must return a `x-workflow-stream-tail-index` header in order for relative resumption to work. Missing the header will fall back to replaying the entire stream.
|
|
87
98
|
</Step>
|
|
88
99
|
|
|
89
100
|
<Step>
|
|
@@ -161,6 +172,28 @@ Now try the flight booking example again. Open it up in a separate tab, or spam
|
|
|
161
172
|
|
|
162
173
|
This approach also handles page refreshes, as the client will automatically reconnect to the stream from the last known position when the UI loads with a stored run ID, following the behavior of [AI SDK's stream resumption](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-resume-streams#chatbot-resume-streams).
|
|
163
174
|
|
|
175
|
+
### Resuming from the end of the stream
|
|
176
|
+
|
|
177
|
+
By default, reconnecting replays the entire stream from the beginning (`startIndex: 0`). If you only need to show recent output — for example, when resuming a long conversation after a page refresh — you can set `initialStartIndex` to a negative value to read from the end of the stream instead:
|
|
178
|
+
|
|
179
|
+
{/*@skip-typecheck: incomplete code sample*/}
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const { messages, sendMessage } = useChat({
|
|
183
|
+
resume: !!activeWorkflowRunId,
|
|
184
|
+
transport: new WorkflowChatTransport({
|
|
185
|
+
initialStartIndex: -20, // Only fetch the last 20 chunks // [!code highlight]
|
|
186
|
+
// ... callbacks as above
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This avoids replaying potentially thousands of chunks and lets the UI render faster. The negative value is resolved server-side, so `-20` on a 500-chunk stream starts at chunk 480.
|
|
192
|
+
|
|
193
|
+
<Callout>
|
|
194
|
+
When using a negative `initialStartIndex`, the reconnection endpoint **must** return the `x-workflow-stream-tail-index` header (as shown in [Step 2](#add-a-stream-reconnection-endpoint) above). The transport uses this header to compute absolute chunk positions so that retries after a disconnect resume from the correct position. If the header is missing, the transport falls back to `startIndex: 0` (replaying the entire stream) and logs a warning.
|
|
195
|
+
</Callout>
|
|
196
|
+
|
|
164
197
|
## Related Documentation
|
|
165
198
|
|
|
166
199
|
- [`WorkflowChatTransport` API Reference](/docs/api-reference/workflow-ai/workflow-chat-transport) - Full configuration options
|
|
@@ -162,6 +162,8 @@ Aside from providing `sleep()` as a tool, there are other use cases for Agents t
|
|
|
162
162
|
When hitting API rate limits, use `RetryableError` with a delay:
|
|
163
163
|
|
|
164
164
|
```typescript lineNumbers
|
|
165
|
+
import { RetryableError } from "workflow";
|
|
166
|
+
|
|
165
167
|
async function callRateLimitedAPI(endpoint: string) {
|
|
166
168
|
"use step";
|
|
167
169
|
|
|
@@ -17,6 +17,9 @@ All the functions and primitives that come with Workflow DevKit by package.
|
|
|
17
17
|
<Card title="workflow/next" href="/docs/api-reference/workflow-next">
|
|
18
18
|
Next.js integration for Workflow DevKit that automatically configures bundling and runtime support.
|
|
19
19
|
</Card>
|
|
20
|
+
<Card title="@workflow/serde" href="/docs/api-reference/workflow-serde">
|
|
21
|
+
Serialization symbols for custom class serialization in workflows.
|
|
22
|
+
</Card>
|
|
20
23
|
<Card title="@workflow/ai" href="/docs/api-reference/workflow-ai">
|
|
21
24
|
Helpers for integrating AI SDK for building AI-powered workflows.
|
|
22
25
|
</Card>
|
|
@@ -193,6 +193,8 @@ export const approvalHook = defineHook({
|
|
|
193
193
|
Tokens are used to identify a specific hook and for resuming a hook. You can customize the token to be more specific to a use case.
|
|
194
194
|
|
|
195
195
|
```typescript lineNumbers
|
|
196
|
+
import { defineHook } from "workflow";
|
|
197
|
+
|
|
196
198
|
const slackHook = defineHook<{ text: string; userId: string }>();
|
|
197
199
|
|
|
198
200
|
export async function slackBotWorkflow(channelId: string) {
|
|
@@ -208,6 +208,7 @@ Here's a more complex example showing how you might stream AI chat responses:
|
|
|
208
208
|
```typescript lineNumbers
|
|
209
209
|
import { getWritable } from "workflow";
|
|
210
210
|
import { generateId, streamText, type UIMessageChunk } from "ai";
|
|
211
|
+
import type { ModelMessage } from "ai";
|
|
211
212
|
|
|
212
213
|
export async function chat(messages: ModelMessage[]) {
|
|
213
214
|
"use workflow";
|
|
@@ -34,7 +34,7 @@ async function myAgent() {
|
|
|
34
34
|
|
|
35
35
|
const agent = new DurableAgent({
|
|
36
36
|
model: "anthropic/claude-haiku-4.5",
|
|
37
|
-
|
|
37
|
+
instructions: "You are a helpful weather assistant.",
|
|
38
38
|
temperature: 0.7,
|
|
39
39
|
tools: {
|
|
40
40
|
getWeather: {
|
|
@@ -249,7 +249,7 @@ async function weatherAgentWorkflow(userQuery: string) {
|
|
|
249
249
|
execute: getWeather,
|
|
250
250
|
},
|
|
251
251
|
},
|
|
252
|
-
|
|
252
|
+
instructions: "You are a helpful weather assistant. Always provide accurate weather information.",
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
await agent.stream({
|
|
@@ -317,6 +317,8 @@ async function multiToolAgentWorkflow(userQuery: string) {
|
|
|
317
317
|
|
|
318
318
|
```typescript
|
|
319
319
|
import { DurableAgent } from "@workflow/ai/agent";
|
|
320
|
+
import { getWritable } from "workflow";
|
|
321
|
+
import type { UIMessageChunk } from "ai";
|
|
320
322
|
import { z } from "zod";
|
|
321
323
|
|
|
322
324
|
async function searchProducts({ query }: { query: string }) {
|
|
@@ -446,7 +448,7 @@ async function agentWithPrepareStep(userMessage: string) {
|
|
|
446
448
|
|
|
447
449
|
const agent = new DurableAgent({
|
|
448
450
|
model: "openai/gpt-4.1-mini", // Default model
|
|
449
|
-
|
|
451
|
+
instructions: "You are a helpful assistant.",
|
|
450
452
|
});
|
|
451
453
|
|
|
452
454
|
await agent.stream({
|
|
@@ -500,7 +502,7 @@ async function agentWithMessageQueue(initialMessage: string) {
|
|
|
500
502
|
|
|
501
503
|
const agent = new DurableAgent({
|
|
502
504
|
model: "anthropic/claude-haiku-4.5",
|
|
503
|
-
|
|
505
|
+
instructions: "You are a helpful assistant.",
|
|
504
506
|
});
|
|
505
507
|
|
|
506
508
|
await agent.stream({
|
|
@@ -812,7 +814,7 @@ async function agentWithUIMessages(userMessage: string) {
|
|
|
812
814
|
|
|
813
815
|
const agent = new DurableAgent({
|
|
814
816
|
model: "anthropic/claude-haiku-4.5",
|
|
815
|
-
|
|
817
|
+
instructions: "You are a helpful assistant.",
|
|
816
818
|
});
|
|
817
819
|
|
|
818
820
|
const result = await agent.stream({
|
|
@@ -71,6 +71,8 @@ export default WorkflowChatTransportOptions;`}
|
|
|
71
71
|
- The `onChatSendMessage` callback receives the full response object, allowing you to extract and store the workflow run ID for session resumption
|
|
72
72
|
- Stream interruptions are automatically detected when a "finish" chunk is not received in the initial response
|
|
73
73
|
- The `maxConsecutiveErrors` option controls how many reconnection attempts are made before giving up (default: 3)
|
|
74
|
+
- `initialStartIndex` (constructor option) sets the default chunk position for the **first** reconnection attempt (e.g. after a page refresh). Subsequent retries within the same reconnection loop always resume from the last received chunk. Negative values (e.g. `-20`) read from the end of the stream, which is useful for showing only recent output without replaying the full conversation. `startIndex` (per-call option on `reconnectToStream`) overrides `initialStartIndex` for a single reconnection
|
|
75
|
+
- When using a negative `initialStartIndex`, the reconnection endpoint must return the `x-workflow-stream-tail-index` response header (via `readable.getTailIndex()`). The transport reads this header to compute absolute chunk positions for retries. Without it, startIndex is assumed to be 0, replaying the entire stream
|
|
74
76
|
|
|
75
77
|
## Examples
|
|
76
78
|
|
|
@@ -39,6 +39,18 @@ export default Run;`}
|
|
|
39
39
|
showSections={["returns"]}
|
|
40
40
|
/>
|
|
41
41
|
|
|
42
|
+
#### WorkflowReadableStream
|
|
43
|
+
|
|
44
|
+
`run.getReadable()` returns a `WorkflowReadableStream` — a standard `ReadableStream` extended with a `getTailIndex()` helper:
|
|
45
|
+
|
|
46
|
+
<TSDoc
|
|
47
|
+
definition={`
|
|
48
|
+
import type { WorkflowReadableStream } from "workflow/api";
|
|
49
|
+
export default WorkflowReadableStream;`}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
`getTailIndex()` returns the index of the last known chunk (0-based), or `-1` when no chunks have been written. This is useful when building [reconnection endpoints](/docs/ai/resumable-streams) that need to inform clients where the stream starts.
|
|
53
|
+
|
|
42
54
|
#### WorkflowReadableStreamOptions
|
|
43
55
|
|
|
44
56
|
<TSDoc
|
|
@@ -144,6 +156,8 @@ export async function POST(req: Request) {
|
|
|
144
156
|
You can also target specific sleep calls by correlation ID:
|
|
145
157
|
|
|
146
158
|
```typescript lineNumbers
|
|
159
|
+
import { getRun } from "workflow/api";
|
|
160
|
+
|
|
147
161
|
const run = getRun("my-run-id"); // @setup
|
|
148
162
|
const { stoppedCount } = await run.wakeUp({
|
|
149
163
|
correlationIds: ["wait_abc123"],
|
|
@@ -91,6 +91,111 @@ export async function POST(req: Request) {
|
|
|
91
91
|
}
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### List Steps for a Run (Without Data)
|
|
95
|
+
|
|
96
|
+
List steps for a workflow run with `resolveData: 'none'` to efficiently get step metadata without fetching serialized input/output. Use `parseStepName` to extract user-friendly display names:
|
|
97
|
+
|
|
98
|
+
```typescript lineNumbers
|
|
99
|
+
import { getWorld } from "workflow/runtime";
|
|
100
|
+
import { parseStepName } from "@workflow/utils/parse-name"; // [!code highlight]
|
|
101
|
+
|
|
102
|
+
export async function GET(req: Request) {
|
|
103
|
+
const url = new URL(req.url);
|
|
104
|
+
const runId = url.searchParams.get("runId");
|
|
105
|
+
|
|
106
|
+
if (!runId) {
|
|
107
|
+
return Response.json({ error: "No runId provided" }, { status: 400 });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const world = getWorld(); // [!code highlight]
|
|
112
|
+
const steps = await world.steps.list({ // [!code highlight]
|
|
113
|
+
runId, // [!code highlight]
|
|
114
|
+
resolveData: "none", // Skip fetching input/output for performance // [!code highlight]
|
|
115
|
+
}); // [!code highlight]
|
|
116
|
+
|
|
117
|
+
// Map steps to a progress view using parseStepName for display
|
|
118
|
+
const progress = steps.data.map((step) => {
|
|
119
|
+
const parsed = parseStepName(step.stepName); // [!code highlight]
|
|
120
|
+
return {
|
|
121
|
+
stepId: step.stepId,
|
|
122
|
+
// Use shortName for UI display (e.g., "fetchUserData") // [!code highlight]
|
|
123
|
+
displayName: parsed?.shortName ?? step.stepName, // [!code highlight]
|
|
124
|
+
// Module info available for debugging // [!code highlight]
|
|
125
|
+
module: parsed?.moduleSpecifier, // [!code highlight]
|
|
126
|
+
status: step.status,
|
|
127
|
+
startedAt: step.startedAt,
|
|
128
|
+
completedAt: step.completedAt,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return Response.json({ progress, cursor: steps.cursor });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return Response.json(
|
|
135
|
+
{ error: "Failed to list steps" },
|
|
136
|
+
{ status: 500 }
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Get Step with Hydrated Input/Output
|
|
143
|
+
|
|
144
|
+
Retrieve a step with its serialized data and hydrate it for display. This example shows how to decrypt and deserialize step input/output:
|
|
145
|
+
|
|
146
|
+
```typescript lineNumbers
|
|
147
|
+
import { getWorld } from "workflow/runtime";
|
|
148
|
+
import { parseStepName } from "@workflow/utils/parse-name"; // [!code highlight]
|
|
149
|
+
import { // [!code highlight]
|
|
150
|
+
hydrateResourceIO, // [!code highlight]
|
|
151
|
+
observabilityRevivers, // [!code highlight]
|
|
152
|
+
} from "@workflow/core/serialization-format"; // [!code highlight]
|
|
153
|
+
|
|
154
|
+
export async function GET(req: Request) {
|
|
155
|
+
const url = new URL(req.url);
|
|
156
|
+
const runId = url.searchParams.get("runId");
|
|
157
|
+
const stepId = url.searchParams.get("stepId");
|
|
158
|
+
|
|
159
|
+
if (!runId || !stepId) {
|
|
160
|
+
return Response.json({ error: "runId and stepId required" }, { status: 400 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const world = getWorld(); // [!code highlight]
|
|
165
|
+
// Fetch step with data (default resolveData behavior) // [!code highlight]
|
|
166
|
+
const step = await world.steps.get(runId, stepId); // [!code highlight]
|
|
167
|
+
|
|
168
|
+
// Hydrate serialized input/output for display // [!code highlight]
|
|
169
|
+
const hydrated = hydrateResourceIO(step, observabilityRevivers); // [!code highlight]
|
|
170
|
+
|
|
171
|
+
// Parse the stepName for user-friendly display
|
|
172
|
+
const parsed = parseStepName(step.stepName);
|
|
173
|
+
|
|
174
|
+
return Response.json({
|
|
175
|
+
stepId: hydrated.stepId,
|
|
176
|
+
displayName: parsed?.shortName ?? step.stepName, // [!code highlight]
|
|
177
|
+
module: parsed?.moduleSpecifier, // [!code highlight]
|
|
178
|
+
status: hydrated.status,
|
|
179
|
+
attempt: hydrated.attempt,
|
|
180
|
+
// Hydrated input/output ready for rendering // [!code highlight]
|
|
181
|
+
input: hydrated.input, // [!code highlight]
|
|
182
|
+
output: hydrated.output, // [!code highlight]
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return Response.json(
|
|
186
|
+
{ error: "Step not found" },
|
|
187
|
+
{ status: 404 }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
<Callout type="info">
|
|
194
|
+
The `stepName` field contains a machine-readable identifier like `step//./src/workflows/order//processPayment`.
|
|
195
|
+
Use `parseStepName()` from `@workflow/utils/parse-name` to extract the `shortName` (e.g., `"processPayment"`)
|
|
196
|
+
and `moduleSpecifier` for display in your UI.
|
|
197
|
+
</Callout>
|
|
198
|
+
|
|
94
199
|
## Related Functions
|
|
95
200
|
|
|
96
201
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) - Higher-level API for working with individual runs by ID.
|
|
@@ -76,3 +76,27 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
|
|
|
76
76
|
deploymentId: "custom-deployment-id" // [!code highlight]
|
|
77
77
|
}); // [!code highlight]
|
|
78
78
|
```
|
|
79
|
+
|
|
80
|
+
### Using `deploymentId: "latest"`
|
|
81
|
+
|
|
82
|
+
Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { start } from "workflow/api";
|
|
86
|
+
import { myWorkflow } from "./workflows/my-workflow";
|
|
87
|
+
|
|
88
|
+
const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
|
|
89
|
+
deploymentId: "latest" // [!code highlight]
|
|
90
|
+
}); // [!code highlight]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
<Callout type="info">
|
|
94
|
+
The `deploymentId` option is currently a Vercel-specific feature. The `"latest"` value resolves to the most recent deployment matching your current environment — the same production target for production deployments, or the same git branch for preview deployments.
|
|
95
|
+
</Callout>
|
|
96
|
+
|
|
97
|
+
<Callout type="warn">
|
|
98
|
+
When using `deploymentId: "latest"`, the workflow run will execute on a potentially different deployment than the one calling `start()`. Be mindful of forward and backward compatibility:
|
|
99
|
+
|
|
100
|
+
- **Workflow identity**: The workflow ID is derived from the function name and file path. If the latest deployment has renamed the workflow function or moved it to a different directory, the workflow ID will no longer match and the run will fail to start.
|
|
101
|
+
- **Input and output compatibility**: The arguments passed to `start()` are serialized by the calling deployment but deserialized by the target deployment. Similarly, the workflow's return value is serialized by the target deployment but deserialized by the caller. If the workflow's expected arguments or return type have changed (e.g. added required fields, removed fields, or changed types), the run may fail or behave unexpectedly. Ensure that input and output schemas remain backward-compatible across deployments.
|
|
102
|
+
</Callout>
|