From 051ad47ae3e1d900a8ae8fa2ddde3b706171c746 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 14:43:16 -0600 Subject: [PATCH 1/8] fix(campaign): export composite proposer --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- scripts/verify-package-exports.mjs | 4 ++++ src/campaign/index.ts | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29bfab0..90c3829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval- --- +## [0.110.1] — 2026-07-09 — proposer portfolio export + +### Fixed + +- Exported the existing `compositeProposer` and `CompositeProposerOptions` from `@tangle-network/agent-eval/campaign`, so consumers can split one population budget across GEPA, SkillOpt, FAPO, memory, trace-analysis, or other proposers without copying the portfolio implementation. +- Added a packed-package import check for the export. + +No proposer behavior changed. + ## [0.108.1] — 2026-07-08 — public catalog docs patch ### Fixed diff --git a/package.json b/package.json index 825946c..932f9e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-eval", - "version": "0.110.0", + "version": "0.110.1", "description": "Evaluate and improve AI agents from runs, traces, judges, and feedback. Compare candidates, cluster failures, measure lift, and gate releases.", "homepage": "https://github.com/tangle-network/agent-eval#readme", "repository": { diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index 94e166a..cd53dea 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -78,6 +78,10 @@ try { if (!('analyzeBeliefPolicy' in beliefState)) { throw new Error('missing belief-state export analyzeBeliefPolicy') } + const campaign = await import('@tangle-network/agent-eval/campaign') + if (!('compositeProposer' in campaign)) { + throw new Error('missing campaign export compositeProposer') + } `, ], appDir, diff --git a/src/campaign/index.ts b/src/campaign/index.ts index 3e7fdda..0ba1bd1 100644 --- a/src/campaign/index.ts +++ b/src/campaign/index.ts @@ -192,6 +192,10 @@ export { type SkillOptEpochRecord, } from './presets/run-skill-opt' export { type AceProposerOptions, aceProposer } from './proposers/ace' +export { + type CompositeProposerOptions, + compositeProposer, +} from './proposers/composite' export { type EvolutionaryProposerOptions, evolutionaryProposer } from './proposers/evolutionary' export { extractFapoAttributionSignals, From 8a802d32574c552ad87f442e5f9f527987dbecef Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 14:51:20 -0600 Subject: [PATCH 2/8] chore(release): lock python client to 0.110.1 --- clients/python/pyproject.toml | 2 +- clients/python/src/agent_eval_rpc/__init__.py | 2 +- clients/python/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 77b2f49..e53c99c 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agent-eval-rpc" -version = "0.110.0" +version = "0.110.1" description = "Python RPC client for @tangle-network/agent-eval — judge content against rubrics over HTTP or stdio RPC. Eval logic runs in the Node runtime; this package is a thin wire client." readme = "README.md" requires-python = ">=3.10" diff --git a/clients/python/src/agent_eval_rpc/__init__.py b/clients/python/src/agent_eval_rpc/__init__.py index ca67217..c58c2fb 100644 --- a/clients/python/src/agent_eval_rpc/__init__.py +++ b/clients/python/src/agent_eval_rpc/__init__.py @@ -58,7 +58,7 @@ try: __version__ = version("agent-eval-rpc") except PackageNotFoundError: - __version__ = "0.110.0" + __version__ = "0.110.1" __all__ = [ "Client", diff --git a/clients/python/uv.lock b/clients/python/uv.lock index 1273a16..b2e96c2 100644 --- a/clients/python/uv.lock +++ b/clients/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "agent-eval-rpc" -version = "0.79.0" +version = "0.110.1" source = { editable = "." } dependencies = [ { name = "httpx" }, From 9c7fc3056b9effddda6a5b91f1467fd3dc1af225 Mon Sep 17 00:00:00 2001 From: drewstone Date: Thu, 9 Jul 2026 14:16:20 -0700 Subject: [PATCH 3/8] fix(improvement): analyze baseline traces before generation 0 so single-generation runs propose with trace context (#334) --- src/campaign/presets/run-optimization.ts | 30 ++++++-- src/contract/self-improve.ts | 11 +-- tests/campaign/presets.test.ts | 89 ++++++++++++++++++++++-- tests/contract-self-improve.test.ts | 19 ++++- 4 files changed, 134 insertions(+), 15 deletions(-) diff --git a/src/campaign/presets/run-optimization.ts b/src/campaign/presets/run-optimization.ts index 1d6e314..96562b8 100644 --- a/src/campaign/presets/run-optimization.ts +++ b/src/campaign/presets/run-optimization.ts @@ -59,10 +59,12 @@ export interface RunOptimizationBaseOptions( toParetoParent(opts.baselineSurface, winnerSurfaceHash, baselineCampaign, -1), ] + // Diagnose the BASELINE traces before generation 0 proposes. The + // between-generation producer call below only fires after gen g to feed gen + // g+1, so without this a single-generation run (maxGenerations = 1) + // proposes blind even though baseline traces exist. Baseline is + // `generation: -1` — the same convention the Pareto accumulator uses above. + // Skipped when the baseline produced no cells (dry/offline modes have no + // traces to analyze) or there is no generation 0 to feed; `propose()` then + // sees the static seed findings exactly as before. + if (opts.analyzeGeneration && opts.maxGenerations > 0 && baselineCampaign.cells.length > 0) { + const fresh = await opts.analyzeGeneration({ + generation: -1, + runDir: `${opts.runDir}/baseline`, + candidates: [ + { surfaceHash: winnerSurfaceHash, campaign: baselineCampaign, composite: winnerComposite }, + ], + history, + }) + if (Array.isArray(fresh)) currentFindings = fresh + } + for (let gen = 0; gen < opts.maxGenerations; gen++) { // Decide: the proposer may stop early based on accumulated history. if (proposer.decide?.({ history }).stop) break diff --git a/src/contract/self-improve.ts b/src/contract/self-improve.ts index 2e5137c..3be248f 100644 --- a/src/contract/self-improve.ts +++ b/src/contract/self-improve.ts @@ -239,11 +239,12 @@ export interface SelfImproveOptions { expectUsage?: 'assert' | 'warn' | 'off' /** - * Per-generation findings producer. After each - * generation is scored, this is called; whatever it returns REPLACES the - * proposer's `findings` for the next generation's `propose()`. Plug a - * trace-analyst registry / HALO here. When absent, findings stay - * `opts.findings`. + * Per-generation findings producer. Runs once on the baseline campaign (as + * `generation: -1`) before generation 0 proposes — so single-generation runs + * propose with trace context — and again after each generation is scored; + * whatever it returns REPLACES the proposer's `findings` for the next + * `propose()`. Plug a trace-analyst registry / HALO here. When absent, + * findings stay `opts.findings`. */ analyzeGeneration?: RunOptimizationOptions['analyzeGeneration'] diff --git a/tests/campaign/presets.test.ts b/tests/campaign/presets.test.ts index deaf97b..fd08006 100644 --- a/tests/campaign/presets.test.ts +++ b/tests/campaign/presets.test.ts @@ -1414,12 +1414,93 @@ describe('runOptimization — analyzeGeneration feeds findings forward', () => { }) expect(seen).toHaveLength(3) - // Gen 0 sees the static seed; gen 1 sees gen-0's produced finding; gen 2 gen-1's. - expect(seen[0]).toEqual([{ claim: 'seed' }]) + // Gen 0 sees the baseline (gen -1) analysis — not the static seed; gen 1 + // sees gen-0's produced finding; gen 2 gen-1's. + expect(seen[0]).toEqual([{ claim: 'gen--1 finding' }]) expect(seen[1]).toEqual([{ claim: 'gen-0 finding' }]) expect(seen[2]).toEqual([{ claim: 'gen-1 finding' }]) - // Producer runs after gens 0 and 1, NOT the last (gen 2 has no next propose()). - expect(analyzed).toEqual([0, 1]) + // Producer runs on the baseline (-1) and after gens 0 and 1, NOT the last + // (gen 2 has no next propose()). + expect(analyzed).toEqual([-1, 0, 1]) + }) + + it("with generations=1, gen 0's propose() receives the baseline (gen -1) trace analysis", async () => { + const seen: unknown[][] = [] + const proposer = { + kind: 'recorder', + async propose(ctx: ProposeContext) { + seen.push(ctx.findings) + return [{ surface: 'S-gen0', label: 'g0', rationale: 'r' }] + }, + } + const result = await runOptimization({ + scenarios: SCENARIOS, + baselineSurface: 'BASE', + dispatchWithSurface: async (surface) => ({ text: String(surface) }), + judges: [passJudge], + proposer, + populationSize: 1, + maxGenerations: 1, + promoteTopK: 1, + runDir, + seed: 1, + findings: [{ claim: 'seed' }], + analyzeGeneration: async ({ generation, runDir: analyzedDir, candidates, history }) => { + // The single propose() of the run is fed by a BASELINE analysis: + // generation -1 (the baseline convention), the baseline runDir, no + // history yet, and the baseline campaign as the sole candidate. + expect(generation).toBe(-1) + expect(analyzedDir).toBe(`${runDir}/baseline`) + expect(history).toEqual([]) + expect(candidates).toHaveLength(1) + expect(candidates[0]!.surfaceHash).toBe(surfaceHash('BASE')) + // Marker derived from the baseline traces: the analyzed cells carry + // the artifact the BASELINE surface dispatched. + const artifacts = candidates[0]!.campaign.cells.map( + (c) => (c.artifact as FakeArtifact).text, + ) + expect(artifacts).toEqual(['BASE', 'BASE']) + return [{ claim: 'baseline finding', from: artifacts.join(',') }] + }, + }) + + // Gen 0 proposed WITH the baseline analysis (not the static seed), and the + // report is traceably derived from the baseline artifacts. + expect(seen).toEqual([[{ claim: 'baseline finding', from: 'BASE,BASE' }]]) + expect(result.generations).toHaveLength(1) + }) + + it('skips the baseline analysis when the baseline produced no cells (nothing to analyze)', async () => { + const seen: unknown[][] = [] + const analyzed: number[] = [] + const proposer = { + kind: 'recorder', + async propose(ctx: ProposeContext) { + seen.push(ctx.findings) + return [{ surface: 'S-gen0', label: 'g0', rationale: 'r' }] + }, + } + await runOptimization({ + // No scenarios → the baseline campaign has zero cells (the dry shape) — + // the producer must NOT run on it, and propose() sees the static seed. + scenarios: [], + baselineSurface: 'BASE', + dispatchWithSurface: async (surface) => ({ text: String(surface) }), + judges: [passJudge], + proposer, + populationSize: 1, + maxGenerations: 1, + promoteTopK: 1, + runDir, + seed: 1, + findings: [{ claim: 'seed' }], + analyzeGeneration: async ({ generation }) => { + analyzed.push(generation) + return [{ claim: 'should not reach gen 0' }] + }, + }) + expect(analyzed).toEqual([]) + expect(seen).toEqual([[{ claim: 'seed' }]]) }) it('without analyzeGeneration, findings stay the static seed every generation', async () => { diff --git a/tests/contract-self-improve.test.ts b/tests/contract-self-improve.test.ts index b2518b9..9089c3e 100644 --- a/tests/contract-self-improve.test.ts +++ b/tests/contract-self-improve.test.ts @@ -210,8 +210,6 @@ describe('selfImprove — forwarded loop knobs', () => { let calls = 0 await selfImprove({ ...base, - // analyzeGeneration runs BETWEEN generations (its findings feed the next), - // so it needs ≥2 generations to fire at least once. budget: { generations: 2, populationSize: 1 }, expectUsage: 'off', analyzeGeneration: async () => { @@ -221,4 +219,21 @@ describe('selfImprove — forwarded loop knobs', () => { }) expect(calls).toBeGreaterThanOrEqual(1) }) + + it('with generations=1, analyzeGeneration fires on the BASELINE (gen -1) so propose() is not blind', async () => { + const analyzed: number[] = [] + await selfImprove({ + ...base, + // Single generation — the common case. The producer analyzes the + // baseline traces first (gen -1) so gen 0 proposes with that report; + // the between-generation call is skipped (gen 0 is the last). + budget: { generations: 1, populationSize: 1 }, + expectUsage: 'off', + analyzeGeneration: async ({ generation }) => { + analyzed.push(generation) + return [{ claim: 'baseline finding' }] + }, + }) + expect(analyzed).toEqual([-1]) + }) }) From 70df4d192547943dcdbbba1e878c81500822df8e Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 14:43:16 -0600 Subject: [PATCH 4/8] fix(campaign): export composite proposer --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- scripts/verify-package-exports.mjs | 4 ++++ src/campaign/index.ts | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29bfab0..90c3829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval- --- +## [0.110.1] — 2026-07-09 — proposer portfolio export + +### Fixed + +- Exported the existing `compositeProposer` and `CompositeProposerOptions` from `@tangle-network/agent-eval/campaign`, so consumers can split one population budget across GEPA, SkillOpt, FAPO, memory, trace-analysis, or other proposers without copying the portfolio implementation. +- Added a packed-package import check for the export. + +No proposer behavior changed. + ## [0.108.1] — 2026-07-08 — public catalog docs patch ### Fixed diff --git a/package.json b/package.json index 825946c..932f9e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-eval", - "version": "0.110.0", + "version": "0.110.1", "description": "Evaluate and improve AI agents from runs, traces, judges, and feedback. Compare candidates, cluster failures, measure lift, and gate releases.", "homepage": "https://github.com/tangle-network/agent-eval#readme", "repository": { diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index 94e166a..cd53dea 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -78,6 +78,10 @@ try { if (!('analyzeBeliefPolicy' in beliefState)) { throw new Error('missing belief-state export analyzeBeliefPolicy') } + const campaign = await import('@tangle-network/agent-eval/campaign') + if (!('compositeProposer' in campaign)) { + throw new Error('missing campaign export compositeProposer') + } `, ], appDir, diff --git a/src/campaign/index.ts b/src/campaign/index.ts index 3e7fdda..0ba1bd1 100644 --- a/src/campaign/index.ts +++ b/src/campaign/index.ts @@ -192,6 +192,10 @@ export { type SkillOptEpochRecord, } from './presets/run-skill-opt' export { type AceProposerOptions, aceProposer } from './proposers/ace' +export { + type CompositeProposerOptions, + compositeProposer, +} from './proposers/composite' export { type EvolutionaryProposerOptions, evolutionaryProposer } from './proposers/evolutionary' export { extractFapoAttributionSignals, From 7c86693745bbf9f19638f2c29cbd55c0003f9b52 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 14:51:20 -0600 Subject: [PATCH 5/8] chore(release): lock python client to 0.110.1 --- clients/python/pyproject.toml | 2 +- clients/python/src/agent_eval_rpc/__init__.py | 2 +- clients/python/uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml index 77b2f49..e53c99c 100644 --- a/clients/python/pyproject.toml +++ b/clients/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agent-eval-rpc" -version = "0.110.0" +version = "0.110.1" description = "Python RPC client for @tangle-network/agent-eval — judge content against rubrics over HTTP or stdio RPC. Eval logic runs in the Node runtime; this package is a thin wire client." readme = "README.md" requires-python = ">=3.10" diff --git a/clients/python/src/agent_eval_rpc/__init__.py b/clients/python/src/agent_eval_rpc/__init__.py index ca67217..c58c2fb 100644 --- a/clients/python/src/agent_eval_rpc/__init__.py +++ b/clients/python/src/agent_eval_rpc/__init__.py @@ -58,7 +58,7 @@ try: __version__ = version("agent-eval-rpc") except PackageNotFoundError: - __version__ = "0.110.0" + __version__ = "0.110.1" __all__ = [ "Client", diff --git a/clients/python/uv.lock b/clients/python/uv.lock index 1273a16..b2e96c2 100644 --- a/clients/python/uv.lock +++ b/clients/python/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "agent-eval-rpc" -version = "0.79.0" +version = "0.110.1" source = { editable = "." } dependencies = [ { name = "httpx" }, From f6de308e4394209632201d4d24e578f5d33f52eb Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 15:19:46 -0600 Subject: [PATCH 6/8] fix(campaign): align composite proposer docs and release checks --- docs/improvement-glossary.md | 21 +++++++------- scripts/verify-package-exports.mjs | 46 ++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/docs/improvement-glossary.md b/docs/improvement-glossary.md index 71a9bca..776e833 100644 --- a/docs/improvement-glossary.md +++ b/docs/improvement-glossary.md @@ -61,25 +61,26 @@ Start at the top row and only move down when the row's *"reach for it when"* mat | `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. | | `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. | | `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. | -| `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. **This is the composite/orchestration proposer** (there is no separate `compositeProposer`). | whatever its level proposers return | Exported; you wire the level proposers. | +| `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. | +| `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. | | `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. | | `memoryCurationProposer` | Same as ACE but you want a compact, deduped, re-ranked memory instead of append-only growth. | memory string | Exported. | | `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. | | `traceAnalystProposer` | Bench-only: race our trace-analysis evidence engine head-to-head inside `compareProposers`. | prompt string | Bench-only. | | `haloProposer` | Bench-only: race the external `halo-engine` analysis against ours. | prompt string | Bench-only, external. | -Default path: `gepaProposer` for prompts; add `parameterSweepProposer` when a config knob is the suspect; wrap both in `fapoProposer` when you want the loop to decide *when* to escalate from words to knobs to code. +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. -## Composing proposers — the only three ways, with runnable code +## Composing proposers — four distinct shapes -There is deliberately **no** `chainProposer`/`compositeProposer` primitive. -Composition happens in exactly three shapes: +Choose the shape that matches the experiment: -1. **Escalate** — `fapoProposer` wraps a prompt + parameter + structural proposer into one, spending the cheapest level first. -2. **Race** — `compareProposers` runs N proposers on one holdout and returns a per-proposer lift CI + pairwise "who won". -3. **Plug in** — hand any composed proposer to the gated loop via `runImprovementLoop({ proposer })`, or the one-liner `improve({ surface, generator })` in `@tangle-network/agent-runtime`. +1. **Portfolio** — `compositeProposer` splits one generation's population across member proposers by fixed weights and returns one provenance-labelled pool. +2. **Escalate** — `fapoProposer` wraps prompt + parameter + structural levels into one proposer and spends on the cheapest level until evidence says to escalate. +3. **Race** — `compareProposers` gives proposers separate loops, then re-scores their winners on one holdout and returns per-proposer lift intervals plus pairwise results. +4. **Plug in** — hand any proposer to `runImprovementLoop({ proposer })`, or use `improve({ surface, generator })` in `@tangle-network/agent-runtime`. -### 1 + 3 — compose by escalation, then run the gated loop +### 2 + 4 — compose by escalation, then run the improvement loop ```ts import { @@ -196,7 +197,7 @@ Every entrant is re-scored on the **same** holdout with the **same** judges, so - Do not put eval logic inside a proposer — scoring lives in `dispatch` + `judges`, proposing lives in the proposer. - 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. - Do not read `lift` without `result.power`/MDE — a "+4" on a valset too small to detect +4 is noise wearing a number. -- Do not look for a `compositeProposer` — `fapoProposer` is the composite; `compareProposers` is the race; `runImprovementLoop`/`improve` is the plug. +- 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. ### neutralizationGate — the placebo / content-causality control diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index cd53dea..abf6a25 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -7,6 +7,7 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const tempRoot = mkdtempSync(join(repoRoot, '.tmp-package-exports-')) try { + verifyVersionLock() const packDir = join(tempRoot, 'pack') const unpackDir = join(tempRoot, 'unpack') const appDir = join(tempRoot, 'app') @@ -90,6 +91,51 @@ try { rmSync(tempRoot, { recursive: true, force: true }) } +function verifyVersionLock() { + const npmVersion = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8')).version + const pyproject = readFileSync(join(repoRoot, 'clients/python/pyproject.toml'), 'utf8') + const pythonSource = readFileSync( + join(repoRoot, 'clients/python/src/agent_eval_rpc/__init__.py'), + 'utf8', + ) + const uvLock = readFileSync(join(repoRoot, 'clients/python/uv.lock'), 'utf8') + const pythonPackageVersion = matchVersion( + pyproject, + /^version = "([^"]+)"$/m, + 'clients/python/pyproject.toml', + ) + const pythonFallbackVersion = matchVersion( + pythonSource, + /except PackageNotFoundError:\s*\n\s*__version__ = "([^"]+)"/, + 'clients/python/src/agent_eval_rpc/__init__.py', + ) + const uvRootVersion = matchVersion( + uvLock, + /\[\[package\]\]\s*\nname = "agent-eval-rpc"\s*\nversion = "([^"]+)"/, + 'clients/python/uv.lock', + ) + const versions = { + npm: npmVersion, + pythonPackage: pythonPackageVersion, + pythonFallback: pythonFallbackVersion, + uvRoot: uvRootVersion, + } + const mismatched = Object.entries(versions).filter(([, version]) => version !== npmVersion) + if (mismatched.length > 0) { + throw new Error( + `release version mismatch: ${Object.entries(versions) + .map(([name, version]) => `${name}=${version}`) + .join(', ')}`, + ) + } +} + +function matchVersion(source, pattern, path) { + const match = source.match(pattern) + if (!match?.[1]) throw new Error(`could not read release version from ${path}`) + return match[1] +} + function run(command, args, cwd) { const result = spawnSync(command, args, { cwd, From d972d4c90c3bab620aa31b79d6ac8f767bf0dc60 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 15:42:08 -0600 Subject: [PATCH 7/8] docs(campaign): distinguish proposer composition modes --- CHANGELOG.md | 2 +- docs/improvement-glossary.md | 6 +++--- scripts/verify-package-exports.mjs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c3829..bc18e95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to `@tangle-network/agent-eval` and its sibling `agent-eval- ## [0.110.1] — 2026-07-09 — proposer portfolio export -### Fixed +### Added - Exported the existing `compositeProposer` and `CompositeProposerOptions` from `@tangle-network/agent-eval/campaign`, so consumers can split one population budget across GEPA, SkillOpt, FAPO, memory, trace-analysis, or other proposers without copying the portfolio implementation. - Added a packed-package import check for the export. diff --git a/docs/improvement-glossary.md b/docs/improvement-glossary.md index 776e833..fa0c4b3 100644 --- a/docs/improvement-glossary.md +++ b/docs/improvement-glossary.md @@ -94,8 +94,8 @@ import { const llm = { baseUrl: process.env.TANGLE_BASE_URL, apiKey: process.env.TANGLE_API_KEY } const model = 'deepseek-v4-flash' -// Compose: prompt edits first (GEPA), escalate to a config knob only when -// prompt-level search plateaus. `fapoProposer` IS the composite proposer. +// Compose by escalation: prompt edits first (GEPA), then a config knob only +// when prompt-level search plateaus. This is distinct from a peer portfolio. const proposer = fapoProposer({ scope: { allowedLevels: ['prompt', 'parameter'] }, // no structural/code tier here promptProposer: gepaProposer({ llm, model, target: 'agent system prompt' }), @@ -148,7 +148,7 @@ const out = await improve(profile, findings, { if (out.shipped) deploy(out.profile) // out.lift is the held-out winner − baseline ``` -### 2 — race proposers head-to-head for a lift CI +### 3 — race proposers head-to-head for a lift CI ```ts import { diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index abf6a25..c7dbca2 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -101,7 +101,7 @@ function verifyVersionLock() { const uvLock = readFileSync(join(repoRoot, 'clients/python/uv.lock'), 'utf8') const pythonPackageVersion = matchVersion( pyproject, - /^version = "([^"]+)"$/m, + /^version\s*=\s*"([^"]+)"$/m, 'clients/python/pyproject.toml', ) const pythonFallbackVersion = matchVersion( From 2ed3826b347b9fec757381d6744dde4600c2c06c Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Thu, 9 Jul 2026 15:42:27 -0600 Subject: [PATCH 8/8] style(release): align version check formatting --- scripts/verify-package-exports.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/verify-package-exports.mjs b/scripts/verify-package-exports.mjs index c7dbca2..c0a74b9 100644 --- a/scripts/verify-package-exports.mjs +++ b/scripts/verify-package-exports.mjs @@ -101,7 +101,7 @@ function verifyVersionLock() { const uvLock = readFileSync(join(repoRoot, 'clients/python/uv.lock'), 'utf8') const pythonPackageVersion = matchVersion( pyproject, - /^version\s*=\s*"([^"]+)"$/m, + /^version\s*=\s*"([^"]+)"$/m, 'clients/python/pyproject.toml', ) const pythonFallbackVersion = matchVersion(