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( let tool_router = turn::built_tools(
self.as_ref(), self.as_ref(),
turn_context.as_ref(), turn_context.as_ref(),
// TODO(CDXENT-441): use the step scoped model
turn_context.model_info(),
&environments, &environments,
&mcp, &mcp,
&extension_data, &extension_data,
@@ -3405,6 +3407,21 @@ impl Session {
) )
.or_cancel(cancellation_token) .or_cancel(cancellation_token)
.await??; .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 { Ok(Arc::new(StepContext {
settings, settings,
token_budget, token_budget,
+9
View File
@@ -70,6 +70,7 @@ use codex_protocol::models::ImageDetail;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::SandboxEnforcement;
use codex_protocol::openai_models::ModelServiceTier; use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ToolMode;
use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxEntry;
@@ -257,6 +258,10 @@ impl StepContext {
tool_router: Arc::new(ToolRouter::from_parts( tool_router: Arc::new(ToolRouter::from_parts(
ToolRegistry::empty_for_test(), ToolRegistry::empty_for_test(),
Vec::new(), Vec::new(),
ToolMode::Direct,
BTreeMap::new(),
/*tool_namespaces_info*/ None,
&[],
)), )),
loaded_agents_md: None, loaded_agents_md: None,
}) })
@@ -877,6 +882,7 @@ pub(crate) fn tool_registry_for_test_step(
) -> (ToolRegistry, Vec<ToolSpec>) { ) -> (ToolRegistry, Vec<ToolSpec>) {
let mut registry = crate::tools::spec_plan::build_core_tool_registry( let mut registry = crate::tools::spec_plan::build_core_tool_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
step_context.mcp.as_ref(), step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*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( let hosted_specs = crate::tools::spec_plan::append_source_tools(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&mut registry, &mut registry,
Vec::new(), Vec::new(),
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 (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref());
let router = Arc::new(ToolRouter::from_registry( let router = Arc::new(ToolRouter::from_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &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 (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref());
let router = ToolRouter::from_registry( let router = ToolRouter::from_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
+3 -1
View File
@@ -1509,13 +1509,14 @@ pub(crate) async fn prepare_tool_recommendations(
skip_all, skip_all,
fields( fields(
turn_id = %turn_context.sub_id, turn_id = %turn_context.sub_id,
model = %turn_context.model_info().slug, model = %model_info.slug,
apps_enabled = turn_context.apps_enabled() apps_enabled = turn_context.apps_enabled()
) )
)] )]
pub(crate) async fn built_tools( pub(crate) async fn built_tools(
sess: &Session, sess: &Session,
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &codex_protocol::openai_models::ModelInfo,
environments: &TurnEnvironmentSnapshot, environments: &TurnEnvironmentSnapshot,
mcp: &Arc<codex_mcp::McpBinding>, mcp: &Arc<codex_mcp::McpBinding>,
step_store: &ExtensionData, step_store: &ExtensionData,
@@ -1584,6 +1585,7 @@ pub(crate) async fn built_tools(
Ok(Arc::new(build_tool_router( Ok(Arc::new(build_tool_router(
sess, sess,
turn_context, turn_context,
model_info,
environments, environments,
mcp, mcp,
apps_enabled, apps_enabled,
+2 -2
View File
@@ -1046,14 +1046,14 @@ impl Session {
if !tc.code_mode_available if !tc.code_mode_available
&& matches!( && 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::CodeMode
| codex_protocol::openai_models::ToolMode::CodeModeOnly | codex_protocol::openai_models::ToolMode::CodeModeOnly
) )
&& let Some(message) = self && let Some(message) = self
.services .services
.code_mode_service .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 })) self.send_event(tc, EventMsg::Warning(WarningEvent { message }))
.await; .await;
@@ -291,6 +291,7 @@ async fn handle_output_item_done_returns_contributed_last_agent_message() {
let (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref()); let (registry, hosted_specs) = tool_registry_for_test_step(step_context.as_ref());
let router = Arc::new(ToolRouter::from_registry( let router = Arc::new(ToolRouter::from_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
+39 -3
View File
@@ -34,7 +34,6 @@ use crate::tools::ExecutedToolCallRecorder;
use crate::tools::context::FunctionToolOutput; use crate::tools::context::FunctionToolOutput;
use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload; use crate::tools::context::ToolPayload;
use crate::tools::effective_tool_mode;
use crate::tools::parallel::ToolCallRuntime; use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolCall; use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource; use crate::tools::router::ToolCallSource;
@@ -205,8 +204,7 @@ impl CodeModeService {
tracker: SharedTurnDiffTracker, tracker: SharedTurnDiffTracker,
) -> Option<CodeModeDispatchWorker> { ) -> Option<CodeModeDispatchWorker> {
let turn = &step_context.turn; let turn = &step_context.turn;
let tool_mode = effective_tool_mode(turn); if !step_context.tool_router.requires_code_mode_worker() {
if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) {
return None; return None;
} }
@@ -442,14 +440,52 @@ fn build_freeform_tool_payload(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::BTreeMap;
use std::sync::Arc;
use super::build_nested_tool_payload; use super::build_nested_tool_payload;
use super::truncate_code_mode_result; 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::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_code_mode::CodeModeToolKind;
use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::openai_models::ToolMode;
use codex_tools::ToolName; use codex_tools::ToolName;
use serde_json::json; 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] #[test]
fn build_nested_tool_payload_uses_function_kind() { fn build_nested_tool_payload_uses_function_kind() {
let payload = build_nested_tool_payload( let payload = build_nested_tool_payload(
+5 -4
View File
@@ -25,6 +25,7 @@ use crate::session::turn_context::TurnContext;
pub(crate) use approvals::ApprovalContext; pub(crate) use approvals::ApprovalContext;
use codex_features::Feature; use codex_features::Feature;
use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::ToolMode;
use codex_tools::ToolName; use codex_tools::ToolName;
use codex_utils_output_truncation::TruncationPolicy; use codex_utils_output_truncation::TruncationPolicy;
@@ -64,8 +65,8 @@ pub(crate) fn tool_user_shell_type(
} }
} }
pub(crate) fn requested_tool_mode(turn_context: &TurnContext) -> ToolMode { pub(crate) fn requested_tool_mode(turn_context: &TurnContext, model_info: &ModelInfo) -> ToolMode {
turn_context.model_info().tool_mode.unwrap_or_else(|| { model_info.tool_mode.unwrap_or_else(|| {
if turn_context.config.features.enabled(Feature::CodeModeOnly) { if turn_context.config.features.enabled(Feature::CodeModeOnly) {
ToolMode::CodeModeOnly ToolMode::CodeModeOnly
} else if turn_context.config.features.enabled(Feature::CodeMode) { } else if turn_context.config.features.enabled(Feature::CodeMode) {
@@ -76,8 +77,8 @@ pub(crate) fn requested_tool_mode(turn_context: &TurnContext) -> ToolMode {
}) })
} }
pub(crate) fn effective_tool_mode(turn_context: &TurnContext) -> ToolMode { pub(crate) fn effective_tool_mode(turn_context: &TurnContext, model_info: &ModelInfo) -> ToolMode {
let requested_tool_mode = requested_tool_mode(turn_context); let requested_tool_mode = requested_tool_mode(turn_context, model_info);
if !turn_context.code_mode_available if !turn_context.code_mode_available
&& requested_tool_mode == ToolMode::CodeMode && requested_tool_mode == ToolMode::CodeMode
&& !turn_context.config.code_mode.disable_in_process_fallback && !turn_context.config.code_mode.disable_in_process_fallback
+11 -1
View File
@@ -106,7 +106,7 @@ impl ToolCallRuntime {
executed_tool_calls.record_tool_call( executed_tool_calls.record_tool_call(
&call, &call,
&source, &source,
super::effective_tool_mode(&self.step_context.turn), self.step_context.tool_router.tool_mode(),
); );
} }
let router = &self.step_context.tool_router; let router = &self.step_context.tool_router;
@@ -349,6 +349,7 @@ impl Drop for ToolCallTimingGuard {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::collections::BTreeMap;
use std::time::Duration; use std::time::Duration;
use crate::session::step_context::StepContext; use crate::session::step_context::StepContext;
@@ -362,6 +363,7 @@ mod tests {
use codex_extension_api::ToolCallOutcome; use codex_extension_api::ToolCallOutcome;
use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::openai_models::ToolMode;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use tokio::sync::Notify; use tokio::sync::Notify;
use tokio::sync::oneshot; use tokio::sync::oneshot;
@@ -425,6 +427,10 @@ mod tests {
let router = Arc::new(ToolRouter::from_parts( let router = Arc::new(ToolRouter::from_parts(
ToolRegistry::from_tools([handler]), ToolRegistry::from_tools([handler]),
Vec::new(), Vec::new(),
ToolMode::Direct,
BTreeMap::new(),
/*tool_namespaces_info*/ None,
&[],
)); ));
let step_context = step_context.with_tool_router_for_test(router); let step_context = step_context.with_tool_router_for_test(router);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
@@ -610,6 +616,10 @@ mod tests {
let router = Arc::new(ToolRouter::from_parts( let router = Arc::new(ToolRouter::from_parts(
ToolRegistry::from_tools([handler]), ToolRegistry::from_tools([handler]),
Vec::new(), Vec::new(),
ToolMode::Direct,
BTreeMap::new(),
/*tool_namespaces_info*/ None,
&[],
)); ));
let step_context = step_context.with_tool_router_for_test(router); let step_context = step_context.with_tool_router_for_test(router);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
+101 -3
View File
@@ -1,4 +1,5 @@
use crate::function_tool::FunctionCallError; use crate::function_tool::FunctionCallError;
use crate::responses_metadata::TurnToolNamespacesInfo;
use crate::session::session::Session; use crate::session::session::Session;
use crate::session::step_context::StepContext; use crate::session::step_context::StepContext;
#[cfg(test)] #[cfg(test)]
@@ -16,7 +17,11 @@ use crate::tools::registry::ToolRegistry;
use crate::tools::spec_plan::finalize_tool_router; use crate::tools::spec_plan::finalize_tool_router;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_protocol::models::SearchToolCallParams; use codex_protocol::models::SearchToolCallParams;
#[cfg(test)]
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode;
use codex_tools::DiscoverableTool; use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName; use codex_tools::ToolName;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use std::borrow::Cow; use std::borrow::Cow;
@@ -65,9 +70,14 @@ pub(crate) fn tool_log_payload<'a>(
payload.log_payload() payload.log_payload()
} }
/// One finalized tool plan: its advertised surfaces and matching executable runtimes.
pub struct ToolRouter { pub struct ToolRouter {
registry: ToolRegistry, registry: ToolRegistry,
model_visible_specs: Arc<[ToolSpec]>, model_visible_specs: Arc<[ToolSpec]>,
tool_mode: ToolMode,
code_mode_tool_names: BTreeMap<String, ToolName>,
tool_namespaces_info: Option<TurnToolNamespacesInfo>,
can_manage_children: bool,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -86,12 +96,14 @@ impl ToolRouter {
#[cfg(test)] #[cfg(test)]
pub(crate) fn from_registry( pub(crate) fn from_registry(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
registry: ToolRegistry, registry: ToolRegistry,
hosted_specs: Vec<ToolSpec>, hosted_specs: Vec<ToolSpec>,
tool_search_handler_cache: &ToolSearchHandlerCache, tool_search_handler_cache: &ToolSearchHandlerCache,
) -> Self { ) -> Self {
finalize_tool_router( finalize_tool_router(
turn_context, turn_context,
model_info,
registry, registry,
hosted_specs, hosted_specs,
tool_search_handler_cache, tool_search_handler_cache,
@@ -99,17 +111,103 @@ impl ToolRouter {
.expect("test tool registry should not contain duplicate tools") .expect("test tool registry should not contain duplicate tools")
} }
pub(crate) fn from_parts(registry: ToolRegistry, model_visible_specs: Vec<ToolSpec>) -> Self { pub(crate) fn from_parts(
Self { registry: ToolRegistry,
model_visible_specs: Vec<ToolSpec>,
tool_mode: ToolMode,
code_mode_tool_names: BTreeMap<String, ToolName>,
tool_namespaces_info: Option<TurnToolNamespacesInfo>,
child_management_tools: &[ToolName],
) -> Self {
let mut router = Self {
registry, registry,
model_visible_specs: model_visible_specs.into(), model_visible_specs: model_visible_specs.into(),
} tool_mode,
code_mode_tool_names,
tool_namespaces_info,
can_manage_children: false,
};
router.can_manage_children = !child_management_tools.is_empty()
&& child_management_tools
.iter()
.all(|name| router.exposes_tool(name));
router
} }
pub(crate) fn model_visible_specs(&self) -> Arc<[ToolSpec]> { pub(crate) fn model_visible_specs(&self) -> Arc<[ToolSpec]> {
Arc::clone(&self.model_visible_specs) Arc::clone(&self.model_visible_specs)
} }
pub(crate) fn tool_mode(&self) -> ToolMode {
self.tool_mode
}
/// Code Mode still needs its dispatcher when the nested tool set is empty.
pub(crate) fn requires_code_mode_worker(&self) -> bool {
matches!(self.tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
}
/// The normalized nested identities chosen after exclusions and collisions.
// Consumed by the follow-up cell-origin migration.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn code_mode_tool_names(&self) -> &BTreeMap<String, ToolName> {
&self.code_mode_tool_names
}
/// Optional request inventory for this exact plan, without publishing it to turn state.
pub(crate) fn tool_namespaces_info(&self) -> Option<&TurnToolNamespacesInfo> {
self.tool_namespaces_info.as_ref()
}
/// Whether the model can both start and interact with a terminal process.
// Consumed by the follow-up live tool-plan selection.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn has_terminal_controls(&self) -> bool {
self.exposes_tool(&ToolName::plain("exec_command"))
&& self.exposes_tool(&ToolName::plain("write_stdin"))
}
/// Whether the configured collaboration backend's child-management tools remain exposed.
// Consumed by the follow-up live tool-plan selection.
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn can_manage_children(&self) -> bool {
self.can_manage_children
}
// Answers if the tool plan lets the model invoke the tool directly, through code mode, or deferred tool search.
fn exposes_tool(&self, name: &ToolName) -> bool {
let name = name.clone().with_default_namespace();
if self
.code_mode_tool_names
.values()
.any(|nested| nested.clone().with_default_namespace() == name)
|| self.model_visible_specs.iter().any(|spec| match spec {
ToolSpec::Function(_) | ToolSpec::Freeform(_) => {
name.is_default_namespace() && spec.name() == name.name
}
ToolSpec::Namespace(namespace) => {
name.namespace.as_deref() == Some(namespace.name.as_str())
&& namespace.tools.iter().any(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) => tool.name == name.name,
ResponsesApiNamespaceTool::Custom(tool) => tool.name == name.name,
})
}
ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => false,
})
{
return true;
}
self.model_visible_specs
.iter()
.any(|spec| matches!(spec, ToolSpec::ToolSearch { .. }))
&& self.registry.entries().any(|tool| {
tool.exposure.is_deferred()
&& tool.runtime.tool_name().with_default_namespace() == name
&& (tool.runtime.immutable_spec().is_some()
|| tool.runtime.search_info().is_some())
})
}
pub(crate) fn deferred_tool_namespaces(&self) -> BTreeMap<String, String> { pub(crate) fn deferred_tool_namespaces(&self) -> BTreeMap<String, String> {
self.registry.deferred_tool_namespaces() self.registry.deferred_tool_namespaces()
} }
+3
View File
@@ -140,6 +140,7 @@ fn test_tool_router(
) -> ToolRouter { ) -> ToolRouter {
let mut registry = build_core_tool_registry( let mut registry = build_core_tool_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
step_context.mcp.as_ref(), step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
@@ -147,6 +148,7 @@ fn test_tool_router(
); );
let hosted_specs = append_source_tools( let hosted_specs = append_source_tools(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&mut registry, &mut registry,
mcp_tools, mcp_tools,
extension_tool_executors, extension_tool_executors,
@@ -154,6 +156,7 @@ fn test_tool_router(
); );
ToolRouter::from_registry( ToolRouter::from_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
+143 -74
View File
@@ -40,6 +40,7 @@ use crate::tools::handlers::multi_agents::WaitAgentHandler;
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS; use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions; use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2; use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
@@ -72,6 +73,7 @@ use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::ToolMode;
use codex_protocol::protocol::MultiAgentVersion; use codex_protocol::protocol::MultiAgentVersion;
use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespace;
@@ -105,6 +107,7 @@ const IMAGEGEN_TOOL_NAME: &str = "imagegen";
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct CoreToolPlanContext<'a> { struct CoreToolPlanContext<'a> {
turn_context: &'a TurnContext, turn_context: &'a TurnContext,
model_info: &'a ModelInfo,
environments: &'a TurnEnvironmentSnapshot, environments: &'a TurnEnvironmentSnapshot,
mcp: &'a codex_mcp::McpBinding, mcp: &'a codex_mcp::McpBinding,
tool_suggest_candidates: Option<&'a crate::tools::router::ToolSuggestCandidates>, tool_suggest_candidates: Option<&'a crate::tools::router::ToolSuggestCandidates>,
@@ -114,9 +117,11 @@ struct CoreToolPlanContext<'a> {
} }
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_tool_router( pub(crate) fn build_tool_router(
session: &Session, session: &Session,
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
environments: &TurnEnvironmentSnapshot, environments: &TurnEnvironmentSnapshot,
mcp: &Arc<codex_mcp::McpBinding>, mcp: &Arc<codex_mcp::McpBinding>,
apps_enabled: bool, apps_enabled: bool,
@@ -131,6 +136,7 @@ pub(crate) fn build_tool_router(
.get::<crate::WaitForEnvironmentToolConfig>(); .get::<crate::WaitForEnvironmentToolConfig>();
let context = CoreToolPlanContext { let context = CoreToolPlanContext {
turn_context, turn_context,
model_info,
environments, environments,
mcp, mcp,
tool_suggest_candidates, tool_suggest_candidates,
@@ -149,6 +155,7 @@ pub(crate) fn build_tool_router(
{ {
append_extension_tool_executors( append_extension_tool_executors(
turn_context, turn_context,
model_info,
history_tools.0.iter().cloned(), history_tools.0.iter().cloned(),
&mut registry, &mut registry,
); );
@@ -160,21 +167,33 @@ pub(crate) fn build_tool_router(
&turn_context.config, &turn_context.config,
apps_enabled, apps_enabled,
&mcp.config().mcp_server_catalog, &mcp.config().mcp_server_catalog,
search_tool_enabled(turn_context), search_tool_enabled(turn_context, model_info),
&mut registry,
);
apply_mcp_tool_exposure_policy(
turn_context,
model_info,
mcp,
&registered_mcp_tools,
&mut registry, &mut registry,
); );
apply_mcp_tool_exposure_policy(turn_context, mcp, &registered_mcp_tools, &mut registry);
let standalone_web_search_tool = append_extension_tool_executors( let standalone_web_search_tool = append_extension_tool_executors(
turn_context, turn_context,
model_info,
extension_tool_executors(session, step_store), extension_tool_executors(session, step_store),
&mut registry, &mut registry,
); );
append_dynamic_tool_runtimes(&turn_context.dynamic_tools, &mut registry); append_dynamic_tool_runtimes(&turn_context.dynamic_tools, &mut registry);
hosted_model_tool_specs(turn_context, standalone_web_search_tool.as_slice()) hosted_model_tool_specs(
turn_context,
model_info,
standalone_web_search_tool.as_slice(),
)
}; };
finalize_tool_router( finalize_tool_router(
turn_context, turn_context,
model_info,
registry, registry,
hosted_specs, hosted_specs,
&session.services.tool_search_handler_cache, &session.services.tool_search_handler_cache,
@@ -183,6 +202,7 @@ pub(crate) fn build_tool_router(
fn apply_mcp_tool_exposure_policy( fn apply_mcp_tool_exposure_policy(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
mcp: &codex_mcp::McpBinding, mcp: &codex_mcp::McpBinding,
registered_mcp_tools: &HashSet<ToolName>, registered_mcp_tools: &HashSet<ToolName>,
registry: &mut ToolRegistry, registry: &mut ToolRegistry,
@@ -228,9 +248,9 @@ fn apply_mcp_tool_exposure_policy(
exposures = exposures.difference(ToolExposures::DEFERRED | ToolExposures::CODE_MODE); exposures = exposures.difference(ToolExposures::DEFERRED | ToolExposures::CODE_MODE);
} }
exposures = if search_tool_enabled(turn_context) exposures = if search_tool_enabled(turn_context, model_info)
&& exposures.contains(ToolExposures::DEFERRED) && exposures.contains(ToolExposures::DEFERRED)
&& (effective_tool_mode(turn_context) != ToolMode::CodeModeOnly && (effective_tool_mode(turn_context, model_info) != ToolMode::CodeModeOnly
|| exposures.contains(ToolExposures::CODE_MODE)) || exposures.contains(ToolExposures::CODE_MODE))
{ {
exposures.difference(ToolExposures::DIRECT) exposures.difference(ToolExposures::DIRECT)
@@ -258,6 +278,7 @@ fn apply_mcp_tool_exposure_policy(
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
pub(crate) fn build_core_tool_registry( pub(crate) fn build_core_tool_registry(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
environments: &TurnEnvironmentSnapshot, environments: &TurnEnvironmentSnapshot,
mcp: &codex_mcp::McpBinding, mcp: &codex_mcp::McpBinding,
tool_suggest_candidates: Option<&crate::tools::router::ToolSuggestCandidates>, tool_suggest_candidates: Option<&crate::tools::router::ToolSuggestCandidates>,
@@ -267,6 +288,7 @@ pub(crate) fn build_core_tool_registry(
crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new()); crate::agent::role::spawn_tool_spec::build(&std::collections::BTreeMap::new());
let context = CoreToolPlanContext { let context = CoreToolPlanContext {
turn_context, turn_context,
model_info,
environments, environments,
mcp, mcp,
tool_suggest_candidates, tool_suggest_candidates,
@@ -283,6 +305,7 @@ pub(crate) fn build_core_tool_registry(
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
pub(crate) fn append_source_tools( pub(crate) fn append_source_tools(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &mut ToolRegistry, registry: &mut ToolRegistry,
mcp_tools: Vec<RegisteredTool>, mcp_tools: Vec<RegisteredTool>,
extension_tool_executors: impl IntoIterator< extension_tool_executors: impl IntoIterator<
@@ -297,10 +320,18 @@ pub(crate) fn append_source_tools(
for tool in mcp_tools { for tool in mcp_tools {
registry.register_external_with_exposure(tool.runtime, tool.exposure); registry.register_external_with_exposure(tool.runtime, tool.exposure);
} }
let standalone_web_search_tool = let standalone_web_search_tool = append_extension_tool_executors(
append_extension_tool_executors(turn_context, extension_tool_executors, registry); turn_context,
model_info,
extension_tool_executors,
registry,
);
append_dynamic_tool_runtimes(dynamic_tools, registry); append_dynamic_tool_runtimes(dynamic_tools, registry);
hosted_model_tool_specs(turn_context, standalone_web_search_tool.as_slice()) hosted_model_tool_specs(
turn_context,
model_info,
standalone_web_search_tool.as_slice(),
)
} }
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
@@ -325,15 +356,14 @@ pub(crate) fn extension_tool_executors<'a>(
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
pub(crate) fn finalize_tool_router( pub(crate) fn finalize_tool_router(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
mut registry: ToolRegistry, mut registry: ToolRegistry,
hosted_specs: Vec<ToolSpec>, hosted_specs: Vec<ToolSpec>,
tool_search_handler_cache: &ToolSearchHandlerCache, tool_search_handler_cache: &ToolSearchHandlerCache,
) -> CodexResult<ToolRouter> { ) -> CodexResult<ToolRouter> {
apply_direct_model_only_namespace_overrides(turn_context, &mut registry); apply_direct_model_only_namespace_overrides(turn_context, &mut registry);
let code_mode_enabled = matches!( let tool_mode = effective_tool_mode(turn_context, model_info);
effective_tool_mode(turn_context), let code_mode_enabled = matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly);
ToolMode::CodeMode | ToolMode::CodeModeOnly
);
if code_mode_enabled { if code_mode_enabled {
for tool_name in [ for tool_name in [
ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME), ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME),
@@ -345,7 +375,7 @@ pub(crate) fn finalize_tool_router(
} }
} }
let tool_search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME); let tool_search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
if search_tool_enabled(turn_context) if search_tool_enabled(turn_context, model_info)
&& registry.entries().any(|tool| { && registry.entries().any(|tool| {
tool.runtime.tool_name() != tool_search_name tool.runtime.tool_name() != tool_search_name
&& tool.exposure.is_deferred() && tool.exposure.is_deferred()
@@ -381,12 +411,13 @@ pub(crate) fn finalize_tool_router(
append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache); append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache);
} }
let code_mode_tool_names = register_code_mode_executors(turn_context, &mut registry); let code_mode_tool_names =
register_code_mode_executors(turn_context, model_info, &mut registry);
let include_tool_namespaces_info = turn_context let include_tool_namespaces_info = turn_context
.config .config
.tool_registry .tool_registry
.turn_metadata_includes_tool_info .turn_metadata_includes_tool_info
&& turn_context.model_info().use_responses_lite; && model_info.use_responses_lite;
if turn_context.config.tool_registry.error_on_tool_collisions { if turn_context.config.tool_registry.error_on_tool_collisions {
if let Some(tool_name) = registry.first_collision() { if let Some(tool_name) = registry.first_collision() {
@@ -444,19 +475,28 @@ pub(crate) fn finalize_tool_router(
} }
} }
let model_visible_specs = let model_visible_specs = build_model_visible_specs(
build_model_visible_specs(turn_context, &registry, &code_mode_tool_names, hosted_specs); turn_context,
if include_tool_namespaces_info { model_info,
turn_context &registry,
.turn_metadata_state &code_mode_tool_names,
.set_tool_namespaces_info(collect_tool_namespaces_info( hosted_specs,
&registry, );
&code_mode_tool_names, let tool_namespaces_info = include_tool_namespaces_info
&model_visible_specs, .then(|| {
)); collect_tool_namespaces_info(&registry, &code_mode_tool_names, &model_visible_specs)
} })
.filter(|info| !info.is_empty());
let child_management_tools = required_child_management_tool_names(turn_context, model_info);
Ok(ToolRouter::from_parts(registry, model_visible_specs)) Ok(ToolRouter::from_parts(
registry,
model_visible_specs,
tool_mode,
code_mode_tool_names,
tool_namespaces_info,
&child_management_tools,
))
} }
fn apply_direct_model_only_namespace_overrides( fn apply_direct_model_only_namespace_overrides(
@@ -495,6 +535,7 @@ fn apply_direct_model_only_namespace_overrides(
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
fn build_model_visible_specs( fn build_model_visible_specs(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &ToolRegistry, registry: &ToolRegistry,
code_mode_tool_names: &BTreeMap<String, ToolName>, code_mode_tool_names: &BTreeMap<String, ToolName>,
hosted_specs: Vec<ToolSpec>, hosted_specs: Vec<ToolSpec>,
@@ -507,13 +548,14 @@ fn build_model_visible_specs(
} }
let tool_name = tool.runtime.tool_name(); let tool_name = tool.runtime.tool_name();
if is_hidden_by_code_mode_only(turn_context, &tool_name, exposure) { if is_hidden_by_code_mode_only(turn_context, model_info, &tool_name, exposure) {
continue; continue;
} }
let spec = tool.runtime.spec(); let spec = tool.runtime.spec();
specs.push(spec_for_model_request( specs.push(spec_for_model_request(
turn_context, turn_context,
model_info,
exposure, exposure,
&tool_name, &tool_name,
code_mode_tool_names, code_mode_tool_names,
@@ -532,12 +574,13 @@ fn build_model_visible_specs(
fn spec_for_model_request( fn spec_for_model_request(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
exposure: ToolExposure, exposure: ToolExposure,
tool_name: &ToolName, tool_name: &ToolName,
code_mode_tool_names: &BTreeMap<String, ToolName>, code_mode_tool_names: &BTreeMap<String, ToolName>,
spec: ToolSpec, spec: ToolSpec,
) -> ToolSpec { ) -> ToolSpec {
let tool_mode = effective_tool_mode(turn_context); let tool_mode = effective_tool_mode(turn_context, model_info);
if matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) if matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
&& exposure.is_available_in_code_mode() && exposure.is_available_in_code_mode()
&& !is_excluded_from_code_mode(turn_context, tool_name) && !is_excluded_from_code_mode(turn_context, tool_name)
@@ -557,17 +600,18 @@ fn spec_for_model_request(
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
fn hosted_model_tool_specs( fn hosted_model_tool_specs(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
registered_extension_tool_names: &[ToolName], registered_extension_tool_names: &[ToolName],
) -> Vec<ToolSpec> { ) -> Vec<ToolSpec> {
// Responses Lite accepts schemas for client-executed tools, not hosted Responses tools. // Responses Lite accepts schemas for client-executed tools, not hosted Responses tools.
if turn_context.model_info().use_responses_lite if model_info.use_responses_lite
|| crate::guardian::is_basic_session_source(&turn_context.session_source) || crate::guardian::is_basic_session_source(&turn_context.session_source)
{ {
return Vec::new(); return Vec::new();
} }
let mut specs = Vec::new(); let mut specs = Vec::new();
let standalone_web_search_available = standalone_web_search_enabled(turn_context) let standalone_web_search_available = standalone_web_search_enabled(turn_context, model_info)
&& registered_extension_tool_names.contains(&ToolName::namespaced("web", "run")); && registered_extension_tool_names.contains(&ToolName::namespaced("web", "run"));
// `Some(Cached/Live/Disabled)` are the options for mode when standalone search is unavailable // `Some(Cached/Live/Disabled)` are the options for mode when standalone search is unavailable
// and the provider supports hosted search. `None` prevents emitting a hosted search tool. // and the provider supports hosted search. `None` prevents emitting a hosted search tool.
@@ -580,15 +624,15 @@ fn hosted_model_tool_specs(
if let Some(hosted_web_search_tool) = create_web_search_tool(WebSearchToolOptions { if let Some(hosted_web_search_tool) = create_web_search_tool(WebSearchToolOptions {
web_search_mode, web_search_mode,
web_search_config, web_search_config,
web_search_tool_type: turn_context.model_info().web_search_tool_type, web_search_tool_type: model_info.web_search_tool_type,
}) { }) {
specs.push(hosted_web_search_tool); specs.push(hosted_web_search_tool);
} }
specs specs
} }
pub(crate) fn search_tool_enabled(turn_context: &TurnContext) -> bool { pub(crate) fn search_tool_enabled(turn_context: &TurnContext, model_info: &ModelInfo) -> bool {
turn_context.model_info().supports_search_tool && namespace_tools_enabled(turn_context) model_info.supports_search_tool && namespace_tools_enabled(turn_context)
} }
pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool { pub(crate) fn tool_suggest_enabled(turn_context: &TurnContext) -> bool {
@@ -606,7 +650,7 @@ fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool {
turn_context.multi_agent_version == MultiAgentVersion::V2 turn_context.multi_agent_version == MultiAgentVersion::V2
} }
fn collab_tools_enabled(turn_context: &TurnContext) -> bool { fn collab_tools_enabled(turn_context: &TurnContext, model_info: &ModelInfo) -> bool {
match turn_context.multi_agent_version { match turn_context.multi_agent_version {
MultiAgentVersion::Disabled => false, MultiAgentVersion::Disabled => false,
MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit( MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit(
@@ -615,12 +659,49 @@ fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
), ),
MultiAgentVersion::V2 => { MultiAgentVersion::V2 => {
turn_context.session_source.get_agent_path().is_none() turn_context.session_source.get_agent_path().is_none()
|| turn_context.model_info().multi_agent_version == Some(MultiAgentVersion::V2) || model_info.multi_agent_version == Some(MultiAgentVersion::V2)
} }
} }
} }
fn image_generation_available(turn_context: &TurnContext) -> bool { fn required_child_management_tool_names(
turn_context: &TurnContext,
model_info: &ModelInfo,
) -> Vec<ToolName> {
if !collab_tools_enabled(turn_context, model_info) {
return Vec::new();
}
let (namespace, names): (_, &[&str]) = match turn_context.multi_agent_version {
MultiAgentVersion::Disabled => return Vec::new(),
MultiAgentVersion::V1 => (
Some(MULTI_AGENT_V1_NAMESPACE),
&["send_input", "wait_agent", "resume_agent", "close_agent"],
),
MultiAgentVersion::V2 => (
namespace_tools_enabled(turn_context)
.then_some(turn_context.config.multi_agent_v2.tool_namespace.as_deref())
.flatten(),
&[
"send_message",
"followup_task",
"interrupt_agent",
"list_agents",
],
),
};
let mut tools = names
.iter()
.map(|name| ToolName::new(namespace.map(str::to_owned), (*name).to_owned()))
.collect::<Vec<_>>();
if multi_agent_v2_enabled(turn_context) && turn_context.config.multi_agent_v2.wait_agent_enabled
{
tools.push(ToolName::new(namespace.map(str::to_owned), "wait_agent"));
}
tools
}
fn image_generation_available(turn_context: &TurnContext, model_info: &ModelInfo) -> bool {
if !turn_context if !turn_context
.config .config
.features .features
@@ -645,11 +726,7 @@ fn image_generation_available(turn_context: &TurnContext) -> bool {
return false; return false;
} }
if !turn_context if !model_info.input_modalities.contains(&InputModality::Image) {
.model_info()
.input_modalities
.contains(&InputModality::Image)
{
return false; return false;
} }
@@ -693,10 +770,11 @@ fn agent_type_description(
fn is_hidden_by_code_mode_only( fn is_hidden_by_code_mode_only(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
tool_name: &ToolName, tool_name: &ToolName,
exposure: ToolExposure, exposure: ToolExposure,
) -> bool { ) -> bool {
let tool_mode = effective_tool_mode(turn_context); let tool_mode = effective_tool_mode(turn_context, model_info);
tool_mode == ToolMode::CodeModeOnly tool_mode == ToolMode::CodeModeOnly
&& exposure.is_available_in_code_mode() && exposure.is_available_in_code_mode()
&& codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name( && codex_code_mode::is_code_mode_nested_tool(&codex_tools::code_mode_name_for_tool_name(
@@ -717,9 +795,10 @@ fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName)
fn register_code_mode_executors( fn register_code_mode_executors(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &mut ToolRegistry, registry: &mut ToolRegistry,
) -> BTreeMap<String, ToolName> { ) -> BTreeMap<String, ToolName> {
let tool_mode = effective_tool_mode(turn_context); let tool_mode = effective_tool_mode(turn_context, model_info);
if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) { if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) {
return BTreeMap::new(); return BTreeMap::new();
} }
@@ -729,7 +808,7 @@ fn register_code_mode_executors(
let mut exec_prompt_tool_specs = Vec::new(); let mut exec_prompt_tool_specs = Vec::new();
let mut deferred_exec_prompt_tool_specs = Vec::new(); let mut deferred_exec_prompt_tool_specs = Vec::new();
let mut included_deferred_mcp_output_schema = false; let mut included_deferred_mcp_output_schema = false;
let deferred_tools_guidance_enabled = search_tool_enabled(turn_context); let deferred_tools_guidance_enabled = search_tool_enabled(turn_context, model_info);
for tool in registry.entries() { for tool in registry.entries() {
let exposure = tool.exposure; let exposure = tool.exposure;
if !exposure.is_available_in_code_mode() { if !exposure.is_available_in_code_mode() {
@@ -807,10 +886,7 @@ fn register_code_mode_executors(
&namespace_descriptions, &namespace_descriptions,
turn_context.config.code_mode.default_exec_yield_time_ms, turn_context.config.code_mode.default_exec_yield_time_ms,
tool_mode == ToolMode::CodeModeOnly, tool_mode == ToolMode::CodeModeOnly,
if unified_image_budget_enabled( if unified_image_budget_enabled(&turn_context.config.features, model_info) {
&turn_context.config.features,
turn_context.model_info(),
) {
codex_code_mode::ImageDetailVisibility::Hidden codex_code_mode::ImageDetailVisibility::Hidden
} else { } else {
codex_code_mode::ImageDetailVisibility::Visible codex_code_mode::ImageDetailVisibility::Visible
@@ -922,10 +998,7 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
if turn_context.config.features.enabled(Feature::ShellTool) if turn_context.config.features.enabled(Feature::ShellTool)
&& turn_context.config.features.enabled(Feature::UnifiedExec) && turn_context.config.features.enabled(Feature::UnifiedExec)
&& !matches!( && !matches!(context.model_info.shell_type, ConfigShellToolType::Disabled)
turn_context.model_info().shell_type,
ConfigShellToolType::Disabled
)
{ {
registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions { registry.add(ExecCommandHandler::new(ExecCommandHandlerOptions {
allow_login_shell: any_environment_allows_login_shell(context.environments), allow_login_shell: any_environment_allows_login_shell(context.environments),
@@ -941,11 +1014,11 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
if turn_context.config.features.enabled(Feature::ViewImage) { if turn_context.config.features.enabled(Feature::ViewImage) {
registry.add(ViewImageHandler::new(ViewImageToolOptions { registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail( can_request_original_image_detail: can_request_original_image_detail(
turn_context.model_info(), context.model_info,
), ),
unified_image_budget: unified_image_budget_enabled( unified_image_budget: unified_image_budget_enabled(
&turn_context.config.features, &turn_context.config.features,
turn_context.model_info(), context.model_info,
), ),
include_environment_id, include_environment_id,
})); }));
@@ -960,10 +1033,10 @@ fn add_core_tool_sources(context: &CoreToolPlanContext<'_>, registry: &mut ToolR
add_collaboration_tools(context, registry); add_collaboration_tools(context, registry);
} }
fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool { fn standalone_web_search_enabled(turn_context: &TurnContext, model_info: &ModelInfo) -> bool {
namespace_tools_enabled(turn_context) namespace_tools_enabled(turn_context)
&& turn_context.provider.capabilities().web_search && turn_context.provider.capabilities().web_search
&& (turn_context.model_info().use_responses_lite && (model_info.use_responses_lite
|| turn_context || turn_context
.config .config
.features .features
@@ -989,10 +1062,7 @@ fn add_shell_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistr
if !environment_mode.has_environment() if !environment_mode.has_environment()
|| !features.enabled(Feature::ShellTool) || !features.enabled(Feature::ShellTool)
|| !features.enabled(Feature::UnifiedExec) || !features.enabled(Feature::UnifiedExec)
|| matches!( || matches!(context.model_info.shell_type, ConfigShellToolType::Disabled)
turn_context.model_info().shell_type,
ConfigShellToolType::Disabled
)
{ {
return; return;
} }
@@ -1065,8 +1135,8 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
} }
if !turn_context.session_source.is_non_root_agent() if !turn_context.session_source.is_non_root_agent()
&& turn_context && context
.model_info() .model_info
.experimental_supported_tools .experimental_supported_tools
.iter() .iter()
.any(|tool| tool == "send_user_message_async") .any(|tool| tool == "send_user_message_async")
@@ -1111,15 +1181,13 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
)); ));
} }
if environment_mode.has_environment() if environment_mode.has_environment() && context.model_info.apply_patch_tool_type.is_some() {
&& turn_context.model_info().apply_patch_tool_type.is_some()
{
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ApplyPatchHandler::new(include_environment_id)); registry.add(ApplyPatchHandler::new(include_environment_id));
} }
if turn_context if context
.model_info() .model_info
.experimental_supported_tools .experimental_supported_tools
.iter() .iter()
.any(|tool| tool == "test_sync_tool") .any(|tool| tool == "test_sync_tool")
@@ -1131,11 +1199,11 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple); let include_environment_id = matches!(environment_mode, ToolEnvironmentMode::Multiple);
registry.add(ViewImageHandler::new(ViewImageToolOptions { registry.add(ViewImageHandler::new(ViewImageToolOptions {
can_request_original_image_detail: can_request_original_image_detail( can_request_original_image_detail: can_request_original_image_detail(
turn_context.model_info(), context.model_info,
), ),
unified_image_budget: unified_image_budget_enabled( unified_image_budget: unified_image_budget_enabled(
&turn_context.config.features, &turn_context.config.features,
turn_context.model_info(), context.model_info,
), ),
include_environment_id, include_environment_id,
})); }));
@@ -1145,7 +1213,7 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, registry: &mut Tool
#[instrument(level = "trace", skip_all)] #[instrument(level = "trace", skip_all)]
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) { fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut ToolRegistry) {
let turn_context = context.turn_context; let turn_context = context.turn_context;
if collab_tools_enabled(turn_context) { if collab_tools_enabled(turn_context, context.model_info) {
if multi_agent_v2_enabled(turn_context) { if multi_agent_v2_enabled(turn_context) {
let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only { let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
ToolExposure::DirectModelOnly ToolExposure::DirectModelOnly
@@ -1205,7 +1273,7 @@ fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, registry: &mut Too
} else { } else {
let agent_type_description = let agent_type_description =
agent_type_description(turn_context, context.default_agent_type_description); agent_type_description(turn_context, context.default_agent_type_description);
let exposure = if search_tool_enabled(turn_context) { let exposure = if search_tool_enabled(turn_context, context.model_info) {
ToolExposure::Deferred ToolExposure::Deferred
} else { } else {
ToolExposure::Direct ToolExposure::Direct
@@ -1285,10 +1353,11 @@ fn append_tool_search_executor(
fn append_extension_tool_executors( fn append_extension_tool_executors(
turn_context: &TurnContext, turn_context: &TurnContext,
model_info: &ModelInfo,
executors: impl IntoIterator<Item = Arc<dyn for<'call> ToolExecutor<ExtensionToolCall<'call>>>>, executors: impl IntoIterator<Item = Arc<dyn for<'call> ToolExecutor<ExtensionToolCall<'call>>>>,
registry: &mut ToolRegistry, registry: &mut ToolRegistry,
) -> Option<ToolName> { ) -> Option<ToolName> {
let standalone_web_search_enabled = standalone_web_search_enabled(turn_context); let standalone_web_search_enabled = standalone_web_search_enabled(turn_context, model_info);
let web_search_mode_on = turn_context.config.web_search_mode.value() != WebSearchMode::Disabled; let web_search_mode_on = turn_context.config.web_search_mode.value() != WebSearchMode::Disabled;
let mut standalone_web_search_tool = None; let mut standalone_web_search_tool = None;
@@ -1299,7 +1368,7 @@ fn append_extension_tool_executors(
continue; continue;
} }
if tool_name == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME) if tool_name == ToolName::namespaced(IMAGE_GEN_NAMESPACE, IMAGEGEN_TOOL_NAME)
&& !image_generation_available(turn_context) && !image_generation_available(turn_context, model_info)
{ {
continue; continue;
} }
+176 -39
View File
@@ -20,6 +20,7 @@ use codex_protocol::error::CodexErrorDetails;
use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ApplyPatchToolType;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ToolMode; use codex_protocol::openai_models::ToolMode;
use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::EnvironmentConfigState;
@@ -46,6 +47,7 @@ use crate::config::CurrentTimeReminderConfig;
use crate::environment_selection::TurnEnvironmentState; use crate::environment_selection::TurnEnvironmentState;
use crate::responses_metadata::CodexResponsesRequestKind; use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_metadata::TurnToolFunctionInfo; use crate::responses_metadata::TurnToolFunctionInfo;
use crate::responses_metadata::TurnToolNamespacesInfo;
use crate::responses_metadata::TurnToolSource; use crate::responses_metadata::TurnToolSource;
use crate::session::step_context::StepContext; use crate::session::step_context::StepContext;
use crate::session::tests::make_session_and_context; use crate::session::tests::make_session_and_context;
@@ -74,12 +76,19 @@ struct ToolPlanInputs {
dynamic_tools: Vec<DynamicToolSpec>, dynamic_tools: Vec<DynamicToolSpec>,
} }
#[derive(Debug, PartialEq)]
struct ToolPlanProbe { struct ToolPlanProbe {
visible_specs: Vec<ToolSpec>, visible_specs: Vec<ToolSpec>,
visible_names: Vec<String>, visible_names: Vec<String>,
namespace_functions: BTreeMap<String, Vec<String>>, namespace_functions: BTreeMap<String, Vec<String>>,
registered_names: Vec<String>, registered_names: Vec<String>,
exposures: BTreeMap<String, ToolExposure>, exposures: BTreeMap<String, ToolExposure>,
tool_namespaces_info: Option<TurnToolNamespacesInfo>,
code_mode_tool_names: BTreeMap<String, ToolName>,
tool_mode: ToolMode,
requires_code_mode_worker: bool,
has_terminal_controls: bool,
can_manage_children: bool,
} }
impl ToolPlanProbe { impl ToolPlanProbe {
@@ -129,6 +138,12 @@ impl ToolPlanProbe {
namespace_functions, namespace_functions,
registered_names, registered_names,
exposures, exposures,
tool_namespaces_info: router.tool_namespaces_info().cloned(),
code_mode_tool_names: router.code_mode_tool_names().clone(),
tool_mode: router.tool_mode(),
requires_code_mode_worker: router.requires_code_mode_worker(),
has_terminal_controls: router.has_terminal_controls(),
can_manage_children: router.can_manage_children(),
} }
} }
@@ -204,29 +219,38 @@ async fn probe_with(
) -> ToolPlanProbe { ) -> ToolPlanProbe {
let (_session, mut turn) = make_session_and_context().await; let (_session, mut turn) = make_session_and_context().await;
configure_turn(&mut turn); configure_turn(&mut turn);
let turn = Arc::new(turn); ToolPlanProbe::from_router(plan_with_model(&turn, turn.model_info(), inputs))
let step_context = StepContext::for_test(Arc::clone(&turn)); }
fn plan_with_model(
turn: &TurnContext,
model_info: &ModelInfo,
inputs: ToolPlanInputs,
) -> ToolRouter {
let mcp = codex_mcp::McpBinding::empty(mcp_config_for_test(&turn.config));
let mut registry = build_core_tool_registry( let mut registry = build_core_tool_registry(
step_context.turn.as_ref(), turn,
&step_context.environments, model_info,
step_context.mcp.as_ref(), &turn.environments,
&mcp,
inputs.tool_suggest_candidates.as_ref(), inputs.tool_suggest_candidates.as_ref(),
inputs.wait_for_environment_tool_config.as_ref(), inputs.wait_for_environment_tool_config.as_ref(),
); );
let hosted_specs = append_source_tools( let hosted_specs = append_source_tools(
step_context.turn.as_ref(), turn,
model_info,
&mut registry, &mut registry,
inputs.tool_runtimes, inputs.tool_runtimes,
inputs.extension_tool_executors, inputs.extension_tool_executors,
&inputs.dynamic_tools, &inputs.dynamic_tools,
); );
let router = ToolRouter::from_registry( ToolRouter::from_registry(
step_context.turn.as_ref(), turn,
model_info,
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
); )
ToolPlanProbe::from_router(router)
} }
async fn probe(configure_turn: impl FnOnce(&mut TurnContext)) -> ToolPlanProbe { async fn probe(configure_turn: impl FnOnce(&mut TurnContext)) -> ToolPlanProbe {
@@ -497,6 +521,7 @@ async fn internal_guardian_sessions_exclude_optional_core_tools() {
let router = super::build_tool_router( let router = super::build_tool_router(
&session, &session,
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
&step_context.mcp, &step_context.mcp,
/*apps_enabled*/ false, /*apps_enabled*/ false,
@@ -548,6 +573,7 @@ async fn internal_guardian_sessions_respect_managed_shell_restrictions() {
let router = super::build_tool_router( let router = super::build_tool_router(
&session, &session,
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
&step_context.mcp, &step_context.mcp,
/*apps_enabled*/ false, /*apps_enabled*/ false,
@@ -583,6 +609,7 @@ async fn internal_guardian_sessions_preserve_code_mode() {
let router = super::build_tool_router( let router = super::build_tool_router(
&session, &session,
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
&step_context.mcp, &step_context.mcp,
/*apps_enabled*/ false, /*apps_enabled*/ false,
@@ -652,6 +679,7 @@ async fn internal_guardian_sessions_require_managed_secondary_environments() {
let router = super::build_tool_router( let router = super::build_tool_router(
&session, &session,
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
&step_context.mcp, &step_context.mcp,
/*apps_enabled*/ false, /*apps_enabled*/ false,
@@ -867,6 +895,7 @@ async fn shell_family_registers_only_unified_exec_tools() {
plan.assert_visible_contains(&["exec_command", "write_stdin"]); plan.assert_visible_contains(&["exec_command", "write_stdin"]);
plan.assert_registered_contains(&["exec_command", "write_stdin"]); plan.assert_registered_contains(&["exec_command", "write_stdin"]);
assert!(plan.has_terminal_controls);
assert!(has_parameter(plan.visible_spec("exec_command"), "shell")); assert!(has_parameter(plan.visible_spec("exec_command"), "shell"));
} }
@@ -907,6 +936,7 @@ async fn login_shell_parameter_follows_selected_environment() {
has_parameter(plan.visible_spec("exec_command"), "login"), has_parameter(plan.visible_spec("exec_command"), "login"),
allow_login_shell allow_login_shell
); );
assert!(plan.has_terminal_controls);
} }
} }
} }
@@ -959,6 +989,7 @@ async fn disabling_shell_tools_disables_command_tools_for_all_environments() {
.await; .await;
remote_environment.assert_visible_lacks(&["exec_command", "write_stdin"]); remote_environment.assert_visible_lacks(&["exec_command", "write_stdin"]);
remote_environment.assert_registered_lacks(&["exec_command", "write_stdin"]); remote_environment.assert_registered_lacks(&["exec_command", "write_stdin"]);
assert!(!remote_environment.has_terminal_controls);
let multiple_local_environments = probe(|turn| { let multiple_local_environments = probe(|turn| {
set_feature(turn, Feature::ShellTool, /*enabled*/ false); set_feature(turn, Feature::ShellTool, /*enabled*/ false);
@@ -1159,6 +1190,7 @@ async fn environment_count_controls_environment_backed_tools() {
"view_image", "view_image",
"request_permissions", "request_permissions",
]); ]);
assert!(!no_environment.has_terminal_controls);
let multiple_environments = probe(|turn| { let multiple_environments = probe(|turn| {
duplicate_primary_environment(turn); duplicate_primary_environment(turn);
@@ -1176,6 +1208,7 @@ async fn environment_count_controls_environment_backed_tools() {
"view_image", "view_image",
"request_permissions", "request_permissions",
]); ]);
assert!(multiple_environments.has_terminal_controls);
assert!(has_parameter( assert!(has_parameter(
multiple_environments.visible_spec("exec_command"), multiple_environments.visible_spec("exec_command"),
"environment_id" "environment_id"
@@ -1206,14 +1239,16 @@ async fn environment_tools_follow_the_step_context() {
let plan = ToolPlanProbe::from_router(ToolRouter::from_registry( let plan = ToolPlanProbe::from_router(ToolRouter::from_registry(
turn.as_ref(), turn.as_ref(),
turn.model_info(),
build_core_tool_registry( build_core_tool_registry(
turn.as_ref(), turn.as_ref(),
turn.model_info(),
&environments, &environments,
mcp.as_ref(), mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
/*wait_for_environment_tool_config*/ None, /*wait_for_environment_tool_config*/ None,
), ),
super::hosted_model_tool_specs(turn.as_ref(), &[]), super::hosted_model_tool_specs(turn.as_ref(), turn.model_info(), &[]),
&Default::default(), &Default::default(),
)); ));
@@ -1424,9 +1459,8 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
#[tokio::test] #[tokio::test]
async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() { async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() {
for enabled in [false, true] { for (enabled, use_responses_lite) in [(false, true), (true, false), (true, true)] {
let mut metadata_state = None; let plan = probe_with(
probe_with(
|turn| { |turn| {
update_config(turn, |config| { update_config(turn, |config| {
config.tool_registry.turn_metadata_includes_tool_info = enabled; config.tool_registry.turn_metadata_includes_tool_info = enabled;
@@ -1434,9 +1468,8 @@ async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() {
set_feature(turn, Feature::CodeMode, /*enabled*/ true); set_feature(turn, Feature::CodeMode, /*enabled*/ true);
update_turn_settings_for_test(turn, |settings| { update_turn_settings_for_test(turn, |settings| {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true; Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
Arc::make_mut(&mut settings.model_info).use_responses_lite = true; Arc::make_mut(&mut settings.model_info).use_responses_lite = use_responses_lite;
}); });
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
}, },
ToolPlanInputs { ToolPlanInputs {
tool_runtimes: vec![ tool_runtimes: vec![
@@ -1453,18 +1486,14 @@ async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() {
) )
.await; .await;
let metadata = metadata_state let Some(namespaces) = plan.tool_namespaces_info else {
.expect("tool planning should capture the turn metadata") assert!(
.to_responses_metadata( !enabled || !use_responses_lite,
"installation".to_string(), "opted-in Responses Lite planning should return namespaces"
"window".to_string(),
CodexResponsesRequestKind::Turn,
); );
let Some(namespaces) = metadata.tool_namespaces_info else {
assert!(!enabled, "opted-in tool planning should publish namespaces");
continue; continue;
}; };
assert!(enabled, "tool namespaces require opt-in"); assert!(enabled && use_responses_lite);
let namespace = namespaces let namespace = namespaces
.get("mcp__registry") .get("mcp__registry")
@@ -1497,6 +1526,80 @@ async fn tool_namespaces_info_is_opt_in_and_tracks_mcp_exposure() {
} }
} }
#[tokio::test]
async fn candidate_model_plan_leaves_selected_model_and_inventory_unchanged() {
let (_session, mut turn) = make_session_and_context().await;
set_features(&mut turn, &[Feature::ShellTool, Feature::UnifiedExec]);
update_config(&mut turn, |config| {
config.tool_registry.turn_metadata_includes_tool_info = true;
});
update_turn_settings_for_test(&mut turn, |settings| {
let model = Arc::make_mut(&mut settings.model_info);
model.tool_mode = Some(ToolMode::Direct);
model.use_responses_lite = true;
model.shell_type = ConfigShellToolType::Disabled;
model.apply_patch_tool_type = None;
});
let selected_model = Arc::clone(turn.model_info());
let selected = ToolPlanProbe::from_router(plan_with_model(
&turn,
turn.model_info(),
ToolPlanInputs::default(),
));
let selected_inventory = selected
.tool_namespaces_info
.clone()
.expect("selected plan inventory");
turn.turn_metadata_state
.set_tool_namespaces_info(selected_inventory.clone());
let mut candidate_model = selected_model.as_ref().clone();
candidate_model.tool_mode = Some(ToolMode::CodeModeOnly);
candidate_model.shell_type = ConfigShellToolType::UnifiedExec;
candidate_model.apply_patch_tool_type = Some(ApplyPatchToolType::Freeform);
let candidate = ToolPlanProbe::from_router(plan_with_model(
&turn,
&candidate_model,
ToolPlanInputs::default(),
));
candidate.assert_visible_contains(&["exec", "wait"]);
candidate.assert_visible_lacks(&["exec_command", "write_stdin", "apply_patch"]);
candidate.assert_registered_contains(&["exec_command", "write_stdin", "apply_patch"]);
assert_eq!(candidate.tool_mode, ToolMode::CodeModeOnly);
assert!(candidate.requires_code_mode_worker);
assert!(candidate.has_terminal_controls);
assert_eq!(
candidate
.tool_namespaces_info
.as_ref()
.expect("candidate inventory")["functions"]
.functions["apply_patch"],
TurnToolFunctionInfo {
name: "apply_patch".to_string(),
direct: false,
code_mode_name: Some("apply_patch".to_string()),
deferred: false,
source: TurnToolSource::Harness,
}
);
let metadata = turn.turn_metadata_state.to_responses_metadata(
"installation".to_string(),
"window".to_string(),
CodexResponsesRequestKind::Turn,
);
assert_eq!(metadata.tool_namespaces_info, Some(selected_inventory));
assert_eq!(turn.model_info(), &selected_model);
assert_eq!(
ToolPlanProbe::from_router(plan_with_model(
&turn,
turn.model_info(),
ToolPlanInputs::default(),
)),
selected,
);
}
#[tokio::test] #[tokio::test]
async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() { async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
for (enabled, second_exposure) in [ for (enabled, second_exposure) in [
@@ -1515,6 +1618,7 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
let step_context = StepContext::for_test(Arc::new(turn)); let step_context = StepContext::for_test(Arc::new(turn));
let mut registry = build_core_tool_registry( let mut registry = build_core_tool_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
step_context.mcp.as_ref(), step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
@@ -1536,6 +1640,7 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
.collect(); .collect();
let hosted_specs = append_source_tools( let hosted_specs = append_source_tools(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&mut registry, &mut registry,
runtimes, runtimes,
Vec::new(), Vec::new(),
@@ -1543,6 +1648,7 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
); );
let result = super::finalize_tool_router( let result = super::finalize_tool_router(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
@@ -1772,6 +1878,7 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
let step_context = StepContext::for_test(Arc::clone(&turn)); let step_context = StepContext::for_test(Arc::clone(&turn));
let mut registry = build_core_tool_registry( let mut registry = build_core_tool_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
step_context.mcp.as_ref(), step_context.mcp.as_ref(),
inputs.tool_suggest_candidates.as_ref(), inputs.tool_suggest_candidates.as_ref(),
@@ -1779,6 +1886,7 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
); );
let hosted_specs = append_source_tools( let hosted_specs = append_source_tools(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&mut registry, &mut registry,
inputs.tool_runtimes, inputs.tool_runtimes,
inputs.extension_tool_executors, inputs.extension_tool_executors,
@@ -1787,6 +1895,7 @@ async fn strict_tool_collisions_reject_external_and_synthetic_duplicates() {
let error = super::finalize_tool_router( let error = super::finalize_tool_router(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
hosted_specs, hosted_specs,
&Default::default(), &Default::default(),
@@ -1915,7 +2024,6 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
(true, ToolExposure::Direct, true), (true, ToolExposure::Direct, true),
(true, ToolExposure::Deferred, false), (true, ToolExposure::Deferred, false),
] { ] {
let mut metadata_state = None;
let plan = probe_with( let plan = probe_with(
|turn| { |turn| {
update_config(turn, |config| { update_config(turn, |config| {
@@ -1929,7 +2037,6 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
Arc::make_mut(&mut settings.model_info).supports_search_tool = true; Arc::make_mut(&mut settings.model_info).supports_search_tool = true;
Arc::make_mut(&mut settings.model_info).use_responses_lite = true; Arc::make_mut(&mut settings.model_info).use_responses_lite = true;
}); });
metadata_state = Some(Arc::clone(&turn.turn_metadata_state));
}, },
ToolPlanInputs { ToolPlanInputs {
tool_runtimes: vec![mcp_runtime( tool_runtimes: vec![mcp_runtime(
@@ -1961,17 +2068,14 @@ async fn code_mode_uses_the_first_normalized_tool_identity() {
}, },
); );
let metadata = metadata_state let tool_namespaces = plan
.expect("tool planning should capture the turn metadata")
.to_responses_metadata(
"installation".to_string(),
"window".to_string(),
CodexResponsesRequestKind::Turn,
);
let tool_namespaces = metadata
.tool_namespaces_info .tool_namespaces_info
.as_ref() .as_ref()
.expect("opted-in Responses Lite should receive the supported tools"); .expect("opted-in Responses Lite plan should contain the supported tools");
assert_eq!(
plan.code_mode_tool_names.get("normalized_alias__lookup"),
Some(&winner_name),
);
assert_eq!( assert_eq!(
tool_namespaces["normalized-alias"].functions["lookup"] tool_namespaces["normalized-alias"].functions["lookup"]
.code_mode_name .code_mode_name
@@ -2040,6 +2144,7 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let first_step_context = StepContext::for_test(Arc::clone(&first_turn)); let first_step_context = StepContext::for_test(Arc::clone(&first_turn));
let mut first_registry = build_core_tool_registry( let mut first_registry = build_core_tool_registry(
first_step_context.turn.as_ref(), first_step_context.turn.as_ref(),
first_step_context.turn.model_info(),
&first_step_context.environments, &first_step_context.environments,
first_step_context.mcp.as_ref(), first_step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
@@ -2049,8 +2154,13 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
first_registry.register_external_with_exposure(first_tool.runtime, first_tool.exposure); first_registry.register_external_with_exposure(first_tool.runtime, first_tool.exposure);
let first_router = ToolRouter::from_registry( let first_router = ToolRouter::from_registry(
first_step_context.turn.as_ref(), first_step_context.turn.as_ref(),
first_step_context.turn.model_info(),
first_registry, first_registry,
super::hosted_model_tool_specs(first_step_context.turn.as_ref(), &[]), super::hosted_model_tool_specs(
first_step_context.turn.as_ref(),
first_step_context.turn.model_info(),
&[],
),
&cache, &cache,
); );
let first_plan = ToolPlanProbe::from_router(first_router); let first_plan = ToolPlanProbe::from_router(first_router);
@@ -2063,6 +2173,7 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
let second_step_context = StepContext::for_test(Arc::clone(&second_turn)); let second_step_context = StepContext::for_test(Arc::clone(&second_turn));
let mut second_registry = build_core_tool_registry( let mut second_registry = build_core_tool_registry(
second_step_context.turn.as_ref(), second_step_context.turn.as_ref(),
second_step_context.turn.model_info(),
&second_step_context.environments, &second_step_context.environments,
second_step_context.mcp.as_ref(), second_step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
@@ -2072,8 +2183,13 @@ async fn tool_search_cache_rebuilds_when_deferred_sources_change() {
second_registry.register_external_with_exposure(second_tool.runtime, second_tool.exposure); second_registry.register_external_with_exposure(second_tool.runtime, second_tool.exposure);
let second_router = ToolRouter::from_registry( let second_router = ToolRouter::from_registry(
second_step_context.turn.as_ref(), second_step_context.turn.as_ref(),
second_step_context.turn.model_info(),
second_registry, second_registry,
super::hosted_model_tool_specs(second_step_context.turn.as_ref(), &[]), super::hosted_model_tool_specs(
second_step_context.turn.as_ref(),
second_step_context.turn.model_info(),
&[],
),
&cache, &cache,
); );
let second_plan = ToolPlanProbe::from_router(second_router); let second_plan = ToolPlanProbe::from_router(second_router);
@@ -2117,6 +2233,7 @@ async fn tool_search_cache_rebuilds_when_deferred_world_state_changes() {
let step_context = StepContext::for_test(Arc::clone(&turn)); let step_context = StepContext::for_test(Arc::clone(&turn));
let mut registry = build_core_tool_registry( let mut registry = build_core_tool_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
&step_context.environments, &step_context.environments,
step_context.mcp.as_ref(), step_context.mcp.as_ref(),
/*tool_suggest_candidates*/ None, /*tool_suggest_candidates*/ None,
@@ -2131,8 +2248,13 @@ async fn tool_search_cache_rebuilds_when_deferred_world_state_changes() {
registry.register_external_with_exposure(tool.runtime, tool.exposure); registry.register_external_with_exposure(tool.runtime, tool.exposure);
let router = ToolRouter::from_registry( let router = ToolRouter::from_registry(
step_context.turn.as_ref(), step_context.turn.as_ref(),
step_context.turn.model_info(),
registry, registry,
super::hosted_model_tool_specs(step_context.turn.as_ref(), &[]), super::hosted_model_tool_specs(
step_context.turn.as_ref(),
step_context.turn.model_info(),
&[],
),
&cache, &cache,
); );
let plan = ToolPlanProbe::from_router(router); let plan = ToolPlanProbe::from_router(router);
@@ -2303,6 +2425,10 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
codex_code_mode::PUBLIC_TOOL_NAME, codex_code_mode::PUBLIC_TOOL_NAME,
codex_code_mode::WAIT_TOOL_NAME, codex_code_mode::WAIT_TOOL_NAME,
]); ]);
assert_eq!(
(plain.tool_mode, plain.requires_code_mode_worker),
(ToolMode::Direct, false)
);
let code_mode_only = probe_with( let code_mode_only = probe_with(
|turn| { |turn| {
@@ -2322,6 +2448,13 @@ async fn code_mode_only_exposes_code_executor_and_hides_nested_tools() {
codex_code_mode::PUBLIC_TOOL_NAME, codex_code_mode::PUBLIC_TOOL_NAME,
codex_code_mode::WAIT_TOOL_NAME, codex_code_mode::WAIT_TOOL_NAME,
]); ]);
assert_eq!(
(
code_mode_only.tool_mode,
code_mode_only.requires_code_mode_worker
),
(ToolMode::CodeModeOnly, true),
);
assert_eq!( assert_eq!(
code_mode_only.namespace_function_names("codex_app"), code_mode_only.namespace_function_names("codex_app"),
Vec::<String>::new().as_slice() Vec::<String>::new().as_slice()
@@ -2484,6 +2617,7 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
}) })
.await; .await;
v1.assert_visible_contains(&[MULTI_AGENT_V1_NAMESPACE]); v1.assert_visible_contains(&[MULTI_AGENT_V1_NAMESPACE]);
assert!(v1.can_manage_children);
v1.assert_visible_lacks(&[ v1.assert_visible_lacks(&[
"spawn_agent", "spawn_agent",
"send_input", "send_input",
@@ -2540,6 +2674,7 @@ async fn multi_agent_feature_selects_one_agent_tool_family() {
}) })
.await; .await;
v2.assert_visible_contains(&[MULTI_AGENT_V2_NAMESPACE]); v2.assert_visible_contains(&[MULTI_AGENT_V2_NAMESPACE]);
assert!(v2.can_manage_children);
v2.assert_visible_lacks(&[ v2.assert_visible_lacks(&[
"spawn_agent", "spawn_agent",
"send_message", "send_message",
@@ -2674,6 +2809,7 @@ async fn multi_agent_v2_can_disable_wait_agent() {
); );
plan.assert_visible_lacks(&["clock"]); plan.assert_visible_lacks(&["clock"]);
plan.assert_registered_lacks(&["collaboration.wait_agent", "clock.sleep"]); plan.assert_registered_lacks(&["collaboration.wait_agent", "clock.sleep"]);
assert!(plan.can_manage_children);
} }
#[tokio::test] #[tokio::test]
@@ -2856,6 +2992,7 @@ async fn multi_agent_v2_bedrock_workers_only_delegate_when_model_supports_v2() {
let spawn_agent_name = ToolName::namespaced("agents", "spawn_agent").to_string(); let spawn_agent_name = ToolName::namespaced("agents", "spawn_agent").to_string();
let followup_task_name = ToolName::namespaced("agents", "followup_task").to_string(); let followup_task_name = ToolName::namespaced("agents", "followup_task").to_string();
assert_eq!(plan.can_manage_children, supports_delegation);
if supports_delegation { if supports_delegation {
plan.assert_visible_contains(&["agents"]); plan.assert_visible_contains(&["agents"]);
plan.assert_registered_contains(&[&spawn_agent_name, &followup_task_name]); plan.assert_registered_contains(&[&spawn_agent_name, &followup_task_name]);
+22
View File
@@ -2666,9 +2666,11 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
let mut previous_model_info = let mut previous_model_info =
model_info_with_context_window("gpt-5.4", /*context_window*/ 273_000); model_info_with_context_window("gpt-5.4", /*context_window*/ 273_000);
previous_model_info.slug = previous_model_family.to_string(); previous_model_info.slug = previous_model_family.to_string();
previous_model_info.use_responses_lite = true;
let mut next_model_info = let mut next_model_info =
model_info_with_context_window("gpt-5.4", /*context_window*/ 125_000); model_info_with_context_window("gpt-5.4", /*context_window*/ 125_000);
next_model_info.slug = next_model.to_string(); next_model_info.slug = next_model.to_string();
next_model_info.use_responses_lite = false;
let models_mock = mount_models_once( let models_mock = mount_models_once(
&server, &server,
@@ -2710,6 +2712,7 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
.with_model(retired_model) .with_model(retired_model)
.with_config(move |config| { .with_config(move |config| {
config.model_provider = model_provider; config.model_provider = model_provider;
config.tool_registry.turn_metadata_includes_tool_info = true;
set_test_compact_prompt(config); set_test_compact_prompt(config);
let _ = config.features.enable(Feature::RemoteCompactionV2); let _ = config.features.enable(Feature::RemoteCompactionV2);
}); });
@@ -2755,6 +2758,25 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on
); );
assert_eq!(requests[2].body_json()["model"].as_str(), Some(next_model)); assert_eq!(requests[2].body_json()["model"].as_str(), Some(next_model));
assert_eq!(requests[3].body_json()["model"].as_str(), Some(next_model)); assert_eq!(requests[3].body_json()["model"].as_str(), Some(next_model));
// Preparing the non-Lite fallback must not clear the previous model's inventory
// before its compaction attempt is sent.
let [first_metadata, compact_metadata] = [&requests[0], &requests[1]].map(|request| {
serde_json::from_str::<Value>(
request.body_json()["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("request should include turn metadata"),
)
.expect("turn metadata should be valid JSON")
});
let inventory = first_metadata["tool_namespaces_info"]
.as_object()
.expect("Responses Lite request should include tool inventory");
assert!(!inventory.is_empty());
assert_eq!(
compact_metadata["tool_namespaces_info"],
first_metadata["tool_namespaces_info"],
);
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]