Skip to content

feat(ui): add Beta dynamic tools toggle to Agent UI settings (#1798) - #1857

Merged
kovtcharov merged 3 commits into
amd:mainfrom
alexey-tyurin:feat/1798-dynamic-tools-toggle
Jun 25, 2026
Merged

feat(ui): add Beta dynamic tools toggle to Agent UI settings (#1798)#1857
kovtcharov merged 3 commits into
amd:mainfrom
alexey-tyurin:feat/1798-dynamic-tools-toggle

Conversation

@alexey-tyurin

Copy link
Copy Markdown
Contributor

Summary

The semantic dynamic tool loader — which cuts first-turn latency on the Doc Agent by trimming each turn's tool prompt to a semantically-matched subset — has shipped dark since #1449: the only way to switch it on was exporting GAIA_DYNAMIC_TOOLS or editing SDK config, so no Agent UI user could discover or try it. This PR adds a single Beta toggle under Settings → Dynamic Tools so users can opt in without touching env vars. It stays off by default and changes no loader behavior — it only controls whether the UI-built agent sets config.dynamic_tools=true.

Why

The feature was complete but undiscoverable from the UI — its only on-ramps were a dev/CI env var and an SDK field. GAIA_DYNAMIC_TOOLS must stay the authoritative dev/CI/eval override (eval tooling and CI depend on it), so the toggle layers on top rather than replacing it: when the env var is set it wins, and the toggle then reflects the effective value and disables itself with an explanation — never a silent no-op that lies about the running state.

Linked issue

Closes #1798

Changes

  • Settings toggle (Beta, off by default). Boolean toggle reusing the existing .toggle-switch pattern with an immediate optimistic save; on failure it reverts and surfaces a visible error (no silent fallback). When GAIA_DYNAMIC_TOOLS is set, the toggle shows the effective value, disables, and explains why.
  • Env-wins precedence centralized in one parser. Extracted dynamic_tools_env_override() so the agent resolver and the settings router parse the same truthy set — no drift between what the toggle shows and what the agent does. GET/PUT /api/settings now return dynamic_tools + dynamic_tools_locked; PUT persists the user's intent even while the env var locks the effective value, so their choice applies once it's unset.
  • Threaded through every agent construction path, not just the active one. _session_agent_kwargs carries the field to both the streaming and non-streaming Doc-agent builds (the only path where the loader is observable). The scheduled-prompt build and the autonomous agent-loop tick also read it — inert on their "full" profile today, wired and commented so they can't silently diverge if that ever changes.
  • Docs + polish (review follow-ups). Documented the toggle as a third enable path in the chat guide; added a disabled-state style to the shared toggle so a locked control looks locked; added a double-click-during-save guard test.
Deviations from the issue sketch (flagged per plan)
Issue/sketch assumed Code reality Resolution in this PR
Wire at the two _chat_helpers.py chat sites Those build the default "full" profile, where the loader is inert; the loader-active path is the doc agent built via registry.create_agent in the else branch Thread dynamic_tools through _session_agent_kwargs, which reaches the active doc path and the inert chat sites in one place
"thread through _session_agent_kwargs covers all sites" server.py's scheduled-prompt path does not call that helper server.py gets its own db.get_setting("dynamic_tools") read + an inert-on-"full" comment
Frontend should mirror custom_model (text input + Save button) The new control is boolean Used the .toggle-switch pattern (immediate save) — appropriate for a boolean, not a text+Save field
Persist "mirroring agent_mode" Settings persist as TEXT; booleans use "true"/"false" (precedent: memory_enabled) Persist "true"/"false"; read with the same string compare; missing → off
TS Settings mirrors SettingsResponse TS Settings omits agent_mode (backend-only) Added dynamic_tools + dynamic_tools_locked to the TS type only
Plan listed only server.py for the inert future-proofed read The autonomous agent_loop.py tick is a separate ChatAgentConfig build site with the same concern Also wired + commented there, so both background paths stay in lock-step

No eval run — deliberate, not skipped. The default-off path is byte-identical to today (config.dynamic_tools is False ⇒ loader None); this PR adds a UI surface + plumbing that sets an already-eval-covered config field and touches no prompt, tool registration, or selection logic. Loader behavior remains covered by #1449/#1762's committed scorecard_tool_selection.json.

Test plan

  • Backend unit — round-trip, env-lock, persist-while-locked, cold-state default-off, per-path contract-shape, and the env-override helper:
    python -m pytest tests/unit/chat/ui/test_server.py \
      tests/unit/chat/ui/test_chat_helpers_model_resolution.py \
      tests/unit/test_chat_dynamic_tools.py -xvs
  • Frontend unit — off/on, visible-label click, env-locked-disabled, error-revert, and single-PUT-on-double-click (7 cases):
    cd src/gaia/apps/webui && npx vitest run src/components/__tests__/SettingsPage.test.tsx
  • Lintpython util/lint.py --all → clean (black/isort/ruff).
  • Cold/empty state — against a fresh UI DB (no dynamic_tools key), GET /api/settings returns dynamic_tools: false, dynamic_tools_locked: false.
  • (Optional, on-hardware) gaia chat --ui → Settings → toggle Dynamic Tools on → start a Doc Agent session → confirm one TOOL_LOADER {…} INFO line in the server log (off ⇒ none). With GAIA_DYNAMIC_TOOLS=1 exported, the toggle renders on and disabled.

Checklist

  • I have linked a GitHub issue above (Closes #1798).
  • I have described why this change is being made, not just what changed.
  • I have run linting and tests locally (python util/lint.py --all, pytest tests/unit/).
  • I have updated documentation if user-visible behavior changed (docs/guides/chat.mdx — the toggle as a third enable path).

@github-actions github-actions Bot added documentation Documentation changes tests Test changes electron Electron app changes agents labels Jun 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Verdict: Approve

This adds a Beta Settings → Dynamic Tools toggle so Agent UI users can opt into the semantic tool loader that previously only GAIA_DYNAMIC_TOOLS or SDK config could reach. The design is sound: off by default, env var still wins (the toggle then shows the effective value and disables itself rather than lying), and the persisted user intent is kept underneath the lock so it applies once the env var is unset. No loader behavior changes — it only sets config.dynamic_tools.

The bottom line: the plumbing is threaded through every agent-construction path (not just the visible one), the env-precedence parse is centralized so the toggle and the agent can't disagree, and there's no silent-fallback — a failed save reverts the optimistic flip and surfaces a visible error. Backend and frontend unit coverage is genuinely thorough, including cold-state default-off and a contract-shape assertion that the toggle actually reaches the factory kwargs. The "no eval run" call is correctly justified: the default-off path is byte-identical to today and touches no prompt/tool-selection logic.

No blocking issues found.

🔍 Technical details

Strengths

  • Env-precedence centralized, not duplicateddynamic_tools_env_override() (agent.py:49) is the single truthy-set parser shared by ChatAgent._resolve_dynamic_tools_enabled and the router's _resolve_dynamic_tools_setting (routers/system.py:826), so the toggle's displayed state and the agent's actual behavior can't drift. The function-local import in the router keeps ui/ → agents/chat/ a lazy downward dep — correct.
  • No silent fallback, both layers — frontend reverts the optimistic flip and shows a role="alert" on save failure (SettingsPage.tsx:134); backend persists intent while locked rather than no-opping (routers/system.py:929). Matches the CLAUDE.md fail-loudly rule.
  • Contract-shape tests, not just invocationtest_non_streaming_forwards_persisted_true asserts create_agent actually receives dynamic_tools=True, and test_cold_state_defaults_to_off exercises the empty-DB path. This is exactly the "mocks prove the call is valid, not just invoked" guard the repo asks for. Frontend covers env-lock-disabled, error-revert, and the double-click single-PUT guard.
  • Persist-while-locked is correct and testedtest_put_persists_intent_even_while_locked proves the user's False intent survives an env lock and re-applies once GAIA_DYNAMIC_TOOLS is unset.

🟢 Minor (optional)

  • The literal db.get_setting("dynamic_tools", "false") == "true" is repeated across four sites (_chat_helpers.py:934 & :1527, server.py:374, agent_loop.py:358). This matches the existing per-setting one-liner pattern so it's not wrong, but a tiny shared _persisted_dynamic_tools(db) -> bool helper would remove the stringly-typed == "true" from four places if you touch this again. Not worth blocking.

@alexey-tyurin

Copy link
Copy Markdown
Contributor Author

Both failing checks are CI infrastructure, not this PR — they died at Lemonade server startup, before any GAIA test ran:

  • Test Agent SDK on Windows (Lemonade Integration) → failed at step Start Lemonade Server for Integration Tests
  • Test GAIA CLI on Windows (Full Integration) → failed at step Start Lemonade Server and Run Tests

Both are Windows integration jobs that gate on a live Lemonade server; the server didn't come up on the runner, so no code under test executed. This PR only changes the Agent UI dynamic-tools toggle, the settings router/plumbing, docs, and unit tests — it touches nothing in the Lemonade backend or LLM-client path. The backend (pytest tests/unit/chat/ui/...) and frontend (vitest SettingsPage) unit suites pass locally.

A re-run should clear these once Lemonade starts on the runner. @kovtcharov-amd — could you kick a re-run if the flake sticks?

@kovtcharov
kovtcharov added this pull request to the merge queue Jun 25, 2026
Merged via the queue into amd:main with commit 88b9328 Jun 25, 2026
30 of 32 checks passed
@itomek itomek mentioned this pull request Jun 30, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents documentation Documentation changes electron Electron app changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ui): expose dynamic tool loader as a Beta toggle in Agent UI settings

2 participants