diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 0623ceb3b6..7b075d0e60 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -12,6 +12,7 @@ use crate::context_manager::estimate_response_item_model_visible_bytes; use crate::context_manager::is_codex_generated_item; use crate::session::session::Session; use crate::session::turn::built_tools; +use crate::session::turn::model_visible_specs_for_prompt; use crate::session::turn_context::TurnContext; use codex_analytics::CompactionImplementation; use codex_analytics::CompactionPhase; @@ -165,7 +166,7 @@ async fn run_remote_compact_task_inner_impl( .await?; let prompt = Prompt { input: prompt_input, - tools: tool_router.model_visible_specs(), + tools: model_visible_specs_for_prompt(tool_router.as_ref(), turn_context.as_ref()), parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls, base_instructions, personality: turn_context.personality, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 2577ec47d0..68c87243ab 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -945,21 +945,7 @@ pub(crate) fn build_prompt( turn_context: &TurnContext, base_instructions: BaseInstructions, ) -> Prompt { - let deferred_dynamic_tools = turn_context - .dynamic_tools - .iter() - .filter(|tool| tool.defer_loading) - .map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone())) - .collect::>(); - let tools = if deferred_dynamic_tools.is_empty() { - router.model_visible_specs() - } else { - router - .model_visible_specs() - .into_iter() - .filter_map(|spec| filter_deferred_dynamic_tool_spec(spec, &deferred_dynamic_tools)) - .collect() - }; + let tools = model_visible_specs_for_prompt(router, turn_context); Prompt { input, @@ -974,6 +960,36 @@ pub(crate) fn build_prompt( } } +pub(crate) fn model_visible_specs_for_prompt( + router: &ToolRouter, + turn_context: &TurnContext, +) -> Vec { + filter_deferred_dynamic_tool_specs( + router.model_visible_specs(), + turn_context.dynamic_tools.as_slice(), + ) +} + +fn filter_deferred_dynamic_tool_specs( + specs: Vec, + dynamic_tools: &[codex_protocol::dynamic_tools::DynamicToolSpec], +) -> Vec { + let deferred_dynamic_tools = dynamic_tools + .iter() + .filter(|tool| tool.defer_loading) + .map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone())) + .collect::>(); + + if deferred_dynamic_tools.is_empty() { + specs + } else { + specs + .into_iter() + .filter_map(|spec| filter_deferred_dynamic_tool_spec(spec, &deferred_dynamic_tools)) + .collect() + } +} + fn filter_deferred_dynamic_tool_spec( spec: ToolSpec, deferred_dynamic_tools: &HashSet, @@ -1003,6 +1019,99 @@ fn filter_deferred_dynamic_tool_spec( } } +#[cfg(test)] +mod prompt_tool_filter_tests { + use super::*; + use codex_protocol::dynamic_tools::DynamicToolSpec; + use codex_tools::AdditionalProperties; + use codex_tools::JsonSchema; + use codex_tools::ResponsesApiNamespace; + use codex_tools::ResponsesApiTool; + use serde_json::json; + use std::collections::BTreeMap; + + #[test] + fn filters_deferred_dynamic_tools_from_prompt_specs() { + let dynamic_tools = vec![DynamicToolSpec { + namespace: Some("codex_app".to_string()), + name: "automation_update".to_string(), + description: "Create, update, view, or delete recurring automations.".to_string(), + input_schema: json!({ + "type": "object", + "properties": { + "mode": { "type": "string" }, + }, + "required": ["mode"], + "additionalProperties": false, + }), + defer_loading: true, + }]; + let specs = vec![ + ToolSpec::ToolSearch { + execution: "sync".to_string(), + description: "Search deferred tools".to_string(), + parameters: object_schema(), + }, + ToolSpec::Namespace(ResponsesApiNamespace { + name: "codex_app".to_string(), + description: "Tools in the codex_app namespace.".to_string(), + tools: vec![ + ResponsesApiNamespaceTool::Function(function_tool( + "automation_update", + Some(true), + )), + ResponsesApiNamespaceTool::Function(function_tool( + "visible_tool", + /*defer_loading*/ None, + )), + ], + }), + ToolSpec::Function(function_tool("plain_tool", /*defer_loading*/ None)), + ]; + + let filtered = filter_deferred_dynamic_tool_specs(specs, &dynamic_tools); + + assert_eq!( + filtered, + vec![ + ToolSpec::ToolSearch { + execution: "sync".to_string(), + description: "Search deferred tools".to_string(), + parameters: object_schema(), + }, + ToolSpec::Namespace(ResponsesApiNamespace { + name: "codex_app".to_string(), + description: "Tools in the codex_app namespace.".to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(function_tool( + "visible_tool", + /*defer_loading*/ None, + ))], + }), + ToolSpec::Function(function_tool("plain_tool", /*defer_loading*/ None)), + ] + ); + } + + fn function_tool(name: &str, defer_loading: Option) -> ResponsesApiTool { + ResponsesApiTool { + name: name.to_string(), + description: format!("{name} description"), + strict: false, + defer_loading, + parameters: object_schema(), + output_schema: None, + } + } + + fn object_schema() -> JsonSchema { + JsonSchema::object( + BTreeMap::new(), + /*required*/ None, + Some(AdditionalProperties::Boolean(false)), + ) + } +} + #[allow(clippy::too_many_arguments)] #[instrument(level = "trace", skip_all,