@tangle-network/agent-eval 0.84.0 → 0.86.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/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval-
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.86.0] — 2026-06-09 — fleet-rebuilt eval primitives
8
+
9
+ One clean, canonical version of five generic patterns the fleet kept hand-rolling across 2–4 product agents each. All additive — no existing export changed.
10
+
11
+ ### Added
12
+
13
+ - **`ExperimentTracker` + `improvementVerdict` + `computeExperimentStats` (root).** Git-provenanced experiment log with N-rep stats (`median` / `mean` / `min` / `max` / `iqr` / `stddev` / `passRate` / `n` + a `stable` flag) and a `KEEP` / `REGRESSION` / `NOISE` / `ITERATE` verdict against a parent. Provenance (`ProvenanceReader`, default `gitProvenanceReader`) and persistence (`ExperimentStore`, with `inMemoryExperimentStore` + `fileExperimentStore`) are injected, so the stats + verdict are pure and unit-testable without a repo or disk. Thresholds (`keepThreshold` / `regressionThreshold` / `iqrUnstableAbove` / `stddevUnstableAbove` / `minRepsForVerdict`) are configurable. Replaces the per-agent `tests/eval/lib/experiment-tracker.ts` copies (tax / insurance / legal).
14
+ - **`EvalTraceStore` + `runScore` (root).** JSONL save / query / compare over the analysis-time `RunRecord` row: `query(filter)`, `getBest(scenarioId)`, and `compareRuns(a, b)` (paired on matched scenarios, best-score-per-scenario). Persistence is injected via `RunRecordBackend` (`inMemoryRunRecordBackend` / `jsonlRunRecordBackend`, which fail loud on a malformed line). Does NOT fork `FileSystemTraceStore` (the rich TraceSchema-v1 span store) — it is the analysis projection beside it. Replaces the hand-rolled `tests/eval/lib/trace-store.ts` copies.
15
+ - **`CostLedger` + `costForUsage` + `modelPriceKey` (root).** Per-run token + USD accounting folded over the substrate's `resolveModelPricing` / `isModelPriced`, with an explicit `costUnknown` axis so a $0 from an unpriced model is never mistaken for a measured free run. Classifies spend by channel (`agent` / `judge` / `verifier` / …), surfaces `unpricedModels` + `fullyPriced`, and computes `costPerCompletedTask`. Generalizes physim's `costForUsage` / `modelPriceKey` and the tax / gtm / agent-builder copies.
16
+ - **`extractUsage` + `extractUsageFromSse` + `extractUsageFromResponse` (`/traces` + root).** Token-usage extraction from a chat-completions response or an SSE stream — OpenAI / Anthropic / camelCase shapes, cache-read tokens, and per-chunk SSE accumulation — returning `null` (not a silent zero) when no usage is present. `captureFetchToRawSink` gains an optional `onUsage` callback that emits the parsed usage off each response (reusing the body it already reads — no extra clone), so a caller folds usage → cost without re-cloning. Replaces the insurance / legal / gtm `raw-capture.ts` copies.
17
+ - **`partitionHeldOut` + `assignHeldOutTag` + `hashToUnit` + `fnv1a32` (root).** Deterministic FNV-1a id+seed held-out splitter. `assignHeldOutTag` stamps a single id; `partitionHeldOut` splits a whole id list into disjoint search / holdout sets and fails loud on duplicate ids, empty input, an under-floor holdout (`minHoldout` / `minSearch` significance floor), or an out-of-range `holdoutFraction`. Generalizes agent-builder's `deterministicSplit` and the frontier persona-splitter; complements the existing 3-way benchmark `deterministicSplit` in `/benchmarks`.
18
+
19
+ ---
20
+
7
21
  ## [0.83.0] — 2026-06-05 — hostedTenantFromEnv
8
22
 
9
23
  ### Added
@@ -577,6 +577,7 @@ type RuntimeBenchmarkTrajectoryRecord = RuntimeTrajectoryRecord & {
577
577
  benchmark?: unknown;
578
578
  condition?: unknown;
579
579
  instanceId?: unknown;
580
+ runtimeDecisionPoints?: unknown;
580
581
  };
581
582
  interface BuildRuntimeBenchmarkBeliefPhase0MeasurementOptions extends Omit<BuildRuntimeBeliefPhase0MeasurementOptions, 'runs' | 'events' | 'decisions' | 'labels'> {
582
583
  records: RuntimeBenchmarkTrajectoryRecord[];
@@ -1720,6 +1720,20 @@ function compactMetadata2(values) {
1720
1720
  }
1721
1721
 
1722
1722
  // src/belief-state/runtime-benchmark-corpus.ts
1723
+ var MAX_STRING_LENGTH = 12e3;
1724
+ var MAX_CONTEXT_LENGTH = 2e4;
1725
+ var MAX_EVIDENCE_DETAIL_LENGTH = 2e3;
1726
+ var MAX_CANDIDATE_ACTIONS = 50;
1727
+ var MAX_EVIDENCE_REFS = 50;
1728
+ var MAX_METADATA_DEPTH = 4;
1729
+ var MAX_METADATA_KEYS = 100;
1730
+ var SENSITIVE_KEY_RE = /(?:authorization|api[_-]?key|token|secret|password|cookie|credential|bearer)/i;
1731
+ var SENSITIVE_VALUE_RES = [
1732
+ /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi,
1733
+ /\b(?:sk|gh[pousr])_[A-Za-z0-9_]{20,}\b/g,
1734
+ /\b(?:sk|ghp|gho|ghu|ghs|ghr)-[A-Za-z0-9_-]{20,}\b/g
1735
+ ];
1736
+ var SENSITIVE_ASSIGNMENT_RE = /\b(api[_-]?key|token|secret|password|cookie)\s*[:=]\s*["']?[^"'\s,;}]+/gi;
1723
1737
  function buildRuntimeBenchmarkBeliefPhase0Measurement(options) {
1724
1738
  const diagnostics = [];
1725
1739
  const trajectory = projectRuntimeTrajectoryEvidence({
@@ -1728,11 +1742,11 @@ function buildRuntimeBenchmarkBeliefPhase0Measurement(options) {
1728
1742
  recordIdOf: runtimeBenchmarkRecordId,
1729
1743
  scenarioIdOf: runtimeBenchmarkScenarioId
1730
1744
  });
1731
- const decisions = options.decisions ?? [];
1745
+ const decisions = options.decisions ?? runtimeBenchmarkDecisionPoints(options.records, diagnostics);
1732
1746
  const labels = options.labels ?? [];
1733
1747
  if (decisions.length === 0) {
1734
1748
  diagnostics.push(
1735
- "no runtime decision points supplied; benchmark lifecycle events alone cannot produce belief decision rows"
1749
+ "no runtime decision points supplied or found on records; benchmark lifecycle events alone cannot produce belief decision rows"
1736
1750
  );
1737
1751
  }
1738
1752
  if (labels.length === 0 && decisions.length > 0) {
@@ -1772,6 +1786,125 @@ function runtimeBenchmarkRecordId(record2) {
1772
1786
  function runtimeBenchmarkScenarioId(record2) {
1773
1787
  return nonEmptyString(record2.instanceId);
1774
1788
  }
1789
+ function runtimeBenchmarkDecisionPoints(records, diagnostics) {
1790
+ const decisions = [];
1791
+ for (let recordIndex = 0; recordIndex < records.length; recordIndex += 1) {
1792
+ const record2 = records[recordIndex];
1793
+ const raw = record2.runtimeDecisionPoints;
1794
+ if (raw === void 0) continue;
1795
+ const recordId = runtimeBenchmarkRecordId(record2) ?? `record[${recordIndex}]`;
1796
+ if (!Array.isArray(raw)) {
1797
+ diagnostics.push(`${recordId}: runtimeDecisionPoints is not an array`);
1798
+ continue;
1799
+ }
1800
+ for (let pointIndex = 0; pointIndex < raw.length; pointIndex += 1) {
1801
+ const point = runtimeBenchmarkDecisionPoint(raw[pointIndex], {
1802
+ diagnostics,
1803
+ path: `${recordId}: runtimeDecisionPoints[${pointIndex}]`
1804
+ });
1805
+ if (!point) {
1806
+ diagnostics.push(
1807
+ `${recordId}: runtimeDecisionPoints[${pointIndex}] is not a RuntimeDecisionPoint`
1808
+ );
1809
+ continue;
1810
+ }
1811
+ decisions.push(point);
1812
+ }
1813
+ }
1814
+ return decisions;
1815
+ }
1816
+ function runtimeBenchmarkDecisionPoint(input, context) {
1817
+ if (!isRecord3(input)) return null;
1818
+ if (typeof input.id !== "string" || input.id.length === 0) return null;
1819
+ if (typeof input.runId !== "string" || input.runId.length === 0) return null;
1820
+ if (typeof input.stepIndex !== "number" || !Number.isInteger(input.stepIndex) || input.stepIndex < 0) {
1821
+ return null;
1822
+ }
1823
+ if (typeof input.kind !== "string" || input.kind.length === 0) return null;
1824
+ return {
1825
+ id: sanitizeString(input.id, MAX_STRING_LENGTH),
1826
+ runId: sanitizeString(input.runId, MAX_STRING_LENGTH),
1827
+ scenarioId: sanitizeOptionalString(input.scenarioId, MAX_STRING_LENGTH),
1828
+ stepIndex: input.stepIndex,
1829
+ kind: sanitizeString(input.kind, MAX_STRING_LENGTH),
1830
+ candidateActions: stringArray(input.candidateActions, {
1831
+ ...context,
1832
+ maxItems: MAX_CANDIDATE_ACTIONS,
1833
+ label: "candidateActions"
1834
+ }),
1835
+ context: sanitizeOptionalString(input.context, MAX_CONTEXT_LENGTH),
1836
+ evidence: runtimeBenchmarkEvidence(input.evidence, context),
1837
+ metadata: sanitizeMetadataRecord(input.metadata)
1838
+ };
1839
+ }
1840
+ function runtimeBenchmarkEvidence(input, context) {
1841
+ if (!Array.isArray(input)) return [];
1842
+ if (input.length > MAX_EVIDENCE_REFS) {
1843
+ context.diagnostics.push(`${context.path}: evidence truncated to ${MAX_EVIDENCE_REFS} refs`);
1844
+ }
1845
+ return input.slice(0, MAX_EVIDENCE_REFS).flatMap((item) => {
1846
+ if (!isRecord3(item)) return [];
1847
+ const source = sanitizeOptionalString(item.source, MAX_STRING_LENGTH);
1848
+ const id = sanitizeOptionalString(item.id, MAX_STRING_LENGTH);
1849
+ if (!source || !id) return [];
1850
+ return [
1851
+ {
1852
+ source,
1853
+ id,
1854
+ detail: sanitizeOptionalString(item.detail, MAX_EVIDENCE_DETAIL_LENGTH),
1855
+ metadata: sanitizeMetadataRecord(item.metadata)
1856
+ }
1857
+ ];
1858
+ });
1859
+ }
1860
+ function stringArray(input, context) {
1861
+ if (!Array.isArray(input)) return void 0;
1862
+ if (input.length > context.maxItems) {
1863
+ context.diagnostics.push(`${context.path}: ${context.label} truncated to ${context.maxItems}`);
1864
+ }
1865
+ const values = input.slice(0, context.maxItems).filter((value) => typeof value === "string" && value.length > 0).map((value) => sanitizeString(value, MAX_STRING_LENGTH));
1866
+ return values.length > 0 ? values : void 0;
1867
+ }
1868
+ function sanitizeMetadataRecord(metadata) {
1869
+ if (!isRecord3(metadata)) return void 0;
1870
+ const sanitized = sanitizeMetadata(metadata);
1871
+ if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) return void 0;
1872
+ return sanitized;
1873
+ }
1874
+ function sanitizeMetadata(value, depth = 0) {
1875
+ if (value == null) return value;
1876
+ if (typeof value === "string") return sanitizeString(value, MAX_STRING_LENGTH);
1877
+ if (typeof value === "number" || typeof value === "boolean") return value;
1878
+ if (Array.isArray(value)) {
1879
+ if (depth >= MAX_METADATA_DEPTH) return "[MaxDepth]";
1880
+ return value.slice(0, MAX_METADATA_KEYS).map((item) => sanitizeMetadata(item, depth + 1));
1881
+ }
1882
+ if (!isRecord3(value)) return void 0;
1883
+ if (depth >= MAX_METADATA_DEPTH) return "[MaxDepth]";
1884
+ const sanitized = {};
1885
+ for (const [key, nested] of Object.entries(value).slice(0, MAX_METADATA_KEYS)) {
1886
+ sanitized[key] = SENSITIVE_KEY_RE.test(key) ? "[REDACTED]" : sanitizeMetadata(nested, depth + 1);
1887
+ }
1888
+ return sanitized;
1889
+ }
1890
+ function sanitizeOptionalString(value, maxLength) {
1891
+ return typeof value === "string" && value.length > 0 ? sanitizeString(value, maxLength) : void 0;
1892
+ }
1893
+ function sanitizeString(value, maxLength) {
1894
+ let sanitized = value;
1895
+ for (const pattern of SENSITIVE_VALUE_RES) {
1896
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
1897
+ }
1898
+ sanitized = sanitized.replace(
1899
+ SENSITIVE_ASSIGNMENT_RE,
1900
+ (_match, key) => `${key}=[REDACTED]`
1901
+ );
1902
+ if (sanitized.length <= maxLength) return sanitized;
1903
+ return sanitized.slice(0, maxLength);
1904
+ }
1905
+ function isRecord3(value) {
1906
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1907
+ }
1775
1908
  function nonEmptyString(value) {
1776
1909
  return typeof value === "string" && value.length > 0 ? value : void 0;
1777
1910
  }