examples: add the spa/direct arm of the tau2-airline onboarding - #424
examples: add the spa/direct arm of the tau2-airline onboarding#424aviweit wants to merge 5 commits into
Conversation
Export the live project as examples/skillberry_benchmarks_tau2_airline/spa/ — the `intervention: spa` arm of that example's PROMPT.md, where the capability is the airline agent's TOOL SURFACE only and reaches the model as a Skillberry Store skill the proxy injects. The `spa/` subfolder keeps the prompt's ONE-ARM-PER-RUN rule visible: the `direct` arm is a separate onboarding in its own project and lands as a sibling, never merged into this one. Contents: both run specs, the adapter + gateway, the generated seed capability (empty SKILL.md + 14 tool scripts + the frozen primitives module), the optimizer instructions, and the split files. PROJECT.md is deliberately NOT exported: it is a project-scaffold artifact read only by the dashboard for display, no example carries one, and its text referred to run directories that exist on one machine. Two paths were rewritten so the export is portable; every other file is byte-identical: - optimizer_instructions_file: absolute -> project-relative `optimizer/INSTRUCTIONS.md`, which resolve_instructions_file() already treats as its default. - runner_repo_path: ../../vendor/... -> ../../../vendor/..., since spa/ sits one level deeper than the live project dir. No credentials: .env is not exported, and the gateway reads OPENAI_BASE_URL/_API_BASE/_API_KEY from the environment. Run trajectories are output, not input, so they stay in their run dir. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Avi Weit <weit@il.ibm.com>
Test: end-to-end optimization run on the SPA armRan a full optimization through the Configuration
Result
Best candidate: Both iterations logged a What I verified1. The store is refreshed with the candidate's skill before every evaluation. The adapter's Worth stating precisely: the refresh is per evaluation, not per iteration — which is the 2. The deployed skill's Second test: 3-iteration run on the same armRepeated the test with a longer budget — Result
Best candidate: All three rejections came from the STRICT fallback ( What the candidates edited, all inside
Same two properties verified again1. Store refreshed with the candidate's skill before every evaluation — 5 evaluations in 2. Tools-only capability — |
|
❌ Automatic Labeling Failed An error occurred while trying to automatically label this pull request. Please check the workflow logs for details and add labels manually. |
Signed-off-by: Avi Weit <weit@il.ibm.com>
18ed80d to
fc895d5
Compare
The spa/ arm shipped a spec, a seed and an adapter but no way to get from a fresh
checkout to a runnable project, so onboarding it meant reading ../PROMPT.md and
hand-running the steps. The other examples each carry a setup.sh; this is the
executable transcript of the same intake phase for this arm.
Five steps: venv + core CLI (dashboard backend optional, --no-dashboard skips
it); the benchmark at its PINNED commit; the Skillberry stack; the project
wiring; the check gate.
Three things the SPA arm needs that the direct arm does not:
* the benchmark is the skillberry build, PINNED, installed with the
[skillberry] extra — that extra is what registers the airline_skillberry
domain, and without it every rollout dies at env construction. Pinned rather
than latest main because the recorded results belong to that commit.
* the Skillberry stack (Store + Proxy-Agent) is PROVISIONED here and started
by run.sh. The intervention skill names a run that provisions on the
operator's behalf as the anti-pattern, so setup does not start services.
* the seed is two halves — my_skill/ (edited) and primitive_tools/ (frozen,
covered by protected_paths) — so both are copied.
It scaffolds into its own base, .capevolve-spa, not the shared .capevolve. The
two arms are separate onboardings; a shared project dir means one arm's seed and
spec overwrite the other's, which delivers candidates one way while the record
says the other. Run dirs land in that base too, so neither arm's runs mix.
The registered-domain check WARNS and never dies: tau2's registry API is not
ours to depend on, and `cap-evolve check` plus the first rollout are the
authoritative gates.
Verified from a clean slate (no venv, no vendor/, no project): exits 0, installs
the benchmark at a3a8326 with airline_skillberry registered, provisions store
0.2.1 + agent e359494, wires .capevolve-spa/project, and `cap-evolve check`
returns ok: true. No credential value is ever echoed — the preflight tests the
variables and greps .env with -q, printing only names.
Signed-off-by: Avi Weit <weit@il.ibm.com>
setup.sh provisions; this starts and runs. Splitting them is the intervention
skill's rule: a run that provisions on the operator's behalf is the anti-pattern,
because provisioning clones and installs, and a run should only ever start what
is already there.
Three services, in an order that matters:
* the benchmark's environment service (:8004). POLLED, never slept on —
importing tau2 pulls in litellm so the first start is ~10s, and a fixed sleep
either wastes time or races the service. LITELLM_LOCAL_MODEL_COST_MAP=True
skips litellm's doomed remote cost-map fetch, which otherwise stalls startup
until timeout.
* the Store (:8000) BEFORE the Proxy-Agent, because SPA binds ONE skill by name
at start and needs the store to answer when it does.
* SPA (:7000) bound to my_skill.
Both starts are idempotent — a healthy service is reported, not restarted.
Restarting SPA mid-evaluation would swap the skill under a running rollout, so
the stack is left running at exit and the stop command is printed instead.
The model split is a correctness rule, not a preference: the AGENT is the
ibm/skillberry-local sentinel that tau2 routes to the proxy, and the USER
SIMULATOR goes STRAIGHT to the gateway. Proxying the simulator would inject the
capability into the very thing measuring the agent. Concurrency defaults to 4,
not the direct arm's 125: every agent call funnels through one proxy and one
store, so a high setting just queues behind them and times out.
--smoke selects the smoke spec; SPEC= and RUN_TS= override. RUN_TS is only
defaulted when the caller did not pin it, so --smoke cannot write into the full
run's directory.
Verified live end to end (exit 0): all three services came up healthy in order
with SPA bound to my_skill and every pid owned by spa_env rather than adopted
from a squatter; cap-evolve resolved the spec from .capevolve-spa and echoed the
same topology; 3/3 rollouts scored with ZERO errored trials through the proxy;
seed and cand_0001 both 1.0; the gate correctly refused a zero delta; test
sealed once at 1.0 and the run finalized.
Signed-off-by: Avi Weit <weit@il.ibm.com>
| # The pinned benchmark checkout, READ-ONLY: task definitions, the real tool | ||
| # implementations, and the reward checks. | ||
| # One level deeper than the live project (examples/<name>/spa/), hence ../../../. | ||
| runner_repo_path: ../../../vendor/skillberry-benchmarks |
There was a problem hiding this comment.
runner_repo_path is resolved as Path(project) / value (core/cap_evolve/specfile.py::resolve_project_path), where project is the deployed project dir passed via --project — and cli.py itself assumes workdir = proj_abs.parent.parent, i.e. the project dir is always exactly two levels below the repo root (.capevolve/project or, here, .capevolve-spa/project).
spa/setup.sh sets BASE="$REPO/.capevolve-spa" and PROJECT="$BASE/project" — also two levels below $REPO. So ../../../vendor/skillberry-benchmarks (three ../) resolves to a directory one level above the repo root, not to $REPO/vendor/skillberry-benchmarks where setup.sh actually clones the benchmark:
>>> from pathlib import Path
>>> p = Path('/repo/.capevolve-spa/project')
>>> (p / '../../../vendor/skillberry-benchmarks').resolve()
PosixPath('/vendor/skillberry-benchmarks') # what this spec produces (wrong)
>>> (p / '../../vendor/skillberry-benchmarks').resolve()
PosixPath('/repo/vendor/skillberry-benchmarks') # what setup.sh actually clones (right)
direct/capevolve.yaml uses ../../vendor/skillberry-benchmarks (two ../) for the identical project-dir depth, and that one is correct.
The PR description justifies the extra ../ as "spa/ is one level deeper" — but that's true only of the example source layout under examples/…/; it isn't true of the deployed project dir at runtime ($BASE/project is the same depth for both arms). cap-evolve check won't catch this either — pipeline_selftest.py passes runner_repo_path through unresolved just to check template placeholders; it never verifies the resolved path exists. So this silently hands the optimizer a --bench-repo pointing at the wrong (likely nonexistent) directory, degrading its read-only benchmark context with no error.
Same issue in capevolve.smoke.yaml:52.
OsherElhadad
left a comment
There was a problem hiding this comment.
Reviewed the diff, checked out the branch into a worktree, and traced the path-resolution logic against core/cap_evolve/cli.py / specfile.py. Summary:
Bug (see inline comment on spa/capevolve.yaml:51, also present in spa/capevolve.smoke.yaml:52): runner_repo_path: ../../../vendor/skillberry-benchmarks resolves to the wrong directory (one level above the repo root) given how spa/setup.sh actually lays out the project ($REPO/.capevolve-spa/project, same depth below $REPO as the direct arm's .capevolve/project). direct/capevolve.yaml's ../../vendor/skillberry-benchmarks is correct for the same depth. cap-evolve check won't catch this since pipeline_selftest.py never verifies the resolved path exists — it just checks template placeholders. Net effect: the optimizer silently gets no/wrong read-only benchmark context (task defs, real tool implementations, reward checks) during actual runs.
Structural asymmetry between the two arms: spa/ ships setup.sh + run.sh as the executable transcript of onboarding (per the file headers, these reproduce the intake/implement-and-check steps in one command). direct/ has no equivalent — no setup.sh/run.sh at all, so there's no reproducible way to stand up and run that arm short of manually re-deriving the onboarding steps from PROMPT.md. Worth adding for parity, or noting explicitly if it's intentionally out of scope for this PR.
Duplication: direct/adapters/adapter.py and spa/adapters/adapter.py share ~300 lines essentially verbatim — _STOP_LEAK_RE/_leaked_stop_continuation, _shown_metrics, _sim_to_rollout, score, and the whole gold-safe argument-level feedback stack (_iter_agent_tool_calls, _user_profile_facts, _localize_action, _localize_communicate, _derive_total_cost, _build_feedback). Given the PR's own stated goal of every-file-byte-identical-except-two-paths portability for examples, duplicating this much scoring/feedback logic across two "arms" of the same benchmark means any future fix to the feedback logic (e.g. the _localize_communicate heuristic, or the STOP-leak regex) has to be applied twice and will drift if someone updates only one. A shared module (even just within examples/skillberry_benchmarks_tau2_airline/) imported by both would remove the drift risk without hurting the "copy this example directory and it just works" portability goal.
Positives:
- Good discipline throughout on gold-safety:
score()/_build_feedback()never read or echo the ground-truth values, only key names — well-guarded, and the module docstrings say why. - The
apply()guard-by-candidate-shape (checking fortools/tools.pyvsmy_skill/SKILL.mdand naming the likely spec/seed mismatch in the error) is a nice fail-loud touch for what would otherwise be a confusing silent misconfiguration. gateway.py(both arms) is careful never to putapi_keyintollm_args, backed by a realassertin its own__main__self-check — good, sincetrajectories()/store: gitwould otherwise commit it.- The
registry._domainsmonkeypatch indirect/adapters/adapter.pymirrors the existing pattern already used inexamples/tau2_airline/adapters/adapter.py, so it's consistent with precedent in this repo (not a new global-state risk introduced by this PR), even though the baseCapabilityAdapterdocstring explicitly calls outapply()'s global-side-effect model as the reasonmaterialize/liveexists for concurrent-candidate evaluation. Not blocking, just flagging that this arm inherits that same single-global-slot constraint.
Not verified: I did not run an actual cap-evolve check/smoke run end-to-end — that requires cloning vendor/skillberry-benchmarks at the pinned commit, installing tau2-bench[skillberry], and live gateway credentials (OPENAI_BASE_URL/OPENAI_API_KEY), none of which are available in this review environment. The runner_repo_path bug above was verified by tracing the actual resolution code path and reproducing the path arithmetic locally, not by running the harness.
What
Exports the live cap-evolve project as
examples/skillberry_benchmarks_tau2_airline/spa/—the
intervention: spaarm of that example'sPROMPT.md. The capability is the airlineagent's tool surface only, and it reaches the model as a Skillberry Store skill that the
proxy injects into the agent's LLM calls, so the benchmark never sees skill files.
The
spa/subfolder keeps the prompt's one arm per run rule visible: thedirectarm is aseparate onboarding in its own project and lands as a sibling, never merged into this one.
The two arms' numbers are not like-for-like and should never be averaged.
Contents
capevolve.yaml+capevolve.smoke.yaml— the real and smoke run specsadapters/adapter.py+gateway.py—tasks/run_trials/score/trajectories/apply,and the OpenAI-compatible gateway wiring
seed_capability/— the generated seed: an intentionally emptymy_skill/SKILL.md,14 agent-visible tool scripts, and the frozen
primitive_tools/functions.pyoptimizer/INSTRUCTIONS.md(+ the prompt-only variant)split_ids.json(all 50 ids) andsplit_ids_task9.json/smoke_split.json(
train = val = test = ["9"])Both specs set
protected_paths: ["primitive_tools/*", "my_skill/SKILL.md"]. Under SPA,SKILL.mdis the prompt-enrichment channel, so freezing it alongside the frozen substrate iswhat makes this a genuinely tools-only capability.
Export changes
Two paths were rewritten so the export is portable; every other file is byte-identical
to the live project:
optimizer_instructions_fileoptimizer/INSTRUCTIONS.md(project-relative — alsoresolve_instructions_file()'s default)runner_repo_path../../vendor/…../../../vendor/…(spa/is one level deeper)Both specs were re-read through cap-evolve's own spec reader afterwards:
intervention,skill_name,protected_paths, split files, and instructions file all resolve, andrunner_repo_pathlands on the pinnedvendor/skillberry-benchmarkscheckout.