@tangle-network/agent-eval 0.123.2 → 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 (65) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +48 -4
  3. package/dist/analyst/index.d.ts +18 -3
  4. package/dist/analyst/index.js +5 -5
  5. package/dist/benchmarks/index.d.ts +10 -3
  6. package/dist/benchmarks/index.js +7 -7
  7. package/dist/campaign/index.d.ts +241 -221
  8. package/dist/campaign/index.js +20 -16
  9. package/dist/{chunk-RQP5UTK5.js → chunk-22VO7T2I.js} +689 -356
  10. package/dist/chunk-22VO7T2I.js.map +1 -0
  11. package/dist/{chunk-EBDOTTZJ.js → chunk-4KO6D2BO.js} +3 -3
  12. package/dist/{chunk-3FCG7FBV.js → chunk-D5JZ7UDZ.js} +2 -2
  13. package/dist/{chunk-BGVTIE2C.js → chunk-GRCDRKII.js} +13 -2
  14. package/dist/chunk-GRCDRKII.js.map +1 -0
  15. package/dist/{chunk-FHFTYX2Q.js → chunk-JKDNAOF5.js} +2 -2
  16. package/dist/{chunk-WXQTVEKM.js → chunk-MHPEGJHC.js} +3 -3
  17. package/dist/{chunk-J7S4YM27.js → chunk-MK3Q6GCL.js} +28 -14
  18. package/dist/chunk-MK3Q6GCL.js.map +1 -0
  19. package/dist/{chunk-DNVPOYUS.js → chunk-N7ZS6PEF.js} +2 -2
  20. package/dist/{chunk-4SOQ4ND2.js → chunk-QFQZ3U3X.js} +1 -1
  21. package/dist/chunk-QFQZ3U3X.js.map +1 -0
  22. package/dist/{chunk-WMJR67FX.js → chunk-QNVBALEX.js} +4 -4
  23. package/dist/{chunk-E3HAD4A3.js → chunk-SJT4OBVL.js} +2 -2
  24. package/dist/{chunk-HQY7LBV2.js → chunk-SUN7QLPB.js} +6 -6
  25. package/dist/{chunk-HQY7LBV2.js.map → chunk-SUN7QLPB.js.map} +1 -1
  26. package/dist/{chunk-5YMKIFYP.js → chunk-WP3GEXKI.js} +3 -3
  27. package/dist/cli.js +3 -3
  28. package/dist/contract/index.d.ts +18 -3
  29. package/dist/contract/index.js +7 -7
  30. package/dist/fuzz.d.ts +8 -1
  31. package/dist/fuzz.js +4 -4
  32. package/dist/index.d.ts +27 -9
  33. package/dist/index.js +14 -12
  34. package/dist/index.js.map +1 -1
  35. package/dist/multishot/index.d.ts +2 -2
  36. package/dist/openapi.json +1 -1
  37. package/dist/rl.d.ts +15 -2
  38. package/dist/rl.js +4 -2
  39. package/dist/rl.js.map +1 -1
  40. package/dist/{run-campaign-ZKR5MQMQ.js → run-campaign-I3JXKVAK.js} +3 -3
  41. package/dist/traces.d.ts +2 -2
  42. package/dist/traces.js +1 -1
  43. package/dist/wire/index.d.ts +16 -1
  44. package/dist/wire/index.js +3 -3
  45. package/docs/campaign-proposers.md +142 -131
  46. package/docs/concepts.md +30 -34
  47. package/docs/design/loop-taxonomy.md +2 -3
  48. package/docs/design.md +0 -1
  49. package/docs/multi-shot-optimization.md +41 -49
  50. package/package.json +1 -1
  51. package/dist/chunk-4SOQ4ND2.js.map +0 -1
  52. package/dist/chunk-BGVTIE2C.js.map +0 -1
  53. package/dist/chunk-J7S4YM27.js.map +0 -1
  54. package/dist/chunk-RQP5UTK5.js.map +0 -1
  55. package/docs/improvement-glossary.md +0 -204
  56. package/docs/self-improvement-map.md +0 -115
  57. /package/dist/{chunk-EBDOTTZJ.js.map → chunk-4KO6D2BO.js.map} +0 -0
  58. /package/dist/{chunk-3FCG7FBV.js.map → chunk-D5JZ7UDZ.js.map} +0 -0
  59. /package/dist/{chunk-FHFTYX2Q.js.map → chunk-JKDNAOF5.js.map} +0 -0
  60. /package/dist/{chunk-WXQTVEKM.js.map → chunk-MHPEGJHC.js.map} +0 -0
  61. /package/dist/{chunk-DNVPOYUS.js.map → chunk-N7ZS6PEF.js.map} +0 -0
  62. /package/dist/{chunk-WMJR67FX.js.map → chunk-QNVBALEX.js.map} +0 -0
  63. /package/dist/{chunk-E3HAD4A3.js.map → chunk-SJT4OBVL.js.map} +0 -0
  64. /package/dist/{chunk-5YMKIFYP.js.map → chunk-WP3GEXKI.js.map} +0 -0
  65. /package/dist/{run-campaign-ZKR5MQMQ.js.map → run-campaign-I3JXKVAK.js.map} +0 -0
@@ -1,204 +0,0 @@
1
- # Improvement glossary + proposer chooser + composition
2
-
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
- > Read this once and you can read any improvement result, pick a proposer, and wire two of them together.
5
-
6
- **Who this is for.**
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: 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
-
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
-
13
- ## The 60-second mental model
14
-
15
- One loop improves one **surface**.
16
- Every generation: a **proposer** reads what failed and emits **candidate** surfaces; a **campaign** measures each candidate by running the agent over **scenarios × reps** and **judging** every run; the loop ranks candidates and re-scores the best on a **holdout**; a **gate** decides whether that beat the **baseline** for real; if yes, it is **promoted**.
17
-
18
- ```text
19
- baseline surface
20
- └─► PROPOSER: read findings ─► N candidate surfaces (one GENERATION)
21
- └─► CAMPAIGN: run agent on scenarios × reps ─► JUDGE each ─► composite
22
- └─► rank candidates ─► re-score best on HOLDOUT
23
- └─► GATE: did it beat baseline (significance)? ship / hold
24
- repeat for maxGenerations
25
- ```
26
-
27
- ## Glossary: one plain sentence each
28
-
29
- Grounded to `agent-eval/src/campaign/types.ts` unless noted.
30
-
31
- | Term | Plain sentence | If you know DSPy/GEPA |
32
- |---|---|---|
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
- | **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
- | **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: 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
- | **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: 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
- | **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: 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
- | **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: 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
- | **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: 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
- | **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: 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
-
54
- ## Which proposer: the chooser
55
-
56
- Every optimizer is a factory `xProposer(opts): SurfaceProposer`, all exported from `@tangle-network/agent-eval/campaign`.
57
- Start at the top row and only move down when the row's *"reach for it when"* matches your failure mode.
58
-
59
- | Proposer factory | Reach for it when | Surface it edits | Wired to the paved path? |
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: `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
- | `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
- | `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
- | `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: a cheap control or a baseline to beat. | any string | Exported. |
69
- | `traceAnalystProposer` | Bench-only: race our trace-analysis evidence engine head-to-head inside `compareProposers`. | prompt string | Bench-only. |
70
- | `haloProposer` | Bench-only: race the external `halo-engine` analysis against ours. | prompt string | Bench-only, external. |
71
-
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
-
74
- ## Composing proposers: four distinct shapes
75
-
76
- Choose the shape that matches the experiment:
77
-
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
-
83
- ### 2 + 4: compose by escalation, then run the improvement loop
84
-
85
- ```ts
86
- import {
87
- fapoProposer,
88
- gepaProposer,
89
- parameterSweepProposer,
90
- defaultProductionGate,
91
- runImprovementLoop,
92
- } from '@tangle-network/agent-eval/campaign'
93
-
94
- const llm = { baseUrl: process.env.TANGLE_BASE_URL, apiKey: process.env.TANGLE_API_KEY }
95
- const model = 'deepseek-v4-flash'
96
-
97
- // Compose by escalation: prompt edits first (GEPA), then a config knob only
98
- // when prompt-level search plateaus. This is distinct from a peer portfolio.
99
- const proposer = fapoProposer({
100
- scope: { allowedLevels: ['prompt', 'parameter'] }, // no structural/code tier here
101
- promptProposer: gepaProposer({ llm, model, target: 'agent system prompt' }),
102
- parameterProposer: parameterSweepProposer({
103
- candidates: [
104
- {
105
- label: 'raise-retrieval-k',
106
- rationale: 'retrieval-miss findings suggest the search budget is too low',
107
- changes: [{ path: 'retrieval.k', value: 10 }],
108
- },
109
- ],
110
- }),
111
- })
112
-
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
- const dispatchWithSurface = async (surface, scenario, ctx) =>
116
- runYourAgent({ surface, scenario, signal: ctx.signal }) // returns the artifact to judge
117
-
118
- const result = await runImprovementLoop({
119
- scenarios: trainScenarios, // proposer trains on these
120
- holdoutScenarios, // winner is re-scored here; proposer never sees them
121
- baselineSurface: currentPromptString,
122
- dispatchWithSurface,
123
- judges, // JudgeConfig[]
124
- proposer,
125
- gate: defaultProductionGate({ holdoutScenarios, deltaThreshold: 0 }),
126
- populationSize: 2, // BREADTH per generation
127
- maxGenerations: 6, // DEPTH
128
- autoOnPromote: 'none', // 'pr' to open a PR on ship
129
- runDir: '/tmp/improve-run', // a REAL path makes the run durable
130
- })
131
-
132
- result.winnerSurface // the promoted surface
133
- result.gateDecision // 'ship' | 'hold' | 'need_more_work' | 'model_ceiling' | 'arch_ceiling'
134
- ```
135
-
136
- Same thing, one line, when you have an `AgentProfile` (facade in `@tangle-network/agent-runtime`):
137
-
138
- ```ts
139
- import { improve } from '@tangle-network/agent-runtime'
140
-
141
- // surface 'prompt'/'skills' pick their proposer automatically; pass `generator`
142
- // to plug the composed FAPO proposer above into the gated loop instead.
143
- const out = await improve(profile, findings, {
144
- surface: 'prompt',
145
- generator: proposer, // any SurfaceProposer, incl. the composed one
146
- scenarios, judge, agent, runDir: '/tmp/improve-run',
147
- })
148
- if (out.shipped) deploy(out.profile) // out.lift is the held-out winner − baseline
149
- ```
150
-
151
- ### 3: race proposers head-to-head for a lift CI
152
-
153
- ```ts
154
- import {
155
- compareProposers,
156
- gepaParetoEntry,
157
- fapoEscalationEntry,
158
- } from '@tangle-network/agent-eval/campaign'
159
-
160
- const config = {
161
- baselineSurface: currentPromptString,
162
- trainScenarios,
163
- holdoutScenarios,
164
- dispatchWithSurface,
165
- judges,
166
- llm, model,
167
- target: 'agent system prompt',
168
- runDir: '/tmp/compare-run',
169
- }
170
-
171
- const comparison = await compareProposers({
172
- proposers: [
173
- gepaParetoEntry(config), // GEPA + Pareto frontier
174
- fapoEscalationEntry({ // FAPO escalation policy
175
- ...config,
176
- parameterCandidates: [
177
- { label: 'raise-retrieval-k', rationale: 'retrieval misses', changes: [{ path: 'retrieval.k', value: 10 }] },
178
- ],
179
- }),
180
- ],
181
- baselineSurface: currentPromptString,
182
- holdoutScenarios,
183
- dispatchWithSurface,
184
- judges,
185
- runDir: '/tmp/compare-run',
186
- })
187
-
188
- comparison.best // highest-lift proposer
189
- comparison.scores // per-proposer { lift, liftCi:{low,high}, cost }: low>0 ⇒ real gain
190
- comparison.pairwise // best vs each other, paired-bootstrap: 'a' | 'b' | 'tie'
191
- ```
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: the only variable is proposal quality.
194
-
195
- ## Common traps (they cost the most)
196
-
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
- - 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
-
202
- ### neutralizationGate: the placebo / content-causality control
203
-
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.
@@ -1,115 +0,0 @@
1
- # The self-improvement map
2
-
3
- One loop. Four roles. A proposer catalog of pluggable strategies. A bench rig that
4
- proves the loop produces real lift. Nothing here is duplicated: it is one engine
5
- pointed at different surfaces. This map exists because the surface count makes it
6
- *look* like many competing systems when it is one.
7
-
8
- Neighbors: [`concepts.md`](./concepts.md) (the mental model), [`trace-analysis.md`](./trace-analysis.md)
9
- (the evidence engine), [`distributed-driver.md`](./distributed-driver.md) (running the loop across cells).
10
-
11
- ## The one loop
12
-
13
- `runImprovementLoop()` (wrapped by `selfImprove()` for the one-call surface). Every
14
- product imports the same function. Each generation it does four things:
15
-
16
- ```
17
- run AGENT on SCENARIOS ──► JUDGE scores each run
18
-
19
- PROPOSER reads the failures and
20
- proposes better SURFACE versions
21
-
22
- GATE: did a candidate beat the parent on a
23
- HELD-OUT split, for real (significance test)?
24
- │ yes → promote │ no → discard
25
-
26
- repeat N generations
27
- ```
28
-
29
- ## The four roles: keep them separate and the confusion clears
30
-
31
- | Role | What it is | Plain meaning |
32
- |---|---|---|
33
- | **Surface** | a *string*: an agent directive, a `SKILL.md`, a playbook, a memory, a judge rubric | **what** gets improved |
34
- | **Proposer** | a `SurfaceProposer` (the catalog below) | **how** candidate surfaces are proposed |
35
- | **Gate** | held-out split + significance (`paretoSignificanceGate` / `heldOutGate` / `defaultProductionGate`) | **did it actually get better**, vs noise |
36
- | **Judge** | scores a run | **how good** any version is |
37
-
38
- ## The proposer catalog (one loop, multiple strategies)
39
-
40
- The package intentionally exposes named proposer factories instead of a hidden
41
- auto-selector. The split that matters: **production proposers** mutate a live
42
- surface; **bench-only proposers** exist solely to be raced inside
43
- `compareProposers`.
44
-
45
- | Proposer factory | Surface | Strategy | Role | Notes |
46
- |---|---|---|---|---|
47
- | `gepaProposer` | prompt | reflective full-surface rewrite + Pareto frontier | **production default** | consumes trace-analysis findings: see below |
48
- | `fapoProposer` | prompt/config/code | reviewed escalation policy over prompt → parameter → structural proposers | production, benchmark | encodes FAPO's scope + reviewer + prompt-first escalation rules; structural generator is injected |
49
- | `parameterSweepProposer` | config | JSON config patch/sweep | production, benchmark | middle FAPO level for parameter/config edits such as `retrieval.k`, `temperature`, `max_tokens` |
50
- | `skillOptProposer` | skill-doc | anchored add/delete/replace patch | production | preserves earlier rules; edit budget = "textual learning rate" |
51
- | `aceProposer` | playbook | append-only, provenance-tagged | production | accumulate hard-won lessons, never summarize away |
52
- | `memoryCurationProposer` | memory | dedup + rank + graft | production | compact alternative to `ace` |
53
- | `evolutionaryProposer` | any | population mutate → measure → select | production | blind search; no reflection over findings |
54
- | `traceAnalystProposer` | prompt | analysis → one LLM edit | **bench-only** | our evidence engine, wrapped as a proposer |
55
- | `haloProposer` | prompt | analysis → one LLM edit | **bench-only**, external | wraps `pip install halo-engine` (Inference.net) |
56
-
57
- Default choice: start with `gepaProposer` for prompt surfaces, add
58
- `parameterSweepProposer` when config knobs are the likely failure mode, and wrap
59
- them with `fapoProposer` when evidence should decide when to escalate.
60
-
61
- ## Trace analysis: what it is and the three places it is used
62
-
63
- "Trace analysis" is the **evidence layer**: it turns raw OTLP traces into "here is
64
- exactly *why* the agent failed" (failure clusters → findings). The engine is
65
- `analyzeRuns()` + the analyst registry (`src/contract/analyze-runs.ts`). It is used in
66
- three places: this is the answer to "if GEPA does its own thing, what is trace
67
- analysis *for*?":
68
-
69
- 1. **Ships to customers**: `analyzeRuns()` → `InsightReport`, the Intelligence product.
70
- 2. **Feeds the proposer**: `gepaProposer` calls `renderAnalystEvidence(ctx.findings,
71
- ctx.report)` (`src/campaign/proposers/gepa.ts`). GEPA's rewrites are grounded
72
- in the diagnosis instead of guessing blind. Trace analysis
73
- **is** on the GEPA side.
74
- 3. **Races HALO**: wrapped as `traceAnalystProposer` so our analysis competes
75
- head-to-head with the external SOTA inside `compareProposers`.
76
-
77
- ## Where HALO fits (and why it feels "removed")
78
-
79
- `haloProposer` is alive (`src/campaign/proposers/halo.ts`, exported from the campaign
80
- barrel) but it is **never in the product loop**. It shells out to an *external* engine
81
- (`halo-engine`): so the analysis genuinely lives outside this repo; we only wrap it.
82
-
83
- Its only job is the **bake-off**. HALO's real opponent is **not** `gepaProposer`: it is
84
- `traceAnalystProposer`. `compareProposers` holds the apply step identical (same
85
- `APPLY_SYSTEM`, same `traces.jsonl`, same held-out scoring) so the only variable is
86
- **analysis quality: HALO vs ours.** A measuring stick, like a benchmark baseline.
87
-
88
- ## `gepa-refine.ts` is the loop on a test bench, not a second loop
89
-
90
- `agent-runtime/bench/src/gepa-refine.ts` runs **this same loop** against a *public
91
- benchmark* (AppWorld, CAD, …) instead of product data. Why a separate rig:
92
-
93
- - On product traces, "+4 lift" can be model noise or a judge flattering itself: no
94
- ground truth.
95
- - On a benchmark the score is **objective and ungameable** (AppWorld runs the agent's
96
- code against its own unit tests). If a GEPA-optimized directive beats a deliberately
97
- weak baseline **on held-out tasks it never trained on**, with a CI excluding zero,
98
- that is a *certified* proof the loop produces real lift.
99
-
100
- Products run the loop to get **better**. `gepa-refine` runs the identical loop to
101
- **prove the loop works at all**.
102
-
103
- ## Where the "mess" feeling actually comes from
104
-
105
- The code is well-factored; the confusion is narrative:
106
-
107
- - **Surface sprawl reads as chaos.** Several proposers with overlapping shapes *look* like
108
- competing loops. They are exported factories for one loop; this map makes that
109
- explicit.
110
- - **The real gap is the missing proof, not the design.** The loop kept being proved on
111
- benchmarks too easy to show value: when a capable model ceilings an extraction task,
112
- **0 findings fire** and the whole trace-analysis→optimizer apparatus is inert. It
113
- earns its keep only on **hard agentic tasks**: which is why the AppWorld REPL run
114
- (multi-turn, real tool execution, unbounded turns) is the one that can finally
115
- separate the evidence-grounded optimizer from baseline.