Specialised LoRA training for tool selection and multi-step orchestration in a 0.5B model
Technical report — Cognitive Core Lab, November 2026. All numbers in tables are generated from run artifacts by scripts/analyze.py and scripts/build_report.py.
Summary
Result. LoRA training of a 0.5B model on 1,000 verified workflow episodes (about 38 minutes on a 16 GB Apple M4) raised workflow success on held-out tasks from 2.2% (untrained) and 12.8% (few-shot prompted) to 53–59% pooled over the four held-out splits, and to 86–91% on new instances of training templates. Every trained configuration beats both baselines on in-distribution, unseen-template and renamed-catalogue tasks (Holm-adjusted p ≤ 0.021).
Where it stops. Gains shrink sharply outside the training distribution. On 16 task templates never seen in training, success is 30–36% (the 4B teacher: 56%). On tool domains absent from training, trained models are no better than the few-shot prompt (≈33% for both), and one template in that split turned out to be flawed (§7). Success on tasks whose dependency graph was seen in training (72–74%) is roughly double that on novel graphs (26–38%): much of what is learned is pattern-specific.
Signal source. Teacher demonstrations (B) and oracle trajectories (C) give statistically indistinguishable aggregate results (55.8% vs 55.9%), with different strengths that track what each data source contains: B almost never handles human approval (0.9%; the teacher produced one accepted approval episode), C is stronger on failure recovery (87.0% vs 66.7%). Mixing both (Dmix, 59.1%) is numerically best, but its advantages do not survive multiple-comparison correction. Training on the two sources sequentially (Dseq) collapses on unseen tool domains (9.2% vs 36.4% for Dmix; −27.2 pts, Holm-adjusted p = 0.021).
Students vs teacher. Distilled students exceed the teacher on in-distribution tasks (86.0% vs 81.0%), because only verified teacher episodes were kept. They use about a quarter of the teacher's peak memory (3.0–3.9 GB vs 13.2 GB) and have 7–10× lower mean latency per episode (measured at different batch sizes, §7). They do not inherit the teacher's generalisation.
Failures. The first two training configurations diverged (§4.1) and are preserved as negative results. All numbers below come from results/ and runs/, generated by scripts/analyze.py.
1. Question and hypotheses
Can a 0.5B-parameter open-weight model be made into a usable tool orchestrator through cheap LoRA training, and does it matter whether the training signal comes from a stronger model or from an independent, programmatic source?
- H1 (training helps). Every LoRA configuration (B, C, D) beats the unmodified baseline (A) on workflow success on held-out tasks.
- H2 (beyond format). The gain is not only output-format compliance: trained models also beat a few-shot-prompted baseline (A⁺), and they improve on diagnostics that format alone cannot fix (dependency safety, argument correctness, recovery, permission handling).
- H3 (generalisation vs memorisation). Gains carry over, with some loss, to unseen task templates (
eval_ood), unseen tool domains (eval_unseen) and renamed catalogues (eval_renamed). A model that only memorised its training graphs would collapse on these splits. - H4 (signal source). With the same budget, teacher demonstrations (B) and oracle trajectories (C) produce different strengths. The comparison is open-ended: B inherits the teacher's blind spots and the coverage bias of rejection sampling, while C is exhaustive but stylistically uniform.
- H5 (combination). Combining sources (D) is at least as good as the better single source, and the combination strategy (mixed vs sequential) matters little.
2. Setup
2.1 Models
- Student / foundation checkpoint:
Qwen/Qwen2.5-0.5B-Instruct@7ae557604adf(Apache-2.0). Every configuration starts from these exact weights. The selection rationale and measured alternatives are in MODEL_SELECTION.md. - Teacher:
Qwen/Qwen3.5-4Bas the MLX 4-bit conversionmlx-community/Qwen3.5-4B-MLX-4bit@32f3e8ecf654(Apache-2.0), run locally in non-thinking mode. No hosted API was available. - Oracle: a programmatic policy (
ccl/oracle.py) that reads the task's gold dependency graph and reacts to real observations. It is the source of config-C data and the validator's ceiling. It is not a model.
2.2 Environment: tools, runtime, action schema
21 typed tools in 12 domains (contacts, calendar, email, weather, FX, orders, files, tickets, maths, flights; held-out: inventory, maps), each simulated deterministically over a seeded world (ccl/tools.py). Four tools need human approval (send_email, issue_refund, delete_file, book_flight), and two are role-gated (issue_refund → support_agent, delete_file → admin). Faults are injected deterministically: transient retryable timeouts, permanent service_unavailable with a declared fallback tool, and not-found entities.
Each turn the model emits exactly one JSON object:
{"calls": [{"tool": "<name>", "args": {...}}, ...]} // >1 call = parallel, independent
{"final": "<answer>", "status": "done|refused|blocked|failed"}The runtime (ccl/runtime.py) parses the action leniently: it accepts code fences, <think> blocks and a bare call object, but records strict-schema compliance separately. It validates arguments against the typed schema, enforces role permissions itself (a forbidden call is never executed and is scored as a violation), asks an approver before consequential calls, and executes calls with deterministic limits: 8 turns, 6 calls per turn, 16 calls total, 2 parse errors, a 2 s tool timeout. Observations are appended as a user message. The same loop drives teacher generation, oracle generation, evaluation, the CLI (scripts/agent.py, with a real terminal approval prompt) and the live server (scripts/serve.py).
2.3 Tasks
44 scenario templates in 8 families: tool selection, args construction (relative dates, 12h→24h times, currency names and symbols to ISO codes, urgency to priority), sequential chains up to depth 5, parallel fan-out/fan-in, recovery (retry, fallback, not-found), permission boundaries, human approval (including denial), and termination (no-tool requests, stop when done). Every task has a gold DAG with per-argument matching rules, an expected final status, required answer facts and expected side effects. Catalogues include 2–6 random distractor tools. 25% of training tasks have tools and parameters renamed from a training alias vocabulary, so models must read the catalogue rather than memorise names.
Validation is executable (ccl/evaluate.py). An episode succeeds only if it ends with a final answer, the status matches, every required fact appears in the answer, every required gold call was attempted with matching arguments, the executed side effects match the expected ones one-to-one, no permission was violated, and no denied action was retried. No model judge is used anywhere.
2.4 Splits and leakage control
train_pool(3,000) andval_pool(150): the 24 training templates only, never the held-out tool domains.eval_id(200): new instances of training templates.eval_ood(240): 16 templates never used in training. These include new phrasings, new graphs (fan-in, chain-into-conversion, book-the-cheapest), depth 4–5 chains (training goes to depth 3) and new permission and approval situations.eval_unseen(120): 4 templates over tool domains absent from training (inventory, maps).eval_renamed(160): training templates with every tool and parameter renamed from a held-out alias vocabulary.eval_catalog_{0,5,12}(96 each): identical in-distribution tasks with fixed distractor counts.
Controls: (i) templates belong to exactly one split; (ii) any eval task whose request text occurs verbatim in the train or validation pool is dropped; (iii) a value-free gold-graph signature is computed per task, and its overlap with training is reported. Eval tasks are tagged graph_seen or graph_novel and results are stratified by that tag. (iv) Held-out tools never appear in training, not even as distractors. (v) Eval and train use different RNG seed ranges, and the name and city pools for training are a strict subset of those for evaluation. The audit is in data/v1/leakage_audit.json:
<!-- BEGIN:data -->
| variant | episodes | SFT rows (assistant turns) | sha256 (first 12) | families (episodes) |
|---|---|---|---|---|
| C | 1000 | 2315 | 1ef97f846690 | approval 39, args 86, chain 245, parallel 85, permission 82, recovery 116, select 259, termination 88 |
| C_25 | 250 | 584 | a47b755c5a2c | approval 11, args 18, chain 61, parallel 19, permission 20, recovery 36, select 65, termination 20 |
| C_50 | 500 | 1149 | 83278ddb3fb2 | approval 20, args 46, chain 116, parallel 49, permission 44, recovery 59, select 131, termination 35 |
| C_full | 3000 | 7000 | e936bda77a6c | approval 125, args 250, chain 750, parallel 250, permission 250, recovery 375, select 750, termination 250 |
| B | 1000 | 2102 | d84b49db8cce | approval 1, args 121, chain 146, parallel 122, permission 45, recovery 131, select 303, termination 131 |
| C_matched | 1000 | 2091 | 4e2b03d5d42f | approval 1, args 121, chain 146, parallel 122, permission 45, recovery 131, select 303, termination 131 |
| task file | n | sha256 (first 12) |
|---|---|---|
| tasks/train_pool.jsonl | 3000 | 649b77769d5b |
| tasks/val_pool.jsonl | 150 | 4109a155f1da |
| tasks/eval_id.jsonl | 200 | 6fabbbc2706a |
| tasks/eval_ood.jsonl | 240 | 25291967e978 |
| tasks/eval_unseen.jsonl | 120 | ac20b173e4fe |
| tasks/eval_renamed.jsonl | 160 | e9170bafa862 |
| tasks/eval_catalog_0.jsonl | 96 | e033c69e7616 |
| tasks/eval_catalog_5.jsonl | 96 | bcdc5d4db006 |
| tasks/eval_catalog_12.jsonl | 96 | ec9e9d0d3c2e |
| eval split | n | request overlap | template overlap | graph-signature overlap | held-out tools seen in train | dropped (verbatim request in train) |
|---|---|---|---|---|---|---|
| eval_id | 200 | 0 | 18 | 100% | 0 | 113 |
| eval_ood | 240 | 0 | 0 | 25% | 0 | 0 |
| eval_unseen | 120 | 0 | 0 | 0% | 0 | 0 |
| eval_renamed | 160 | 0 | 17 | 100% | 0 | 81 |
| eval_catalog_0 | 96 | 0 | 17 | 100% | 0 | - |
| eval_catalog_5 | 96 | 0 | 17 | 100% | 0 | - |
| eval_catalog_12 | 96 | 0 | 17 | 100% | 0 | - |
<!-- END:data -->
scripts/build_datasets.py reproduces every task file byte-for-byte; this was verified by rebuilding and comparing all 11 SHA-256 hashes.
3. Training data
3.1 B — teacher demonstrations (distillation through data)
The teacher ran the same runtime, with the same system prompt and catalogue rendering, on the first 1,600 tasks of the training pool. Pass 1 decoded greedily. Pass 2 drew one T=0.7 sample for every task rejected in pass 1 (rejection sampling). An episode was accepted only if it passed the executable validator and had no parse errors, hallucinated tools, invalid-argument errors, unnecessary calls or dependency-unsafe calls. Every episode, accepted or not, is kept with its rejection reasons in data/v1/teacher/pass{1,2}.jsonl.gz. No synthetic label was ever substituted for a missing teacher demonstration. Tasks the teacher could not solve are simply absent from B.
<!-- BEGIN:teacher -->
| pass | decoding | episodes | accepted | acceptance | rejection reasons (an episode can have several) |
|---|---|---|---|---|---|
| pass1 | greedy | 1600 | 939 | 58.7% | not_success 458, unnecessary_calls 304, parse_error 187, dependency_unsafe 185, invalid_args 117 |
| pass2 | T=0.7, top-p 0.95 (rejected tasks only) | 661 | 72 | 10.9% | not_success 443, unnecessary_calls 270, parse_error 214, dependency_unsafe 143, invalid_args 84 |
Teacher: mlx-community/Qwen3.5-4B-MLX-4bit (upstream Qwen/Qwen3.5-4B, apache-2.0). Tasks offered: 1600; accepted episodes: 1011 (63.2% of tasks). Accepted episodes by family: approval 1, args 122, chain 146, parallel 124, permission 47, recovery 132, select 308, termination 131. Wall-clock: pass 1 3.70 h, pass 2 1.96 h. API cost: $0 (local). <!-- END:teacher -->
3.2 C — independently generated, execution-verified trajectories
The oracle (§2.1) ran on the same task pool. Its trajectories pass through the same runtime and the same acceptance filter (3,000/3,000 accepted). C contains no model outputs.
3.3 Normalisation and budget matching
Assistant turns from both sources are re-serialised to canonical JSON, both as targets and in the history, so B and C differ only in their decisions (which tools, which arguments, sequential vs parallel, answer wording, status), not in surface format. Each single-source variant has exactly 1,000 episodes: B is a seeded random sample of accepted teacher episodes, and C covers 1,000 random tasks from the same 1,600 offered to the teacher. Every configuration trains for 250 optimizer steps × 8 episodes with identical hyperparameters. Dmix (B ∪ C, 2,000 episodes) therefore sees each episode about once; B and C see theirs about twice. Dseq runs B for 125 steps, then C for 125 steps, on one cosine schedule.
Ablations (single seed): C_matched, the oracle on exactly B's 1,000 tasks, isolates label source from task coverage; C_25 / C_50 / C_full train on 250, 500 and 3,000 oracle episodes at the same step budget.
4. Training
Final configuration (used for every reported run): LoRA rank 16, scale 20, dropout 0, applied to every linear layer of the last 16 of 24 blocks (5,865,472 trainable parameters, ~1.2% of the 494M model). AdamW, lr 1e-5, 20 warmup steps, cosine decay to 1e-6, global gradient-norm clipping at 1.0, batch 4 × 2 gradient-accumulation, max length 2,048, gradient checkpointing. The loss covers assistant tokens only. Each episode is packed into one sequence with every assistant turn supervised; this gives the same loss as one example per turn under teacher forcing, but processes the shared prefix once. Batch order is a deterministic function of the seed. Checkpoints hold adapter, optimizer state and step, and are written atomically; a test (tests/test_train.py) checks that an interrupted run, once resumed, reproduces the uninterrupted adapter. B, C, Dmix and Dseq use seeds 0, 1 and 2; the ablations (C_matched, C_25, C_50, C_full) use seed 0 only (see §7). All 16 reported runs pass the stability rule below (logs/stability.jsonl).
4.1 Failed training configurations (preserved)
The final configuration is the third one tried. The first two diverged; both are kept with their loss curves and evaluations in archive/, and none of their numbers is used in any comparison.
| attempt | setting | outcome |
|---|---|---|
v1 (archive/v1_lr1e-4_noclip) | all 24 layers, scale 20, lr 1e-4, no clipping | 10 of 11 runs diverged at the end of warmup (loss 0.2 → 6–14); their adapters emit malformed output or finalise without tool calls (0% success). C_s2 alone recovered (46.5% on eval_id). |
lr pilot (experiments/lr_pilot) | same, + clipping, lr ∈ {1e-5, 3e-5, 1e-4}, 60 steps on Dmix | 3e-5 and 1e-4 diverged; 1e-5 looked stable over 60 steps, so it was selected by the pre-registered rule. |
v1b (archive/v1b_lr1e-5_alllayers) | all 24 layers, scale 20, lr 1e-5, clipping | B_s0 diverged within 30 steps (pre-clip gradient norm ~10³ → ~2.8×10⁷). Stopped. |
stability study (experiments/grad_diag) | (a) last 16 layers, scale 20, lr 1e-5 vs (b) all layers, scale 2, lr 1e-4; 100 steps on B | (a) stable (grad norm 27 → 5); (b) failed the loss criterion. (a) adopted for all runs. |
Diagnosis (experiments/grad_diag/init_grads*.log): at initialisation, before any update, the gradient is dominated by v_proj.lora_b in the earliest adapted layers (layer 0 when all layers are adapted) and varies ~20× between batches. With LoRA scale 20, small updates to these layers move the model sharply; restricting LoRA to the last 16 blocks (MLX-LM's default) removes the most sensitive layers. Selection rules were written before each pilot ran (PLAN.md in each folder). One weakness: the lr pilot's raw-loss rule was noise-sensitive (single-batch loss at initialisation already spans 0.32–0.80) and only 60 steps long, which is why it passed an lr that later diverged; the stability study uses a smoothed loss and a gradient-norm criterion over 100 steps.
<!-- BEGIN:training -->
| run | steps | train time (min) | peak mem (GB) | final val loss (oracle val) | trainable params | adapter sha256 (first 12) |
|---|---|---|---|---|---|---|
| B_s0 | 250 | 38.0 | 4.01 | 0.4115 | 5,865,472 | 9ff128410465 |
| B_s1 | 250 | 37.1 | 4.01 | 0.4062 | 5,865,472 | df5a7ea40248 |
| B_s2 | 250 | 37.8 | 4.01 | 0.4319 | 5,865,472 | 4e81a988bdca |
| C_25_s0 | 250 | 38.5 | 3.80 | 0.0095 | 5,865,472 | 1fa38da12bef |
| C_50_s0 | 250 | 37.5 | 3.80 | 0.0042 | 5,865,472 | 9f26700b3b26 |
| C_full_s0 | 250 | 34.8 | 3.90 | 0.0024 | 5,865,472 | 76551ac50010 |
| C_matched_s0 | 250 | 37.6 | 3.90 | 0.0095 | 5,865,472 | 87ce5656ffef |
| C_s0 | 250 | 37.6 | 3.90 | 0.0031 | 5,865,472 | 7354e6f5c4bc |
| C_s1 | 250 | 37.0 | 3.90 | 0.0039 | 5,865,472 | 029a043ed73f |
| C_s2 | 250 | 37.9 | 3.90 | 0.0028 | 5,865,472 | 28539a4006dc |
| Dmix_s0 | 250 | 37.2 | 4.01 | 0.0143 | 5,865,472 | 3533d0103d9e |
| Dmix_s1 | 250 | 40.9 | 4.01 | 0.0120 | 5,865,472 | 4e6d38c2605a |
| Dmix_s2 | 250 | 37.6 | 4.01 | 0.0137 | 5,865,472 | 67fc1ebaf6b6 |
| Dseq_s0 | 250 | 37.2 | 4.01 | 0.0033 | 5,865,472 | dac1f13925e0 |
| Dseq_s1 | 250 | 41.3 | 4.01 | 0.0039 | 5,865,472 | 9ddb37cae95e |
| Dseq_s2 | 250 | 34.8 | 4.01 | 0.0037 | 5,865,472 | 61a179b00eed |
Total LoRA training wall-clock: 10.05 h on one Apple M4. <!-- END:training -->
5. Evaluation protocol
All configurations run on identical task files with greedy decoding, batched (64 episodes in lock-step, max 256 new tokens per turn), on the same machine. Configuration A⁺ uses the same weights as A, plus two worked examples (with tools that are not available) appended to the system prompt. Metrics are computed per episode (§2.3 for success):
| metric | definition |
|---|---|
| success | executable validator passes (workflow completion) |
| tool_selection | set-level F1 between attempted tools and gold tools (fallbacks count); for no-tool tasks, 1 iff no call |
| arg_correct | among gold calls whose tool was attempted, fraction with an attempt whose arguments match |
| dependency_safe | no call used an identifier (ID, email) not yet observed, and no dependent call was issued before its prerequisite returned |
| strict_schema | every turn was exactly one schema-conforming JSON object |
| hallucinated_tools / unnecessary_calls / invalid_args / parse_errors | counts per episode |
| turns, latency, tokens/s, peak memory | efficiency |
Uncertainty. Each trained configuration's CI comes from a hierarchical bootstrap: resample training seeds, then tasks (4,000 resamples, 95% percentile intervals). Paired comparisons use per-task outcomes averaged over seeds, with a bootstrap CI on the difference and a two-sided sign-flip permutation p-value. Single-run configurations (A, A⁺, teacher, ablations) carry task-level bootstrap CIs only. Those CIs do not include training-seed variance, so they understate uncertainty.
6. Results
<!-- BEGIN:tables -->
Values: mean [95% CI]. Trained configs pool seeds via a hierarchical bootstrap (resample seeds, then tasks).
Workflow success (%) by split
| config | seeds | eval_id | eval_ood | eval_unseen | eval_renamed | all |
|---|---|---|---|---|---|---|
| A_base | 1 | 5.0 [2.0, 8.0] | 0.0 [0.0, 0.0] | 0.0 [0.0, 0.0] | 3.8 [1.2, 6.9] | 2.2 [1.2, 3.3] |
| A_fewshot | 1 | 12.0 [8.0, 16.5] | 3.8 [1.7, 6.2] | 33.3 [25.0, 41.7] | 11.9 [6.9, 16.9] | 12.8 [10.4, 15.3] |
| B | 3 | 86.0 [81.0, 90.7] | 30.3 [23.9, 37.5] | 33.3 [17.5, 48.3] | 73.3 [66.5, 80.2] | 55.8 [51.5, 60.2] |
| C | 3 | 89.7 [85.3, 93.5] | 30.3 [23.9, 36.8] | 33.1 [24.2, 42.5] | 69.2 [61.3, 77.3] | 55.9 [51.6, 60.0] |
| Dmix | 3 | 91.0 [86.7, 94.7] | 35.6 [29.2, 42.8] | 36.4 [21.1, 53.3] | 71.7 [64.4, 78.8] | 59.1 [53.3, 64.7] |
| Dseq | 3 | 90.3 [86.2, 94.0] | 32.8 [27.4, 38.6] | 9.2 [3.3, 16.7] | 71.0 [64.4, 77.7] | 53.3 [49.8, 57.0] |
| C_matched | 1 | 81.0 [75.5, 86.5] | 29.2 [23.3, 35.0] | 43.3 [34.2, 52.5] | 65.6 [58.1, 72.5] | 54.0 [50.4, 57.6] |
| C_25 | 1 | 87.0 [82.0, 91.5] | 33.3 [27.5, 39.6] | 28.3 [20.8, 36.7] | 67.5 [60.6, 74.4] | 55.0 [51.4, 58.8] |
| C_50 | 1 | 92.5 [88.5, 96.0] | 32.5 [26.7, 38.8] | 49.2 [40.0, 58.3] | 80.0 [73.8, 86.2] | 62.5 [58.9, 66.0] |
| C_full | 1 | 90.5 [86.0, 94.5] | 30.4 [24.6, 36.2] | 17.5 [10.8, 24.2] | 63.1 [55.6, 70.6] | 52.2 [48.6, 55.8] |
| teacher | 1 | 81.0 [75.5, 86.5] | 56.2 [50.0, 62.5] | 75.0 [66.7, 82.5] | 80.6 [74.4, 86.9] | 71.7 [68.5, 74.9] |
| oracle | 1 | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] |
Diagnostics (all main splits pooled)
| config | tool_selection | arg_correct | dependency_safe | strict_schema | hallucinated_tools | unnecessary_calls | invalid_args | parse_errors | turns |
|---|---|---|---|---|---|---|---|---|---|
| A_base | 24.3 [21.3, 27.3] | 59.5 [51.4, 67.2] | 94.3 [92.5, 95.8] | 20.1 [17.2, 23.2] | 0.0 [0.0, 0.1] | 0.5 [0.3, 0.6] | 0.1 [0.1, 0.2] | 1.9 [1.7, 2.0] | 2.7 [2.6, 2.9] |
| A_fewshot | 55.2 [52.0, 58.4] | 44.2 [40.1, 48.3] | 75.3 [72.1, 78.3] | 64.7 [61.4, 68.3] | 0.3 [0.2, 0.4] | 1.2 [1.0, 1.4] | 0.3 [0.2, 0.4] | 0.6 [0.5, 0.7] | 2.7 [2.6, 2.8] |
| B | 79.4 [76.8, 82.1] | 78.0 [73.7, 82.5] | 90.8 [87.5, 93.7] | 99.3 [98.7, 99.7] | 0.1 [0.1, 0.2] | 1.1 [0.9, 1.3] | 0.2 [0.1, 0.2] | 0.0 [0.0, 0.0] | 2.9 [2.7, 3.2] |
| C | 80.9 [78.3, 83.4] | 79.6 [76.6, 82.5] | 91.9 [89.6, 94.1] | 98.6 [97.1, 100.0] | 0.1 [0.1, 0.1] | 0.4 [0.4, 0.6] | 0.0 [0.0, 0.1] | 0.0 [0.0, 0.0] | 2.4 [2.3, 2.5] |
| Dmix | 81.4 [78.5, 84.3] | 80.9 [75.4, 85.4] | 92.1 [89.4, 94.5] | 99.1 [98.0, 99.8] | 0.1 [0.0, 0.1] | 0.7 [0.6, 1.0] | 0.1 [0.1, 0.1] | 0.0 [0.0, 0.0] | 2.7 [2.5, 3.0] |
| Dseq | 82.2 [78.9, 85.6] | 76.9 [72.6, 81.0] | 94.3 [92.5, 96.1] | 99.1 [98.0, 100.0] | 0.1 [0.0, 0.1] | 0.8 [0.7, 1.0] | 0.1 [0.0, 0.2] | 0.0 [0.0, 0.0] | 2.8 [2.7, 2.9] |
| C_matched | 80.4 [78.1, 82.8] | 77.1 [73.9, 80.1] | 89.0 [86.7, 91.2] | 97.8 [96.7, 98.8] | 0.1 [0.0, 0.1] | 0.5 [0.5, 0.6] | 0.1 [0.1, 0.2] | 0.0 [0.0, 0.0] | 2.5 [2.4, 2.5] |
| C_25 | 80.2 [77.7, 82.6] | 80.1 [77.0, 83.1] | 92.6 [90.7, 94.4] | 97.8 [96.7, 98.8] | 0.0 [0.0, 0.0] | 0.5 [0.4, 0.5] | 0.1 [0.0, 0.1] | 0.0 [0.0, 0.0] | 2.4 [2.4, 2.5] |
| C_50 | 83.5 [81.2, 85.7] | 84.2 [81.5, 86.9] | 91.1 [89.0, 93.1] | 96.2 [94.7, 97.5] | 0.0 [0.0, 0.1] | 0.5 [0.4, 0.5] | 0.1 [0.0, 0.1] | 0.0 [0.0, 0.1] | 2.6 [2.5, 2.6] |
| C_full | 78.0 [75.4, 80.5] | 75.3 [72.1, 78.5] | 87.9 [85.4, 90.3] | 97.8 [96.7, 98.8] | 0.1 [0.1, 0.1] | 0.6 [0.5, 0.6] | 0.0 [0.0, 0.1] | 0.0 [0.0, 0.0] | 2.5 [2.4, 2.5] |
| teacher | 85.2 [83.0, 87.4] | 86.3 [83.6, 88.9] | 86.8 [84.3, 89.3] | 81.9 [79.2, 84.7] | 0.0 [0.0, 0.0] | 0.3 [0.3, 0.4] | 0.1 [0.1, 0.1] | 0.2 [0.2, 0.3] | 2.5 [2.4, 2.6] |
| oracle | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 0.0 [0.0, 0.0] | 0.0 [0.0, 0.0] | 0.0 [0.0, 0.0] | 0.0 [0.0, 0.0] | 2.5 [2.4, 2.5] |
Paired differences in success (percentage points; seed-averaged per task)
p_holm: Holm-Bonferroni adjusted over all 84 tests in this table. The smallest attainable permutation p is ~0.0002 (5,000 sign-flips).
| comparison | split | diff [95% CI] | p (sign-flip) | p_holm | n |
|---|---|---|---|---|---|
| A_fewshot_vs_A_base | eval_id | +7.0 [+1.5, +12.5] | 0.0202 | 0.4251 | 200 |
| A_fewshot_vs_A_base | eval_ood | +3.8 [+1.7, +6.2] | 0.0047 | 0.1282 | 240 |
| A_fewshot_vs_A_base | eval_unseen | +33.3 [+25.0, +41.7] | 0.0002 | 0.0210 | 120 |
| A_fewshot_vs_A_base | eval_renamed | +8.1 [+2.5, +13.8] | 0.0157 | 0.3659 | 160 |
| B_vs_A_base | eval_id | +81.0 [+75.7, +86.0] | 0.0002 | 0.0210 | 200 |
| B_vs_A_base | eval_ood | +30.3 [+25.7, +35.3] | 0.0002 | 0.0210 | 240 |
| B_vs_A_base | eval_unseen | +33.3 [+26.4, +40.3] | 0.0002 | 0.0210 | 120 |
| B_vs_A_base | eval_renamed | +69.6 [+63.1, +76.0] | 0.0002 | 0.0210 | 160 |
| C_vs_A_base | eval_id | +84.7 [+79.7, +89.2] | 0.0002 | 0.0210 | 200 |
| C_vs_A_base | eval_ood | +30.3 [+25.1, +35.4] | 0.0002 | 0.0210 | 240 |
| C_vs_A_base | eval_unseen | +33.1 [+26.7, +40.0] | 0.0002 | 0.0210 | 120 |
| C_vs_A_base | eval_renamed | +65.4 [+59.4, +71.7] | 0.0002 | 0.0210 | 160 |
| Dmix_vs_A_base | eval_id | +86.0 [+81.2, +90.3] | 0.0002 | 0.0210 | 200 |
| Dmix_vs_A_base | eval_ood | +35.6 [+30.1, +40.7] | 0.0002 | 0.0210 | 240 |
| Dmix_vs_A_base | eval_unseen | +36.4 [+29.7, +43.3] | 0.0002 | 0.0210 | 120 |
| Dmix_vs_A_base | eval_renamed | +67.9 [+61.3, +74.2] | 0.0002 | 0.0210 | 160 |
| Dseq_vs_A_base | eval_id | +85.3 [+80.8, +89.5] | 0.0002 | 0.0210 | 200 |
| Dseq_vs_A_base | eval_ood | +32.8 [+27.9, +37.8] | 0.0002 | 0.0210 | 240 |
| Dseq_vs_A_base | eval_unseen | +9.2 [+5.3, +13.3] | 0.0002 | 0.0210 | 120 |
| Dseq_vs_A_base | eval_renamed | +67.3 [+60.8, +73.8] | 0.0002 | 0.0210 | 160 |
| C_matched_vs_A_base | eval_id | +76.0 [+70.0, +81.5] | 0.0002 | 0.0210 | 200 |
| C_matched_vs_A_base | eval_ood | +29.2 [+23.3, +35.0] | 0.0002 | 0.0210 | 240 |
| C_matched_vs_A_base | eval_unseen | +43.3 [+34.2, +52.5] | 0.0002 | 0.0210 | 120 |
| C_matched_vs_A_base | eval_renamed | +61.9 [+54.4, +69.4] | 0.0002 | 0.0210 | 160 |
| C_25_vs_A_base | eval_id | +82.0 [+76.5, +87.0] | 0.0002 | 0.0210 | 200 |
| C_25_vs_A_base | eval_ood | +33.3 [+27.5, +39.6] | 0.0002 | 0.0210 | 240 |
| C_25_vs_A_base | eval_unseen | +28.3 [+20.8, +36.7] | 0.0002 | 0.0210 | 120 |
| C_25_vs_A_base | eval_renamed | +63.7 [+56.2, +71.2] | 0.0002 | 0.0210 | 160 |
| C_50_vs_A_base | eval_id | +87.5 [+82.5, +92.0] | 0.0002 | 0.0210 | 200 |
| C_50_vs_A_base | eval_ood | +32.5 [+26.7, +38.8] | 0.0002 | 0.0210 | 240 |
| C_50_vs_A_base | eval_unseen | +49.2 [+40.0, +58.3] | 0.0002 | 0.0210 | 120 |
| C_50_vs_A_base | eval_renamed | +76.2 [+69.4, +83.1] | 0.0002 | 0.0210 | 160 |
| C_full_vs_A_base | eval_id | +85.5 [+80.5, +90.0] | 0.0002 | 0.0210 | 200 |
| C_full_vs_A_base | eval_ood | +30.4 [+24.6, +36.2] | 0.0002 | 0.0210 | 240 |
| C_full_vs_A_base | eval_unseen | +17.5 [+10.8, +24.2] | 0.0002 | 0.0210 | 120 |
| C_full_vs_A_base | eval_renamed | +59.4 [+51.9, +67.5] | 0.0002 | 0.0210 | 160 |
| teacher_vs_A_base | eval_id | +76.0 [+70.0, +82.0] | 0.0002 | 0.0210 | 200 |
| teacher_vs_A_base | eval_ood | +56.2 [+50.0, +62.5] | 0.0002 | 0.0210 | 240 |
| teacher_vs_A_base | eval_unseen | +75.0 [+66.7, +82.5] | 0.0002 | 0.0210 | 120 |
| teacher_vs_A_base | eval_renamed | +76.9 [+70.0, +83.1] | 0.0002 | 0.0210 | 160 |
| oracle_vs_A_base | eval_id | +95.0 [+92.0, +98.0] | 0.0002 | 0.0210 | 200 |
| oracle_vs_A_base | eval_ood | +100.0 [+100.0, +100.0] | 0.0002 | 0.0210 | 240 |
| oracle_vs_A_base | eval_unseen | +100.0 [+100.0, +100.0] | 0.0002 | 0.0210 | 120 |
| oracle_vs_A_base | eval_renamed | +96.2 [+93.1, +98.8] | 0.0002 | 0.0210 | 160 |
| B_vs_A_fewshot | eval_id | +74.0 [+68.0, +79.7] | 0.0002 | 0.0210 | 200 |
| B_vs_A_fewshot | eval_ood | +26.5 [+21.9, +31.2] | 0.0002 | 0.0210 | 240 |
| B_vs_A_fewshot | eval_unseen | -0.0 [-8.6, +8.9] | 1.0000 | 1.0000 | 120 |
| B_vs_A_fewshot | eval_renamed | +61.5 [+54.6, +68.5] | 0.0002 | 0.0210 | 160 |
| C_vs_A_fewshot | eval_id | +77.7 [+72.0, +83.0] | 0.0002 | 0.0210 | 200 |
| C_vs_A_fewshot | eval_ood | +26.5 [+21.4, +31.7] | 0.0002 | 0.0210 | 240 |
| C_vs_A_fewshot | eval_unseen | -0.3 [-8.6, +8.3] | 1.0000 | 1.0000 | 120 |
| C_vs_A_fewshot | eval_renamed | +57.3 [+50.4, +64.4] | 0.0002 | 0.0210 | 160 |
| Dmix_vs_A_fewshot | eval_id | +79.0 [+73.3, +84.3] | 0.0002 | 0.0210 | 200 |
| Dmix_vs_A_fewshot | eval_ood | +31.8 [+26.7, +37.1] | 0.0002 | 0.0210 | 240 |
| Dmix_vs_A_fewshot | eval_unseen | +3.1 [-5.3, +11.7] | 0.5141 | 1.0000 | 120 |
| Dmix_vs_A_fewshot | eval_renamed | +59.8 [+52.7, +66.9] | 0.0002 | 0.0210 | 160 |
| Dseq_vs_A_fewshot | eval_id | +78.3 [+72.8, +83.3] | 0.0002 | 0.0210 | 200 |
| Dseq_vs_A_fewshot | eval_ood | +29.0 [+24.0, +34.0] | 0.0002 | 0.0210 | 240 |
| Dseq_vs_A_fewshot | eval_unseen | -24.2 [-32.8, -15.6] | 0.0002 | 0.0210 | 120 |
| Dseq_vs_A_fewshot | eval_renamed | +59.2 [+52.1, +66.2] | 0.0002 | 0.0210 | 160 |
| C_vs_B | eval_id | +3.7 [-1.5, +8.8] | 0.1837 | 1.0000 | 200 |
| C_vs_B | eval_ood | +0.0 [-5.3, +5.1] | 1.0000 | 1.0000 | 240 |
| C_vs_B | eval_unseen | -0.3 [-5.6, +5.0] | 1.0000 | 1.0000 | 120 |
| C_vs_B | eval_renamed | -4.2 [-9.2, +0.8] | 0.1080 | 1.0000 | 160 |
| Dmix_vs_B | eval_id | +5.0 [+0.2, +10.0] | 0.0512 | 0.9735 | 200 |
| Dmix_vs_B | eval_ood | +5.3 [+1.1, +9.7] | 0.0152 | 0.3659 | 240 |
| Dmix_vs_B | eval_unseen | +3.1 [-2.2, +8.3] | 0.3099 | 1.0000 | 120 |
| Dmix_vs_B | eval_renamed | -1.7 [-6.5, +2.7] | 0.5201 | 1.0000 | 160 |
| Dmix_vs_C | eval_id | +1.3 [-0.5, +3.3] | 0.2414 | 1.0000 | 200 |
| Dmix_vs_C | eval_ood | +5.3 [+1.1, +9.4] | 0.0165 | 0.3659 | 240 |
| Dmix_vs_C | eval_unseen | +3.3 [-1.7, +8.6] | 0.2542 | 1.0000 | 120 |
| Dmix_vs_C | eval_renamed | +2.5 [-1.2, +6.3] | 0.2404 | 1.0000 | 160 |
| Dseq_vs_Dmix | eval_id | -0.7 [-3.3, +2.0] | 0.7018 | 1.0000 | 200 |
| Dseq_vs_Dmix | eval_ood | -2.8 [-5.8, +0.1] | 0.0842 | 1.0000 | 240 |
| Dseq_vs_Dmix | eval_unseen | -27.2 [-33.9, -20.3] | 0.0002 | 0.0210 | 120 |
| Dseq_vs_Dmix | eval_renamed | -0.6 [-3.7, +2.5] | 0.7961 | 1.0000 | 160 |
| C_matched_vs_C | eval_id | -8.7 [-13.2, -4.2] | 0.0005 | 0.0210 | 200 |
| C_matched_vs_C | eval_ood | -1.1 [-5.4, +3.1] | 0.6478 | 1.0000 | 240 |
| C_matched_vs_C | eval_unseen | +10.3 [+3.1, +17.5] | 0.0087 | 0.2274 | 120 |
| C_matched_vs_C | eval_renamed | -3.5 [-8.5, +1.5] | 0.2017 | 1.0000 | 160 |
| C_matched_vs_B | eval_id | -5.0 [-9.7, -0.5] | 0.0365 | 0.7298 | 200 |
| C_matched_vs_B | eval_ood | -1.1 [-6.7, +4.7] | 0.7418 | 1.0000 | 240 |
| C_matched_vs_B | eval_unseen | +10.0 [+2.2, +17.8] | 0.0140 | 0.3499 | 120 |
| C_matched_vs_B | eval_renamed | -7.7 [-12.3, -3.5] | 0.0005 | 0.0210 | 160 |
Post-hoc: eval_unseen excluding the flawed un_timezone template (success %, n=90 tasks)
Added after results were seen; reported alongside, never instead of, the pre-registered eval_unseen.
| config | success |
|---|---|
| A_base | 0.0 [0.0, 0.0] |
| A_fewshot | 44.4 [34.4, 54.4] |
| B | 44.4 [23.3, 63.3] |
| C | 43.7 [32.2, 54.8] |
| Dmix | 48.5 [27.8, 70.0] |
| Dseq | 11.9 [3.7, 22.2] |
| C_matched | 57.8 [47.8, 67.8] |
| C_25 | 37.8 [27.8, 47.8] |
| C_50 | 65.6 [55.6, 74.4] |
| C_full | 23.3 [14.4, 32.2] |
| teacher | 100.0 [100.0, 100.0] |
| oracle | 100.0 [100.0, 100.0] |
Catalogue-size ablation (success %, eval_catalog_k: k distractor tools)
| config | k=0 | k=5 | k=12 |
|---|---|---|---|
| A_base | 3.1 [0.0, 7.3] | 4.2 [1.0, 8.3] | 5.2 [1.0, 10.4] |
| A_fewshot | 16.7 [9.4, 24.0] | 12.5 [6.2, 19.8] | 17.7 [10.4, 26.0] |
| B | 85.4 [78.1, 91.7] | 82.3 [75.0, 89.6] | 80.2 [71.9, 87.5] |
| C | 84.4 [77.1, 91.7] | 82.3 [74.0, 89.6] | 84.4 [77.1, 91.7] |
| Dmix | 91.7 [85.4, 96.9] | 90.6 [84.4, 95.8] | 89.6 [83.3, 94.8] |
| Dseq | 91.7 [85.4, 96.9] | 90.6 [84.4, 95.8] | 88.5 [82.3, 94.8] |
| oracle | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] | 100.0 [100.0, 100.0] |
Success (%) by family (pooled main splits, seed-averaged)
| config | approval | args | chain | parallel | permission | recovery | select | termination |
|---|---|---|---|---|---|---|---|---|
| A_base | 0.0 (n=38) | 0.0 (n=54) | 0.0 (n=189) | 0.0 (n=106) | 0.0 (n=30) | 0.0 (n=105) | 0.6 (n=160) | 39.5 (n=38) |
| A_fewshot | 0.0 (n=38) | 13.0 (n=54) | 12.7 (n=189) | 8.5 (n=106) | 3.3 (n=30) | 3.8 (n=105) | 29.4 (n=160) | 0.0 (n=38) |
| B | 0.9 (n=38) | 54.9 (n=54) | 40.0 (n=189) | 64.2 (n=106) | 20.0 (n=30) | 66.7 (n=105) | 80.8 (n=160) | 60.5 (n=38) |
| C | 43.9 (n=38) | 56.8 (n=54) | 35.3 (n=189) | 46.5 (n=106) | 0.0 (n=30) | 87.0 (n=105) | 77.9 (n=160) | 60.5 (n=38) |
| Dmix | 43.9 (n=38) | 60.5 (n=54) | 42.5 (n=189) | 46.5 (n=106) | 23.3 (n=30) | 85.1 (n=105) | 79.6 (n=160) | 60.5 (n=38) |
| Dseq | 47.4 (n=38) | 54.3 (n=54) | 34.6 (n=189) | 40.9 (n=106) | 21.1 (n=30) | 80.6 (n=105) | 71.0 (n=160) | 61.4 (n=38) |
| C_matched | 13.2 (n=38) | 57.4 (n=54) | 24.9 (n=189) | 61.3 (n=106) | 3.3 (n=30) | 81.9 (n=105) | 81.9 (n=160) | 60.5 (n=38) |
| C_25 | 47.4 (n=38) | 55.6 (n=54) | 33.3 (n=189) | 50.9 (n=106) | 10.0 (n=30) | 81.0 (n=105) | 75.0 (n=160) | 60.5 (n=38) |
| C_50 | 50.0 (n=38) | 68.5 (n=54) | 40.2 (n=189) | 64.2 (n=106) | 40.0 (n=30) | 79.0 (n=105) | 82.5 (n=160) | 60.5 (n=38) |
| C_full | 34.2 (n=38) | 59.3 (n=54) | 24.3 (n=189) | 51.9 (n=106) | 0.0 (n=30) | 82.9 (n=105) | 75.0 (n=160) | 60.5 (n=38) |
| teacher | 36.8 (n=38) | 74.1 (n=54) | 53.4 (n=189) | 88.7 (n=106) | 43.3 (n=30) | 68.6 (n=105) | 95.0 (n=160) | 78.9 (n=38) |
| oracle | 100.0 (n=38) | 100.0 (n=54) | 100.0 (n=189) | 100.0 (n=106) | 100.0 (n=30) | 100.0 (n=105) | 100.0 (n=160) | 100.0 (n=38) |
Success (%) by gold workflow depth (pooled main splits, seed-averaged)
| config | 0 | 1 | 2 | 4 | 5 |
|---|---|---|---|---|---|
| A_base | 39.5 (n=38) | 0.2 (n=410) | 0.0 (n=242) | 0.0 (n=15) | 0.0 (n=15) |
| A_fewshot | 0.0 (n=38) | 16.3 (n=410) | 10.3 (n=242) | 0.0 (n=15) | 0.0 (n=15) |
| B | 60.5 (n=38) | 69.0 (n=410) | 34.3 (n=242) | 82.2 (n=15) | 4.4 (n=15) |
| C | 60.5 (n=38) | 67.9 (n=410) | 40.9 (n=242) | 13.3 (n=15) | 0.0 (n=15) |
| Dmix | 60.5 (n=38) | 67.9 (n=410) | 47.0 (n=242) | 71.1 (n=15) | 0.0 (n=15) |
| Dseq | 61.4 (n=38) | 61.7 (n=410) | 40.4 (n=242) | 66.7 (n=15) | 0.0 (n=15) |
| C_matched | 60.5 (n=38) | 70.7 (n=410) | 31.4 (n=242) | 0.0 (n=15) | 0.0 (n=15) |
| C_25 | 60.5 (n=38) | 64.4 (n=410) | 43.8 (n=242) | 20.0 (n=15) | 0.0 (n=15) |
| C_50 | 60.5 (n=38) | 75.1 (n=410) | 48.3 (n=242) | 13.3 (n=15) | 0.0 (n=15) |
| C_full | 60.5 (n=38) | 66.8 (n=410) | 32.6 (n=242) | 0.0 (n=15) | 0.0 (n=15) |
| teacher | 78.9 (n=38) | 82.9 (n=410) | 56.6 (n=242) | 33.3 (n=15) | 26.7 (n=15) |
| oracle | 100.0 (n=38) | 100.0 (n=410) | 100.0 (n=242) | 100.0 (n=15) | 100.0 (n=15) |
Success (%) by graph seen in train (pooled main splits, seed-averaged)
| config | False | True |
|---|---|---|
| A_base | 0.0 (n=300) | 3.8 (n=420) |
| A_fewshot | 14.0 (n=300) | 11.9 (n=420) |
| B | 33.4 (n=300) | 71.8 (n=420) |
| C | 32.4 (n=300) | 72.6 (n=420) |
| Dmix | 38.2 (n=300) | 74.0 (n=420) |
| Dseq | 25.6 (n=300) | 73.2 (n=420) |
| C_matched | 36.0 (n=300) | 66.9 (n=420) |
| C_25 | 33.3 (n=300) | 70.5 (n=420) |
| C_50 | 41.0 (n=300) | 77.9 (n=420) |
| C_full | 26.7 (n=300) | 70.5 (n=420) |
| teacher | 66.3 (n=300) | 75.5 (n=420) |
| oracle | 100.0 (n=300) | 100.0 (n=420) |
Success (%) by fault-injected / not-found (pooled main splits, seed-averaged)
| config | False | True |
|---|---|---|
| A_base | 2.6 (n=615) | 0.0 (n=105) |
| A_fewshot | 14.3 (n=615) | 3.8 (n=105) |
| B | 54.0 (n=615) | 66.7 (n=105) |
| C | 50.6 (n=615) | 87.0 (n=105) |
| Dmix | 54.7 (n=615) | 85.1 (n=105) |
| Dseq | 48.7 (n=615) | 80.6 (n=105) |
| C_matched | 49.3 (n=615) | 81.9 (n=105) |
| C_25 | 50.6 (n=615) | 81.0 (n=105) |
| C_50 | 59.7 (n=615) | 79.0 (n=105) |
| C_full | 47.0 (n=615) | 82.9 (n=105) |
| teacher | 72.2 (n=615) | 68.6 (n=105) |
| oracle | 100.0 (n=615) | 100.0 (n=105) |
Efficiency (per evaluation run)
| run | peak mem GB | wall s | episodes | mean turns | gen tok/s | mean latency/episode s |
|---|---|---|---|---|---|---|
| A_base | 2.98 | 857 | 1008 | 2.68 | 126 | 0.85 |
| A_fewshot | 3.90 | 1137 | 1008 | 2.71 | 87 | 1.12 |
| B_s0 | 3.49 | 1023 | 1008 | 2.61 | 93 | 1.01 |
| B_s1 | 3.83 | 883 | 720 | 3.14 | 88 | 1.23 |
| B_s2 | 3.43 | 811 | 720 | 2.93 | 94 | 1.13 |
| C_25_s0 | 3.03 | 624 | 720 | 2.43 | 90 | 0.87 |
| C_50_s0 | 3.10 | 620 | 720 | 2.56 | 97 | 0.86 |
| C_full_s0 | 3.01 | 575 | 720 | 2.45 | 100 | 0.80 |
| C_matched_s0 | 3.24 | 648 | 720 | 2.47 | 91 | 0.90 |
| C_s0 | 3.16 | 870 | 1008 | 2.39 | 91 | 0.86 |
| C_s1 | 3.03 | 597 | 720 | 2.36 | 91 | 0.83 |
| C_s2 | 3.33 | 658 | 720 | 2.51 | 89 | 0.91 |
| Dmix_s0 | 3.10 | 949 | 1008 | 2.53 | 92 | 0.94 |
| Dmix_s1 | 3.12 | 685 | 720 | 2.57 | 90 | 0.95 |
| Dmix_s2 | 3.49 | 811 | 720 | 3.00 | 88 | 1.13 |
| Dseq_s0 | 3.14 | 975 | 1008 | 2.64 | 87 | 0.97 |
| Dseq_s1 | 3.12 | 667 | 720 | 2.75 | 94 | 0.93 |
| Dseq_s2 | 3.32 | 749 | 720 | 2.82 | 87 | 1.04 |
| oracle | 0.00 | 0 | 1008 | 2.42 | - | 0.00 |
| teacher | 13.17 | 5801 | 720 | 2.49 | 12 | 8.05 |
<!-- END:tables -->
6.1 Findings by hypothesis
Paired p-values below are Holm-adjusted over all 84 tests in the table above. Paired CIs average each task over seeds and so exclude training-seed variance; the hierarchical CIs in the first table are the conservative ones.
- H1 (training helps): supported. Every trained configuration beats A on every split (e.g. B on eval_id +81.0 pts [+75.7, +86.0]). Pooled success: A 2.2%, B 55.8%, C 55.9%, Dmix 59.1%, Dseq 53.3%.
- H2 (beyond format): supported, except on unseen tools. Against the few-shot prompt (A⁺), B gains +74.0 pts on eval_id, +26.5 on eval_ood and +61.5 on eval_renamed; C, Dmix and Dseq are similar. Diagnostics that format cannot fix also improve: argument correctness 44% (A⁺) → 77–81%, dependency safety 75% → 91–94%, tool-selection F1 55% → 79–82%. On eval_unseen B, C and Dmix are level with A⁺ (differences −0.3 to +3.1 pts, all n.s.), and Dseq is worse (−24.2).
- H3 (generalisation): partly supported. Renaming every tool and parameter costs only ~13–21 pts (eval_id → eval_renamed), so models read the catalogue rather than memorise names. But unseen templates drop to 30–36%, and success on novel dependency graphs (26–38%) is about half that on graphs seen in training (72–74%). Depth-5 chains are almost never solved (≤4.4%, n=15). Most of the gain is pattern-specific.
- H4 (signal source): no aggregate difference; different profiles. C − B is between −4.2 and +3.7 pts on every split (all n.s.). By family (descriptive, not tested): B is near zero on approval (0.9% vs C 43.9%), which matches the single accepted approval episode in the teacher data; C is stronger on recovery (87.0% vs 66.7%) and makes fewer unnecessary calls (0.4 vs 1.1 per episode); B is stronger on parallel calls (64.2% vs 46.5%) and permission (20.0% vs 0.0%, n=30). C_matched (oracle labels on exactly B's tasks) loses 8.7 pts to C on eval_id (p_holm 0.021) and 7.7 to B on eval_renamed (p_holm 0.021); on one seed this suggests that which tasks are covered matters at least as much as who labelled them.
- H5 (combination): partly refuted. Dmix is numerically best pooled (59.1%) and beats B on eval_id (+5.0) and both B and C on eval_ood (+5.3 each), but none of these survives correction (p_holm ≥ 0.37). Strategy matters: sequential B→C (Dseq) matches Dmix in-distribution but falls on eval_unseen by −27.2 pts [−33.9, −20.3] (p_holm 0.021), consistently across all three seeds (8.3%, 4.2% and 15.0% per seed vs 22–53% for Dmix). On the inventory templates Dseq often mangles identifiers (e.g. calling
reserve_stockwith"535"forSKU-535); the cause is not established.
6.2 Ablations (single seed; seed-to-seed spread of C pooled success is 53.6–58.6%)
- Data quantity (oracle data, fixed 250 × 8 episode budget): C_25 55.0%, C (1,000) 55.9%, C_50 62.5%, C_full 52.2%. No monotonic trend; C_50 is the best single run, mainly on eval_unseen and eval_renamed. With one seed per point, only C_50's lead is outside C's own seed spread, and only modestly. C_full sees each of its 3,000 episodes 0.67 times, so it is not "more training".
- Catalogue size (0, 5, 12 distractor tools; seed 0): trained models lose at most 5.2 pts from 0 to 12 distractors, with overlapping CIs; distractors are not a bottleneck at this scale.
- Unseen tools / combination strategy: see H2 and H5.
7. Limitations
- Synthetic, simulated environment. 21 deterministic tools and 44 templated scenarios. Requests are template-generated English; real APIs, real users and open-ended requests are not tested.
- Weak, local teacher. No hosted API was available, so the teacher is a 4-bit 4B model that solves 71.7% of held-out tasks. B is bounded by it: only 63.2% of offered tasks yielded an accepted episode, and approval tasks yielded one. B-vs-C differences are therefore confounded with task coverage (C_matched isolates this on one seed only).
- Flawed template.
un_timezone(30 of 120 eval_unseen tasks) is answerable from world knowledge, and its gold graph requires ageocodecall the system prompt never demands; every model, including the teacher, scores 0%. It was found after results were seen. The pre-registered split is reported unchanged; a clearly labelled post-hoc table excludes the template. - Uneven coverage of behaviours. eval_id and eval_renamed contain no permission tasks (all such requests overlapped the training pool verbatim and were removed), so permission handling is measured only on eval_ood (n=30). Approval (n=38), depth-4 and depth-5 strata (n=15 each) are small.
- Seeds. Main configurations have 3 seeds each; ablations 1. Seed-to-seed spread is large (e.g. Dmix eval_unseen 22–53%), so single-seed ablation differences of a few points are not evidence.
- Hyperparameters were not swept per configuration. The final setting was chosen after two diverged configurations, using pilots on Dmix and B (§4.1); all configurations share it. Other configurations might prefer different settings. Selection used validation loss on oracle data.
- Multiple comparisons. 84 paired tests are reported; Holm-adjusted p-values are given. The smallest attainable permutation p (~0.0002) limits how small adjusted values can be.
- Decoding and measurement. Greedy decoding, batched (64 episodes; teacher 24 because of memory), which is not bit-for-bit deterministic. Latency and throughput are measured at those different batch sizes and are not a like-for-like speed comparison.
- Run history. Teacher pass 1 was interrupted once (Metal out-of-memory while other GPU jobs ran) and pass 2 once (a simulator crash on malformed teacher arguments, fixed by validating array items; outcomes of earlier episodes were unaffected). The prefill batch changed from 8 to 16 for the last 75 pass-2 episodes. The Mac slept during Dseq_s1 training; the trainer's clock excludes the sleep, the queue's wall-clock does not. All student evaluations ran after the simulator fix.
- Provenance. The teacher's 4-bit weights were not revision-pinned at download time; the recorded revision is the Hub HEAD at the time (see MODEL_SELECTION.md). The few-shot baseline uses one prompt design; a better prompt could narrow the gap.
8. Compute and cost
<!-- BEGIN:queue --> Successful queue steps: 33 (training 11.18 h, evaluation 4.99 h wall-clock). <!-- END:queue -->
All work ran on one Apple M4 laptop with 16 GB of unified memory; no cloud compute and no API spend.
| stage | wall-clock |
|---|---|
| Teacher generation (pass 1 + pass 2, 2,261 episodes) | 5.6 h |
| Final training (16 runs × 250 steps, trainer clock) | 10.1 h |
| Final evaluation (queue) | 5.0 h |
| Failed/diagnostic training: v1 (11 runs) + v1 evaluations | 8.4 h + 3.4 h |
| LR pilot, v1b run, stability study | ≈ 1.5 h |
Peak memory: ≈ 4.0 GB for training, 3.0–3.9 GB for student evaluation, 13.2 GB for teacher evaluation. Cost: electricity only.
9. Reproducibility
See the README for the full sequence of commands. Every stage is deterministic given its seed and resumable; the remaining non-determinism is in batched GPU arithmetic. Dataset hashes are in data/v1/manifest.json and data/v1/sft/variants.json, adapter hashes in §4, and the pinned environment in requirements.lock. Model weights and adapters are not committed (size); adapter hashes allow verification of re-trained or shared adapters.