octaflow 0.29.2 → 0.31.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
@@ -198,13 +198,14 @@ next to what your steps actually do.
198
198
  | 🧩 | **Typed DAG** — Zod-validated input/output per step; dependency outputs are typed |
199
199
  | ⚡ | **Auto-parallelism** — dependency-free steps run concurrently; a step starts when all its deps complete |
200
200
  | 🔁 | **Retry & timeout** — per-step `maxAttempts`, fixed/exponential backoff, wall-clock timeout |
201
+ | 🩹 | **Best-effort steps** — `onFailure: { output }` completes a step with a stand-in once its budget is spent, so one analysis that could not run costs a section of the result, not the run |
201
202
  | 💤 | **Durable sleep** — hold a step in the queue for N ms (survives restarts) |
202
203
  | 🔀 | **Conditional branching** — `when` guards skip a step and its branch; `join: 'any'` converges |
203
204
  | 🎯 | **Decisions** — typed choices with a confidence from a swappable decider (a function, a model, your own); below a threshold a person settles it |
204
205
  | ⏳ | **Deadlines** — a budget on a suspended step (fail, or continue with a stand-in answer) and on a whole run |
205
206
  | ♻️ | **Retry a failed run** — `retryWorkflow` resumes from the failure point; completed steps keep their output |
206
207
  | 🧹 | **Delete the runs about a record** — `purgeWorkflows({ entityRef })` cancels and deletes them with their sub-workflow runs and steps, for an erasure or a deleted record; refuses only while a step is executing |
207
- | 🚦 | **Concurrency & rate limiting** — per-step-type caps and token buckets via a pluggable gate |
208
+ | 🚦 | **Concurrency & rate limiting** — caps and token buckets per step type or per shared resource (`resources: ['llm:local']`), via a pluggable gate |
208
209
  | ⏰ | **Cron / scheduled starts** — fire workflows on a schedule (pg-boss) |
209
210
  | 🔑 | **Start idempotency** — a dedup key collapses double-clicks / overlapping ticks |
210
211
  | 🗺️ | **Dynamic fan-out / map** — spawn one child step per item of a runtime-sized list |
@@ -214,7 +215,7 @@ next to what your steps actually do.
214
215
  | 🚑 | **Crash recovery** — a step whose worker died is re-queued while its attempt budget lasts |
215
216
  | 💓 | **Heartbeats** — a long step proves it is alive, so a dead one is caught in seconds, not minutes |
216
217
  | 🔭 | **Observability** — lifecycle events (run history), per-step spans, and `createMeterObserver` turning the events into OpenTelemetry-style metrics — all pluggable, all structural |
217
- | 🤖 | **AI add-on** — instrumented models, token/cost capture, quota, daily rollups |
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`) |
218
219
  | 🧱 | **Pluggable everything** — `WorkflowStore`, `Dispatcher`, `StepGate`, `FlowObserver`, hooks |
219
220
 
220
221
  ---
@@ -1,9 +1,10 @@
1
- import { A as TypedStep, H as RetryPolicy, M as TypedWorkflow, f as Decider, j as TypedStepContext, vt as FlowErrorShape, yt as Result } from "../index-B8SvtiYn.js";
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";
2
2
  import { n as Logger } from "../logger-BCus3AOa.js";
3
- import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-BV-Vany9.js";
3
+ import { i as BuildStepContextArgs, o as WorkflowHooks } from "../hooks-mWaAKHqn.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
6
  import { z } from "zod";
6
- import { EmbeddingModelV4, LanguageModelV4, SharedV4ProviderMetadata } from "@ai-sdk/provider";
7
+ import { EmbeddingModelV4, LanguageModelV4, LanguageModelV4Usage, SharedV4ProviderMetadata } from "@ai-sdk/provider";
7
8
  //#region src/ai/instrumented-model.d.ts
8
9
  export interface AccumulatedUsage {
9
10
  inputTokens: number;
@@ -11,6 +12,8 @@ export interface AccumulatedUsage {
11
12
  cacheReadTokens: number;
12
13
  cacheWriteTokens: number;
13
14
  modelId: string;
15
+ /** The provider the model belongs to (`model.provider`), when the call reported it. */
16
+ provider?: string;
14
17
  }
15
18
  export interface UsageAccumulator {
16
19
  /** Record usage from a single LLM call (additive — supports multiple calls per step). */
@@ -134,6 +137,16 @@ interface DefineAiStepConfig<TInput extends Record<string, unknown>, TOutput ext
134
137
  timeoutMs?: number;
135
138
  /** Optional durable start delay in ms — held in the queue once the step is ready. */
136
139
  delayMs?: number;
140
+ /**
141
+ * What a terminal failure does — see `defineStep`'s `onFailure`. `{ output }`
142
+ * completes the step with a stand-in and the DAG carries on; the model's
143
+ * refusal or outage then costs one section of the result, not the run.
144
+ */
145
+ onFailure?: 'fail' | {
146
+ output: TOutput;
147
+ };
148
+ /** Resources the step draws on (the model, a vendor), for the admission gate — see `defineStep`. */
149
+ resources?: readonly string[];
137
150
  }
138
151
  /**
139
152
  * Define a type-safe AI workflow step. Identical to flow-core's `defineStep`
@@ -211,6 +224,32 @@ export interface AiUsageRecorder {
211
224
  date: string;
212
225
  }): Promise<void>;
213
226
  }
227
+ /**
228
+ * What one step's model usage looks like to a metering sink: the booking
229
+ * `@octabits-io/metering`'s `record` takes, by shape — tokens as the vendor
230
+ * reported them, the provider as the service, and the attribution octaflow
231
+ * knows (the run, the workflow type as the feature, the step). A sink that
232
+ * runs inside `withUsage` adds the paying account from the ambient context.
233
+ */
234
+ export interface AiModelUsageEvent {
235
+ kind: 'model';
236
+ /** The provider (`model.provider`), or `'model'` when the call did not report one. */
237
+ service: string;
238
+ model: string;
239
+ inputTokens: number;
240
+ outputTokens: number;
241
+ cacheReadTokens: number;
242
+ cacheWriteTokens: number;
243
+ context: {
244
+ runId: string;
245
+ feature: string;
246
+ correlationId?: string;
247
+ };
248
+ }
249
+ /** Where a step's model usage is booked — `@octabits-io/metering`'s `Metering` satisfies it. Optional: without one nothing is booked. */
250
+ export interface AiUsageSink {
251
+ record(event: AiModelUsageEvent): unknown;
252
+ }
214
253
  export interface AiQuotaPolicy {
215
254
  /** Return an error (e.g. `ai_quota_exceeded`) to reject a workflow start. */
216
255
  checkQuota(): Promise<Result<void, FlowErrorShape>>;
@@ -220,6 +259,12 @@ export interface CreateAiWorkflowHooksDeps<THost = unknown> {
220
259
  usageRecorder: AiUsageRecorder;
221
260
  /** Optional quota guard. BYOK exemption etc. is the policy's concern. */
222
261
  quotaPolicy?: AiQuotaPolicy;
262
+ /**
263
+ * Optional metering sink: every step's model usage is booked here as well,
264
+ * unpriced, attributed to the run and the workflow type — so a model call
265
+ * and the paid API calls around it land in one table with one context.
266
+ */
267
+ usage?: AiUsageSink;
223
268
  /** Defaults to `createCostEstimator()` (the built-in pricing table). */
224
269
  costEstimator?: CostEstimator;
225
270
  /** Clock injection for tests. */
@@ -576,4 +621,157 @@ export declare function thinkingSettingsFor(providerId: string, modelId: string,
576
621
  budgetTokens: number;
577
622
  }): ThinkingSettings;
578
623
  //#endregion
579
- export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, embeddingDimensionsFor, isSubprocessorResidency, resolveEndpointResidency };
624
+ //#region src/ai/model-json.d.ts
625
+ /** Which step produced the JSON. `retry` and `convert` are `withJsonRepair`'s. */
626
+ export type ModelJsonRepair = 'fence' | 'prose' | 'array_merge' | 'retry' | 'convert';
627
+ /**
628
+ * The span or metric attribute that names a repair, in the GenAI convention's
629
+ * namespace so `@octabits-io/telemetry`'s redaction leaves it alone: its last
630
+ * segment is `repaired`, not a credential or a person's data. Set it on the
631
+ * step's span from `withJsonRepair`'s `onRepair`, and a dashboard counts
632
+ * repairs per model.
633
+ */
634
+ export declare const MODEL_JSON_REPAIRED_ATTRIBUTE = "gen_ai.response.repaired";
635
+ /** Where a repaired answer is also reported: `result.providerMetadata.octaflow.repaired`. */
636
+ export declare const MODEL_JSON_PROVIDER_METADATA_KEY = "octaflow";
637
+ export type ModelJsonErrorKey =
638
+ /** Nothing but whitespace, or an empty fence. */
639
+ 'model_json_empty' |
640
+ /** No step produced JSON: not valid as it is, and no outermost object or array parses. */
641
+ 'model_json_invalid' |
642
+ /** The JSON is not an object or an array of objects (a scalar, an array of scalars, a mixed array). */
643
+ 'model_json_not_object' |
644
+ /** The JSON parsed but the schema refused it; `issues` carries why. */
645
+ 'model_json_schema';
646
+ /** The default length of the excerpt a {@link ModelJsonError} keeps. */
647
+ export declare const DEFAULT_MODEL_JSON_EXCERPT_LENGTH = 500;
648
+ /**
649
+ * The answer could not be read as the JSON that was asked for.
650
+ *
651
+ * `message` names the reason and never quotes the answer: it is what lands in
652
+ * a step's `error` column and in a log line. `excerpt` is the first
653
+ * `excerptLength` characters (500 by default) for a debug trace that wants to
654
+ * see what the model said.
655
+ */
656
+ export declare class ModelJsonError extends Error {
657
+ readonly name = "ModelJsonError";
658
+ readonly key: ModelJsonErrorKey;
659
+ /** The first characters of the answer, cut at the configured length. */
660
+ readonly excerpt: string;
661
+ /** The schema's issues, for `model_json_schema`. */
662
+ readonly issues?: readonly unknown[];
663
+ /** The step that produced the JSON the schema then refused. */
664
+ readonly repair?: ModelJsonRepair;
665
+ constructor(args: {
666
+ key: ModelJsonErrorKey;
667
+ excerpt: string;
668
+ issues?: readonly unknown[];
669
+ repair?: ModelJsonRepair;
670
+ });
671
+ }
672
+ /** True when `error` is a {@link ModelJsonError}, or wraps one as its `cause` (`generateObject` re-wraps). */
673
+ export declare function isModelJsonError(error: unknown): error is ModelJsonError;
674
+ export interface ParseModelJsonOptions<T> {
675
+ /** Validate the value; the issues go on the error. */
676
+ schema?: z.ZodType<T>;
677
+ /**
678
+ * Merge an array of objects into one object: array fields concatenated in
679
+ * order, the first non-empty scalar kept, `null` never overwriting a value,
680
+ * nested objects merged the same way. For a model that answered one object
681
+ * per image when one object was asked for. Off by default: an array answer
682
+ * is then returned as the array.
683
+ */
684
+ merge?: boolean;
685
+ /** How many characters of the answer the error keeps. Default 500. */
686
+ excerptLength?: number;
687
+ }
688
+ export interface ModelJsonParsed<T> {
689
+ value: T;
690
+ /** The step that produced the JSON; absent when the answer parsed as it was. */
691
+ repair?: ModelJsonRepair;
692
+ }
693
+ /**
694
+ * Read a model's answer as JSON, leniently — see the module header for the
695
+ * steps. Returns the value, and which repair it took, or a {@link ModelJsonError}.
696
+ */
697
+ export declare function parseModelJson<T = unknown>(text: string, options?: ParseModelJsonOptions<T>): Result<ModelJsonParsed<T>, ModelJsonError>;
698
+ /**
699
+ * The same recovery as a `repairText` function for the AI SDK's
700
+ * `generateObject` / `Output.object`: given the raw text the SDK could not
701
+ * parse, the repaired JSON as a string, or `null` when no step helps (the SDK
702
+ * then reports its own `NoObjectGeneratedError`).
703
+ *
704
+ * ```ts
705
+ * await generateObject({ model, schema, prompt, repairText: modelJsonRepairText({ merge: true }) });
706
+ * ```
707
+ */
708
+ export declare function modelJsonRepairText(options?: {
709
+ merge?: boolean;
710
+ }): (args: {
711
+ text: string;
712
+ error: unknown;
713
+ }) => Promise<string | null>;
714
+ /** Internal: the recovery without the schema, shared with `withJsonRepair`. */
715
+ export type RecoveredModelJson = {
716
+ ok: true;
717
+ value: Record<string, unknown> | Record<string, unknown>[];
718
+ repair?: ModelJsonRepair;
719
+ } | {
720
+ ok: false;
721
+ key: Exclude<ModelJsonErrorKey, 'model_json_schema'>;
722
+ };
723
+ /** @internal */
724
+ export declare function recoverModelJson(text: string, options: {
725
+ merge: boolean;
726
+ }): RecoveredModelJson;
727
+ /**
728
+ * Merge objects a model answered "per item" into one: array fields are
729
+ * concatenated in order, nested objects merged the same way, and for a scalar
730
+ * the first non-empty value is kept — `null` and `''` never overwrite a value.
731
+ */
732
+ export declare function mergeModelJsonObjects(objects: readonly Record<string, unknown>[]): Record<string, unknown>;
733
+ /** @internal */
734
+ export declare function excerptOf(text: string, length?: number): string;
735
+ //#endregion
736
+ //#region src/ai/json-repair.d.ts
737
+ /** What `onRepair` is told: once per repaired answer, after the answer is final. */
738
+ export interface ModelJsonRepairEvent {
739
+ /** The wrapped model's id. */
740
+ modelId: string;
741
+ /** The step that produced the JSON — the value for `gen_ai.response.repaired`. */
742
+ repair: ModelJsonRepair;
743
+ /** Requests made beyond the first: 0 for a parse-level repair, 1 for a retry or a conversion, 2 for both. */
744
+ extraRequests: number;
745
+ }
746
+ export interface JsonRepairOptions {
747
+ /** Merge an array of objects into one — see `parseModelJson`. Skipped when the requested schema is itself an array. */
748
+ merge?: boolean;
749
+ /** On a parse failure, one more request with the provider's JSON mode off. Default `false`. */
750
+ retryWithoutJsonMode?: boolean;
751
+ /**
752
+ * On a parse failure (after the retry, when both are on), one call that is
753
+ * handed the schema and the prose and asked for the JSON. `true` uses the
754
+ * wrapped model; a `LanguageModelV4` uses that one, for a cheaper converter.
755
+ * Default off.
756
+ */
757
+ convertWith?: LanguageModelV4 | true;
758
+ /** How many characters of the answer the error keeps. Default 500. */
759
+ excerptLength?: number;
760
+ /** Told of every repaired answer: set `MODEL_JSON_REPAIRED_ATTRIBUTE` on the step's span, count it on a meter. Must not throw. */
761
+ onRepair?: (event: ModelJsonRepairEvent) => void;
762
+ }
763
+ /**
764
+ * Wrap a model so a JSON answer that is nearly JSON is read as the JSON it
765
+ * meant — see the module header. The result's text becomes the canonical
766
+ * JSON, and `providerMetadata.octaflow.repaired` names the step taken.
767
+ */
768
+ export declare function withJsonRepair(model: LanguageModelV4, options?: JsonRepairOptions): LanguageModelV4;
769
+ /**
770
+ * Two usages added field by field — `inputTokens`, `outputTokens` and
771
+ * whatever a provider adds — so the instrumented model outside the wrapper
772
+ * counts every request the repair made. An undefined on one side keeps the
773
+ * other; `raw` is the last call's.
774
+ */
775
+ export declare function addUsage(a: LanguageModelV4Usage, b: LanguageModelV4Usage): LanguageModelV4Usage;
776
+ //#endregion
777
+ export { AI_PROVIDERS, AI_PROVIDER_IDS, AiEndpointRegion, AiEndpointResidency, AiProcessingRegion, AiProviderId, AiProviderMeta, AiProviderModelOption, AiResidencyError, AiResidencyResult, AiVendorResidency, CircuitBreaker, CircuitBreakerOptions, DEFAULT_AI_PROVIDER, DEFAULT_EMBEDDING_PROVIDER, DEFAULT_OLLAMA_PROBE_TIMEOUT_MS, DEFAULT_PROVIDER_ERROR_DETAIL_LENGTH, DEFAULT_PROVIDER_PROBE_PROMPT, DEFAULT_PROVIDER_PROBE_TIMEOUT_MS, EMBEDDING_PROVIDERS, EMBEDDING_PROVIDER_IDS, EmbeddingProviderId, EmbeddingProviderMeta, ExplainProviderErrorOptions, FATAL_PROVIDER_ERROR_KEYS, OPENAI_COMPATIBLE_PROVIDER, OPENAI_COMPATIBLE_PROVIDER_ID, OllamaProbeOptions, OllamaProbeReport, OpenAiCompatibleProviderId, OpenAiCompatibleProviderMeta, PROVIDER_ERROR_PHRASES, PROVIDER_ERROR_REDACTED, PROVIDER_ERROR_STATUS, ProbeableProvider, ProviderError, ProviderErrorExplanation, ProviderErrorKey, ProviderErrorPhrase, ProviderErrorPreset, ProviderProbeFailure, ProviderProbeOptions, ProviderProbeReport, createCircuitBreaker, embeddingDimensionsFor, explainProviderError, isFatalProviderError, isProviderError, isSubprocessorResidency, maskKeys, resolveEndpointResidency, testOllamaModel, testProvider, withCircuitBreaker };