test(tool-loader): pin #800 doc-profile data-vs-recall disambiguation#1844
Conversation
Review — ApproveVerdict: Approve. This is a clean test + docs closeout for #800 with no runtime code changes. It adds a deterministic regression test, a live eval scenario, and an in-repo note that together pin a fix (the dynamic tool loader) that until now had no test guarding it. The work is solid and the claims hold up. I re-ran the 4 new unit tests against the real loader/config and they pass; the central argument — that memory One thing worth a follow-up, non-blocking: the committed eval scenario currently records a FAIL (per the PR description) for a reason unrelated to #800 — a bug in how the data tool handles the monthly-summary CSV plus an agent hallucination on turn 1. Shipping a "live proof" scenario that's red can read as "the feature is broken" to anyone who runs the suite without reading this PR. The unit test is correctly the binding gate, so this is fine to merge — but please file a tracking issue for that separate data-tool bug so the red scenario has a paper trail and eventually goes green. 🔍 Technical detailsVerification performed
Issues🟡 Committed eval scenario is currently red for an unrelated reason ( 🟢 Thin top-salesperson margin ( Strengths
|
CI failures are pre-existing Windows/Lemonade infra flakiness — not caused by this PRThe two red checks are both Windows + real-Lemonade-Server integration steps that this PR's diff (one unit test, one eval scenario YAML, one docs note) neither touches nor runs. What actually failed
Why this PR triggered them (but didn't cause the failure)Both workflows trigger on Evidence it's flaky infra, not a code defect
Recommendation
|
Summary
Before this PR, #800's scratchpad/memory tool collision was resolved in code (by the #688 dynamic tool loader, landed via #1449/#1450/#1451) but nothing pinned it — no test asserted that the structured-data tool and memory
recallcan't crowd each other out of the prompt, so the fix could silently regress. This adds a deterministic regression test, a live eval scenario, and an in-repo note that together lock the resolution and let #800 close with evidence. No runtime code changes — this is a closeout + regression, not a feature.Why
#800 is a coordination tracker: it exists to prove the collision is fixed, not leave it as a known gap. The decisive finding (verified on
main) is that the literal pair in the title —scratchpad.query_datavsmemory.recall— cannot occur in the loaded profile: scratchpad tools are registered only for the ChatAgentdata/fullprofiles, neverdoc, which is the only profile the loader is wired to. The real doc-profile arbitration isanalyze_data_file(structured-data, conditional) vsrecall(CORE, always-on). That asymmetry is the resolution — and until now no test encoded it.Linked issue
Closes #800
Changes
recall ∈ CORE(always present) whileanalyze_data_fileloads only when the turn's query clears the semantic threshold, using the realDOC_CORE_TOOLS/DOC_BUNDLESconfig and production τ/cap (not hand-picked literals), with a fresh loader per case so it tests the cold/empty-memory new-user state.sales_data_2025.csvcorpus, so the disambiguation is checked against a real model, not just unit logic.Deviations from the approved sketch (#800's body)
Flagged per CLAUDE.md — the landed design diverges from the original sketch in several places:
mainscratchpad.query_datavsmemory.recalldocprofile; loader isdoc-onlyanalyze_data_file(conditional) vsrecall(CORE)recallis CORE → always present; the conditional side isanalyze_data_filerecallis intentionally always-onmaintool_historyTOOL_LOADER {json}INFO lines;gaia.eval.tool_recallconsumes themscratchpad/browserindocdocprofilesmart_discovery(hardware artifact); fixture is also independently staleTest plan
python -m pytest tests/unit/test_tool_loader_disambiguation.py -v→ 4 passed (the AC Update Documentation #5 gate)python -m pytest tests/unit/test_tool_loader_selection.py tests/unit/test_chat_tool_bundles.py tests/unit/test_chat_dynamic_tools.py -q→ 67 passed (no loader regression; confirms the new test's registry/embedder assumptions match shipped config)python -m pytest tests/test_eval.py -k scenarios -q→ 27 passed (validates the new scenario YAML: required fields, sequential turns, existing corpus path)python util/lint.py --all→ cleangaia eval agent --category tool_selection --agent-type doc→ the 4 pre-existing scenarios match the Tool loader — Part 3: skill-driven signal (gated on #887) #1451 Part-3 proof (no selection regression)Checklist
Closes #800).python util/lint.py --all,pytest tests/unit/).#800 Acceptance Criteria — Proof
Verdict: all 7 ACs satisfied on
main+ this PR. The collision is resolved structurally by the #688 dynamic tool loader (landed via #1449/#1450/#1451); this PR pins it. Three ACs are satisfied with the documented reframing above.query_data&recallnot both unless justifiedAC #1 — Tool-prompt token reduction
Reframed: the ~12K figure assumed the text path; the real cost (and gate) is the native tool-schema path / TTFT. The 38-tool doc profile is capped to 14 (
DEFAULT_MAX_TOOLS).test_core_only_is_the_reduction_best_casepins the always-on CORE floor at ≤45% of the native baseline (~50–60% reduction).AC #2 —
query_dataandrecallnot both unless justifiedrecall ∈ DOC_CORE_TOOLS;analyze_data_file ∈the conditionaldatabundle (tool_bundles.py). The conditional tool loads only when the query clears τ:AC #3 — Core tools always available despite heuristic failure
CORE is admitted unconditionally and is cap-/eviction-exempt; on embedder failure the loader disables for the session and falls back to the full registry, logging loudly (
tool_loader.py).AC #4 — Decisions logged for eval/tune
Satisfied with a deliberate deviation: decisions are emitted as structured
TOOL_LOADER {json}INFO lines (not the SQLtool_historytable — avoids a UI-DB migration). Consumed bysrc/gaia/eval/tool_recall.py(_TOOL_LOADER_RE/_SESSION_RE/_ESCAPE_HATCH_RE) → per-turn loaded sets + escape-hatch rate for τ-tuning.AC #5 — End-to-end regression test (data-query vs recall) — new in this PR
tests/unit/test_tool_loader_disambiguation.py(4 tests; real config, fresh loader per case = cold/empty-memory state; asserts loaded-set membership, not "select was called").eval/scenarios/tool_selection/data_vs_recall_disambiguation.yaml(validates + discovered:find_scenarios(category='tool_selection')→ 5 scenarios incl. the new one).AC #6 — Eval suite passes, no selection regression
Live serial run on Gemma-4-E4B corroborates the #1451 Part-3 success-criteria proof — no regression:
known_path_readno_tools_neededmulti_step_plansmart_discovery* hardware artifact of the local Apple-Silicon box (Metal llama.cpp ~14–19 tok/s), not a behavior change. The new scenario records FAIL 6.58 for reasons unrelated to #800: the disambiguation works (agent routed both aggregates to
analyze_data_file, never misusedrecall; Turn 2 returned the exact answer), but Turn 1's correctness failed onanalyze_data_file's date-filter handling of the monthly-summary CSV plus an agent hallucination — flagged as a separate follow-up. Committed baseline refresh deferred to a clean run on target hardware.AC #7 — Mid-conversation re-evaluation works
Proven for in-profile pivots (the sketch's "file→web" example is out of the doc profile — browser tools aren't registered there):