@voltro/ai 0.28.0 → 0.29.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 { QueryDescriptor } from '@voltro/database';
16
16
  import { QueryProcedureDescriptor } from '@voltro/protocol';
17
17
  import { Runtime } from 'effect';
18
18
  import { Schema } from 'effect';
19
+ import { SemanticCacheShape } from '@voltro/cache';
19
20
  import { simulateStreamingMiddleware } from 'ai';
20
21
  import { SpeechModelV4 } from '@ai-sdk/provider';
21
22
  import { Stream } from 'effect';
@@ -468,6 +469,11 @@ export declare interface AppToolPolicy {
468
469
  */
469
470
  export declare const appTools: (entries: ReadonlyArray<AppToolEntry>, policy?: AppToolPolicy) => ReadonlyArray<AnyTool>;
470
471
 
472
+ export declare interface AssertionFailure {
473
+ readonly assertion: EvalAssertion;
474
+ readonly detail: string;
475
+ }
476
+
471
477
  /** Render the capability map into a system prompt that enumerates EXACTLY the
472
478
  * tables + columns the model may use — the soft in-grammar steer that pairs
473
479
  * with the hard validator. */
@@ -651,6 +657,9 @@ declare const DEFAULT_INPUT: Schema.Struct<{
651
657
  prompt: typeof Schema.String;
652
658
  }>;
653
659
 
660
+ /** The default judge threshold when a judge is configured without one. */
661
+ export declare const DEFAULT_JUDGE_THRESHOLD = 0.7;
662
+
654
663
  /** Default input schema type when `input` is omitted (`{ prompt: string }`). */
655
664
  export declare type DefaultAgentInputSchema = typeof DEFAULT_INPUT;
656
665
 
@@ -701,6 +710,25 @@ export declare const defineAgent: <S extends Schema.Schema.Any = DefaultAgentInp
701
710
  */
702
711
  export declare const defineAgentExecutor: <S extends Schema.Schema.Any = DefaultAgentInputSchema>(descriptor: AgentDescriptor<S>, config: AgentExecutorConfig<AgentInput<S>>) => AgentExecutor<S>;
703
712
 
713
+ /**
714
+ * Declare an eval suite. Validated at module load — an empty case list, a
715
+ * duplicate case id, or a nonsensical threshold throws loudly so a broken eval
716
+ * fails discovery, never at replay time. Call it as the default export of a
717
+ * `*.eval.ts` file; `voltro eval` discovers + runs it.
718
+ */
719
+ export declare const defineEval: (input: DefineEvalInput) => EvalDefinition;
720
+
721
+ export declare interface DefineEvalInput {
722
+ readonly name: string;
723
+ readonly cases: ReadonlyArray<EvalCase>;
724
+ /** Assertions applied to EVERY case. */
725
+ readonly assert?: ReadonlyArray<EvalAssertion>;
726
+ readonly judge?: EvalJudgeConfig;
727
+ /** Tool names the replay may call (resolved by the runner against the app's
728
+ * registered `*.tool.tsx`). Empty = a bare generate. */
729
+ readonly tools?: ReadonlyArray<string>;
730
+ }
731
+
704
732
  export declare const defineTool: <A, I, O>(def: {
705
733
  readonly name: string;
706
734
  readonly description: string;
@@ -760,6 +788,113 @@ export declare const estimateCostUsd: (usage: AiUsage, opts: {
760
788
  readonly actualCostUsd?: number;
761
789
  }) => CostBreakdown;
762
790
 
791
+ /**
792
+ * A HARD assertion over the replayed output — a pure predicate, no model in the
793
+ * loop. These are the deterministic half of the gate: an LLM judge decides "is
794
+ * this answer good", an assertion decides "does it literally contain the order id
795
+ * / never leak the system prompt / come back non-empty within the latency bound".
796
+ */
797
+ export declare type EvalAssertion = {
798
+ readonly kind: 'contains';
799
+ readonly value: string;
800
+ readonly caseInsensitive?: boolean;
801
+ } | {
802
+ readonly kind: 'notContains';
803
+ readonly value: string;
804
+ readonly caseInsensitive?: boolean;
805
+ } | {
806
+ readonly kind: 'matches';
807
+ readonly pattern: string;
808
+ readonly flags?: string;
809
+ } | {
810
+ readonly kind: 'equals';
811
+ readonly value: string;
812
+ readonly trim?: boolean;
813
+ } | {
814
+ readonly kind: 'maxLatencyMs';
815
+ readonly value: number;
816
+ } | {
817
+ readonly kind: 'nonEmpty';
818
+ };
819
+
820
+ export declare interface EvalCase {
821
+ /** Stable id — names the case in the report + the branch. */
822
+ readonly id: string;
823
+ readonly input: EvalInput;
824
+ /** The output the ORIGINAL recorded run produced — the baseline the judge
825
+ * compares the replay against. Optional: a case can gate on assertions alone. */
826
+ readonly golden?: string;
827
+ /** Assertions for THIS case (run in addition to the eval-wide `assert`). */
828
+ readonly assert?: ReadonlyArray<EvalAssertion>;
829
+ }
830
+
831
+ export declare interface EvalCaseResult {
832
+ readonly id: string;
833
+ readonly pass: boolean;
834
+ readonly assertionFailures: ReadonlyArray<AssertionFailure>;
835
+ /** Present when the eval declared a judge. */
836
+ readonly judge?: JudgeVerdict;
837
+ readonly output: EvalReplayOutput;
838
+ }
839
+
840
+ export declare interface EvalDefinition extends DefineEvalInput {
841
+ readonly __eval: true;
842
+ }
843
+
844
+ /** The captured inputs of a recorded run — what the replay re-sends. `messages`
845
+ * carries the full prior conversation for a multi-turn recorded run; `prompt` is
846
+ * the single user turn. */
847
+ export declare interface EvalInput {
848
+ readonly prompt: string;
849
+ readonly system?: string;
850
+ readonly messages?: ReadonlyArray<ModelMessage>;
851
+ }
852
+
853
+ /** Grade one replay. Injected so the pure runner never needs a provider. */
854
+ export declare type EvalJudge = (args: {
855
+ readonly case: EvalCase;
856
+ readonly output: EvalReplayOutput;
857
+ readonly config: EvalJudgeConfig;
858
+ }) => Promise<JudgeVerdict>;
859
+
860
+ /** LLM-judge configuration. The judge scores the replay against the rubric (and,
861
+ * when present, the golden baseline); a score below `threshold` fails the case. */
862
+ export declare interface EvalJudgeConfig {
863
+ /** What "good" means for these cases — the grading rubric handed to the judge. */
864
+ readonly rubric: string;
865
+ /** Minimum score (0..1) to pass. Default 0.7. */
866
+ readonly threshold?: number;
867
+ }
868
+
869
+ /** Re-run a single case. Injected by the caller — the CLI wires the app's
870
+ * provider (against a branched store); a test passes a deterministic fake. */
871
+ export declare type EvalReplay = (args: {
872
+ readonly case: EvalCase;
873
+ readonly def: EvalDefinition;
874
+ }) => Promise<EvalReplayOutput>;
875
+
876
+ /** What one replay produced. `latencyMs` feeds the `maxLatencyMs` assertion. */
877
+ export declare interface EvalReplayOutput {
878
+ readonly text: string;
879
+ readonly latencyMs: number;
880
+ }
881
+
882
+ export declare interface EvalReport {
883
+ readonly name: string;
884
+ readonly ok: boolean;
885
+ readonly passed: number;
886
+ readonly failed: number;
887
+ readonly cases: ReadonlyArray<EvalCaseResult>;
888
+ /** The isolated data branch this suite's replays target — reused branching
889
+ * machinery (`branchNamespaceName`). Set by the CLI when `--branch` is used. */
890
+ readonly branch?: {
891
+ readonly id: string;
892
+ };
893
+ }
894
+
895
+ /** Run every assertion against one output; return only the failures. Pure. */
896
+ export declare const evaluateAssertions: (out: EvalReplayOutput, assertions: ReadonlyArray<EvalAssertion>) => ReadonlyArray<AssertionFailure>;
897
+
763
898
  declare const EXECUTOR_BRAND: "@voltro/ai/AgentExecutor";
764
899
 
765
900
  /** The `exposeAsTool` annotation shape (mirrors @voltro/protocol's, structural
@@ -1116,12 +1251,30 @@ export declare const isAgentDescriptor: (x: unknown) => x is AnyAgentDescriptor;
1116
1251
  /** Discovery predicate for `*.agent.server.tsx` default exports. */
1117
1252
  export declare const isAgentExecutor: (x: unknown) => x is AnyAgentExecutor;
1118
1253
 
1254
+ export declare const isEvalDefinition: (value: unknown) => value is EvalDefinition;
1255
+
1119
1256
  /** Whether a stream with this id is currently registered (in flight). */
1120
1257
  export declare const isStreamActive: (streamId: string) => boolean;
1121
1258
 
1122
1259
  /** Is `x` a `defineTool(...)` result? */
1123
1260
  export declare const isTool: (x: unknown) => x is AnyTool;
1124
1261
 
1262
+ export declare interface JudgeVerdict {
1263
+ readonly pass: boolean;
1264
+ /** 0..1. Below `judge.threshold` fails the case even when `pass` is true. */
1265
+ readonly score: number;
1266
+ readonly reason: string;
1267
+ }
1268
+
1269
+ /**
1270
+ * The real LLM judge: asks the model for a structured `{ pass, score, reason }`
1271
+ * verdict via `generateObject` (schema-constrained, so a malformed grade surfaces
1272
+ * as a typed error rather than a parse crash). A judge failure is turned into a
1273
+ * failing verdict rather than throwing, so one flaky grade fails ONE case instead
1274
+ * of aborting the whole eval — the report stays a complete deploy-gate signal.
1275
+ */
1276
+ export declare const llmJudge: EvalJudge;
1277
+
1125
1278
  /** The synthesized `<name>.messages` body — load the thread's messages. */
1126
1279
  export declare const loadAgentMessages: (store: DataStore, threadId: string) => Effect.Effect<ReadonlyArray<AgentMessage>>;
1127
1280
 
@@ -1410,12 +1563,47 @@ export declare const providerFromEnv: (env?: Record<string, string | undefined>)
1410
1563
 
1411
1564
  export declare type ProviderName = 'mock' | 'anthropic' | 'openai' | 'gateway';
1412
1565
 
1566
+ /** The minimal store surface {@link recordReads} needs — any object with a
1567
+ * `query(descriptor)` that returns rows. Structurally a subset of the
1568
+ * framework `DataStore`, so no `@voltro/runtime` import is required. */
1569
+ export declare interface QueryableStore {
1570
+ query(descriptor: {
1571
+ readonly table: string;
1572
+ } & Record<string, unknown>): Promise<ReadonlyArray<Record<string, unknown>>>;
1573
+ }
1574
+
1575
+ /** A store wrapper that records the dependency set of everything read through
1576
+ * it — the provenance the semantic cache tags entries with. */
1577
+ export declare interface ReadRecorder<S extends QueryableStore> {
1578
+ /** Pass THIS where the real store would go; every `query()` it serves records
1579
+ * the table + each returned row's id. All other methods pass through. */
1580
+ readonly store: S;
1581
+ /** The accumulated dependency tags (deduped): `table:<t>` for each table
1582
+ * queried + `row:<t>:<id>` for each row returned that carries an `id`. Feed
1583
+ * this to a call's `deps`. */
1584
+ deps(): ReadonlyArray<string>;
1585
+ /** Drop everything recorded so far (reuse the recorder for a new answer). */
1586
+ reset(): void;
1587
+ }
1588
+
1413
1589
  /**
1414
1590
  * Price a usage tally and write a ledger row. Returns the computed
1415
1591
  * `CostBreakdown` so the caller can surface the cost without a re-query.
1416
1592
  */
1417
1593
  export declare const recordAiUsage: (store: DataStore, record: AiUsageRecordInput) => Effect.Effect<CostBreakdown>;
1418
1594
 
1595
+ /**
1596
+ * Wrap a store so the rows it serves become a dependency set — automatic
1597
+ * provenance capture with no runtime edit. Every `query()` records
1598
+ * `tableDep(table)` and, for each returned row with an `id`, `rowDep(table,
1599
+ * id)`. Read your data through `rec.store`, build the prompt from the rows,
1600
+ * then pass `rec.deps()` as the call's dependency set.
1601
+ *
1602
+ * The wrapper is a `Proxy`, so the returned `store` has the SAME type + full
1603
+ * method surface as the input — only `query` is intercepted.
1604
+ */
1605
+ export declare const recordReads: <S extends QueryableStore>(store: S) => ReadRecorder<S>;
1606
+
1419
1607
  /**
1420
1608
  * Redis-backed resumable-stream store — the production "resumable via
1421
1609
  * Redis-Replay" path. The append-only log is a Redis LIST per `streamId`
@@ -1690,6 +1878,102 @@ export declare interface RunAssistantInput {
1690
1878
  */
1691
1879
  export declare const runDataCopilot: (question: string, schema: CopilotSchema, deps: DataCopilotDeps) => Promise<CopilotValidation>;
1692
1880
 
1881
+ /**
1882
+ * Replay + score every case in an eval. The orchestrator is pure over its
1883
+ * injected `replay` / `judge`, so it runs identically under a real provider and
1884
+ * under a deterministic test fake. `ok` is the deploy-gate signal: false iff any
1885
+ * case failed.
1886
+ */
1887
+ export declare const runEval: (def: EvalDefinition, deps: RunEvalDeps) => Promise<EvalReport>;
1888
+
1889
+ export declare interface RunEvalDeps {
1890
+ readonly replay: EvalReplay;
1891
+ /** Omitted → assertions-only gating (no LLM in the loop). */
1892
+ readonly judge?: EvalJudge;
1893
+ readonly branchId?: string;
1894
+ }
1895
+
1896
+ /**
1897
+ * Combine assertion failures + the judge verdict into one pass/fail. Pure. A case
1898
+ * passes iff no assertion failed AND (no judge, or the judge's score clears the
1899
+ * threshold). Assertions are deterministic and always decisive; the judge only
1900
+ * ever ADDS a reason to fail, never rescues an assertion failure.
1901
+ */
1902
+ export declare const scoreCase: (c: EvalCase, out: EvalReplayOutput, evalWideAssertions: ReadonlyArray<EvalAssertion>, judgeConfig: EvalJudgeConfig | undefined, judgeVerdict: JudgeVerdict | undefined) => EvalCaseResult;
1903
+
1904
+ /** Injectable collaborators for a semantic-cached call. */
1905
+ export declare interface SemanticCacheDeps {
1906
+ /** The semantic cache (build with `makeSemanticCache(store)` from
1907
+ * `@voltro/cache` over a memory / RESP `CacheStore`). */
1908
+ readonly cache: SemanticCacheShape;
1909
+ /** Embed a prompt to a vector. Defaults to `@voltro/ai`'s `embed` (the
1910
+ * env-configured provider). Injectable for tests + provider choice. */
1911
+ readonly embed?: (text: string) => Effect.Effect<ReadonlyArray<number>, AiError>;
1912
+ }
1913
+
1914
+ /** Per-call cache policy. */
1915
+ export declare interface SemanticCachePolicy {
1916
+ /** Minimum cosine similarity ([-1, 1]) for a hit. Default 0.95 — high, so
1917
+ * only genuine near-duplicates share an answer. */
1918
+ readonly threshold?: number;
1919
+ /** TTL floor in ms — the answer expires after this even if nothing it
1920
+ * depends on changes. Omit for "live until a dependency changes". */
1921
+ readonly ttlMs?: number;
1922
+ /** The dependency set: the source rows/tables this answer read, as tags
1923
+ * (`rowDep` / `tableDep`, or {@link recordReads}). A change to any of them
1924
+ * evicts the entry. Empty ⇒ cached but TTL-only (never dependency-evicted)
1925
+ * — correct only for an answer that reads no data. */
1926
+ readonly deps: ReadonlyArray<string>;
1927
+ }
1928
+
1929
+ /**
1930
+ * `generateObject`, semantically cached + dependency-evicted. Same contract as
1931
+ * {@link semanticGenerateText} for the structured surface.
1932
+ *
1933
+ * Note on serialization: the DECODED object is stored. With the memory backend
1934
+ * it round-trips by reference; with a RESP backend it is JSON — so a schema
1935
+ * whose decoded form is not JSON-safe (class instances, non-plain branded
1936
+ * carriers) will not survive a cross-process hit. For those, cache the text
1937
+ * form or key on a JSON-safe projection.
1938
+ */
1939
+ export declare const semanticGenerateObject: <A, I>(options: GenerateObjectOptions<A, I>, policy: SemanticCachePolicy, deps: SemanticCacheDeps) => Effect.Effect<SemanticResult<A>, AiError>;
1940
+
1941
+ /**
1942
+ * `generateText`, semantically cached + dependency-evicted.
1943
+ *
1944
+ * On a near-duplicate prompt (embedding within `threshold`) returns the cached
1945
+ * text with `cached: true` and zero usage. On a miss it generates, records the
1946
+ * answer under its dependency set, and returns `cached: false`.
1947
+ *
1948
+ * ```ts
1949
+ * const rec = recordReads(ctx.store)
1950
+ * const docs = yield* Effect.promise(() => rec.store.query(docsQuery))
1951
+ * const res = yield* semanticGenerateText(
1952
+ * { prompt: buildPrompt(docs) },
1953
+ * { deps: rec.deps() },
1954
+ * { cache },
1955
+ * )
1956
+ * // res.cached === true on the next near-identical prompt — until a doc changes.
1957
+ * ```
1958
+ */
1959
+ export declare const semanticGenerateText: (options: GenerateTextOptions, policy: SemanticCachePolicy, deps: SemanticCacheDeps) => Effect.Effect<SemanticResult<string>, AiError>;
1960
+
1961
+ /** The outcome of a semantic-cached generation. */
1962
+ export declare interface SemanticResult<A> {
1963
+ readonly value: A;
1964
+ /** `true` when served from cache (zero tokens spent). */
1965
+ readonly cached: boolean;
1966
+ /** Underlying generation usage — all-zero when `cached`. */
1967
+ readonly usage: SemanticUsage;
1968
+ }
1969
+
1970
+ /** Usage tally shape shared by both generate results; all-zero on a cache hit. */
1971
+ export declare interface SemanticUsage {
1972
+ readonly inputTokens: number | undefined;
1973
+ readonly outputTokens: number | undefined;
1974
+ readonly totalTokens: number | undefined;
1975
+ }
1976
+
1693
1977
  /**
1694
1978
  * One persisted event of a RESUMABLE stream: an `AgentEvent` plus its monotonic
1695
1979
  * 1-based position. `seq` is the resume cursor — a client that has rendered up