Repository navigation
Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The tool-selection timeout has two separate expiry paths, and they handle failure differently. The retrieval-timeout path correctly leaves
_relevant_toolsunset 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 laterif 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
ToolIndexinit — common on local setups, wherefastembedmodel 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
labin the private maintainer-preview repository, ordevin the public repository.mainremains 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
Checklist
labin maintainer-preview;devin the public repository)docker compose uporuvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.How to Test
lanes=['fastembed'], built-in Email MCP server connected, modelgpt-oss:20b, Agent mode.check my inbox for unread emails. Logs showTool index init exceeded 1.5s; falling back to always-available tools, and the model's reasoning lists only the five always-available tools. Nomcp_email_*call is made.Tool index init exceeded ... falling back to keyword tool selection, the email tools appear in the offered schema, the normal permission gate appears, andlist_email_accounts/list_emailsare called successfully.Reproduction log (before the change):
Indexed 73 built-in toolslands 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:and answers "I don't have direct access to your Gmail inbox" — accurately describing the manifest it was given.
ToolIndex._KEYWORD_HINTSmaps{"email", "emails", "mail", "inbox", "unread"}tolist_email_accounts,list_emails,search_emails,read_email, etc. It is never consulted.bash,python,grep,glob,ls,web_searchare unaffected — all are covered byALWAYS_AVAILABLE, so none should regress.The change
src/agent_loop.pyonly: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 = NoneThe 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_SECONDShere — 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:
No automated test added: the selection block sits inside the request path and the existing
tests/test_agent_loop.pyharness 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 initializedrepeats on later requests despiteODYSSEUS_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.