tau2 airline — SPA variant (tau2_airline_spa) - #316
Conversation
|
❌ 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. |
|
Thanks for this, @aviweit — this is a substantial piece of integration work and it's clear how much plumbing went into it. I reviewed the diff and then actually stood the whole stack up on a clean machine (real LLM via an OpenAI-compatible LiteLLM gateway, What's good
What I ran
Run B's accept line, for the record: {"kind": "step", "candidate": "cand_0001", "accept": true,
"reason": "paired Δ̄=+0.5000 > 0.0·SE=0.0000 (SE=0.2887, n=4)",
"val": 0.5, "parent": "seed", "parent_val": 0.0,
"optimizer_seconds": 794.11, "runner_seconds": 185.53, "opt_cost_usd": 9.404635}Per task, tasks 11 and 19 flipped So the design is sound and the whole pipeline — eval, diagnose, propose, upload, restart, re-eval, gate — does work. Run A vs Run B is the diff between "works" and "doesn't": two config/wiring gaps, both fixable in this PR. Details below. Blocking0. The frozen seed mis-declares the primitive signatures, which artificially depresses the baseline. This is the one I'd fix first, because it affects what any number from this example means. def book_reservation(..., flight_type: str, cabin: str, flights: str,
passengers: str, payment_methods: str,
total_baggages: 0, nonfree_baggages: 0, insurance: str): ...tau2's actual airline tool is: def book_reservation(..., flight_type: FlightType, cabin: CabinClass,
flights: List[FlightInfo | dict], passengers: List[Passenger | dict],
payment_methods: List[Payment | dict],
total_baggages: int, nonfree_baggages: int, insurance: Insurance): ...Same for That's not theoretical — it's what I measured. Four tasks (8, 11, 19, 22), one trial: baseline val 0.000, all four "Database state does NOT match the expected final state". And the optimizer's own
Three of its five design choices ( 1. The per-candidate SPA restart crashes the whole run. The optimizer created Three things stack up here:
2. skill_name = skill_dirs[-1].name
if skill_name != Adapter._current_skill_name:
spa_env.restart_spa(skill_name)
3. 4. LLM-authored code runs unsandboxed on the host. 5. The guard-bypass claim doesn't hold as shipped.
Medium6. The quick-test spec can't do what it claims. It's described as "verify the full eval+optimize pipeline end-to-end", but:
7. 8. No holdout in {"kind": "splits_warning", "msg": "test overlaps train/val (no-holdout fit) — the test number is NOT held out; report it as a fit metric"}That's a legitimate deliberate choice ( 9. Runner cost and tokens are always zero. From a real rollout: 10. The custom optimizer instructions can silently not apply. 11. 12. No smoke/run entrypoint, no docs, not on the site. Every other example ships 13. Setup is fragile on a cold machine. Two things I hit:
Small stuff
Suggested orderIf it were me: #0 first (it decides what any number here means), then #1 and #2 (the difference between "a run finishes" and "a run reports a trustworthy number"), then #6 so the shipped quick-test can actually exercise the loop out of the box, then #3/#4/#5, then docs + None of this changes the headline: the architecture works, and the loop closed with a gate-accepted |
|
Addendum — Run B's Same four task ids in
Two things fall out of that:
Credit where due on the same output: the infra failure was handled exactly per the contract — |
|
Last addendum — the run finished, and the {"kind": "finalize", "test_reward": 0.3333, "test_baseline_reward": 0.5,
"test_delta": -0.166667, "best_id": "cand_0001"}The seed scored higher on test than the optimized candidate. Laid out per task (same four ids in
The seed went 0.000 → 0.500 on identical tasks between its val and test evaluations — it passed tasks 8 and 19 on the second roll having failed both on the first. Task 19 is precisely the task the candidate's SKILL.md cancellation prose was written to fix, and the unmodified seed passes it too on a re-roll. So the
That's everything from my side. Net read unchanged: the integration is real, the loop closes, and the work needed is in |
…an push startup. - the default is 20 attempts * 5s = 100s. Increase the store's max to 60 attempts (300s = 5 minutes).
A flaky SPA restart (stale PID sentinel, port contention, slow cold start) previously propagated a RuntimeError that aborted the entire optimization run. Now retries up to 2 times with stop/cleanup between attempts before raising, so transient failures don't lose the whole run budget.
- the skill has 14 wrapper tools into the primitive tools - optimizer works on that skill on every iteration
Every cap-evolve run must start against a store holding exactly the frozen primitives plus one freshly-imported skill, so a baseline measures the seed and never a modified skill or a stale wrapper left by an earlier run. spa_env.reset_store_to_skill() is the new entry point, called from adapter.apply() -- which cap-evolve invokes via live() before EVERY evaluation, with the baseline handed the seed, so run start is covered by construction rather than by a separate hook. It deletes the skill with its own tools and snippets, purges non-primitive orphans from earlier runs, re-imports the skill directory, then re-verifies the primitive set and raises if any went missing. The store's own cascade cannot be used for this. DELETE /skills/<name> ?delete_tools=true runs its tool cascade while the skill is still registered as a dependent of its own tools, so every tools_service.delete raises ObjectInUseError, which the cascade swallows as a warning: the skill goes away, the tools silently survive and deleted_tools comes back empty. Re-importing then mints fresh UUIDs under the same names, orphaning ~14 tools per iteration. Deleting the skill FIRST frees the dependency and lets the tools be removed. The frozen primitives are never touched. A tool is spared if it carries the primitive-tool tag, or its name is one of the 14, or it came from functions.py, and also whenever its metadata cannot be read -- unknown means keep, since leaking a stale wrapper is far cheaper than deleting a primitive. Also fixes a latent hole in the primitive tripwire: collection endpoints return a bare JSON list when no limit/offset is passed, but the check looked for a "tools" key and so returned an empty set, passing vacuously. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review skillberry-ai#1: a failed SPA restart aborted the whole run. apply() raised, and core enters live() inline, so the RuntimeError escaped the evaluation and lost the remaining budget over one flaky restart. apply() now records the failure in Adapter._deploy_error instead of raising; run_batch/run_trials short-circuit to Rollout(error=...), which aggregate_scores already excludes from the mean rather than scoring 0.0. The flag is cleared at the top of every apply(), so a failure never leaks into the next candidate. A candidate with no SKILL.md follows the same path -- it previously raised. Also: setup.sh's printed "Next:" command now exports SPA_PROVIDER_NAME and SPA_MODEL_NAME so the documented run path reproduces the verified provider. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review skillberry-ai#3: stop_spa() SIGKILLed whoever owned port 7000 -- on macOS that is ControlCenter (AirPlay Receiver). It now uses the PID SPA recorded in its sentinel, and falls back to the port owner only after confirming the process is SPA, warning instead of killing when it is not. SIGTERM before SIGKILL. Every port lookup now filters -sTCP:LISTEN. Unfiltered, lsof also returns clients of the port -- including cap-evolve's own hill-climb runner talking to SPA, which the old kill -9 would have killed mid-run. SKILLBERRY_AGENT_PORT is dropped: it moved the health check but not the routing, which tau2 and SPA hardcode. PROMPT.md documents 7000/7001 as fixed and the AirPlay conflict; setup.sh and start_spa() preflight the ports and fail fast naming the offending PID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- delete split_ids_task9.json (byte-identical to split_ids.quick-test.json, referenced by no spec); PROMPT.md now points at the shipped split files and states there is no --split-ids-file flag, because there isn't one - PROMPT.md: drop the reference to the untracked DESIGN-optimization-rework.md - PROMPT.md §7: regenerate the budget block from capevolve.yaml (was 20/1/1 vs the spec's 40/10/5) - setup.sh: accept either OPENAI_API_BASE or OPENAI_BASE_URL and derive the other, instead of demanding the same URL twice; refuse two different URLs - spa_env.py: drop the unused urllib.error import Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review skillberry-ai#9: runner cost/tokens always report zero, so max_usd can never bind. Fixing that needs SPA to return usage with the completion, which is out of scope here, so take the reviewer's other option and document it. PROMPT.md §7 gains "ONLY OPTIMIZER SPEND IS BUDGETED HERE": every rollout reports cost_usd 0.0 / tokens 0, max_usd is inert and only max_optimizer_usd and optimizer_usd_per_iter bind, the cost panel is blank for the agent half, and a 0 there means "not measured" rather than "free". Records the mechanism from tau2's code: get_cost() returns None if ANY message lacks a cost, and the orchestrator then nulls BOTH halves -- so the unpriced SPA-proxied agent messages also zero the user-simulator cost, which never touches SPA. The model strings here are absent from litellm's price map too, and the adapter hardcodes tokens=0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review skillberry-ai#11: teardown.sh removed $REPO/.venv and all of $REPO/.capevolve, both shared with skillsbench and tau2_airline -- the latter holding every example's run artifacts. teardown.sh now does three things: stop the three services, remove their PID sentinels and logs, and remove the repos this example cloned (vendor/skillberry-{store,agent,benchmarks}). One option, --keep-clones. .capevolve is not touched at all -- run artifacts and the shared project dir are the user's to manage, and setup.sh refreshes this example's files there on every run. .venv is never removed. vendor/ itself survives so vendor/skillsbench does too, and is rmdir'd only if empty. No flag can reach anything shared, and safe_rm hard-refuses /, $HOME, $REPO, .capevolve, the venv and anything outside the repo. On exit it warns which paths were deliberately left. PROMPT.md gains a "SETUP / TEARDOWN SCRIPT REQUIREMENTS" section so a regenerated teardown obeys the same contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review skillberry-ai#10: cap-evolve resolves optimizer_instructions_file relative to the CWD first and only then relative to the project (core/cap_evolve/cli.py). The repo-relative path therefore resolved only when the run started from the repo root; from any other directory the flag was silently omitted and the optimizer got the generic scaffolded template -- losing the MODIFY-only constraint and the store-import rules with no warning. setup.sh now copies optimizer/INSTRUCTIONS.md into $PROJECT/optimizer/ (the pattern examples/skillsbench/setup.sh already uses) and all three specs use the project-relative optimizer/INSTRUCTIONS.md. Verified from four working directories: all now load the custom file; previously only the repo root did. This also fixes a check nobody had run: pipeline_selftest.py resolves the key strictly project-relative, so the old value would have been reported as pointing at a missing file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@OsherElhadad , thanks a lot for the feedback. I made changes fixing issues and addressing most of your comments. Can you please try it again? I am aware of comment number 0. The primitive tools seed, including their signature, are based on the skillberry-skill-maker/skillberry-benchmarks (cc @eranra , @bcarmeli ). I think addressing it requires more design changes that we may want to discuss. Our first goal would be to integrate Tau2 SPA into Cap-evolve and to see that the optimization process properly use SPA as a black-box optimization approach. Please find below my current settings I am using for the end to end runs. Thanks ! Settings.envcapevolve.yamlsplit_ids.jsonInvocationteardown
note: does not touch setup
runCopy/paste the invocation command that setup.sh emits. |
tau2 airline — SPA variant (
tau2_airline_spa)Adds a new end-to-end cap-evolve example that optimizes tau2 airline tasks with
LLM calls routed through Skillberry Proxy-Agent (SPA) + Skillberry Store,
instead of calling the upstream LLM directly.
What's new
adapters/adapter.pyrun_tasksto SPAadapters/spa_env.pyseed_capability/primitive_skill/optimizer/INSTRUCTIONS.mdsetup.sh/teardown.shcapevolve.yamlcapevolve.smoke.yamlcapevolve.quick-test.yamlsplit_ids*.jsonPROMPT.mdKey design points
createnewcomposite skill packages alongside
primitive_skill/. It cannot edit the seed.adapter.apply()uploads the new composite skillto the store and restarts SPA with
SKILL_NAME=<candidate>before each eval.OPENAI_API_KEY/OPENAI_BASE_URL(or theirIBM_*equivalents) — nothing hardcoded.skillberry-benchmarks @ a3a8326,skillberry-store @ 0.2.1,skillberry-agent @ e359494.How to run