@voltro/ai 0.28.0 → 0.30.0

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.
@@ -0,0 +1,304 @@
1
+ import { DataStore } from '@voltro/database';
2
+ import { Effect } from 'effect';
3
+ import { Schema } from 'effect';
4
+ import { StepRetryPolicy } from '@voltro/workflow';
5
+
6
+ /** The provider-call retry default for an INLINE call.
7
+ *
8
+ * A rate limit and a 5xx are how a model call fails, and both are transient.
9
+ * Left to itself an unretried call fails the whole durable run for a condition
10
+ * that resolves in two seconds. Overridable per step; `retry: { maxAttempts:
11
+ * 1 }` opts out. An OFFLOADED call retries in the dispatcher instead, on the
12
+ * same rules — see `decideInferenceFailure`. */
13
+ export declare const AI_STEP_RETRY: StepRetryPolicy;
14
+
15
+ /** Provider-specific knobs forwarded verbatim to the Vercel AI SDK's
16
+ * `providerOptions` on generateText / streamText / generateObject. The outer
17
+ * key is the provider id, the inner record its options. The headline use is
18
+ * REASONING EFFORT on reasoning models — `gpt-5.x` defaults to a high effort
19
+ * that costs many seconds per call even for a one-line answer, so a routing/
20
+ * help agent should pin it low:
21
+ *
22
+ * providerOptions: { openai: { reasoningEffort: 'low' } } // or 'minimal'
23
+ * providerOptions: { anthropic: { thinking: { type: 'disabled' } } }
24
+ *
25
+ * Shape matches the SDK's `ProviderOptions` (Record<provider, Record<key,
26
+ * JSONValue>>). The value is the standard JSON value (mutable object + array,
27
+ * so it stays assignable to the SDK's `SharedV2ProviderOptions` under
28
+ * `exactOptionalPropertyTypes`) — NOT `unknown`, which the SDK rejects. */
29
+ declare type AiJsonValue = null | boolean | number | string | {
30
+ [key: string]: AiJsonValue;
31
+ } | AiJsonValue[];
32
+
33
+ /**
34
+ * A schema-constrained completion as a durable step.
35
+ *
36
+ * The schema is the step's success schema too, so the journaled value is
37
+ * decoded on replay exactly as it was on the first run — a structured result
38
+ * that only validated the first time through would be the worst kind of replay
39
+ * bug.
40
+ *
41
+ * Offloaded, the schema is rendered to JSON Schema for the dispatcher (a
42
+ * JavaScript Schema cannot be journaled) and the DECODE still happens here,
43
+ * where the real schema exists.
44
+ */
45
+ export declare const aiObjectStep: <A, I>(options: AiObjectStepOptions<A, I>) => Effect.Effect<A, unknown>;
46
+
47
+ export declare interface AiObjectStepOptions<A, I> extends AiStepBaseOptions {
48
+ readonly schema: Schema.Schema<A, I>;
49
+ }
50
+
51
+ export declare class AiOffloadError extends Error {
52
+ readonly attempts: number;
53
+ constructor(stepName: string, message: string, attempts: number);
54
+ }
55
+
56
+ export declare class AiOffloadMisuseError extends Error {
57
+ constructor(stepName: string);
58
+ }
59
+
60
+ declare type AiProviderOptions = Record<string, Record<string, AiJsonValue>>;
61
+
62
+ /**
63
+ * A text completion as a durable step.
64
+ *
65
+ * ```ts
66
+ * const summary = yield* aiStep({
67
+ * name: 'summarise-thread',
68
+ * prompt: `Summarise:\n${thread}`,
69
+ * store: ctx.store,
70
+ * offload: true,
71
+ * })
72
+ * ```
73
+ *
74
+ * On replay the journal answers and the model is not called again.
75
+ */
76
+ export declare const aiStep: (options: AiStepOptions) => Effect.Effect<AiStepResult, unknown>;
77
+
78
+ export declare interface AiStepBaseOptions {
79
+ /** Step name, as it appears in the journal and the dashboard. It also keys
80
+ * the offloaded call's row, so it has to be stable across a replay — which
81
+ * it is, being a literal in the body. */
82
+ readonly name: string;
83
+ readonly prompt: string;
84
+ readonly system?: string;
85
+ readonly provider?: ProviderConfig;
86
+ readonly fallbacks?: ProviderFallbacks;
87
+ readonly maxTokens?: number;
88
+ readonly providerOptions?: AiProviderOptions;
89
+ readonly retry?: StepRetryPolicy;
90
+ readonly recordPrompt?: PromptRecording;
91
+ /**
92
+ * The app's store.
93
+ *
94
+ * Used for the spend ledger, and REQUIRED for `offload` — that is where the
95
+ * queue lives. Omitting it on an inline call records no usage, which is a
96
+ * legitimate choice for a throwaway call and is reported as such rather than
97
+ * silently assumed.
98
+ */
99
+ readonly store?: DataStore;
100
+ /** Carried onto the usage row so spend can be split per tenant. */
101
+ readonly tenantId?: string | null;
102
+ /**
103
+ * Suspend the run while the model thinks, instead of holding a worker.
104
+ *
105
+ * The run parks on a durable deferred and a dispatcher performs the call.
106
+ * Costs one suspend/resume round trip; buys back a worker for the length of
107
+ * the call. Requires `store`.
108
+ */
109
+ readonly offload?: boolean;
110
+ /** End-to-end ceiling on an offloaded call. Default one hour. */
111
+ readonly offloadTimeoutMs?: number;
112
+ }
113
+
114
+ export declare interface AiStepOptions extends AiStepBaseOptions {
115
+ }
116
+
117
+ /** What an `aiStep` returns. The usage tally is part of the RESULT, not only of
118
+ * the ledger — a workflow that wants to branch on token count should not have
119
+ * to query a table to see what it just spent. */
120
+ export declare interface AiStepResult {
121
+ readonly text: string;
122
+ readonly inputTokens: number | null;
123
+ readonly outputTokens: number | null;
124
+ readonly totalTokens: number | null;
125
+ }
126
+
127
+ declare type AnthropicModel = 'claude-opus-4-8' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | (string & {});
128
+
129
+ /** Default ceiling on an offloaded call, end to end — one hour. Generous
130
+ * because the wait itself is free (no worker is held), and a run that gives up
131
+ * on a slow provider at ten minutes is a run somebody has to re-drive by hand. */
132
+ export declare const DEFAULT_OFFLOAD_TIMEOUT_MS: number;
133
+
134
+ /** Creator namespaces the Vercel AI Gateway routes to. Open union — the named
135
+ * arms are autocomplete, any other creator string still works. */
136
+ declare type GatewayCreator = 'openai' | 'anthropic' | 'google' | (string & {});
137
+
138
+ /** The gateway addresses models as `creator/model` ids. The template-literal
139
+ * type enforces that shape at COMPILE time — what `providerFromEnv` otherwise
140
+ * only catches as a runtime throw (`{ name: 'gateway', model: 'gpt-5.5' }` is
141
+ * now a type error, not a boot crash). */
142
+ declare type GatewayModel = `${GatewayCreator}/${string}`;
143
+
144
+ /** A scripted inline file the mock model emits on a turn (usually an image).
145
+ * `data` is base64 (a short ascii string works for assertions). */
146
+ declare interface MockFile {
147
+ readonly mediaType: string;
148
+ readonly data: string;
149
+ }
150
+
151
+ /** A consumable sequence of model turns, plus a recorder for asserting on
152
+ * what the model was asked. Each `doGenerate`/`doStream` advances the
153
+ * cursor by one turn; running past the end repeats the last turn's text
154
+ * (or empties) so a loop that over-steps doesn't hang. */
155
+ declare class MockScript {
156
+ private readonly turns;
157
+ private cursor;
158
+ readonly calls: Array<{
159
+ readonly prompt: unknown;
160
+ readonly tools: ReadonlyArray<string>;
161
+ }>;
162
+ constructor(turns: ReadonlyArray<MockTurn>);
163
+ record(prompt: unknown, tools: ReadonlyArray<string>): void;
164
+ next(): MockTurn;
165
+ reset(): void;
166
+ }
167
+
168
+ /** A scripted citation the mock model emits on a turn (RAG grounding). */
169
+ declare interface MockSource {
170
+ readonly sourceType?: 'url' | 'document';
171
+ readonly id?: string;
172
+ readonly url?: string;
173
+ readonly title?: string;
174
+ readonly mediaType?: string;
175
+ readonly filename?: string;
176
+ }
177
+
178
+ /** A scripted tool call the mock model emits on a turn. */
179
+ declare interface MockToolCall {
180
+ readonly name: string;
181
+ readonly input: unknown;
182
+ }
183
+
184
+ /** One model turn. A turn can carry text (streamed word-by-word), scripted
185
+ * tool calls (which trigger the SDK's tool loop), cited sources, inline files,
186
+ * or an error. A turn with tool calls + no text drives one LLM↔tool
187
+ * round-trip. */
188
+ declare interface MockTurn {
189
+ readonly text?: string;
190
+ /** Reasoning ("thinking") the mock streams before the answer text. */
191
+ readonly reasoning?: string;
192
+ readonly toolCalls?: ReadonlyArray<MockToolCall>;
193
+ readonly sources?: ReadonlyArray<MockSource>;
194
+ readonly files?: ReadonlyArray<MockFile>;
195
+ readonly error?: {
196
+ readonly message: string;
197
+ };
198
+ }
199
+
200
+ declare type OpenAIModel = 'gpt-5.5' | 'gpt-4o' | 'gpt-4o-mini' | (string & {});
201
+
202
+ /** The digest form. A prefix, not the whole hash: this is a correlation handle,
203
+ * and a full sha256 in a dashboard cell is a wall of hex nobody reads. */
204
+ export declare const promptDigest: (prompt: string) => string;
205
+
206
+ /**
207
+ * What gets written to `_voltro_workflow_run_steps.input` for a prompt.
208
+ *
209
+ * Exported because it is the security-relevant half of this module and it must
210
+ * be assertable on its own: "the raw prompt is not in the recorded input" is a
211
+ * property about a string, and testing it through a live model call would test
212
+ * the mock instead.
213
+ */
214
+ export declare const promptRecordFor: (prompt: string, system: string | undefined, mode: PromptRecording) => Record<string, unknown>;
215
+
216
+ /**
217
+ * How the prompt is written to the step row the dashboard renders.
218
+ *
219
+ * `'digest'` (default) — a sha256 prefix plus the character count. Enough to
220
+ * tell two runs apart, to spot a prompt that ballooned, and to correlate a
221
+ * cached result; not enough to leak the content of one.
222
+ *
223
+ * `'full'` — the prompt verbatim. Right for a prompt built from constants and
224
+ * a row id; wrong for one built from a customer's message, and typing it out is
225
+ * the point.
226
+ *
227
+ * `'none'` — record nothing about the prompt.
228
+ */
229
+ export declare type PromptRecording = 'digest' | 'full' | 'none';
230
+
231
+ /**
232
+ * A resolved provider choice — a DISCRIMINATED UNION on `name`. Each provider
233
+ * only carries the fields that apply to it: the mock-only `mockText` / `script`
234
+ * cannot appear on a real provider, and a real provider always carries a
235
+ * `model` typed to that provider's ids. `apiKey` / `baseURL` switch
236
+ * `resolveModel` to a per-config provider instance (per-agent key / BYOK);
237
+ * omit them to fall back to the provider's standard env var (OPENAI_API_KEY /
238
+ * ANTHROPIC_API_KEY / AI_GATEWAY_API_KEY).
239
+ */
240
+ declare type ProviderConfig = {
241
+ readonly name: 'mock';
242
+ /** Optional for mock — defaults to `'mock'`. */
243
+ readonly model?: string;
244
+ /** The exact text the mock returns (instead of echoing the prompt).
245
+ * Lets tests/demos drive `generateObject` with canned JSON. */
246
+ readonly mockText?: string;
247
+ /** A multi-turn script. Takes precedence over `mockText` — each
248
+ * `doGenerate`/`doStream` consumes the next turn, so the SDK's tool
249
+ * loop runs deterministically. */
250
+ readonly script?: MockScript;
251
+ /** Provider metadata the mock attaches to its `doGenerate` result — lets a
252
+ * test exercise the gateway-cost path (`gatewayCostUsd`) deterministically
253
+ * by faking `{ gateway: { cost } }`. Also surfaced by the mock speech +
254
+ * video media models. */
255
+ readonly providerMetadata?: AiProviderOptions;
256
+ /** Canned output for the media models (`resolveImageModel` /
257
+ * `resolveSpeechModel` / `resolveVideoModel`). Every field is optional and
258
+ * falls back to a deterministic default, so `useMockAi` yields valid media
259
+ * without configuration; set a field to assert on exact bytes / URL. */
260
+ readonly mockMedia?: {
261
+ /** base64 image payload `generateImage` decodes to bytes (default: a
262
+ * 1×1 PNG header, so media-type detection resolves to `image/png`). */
263
+ readonly imageBase64?: string;
264
+ /** base64 audio payload `generateSpeech` decodes to bytes. */
265
+ readonly audioBase64?: string;
266
+ /** URL the mock video model yields (the default branch — most video
267
+ * providers return a URL rather than inline bytes). */
268
+ readonly videoUrl?: string;
269
+ /** base64 the mock video model yields INSTEAD of a URL — exercises the
270
+ * bytes branch of `generateVideo`. Takes precedence over `videoUrl`. */
271
+ readonly videoBase64?: string;
272
+ };
273
+ } | {
274
+ readonly name: 'anthropic';
275
+ readonly model: AnthropicModel;
276
+ readonly apiKey?: string;
277
+ readonly baseURL?: string;
278
+ } | {
279
+ readonly name: 'openai';
280
+ readonly model: OpenAIModel;
281
+ readonly apiKey?: string;
282
+ readonly baseURL?: string;
283
+ } | {
284
+ readonly name: 'gateway';
285
+ /** A `creator/model` id, e.g. 'openai/gpt-5.5'. */
286
+ readonly model: GatewayModel;
287
+ readonly apiKey?: string;
288
+ readonly baseURL?: string;
289
+ };
290
+
291
+ /**
292
+ * An ordered provider/model fallback chain. When a call FAILS on the primary
293
+ * provider (`provider ?? providerFromEnv()`) — after that provider's OWN
294
+ * SDK-level retries — the call re-runs against `fallbacks[0]`, then
295
+ * `fallbacks[1]`, … until one succeeds. Exhausting every option surfaces the
296
+ * LAST provider's typed error (an `AiError({ reason: 'generation' })`). Only a
297
+ * `generation` failure (the provider call itself) falls through — a `decode`
298
+ * failure (the model answered but the output didn't satisfy the schema) is a
299
+ * prompt/schema problem another provider won't fix, so it surfaces immediately.
300
+ * An empty / absent chain leaves behavior unchanged.
301
+ */
302
+ declare type ProviderFallbacks = ReadonlyArray<ProviderConfig>;
303
+
304
+ export { }
@@ -0,0 +1,153 @@
1
+ import { r as e } from "./provider-BOC5wBxx.js";
2
+ import { O as t, f as n, g as r, h as i, j as a, o, s, t as c } from "./inferenceQueue-B25RTJZ4.js";
3
+ import { Effect as l, Schema as u } from "effect";
4
+ import { eq as d } from "@voltro/database";
5
+ import { createHash as f } from "node:crypto";
6
+ import { CurrentWorkflowExecutionId as p, awaitSignalSuspending as m, step as h } from "@voltro/workflow";
7
+ //#region src/inferStep.ts
8
+ var g = {
9
+ maxAttempts: 4,
10
+ strategy: "exponential",
11
+ baseDelay: "1 second",
12
+ maxDelay: "30 seconds",
13
+ jitter: !0,
14
+ respectRetryAfter: !0
15
+ }, _ = 60 * 6e4, v = (e) => `sha256:${f("sha256").update(e).digest("hex").slice(0, 16)}`, y = (e, t, n) => n === "none" ? {} : n === "full" ? {
16
+ prompt: e,
17
+ ...t === void 0 ? {} : { system: t }
18
+ } : {
19
+ promptDigest: v(e),
20
+ promptChars: e.length,
21
+ ...t === void 0 ? {} : {
22
+ systemDigest: v(t),
23
+ systemChars: t.length
24
+ }
25
+ }, b = (e) => ({
26
+ provider: String(e.provider ?? "unknown"),
27
+ model: String(e.model ?? "unknown")
28
+ }), x = (e, n, r, i) => {
29
+ let o = e.store;
30
+ if (o === void 0) return l.void;
31
+ let { provider: s, model: c } = b(n), u = t(i.providerMetadata);
32
+ return a(o, {
33
+ provider: s,
34
+ model: c,
35
+ operation: r,
36
+ agent: e.name,
37
+ usage: i.usage,
38
+ ...e.tenantId === void 0 ? {} : { tenantId: e.tenantId },
39
+ ...u === void 0 ? {} : { actualCostUsd: u }
40
+ }).pipe(l.asVoid, l.catchAllCause(() => l.void));
41
+ }, S = u.Struct({
42
+ text: u.String,
43
+ inputTokens: u.NullOr(u.Number),
44
+ outputTokens: u.NullOr(u.Number),
45
+ totalTokens: u.NullOr(u.Number)
46
+ }), C = (e) => typeof e == "object" && !!e && e.__voltro_ai_error === !0, w = class extends Error {
47
+ attempts;
48
+ constructor(e, t, n) {
49
+ super(`aiStep('${e}'): the offloaded model call gave up after ${n} attempt(s): ${t}. The queued call is in _voltro_ai_inferences with status 'failed' — the Flow tab shows it with the provider's own message.`), this.name = "AiOffloadError", this.attempts = n;
50
+ }
51
+ }, T = class extends Error {
52
+ constructor(e) {
53
+ super(`aiStep('${e}'): offload: true needs \`store\` — the queued call lives in a table and the dispatcher reads it from there. Pass ctx.store.`), this.name = "AiOffloadMisuseError";
54
+ }
55
+ }, E = (e, t, n, r) => l.gen(function* () {
56
+ let i = e.store;
57
+ if (i === void 0) return yield* l.die(new T(e.name));
58
+ let a = yield* p;
59
+ if (a === void 0) return yield* l.die(/* @__PURE__ */ Error(`aiStep('${e.name}'): offload: true is only valid inside a workflow body — there is no execution to suspend here.`));
60
+ let { provider: u, model: f } = b(t), m = s(a, e.name), h = yield* l.promise(() => i.query({
61
+ table: "_voltro_workflow_runs",
62
+ predicate: d("executionId", a),
63
+ order: [],
64
+ take: 1,
65
+ skip: void 0,
66
+ projection: void 0
67
+ })), g = String(h[0]?.tag ?? ""), _ = h[0]?.id;
68
+ return yield* l.promise(() => i.insertIgnore(c, {
69
+ id: m,
70
+ tenantId: e.tenantId ?? null,
71
+ executionId: a,
72
+ workflowName: g,
73
+ runId: typeof _ == "string" ? _ : null,
74
+ signalName: o(m),
75
+ stepName: e.name,
76
+ status: "pending",
77
+ kind: n,
78
+ provider: u,
79
+ model: f,
80
+ prompt: e.prompt,
81
+ system: e.system ?? null,
82
+ maxTokens: e.maxTokens ?? null,
83
+ providerOptions: e.providerOptions ?? null,
84
+ jsonSchema: r,
85
+ attempts: 0,
86
+ enqueuedAt: /* @__PURE__ */ new Date()
87
+ }, { conflictColumns: ["id"] })), m;
88
+ }), D = (e, t, n, r, i) => l.gen(function* () {
89
+ let a = yield* h({
90
+ name: `${e.name}:enqueue`,
91
+ success: u.String,
92
+ input: i,
93
+ execute: E(e, t, n, r)
94
+ }), s = yield* m({ store: e.store }, {
95
+ name: o(a),
96
+ schema: u.Unknown,
97
+ timeoutMs: e.offloadTimeoutMs ?? 36e5
98
+ });
99
+ return C(s) ? yield* l.die(new w(e.name, s.message, s.attempts)) : s;
100
+ }), O = (t) => {
101
+ let n = t.provider ?? e(), r = {
102
+ ...b(n),
103
+ operation: "generateText",
104
+ offloaded: t.offload === !0,
105
+ ...y(t.prompt, t.system, t.recordPrompt ?? "digest")
106
+ };
107
+ return t.offload === !0 ? D(t, n, "text", null, r) : h({
108
+ name: t.name,
109
+ success: S,
110
+ input: r,
111
+ retry: t.retry ?? g,
112
+ execute: i({
113
+ prompt: t.prompt,
114
+ provider: n,
115
+ ...t.system === void 0 ? {} : { system: t.system },
116
+ ...t.fallbacks === void 0 ? {} : { fallbacks: t.fallbacks },
117
+ ...t.maxTokens === void 0 ? {} : { maxTokens: t.maxTokens },
118
+ ...t.providerOptions === void 0 ? {} : { providerOptions: t.providerOptions }
119
+ }).pipe(l.tap((e) => x(t, n, "generateText", e)), l.map((e) => ({
120
+ text: e.text,
121
+ inputTokens: e.usage.inputTokens ?? null,
122
+ outputTokens: e.usage.outputTokens ?? null,
123
+ totalTokens: e.usage.totalTokens ?? null
124
+ })))
125
+ });
126
+ }, k = (t) => {
127
+ let i = t.provider ?? e(), a = {
128
+ ...b(i),
129
+ operation: "generateObject",
130
+ offloaded: t.offload === !0,
131
+ ...y(t.prompt, t.system, t.recordPrompt ?? "digest")
132
+ };
133
+ return t.offload === !0 ? l.gen(function* () {
134
+ let e = yield* r(t.schema, "aiObjectStep"), n = yield* D(t, i, "object", e, a);
135
+ return yield* u.decodeUnknown(t.schema)(n);
136
+ }) : h({
137
+ name: t.name,
138
+ success: t.schema,
139
+ input: a,
140
+ retry: t.retry ?? g,
141
+ execute: n({
142
+ prompt: t.prompt,
143
+ schema: t.schema,
144
+ provider: i,
145
+ ...t.system === void 0 ? {} : { system: t.system },
146
+ ...t.fallbacks === void 0 ? {} : { fallbacks: t.fallbacks },
147
+ ...t.maxTokens === void 0 ? {} : { maxTokens: t.maxTokens },
148
+ ...t.providerOptions === void 0 ? {} : { providerOptions: t.providerOptions }
149
+ }).pipe(l.tap((e) => x(t, i, "generateObject", e)), l.map((e) => e.object))
150
+ });
151
+ };
152
+ //#endregion
153
+ export { g as AI_STEP_RETRY, w as AiOffloadError, T as AiOffloadMisuseError, _ as DEFAULT_OFFLOAD_TIMEOUT_MS, k as aiObjectStep, O as aiStep, v as promptDigest, y as promptRecordFor };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/ai",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Effect-native AI primitives — generateText / generateObject / streamText, tools, agents, media generation, cost tracking + budgets, and provider fallback.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -37,6 +37,11 @@
37
37
  "types": "./dist/test.d.ts",
38
38
  "import": "./dist/test.js",
39
39
  "default": "./dist/test.js"
40
+ },
41
+ "./workflow": {
42
+ "types": "./dist/workflow.d.ts",
43
+ "import": "./dist/workflow.js",
44
+ "default": "./dist/workflow.js"
40
45
  }
41
46
  },
42
47
  "main": "./dist/index.js",
@@ -51,8 +56,10 @@
51
56
  "@ai-sdk/gateway": "^4.0.30",
52
57
  "@ai-sdk/openai": "^4.0.22",
53
58
  "@ai-sdk/provider": "^4.0.4",
54
- "@voltro/database": "0.28.0",
55
- "@voltro/protocol": "0.28.0",
59
+ "@voltro/cache": "0.30.0",
60
+ "@voltro/database": "0.30.0",
61
+ "@voltro/protocol": "0.30.0",
62
+ "@voltro/workflow": "0.30.0",
56
63
  "ai": "^7.0.40"
57
64
  },
58
65
  "peerDependencies": {