@dbos-inc/vercel-ai 0.1.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DBOS, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # @dbos-inc/vercel-ai
2
+
3
+ [DBOS](https://docs.dbos.dev/) durable execution for the [Vercel AI SDK](https://ai-sdk.dev/).
4
+
5
+ This package makes AI SDK **agents** durable, backed by your Postgres database.
6
+ All you have to do is wrap your model with `durableCalls` and run your generation inside a DBOS workflow.
7
+ Then, this integration automatically checkpoints every action your agents take in Postgres.
8
+ If your process crashes mid-agent, DBOS replays the completed steps from their checkpoints and the agent resumes exactly where it left off.
9
+
10
+ This package is implemented as standard AI SDK [middleware](https://ai-sdk.dev/docs/ai-sdk-core/middleware), so you keep your provider, your model configuration, and the familiar APIs like `generateText`, `streamText`, and `ToolLoopAgent`.
11
+ Durability is transparent to your agent code.
12
+
13
+ ```ts
14
+ import { DBOS } from '@dbos-inc/dbos-sdk';
15
+ import { generateText, wrapLanguageModel } from 'ai';
16
+ import { openai } from '@ai-sdk/openai';
17
+ import { durableCalls } from '@dbos-inc/vercel-ai';
18
+
19
+ const model = wrapLanguageModel({
20
+ model: openai('gpt-5'),
21
+ middleware: durableCalls({ retriesAllowed: true, maxAttempts: 5 }),
22
+ });
23
+
24
+ const researchAgent = DBOS.registerWorkflow(
25
+ async (question: string) => {
26
+ const { text } = await generateText({
27
+ model,
28
+ prompt: question,
29
+ system: 'You are a helpful research assistant.',
30
+ });
31
+ return text;
32
+ },
33
+ { name: 'researchAgent' },
34
+ );
35
+
36
+ DBOS.setConfig({ name: 'my-agent', systemDatabaseUrl: process.env.DBOS_SYSTEM_DATABASE_URL });
37
+ await DBOS.launch();
38
+
39
+ console.log(await researchAgent('Why did the agent cross the road?'));
40
+ ```
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ npm install @dbos-inc/vercel-ai @dbos-inc/dbos-sdk ai
46
+ ```
47
+
48
+ Requires AI SDK v7+ and a Postgres database for DBOS.
49
+
50
+ ## How it works
51
+
52
+ When an agent runs inside a DBOS workflow, DBOS makes three things durable:
53
+
54
+ - **Every model call.** `durableCalls()` is AI SDK middleware that intercepts `doGenerate`/`doStream` and runs each call through [`DBOS.runStep`](https://docs.dbos.dev/typescript/tutorials/step-tutorial). The complete result (content, usage, finish reason, response metadata) is checkpointed in Postgres. On recovery, completed calls replay from their checkpoints without contacting the model provider.
55
+ - **The agent loop.** Because DBOS workflows replay deterministically on recovery and each model call replays from its checkpoint, a multi-step, tool-calling agent resumes from the first unfinished step instead of restarting from the beginning.
56
+ - **Tool calls.** MCP tools (via [`durableMCPTools`](#mcp-tools)) are checkpointed automatically. Your own tools' side effects are durable when you wrap their `execute` in `DBOS.runStep` (see [Tools](#tools)).
57
+
58
+ Outside a workflow (or inside another step) the wrapped model calls the provider directly with no checkpointing, so the same model works anywhere in your app.
59
+
60
+ All DBOS step options are accepted and apply per model call:
61
+
62
+ ```ts
63
+ durableCalls({
64
+ retriesAllowed: true, // retry failed model calls (default: true)
65
+ maxAttempts: 5, // total attempts when retries are allowed (default: 3)
66
+ intervalSeconds: 1, // delay before first retry (default: 1)
67
+ backoffRate: 2, // exponential backoff multiplier (default: 2)
68
+ shouldRetry: (error) => true, // per-error retry predicate (default: skip provider-declared non-retryable errors and aborts)
69
+ timeoutMS: 60000, // per-attempt timeout
70
+ name: 'my-model-call', // step name (default: "<provider>.<modelId>.<operation>")
71
+ });
72
+ ```
73
+
74
+ Retries are on by default so that a transient provider error is absorbed inside a single durable step.
75
+ The default `shouldRetry` treats errors the provider marks non-retryable (an AI SDK `APICallError`/`GatewayError` with `isRetryable === false`, e.g. a 401 or an invalid-request 400) and aborts/timeouts as terminal, so they fail fast instead of retrying `maxAttempts` times.
76
+ Pass your own `shouldRetry` to override it, or `retriesAllowed: false` to disable step retries.
77
+
78
+ Because DBOS owns retries by default, pass `maxRetries: 0` to the AI SDK call so retry behavior is governed in one place; otherwise the two compose multiplicatively and each AI SDK retry is a fresh step.
79
+
80
+ ## Streaming
81
+
82
+ You can stream durable model responses inside a workflow with `streamText`.
83
+ During streaming, DBOS checkpoints only the final completed output, not individual deltas.
84
+ As a consequence:
85
+
86
+ - You can safely forward streamed deltas to a UI or terminal, but you should not perform durable steps on them because model responses are not resumable. Instead, run your own durable steps on the complete result (`result.text`) after the stream ends. Tool calls performed by the AI SDK during streaming are already durable because they execute after the model call has been checkpointed.
87
+ - Do not exit a stream before it completes. To stop reading early, either drain the stream (`await result.consumeStream()`) or abort it.
88
+ - To abort early, pass an `abortSignal` to `streamText` and fire it. The output streamed so far becomes the durable result for that model call.
89
+
90
+ ```ts
91
+ import { streamText } from 'ai';
92
+ import { durableCalls } from '@dbos-inc/vercel-ai';
93
+
94
+ const model = wrapLanguageModel({
95
+ model: openai('gpt-5'),
96
+ middleware: durableCalls({ retriesAllowed: true, maxAttempts: 5 }),
97
+ });
98
+
99
+ const streamingAgent = DBOS.registerWorkflow(async (prompt: string) => {
100
+ const result = streamText({ model, prompt });
101
+ for await (const delta of result.textStream) {
102
+ process.stdout.write(delta);
103
+ }
104
+ return await result.text;
105
+ }, { name: 'streamingAgent' });
106
+ ```
107
+
108
+ ## Tools
109
+
110
+ Model calls in a tool-calling loop are each checkpointed individually, so a recovered agent resumes mid-loop.
111
+ You should wrap your tool's `execute` in a DBOS step so it is checkpointed too.
112
+
113
+ ```ts
114
+ import { tool, stepCountIs } from 'ai';
115
+ import { z } from 'zod';
116
+
117
+ const agent = DBOS.registerWorkflow(async (question: string) => {
118
+ const result = await generateText({
119
+ model,
120
+ prompt: question,
121
+ tools: {
122
+ getWeather: tool({
123
+ description: 'Get the weather for a city',
124
+ inputSchema: z.object({ city: z.string() }),
125
+ execute: ({ city }) => DBOS.runStep(() => fetchWeather(city), { name: 'getWeather' }),
126
+ }),
127
+ },
128
+ stopWhen: stepCountIs(10),
129
+ });
130
+ return result.text;
131
+ }, { name: 'weatherAgent' });
132
+ ```
133
+
134
+ ### MCP tools
135
+
136
+ `durableMCPTools` wraps an [MCP](https://modelcontextprotocol.io/) client (e.g. from [`@ai-sdk/mcp`](https://www.npmjs.com/package/@ai-sdk/mcp)) so both the tool listing and every tool call run as durable steps.
137
+ Each tool call is checkpointed so recovery replays results instead of re-invoking the tool:
138
+
139
+ ```ts
140
+ import { createMCPClient } from '@ai-sdk/mcp';
141
+ import { durableMCPTools } from '@dbos-inc/vercel-ai';
142
+
143
+ const agent = DBOS.registerWorkflow(async (question: string) => {
144
+ const mcpClient = await createMCPClient({ transport: { type: 'http', url: MCP_URL } });
145
+ const tools = await durableMCPTools(mcpClient);
146
+ const result = await generateText({ model, prompt: question, tools, stopWhen: stepCountIs(10) });
147
+ return result.text;
148
+ }, { name: 'mcpAgent' });
149
+ ```
150
+
151
+ To use the client's explicit-schema mode (tool subsetting, typed inputs, output schemas), pass `toolOptions`; it is forwarded to `client.tools()` for both the listing and each tool call:
152
+
153
+ ```ts
154
+ const tools = await durableMCPTools(mcpClient, {
155
+ toolOptions: { schemas: { 'get-weather': { inputSchema: z.object({ city: z.string() }) } } },
156
+ });
157
+ ```
158
+
159
+ ## Concurrency
160
+
161
+ Run **one durable model call at a time within a single workflow**.
162
+ DBOS requires workflows to be deterministic, but the AI SDK issues concurrent model calls in nondeterministic order.
163
+ To guard against nondeterminism, this integration throws an error if it detects concurrent durable model calls in the same workflow.
164
+ Sequential calls (including a normal tool-calling loop, where each model call completes before the next begins) are unaffected.
165
+
166
+ To fan out model calls in parallel, give each its own **child workflow**:
167
+
168
+ ```ts
169
+ const summarizeOne = DBOS.registerWorkflow(
170
+ async (doc: string) => (await generateText({ model, prompt: `Summarize: ${doc}` })).text,
171
+ { name: 'summarizeOne' },
172
+ );
173
+
174
+ const summarizeAll = DBOS.registerWorkflow(async (docs: string[]) => {
175
+ const handles = await Promise.all(
176
+ docs.map((doc) => DBOS.startWorkflow(summarizeOne)(doc)),
177
+ );
178
+ return Promise.all(handles.map((h) => h.getResult()));
179
+ }, { name: 'summarizeAll' });
180
+ ```
181
+
182
+ ## Embeddings
183
+
184
+ `durableEmbeddingCalls` enables durable calls to embedding models:
185
+
186
+ ```ts
187
+ import { embedMany, wrapEmbeddingModel } from 'ai';
188
+ import { durableEmbeddingCalls } from '@dbos-inc/vercel-ai';
189
+
190
+ const embeddingModel = wrapEmbeddingModel({
191
+ model: openai.textEmbeddingModel('text-embedding-3-small'),
192
+ middleware: durableEmbeddingCalls({ retriesAllowed: true }),
193
+ });
194
+
195
+ const { embeddings } = await embedMany({ model: embeddingModel, values: chunks, maxParallelCalls: 1 });
196
+ ```
197
+
198
+ Pass `maxParallelCalls: 1` when embedding more values than the model's per-call limit. `embedMany` otherwise splits the input into batches and runs them concurrently, which the concurrency guard rejects (their step order would be nondeterministic on replay); `maxParallelCalls: 1` runs the batches sequentially, keeping them durable and replay-safe.
199
+
200
+ ## Images
201
+
202
+ `durableImageCalls` makes image generation durable:
203
+
204
+ ```ts
205
+ import { generateImage, wrapImageModel } from 'ai';
206
+ import { durableImageCalls } from '@dbos-inc/vercel-ai';
207
+
208
+ const imageModel = wrapImageModel({ model: openai.imageModel('gpt-image-1'), middleware: durableImageCalls() });
209
+
210
+ const { images } = await generateImage({ model: imageModel, prompt: 'a durable cat' });
211
+ ```
212
+
@@ -0,0 +1,3 @@
1
+ export { durableCalls, durableEmbeddingCalls, durableImageCalls } from './middleware';
2
+ export { durableMCPTools, DurableMCPToolsOptions, MCPClientLike } from './mcp';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.durableMCPTools = exports.durableImageCalls = exports.durableEmbeddingCalls = exports.durableCalls = void 0;
4
+ var middleware_1 = require("./middleware");
5
+ Object.defineProperty(exports, "durableCalls", { enumerable: true, get: function () { return middleware_1.durableCalls; } });
6
+ Object.defineProperty(exports, "durableEmbeddingCalls", { enumerable: true, get: function () { return middleware_1.durableEmbeddingCalls; } });
7
+ Object.defineProperty(exports, "durableImageCalls", { enumerable: true, get: function () { return middleware_1.durableImageCalls; } });
8
+ var mcp_1 = require("./mcp");
9
+ Object.defineProperty(exports, "durableMCPTools", { enumerable: true, get: function () { return mcp_1.durableMCPTools; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,2CAAsF;AAA7E,0GAAA,YAAY,OAAA;AAAE,mHAAA,qBAAqB,OAAA;AAAE,+GAAA,iBAAiB,OAAA;AAC/D,6BAA+E;AAAtE,sGAAA,eAAe,OAAA"}
@@ -0,0 +1,6 @@
1
+ import { StepConfig } from '@dbos-inc/dbos-sdk';
2
+ export declare function isInWorkflowFunction(): boolean;
3
+ export declare function assertNotInTransaction(operation: string): void;
4
+ export declare function restoreAISDKErrorIdentity(error: unknown): unknown;
5
+ export declare function withErrorClassification(options: StepConfig): StepConfig;
6
+ //# sourceMappingURL=internal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.d.ts","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEtD,wBAAgB,oBAAoB,IAAI,OAAO,CAG9C;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAI9D;AA2BD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAejE;AAKD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,CAMvE"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isInWorkflowFunction = isInWorkflowFunction;
4
+ exports.assertNotInTransaction = assertNotInTransaction;
5
+ exports.restoreAISDKErrorIdentity = restoreAISDKErrorIdentity;
6
+ exports.withErrorClassification = withErrorClassification;
7
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
8
+ function isInWorkflowFunction() {
9
+ // True only in workflow code proper: not in a step, transaction, or outside DBOS.
10
+ return dbos_sdk_1.DBOS.isInWorkflow();
11
+ }
12
+ function assertNotInTransaction(operation) {
13
+ if (dbos_sdk_1.DBOS.isInTransaction()) {
14
+ throw new Error(`Cannot call ${operation} inside a DBOS transaction; run it in workflow or step code.`);
15
+ }
16
+ }
17
+ // Aborts/timeouts are deliberate cancellations, never transient; retrying just re-runs an already-cancelled call.
18
+ function isAbortError(error) {
19
+ const name = error?.name;
20
+ return name === 'AbortError' || name === 'TimeoutError';
21
+ }
22
+ // AI SDK errors (APICallError, GatewayError) expose an isRetryable flag; treat an explicit false, and any abort, as terminal.
23
+ function isNonRetryable(error) {
24
+ try {
25
+ return (isAbortError(error) ||
26
+ (typeof error === 'object' &&
27
+ error !== null &&
28
+ 'isRetryable' in error &&
29
+ error.isRetryable === false));
30
+ }
31
+ catch {
32
+ // A throwing accessor must not replace the step's real error; treat as retryable.
33
+ return false;
34
+ }
35
+ }
36
+ // serialize-error drops the Symbol-keyed AI SDK error markers on replay, so isInstance() fails on the revived error.
37
+ // The markers derive from error.name (which survives), so re-attach them; no @ai-sdk/provider/@ai-sdk/gateway import.
38
+ // AISDKError (APICallError, ...) and GatewayError use distinct marker namespaces — ai's retry predicate checks both.
39
+ function restoreAISDKErrorIdentity(error) {
40
+ // Wrap the whole body: identity restoration must never replace or mask the real error (a frozen/non-extensible
41
+ // error throws on assignment, a throwing `name` getter throws on read) — on failure, return the error untouched.
42
+ try {
43
+ const name = error?.name;
44
+ if (error === null || typeof error !== 'object' || typeof name !== 'string')
45
+ return error;
46
+ const base = name.startsWith('AI_') ? 'vercel.ai.error' : name.startsWith('Gateway') ? 'vercel.ai.gateway.error' : undefined;
47
+ if (base !== undefined) {
48
+ error[Symbol.for(base)] = true;
49
+ error[Symbol.for(`${base}.${name}`)] = true;
50
+ }
51
+ }
52
+ catch {
53
+ // Fall through and return the error as-is.
54
+ }
55
+ return error;
56
+ }
57
+ // Default to DBOS-owned retries so a transient provider error is absorbed inside one step (never checkpointed as an
58
+ // error that replay would re-run); the default shouldRetry skips provider-declared non-retryable errors and aborts.
59
+ // A caller's retriesAllowed/shouldRetry wins, but an explicit `undefined` falls back to the default.
60
+ function withErrorClassification(options) {
61
+ return {
62
+ ...options,
63
+ retriesAllowed: options.retriesAllowed ?? true,
64
+ shouldRetry: options.shouldRetry ?? ((error) => !isNonRetryable(error)),
65
+ };
66
+ }
67
+ //# sourceMappingURL=internal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal.js","sourceRoot":"","sources":["../src/internal.ts"],"names":[],"mappings":";;AAEA,oDAGC;AAED,wDAIC;AA2BD,8DAeC;AAKD,0DAMC;AAhED,iDAAsD;AAEtD,SAAgB,oBAAoB;IAClC,kFAAkF;IAClF,OAAO,eAAI,CAAC,YAAY,EAAE,CAAC;AAC7B,CAAC;AAED,SAAgB,sBAAsB,CAAC,SAAiB;IACtD,IAAI,eAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,eAAe,SAAS,8DAA8D,CAAC,CAAC;IAC1G,CAAC;AACH,CAAC;AAED,kHAAkH;AAClH,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,IAAI,GAAI,KAAmC,EAAE,IAAI,CAAC;IACxD,OAAO,IAAI,KAAK,YAAY,IAAI,IAAI,KAAK,cAAc,CAAC;AAC1D,CAAC;AAED,8HAA8H;AAC9H,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,CAAC;QACH,OAAO,CACL,YAAY,CAAC,KAAK,CAAC;YACnB,CAAC,OAAO,KAAK,KAAK,QAAQ;gBACxB,KAAK,KAAK,IAAI;gBACd,aAAa,IAAI,KAAK;gBACrB,KAAmC,CAAC,WAAW,KAAK,KAAK,CAAC,CAC9D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,kFAAkF;QAClF,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,qHAAqH;AACrH,sHAAsH;AACtH,qHAAqH;AACrH,SAAgB,yBAAyB,CAAC,KAAc;IACtD,+GAA+G;IAC/G,iHAAiH;IACjH,IAAI,CAAC;QACH,MAAM,IAAI,GAAI,KAAmC,EAAE,IAAI,CAAC;QACxD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7H,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACtB,KAAiC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;YAC3D,KAAiC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC;QAC3E,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,oHAAoH;AACpH,oHAAoH;AACpH,qGAAqG;AACrG,SAAgB,uBAAuB,CAAC,OAAmB;IACzD,OAAO;QACL,GAAG,OAAO;QACV,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,IAAI;QAC9C,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;KACjF,CAAC;AACJ,CAAC"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { StepConfig } from '@dbos-inc/dbos-sdk';
2
+ import type { ToolSet } from 'ai' with { 'resolution-mode': 'import' };
3
+ export interface MCPClientLike {
4
+ tools(options?: unknown): Promise<Record<string, unknown>>;
5
+ close?(): Promise<void>;
6
+ }
7
+ export interface DurableMCPToolsOptions extends StepConfig {
8
+ /** Forwarded to client.tools() on listing and on each call (e.g. { schemas } for subsetting and output schemas). */
9
+ toolOptions?: unknown;
10
+ }
11
+ /**
12
+ * Wraps an MCP client so its tool listing and each tool call run as durable DBOS steps: the tool
13
+ * list (JSON schemas) is checkpointed so recovery needs no live connection, and each tool call is
14
+ * checkpointed so a recovered workflow replays results instead of re-invoking the tool.
15
+ */
16
+ export declare function durableMCPTools(client: MCPClientLike, options?: DurableMCPToolsOptions): Promise<ToolSet>;
17
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC,OAAO,iBAAiB,EAAE,QAAQ,EAAE,CAAC;AAKvE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAaD,MAAM,WAAW,sBAAuB,SAAQ,UAAU;IACxD,oHAAoH;IACpH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAqCD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,OAAO,CAAC,CAwEnH"}
package/dist/mcp.js ADDED
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.durableMCPTools = durableMCPTools;
4
+ const dbos_sdk_1 = require("@dbos-inc/dbos-sdk");
5
+ const internal_1 = require("./internal");
6
+ function isAsyncIterable(value) {
7
+ return typeof value?.[Symbol.asyncIterator] === 'function';
8
+ }
9
+ // Mirror of @ai-sdk/mcp's toModelOutput: MCP content becomes model content (text stays text, images become files).
10
+ function mcpToolOutput(output) {
11
+ const result = output;
12
+ if (result === null || typeof result !== 'object' || !Array.isArray(result.content)) {
13
+ return { type: 'json', value: output };
14
+ }
15
+ return {
16
+ type: 'content',
17
+ value: result.content.map((part) => {
18
+ if (part.type === 'text' && typeof part.text === 'string') {
19
+ return { type: 'text', text: part.text };
20
+ }
21
+ if (part.type === 'image' && part.data !== undefined && part.mimeType !== undefined) {
22
+ return { type: 'file', mediaType: part.mimeType, data: { type: 'data', data: part.data } };
23
+ }
24
+ return { type: 'text', text: JSON.stringify(part) };
25
+ }),
26
+ };
27
+ }
28
+ /**
29
+ * Wraps an MCP client so its tool listing and each tool call run as durable DBOS steps: the tool
30
+ * list (JSON schemas) is checkpointed so recovery needs no live connection, and each tool call is
31
+ * checkpointed so a recovered workflow replays results instead of re-invoking the tool.
32
+ */
33
+ async function durableMCPTools(client, options = {}) {
34
+ const { toolOptions, ...stepOptions } = options;
35
+ const stepConfig = (0, internal_1.withErrorClassification)(stepOptions);
36
+ const { asSchema, dynamicTool, jsonSchema } = await import('ai');
37
+ const run = (name, fn, config = stepConfig) => {
38
+ (0, internal_1.assertNotInTransaction)(name);
39
+ if (!(0, internal_1.isInWorkflowFunction)())
40
+ return fn();
41
+ // Restore the AI SDK error identity a replay revival strips, so the SDK's retry/catch logic behaves the same.
42
+ return dbos_sdk_1.DBOS.runStep(fn, { ...config, name }).catch((error) => {
43
+ throw (0, internal_1.restoreAISDKErrorIdentity)(error);
44
+ });
45
+ };
46
+ // Checkpoint the tool list as plain JSON schemas, so replay reconstructs tools without the live client.
47
+ const listed = await run('mcp.listTools', async () => {
48
+ const tools = await client.tools(toolOptions);
49
+ const defs = {};
50
+ for (const [name, rawTool] of Object.entries(tools)) {
51
+ const tool = rawTool;
52
+ defs[name] = {
53
+ description: typeof tool.description === 'string' ? tool.description : undefined,
54
+ title: typeof tool.title === 'string' ? tool.title : undefined,
55
+ metadata: tool.metadata,
56
+ meta: tool._meta,
57
+ convertsOutput: typeof tool.toModelOutput === 'function',
58
+ // Await: a Schema's jsonSchema may be a Promise, which would otherwise checkpoint as {} and yield an empty schema.
59
+ inputJsonSchema: await asSchema(tool.inputSchema).jsonSchema,
60
+ };
61
+ }
62
+ return defs;
63
+ });
64
+ const durable = {};
65
+ for (const [name, def] of Object.entries(listed)) {
66
+ const reconstructed = dynamicTool({
67
+ description: def.description,
68
+ title: def.title,
69
+ metadata: def.metadata,
70
+ inputSchema: jsonSchema(def.inputJsonSchema),
71
+ // MCP clients convert results via a pure toModelOutput; reapply an equivalent so results reach the model as content, not raw JSON.
72
+ toModelOutput: def.convertsOutput ? ({ output }) => mcpToolOutput(output) : undefined,
73
+ // Re-fetch the live tool inside the step (its execute closure can't be checkpointed); replay returns the recorded result.
74
+ execute: (input, execOptions) => {
75
+ const signal = execOptions?.abortSignal;
76
+ // An aborted consumer is done with this call, whatever the failure looks like; a retry would re-run a cancelled side effect.
77
+ const callConfig = {
78
+ ...stepConfig,
79
+ shouldRetry: async (error) => !signal?.aborted && (stepConfig.shouldRetry ? await stepConfig.shouldRetry(error) : true),
80
+ };
81
+ return run(`mcp.tool.${name}`, async () => {
82
+ const tool = (await client.tools(toolOptions))[name];
83
+ if (typeof tool?.execute !== 'function')
84
+ throw new Error(`MCP tool "${name}" is not executable.`);
85
+ const output = await tool.execute(input, execOptions);
86
+ // A streaming execute can't checkpoint mid-flight; drain it and record the final value (the last yield).
87
+ if (isAsyncIterable(output)) {
88
+ let last;
89
+ for await (last of output)
90
+ ;
91
+ return last;
92
+ }
93
+ return output;
94
+ }, callConfig);
95
+ },
96
+ });
97
+ // @ai-sdk/mcp spreads the MCP _meta onto the tool object; preserve it for consumers that read it.
98
+ durable[name] = def.meta === undefined ? reconstructed : Object.assign(reconstructed, { _meta: def.meta });
99
+ }
100
+ return durable;
101
+ }
102
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { StepConfig } from '@dbos-inc/dbos-sdk';
2
+ import type { EmbeddingModelMiddleware, ImageModelMiddleware, LanguageModelMiddleware } from 'ai' with { 'resolution-mode': 'import' };
3
+ /** AI SDK language-model middleware that runs each model call as a durable, checkpointed DBOS step (replayed on recovery); outside a workflow it calls the model directly. */
4
+ export declare function durableCalls(options?: StepConfig): LanguageModelMiddleware;
5
+ /** AI SDK embedding-model middleware that runs each embedding call as a durable DBOS step, like {@link durableCalls}. */
6
+ export declare function durableEmbeddingCalls(options?: StepConfig): EmbeddingModelMiddleware;
7
+ /**
8
+ * AI SDK image-model middleware that runs each image generation as a durable DBOS step, like {@link durableCalls}.
9
+ * No concurrency guard: generateImage splits `n > maxImagesPerCall` into batches it dispatches synchronously (no
10
+ * await before doGenerate), so their step order is deterministic on replay — unlike embedMany's parallel batches.
11
+ */
12
+ export declare function durableImageCalls(options?: StepConfig): ImageModelMiddleware;
13
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAItD,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,IAAI,CAAC,OAAO,iBAAiB,EAAE,QAAQ,EAAE,CAAC;AAgDvI,8KAA8K;AAC9K,wBAAgB,YAAY,CAAC,OAAO,GAAE,UAAe,GAAG,uBAAuB,CAsK9E;AAED,yHAAyH;AACzH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAsBxF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,UAAe,GAAG,oBAAoB,CAmBhF"}