@voltro/ai 0.32.0 → 0.34.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.
@@ -12,6 +12,10 @@ import { StepRetryPolicy } from '@voltro/workflow';
12
12
  * same rules — see `decideInferenceFailure`. */
13
13
  export declare const AI_STEP_RETRY: StepRetryPolicy;
14
14
 
15
+ export declare class AiBudgetMisuseError extends Error {
16
+ constructor(stepName: string);
17
+ }
18
+
15
19
  /** Provider-specific knobs forwarded verbatim to the Vercel AI SDK's
16
20
  * `providerOptions` on generateText / streamText / generateObject. The outer
17
21
  * key is the provider id, the inner record its options. The headline use is
@@ -80,7 +84,17 @@ export declare interface AiStepBaseOptions {
80
84
  * the offloaded call's row, so it has to be stable across a replay — which
81
85
  * it is, being a literal in the body. */
82
86
  readonly name: string;
83
- readonly prompt: string;
87
+ /**
88
+ * The prompt: either a bare string, or a `definePrompt(...).render(vars)`
89
+ * result.
90
+ *
91
+ * The rendered form is what buys provenance — the step row, the spend ledger
92
+ * row and `_voltro_prompts` all end up carrying the same `promptId` + content
93
+ * digest, so "which prompt version produced this run" is a lookup rather than
94
+ * an archaeology exercise across deploys.
95
+ */
96
+ readonly prompt: string | RenderedPrompt;
97
+ /** Overrides a rendered prompt's own system message for this call. */
84
98
  readonly system?: string;
85
99
  readonly provider?: ProviderConfig;
86
100
  readonly fallbacks?: ProviderFallbacks;
@@ -109,6 +123,61 @@ export declare interface AiStepBaseOptions {
109
123
  readonly offload?: boolean;
110
124
  /** End-to-end ceiling on an offloaded call. Default one hour. */
111
125
  readonly offloadTimeoutMs?: number;
126
+ /**
127
+ * Hold this call to a per-tenant USD ceiling, checked BEFORE anything is
128
+ * spent. Requires `store` (the counter and the hold both live in tables).
129
+ *
130
+ * See {@link AiStepBudget} — `onExceeded: 'suspend'` is the mode that stops
131
+ * spending without destroying the run.
132
+ */
133
+ readonly budget?: AiStepBudget;
134
+ }
135
+
136
+ /**
137
+ * A per-step USD ceiling, and what happens at it.
138
+ *
139
+ * `onExceeded` is the whole point of this option existing. The framework had
140
+ * two behaviours and neither is a budget CONTROL:
141
+ *
142
+ * - `'fail'` is `requireAiBudget`'s: the call fails with the typed
143
+ * `AiBudgetExceeded` and the whole durable run dies with it, nine steps of
144
+ * work included. Correct for a hard cap you genuinely want enforced by
145
+ * destruction, and the reason budgets get set high.
146
+ * - observation-only (`defineCostBudget`) never stops anything at all.
147
+ *
148
+ * `'suspend'` is the third: the run PARKS on a durable hold, frees its worker,
149
+ * and resumes when the window rolls over or an operator lifts it — then
150
+ * re-checks and continues from where it stopped. Nothing is spent while held,
151
+ * and nothing is lost.
152
+ */
153
+ export declare interface AiStepBudget {
154
+ /** Stable budget name — what a hold row records and an operator lifts.
155
+ * Default `'ai-usd'`. */
156
+ readonly name?: string;
157
+ /** The per-tenant ceiling in USD. */
158
+ readonly limitUsd: number;
159
+ /** Rolling-window start; folds into the reservation counter's bucket key, so
160
+ * a new window reserves against a fresh counter. */
161
+ readonly since?: Date;
162
+ /**
163
+ * What THIS call is expected to cost, RESERVED atomically before the provider
164
+ * is contacted.
165
+ *
166
+ * Omitting it makes the gate a read: the ceiling still holds against spend
167
+ * that WAS reserved, but N concurrent runs can each observe headroom and then
168
+ * each spend. Pass an estimate for a real cap — that is the difference between
169
+ * a ceiling and a speed bump, and it is the same argument
170
+ * `requireAiBudget`'s CAS-reservation makes against a check-then-act SUM.
171
+ */
172
+ readonly estimateUsd?: number;
173
+ /** Default `'fail'` — the existing behaviour stays the default, because
174
+ * suspending is a change in what a run DOES and should be asked for. */
175
+ readonly onExceeded?: 'fail' | 'suspend';
176
+ /** Ceiling on how long a hold may park, across every re-park. Default 7 days. */
177
+ readonly holdTimeoutMs?: number;
178
+ /** How often a held run wakes to re-read the counter. Default 15 minutes —
179
+ * a tumbling window rolls over on a clock nothing notifies us about. */
180
+ readonly recheckEveryMs?: number;
112
181
  }
113
182
 
114
183
  export declare interface AiStepOptions extends AiStepBaseOptions {
@@ -126,6 +195,10 @@ export declare interface AiStepResult {
126
195
 
127
196
  declare type AnthropicModel = 'claude-opus-4-8' | 'claude-sonnet-4-6' | 'claude-haiku-4-5-20251001' | (string & {});
128
197
 
198
+ /** The name a hold is scoped to when the caller does not choose one. Operators
199
+ * lift by this string, so it has to be stable and sayable. */
200
+ export declare const DEFAULT_AI_BUDGET_NAME = "ai-usd";
201
+
129
202
  /** Default ceiling on an offloaded call, end to end — one hour. Generous
130
203
  * because the wait itself is free (no worker is held), and a run that gives up
131
204
  * on a slow provider at ten minutes is a run somebody has to re-drive by hand. */
@@ -199,8 +272,38 @@ declare interface MockTurn {
199
272
 
200
273
  declare type OpenAIModel = 'gpt-5.5' | 'gpt-4o' | 'gpt-4o-mini' | (string & {});
201
274
 
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. */
275
+ declare interface PromptDefinition {
276
+ /**
277
+ * Stable identity across revisions. Dotted, like a descriptor tag
278
+ * (`support.triage`) — the same vocabulary the rest of the framework uses for
279
+ * "the name of a thing an app owns".
280
+ */
281
+ readonly id: string;
282
+ /**
283
+ * The template, with `{{name}}` placeholders. A STRING, not a function, and
284
+ * that is the design: a function has no stable content to hash, so a
285
+ * function-built prompt could be versioned only by hashing its output — which
286
+ * would make every distinct customer message a new "version". Compose in the
287
+ * variables instead.
288
+ */
289
+ readonly template: string;
290
+ /** Optional system message. Part of the digest — changing it IS a new
291
+ * version, because it changes the model's behaviour as surely as the
292
+ * template does. */
293
+ readonly system?: string;
294
+ /** Optional human label for this revision (`'shorter-system'`). Metadata:
295
+ * deliberately NOT part of the digest, so relabelling does not fork a
296
+ * version. */
297
+ readonly label?: string;
298
+ }
299
+
300
+ /** The digest form: a prefix, not the whole hash. This is a correlation handle,
301
+ * and a full sha256 in a dashboard cell is a wall of hex nobody reads.
302
+ *
303
+ * Lives here rather than in `inferStep.ts` (where it started) because it is now
304
+ * the identity of a versioned artefact, and `inferStep` imports
305
+ * `@voltro/workflow` — a prompt registry has no business needing the workflow
306
+ * engine to compute a hash. `inferStep` re-exports it. */
204
307
  export declare const promptDigest: (prompt: string) => string;
205
308
 
206
309
  /**
@@ -210,8 +313,15 @@ export declare const promptDigest: (prompt: string) => string;
210
313
  * be assertable on its own: "the raw prompt is not in the recorded input" is a
211
314
  * property about a string, and testing it through a live model call would test
212
315
  * the mock instead.
316
+ *
317
+ * `stamp` is the PROVENANCE half and is recorded in EVERY mode, `'none'`
318
+ * included. That is deliberate and it is the point of the split: the reason to
319
+ * record nothing about a prompt is that its TEXT is sensitive, and a prompt id
320
+ * plus a content digest is neither the text nor derivable from it. Dropping the
321
+ * provenance along with the content would mean the most privacy-conscious
322
+ * setting is also the one where you cannot tell which prompt version ran.
213
323
  */
214
- export declare const promptRecordFor: (prompt: string, system: string | undefined, mode: PromptRecording) => Record<string, unknown>;
324
+ export declare const promptRecordFor: (prompt: string, system: string | undefined, mode: PromptRecording, stamp?: PromptStamp) => Record<string, unknown>;
215
325
 
216
326
  /**
217
327
  * How the prompt is written to the step row the dashboard renders.
@@ -228,6 +338,21 @@ export declare const promptRecordFor: (prompt: string, system: string | undefine
228
338
  */
229
339
  export declare type PromptRecording = 'digest' | 'full' | 'none';
230
340
 
341
+ /** A prompt's identity, as it is stamped onto a step row and a usage row. */
342
+ declare interface PromptStamp {
343
+ /** The author-chosen stable id, e.g. `support.triage`. */
344
+ readonly promptId: string;
345
+ /** Content digest of the template (+ system). CHANGES when the template does
346
+ * — that is the version. */
347
+ readonly digest: string;
348
+ /** The monotonic revision `recordPromptVersion` assigned, once it has been
349
+ * registered. Absent before first registration; the digest is the identity
350
+ * either way. */
351
+ readonly revision?: number;
352
+ /** Optional human label the author gave this revision. */
353
+ readonly label?: string;
354
+ }
355
+
231
356
  /**
232
357
  * A resolved provider choice — a DISCRIMINATED UNION on `name`. Each provider
233
358
  * only carries the fields that apply to it: the mock-only `mockText` / `script`
@@ -301,4 +426,30 @@ declare type ProviderConfig = {
301
426
  */
302
427
  declare type ProviderFallbacks = ReadonlyArray<ProviderConfig>;
303
428
 
429
+ /** What `.render(vars)` produces — a stamp plus the text to send. */
430
+ declare interface RenderedPrompt extends PromptStamp {
431
+ readonly prompt: string;
432
+ readonly system?: string;
433
+ /** The artefact this was rendered from. Carried along so a consumer that
434
+ * receives only the rendered value (`aiStep`) can REGISTER the version
435
+ * without the app having to remember a separate boot-time call. It is the
436
+ * template — code, not data — so carrying it costs nothing a repository
437
+ * checkout does not already cost. */
438
+ readonly definition: PromptDefinition;
439
+ }
440
+
441
+ /**
442
+ * Split a step's `prompt` option into the text to send, the system message, and
443
+ * the provenance stamp (if it came from a `definePrompt`).
444
+ *
445
+ * `system` on the step wins over the prompt definition's, so a caller can
446
+ * override for one call without forking a prompt version — the digest still
447
+ * describes the ARTEFACT, and the override is visible in the step row.
448
+ */
449
+ export declare const resolveStepPrompt: (options: Pick<AiStepBaseOptions, "prompt" | "system">) => {
450
+ readonly text: string;
451
+ readonly system: string | undefined;
452
+ readonly rendered: RenderedPrompt | undefined;
453
+ };
454
+
304
455
  export { }
package/dist/workflow.js CHANGED
@@ -1,153 +1,228 @@
1
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";
2
+ import { C as t, D as n, E as r, H as i, U as a, m as o, o as s, p as c, s as l, t as u, y as d, z as f } from "./inferenceQueue-uj2kKyqp.js";
3
+ import { Effect as p, Schema as m } from "effect";
4
+ import { eq as h } from "@voltro/database";
5
+ import { CurrentWorkflowExecutionId as g, awaitSignalSuspending as _, step as v, suspendForBudget as y } from "@voltro/workflow";
7
6
  //#region src/inferStep.ts
8
- var g = {
7
+ var b = {
9
8
  maxAttempts: 4,
10
9
  strategy: "exponential",
11
10
  baseDelay: "1 second",
12
11
  maxDelay: "30 seconds",
13
12
  jitter: !0,
14
13
  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 }
14
+ }, x = 60 * 6e4, S = "ai-usd", C = (e, t, n, r) => {
15
+ let i = r === void 0 ? {} : {
16
+ promptId: r.promptId,
17
+ promptVersion: r.digest,
18
+ ...r.revision === void 0 ? {} : { promptRevision: r.revision },
19
+ ...r.label === void 0 ? {} : { promptLabel: r.label }
20
+ };
21
+ return n === "none" ? i : n === "full" ? {
22
+ ...i,
23
+ prompt: e,
24
+ ...t === void 0 ? {} : { system: t }
25
+ } : {
26
+ ...i,
27
+ promptDigest: o(e),
28
+ promptChars: e.length,
29
+ ...t === void 0 ? {} : {
30
+ systemDigest: o(t),
31
+ systemChars: t.length
32
+ }
33
+ };
34
+ }, w = (e) => c(e.prompt) ? {
35
+ text: e.prompt.prompt,
36
+ system: e.system ?? e.prompt.system,
37
+ rendered: e.prompt
18
38
  } : {
19
- promptDigest: v(e),
20
- promptChars: e.length,
21
- ...t === void 0 ? {} : {
22
- systemDigest: v(t),
23
- systemChars: t.length
24
- }
25
- }, b = (e) => ({
39
+ text: e.prompt,
40
+ system: e.system,
41
+ rendered: void 0
42
+ }, T = (e) => ({
26
43
  provider: String(e.provider ?? "unknown"),
27
44
  model: String(e.model ?? "unknown")
28
- }), x = (e, n, r, i) => {
45
+ }), E = (e, t, n, r, a) => {
29
46
  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, {
47
+ if (o === void 0) return p.void;
48
+ let { provider: s, model: c } = T(t), l = f(r.providerMetadata);
49
+ return i(o, {
33
50
  provider: s,
34
51
  model: c,
35
- operation: r,
52
+ operation: n,
36
53
  agent: e.name,
37
- usage: i.usage,
54
+ usage: r.usage,
38
55
  ...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 {
56
+ ...l === void 0 ? {} : { actualCostUsd: l },
57
+ ...a === void 0 ? {} : { prompt: a }
58
+ }).pipe(p.asVoid, p.catchAllCause(() => p.void));
59
+ }, D = (e, t) => {
60
+ let n = e.store;
61
+ return n === void 0 || t === void 0 ? p.void : d(n, {
62
+ ...t.definition,
63
+ digest: t.digest
64
+ }).pipe(p.asVoid, p.catchAllCause(() => p.void));
65
+ }, O = m.Struct({
66
+ text: m.String,
67
+ inputTokens: m.NullOr(m.Number),
68
+ outputTokens: m.NullOr(m.Number),
69
+ totalTokens: m.NullOr(m.Number)
70
+ }), k = (e) => typeof e == "object" && !!e && e.__voltro_ai_error === !0, A = class extends Error {
47
71
  attempts;
48
72
  constructor(e, t, n) {
49
73
  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
74
  }
51
- }, T = class extends Error {
75
+ }, j = class extends Error {
52
76
  constructor(e) {
53
77
  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
78
  }
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({
79
+ }, M = (e, t, n, r, i) => p.gen(function* () {
80
+ let a = e.store;
81
+ if (a === void 0) return yield* p.die(new j(e.name));
82
+ let o = yield* g;
83
+ if (o === void 0) return yield* p.die(/* @__PURE__ */ Error(`aiStep('${e.name}'): offload: true is only valid inside a workflow body — there is no execution to suspend here.`));
84
+ let { provider: c, model: d } = T(t), f = l(o, e.name), m = yield* p.promise(() => a.query({
61
85
  table: "_voltro_workflow_runs",
62
- predicate: d("executionId", a),
86
+ predicate: h("executionId", o),
63
87
  order: [],
64
88
  take: 1,
65
89
  skip: void 0,
66
90
  projection: void 0
67
- })), g = String(h[0]?.tag ?? ""), _ = h[0]?.id;
68
- return yield* l.promise(() => i.insertIgnore(c, {
69
- id: m,
91
+ })), _ = String(m[0]?.tag ?? ""), v = m[0]?.id;
92
+ return yield* p.promise(() => a.insertIgnore(u, {
93
+ id: f,
70
94
  tenantId: e.tenantId ?? null,
71
- executionId: a,
72
- workflowName: g,
73
- runId: typeof _ == "string" ? _ : null,
74
- signalName: o(m),
95
+ executionId: o,
96
+ workflowName: _,
97
+ runId: typeof v == "string" ? v : null,
98
+ signalName: s(f),
75
99
  stepName: e.name,
76
100
  status: "pending",
77
101
  kind: n,
78
- provider: u,
79
- model: f,
80
- prompt: e.prompt,
81
- system: e.system ?? null,
102
+ provider: c,
103
+ model: d,
104
+ prompt: i.text,
105
+ system: i.system ?? null,
106
+ promptId: i.rendered?.promptId ?? null,
107
+ promptDigest: i.rendered?.digest ?? null,
108
+ promptRevision: i.rendered?.revision ?? null,
82
109
  maxTokens: e.maxTokens ?? null,
83
110
  providerOptions: e.providerOptions ?? null,
84
111
  jsonSchema: r,
85
112
  attempts: 0,
86
113
  enqueuedAt: /* @__PURE__ */ new Date()
87
- }, { conflictColumns: ["id"] })), m;
88
- }), D = (e, t, n, r, i) => l.gen(function* () {
89
- let a = yield* h({
114
+ }, { conflictColumns: ["id"] })), f;
115
+ }), N = (e, t, n, r, i, a) => p.gen(function* () {
116
+ let o = yield* v({
90
117
  name: `${e.name}:enqueue`,
91
- success: u.String,
118
+ success: m.String,
92
119
  input: i,
93
- execute: E(e, t, n, r)
94
- }), s = yield* m({ store: e.store }, {
95
- name: o(a),
96
- schema: u.Unknown,
120
+ execute: M(e, t, n, r, a)
121
+ });
122
+ yield* D(e, a.rendered);
123
+ let c = yield* _({ store: e.store }, {
124
+ name: s(o),
125
+ schema: m.Unknown,
97
126
  timeoutMs: e.offloadTimeoutMs ?? 36e5
98
127
  });
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),
128
+ return k(c) ? yield* p.die(new A(e.name, c.message, c.attempts)) : c;
129
+ }), P = class extends Error {
130
+ constructor(e) {
131
+ super(`aiStep('${e}'): budget needs \`store\` — the reservation counter and the hold both live in tables. Pass ctx.store.`), this.name = "AiBudgetMisuseError";
132
+ }
133
+ }, F = (e) => p.gen(function* () {
134
+ let t = e.budget;
135
+ if (t === void 0) return;
136
+ let n = e.store;
137
+ if (n === void 0) return yield* p.die(new P(e.name));
138
+ let r = e.tenantId ?? null, i = t.name ?? "ai-usd", o = () => a(n, {
139
+ tenantId: r,
140
+ limitUsd: t.limitUsd,
141
+ addUsd: 0,
142
+ ...t.since === void 0 ? {} : { since: t.since }
143
+ }).pipe(p.as(null), p.catchTag("AiBudgetExceeded", (e) => p.succeed(e))), s = () => t.estimateUsd === void 0 ? p.void : a(n, {
144
+ tenantId: r,
145
+ limitUsd: t.limitUsd,
146
+ addUsd: t.estimateUsd,
147
+ ...t.since === void 0 ? {} : { since: t.since }
148
+ }), c = yield* o();
149
+ if (c === null) return yield* s();
150
+ if ((t.onExceeded ?? "fail") !== "suspend") return yield* p.fail(c);
151
+ let l = yield* g;
152
+ return l === void 0 ? yield* p.fail(c) : (yield* y({
153
+ store: n,
154
+ budget: i,
155
+ tenantId: r,
156
+ executionId: l,
157
+ workflowName: yield* I(n, l),
158
+ stepName: e.name,
159
+ spentMicroUsd: Math.round(c.spentUsd * 1e6),
160
+ limitMicroUsd: Math.round(t.limitUsd * 1e6),
161
+ hasHeadroom: o().pipe(p.map((e) => e === null)),
162
+ ...t.holdTimeoutMs === void 0 ? {} : { timeoutMs: t.holdTimeoutMs },
163
+ ...t.recheckEveryMs === void 0 ? {} : { recheckEveryMs: t.recheckEveryMs }
164
+ }).pipe(p.catchAll((e) => p.die(e))), yield* s());
165
+ }), I = (e, t) => p.promise(async () => {
166
+ let n = await e.query({
167
+ table: "_voltro_workflow_runs",
168
+ predicate: h("executionId", t),
169
+ order: [],
170
+ take: 1,
171
+ skip: void 0,
172
+ projection: void 0
173
+ });
174
+ return String(n[0]?.tag ?? "");
175
+ }), L = (t) => {
176
+ let n = t.provider ?? e(), i = w(t), a = {
177
+ ...T(n),
103
178
  operation: "generateText",
104
179
  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({
180
+ ...C(i.text, i.system, t.recordPrompt ?? "digest", i.rendered)
181
+ }, o = (e) => t.budget === void 0 ? e : p.zipRight(F(t), e);
182
+ return t.offload === !0 ? o(N(t, n, "text", null, a, i)) : o(v({
108
183
  name: t.name,
109
- success: S,
110
- input: r,
111
- retry: t.retry ?? g,
112
- execute: i({
113
- prompt: t.prompt,
184
+ success: O,
185
+ input: a,
186
+ retry: t.retry ?? b,
187
+ execute: r({
188
+ prompt: i.text,
114
189
  provider: n,
115
- ...t.system === void 0 ? {} : { system: t.system },
190
+ ...i.system === void 0 ? {} : { system: i.system },
116
191
  ...t.fallbacks === void 0 ? {} : { fallbacks: t.fallbacks },
117
192
  ...t.maxTokens === void 0 ? {} : { maxTokens: t.maxTokens },
118
193
  ...t.providerOptions === void 0 ? {} : { providerOptions: t.providerOptions }
119
- }).pipe(l.tap((e) => x(t, n, "generateText", e)), l.map((e) => ({
194
+ }).pipe(p.tap((e) => E(t, n, "generateText", e, i.rendered)), p.tap(() => D(t, i.rendered)), p.map((e) => ({
120
195
  text: e.text,
121
196
  inputTokens: e.usage.inputTokens ?? null,
122
197
  outputTokens: e.usage.outputTokens ?? null,
123
198
  totalTokens: e.usage.totalTokens ?? null
124
199
  })))
125
- });
126
- }, k = (t) => {
127
- let i = t.provider ?? e(), a = {
128
- ...b(i),
200
+ }));
201
+ }, R = (r) => {
202
+ let i = r.provider ?? e(), a = w(r), o = {
203
+ ...T(i),
129
204
  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,
205
+ offloaded: r.offload === !0,
206
+ ...C(a.text, a.system, r.recordPrompt ?? "digest", a.rendered)
207
+ }, s = (e) => r.budget === void 0 ? e : p.zipRight(F(r), e);
208
+ return r.offload === !0 ? s(p.gen(function* () {
209
+ let e = yield* n(r.schema, "aiObjectStep"), t = yield* N(r, i, "object", e, o, a);
210
+ return yield* m.decodeUnknown(r.schema)(t);
211
+ })) : s(v({
212
+ name: r.name,
213
+ success: r.schema,
214
+ input: o,
215
+ retry: r.retry ?? b,
216
+ execute: t({
217
+ prompt: a.text,
218
+ schema: r.schema,
144
219
  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
- });
220
+ ...a.system === void 0 ? {} : { system: a.system },
221
+ ...r.fallbacks === void 0 ? {} : { fallbacks: r.fallbacks },
222
+ ...r.maxTokens === void 0 ? {} : { maxTokens: r.maxTokens },
223
+ ...r.providerOptions === void 0 ? {} : { providerOptions: r.providerOptions }
224
+ }).pipe(p.tap((e) => E(r, i, "generateObject", e, a.rendered)), p.tap(() => D(r, a.rendered)), p.map((e) => e.object))
225
+ }));
151
226
  };
152
227
  //#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 };
228
+ export { b as AI_STEP_RETRY, P as AiBudgetMisuseError, A as AiOffloadError, j as AiOffloadMisuseError, S as DEFAULT_AI_BUDGET_NAME, x as DEFAULT_OFFLOAD_TIMEOUT_MS, R as aiObjectStep, L as aiStep, o as promptDigest, C as promptRecordFor, w as resolveStepPrompt };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/ai",
3
- "version": "0.32.0",
3
+ "version": "0.34.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",
@@ -42,7 +42,8 @@
42
42
  "types": "./dist/workflow.d.ts",
43
43
  "import": "./dist/workflow.js",
44
44
  "default": "./dist/workflow.js"
45
- }
45
+ },
46
+ "./package.json": "./package.json"
46
47
  },
47
48
  "main": "./dist/index.js",
48
49
  "module": "./dist/index.js",
@@ -56,10 +57,10 @@
56
57
  "@ai-sdk/gateway": "^4.0.30",
57
58
  "@ai-sdk/openai": "^4.0.22",
58
59
  "@ai-sdk/provider": "^4.0.4",
59
- "@voltro/cache": "0.32.0",
60
- "@voltro/database": "0.32.0",
61
- "@voltro/protocol": "0.32.0",
62
- "@voltro/workflow": "0.32.0",
60
+ "@voltro/cache": "0.34.0",
61
+ "@voltro/database": "0.34.0",
62
+ "@voltro/protocol": "0.34.0",
63
+ "@voltro/workflow": "0.34.0",
63
64
  "ai": "^7.0.40"
64
65
  },
65
66
  "peerDependencies": {