@tangle-network/agent-eval 0.145.13 → 0.145.15

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.
@@ -223,6 +223,10 @@ uv sync --frozen --group gepa-release
223
223
  uv sync --frozen --group gepa-source
224
224
  ```
225
225
 
226
+ The published package supports the standard `gepa` engine.
227
+ The composed recipes below — `sequential`, `adaptive-sequential`, `best-of`, `vote`, and `omni` — need the tested official source revision.
228
+ Move that revision only after both the release and the source compatibility tests pass.
229
+
226
230
  ## Configure GEPA
227
231
 
228
232
  `gepaOptimizationMethod()` accepts text surfaces and component surfaces.
package/docs/concepts.md CHANGED
@@ -13,12 +13,38 @@ Use the lower-level functions when you need direct control over execution, stora
13
13
  | Function | When to call it | What you give it | What you get back |
14
14
  |---|---|---|---|
15
15
  | **`defineAgentEval()`** | You have scenarios, an agent, a judge, and a baseline surface, and you want one object you can score or improve. | scenarios, agent, judge, baseline surface | `{ evaluate(), improve() }` where `evaluate()` returns a campaign result and `improve()` returns a report |
16
- | **`selfImprove()`** | You want candidate generation, scoring, and a release decision in one call. | scenarios, agent, judge, baseline surface | report, ship/hold decision, winner surface |
16
+ | **`selfImprove()`** | You want candidate generation, scoring, and a release decision in one call. | scenarios, agent, judge, baseline surface | report, winner surface, and a `gateDecision` (see below) |
17
17
  | **`loadEvalFixtureScenarios()`** | You want agents to add evals as folders with `PROMPT.md`, checks, and starter files. | `evals/<name>/PROMPT.md + EVAL.ts + package.json` | `Scenario[]` that runs through `runCampaign`; pair with `planEvalFixtureRun()` before spending tokens |
18
18
  | **`analyzeRuns()`** | You have existing runs and do not need to invoke an agent. | `RunRecord[]` and options | `InsightReport` |
19
19
  | **Intake adapters** (`fromFeedbackTable`, `fromOtelSpans`) | Your data isn't already in `RunRecord` shape: it's in Obsidian, Sheets, an OTel collector, etc. | source-specific input | `RunRecord[]` ready to pipe into `analyzeRuns()` |
20
+ | **`sealExperiment()` / `openSealedExperiment()`** | The result must convince a reader who does not trust you, so the rules must be fixed before the data arrives. | arms, admission funnel, estimand, interval, decision table | a hashed rule tree plus executors that can run no other rule ([`experiment.md`](./experiment.md)) |
21
+ | **`runEquivalenceCheck()`** | The work has no held-out test suite, so no answer key exists to grade against. | a claim, two blind arms, an injected checker | a certification naming who vouched and how it can fail ([`verification-strategies.md`](./verification-strategies.md)) |
22
+ | **`AnalystRegistry.runExact()`** | A batch of runs failed and you need cited findings, with the caller owning every execution choice. | recorded evidence, a declared analyst list | findings with evidence references, an execution plan, and a receipt ([`trace-analysis.md`](./trace-analysis.md)) |
20
23
 
21
24
  See [`customer-journeys.md`](./customer-journeys.md) for runnable paths from existing logs, human ratings, and a callable agent.
25
+ The [README front-door table](../README.md#which-front-door) lists every callable entry point with a runnable example.
26
+
27
+ ### The five release decisions
28
+
29
+ `selfImprove()` and every gate return a `GateDecision`, not a two-way ship/hold flag.
30
+ Folding the last three into `hold` throws away the action each one names.
31
+
32
+ | Decision | What it means | What to do next |
33
+ |---|---|---|
34
+ | `ship` | Every gate passed on sufficient evidence. | Release the candidate. |
35
+ | `hold` | A gate failed on sufficient evidence. | Reject this candidate. |
36
+ | `need_more_work` | A gate could not decide: the evidence was missing, or the paired sample was too small to claim significance. | Gather more runs, then gate again. |
37
+ | `model_ceiling` | Reserved for a caller-supplied gate that attributes the limit to the model. | Handle it; no gate in this package emits it. |
38
+ | `arch_ceiling` | Reserved for a caller-supplied gate that attributes the limit to the architecture. | Handle it; no gate in this package emits it. |
39
+
40
+ The last two are part of the taxonomy and of the composition order, but no built-in gate returns them today.
41
+ Handle all five anyway: a caller's own gate may return either, and the type will not let you ignore them.
42
+
43
+ `need_more_work` is not a quiet `hold`.
44
+ "Gather more evidence" and "reject this candidate" are different actions, and folding the first into the second abandons a real gain that was only underpowered.
45
+
46
+ When gates are composed, `ship` requires every gate to ship.
47
+ Otherwise the strongest hold wins, in this order: `arch_ceiling`, `model_ceiling`, `hold`, `need_more_work`.
22
48
 
23
49
  `analyzeRuns()` and the high-level contract return the same `InsightReport` shape.
24
50
  It contains score distributions, paired lift intervals, judge agreement, cost, failure clusters, contamination checks, outcome correlation, and recommendations.
@@ -92,6 +118,48 @@ that can seed memory, replay scenarios, and optimization.
92
118
  | **Composite score** | A 0..1 number combining all dimensions. The single number you gate on. |
93
119
  | **Rubric version** | A stable hash of the rubric. Scores from different rubric versions are not comparable. |
94
120
 
121
+ ### Running an evaluation
122
+
123
+ | Term | Plain English |
124
+ |---|---|
125
+ | **Case** (`Scenario`) | One task the agent must do. The unit every score is per. |
126
+ | **Surface** | The value being changed: a prompt, a skill, or a serialized configuration. |
127
+ | **Dispatch** | The function that runs your agent on one case and returns the artifact. |
128
+ | **Campaign** | One complete pass of every case, executed, scored, and cached under a run directory. |
129
+ | **Cell** | One (case × replicate) of a campaign. Cells are cached, so a rerun skips the ones that finished. |
130
+ | **Receipt** | The record of what one paid call actually cost, in dollars and tokens. Absent when nothing measured it. |
131
+ | **Cost ledger** | The spend account receipts are written to. A capped ledger refuses a call that would exceed the cap. |
132
+ | **Provenance** | Where a number came from: the package version, the source revision, the run identity, the exact attempt. |
133
+ | **`RunRecord`** | The analysis-time projection of one run: who ran, on what, with which seed, at what cost, and what it scored. |
134
+
135
+ ### Improving a surface
136
+
137
+ | Term | Plain English |
138
+ |---|---|
139
+ | **Optimizer** | Any procedure that writes candidate surfaces and picks one. |
140
+ | **GEPA** | An open-source optimizer that mutates text using reflection over failures. It searches; this package executes and scores. |
141
+ | **SkillOpt** | Microsoft's skill optimizer. Same division of labour. |
142
+ | **Engine** | One named search procedure inside GEPA. |
143
+ | **Recipe** | How several engines are composed: in order, adaptively, best-of, or by vote. |
144
+ | **Train cases** | Evidence the optimizer reads to write candidates. |
145
+ | **Selection cases** | Evidence the optimizer reads to choose among its candidates. |
146
+ | **Final cases** | Held back from the optimizer entirely. They produce the reported lift. |
147
+
148
+ The three-way split is the reason a reported lift means anything.
149
+ An optimizer that saw the final cases can score well on them without the agent getting better.
150
+
151
+ ### Proving a result
152
+
153
+ | Term | Plain English |
154
+ |---|---|
155
+ | **Experiment** | The rules — arms, funnel, estimand, interval, decision — written as data before the data arrives. |
156
+ | **Seal** | A hash of that whole rule tree. The execution surface accepts no rule outside it. |
157
+ | **Estimand** | The exact quantity being measured, for example the paired difference in pass rate. |
158
+ | **Funnel** | The denominator chain: how many rows entered, what each stage removed, and how many remain. |
159
+ | **Verification strategy** | One of ten ways to certify a result, each with a documented way it can certify a wrong one. |
160
+ | **Certification** | Who vouched for a verdict, with what checker version, and what the checker did not check. |
161
+ | **Analyst** | A function that reads recorded evidence and returns findings that cite it. |
162
+
95
163
  ## The feedback trajectory loop
96
164
 
97
165
  Normal review activity can provide labels without a separate labeling interface:
@@ -154,19 +222,21 @@ When you have a multi-step pipeline (install → typecheck → build → lint
154
222
 
155
223
  ```ts
156
224
  const verifier = new MultiLayerVerifier([
157
- installLayer, // runs `pnpm install`
158
- typecheckLayer, // runs `tsc --noEmit`, depends on install
159
- buildLayer, // runs `pnpm build`, depends on typecheck
160
- semanticLayer, // LLM judge, weight 3, depends on build
225
+ installLayer, // runs `pnpm install`
226
+ typecheckLayer, // runs `tsc --noEmit`, depends on install
227
+ buildLayer, // runs `pnpm build`, depends on typecheck
228
+ semanticLayer, // LLM judge, weight 3, depends on build
161
229
  ])
162
230
 
163
- const report = await verifier.run({ env: { runner, workdir, ... } })
164
- report.allPass // boolean: every layer passed
165
- report.taskScore // complete task score, or undefined
166
- report.blendedScore // diagnostic weighted aggregate, possibly partial
167
- report.layers // per-layer status, findings, duration
231
+ const report = await verifier.run({ env })
232
+ report.allPass // boolean: every layer passed
233
+ report.taskScore // complete task score, or undefined
234
+ report.blendedScore // diagnostic weighted aggregate, possibly partial
235
+ report.layers // per-layer status, findings, duration
168
236
  ```
169
237
 
238
+ `env` carries the sandbox driver, the working directory, and the harness commands each layer runs.
239
+
170
240
  Use `taskScore` when creating task labels or training data.
171
241
  An errored, timed-out, skipped, or incomplete scoring panel leaves `taskScore` undefined.
172
242
  Use `blendedScore` only to inspect the measurements that did complete.
@@ -184,9 +254,31 @@ Two questions to answer before trusting any LLM judge:
184
254
  1. **Does it agree with humans?** `calibrateJudge(golden, candidate)` reports Pearson, MAE, integer-rounded κ, and worst-N miscalibrations vs a human golden set.
185
255
  2. **Does it agree with itself / other judges?** `continuousAgreement(scores)` and `calibrateJudgeContinuous(golden, candidate)` report κ_w + ICC(2,1) + Pearson + Spearman with bootstrap 95% CIs on the raw [0,1] scores.
186
256
 
187
- Why two κ flavours: the original `calibrateJudge` rounds scores to ints before computing κ. For fine-grained judges that loses information: 0.78 vs 0.81 both round to "1" and look perfectly agreed. Use `calibrateJudgeContinuous` (or `continuousAgreement` for N≥2 raters) when scores are continuous. ICC(2,1) catches systematic bias that Pearson misses: if judge B scores 2× judge A, Pearson stays ≈ 1 while ICC drops: that's the signal.
257
+ Each statistic answers a different question:
258
+
259
+ | Statistic | What it answers | What it misses |
260
+ |---|---|---|
261
+ | Pearson | Do the two raters move together? | Constant offset and constant scaling |
262
+ | Spearman | Do they rank the same way? | The size of any gap |
263
+ | MAE (mean absolute error) | How far apart are they, on average? | Whether the gap is systematic |
264
+ | κ (Cohen's kappa) | Do they agree more than chance? | Everything below the rounding step |
265
+ | ICC(2,1) | Do they agree in absolute value, not just in shape? | — |
266
+
267
+ Use two flavours of κ for one reason.
268
+ `calibrateJudge` rounds each score to an integer first.
269
+ For a fine-grained judge that throws information away: 0.78 and 0.81 both round to 1 and look perfectly agreed.
270
+ Use `calibrateJudgeContinuous`, or `continuousAgreement` for two or more raters, when the scores are continuous.
271
+
272
+ ICC(2,1) catches a bias Pearson cannot see.
273
+ If judge B always scores twice judge A, the two move together perfectly and Pearson stays near 1, while ICC drops.
274
+ That drop is the signal.
275
+
276
+ Every reported interval is a bootstrap 95 % interval: the statistic is recomputed on many resamples of the data, and the middle 95 % of those values is the interval.
188
277
 
189
- Bias probes (`positionalBias`, `verbosityBias`, `selfPreference`) cover the orthogonal failure modes: position-dependent scoring, length-correlated scoring, and judge-prefers-its-own-family.
278
+ Three bias probes cover three separate failure modes.
279
+ `positionalBias` finds a judge that scores by position.
280
+ `verbosityBias` finds one that scores by length.
281
+ `selfPreference` finds one that prefers output from its own model family.
190
282
 
191
283
  ## Trace Model
192
284
 
@@ -222,3 +314,8 @@ release decision.
222
314
  - **Building a code-generator eval?** → Start with `BuilderSession`, `SandboxHarness`, and `MultiLayerVerifier`.
223
315
  - **Multi-layer verifier?** → Use [control-runtime.md](./control-runtime.md) and `MultiLayerVerifier` for ordered gates with dependencies.
224
316
  - **Adding a new judge or rubric?** → `src/wire/rubrics.ts` for the cross-language path; `src/anti-slop.ts` and `src/judges.ts` for the in-process path.
317
+ - **Registering an experiment before the data arrives?** Read [experiment.md](./experiment.md) for the rule AST, the seal, the funnel, and the refusals.
318
+ - **Certifying a result with no answer key?** Read [verification-strategies.md](./verification-strategies.md) for the ten-member family and the blind two-arm protocol.
319
+ - **Reading a verdict someone else produced?** Read [verdicts.md](./verdicts.md) for what `certification` carries and what an absent one means.
320
+ - **Grading a finding by executing its repair?** Read [trace-repair-grader.md](./trace-repair-grader.md), and [trajectory-replay.md](./trajectory-replay.md) for re-executing a recorded failure.
321
+ - **Wondering why this package exists at all?** Read [charter.md](./charter.md) for the four end-states it is built against.
@@ -34,7 +34,7 @@ Evaluation measures whether the result met its requirements, whether another att
34
34
  | “I need train/dev/test/holdout examples.” | `Dataset` plus feedback trajectory conversion | Stable splits and contamination control. |
35
35
  | “Which optimization procedure wins?” | `compareOptimizationMethods` | Runs complete methods on shared train and selection cases, then compares them on separate final cases. |
36
36
  | “Improve a multi-turn agent with candidates from my runtime.” | `runImprovementLoop` | Evaluates caller-generated candidates and applies a separate release rule. |
37
- | “Improve prompts, then code if prompts plateau.” | `runPromptEvolution`, composite mutator, code mutator | Bounded evolution with telemetry and lineage. |
37
+ | “Improve prompts, then code if prompts plateau.” | `gepaOptimizationMethod` or `externalTextOptimizationMethod` for the prompt; agent-runtime's worktree path for the code | Text search stays here; executable code changes belong to the runtime. |
38
38
  | “Find why a regression happened.” | bisector, traces, run records | Narrows changes and preserves evidence. |
39
39
  | “Expose evals to another language.” | Wire protocol and Python client | HTTP/RPC boundary for non-TypeScript apps. |
40
40
 
@@ -241,6 +241,32 @@ A definition a strategy cannot compile fails loud with `AnalystExpressivenessErr
241
241
  `AnalystContext.probe` (`ExecutionProbe`) is the optional live-execution port: a runtime that owns a sandbox or checkout fills it so an analyst can run a bounded command against the run's produced state and read a typed outcome.
242
242
  This package defines only the port; an absent probe means the analyst works from recorded evidence.
243
243
 
244
+ ## Exact Runs
245
+
246
+ Call `AnalystRegistry.runExact()` when the caller, and not the registry, must own every execution choice.
247
+ The runnable minimum is [`examples/custom-trace-analyst`](../examples/custom-trace-analyst/).
248
+
249
+ The `analystIds` array is the execution order, and exact runs are serial.
250
+ A caller that needs recursive or concurrent scheduling composes exact runs through its own runtime rather than adding a second scheduler here.
251
+ Every option must be present, and `null` disables a channel on purpose, so a missing budget can never be read as an unlimited one.
252
+
253
+ An analyst reaches an exact run only when it declares `executionConfig`: canonical JSON for every behavior knob that `version` does not already bind.
254
+ The receipt stores a digest of it, so two runs that behaved differently cannot look identical.
255
+ `defineCustomAnalyst()` returns an exact-capable analyst when that field is present, and the built-in analysts already declare it.
256
+
257
+ Every other live component admitted to an exact run — the cost ledger, a registry hook, a registry chat client — carries an `ExactExecutionComponentIdentity`: a non-secret `id`, a `version`, and a canonical `config` object.
258
+ `snapshotExactExecutionComponentIdentity()` reduces each one to an `ExactExecutionComponentSnapshot`, which keeps `id` and `version` and replaces `config` with `config_digest`.
259
+ That is how a receipt names what ran without ever storing a credential.
260
+
261
+ Three rules govern what a finished exact run may claim.
262
+
263
+ 1. Lifecycle hooks receive frozen snapshots. A hook observes the planned context; it cannot rewrite it.
264
+ 2. Persisted results store configuration digests, never raw configuration. The plan records the exact equal or weighted allocation for every routed analyst, and archival validates each summary against that same plan.
265
+ 3. Every receipt says whether it is `complete` or `failed`. A complete receipt must cover the whole plan. A failed receipt may cover only the prefix that ran.
266
+
267
+ Any failure after an exact run starts rejects with `ExactAnalystRunExecutionError`.
268
+ Its immutable failed receipt keeps the summaries, findings, usage, and cost that were already valid, so a late failure does not erase what was measured before it.
269
+
244
270
  ## Measure Analyst Quality
245
271
 
246
272
  Measure the analyst on labeled traces before using its findings for automated changes.
@@ -15,7 +15,7 @@ The pre-pass that decides which rows the arms run on is in [trace-repair-admissi
15
15
  | --- | --- | --- |
16
16
  | scaffold | mini-swe-agent | The corpus recorded it, so a continuation stays in the same distribution as the prefix. |
17
17
  | step budget | 20 model calls | Bounds a rollout without a wall-clock limit, which would end rollouts at different points. |
18
- | temperature | 0 | With a fixed seed, the same prefix draws the same continuation. |
18
+ | temperature | 0 | Removes the sampler as a source of variation the policy controls. It does not make a continuation repeat: measured against the z.ai seat, 19 of 20 replies to one identical prompt were distinct on `glm-5.3` and 8 of 20 on `glm-4.7`. A paired design must carry that variation as a threat to validity — see [trace-repair-gated-stop.md](./trace-repair-gated-stop.md). |
19
19
  | command timeout | 30 s | The recorded runs used the scaffold's own 30-second limit. A longer limit lets the continuation finish commands the recorded agent could not. |
20
20
  | network | `none` | A container with a network can install what the recorded run could not. |
21
21
  | format-error cap | 3 consecutive turns | Ends a rollout that has stopped producing actions. |
@@ -0,0 +1,119 @@
1
+ # What unconditional continuation rescues
2
+
3
+ TB-Repair's admission condition 3 asks whether a row is rescued by continuing from the recorded end state with no intervention.
4
+ Both milestone runs answered it under a control pinned to **zero model calls**.
5
+ A rollout that makes no model call executes no command, so the container the control graded held the same bytes the end-state check had already graded as failing: on the 32 rows measured here that control returned **0 passes in 96 rollouts**, the only answer it could return.
6
+
7
+ This is the same question asked with a budget.
8
+
9
+ Source: [`scripts/tb-repair-freelunch.ts`](../scripts/tb-repair-freelunch.ts).
10
+ The control contract is in [trace-repair-admission.md](./trace-repair-admission.md); the policy is in [trace-repair-continuation.md](./trace-repair-continuation.md).
11
+
12
+ ## The answer
13
+
14
+ **3 of 64 rollouts, 4.7 %.** Two of 32 rows were rescued at least once, 6.2 %.
15
+
16
+ The 64 rollouts are not 64 independent draws.
17
+ A seed-derivation defect (threat 8) made the second pass repeat the first pass's seed, and 14 of the 32 second-pass rollouts are byte-identical action repeats of their first-pass rollout.
18
+ Over the 50 distinct rollouts the rate is **3 of 50, 6.0 %**.
19
+ All three rescues come from pairs whose two rollouts differ, so no rescue is a repeat counted twice.
20
+
21
+ | interval, 95 % | rescue rate |
22
+ | --- | --- |
23
+ | task-clustered bootstrap (3 clusters, 10 000 resamples, seed 7) | 0.0 % – 10.0 % |
24
+ | row-clustered bootstrap (32 clusters) | 0.0 % – 12.5 % |
25
+ | exact Clopper-Pearson on rollouts | 1.0 % – 13.1 % |
26
+
27
+ Three clusters cannot carry a stable interval; the row-clustered and exact intervals are reported beside it for that reason.
28
+
29
+ **The rate is not zero, and it is not noise.** One row, `count-dataset-tokens__HL3ZzrX`, was rescued in **both** of its rollouts on a suite that returned the same verdict in all 16 certification replicates.
30
+
31
+ ## What a rescue looked like
32
+
33
+ `count-dataset-tokens__HL3ZzrX`, 13 steps, submitted. The recorded agent had computed the right answer and written it in a form the grader rejected. The continuation recomputed the count offline, then found and removed the formatting defect:
34
+
35
+ ```
36
+ 10. cat /app/answer.txt
37
+ 11. printf '%s' "$(cat /app/answer.txt)" > /app/answer.txt && cat /app/answer.txt | xxd | head -5
38
+ 12. printf '%s' "$(cat /app/answer.txt)" > /app/answer.txt && od -c /app/answer.txt
39
+ 13. echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT
40
+ ```
41
+
42
+ `password-recovery__oDL7kv9`, 19 steps, submitted, rescued in 1 of 2 rollouts. This one is not a formatting fix — it is the task being solved: the continuation searched the disk image, hexdumped it, carved an embedded archive, and wrote the recovered password.
43
+
44
+ ## By task, and the asymmetry that explains it
45
+
46
+ | task | rows | rollouts | passes | rate | recorded steps that used the network |
47
+ | --- | --- | --- | --- | --- | --- |
48
+ | `count-dataset-tokens` | 10 | 20 | 2 | 10.0 % | 24.3 % |
49
+ | `password-recovery` | 6 | 12 | 1 | 8.3 % | 0 % |
50
+ | `sanitize-git-repo` | 16 | 32 | 0 | 0 % | 0 % |
51
+
52
+ The pinned policy disables the network the recorded agents had. Measured on the continuations' own actions, `count-dataset-tokens` rollouts spent **30.2 % of their steps reaching for a network that was not there** (114 of 377), against 0.8 % for `sanitize-git-repo` and 0 % for `password-recovery`.
53
+ `password-recovery` is the clean sub-population: its recordings never used the network, so its 8.3 % is unconfounded by the policy.
54
+
55
+ ## By exit status
56
+
57
+ | exit status | rollouts | passes | rate |
58
+ | --- | --- | --- | --- |
59
+ | `submitted` | 15 | 3 | 20.0 % |
60
+ | `step-budget-exhausted` | 46 | 0 | 0 % |
61
+ | `model-error` | 3 | 0 | 0 % |
62
+
63
+ Every rescue came from a rollout that decided it was finished. No rollout that burned all 20 steps ever passed.
64
+ The three `model-error` rollouts were ended by provider 503s after four retries; they are recorded and graded, never dropped, so the rate is a lower bound by at most those three.
65
+
66
+ ## What it cost
67
+
68
+ 64 rollouts, priced from the token counts the provider reported for this run's own calls at the router's published `glm-5.2` rate.
69
+
70
+ | quantity | min | median | p90 | max | sum |
71
+ | --- | --- | --- | --- | --- | --- |
72
+ | prompt tokens | 15 146 | 170 939 | 260 088 | 321 288 | 10 734 764 |
73
+ | completion tokens | 151 | 6 663 | 14 961 | 23 866 | 546 346 |
74
+ | continuation steps | 1 | 20 | 20 | 20 | 1 143 |
75
+ | cost, USD | 0.0138 | 0.1506 | 0.2281 | 0.2821 | **9.4806** |
76
+
77
+ **$0.1481 per rollout, $0.2963 per row at n = 2.** A paired study can budget-match against those two numbers directly.
78
+
79
+ Cost is attributed per call, not from the router's account counter: `GET /v1/credits` covers the whole key, and 18 other processes on this host were calling it during the run. The counter's delta over the two passes was $8.60, which is neither this run's cost nor an upper bound on it once other traffic is in both directions.
80
+
81
+ ## What the number means
82
+
83
+ A **high** rate would have meant unconditional continuation captures most of the available headroom, killing the gated-stop thesis. It did not.
84
+
85
+ A **low but non-zero** rate means the headroom exists and a gate could claim it — which licenses a paired study without proving it. That is where this lands, with two qualifications that matter more than the point estimate:
86
+
87
+ - The rescues are **not free**. Each cost $0.148 and up to 20 model calls. A gate that fires on every failed row pays that on every row.
88
+ - **Condition 3 is now calibrated.** It has a real screen rate to compare against: 4.7 % of rollouts and 6.2 % of rows, against the 0 % a zero-step control was structurally obliged to report.
89
+
90
+ ## Threats
91
+
92
+ 1. **Reconstructed assistant messages.** The corpus stores each recorded assistant turn as an elided placeholder, so the continuation inherits the bash block without the reasoning that produced it.
93
+ 2. **Network asymmetry.** The recorded agents had internet; the pinned policy does not. On `count-dataset-tokens` that consumed 30.2 % of continuation steps, so the overall rate is a lower bound for a networked continuation.
94
+ 3. **Three clusters.** A task-clustered interval over three tasks is coarse by construction.
95
+ 4. **One model, `glm-5.2` at temperature 0.** Not a statement about continuation in general.
96
+ 5. **n = 2.** Within-row variance is measured on two draws. One row rescued twice, one rescued once of two.
97
+ 6. **Wall-time is not clean.** For part of the first pass the measurement seat was not held, because a killed sibling wrapper's exit trap removed a lock it no longer owned. Verdicts and token counts are unaffected; latency and throughput are not clean.
98
+ 7. **Snapshot boundary.** State moves between container generations as a committed image, so a process the recording left running does not survive.
99
+ 8. **The two passes shared one seed.** Each pass called `runContinuation` with `rollouts: 1`, and the seed derived from that call's internal index, which is always 0. Shifting `ROLLOUT_BASE` therefore never reached the seed, and both passes sent the provider the index-0 seed on an identical prompt. Measured on the records: 14 of 32 row pairs are byte-identical action sequences, and 31 of 32 share their first action. The runner now forwards the pass's base index through `rolloutBase`, so a future pass draws its own seed.
100
+
101
+ ## Reproducing
102
+
103
+ ```bash
104
+ # containers only, no model calls, no seat needed
105
+ npx tsx scripts/tb-repair-freelunch.ts --stop-points-only
106
+
107
+ # one uniform pass over every row, under the measurement seat
108
+ TBR_FL_ROLLOUTS=1 TBR_FL_ROLLOUT_BASE=0 TBR_FL_OUT=freelunch-pass1.json \
109
+ npx tsx scripts/tb-repair-freelunch.ts
110
+ ```
111
+
112
+ `--plan` prints the denominator chain and the selected rows without opening a container.
113
+ The pre-registration, its amendments, and the raw per-rollout records are in `~/bench-cache/freelunch-20260810/`.
114
+
115
+ ## The raw records stay local
116
+
117
+ `freelunch.json` holds every continuation's actions and observations, which are container state. GitHub push protection refused an earlier commit of it because a container carried a **Hugging Face user access token** in its cached credentials, at `free-lunch-n2.json:1883`.
118
+
119
+ Raw per-rollout records are therefore kept out of the repository. What is committed is the runner, this report, and the numbers derived from the records. Anyone re-running the campaign should treat the artifact directory as credential-bearing.
@@ -0,0 +1,114 @@
1
+ # Gated stop against blind continuation
2
+
3
+ The study asks one question.
4
+ At one matched total token budget, does an agent that may stop only after an executable held-out check passes finish more rows than the same agent spending the identical budget on unconditional continuation?
5
+
6
+ The mechanism under test is budget allocation, not detection.
7
+ A row that clears the check early returns its unspent budget to a pool.
8
+ The pool pays for extra steps on rows that have not cleared.
9
+ Detection quality is not the claim: on this corpus the recorded done-signal fires on 51.69 % of failed runs and 31.14 % of successes, which is 62.5 % precision as a success predictor.
10
+
11
+ Design and runner: [`benchmarks/trace-repair/gated-stop-ab/design.json`](../benchmarks/trace-repair/gated-stop-ab/design.json) and [`scripts/tb-gated-stop-ab.ts`](../scripts/tb-gated-stop-ab.ts).
12
+ The registration primitives are described in [experiment.md](./experiment.md).
13
+
14
+ ## The two arms
15
+
16
+ | arm | role | stop rule | graded at |
17
+ | --- | --- | --- | --- |
18
+ | `blind-continue` | control | spend the allotment unconditionally | best intermediate state |
19
+ | `gated-continue` | treatment | stop when the held-out check passes | best intermediate state |
20
+
21
+ Both arms replay the same recorded prefix, run the same scaffold, and are graded by the same injected suite after every step.
22
+ The check that gates the treatment arm is the check that scores both arms.
23
+
24
+ That symmetry forces a control the report must carry.
25
+ The gate is the outcome, so the treatment arm cannot lose a success it already reached, while the control arm can regress out of one.
26
+ The control arm is therefore graded twice, at its final state and at its best intermediate state, and both contrasts are reported.
27
+ When the two disagree, the harsher contrast is the headline.
28
+
29
+ ## Choosing the draw
30
+
31
+ The admitted set is the ceiling, not the draw.
32
+ `settlingDraw` returns the smallest draw that clears the registered power target at the settling effect of 0.10.
33
+ Spending more rows than the design needs is as undisciplined as spending fewer.
34
+
35
+ The search runs at 15 000 trials and the registered gate runs at 3 000.
36
+ Near the floor the two estimates straddle the target, because the standard error at 3 000 trials is about 0.007 and adjacent draws differ by less.
37
+ A draw must clear the target under both before the search accepts it.
38
+
39
+ ## The identity gate runs before the spend
40
+
41
+ `confirm` evaluates the registered `servedModel` gate before it grades a row.
42
+ The gate compares the pinned model id against the id the seat reports.
43
+ Its registered action on failure is `abort`.
44
+
45
+ A contrast measured on a substituted model belongs to an experiment nobody registered.
46
+ The gate therefore ends the run at zero spend and writes `confirm-refusal.json`, which records the pinned id, the served id, the rows graded, and the dollars spent.
47
+ A refusal is a verdict object, not prose beside one.
48
+
49
+ ## Measured seat behaviour
50
+
51
+ These facts were measured against the z.ai coding seat and they bound what the study can claim.
52
+
53
+ | fact | measurement | n |
54
+ | --- | --- | --- |
55
+ | `glm-5.2`, `glm-5.1` and `glm-5` are all answered by `glm-5.3` | served id differs from requested id | 3 ids |
56
+ | `glm-4.7` and `glm-4.6` are answered by themselves | served id equals requested id | 2 ids |
57
+ | `glm-5.3` is not deterministic at temperature 0 | 19 of 20 replies distinct | 20 |
58
+ | `glm-4.7` is not deterministic at temperature 0 | 8 of 20 replies distinct | 20 |
59
+ | the scaffold's first turn draws more than one action block | 6 of 20 replies, both models | 20 per model |
60
+ | the same rate inside a run, behind a replayed prefix | 4 of 24 steps | 24 |
61
+
62
+ Temperature 0 does not give a repeated continuation on this provider.
63
+ A paired design that assumes it must treat run-to-run variation as a threat to validity and report it.
64
+
65
+ ## Label quality on `qemu-startup`
66
+
67
+ One row, `qemu-startup__PnXK6EH::ord0`, is recorded at reward 0 and passes its own held-out suite on the replayed end state.
68
+ The end-state screen measures exactly that condition, so the registered funnel excludes the row at the `recorded-end-state-fails-its-own-suite` stage.
69
+ The row is absent from the draw.
70
+
71
+ The disagreement rate is 1 of 18 screened rows on `qemu-startup`, against 0 of 285 on every other task.
72
+ The concentration is the finding, not the single row.
73
+ `qemu-startup` rows still enter the study, because the screen tests each row against the condition that would disqualify it and the remaining rows pass that test.
74
+ A task whose labels disagree with its own oracle at 5.6 % cannot carry a study on its own, and this design does not ask it to: it contributes 8 of 151 rows inside a task-clustered bootstrap that resamples whole tasks.
75
+
76
+ ## Running the confirmatory arm
77
+
78
+ ```bash
79
+ node --import tsx scripts/tb-gated-stop-ab.ts design # re-seal; prints both digests
80
+ node --import tsx scripts/tb-gated-stop-ab.ts confirm # identity gate, then both arms
81
+ ```
82
+
83
+ `design` writes to the work directory. Copy the result over `benchmarks/trace-repair/gated-stop-ab/design.json` to move the checked-in seal forward; the next `design` reads that file to report the digest it replaces.
84
+
85
+ `confirm` runs the control arm at the seat's concurrency limit and the treatment arm one row at a time.
86
+ The treatment arm is serial because its pool is sequential state: a row draws the budget that earlier rows returned, so processing order decides allocation.
87
+ Running those rows concurrently would change the allocation the seal registered, which makes the scheduling part of the experiment rather than a detail of it.
88
+
89
+ The control arm therefore finishes in about a third of the wall time of the treatment arm on the same draw.
90
+
91
+ Every finished row is written to `confirm-runs.json` before the next row starts.
92
+ A re-invocation reads that file, skips rows already held, and rebuilds the pool from the entitlement and spend of each held treatment row.
93
+ An interrupted run costs the row in flight and nothing before it.
94
+
95
+ ## Reading the result
96
+
97
+ `confirm-report.json` carries the digest that produced it, both contrasts, the matched-budget verdict and the served model ids observed across every step.
98
+
99
+ The matched-budget rule is a refusal object, not an assertion.
100
+ The treatment arm can only spend returned budget on rows that come after the row that returned it, so budget freed by the last rows has nowhere to go.
101
+ When that trailing shortfall pushes the arms more than 5 % apart, the registered decision table returns `contrast-refused-unmatched-budget` and the contrast is not read.
102
+
103
+ ## The measured result
104
+
105
+ The confirmatory run completed both arms over the sealed draw: 302 rows graded, 151 per arm.
106
+
107
+ The corrected primary contrast, best intermediate state in both arms, is **+0.0596** with a 95 % cluster-bootstrap interval of **[-0.0061, +0.1210]**.
108
+ The interval includes zero, so the registered decision table reads `no-effect-resolved-at-this-n`.
109
+ The study does not certify a gated-stop advantage at this draw.
110
+
111
+ Two degradations bound what this run can claim.
112
+
113
+ - 15 gated-arm rows were degraded by provider 429 rate-limit responses, and the treatment arm carries all of them; the contrast above is the corrected value after the tail audit accounted for them.
114
+ - The registered estimand pipeline as first shipped crashed at report time, because the evidence rows carried a boolean where the paired-mean-diff estimand requires a number; the contrast above was recomputed from the persisted `confirm-runs.json` after the defect was fixed. The runner now writes the verdict as 1 or 0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-eval",
3
- "version": "0.145.13",
3
+ "version": "0.145.15",
4
4
  "description": "Evaluate and improve AI agents from runs, traces, judges, and feedback. Compare candidates, cluster failures, measure lift, and gate releases.",
5
5
  "homepage": "https://github.com/tangle-network/agent-eval#readme",
6
6
  "repository": {
@@ -180,8 +180,8 @@
180
180
  "dependencies": {
181
181
  "@asteasolutions/zod-to-openapi": "^9.1.0",
182
182
  "@hono/node-server": "^2.0.12",
183
- "@tangle-network/agent-core": "0.8.0",
184
- "@tangle-network/agent-interface": "0.52.0",
183
+ "@tangle-network/agent-core": "0.9.0",
184
+ "@tangle-network/agent-interface": "0.53.0",
185
185
  "@tangle-network/agent-trace-contract": "^1.0.2",
186
186
  "hono": "^4.12.32",
187
187
  "linear-sum-assignment": "1.0.9",