@tangle-network/agent-eval 0.125.0 → 0.126.1

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 (82) hide show
  1. package/CHANGELOG.md +62 -35
  2. package/README.md +270 -189
  3. package/dist/analyst/index.d.ts +15 -145
  4. package/dist/analyst/index.js +33 -47
  5. package/dist/analyst/index.js.map +1 -1
  6. package/dist/benchmarks/index.d.ts +45 -162
  7. package/dist/benchmarks/index.js +8 -9
  8. package/dist/campaign/index.d.ts +3674 -5393
  9. package/dist/campaign/index.js +21 -95
  10. package/dist/{chunk-R226UZOI.js → chunk-474LBSOX.js} +2 -2
  11. package/dist/{chunk-HM6V7F3M.js → chunk-FO7HEH76.js} +3 -3
  12. package/dist/chunk-IILEIWGW.js +635 -0
  13. package/dist/chunk-IILEIWGW.js.map +1 -0
  14. package/dist/{chunk-EQUK3RFS.js → chunk-J5SQWP6Y.js} +8 -5
  15. package/dist/chunk-J5SQWP6Y.js.map +1 -0
  16. package/dist/{chunk-W5B3ZGP3.js → chunk-KE2VWPZX.js} +8 -6
  17. package/dist/{chunk-W5B3ZGP3.js.map → chunk-KE2VWPZX.js.map} +1 -1
  18. package/dist/{chunk-DT7OXY3C.js → chunk-LUNF2SEL.js} +538 -851
  19. package/dist/chunk-LUNF2SEL.js.map +1 -0
  20. package/dist/chunk-NGUYT5CI.js +4637 -0
  21. package/dist/chunk-NGUYT5CI.js.map +1 -0
  22. package/dist/{chunk-QFQZ3U3X.js → chunk-OCFJACJU.js} +2 -2
  23. package/dist/{chunk-GID26AN4.js → chunk-P22LJ3Y2.js} +4 -6
  24. package/dist/{chunk-GID26AN4.js.map → chunk-P22LJ3Y2.js.map} +1 -1
  25. package/dist/{chunk-SJT4OBVL.js → chunk-SDPM6554.js} +3 -3
  26. package/dist/{chunk-D5JZ7UDZ.js → chunk-UCLVDLCH.js} +136 -50
  27. package/dist/chunk-UCLVDLCH.js.map +1 -0
  28. package/dist/chunk-VMUENW6F.js +7274 -0
  29. package/dist/chunk-VMUENW6F.js.map +1 -0
  30. package/dist/{chunk-JKDNAOF5.js → chunk-W4L6C2XT.js} +2 -2
  31. package/dist/chunk-WGXIEX7P.js +116 -0
  32. package/dist/chunk-WGXIEX7P.js.map +1 -0
  33. package/dist/{chunk-GRCDRKII.js → chunk-WS3NZZQQ.js} +58 -20
  34. package/dist/chunk-WS3NZZQQ.js.map +1 -0
  35. package/dist/cli.js +3 -3
  36. package/dist/contract/index.d.ts +3220 -3094
  37. package/dist/contract/index.js +173 -42
  38. package/dist/contract/index.js.map +1 -1
  39. package/dist/control.js +2 -3
  40. package/dist/fuzz.d.ts +14 -1
  41. package/dist/fuzz.js +1 -1
  42. package/dist/hosted/index.d.ts +8 -1
  43. package/dist/index.d.ts +71 -687
  44. package/dist/index.js +178 -497
  45. package/dist/index.js.map +1 -1
  46. package/dist/openapi.json +1 -1
  47. package/dist/rl.d.ts +5 -100
  48. package/dist/rl.js +4 -5
  49. package/dist/rl.js.map +1 -1
  50. package/dist/{run-campaign-I3JXKVAK.js → run-campaign-LVFKZCEU.js} +3 -3
  51. package/dist/traces.js +2 -3
  52. package/dist/wire/index.d.ts +14 -1
  53. package/dist/wire/index.js +3 -3
  54. package/docs/campaign-proposers.md +363 -168
  55. package/docs/design/loop-taxonomy.md +142 -190
  56. package/docs/design.md +1 -1
  57. package/docs/distributed-driver.md +8 -11
  58. package/docs/feature-guide.md +20 -19
  59. package/docs/knowledge-readiness.md +2 -5
  60. package/docs/multi-shot-optimization.md +35 -27
  61. package/docs/rollout.md +5 -5
  62. package/package.json +4 -4
  63. package/dist/chunk-A62YMFWA.js +0 -9269
  64. package/dist/chunk-A62YMFWA.js.map +0 -1
  65. package/dist/chunk-A6GT67HT.js +0 -550
  66. package/dist/chunk-A6GT67HT.js.map +0 -1
  67. package/dist/chunk-D5JZ7UDZ.js.map +0 -1
  68. package/dist/chunk-DT7OXY3C.js.map +0 -1
  69. package/dist/chunk-EQUK3RFS.js.map +0 -1
  70. package/dist/chunk-GC4ATIKK.js +0 -317
  71. package/dist/chunk-GC4ATIKK.js.map +0 -1
  72. package/dist/chunk-GRCDRKII.js.map +0 -1
  73. package/dist/chunk-LOW3U7JZ.js +0 -328
  74. package/dist/chunk-LOW3U7JZ.js.map +0 -1
  75. package/dist/chunk-PMITBABE.js +0 -3841
  76. package/dist/chunk-PMITBABE.js.map +0 -1
  77. /package/dist/{chunk-R226UZOI.js.map → chunk-474LBSOX.js.map} +0 -0
  78. /package/dist/{chunk-HM6V7F3M.js.map → chunk-FO7HEH76.js.map} +0 -0
  79. /package/dist/{chunk-QFQZ3U3X.js.map → chunk-OCFJACJU.js.map} +0 -0
  80. /package/dist/{chunk-SJT4OBVL.js.map → chunk-SDPM6554.js.map} +0 -0
  81. /package/dist/{chunk-JKDNAOF5.js.map → chunk-W4L6C2XT.js.map} +0 -0
  82. /package/dist/{run-campaign-I3JXKVAK.js.map → run-campaign-LVFKZCEU.js.map} +0 -0
@@ -1,247 +1,442 @@
1
- # Candidate Generation and Method Comparison
1
+ # Optimization Methods And Candidate Generators
2
2
 
3
- `SurfaceProposer` generates candidate prompts or configs.
4
- It does not run an agent, score output, or choose a winner.
3
+ Agent Eval supports two different extension points.
5
4
 
6
- `OptimizationMethod` runs a complete search procedure on train and selection data.
7
- It returns one selected surface plus its optimization cost.
5
+ An `OptimizationMethod` owns a complete search procedure and returns one selected surface.
6
+ Use it for official GEPA, official SkillOpt, or another optimizer with its own search and selection behavior.
8
7
 
9
- `compareOptimizationMethods` runs multiple methods, waits for every method to finish, then scores their selected surfaces on the same final test data.
8
+ A `SurfaceProposer` suggests candidates inside Agent Eval's campaign loop.
9
+ Use it for caller-defined logic, an agent-runtime worker, or declared parameter combinations.
10
10
 
11
- | API | Responsibility |
12
- |---|---|
13
- | `SurfaceProposer` | Suggest the next candidate surface. |
14
- | `runOptimization` | Run and score candidates on training scenarios. |
15
- | `runImprovementLoop` | Optimize one surface and apply a release rule on separate scenarios. |
16
- | `OptimizationMethod` | Adapt one complete optimization procedure for comparison. |
17
- | `compareOptimizationMethods` | Compare selected surfaces on shared final test data. |
11
+ Do not wrap a complete external optimizer in `SurfaceProposer`.
12
+ That would split its search state from its own selection behavior and make budgets harder to compare.
13
+
14
+ ## Adapt A Third-Party Text Optimizer
18
15
 
19
- ## Candidate Generators
16
+ `externalTextOptimizationMethod()` is the general adapter for a package that already owns text or component search.
17
+ The starting candidate is a string for a text surface or a `Record<string, string>` for named components.
18
+ The returned candidate must keep the same form.
20
19
 
21
- Every `SurfaceProposer.propose(ctx)` receives the current surface, prior candidate scores, findings, requested candidate count, generation number, and cancellation signal.
22
- It may also receive a larger analysis report, a labeled scenario store, or Pareto parents when the caller provides them.
20
+ The `run` callback receives only `trainSet` and `selectionSet`.
21
+ It does not receive final test cases.
22
+ Pass `context.evaluate` to the optimizer so every candidate is executed and scored by the configured Agent Eval path.
23
+ Unknown case IDs and calls beyond `maxEvaluations` are rejected.
23
24
 
24
- A proposer may return a bare surface or a labeled candidate:
25
+ Optimizer-owned model or service calls must use `context.cost.runPaidCall()`.
26
+ The example below assumes the upstream package enforces `maxCostUsd` for the complete run and returns aggregate usage.
27
+ If the package exposes a model callback instead, wrap each model call separately with the same cost ledger and phase.
25
28
 
26
29
  ```ts
27
- {
28
- surface: 'the complete new prompt or config',
29
- label: 'require-citations',
30
- rationale: 'three training failures omitted source references',
30
+ import { externalTextOptimizationMethod } from '@tangle-network/agent-eval/campaign'
31
+ import { optimize } from 'your-text-optimizer'
32
+
33
+ interface SupportCase {
34
+ id: string
35
+ kind: 'support'
36
+ question: string
31
37
  }
32
- ```
33
38
 
34
- Use a labeled `ProposedCandidate` when you need the result to retain why the candidate was generated.
39
+ interface SupportArtifact {
40
+ answer: string
41
+ }
42
+
43
+ const method = externalTextOptimizationMethod<SupportCase, SupportArtifact>({
44
+ name: 'your-text-optimizer',
45
+ source: {
46
+ kind: 'package',
47
+ package: 'your-text-optimizer',
48
+ version: '2.3.1',
49
+ sourceUrl: 'https://github.com/your-org/your-text-optimizer',
50
+ revision: '4f17c2a',
51
+ },
52
+ objective: 'Improve answer accuracy and citation quality.',
53
+ evaluationId: 'support-quality',
54
+ maxEvaluations: 60,
55
+ maxOptimizerCostUsd: 2,
56
+ resume: 'if-compatible',
57
+ describeScenario: (scenario) => ({ question: scenario.question }),
58
+ describeArtifact: (artifact) => ({ answer: artifact.answer }),
59
+ run: async (context) => {
60
+ const paid = await context.cost.runPaidCall({
61
+ actor: context.name,
62
+ model: 'your-text-optimizer',
63
+ maximumCharge: { externallyEnforcedMaximumUsd: 2 },
64
+ execute: (signal) =>
65
+ optimize({
66
+ initialCandidate: context.seedCandidate,
67
+ train: context.trainSet,
68
+ selection: context.selectionSet,
69
+ evaluate: context.evaluate,
70
+ maxEvaluations: context.maxEvaluations,
71
+ maxCostUsd: 2,
72
+ seed: context.seed,
73
+ stateDir: context.stateDir,
74
+ resume: context.restoreRequested,
75
+ artifactDir: context.artifactDir,
76
+ signal,
77
+ }),
78
+ receipt: (result) => ({
79
+ model: 'your-text-optimizer',
80
+ inputTokens: result.usage.inputTokens,
81
+ outputTokens: result.usage.outputTokens,
82
+ actualCostUsd: result.usage.costUsd,
83
+ }),
84
+ })
85
+ if (!paid.succeeded) throw paid.error
86
+ if (context.restoreRequested && !paid.value.resumed) {
87
+ throw new Error('The optimizer could not restore the requested compatible state.')
88
+ }
89
+ return {
90
+ bestCandidate: paid.value.bestCandidate,
91
+ resumed: context.restoreRequested,
92
+ costAccounting: { kind: 'metered' },
93
+ }
94
+ },
95
+ })
96
+ ```
35
97
 
36
- | Factory | Use it for | Surface |
37
- |---|---|---|
38
- | `gepaProposer` | Rewrite a prompt from prior scores and findings. | string |
39
- | `skillOptProposer` | Apply bounded edits to a structured skill or runbook. | string |
40
- | `aceProposer` | Append distinct lessons from findings. | string |
41
- | `memoryCurationProposer` | Deduplicate and compact lessons from findings. | string |
42
- | `parameterSweepProposer` | Apply declared changes to a JSON config. | JSON string |
43
- | `fapoProposer` | Try prompt, parameter, and optional structural changes under one escalation policy. | caller-defined |
98
+ Replace the import and field names with the upstream package API.
99
+ `source` records caller-declared package identity.
100
+ `evaluationId` identifies the execution and scoring behavior; use a commit, content hash, or another stable identifier and change it whenever that behavior changes.
101
+ Agent Eval derives the run ID from the complete compatible input instead of requiring a private schema number.
102
+ The callback writes checkpoints under `stateDir`, restores them only when `restoreRequested` is true, and reports whether restoration occurred.
103
+ Agent Eval adds the run ID, evaluation count, artifact directory, source identity, and optimizer token usage to the method result.
44
104
 
45
105
  ## Compare Complete Methods
46
106
 
47
- The following call owns the shared baseline, runner, judges, directories, and three data sets.
48
- Method configuration contains only settings that differ by method.
107
+ `compareOptimizationMethods()` gives every method the same:
108
+
109
+ - starting surface,
110
+ - train cases,
111
+ - selection cases,
112
+ - execution function,
113
+ - judges,
114
+ - seed,
115
+ - campaign defaults.
116
+
117
+ An optimization method never receives the final test cases.
118
+ After every method finishes, Agent Eval scores the selected surfaces on the same final cases and reports paired lift estimates.
49
119
 
50
120
  ```ts
51
121
  import {
52
- type BuiltinOptimizationMethodConfig,
53
122
  compareOptimizationMethods,
54
- gepaParetoMethod,
55
- gepaReflectionMethod,
56
- skillOptMethod,
123
+ gepaOptimizationMethod,
124
+ skillOptOptimizationMethod,
57
125
  } from '@tangle-network/agent-eval/campaign'
58
126
 
59
- const methodConfig: BuiltinOptimizationMethodConfig<MyScenario, MyArtifact> = {
60
- llm,
61
- model,
62
- target: 'the complete prompt or config being improved',
63
- populationSize: 2,
64
- maxGenerations: 3,
65
- maxEpochs: 6,
127
+ const optimizer = {
128
+ model: 'gpt-4.1-mini',
129
+ baseUrl: 'https://api.openai.com/v1',
130
+ apiKey: process.env.OPENAI_API_KEY!,
131
+ budget: {
132
+ maxCostUsd: 5,
133
+ maxRequests: 100,
134
+ maxRequestBytes: 2_000_000,
135
+ maxResponseBytes: 2_000_000,
136
+ maxOutputTokensPerRequest: 32_768,
137
+ pricing: {
138
+ inputUsdPerMillion: Number(process.env.OPTIMIZER_INPUT_USD_PER_MILLION),
139
+ outputUsdPerMillion: Number(process.env.OPTIMIZER_OUTPUT_USD_PER_MILLION),
140
+ },
141
+ },
66
142
  }
67
143
 
68
- const comparison = await compareOptimizationMethods<MyScenario, MyArtifact>({
69
- methods: [
70
- gepaReflectionMethod(methodConfig),
71
- gepaParetoMethod(methodConfig),
72
- skillOptMethod(methodConfig),
73
- ],
144
+ const gepa = gepaOptimizationMethod<MyCase, MyArtifact>({
145
+ name: 'gepa',
146
+ objective: 'Improve the instructions so the agent emits valid JSON.',
147
+ evaluationId: 'json-agent',
148
+ recipe: {
149
+ kind: 'engine',
150
+ run: {
151
+ engine: 'gepa',
152
+ maxEvaluations: 40,
153
+ maxProposerCostUsd: 5,
154
+ },
155
+ },
156
+ optimizer,
157
+ describeScenario: (scenario) => ({ input: scenario.input }),
158
+ describeArtifact: (artifact) => ({ output: artifact.output }),
159
+ })
160
+
161
+ const skillopt = skillOptOptimizationMethod<MyCase, MyArtifact>({
162
+ name: 'skillopt',
163
+ objective: 'Improve the instructions so the agent emits valid JSON.',
164
+ evaluationId: 'json-agent',
165
+ trainer: {
166
+ epochs: 2,
167
+ batchSize: 4,
168
+ },
169
+ optimizer,
170
+ maxEvaluations: 80,
171
+ describeScenario: (scenario) => ({ input: scenario.input }),
172
+ describeArtifact: (artifact) => ({ output: artifact.output }),
173
+ })
174
+
175
+ const comparison = await compareOptimizationMethods({
176
+ methods: [gepa, skillopt],
74
177
  baselineSurface,
75
178
  trainScenarios,
76
179
  selectionScenarios,
77
180
  testScenarios,
78
181
  dispatchWithSurface,
79
182
  judges,
80
- runDir,
183
+ runDir: '.agent-eval/optimizer-comparison',
81
184
  optimizationRunOptions: {
82
- costCeiling: 5,
83
- dispatchTimeoutMs: 60_000,
84
185
  maxConcurrency: 4,
85
186
  },
86
- optimizationConcurrency: 2,
87
- costCeiling: 2,
88
- maxConcurrency: 4,
187
+ costCeiling: 23,
89
188
  confidence: 0.95,
90
189
  })
91
190
  ```
92
191
 
93
- The runnable version is [`examples/compare-optimization-methods`](../examples/compare-optimization-methods/).
192
+ `costCeiling` is one limit shared by optimizer-model calls, train and selection evaluations, and final test scoring.
193
+ `comparison.scores` contains the final-case baseline score, selected score, lift, simultaneous interval, cost status, duration, and selected surface for each method.
194
+ Official method scores contain optimizer and bridge package versions, source revisions and source-tree hashes, Python runtime, custom engine module hashes, compatible run ID, exact attempt ID, resume status, evaluation count, artifact directory, and available optimizer token usage.
195
+ `comparison.pairwise` compares the highest-ranked method with every other method.
196
+ Ranking follows estimated lift, so inspect intervals before claiming a difference.
197
+
198
+ The runnable version is in [`examples/compare-optimization-methods`](../examples/compare-optimization-methods/).
199
+
200
+ ## Install Official GEPA
201
+
202
+ Install the bridge and the source revision tested by this release:
94
203
 
95
- ## External GEPA
204
+ ```sh
205
+ python -m pip install agent-eval-rpc
206
+ python -m pip install "gepa[full] @ git+https://github.com/gepa-ai/gepa.git@f919db0a622e2e9f9204779b81fe00cc1b2d808f"
207
+ ```
96
208
 
97
- `gepaOptimizationMethod()` passes a text surface, such as a prompt or JSON retrieval policy, to GEPA's own `optimize_anything` API.
98
- GEPA remains the source of truth for built-in and caller-registered engine names.
99
- It does not edit a repository, ingest a knowledge base, or replace a retrieval engine.
209
+ The published `gepa==0.1.4` wheel does not contain the required Optimize Anything API.
210
+ The Agent Eval Python package cannot declare a Git dependency in its PyPI metadata, so the GEPA source install is separate.
100
211
 
101
- Install the optional Python bridge first:
212
+ From this repository:
102
213
 
103
214
  ```sh
104
- pip install 'agent-eval-rpc[gepa]'
215
+ cd clients/python
216
+ uv sync --frozen --group gepa-source
105
217
  ```
106
218
 
107
- The extra pins a GEPA source commit because the published `gepa==0.1.4` package does not contain this multi-engine API.
108
- Update the pin only after repeating the integration smoke and comparison benchmarks.
219
+ ## Configure GEPA
220
+
221
+ `gepaOptimizationMethod()` accepts text surfaces and component surfaces.
222
+ A component surface has this shape:
109
223
 
110
224
  ```ts
111
- import { gepaOptimizationMethod } from '@tangle-network/agent-eval/campaign'
225
+ const baselineSurface = {
226
+ kind: 'components' as const,
227
+ components: {
228
+ planner: 'Plan the task.',
229
+ executor: 'Execute the plan.',
230
+ },
231
+ }
232
+ ```
112
233
 
113
- const externalGepa = gepaOptimizationMethod<MyScenario, MyArtifact>({
234
+ The `recipe` maps directly to official GEPA operations:
235
+
236
+ | Recipe | Official behavior |
237
+ |---|---|
238
+ | `engine` | Run one registered GEPA engine. |
239
+ | `sequential` | Run engines in order and retain the best result across stages. |
240
+ | `adaptive-sequential` | Switch engines after a configured period without improvement. |
241
+ | `best-of` | Run independent engines and choose the highest selection score. |
242
+ | `vote` | Run independent engines and use GEPA's vote composition. |
243
+ | `omni` | Run official best-of exploration, then continue from its winner. |
244
+
245
+ Each engine run requires `maxEvaluations` and `maxProposerCostUsd`.
246
+ `engineConfig` carries the JSON-safe subset of configuration for the registered GEPA engine.
247
+ GEPA validates the engine name and those values.
248
+ Python callables, classes, custom loggers, and callbacks cannot be serialized through this TypeScript bridge.
249
+ For a custom engine, set `engineModules` to public dotted Python modules that call GEPA's official `register_engine()` function when imported.
250
+ The optimizer process imports those modules before GEPA resolves the engine name.
251
+
252
+ The standard GEPA engine accepts the official `GEPAConfig` fields.
253
+ Give Agent Eval the model, exact endpoint rates, and provider connection separately:
254
+
255
+ ```ts
256
+ const method = gepaOptimizationMethod({
257
+ objective: 'Improve the complete system prompt.',
258
+ evaluationId: 'support-agent',
114
259
  recipe: {
115
- kind: 'best-of-then-continue',
116
- explore: [
117
- { engine: 'gepa', maxEvaluations: 6, maxProposerCostUsd: 5 },
118
- { engine: 'autoresearch', maxEvaluations: 6, maxProposerCostUsd: 5 },
119
- { engine: 'meta_harness', maxEvaluations: 6, maxProposerCostUsd: 5 },
120
- ],
121
- continueWith: { engine: 'gepa', maxEvaluations: 6, maxProposerCostUsd: 5 },
260
+ kind: 'engine',
261
+ run: {
262
+ engine: 'gepa',
263
+ maxEvaluations: 60,
264
+ maxProposerCostUsd: 8,
265
+ maxConcurrency: 8,
266
+ },
122
267
  },
123
- objective: 'Return a better retrieval policy as JSON.',
124
- describeScenario: (scenario) => ({ id: scenario.id }),
268
+ optimizer: {
269
+ model: 'gpt-4.1-mini',
270
+ baseUrl: 'https://api.openai.com/v1',
271
+ apiKey: process.env.OPENAI_API_KEY!,
272
+ budget: {
273
+ maxCostUsd: 8,
274
+ maxRequests: 100,
275
+ maxRequestBytes: 2_000_000,
276
+ maxResponseBytes: 2_000_000,
277
+ maxOutputTokensPerRequest: 32_768,
278
+ pricing: {
279
+ inputUsdPerMillion: 0.4,
280
+ outputUsdPerMillion: 1.6,
281
+ },
282
+ },
283
+ },
284
+ describeScenario: (scenario) => ({ input: scenario.input }),
285
+ describeArtifact: (artifact) => ({ output: artifact.output }),
125
286
  })
287
+ ```
126
288
 
127
- const comparison = await compareOptimizationMethods({
128
- methods: [gepaParetoMethod(methodConfig), externalGepa],
129
- baselineSurface,
130
- trainScenarios,
131
- selectionScenarios,
132
- testScenarios,
133
- dispatchWithSurface,
134
- judges,
135
- runDir,
136
- })
289
+ Replace the rates with the exact rates charged by your endpoint.
290
+ With `optimizer`, every recipe stage must use the standard `gepa` engine.
291
+ Agent Eval keeps the provider key outside Python, enforces the shared model budget, and records exact provider usage.
292
+ `maxProposerCostUsd` also limits each individual GEPA engine stage.
293
+
294
+ Other official engines can still receive their own settings:
295
+
296
+ ```ts
297
+ recipe: {
298
+ kind: 'engine',
299
+ run: {
300
+ engine: 'autoresearch',
301
+ maxEvaluations: 60,
302
+ maxProposerCostUsd: 8,
303
+ engineConfig: {
304
+ command: ['python', 'run_research.py'],
305
+ },
306
+ },
307
+ }
137
308
  ```
138
309
 
139
- This recipe is GEPA's published Omni shape: the bridge calls GEPA's `optimize_best_of(...)` for the parallel exploration stage, then its `optimize_anything(...)` for the fresh continuation stage.
140
- It does not implement a local optimizer or scheduler.
141
- GEPA reports Omni winning its matched-budget, 10-task Frontier-CS experiment, but that is not evidence that it wins your task.
142
- Run it alongside current methods and select only on fresh final cases.
310
+ Their external model spend remains incomplete unless that engine reports it.
311
+ Keep API keys in environment variables or `runner.env`.
312
+ Do not place credentials in `engineConfig` because run settings are persisted.
313
+
314
+ `describeScenario()` controls the train and selection data sent to GEPA.
315
+ `describeArtifact()` controls the execution evidence returned after a candidate is scored.
316
+ Neither callback can receive a final test case.
143
317
 
144
- The bridge serializes only the values returned by `describeScenario()` for train and selection cases.
145
- Final test cases remain in `compareOptimizationMethods()` and are first scored after GEPA exits.
146
- By default, GEPA starts in its empty run directory.
147
- Do not set `runner.cwd` to a location that contains final cases.
318
+ ## Install Official SkillOpt
148
319
 
149
- Each engine run declares its own `maxEvaluations` and `maxProposerCostUsd`.
150
- The local callback enforces the sum of the recipe's evaluation limits before it runs an agent or judge.
151
- The sum of `maxProposerCostUsd` values is a requested GEPA spend cap, but GEPA's reported cost is not an agent-eval receipt.
152
- The comparison therefore marks that method's cost accounting incomplete and never treats a reported `$0` as confirmed spend.
320
+ Install the SkillOpt source revision tested by this release:
321
+
322
+ ```sh
323
+ python -m pip install agent-eval-rpc
324
+ python -m pip install \
325
+ "skillopt @ git+https://github.com/microsoft/SkillOpt.git@61735e3922efc2b90c6d6cab561e62e98452ca90"
326
+ ```
153
327
 
154
- Use `{ kind: 'engine', run: { ... } }` to call one GEPA engine directly.
155
- `engineConfig` is passed directly to that GEPA engine.
156
- The bridge accepts any trimmed engine string and GEPA validates whether it is installed or registered.
328
+ From this repository:
157
329
 
158
- For `agent-knowledge`, use this only to compare a text retrieval policy against `runRetrievalImprovementLoop()`.
159
- Keep source acquisition, knowledge writes, provenance, freshness, memory, and promotion in `agent-knowledge`.
330
+ ```sh
331
+ cd clients/python
332
+ uv sync --frozen --group skillopt-source
333
+ ```
160
334
 
161
- ## Data Use
335
+ The published `skillopt==0.2.0` wheel omits the prompt files required by `ReflACTTrainer`.
336
+ The tested source revision contains all 21 files.
162
337
 
163
- | Set | Who can read it | Purpose |
164
- |---|---|---|
165
- | Train | Optimization methods and candidate generators | Generate and fit candidates. |
166
- | Selection | Optimization methods | Accept candidates, stop early, and select one surface per method. |
167
- | Test | `compareOptimizationMethods` only | Estimate final lift and rank methods. |
338
+ `skillOptOptimizationMethod()` runs SkillOpt's official `ReflACTTrainer`.
339
+ Agent Eval supplies an environment adapter that sends each candidate and case back to the TypeScript execution and judging path.
340
+ SkillOpt's own test evaluation is disabled.
341
+ This integration uses SkillOpt's OpenAI-compatible optimizer backend so every model call can pass through the metered proxy.
342
+ Use SkillOpt directly when you need one of its CLI or provider-specific backends.
168
343
 
169
- All three sets must be non-empty and pairwise disjoint by scenario ID.
170
- Test must contain at least two scenarios.
171
- Two is only an API minimum; use enough scenarios to detect the effect size that matters for your product.
344
+ `maxEvaluations` is a hard callback limit, not a prediction of SkillOpt's internal work.
345
+ The official trainer decides how many rollouts each enabled phase needs.
346
+ The callback rejects the first request beyond the declared limit, including work from slow updates or meta-skill phases.
172
347
 
173
- Each method receives independent copies of train and selection scenarios.
174
- The final test set is absent from `OptimizationMethodInput`.
175
- Every method finishes before the first test call starts.
176
- When `optimizationConcurrency` is greater than one, the shared runner and judges must support concurrent calls.
348
+ SkillOpt connects to a local proxy rather than receiving the provider key.
349
+ The proxy enforces the declared model limits before each call and records provider token usage at the rates supplied in `optimizer.budget`.
350
+ Missing token usage, an oversized request or response, a wrong model, streaming, and a call beyond budget all fail loudly.
177
351
 
178
- ## Execution And Cost
352
+ ## Use Official DSPy Optimizers
179
353
 
180
- `optimizationConcurrency` controls how many methods run at once.
181
- `optimizationRunOptions.maxConcurrency` controls scenario calls inside each method.
182
- Top-level `maxConcurrency` controls scenario calls during final test scoring.
354
+ Do not convert a DSPy program into an `OptimizationMethod`.
355
+ Install `agent-eval-rpc[dspy]`, create `DspyJudgeMetric`, and pass it to official DSPy:
183
356
 
184
- `optimizationRunOptions.costCeiling` is a separate limit for each method.
185
- Top-level `costCeiling` is one shared limit across baseline and selected-surface scoring on final test.
357
+ ```python
358
+ import dspy
186
359
 
187
- The result reports three cost objects:
360
+ from agent_eval_rpc import DspyJudgeMetric
188
361
 
189
- ```ts
190
- comparison.optimizationCost
191
- comparison.testCost
192
- comparison.totalCost
362
+ metric = DspyJudgeMetric(rubric_name="answer-quality")
363
+ gepa = dspy.GEPA(
364
+ metric=metric.feedback,
365
+ reflection_lm=dspy.LM("openai/gpt-4.1-mini"),
366
+ max_metric_calls=100,
367
+ )
368
+ mipro = dspy.MIPROv2(metric=metric, auto="light")
193
369
  ```
194
370
 
195
- Each object contains `totalCostUsd`, `accountingComplete`, and `incompleteReasons`.
196
- An unknown provider charge therefore cannot appear as a trustworthy zero-dollar total.
197
- Cost breaks a lift tie only when every method in that tied group reports complete accounting.
371
+ This keeps program compilation, traces, demos, and optimizer state inside DSPy.
372
+ Agent Eval supplies the shared rubric and returns rich feedback for `dspy.GEPA`.
373
+ DSPy 3.2.1 requires GEPA 0.0.27.
374
+ Run it in a separate Python environment from the general GEPA bridge, which uses GEPA 0.1.4.
198
375
 
199
- ## Read The Result
376
+ ## Resume A Compatible Run
200
377
 
201
- ```ts
202
- for (const method of comparison.scores) {
203
- console.log({
204
- rank: method.rank,
205
- name: method.name,
206
- lift: method.lift,
207
- interval: method.liftCi,
208
- scenarios: method.scenarioScores,
209
- optimizationCostUsd: method.optimizationCost.totalCostUsd,
210
- costComplete: method.optimizationCost.accountingComplete,
211
- })
212
- }
213
- ```
378
+ Both official methods default to `resume: 'never'`.
379
+ Use `resume: 'if-compatible'` to restore matching SkillOpt state or a matching direct GEPA engine.
380
+ Use `resume: 'required'` when missing or incompatible state should fail.
381
+ Direct GEPA resume also requires `trustResumeState: true` because upstream checkpoints use Python pickle.
382
+ Set it only for checkpoints created locally in a directory you control.
383
+ Composed GEPA recipes restart and never report that official state was restored.
214
384
 
215
- `rank` orders methods by estimated lift, then by cost only when every method with that lift has complete cost accounting.
216
- It does not mean the higher-ranked method is conclusively better.
217
- Read `liftCi` and `comparison.pairwise[].favored` before making that claim.
218
- `scenarioScores` contains the paired values used to compute each method's result.
385
+ A match includes:
219
386
 
220
- Repetitions are averaged within each test scenario before scenarios are resampled.
221
- The intervals assume scenarios are the independent sampling units.
387
+ - optimizer and bridge package versions, revisions, and source-tree hashes,
388
+ - Python runtime and custom engine module hashes,
389
+ - recipe or trainer settings,
390
+ - starting surface,
391
+ - train and selection descriptions,
392
+ - evaluation ID for execution and scoring behavior,
393
+ - seed,
394
+ - limits that affect the run.
222
395
 
223
- `confidence: 0.95` applies to the complete family of method-vs-baseline and possible method-vs-method contrasts.
224
- The implementation adjusts each interval for that family and raises the default resample count when more methods require finer interval tails.
225
- An explicit resample count that is too small is rejected before optimization starts.
396
+ Use a commit, content hash, or another stable value for `evaluationId`.
397
+ Change it whenever dispatch behavior, judges, model settings, or scoring logic changes.
398
+ Concurrent processes cannot write the same compatible run at the same time.
226
399
 
227
- The final test data is spent when this function ranks methods.
228
- If you choose a method from this result and later claim its deployed effect, confirm that claim on new data that was not used for this ranking.
400
+ ## Write A Custom Candidate Generator
229
401
 
230
- ## FAPO
402
+ Use `SurfaceProposer` when your code or runtime owns candidate creation.
403
+ The proposer receives the current surface, prior campaign history, findings, generation number, requested population size, and cancellation signal.
231
404
 
232
- `fapoProposer` can move from prompt edits to declared parameter edits and then to an injected structural proposer.
233
- Every level must accept and return the same surface representation.
405
+ ```ts
406
+ import type { SurfaceProposer } from '@tangle-network/agent-eval/campaign'
407
+
408
+ const proposer: SurfaceProposer = {
409
+ kind: 'product-rules',
410
+ async propose({ currentSurface, populationSize }) {
411
+ const prompt = String(currentSurface)
412
+ return [
413
+ {
414
+ surface: `${prompt}\nReturn JSON only.`,
415
+ label: 'json-only',
416
+ rationale: 'Training failures included prose around the JSON object.',
417
+ },
418
+ {
419
+ surface: `${prompt}\nInclude every required field, using null when unknown.`,
420
+ label: 'required-fields',
421
+ rationale: 'Training failures omitted fields.',
422
+ },
423
+ ].slice(0, populationSize)
424
+ },
425
+ }
426
+ ```
234
427
 
235
- `agent-eval` does not generate repository code itself.
236
- Pass a code-capable `structuralProposer` from your runtime or application when structural edits are part of the comparison.
428
+ Return a label and rationale when they will help later analysis.
429
+ Candidate creation must not read final test results.
237
430
 
238
- Use `fapoEscalationMethod(config)` to compare the complete FAPO procedure with other methods.
431
+ ## Data And Cost Rules
239
432
 
240
- ## Common Errors
433
+ - Train and selection cases are visible to complete optimization methods.
434
+ - Train and selection cases may influence candidate generation, selection, and stopping.
435
+ - Final test cases may only compare surfaces after every method finishes.
436
+ - The same dispatch and judges score every method.
437
+ - Missing cost remains unknown.
438
+ - A method must declare bounded work before it starts.
439
+ - Credentials belong in process environment variables.
440
+ - Resumed state must match every input that can change the result.
241
441
 
242
- - Do not pass a raw `SurfaceProposer` to `compareOptimizationMethods`.
243
- - Do not let a custom `OptimizationMethod` load final test rows from another source.
244
- - Do not compare methods with different runners, judges, or final test scenarios.
245
- - Do not read `method.optimizationCost` as total comparison cost.
246
- - Do not report a dollar total as complete when `accountingComplete` is false.
247
- - Do not reuse the final test set for repeated method selection and continue calling it untouched.
442
+ These rules make method comparisons inspectable without pretending different optimizers have identical internals.