Skip to content

fix(agent): keep keyword tool fallback when tool index init times out - #7011

Open
zedleje wants to merge 1 commit into
odysseus-dev:devfrom
zedleje:fix/tool-rag-init-timeout-fallback
Open

zedleje wants to merge 1 commit into
odysseus-dev:devfrom
zedleje:fix/tool-rag-init-timeout-fallback

Conversation

@zedleje

@zedleje zedleje commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary

The tool-selection timeout has two separate expiry paths, and they handle failure differently. The retrieval-timeout path correctly leaves _relevant_tools unset so the deterministic keyword hints still run, and it carries a comment explaining why. The index-init-timeout path does not: it hard-codes _relevant_tools = set(ALWAYS_AVAILABLE), which makes the later if not _relevant_tools: guard false and skips the keyword hints entirely.

This makes the init path behave like the retrieval path. The practical effect is that a cold ToolIndex init — common on local setups, where fastembed model load plus embedding ~73 tool descriptions can exceed the budget — silently removes tools the keyword table explicitly covers, even when the request named them.

Target branch

  • This PR targets the correct integration branch: lab in the private maintainer-preview repository, or dev in the public repository. main remains release-curated.

Linked Issue

Part of #5771, refs #6709.

Related but distinct: #6709 reports the keyword fallback running and then excluding mcp__<server_id>__* tools. Here the fallback never runs at all, so even built-in tools the hint table covers are unreachable. Both are worth fixing; they are separate lines.

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)
  • New feature (non-breaking — adds new behaviour)
  • Breaking change (changes or removes existing behaviour)
  • Refactor / cleanup (behaviour unchanged)
  • Documentation only
  • CI / tooling / configuration

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets the correct integration branch (lab in maintainer-preview; dev in the public repository)
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up or uvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.
  • I did not run the app/runtime validation and stated that gap in How to Test. Leave this unchecked when the app-run box above is checked.

How to Test

  1. Docker install, lanes=['fastembed'], built-in Email MCP server connected, model gpt-oss:20b, Agent mode.
  2. Before: new chat, check my inbox for unread emails. Logs show Tool index init exceeded 1.5s; falling back to always-available tools, and the model's reasoning lists only the five always-available tools. No mcp_email_* call is made.
  3. After: same request. Logs show Tool index init exceeded ... falling back to keyword tool selection, the email tools appear in the offered schema, the normal permission gate appears, and list_email_accounts / list_emails are called successfully.

Reproduction log (before the change):

[tool-rag] Low-signal query; will run RAG retrieval
ToolIndex initialized (lanes=['fastembed'])
[tool-rag] Tool index init exceeded 1.5s; falling back to always-available tools
Indexed 73 built-in tools

Indexed 73 built-in tools lands just after the timeout — the index works, init is simply ~1.7s (ToolIndex() ~458ms + index_builtin_tools() ~1025ms) against a 1.5s budget. The model then receives only:

web_search, web_fetch, manage_memory, ask_user, update_plan

and answers "I don't have direct access to your Gmail inbox" — accurately describing the manifest it was given. ToolIndex._KEYWORD_HINTS maps {"email", "emails", "mail", "inbox", "unread"} to list_email_accounts, list_emails, search_emails, read_email, etc. It is never consulted.

  1. Also confirm bash, python, grep, glob, ls, web_search are unaffected — all are covered by ALWAYS_AVAILABLE, so none should regress.

The change

src/agent_loop.py only:

             except asyncio.TimeoutError:
                 logger.warning(
-                    "[tool-rag] Tool index init exceeded %.1fs; falling back to always-available tools",
+                    "[tool-rag] Tool index init exceeded %.1fs; falling back to keyword tool selection",
                     _TOOL_SELECTION_TIMEOUT_SECONDS,
                 )
                 tool_idx = None
-                _relevant_tools = set(ALWAYS_AVAILABLE)
+                # Leave _relevant_tools unset so the keyword fallback below still
+                # runs. Hard-coding ALWAYS_AVAILABLE here skipped the deterministic
+                # keyword hints whenever a cold index init was slow, silently
+                # stripping email/calendar tools from queries that named them
+                # outright. Matches the retrieval-timeout path below.
+                _relevant_tools = None

The warning text is updated too, since after this change the fallback really is keyword selection rather than always-available.

I deliberately did not change _TOOL_SELECTION_TIMEOUT_SECONDS here — that value and its override are covered by #5772, and this PR stays clear of it to avoid a conflict. This change is about the degradation path, not the budget: a timeout should fall back to the deterministic selector, not past it.

Checks run:

python -m py_compile src/agent_loop.py
docker compose up -d --build
docker compose logs --tail=120 odysseus

No automated test added: the selection block sits inside the request path and the existing tests/test_agent_loop.py harness mocks the surrounding imports. I can add a targeted test if you would like one and can suggest the seam to use.

Note: this makes the first turn after a cold start correct. The index still re-initialises per turn in this configuration (ToolIndex initialized repeats on later requests despite ODYSSEUS_TOOL_INDEX_PREWARM=1), which is a separate issue I have not touched here.

Visual / UI changes — REQUIRED if you touched anything that renders

N/A — no rendering, CSS, HTML, or static/js/ changes. One backend file only.

@github-actions github-actions Bot added needs work PR description incomplete — please update before review ready for review Description complete — ready for maintainer review and removed needs work PR description incomplete — please update before review labels Oct 31, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant