@ai-sdk/workflow 2.0.53 → 2.0.55

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/video.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { experimental_startVideo, GetVideoStatusResult } from 'ai';
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
- status: 'completed';
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 { type WorkflowGenerateVideoOptions, type WorkflowGenerateVideoResult, experimental_generateVideo };
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
- "use step";
14
- const hasIdempotencyKey = Object.keys(options.headers ?? {}).some(
15
- (key) => key.toLowerCase() === "idempotency-key"
16
- );
17
- return experimental_startVideo({
18
- ...options,
19
- headers: {
20
- ...options.headers,
21
- ...hasIdempotencyKey ? {} : {
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
- "use step";
30
- return experimental_getVideoStatus(model, options);
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
- if (typeof options.model !== "string" && (options.model.specificationVersion !== "v4" || options.model.handleWebhookOption == null)) {
35
- throw new Error(
36
- "Workflow video generation requires a model with native webhook support."
37
- );
38
- }
39
- let operation;
40
- let startWarnings;
41
- {
42
- var _stack = [];
43
- try {
44
- const webhook = __using(_stack, createWebhook());
45
- const startResult = await startVideoStep({
46
- ...options,
47
- webhookUrl: webhook.url
48
- });
49
- operation = startResult.operation;
50
- startWarnings = startResult.warnings;
51
- await webhook;
52
- } catch (_) {
53
- var _error = _, _hasError = true;
54
- } finally {
55
- __callDispose(_stack, _error, _hasError);
56
- }
57
- }
58
- const statusResult = await getVideoStatusStep(options.model, {
59
- operation,
60
- headers: options.headers,
61
- maxRetries: options.maxRetries
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
- export {
77
- experimental_generateVideo
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":";;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,eAAe,uBAAuB;AAsB/C,eAAe,eACb,SAC2B;AAC3B;AAEA,QAAM,oBAAoB,OAAO,KAAK,QAAQ,WAAW,CAAC,CAAC,EAAE;AAAA,IAC3D,SAAO,IAAI,YAAY,MAAM;AAAA,EAC/B;AAEA,SAAO,wBAAwB;AAAA,IAC7B,GAAG;AAAA,IACH,SAAS;AAAA,MACP,GAAG,QAAQ;AAAA,MACX,GAAI,oBACA,CAAC,IACD;AAAA,QACE,mBAAmB,wBAAwB,gBAAgB,EAAE,MAAM;AAAA,MACrE;AAAA,IACN;AAAA,EACF,CAAC;AACH;AAEA,eAAe,aAAa;AAE5B,eAAe,mBACb,OACA,SAC+B;AAC/B;AAEA,SAAO,4BAA4B,OAAO,OAAO;AACnD;AAEA,mBAAmB,aAAa;AAShC,eAAsB,2BACpB,SACsC;AACtC,MACE,OAAO,QAAQ,UAAU,aACxB,QAAQ,MAAM,yBAAyB,QACtC,QAAQ,MAAM,uBAAuB,OACvC;AACA,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AAEJ;AACE;AAAA;AAAA,YAAM,UAAU,8BAAc;AAE9B,YAAM,cAAc,MAAM,eAAe;AAAA,QACvC,GAAG;AAAA,QACH,YAAY,QAAQ;AAAA,MACtB,CAAC;AAED,kBAAY,YAAY;AACxB,sBAAgB,YAAY;AAC5B,YAAM;AAAA,aATN;AAAA;AAAA;AAAA;AAAA;AAAA,EAUF;AAEA,QAAM,eAAe,MAAM,mBAAmB,QAAQ,OAAO;AAAA,IAC3D;AAAA,IACA,SAAS,QAAQ;AAAA,IACjB,YAAY,QAAQ;AAAA,EACtB,CAAC;AAED,MAAI,aAAa,WAAW,SAAS;AACnC,UAAM,IAAI,MAAM,aAAa,KAAK;AAAA,EACpC;AAEA,MAAI,aAAa,WAAW,aAAa;AACvC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAU,CAAC,GAAG,eAAe,GAAG,aAAa,QAAQ;AAAA,EACvD;AACF;","names":[]}
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