feat(analyst): trace analysts run official DSPy RLMs; retire the Ax stack - #495
Conversation
…tack - clients/python dspy_rlm_bridge: official dspy.RLM in a child process, seven allowlisted trace tools, strict JSON I/O (14/14 tests) - src/analyst engine contract (engine.ts, dspy-rlm-engine.ts, trace-tool-callback.ts); callLlmJson demoted to an explicit non-recursive baseline - store-backed citation verification: cited trace/span ids must resolve in the trace store, encoded ids are rejected - external-optimizer model proxy fails on zero-usage 200 responses and on over-reserved output after recording the actual charge - delete ax-service, ax-cost-service, trace-analyst/loop, structure-findings, AxGepaSteeringOptimizer (no external callers) - repin analyst benchmark implementation and dependency-lock digests for the changed fingerprint set
…produced it The CodeTraceBench artifact records the implementation that ran it; binding those fields to the live ANALYST_BENCHMARK_* constants asserted that the current implementation produced a run it did not. The published digests are now literal historical facts, and a fresh certified run is required before any headline claim about the current engine.
…s removed surface - verify-package-exports no longer requires the deleted buildTraceAnalystTools and pins every removed analyst, steering, and traces export with the script's negative-assertion convention - CHANGELOG documents the breaking analyst-engine surface: required engine, defineTraceAnalyst returning TraceAnalystDefinition without a cost field, createTraceAnalyst rename, instructions rename, pairwise-only steering, the Ax stack removal, and the Python dspy extra - the published CodeTraceBench README states its numbers came from the retired one-shot runner at implementation 4dba263b and forbids citing them for the current engine without a fresh certified run
… attested sandbox Closes the adversarial-audit findings on the DSPy engine boundary: - excerpt citations verify against span attributes and status_message (or a cited finding's content fields) only — identifiers, names, statuses, and timestamps are no longer quotable evidence — and excerpts under 8 chars are rejected as unverifiable - one malformed finding row from the bridge no longer discards the whole paid investigation: rows are validated per row, rejections are logged and counted in runtime.rejectedFindings - the bridge's canonical fixture emits a grammar-valid subject (incorrect-step-2); step:2 was certified Python-side but rejected by the TypeScript finding-subject schema - the Deno/Pyodide sandbox pins pyodide via an import map (npm latest flipped 0.29.3 to 314.0.3 during development, silently switching interpreters on a cold cache) and the pin is recorded in runtime identity - the sandbox attestation is derived from the deno command that actually ran (config, node-modules-dir, permission flags with scoped-path counts) instead of hardcoded constants - model output answer and findings_json are stripped before validation so a trailing newline cannot abort a fully-paid run - the analyst benchmark implementation digest is repinned for these changes TS: typecheck clean, 4525 passed / 3 skipped. Python: 17/17 bridge tests plus a live inspect through real Deno booting pinned pyodide 314.0.3.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — ef36ad97
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-30T22:48:04Z
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 71bca06f
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-30T23:07:11Z
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 71bca06f
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-30T23:08:03Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 130.8s (2 bridge agents) |
| Total | 130.8s |
💰 Value — sound
Replaces the bespoke Ax agent loop with the official DSPy RLM in a sandboxed child process behind a clean pluggable engine seam; reuses existing cost-metered subprocess infra, deletes a fully-dead stack with zero remaining callers.
- What it does: Trace analysts are now run by the official dspy.RLM (Recursive Language Model) executing in a sandboxed Deno/Pyodide child process. A new TraceAnalysisEngine interface (src/analyst/engine.ts:60) decouples the analyst definition (research question + evidence rules, no model/credentials) from the execution engine. createDspyRlmTraceEngine (src/analyst/dspy-rlm-engine.ts:39) composes the pre-existing
- Goals it achieves: (1) Make 'a trace analyst IS an RLM program' literally true in code — the recursive engine is the official upstream library, not a hand-rolled Ax agent loop. (2) Beat the published one-shot direct baseline (32.73% scored F1) with a recursive approach that retrieves evidence on demand. (3) Introduce a stable engine boundary so the analyst definition and finding contract don't change when swapping D
- Assessment: Sound and well-grained. The TraceAnalysisEngine seam is clean: defineTraceAnalyst now returns a pure definition (src/analyst/define.ts) with no model, credentials, or execution state; you bind it to any engine via runTraceAnalyst (src/analyst/kind-factory.ts:48). The DSPy engine reuses existing infrastructure rather than reinventing — startExternalOptimizerModelProxy and runExternalOptimizerProces
- Better / existing approach: none — this is the right approach. I searched for an existing recursive-loop primitive in agent-eval that this could reuse: TraceAnalysisEngine is referenced only in the new files, and there is no other RLM/PythonInterpreter/recursive-agent-loop in the substrate (grep across src/*.ts). The only prior recursive loop was the deleted Ax-based trace-analyst/loop.ts, which this replaces. agent-eval is
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound
Replaces a zero-caller custom Ax stack with the official DSPy RLM behind a clean, minimal engine interface that reuses the codebase's existing subprocess/cost-ledger/trace-store machinery and is wired as the default benchmark runner.
- Integration: Fully reachable.
createDspyRlmTraceEngineis exported from both barrels (src/index.ts:70, src/analyst/index.ts:191), is the default analyst runner in the benchmark command (benchmark-command.ts:186 via createPublicBenchmarkRlmRunner), and plugs intoanalyzeTraces(trace-analyst/analyst.ts:28 options.engine) andbuildDefaultAnalystRegistry(default-registry.ts:26). The deleted Ax stack leaves - Fit with existing patterns: Fits the codebase grain precisely. The new
TraceAnalysisEngineinterface (engine.ts:60) is a 5-field abstraction that decouples the research engine from the analyst definition —defineTraceAnalyst(define.ts:10) returns a pure value with no model/credentials, bound to any engine at run time. This is consolidation, not divergence: it retires the parallel Ax approach and routes through the same - Real-world viability: Holds up off the happy path. The engine starts an authenticated loopback trace-tool callback (trace-tool-callback.ts) and a cost-metered model proxy, spawns the Python bridge with a sandbox startup probe (
print(1+1)must return '2' before any paid call), reconciles bridge-reported model-call counts against proxy-recorded completions (dspy-rlm-engine.ts:138), and tolerates per-row finding noise w - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
No concerns — sound change, no better or existing approach found. ✅
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 1 (1 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 235.9s (2 bridge agents) |
| Total | 235.9s |
💰 Value — sound-with-nits
Replaces the hand-rolled Ax agent loop with a clean TraceAnalysisEngine abstraction backed by the official DSPy RLM in a sandboxed child process — a coherent migration along the codebase's established bridge grain; only minor helper duplication remains.
- What it does: Introduces a TraceAnalysisEngine interface (src/analyst/engine.ts) whose production implementation (createDspyRlmTraceEngine, src/analyst/dspy-rlm-engine.ts) runs the official dspy.RLM in a Deno/Pyodide child process via a Python bridge (clients/python/src/agent_eval_rpc/dspy_rlm_bridge.py), with seven allowlisted trace tools exposed over an authenticated loopback socket (src/analyst/trace-tool-ca
- Goals it achieves: (1) Pin the semantics so 'a trace analyst IS an RLM program' uses the upstream DSPy RLM rather than a hand-maintained Ax agent loop, on the path to beating the published 13.47%/32.73% CodeTraceBench direct-baseline numbers. (2) Remove the @ax-llm/ax dependency, which carried its own agent runtime and fought agent-eval's zero-upward-dependency substrate doctrine. (3) Establish a clean engine bounda
- Assessment: Sound migration executed in the codebase's grain. The new TraceAnalysisEngine interface is the right decoupling layer — analyst kinds and finding contracts are now engine-agnostic (kind-factory.ts:85 calls args.engine.analyze with no engine-specific assumptions). The DSPy RLM bridge reuses the exact established subprocess contract (runExternalOptimizerProcess + module string, identical to gepa_bri
- Better / existing approach: none — this is the right approach. I searched for duplication and a materially better architecture: (1) The Python bridge reuses optimizer_bridge_common.atomic_write_json and the same --input/--output contract as gepa_bridge.py/skillopt_bridge.py — no reinvention. (2) There is no pre-existing recursive-engine abstraction this duplicates; the old loop.ts was Ax-specific and is correctly superseded
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound
The DSPy RLM engine is the default on the main CLI benchmark path and the documented programmatic entry, cleanly replacing the heavier Ax stack with the official dspy.RLM behind a thin swappable interface.
- Integration: Fully reachable on three live paths: (1) the CLI analyst-benchmark command defaults to createPublicBenchmarkRlmRunner which builds createDspyRlmTraceEngine (benchmark-command.ts:186, benchmark-public-rlm.ts:73); (2) analyzeTraces takes the engine directly (trace-analyst/analyst.ts:52,81) and is the documented quickstart (docs/trace-analysis.md:37-55); (3) buildDefaultAnalystRegistry + createTraceA
- Fit with existing patterns: Fits the codebase grain precisely. The TraceAnalysisEngine interface (engine.ts:60) separates the research definition (question/instructions/tools/limits) from the runner, explicitly allowing DSPy RLM, HALO, or caller-owned implementations — a substrate primitive per the repo's layering doctrine. It consolidates a competing pattern (the custom AxGepaSteeringOptimizer + @ax-llm/ax dependency) into
- Real-world viability: Built beyond the happy path. Cost is bounded (maxCostUsd default $1, dspy-rlm-engine.ts:49), model calls are metered through a proxy that cross-checks reported vs recorded completions and throws on mismatch (dspy-rlm-engine.ts:138), tool calls are capped by maxToolCalls via an authenticated loopback-only callback (trace-tool-callback.ts:17), AbortSignal propagates through every layer, and cleanup
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 Small benchmark helpers duplicated between the two runners [duplication] ``
trajectoryIdFromCaseId (benchmark-public-rlm.ts:221 and benchmark-public-model.ts:641) and isPaidCallControlError (benchmark-public-rlm.ts:229 and benchmark-public-model.ts:358) are byte-identical across the RLM and direct runners; pricingForModel is also near-identical. These could be extracted to a shared benchmark-public-adapters or benchmark-public-utils module. This is a pre-existing pattern (the direct runner always carried its own copies), newly multiplied by adding the RLM runner — not b
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 23 non-blocking findings — 71bca06f
Full multi-shot audit completed 8/8 planned shots over 67 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-30T23:31:37Z · immutable trace
✅ No Blockers —
|
…-evolution # Conflicts: # src/analyst/default-registry.ts # src/analyst/define.ts # src/analyst/index.ts # src/analyst/kind-factory.ts # src/analyst/kinds/kinds.test.ts
…-evolution # Conflicts: # src/analyst/benchmark-implementation.ts # src/analyst/benchmark-reference-result.test.ts
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 1bed96cb
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-07-31T00:07:59Z
tangletools
left a comment
There was a problem hiding this comment.
🟡 Value Audit — sound-with-nits
| Verdict | sound-with-nits |
| Concerns | 1 (1 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 104.0s (2 bridge agents) |
| Total | 104.0s |
💰 Value — sound-with-nits
Replaces a hand-rolled Ax agent loop with the official DSPy RLM running in a sandboxed child process behind a clean engine seam — directionally correct and in-grain, with one duplicated loopback-server skeleton worth extracting later.
- What it does: Trace analysts become recursive research programs: a new TraceAnalysisEngine interface (src/analyst/engine.ts:60) decouples the analyst definition from the recursive engine, and createDspyRlmTraceEngine (src/analyst/dspy-rlm-engine.ts:41) runs the official dspy.RLM inside a sandboxed Deno/Pyodide child process (clients/python/src/agent_eval_rpc/dspy_rlm_bridge.py) with seven allowlisted trace tool
- Goals it achieves: (1) Make a trace analyst BE the official DSPy RLM rather than a bespoke reimplementation of recursion, so the published CodeTraceBench numbers (13.47%/32.73%) can be beaten by the real recursive path instead of the one-shot baseline. (2) Delete ~5000 lines of custom agent-loop machinery and a third-party dependency (Ax) in favor of the upstream canonical implementation. (3) Introduce a clean engin
- Assessment: Sound and in the grain of the codebase. The engine seam is well-placed: the definition, finding schema, cost ledger, and exact-run identity are the stable substrate, and the engine is the variable — createDspyRlmTraceEngine reuses the existing external-optimizer subprocess spawner, model proxy, and cost-metering infrastructure rather than inventing new process management. analyzeTraces (src/trace-
- Better / existing approach: Searched for an existing recursive/agent-loop implementation to reuse: the old trace-analyst/loop.ts WAS the bespoke loop and it is correctly being deleted, not extended — so no existing capability is being reinvented. The one materially better design is at the HTTP boundary: trace-tool-callback.ts (src/analyst/trace-tool-callback.ts:37-58) is a near-verbatim copy of the authenticated-loopback-ser
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content
🎯 Usefulness — sound
The DSPy RLM trace-analyst engine replaces the deleted Ax stack and is fully wired into the CLI, public API, default registry, and benchmark command, reusing the existing external-optimizer subprocess/proxy grain.
- Integration: Fully reachable from four surfaces: the
agent-eval analyst-benchmarkCLI command (cli.ts:16 → runAnalystBenchmarkCommand, with createPublicBenchmarkRlmRunner as the default candidate runner at benchmark-command.ts:186), the public createDspyRlmTraceEngine export (src/index.ts:70), the standalone analyzeTraces wrapper (trace-analyst/analyst.ts:52, engine required), and buildDefaultAnalystRegistry - Fit with existing patterns: Follows the established grain precisely: the new engine reuses the same runExternalOptimizerProcess / startExternalOptimizerModelProxy / runWithCleanup trio that GEPA (gepa-optimization-method.ts:347) and SkillOpt (skillopt-optimization-method.ts:248) already use, and the Python bridge is a sibling of gepa_bridge.py and skillopt_bridge.py. TraceAnalysisEngine (engine.ts:60) is engine-agnostic by d
- Real-world viability: Handles realistic conditions: per-row finding parsing tolerates malformed model output without discarding the rest of a paid investigation (dspy-rlm-engine.ts:206-222); model-call counts are cross-checked between the bridge's self-report and the provider proxy's recorded completions and throw on mismatch (dspy-rlm-engine.ts:155); cost accounting is enforced per observation (benchmark-command.ts:34
- Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 Third copy of the authenticated loopback-server skeleton [duplication] ``
trace-tool-callback.ts reproduces the server lifecycle, abort handling, and active-handler tracking already in external-optimizer-callback.ts and external-optimizer-model-proxy.ts (same accepting flag, activeControllers/activeHandlers Sets, abortRequest that destroys request+response, closePromise ??= idempotent close, waitForActiveHandlers loop — compare src/analyst/trace-tool-callback.ts:37-58 and :124-138 with src/campaign/external-optimizer-callback.ts:26-66). A shared createAuthenticatedLoo
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
✅ No Blockers —
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Approved — 25 non-blocking findings — 1bed96cb
Full multi-shot audit completed 8/8 planned shots over 67 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-07-31T00:32:21Z · immutable trace
What
Trace analysts are now recursive research programs: the official
dspy.RLMruns in a sandboxed Deno/Pyodide child process with seven allowlisted trace tools, strict JSON I/O, and metered model calls through the external-optimizer proxy.callLlmJsonsurvives only as the explicit one-shotdirectbenchmark baseline. The Ax stack (ax-service,ax-cost-service,trace-analyst/loop,AxGepaSteeringOptimizer, the@ax-llm/axdependency) is deleted — audited zero external callers.New engine boundary:
src/analyst/engine.ts,dspy-rlm-engine.ts,trace-tool-callback.ts,benchmark-public-rlm.ts, andclients/python/src/agent_eval_rpc/dspy_rlm_bridge.py(officialdspy.RLMin a child process; 14 dedicated tests plus a live-Deno inspect path).Why
The published CodeTraceBench numbers (13.47% all-row / 32.73% scored F1 with test artifacts; 15.02% / 30.99% trajectory-only) came from the one-shot direct runner. The recursive-RLM engine is the path to beating that; the semantics were pinned as "a trace analyst IS an RLM program", and the code now matches.
Audit hardening (two independent adversarial passes)
Security/correctness and API/DX audits ran against the full range. Fixed in this PR:
span_id, status, name, or a timestamp digit verified. Excerpts now match spanattributes/status_message(or a cited finding's content fields) only, with an 8-char minimum; six attack cases pinned as tests.runner.jsimportsnpm:pyodideunversioned and--no-configmeans no lockfile — npmlatestflipped0.29.3→314.0.3during development, silently switching interpreters. An import map now pinspyodide@314.0.3and the pin is recorded in runtime identity._runtime_identityignored the deno command and reported constants. The sandbox record is now derived from the command that ran (config, node-modules-dir, permission flags with scoped-path counts, never raw paths).runtime.rejectedFindings.step:2, which the TypeScript finding-subject grammar rejects; fixture now emitsincorrect-step-2.answer/findings_jsonraised after all money was spent; model output is stripped before validation.verify-package-exportsdemanded the deletedbuildTraceAnalystTools; it now negative-pins all removed exports. CHANGELOG documents the full breaking surface. The benchmark README states its numbers describe the retired runner and forbids citing them for the new engine without a fresh certified run.What the security audit could NOT break: live sandbox escape attempts (host FS, network, env, subprocess — all blocked), tool-allowlist bypass (enforced on both sides, zero schema drift across all 7 tools), secret leakage into the child (allowlisted env, ephemeral proxy token only), ReDoS (RE2JS), subprocess leaks (process-group kill with escalation), and the store-backed citation check (base64/foreign ids rejected; net-new versus the pre-image).
Known deferred (audit findings not in this PR)
output_tokensabove the completion cap; the proxy's over-output rejection then kills the run (availability, not cost, is the exposure).maxToolCallsnot forwarded; response body dropped byraise_for_status), burning iterations.--max-cost-usdfeeds both the per-analysis and run-wide caps; per-analysis needs its own flag.traced-analyststep spans are emitted post-hoc with ~0 duration (onTurnremoval).ModuleNotFoundError; the bridge'sinspectpreflight exists but is unwired from TypeScript.DEFAULT_TRACE_ANALYST_KINDS,*_KIND_SPEC,kind-factory.ts).Verification
pnpm typecheckclean;pnpm test4,525 passed / 0 failed / 3 skipped (339 files);pnpm build+node scripts/verify-package-exports.mjsgreen.inspectthrough real Deno booting pinned pyodide314.0.3with the derived attestation. (test_official_optimizer_compatibility/skillopt failures in a local venv reproduce identically at the merge base — environment extras, not this change.)44633a52…(78 files), dependency lockb00ffeed…(4 files), checker green.Not in this PR
A fresh certified benchmark run with the DSPy engine (paid) and the labeled comparison against the retired runner's numbers. Until that lands, no headline accuracy number may be cited for the new engine — the reference-result test and benchmark README both enforce the provenance split.