feat: MCP tool calling reliability test framework#718
Conversation
|
@claude review this PR |
|
Claude finished @itomek's task —— View job PR Review: MCP Tool Calling Reliability Test Framework
SummaryThis is a well-structured PR that adds a comprehensive MCP reliability evaluation framework along with a boot-time initialization system for the Agent UI. The code is clean, well-documented, and includes thorough test coverage. Review Feedback🟢 Strengths1. Clean Architecture - The
2. Comprehensive Test Coverage - 16 new tests covering:
3. Well-Designed Eval Scenarios - The 10 MCP reliability scenarios cover a good range:
4. Good UX - Frontend changes properly:
🟡 Suggestions (Non-blocking)1. Consider using
while dep.status not in (JobStatus.DONE, JobStatus.FAILED):
if time.monotonic() >= deadline:
...
await asyncio.sleep(_DEPENDENCY_POLL_INTERVAL)An 2. CLI table formatting edge case
print(f" {sid:<40} {rate_str:>12} {result_str}")Consider truncating long IDs or using dynamic width. 3. Test isolation with subprocess
🔴 Issues (Should Address)1. Unused import in test file
import ioThis import is not used in the test file. 2.
📋 Test Plan VerificationBased on code review, the test plan items should work:
Overall AssessmentApprove ✅ - This is high-quality code with good design patterns, comprehensive tests, and proper documentation. The minor issues noted above are non-blocking. The PR successfully adds:
Nice work! 🎉 |
b8213d6 to
5be18ad
Compare
c74d1a2 to
5f20088
Compare
8175a66 to
2257e86
Compare
|
Addressed the blockers + important items from the review (force-pushed to Fixed
Not addressed in this push (deferring for your call)
Verified locally:
Re-running CI now. |
|
Five concrete fixes for real eval-discovered regressions, each paired with a regression test — this is exactly how eval-driven development should work. The bulk of the diff is deletion of session logs that should never have shipped; the actual code changes are focused and well-reasoned. SummaryThe lie-on-loop fix ( The one thing to eyeball before merge: the MCP tool limit silently went from 10 → 100, which is a meaningful behaviour change for local-model users. Issues🟡 MCP tool-limit default raised 10 → 100 without changelog note (
|
|
Addressed the second-round review (force-pushed): CI-blocking lint fixes
Review nits addressed
Still deferred (worth a separate PR, called out in PR description gaps)
Verified locally:
|
2257e86 to
452b3e6
Compare
Establishes an automated harness for verifying GAIA's agent loop +
Lemonade + local model reliably select the correct MCP tool for a
given user request. Closes the gap where tool-selection regressions
could only be caught by manual testing, and surfaces several
framework-side bugs the new eval immediately exposed.
The eval
- ``gaia eval agent`` (src/gaia/eval/runner.py) spawns a subprocess
acting as user-simulator + judge against the Agent UI MCP backend.
- ``analyze_failures.py`` produces per-tool rollups and failure
taxonomies from the trace JSON written by each run.
- eval/prompts/judge_turn.md adds STEP 0 — for ``verbatim``-tagged
scenarios the rubric scopes to tool selection only (pipeline
correctness), not whether the tool's underlying operation succeeds
at the hardware/service layer (vendor concern, outside scope).
Test scenarios
- ``eval/scenarios/mcp_reliability/`` — 10 generic MCP tool-call
probes (single-param, multi-param, chained, conditional).
- ``eval/scenarios/mcp_tool_reliability/`` — ~80 richer probes
covering system-mode toggles, display-lens controls, dark mode,
mic/audio, USB charging, battery/memory/storage status, etc.
- ``tests/unit/eval/test_mcp_tool_reliability_scenarios.py``
validates each YAML's structure (turns/objective/success_criteria).
Agent-loop fixes the eval surfaced
- Sanitise MCP server names — strip redundant ``mcp`` tokens before
namespacing so doubled prefixes like ``mcp_X_mcp_<tool>`` cannot
occur. Small local models otherwise learn to truncate to the
doubled prefix and emit non-existent tool names.
- Truthful loop-break — when ``max_consecutive_repeats`` fires on a
sequence of identical *failed* calls, the framework no longer
hard-codes ``"Task completed with <name>"``; it surfaces the
underlying error instead.
- JSON-envelope suppression for tool-calling models — ``model_id``
must be set BEFORE ``_register_tools()`` so the system-prompt
cache built during MCP registration sees the correct
``is_tool_calling_model`` result. Without this, the embedded-JSON
template leaked into Gemma-4's prompt despite the existing
suppression check.
- Native-path loop-break — the helper must unwrap
``previous_outputs`` (each entry is ``{tool, args, result}``)
before consulting the last result. The legacy single-tool path
appends bare results to ``step_results``; the native path uses
a different list. Without the unwrap the truthful-summary
fallback path never fired.
Test coverage
113 unit tests across new and extended files:
- ``test_sanitize_server_name.py`` (NEW, 12 cases + idempotency + warn-once)
- ``test_loop_break_truthful.py`` (NEW, 7 cases incl. native-path regression)
- ``test_tool_not_found_error.py`` (NEW, 6 cases incl. no-echo invariant)
- ``test_agent_source_invariants.py`` (NEW, AST guard on duplicate "Task completed" literals)
- ``test_response_format.py`` (extended — verbatim rule + MCP mixin prompt)
- ``test_mcp_client.py`` (extended — sanitised prefix + register/unregister symmetry)
- ``test_mcp_client_mixin.py`` (extended — covers new prefix field)
- ``test_mcp_tool_reliability_scenarios.py`` (NEW, validates the 80+ YAML scenarios)
|
This PR ships four well-diagnosed framework fixes, 10 generic MCP reliability scenarios, and 113 new tests — the eval infrastructure is doing exactly what it was built to do. One shipped file needs cleanup before merge; the rest is ready. Issues Found🟡 Important — OEM-specific internals shipped in the public package (
|
Address bot review items on PR #718: - analyze_failures.py: drop OEM-specific defaults and naming. The hard- coded log dir and EDT tz offset move to CLI flags (--tool-log-tz- offset-hours, --tool-log-glob); file glob defaults to *.log; module docstring and class names are vendor-neutral (OEMLogEntry -> ToolLogEntry). Public-facing analyzer works for any MCP service emitting bracketed-timestamp logs. - cli.py: hoist json and defaultdict to module-level imports; drop the 6 inner re-imports flagged by W0404. Genericize --mcp-tool-limit help text to explain the small-model tradeoff instead of citing a vendor. - chat/agent.py: isort fix (_TOOL_REGISTRY ordering) unblocking CI. - test_sanitize_server_name.py: convert _logged_sanitisations manual clears into an autouse fixture so isolation is automatic, not per- test bookkeeping.
|
This PR ships solid, root-cause-driven fixes for five concrete agent-loop failures, backed by 113 unit tests and a clean deletion of stale AI-generated run logs. The single most important thing: the Issues🟡 Important —
|
CI block: - cli.py: drop the last `import json as _json` alias (W0404 reimport flagged by pylint without `-E`). The aliased loads call now uses the top-level json module. Bot review #3: - judge_turn.md: collapse "the the OEM MCP service's tools" duplicate and drop the OEM-specific percentage; the rule is vendor-agnostic. - cli.py `_print_reliability_summary`: guard ANSI escapes behind `sys.stdout.isatty()` so piped output (CI, logs, non-ANSI Windows shells) doesn't show literal `^[[32m` sequences. - blender/chat agent.py `_post_process_tool_result` docstrings: note the `super()` requirement for `single_tool_per_turn=True` subclasses. - test_agent_source_invariants.py: scope the "Task completed with" literal walk to `_build_loop_break_summary`'s body so unrelated mentions (test assertions, future docstrings) don't trip the gate.
|
Four real bugs fixed, all with targeted regression tests and clear root-cause documentation — this is exactly what the eval framework should surface. No blocking issues; two latent correctness concerns worth addressing before the next agent ships with SummaryP1–P4 fixes are correct and well-tested: the Main concern: both Issues Found🟡 Important —
|
Address PR #718 bot review: - BlenderAgent / ChatAgent _post_process_tool_result: previous push only documented the super() requirement; this push actually delegates through. Without this, any subclass setting single_tool_per_turn=True would loop indefinitely because _single_tool_done never gets flipped. - MCPTool.to_gaia_format: raw_server_name typed as str = None, which pylint/mypy reject. Promote to Optional[str] (already imported). - _disconnect_cached_agent: replace the silent except-pass with a logger.debug so disconnect failures during cache eviction are observable. The __del__ case in mcp/mixin.py keeps its bare pass (Python __del__ contract: must not raise). - analyze_failures.DEFAULT_SCENARIOS_DIR: was pointing at the gitignored vendor-coupled mcp_tool_reliability dir, giving fresh contributors a silent "no scenarios found" experience. Default to the public mcp_reliability dir; private scenarios still work via --scenarios-dir.
|
Five real framework bugs fixed, solid test coverage, clear PR description. Two items need resolution before merge. SummaryThis PR ships an MCP tool-calling reliability eval framework together with four framework-level fixes that the eval directly exposed: JSON-envelope leak (P2), truthful loop-break summary (P1/P3), server-name sanitisation (P1), and Issues Found🟡
|
Address PR #718 bot review: - analyze_failures._self_check: drop the hard-coded "expected 41 tools" warning that would fire on every public-scenario run. Replaced with an opt-in --expected-tool-count flag (default None = no check); the warning only fires when the caller asserts a known fixed tool count. - mcp/mixin.py __del__: replace silent except-pass with a defensive logger.debug (wrapped in its own try so __del__ still can't raise). Mirrors the fix already applied in ui/_chat_helpers.py. - chat/agent.py mcp_tool_limit: keep the 100 default but annotate with the validation evidence (49-tool MCP server, Gemma-4-E4B, 100% pass rate on smoke scenarios in this PR) and the small-model bloat tradeoff. - mcp_reliability/simple_tool_call.yaml: switch from a non-existent "system_status" tool to a file-listing tool that exists in default GAIA, so the scenario actually runs out of the box. - test_loop_break_truthful.py: rename test_helper_rejects_wrapper_dicts_silently to the self-describing test_caller_must_unwrap_previous_outputs_before_passing, and trim the 14-line preamble comment per CLAUDE.md's "one short line max" guidance.
|
This PR delivers real value — four well-diagnosed agent-loop bugs fixed with targeted tests, a clean eval framework extension, and long-overdue session-artifact cleanup. The Issues🟡
|
Address PR #718 bot review: - cli.py --reset-between-scenarios / --lemonade-model / --lemonade-ctx-size: these were accepted by the parser but never plumbed into AgentEvalRunner — silent no-ops. Per CLAUDE.md fail-loudly, raise NotImplementedError when any of them is set with a message pointing users at driver-script-level restarts in the meantime. The help text now opens with [NOT YET IMPLEMENTED] so --help reflects reality. - tests/unit/eval/test_analyze_failures.py: new file. 22 tests cover _derive_tool_and_style, _parse_tool_timestamp (with tz offset), correlate_log_to_turn (all three bucket paths), _bucket_counts (including the no_log_data fallback), build_failure_records (pass/fail), build_per_tool_report (iteration aggregation + meets_gate), _self_check (with and without --expected-tool-count), and _fmt_pct. - ui/_chat_helpers.py: upgrade the cache-eviction disconnect failure from logger.debug to logger.warning — a leaked MCP subprocess is observable (stuck port, orphaned process) and worth surfacing. - agents/base/agent.py: extract the "[SYSTEM: Tool call complete...]" suffix into a module-level _SINGLE_TOOL_DONE_SUFFIX constant so it's greppable and strippable from test fixtures that record role history. - tests/unit/agents/test_loop_break_truthful.py: rename the wrapped- dicts test to the self-describing test_wrapped_dicts_fall_through_to_success_illustrating_required_unwrap and frame it as a "trap test" — documents the wrong behaviour to pin the caller-unwrap contract, not endorsement.
|
This is a well-engineered PR that ships real framework fixes (P1–P3 in the author's taxonomy) alongside the eval harness that found them. The five bugs are clearly described, the fixes are targeted, and the 113-unit-test suite is unusually thorough. Two items need attention before merge. Issues🟡
|
… injection Address PR #718 bot review: - ChatAgentConfig.mcp_tool_limit + --mcp-tool-limit CLI default: drop 100 → 50. 50 covers the 49-tool MCP server already validated in this PR at 100% pass rate on Gemma-4-E4B (one tool of headroom), and is still 5× the previous 10. Workflows with >50 tools should override explicitly and warrant a fresh eval on the target model. Both config-class default and the CLI argparse default + help text are updated in lockstep. - Agent._inject_recovery_plan: add a warning log + docstring caveat when called while already in STATE_EXECUTING_PLAN (in-flight plan gets replaced — usually a subclass-hook bug, not intent). The docstring also flags that fanout/native-batch call sites haven't been exercised here. - Sharpened the 3 pylint: disable-next=assignment-from-none comments to explain WHY they're needed (pylint infers None from the base body even though the annotation is Optional[List[...]]). The bot review's suggestion to delete them was incorrect — pylint still flags the call sites without them. - .gitignore: append missing trailing newline at EOF for cleaner future diffs.
# GAIA v0.19.0 Release Notes GAIA v0.19.0 tightens the agent loop against silent-failure regressions and continues the focused-agent split. A new `gaia eval agent` reliability harness exercises tool selection end-to-end against the local Agent UI MCP backend and surfaced four agent-loop bugs that previously produced silently-wrong "Task completed" answers — all four are fixed in this release. Dedicated `BrowserAgent` and `AnalystAgent` replace the ChatAgent-backed web and data profiles. Agents can now declare a `REQUIRED_HARDWARE` capability tier that is validated at startup against the running Lemonade server. GAIA can connect to a remote Lemonade Server protected by an API key. And a new CI job auto-implements PRs for bulletproof bug issues in parallel with the existing triage path. **Why upgrade:** - **MCP tool-calling reliability framework + four framework fixes** — `gaia eval agent` runs a user-simulator + judge against the local MCP backend; the harness uncovered and fixed silent loop-break lies, a ~85-token JSON-envelope leak in tool-calling-model prompts, a missed native-path failure check, and an over-broad eval rubric. - **Specialized agents** — `BrowserAgent` and `AnalystAgent` ship as dedicated implementations behind the `gaia browse` and `gaia analyze` CLIs, replacing the monolithic ChatAgent-backed profiles. - **Hardware-requirement validation** — agents can declare `REQUIRED_HARDWARE` and fail fast at startup when the host's device tier (CPU/iGPU/dGPU/NPU/hybrid) does not satisfy it, instead of silently degrading. - **Remote Lemonade auth** — setting `LEMONADE_API_KEY` threads `Authorization: Bearer <key>` through every Lemonade-bound HTTP path; wrong/missing key surfaces an actionable error naming the variable. - **CI auto-fix for bulletproof bug issues** — bug-labelled issues that meet the "1-2 files, < 50 lines, 100% confidence" bar now get an auto-implemented PR in parallel with the standard triage comment. ## What's New ### MCP Tool-Calling Reliability Framework A new end-to-end eval harness lands at `src/gaia/eval/runner.py` (PR [amd#718](amd#718)) and is invoked via `gaia eval agent`. The runner spawns a subprocess that acts as user-simulator + LLM judge against the live Agent UI MCP backend, exercises 10 generic scenarios (no-param, single-param, multi-step, conditional, error-handling, "no tool needed"), and produces per-tool failure rollups via `analyze_failures.py`. The judge rubric in `judge_turn.md` grades on tool selection alone for `verbatim`-tagged scenarios — it explicitly does not penalize the agent for underlying-service failures, which had been conflating pipeline correctness with hardware availability. Running the harness against the agent loop immediately exposed four framework regressions, all fixed in this release: - **Silent loop-break lie.** When small local models emitted just the server prefix (`mcp_foo_mcp`) instead of the full registered tool name, the agent retried four times and then hard-coded `"Task completed with mcp_foo_mcp. No further action needed"` as the final answer despite zero successful tool calls. Server-name sanitisation now strips redundant `mcp` tokens before namespacing, and a new `Agent._build_loop_break_summary` helper branches on whether the last result was an error so the user sees the actual error wording. A new AST guard in `tests/unit/agents/test_agent_source_invariants.py` prevents the lie-on-loop literal from reappearing. - **JSON envelope leak in tool-calling-model prompts.** The `{"tool": ..., "tool_args": ...}` template was supposed to be suppressed for models that support native `tools=[]` calling, but `self.model_id` was assigned *after* `_register_tools()` ran, so the suppression check always returned False and ~85 redundant tokens shipped in every prompt. Moving the `model_id = model_id` assignment ahead of registration closes the gap; a regression test asserts the `==== RESPONSE FORMAT ====` block does not appear in a tool-calling agent's system prompt. - **Loop-break helper missed the native path.** The shared helper looked at `step_results[-1]` to detect a failure streak, but the native-tool-call path appends to `previous_outputs` instead (wrapper dicts). On native callers the helper saw an empty list, missed every failure, and returned `"Task completed"`. The fix unwraps `previous_outputs` at the call site; the regression test exercises the helper with a sequence of error results. - **Eval rubric conflated pipeline and hardware.** Some MCP services wrap real operation failures in a `status: success` envelope with the failure buried in `data.content[*].text`. On dev hardware where vendor-side features don't all work, the judge graded the agent as failing even when it picked the right tool. The new STEP 0 in `judge_turn.md` overrides the rubric for `verbatim`-tagged scenarios — `correctness = 10` if the right tool was invoked, `0` if the wrong tool was, regardless of underlying op success. Eight new unit-test files cover the sanitiser matrix, loop-break helper, candidate-list invariant, scenario validation, and `--iterations` flag. ### Dedicated Browser and Analyst Agents The Agent UI's web and data entries previously routed to `ChatAgent` profiles, which meant they carried the full monolithic agent surface instead of the focused tool sets the use cases actually need. PR [amd#1070](amd#1070) adds dedicated `BrowserAgent` and `AnalystAgent` implementations and wires the built-in web/data registrations (plus their `lite` variants) to them. Two new CLI subcommands ship alongside: `gaia browse` for the browser flow and `gaia analyze` for the analyst flow. Both compose the relevant tool mixins explicitly rather than inheriting everything from the ChatAgent shim. ### Hardware-Requirement Validation for Agents Agents can now declare a hardware capability tier via `REQUIRED_HARDWARE = HardwareRequirement(min_device=...)` (PR [amd#1057](amd#1057)). At agent startup, `LemonadeManager.ensure_ready(required_min_device=...)` queries the running Lemonade server through `LemonadeClient.get_system_info()` and raises `HardwareRequirementError` if the host's reported device tier does not satisfy the declaration. NPU-only or dGPU-only agents fail fast at construction time instead of silently degrading at first inference. This is Phase 1 — validation only. The resolved `recipe` is computed and logged for debugging but is *not* applied to the Lemonade server startup path; a follow-up will wire the resolved recipe through if the project chooses to. ### `LEMONADE_API_KEY` for Authenticated Remote Lemonade Before this release, GAIA could not connect to a remote Lemonade Server protected by an API key — every request returned `401` regardless of the user's configuration. PR [amd#1149](amd#1149) (closes [amd#1139](amd#1139)) threads `LEMONADE_API_KEY` (from `.env` or the shell environment) as `Authorization: Bearer <key>` through every Lemonade-bound HTTP path in GAIA: the central `LemonadeClient._send_request`, the four `requests`-bypass sites, both OpenAI-SDK constructor sites, `LemonadeProvider`, `VLMClient`, the Agent UI router (`system.py`), Agent UI chat helpers, the server startup probes, and the base `Agent` health probe. Behaviour is fully additive — when the env var is unset, every call path behaves identically to v0.18.1. A wrong or missing key produces a fixed-string error naming `LEMONADE_API_KEY` (the response body is intentionally not echoed back, to avoid leaking reflected `Authorization` headers from misconfigured proxies). ### CI Auto-Fix for Bulletproof Bug Issues PR [amd#1159](amd#1159) adds a Claude-powered auto-fix job to `.github/workflows/claude.yml` that runs in parallel with the existing `issue-handler` triage. When a new issue lands with the `bug` label, Claude reads the report, and if the fix passes a strict "bulletproof" bar — 1–2 files touched, fewer than 50 lines changed, 100% confidence, no hardware-dependent verification needed — it implements the change, validates with `python util/lint.py` and the relevant unit tests, opens a PR, and posts the PR link plus step-by-step verification instructions back on the originating issue. Bug reports that don't meet the bar still get the standard triage comment from `issue-handler`; the auto-fixer exits silently for the rest. ## Bug Fixes - **BrowserAgent and AnalystAgent crashed instantly in the Agent UI** (PR [amd#1202](amd#1202)) — Selecting either of the new split agents and sending any message raised `AttributeError: '<Agent>' object has no attribute '_mcp_manager'`. The MCP client mixin's optional attribute was never initialised on the split agents, and `get_mcp_status_report()` — invoked on every `/api/chat/send` — hit the undefined dereference. The two-layer fix adds a class-level `_mcp_manager: Optional[MCPClientManager] = None` on `MCPClientMixin` (covering every future agent that inherits it) plus explicit per-agent initialisation in the five affected agents (documenting intent at the point of use). CLI paths were unaffected — only the Agent UI's `/api/chat/send` triggered the bug. - **`LEMONADE_BASE_URL` env var normalisation** (PR [amd#1160](amd#1160)) — Setting `LEMONADE_BASE_URL` to a value without the trailing `/api/v1` suffix produced 404s deep in the request pipeline. The variable is now normalised on load so both `http://host:8000` and `http://host:8000/api/v1` resolve to the same canonical form. - **Custom agents on disk now visible to the Agent UI** (PR [amd#1138](amd#1138)) — `~/.gaia/agents/` registrations were not surfacing to the Agent UI's agent list because the registry discovery pass skipped the disk source on cold start. The fix lists custom-directory agents alongside the built-ins on every request. - **`pr-review` CI re-enabled and credit-resilient** (PR [amd#1163](amd#1163)) — The Claude-based `pr-review` job had been disabled after an Anthropic credit window; all Claude-backed jobs now treat 429 quota errors as non-fatal warnings so CI keeps moving when the project hits its quota. - **Silent skips in `test_sdk.py` removed** (PR [amd#1190](amd#1190)) — Tests that depended on an environment fixture were silently `pytest.skip()`-ing instead of being explicitly conditional. The skips are now gated on the actual fixture and surface as `XFAIL` when intentionally inapplicable. Closes the first slice of [amd#877](amd#877). - **`refresh-context7` fails loudly on unexpected status** (PR [amd#1073](amd#1073)) — The terminal job of the publish workflow previously masked non-cooldown HTTP responses as "OK". It now distinguishes the known HTTP 400 cooldown window from any other status code, so a regression that breaks the Context7 refresh is no longer indistinguishable from the documented cooldown. ## Tooling & Docs - **CLI smoke-test for every subcommand and console script** (PR [amd#1193](amd#1193)) — Every `gaia <subcommand>` and every console script declared in `setup.py` is now exercised with `--help` in CI, catching import-time regressions and shadowed entry points before they ship. - **Fail-path coverage for `GovernedAgentMixin` and `CheckpointBridge`** (PR [amd#1161](amd#1161)) — Adds unit tests for the error/abort branches of the governance layer that were previously only exercised on the happy path. - **Custom-agent MCP harness for installer testing** (PR [amd#1069](amd#1069)) — A reproducible installer-level MCP harness for custom agents, so installer-affecting changes are tested against the real agent-registration path rather than mocks. - **Dependabot revived, agent-ui entry added, patch auto-merge shipped** (PR [amd#1191](amd#1191)) — Dependabot PRs are flowing again, the Agent UI npm workspace is now covered, and patch-bump PRs that pass CI auto-merge. ## Full Changelog **17 commits** since v0.18.1: - `6c6e4c34` — fix(agents): unbreak BrowserAgent/AnalystAgent in Agent UI (amd#1202) - `6379e183` — feat(agent-hub): discover installed agent entry points (amd#1187) - `a79acc88` — test(cli): smoke-test every subcommand and console script with --help (amd#1193) - `f7a75902` — ci(dependabot): revive PRs, add agent-ui entry, ship patch auto-merge (amd#1191) - `70d75b70` — fix(tests): remove silent skips in test_sdk.py (amd#877 Part A) (amd#1190) - `f8b2c1ab` — feat: MCP tool calling reliability test framework (amd#718) - `f4270687` — feat(agent-hub): Agent Hub UI + platform plan + hub skeleton (amd#1103) - `6ef1feee` — fix(llm): normalize LEMONADE_BASE_URL env var to include /api/v1 suffix (amd#1160) - `72b77167` — fix(ci): re-enable pr-review and make all Claude jobs credit-resilient (amd#1163) - `b46bf654` — fix(agent-ui): list custom agents from disk (amd#1138) - `63aedb47` — test: add fail-path coverage for GovernedAgentMixin and CheckpointBridge (amd#1161) - `7b8f22c0` — feat(llm): support LEMONADE_API_KEY for authenticated remote Lemonade (amd#1149) - `b22fa73d` — feat(ci): auto-fix job for bulletproof bug issues (amd#1159) - `6b7b9e7d` — fix(ci): make refresh-context7 fail loudly on unexpected status (amd#1073) - `e2d4e2d7` — feat(sdk): hardware requirement validation for agents (amd#1057) - `1a73dcc5` — feat(agents): add browser and analyst agents (amd#1070) - `2554424b` — test(installer): add custom agent MCP harness (amd#1069) Full Changelog: [v0.18.1...v0.19.0](amd/gaia@v0.18.1...v0.19.0) ## Release checklist - [x] `util/validate_release_notes.py docs/releases/v0.19.0.mdx` passes - [x] `src/gaia/version.py` → `0.19.0` - [x] `src/gaia/apps/webui/package.json` → `0.19.0` - [x] Navbar label in `docs/docs.json` → `v0.19.0 · Lemonade 10.2.0` - [x] All 17 commits in range (v0.18.1..HEAD) are represented in the notes - [ ] Review from @kovtcharov-amd addressed
…ariant (amd#1203) Build Installers on the merged [v0.19.0 release commit](amd@6f8bba0) fails on both `ubuntu-latest` and `windows-latest` legs of the **Custom agent MCP harness** matrix because [`tests/installer/test_custom_agent_mcp_harness.py:199`](https://github.com/amd/gaia/blob/main/tests/installer/test_custom_agent_mcp_harness.py#L199) still encodes the pre-amd#718 error wording. PR [amd#718](amd#718) deliberately replaced `"Tool '<name>' not found"` with the generic `"Unknown tool name. Use only tools listed in your AVAILABLE TOOLS section."` to prevent small models from interpreting the echoed bad name as self-confirmation and looping on it — that's the "no-echo invariant on the candidate-list message" called out in v0.19.0's release notes. The test from [amd#1069](amd#1069) missed the migration. This is a one-line test update to the second assertion in `test_custom_agent_with_mcp_reports_diagnosable_connection_failure`. The test's "diagnosable connection failure" intent is preserved by the **first** assertion in the same function, which still verifies `get_mcp_status_report()` surfaces the transport-level error (`"MCP server process died (exit code: 17)"`) — that's the actual user-facing diagnostic for a dead MCP server. This unblocks the v0.19.0 release ([amd#1201](amd#1201)) — pre-tag verification requires a green Build Installers run on the merged commit, which currently fails because of this single assertion. ## Test plan - [x] `python -m pytest tests/installer/test_custom_agent_mcp_harness.py -xvs` — 3/3 pass locally - [x] Lint: change is a string-literal update; no formatter impact - [ ] Build Installers run on this branch goes green
Why this matters
GAIA had no automated way to verify the agent loop + Lemonade + local model reliably select the correct MCP tool in response to natural-language requests. Tool-selection regressions could only be caught by manual testing. This PR ships an end-to-end eval harness, 10 generic example scenarios, and several framework-side fixes the eval immediately exposed — including a silent lie on tool loops, a JSON-envelope leak for tool-calling models, and a server-name doubling pattern that trained small models to truncate tool names.
What changed
Eval framework
gaia eval agent(src/gaia/eval/runner.py) — spawns a subprocess that acts as user-simulator + judge against the Agent UI MCP backend.analyze_failures.py— produces per-tool failure rollups from trace JSON.judge_turn.mdSTEP 0 — pipeline-scope override forverbatim-tagged scenarios: the rubric grades on tool selection only, not whether the tool's underlying operation succeeded at the hardware layer.Generic test scenarios (
eval/scenarios/mcp_reliability/)Agent-loop fixes (see "Problems found" below)
model_idordering inAgent.__init__Problems found, in the order we found them
P1 — Bare-prefix tool calls and the silent-success lie. Small local models sometimes emit just the server prefix (e.g.
mcp_foo_mcpwhen the registered tool ismcp_foo_mcp_<tool>) instead of the full name. The framework returnedTool not foundand the model retried with the same bad name four times. At that point the loop-detection path hard-coded"Task completed with mcp_foo_mcp. No further action needed"as the final answer — a silent lie to the user.Fix: sanitise MCP server names so the framework strips redundant
mcptokens before namespacing (a server namedfoo_mcpproducesmcp_foo_<tool>instead ofmcp_foo_mcp_<tool>). ExtractedAgent._build_loop_break_summarythat branches on whether the most recent result was an error — surfaces the actual error wording instead of "Task completed".P2 — JSON-envelope leak. The framework was supposed to suppress the embedded-JSON response-format template for models that support native
tools=[]calling (Phase 2 work). The check ran againstself.model_id, butmodel_idwas assigned AFTER_register_tools()ran. The MCP registration triggered system-prompt caching whilemodel_idwas stillNone, so the suppression check returned False and the envelope template leaked into every tool-calling agent's prompt (~85 redundant tokens teaching{"tool": ..., "tool_args": ...}alongside the native schema).Fix: moved
self.model_id = model_idto before_register_tools()inAgent.__init__. Added a regression test that asserts a tool-calling agent's system prompt does NOT contain the==== RESPONSE FORMAT ====block.P3 — Loop-break helper missed the native path. Phase 4 extracted
_build_loop_break_summaryto share logic between two call sites. The helper looked atstep_results[-1]to decide if recent calls were errors. The legacy single-tool path appends tostep_results(correct). The native-tool-call path appends toprevious_outputsinstead — wrapper dicts of shape{tool, args, result}. The helper at the native site saw an empty list, never noticed the failures, and returned"Task completed with <name>"despite a loop of errors.Fix: at the native call site, unwrap
previous_outputsbefore passing to the helper ([o.get("result") for o in previous_outputs]). Added a regression test exercising the helper with a sequence of error results.P4 — Eval rubric conflated test scope. Original
success_criteriastrings read"PASS if agent calls <tool> AND the tool succeeds". The LLM judge interpreted "the tool succeeds" as "the underlying operation worked at the hardware/service layer". Some MCP services wrap real operation failures in astatus: successenvelope with the actual outcome buried indata.content[*].text("Failed to ..." / "error code: N"). On dev hardware where many vendor-side features don't work, the judge graded the agent as failing even when it picked the right tool — because the tool's underlying op failed.Fix:
judge_turn.mdSTEP 0 overrides the rubric forverbatim-tagged scenarios —correctness = 10if the right tool was invoked,correctness = 0if the wrong tool was invoked, regardless of underlying op success or what the agent's prose said. Pipeline correctness only.P5 — Module caching gotcha during iteration. Edits to a custom agent's
_get_system_prompt()didn't take effect through the Agent UI chat endpoint until the backend process was restarted. Python caches imported modules at startup, so the agent registry held a stale class reference. Not a framework bug, but documented as a UX gotcha — restart the backend after editing custom-agent code.Test coverage
113 unit tests across new and extended files:
tests/unit/mcp/client/test_sanitize_server_name.py(NEW) — 12-case sanitiser matrix + idempotency + warn-oncetests/unit/agents/test_loop_break_truthful.py(NEW) — 7 cases including the native-path regressiontests/unit/agents/test_tool_not_found_error.py(NEW) — 6 cases including no-echo invariant on the candidate-list messagetests/unit/agents/test_agent_source_invariants.py(NEW) — AST guard against the lie-on-loop literal reappearingtests/unit/agents/test_response_format.py(extended) — verbatim rule + MCP mixin prompttests/unit/mcp/client/test_mcp_client.py(extended) — sanitised prefix + register/unregister symmetrytests/unit/mcp/client/test_mcp_client_mixin.py(extended) — covers the newclient.prefixattributetests/unit/eval/test_mcp_reliability_scenarios.py(NEW) — validates the 10 generic YAML scenariostests/unit/eval/test_iterations.py(NEW) — exercises the--iterationsflagTest plan
python -m pytest tests/unit/agents/ tests/unit/mcp/ tests/unit/eval/ -q— all greenpython util/lint.py --all— cleangaia eval agent --category mcp_reliability --iterations 3— verify no trace contains"Task completed with mcp_"(the lie-on-loop signature) and no doubled-prefix tool names in any trace