Finalize model-specific tool plans in ToolRouter (#41195)

## Why

Planning tools for a candidate model must not change the selected model or its
Responses Lite tool inventory. Otherwise, preparing a fallback can overwrite
metadata before the current model's request is sent.

## What changed

- Build tool registries and visible specs from an explicit `ModelInfo`.
- Store the finalized tool mode, code-mode names, namespace inventory, and
  terminal and child-management capabilities on `ToolRouter`.
- Publish namespace inventory only after selecting the step's router, and use
  that router's mode when dispatching and recording tool calls.

## Testing

Added coverage for candidate-model isolation, fallback compaction metadata,
code-mode worker selection, normalized tool names, and router capabilities.

GitOrigin-RevId: 56df2de8f42c7e93a4c4dcd117720e40a0cca3db
This commit is contained in:
Abhinav authored and copyberry committed 2026-08-27 20:01:39 +00:00
1 parent 8aea62b2d8
commit 0182ff3480
13 files changed
+532 -127

No files matched your search

+39 -3
View File
@@ -34,7 +34,6 @@ use crate::tools::ExecutedToolCallRecorder;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload;
use crate::tools::effective_tool_mode;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource;
@@ -205,8 +204,7 @@ impl CodeModeService {
tracker: SharedTurnDiffTracker,
) -> Option<CodeModeDispatchWorker> {
let turn = &step_context.turn;
let tool_mode = effective_tool_mode(turn);
if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) {
if !step_context.tool_router.requires_code_mode_worker() {
return None;
}
@@ -442,14 +440,52 @@ fn build_freeform_tool_payload(
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::sync::Arc;
use super::build_nested_tool_payload;
use super::truncate_code_mode_result;
use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolRegistry;
use crate::tools::router::ToolRouter;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_code_mode::CodeModeToolKind;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::openai_models::ToolMode;
use codex_tools::ToolName;
use serde_json::json;
#[tokio::test]
async fn turn_worker_uses_step_router_mode_instead_of_admitted_turn() {
let (session, turn) = make_session_and_context().await;
assert_eq!(
crate::tools::effective_tool_mode(&turn, turn.model_info()),
ToolMode::Direct
);
let session = Arc::new(session);
let step_context = StepContext::for_test(Arc::new(turn));
let router = Arc::new(ToolRouter::from_parts(
ToolRegistry::empty_for_test(),
Vec::new(),
ToolMode::CodeModeOnly,
BTreeMap::new(),
/*tool_namespaces_info*/ None,
&[],
));
let step_context = step_context.with_tool_router_for_test(router);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let worker =
session
.services
.code_mode_service
.start_turn_worker(&session, step_context, tracker);
assert!(worker.is_some());
}
#[test]
fn build_nested_tool_payload_uses_function_kind() {
let payload = build_nested_tool_payload(