@danypops/pi-eval-harness 0.2.0 → 0.3.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
@@ -48,6 +48,20 @@ would only hide the assertion, not simplify it.
48
48
  A `Checker` that only ever gets exercised against a real, expensive `pi-process-harness` run has
49
49
  no fast, deterministic proof it is correct in isolation -- write the fixture test first.
50
50
 
51
+ ## Real live-LLM smoke test (opt-in, real cost -- never run automatically)
52
+
53
+ `scripts/real-llm-smoke-test.ts` spawns a genuine `pi` process against a real model (no faux
54
+ provider, no scripted tool calls) and runs the captured trace through this package's own
55
+ matching/rollup functions -- confirming they handle a real model's own event shape, not just the
56
+ faux provider's conveniences every other test here relies on. Run explicitly:
57
+
58
+ ```sh
59
+ bun scripts/real-llm-smoke-test.ts
60
+ ```
61
+
62
+ Requires real provider credentials already configured in your own ambient Pi profile (this
63
+ script deliberately runs with `isolatedHome: false`) and incurs a real, billed API call.
64
+
51
65
  ## License
52
66
 
53
67
  MIT
@@ -17,6 +17,8 @@ export interface AblationDelta {
17
17
  readonly meanScoreDelta: number;
18
18
  readonly meanDurationMsDelta: number;
19
19
  readonly meanTokensInDelta: number;
20
+ readonly meanCacheReadTokensDelta: number;
21
+ readonly meanCacheWriteTokensDelta: number;
20
22
  }
21
23
  /** One config's own trial metrics, plus its delta vs baseline. `delta` is undefined for the baseline itself. */
22
24
  export interface AblationResult {
package/dist/ablation.js CHANGED
@@ -32,6 +32,8 @@ export async function ablate(configs, n, options = {}) {
32
32
  meanScoreDelta: metrics.meanScore - baseline.meanScore,
33
33
  meanDurationMsDelta: metrics.meanDurationMs - baseline.meanDurationMs,
34
34
  meanTokensInDelta: metrics.meanTokensIn - baseline.meanTokensIn,
35
+ meanCacheReadTokensDelta: metrics.meanCacheReadTokens - baseline.meanCacheReadTokens,
36
+ meanCacheWriteTokensDelta: metrics.meanCacheWriteTokens - baseline.meanCacheWriteTokens,
35
37
  },
36
38
  };
37
39
  });
@@ -45,10 +47,10 @@ export function formatAblation(label, results) {
45
47
  const lines = [`=== Ablation: ${label} ===`];
46
48
  for (const result of results) {
47
49
  const m = result.metrics;
48
- lines.push(` ${result.config.name.padEnd(14)} pass_rate=${m.passRate.toFixed(2)} mean_score=${m.meanScore.toFixed(2)} mean_duration_ms=${m.meanDurationMs.toFixed(0)} tokens_in=${m.meanTokensIn.toFixed(0)}`);
50
+ lines.push(` ${result.config.name.padEnd(14)} pass_rate=${m.passRate.toFixed(2)} mean_score=${m.meanScore.toFixed(2)} mean_duration_ms=${m.meanDurationMs.toFixed(0)} tokens_in=${m.meanTokensIn.toFixed(0)} cache_read=${m.meanCacheReadTokens.toFixed(0)} cache_write=${m.meanCacheWriteTokens.toFixed(0)}`);
49
51
  if (result.delta) {
50
52
  const d = result.delta;
51
- lines.push(` ${"(vs baseline)".padEnd(14)} Δpass_rate=${signed(d.passRateDelta, 2)} Δscore=${signed(d.meanScoreDelta, 2)} Δduration_ms=${signed(d.meanDurationMsDelta, 0)} Δtokens_in=${signed(d.meanTokensInDelta, 0)}`);
53
+ lines.push(` ${"(vs baseline)".padEnd(14)} Δpass_rate=${signed(d.passRateDelta, 2)} Δscore=${signed(d.meanScoreDelta, 2)} Δduration_ms=${signed(d.meanDurationMsDelta, 0)} Δtokens_in=${signed(d.meanTokensInDelta, 0)} Δcache_read=${signed(d.meanCacheReadTokensDelta, 0)} Δcache_write=${signed(d.meanCacheWriteTokensDelta, 0)}`);
52
54
  }
53
55
  }
54
56
  return lines.join("\n");
package/dist/trials.d.ts CHANGED
@@ -11,6 +11,9 @@ export interface TrialResult {
11
11
  readonly durationMs: number;
12
12
  readonly tokensIn: number;
13
13
  readonly tokensOut: number;
14
+ /** Real cache-read/cache-write tokens -- under prompt caching, the dominant real component of total context size; tokensIn alone (a provider's own incremental, non-cached count) understates it. */
15
+ readonly cacheReadTokens: number;
16
+ readonly cacheWriteTokens: number;
14
17
  readonly costUsd: number;
15
18
  readonly error?: string;
16
19
  }
@@ -26,6 +29,8 @@ export interface TrialMetrics {
26
29
  readonly meanDurationMs: number;
27
30
  readonly meanTokensIn: number;
28
31
  readonly meanTokensOut: number;
32
+ readonly meanCacheReadTokens: number;
33
+ readonly meanCacheWriteTokens: number;
29
34
  readonly meanCostUsd: number;
30
35
  }
31
36
  /**
package/dist/trials.js CHANGED
@@ -55,6 +55,8 @@ export function aggregateTrials(results, options = {}) {
55
55
  meanDurationMs: mean(results.map((result) => result.durationMs)),
56
56
  meanTokensIn: mean(results.map((result) => result.tokensIn)),
57
57
  meanTokensOut: mean(results.map((result) => result.tokensOut)),
58
+ meanCacheReadTokens: mean(results.map((result) => result.cacheReadTokens)),
59
+ meanCacheWriteTokens: mean(results.map((result) => result.cacheWriteTokens)),
58
60
  meanCostUsd: mean(results.map((result) => result.costUsd)),
59
61
  };
60
62
  }
@@ -80,6 +82,8 @@ export async function runTrials(runOne, n, options = {}) {
80
82
  durationMs: 0,
81
83
  tokensIn: 0,
82
84
  tokensOut: 0,
85
+ cacheReadTokens: 0,
86
+ cacheWriteTokens: 0,
83
87
  costUsd: 0,
84
88
  error: error instanceof Error ? error.message : String(error),
85
89
  };
package/dist/turns.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface Turn {
14
14
  readonly tokensIn: number;
15
15
  readonly tokensOut: number;
16
16
  readonly cacheReadTokens: number;
17
+ /** New context tokens written to the provider's cache this turn -- the other real component of total context size under prompt caching, alongside cacheReadTokens. */
18
+ readonly cacheWriteTokens: number;
17
19
  /** Real cost in USD from Usage.cost.total, when the provider reports pricing. */
18
20
  readonly costUsd: number;
19
21
  /** Number of tool calls dispatched from this turn. */
@@ -29,6 +31,7 @@ export interface RunUsageSummary {
29
31
  readonly tokensIn: number;
30
32
  readonly tokensOut: number;
31
33
  readonly cacheReadTokens: number;
34
+ readonly cacheWriteTokens: number;
32
35
  readonly costUsd: number;
33
36
  readonly toolCalls: number;
34
37
  /** Every tool name called across the whole run, in dispatch order. */
package/dist/turns.js CHANGED
@@ -15,6 +15,7 @@ export function deriveTurns(events) {
15
15
  tokensIn: usage?.input ?? 0,
16
16
  tokensOut: usage?.output ?? 0,
17
17
  cacheReadTokens: usage?.cacheRead ?? 0,
18
+ cacheWriteTokens: usage?.cacheWrite ?? 0,
18
19
  costUsd: usage?.cost.total ?? 0,
19
20
  toolCalls: event.toolResults.length,
20
21
  toolNames: event.toolResults.map((result) => result.toolName),
@@ -29,6 +30,7 @@ export function summarizeRunUsage(turns) {
29
30
  tokensIn: turns.reduce((sum, turn) => sum + turn.tokensIn, 0),
30
31
  tokensOut: turns.reduce((sum, turn) => sum + turn.tokensOut, 0),
31
32
  cacheReadTokens: turns.reduce((sum, turn) => sum + turn.cacheReadTokens, 0),
33
+ cacheWriteTokens: turns.reduce((sum, turn) => sum + turn.cacheWriteTokens, 0),
32
34
  costUsd: turns.reduce((sum, turn) => sum + turn.costUsd, 0),
33
35
  toolCalls: turns.reduce((sum, turn) => sum + turn.toolCalls, 0),
34
36
  toolNames: turns.flatMap((turn) => turn.toolNames),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-eval-harness",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scores a real agent run's own tool-call behavior -- AND/OR tool-call matching, graduated checker composition, and turn/tool-call/token-usage rollups -- over Pi's own real AgentSessionEvent stream (e.g. from @danypops/pi-process-harness).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/ablation.ts CHANGED
@@ -19,6 +19,8 @@ export interface AblationDelta {
19
19
  readonly meanScoreDelta: number;
20
20
  readonly meanDurationMsDelta: number;
21
21
  readonly meanTokensInDelta: number;
22
+ readonly meanCacheReadTokensDelta: number;
23
+ readonly meanCacheWriteTokensDelta: number;
22
24
  }
23
25
 
24
26
  /** One config's own trial metrics, plus its delta vs baseline. `delta` is undefined for the baseline itself. */
@@ -55,6 +57,8 @@ export async function ablate(configs: readonly AblationConfig[], n: number, opti
55
57
  meanScoreDelta: metrics.meanScore - baseline.meanScore,
56
58
  meanDurationMsDelta: metrics.meanDurationMs - baseline.meanDurationMs,
57
59
  meanTokensInDelta: metrics.meanTokensIn - baseline.meanTokensIn,
60
+ meanCacheReadTokensDelta: metrics.meanCacheReadTokens - baseline.meanCacheReadTokens,
61
+ meanCacheWriteTokensDelta: metrics.meanCacheWriteTokens - baseline.meanCacheWriteTokens,
58
62
  },
59
63
  };
60
64
  });
@@ -71,12 +75,12 @@ export function formatAblation(label: string, results: readonly AblationResult[]
71
75
  for (const result of results) {
72
76
  const m = result.metrics;
73
77
  lines.push(
74
- ` ${result.config.name.padEnd(14)} pass_rate=${m.passRate.toFixed(2)} mean_score=${m.meanScore.toFixed(2)} mean_duration_ms=${m.meanDurationMs.toFixed(0)} tokens_in=${m.meanTokensIn.toFixed(0)}`,
78
+ ` ${result.config.name.padEnd(14)} pass_rate=${m.passRate.toFixed(2)} mean_score=${m.meanScore.toFixed(2)} mean_duration_ms=${m.meanDurationMs.toFixed(0)} tokens_in=${m.meanTokensIn.toFixed(0)} cache_read=${m.meanCacheReadTokens.toFixed(0)} cache_write=${m.meanCacheWriteTokens.toFixed(0)}`,
75
79
  );
76
80
  if (result.delta) {
77
81
  const d = result.delta;
78
82
  lines.push(
79
- ` ${"(vs baseline)".padEnd(14)} Δpass_rate=${signed(d.passRateDelta, 2)} Δscore=${signed(d.meanScoreDelta, 2)} Δduration_ms=${signed(d.meanDurationMsDelta, 0)} Δtokens_in=${signed(d.meanTokensInDelta, 0)}`,
83
+ ` ${"(vs baseline)".padEnd(14)} Δpass_rate=${signed(d.passRateDelta, 2)} Δscore=${signed(d.meanScoreDelta, 2)} Δduration_ms=${signed(d.meanDurationMsDelta, 0)} Δtokens_in=${signed(d.meanTokensInDelta, 0)} Δcache_read=${signed(d.meanCacheReadTokensDelta, 0)} Δcache_write=${signed(d.meanCacheWriteTokensDelta, 0)}`,
80
84
  );
81
85
  }
82
86
  }
package/src/trials.ts CHANGED
@@ -12,6 +12,9 @@ export interface TrialResult {
12
12
  readonly durationMs: number;
13
13
  readonly tokensIn: number;
14
14
  readonly tokensOut: number;
15
+ /** Real cache-read/cache-write tokens -- under prompt caching, the dominant real component of total context size; tokensIn alone (a provider's own incremental, non-cached count) understates it. */
16
+ readonly cacheReadTokens: number;
17
+ readonly cacheWriteTokens: number;
15
18
  readonly costUsd: number;
16
19
  readonly error?: string;
17
20
  }
@@ -28,6 +31,8 @@ export interface TrialMetrics {
28
31
  readonly meanDurationMs: number;
29
32
  readonly meanTokensIn: number;
30
33
  readonly meanTokensOut: number;
34
+ readonly meanCacheReadTokens: number;
35
+ readonly meanCacheWriteTokens: number;
31
36
  readonly meanCostUsd: number;
32
37
  }
33
38
 
@@ -86,6 +91,8 @@ export function aggregateTrials(results: readonly TrialResult[], options: { read
86
91
  meanDurationMs: mean(results.map((result) => result.durationMs)),
87
92
  meanTokensIn: mean(results.map((result) => result.tokensIn)),
88
93
  meanTokensOut: mean(results.map((result) => result.tokensOut)),
94
+ meanCacheReadTokens: mean(results.map((result) => result.cacheReadTokens)),
95
+ meanCacheWriteTokens: mean(results.map((result) => result.cacheWriteTokens)),
89
96
  meanCostUsd: mean(results.map((result) => result.costUsd)),
90
97
  };
91
98
  }
@@ -119,6 +126,8 @@ export async function runTrials(runOne: () => Promise<TrialResult>, n: number, o
119
126
  durationMs: 0,
120
127
  tokensIn: 0,
121
128
  tokensOut: 0,
129
+ cacheReadTokens: 0,
130
+ cacheWriteTokens: 0,
122
131
  costUsd: 0,
123
132
  error: error instanceof Error ? error.message : String(error),
124
133
  };
package/src/turns.ts CHANGED
@@ -15,6 +15,8 @@ export interface Turn {
15
15
  readonly tokensIn: number;
16
16
  readonly tokensOut: number;
17
17
  readonly cacheReadTokens: number;
18
+ /** New context tokens written to the provider's cache this turn -- the other real component of total context size under prompt caching, alongside cacheReadTokens. */
19
+ readonly cacheWriteTokens: number;
18
20
  /** Real cost in USD from Usage.cost.total, when the provider reports pricing. */
19
21
  readonly costUsd: number;
20
22
  /** Number of tool calls dispatched from this turn. */
@@ -39,6 +41,7 @@ export function deriveTurns(events: readonly AgentSessionEvent[]): Turn[] {
39
41
  tokensIn: usage?.input ?? 0,
40
42
  tokensOut: usage?.output ?? 0,
41
43
  cacheReadTokens: usage?.cacheRead ?? 0,
44
+ cacheWriteTokens: usage?.cacheWrite ?? 0,
42
45
  costUsd: usage?.cost.total ?? 0,
43
46
  toolCalls: event.toolResults.length,
44
47
  toolNames: event.toolResults.map((result) => result.toolName),
@@ -53,6 +56,7 @@ export interface RunUsageSummary {
53
56
  readonly tokensIn: number;
54
57
  readonly tokensOut: number;
55
58
  readonly cacheReadTokens: number;
59
+ readonly cacheWriteTokens: number;
56
60
  readonly costUsd: number;
57
61
  readonly toolCalls: number;
58
62
  /** Every tool name called across the whole run, in dispatch order. */
@@ -66,6 +70,7 @@ export function summarizeRunUsage(turns: readonly Turn[]): RunUsageSummary {
66
70
  tokensIn: turns.reduce((sum, turn) => sum + turn.tokensIn, 0),
67
71
  tokensOut: turns.reduce((sum, turn) => sum + turn.tokensOut, 0),
68
72
  cacheReadTokens: turns.reduce((sum, turn) => sum + turn.cacheReadTokens, 0),
73
+ cacheWriteTokens: turns.reduce((sum, turn) => sum + turn.cacheWriteTokens, 0),
69
74
  costUsd: turns.reduce((sum, turn) => sum + turn.costUsd, 0),
70
75
  toolCalls: turns.reduce((sum, turn) => sum + turn.toolCalls, 0),
71
76
  toolNames: turns.flatMap((turn) => turn.toolNames),