@tangle-network/agent-eval 0.123.1 → 0.123.3

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +191 -157
  3. package/dist/analyst/index.d.ts +27 -4
  4. package/dist/analyst/index.js +6 -6
  5. package/dist/authenticity/index.js +3 -2
  6. package/dist/authenticity/index.js.map +1 -1
  7. package/dist/benchmarks/index.d.ts +12 -4
  8. package/dist/benchmarks/index.js +9 -9
  9. package/dist/campaign/index.d.ts +270 -254
  10. package/dist/campaign/index.js +22 -18
  11. package/dist/{chunk-GS3FJGUF.js → chunk-22VO7T2I.js} +805 -366
  12. package/dist/chunk-22VO7T2I.js.map +1 -0
  13. package/dist/{chunk-LBAHQOBI.js → chunk-4KO6D2BO.js} +38 -12
  14. package/dist/chunk-4KO6D2BO.js.map +1 -0
  15. package/dist/{chunk-3FCG7FBV.js → chunk-D5JZ7UDZ.js} +2 -2
  16. package/dist/{chunk-XJYR7XFV.js → chunk-GC4ATIKK.js} +1 -1
  17. package/dist/chunk-GC4ATIKK.js.map +1 -0
  18. package/dist/{chunk-BGVTIE2C.js → chunk-GRCDRKII.js} +13 -2
  19. package/dist/chunk-GRCDRKII.js.map +1 -0
  20. package/dist/{chunk-FHFTYX2Q.js → chunk-JKDNAOF5.js} +2 -2
  21. package/dist/{chunk-S3UZOQ5Y.js → chunk-LOW3U7JZ.js} +2 -2
  22. package/dist/{chunk-FC5NDO3E.js → chunk-MHPEGJHC.js} +4 -4
  23. package/dist/{chunk-NJC7U437.js → chunk-MK3Q6GCL.js} +33 -18
  24. package/dist/chunk-MK3Q6GCL.js.map +1 -0
  25. package/dist/{chunk-U5CHZ5M3.js → chunk-N7ZS6PEF.js} +4 -4
  26. package/dist/{chunk-A5S77LSE.js → chunk-QFQZ3U3X.js} +2 -2
  27. package/dist/chunk-QFQZ3U3X.js.map +1 -0
  28. package/dist/{chunk-G2GPNLSX.js → chunk-QNVBALEX.js} +4 -4
  29. package/dist/{chunk-OYZAPX5G.js → chunk-R226UZOI.js} +2 -2
  30. package/dist/{chunk-6WX7CBAR.js → chunk-SJT4OBVL.js} +20 -9
  31. package/dist/chunk-SJT4OBVL.js.map +1 -0
  32. package/dist/{chunk-HZJF4IUO.js → chunk-SUN7QLPB.js} +6 -6
  33. package/dist/{chunk-HZJF4IUO.js.map → chunk-SUN7QLPB.js.map} +1 -1
  34. package/dist/{chunk-VJ7T5WIO.js → chunk-WP3GEXKI.js} +3 -3
  35. package/dist/cli.js +101 -11
  36. package/dist/cli.js.map +1 -1
  37. package/dist/contract/index.d.ts +115 -8
  38. package/dist/contract/index.js +11 -9
  39. package/dist/contract/index.js.map +1 -1
  40. package/dist/control.js +3 -3
  41. package/dist/fuzz.d.ts +8 -1
  42. package/dist/fuzz.js +7 -6
  43. package/dist/fuzz.js.map +1 -1
  44. package/dist/hosted/index.d.ts +8 -2
  45. package/dist/index.d.ts +36 -10
  46. package/dist/index.js +18 -16
  47. package/dist/index.js.map +1 -1
  48. package/dist/multishot/index.d.ts +2 -2
  49. package/dist/openapi.json +1 -1
  50. package/dist/rl.d.ts +24 -3
  51. package/dist/rl.js +6 -4
  52. package/dist/rl.js.map +1 -1
  53. package/dist/{run-campaign-ZKR5MQMQ.js → run-campaign-I3JXKVAK.js} +3 -3
  54. package/dist/storyboard/index.js +1 -1
  55. package/dist/storyboard/index.js.map +1 -1
  56. package/dist/traces.d.ts +2 -2
  57. package/dist/traces.js +3 -3
  58. package/dist/wire/index.d.ts +77 -5
  59. package/dist/wire/index.js +3 -3
  60. package/docs/adapters-observability.md +6 -6
  61. package/docs/building-doctrine.md +5 -5
  62. package/docs/campaign-proposers.md +142 -131
  63. package/docs/concepts.md +49 -53
  64. package/docs/customer-journeys.md +80 -155
  65. package/docs/design/loop-taxonomy.md +27 -29
  66. package/docs/design.md +69 -0
  67. package/docs/distributed-driver.md +14 -14
  68. package/docs/eval-surface-map.md +11 -11
  69. package/docs/hosted-ingest-spec.md +4 -4
  70. package/docs/insight-report.md +32 -27
  71. package/docs/multi-shot-optimization.md +41 -49
  72. package/docs/research-report-methodology.md +9 -9
  73. package/docs/trace-analysis.md +2 -2
  74. package/docs/wire-protocol.md +16 -16
  75. package/package.json +2 -1
  76. package/dist/chunk-6WX7CBAR.js.map +0 -1
  77. package/dist/chunk-A5S77LSE.js.map +0 -1
  78. package/dist/chunk-BGVTIE2C.js.map +0 -1
  79. package/dist/chunk-GS3FJGUF.js.map +0 -1
  80. package/dist/chunk-LBAHQOBI.js.map +0 -1
  81. package/dist/chunk-NJC7U437.js.map +0 -1
  82. package/dist/chunk-XJYR7XFV.js.map +0 -1
  83. package/docs/auto-research-loop-end-to-end.md +0 -186
  84. package/docs/improvement-glossary.md +0 -204
  85. package/docs/self-improvement-map.md +0 -115
  86. /package/dist/{chunk-3FCG7FBV.js.map → chunk-D5JZ7UDZ.js.map} +0 -0
  87. /package/dist/{chunk-FHFTYX2Q.js.map → chunk-JKDNAOF5.js.map} +0 -0
  88. /package/dist/{chunk-S3UZOQ5Y.js.map → chunk-LOW3U7JZ.js.map} +0 -0
  89. /package/dist/{chunk-FC5NDO3E.js.map → chunk-MHPEGJHC.js.map} +0 -0
  90. /package/dist/{chunk-U5CHZ5M3.js.map → chunk-N7ZS6PEF.js.map} +0 -0
  91. /package/dist/{chunk-G2GPNLSX.js.map → chunk-QNVBALEX.js.map} +0 -0
  92. /package/dist/{chunk-OYZAPX5G.js.map → chunk-R226UZOI.js.map} +0 -0
  93. /package/dist/{chunk-VJ7T5WIO.js.map → chunk-WP3GEXKI.js.map} +0 -0
  94. /package/dist/{run-campaign-ZKR5MQMQ.js.map → run-campaign-I3JXKVAK.js.map} +0 -0
@@ -1,210 +1,135 @@
1
- # Customer journeys
1
+ # Adoption Paths
2
2
 
3
- Three end-to-end journeys covering the surface of `@tangle-network/agent-eval`. Each one is a runnable example under `examples/` — clone the repo and `pnpm tsx examples/<journey>/index.ts` to see the actual output.
3
+ Choose the path that matches the data and code you already have.
4
4
 
5
- The three journeys map to three customer-maturity stages:
5
+ | Starting point | API | Result |
6
+ |---|---|---|
7
+ | Completed OpenTelemetry spans | `fromOtelSpans()` and `analyzeRuns()` | Failure, score, token, and cost summaries |
8
+ | Human ratings | `fromFeedbackTable()` and `analyzeRuns()` | Reviewer agreement, largest disagreements, and score distributions |
9
+ | A runnable agent, scenarios, and a judge | `defineAgentEval()` | Repeatable evaluation and optional prompt improvement |
6
10
 
7
- 1. **Logs but no eval discipline** [Production traces journey](#1-production-traces-journey-customer-otel-traces)
8
- 2. **Ratings but no closed loop** [Feedback corpus journey](#2-feedback-corpus-journey-customer-feedback-loop)
9
- 3. **Scenarios, judge, agent — full closed loop** → [Closed-loop journey](#3-closed-loop-journey-selfimprove-quickstart)
11
+ All three paths return plain objects and run in your process.
12
+ They call a remote service only when you pass a model client, exporter, or hosted endpoint.
10
13
 
11
- Each section: what the customer has, what they want, the code, what the report looks like.
14
+ ## 1. Analyze Existing Traces
12
15
 
13
- ---
14
-
15
- ## 1. Production traces journey — `customer-otel-traces`
16
-
17
- **The customer:** an agentic GTM-as-a-service company. Multiple agent steps in prod (social media posting, image generation, translation). OTel observability piped to their collector. Doesn't run formal evals. CTO hand-rolled their tracing.
18
-
19
- **The frustration:** "Which step is unreliable? What's our cost-quality profile? Where do we fix next?" They have the data; they don't have the answer.
20
-
21
- **What they need from agent-eval:** day-1 analysis of their existing logs. No scenarios, no judges, no closed loop. Just turn the trace stream into a decision packet.
22
-
23
- ### The code
16
+ Use this path when the agent already emits OpenTelemetry spans and you do not want to run it again.
24
17
 
25
18
  ```ts
26
19
  import { analyzeRuns, fromOtelSpans } from '@tangle-network/agent-eval/contract'
27
20
 
28
- const runs = fromOtelSpans({ spans: yourOtelStream })
21
+ const runs = fromOtelSpans({ spans: yourOtelSpans })
29
22
  const report = await analyzeRuns({ runs })
30
23
 
31
- // report.failureClusters → root causes
32
- // report.costQuality.pareto → cost-vs-quality scatter
33
- // report.composite → distribution
34
- // report.recommendations → top-3 actions
24
+ console.log(report.composite)
25
+ console.log(report.costQuality)
26
+ console.log(report.recommendations)
35
27
  ```
36
28
 
37
- ### What the report shows
29
+ `fromOtelSpans()` groups spans by run ID and reads recorded scores, failures, model IDs, token counts, and costs.
30
+ It does not infer values that are missing from the spans.
38
31
 
39
- ```
40
- Runs analyzed: 40
41
- Composite mean: 0.721 (p50: 0.717, p95: 0.925, stddev: 0.210)
42
- Cost mean: $0.103 (p95: $0.131)
43
-
44
- ── Failures ──
45
- 6 runs with status=ERROR or failureMode set:
46
- tool.search (3x)
47
- agent.turn (3x)
32
+ Add model-based failure clustering only when you need it:
48
33
 
49
- ── Cost-quality Pareto ──
50
- 1 candidate(s) plotted; 1 on the frontier
51
- otel-default: cost=$0.103 quality=0.721 (frontier)
34
+ ```ts
35
+ const report = await analyzeRuns({
36
+ runs,
37
+ analyst,
38
+ })
52
39
 
53
- ── Recommendations ──
54
- [medium] expand-corpus — Mean composite 0.721 has room
40
+ console.log(report.failureClusters)
55
41
  ```
56
42
 
57
- ### Next steps for this customer
58
-
59
- 1. Wire an `AnalystRegistry` to cluster the 6 failures by root cause via LLM analysis.
60
- 2. Add `outcomeSignal` once they have downstream conversion / engagement / post-engagement data, and the report fits a reward model showing whether their score predicts the customer outcome.
61
- 3. Once they identify a step worth optimizing (translation, say), graduate to journey #3 — wrap that step as an `agent(surface, scenario)` and call `defineAgentEval()`.
62
-
63
- **Runnable:** [`examples/customer-otel-traces/`](../examples/customer-otel-traces/)
64
-
65
- ---
66
-
67
- ## 2. Feedback corpus journey — `customer-feedback-loop`
43
+ Runnable example: [`examples/customer-otel-traces`](../examples/customer-otel-traces/)
68
44
 
69
- **The customer:** a research-validation team. A GitHub Action fires `claude -p` against the next claim, writes the research output to Obsidian. Three reviewers (Alice, Bob, Carol) tag results `#approved` or `#rejected`. Outputs feed a knowledge base. Knowledge feeds content. Content feeds engagement. The founder wants more engagement faster.
45
+ ## 2. Analyze Human Ratings
70
46
 
71
- **The frustration:** "We disagree on what's good. We don't know if our 'good' actually drives engagement. Reviewing every claim is slow."
72
-
73
- **What they need from agent-eval:** turn the approve/reject corpus into actionable signal:
74
- - Where do reviewers disagree? (triage list)
75
- - Can we synthesize each reviewer's taste into an LLM judge? (auto-grade)
76
- - Does the taste actually predict downstream engagement? (close the loop)
77
-
78
- ### The code
47
+ Use this path when multiple people score the same outputs in a database, spreadsheet, or review tool.
79
48
 
80
49
  ```ts
81
50
  import { analyzeRuns, fromFeedbackTable } from '@tangle-network/agent-eval/contract'
82
51
 
83
- // 1. Parse Obsidian #approved / #rejected tags into a flat table:
84
- const ratings = parseObsidianVault('./research-vault')
85
- // [{ runId: 'claim-1', rater: 'alice', rating: true }, ...]
52
+ const ratings = [
53
+ { runId: 'answer-1', rater: 'alice', rating: true },
54
+ { runId: 'answer-1', rater: 'bob', rating: false },
55
+ { runId: 'answer-2', rater: 'alice', rating: true },
56
+ { runId: 'answer-2', rater: 'bob', rating: true },
57
+ ]
86
58
 
87
- // 2. Pipe through the adapter:
88
59
  const { runs, raterScores } = fromFeedbackTable({ ratings })
60
+ const report = await analyzeRuns({ runs, raterScores })
89
61
 
90
- // 3. Analyze:
91
- const report = await analyzeRuns({
92
- runs,
93
- raterScores,
94
- // Optional: close the loop with engagement data once you have it.
95
- outcomeSignal: { metric: 'engagement_rate', valueByRunId: enrichedFromProd },
96
- })
97
-
98
- // report.interRater.disagreementCases → top 20 claims worth a meeting
99
- // report.outcomeCorrelation → does team taste predict engagement?
100
- // report.recommendations → action list
101
- ```
102
-
103
- ### What the report shows
104
-
62
+ console.log(report.interRater?.kappa)
63
+ console.log(report.interRater?.icc)
64
+ console.log(report.interRater?.disagreementCases)
105
65
  ```
106
- Runs analyzed: 30
107
- Composite mean: 0.756 (approve rate ~76%)
108
-
109
- ── Inter-rater agreement ──
110
- Raters: 3 (alice, bob, carol)
111
- Jointly rated runs: 30
112
- Pairwise pearson κ:
113
- alice::bob 0.53
114
- alice::carol 0.55
115
- bob::carol 0.21
116
- Mean κ: 0.43
117
-
118
- ── Top 5 disagreement cases ──
119
- claim-1 range=1.00 ratings: alice=0, bob=0, carol=1
120
- claim-7 range=1.00 ratings: alice=0, bob=1, carol=0
121
- ...
122
-
123
- ── Recommendations ──
124
- [high] recalibrate — Inter-rater agreement κ=0.43 is below 0.5
125
- Raters disagree on what 'good' looks like. Refine the rubric or triage the disagreement cases.
126
- ```
127
-
128
- ### Next steps for this customer
129
-
130
- 1. **Triage meeting on the disagreement cases.** Mean κ=0.43 means the rubric is ambiguous; clarify it on the cases that split.
131
- 2. **Calibrate one LLM judge per reviewer.** Each reviewer's history is the gold signal — substrate primitive `calibrateJudge` against `raterScores` filtered to that reviewer.
132
- 3. **Add engagement as `outcomeSignal`** once the content downstream is instrumented. The `outcomeCorrelation` section tells the team whether their taste predicts the founder's token-max goal — and if not, the linear reward model says how to retarget.
133
- 4. **Graduate to journey #3** — wrap the research-generation Claude-P call as an `agent(surface, scenario)`, use the calibrated judges, run `evalKit.improve()` nightly. Open a PR against the GitHub Action when the holdout approval rate beats baseline.
134
-
135
- **Runnable:** [`examples/customer-feedback-loop/`](../examples/customer-feedback-loop/)
136
66
 
137
- ---
67
+ Weighted kappa and ICC measure absolute agreement.
68
+ Pearson and Spearman measure correlation and are reported separately because reviewers can correlate while using different score levels.
138
69
 
139
- ## 3. Closed-loop journey `selfimprove-quickstart`
70
+ Review the largest disagreements before using those labels to calibrate a model judge.
71
+ Test the judge on human ratings that were not used during calibration.
140
72
 
141
- **The customer:** a team with a scenario corpus, a judge, and an agent. Wants to improve the prompt under statistical confidence — propose better candidates, gate on holdout lift, ship the winner.
73
+ Runnable example: [`examples/customer-feedback-loop`](../examples/customer-feedback-loop/)
142
74
 
143
- **The frustration:** "We can run an A/B by hand but we don't know if the improvement is real. We don't have time to run paired bootstrap by hand. We want a function that decides."
75
+ ## 3. Evaluate Or Improve A Runnable Agent
144
76
 
145
- **What they need from agent-eval:** one reusable eval definition propose, score, gate, ship — with the full rigor packet on the way out.
146
-
147
- ### The code
77
+ Use this path when you can call the agent for a scenario and score the returned artifact.
148
78
 
149
79
  ```ts
150
80
  import { defineAgentEval } from '@tangle-network/agent-eval/contract'
151
81
 
152
82
  const evalKit = defineAgentEval({
153
83
  scenarios,
154
- agent: async (surface, scenario) =>
155
- await myAgent.run({ systemPrompt: (surface as { systemPrompt: string }).systemPrompt, scenario }),
84
+ agent: async (prompt, scenario) => yourAgent.run({ prompt: String(prompt), scenario }),
156
85
  judge: {
157
- name: 'rubric',
158
- dimensions: [{ key: 'clarity', weight: 1 }, { key: 'concision', weight: 1 }],
159
- score: async ({ artifact }) => myJudgeFn(artifact),
86
+ name: 'task-quality',
87
+ dimensions: [
88
+ { key: 'correct', description: 'The answer is correct' },
89
+ { key: 'complete', description: 'The answer covers the whole request' },
90
+ ],
91
+ score: ({ artifact, scenario }) => scoreArtifact(artifact, scenario),
160
92
  },
161
- baselineSurface: { kind: 'prompt', systemPrompt: 'You write marketing copy...' },
162
- budget: { generations: 3, populationSize: 2 },
93
+ baselineSurface: currentPrompt,
163
94
  })
164
95
 
165
- const result = await evalKit.improve()
166
-
167
- result.gateDecision // 'ship' | 'hold' | ...
168
- result.insight // full decision packet
169
- ```
170
-
171
- ### What the report shows
172
-
96
+ const baseline = await evalKit.evaluate()
97
+ const candidate = await evalKit.evaluate({ surface: proposedPrompt })
173
98
  ```
174
- ═══ selfImprove() decision packet ═══
175
99
 
176
- Gate decision: ship
177
- Raw lift: +0.361
100
+ Call `.evaluate()` when you already have a candidate to compare.
101
+ Call `.improve()` when you want the library to generate and test candidates:
178
102
 
179
- ── Statistical lift (paired bootstrap) ──
180
- delta: +0.359
181
- CI95: [0.311, 0.408]
182
- pValue: 0.0013
183
- Cohen's d: 8.58
184
- MDE @ 80% power: 1.401
185
- required n at observed effect: 122
103
+ ```ts
104
+ const result = await evalKit.improve({
105
+ llm: {
106
+ baseUrl: process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1',
107
+ apiKey: process.env.OPENAI_API_KEY!,
108
+ model: 'gpt-4.1-mini',
109
+ },
110
+ })
186
111
 
187
- ── Recommendations ──
188
- [critical] ship — Ship — lift 0.359 (95% CI 0.311..0.408)
112
+ console.log(result.winner.surface)
113
+ console.log(result.lift)
114
+ console.log(result.gateDecision)
189
115
  ```
190
116
 
191
- ### Next steps for this customer
192
-
193
- 1. **Ship the winner.** Either accept `result.winner.surface` programmatically and roll it out, or pass `autoOnPromote: 'pr'` + a GitHub repo to have selfImprove open a PR for you.
194
- 2. **Wire `hostedTenant`** to ship the decision packet to a dashboard (the hosted Intelligence orchestrator, or your own implementation of the wire spec).
195
- 3. **Add `canaryScenarios`** to guard against the holdout leaking into the candidate prompt.
196
- 4. **Add `outcomeSignal`** in `analyzeRuns()` for any post-deploy reruns to verify the predicted lift actually shows up in real outcomes.
117
+ The default candidate generator calls the configured model.
118
+ The agent and judge may make their own calls depending on your implementation.
119
+ Set generation, population, concurrency, and dollar limits through `budget`.
197
120
 
198
- **Runnable:** [`examples/selfimprove-quickstart/`](../examples/selfimprove-quickstart/)
121
+ For production use, provide enough scenarios to keep candidate generation and final comparison disjoint.
122
+ Use an explicit held-back scenario set when the split must remain stable across runs.
199
123
 
200
- ---
124
+ Runnable example: [`examples/selfimprove-quickstart`](../examples/selfimprove-quickstart/)
201
125
 
202
- ## How the three journeys compose
126
+ ## Moving Between Paths
203
127
 
204
- Journey #1 + #2 + #3 are **maturity stages**, not exclusive products. A team typically:
128
+ The paths compose without changing data formats:
205
129
 
206
- 1. Starts with **#1** (analyze production logs) to find what's broken.
207
- 2. Adds **#2** (feedback corpus) once they have a sense of where to improve, to calibrate what "good" means.
208
- 3. Graduates to **#3** (closed loop) once they have scenarios + judges, to automate the improvement.
130
+ 1. Convert traces or ratings into `RunRecord[]` and use `analyzeRuns()` to find recurring failures.
131
+ 2. Turn those failures into representative scenarios and deterministic checks where possible.
132
+ 3. Use `defineAgentEval()` to compare changes against the same scenarios.
133
+ 4. Use `.improve()` only after the evaluation reliably separates known-good from known-bad behavior.
209
134
 
210
- Same substrate, same `InsightReport` shape, no rip-and-replace between stages. The data you collect in #1 informs the scenarios you derive in #2 which feed the loop in #3.
135
+ See [`concepts.md`](./concepts.md) for data types and [`examples/README.md`](../examples/README.md) for the full runnable index.
@@ -6,10 +6,8 @@ layers, and the layers were getting conflated. Every role below has exactly
6
6
  one meaning. Use these words and nothing else.
7
7
 
8
8
  Cross-links: [`concepts.md`](../concepts.md) (eval mental model),
9
- [`campaign-proposers.md`](../campaign-proposers.md) (proposer catalog),
10
- [`multi-shot-optimization.md`](../multi-shot-optimization.md) (GEPA), and
11
- [`auto-research-loop-end-to-end.md`](../auto-research-loop-end-to-end.md)
12
- (analyst / autoresearch).
9
+ [`campaign-proposers.md`](../campaign-proposers.md) (proposer catalog), and
10
+ [`multi-shot-optimization.md`](../multi-shot-optimization.md) (GEPA).
13
11
 
14
12
  ## Core Roles
15
13
 
@@ -18,12 +16,12 @@ Cross-links: [`concepts.md`](../concepts.md) (eval mental model),
18
16
  | **Execution driver** | The thing that decides or routes the next turn/action inside a sandbox or worker conversation. | Inner layer only |
19
17
  | **Surface proposer** | The thing that proposes the next prompt/config/code surface for the improvement loop to measure. | Outer layer only |
20
18
  | **Worker** | An agent harness instance (Claude Code, Codex, OpenCode, …) running inside a sandbox. Does the actual work; responds in chat. | Inner layer only |
21
- | **Sandbox** | A multi-harness VM. Hosts **1..N workers**, which can share a workspace. Not an agent the substrate an agent runs in. | Inner layer only |
19
+ | **Sandbox** | A multi-harness VM. Hosts **1..N workers**, which can share a workspace. Not an agent: the substrate an agent runs in. | Inner layer only |
22
20
  | **Measurement** | Runs the worker over a set of scenarios and judges the outputs into a scorecard with confidence intervals. This is `runCampaign`. | Outer layer |
23
21
 
24
22
  Two facts that trip people up:
25
23
 
26
- 1. **A sandbox is not a worker.** One sandbox can hold ten workers a driver
24
+ 1. **A sandbox is not a worker.** One sandbox can hold ten workers: a driver
27
25
  can coordinate CC + Codex + OpenCode siblings sharing one workspace, or a
28
26
  fleet spread across machines. `runLoop`'s placement encodes exactly this:
29
27
  `{ sibling, sandboxId }` = co-located workers; `{ fleet, fleetId,
@@ -38,21 +36,21 @@ Two facts that trip people up:
38
36
  There are two loops. The outer one improves the thing the inner one runs.
39
37
 
40
38
  ```
41
- runImprovementLoop OUTER loop improve the agent over time
39
+ runImprovementLoop OUTER loop: improve the agent over time
42
40
 
43
41
  ├─ PROPOSER = SurfaceProposer proposes a candidate SURFACE
44
42
  │ (evolutionary mutator | (the worker's system prompt / tools / config)
45
- │ reflective analyst) NOT a conversation turn
43
+ │ reflective analyst) : NOT a conversation turn
46
44
 
47
45
  └─ for each candidate surface:
48
46
 
49
- runCampaign a MEASUREMENT scores ONE surface
47
+ runCampaign a MEASUREMENT: scores ONE surface
50
48
 
51
49
  └─ for each scenario × rep:
52
50
 
53
- dispatch(scenario) THE SEAM topology-opaque, returns an artifact
51
+ dispatch(scenario) THE SEAM: topology-opaque, returns an artifact
54
52
 
55
- └─ runLoop / runMultishot INNER loop one conversation
53
+ └─ runLoop / runMultishot INNER loop: one conversation
56
54
  ├─ DRIVER = persona / user / planner chats with ↓
57
55
  └─ WORKERS = 1..N agent harnesses in 1..M sandboxes
58
56
 
@@ -82,18 +80,18 @@ opinion about execution topology: the topology lives inside `dispatch`.
82
80
  - The worker is the agent in the sandbox. The driver talks to it. ✓
83
81
  - `runCampaign` is a **measurement**, not a worker. It *runs the worker* (via
84
82
  `dispatch`); the worker does not "run the eval".
85
- - The outer improvement loop has **no single worker** its proposer proposes a
83
+ - The outer improvement loop has **no single worker**: its proposer proposes a
86
84
  *surface*, and each surface is scored by a *measurement* that drives the
87
85
  inner workers.
88
86
 
89
- ## The dataset flywheel why every loop run matters
87
+ ## The dataset flywheel: why every loop run matters
90
88
 
91
89
  **Every loop run, regardless of why it ran, feeds the same dataset.** This is
92
90
  the through-line that ties measurement and improvement together.
93
91
 
94
92
  When `runCampaign` runs with a `labeledStore`, each cell captures
95
93
  `(scenario, artifact, judgeScore, source)` into the `LabeledScenarioStore`.
96
- The `source` discriminates *why* the run happened but the captured tuple is
94
+ The `source` discriminates *why* the run happened: but the captured tuple is
97
95
  identical in shape:
98
96
 
99
97
  | `captureSource` | The run that produced it |
@@ -109,7 +107,7 @@ production conversation, and an autoresearch loop all deposit the same
109
107
  `(input, output, reward)` tuples. The optimization proposer later samples from
110
108
  that corpus to evolve the surface. So:
111
109
 
112
- > Running *any* loop even one whose purpose is not optimization builds the
110
+ > Running *any* loop: even one whose purpose is not optimization: builds the
113
111
  > dataset that optimization needs. The flywheel turns whether or not you are
114
112
  > currently optimizing.
115
113
 
@@ -147,7 +145,7 @@ interface SurfaceProposer<TFindings = unknown> {
147
145
  | Implementation | Strategy | How it proposes | Where it lives |
148
146
  |---|---|---|---|
149
147
  | `evolutionaryProposer` | Evolutionary (GEPA / AxGEPA) | Standalone `SurfaceProposer`. Mutates the current best surface into N candidates, blind to history beyond the current best. Optimizes against the dataset's rewards. | **agent-eval** (pure: dataset → surface, no sandbox) |
150
- | Runtime reflective proposer | Reflective | Cheap generator: drafts patches from the report and applies them into a worktree (shots=1, no sandbox). | **agent-runtime** implements agent-eval's proposer contract |
148
+ | Runtime reflective proposer | Reflective | Cheap generator: drafts patches from the report and applies them into a worktree (shots=1, no sandbox). | **agent-runtime**: implements agent-eval's proposer contract |
151
149
  | Runtime agentic proposer | Agentic | Full generator: runs a coding harness in the worktree (≤ `maxImprovementShots`) to edit in place. | **agent-runtime** |
152
150
 
153
151
  This resolves the prior duplication where `runImprovementLoop` (evolutionary,
@@ -158,7 +156,7 @@ same cost dial, not separate outer loops. The dependency direction permits this
158
156
  cleanly: agent-eval is the leaf and owns the proposer contract; agent-runtime
159
157
  imports agent-eval and implements it.
160
158
 
161
- ## What "the surface" is improvement tiers
159
+ ## What "the surface" is: improvement tiers
162
160
 
163
161
  `MutableSurface` is the thing the proposer changes. It has tiers, least → most
164
162
  invasive. `MutableSurface = string | CodeSurface` spans all of them: `string`
@@ -179,36 +177,36 @@ not represented by the candidate tree.
179
177
 
180
178
  The cost/capability distinction:
181
179
 
182
- - **`reflectiveGenerator`** updates the *signatures* prompt + tool surface
180
+ - **`reflectiveGenerator`** updates the *signatures*: prompt + tool surface
183
181
  (tiers 1–2). Cheap (drafts patches, no sandbox), reversible, measured
184
182
  directly against the dataset.
185
183
  - **`agenticGenerator`** updates the *code* (tier 4). A coding harness reads
186
184
  the repository + the report, edits in a worktree, iterates up to
187
- `maxImprovementShots` measured by re-running the inner loop against the
185
+ `maxImprovementShots`: measured by re-running the inner loop against the
188
186
  changed code.
189
187
 
190
188
  Both are implementations of the one proposer contract (propose → measure → gate
191
- → PR). They differ only in *what* they edit and *how invasive* it is and both
189
+ → PR). They differ only in *what* they edit and *how invasive* it is: and both
192
190
  consume the **same dataset** the flywheel builds.
193
191
 
194
192
  ## Vocabulary quick reference
195
193
 
196
- - **shot** one conversational turn (driver says X, worker responds Y). Used
194
+ - **shot**: one conversational turn (driver says X, worker responds Y). Used
197
195
  in `runMultishot`. Never used to mean a whole eval run.
198
- - **runMultishot** many shots in one conversation; persona-driver ↔ one
196
+ - **runMultishot**: many shots in one conversation; persona-driver ↔ one
199
197
  router-agent. agent-eval.
200
- - **runLoop** driver ↔ workers in sandboxes; topology-agnostic execution.
198
+ - **runLoop**: driver ↔ workers in sandboxes; topology-agnostic execution.
201
199
  agent-runtime.
202
- - **runCampaign** a measurement: a surface scored over N scenarios × M reps.
200
+ - **runCampaign**: a measurement: a surface scored over N scenarios × M reps.
203
201
  agent-eval. (A "campaign" = a coordinated batch of measurements.)
204
- - **runOptimization** the improvement loop body: proposer suggests surfaces, each is measured, and only a candidate that beats the global incumbent is promoted. agent-eval.
205
- - **runImprovementLoop** `runOptimization` + holdout re-score + release gate
202
+ - **runOptimization**: the improvement loop body: proposer suggests surfaces, each is measured, and only a candidate that beats the global incumbent is promoted. agent-eval.
203
+ - **runImprovementLoop**: `runOptimization` + holdout re-score + release gate
206
204
  + optional PR. agent-eval.
207
- - **runAnalystLoop** reflective autoresearch: findings + knowledge updates +
205
+ - **runAnalystLoop**: reflective autoresearch: findings + knowledge updates +
208
206
  improvement proposals. agent-runtime.
209
- - **SurfaceProposer** the contract a surface proposer implements.
207
+ - **SurfaceProposer**: the contract a surface proposer implements.
210
208
  `evolutionaryProposer` (agent-eval) is one; agent-runtime can provide
211
209
  reflective or agentic implementations.
212
- - **CandidateGenerator** the byte-producing seam inside a runtime proposer;
210
+ - **CandidateGenerator**: the byte-producing seam inside a runtime proposer;
213
211
  `reflectiveGenerator` (cheap, no sandbox) and `agenticGenerator` (coding
214
212
  harness in the worktree) are the two cost settings. agent-runtime.
package/docs/design.md ADDED
@@ -0,0 +1,69 @@
1
+ # Design rationale
2
+
3
+ Why this package is shaped the way it is.
4
+ This is background reading, not reference.
5
+ The API itself is documented in [`concepts.md`](./concepts.md) and the [`README`](../README.md).
6
+
7
+ ## Composition with the Tangle agent stack
8
+
9
+ `agent-eval` is one package in a larger stack.
10
+ It sits at the bottom of the layering: consumers depend on it, it depends on none of them.
11
+
12
+ ```
13
+ agent-runtime Runs agents (chat turns, one-shot tasks, multi-attempt loops), captures every
14
+ run as a trace, and exposes improve(), which composes agent-eval's improvement
15
+ loop. Produces the RunRecords + traces agent-eval scores. Depends on agent-eval.
16
+
17
+ agent-eval selfImprove, analyzeRuns, runCampaign + surface proposers (GEPA proposer, …),
18
+ (this repo) the gates (heldOutGate, defaultProductionGate, paretoSignificanceGate), the
19
+ InsightReport, the RL bridge, the wire protocol. Depends on neither consumer.
20
+
21
+ agent-knowledge proposeKnowledgeWrites / applyKnowledgeWriteBlocks. agent-eval's analyst
22
+ findings feed it; the knowledge gate consumes them. Depends on agent-eval.
23
+
24
+ sandbox Sandbox.create, streamPrompt. One execution environment the runtime's
25
+ loops run on; agent-eval scores what comes back.
26
+ ```
27
+
28
+ None of the sibling packages are required to use `agent-eval`; the library stands alone.
29
+ The stack context matters only if you adopt more of it later.
30
+
31
+ ## The dependency rule
32
+
33
+ This section is the public rationale.
34
+ The enforceable maintainer rule lives in [`CLAUDE.md`](../CLAUDE.md#repo-layering--this-package-is-the-substrate).
35
+
36
+ **`agent-eval` has zero upward dependencies on a consumer.**
37
+ This is what keeps the package reusable outside our own stack: nothing in here imports from `agent-runtime`, `agent-knowledge`, or `sandbox`, whether at runtime, in development dependencies, or as type-only imports.
38
+
39
+ The placement test for any shared type: *does this concept make sense without a running agent loop?*
40
+
41
+ - Yes: it lives here. A judge score, a run record, a scenario, and a pass/fail verdict are all meaningful for a pile of logs with no agent attached.
42
+ - No: it lives in the runtime layer. A validation context carrying an abort signal and a concrete sandbox session only exist mid-run.
43
+
44
+ When in doubt, the type moves down into `agent-eval`: subtracting a dependency from a consumer is always cheaper than adding one here.
45
+ Agent profile shape is the shared `@tangle-network/agent-interface` contract, so neither layer owns it.
46
+
47
+ ## Why "surface"
48
+
49
+ The improvement loop needs one word for "the thing being changed", because it deliberately does not care what that thing is: a system prompt, a config object, a skill file, a set of few-shot examples.
50
+ Proposers emit candidate surfaces, campaigns measure them, gates compare them against the baseline surface.
51
+ Where a doc can say "prompt" concretely, it should; `surface` appears in API names where the generality is the point.
52
+
53
+ ## Why the report never invents signal
54
+
55
+ Every section of the `analyzeRuns()` report is opt-in based on what the input data supports.
56
+ If runs carry no judge scores, the judge section is empty rather than defaulted.
57
+ If there is no baseline/candidate split, no lift is reported.
58
+ Missing evidence is never scored as zero; a judge that throws is recorded as a failed cell, not silently folded into the average.
59
+ The reasoning: a fabricated zero poisons every statistic downstream, and an eval library that quietly fabricates is worse than no eval at all.
60
+
61
+ ## Maintainer docs
62
+
63
+ These files record operating conventions for maintainers and internal agents.
64
+ They are not adoption reference:
65
+
66
+ - [`building-doctrine.md`](./building-doctrine.md): conventions our agents follow when consuming this package (reachable model defaults, probe-before-debug, experiment integrity checklist)
67
+ - [`design/loop-taxonomy.md`](./design/loop-taxonomy.md): the internal vocabulary for execution drivers, workers, measurements, and proposers
68
+ - [`research-report-methodology.md`](./research-report-methodology.md): the evidence standard our own research reports are held to
69
+ - [`.claude/skills/agent-eval/SKILL.md`](../.claude/skills/agent-eval/SKILL.md): directives for LLM agents writing integration code, encoding bug classes we have already shipped and fixed once
@@ -1,10 +1,10 @@
1
- # Distributed campaign execution coordinator-on-A, workers-on-B
1
+ # Distributed campaign execution: coordinator-on-A, workers-on-B
2
2
 
3
3
  The coordinator process (running `runCampaign` / `runImprovementLoop` /
4
4
  `gepaProposer`) and the worker (running your actual agent) **do not have to live in the
5
5
  same process, machine, region, or cloud.** `Dispatch` is just a
6
6
  function: scenario in, artifact out. Whatever returns the artifact is
7
- the worker local, remote, sandboxed, or fanned out across a fleet.
7
+ the worker: local, remote, sandboxed, or fanned out across a fleet.
8
8
 
9
9
  ## Why you'd want this
10
10
 
@@ -13,7 +13,7 @@ the worker — local, remote, sandboxed, or fanned out across a fleet.
13
13
  | **Coordinator on your VPC, workers on our sandbox fleet** | Coordinator holds secrets, training data, prompt corpus; workers stay stateless and scale horizontally |
14
14
  | **Multi-region campaigns** | Each cell runs in the region closest to its target API (latency, compliance, data residency) |
15
15
  | **Coordinator-as-a-service** | Long-running optimization process; reuses across many short-lived worker invocations |
16
- | **Heterogeneous workers** | One cell on a CPU container, another on a GPU box, another against a third-party API same Dispatch shape, different placement |
16
+ | **Heterogeneous workers** | One cell on a CPU container, another on a GPU box, another against a third-party API: same Dispatch shape, different placement |
17
17
  | **Budget-isolated workers** | Worker boxes get scoped, time-bounded credentials; coordinator never holds production keys |
18
18
 
19
19
  ## Two new pieces in 0.45.0
@@ -22,7 +22,7 @@ the worker — local, remote, sandboxed, or fanned out across a fleet.
22
22
  |---|---|
23
23
  | **`DispatchContext.placement?: string`** | Opaque placement key the substrate forwards to the Dispatch. |
24
24
  | **`RunCampaignOptions.cellPlacement?(input) → string \| undefined`** | Strategy function the substrate calls per cell to compute the placement key. |
25
- | **`@tangle-network/agent-eval/adapters/http`** | `httpDispatch` (client) + `runDispatchServer` (server) wire shape for HTTP-based remote workers. |
25
+ | **`@tangle-network/agent-eval/adapters/http`** | `httpDispatch` (client) + `runDispatchServer` (server): wire shape for HTTP-based remote workers. |
26
26
 
27
27
  Both ends of the wire are in the same package; no peer dep, no separate
28
28
  install. The substrate doesn't strategy-pick; you provide the
@@ -31,7 +31,7 @@ Dispatch reads it. Clean seam, no policy baked in.
31
31
 
32
32
  ## The three reference topologies
33
33
 
34
- ### 1. In-process (the default what you already have)
34
+ ### 1. In-process (the default: what you already have)
35
35
 
36
36
  ```ts
37
37
  await runCampaign({
@@ -74,7 +74,7 @@ import { runDispatchServer } from '@tangle-network/agent-eval/adapters/http'
74
74
 
75
75
  const handle = await runDispatchServer<MyScenario, MyArtifact>({
76
76
  dispatch: async (scenario, ctx) => {
77
- // your agent call OpenAI, LangChain, your sandbox, anything.
77
+ // your agent: call OpenAI, LangChain, your sandbox, anything.
78
78
  const artifact = await runMyAgent(scenario, ctx.signal)
79
79
  return artifact
80
80
  },
@@ -85,7 +85,7 @@ console.log(`worker listening on ${handle.port}`)
85
85
  ```
86
86
 
87
87
  Cancellation, retries on 5xx / 408 / 429, bounded timeouts, optional
88
- custom auth headers, optional `fetchImpl` override all there.
88
+ custom auth headers, optional `fetchImpl` override: all there.
89
89
 
90
90
  ### 3. Multi-region fan-out
91
91
 
@@ -121,7 +121,7 @@ await runCampaign({
121
121
  })
122
122
  ```
123
123
 
124
- `cellPlacement` is a pure function the substrate calls per cell no
124
+ `cellPlacement` is a pure function the substrate calls per cell: no
125
125
  state. Use whatever signal you want (tags, hash of scenario id,
126
126
  round-robin, region-affinity from a previous run, scheduling table).
127
127
 
@@ -134,7 +134,7 @@ round-robin, region-affinity from a previous run, scheduling table).
134
134
  | **Retries** | Idempotent retries on 5xx / 408 / 429 with exponential backoff + jitter. Coordinator aborts never retry. |
135
135
  | **Auth** | Bearer token on `Authorization`; pluggable via `auth: string \| () => string \| Promise<string>` for rotation/refresh. |
136
136
  | **Payload size** | Server enforces `maxBodyBytes` (default 10 MB). |
137
- | **Traces** | Both ends emit OTel if both point at the same OTLP collector, you get a unified trace per cell. See `docs/adapters-observability.md`. |
137
+ | **Traces** | Both ends emit OTel: if both point at the same OTLP collector, you get a unified trace per cell. See `docs/adapters-observability.md`. |
138
138
  | **Cost** | Worker's `ctx.cost.runPaidCall(...)` writes durable receipts in the worker process. Roll up those receipts server-side and attach them to worker telemetry; they are not forwarded to the coordinator automatically. |
139
139
 
140
140
  ## Running the reference example
@@ -142,10 +142,10 @@ round-robin, region-affinity from a previous run, scheduling table).
142
142
  See `examples/distributed-driver/`:
143
143
 
144
144
  ```sh
145
- # Terminal 1 worker
145
+ # Terminal 1: worker
146
146
  pnpm tsx examples/distributed-driver/worker.ts
147
147
 
148
- # Terminal 2 coordinator
148
+ # Terminal 2: coordinator
149
149
  WORKER_URL=http://localhost:8080/dispatch \
150
150
  WORKER_TOKEN=dev-token \
151
151
  pnpm tsx examples/distributed-driver/driver.ts
@@ -157,17 +157,17 @@ and using `cellPlacement` to fan across many of them.
157
157
 
158
158
  ## Known gaps + follow-ups
159
159
 
160
- - **Cost roll-up across the wire** worker-side `ctx.cost` observations
160
+ - **Cost roll-up across the wire**: worker-side `ctx.cost` observations
161
161
  stay on the worker. We need to forward them in the response body so
162
162
  `defaultProductionGate`'s `budgetUsd` ceiling reflects total spend, not
163
163
  coordinator-side spend. Tracked as a 0.45.x follow-up.
164
- - **Per-cell artifact streaming** when the worker writes intermediate
164
+ - **Per-cell artifact streaming**: when the worker writes intermediate
165
165
  artifacts via `ctx.artifacts.write`, those land on the worker's
166
166
  storage. For multi-worker campaigns you'll want a shared object store
167
167
  (S3/GCS) reachable from both sides; today consumers wire that as a
168
168
  `CampaignStorage` impl. A reference S3-backed storage is on the
169
169
  roadmap.
170
- - **gRPC / NATS / Temporal transports** the wire is HTTP today by
170
+ - **gRPC / NATS / Temporal transports**: the wire is HTTP today by
171
171
  default because everything speaks HTTP. Other transports can ship as
172
172
  additional adapters; the `Dispatch` interface itself is
173
173
  transport-agnostic.