test(eval): behavior-E2E harness — assert tool side-effects against real model#1431
Conversation
_extract_embedded_tool_call now collects ALL {…} candidates tagged
fenced/unfenced and applies a 4-rule decision:
1. ≥1 unfenced → return the first (unchanged, zero regression).
2. exactly 1 fenced → return it (fixes the Zephyr capture case).
3. >1 fenced + 0 unfenced → ambiguous → None + logger.warning.
4. else → None.
Closes #1428.
…o docstring/prompt Builder now detects when the LLM emits hallucinated success markers (✅, "Agent Created", "File location") without calling create_agent, injects a corrective turn, and loops. If create_agent returns "Error:…" the loop ends immediately with an honest failure — never a fabricated success message. System prompt and create_agent docstring list the 4 missing built-in tools: code_index, filesystem, scratchpad, browser. The "output ONLY the bare JSON object" rule is added to the system prompt to directly attack the narrate-then-fence-then-fabricate pattern.
Each failed discover() / register_from_dir() attempt now stores the error reason in _load_errors (agent_id → "type: message"). The public get_load_error(agent_id) accessor lets callers (chat helpers, future UI) surface a meaningful reason instead of just "unknown agent". One broken agent still does not block discovery of other agents.
All 4 sites in _chat_helpers.py that previously did agent_type="chat" on a registry miss now surface a friendly message via _agent_unavailable_message() instead of silently impersonating a different agent. Non-streaming: returns the error string directly from _do_chat. Streaming: emits a final SSE answer event and returns from _run_agent. When the registry recorded a load error for the agent, the reason is included in the message so the user knows why it failed.
Review: behavior-E2E harness (#1431)Summary — Solid, well-targeted addition. The side-effect-not-reply premise, planted unguessable tokens, N× repetition, and Issues🟡 Side-effect check duplicates the builder's slug logic instead of reusing it ( from gaia.agents.builder.agent import _normalize_agent_id, _split_camel_case
...
agent_id = _normalize_agent_id(_split_camel_case(planted_name.strip()))Apply in both 🟡 🟡 Workflow venv is created but never activated ( 🟢 🟢 🟢 Dead field Strengths
VerdictApprove with suggestions. Nothing blocks the runtime path. Please reuse |
…d-error key
Addresses two issues raised in PR review:
- The fabrication-marker check ("Agent Created", "✅", "File location") fired
on every tool-less turn, including the post-success summary turn after
create_agent already ran. A natural "✅ Your agent is ready" summary was
flagged as fabricated → corrective turn → model called create_agent again →
"already exists" error → builder reported failure despite the agent
having been created successfully.
Fix: track created_ok=True when create_agent returns a non-error result.
Post-success turns bypass the fabrication check and return the summary directly.
- get_load_error() docstring now notes that errors are keyed by directory
name so callers understand when a type-string lookup may return None.
kovtcharov-amd
left a comment
There was a problem hiding this comment.
Approving. Test/CI/docs only, stacks cleanly on #1430. Asserting the tool's side-effect (file on disk + /api/agents listing) rather than the reply, with planted unguessable names, is exactly the harness this failure class needed — and the pure-helper split keeps the 24 unit tests model/server-free.
Minor (non-blocking): the push: trigger targets [self-hosted, strix-halo], which doesn't exist until #1297, so pushes matching the path filters will queue a runner-less job that shows as pending in Actions. Fine since it's documented — just flagging the UI noise until the runner lands.
Mocked unit tests and UI-render E2E tests both miss the class of bug where an agent claims success but the underlying tool never ran (reproduced in #1428). This harness catches it by driving the real server with a real model, repeating each scenario N× with a planted unguessable name, and asserting the file-system side-effect occurred — not just that the agent produced a reply. - `src/gaia/eval/behavior_harness.py`: reusable `Scenario`/`BehaviorHarness`, `Verdict` enum (true_success / false_success / honest_failure / error), `_aggregate_verdicts` with hard-fail on any false_success, builder scenario - `tests/unit/eval/test_behavior_harness.py`: 24 deterministic unit tests, no model or network required, runs in normal CI - `tests/integration/eval/test_behavior_e2e.py`: live test gated by `@pytest.mark.real_model` and `require_real_model` fixture (auto-skip when Lemonade is absent) - `.github/workflows/test_agent_behavior_e2e.yml`: CI workflow ready for `[self-hosted, strix-halo]`; activates once #1297 lands - `pyproject.toml`: registers the `real_model` pytest marker - `docs/sdk/testing.mdx`: documents the pattern for future agents Closes #1429
…#1429) Follow-up to the harness commit: pyproject.toml registers the new `real_model` pytest marker (required by --strict-markers), and docs/sdk/testing.mdx documents the side-effect assertion pattern for future agent contributors.
- Import _normalize_agent_id/_split_camel_case from builder instead of re-implementing slug logic in two places; harness now derives the expected agent path with the same function the builder uses - Remove dead Scenario._planted_names field (never read or written) - Log with exc_info=True in _agent_listed on Exception instead of silently returning False - Use str.removesuffix() instead of rstrip() for /api/v1 URL strip in the integration test fixture - Fix workflow: gaia init --check (invalid flag) -> gaia init --skip-lemonade - Fix workflow: add "uv run" prefix to pytest step so the venv is active without a separate activate step
87f4bca to
05e7429
Compare
|
All items addressed in the rebase+fix commit ( 🟡 🟡 🟡 Workflow venv not activated — All 🟢 🟢 🟢 Dead Also removed three unused imports ( |
|
🟡
Or, since this is an optional extra check after the file-exists check already confirmed success, you could simply let the exception propagate to the outer |
|
Removed the try/except from |
|
🟡 The workflow uploads Or, more robustly, pass a fixed # in test_builder_creates_agent_file
import tempfile, pathlib
artifact_dir = pathlib.Path(tempfile.mkdtemp(prefix="gaia-behavior-e2e-")) / "artifacts"then the original glob |
|
Fixed — updated the upload glob to |
Both unit tests (mocked LLM) and UI-render E2E tests passed while the builder silently failed to create agents ~40% of the time (#1428). The failure class — "agent claims success but the tool never ran" — is invisible to tests that mock the model or only check that the UI rendered a reply.
This adds a reusable behavior-E2E harness that catches it: drive a tool-using agent through the real server with a real model, repeat each scenario N×, and assert the tool's side-effect actually occurred (file on disk, registry entry). A "success" reply with no side-effect is a hard failure. Built-in scenario: builder — plants an unguessable agent name per run, confirms
~/.gaia/agents/<id>/agent.pyexists and appears inGET /api/agents.In-CI execution is deferred to #1297 (self-hosted Strix Halo runner); the workflow YAML is present and ready. Validated out-of-band on Strix Halo class hardware.
Test plan
pytest tests/unit/eval/test_behavior_harness.py— 24 deterministic unit tests (no model, no network) — PASSQwen3.5-35B-A3B-GGUF— 5/5 builder runs write the file;test_builder_creates_agent_filePASSED in 244s (5 agents: test-721e1b6e, test-a40e71cb, test-f85a63ec, test-dcfdbf16, test-2718354e)test_zephyr_regression_fixtureand 6 additionalTestSingleFencedCandidatetests FAIL on reverted code; all 14 PASS on fixed codepyproject.tomlregistersreal_modelmarker;pytest --strict-markerspasses — PASS.github/workflows/test_agent_behavior_e2e.ymlpresent withbuilderpath filter and[self-hosted, strix-halo]runnerNote on stochastic RED: a live 5-run batch with the pre-fix parser can pass if the model uses bare tool calls (P ≈ 8% per batch). The deterministic RED proof is the unit test suite —
test_zephyr_regression_fixturereplays the verbatim Zephyr capture and assertstool=Nonewithout the fix,tool="create_agent"with it.Stacks on / Closes #1429
Related: #1428, #883, #1297