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

+17
View File
@@ -3398,6 +3398,8 @@ impl Session {
let tool_router = turn::built_tools(
self.as_ref(),
turn_context.as_ref(),
// TODO(CDXENT-441): use the step scoped model
turn_context.model_info(),
&environments,
&mcp,
&extension_data,
@@ -3405,6 +3407,21 @@ impl Session {
)
.or_cancel(cancellation_token)
.await??;
// Publish inventory after planning rather than during finalization, so constructing
// additional candidate plans cannot overwrite turn-wide metadata.
if turn_context
.config
.tool_registry
.turn_metadata_includes_tool_info
&& turn_context.model_info().use_responses_lite
{
turn_context.turn_metadata_state.set_tool_namespaces_info(
tool_router
.tool_namespaces_info()
.cloned()
.unwrap_or_default(),
);
}
Ok(Arc::new(StepContext {
settings,
token_budget,
+9
View File
@@ -70,6 +70,7 @@ use codex_protocol::models::ImageDetail;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -257,6 +258,10 @@ impl StepContext {
tool_router: Arc::new(ToolRouter::from_parts(
ToolRegistry::empty_for_test(),
Vec::new(),
ToolMode::Direct,
BTreeMap::new(),
/*tool_namespaces_info*/ None,
&[],
)),
loaded_agents_md: None,
})
@@ -877,6 +882,7 @@ pub(crate) fn tool_registry_for_test_step(
) -> (ToolRegistry, Vec<ToolSpec>) {
let mut registry = crate::tools::spec_plan::build_core_tool_registry(
step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments,
step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None,
@@ -884,6 +890,7 @@ pub(crate) fn tool_registry_for_test_step(
);
let hosted_specs = crate::tools::spec_plan::append_source_tools(
step_context.turn.as_ref(),
step_context.turn.model_info(),
&mut registry,
Vec::new(),
Vec::new(),
@@ -897,6 +904,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
let (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref());
let router = Arc::new(ToolRouter::from_registry(
step_context.turn.as_ref(),
step_context.turn.model_info(),
registry,
hosted_specs,
&Default::default(),
@@ -11612,6 +11620,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
let (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref());
let router = ToolRouter::from_registry(
step_context.turn.as_ref(),
step_context.turn.model_info(),
registry,
hosted_specs,
&Default::default(),
+3 -1
View File
@@ -1509,13 +1509,14 @@ pub(crate) async fn prepare_tool_recommendations(
skip_all,
fields(
turn_id = %turn_context.sub_id,
model = %turn_context.model_info().slug,
model = %model_info.slug,
apps_enabled = turn_context.apps_enabled()
)
)]
pub(crate) async fn built_tools(
sess: &Session,
turn_context: &TurnContext,
model_info: &codex_protocol::openai_models::ModelInfo,
environments: &TurnEnvironmentSnapshot,
mcp: &Arc<codex_mcp::McpBinding>,
step_store: &ExtensionData,
@@ -1584,6 +1585,7 @@ pub(crate) async fn built_tools(
Ok(Arc::new(build_tool_router(
sess,
turn_context,
model_info,
environments,
mcp,
apps_enabled,
+2 -2
View File
@@ -1046,14 +1046,14 @@ impl Session {
if !tc.code_mode_available
&& matches!(
crate::tools::requested_tool_mode(tc),
crate::tools::requested_tool_mode(tc, tc.model_info()),
codex_protocol::openai_models::ToolMode::CodeMode
| codex_protocol::openai_models::ToolMode::CodeModeOnly
)
&& let Some(message) = self
.services
.code_mode_service
.take_unavailable_warning(crate::tools::effective_tool_mode(tc))
.take_unavailable_warning(crate::tools::effective_tool_mode(tc, tc.model_info()))
{
self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
.await;