@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.
- package/CHANGELOG.md +29 -0
- package/README.md +191 -157
- package/dist/analyst/index.d.ts +27 -4
- package/dist/analyst/index.js +6 -6
- package/dist/authenticity/index.js +3 -2
- package/dist/authenticity/index.js.map +1 -1
- package/dist/benchmarks/index.d.ts +12 -4
- package/dist/benchmarks/index.js +9 -9
- package/dist/campaign/index.d.ts +270 -254
- package/dist/campaign/index.js +22 -18
- package/dist/{chunk-GS3FJGUF.js → chunk-22VO7T2I.js} +805 -366
- package/dist/chunk-22VO7T2I.js.map +1 -0
- package/dist/{chunk-LBAHQOBI.js → chunk-4KO6D2BO.js} +38 -12
- package/dist/chunk-4KO6D2BO.js.map +1 -0
- package/dist/{chunk-3FCG7FBV.js → chunk-D5JZ7UDZ.js} +2 -2
- package/dist/{chunk-XJYR7XFV.js → chunk-GC4ATIKK.js} +1 -1
- package/dist/chunk-GC4ATIKK.js.map +1 -0
- package/dist/{chunk-BGVTIE2C.js → chunk-GRCDRKII.js} +13 -2
- package/dist/chunk-GRCDRKII.js.map +1 -0
- package/dist/{chunk-FHFTYX2Q.js → chunk-JKDNAOF5.js} +2 -2
- package/dist/{chunk-S3UZOQ5Y.js → chunk-LOW3U7JZ.js} +2 -2
- package/dist/{chunk-FC5NDO3E.js → chunk-MHPEGJHC.js} +4 -4
- package/dist/{chunk-NJC7U437.js → chunk-MK3Q6GCL.js} +33 -18
- package/dist/chunk-MK3Q6GCL.js.map +1 -0
- package/dist/{chunk-U5CHZ5M3.js → chunk-N7ZS6PEF.js} +4 -4
- package/dist/{chunk-A5S77LSE.js → chunk-QFQZ3U3X.js} +2 -2
- package/dist/chunk-QFQZ3U3X.js.map +1 -0
- package/dist/{chunk-G2GPNLSX.js → chunk-QNVBALEX.js} +4 -4
- package/dist/{chunk-OYZAPX5G.js → chunk-R226UZOI.js} +2 -2
- package/dist/{chunk-6WX7CBAR.js → chunk-SJT4OBVL.js} +20 -9
- package/dist/chunk-SJT4OBVL.js.map +1 -0
- package/dist/{chunk-HZJF4IUO.js → chunk-SUN7QLPB.js} +6 -6
- package/dist/{chunk-HZJF4IUO.js.map → chunk-SUN7QLPB.js.map} +1 -1
- package/dist/{chunk-VJ7T5WIO.js → chunk-WP3GEXKI.js} +3 -3
- package/dist/cli.js +101 -11
- package/dist/cli.js.map +1 -1
- package/dist/contract/index.d.ts +115 -8
- package/dist/contract/index.js +11 -9
- package/dist/contract/index.js.map +1 -1
- package/dist/control.js +3 -3
- package/dist/fuzz.d.ts +8 -1
- package/dist/fuzz.js +7 -6
- package/dist/fuzz.js.map +1 -1
- package/dist/hosted/index.d.ts +8 -2
- package/dist/index.d.ts +36 -10
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/multishot/index.d.ts +2 -2
- package/dist/openapi.json +1 -1
- package/dist/rl.d.ts +24 -3
- package/dist/rl.js +6 -4
- package/dist/rl.js.map +1 -1
- package/dist/{run-campaign-ZKR5MQMQ.js → run-campaign-I3JXKVAK.js} +3 -3
- package/dist/storyboard/index.js +1 -1
- package/dist/storyboard/index.js.map +1 -1
- package/dist/traces.d.ts +2 -2
- package/dist/traces.js +3 -3
- package/dist/wire/index.d.ts +77 -5
- package/dist/wire/index.js +3 -3
- package/docs/adapters-observability.md +6 -6
- package/docs/building-doctrine.md +5 -5
- package/docs/campaign-proposers.md +142 -131
- package/docs/concepts.md +49 -53
- package/docs/customer-journeys.md +80 -155
- package/docs/design/loop-taxonomy.md +27 -29
- package/docs/design.md +69 -0
- package/docs/distributed-driver.md +14 -14
- package/docs/eval-surface-map.md +11 -11
- package/docs/hosted-ingest-spec.md +4 -4
- package/docs/insight-report.md +32 -27
- package/docs/multi-shot-optimization.md +41 -49
- package/docs/research-report-methodology.md +9 -9
- package/docs/trace-analysis.md +2 -2
- package/docs/wire-protocol.md +16 -16
- package/package.json +2 -1
- package/dist/chunk-6WX7CBAR.js.map +0 -1
- package/dist/chunk-A5S77LSE.js.map +0 -1
- package/dist/chunk-BGVTIE2C.js.map +0 -1
- package/dist/chunk-GS3FJGUF.js.map +0 -1
- package/dist/chunk-LBAHQOBI.js.map +0 -1
- package/dist/chunk-NJC7U437.js.map +0 -1
- package/dist/chunk-XJYR7XFV.js.map +0 -1
- package/docs/auto-research-loop-end-to-end.md +0 -186
- package/docs/improvement-glossary.md +0 -204
- package/docs/self-improvement-map.md +0 -115
- /package/dist/{chunk-3FCG7FBV.js.map → chunk-D5JZ7UDZ.js.map} +0 -0
- /package/dist/{chunk-FHFTYX2Q.js.map → chunk-JKDNAOF5.js.map} +0 -0
- /package/dist/{chunk-S3UZOQ5Y.js.map → chunk-LOW3U7JZ.js.map} +0 -0
- /package/dist/{chunk-FC5NDO3E.js.map → chunk-MHPEGJHC.js.map} +0 -0
- /package/dist/{chunk-U5CHZ5M3.js.map → chunk-N7ZS6PEF.js.map} +0 -0
- /package/dist/{chunk-G2GPNLSX.js.map → chunk-QNVBALEX.js.map} +0 -0
- /package/dist/{chunk-OYZAPX5G.js.map → chunk-R226UZOI.js.map} +0 -0
- /package/dist/{chunk-VJ7T5WIO.js.map → chunk-WP3GEXKI.js.map} +0 -0
- /package/dist/{run-campaign-ZKR5MQMQ.js.map → run-campaign-I3JXKVAK.js.map} +0 -0
|
@@ -1,210 +1,135 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Adoption Paths
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Choose the path that matches the data and code you already have.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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:
|
|
21
|
+
const runs = fromOtelSpans({ spans: yourOtelSpans })
|
|
29
22
|
const report = await analyzeRuns({ runs })
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
```ts
|
|
35
|
+
const report = await analyzeRuns({
|
|
36
|
+
runs,
|
|
37
|
+
analyst,
|
|
38
|
+
})
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
[medium] expand-corpus — Mean composite 0.721 has room
|
|
40
|
+
console.log(report.failureClusters)
|
|
55
41
|
```
|
|
56
42
|
|
|
57
|
-
|
|
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
|
-
|
|
45
|
+
## 2. Analyze Human Ratings
|
|
70
46
|
|
|
71
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
Runnable example: [`examples/customer-feedback-loop`](../examples/customer-feedback-loop/)
|
|
142
74
|
|
|
143
|
-
|
|
75
|
+
## 3. Evaluate Or Improve A Runnable Agent
|
|
144
76
|
|
|
145
|
-
|
|
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 (
|
|
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: '
|
|
158
|
-
dimensions: [
|
|
159
|
-
|
|
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:
|
|
162
|
-
budget: { generations: 3, populationSize: 2 },
|
|
93
|
+
baselineSurface: currentPrompt,
|
|
163
94
|
})
|
|
164
95
|
|
|
165
|
-
const
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
112
|
+
console.log(result.winner.surface)
|
|
113
|
+
console.log(result.lift)
|
|
114
|
+
console.log(result.gateDecision)
|
|
189
115
|
```
|
|
190
116
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
126
|
+
## Moving Between Paths
|
|
203
127
|
|
|
204
|
-
|
|
128
|
+
The paths compose without changing data formats:
|
|
205
129
|
|
|
206
|
-
1.
|
|
207
|
-
2.
|
|
208
|
-
3.
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
43
|
+
│ reflective analyst) : NOT a conversation turn
|
|
46
44
|
│
|
|
47
45
|
└─ for each candidate surface:
|
|
48
46
|
│
|
|
49
|
-
runCampaign a MEASUREMENT
|
|
47
|
+
runCampaign a MEASUREMENT: scores ONE surface
|
|
50
48
|
│
|
|
51
49
|
└─ for each scenario × rep:
|
|
52
50
|
│
|
|
53
|
-
dispatch(scenario) THE SEAM
|
|
51
|
+
dispatch(scenario) THE SEAM: topology-opaque, returns an artifact
|
|
54
52
|
│
|
|
55
|
-
└─ runLoop / runMultishot INNER loop
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
196
|
+
- **runMultishot**: many shots in one conversation; persona-driver ↔ one
|
|
199
197
|
router-agent. agent-eval.
|
|
200
|
-
- **runLoop
|
|
198
|
+
- **runLoop**: driver ↔ workers in sandboxes; topology-agnostic execution.
|
|
201
199
|
agent-runtime.
|
|
202
|
-
- **runCampaign
|
|
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
|
|
205
|
-
- **runImprovementLoop
|
|
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
|
|
205
|
+
- **runAnalystLoop**: reflective autoresearch: findings + knowledge updates +
|
|
208
206
|
improvement proposals. agent-runtime.
|
|
209
|
-
- **SurfaceProposer
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
145
|
+
# Terminal 1: worker
|
|
146
146
|
pnpm tsx examples/distributed-driver/worker.ts
|
|
147
147
|
|
|
148
|
-
# Terminal 2
|
|
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
|
|
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
|
|
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
|
|
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.
|