-
Notifications
You must be signed in to change notification settings - Fork 162
feat(skills): make image generation reachable from the flagship - #3073
feat(skills): make image generation reachable from the flagship #3073kovtcharov-amd wants to merge 4 commits into
Conversation
Skill audit
✅ All audited skills cleared the tier they claim. Per-finding detail is withheld here on purpose. Read it in the Security > Code scanning tab, or download the |
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.
What Enabling Code Scanning Means:
- The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
- Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
- You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.
For more information about GitHub Code Scanning, check out the documentation.
Request changes
This turns image generation on for the flagship agent, keeps the Stable Diffusion "persona" prompt out of the way by moving the procedure into a new image-gen skill, and rewrites the failure messages so a slow first-time model download is no longer reported as "your server is down". The trade is well argued and the tests around it are unusually good — but three things should be fixed before merge.
1. Two of the new tests need a live embedding model, and the CI job that runs them doesn't have one. They ask the agent to pick tools for "draw me a picture...", which requires the embedder; without it the agent returns "selection disabled" and the tests fail. The evidence bundle says as much — the same runner couldn't exercise this. Either skip these when no embedder is present, or feed the selector a stub, otherwise this lane goes red on every PR.
2. The new error messages tell people to run a command that no longer exists on current installs. The "pre-fetch the model" and "start the server" advice hardcodes the old lemonade-server CLI; modern installs ship a different binary, and the repo already has a helper that emits the right command for the machine it's on (with a test asserting the old form is never named). The new image-gen skill repeats the same stale command, so the agent will read it out to users. Worth fixing in all three places at once.
3. The skill's "change one thing about the last image" advice can't actually work. It tells the model to reuse the previous image's seed, which is what keeps the second picture recognisably the same — but the tool only records a seed when the user asked for one, so for the normal case the history has none. Either record a concrete seed on every generation, or soften the skill text so it doesn't promise something the history can't deliver.
Worth a quick check too: moving the image output folder to an absolute path under the user's home may trip an existing golden-prompt test that relied on it being relative — please run that one test locally.
Nothing security-sensitive in this change.
Real-world evidence
evidence-bundle.md is present and substantive: it was captured on a no-inference runner and covers the CLI (gaia skill audit/import/info/list on the new skill), the live HTTP route (/api/agents returning the new tools_count: 70), real registered-tool calls with no mocks, and the prompt composition. The rewritten error path was hit for real with nothing listening:
=== generate_image() with no Lemonade Server === status: error error: Cannot reach Lemonade Server. Start it with `lemonade-server serve`, or set LEMONADE_BASE_URL to a running server. (Request failed: HTTPConnectionPool(host='localhost', port=13305): ... Connection refused)
=== system prompt (flagship, SD tools ON) === length: 11667 chars contains 'expert image generation assistant': False bundle line: - image_gen: Generate images from a text prompt (Stable Diffusion).
That evidence supports the core claim — the capability is reachable and the persona stays out. Marked pending strix-halo lane: a real generated PNG, the chat-model eviction/reload the docs now warn about, and Agent UI pixels. Those deferrals are fine for this lane.
Two gaps do bear on the verdict. The bundle explicitly records that per-turn tool selection could not be run ("semantic selection needs the embedder, which is not up on this runner") — that is the same condition CI runs under, which is finding 1. And it shows no gaia eval agent comparison, which this repo requires for tool-registration and system-prompt changes; if that ran elsewhere, please link it. Everything not covered above rests on static review. I could not read the PR description directly (the GitHub CLI is unavailable in this review environment), so evidence is judged from the bundle alone.
🔍 Technical details
🔴 Critical
Selection tests fail without a live embedder — test_gaia_agent.yml has none (hub/agents/gaia/python/tests/test_sd_tools_do_not_rewrite_the_prompt.py:110, :123)
_select_fresh calls the real ToolLoader.select, which embeds the query via MemoryMixin._embed_text → Lemonade. tool_loader.py:266-277 catches the failure, sets _session_disabled, and returns None. The workflow runs pytest hub/agents/gaia/python/tests/ on a plain ubuntu-latest with no Lemonade (.github/workflows/test_gaia_agent.yml:85), so assert selected is not None fails for all three cases. Sibling tests in this package avoid it by injecting a fake embed_fn (test_lazy_skill_activation.py:102-109). Cheapest fix keeps the assertion meaningful where an embedder exists:
selected = _select_fresh(flagship, query)
if selected is None:
pytest.skip("dynamic tool selection needs a live embedder")
assert SD_TOOLS <= set(selected)
Injecting a deterministic embed_fn would be stronger — a skip means CI never checks the behaviour the test was written for.
🟡 Important
Remedy commands name the removed legacy CLI (src/gaia/sd/mixin.py:473-474, :479-481; hub/skills/image-gen/SKILL.md:99)
describe_client_hint/describe_start_hint exist for exactly this and resolve modern vs. legacy tooling per machine — mixin.py:34 already imports the first and uses it at :599. test_remedy_commands_are_runnable.py:113-117 asserts a modern install never sees lemonade-server load; the new text names it unconditionally, so on a modern install both branches point at a binary that isn't there.
if "timed out" in lowered or "timeout" in lowered:
return (
f"Timed out waiting for {model}; the server is running but did "
"not answer in time. First use of an SD model both downloads "
"and loads several GB. "
f"{describe_client_hint('pull', model).instruction} Then confirm "
f"it loads: {describe_client_hint('load', model).instruction} ({raw})"
)
if "connection refused" in lowered or "failed to establish" in lowered:
return (
f"Cannot reach Lemonade Server. {describe_start_hint().instruction} "
"Or set LEMONADE_BASE_URL to a running server. "
f"({raw})"
)
Needs describe_start_hint added to the lemonade_launcher import. Note this also changes what tests/unit/test_sd_error_messages.py:46,:37 should assert — they currently pin the literal lemonade-server serve / pull strings, so they'd need to mock resolve_lemonade the way test_remedy_commands_are_runnable.py does, or assert against the helper's output. As written those assertions are environment-dependent: with no client binary resolved, describe_client_hint emits prose containing no pull at all.
Generation history never records the seed the skill tells the model to reuse (src/gaia/sd/mixin.py:396; hub/skills/image-gen/SKILL.md:74-79)
result["seed"] = seed stores the requested seed, which is None on any call that didn't specify one — the normal case. The response is parsed for b64_json only (:379), so the server-chosen seed is discarded. get_generation_history() therefore returns "seed": null, and the skill's "keep everything else — including the seed" instruction has nothing to keep. Recording one at call time fixes it (add import random at the top):
start_time = time.time()
# Record a concrete seed — history-driven iteration ("same but at
# sunset") needs one, and the server never returns the one it picked.
if seed is None:
seed = random.randrange(2**32)
Alternative, if pinning the seed is undesirable: drop the seed-reuse paragraph from the skill and say the second image will differ.
The absolute output dir may break the row13 golden (hub/agents/chat/python/gaia_agent_chat/agent.py:1486-1491, tests/unit/test_profilespec_characterization.py:266-271)
That harness builds a full-profile ChatAgent with enable_sd_tools=True under patch.object(Path, "home", return_value=Path("/fake/home")) and a cleared os.environ. GAIA_CONFIG_DIR is computed at import of gaia.config, which nothing in the harness's import chain pulls in at module level — so if that first import happens inside the patch, the new sd_output_dir is /fake/home/.gaia/cache/sd/images, mkdir raises PermissionError, the new except Exception swallows it, and the SD tools are missing from a golden that lists them. If instead the module was already imported, the test writes into the developer's real ~/.gaia. I could not run pytest in this review environment — please run pytest tests/unit/test_profilespec_characterization.py -k row13 both standalone and as part of the full file to confirm which.
LLM-affecting change with no eval run shown (hub/agents/gaia/python/gaia_agent/agent.py:178, src/gaia/sd/mixin.py:147)
Three tools added to the flagship registry, a changed model parameter description, and a system-prompt composition change all sit on CLAUDE.md's "requires gaia eval agent before merge" list. evidence-bundle.md contains no scorecard comparison. Link the run if it happened elsewhere.
🟢 Minor
A connect-timeout is reported as "the server is running" (src/gaia/sd/mixin.py:469) — requests renders a connect timeout as Connection to host timed out. (connect timeout=5), which hits the timeout branch and tells the user a server that never answered a SYN is healthy. Same substring-ordering class of bug the helper was written to fix. Checking "connect timeout" before the general timeout test covers it.
The characterization harness's litter guard is now dead (tests/unit/test_profilespec_characterization.py:266-269) — it redirects cwd to tmp_path solely because init_sd used to mkdir a relative .gaia/. With the absolute path that redirect does nothing; see the 🟡 above for why it may do worse than nothing.
Strengths
- The prompt/tools split is the right call and is pinned properly. Suppressing the 5K SD persona while keeping the one-line
image_genbundle entry gets the capability without the identity, andtest_the_capability_is_still_advertised_as_a_bundlestops a future "cleanup" from removing the only thing that makes it discoverable. - Bundle/manifest bookkeeping is complete.
image_genadded toFULL_BUNDLES, the three names added toFULL_OPTIONAL_TOOLSwith the reason documented, andtools_countmoved in both the manifest andbuild_gaia()— so the existing drift guards stay meaningful rather than being worked around. - The starter-skill honesty guard was extended, not bypassed. Wiring
init_sdinto the registry fixture (test_starter_skills.py:199) and addinggenerate_imageto the guards-the-guard assertion keepstools_requiredvalidation from going vacuous for the new skill. - The
image-genskill is written for the constraint that actually matters — generation is slow, so spend the thinking before the call — and it explicitly forbids silent fallback to a smaller model, matching the repo's fail-loudly rule.
2c16038 to
e51acd1
Compare
Approve — this PR correctly enables a feature that already existed but was unreachable: enable_sd_tools was always False on the flagship, so asking for an image got nothing even though the tools were built and registered.
Three fixes alongside the flag change are each well-reasoned:
- The SD "expert image generation assistant" persona (≈5 K chars) now gets unconditionally stripped from
_get_mixin_prompts. It was auto-discovered and landing at the front of the system prompt — wrong identity for every non-image turn. The procedure moves to theimage-genskill where it only renders on demand. - Timeout vs. connection-refused are now distinguished in
_describe_client_error. The previous substring test for"connect"matched both (both carryHTTPConnectionPool), routing a healthy-but-slow model download into "start the server" advice. - The SD output directory is now absolute under
~/.gaia/cache/sd/imagesinstead of cwd-relative, which would resolve to the package directory for a daemon-launched sidecar.
Tests pin all three behaviours, doc updates are consistent (guide, CHANGELOG, SKILL.md, starter-skills, YAML manifest all agree on 70 tools), and the arithmetic in the guide (55 +たす 7 +たす 4 +たす 3 +たす 1 =わ 70) checks out.
🟢 Minor follow-up — the except Exception block around init_sd in agent.py was pre-existing. The PR improved it (DEBUG → WARNING + exc_info=True), but still swallows the error without re-raising. CLAUDE.md asks for that to be fixed when touching the file. Not blocking this merge, but worth a clean-up when the file is next touched.
🔍 Technical details
Silent-fallback note (hub/agents/chat/python/gaia_agent_chat/agent.py): the except Exception as _sd_err: logger.warning(...) handler discards the error and lets the agent proceed without SD tools. init_sd in practice can only fail with filesystem errors (directory creation) or unexpected SDK exceptions — the SD client doesn't contact the server at init time, so "Lemonade not running" is not a trigger. Still, CLAUDE.md §"No Silent Fallbacks" says: "add a specific exception type, log with context, or re-raise." exc_info=True adds context; narrowing to OSError (the realistic failure) or re-raising would complete the fix.
_get_mixin_prompts is now unconditional — even a deliberately standalone SD agent would get the persona stripped. This is intentional and pinned by test_the_sd_persona_stays_out_of_the_system_prompt, but authors composing SDToolsMixin in isolation should know the prompt is now opt-in via skill, not auto-included.
Selection skip logic (_select_fresh in the new test file): if select() returns None for a reason other than session_disabled, the caller does set(None) → TypeError. In practice this can't happen (the loader only returns None when disabled), but a guard would make the skip path more robust.
kovtcharov-amd
commented
Sep 1, 2026
Rebased onto main and addressed the except Exception finding.
The merge conflict was a false alarm in substance — main added two proactive-skill-discovery config fields immediately above the enable_sd_tools line this PR flips, so git flagged adjacent additions rather than a real disagreement. Both sides are kept.
On the swallowed error: an unexpected failure during image-tool setup now surfaces instead of quietly costing the agent its tools. Only a genuine filesystem problem is tolerated, and when that happens the log names the directory and how to fix it. A down Lemonade was never a trigger here, which I confirmed rather than assumed.
Also documented that the Stable Diffusion prompt is opt-in, so anyone composing that mixin into their own agent isn't surprised by its absence.
Not verified: LLM behaviour. This changes tool registration, which normally needs an eval run. I could not produce one — the CI gate is down (#3016) and no local backend was available. Treat selection quality as unmeasured.
🔍 Technical details
Conflict — hub/agents/gaia/python/gaia_agent/agent.py only. Main's #3010 inserted skill_discovery / skill_discovery_threshold directly above the enable_sd_tools block; resolution keeps both, with enable_sd_tools: bool = True. Merged rather than rebased so nothing already pushed gets rewritten.
except Exception → except OSError (gaia_agent_chat/agent.py). Verified before narrowing, not inferred:
LemonadeClient.__init__is URL parsing plus attribute assignment — no network call. Constructed against a dead port in 0.2 ms, so "Lemonade not running" cannot reach this handler.resolve_lemonade_api_keyand_get_lemonade_confighave no raise paths.@toolassigns into_TOOL_REGISTRYwith no duplicate check.- That leaves
sd_output_dir.mkdir(parents=True, exist_ok=True)→OSError.
Both branches exercised: injected PermissionError → caught, WARNING naming the path and remedy, agent builds without SD tools; injected RuntimeError → propagates. GAIA_CONFIG_DIR is a real env override (src/gaia/config.py:24), so the suggested remedy is honest.
_select_fresh — pytest.fail on None with the session enabled, instead of falling through to set(None). Preserves the property that a real selection regression fails rather than skips.
Validation — 484 passed, 16 skipped across hub/agents/gaia/python/tests/, test_chat_tool_bundles, test_sd_error_messages, test_starter_skills. util/lint.py --all: Black, isort, Flake8 pass. mypy reports 13 pre-existing errors, all in files untouched here (factory/harvest/* from #3111, ui/sse_translation.py from #3026); src/gaia/sd/mixin.py is clean.
Eval — gaia eval agent not run. Lemonade was not listening on 13305 or 8000, and ANTHROPIC_API_KEY is set to the placeholder dummy, which the judge would pass straight to the SDK and 401 on rather than falling back to subscription auth. The author's own run hit the 900 s cap on every tool_selection scenario, so there is still no scorecard to diff against gemma-4-e4b-d71cd914. Separately, no generated PNG has been produced end to end — image output remains unproven.
9a6d41b to
56647f2
Compare
🟡 The new unit test pins the wrong CLI command — fixing the prior-review finding now also requires updating this test.
tests/unit/test_sd_error_messages.py was added in this push and explicitly asserts "lemonade-server serve" in message. The prior review flagged that lemonade-server is the removed legacy binary and that the correct fix is to call describe_start_hint() / describe_client_hint() (the helpers that already exist in mixin.py and resolve the right command per install). By hardcoding the assertion, the test now locks in the wrong behaviour — passing today, but blocking any fix of the underlying 🟡 finding.
The selection-test skip logic (session_disabled guard in _select_fresh) is a clean resolution of the prior critical finding; that part is good.
🔍 Technical details
tests/unit/test_sd_error_messages.py:741:
assert "lemonade-server serve" in message
This assertion mirrors the literal string in SDToolsMixin._describe_client_error (src/gaia/sd/mixin.py:681-684). The prior review asked for both the mixin.py error messages and the SKILL.md copy to use describe_start_hint() / describe_client_hint() instead of the hardcoded binary. If that fix lands, this assertion will need to change too — but as written it pins the wrong string and will fail once the mixin is corrected, which may cause the author to revert the fix rather than update both together.
The seed-not-recorded issue (result["seed"] = seed stores None for default calls, SKILL.md promises the seed is always reusable) is unchanged from the prior review — not a new regression, but still open.
PR #2995 removed the standalone SD agent on the grounds that image generation stayed available on ChatAgent behind ``enable_sd_tools``. Nothing turned that flag on, so asking the flagship to draw anything produced a plain "I can't" — the tools were registered nowhere a user could reach them, and no skill described how to use them. Turning the flag on is not enough by itself. Two things had to come with it: - The flagship selects tools per turn, and a tool in neither CORE nor a bundle is never surfaced. Without an ``image_gen`` bundle the three SD tools would have been registered and still invisible. - ``SDToolsMixin`` ships a ``get_sd_system_prompt`` fragment that opens with "You are an expert image generation assistant". Prompt composition auto-discovers it, so enabling SD front-loaded the flagship's prompt with that identity and grew it 40% (12,310 -> 17,281 chars) for every turn, image or not. The procedure belongs in the skill, which renders only when the turn calls for it; the composed prompt is now byte-identical to before apart from the one-line bundle menu entry. Also fixes an error message that sent users the wrong way: a read timeout carries "HTTPConnectionPool" in its text, so the old substring test reported a healthy server as unreachable. The first SD generation downloads ~6.6 GB and routinely outlasts the request window, making that the message users hit most. Tool descriptions claimed SD-Turbo was the default; ``init_sd`` has defaulted to SDXL-Turbo, which the new skill documents.
...s up The three semantic tool-selection tests failed the whole Gaia Agent job on every PR run. They score a real query against real tool descriptions, which needs live embeddings, but that job is a plain ubuntu runner with no Lemonade behind it — so `select()` returned None and the assertions blew up. They passed when written because the authoring box had Lemonade resident on 13305. Cold runner, warm dev box: the tests only ever exercised the warm one. Skip is keyed off the loader's own `session_disabled` flag rather than a bare `None`, so a genuine selection regression still fails here instead of being quietly skipped. The two `assert selected is not None` lines they replaced are now unreachable and dropped.
...D tools Adding the three SD tools moved the flagship from 67 to 70, but three docs still said 67 and the guide's tool table listed only the two vision tools — so the package would have shipped a manifest and a doc set that disagree. Fixes the count in the guide, the npm SKILL.md confirmation section, and the unreleased 0.1.1 tool-selection entry, adds the image tools to the guide's Images row, and gives the CHANGELOG the entry naming the capability. Count verified against the built registry, not arithmetic: a default GaiaAgent registers 70 tools with memory on (65 with GAIA_MEMORY_DISABLED, which drops the 5 memory tools).
... vanishing A bare `except Exception` around `init_sd` meant any failure cost the agent its image tools and left only a log line. Narrowed to `OSError`, which is the only thing that can realistically fail there — verified: the SD client makes no network call at construction (`LemonadeClient.__init__` is URL parsing and attribute assignment, 0.2ms against a dead port), so a down Lemonade is not a trigger and only the output-dir `mkdir` can raise. Anything else is a bug and now propagates rather than silently degrading the agent. The warning also names the directory and the remedy, so a permissions problem is fixable from the log instead of just reported. Two smaller items from the same review: - `SDToolsMixin` docstring now says the SD system prompt is opt-in. ChatAgent drops it and the guidance lives in the `image-gen` skill, which an author composing the mixin standalone would otherwise discover the hard way. - `_select_fresh` fails explicitly if `select()` ever returns None while the session is enabled, instead of falling through to `set(None)` and a confusing TypeError.
56647f2 to
771257c
Compare
🟡 The new _describe_client_error method still hardcodes lemonade-server pull, lemonade-server load, and lemonade-server serve — on a modern install the client binary is lemonade and the server daemon is lemond, so these commands don't exist. The prior review flagged this same issue in the old string-branching code; the refactor renamed the site but kept the wrong strings.
What makes this push worse: test_sd_error_messages.py now pins lemonade-server serve in an assertion, so fixing the underlying code will require fixing the test simultaneously. test_remedy_commands_are_runnable.py:117,138 already asserts that describe_client_hint — the helper that resolves the correct binary — never emits lemonade-server load or lemonade-server pull on a modern install. That test and the new pinned assertion are now in direct conflict.
The seed issue from the prior review (history records the requested seed, which is None for most calls, making the skill's "reuse the seed" instruction hollow) is also not addressed here, but that was flagged before and is not new to this push.
🔍 Technical details
src/gaia/sd/mixin.py:478-487 — the new _describe_client_error method:
"Pre-fetch it with `lemonade-server pull " f"{model}`, confirm it loads with `lemonade-server load " f"{model}`, then retry. ({raw})"
"Cannot reach Lemonade Server. Start it with " "`lemonade-server serve`, or set LEMONADE_BASE_URL to a running "
The helper that resolves the right binary already exists and is already imported in this file (describe_client_hint, describe_start_hint from gaia.llm.lemonade_launcher; mixin.py:34 already imports describe_client_hint and uses it at line ~600 for the non-SD path). Use them here the same way.
tests/unit/test_sd_error_messages.py:741 pins assert "lemonade-server serve" in message — this will need to be updated to assert against whatever describe_start_hint().instruction returns, the same pattern test_remedy_commands_are_runnable.py uses.
hub/skills/image-gen/SKILL.md:555-558 also names lemonade-server pull <model> directly in the failure-handling section.
Asking the flagship agent to draw something produced nothing. PR #2995 removed the standalone SD agent on the grounds that image generation stayed available on ChatAgent behind
enable_sd_tools, but nothing ever turned that flag on and no skill described the capability, so it was unreachable in practice. It now works out of the box: "draw me a picture of a red bicycle" selects the image tools, generates a PNG, and reports the path — with a starter skill covering prompt expansion, iterating on a previous image, and what to say when generation fails.Flipping the flag alone would have been a no-op. The flagship picks tools per turn, so the three SD tools also needed a bundle or they would have been registered and still invisible to the model. Enabling them also dragged in
SDToolsMixin's prompt fragment, which opens with "You are an expert image generation assistant" and grew the flagship's system prompt 40% on every turn; that guidance now lives in the skill, where it renders only when a turn calls for it.🔍 Technical details
GaiaAgentConfig.enable_sd_tools→True.ChatAgentConfigstaysFalseon purpose: the base class backs the doc/file profiles, where evicting the chat model to draw is the wrong trade. The flagship is the general-purpose surface.image_genbundle inFULL_BUNDLES(+ the three names inFULL_OPTIONAL_TOOLS, since a plain ChatAgent onfulllacks them). Without ittest_core_and_bundles_cover_the_flagship_registry_exactlyfails andload_toolshas no way back to the tools.ChatAgent._get_mixin_promptsnow drops the SD fragment unconditionally. Measured: 12,310 → 17,281 chars with it, back to 12,378 without — the +68 is the bundle's one-line menu entry.init_sdis given an absoluteoutput_dirunderGAIA_CONFIG_DIR. The mixin default is relative to cwd, which for a daemon-launched sidecar is the package directory — images would have landed in site-packages, outside the agent's ownallowed_paths._describe_client_errorsplits timeouts from refused connections. Arequestsread timeout carriesHTTPConnectionPoolin its text, so the old"connect" in msgtest told users to restart a server that was fine — the message they'd hit most, since the first generation downloads ~6.6 GB.tools_count67 → 70 in bothgaia-agent.yamlandbuild_gaia().init_sdhas always defaulted to SDXL-Turbo.Test plan
pytest tests/unit/test_starter_skills.py— 173 passed, 13 skipped; the newimage-genparametrization is picked up automatically. Required teaching theregistry_tool_namesfixture aboutSDToolsMixin(it registers viainit_sd, not aregister_*method); the assertion itself was not weakened.pytest hub/agents/gaia/python/tests/ tests/unit/test_chat_tool_bundles.py— 400 passed, 13 skipped, including both bundle drift gates.gaia skill audit ./hub/skills/image-gen/—ALLOW ✅, no findings.src/gaia tests— 0 findings; black and isort clean on every changed file.origin/mainand on this branch and diffed the composed system prompt: identical except one added line,- image_gen: Generate images from a text prompt (Stable Diffusion).lemonade-server serve..."; bad model → lists the four valid names; load timeout → names the model and thepullcommand instead of blaming the connection.image-genloaded, "draw me a picture of a red bicycle" made the model calllist_sd_models()first (the skill's opening step) and, in an earlier run, callgenerate_imagewith a fully expanded prompt.SDXL-Turbodownloads and registers fine (6.6 GB pulled), but Lemonade 10.10.0 on this machine cannot start itssd-cppbackend:POST /api/v1/loadreturns500 model_load_error: sd-server failed to start or become ready, and no sd-server binary is present in the install. That is an environment gap, not a code path this PR changes — but it means no reviewer should treat image output as proven. Please re-rungenerate_imageon a box with a working sd-cpp backend before merging.gaia eval agent --category tool_selection— NOT comparable on this machine. Every scenario hit the 900s cap (data_vs_recall_disambiguation2427s,known_path_read1034s,multi_step_plan1173s), all scoringn/a, so there is no scorecard to diff againstgemma-4-e4b-d71cd914. Cause is environmental: Gemma was resident at ctx 131072 (the baseline pins 32768) and a second agent's backend shares this Lemonade. The prompt-diff and tool-selection measurements above are the substitute evidence — they show this change contributes 68 characters and zero selected tools to every one of those five scenarios — but they are not a substitute for the eval, and it should be re-run on a clean box.