@voltro/ai 0.74.0 → 0.75.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/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ import { LanguageModelV4 } from '@ai-sdk/provider';
16
16
  import { LanguageModelV4Source } from '@ai-sdk/provider';
17
17
  import { ModelMessage } from 'ai';
18
18
  import { PluginMediaPersistCapability } from '@voltro/protocol';
19
+ import { PluginMediaPersistMetadata } from '@voltro/protocol';
19
20
  import { QueryDescriptor } from '@voltro/database';
20
21
  import { QueryProcedureDescriptor } from '@voltro/protocol';
21
22
  import { Runtime } from 'effect';
@@ -28,6 +29,8 @@ import { Stream } from 'effect';
28
29
  import { Table } from '@voltro/database';
29
30
  import { ToolSet } from 'ai';
30
31
  import { TranscriptionModelV4 } from '@ai-sdk/provider';
32
+ import { VoidIfEmpty } from 'effect/Types';
33
+ import { YieldableError } from 'effect/Cause';
31
34
 
32
35
  /** Count of in-flight streams (diagnostics / leak detection). */
33
36
  export declare const activeStreamCount: () => number;
@@ -405,6 +408,29 @@ declare const AiError_base: Schema.TaggedErrorClass<AiError, "AiError", {
405
408
  }>>;
406
409
  }>;
407
410
 
411
+ /**
412
+ * The one way a CAUGHT provider failure becomes an `AiError`.
413
+ *
414
+ * Every call site used to write `new AiError({ message: String(e.message) })`,
415
+ * which kept the top message and dropped `cause`. For a network failure that
416
+ * leaves `fetch failed` with nothing under it — no `ECONNREFUSED`, no TLS code —
417
+ * so the one fact that says what to fix was gone. Two sites were worse and
418
+ * dropped the provider's message too (`structured provider call failed`).
419
+ *
420
+ * Two things carry the cause now, deliberately split by audience:
421
+ * - the CODES of the cause chain (`ECONNREFUSED`, `CERT_HAS_EXPIRED`, …) join
422
+ * the message. Codes only: a nested message can carry an internal host or
423
+ * address, and `message` is a schema field — it is encoded, and it reaches a
424
+ * browser whenever a procedure declares `AiError` in its error union;
425
+ * - the raw failure stays on the instance as `cause`, NOT enumerable, so a
426
+ * server log or `error.cause` has the whole chain and the encoder never
427
+ * sees it.
428
+ */
429
+ export declare const aiGenerationError: (cause: unknown, options?: {
430
+ readonly prefix?: string;
431
+ readonly evidence?: AiCallEvidence;
432
+ }) => AiError;
433
+
408
434
  /**
409
435
  * One offloaded model call.
410
436
  *
@@ -469,6 +495,18 @@ export declare interface AiUsage {
469
495
  readonly totalTokens?: number | undefined;
470
496
  }
471
497
 
498
+ /** The usage ledger could not be written — the call it describes is not booked. */
499
+ export declare class AiUsageLedgerError extends AiUsageLedgerError_base<{
500
+ readonly receiptKey: string;
501
+ readonly cause: unknown;
502
+ }> {
503
+ get message(): string;
504
+ }
505
+
506
+ declare const AiUsageLedgerError_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
507
+ readonly _tag: "AiUsageLedgerError";
508
+ } & Readonly<A>;
509
+
472
510
  /**
473
511
  * Price a usage tally and write a ledger row. Returns the computed
474
512
  * `CostBreakdown` so the caller can surface the cost without a re-query.
@@ -476,6 +514,40 @@ export declare interface AiUsage {
476
514
  /** What `recordAiUsage` did with the receipt: wrote it, or found it already written. */
477
515
  export declare type AiUsageReceipt = 'recorded' | 'replayed';
478
516
 
517
+ /**
518
+ * A receipt was recorded again with DIFFERENT values than the row already
519
+ * written under it.
520
+ *
521
+ * A replay is the same paid call recorded twice, so its values are the same by
522
+ * definition. Different values under one receipt mean one of them is wrong — a
523
+ * second call billed under the first one's identity, or a caller whose receipt
524
+ * key is not the identity it thinks it is. Before this existed the second
525
+ * recording answered `replayed` and handed back the values it was GIVEN, so the
526
+ * caller saw a charge that was never booked and no sign of the disagreement.
527
+ */
528
+ export declare class AiUsageReceiptConflict extends AiUsageReceiptConflict_base<{
529
+ readonly receiptKey: string;
530
+ readonly recorded: AiUsageReceiptFacts;
531
+ readonly attempted: AiUsageReceiptFacts;
532
+ }> {
533
+ get message(): string;
534
+ }
535
+
536
+ declare const AiUsageReceiptConflict_base: new <A extends Record<string, any> = {}>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
537
+ readonly _tag: "AiUsageReceiptConflict";
538
+ } & Readonly<A>;
539
+
540
+ /** The facts of one usage row that a replay must agree with. */
541
+ export declare interface AiUsageReceiptFacts {
542
+ readonly tenantId: string | null;
543
+ readonly provider: string;
544
+ readonly model: string;
545
+ readonly operation: string;
546
+ readonly inputTokens: number;
547
+ readonly outputTokens: number;
548
+ readonly costMicroUsd: number;
549
+ }
550
+
479
551
  export declare interface AiUsageRecordInput {
480
552
  readonly tenantId?: string | null;
481
553
  readonly provider: ProviderName | string;
@@ -690,6 +762,9 @@ export declare const buildCopilotSystemPrompt: (schema: CopilotSchema) => string
690
762
  * `execute` tools ignore it. */
691
763
  export declare const buildSdkTools: (tools: Record<string, AnyTool>, ctx?: ToolContext) => ToolSet;
692
764
 
765
+ /** The names the built-in generators resolve without a registration. */
766
+ export declare const BUILTIN_MEDIA_PROVIDERS: ReadonlyArray<string>;
767
+
693
768
  /** The names `generateSpeech` resolves without a registration. */
694
769
  export declare const BUILTIN_SPEECH_PROVIDERS: ReadonlyArray<string>;
695
770
 
@@ -725,6 +800,9 @@ export declare const capabilitiesOf: (raw: RawGatewayModel) => ModelCapabilities
725
800
  /** Clear the global middleware stack. */
726
801
  export declare const clearAiMiddleware: () => void;
727
802
 
803
+ /** Test seam: forget every registration. */
804
+ export declare const clearMediaTaskProviders: () => void;
805
+
728
806
  /** Test seam: forget every registration. */
729
807
  export declare const clearSpeechProviders: () => void;
730
808
 
@@ -1865,6 +1943,10 @@ export declare interface InferenceDispatchOptions {
1865
1943
  readonly maxAttempts?: number;
1866
1944
  readonly baseDelayMs?: number;
1867
1945
  readonly maxDelayMs?: number;
1946
+ /** Between polls of a task-provider media job. Default {@link DEFAULT_MEDIA_TASK_POLL_MS}. */
1947
+ readonly taskPollMs?: number;
1948
+ /** A task still pending this long after submission is failed. Default {@link DEFAULT_MEDIA_TASK_TIMEOUT_MS}. */
1949
+ readonly taskTimeoutMs?: number;
1868
1950
  }
1869
1951
 
1870
1952
  export declare interface InferenceDispatchResult {
@@ -1876,6 +1958,9 @@ export declare interface InferenceDispatchResult {
1876
1958
  readonly failed: number;
1877
1959
  readonly retried: number;
1878
1960
  readonly reclaimed: number;
1961
+ /** Task-provider media jobs asked once and found still pending — released
1962
+ * back to the queue for a later claim, without a lease held across the job. */
1963
+ readonly polled: number;
1879
1964
  readonly failures: ReadonlyArray<{
1880
1965
  readonly id: string;
1881
1966
  readonly detail: string;
@@ -2245,6 +2330,105 @@ export declare interface McpTransport {
2245
2330
  readonly close: () => Effect.Effect<void>;
2246
2331
  }
2247
2332
 
2333
+ /** One artifact a finished task produced — a URL the dispatcher re-hosts, or
2334
+ * bytes (raw or base64) it stores. Same shape the built-in generators hand
2335
+ * over, so both paths persist identically. */
2336
+ export declare interface MediaTaskArtifact {
2337
+ readonly url?: string;
2338
+ readonly data?: Uint8Array | string;
2339
+ readonly mediaType: string;
2340
+ }
2341
+
2342
+ /**
2343
+ * The provider's handle for one submitted task.
2344
+ *
2345
+ * Stored on the queue row VERBATIM as JSON and handed back to `poll`, so it
2346
+ * may carry whatever the provider needs to find the task again — a request
2347
+ * id, the endpoint it was submitted to, a region. It must survive
2348
+ * `JSON.stringify`: a handle that does not is refused before it is stored,
2349
+ * because a handle that came back different would poll a task that does not
2350
+ * exist.
2351
+ */
2352
+ export declare type MediaTaskHandle = Readonly<Record<string, unknown>>;
2353
+
2354
+ export declare interface MediaTaskProvider {
2355
+ /** Submit the job; return the provider's handle as soon as it has one. */
2356
+ readonly submit: (request: MediaTaskSubmitRequest) => Effect.Effect<MediaTaskHandle, AiError>;
2357
+ /** Ask the provider about one handle. Called once per dispatcher claim. */
2358
+ readonly poll: (handle: MediaTaskHandle, request: MediaTaskSubmitRequest) => Effect.Effect<MediaTaskStatus, AiError>;
2359
+ /**
2360
+ * The provider de-duplicates on `idempotencyKey`: a second `submit` with the
2361
+ * same key returns the SAME task's handle rather than starting a second job.
2362
+ *
2363
+ * This closes the one window the handle cannot: a worker lost between the
2364
+ * provider accepting the job and the handle reaching the row. Without this
2365
+ * flag such a row is resumed with `AiOffloadError.outcome === 'unknown'` —
2366
+ * the job may have run and been charged for, and nothing can find it. With
2367
+ * it, the reclaiming worker submits again under the same key, receives the
2368
+ * same handle, and polls it.
2369
+ *
2370
+ * Say it only when the provider guarantees it. A `true` over a provider that
2371
+ * ignores the key turns the lost-worker case into a second paid job — the
2372
+ * exact outcome the `unknown` report exists to avoid.
2373
+ */
2374
+ readonly idempotentSubmit?: boolean;
2375
+ }
2376
+
2377
+ /** The task provider registered under `name`, or `undefined`. */
2378
+ export declare const mediaTaskProvider: (name: string) => MediaTaskProvider | undefined;
2379
+
2380
+ export declare type MediaTaskStatus = {
2381
+ readonly status: 'pending';
2382
+ /** How long to wait before asking again, when the provider says. Bounded
2383
+ * below by the dispatcher's own poll interval. */
2384
+ readonly retryAfterMs?: number;
2385
+ } | {
2386
+ readonly status: 'succeeded';
2387
+ readonly artifacts: ReadonlyArray<MediaTaskArtifact>;
2388
+ /** What the task consumed, in the provider's unit — seconds of video,
2389
+ * characters of text, or tokens. Recorded in the usage ledger under
2390
+ * the job's row and attempt. */
2391
+ readonly usage?: MediaUsage | MeteredUsage;
2392
+ readonly providerMetadata?: unknown;
2393
+ } | {
2394
+ readonly status: 'failed';
2395
+ /** The provider's own message. It is what the parked run is told. */
2396
+ readonly message: string;
2397
+ /**
2398
+ * Whether a fresh submission could succeed. Default `false`: a task the
2399
+ * provider ran and failed is a paid job, and re-submitting it is a second
2400
+ * charge, not a retry. Say `true` for a transient failure the provider
2401
+ * itself names as such (capacity, an upstream outage) — then the job is
2402
+ * re-submitted under the next attempt, with the dispatcher's backoff.
2403
+ */
2404
+ readonly retryable?: boolean;
2405
+ };
2406
+
2407
+ /**
2408
+ * What the dispatcher hands `submit`.
2409
+ *
2410
+ * `idempotencyKey` is derived from the queue row and the attempt — the same
2411
+ * two things the artifacts are stored under — so it is stable across a lost
2412
+ * worker and different for a deliberate re-submission. Forward it to the
2413
+ * provider when the provider takes one: see `MediaTaskProvider.idempotentSubmit`
2414
+ * for what that buys.
2415
+ */
2416
+ export declare interface MediaTaskSubmitRequest {
2417
+ readonly modality: 'image' | 'video' | 'speech';
2418
+ readonly prompt: string;
2419
+ readonly model?: string;
2420
+ /** The job's `params`, as declared on `mediaStep`. */
2421
+ readonly params: Readonly<Record<string, unknown>>;
2422
+ /** URL inputs, as declared on `mediaStep`. Never bytes. */
2423
+ readonly inputs: Readonly<Record<string, unknown>>;
2424
+ readonly providerOptions?: AiProviderOptions;
2425
+ readonly tenantId: string | null;
2426
+ /** `<queue row id>#<attempt>`. */
2427
+ readonly idempotencyKey: string;
2428
+ /** Aborted on Effect interruption; pass to provider I/O. */
2429
+ readonly abortSignal?: AbortSignal;
2430
+ }
2431
+
2248
2432
  /** Token tally, mirroring `GenerateTextResult['usage']`. Only `generateImage`
2249
2433
  * reports one (the speech + video model specs carry no usage), which is why
2250
2434
  * it's optional on every media result. */
@@ -2739,6 +2923,9 @@ export declare interface QueuedInference {
2739
2923
  readonly transcription: Record<string, unknown> | null;
2740
2924
  /** The media job for a `media` call. */
2741
2925
  readonly media: QueuedMediaJob | null;
2926
+ /** The stored provider handle of a task-provider media job, or `null`. */
2927
+ readonly taskHandle: Record<string, unknown> | null;
2928
+ readonly taskSubmittedAt: Date | null;
2742
2929
  readonly attempts: number;
2743
2930
  readonly fence: number;
2744
2931
  }
@@ -2748,6 +2935,8 @@ export declare interface QueuedMediaJob {
2748
2935
  readonly modality: 'image' | 'video' | 'speech';
2749
2936
  readonly params: Record<string, unknown>;
2750
2937
  readonly inputs: Record<string, unknown>;
2938
+ /** Owner, visibility and tags for the stored artifacts — see `PluginMediaPersistMetadata`. */
2939
+ readonly persist?: PluginMediaPersistMetadata;
2751
2940
  }
2752
2941
 
2753
2942
  /** What a media job resumes its run with: hosted artifacts, never bytes. */
@@ -2818,7 +3007,20 @@ export declare interface ReadRecorder<S extends QueryableStore> {
2818
3007
 
2819
3008
  export declare const recordAiUsage: (store: DataStore, record: AiUsageRecordInput) => Effect.Effect<CostBreakdown & {
2820
3009
  readonly receipt: AiUsageReceipt;
2821
- }>;
3010
+ }, AiUsageReceiptConflict | AiUsageLedgerError>;
3011
+
3012
+ /**
3013
+ * `recordAiUsage` for a call that has ALREADY been paid for, where failing the
3014
+ * caller would lose a finished answer over a ledger row. Never fails — and never
3015
+ * silent: a ledger failure or a receipt conflict is logged at error level with
3016
+ * everything needed to reconcile it by hand.
3017
+ *
3018
+ * The framework's three callers each swallowed the failure outright
3019
+ * (`.catch(() => {})`, `catchAllCause(() => Effect.void)` twice). "Do not fail
3020
+ * the call" was right; "leave no trace" was not — an unbooked charge that
3021
+ * nothing reports is found only when the invoice disagrees with the ledger.
3022
+ */
3023
+ export declare const recordAiUsageBestEffort: (store: DataStore, record: AiUsageRecordInput) => Effect.Effect<void>;
2822
3024
 
2823
3025
  /**
2824
3026
  * Register a prompt version, idempotently, and return it.
@@ -2876,6 +3078,18 @@ export declare const redisResumableStreamStore: (redis: ResumableRedis, opts?: {
2876
3078
  */
2877
3079
  export declare const refreshMcpToolset: (server: McpServer, policy: McpToolPolicy) => Effect.Effect<McpToolset, McpError>;
2878
3080
 
3081
+ /**
3082
+ * The provider config `mediaStep` accepts for a registered task provider —
3083
+ * named, not enumerated, exactly like `RegisteredSpeechConfig`.
3084
+ */
3085
+ export declare interface RegisteredMediaTaskConfig {
3086
+ readonly name: string;
3087
+ readonly model?: string;
3088
+ }
3089
+
3090
+ /** Every registered name. */
3091
+ export declare const registeredMediaTaskProviders: () => ReadonlyArray<string>;
3092
+
2879
3093
  /**
2880
3094
  * Synthesize speech audio from text. Returns the audio bytes + media type and
2881
3095
  * provider metadata; provider failures surface as `AiError`.
@@ -2898,6 +3112,29 @@ export declare interface RegisteredSpeechConfig {
2898
3112
  /** Every registered name — what a picker asks for. */
2899
3113
  export declare const registeredSpeechProviders: () => ReadonlyArray<string>;
2900
3114
 
3115
+ /**
3116
+ * Register an external-task media provider under a name.
3117
+ *
3118
+ * The name then works in `mediaStep({ provider: { name, model } })`: the
3119
+ * dispatcher submits through the provider, keeps the handle on the queue row,
3120
+ * polls it once per claim, and resumes the run with the hosted artifacts.
3121
+ * Taking over a built-in name needs `{ replaceBuiltin: true }`.
3122
+ */
3123
+ export declare const registerMediaTaskProvider: (name: string, provider: MediaTaskProvider, options?: RegisterMediaTaskProviderOptions) => void;
3124
+
3125
+ export declare interface RegisterMediaTaskProviderOptions {
3126
+ /**
3127
+ * Take over a BUILT-IN name (`openai`, `gateway`, `anthropic`, `mock`).
3128
+ *
3129
+ * A registration under a built-in name routes every `mediaStep` that names
3130
+ * it through the task contract instead of the generator. Refused unless the
3131
+ * intent is in the call, for the reason the speech registry gives: a silent
3132
+ * shadow makes the same config mean different things depending on which
3133
+ * module loaded first.
3134
+ */
3135
+ readonly replaceBuiltin?: boolean;
3136
+ }
3137
+
2901
3138
  /**
2902
3139
  * Register a speech provider under a name.
2903
3140
  *