octaflow 0.30.0 → 0.32.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.
package/README.md CHANGED
@@ -215,7 +215,7 @@ next to what your steps actually do.
215
215
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
216
216
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
217
217
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
218
- | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, refusal-aware models, lenient JSON from local models (`withJsonRepair`), a vendor's refusal explained by key and hint (`explainProviderError`), a per-run circuit breaker, a connection test (`testProvider`) |
218
+ | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups, a `usage` sink that books each step into `@octabits-io/metering` (the paid APIs around the model in the same table, priced at read time), refusal-aware models, lenient JSON from local models (`withJsonRepair`), a vendor's refusal explained by key and hint (`explainProviderError`), a per-run circuit breaker, a connection test (`testProvider`) |
219
219
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
220
220
 
221
221
  ---
@@ -254,6 +254,12 @@ pnpm add ai @ai-sdk/provider
254
254
  a `schema`, and is meant to be pasted into your own migration system; `applySchema(pool, ddl)`
255
255
  is the dev and test shortcut.
256
256
 
257
+ The stores, the gate and the event sink reach the database only through the `SqlExecutor` seam
258
+ (`query` plus a same-connection `transaction`). Three executors ship with it: `poolExecutor(pool)`
259
+ for `pg`, `postgresJsExecutor(sql)` for a postgres.js host (`transaction` is `sql.begin`), and
260
+ `pgliteExecutor(pglite)` for PGlite in tests. Wrap any of them to set row-level-security GUCs
261
+ first.
262
+
257
263
  #### The Drizzle columns
258
264
 
259
265
  The rule for every table-owning capability on the platform: `./postgres` ships the store over
@@ -1,9 +1,10 @@
1
- import { A as TypedStep, H as RetryPolicy, M as TypedWorkflow, bt as Result, f as Decider, j as TypedStepContext, yt as FlowErrorShape } from "../index-Du05Lnwr.js";
1
+ import { M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, St as Result, U as RetryPolicy, f as Decider, gt as JoinRule, j as TypedStep, xt as FlowErrorShape } from "../index-D0XhoTkE.js";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
- import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-mWaAKHqn.js";
3
+ import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-C46TPBEg.js";
4
4
  import { C as isSubprocessorResidency, S as embeddingDimensionsFor, _ as EmbeddingProviderMeta, a as AiProcessingRegion, b as OpenAiCompatibleProviderId, c as AiProviderModelOption, d as AiVendorResidency, f as DEFAULT_AI_PROVIDER, g as EmbeddingProviderId, h as EMBEDDING_PROVIDER_IDS, i as AiEndpointResidency, l as AiResidencyError, m as EMBEDDING_PROVIDERS, n as AI_PROVIDER_IDS, o as AiProviderId, p as DEFAULT_EMBEDDING_PROVIDER, r as AiEndpointRegion, s as AiProviderMeta, t as AI_PROVIDERS, u as AiResidencyResult, v as OPENAI_COMPATIBLE_PROVIDER, w as resolveEndpointResidency, x as OpenAiCompatibleProviderMeta, y as OPENAI_COMPATIBLE_PROVIDER_ID } from "../provider-registry-C_mOfdHH.js";
5
- import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, b as PROVIDER_ERROR_REDACTED, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, g as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as ProbeableProvider, p as CircuitBreakerOptions, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as ProviderProbeFailure, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../provider-probe-l2Jmpuhr.js";
5
+ import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, b as PROVIDER_ERROR_REDACTED, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, g as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as ProbeableProvider, p as CircuitBreakerOptions, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as ProviderProbeFailure, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../provider-probe-_rio4ykx.js";
6
6
  import { z } from "zod";
7
+ import { ToolSet } from "ai";
7
8
  import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
8
9
  //#region src/ai/instrumented-model.d.ts
9
10
  export interface AccumulatedUsage {
@@ -12,6 +13,8 @@ export interface AccumulatedUsage {
12
13
  cacheReadTokens: number;
13
14
  cacheWriteTokens: number;
14
15
  modelId: string;
16
+ /** The provider the model belongs to (`model.provider`), when the call reported it. */
17
+ provider?: string;
15
18
  }
16
19
  export interface UsageAccumulator {
17
20
  /** Record usage from a single LLM call (additive — supports multiple calls per step). */
@@ -123,35 +126,18 @@ export interface AiContext<THost = unknown> {
123
126
  //#region src/ai/define-ai-step.d.ts
124
127
  /** An AI workflow step — a flow-core `TypedStep` whose context is an `AiContext`. */
125
128
  export type AiTypedStep<TInput extends Record<string, unknown> = Record<string, unknown>, TOutput extends Record<string, unknown> = Record<string, unknown>, THost = unknown, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>> = {}> = TypedStep<TInput, TOutput, TDeps, AiContext<THost>>;
126
- interface DefineAiStepConfig<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>, THost, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>>> {
127
- type: string;
128
- workflowInputSchema: z.ZodType<TInput>;
129
- outputSchema: z.ZodType<TOutput>;
130
- dependencies?: TDeps;
131
- handler: (ctx: TypedStepContext<TInput, TDeps, AiContext<THost>>) => Promise<TOutput>;
132
- /** Optional retry policy (max attempts, backoff) — e.g. for provider 429s. */
133
- retry?: RetryPolicy;
134
- /** Optional per-step wall-clock timeout in ms. */
135
- timeoutMs?: number;
136
- /** Optional durable start delay in ms — held in the queue once the step is ready. */
137
- delayMs?: number;
138
- /**
139
- * What a terminal failure does — see `defineStep`'s `onFailure`. `{ output }`
140
- * completes the step with a stand-in and the DAG carries on; the model's
141
- * refusal or outage then costs one section of the result, not the run.
142
- */
143
- onFailure?: 'fail' | {
144
- output: TOutput;
145
- };
146
- /** Resources the step draws on (the model, a vendor), for the admission gate — see `defineStep`. */
147
- resources?: readonly string[];
148
- }
129
+ /**
130
+ * `defineStep`'s config with the handler context fixed to `AiContext<THost>` —
131
+ * every option a step has (`when`, `join`, heartbeats, `onFailure`, …), so an
132
+ * AI step branches and converges like any other.
133
+ */
134
+ export type DefineAiStepConfig<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>, THost, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>>, TJoin extends JoinRule = 'all'> = DefineStepConfig<TInput, TOutput, TDeps, AiContext<THost>, TJoin>;
149
135
  /**
150
136
  * Define a type-safe AI workflow step. Identical to flow-core's `defineStep`
151
137
  * except the handler context is fixed to `AiContext<THost>`, so handlers get a
152
138
  * ready-to-use `ctx.context.model` (instrumented) and `ctx.context.host`.
153
139
  */
154
- export declare function defineAiStep<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>, THost = unknown, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>> = {}>(config: DefineAiStepConfig<TInput, TOutput, THost, TDeps>): AiTypedStep<TInput, TOutput, THost, TDeps>;
140
+ export declare function defineAiStep<TInput extends Record<string, unknown>, TOutput extends Record<string, unknown>, THost = unknown, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>> = {}, TJoin extends JoinRule = 'all'>(config: DefineAiStepConfig<TInput, TOutput, THost, TDeps, TJoin>): AiTypedStep<TInput, TOutput, THost, TDeps>;
155
141
  /** Build a typed AI workflow from `defineAiStep` definitions. */
156
142
  export declare function buildAiWorkflow<TInput extends Record<string, unknown>, THost = unknown, TSteps extends Record<string, TypedStep<any, any, any, AiContext<THost>>> = {}>(config: {
157
143
  type: string;
@@ -184,6 +170,114 @@ export interface ModelDeciderOptions {
184
170
  */
185
171
  export declare function createModelDecider<THost = unknown>(options?: ModelDeciderOptions): Decider<AiContext<THost>>;
186
172
  //#endregion
173
+ //#region src/ai/agent.d.ts
174
+ /** What one agent run did — persisted with the answer, never parsed. */
175
+ export interface AgentTrace {
176
+ /** Model calls the loop made. */
177
+ rounds: number;
178
+ /** Tool names in the order they were called, the exit last. */
179
+ toolCalls: string[];
180
+ /** Input + output tokens across all rounds. */
181
+ tokens: number;
182
+ }
183
+ /** One way out of an agent: the model ends the run by calling it, and its arguments are the value. */
184
+ export interface AgentExit<T = unknown> {
185
+ description: string;
186
+ schema: z.ZodType<T>;
187
+ }
188
+ export type AgentExits = Record<string, AgentExit<any>>;
189
+ /** The exit an agent took, typed by its name. */
190
+ export type AgentExitTaken<TExits extends AgentExits> = { [K in keyof TExits & string]: {
191
+ exit: K;
192
+ value: TExits[K] extends AgentExit<infer T> ? T : never;
193
+ }; }[keyof TExits & string];
194
+ interface AgentLimits {
195
+ /** The model — in a step, `ctx.context.model`, so usage lands on the step. */
196
+ model: LanguageModelV4;
197
+ system: string;
198
+ prompt: string;
199
+ /** The tools the agent works with. Their names must not collide with an exit's. */
200
+ tools?: ToolSet;
201
+ /** Most model calls before the run gives up. Default 8. */
202
+ maxRounds?: number;
203
+ /** Most input + output tokens before the run gives up. Default: no cap. */
204
+ maxTokens?: number;
205
+ /**
206
+ * Most output tokens per model call, passed to the AI SDK. Default: the
207
+ * provider's — which on a local server can be small enough to cut a
208
+ * thinking model off before it calls anything.
209
+ */
210
+ maxOutputTokens?: number;
211
+ /** Called after every round; `false` stops the run — pass the step's `heartbeat`. */
212
+ heartbeat?: () => Promise<boolean>;
213
+ abortSignal?: AbortSignal;
214
+ /** Passed to the AI SDK. Default 0 — the step's retry policy owns retries. */
215
+ maxRetries?: number;
216
+ }
217
+ export interface RunAgentToExitOptions<TExits extends AgentExits> extends AgentLimits {
218
+ exits: TExits;
219
+ }
220
+ export interface RunAgentOptions<TOutput> extends AgentLimits {
221
+ /** The answer's shape — the arguments of the one exit, `answer`. */
222
+ output: z.ZodType<TOutput>;
223
+ }
224
+ export interface AgentRun<TOutput> {
225
+ output: TOutput;
226
+ trace: AgentTrace;
227
+ }
228
+ /**
229
+ * Run one agent until it takes one of `exits`, or fails. Stopping on a limit
230
+ * before an exit is not retryable — the same budget would stop the same way —
231
+ * so the step fails, or takes its `onFailure` stand-in.
232
+ */
233
+ export declare function runAgentToExit<TExits extends AgentExits>(options: RunAgentToExitOptions<TExits>): Promise<AgentExitTaken<TExits> & {
234
+ trace: AgentTrace;
235
+ }>;
236
+ /** Run one agent to a typed answer: `runAgentToExit` with the single exit `answer`. */
237
+ export declare function runAgent<TOutput>(options: RunAgentOptions<TOutput>): Promise<AgentRun<TOutput>>;
238
+ /** The trace as a schema — for a step output that carries one. */
239
+ export declare const AGENT_TRACE_SCHEMA: z.ZodObject<{
240
+ rounds: z.ZodNumber;
241
+ toolCalls: z.ZodArray<z.ZodString>;
242
+ tokens: z.ZodNumber;
243
+ }, z.core.$strip>;
244
+ interface DefineAgentStepConfig<TInput extends Record<string, unknown>, TAnswer extends Record<string, unknown>, THost, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>>> {
245
+ type: string;
246
+ workflowInputSchema: z.ZodType<TInput>;
247
+ /** The answer's shape — the step outputs `{ answer, trace }`. */
248
+ answerSchema: z.ZodType<TAnswer>;
249
+ dependencies?: TDeps;
250
+ system: string;
251
+ /** The task, built from the workflow input and the dependencies' outputs. */
252
+ prompt: (ctx: TypedStepContext<TInput, TDeps, AiContext<THost>>) => string;
253
+ tools?: (ctx: TypedStepContext<TInput, TDeps, AiContext<THost>>) => ToolSet;
254
+ maxRounds?: number;
255
+ maxTokens?: number;
256
+ maxOutputTokens?: number;
257
+ retry?: RetryPolicy;
258
+ timeoutMs?: number;
259
+ /** See `defineStep`'s `heartbeatTimeoutMs` — every round beats. */
260
+ heartbeatTimeoutMs?: number;
261
+ /** See `defineStep`'s `onFailure`. */
262
+ onFailure?: 'fail' | {
263
+ output: {
264
+ answer: TAnswer;
265
+ trace: AgentTrace;
266
+ };
267
+ };
268
+ resources?: readonly string[];
269
+ when?: (ctx: TypedStepContext<TInput, TDeps, AiContext<THost>>) => boolean | Promise<boolean>;
270
+ }
271
+ /**
272
+ * A step that runs one agent on the step's instrumented model. Its output is
273
+ * `{ answer, trace }`: dependents read `ctx.deps.<key>.answer`, and the trace
274
+ * stays on the run for whoever reads it back.
275
+ */
276
+ export declare function defineAgentStep<TInput extends Record<string, unknown>, TAnswer extends Record<string, unknown>, THost = unknown, TDeps extends Record<string, TypedStep<any, any, any, AiContext<THost>>> = {}>(config: DefineAgentStepConfig<TInput, TAnswer, THost, TDeps>): TypedStep<TInput, {
277
+ answer: TAnswer;
278
+ trace: AgentTrace;
279
+ }, TDeps, AiContext<THost>>;
280
+ //#endregion
187
281
  //#region src/ai/hooks.d.ts
188
282
  export interface AiModelResolver<THost = unknown> {
189
283
  /** Resolve the base (uninstrumented) model for a step. */
@@ -222,6 +316,32 @@ export interface AiUsageRecorder {
222
316
  date: string;
223
317
  }): Promise<void>;
224
318
  }
319
+ /**
320
+ * What one step's model usage looks like to a metering sink: the booking
321
+ * `@octabits-io/metering`'s `record` takes, by shape — tokens as the vendor
322
+ * reported them, the provider as the service, and the attribution octaflow
323
+ * knows (the run, the workflow type as the feature, the step). A sink that
324
+ * runs inside `withUsage` adds the paying account from the ambient context.
325
+ */
326
+ export interface AiModelUsageEvent {
327
+ kind: 'model';
328
+ /** The provider (`model.provider`), or `'model'` when the call did not report one. */
329
+ service: string;
330
+ model: string;
331
+ inputTokens: number;
332
+ outputTokens: number;
333
+ cacheReadTokens: number;
334
+ cacheWriteTokens: number;
335
+ context: {
336
+ runId: string;
337
+ feature: string;
338
+ correlationId?: string;
339
+ };
340
+ }
341
+ /** Where a step's model usage is booked — `@octabits-io/metering`'s `Metering` satisfies it. Optional: without one nothing is booked. */
342
+ export interface AiUsageSink {
343
+ record(event: AiModelUsageEvent): unknown;
344
+ }
225
345
  export interface AiQuotaPolicy {
226
346
  /** Return an error (e.g. `ai_quota_exceeded`) to reject a workflow start. */
227
347
  checkQuota(): Promise<Result<void, FlowErrorShape>>;
@@ -231,6 +351,12 @@ export interface CreateAiWorkflowHooksDeps<THost = unknown> {
231
351
  usageRecorder: AiUsageRecorder;
232
352
  /** Optional quota guard. BYOK exemption etc. is the policy's concern. */
233
353
  quotaPolicy?: AiQuotaPolicy;
354
+ /**
355
+ * Optional metering sink: every step's model usage is booked here as well,
356
+ * unpriced, attributed to the run and the workflow type — so a model call
357
+ * and the paid API calls around it land in one table with one context.
358
+ */
359
+ usage?: AiUsageSink;
234
360
  /** Defaults to `createCostEstimator()` (the built-in pricing table). */
235
361
  costEstimator?: CostEstimator;
236
362
  /** Clock injection for tests. */
package/dist/ai/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { n as err, r as ok, t as noopLogger } from "../logger-epkdcXDA.js";
2
- import { _ as defineStep, m as buildWorkflow } from "../core-DBqxBk3R.js";
2
+ import { C as nonRetryableError, _ as defineStep, m as buildWorkflow } from "../core-DBqxBk3R.js";
3
3
  import { S as readRefusalDetails, _ as maskKeys, a as testProvider, b as createRefusalAwareModel, c as DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, d as PROVIDER_ERROR_REDACTED, f as PROVIDER_ERROR_STATUS, g as isProviderError, h as isFatalProviderError, i as testOllamaModel, l as FATAL_PROVIDER_ERROR_KEYS, m as explainProviderError, n as DEFAULT_PROVIDER_PROBE_PROMPT, o as createCircuitBreaker, p as ProviderError, r as DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, s as withCircuitBreaker, t as DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, u as PROVIDER_ERROR_PHRASES, v as AiRefusalError, x as isAiRefusalError, y as REFUSAL_FINISH_REASON } from "../provider-probe-CGN2MpaZ.js";
4
4
  import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, i as DEFAULT_EMBEDDING_PROVIDER, l as embeddingDimensionsFor, n as AI_PROVIDER_IDS, o as EMBEDDING_PROVIDER_IDS, r as DEFAULT_AI_PROVIDER, s as OPENAI_COMPATIBLE_PROVIDER, t as AI_PROVIDERS, u as isSubprocessorResidency } from "../provider-registry-B91lgOmu.js";
5
5
  import { z } from "zod";
6
- import { Output, generateText, wrapEmbeddingModel, wrapLanguageModel } from "ai";
6
+ import { Output, generateText, stepCountIs, tool, wrapEmbeddingModel, wrapLanguageModel } from "ai";
7
7
  //#region src/ai/instrumented-model.ts
8
8
  function createUsageAccumulator() {
9
9
  const total = {
@@ -21,6 +21,7 @@ function createUsageAccumulator() {
21
21
  total.cacheReadTokens += usage.cacheReadTokens;
22
22
  total.cacheWriteTokens += usage.cacheWriteTokens;
23
23
  if (usage.modelId) total.modelId = usage.modelId;
24
+ if (usage.provider) total.provider = usage.provider;
24
25
  hasAny = true;
25
26
  },
26
27
  get() {
@@ -48,7 +49,8 @@ function createInstrumentedModel(model, accumulator) {
48
49
  outputTokens: usage.outputTokens?.total ?? 0,
49
50
  cacheReadTokens: usage.inputTokens?.cacheRead ?? 0,
50
51
  cacheWriteTokens: usage.inputTokens?.cacheWrite ?? 0,
51
- modelId: model.modelId
52
+ modelId: model.modelId,
53
+ provider: model.provider
52
54
  });
53
55
  return result;
54
56
  },
@@ -63,7 +65,8 @@ function createInstrumentedModel(model, accumulator) {
63
65
  outputTokens: u.outputTokens?.total ?? 0,
64
66
  cacheReadTokens: u.inputTokens?.cacheRead ?? 0,
65
67
  cacheWriteTokens: u.inputTokens?.cacheWrite ?? 0,
66
- modelId: model.modelId
68
+ modelId: model.modelId,
69
+ provider: model.provider
67
70
  });
68
71
  }
69
72
  } }));
@@ -409,6 +412,141 @@ function createModelDecider(options = {}) {
409
412
  };
410
413
  }
411
414
  //#endregion
415
+ //#region src/ai/agent.ts
416
+ const DEFAULT_MAX_ROUNDS = 8;
417
+ function tokensOf(steps) {
418
+ return steps.reduce((sum, s) => sum + (s.usage.inputTokens ?? 0) + (s.usage.outputTokens ?? 0), 0);
419
+ }
420
+ /** The first exit called with valid arguments in a round — an invalid one is answered with its error and the loop goes on. */
421
+ function exitIn(step, exits) {
422
+ return step?.toolCalls.find((c) => !c.invalid && Object.hasOwn(exits, c.toolName));
423
+ }
424
+ /**
425
+ * Run one agent until it takes one of `exits`, or fails. Stopping on a limit
426
+ * before an exit is not retryable — the same budget would stop the same way —
427
+ * so the step fails, or takes its `onFailure` stand-in.
428
+ */
429
+ async function runAgentToExit(options) {
430
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
431
+ const { maxTokens, exits } = options;
432
+ for (const name of Object.keys(exits)) if (options.tools && Object.hasOwn(options.tools, name)) throw new Error(`agent tool "${name}" collides with an exit`);
433
+ const exitTools = Object.fromEntries(Object.entries(exits).map(([name, e]) => [name, tool({
434
+ description: e.description,
435
+ inputSchema: e.schema,
436
+ execute: async () => ({ accepted: true })
437
+ })]));
438
+ const stopWhen = [({ steps }) => exitIn(steps.at(-1), exits) !== void 0];
439
+ if (maxTokens !== void 0) stopWhen.push(({ steps }) => tokensOf(steps) >= maxTokens);
440
+ const abort = new AbortController();
441
+ const signal = options.abortSignal ? AbortSignal.any([options.abortSignal, abort.signal]) : abort.signal;
442
+ const exitList = Object.keys(exits).join(", ");
443
+ const reminder = (finish) => finish === "length" ? `Your reply was cut off at the output limit. Think less, act now: call a tool, or finish by calling one of: ${exitList}.` : `Continue with your tools. To finish, call one of: ${exitList}.`;
444
+ const steps = [];
445
+ const messages = [{
446
+ role: "user",
447
+ content: options.prompt
448
+ }];
449
+ let taken;
450
+ while (steps.length < maxRounds) {
451
+ const result = await generateText({
452
+ model: options.model,
453
+ system: options.system,
454
+ messages,
455
+ tools: {
456
+ ...options.tools,
457
+ ...exitTools
458
+ },
459
+ stopWhen: [stepCountIs(maxRounds - steps.length), ...stopWhen],
460
+ maxOutputTokens: options.maxOutputTokens,
461
+ maxRetries: options.maxRetries ?? 0,
462
+ abortSignal: signal,
463
+ onStepFinish: options.heartbeat ? async () => {
464
+ if (!await options.heartbeat()) abort.abort();
465
+ } : void 0
466
+ });
467
+ steps.push(...result.steps);
468
+ taken = exitIn(steps.at(-1), exits);
469
+ const budgetLeft = maxTokens === void 0 || tokensOf(steps) < maxTokens;
470
+ if (taken || result.finishReason === "tool-calls" || !budgetLeft) break;
471
+ messages.push(...result.responseMessages, {
472
+ role: "user",
473
+ content: reminder(result.finishReason)
474
+ });
475
+ }
476
+ const trace = {
477
+ rounds: steps.length,
478
+ toolCalls: steps.flatMap((s) => s.toolCalls.map((c) => c.toolName)),
479
+ tokens: tokensOf(steps)
480
+ };
481
+ if (!taken) throw nonRetryableError(`agent stopped without taking an exit after ${trace.rounds} rounds and ${trace.tokens} tokens (limits: ${maxRounds} rounds${maxTokens !== void 0 ? `, ${maxTokens} tokens` : ""})`);
482
+ return {
483
+ exit: taken.toolName,
484
+ value: taken.input,
485
+ trace
486
+ };
487
+ }
488
+ /** Run one agent to a typed answer: `runAgentToExit` with the single exit `answer`. */
489
+ async function runAgent(options) {
490
+ const { output, ...rest } = options;
491
+ const run = await runAgentToExit({
492
+ ...rest,
493
+ exits: { answer: {
494
+ description: "Give your final answer. Call this once you are done.",
495
+ schema: output
496
+ } }
497
+ });
498
+ return {
499
+ output: run.value,
500
+ trace: run.trace
501
+ };
502
+ }
503
+ /** The trace as a schema — for a step output that carries one. */
504
+ const AGENT_TRACE_SCHEMA = z.object({
505
+ rounds: z.number(),
506
+ toolCalls: z.array(z.string()),
507
+ tokens: z.number()
508
+ });
509
+ /**
510
+ * A step that runs one agent on the step's instrumented model. Its output is
511
+ * `{ answer, trace }`: dependents read `ctx.deps.<key>.answer`, and the trace
512
+ * stays on the run for whoever reads it back.
513
+ */
514
+ function defineAgentStep(config) {
515
+ return defineStep({
516
+ type: config.type,
517
+ workflowInputSchema: config.workflowInputSchema,
518
+ outputSchema: z.object({
519
+ answer: config.answerSchema,
520
+ trace: AGENT_TRACE_SCHEMA
521
+ }),
522
+ dependencies: config.dependencies,
523
+ retry: config.retry,
524
+ timeoutMs: config.timeoutMs,
525
+ heartbeatTimeoutMs: config.heartbeatTimeoutMs,
526
+ onFailure: config.onFailure,
527
+ resources: config.resources,
528
+ when: config.when,
529
+ handler: async (ctx) => {
530
+ const { output, trace } = await runAgent({
531
+ model: ctx.context.model,
532
+ system: config.system,
533
+ prompt: config.prompt(ctx),
534
+ tools: config.tools?.(ctx),
535
+ output: config.answerSchema,
536
+ maxRounds: config.maxRounds,
537
+ maxTokens: config.maxTokens,
538
+ maxOutputTokens: config.maxOutputTokens,
539
+ heartbeat: ctx.heartbeat,
540
+ abortSignal: ctx.signal
541
+ });
542
+ return {
543
+ answer: output,
544
+ trace
545
+ };
546
+ }
547
+ });
548
+ }
549
+ //#endregion
412
550
  //#region src/ai/hooks.ts
413
551
  /**
414
552
  * Wire the AI behaviors (model instrumentation, token/cost capture, quota,
@@ -440,7 +578,7 @@ function createAiWorkflowHooks(deps) {
440
578
  usage
441
579
  };
442
580
  },
443
- async onAfterStep({ workflowId, stepId, context }) {
581
+ async onAfterStep({ workflowId, stepId, workflow, step, context }) {
444
582
  const acc = context.usage;
445
583
  if (!acc.hasUsage()) return;
446
584
  const usage = acc.get();
@@ -456,6 +594,19 @@ function createAiWorkflowHooks(deps) {
456
594
  usage,
457
595
  costMicros
458
596
  });
597
+ deps.usage?.record({
598
+ kind: "model",
599
+ service: usage.provider ?? "model",
600
+ model: usage.modelId,
601
+ inputTokens: usage.inputTokens,
602
+ outputTokens: usage.outputTokens,
603
+ cacheReadTokens: usage.cacheReadTokens,
604
+ cacheWriteTokens: usage.cacheWriteTokens,
605
+ context: {
606
+ runId: String(workflowId),
607
+ feature: workflow?.type ?? step?.key ?? "workflow"
608
+ }
609
+ });
459
610
  },
460
611
  async onWorkflowCompleted({ workflowId, workflow }) {
461
612
  if (!deps.usageRecorder.recordWorkflowDaily) return;
@@ -1134,4 +1285,4 @@ function isRecord(value) {
1134
1285
  return typeof value === "object" && value !== null && !Array.isArray(value);
1135
1286
  }
1136
1287
  //#endregion
1137
- export { AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_JSON_EXCERPT_LENGTH, DEFAULT_MODEL_PRICING, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, FATAL_PROVIDER_ERROR_KEYS, MODEL_JSON_PROVIDER_METADATA_KEY, MODEL_JSON_REPAIRED_ATTRIBUTE, ModelJsonError, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProviderError, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, addUsage, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCircuitBreaker, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAiStep, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, mergeModelJsonObjects, modelJsonRepairText, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
1288
+ export { AGENT_TRACE_SCHEMA, AI_PROVIDERS, AI_PROVIDER_IDS, AiRefusalError, DEFAULT_AI_PROVIDER, DEFAULT_AI_QUOTA, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_MODEL_JSON_EXCERPT_LENGTH, DEFAULT_MODEL_PRICING, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, FATAL_PROVIDER_ERROR_KEYS, MODEL_JSON_PROVIDER_METADATA_KEY, MODEL_JSON_REPAIRED_ATTRIBUTE, ModelJsonError, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProviderError, REFUSAL_FINISH_REASON, ZERO_MODEL_PRICING, addUsage, anthropicThinkingSettings, buildAiWorkflow, createAiQuotaService, createAiUsageAggregationService, createAiWorkflowHooks, createCircuitBreaker, createCostEstimator, createEmbeddingUsageAccumulator, createInstrumentedEmbeddingModel, createInstrumentedModel, createModelDecider, createRefusalAwareModel, createUsageAccumulator, defineAgentStep, defineAiStep, embeddingDimensionsFor, estimateCostMicros, excerptOf, explainProviderError, isAiRefusalError, isFatalProviderError, isModelJsonError, isProviderError, isSubprocessorResidency, maskKeys, mergeModelJsonObjects, modelJsonRepairText, parseModelJson, readRefusalDetails, recoverModelJson, resolveEndpointResidency, runAgent, runAgentToExit, supportsAdaptiveThinking, supportsToolSearch, testOllamaModel, testProvider, thinkingSettingsFor, withCircuitBreaker, withJsonRepair };
@@ -1,5 +1,5 @@
1
1
  import { b as OpenAiCompatibleProviderId, g as EmbeddingProviderId, i as AiEndpointResidency, o as AiProviderId } from "../../provider-registry-C_mOfdHH.js";
2
- import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, o as ProbeableProvider, p as CircuitBreakerOptions, s as ProviderProbeFailure, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../../provider-probe-l2Jmpuhr.js";
2
+ import { A as maskKeys, C as ProviderErrorExplanation, D as explainProviderError, E as ProviderErrorPreset, O as isFatalProviderError, S as ProviderError, T as ProviderErrorPhrase, _ as ExplainProviderErrorOptions, a as OllamaProbeReport, c as ProviderProbeOptions, d as testProvider, f as CircuitBreaker, h as withCircuitBreaker, i as OllamaProbeOptions, k as isProviderError, l as ProviderProbeReport, m as createCircuitBreaker, o as ProbeableProvider, p as CircuitBreakerOptions, s as ProviderProbeFailure, u as testOllamaModel, v as FATAL_PROVIDER_ERROR_KEYS, w as ProviderErrorKey, x as PROVIDER_ERROR_STATUS, y as PROVIDER_ERROR_PHRASES } from "../../provider-probe-_rio4ykx.js";
3
3
  import { EmbeddingModelV4, LanguageModelV4 } from "@ai-sdk/provider";
4
4
  //#region src/ai/providers/index.d.ts
5
5
  export type LanguageModelFactory = (modelId: string) => LanguageModelV4;
@@ -1,4 +1,4 @@
1
- import { bt as Result, et as StepId, st as WorkflowId, yt as FlowErrorShape } from "./index-Du05Lnwr.js";
1
+ import { St as Result, lt as WorkflowId, nt as StepId, xt as FlowErrorShape } from "./index-D0XhoTkE.js";
2
2
  //#region src/core/dispatcher.d.ts
3
3
  /**
4
4
  * What a step job is for. A queue carries two kinds of work for a step, and
@@ -1,4 +1,4 @@
1
- import { U as StartOptions, bt as Result, et as StepId, ot as WorkflowDefinition, st as WorkflowId, tt as StepRecord, ut as WorkflowRecord, yt as FlowErrorShape } from "./index-Du05Lnwr.js";
1
+ import { St as Result, W as StartOptions, ct as WorkflowDefinition, ft as WorkflowRecord, lt as WorkflowId, nt as StepId, rt as StepRecord, xt as FlowErrorShape } from "./index-D0XhoTkE.js";
2
2
  //#region src/core/hooks.d.ts
3
3
  interface BeforeStartArgs {
4
4
  definition: WorkflowDefinition;
@@ -61,6 +61,8 @@ declare function computeReadiness(steps: StepRecord[], joinOf: (stepType: string
61
61
  type WorkflowId = number;
62
62
  type StepId = number;
63
63
  type WorkflowStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
64
+ /** A durable start delay in ms — fixed, or computed from the run's input when the step becomes ready. */
65
+ type StepDelay = number | ((workflowInput: Record<string, unknown>) => number);
64
66
  type StepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'waiting' | 'mapping' | 'compensating' | 'compensated';
65
67
  /**
66
68
  * Declarative workflow definition: a DAG of steps with dependencies.
@@ -278,9 +280,11 @@ interface StepRegistration<TContext = unknown> {
278
280
  /**
279
281
  * Durable start delay in ms: once the step becomes ready (all deps complete), its
280
282
  * first dispatch is held for this long via the queue. A no-op handler with a delay
281
- * is a durable "sleep" step. Does not affect retry backoff.
283
+ * is a durable "sleep" step. Does not affect retry backoff. A function computes
284
+ * it from the run's input when the step becomes ready — a wait whose length the
285
+ * run decided (an agent's "check again in 3 days").
282
286
  */
283
- delayMs?: number;
287
+ delayMs?: StepDelay;
284
288
  /**
285
289
  * When true, a ready step **suspends** (status `waiting`) instead of being dispatched,
286
290
  * until `engine.resumeStep(workflowId, stepKey, payload)` delivers an external event.
@@ -326,7 +330,7 @@ interface StepHandlerRegistry<TContext = unknown> {
326
330
  resources?: readonly string[];
327
331
  heartbeatTimeoutMs?: number;
328
332
  heartbeat?: 'auto' | 'manual';
329
- delayMs?: number;
333
+ delayMs?: StepDelay;
330
334
  waitForEvent?: boolean;
331
335
  map?: boolean;
332
336
  childType?: string;
@@ -451,8 +455,8 @@ interface TypedStep<TInput extends Record<string, unknown> = Record<string, unkn
451
455
  readonly heartbeatTimeoutMs?: number;
452
456
  /** Who beats: the engine on a timer (`'auto'`, default) or only `ctx.heartbeat()`. */
453
457
  readonly heartbeat?: 'auto' | 'manual';
454
- /** Optional durable start delay in ms (held in the queue once the step is ready). */
455
- readonly delayMs?: number;
458
+ /** Optional durable start delay in ms (held in the queue once the step is ready), fixed or from the run's input. */
459
+ readonly delayMs?: StepDelay;
456
460
  /** When true, the step suspends (waiting) until `engine.resumeStep` delivers an event. */
457
461
  readonly waitForEvent?: boolean;
458
462
  /** Untyped guard built from the typed `when` config — `false` skips the step. */
@@ -610,8 +614,11 @@ interface DefineStepConfig<TInput extends Record<string, unknown>, TOutput exten
610
614
  * silence also means a *hung* handler.
611
615
  */
612
616
  heartbeat?: 'auto' | 'manual';
613
- /** Optional durable start delay in ms — held in the queue once the step is ready. */
614
- delayMs?: number;
617
+ /**
618
+ * Optional durable start delay in ms — held in the queue once the step is
619
+ * ready. A function computes it from the run's input at that moment.
620
+ */
621
+ delayMs?: number | ((workflowInput: TInput) => number);
615
622
  /** When true, the step suspends (waiting) until `engine.resumeStep` delivers an event. */
616
623
  waitForEvent?: boolean;
617
624
  /**
@@ -769,8 +776,8 @@ declare function defineSubWorkflowStep<TOutput extends Record<string, unknown> =
769
776
  */
770
777
  declare function defineSleepStep<TContext = unknown, TDeps extends Record<string, TypedStep<any, any, any, TContext>> = {}>(config: {
771
778
  type: string;
772
- /** How long to sleep once the step becomes ready, in ms. */
773
- sleepMs: number;
779
+ /** How long to sleep once the step becomes ready, in ms — fixed, or computed from the run's input then. */
780
+ sleepMs: StepDelay;
774
781
  dependencies?: TDeps;
775
782
  }): TypedStep<Record<string, never>, Record<string, never>, TDeps, TContext>;
776
783
  /** Minimal engine surface `buildWorkflow().start()` needs — avoids a circular import. */
@@ -1098,4 +1105,4 @@ declare function toPublicStep(step: StepRecord): PublicWorkflowStep;
1098
1105
  */
1099
1106
  declare function toPublicWorkflow(workflow: WorkflowWithSteps): PublicWorkflow;
1100
1107
  //#endregion
1101
- export { StepHandlerRegistry as $, TypedStep as A, FlowError as B, defineDecisionOutcomeStep as C, resolveDecision as D, functionDecider as E, defineMapStep as F, StepCompensationContext as G, RetryPolicy as H, defineSleepStep as I, StepError as J, StepConditionHandler as K, defineStep as L, TypedWorkflow as M, WorkflowOutput as N, StepDeps as O, buildWorkflow as P, StepHandlerNotFoundError as Q, defineSubWorkflowStep as R, DecisionStep as S, ok as St, defineDecisionStep as T, StartOptions as U, InvalidWorkflowDefinitionError as V, StepCompensateHandler as W, StepFailurePolicy as X, StepExecutionContext as Y, StepHandler as Z, DecisionOutput as _, computeReadiness as _t, STEP_DISPLAY_STATUS as a, WorkflowCreatedResult as at, DecisionResponse as b, Result as bt, WORKFLOW_STATUS_SCHEMA as c, WorkflowNotFoundError as ct, toPublicWorkflow as d, WorkflowStatus as dt, StepId as et, Decider as f, WorkflowStepRunningError as ft, DecisionOutcome as g, ReadinessPlan as gt, DecisionChoices as h, PlannedSkip as ht, PublicWorkflowStep as i, WaitTimeoutPolicy as it, TypedStepContext as j, StepOutput as k, toDisplayStepStatus as l, WorkflowNotRetryableError as lt, DecisionAnswers as m, JoinRule as mt, PUBLIC_WORKFLOW_STEP_SCHEMA as n, StepRegistration as nt, STEP_DISPLAY_STATUS_SCHEMA as o, WorkflowDefinition as ot, DecisionAnswer as p, WorkflowWithSteps as pt, StepDefinition as q, PublicWorkflow as r, StepStatus as rt, StepDisplayStatus as s, WorkflowId as st, PUBLIC_WORKFLOW_SCHEMA as t, StepRecord as tt, toPublicStep as u, WorkflowRecord as ut, DecisionQuestions as v, isTerminalStepStatus as vt, defineDecisionReviewStep as w, DecisionReview as x, err as xt, DecisionRequest as y, FlowErrorShape as yt, defineWaitStep as z };
1108
+ export { StepHandler as $, StepOutput as A, defineWaitStep as B, defineDecisionOutcomeStep as C, err as Ct, resolveDecision as D, functionDecider as E, buildWorkflow as F, StepCompensateHandler as G, InvalidWorkflowDefinitionError as H, defineMapStep as I, StepDefinition as J, StepCompensationContext as K, defineSleepStep as L, TypedStepContext as M, TypedWorkflow as N, DefineStepConfig as O, WorkflowOutput as P, StepFailurePolicy as Q, defineStep as R, DecisionStep as S, Result as St, defineDecisionStep as T, RetryPolicy as U, FlowError as V, StartOptions as W, StepError as X, StepDelay as Y, StepExecutionContext as Z, DecisionOutput as _, PlannedSkip as _t, STEP_DISPLAY_STATUS as a, StepStatus as at, DecisionResponse as b, isTerminalStepStatus as bt, WORKFLOW_STATUS_SCHEMA as c, WorkflowDefinition as ct, toPublicWorkflow as d, WorkflowNotRetryableError as dt, StepHandlerNotFoundError as et, Decider as f, WorkflowRecord as ft, DecisionOutcome as g, JoinRule as gt, DecisionChoices as h, WorkflowWithSteps as ht, PublicWorkflowStep as i, StepRegistration as it, TypedStep as j, StepDeps as k, toDisplayStepStatus as l, WorkflowId as lt, DecisionAnswers as m, WorkflowStepRunningError as mt, PUBLIC_WORKFLOW_STEP_SCHEMA as n, StepId as nt, STEP_DISPLAY_STATUS_SCHEMA as o, WaitTimeoutPolicy as ot, DecisionAnswer as p, WorkflowStatus as pt, StepConditionHandler as q, PublicWorkflow as r, StepRecord as rt, StepDisplayStatus as s, WorkflowCreatedResult as st, PUBLIC_WORKFLOW_SCHEMA as t, StepHandlerRegistry as tt, toPublicStep as u, WorkflowNotFoundError as ut, DecisionQuestions as v, ReadinessPlan as vt, defineDecisionReviewStep as w, ok as wt, DecisionReview as x, FlowErrorShape as xt, DecisionRequest as y, computeReadiness as yt, defineSubWorkflowStep as z };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as StepHandlerRegistry, A as TypedStep, B as FlowError, C as defineDecisionOutcomeStep, D as resolveDecision, E as functionDecider, F as defineMapStep, G as StepCompensationContext, H as RetryPolicy, I as defineSleepStep, J as StepError, K as StepConditionHandler, L as defineStep, M as TypedWorkflow, N as WorkflowOutput, O as StepDeps, P as buildWorkflow, Q as StepHandlerNotFoundError, R as defineSubWorkflowStep, S as DecisionStep, St as ok, T as defineDecisionStep, U as StartOptions, V as InvalidWorkflowDefinitionError, W as StepCompensateHandler, X as StepFailurePolicy, Y as StepExecutionContext, Z as StepHandler, _ as DecisionOutput, _t as computeReadiness, a as STEP_DISPLAY_STATUS, at as WorkflowCreatedResult, b as DecisionResponse, bt as Result, c as WORKFLOW_STATUS_SCHEMA, ct as WorkflowNotFoundError, d as toPublicWorkflow, dt as WorkflowStatus, et as StepId, f as Decider, ft as WorkflowStepRunningError, g as DecisionOutcome, gt as ReadinessPlan, h as DecisionChoices, ht as PlannedSkip, i as PublicWorkflowStep, it as WaitTimeoutPolicy, j as TypedStepContext, k as StepOutput, l as toDisplayStepStatus, lt as WorkflowNotRetryableError, m as DecisionAnswers, mt as JoinRule, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepRegistration, o as STEP_DISPLAY_STATUS_SCHEMA, ot as WorkflowDefinition, p as DecisionAnswer, pt as WorkflowWithSteps, q as StepDefinition, r as PublicWorkflow, rt as StepStatus, s as StepDisplayStatus, st as WorkflowId, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepRecord, u as toPublicStep, ut as WorkflowRecord, v as DecisionQuestions, vt as isTerminalStepStatus, w as defineDecisionReviewStep, x as DecisionReview, xt as err, y as DecisionRequest, yt as FlowErrorShape, z as defineWaitStep } from "./index-Du05Lnwr.js";
1
+ import { $ as StepHandler, A as StepOutput, B as defineWaitStep, C as defineDecisionOutcomeStep, Ct as err, D as resolveDecision, E as functionDecider, F as buildWorkflow, G as StepCompensateHandler, H as InvalidWorkflowDefinitionError, I as defineMapStep, J as StepDefinition, K as StepCompensationContext, L as defineSleepStep, M as TypedStepContext, N as TypedWorkflow, O as DefineStepConfig, P as WorkflowOutput, Q as StepFailurePolicy, R as defineStep, S as DecisionStep, St as Result, T as defineDecisionStep, U as RetryPolicy, V as FlowError, W as StartOptions, X as StepError, Y as StepDelay, Z as StepExecutionContext, _ as DecisionOutput, _t as PlannedSkip, a as STEP_DISPLAY_STATUS, at as StepStatus, b as DecisionResponse, bt as isTerminalStepStatus, c as WORKFLOW_STATUS_SCHEMA, ct as WorkflowDefinition, d as toPublicWorkflow, dt as WorkflowNotRetryableError, et as StepHandlerNotFoundError, f as Decider, ft as WorkflowRecord, g as DecisionOutcome, gt as JoinRule, h as DecisionChoices, ht as WorkflowWithSteps, i as PublicWorkflowStep, it as StepRegistration, j as TypedStep, k as StepDeps, l as toDisplayStepStatus, lt as WorkflowId, m as DecisionAnswers, mt as WorkflowStepRunningError, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as StepId, o as STEP_DISPLAY_STATUS_SCHEMA, ot as WaitTimeoutPolicy, p as DecisionAnswer, pt as WorkflowStatus, q as StepConditionHandler, r as PublicWorkflow, rt as StepRecord, s as StepDisplayStatus, st as WorkflowCreatedResult, t as PUBLIC_WORKFLOW_SCHEMA, tt as StepHandlerRegistry, u as toPublicStep, ut as WorkflowNotFoundError, v as DecisionQuestions, vt as ReadinessPlan, w as defineDecisionReviewStep, wt as ok, x as DecisionReview, xt as FlowErrorShape, y as DecisionRequest, yt as computeReadiness, z as defineSubWorkflowStep } from "./index-D0XhoTkE.js";
2
2
  import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
3
- import { A as FinishWorkflowParams, C as gateRuleKeys, D as CreateWorkflowStep, E as CreateWorkflowParams, F as WorkflowTreeFilter, M as ReopenWorkflowParams, N as TransactionalScope, O as CreatedWorkflow, P as WorkflowStore, S as createInMemoryStepGate, T as CompleteStepParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ListWorkflowsFilters, k as FailStepParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as AddChildStep, x as StepGateRequest, y as StepGate } from "./observability-CkC4FRCp.js";
4
- import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-ClQMXA9E.js";
5
- import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-mWaAKHqn.js";
3
+ import { A as FinishWorkflowParams, C as gateRuleKeys, D as CreateWorkflowStep, E as CreateWorkflowParams, F as WorkflowTreeFilter, M as ReopenWorkflowParams, N as TransactionalScope, O as CreatedWorkflow, P as WorkflowStore, S as createInMemoryStepGate, T as CompleteStepParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ListWorkflowsFilters, k as FailStepParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as AddChildStep, x as StepGateRequest, y as StepGate } from "./observability-0MLzBjuV.js";
4
+ import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-B3ruILCN.js";
5
+ import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-C46TPBEg.js";
6
6
  //#region src/core/registry.d.ts
7
7
  /**
8
8
  * Creates an in-memory step handler registry.
@@ -168,4 +168,4 @@ export declare function createInMemoryWorkflowStore(partitionKey?: string): Work
168
168
  };
169
169
  };
170
170
  //#endregion
171
- export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, Decider, DecisionAnswer, DecisionAnswers, DecisionChoices, DecisionOutcome, DecisionOutput, DecisionQuestions, DecisionRequest, DecisionResponse, DecisionReview, DecisionStep, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowMeter, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepFailurePolicy, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStepRunningError, WorkflowStore, WorkflowTreeFilter, WorkflowWithSteps, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createMeterObserver, createRecordingObserver, createRecordingTracer, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, functionDecider, gateRuleKeys, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, resolveDecision, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
171
+ export { AddChildStep, AfterStepArgs, BeforeStartArgs, BeforeStartResult, BuildStepContextArgs, CompleteStepParams, ConcurrencyRule, CreateWorkflowParams, CreateWorkflowStep, CreatedWorkflow, Decider, DecisionAnswer, DecisionAnswers, DecisionChoices, DecisionOutcome, DecisionOutput, DecisionQuestions, DecisionRequest, DecisionResponse, DecisionReview, DecisionStep, DefineStepConfig, DispatchKind, DispatchStepPayload, Dispatcher, EnqueueOptions, FailStepParams, FinishWorkflowParams, FlowError, FlowErrorShape, FlowEvent, FlowEventType, FlowMeter, FlowObserver, FlowSpan, FlowTracer, InMemoryStepGateConfig, InvalidWorkflowDefinitionError, JoinRule, ListWorkflowsFilters, LogAttributes, Logger, PUBLIC_WORKFLOW_SCHEMA, PUBLIC_WORKFLOW_STEP_SCHEMA, PlannedSkip, PublicWorkflow, PublicWorkflowStep, RateRule, ReadinessPlan, RecordedSpan, RecordingObserver, RecordingTracer, ReopenWorkflowParams, Result, RetryPolicy, STEP_DISPLAY_STATUS, STEP_DISPLAY_STATUS_SCHEMA, StartOptions, StepCompensateHandler, StepCompensationContext, StepConditionHandler, StepDefinition, StepDelay, StepDeps, StepDisplayStatus, StepError, StepExecutionContext, StepFailurePolicy, StepGate, StepGateDecision, StepGateRequest, StepHandler, StepHandlerNotFoundError, StepHandlerRegistry, StepId, StepOutput, StepRecord, StepRegistration, StepStatus, TransactionalScope, TypedStep, TypedStepContext, TypedWorkflow, WORKFLOW_STATUS_SCHEMA, WaitTimeoutPolicy, WorkflowCompletedArgs, WorkflowCreatedResult, WorkflowDefinition, WorkflowHooks, WorkflowId, WorkflowNotFoundError, WorkflowNotRetryableError, WorkflowOutput, WorkflowRecord, WorkflowStatus, WorkflowStepRunningError, WorkflowStore, WorkflowTreeFilter, WorkflowWithSteps, buildWorkflow, combineObservers, computeReadiness, createInMemoryStepGate, createMeterObserver, createRecordingObserver, createRecordingTracer, defineDecisionOutcomeStep, defineDecisionReviewStep, defineDecisionStep, defineMapStep, defineSleepStep, defineStep, defineSubWorkflowStep, defineWaitStep, err, functionDecider, gateRuleKeys, isTerminalStepStatus, noopLogger, noopObserver, noopTracer, ok, resolveDecision, toDisplayStepStatus, toPublicStep, toPublicWorkflow };
package/dist/index.js CHANGED
@@ -436,10 +436,18 @@ function createWorkflowEngine(deps) {
436
436
  }
437
437
  };
438
438
  }
439
- /** Durable start delay for a ready step, from its registered `delayMs`. */
440
- function readyStepDelay(stepType) {
441
- const delayMs = registry.getRegistration(stepType)?.delayMs;
442
- if (!delayMs || delayMs <= 0) return void 0;
439
+ /**
440
+ * Durable start delay for a ready step, from its registered `delayMs`. A
441
+ * function reads the run's input — the one extra read, and only for such steps.
442
+ */
443
+ async function readyStepDelay(stepType, workflowId, st = store) {
444
+ const delay = registry.getRegistration(stepType)?.delayMs;
445
+ let delayMs = typeof delay === "number" ? delay : void 0;
446
+ if (typeof delay === "function") {
447
+ const workflow = await st.getWorkflow(workflowId);
448
+ delayMs = workflow ? delay(workflow.input ?? {}) : void 0;
449
+ }
450
+ if (!delayMs || !Number.isFinite(delayMs) || delayMs <= 0) return void 0;
443
451
  return { startAfterSeconds: Math.ceil(delayMs / 1e3) };
444
452
  }
445
453
  /** Whether a step type suspends on readiness instead of dispatching. */
@@ -531,7 +539,7 @@ function createWorkflowEngine(deps) {
531
539
  stepKey,
532
540
  stepType
533
541
  };
534
- const delay = readyStepDelay(stepType);
542
+ const delay = await readyStepDelay(stepType, workflowId, st);
535
543
  const result = scope ? await dispatcher.enqueueStepIn(scope.handle, payload, delay) : await dispatcher.enqueueStep(payload, delay);
536
544
  if (!result.ok) {
537
545
  logger.error("Failed to enqueue step", new Error(result.error.message), {
@@ -1289,7 +1297,7 @@ function createWorkflowEngine(deps) {
1289
1297
  stepId: child.id,
1290
1298
  stepKey: child.key,
1291
1299
  stepType: child.type
1292
- }, readyStepDelay(child.type));
1300
+ }, await readyStepDelay(child.type, workflowId));
1293
1301
  if (!result.ok) logger.error("Failed to enqueue map child", new Error(result.error.message), {
1294
1302
  workflowId,
1295
1303
  stepKey: child.key
@@ -1,4 +1,4 @@
1
- import { dt as WorkflowStatus, et as StepId, pt as WorkflowWithSteps, st as WorkflowId, tt as StepRecord, ut as WorkflowRecord } from "./index-Du05Lnwr.js";
1
+ import { ft as WorkflowRecord, ht as WorkflowWithSteps, lt as WorkflowId, nt as StepId, pt as WorkflowStatus, rt as StepRecord } from "./index-D0XhoTkE.js";
2
2
  //#region src/core/store.d.ts
3
3
  interface CreateWorkflowStep {
4
4
  key: string;
@@ -1,6 +1,6 @@
1
- import "../index-Du05Lnwr.js";
1
+ import "../index-D0XhoTkE.js";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
- import { r as Dispatcher } from "../dispatcher-ClQMXA9E.js";
3
+ import { r as Dispatcher } from "../dispatcher-B3ruILCN.js";
4
4
  import { z } from "zod";
5
5
  import { PgBoss, Schedule } from "pg-boss";
6
6
  //#region src/pg-boss/payload.d.ts
@@ -1,5 +1,5 @@
1
- import { st as WorkflowId } from "../index-Du05Lnwr.js";
2
- import { P as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-CkC4FRCp.js";
1
+ import { lt as WorkflowId } from "../index-D0XhoTkE.js";
2
+ import { P as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-0MLzBjuV.js";
3
3
  import { Pool } from "pg";
4
4
  //#region src/postgres/ddl.d.ts
5
5
  /**
@@ -42,6 +42,53 @@ export interface SqlExecutor {
42
42
  * batteries-included executor used by the `createPg*` adapters.
43
43
  */
44
44
  export declare function poolExecutor(pool: Pool): SqlExecutor;
45
+ /**
46
+ * The slice of a postgres.js `sql` instance {@link postgresJsExecutor} uses —
47
+ * structural, so the driver is never imported. `sql.unsafe` resolves to the row
48
+ * list with the affected-row `count` on it; `sql.begin` runs its callback on one
49
+ * reserved connection inside `BEGIN`/`COMMIT` and hands it a `sql` of the same shape.
50
+ */
51
+ export interface PostgresJsSqlLike {
52
+ unsafe(query: string, params?: unknown[]): PromiseLike<unknown[] & {
53
+ count?: number | null;
54
+ }>;
55
+ begin<T>(fn: (tx: PostgresJsSqlLike) => Promise<T>): PromiseLike<T>;
56
+ }
57
+ /**
58
+ * Build a {@link SqlExecutor} over a postgres.js `sql` instance: top-level
59
+ * queries run on its pool (autocommit); `transaction` runs `fn` inside
60
+ * `sql.begin`, so every query of `fn` shares one connection and a throw rolls
61
+ * back. For a host whose app already speaks postgres.js (Drizzle's
62
+ * `drizzle-orm/postgres-js`) and that would otherwise open a `pg` pool just for
63
+ * the workflow store.
64
+ *
65
+ * ```ts
66
+ * import postgres from 'postgres';
67
+ * const sql = postgres(process.env.DATABASE_URL);
68
+ * const store = createWorkflowStore({ exec: postgresJsExecutor(sql), partitionKey });
69
+ * ```
70
+ *
71
+ * Wrap the result to set row-level-security GUCs first, exactly as with
72
+ * {@link poolExecutor}.
73
+ */
74
+ export declare function postgresJsExecutor(sql: PostgresJsSqlLike): SqlExecutor;
75
+ /** The slice of a PGlite instance {@link pgliteExecutor} uses — structural, so PGlite is never imported. */
76
+ export interface PgliteLike {
77
+ query<R>(query: string, params?: unknown[]): Promise<{
78
+ rows: R[];
79
+ affectedRows?: number;
80
+ }>;
81
+ }
82
+ /** A PGlite instance: {@link PgliteLike} plus its `transaction`. */
83
+ export interface PgliteDatabaseLike extends PgliteLike {
84
+ transaction<T>(fn: (tx: PgliteLike) => Promise<T>): Promise<T>;
85
+ }
86
+ /**
87
+ * Build a {@link SqlExecutor} over a PGlite instance — the embedded Postgres a
88
+ * host runs its tests (or a single-process deployment) on. `transaction` uses
89
+ * PGlite's own, which serialises with every other query on the instance.
90
+ */
91
+ export declare function pgliteExecutor(db: PgliteDatabaseLike): SqlExecutor;
45
92
  /**
46
93
  * Normalize a `Pool | SqlExecutor` argument to a {@link SqlExecutor}. Lets the
47
94
  * read helpers accept either the batteries-included pool or a host's RLS-scoped
@@ -126,6 +126,58 @@ function poolExecutor(pool) {
126
126
  };
127
127
  }
128
128
  /**
129
+ * Build a {@link SqlExecutor} over a postgres.js `sql` instance: top-level
130
+ * queries run on its pool (autocommit); `transaction` runs `fn` inside
131
+ * `sql.begin`, so every query of `fn` shares one connection and a throw rolls
132
+ * back. For a host whose app already speaks postgres.js (Drizzle's
133
+ * `drizzle-orm/postgres-js`) and that would otherwise open a `pg` pool just for
134
+ * the workflow store.
135
+ *
136
+ * ```ts
137
+ * import postgres from 'postgres';
138
+ * const sql = postgres(process.env.DATABASE_URL);
139
+ * const store = createWorkflowStore({ exec: postgresJsExecutor(sql), partitionKey });
140
+ * ```
141
+ *
142
+ * Wrap the result to set row-level-security GUCs first, exactly as with
143
+ * {@link poolExecutor}.
144
+ */
145
+ function postgresJsExecutor(sql) {
146
+ const over = (conn, inTransaction) => ({
147
+ async query(text, params) {
148
+ const rows = await conn.unsafe(text, params ?? []);
149
+ return {
150
+ rows: Array.from(rows),
151
+ rowCount: rows.count ?? rows.length
152
+ };
153
+ },
154
+ transaction(fn) {
155
+ return inTransaction ? fn(over(conn, true)) : Promise.resolve(conn.begin((tx) => fn(over(tx, true))));
156
+ }
157
+ });
158
+ return over(sql, false);
159
+ }
160
+ /**
161
+ * Build a {@link SqlExecutor} over a PGlite instance — the embedded Postgres a
162
+ * host runs its tests (or a single-process deployment) on. `transaction` uses
163
+ * PGlite's own, which serialises with every other query on the instance.
164
+ */
165
+ function pgliteExecutor(db) {
166
+ const over = (conn, tx) => ({
167
+ async query(text, params) {
168
+ const res = await conn.query(text, params ?? []);
169
+ return {
170
+ rows: res.rows,
171
+ rowCount: res.affectedRows ?? res.rows.length
172
+ };
173
+ },
174
+ transaction(fn) {
175
+ return tx ? fn(over(conn, true)) : db.transaction((t) => fn(over(t, true)));
176
+ }
177
+ });
178
+ return over(db, false);
179
+ }
180
+ /**
129
181
  * Normalize a `Pool | SqlExecutor` argument to a {@link SqlExecutor}. Lets the
130
182
  * read helpers accept either the batteries-included pool or a host's RLS-scoped
131
183
  * executor without a breaking signature change. `SqlExecutor` is discriminated by
@@ -794,4 +846,4 @@ async function readFlowEvents(db, params) {
794
846
  }));
795
847
  }
796
848
  //#endregion
797
- export { FLOW_EVENT_DDL, FLOW_GATE_DDL, FLOW_STORE_DDL, applySchema, createEventSink, createPgEventSink, createPgStepGate, createPgWorkflowStore, createSchemaDdl, createStepGate, createWorkflowStore, flowEventDdl, flowGateDdl, flowStoreDdl, poolExecutor, readFlowEvents, toExecutor };
849
+ export { FLOW_EVENT_DDL, FLOW_GATE_DDL, FLOW_STORE_DDL, applySchema, createEventSink, createPgEventSink, createPgStepGate, createPgWorkflowStore, createSchemaDdl, createStepGate, createWorkflowStore, flowEventDdl, flowGateDdl, flowStoreDdl, pgliteExecutor, poolExecutor, postgresJsExecutor, readFlowEvents, toExecutor };
@@ -1,4 +1,4 @@
1
- import { bt as Result } from "./index-Du05Lnwr.js";
1
+ import { St as Result } from "./index-D0XhoTkE.js";
2
2
  import { o as AiProviderId } from "./provider-registry-C_mOfdHH.js";
3
3
  import { LanguageModelV4 } from "@ai-sdk/provider";
4
4
  //#region src/ai/provider-errors.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octaflow",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -122,6 +122,7 @@
122
122
  "@ai-sdk/openai": "^4.0.95",
123
123
  "@ai-sdk/openai-compatible": "^3.0.70",
124
124
  "@ai-sdk/provider": "^4.0.28",
125
+ "@electric-sql/pglite": "0.5.10",
125
126
  "@testcontainers/postgresql": "^12.2.1",
126
127
  "@types/node": "^26.6.5",
127
128
  "@types/pg": "^8.23.1",