@ai-sdk/workflow 2.0.53 → 2.0.54
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/CHANGELOG.md +15 -0
- package/dist/client.d.ts +2 -160
- package/dist/client.js +2 -8
- package/dist/index.d.ts +879 -887
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2344 -2914
- package/dist/index.js.map +1 -1
- package/dist/video.d.ts +7 -6
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +108 -69
- package/dist/video.js.map +1 -1
- package/dist/workflow-chat-transport-Bduf4ZDx.js +366 -0
- package/dist/workflow-chat-transport-Bduf4ZDx.js.map +1 -0
- package/dist/workflow-chat-transport-DwOgX7dK.d.ts +161 -0
- package/dist/workflow-chat-transport-DwOgX7dK.d.ts.map +1 -0
- package/package.json +7 -7
- package/src/do-stream-step.ts +81 -3
- package/src/stream-text-iterator.ts +32 -42
- package/dist/chunk-CKYLPPDM.js +0 -358
- package/dist/chunk-CKYLPPDM.js.map +0 -1
- package/dist/chunk-UAWBPTDW.js +0 -51
- package/dist/chunk-UAWBPTDW.js.map +0 -1
- package/dist/client.js.map +0 -1
package/dist/video.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { GetVideoStatusResult, experimental_startVideo } from "ai";
|
|
2
|
+
//#region src/generate-video.d.ts
|
|
3
3
|
type StartVideoOptions = Parameters<typeof experimental_startVideo>[0];
|
|
4
4
|
/**
|
|
5
5
|
* Options for durable video generation in a workflow.
|
|
@@ -10,7 +10,7 @@ type WorkflowGenerateVideoOptions = Omit<StartVideoOptions, 'abortSignal' | 'web
|
|
|
10
10
|
* Hosted videos remain URLs and are not downloaded automatically.
|
|
11
11
|
*/
|
|
12
12
|
type WorkflowGenerateVideoResult = Extract<GetVideoStatusResult, {
|
|
13
|
-
|
|
13
|
+
status: 'completed';
|
|
14
14
|
}>;
|
|
15
15
|
/**
|
|
16
16
|
* Generates a video durably inside a workflow using a provider webhook.
|
|
@@ -19,6 +19,7 @@ type WorkflowGenerateVideoResult = Extract<GetVideoStatusResult, {
|
|
|
19
19
|
* generated webhook URL. The returned video data is not downloaded, allowing
|
|
20
20
|
* the workflow to decide how to handle provider-hosted URLs.
|
|
21
21
|
*/
|
|
22
|
-
declare function experimental_generateVideo(options: WorkflowGenerateVideoOptions): Promise<WorkflowGenerateVideoResult>;
|
|
23
|
-
|
|
24
|
-
export {
|
|
22
|
+
export declare function experimental_generateVideo(options: WorkflowGenerateVideoOptions): Promise<WorkflowGenerateVideoResult>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export type { WorkflowGenerateVideoOptions, WorkflowGenerateVideoResult };
|
|
25
|
+
//# sourceMappingURL=video.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video.d.ts","names":[],"sources":["../src/generate-video.ts"],"mappings":";;KAQK,oBAAoB,kBAAkB;;;;KAM/B,+BAA+B,KACzC;;;;;KAQU,8BAA8B,QACxC;EACE;;;;;;;;;wBA6CkB,2BACpB,SAAS,+BACR,QAAQ"}
|
package/dist/video.js
CHANGED
|
@@ -1,79 +1,118 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__callDispose,
|
|
3
|
-
__using
|
|
4
|
-
} from "./chunk-UAWBPTDW.js";
|
|
5
|
-
|
|
6
|
-
// src/generate-video.ts
|
|
7
|
-
import {
|
|
8
|
-
experimental_getVideoStatus,
|
|
9
|
-
experimental_startVideo
|
|
10
|
-
} from "ai";
|
|
1
|
+
import { experimental_getVideoStatus, experimental_startVideo } from "ai";
|
|
11
2
|
import { createWebhook, getStepMetadata } from "workflow";
|
|
3
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/usingCtx.js
|
|
4
|
+
function _usingCtx() {
|
|
5
|
+
var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
|
|
6
|
+
var n = Error();
|
|
7
|
+
return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
|
|
8
|
+
}, e = {}, n = [];
|
|
9
|
+
function using(r, e) {
|
|
10
|
+
if (null != e) {
|
|
11
|
+
if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
|
|
12
|
+
if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
|
|
13
|
+
if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
|
|
14
|
+
if ("function" != typeof o) throw new TypeError("Object is not disposable.");
|
|
15
|
+
t && (o = function o() {
|
|
16
|
+
try {
|
|
17
|
+
t.call(e);
|
|
18
|
+
} catch (r) {
|
|
19
|
+
return Promise.reject(r);
|
|
20
|
+
}
|
|
21
|
+
}), n.push({
|
|
22
|
+
v: e,
|
|
23
|
+
d: o,
|
|
24
|
+
a: r
|
|
25
|
+
});
|
|
26
|
+
} else r && n.push({
|
|
27
|
+
d: e,
|
|
28
|
+
a: r
|
|
29
|
+
});
|
|
30
|
+
return e;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
e,
|
|
34
|
+
u: using.bind(null, !1),
|
|
35
|
+
a: using.bind(null, !0),
|
|
36
|
+
d: function d() {
|
|
37
|
+
var o, t = this.e, s = 0;
|
|
38
|
+
function next() {
|
|
39
|
+
for (; o = n.pop();) try {
|
|
40
|
+
if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
|
|
41
|
+
if (o.d) {
|
|
42
|
+
var r = o.d.call(o.v);
|
|
43
|
+
if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
|
|
44
|
+
} else s |= 1;
|
|
45
|
+
} catch (r) {
|
|
46
|
+
return err(r);
|
|
47
|
+
}
|
|
48
|
+
if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
|
|
49
|
+
if (t !== e) throw t;
|
|
50
|
+
}
|
|
51
|
+
function err(n) {
|
|
52
|
+
return t = t !== e ? new r(n, t) : n, next();
|
|
53
|
+
}
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/generate-video.ts
|
|
12
60
|
async function startVideoStep(options) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"idempotency-key": `aisdk_workflow_video_${getStepMetadata().stepId}`
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
});
|
|
61
|
+
"use step";
|
|
62
|
+
const hasIdempotencyKey = Object.keys(options.headers ?? {}).some((key) => key.toLowerCase() === "idempotency-key");
|
|
63
|
+
return experimental_startVideo({
|
|
64
|
+
...options,
|
|
65
|
+
headers: {
|
|
66
|
+
...options.headers,
|
|
67
|
+
...hasIdempotencyKey ? {} : { "idempotency-key": `aisdk_workflow_video_${getStepMetadata().stepId}` }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
26
70
|
}
|
|
27
71
|
startVideoStep.maxRetries = 0;
|
|
28
72
|
async function getVideoStatusStep(model, options) {
|
|
29
|
-
|
|
30
|
-
|
|
73
|
+
"use step";
|
|
74
|
+
return experimental_getVideoStatus(model, options);
|
|
31
75
|
}
|
|
32
76
|
getVideoStatusStep.maxRetries = 0;
|
|
77
|
+
/**
|
|
78
|
+
* Generates a video durably inside a workflow using a provider webhook.
|
|
79
|
+
*
|
|
80
|
+
* The workflow suspends without consuming compute until the provider calls the
|
|
81
|
+
* generated webhook URL. The returned video data is not downloaded, allowing
|
|
82
|
+
* the workflow to decide how to handle provider-hosted URLs.
|
|
83
|
+
*/
|
|
33
84
|
async function experimental_generateVideo(options) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (statusResult.status === "error") {
|
|
64
|
-
throw new Error(statusResult.error);
|
|
65
|
-
}
|
|
66
|
-
if (statusResult.status !== "completed") {
|
|
67
|
-
throw new Error(
|
|
68
|
-
"Video generation did not complete after webhook notification."
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
...statusResult,
|
|
73
|
-
warnings: [...startWarnings, ...statusResult.warnings]
|
|
74
|
-
};
|
|
85
|
+
if (typeof options.model !== "string" && (options.model.specificationVersion !== "v4" || options.model.handleWebhookOption == null)) throw new Error("Workflow video generation requires a model with native webhook support.");
|
|
86
|
+
let operation;
|
|
87
|
+
let startWarnings;
|
|
88
|
+
try {
|
|
89
|
+
var _usingCtx$1 = _usingCtx();
|
|
90
|
+
const webhook = _usingCtx$1.u(createWebhook());
|
|
91
|
+
const startResult = await startVideoStep({
|
|
92
|
+
...options,
|
|
93
|
+
webhookUrl: webhook.url
|
|
94
|
+
});
|
|
95
|
+
operation = startResult.operation;
|
|
96
|
+
startWarnings = startResult.warnings;
|
|
97
|
+
await webhook;
|
|
98
|
+
} catch (_) {
|
|
99
|
+
_usingCtx$1.e = _;
|
|
100
|
+
} finally {
|
|
101
|
+
_usingCtx$1.d();
|
|
102
|
+
}
|
|
103
|
+
const statusResult = await getVideoStatusStep(options.model, {
|
|
104
|
+
operation,
|
|
105
|
+
headers: options.headers,
|
|
106
|
+
maxRetries: options.maxRetries
|
|
107
|
+
});
|
|
108
|
+
if (statusResult.status === "error") throw new Error(statusResult.error);
|
|
109
|
+
if (statusResult.status !== "completed") throw new Error("Video generation did not complete after webhook notification.");
|
|
110
|
+
return {
|
|
111
|
+
...statusResult,
|
|
112
|
+
warnings: [...startWarnings, ...statusResult.warnings]
|
|
113
|
+
};
|
|
75
114
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
115
|
+
//#endregion
|
|
116
|
+
export { experimental_generateVideo };
|
|
117
|
+
|
|
79
118
|
//# sourceMappingURL=video.js.map
|
package/dist/video.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/generate-video.ts"],"sourcesContent":["import {\n experimental_getVideoStatus,\n experimental_startVideo,\n type GetVideoStatusResult,\n type StartVideoResult,\n} from 'ai';\nimport { createWebhook, getStepMetadata } from 'workflow';\n\ntype StartVideoOptions = Parameters<typeof experimental_startVideo>[0];\ntype GetVideoStatusOptions = Parameters<typeof experimental_getVideoStatus>[1];\n\n/**\n * Options for durable video generation in a workflow.\n */\nexport type WorkflowGenerateVideoOptions = Omit<\n StartVideoOptions,\n 'abortSignal' | 'webhookUrl'\n>;\n\n/**\n * A completed video generation result containing provider video data.\n * Hosted videos remain URLs and are not downloaded automatically.\n */\nexport type WorkflowGenerateVideoResult = Extract<\n GetVideoStatusResult,\n { status: 'completed' }\n>;\n\nasync function startVideoStep(\n options: StartVideoOptions,\n): Promise<StartVideoResult> {\n 'use step';\n\n const hasIdempotencyKey = Object.keys(options.headers ?? {}).some(\n key => key.toLowerCase() === 'idempotency-key',\n );\n\n return experimental_startVideo({\n ...options,\n headers: {\n ...options.headers,\n ...(hasIdempotencyKey\n ? {}\n : {\n 'idempotency-key': `aisdk_workflow_video_${getStepMetadata().stepId}`,\n }),\n },\n });\n}\n\nstartVideoStep.maxRetries = 0;\n\nasync function getVideoStatusStep(\n model: StartVideoOptions['model'],\n options: GetVideoStatusOptions,\n): Promise<GetVideoStatusResult> {\n 'use step';\n\n return experimental_getVideoStatus(model, options);\n}\n\ngetVideoStatusStep.maxRetries = 0;\n\n/**\n * Generates a video durably inside a workflow using a provider webhook.\n *\n * The workflow suspends without consuming compute until the provider calls the\n * generated webhook URL. The returned video data is not downloaded, allowing\n * the workflow to decide how to handle provider-hosted URLs.\n */\nexport async function experimental_generateVideo(\n options: WorkflowGenerateVideoOptions,\n): Promise<WorkflowGenerateVideoResult> {\n if (\n typeof options.model !== 'string' &&\n (options.model.specificationVersion !== 'v4' ||\n options.model.handleWebhookOption == null)\n ) {\n throw new Error(\n 'Workflow video generation requires a model with native webhook support.',\n );\n }\n\n let operation: StartVideoResult['operation'];\n let startWarnings: StartVideoResult['warnings'];\n\n {\n using webhook = createWebhook();\n\n const startResult = await startVideoStep({\n ...options,\n webhookUrl: webhook.url,\n });\n\n operation = startResult.operation;\n startWarnings = startResult.warnings;\n await webhook;\n }\n\n const statusResult = await getVideoStatusStep(options.model, {\n operation,\n headers: options.headers,\n maxRetries: options.maxRetries,\n });\n\n if (statusResult.status === 'error') {\n throw new Error(statusResult.error);\n }\n\n if (statusResult.status !== 'completed') {\n throw new Error(\n 'Video generation did not complete after webhook notification.',\n );\n }\n\n return {\n ...statusResult,\n warnings: [...startWarnings, ...statusResult.warnings],\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"video.js","names":[],"sources":["../src/generate-video.ts"],"sourcesContent":["import {\n experimental_getVideoStatus,\n experimental_startVideo,\n type GetVideoStatusResult,\n type StartVideoResult,\n} from 'ai';\nimport { createWebhook, getStepMetadata } from 'workflow';\n\ntype StartVideoOptions = Parameters<typeof experimental_startVideo>[0];\ntype GetVideoStatusOptions = Parameters<typeof experimental_getVideoStatus>[1];\n\n/**\n * Options for durable video generation in a workflow.\n */\nexport type WorkflowGenerateVideoOptions = Omit<\n StartVideoOptions,\n 'abortSignal' | 'webhookUrl'\n>;\n\n/**\n * A completed video generation result containing provider video data.\n * Hosted videos remain URLs and are not downloaded automatically.\n */\nexport type WorkflowGenerateVideoResult = Extract<\n GetVideoStatusResult,\n { status: 'completed' }\n>;\n\nasync function startVideoStep(\n options: StartVideoOptions,\n): Promise<StartVideoResult> {\n 'use step';\n\n const hasIdempotencyKey = Object.keys(options.headers ?? {}).some(\n key => key.toLowerCase() === 'idempotency-key',\n );\n\n return experimental_startVideo({\n ...options,\n headers: {\n ...options.headers,\n ...(hasIdempotencyKey\n ? {}\n : {\n 'idempotency-key': `aisdk_workflow_video_${getStepMetadata().stepId}`,\n }),\n },\n });\n}\n\nstartVideoStep.maxRetries = 0;\n\nasync function getVideoStatusStep(\n model: StartVideoOptions['model'],\n options: GetVideoStatusOptions,\n): Promise<GetVideoStatusResult> {\n 'use step';\n\n return experimental_getVideoStatus(model, options);\n}\n\ngetVideoStatusStep.maxRetries = 0;\n\n/**\n * Generates a video durably inside a workflow using a provider webhook.\n *\n * The workflow suspends without consuming compute until the provider calls the\n * generated webhook URL. The returned video data is not downloaded, allowing\n * the workflow to decide how to handle provider-hosted URLs.\n */\nexport async function experimental_generateVideo(\n options: WorkflowGenerateVideoOptions,\n): Promise<WorkflowGenerateVideoResult> {\n if (\n typeof options.model !== 'string' &&\n (options.model.specificationVersion !== 'v4' ||\n options.model.handleWebhookOption == null)\n ) {\n throw new Error(\n 'Workflow video generation requires a model with native webhook support.',\n );\n }\n\n let operation: StartVideoResult['operation'];\n let startWarnings: StartVideoResult['warnings'];\n\n {\n using webhook = createWebhook();\n\n const startResult = await startVideoStep({\n ...options,\n webhookUrl: webhook.url,\n });\n\n operation = startResult.operation;\n startWarnings = startResult.warnings;\n await webhook;\n }\n\n const statusResult = await getVideoStatusStep(options.model, {\n operation,\n headers: options.headers,\n maxRetries: options.maxRetries,\n });\n\n if (statusResult.status === 'error') {\n throw new Error(statusResult.error);\n }\n\n if (statusResult.status !== 'completed') {\n throw new Error(\n 'Video generation did not complete after webhook notification.',\n );\n }\n\n return {\n ...statusResult,\n warnings: [...startWarnings, ...statusResult.warnings],\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAe,eACb,SAC2B;CAC3B;CAEA,MAAM,oBAAoB,OAAO,KAAK,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,MAC3D,QAAO,IAAI,YAAY,MAAM,iBAC/B;CAEA,OAAO,wBAAwB;EAC7B,GAAG;EACH,SAAS;GACP,GAAG,QAAQ;GACX,GAAI,oBACA,CAAC,IACD,EACE,mBAAmB,wBAAwB,gBAAgB,CAAC,CAAC,SAC/D;EACN;CACF,CAAC;AACH;AAEA,eAAe,aAAa;AAE5B,eAAe,mBACb,OACA,SAC+B;CAC/B;CAEA,OAAO,4BAA4B,OAAO,OAAO;AACnD;AAEA,mBAAmB,aAAa;;;;;;;;AAShC,eAAsB,2BACpB,SACsC;CACtC,IACE,OAAO,QAAQ,UAAU,aACxB,QAAQ,MAAM,yBAAyB,QACtC,QAAQ,MAAM,uBAAuB,OAEvC,MAAM,IAAI,MACR,yEACF;CAGF,IAAI;CACJ,IAAI;CAEJ,IAAA;;EACE,MAAM,UAAA,YAAA,EAAU,cAAc,CAAA;EAE9B,MAAM,cAAc,MAAM,eAAe;GACvC,GAAG;GACH,YAAY,QAAQ;EACtB,CAAC;EAED,YAAY,YAAY;EACxB,gBAAgB,YAAY;EAC5B,MAAM;;;;;;CAGR,MAAM,eAAe,MAAM,mBAAmB,QAAQ,OAAO;EAC3D;EACA,SAAS,QAAQ;EACjB,YAAY,QAAQ;CACtB,CAAC;CAED,IAAI,aAAa,WAAW,SAC1B,MAAM,IAAI,MAAM,aAAa,KAAK;CAGpC,IAAI,aAAa,WAAW,aAC1B,MAAM,IAAI,MACR,+DACF;CAGF,OAAO;EACL,GAAG;EACH,UAAU,CAAC,GAAG,eAAe,GAAG,aAAa,QAAQ;CACvD;AACF"}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { convertAsyncIteratorToReadableStream, getErrorMessage } from "@ai-sdk/provider-utils";
|
|
2
|
+
import { parseJsonEventStream, uiMessageChunkSchema } from "ai";
|
|
3
|
+
import { createAsyncIterableStream } from "ai/internal";
|
|
4
|
+
//#region src/normalize-ui-message-stream.ts
|
|
5
|
+
const newPartFrameState = () => ({
|
|
6
|
+
open: /* @__PURE__ */ new Set(),
|
|
7
|
+
ended: /* @__PURE__ */ new Set()
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Repairs the framing for a single `*-start` / `*-delta` / `*-end` chunk
|
|
11
|
+
* against the running framing state, yielding the chunks the consumer should
|
|
12
|
+
* see. Text and reasoning parts share this logic (`startType` differentiates
|
|
13
|
+
* the synthesized start chunk).
|
|
14
|
+
*
|
|
15
|
+
* @yields the chunks (possibly synthesized, possibly none) the consumer should see.
|
|
16
|
+
*/
|
|
17
|
+
function* repairPart(kind, id, chunk, state, startType) {
|
|
18
|
+
if (kind === "start") {
|
|
19
|
+
if (state.open.has(id) || state.ended.has(id)) return;
|
|
20
|
+
state.open.add(id);
|
|
21
|
+
yield chunk;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (state.ended.has(id)) return;
|
|
25
|
+
if (!state.open.has(id)) {
|
|
26
|
+
state.open.add(id);
|
|
27
|
+
yield {
|
|
28
|
+
type: startType,
|
|
29
|
+
id
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (kind === "end") {
|
|
33
|
+
state.open.delete(id);
|
|
34
|
+
state.ended.add(id);
|
|
35
|
+
}
|
|
36
|
+
yield chunk;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Normalizes the part framing of a UI message stream so it is always
|
|
40
|
+
* well-formed for the AI SDK's stream consumer (`processUIMessageStream`,
|
|
41
|
+
* which backs `useChat`/`readUIMessageStream`).
|
|
42
|
+
*
|
|
43
|
+
* ## Why this exists
|
|
44
|
+
*
|
|
45
|
+
* The consumer maintains a map of "active" text/reasoning parts keyed by id.
|
|
46
|
+
* A `text-delta`/`text-end` for an id that has no open part is a fatal error
|
|
47
|
+
* (`Received text-delta for missing text part with ID "0" ...`) that kills the
|
|
48
|
+
* whole turn. Two properties of the durable streaming model make that error
|
|
49
|
+
* reachable:
|
|
50
|
+
*
|
|
51
|
+
* - A workflow run owns a single shared stream. Multi-step turns can reuse the
|
|
52
|
+
* same part id (commonly `"0"`), so a dropped or duplicated `*-start` can
|
|
53
|
+
* orphan the rest of that part's content.
|
|
54
|
+
* - The same stream is read across reconnects, and a stream-producing step can
|
|
55
|
+
* run more than once (retry/redelivery, or the concurrent-worker duplication
|
|
56
|
+
* tracked in vercel/workflow#2331 and #2039). Either can interleave or
|
|
57
|
+
* duplicate chunks on the shared stream — e.g. a `finish-step` landing in the
|
|
58
|
+
* middle of another execution's text part.
|
|
59
|
+
*
|
|
60
|
+
* Since the content is still flowing and only the framing is damaged, repairing
|
|
61
|
+
* the framing here degrades the worst case to "text begins slightly into the
|
|
62
|
+
* step" or "a duplicated tail is dropped" instead of a dead turn.
|
|
63
|
+
*
|
|
64
|
+
* ## What it does
|
|
65
|
+
*
|
|
66
|
+
* Mirrors the consumer's explicit-end part-lifetime state machine per part type:
|
|
67
|
+
* - keeps open parts active across `finish-step`, while allowing ended ids to
|
|
68
|
+
* be reused by a later step;
|
|
69
|
+
* - synthesizes a missing `*-start` when an orphaned `*-delta`/`*-end` arrives;
|
|
70
|
+
* - drops a re-delivered `*-start`/`*-delta`/`*-end` for a part already
|
|
71
|
+
* open or ended since the latest step boundary (reconnect/replay overlap).
|
|
72
|
+
*
|
|
73
|
+
* A well-formed stream passes through unchanged.
|
|
74
|
+
*
|
|
75
|
+
* ## Scope: text and reasoning only
|
|
76
|
+
*
|
|
77
|
+
* `tool-input-delta` raises the same class of fatal error (`Received
|
|
78
|
+
* tool-input-delta for missing tool call ...`), but tool parts are deliberately
|
|
79
|
+
* left untouched: the consumer does not reset its tool-call map on `finish-step`
|
|
80
|
+
* and tool-call ids are unique, so the step-boundary id-reuse orphaning that
|
|
81
|
+
* makes text/reasoning fragile does not apply to them. If a future duplication
|
|
82
|
+
* mode is found to orphan tool-input parts, extend the same machine to that
|
|
83
|
+
* family rather than special-casing it.
|
|
84
|
+
*
|
|
85
|
+
* @param source the raw UI message chunk stream to normalize.
|
|
86
|
+
* @yields the framing-corrected UI message chunks.
|
|
87
|
+
*/
|
|
88
|
+
async function* normalizeUIMessageStreamParts(source) {
|
|
89
|
+
const text = newPartFrameState();
|
|
90
|
+
const reasoning = newPartFrameState();
|
|
91
|
+
for await (const chunk of source) switch (chunk.type) {
|
|
92
|
+
case "reset-step":
|
|
93
|
+
text.open.clear();
|
|
94
|
+
text.ended.clear();
|
|
95
|
+
reasoning.open.clear();
|
|
96
|
+
reasoning.ended.clear();
|
|
97
|
+
yield chunk;
|
|
98
|
+
break;
|
|
99
|
+
case "finish-step":
|
|
100
|
+
text.ended.clear();
|
|
101
|
+
reasoning.ended.clear();
|
|
102
|
+
yield chunk;
|
|
103
|
+
break;
|
|
104
|
+
case "text-start":
|
|
105
|
+
yield* repairPart("start", chunk.id, chunk, text, "text-start");
|
|
106
|
+
break;
|
|
107
|
+
case "text-delta":
|
|
108
|
+
yield* repairPart("delta", chunk.id, chunk, text, "text-start");
|
|
109
|
+
break;
|
|
110
|
+
case "text-end":
|
|
111
|
+
yield* repairPart("end", chunk.id, chunk, text, "text-start");
|
|
112
|
+
break;
|
|
113
|
+
case "reasoning-start":
|
|
114
|
+
yield* repairPart("start", chunk.id, chunk, reasoning, "reasoning-start");
|
|
115
|
+
break;
|
|
116
|
+
case "reasoning-delta":
|
|
117
|
+
yield* repairPart("delta", chunk.id, chunk, reasoning, "reasoning-start");
|
|
118
|
+
break;
|
|
119
|
+
case "reasoning-end":
|
|
120
|
+
yield* repairPart("end", chunk.id, chunk, reasoning, "reasoning-start");
|
|
121
|
+
break;
|
|
122
|
+
default: yield chunk;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/workflow-chat-transport.ts
|
|
127
|
+
function createOrphanFilter() {
|
|
128
|
+
const seenStartedIds = /* @__PURE__ */ new Set();
|
|
129
|
+
const seenStartedToolCallIds = /* @__PURE__ */ new Set();
|
|
130
|
+
let warnedOnce = false;
|
|
131
|
+
function warnOnce(orphanKind, orphanRef) {
|
|
132
|
+
if (warnedOnce) return;
|
|
133
|
+
warnedOnce = true;
|
|
134
|
+
console.warn(`[WorkflowChatTransport] Dropping orphan UI chunk (${orphanKind} for id "${orphanRef}") on resume — the resume position landed mid-part. The dropped chunk(s) reference a part whose start chunk wasn't in the resumed window. To preserve the full message, configure your stream endpoint to rewind to a step boundary before returning the readable. See: https://workflow.dev/docs/ai/resumable-streams#mid-part-resumes`);
|
|
135
|
+
}
|
|
136
|
+
function shouldDrop(chunk) {
|
|
137
|
+
switch (chunk.type) {
|
|
138
|
+
case "reset-step":
|
|
139
|
+
seenStartedIds.clear();
|
|
140
|
+
seenStartedToolCallIds.clear();
|
|
141
|
+
return false;
|
|
142
|
+
case "text-start":
|
|
143
|
+
case "reasoning-start":
|
|
144
|
+
seenStartedIds.add(chunk.id);
|
|
145
|
+
return false;
|
|
146
|
+
case "tool-input-start":
|
|
147
|
+
case "tool-input-available":
|
|
148
|
+
case "tool-input-error":
|
|
149
|
+
seenStartedToolCallIds.add(chunk.toolCallId);
|
|
150
|
+
return false;
|
|
151
|
+
case "text-delta":
|
|
152
|
+
case "text-end":
|
|
153
|
+
case "reasoning-delta":
|
|
154
|
+
case "reasoning-end":
|
|
155
|
+
if (seenStartedIds.has(chunk.id)) return false;
|
|
156
|
+
warnOnce(chunk.type, chunk.id);
|
|
157
|
+
return true;
|
|
158
|
+
case "tool-input-delta":
|
|
159
|
+
case "tool-approval-request":
|
|
160
|
+
case "tool-output-available":
|
|
161
|
+
case "tool-output-error":
|
|
162
|
+
case "tool-output-denied":
|
|
163
|
+
if (seenStartedToolCallIds.has(chunk.toolCallId)) return false;
|
|
164
|
+
warnOnce(chunk.type, chunk.toolCallId);
|
|
165
|
+
return true;
|
|
166
|
+
default: return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { shouldDrop };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A transport implementation for managing chat workflows with support for
|
|
173
|
+
* streaming responses and automatic reconnection to interrupted streams.
|
|
174
|
+
*
|
|
175
|
+
* This class implements the ChatTransport interface from the AI SDK and provides
|
|
176
|
+
* reliable message streaming with automatic recovery from network interruptions
|
|
177
|
+
* or function timeouts.
|
|
178
|
+
*
|
|
179
|
+
* @template UI_MESSAGE - The type of UI messages being sent and received,
|
|
180
|
+
* must extend the UIMessage interface from the AI SDK.
|
|
181
|
+
*
|
|
182
|
+
* @implements {ChatTransport<UI_MESSAGE>}
|
|
183
|
+
*/
|
|
184
|
+
var WorkflowChatTransport = class {
|
|
185
|
+
/**
|
|
186
|
+
* Creates a new WorkflowChatTransport instance.
|
|
187
|
+
*
|
|
188
|
+
* @param options - Configuration options for the transport
|
|
189
|
+
* @param options.api - API endpoint for chat requests (defaults to '/api/chat')
|
|
190
|
+
* @param options.fetch - Custom fetch implementation (defaults to global fetch)
|
|
191
|
+
* @param options.onChatSendMessage - Callback after sending messages
|
|
192
|
+
* @param options.onChatEnd - Callback when chat stream ends
|
|
193
|
+
* @param options.maxConsecutiveErrors - Maximum consecutive errors for reconnection
|
|
194
|
+
* @param options.prepareSendMessagesRequest - Function to prepare send messages request
|
|
195
|
+
* @param options.prepareReconnectToStreamRequest - Function to prepare reconnect request
|
|
196
|
+
*/
|
|
197
|
+
constructor(options = {}) {
|
|
198
|
+
this.api = options.api ?? "/api/chat";
|
|
199
|
+
this.fetch = options.fetch ?? fetch.bind(globalThis);
|
|
200
|
+
this.onChatSendMessage = options.onChatSendMessage;
|
|
201
|
+
this.onChatEnd = options.onChatEnd;
|
|
202
|
+
this.maxConsecutiveErrors = options.maxConsecutiveErrors ?? 3;
|
|
203
|
+
this.initialStartIndex = options.initialStartIndex ?? 0;
|
|
204
|
+
this.prepareSendMessagesRequest = options.prepareSendMessagesRequest;
|
|
205
|
+
this.prepareReconnectToStreamRequest = options.prepareReconnectToStreamRequest;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Sends messages to the chat endpoint and returns a stream of response chunks.
|
|
209
|
+
*
|
|
210
|
+
* This method handles the entire chat lifecycle including:
|
|
211
|
+
* - Sending messages to the /api/chat endpoint
|
|
212
|
+
* - Streaming response chunks
|
|
213
|
+
* - Automatic reconnection if the stream is interrupted
|
|
214
|
+
*
|
|
215
|
+
* @param options - Options for sending messages
|
|
216
|
+
* @param options.trigger - The type of message submission ('submit-message' or 'regenerate-message')
|
|
217
|
+
* @param options.chatId - Unique identifier for this chat session
|
|
218
|
+
* @param options.messageId - Optional message ID for tracking specific messages
|
|
219
|
+
* @param options.messages - Array of UI messages to send
|
|
220
|
+
* @param options.abortSignal - Optional AbortSignal to cancel the request
|
|
221
|
+
*
|
|
222
|
+
* @returns A ReadableStream of UIMessageChunk objects containing the response
|
|
223
|
+
* @throws Error if the fetch request fails or returns a non-OK status
|
|
224
|
+
*/
|
|
225
|
+
async sendMessages(options) {
|
|
226
|
+
return convertAsyncIteratorToReadableStream(normalizeUIMessageStreamParts(this.sendMessagesIterator(options)));
|
|
227
|
+
}
|
|
228
|
+
async *sendMessagesIterator(options) {
|
|
229
|
+
const { chatId, messages, abortSignal, trigger, messageId } = options;
|
|
230
|
+
let gotFinish = false;
|
|
231
|
+
let chunkIndex = 0;
|
|
232
|
+
const requestConfig = this.prepareSendMessagesRequest ? await this.prepareSendMessagesRequest({
|
|
233
|
+
id: chatId,
|
|
234
|
+
messages,
|
|
235
|
+
requestMetadata: options.metadata,
|
|
236
|
+
body: options.body,
|
|
237
|
+
credentials: void 0,
|
|
238
|
+
headers: options.headers,
|
|
239
|
+
api: this.api,
|
|
240
|
+
trigger,
|
|
241
|
+
messageId
|
|
242
|
+
}) : void 0;
|
|
243
|
+
const url = requestConfig?.api ?? this.api;
|
|
244
|
+
const response = await this.fetch(url, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
body: JSON.stringify(requestConfig?.body ?? {
|
|
247
|
+
messages,
|
|
248
|
+
...options.body
|
|
249
|
+
}),
|
|
250
|
+
headers: requestConfig?.headers,
|
|
251
|
+
credentials: requestConfig?.credentials,
|
|
252
|
+
signal: abortSignal
|
|
253
|
+
});
|
|
254
|
+
if (!response.ok || !response.body) throw new Error(`Failed to fetch chat: ${response.status} ${await response.text()}`);
|
|
255
|
+
const workflowRunId = response.headers.get("x-workflow-run-id");
|
|
256
|
+
if (!workflowRunId) throw new Error("Workflow run ID not found in \"x-workflow-run-id\" response header");
|
|
257
|
+
await this.onChatSendMessage?.(response, options);
|
|
258
|
+
try {
|
|
259
|
+
const chunkStream = parseJsonEventStream({
|
|
260
|
+
stream: response.body,
|
|
261
|
+
schema: uiMessageChunkSchema
|
|
262
|
+
});
|
|
263
|
+
for await (const chunk of createAsyncIterableStream(chunkStream)) {
|
|
264
|
+
if (!chunk.success) throw chunk.error;
|
|
265
|
+
chunkIndex++;
|
|
266
|
+
yield chunk.value;
|
|
267
|
+
if (chunk.value.type === "finish") gotFinish = true;
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error("Error in chat POST stream", error);
|
|
271
|
+
}
|
|
272
|
+
if (gotFinish) await this.onFinish(gotFinish, {
|
|
273
|
+
chatId,
|
|
274
|
+
chunkIndex
|
|
275
|
+
});
|
|
276
|
+
else yield* this.reconnectToStreamIterator(options, workflowRunId, chunkIndex);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Reconnects to an existing chat stream that was previously interrupted.
|
|
280
|
+
*
|
|
281
|
+
* This method is useful for resuming a chat session after network issues,
|
|
282
|
+
* page refreshes, or Vercel Function timeouts.
|
|
283
|
+
*
|
|
284
|
+
* @param options - Options for reconnecting to the stream
|
|
285
|
+
* @param options.chatId - The chat ID to reconnect to
|
|
286
|
+
*
|
|
287
|
+
* @returns A ReadableStream of UIMessageChunk objects
|
|
288
|
+
* @throws Error if the reconnection request fails or returns a non-OK status
|
|
289
|
+
*/
|
|
290
|
+
async reconnectToStream(options) {
|
|
291
|
+
const reconnectIterator = normalizeUIMessageStreamParts(this.reconnectToStreamIterator(options));
|
|
292
|
+
return convertAsyncIteratorToReadableStream(reconnectIterator);
|
|
293
|
+
}
|
|
294
|
+
async *reconnectToStreamIterator(options, workflowRunId, initialChunkIndex = 0) {
|
|
295
|
+
let chunkIndex = initialChunkIndex;
|
|
296
|
+
const explicitStartIndex = options.startIndex ?? this.initialStartIndex;
|
|
297
|
+
let useExplicitStartIndex = initialChunkIndex === 0 && explicitStartIndex !== 0;
|
|
298
|
+
const defaultApi = `${this.api}/${encodeURIComponent(workflowRunId ?? options.chatId)}/stream`;
|
|
299
|
+
const requestConfig = this.prepareReconnectToStreamRequest ? await this.prepareReconnectToStreamRequest({
|
|
300
|
+
id: options.chatId,
|
|
301
|
+
requestMetadata: options.metadata,
|
|
302
|
+
body: void 0,
|
|
303
|
+
credentials: void 0,
|
|
304
|
+
headers: void 0,
|
|
305
|
+
api: defaultApi
|
|
306
|
+
}) : void 0;
|
|
307
|
+
const baseUrl = requestConfig?.api ?? defaultApi;
|
|
308
|
+
let gotFinish = false;
|
|
309
|
+
let consecutiveErrors = 0;
|
|
310
|
+
let replayFromStart = false;
|
|
311
|
+
const orphanFilter = useExplicitStartIndex && explicitStartIndex < 0 ? createOrphanFilter() : null;
|
|
312
|
+
while (!gotFinish) {
|
|
313
|
+
const url = `${baseUrl}?startIndex=${useExplicitStartIndex ? explicitStartIndex : replayFromStart ? 0 : chunkIndex}`;
|
|
314
|
+
const response = await this.fetch(url, {
|
|
315
|
+
headers: requestConfig?.headers,
|
|
316
|
+
credentials: requestConfig?.credentials,
|
|
317
|
+
signal: options.abortSignal
|
|
318
|
+
});
|
|
319
|
+
if (!response.ok || !response.body) throw new Error(`Failed to fetch chat: ${response.status} ${await response.text()}`);
|
|
320
|
+
if (useExplicitStartIndex && explicitStartIndex > 0) chunkIndex = explicitStartIndex;
|
|
321
|
+
else if (useExplicitStartIndex && explicitStartIndex < 0) {
|
|
322
|
+
const tailIndexHeader = response.headers.get("x-workflow-stream-tail-index");
|
|
323
|
+
const tailIndex = tailIndexHeader !== null ? parseInt(tailIndexHeader, 10) : NaN;
|
|
324
|
+
if (!Number.isNaN(tailIndex)) chunkIndex = Math.max(0, tailIndex + 1 + explicitStartIndex);
|
|
325
|
+
else {
|
|
326
|
+
console.warn(`[WorkflowChatTransport] Negative initialStartIndex is configured (${explicitStartIndex}) but the reconnection endpoint did not return a valid "x-workflow-stream-tail-index" header. Retries will replay the stream from the beginning. See: https://workflow.dev/docs/ai/resumable-streams#resuming-from-the-end-of-the-stream`);
|
|
327
|
+
replayFromStart = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
useExplicitStartIndex = false;
|
|
331
|
+
try {
|
|
332
|
+
const chunkStream = parseJsonEventStream({
|
|
333
|
+
stream: response.body,
|
|
334
|
+
schema: uiMessageChunkSchema
|
|
335
|
+
});
|
|
336
|
+
for await (const chunk of createAsyncIterableStream(chunkStream)) {
|
|
337
|
+
if (!chunk.success) throw chunk.error;
|
|
338
|
+
chunkIndex++;
|
|
339
|
+
if (orphanFilter?.shouldDrop(chunk.value)) continue;
|
|
340
|
+
yield chunk.value;
|
|
341
|
+
if (chunk.value.type === "finish") gotFinish = true;
|
|
342
|
+
}
|
|
343
|
+
consecutiveErrors = 0;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error("Error in chat GET reconnectToStream", error);
|
|
346
|
+
consecutiveErrors++;
|
|
347
|
+
if (consecutiveErrors >= this.maxConsecutiveErrors) throw new Error(`Failed to reconnect after ${this.maxConsecutiveErrors} consecutive errors. Last error: ${getErrorMessage(error)}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
await this.onFinish(gotFinish, {
|
|
351
|
+
chatId: options.chatId,
|
|
352
|
+
chunkIndex
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
async onFinish(gotFinish, { chatId, chunkIndex }) {
|
|
356
|
+
if (gotFinish) await this.onChatEnd?.({
|
|
357
|
+
chatId,
|
|
358
|
+
chunkIndex
|
|
359
|
+
});
|
|
360
|
+
else throw new Error("No finish chunk received");
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
//#endregion
|
|
364
|
+
export { normalizeUIMessageStreamParts as n, WorkflowChatTransport as t };
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=workflow-chat-transport-Bduf4ZDx.js.map
|