@tangle-network/agent-eval 0.123.1 → 0.123.2
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 +5 -0
- package/README.md +151 -161
- package/dist/analyst/index.d.ts +9 -1
- package/dist/analyst/index.js +5 -5
- package/dist/authenticity/index.js +3 -2
- package/dist/authenticity/index.js.map +1 -1
- package/dist/benchmarks/index.d.ts +2 -1
- package/dist/benchmarks/index.js +6 -6
- package/dist/campaign/index.d.ts +29 -33
- package/dist/campaign/index.js +6 -6
- package/dist/{chunk-A5S77LSE.js → chunk-4SOQ4ND2.js} +2 -2
- package/dist/{chunk-VJ7T5WIO.js → chunk-5YMKIFYP.js} +3 -3
- package/dist/{chunk-U5CHZ5M3.js → chunk-DNVPOYUS.js} +4 -4
- package/dist/{chunk-6WX7CBAR.js → chunk-E3HAD4A3.js} +19 -8
- package/dist/chunk-E3HAD4A3.js.map +1 -0
- package/dist/{chunk-LBAHQOBI.js → chunk-EBDOTTZJ.js} +37 -11
- package/dist/chunk-EBDOTTZJ.js.map +1 -0
- package/dist/{chunk-XJYR7XFV.js → chunk-GC4ATIKK.js} +1 -1
- package/dist/chunk-GC4ATIKK.js.map +1 -0
- package/dist/{chunk-HZJF4IUO.js → chunk-HQY7LBV2.js} +3 -3
- package/dist/{chunk-NJC7U437.js → chunk-J7S4YM27.js} +6 -5
- package/dist/chunk-J7S4YM27.js.map +1 -0
- package/dist/{chunk-S3UZOQ5Y.js → chunk-LOW3U7JZ.js} +2 -2
- package/dist/{chunk-OYZAPX5G.js → chunk-R226UZOI.js} +2 -2
- package/dist/{chunk-GS3FJGUF.js → chunk-RQP5UTK5.js} +120 -14
- package/dist/chunk-RQP5UTK5.js.map +1 -0
- package/dist/{chunk-G2GPNLSX.js → chunk-WMJR67FX.js} +3 -3
- package/dist/{chunk-FC5NDO3E.js → chunk-WXQTVEKM.js} +3 -3
- package/dist/cli.js +100 -10
- package/dist/cli.js.map +1 -1
- package/dist/contract/index.d.ts +97 -5
- package/dist/contract/index.js +9 -7
- package/dist/contract/index.js.map +1 -1
- package/dist/control.js +3 -3
- package/dist/fuzz.js +3 -2
- package/dist/fuzz.js.map +1 -1
- package/dist/hosted/index.d.ts +8 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/openapi.json +1 -1
- package/dist/rl.d.ts +9 -1
- package/dist/rl.js +4 -4
- package/dist/storyboard/index.js +1 -1
- package/dist/storyboard/index.js.map +1 -1
- package/dist/traces.js +3 -3
- package/dist/wire/index.d.ts +61 -4
- package/dist/wire/index.js +2 -2
- package/docs/adapters-observability.md +6 -6
- package/docs/building-doctrine.md +5 -5
- package/docs/concepts.md +29 -29
- package/docs/customer-journeys.md +80 -155
- package/docs/design/loop-taxonomy.md +26 -27
- package/docs/design.md +70 -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/improvement-glossary.md +38 -38
- package/docs/insight-report.md +32 -27
- package/docs/multi-shot-optimization.md +8 -8
- package/docs/research-report-methodology.md +9 -9
- package/docs/self-improvement-map.md +13 -13
- 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-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/dist/{chunk-A5S77LSE.js.map → chunk-4SOQ4ND2.js.map} +0 -0
- /package/dist/{chunk-VJ7T5WIO.js.map → chunk-5YMKIFYP.js.map} +0 -0
- /package/dist/{chunk-U5CHZ5M3.js.map → chunk-DNVPOYUS.js.map} +0 -0
- /package/dist/{chunk-HZJF4IUO.js.map → chunk-HQY7LBV2.js.map} +0 -0
- /package/dist/{chunk-S3UZOQ5Y.js.map → chunk-LOW3U7JZ.js.map} +0 -0
- /package/dist/{chunk-OYZAPX5G.js.map → chunk-R226UZOI.js.map} +0 -0
- /package/dist/{chunk-G2GPNLSX.js.map → chunk-WMJR67FX.js.map} +0 -0
- /package/dist/{chunk-FC5NDO3E.js.map → chunk-WXQTVEKM.js.map} +0 -0
package/docs/design.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
- [`self-improvement-map.md`](./self-improvement-map.md): how the one improvement loop is pointed at different surfaces, and the proposer bake-off narrative
|
|
68
|
+
- [`design/loop-taxonomy.md`](./design/loop-taxonomy.md): the internal vocabulary for execution drivers, workers, measurements, and proposers
|
|
69
|
+
- [`research-report-methodology.md`](./research-report-methodology.md): the evidence standard our own research reports are held to
|
|
70
|
+
- [`.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.
|
package/docs/eval-surface-map.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Eval surface map
|
|
1
|
+
# Eval surface map: which primitive, when
|
|
2
2
|
|
|
3
3
|
The eval surface is a small set of orthogonal primitives. They compose; they do
|
|
4
|
-
not overlap. If two seem interchangeable, read the "use when"
|
|
4
|
+
not overlap. If two seem interchangeable, read the "use when": the distinction
|
|
5
5
|
is real and load-bearing. **Do not add a new wrapper to bridge two of these; the
|
|
6
6
|
composition point already exists (see Produced-state grading below).**
|
|
7
7
|
|
|
@@ -10,15 +10,15 @@ composition point already exists (see Produced-state grading below).**
|
|
|
10
10
|
| Primitive | Use when | Returns |
|
|
11
11
|
|---|---|---|
|
|
12
12
|
| `runCampaign` | The measurement primitive. Run a dispatch over scenarios × seeds × reps, score each with judges, aggregate. Caller owns the dispatch. | `CampaignResult` |
|
|
13
|
-
| `runEval` | The simplest preset over `runCampaign
|
|
13
|
+
| `runEval` | The simplest preset over `runCampaign`: just score, no loop, no gate. The 80% "I want a scorecard" case. | `CampaignResult` |
|
|
14
14
|
| `runProfileMatrix` | Factor the SAME scenarios across N agent **profiles** (models / prompt variants), with RunRecord stamping + a real-backend integrity guard. | `RunRecord[]` |
|
|
15
15
|
| `runOptimization` | GENERATE: measured or validated premeasured baseline → N generations of propose → measure → rank → promote. No release gate. | generations + winner |
|
|
16
16
|
| `runImprovementLoop` | The release-gate shell around `runOptimization`: adds a held-out re-score + a promotion gate (+ optional auto-PR). | gate decision + winner |
|
|
17
|
-
| `runEvalCampaign` | Inversion-of-control variant of `runCampaign
|
|
17
|
+
| `runEvalCampaign` | Inversion-of-control variant of `runCampaign`: the runner is handed a pre-wired trace/sink/emitter and integrity gating as a precondition. Use when you need full capture by construction. | `CampaignResult` + records |
|
|
18
18
|
|
|
19
19
|
When variants of the same task run inside one `runCampaign`, give those scenarios the same `seedGroup` so each repetition uses common randomness.
|
|
20
20
|
Use `runProfileMatrix` instead when profiles are separate campaign axes.
|
|
21
|
-
| `runAgentMatrix` | The bare N-axis cartesian scheduler with concurrency control. The layer beneath the eval surface
|
|
21
|
+
| `runAgentMatrix` | The bare N-axis cartesian scheduler with concurrency control. The layer beneath the eval surface: reach for it only when you need raw scheduling, not eval semantics. | cell results |
|
|
22
22
|
|
|
23
23
|
Mental model: **measure** (`runCampaign`/`runEval`) → **factor** (`runProfileMatrix`) →
|
|
24
24
|
**generate** (`runOptimization`) → **gate** (`runImprovementLoop`). `runEvalCampaign`
|
|
@@ -27,11 +27,11 @@ is `runCampaign` with capture inverted; `runAgentMatrix` is the scheduler undern
|
|
|
27
27
|
Merging any two of these conflates distinct mental models (measure ≠ search ≠
|
|
28
28
|
release-gate). Keep them separate; pick by the table.
|
|
29
29
|
|
|
30
|
-
## Produced-state grading
|
|
30
|
+
## Produced-state grading: there is NO persona-dispatch wrapper
|
|
31
31
|
|
|
32
32
|
To grade what an agent actually **produced** (filed the proposal, wrote the
|
|
33
33
|
artifact) rather than what it said, the composition point is a **judge that wraps
|
|
34
|
-
`verifyCompletion
|
|
34
|
+
`verifyCompletion`**: not a dedicated runner. The pipeline:
|
|
35
35
|
|
|
36
36
|
```
|
|
37
37
|
runtime/app-tool events ──► extractProducedState(events) ──► ProducedState
|
|
@@ -43,20 +43,20 @@ runtime/app-tool events ──► extractProducedState(events) ──► Produce
|
|
|
43
43
|
|
|
44
44
|
`extractProducedState` is a pure function over the produced-event stream; the
|
|
45
45
|
judge calls it inline. This is why **`runProducedStatePersonaDispatch` does not
|
|
46
|
-
exist and should not be built
|
|
46
|
+
exist and should not be built**: it would be a fourth layer over a composition
|
|
47
47
|
that is already one judge. (Archetype: `playback.ts` `scoreUserStory`.)
|
|
48
48
|
|
|
49
49
|
### The in-band body contract
|
|
50
50
|
|
|
51
|
-
Produced events carry their **body in-band
|
|
51
|
+
Produced events carry their **body in-band**: the grader never reaches into a
|
|
52
52
|
product database to recover it:
|
|
53
53
|
|
|
54
54
|
- `artifact` events carry `content` (the persisted file body).
|
|
55
|
-
- `proposal_created` events carry `content` (the `submit_proposal` description)
|
|
55
|
+
- `proposal_created` events carry `content` (the `submit_proposal` description) -
|
|
56
56
|
same role, same field name. A title-only filing omits it; a content-less
|
|
57
57
|
proposal is graded presence-only (and, by the completion oracle's rule, does
|
|
58
58
|
not count as a completed deliverable).
|
|
59
59
|
|
|
60
60
|
A consumer that finds itself re-fetching a deliverable's body from its own DB to
|
|
61
|
-
grade it is working around a thin event
|
|
61
|
+
grade it is working around a thin event: fix the event (carry `content`), don't
|
|
62
62
|
add an enrichment band-aid.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Hosted-ingest wire spec
|
|
1
|
+
# Hosted-ingest wire spec: `2026-05-26.v1`
|
|
2
2
|
|
|
3
3
|
The schema **every** orchestrator (ours, partners' self-hosted ones,
|
|
4
4
|
any future open implementation) must accept. Frozen under semver:
|
|
@@ -149,7 +149,7 @@ Any orchestrator implementing this spec MUST:
|
|
|
149
149
|
5. **Persist eval-runs durably**: at least the event + cell scores must
|
|
150
150
|
survive an orchestrator restart. Trace spans MAY be best-effort.
|
|
151
151
|
6. **Provide read access**: GET endpoints for the tenant to list + fetch
|
|
152
|
-
their own runs. Wire format for reads is NOT part of this spec
|
|
152
|
+
their own runs. Wire format for reads is NOT part of this spec: each
|
|
153
153
|
orchestrator can pick its own (REST + JSON, gRPC, GraphQL).
|
|
154
154
|
|
|
155
155
|
Servers SHOULD also:
|
|
@@ -163,7 +163,7 @@ Servers SHOULD also:
|
|
|
163
163
|
|
|
164
164
|
## Reference implementation
|
|
165
165
|
|
|
166
|
-
`examples/hosted-ingest-server
|
|
166
|
+
`examples/hosted-ingest-server/`: a minimal hono-based receiver. ~200
|
|
167
167
|
LOC. Validates auth, accepts ingest, stores in memory, exposes a
|
|
168
168
|
read endpoint. Runs anywhere Node runs.
|
|
169
169
|
|
|
@@ -200,4 +200,4 @@ receiver's `GET /v1/runs` lists it back.
|
|
|
200
200
|
Servers MUST reject requests with `X-Tangle-Wire-Version` they don't
|
|
201
201
|
support, with a 400 listing the versions they DO accept.
|
|
202
202
|
|
|
203
|
-
The version string IS the spec id
|
|
203
|
+
The version string IS the spec id: pin against it.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Improvement glossary + proposer chooser + composition
|
|
2
2
|
|
|
3
|
-
> **In plain terms:** this is the dictionary for the *improvement* half of the stack
|
|
3
|
+
> **In plain terms:** this is the dictionary for the *improvement* half of the stack: the words that show up when you optimize an agent (proposer, surface, candidate, generation, holdout, lift, gate…) rather than when you *run* one (driver, worker, iteration: those live in [`agent-runtime/docs/glossary.md`](../../agent-runtime/docs/glossary.md)).
|
|
4
4
|
> Read this once and you can read any improvement result, pick a proposer, and wire two of them together.
|
|
5
5
|
|
|
6
6
|
**Who this is for.**
|
|
7
7
|
A **novice** (never seen the repo) should be able to read a `CampaignResult`, a `ProposeContext`, and a proposer chooser table without opening the source.
|
|
8
|
-
An **expert** who knows DSPy/GEPA should be able to map their existing mental model onto our names in about a minute
|
|
9
|
-
If code and this file disagree, the code wins
|
|
8
|
+
An **expert** who knows DSPy/GEPA should be able to map their existing mental model onto our names in about a minute: every core term below carries a *"if you know DSPy/GEPA"* line.
|
|
9
|
+
If code and this file disagree, the code wins: fix this file the same turn (the anti-staleness law).
|
|
10
10
|
|
|
11
11
|
Neighbors, so this page does not duplicate them: [`concepts.md`](./concepts.md) (eval mental model), [`self-improvement-map.md`](./self-improvement-map.md) (one loop / four roles / proposer catalog), [`campaign-proposers.md`](./campaign-proposers.md) (proposer ELI5), [`eval-surface-map.md`](./eval-surface-map.md) (which `run*` primitive), [`design/loop-taxonomy.md`](./design/loop-taxonomy.md) (execution vs proposer layering).
|
|
12
12
|
|
|
@@ -24,63 +24,63 @@ baseline surface
|
|
|
24
24
|
repeat for maxGenerations
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
## Glossary
|
|
27
|
+
## Glossary: one plain sentence each
|
|
28
28
|
|
|
29
29
|
Grounded to `agent-eval/src/campaign/types.ts` unless noted.
|
|
30
30
|
|
|
31
31
|
| Term | Plain sentence | If you know DSPy/GEPA |
|
|
32
32
|
|---|---|---|
|
|
33
|
-
| **surface** | The one thing being changed this run
|
|
34
|
-
| **proposer** | The strategy that, given the current surface + what failed, proposes the next batch of candidate surfaces to measure
|
|
33
|
+
| **surface** | The one thing being changed this run: a prompt string, a JSON config string, or a finalized code candidate (`MutableSurface = string \| CodeSurface`, `types.ts`). | The optimized artifact: a signature/predictor's instruction text, or the module config. |
|
|
34
|
+
| **proposer** | The strategy that, given the current surface + what failed, proposes the next batch of candidate surfaces to measure: it does **not** run the agent or score anything (`SurfaceProposer.propose`, `types.ts:286`). | The optimizer / teleprompter (MIPRO, BootstrapFewShot, GEPA's reflective proposer). |
|
|
35
35
|
| **candidate** | One proposed surface plus its human `label` and `rationale`, ready to be measured (`ProposedCandidate`, `types.ts:166`). | One trial instruction/program the optimizer wants to evaluate. |
|
|
36
36
|
| **generation** | One round of *propose → measure → rank → promote*; the loop runs up to `maxGenerations` of them (`GenerationRecord`, `types.ts:549`). | One GEPA iteration / optimization step. |
|
|
37
|
-
| **populationSize** | BREADTH
|
|
38
|
-
| **population budget** | Informal name for the pair `{ populationSize, maxGenerations }
|
|
37
|
+
| **populationSize** | BREADTH: how many candidate surfaces the proposer returns *this* generation (`ProposeContext.populationSize`, `types.ts:237`). Paired with `maxGenerations` (DEPTH) as the search budget. | Beam width / number of minibatch candidates per step. |
|
|
38
|
+
| **population budget** | Informal name for the pair `{ populationSize, maxGenerations }`: the total candidates the search may evaluate (breadth × depth). | Optimizer trial budget. |
|
|
39
39
|
| **campaign** | One measurement: run a dispatch over scenarios × seeds × reps, judge each output, aggregate → `CampaignResult` (`runCampaign`; a "campaign" = a coordinated batch of measurements). | One evaluation pass of a candidate over a valset. |
|
|
40
|
-
| **cell** | The atomic measurement unit
|
|
40
|
+
| **cell** | The atomic measurement unit: exactly one `(scenario, rep)` execution producing one artifact + its judge scores + its cost (`CampaignCellResult`, `types.ts:509`; `DispatchContext.cellId/rep`). | One (example, seed) evaluation datapoint. |
|
|
41
41
|
| **scenario** | One input case with a stable `id` + `kind` (consumers attach their payload: persona, task, requirement) (`Scenario`, `types.ts:23`). | One dataset example / `dspy.Example`. |
|
|
42
|
-
| **rep** | The repetition index
|
|
42
|
+
| **rep** | The repetition index: the same scenario run more than once so noise/variance is measurable rather than mistaken for signal (`ctx.rep`). | Repeated sampling of the same example (temperature/seed variance). |
|
|
43
43
|
| **judge** | A scorer: given an artifact, return dimensions + a single `composite` + free-form `notes`; it throws on failure rather than silently scoring 0 (`JudgeConfig` → `JudgeScore`, `types.ts:94/116`). | The metric function, but pluggable (LLM-judge, deterministic checks, or an ensemble). |
|
|
44
|
-
| **composite** | The single 0..1 number that combines all judge dimensions
|
|
45
|
-
| **findings** | The failure analysis handed to the proposer so it edits from evidence, not guesses
|
|
44
|
+
| **composite** | The single 0..1 number that combines all judge dimensions: the number you rank and gate on (`JudgeScore.composite`, `types.ts:116`). | The scalar metric value. |
|
|
45
|
+
| **findings** | The failure analysis handed to the proposer so it edits from evidence, not guesses: worst cells + judge reasons, or a trace-analyst's clusters (`ProposeContext.findings`). | GEPA's reflective feedback / the "textual gradient". |
|
|
46
46
|
| **baseline** | The starting surface (the current prompt/config) every candidate is measured against (`baselineSurface`). | The unoptimized program you compare lift against. |
|
|
47
|
-
| **holdout** | A separate scenario split the winner is re-scored on and that the proposer is *never* allowed to see
|
|
48
|
-
| **lift** | Winner minus baseline `composite` on the holdout
|
|
49
|
-
| **MDE** | Minimum Detectable Effect
|
|
50
|
-
| **Pareto frontier** | The set of surfaces that are non-dominated across the per-scenario score vectors
|
|
47
|
+
| **holdout** | A separate scenario split the winner is re-scored on and that the proposer is *never* allowed to see: a compile-time firewall (`ProposeContext.judgeScores: never`, `types.ts:265`) keeps held-out verdicts out of proposal so the optimizer can't game the acceptance axis. | The held-out valset/testset: but here it is *write-only* to the optimizer. |
|
|
48
|
+
| **lift** | Winner minus baseline `composite` on the holdout: the actual improvement, reported with a bootstrap confidence interval (`ImproveResult.lift`, `ProposerScore.lift`). | Δ metric between optimized and baseline program. |
|
|
49
|
+
| **MDE** | Minimum Detectable Effect: the smallest lift your budget could statistically distinguish from noise, computed up front from baseline cells; a structurally-hopeless budget warns before you spend (surfaced as `result.power` / "power preflight", `agent-optimization-map.md`). Not spelled "MDE" in code yet: this is the standard name for it. | Power analysis on the eval set; the reason a tiny valset can't certify a small gain. |
|
|
50
|
+
| **Pareto frontier** | The set of surfaces that are non-dominated across the per-scenario score vectors: a candidate worse on the mean but uniquely best on one hard scenario survives, so its lesson is not discarded (`ParetoParent`, `types.ts:198`; GEPA, arXiv:2507.19457). | Exactly GEPA's Pareto candidate pool: same paper, same idea. |
|
|
51
51
|
| **gate** | The promotion decision: does the winner beat baseline on the holdout with significance, returning one of five verdicts `ship / hold / need_more_work / model_ceiling / arch_ceiling` (`Gate`, `GateDecision`, `types.ts:315/340`). | The accept/reject rule on the held-out valset, plus a significance test and ceiling diagnosis. |
|
|
52
|
-
| **promotion** | What happens on a `ship` verdict
|
|
52
|
+
| **promotion** | What happens on a `ship` verdict: the winning surface is written back into the profile field it came from (`GenerationRecord.promoted`; `applyWinnerToProfile`, `improve.ts`). | Committing the optimized program as the new default. |
|
|
53
53
|
|
|
54
|
-
## Which proposer
|
|
54
|
+
## Which proposer: the chooser
|
|
55
55
|
|
|
56
56
|
Every optimizer is a factory `xProposer(opts): SurfaceProposer`, all exported from `@tangle-network/agent-eval/campaign`.
|
|
57
57
|
Start at the top row and only move down when the row's *"reach for it when"* matches your failure mode.
|
|
58
58
|
|
|
59
59
|
| Proposer factory | Reach for it when | Surface it edits | Wired to the paved path? |
|
|
60
60
|
|---|---|---|---|
|
|
61
|
-
| `gepaProposer` | You want the strong default: reflective full-surface prompt rewrites, grounded in findings, keeping a Pareto frontier of complementary winners. | prompt string | **Yes
|
|
62
|
-
| `skillOptProposer` | You are editing a structured `SKILL.md`/runbook and want small anchored add/delete/replace patches that preserve earlier rules. | skill/prompt string | **Yes
|
|
63
|
-
| `parameterSweepProposer` | The likely fix is a config knob, not words
|
|
64
|
-
| `fapoProposer` | You want *evidence to decide when to escalate*: try prompt edits first, move to parameters, then to structural code
|
|
61
|
+
| `gepaProposer` | You want the strong default: reflective full-surface prompt rewrites, grounded in findings, keeping a Pareto frontier of complementary winners. | prompt string | **Yes: `improve({ surface: 'prompt' })` default.** Proven live. |
|
|
62
|
+
| `skillOptProposer` | You are editing a structured `SKILL.md`/runbook and want small anchored add/delete/replace patches that preserve earlier rules. | skill/prompt string | **Yes: `improve({ surface: 'skills' })` default.** Not yet proven live. |
|
|
63
|
+
| `parameterSweepProposer` | The likely fix is a config knob, not words: `retrieval.k`, `temperature`, `max_tokens`. You give it candidate patches; it applies them to a JSON surface. | JSON config string | Yes, but you supply the candidate list. |
|
|
64
|
+
| `fapoProposer` | You want *evidence to decide when to escalate*: try prompt edits first, move to parameters, then to structural code: one scoped change per cycle, only escalating when the cheaper level is exhausted. | whatever its level proposers return | Exported; you wire the level proposers. |
|
|
65
65
|
| `compositeProposer` | You want several proposers to share one candidate-generation budget in the same round. It allocates the population by declared weights, preserves member provenance, deduplicates surfaces, and isolates a member failure unless every member fails. | whatever its member proposers return | Exported; you wire the member proposers. |
|
|
66
66
|
| `aceProposer` | You are accumulating hard-won lessons into a playbook and must **never** summarize an old lesson away (append-only, provenance-tagged). | playbook string | Exported. |
|
|
67
67
|
| `memoryCurationProposer` | Same as ACE but you want a compact, deduped, re-ranked memory instead of append-only growth. | memory string | Exported. |
|
|
68
|
-
| `evolutionaryProposer` | You want blind population search (mutate → measure → select) with no reflection over findings
|
|
68
|
+
| `evolutionaryProposer` | You want blind population search (mutate → measure → select) with no reflection over findings: a cheap control or a baseline to beat. | any string | Exported. |
|
|
69
69
|
| `traceAnalystProposer` | Bench-only: race our trace-analysis evidence engine head-to-head inside `compareProposers`. | prompt string | Bench-only. |
|
|
70
70
|
| `haloProposer` | Bench-only: race the external `halo-engine` analysis against ours. | prompt string | Bench-only, external. |
|
|
71
71
|
|
|
72
72
|
Default path: `gepaProposer` for prompts; add `parameterSweepProposer` when a config knob is the suspect; wrap levels in `fapoProposer` when the loop should decide *when* to escalate, or use `compositeProposer` when multiple proposer families must split one fixed population budget.
|
|
73
73
|
|
|
74
|
-
## Composing proposers
|
|
74
|
+
## Composing proposers: four distinct shapes
|
|
75
75
|
|
|
76
76
|
Choose the shape that matches the experiment:
|
|
77
77
|
|
|
78
|
-
1. **Portfolio
|
|
79
|
-
2. **Escalate
|
|
80
|
-
3. **Race
|
|
81
|
-
4. **Plug in
|
|
78
|
+
1. **Portfolio**: `compositeProposer` splits one generation's population across member proposers by fixed weights and returns one provenance-labelled pool.
|
|
79
|
+
2. **Escalate**: `fapoProposer` wraps prompt + parameter + structural levels into one proposer and spends on the cheapest level until evidence says to escalate.
|
|
80
|
+
3. **Race**: `compareProposers` gives proposers separate loops, then re-scores their winners on one holdout and returns per-proposer lift intervals plus pairwise results.
|
|
81
|
+
4. **Plug in**: hand any proposer to `runImprovementLoop({ proposer })`, or use `improve({ surface, generator })` in `@tangle-network/agent-runtime`.
|
|
82
82
|
|
|
83
|
-
### 2 + 4
|
|
83
|
+
### 2 + 4: compose by escalation, then run the improvement loop
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
86
|
import {
|
|
@@ -110,8 +110,8 @@ const proposer = fapoProposer({
|
|
|
110
110
|
}),
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
// dispatchWithSurface scores ONE surface on ONE scenario
|
|
114
|
-
// opaque seam (one LLM call, one worker, or a whole fleet
|
|
113
|
+
// dispatchWithSurface scores ONE surface on ONE scenario: this is the topology-
|
|
114
|
+
// opaque seam (one LLM call, one worker, or a whole fleet: the loop can't tell).
|
|
115
115
|
const dispatchWithSurface = async (surface, scenario, ctx) =>
|
|
116
116
|
runYourAgent({ surface, scenario, signal: ctx.signal }) // returns the artifact to judge
|
|
117
117
|
|
|
@@ -148,7 +148,7 @@ const out = await improve(profile, findings, {
|
|
|
148
148
|
if (out.shipped) deploy(out.profile) // out.lift is the held-out winner − baseline
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
### 3
|
|
151
|
+
### 3: race proposers head-to-head for a lift CI
|
|
152
152
|
|
|
153
153
|
```ts
|
|
154
154
|
import {
|
|
@@ -186,19 +186,19 @@ const comparison = await compareProposers({
|
|
|
186
186
|
})
|
|
187
187
|
|
|
188
188
|
comparison.best // highest-lift proposer
|
|
189
|
-
comparison.scores // per-proposer { lift, liftCi:{low,high}, cost }
|
|
189
|
+
comparison.scores // per-proposer { lift, liftCi:{low,high}, cost }: low>0 ⇒ real gain
|
|
190
190
|
comparison.pairwise // best vs each other, paired-bootstrap: 'a' | 'b' | 'tie'
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Every entrant is re-scored on the **same** holdout with the **same** judges, so the comparison never trusts how a proposer measured itself
|
|
193
|
+
Every entrant is re-scored on the **same** holdout with the **same** judges, so the comparison never trusts how a proposer measured itself: the only variable is proposal quality.
|
|
194
194
|
|
|
195
195
|
## Common traps (they cost the most)
|
|
196
196
|
|
|
197
|
-
- Do not put eval logic inside a proposer
|
|
198
|
-
- Do not let a proposer read held-out judge scores
|
|
199
|
-
- Do not read `lift` without `result.power`/MDE
|
|
197
|
+
- Do not put eval logic inside a proposer: scoring lives in `dispatch` + `judges`, proposing lives in the proposer.
|
|
198
|
+
- Do not let a proposer read held-out judge scores: `ProposeContext` makes that a compile error on purpose; a proposer that games the acceptance axis is an oracle, not an optimizer.
|
|
199
|
+
- Do not read `lift` without `result.power`/MDE: a "+4" on a valset too small to detect +4 is noise wearing a number.
|
|
200
200
|
- Do not confuse `compositeProposer` with `fapoProposer`: the former allocates one fixed population across peers, while the latter escalates through ordered levels from cheaper to more structural changes.
|
|
201
201
|
|
|
202
|
-
### neutralizationGate
|
|
202
|
+
### neutralizationGate: the placebo / content-causality control
|
|
203
203
|
|
|
204
204
|
Standard gates prove a candidate *beat baseline*. `neutralizationGate` proves the candidate's **content** caused the lift, not the extra prompt bytes: it blanks the candidate's added content to byte-length-matched filler, holds everything else fixed, and requires the lift to vanish. A fully-neutralized candidate that still scores is decorative and is rejected. Exports: `neutralizationGate` (`src/campaign/gates/neutralization-gate.ts`), `neutralizeText` (`src/campaign/neutralize.ts`). Since 0.107.0. Pair it with `heldOutGate` in any loop that promotes authored artifacts (prompts, tool docs, knowledge) so a lift that is really just added prompt size cannot be shipped.
|