@plune-ai/cli 0.26.0 → 0.28.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.cjs CHANGED
@@ -54,6 +54,10 @@ __export(src_exports, {
54
54
  });
55
55
  module.exports = __toCommonJS(src_exports);
56
56
 
57
+ // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.15_tsx@4.22.4_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
58
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
59
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
60
+
57
61
  // src/cli/commands/run.ts
58
62
  var fs5 = __toESM(require("fs"), 1);
59
63
  var path4 = __toESM(require("path"), 1);
@@ -508,7 +512,8 @@ function makeAnthropicProvider(config, env, pricing) {
508
512
  usage: {
509
513
  input_tokens: res.usage.input_tokens,
510
514
  output_tokens: res.usage.output_tokens
511
- }
515
+ },
516
+ ...res.stop_reason === "max_tokens" ? { finish_reason: "length" } : res.stop_reason !== null && res.stop_reason !== void 0 ? { finish_reason: res.stop_reason } : {}
512
517
  };
513
518
  } catch (err) {
514
519
  throw normalizeProviderError(err, apiKey, ENV_VAR);
@@ -555,8 +560,11 @@ function makeOpenAiCompatibleProvider(opts) {
555
560
  { max_retries: maxRetries }
556
561
  );
557
562
  const reportedCost = reportsCost ? res.usage?.cost : void 0;
563
+ const choice = res.choices[0];
564
+ const finish = choice?.error !== void 0 ? "error" : choice?.finish_reason;
558
565
  return {
559
- output: res.choices[0]?.message?.content ?? "",
566
+ output: choice?.message?.content ?? "",
567
+ ...typeof finish === "string" ? { finish_reason: finish } : {},
560
568
  usage: {
561
569
  input_tokens: res.usage?.prompt_tokens ?? 0,
562
570
  output_tokens: res.usage?.completion_tokens ?? 0
@@ -691,21 +699,110 @@ function getDefaultEmbedder() {
691
699
  }
692
700
 
693
701
  // src/cache/cache.ts
694
- var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
702
+ var import_node_module = require("module");
703
+
704
+ // src/util/hash.ts
705
+ var import_node_crypto = require("crypto");
706
+
707
+ // src/util/canonical-json.ts
708
+ function normalizeValue(value) {
709
+ if (value === null) return null;
710
+ if (typeof value === "boolean") return value;
711
+ if (typeof value === "number") {
712
+ if (!isFinite(value)) {
713
+ throw new Error(
714
+ `canonicalJson: non-finite number rejected: ${String(value)}`
715
+ );
716
+ }
717
+ return value === 0 ? 0 : value;
718
+ }
719
+ if (typeof value === "string") {
720
+ return value.normalize("NFC");
721
+ }
722
+ if (Array.isArray(value)) {
723
+ return value.map(normalizeValue);
724
+ }
725
+ if (typeof value === "object") {
726
+ const proto = Object.getPrototypeOf(value);
727
+ if (proto !== null && proto !== Object.prototype) {
728
+ const name = value.constructor?.name ?? "unknown";
729
+ throw new Error(
730
+ `canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
731
+ );
732
+ }
733
+ const obj = value;
734
+ const sorted = {};
735
+ for (const key of Object.keys(obj).sort()) {
736
+ sorted[key] = normalizeValue(obj[key]);
737
+ }
738
+ return sorted;
739
+ }
740
+ throw new Error(
741
+ `canonicalJson: unsupported value type: ${typeof value}`
742
+ );
743
+ }
744
+ function canonicalJson(value) {
745
+ return JSON.stringify(normalizeValue(value));
746
+ }
747
+
748
+ // src/util/hash.ts
749
+ function sha256(input) {
750
+ return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
751
+ }
752
+ var CACHE_POLICY_VERSION = 1;
753
+ function cacheKey(inputs, policyVersion = CACHE_POLICY_VERSION) {
754
+ const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
755
+ return sha256(
756
+ canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved, policy: policyVersion })
757
+ );
758
+ }
759
+ function configHash(config) {
760
+ return sha256(canonicalJson(config));
761
+ }
762
+
763
+ // src/cache/cache.ts
695
764
  var SCHEMA_VERSION = 1;
696
765
  var META_DDL = "CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);";
697
766
  var COMPLETIONS_DDL = "CREATE TABLE IF NOT EXISTS completions (cache_key TEXT PRIMARY KEY, output TEXT NOT NULL, input_tokens INTEGER NOT NULL, output_tokens INTEGER NOT NULL, created_at INTEGER NOT NULL);";
767
+ var UNCACHEABLE_FINISH = /* @__PURE__ */ new Set(["length", "max_tokens", "error", "content_filter", "pause_turn"]);
768
+ function isCacheable(res) {
769
+ if (res.output.trim() === "") return false;
770
+ return res.finish_reason === void 0 || !UNCACHEABLE_FINISH.has(res.finish_reason);
771
+ }
772
+ var missingNoted = false;
773
+ function loadDatabase() {
774
+ try {
775
+ return (0, import_node_module.createRequire)(importMetaUrl)("better-sqlite3");
776
+ } catch {
777
+ if (!missingNoted) {
778
+ missingNoted = true;
779
+ console.error(
780
+ "plune: completion cache disabled (better-sqlite3 is not installed or failed to load). Reinstall with a C++ toolchain and Python available, or ignore: runs work without the cache."
781
+ );
782
+ }
783
+ return void 0;
784
+ }
785
+ }
698
786
  function openCache(file, opts = {}) {
699
787
  const now = opts.now ?? Date.now;
788
+ const Database = loadDatabase();
789
+ if (Database === void 0) return new NoOpCache();
700
790
  try {
701
- const db = new import_better_sqlite3.default(file);
791
+ const db = new Database(file);
702
792
  try {
703
793
  ensureSchema(db);
704
794
  } catch (err) {
705
795
  db.close();
706
796
  throw err;
707
797
  }
708
- return new SqliteCache(db, now);
798
+ let stale;
799
+ try {
800
+ stale = markPolicy(db, opts.policyVersion ?? CACHE_POLICY_VERSION);
801
+ } catch (err) {
802
+ db.close();
803
+ throw err;
804
+ }
805
+ return new SqliteCache(db, now, stale);
709
806
  } catch {
710
807
  return new NoOpCache();
711
808
  }
@@ -720,17 +817,33 @@ function ensureSchema(db) {
720
817
  db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("schema_version", current);
721
818
  }
722
819
  }
820
+ function markPolicy(db, version) {
821
+ const current = String(version);
822
+ const stored = db.prepare("SELECT value FROM meta WHERE key = 'policy_version'").get()?.value;
823
+ if (stored === current) return 0;
824
+ return db.transaction(() => {
825
+ const { n } = db.prepare("SELECT COUNT(*) AS n FROM completions").get();
826
+ db.exec("DELETE FROM completions");
827
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)").run("policy_version", current);
828
+ return n;
829
+ })();
830
+ }
723
831
  var SqliteCache = class {
724
- constructor(db, now) {
832
+ constructor(db, now, staleEntries) {
725
833
  this.db = db;
726
834
  this.now = now;
835
+ this.staleEntries = staleEntries;
727
836
  }
728
837
  db;
729
838
  now;
839
+ staleEntries;
730
840
  get(key, opts = {}) {
731
841
  try {
732
842
  const row = this.db.prepare("SELECT output, input_tokens, output_tokens, created_at FROM completions WHERE cache_key = ?").get(key);
733
843
  if (row === void 0) return void 0;
844
+ if (typeof row.output !== "string" || !Number.isFinite(row.input_tokens) || !Number.isFinite(row.output_tokens) || !Number.isFinite(row.created_at)) {
845
+ return void 0;
846
+ }
734
847
  if (opts.maxAgeMs !== void 0 && this.now() - row.created_at > opts.maxAgeMs) {
735
848
  return void 0;
736
849
  }
@@ -756,6 +869,12 @@ var SqliteCache = class {
756
869
  } catch {
757
870
  }
758
871
  }
872
+ delete(key) {
873
+ try {
874
+ this.db.prepare("DELETE FROM completions WHERE cache_key = ?").run(key);
875
+ } catch {
876
+ }
877
+ }
759
878
  clear() {
760
879
  try {
761
880
  this.db.exec("DELETE FROM completions");
@@ -772,6 +891,8 @@ var NoOpCache = class {
772
891
  }
773
892
  set() {
774
893
  }
894
+ delete() {
895
+ }
775
896
  clear() {
776
897
  }
777
898
  close() {
@@ -782,60 +903,72 @@ var NoOpCache = class {
782
903
  var fs3 = __toESM(require("fs"), 1);
783
904
  var path2 = __toESM(require("path"), 1);
784
905
 
785
- // src/util/hash.ts
786
- var import_node_crypto = require("crypto");
787
-
788
- // src/util/canonical-json.ts
789
- function normalizeValue(value) {
790
- if (value === null) return null;
791
- if (typeof value === "boolean") return value;
792
- if (typeof value === "number") {
793
- if (!isFinite(value)) {
794
- throw new Error(
795
- `canonicalJson: non-finite number rejected: ${String(value)}`
796
- );
797
- }
798
- return value === 0 ? 0 : value;
799
- }
800
- if (typeof value === "string") {
801
- return value.normalize("NFC");
906
+ // src/assertions/json-extract.ts
907
+ function extractJson(output, mode) {
908
+ if (mode === "strict") {
909
+ return tryParse(output.trim());
802
910
  }
803
- if (Array.isArray(value)) {
804
- return value.map(normalizeValue);
911
+ const fenced = extractFencedBlock(output);
912
+ if (fenced !== null) {
913
+ const r = tryParse(fenced);
914
+ if (r.ok) return r;
805
915
  }
806
- if (typeof value === "object") {
807
- const proto = Object.getPrototypeOf(value);
808
- if (proto !== null && proto !== Object.prototype) {
809
- const name = value.constructor?.name ?? "unknown";
810
- throw new Error(
811
- `canonicalJson: unsupported non-plain object (${name}) \u2014 only plain objects, arrays, and primitives are serializable`
812
- );
813
- }
814
- const obj = value;
815
- const sorted = {};
816
- for (const key of Object.keys(obj).sort()) {
817
- sorted[key] = normalizeValue(obj[key]);
818
- }
819
- return sorted;
916
+ for (const run of balancedRuns(output)) {
917
+ const r = tryParse(run);
918
+ if (r.ok) return r;
820
919
  }
821
- throw new Error(
822
- `canonicalJson: unsupported value type: ${typeof value}`
823
- );
920
+ return { ok: false };
824
921
  }
825
- function canonicalJson(value) {
826
- return JSON.stringify(normalizeValue(value));
922
+ function tryParse(s) {
923
+ try {
924
+ return { ok: true, value: JSON.parse(s) };
925
+ } catch {
926
+ return { ok: false };
927
+ }
827
928
  }
828
-
829
- // src/util/hash.ts
830
- function sha256(input) {
831
- return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex");
929
+ function extractFencedBlock(s) {
930
+ const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
931
+ return m?.[1]?.trim() ?? null;
832
932
  }
833
- function cacheKey(inputs) {
834
- const { provider, model, temperature, max_tokens, prompt_resolved } = inputs;
835
- return sha256(canonicalJson({ provider, model, temperature, max_tokens, prompt_resolved }));
933
+ function* balancedRuns(s) {
934
+ let i = 0;
935
+ while (i < s.length) {
936
+ const rel = s.slice(i).search(/[{[]/);
937
+ if (rel === -1) return;
938
+ const start = i + rel;
939
+ const run = balancedFrom(s, start);
940
+ if (run === null) {
941
+ i = start + 1;
942
+ } else {
943
+ yield run;
944
+ i = start + run.length;
945
+ }
946
+ }
836
947
  }
837
- function configHash(config) {
838
- return sha256(canonicalJson(config));
948
+ function balancedFrom(s, start) {
949
+ const open = s[start];
950
+ const close = open === "{" ? "}" : "]";
951
+ let depth = 0;
952
+ let inString = false;
953
+ let escaped = false;
954
+ for (let i = start; i < s.length; i++) {
955
+ const ch = s[i];
956
+ if (inString) {
957
+ if (escaped) escaped = false;
958
+ else if (ch === "\\") escaped = true;
959
+ else if (ch === '"') inString = false;
960
+ continue;
961
+ }
962
+ if (ch === '"') {
963
+ inString = true;
964
+ } else if (ch === open) {
965
+ depth++;
966
+ } else if (ch === close) {
967
+ depth--;
968
+ if (depth === 0) return s.slice(start, i + 1);
969
+ }
970
+ }
971
+ return null;
839
972
  }
840
973
 
841
974
  // src/assertions/interpolate.ts
@@ -911,76 +1044,6 @@ var containsAll = {
911
1044
 
912
1045
  // src/assertions/json-schema.ts
913
1046
  var import_ajv = require("ajv");
914
-
915
- // src/assertions/json-extract.ts
916
- function extractJson(output, mode) {
917
- if (mode === "strict") {
918
- return tryParse(output.trim());
919
- }
920
- const fenced = extractFencedBlock(output);
921
- if (fenced !== null) {
922
- const r = tryParse(fenced);
923
- if (r.ok) return r;
924
- }
925
- for (const run of balancedRuns(output)) {
926
- const r = tryParse(run);
927
- if (r.ok) return r;
928
- }
929
- return { ok: false };
930
- }
931
- function tryParse(s) {
932
- try {
933
- return { ok: true, value: JSON.parse(s) };
934
- } catch {
935
- return { ok: false };
936
- }
937
- }
938
- function extractFencedBlock(s) {
939
- const m = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
940
- return m?.[1]?.trim() ?? null;
941
- }
942
- function* balancedRuns(s) {
943
- let i = 0;
944
- while (i < s.length) {
945
- const rel = s.slice(i).search(/[{[]/);
946
- if (rel === -1) return;
947
- const start = i + rel;
948
- const run = balancedFrom(s, start);
949
- if (run === null) {
950
- i = start + 1;
951
- } else {
952
- yield run;
953
- i = start + run.length;
954
- }
955
- }
956
- }
957
- function balancedFrom(s, start) {
958
- const open = s[start];
959
- const close = open === "{" ? "}" : "]";
960
- let depth = 0;
961
- let inString = false;
962
- let escaped = false;
963
- for (let i = start; i < s.length; i++) {
964
- const ch = s[i];
965
- if (inString) {
966
- if (escaped) escaped = false;
967
- else if (ch === "\\") escaped = true;
968
- else if (ch === '"') inString = false;
969
- continue;
970
- }
971
- if (ch === '"') {
972
- inString = true;
973
- } else if (ch === open) {
974
- depth++;
975
- } else if (ch === close) {
976
- depth--;
977
- if (depth === 0) return s.slice(start, i + 1);
978
- }
979
- }
980
- return null;
981
- }
982
-
983
- // src/assertions/json-schema.ts
984
1047
  var jsonSchema = {
985
1048
  run(ctx) {
986
1049
  const mode = ctx.params.extract ?? "auto";
@@ -1340,6 +1403,12 @@ var RunConfigError = class extends Error {
1340
1403
  this.name = "RunConfigError";
1341
1404
  }
1342
1405
  };
1406
+ var CACHE_MISS_CODE = "CACHE_MISS";
1407
+ function answerParses(output, assertions) {
1408
+ return assertions.every(
1409
+ (a) => a.type !== "json-schema" || extractJson(output, a.extract ?? "auto").ok
1410
+ );
1411
+ }
1343
1412
  function resolvePrompt(template, vars) {
1344
1413
  return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
1345
1414
  if (!(key in vars)) {
@@ -1367,17 +1436,35 @@ async function runRow(p) {
1367
1436
  const usage2 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
1368
1437
  return { vars: p.row.vars, output: null, cached: false, usage: usage2, assertions: [] };
1369
1438
  }
1370
- const key = cacheKey({
1371
- provider: p.providerConfig.type,
1372
- model: p.providerConfig.model,
1373
- temperature: temperature ?? 0,
1374
- max_tokens: maxTokens,
1375
- prompt_resolved: prompt5
1376
- });
1439
+ const key = cacheKey(
1440
+ {
1441
+ provider: p.providerConfig.type,
1442
+ model: p.providerConfig.model,
1443
+ temperature: temperature ?? 0,
1444
+ max_tokens: maxTokens,
1445
+ prompt_resolved: prompt5
1446
+ },
1447
+ p.policyVersion
1448
+ );
1377
1449
  let output;
1378
1450
  let usage;
1379
1451
  let cached = false;
1380
- const hit = p.noCache ? void 0 : p.cache.get(key);
1452
+ let cacheSaved;
1453
+ let hit = p.noCache ? void 0 : p.cache.get(key, p.cacheMaxAgeMs !== void 0 ? { maxAgeMs: p.cacheMaxAgeMs } : {});
1454
+ if (hit !== void 0 && !answerParses(hit.output, p.assertions)) {
1455
+ if (p.cacheStrict !== true) p.cache.delete(key);
1456
+ hit = void 0;
1457
+ }
1458
+ if (hit === void 0 && p.cacheStrict === true) {
1459
+ return {
1460
+ vars: p.row.vars,
1461
+ output: null,
1462
+ cached: false,
1463
+ latency_ms: p.now() - t0,
1464
+ error: { code: CACHE_MISS_CODE, message: "no usable cache entry (--cache-strict)" },
1465
+ assertions: []
1466
+ };
1467
+ }
1381
1468
  if (hit !== void 0) {
1382
1469
  output = hit.output;
1383
1470
  cached = true;
@@ -1386,6 +1473,11 @@ async function runRow(p) {
1386
1473
  output_tokens: hit.usage.output_tokens,
1387
1474
  cost_usd: 0
1388
1475
  };
1476
+ cacheSaved = {
1477
+ input_tokens: hit.usage.input_tokens,
1478
+ output_tokens: hit.usage.output_tokens,
1479
+ cost_usd: p.provider.estimateCost(hit.usage).cost_usd
1480
+ };
1389
1481
  } else {
1390
1482
  try {
1391
1483
  const res = await p.provider.complete({
@@ -1403,7 +1495,9 @@ async function runRow(p) {
1403
1495
  // lets a config pricing override win, else falls back to the table estimate (ADR-PRC01).
1404
1496
  cost_usd: p.provider.estimateCost(res.usage, res.cost_usd).cost_usd
1405
1497
  };
1406
- if (!p.noCache) p.cache.set(key, res);
1498
+ if (!p.noCache && isCacheable(res) && answerParses(res.output, p.assertions)) {
1499
+ p.cache.set(key, res);
1500
+ }
1407
1501
  } catch (err) {
1408
1502
  return {
1409
1503
  vars: p.row.vars,
@@ -1449,6 +1543,7 @@ async function runRow(p) {
1449
1543
  cached,
1450
1544
  usage: finalUsage,
1451
1545
  latency_ms: p.now() - t0,
1546
+ ...cacheSaved !== void 0 ? { cache_saved: cacheSaved } : {},
1452
1547
  ...assertionError !== void 0 ? { error: assertionError } : {},
1453
1548
  assertions: records
1454
1549
  };
@@ -1483,19 +1578,41 @@ function summarize(evals, durationMs) {
1483
1578
  cost_usd: 0,
1484
1579
  duration_ms: durationMs
1485
1580
  };
1581
+ const cache = {
1582
+ from_cache: 0,
1583
+ from_model: 0,
1584
+ missed: 0,
1585
+ errored: 0,
1586
+ saved: { input_tokens: 0, output_tokens: 0, cost_usd: 0 }
1587
+ };
1486
1588
  for (const ev of evals) {
1487
1589
  for (const row of ev.rows) {
1488
1590
  summary.total += 1;
1489
1591
  summary[rowStatus(row)] += 1;
1490
1592
  summary.cost_usd += row.usage?.cost_usd ?? 0;
1593
+ if (row.cached) {
1594
+ cache.from_cache += 1;
1595
+ cache.saved.input_tokens += row.cache_saved?.input_tokens ?? 0;
1596
+ cache.saved.output_tokens += row.cache_saved?.output_tokens ?? 0;
1597
+ cache.saved.cost_usd += row.cache_saved?.cost_usd ?? 0;
1598
+ } else if (row.error?.code === CACHE_MISS_CODE) cache.missed += 1;
1599
+ else if (row.output !== null) cache.from_model += 1;
1600
+ else if (row.error !== void 0) cache.errored += 1;
1491
1601
  }
1492
1602
  }
1493
- return summary;
1603
+ return { ...summary, cache };
1494
1604
  }
1495
1605
  async function runOrchestration(config, options, deps) {
1606
+ const noCache = options.noCache ?? false;
1607
+ const cacheStrict = options.cacheStrict ?? false;
1608
+ if (noCache && cacheStrict) {
1609
+ throw new RunConfigError("--no-cache and --cache-strict cannot be used together");
1610
+ }
1611
+ if (noCache && options.cacheMaxAgeMs !== void 0) {
1612
+ throw new RunConfigError("--no-cache and --cache-max-age cannot be used together");
1613
+ }
1496
1614
  const started = deps.now();
1497
1615
  const dryRun = options.dryRun ?? false;
1498
- const noCache = options.noCache ?? false;
1499
1616
  const evalResults = [];
1500
1617
  const prepared = selectEvals(config.evals, options.only).map((ev) => {
1501
1618
  const template = resolveTemplate(ev, deps.baseDir);
@@ -1522,7 +1639,9 @@ async function runOrchestration(config, options, deps) {
1522
1639
  cache: deps.cache,
1523
1640
  now: deps.now,
1524
1641
  dryRun,
1525
- noCache
1642
+ noCache,
1643
+ cacheStrict,
1644
+ ...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
1526
1645
  })
1527
1646
  );
1528
1647
  const passed = rowResults.every((r) => rowStatus(r) === "passed");
@@ -1555,6 +1674,7 @@ function loadDataset(ref, baseDir) {
1555
1674
 
1556
1675
  // src/cli/commands/run.ts
1557
1676
  var NOOP_CACHE = { get: () => void 0, set: () => {
1677
+ }, delete: () => {
1558
1678
  }, clear: () => {
1559
1679
  }, close: () => {
1560
1680
  } };
@@ -1591,6 +1711,11 @@ async function handleRun(options, depsFactory) {
1591
1711
  const factory = depsFactory ?? ((c, d) => buildRealDeps(c, d, options.dryRun));
1592
1712
  const deps = factory(config, baseDir);
1593
1713
  try {
1714
+ const stale = deps.cache.staleEntries ?? 0;
1715
+ if (stale > 0) {
1716
+ process.stderr.write(`cache policy changed, ${stale} entries are stale (dropped)
1717
+ `);
1718
+ }
1594
1719
  const result = await runOrchestration(
1595
1720
  config,
1596
1721
  {
@@ -1598,7 +1723,9 @@ async function handleRun(options, depsFactory) {
1598
1723
  ...options.only !== void 0 ? { only: options.only } : {},
1599
1724
  ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
1600
1725
  ...options.noCache !== void 0 ? { noCache: options.noCache } : {},
1601
- ...options.bail !== void 0 ? { bail: options.bail } : {}
1726
+ ...options.bail !== void 0 ? { bail: options.bail } : {},
1727
+ ...options.cacheStrict === true ? { cacheStrict: true } : {},
1728
+ ...options.cacheMaxAgeMs !== void 0 ? { cacheMaxAgeMs: options.cacheMaxAgeMs } : {}
1602
1729
  },
1603
1730
  deps
1604
1731
  );
@@ -1645,6 +1772,7 @@ var rowResultSchema = import_zod3.z.object({
1645
1772
  latency_ms: import_zod3.z.number().optional(),
1646
1773
  error: runErrorSchema.optional(),
1647
1774
  assertions: import_zod3.z.array(assertionResultRecordSchema2),
1775
+ cache_saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() }).optional(),
1648
1776
  /** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
1649
1777
  inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
1650
1778
  });
@@ -1657,7 +1785,14 @@ var summarySchema = import_zod3.z.object({
1657
1785
  blocked: import_zod3.z.number().optional(),
1658
1786
  skipped: import_zod3.z.number().optional(),
1659
1787
  cost_usd: import_zod3.z.number(),
1660
- duration_ms: import_zod3.z.number()
1788
+ duration_ms: import_zod3.z.number(),
1789
+ cache: import_zod3.z.object({
1790
+ from_cache: import_zod3.z.number(),
1791
+ from_model: import_zod3.z.number(),
1792
+ missed: import_zod3.z.number(),
1793
+ errored: import_zod3.z.number(),
1794
+ saved: import_zod3.z.object({ input_tokens: import_zod3.z.number(), output_tokens: import_zod3.z.number(), cost_usd: import_zod3.z.number() })
1795
+ }).optional()
1661
1796
  });
1662
1797
  var evalResultSchema = import_zod3.z.object({
1663
1798
  id: import_zod3.z.string(),
package/dist/index.d.cts CHANGED
@@ -15,6 +15,24 @@ interface Summary {
15
15
  skipped?: number;
16
16
  cost_usd: number;
17
17
  duration_ms: number;
18
+ /** plune-ai/plune#1696. Where the answers came from. Optional and additive; absent in older runs. */
19
+ cache?: CacheSummary;
20
+ }
21
+ /**
22
+ * `from_cache` rows were answered by the store, `from_model` by a model call, `missed` had no entry
23
+ * under `--cache-strict` and were not answered. The `saved` totals are the sum of the rows' `cache_saved`.
24
+ */
25
+ interface CacheSummary {
26
+ from_cache: number;
27
+ from_model: number;
28
+ missed: number;
29
+ /** Rows the provider failed on: answered by neither, so the four counts add up to `total` (dry-run rows aside). */
30
+ errored: number;
31
+ saved: {
32
+ input_tokens: number;
33
+ output_tokens: number;
34
+ cost_usd: number;
35
+ };
18
36
  }
19
37
  interface AssertionResultRecord {
20
38
  type: string;
@@ -87,6 +105,12 @@ interface RowResult {
87
105
  latency_ms?: number;
88
106
  error?: RunError;
89
107
  assertions: AssertionResultRecord[];
108
+ /** plune-ai/plune#1696. On a cache hit: what the call would have cost. Optional and additive. */
109
+ cache_saved?: {
110
+ input_tokens: number;
111
+ output_tokens: number;
112
+ cost_usd: number;
113
+ };
90
114
  /**
91
115
  * ADR 0045. Say it when the test was never judged, rather than letting `failed` stand for both
92
116
  * "the environment broke" and "the code is wrong".
@@ -231,6 +255,11 @@ interface CompletionResponse {
231
255
  * estimated downstream (ADR-PRC02). Additive + optional: existing providers omit it.
232
256
  */
233
257
  cost_usd?: number;
258
+ /**
259
+ * Why the model stopped, normalised: `length` (hit the token limit), `error` or `content_filter`
260
+ * mark an answer the cache must not keep. Absent when the provider does not say. Optional and additive.
261
+ */
262
+ finish_reason?: string;
234
263
  }
235
264
  interface CostEstimate {
236
265
  cost_usd: number;
@@ -260,6 +289,10 @@ interface Cache {
260
289
  maxAgeMs?: number;
261
290
  }): CompletionResponse | undefined;
262
291
  set(key: string, value: CompletionResponse): void;
292
+ /** Remove one entry (a cached answer that turned out unusable). Best-effort. */
293
+ delete(key: string): void;
294
+ /** Entries left by an older policy version, found when the store was opened (0 when none or unknown). */
295
+ readonly staleEntries?: number;
263
296
  clear(): void;
264
297
  close(): void;
265
298
  }
@@ -280,6 +313,9 @@ interface RunOptions {
280
313
  concurrency?: number;
281
314
  noCache?: boolean;
282
315
  bail?: boolean;
316
+ cacheStrict?: boolean;
317
+ /** Milliseconds; parsed from `--cache-max-age` by the caller. */
318
+ cacheMaxAgeMs?: number;
283
319
  }
284
320
  declare function handleRun(options: RunOptions, depsFactory?: (config: Config, baseDir: string) => RunDeps): Promise<RunResult>;
285
321
 
@@ -449,6 +485,17 @@ declare const runResultSchema: z.ZodObject<{
449
485
  skipped: z.ZodOptional<z.ZodNumber>;
450
486
  cost_usd: z.ZodNumber;
451
487
  duration_ms: z.ZodNumber;
488
+ cache: z.ZodOptional<z.ZodObject<{
489
+ from_cache: z.ZodNumber;
490
+ from_model: z.ZodNumber;
491
+ missed: z.ZodNumber;
492
+ errored: z.ZodNumber;
493
+ saved: z.ZodObject<{
494
+ input_tokens: z.ZodNumber;
495
+ output_tokens: z.ZodNumber;
496
+ cost_usd: z.ZodNumber;
497
+ }, z.core.$strip>;
498
+ }, z.core.$strip>>;
452
499
  }, z.core.$strip>;
453
500
  evals: z.ZodArray<z.ZodObject<{
454
501
  id: z.ZodString;
@@ -477,6 +524,11 @@ declare const runResultSchema: z.ZodObject<{
477
524
  passed: z.ZodBoolean;
478
525
  }, z.core.$strip>>>;
479
526
  }, z.core.$strip>>;
527
+ cache_saved: z.ZodOptional<z.ZodObject<{
528
+ input_tokens: z.ZodNumber;
529
+ output_tokens: z.ZodNumber;
530
+ cost_usd: z.ZodNumber;
531
+ }, z.core.$strip>>;
480
532
  inconclusive: z.ZodOptional<z.ZodEnum<{
481
533
  blocked: "blocked";
482
534
  skipped: "skipped";