core: cache the tool search handler per session (#27258)

## Why

Tool router construction rebuilds the deferred-tool BM25 index during
session initialization and before each sampling continuation, even when
the searchable tool metadata is unchanged. Local profiling measured
`append_tool_search_executor` at roughly 113 ms per continuation, making
repeated index construction the largest measured router-building cost.

## What changed

- Add a session-scoped `ToolSearchHandlerCache` so continuations and
user turns can reuse the existing handler.
- Key reuse on the complete ordered `Vec<ToolSearchInfo>`, rebuilding
when searchable text, loadable tool specs, source metadata, or ordering
changes.
- Build handlers outside the cache lock and recheck before publishing
them, avoiding holding the mutex during index construction.

## Verification

- `cache_reuses_identical_search_infos_and_rebuilds_changed_inputs`
covers exact cache reuse and invalidation when the ordered search
metadata changes.
- Local rollout profiling showed the initial router build populating the
cache and unchanged later continuations reusing it:
  - uncached: 118 ms median across 14 spans from 3 rollouts
  - cached: 4 ms median across 12 spans from 3 rollouts
This commit is contained in:
mchen-oai authored and GitHub committed 2026-06-15 14:48:30 -07:00
1 parent fbbe7706d6
commit af99f6a72f
12 files changed
+169 -29

No files matched your search

+5
View File
@@ -120,6 +120,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
&Default::default(),
);
let parallel_tool_name = ["exec_command", "shell_command"]
@@ -199,6 +200,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
&Default::default(),
);
let call = ToolCall {
@@ -234,6 +236,7 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
&Default::default(),
);
assert!(!router.tool_supports_parallel(&ToolCall {
@@ -288,6 +291,7 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
extension_tool_executors: Vec::new(),
dynamic_tools: &dynamic_tools,
},
&Default::default(),
);
assert_eq!(
@@ -349,6 +353,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
extension_tool_executors: extension_tool_executors(&session),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
&Default::default(),
);
assert!(