diff --git a/codex-rs/core/src/context/world_state/top_level_tools.rs b/codex-rs/core/src/context/world_state/top_level_tools.rs index 93b1b42010..67c6aeab48 100644 --- a/codex-rs/core/src/context/world_state/top_level_tools.rs +++ b/codex-rs/core/src/context/world_state/top_level_tools.rs @@ -39,9 +39,6 @@ const ACTION_ADDED: &str = "added"; const ACTION_REMOVED: &str = "removed"; const ACTION_SCHEMA_CHANGED: &str = "schema_changed"; -const EXEC_TOOLS_UPDATE_HINT: &str = "Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier version in full. Unlisted tools keep their latest definitions and `tools` methods."; -const EXEC_TOOLS_REMOVED_DEFINITIONS_HEADER: &str = "Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged."; - /// The exact serialized Responses Lite declarations visible in one captured step. pub(crate) struct TopLevelToolsState { messages: ResolvedIncrementalToolMessages, @@ -391,7 +388,8 @@ impl TopLevelToolsState { }) .collect::>(); if !changed.is_empty() { - let update = render_exec_update(&changed); + let update = + render_exec_update(&changed, &self.messages.exec_tools_update_hint); if let Some(metrics) = self.metrics.as_deref() { let added = changed .iter() @@ -458,7 +456,11 @@ impl TopLevelToolsState { ); } updates.push( - WorldStateUpdate::fragment(RemovedExecDefinitions(removed)).standalone(), + WorldStateUpdate::fragment(RemovedExecDefinitions { + header: self.messages.exec_tools_removed_definitions_header.clone(), + names: removed, + }) + .standalone(), ); } } @@ -550,19 +552,28 @@ impl ContextualUserFragment for RemovedTools { } /// Render a nonempty batch of complete replacement definitions with the exec update hint. -fn render_exec_update(definitions: &[&ExecDefinition]) -> IncrementalToolsUpdate { +fn render_exec_update(definitions: &[&ExecDefinition], hint: &str) -> IncrementalToolsUpdate { let description = definitions .iter() .map(|entry| entry.description.as_str()) .collect::>() .join("\n\n"); - IncrementalToolsUpdate(format!( - "{EXEC_TOOLS_UPDATE_HINT}\n\n## Namespace: functions\n{description}" - )) + let mut text = hint.to_owned(); + if !text.is_empty() { + text.push_str("\n\n"); + } + text.push_str("## Namespace: functions\n"); + text.push_str(&description); + IncrementalToolsUpdate(text) } /// Removal notice containing the stable keys of withdrawn tools or namespace instructions. -struct RemovedExecDefinitions(Vec); +struct RemovedExecDefinitions { + /// Guidance preceding the removed keys; an empty header emits only the keys. + header: String, + /// Stable entry keys that are no longer present in the current catalog. + names: Vec, +} impl ContextualUserFragment for RemovedExecDefinitions { fn role(&self) -> &'static str { @@ -582,10 +593,13 @@ impl ContextualUserFragment for RemovedExecDefinitions { } fn body(&self) -> ContentItem { - input_text(format!( - "{EXEC_TOOLS_REMOVED_DEFINITIONS_HEADER}\n- {}", - self.0.join("\n- ") - )) + let mut text = self.header.clone(); + if !text.is_empty() { + text.push('\n'); + } + text.push_str("- "); + text.push_str(&self.names.join("\n- ")); + input_text(text) } } diff --git a/codex-rs/core/src/context/world_state/top_level_tools_tests.rs b/codex-rs/core/src/context/world_state/top_level_tools_tests.rs index a0474f9b57..5a55fcd5bc 100644 --- a/codex-rs/core/src/context/world_state/top_level_tools_tests.rs +++ b/codex-rs/core/src/context/world_state/top_level_tools_tests.rs @@ -5,9 +5,11 @@ use crate::context::world_state::test_support::render_section_cases; use crate::context_manager::updates::merge_world_state_updates; use crate::session::extension_metrics::from_session_telemetry; use codex_code_mode::ExecDefinition; +use codex_models_manager::model_info::model_info_from_slug; use codex_otel::MetricsClient; use codex_otel::MetricsConfig; use codex_otel::SessionTelemetry; +use codex_prompts::ResolvedModelMessages; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use opentelemetry_sdk::metrics::InMemoryMetricExporter; @@ -840,15 +842,25 @@ fn hidden_exec_is_not_reintroduced_and_returning_exec_restores_full_catalog() { role: "developer".to_string(), tools: Arc::from(vec![original]), }, - ContextualUserFragment::into(RemovedExecDefinitions( - vec!["Tool: tools.a".to_string(),] - )), + ContextualUserFragment::into(RemovedExecDefinitions { + header: ResolvedIncrementalToolMessages::default() + .exec_tools_removed_definitions_header, + names: vec!["Tool: tools.a".to_string()], + }), ], ); } -#[test] -fn changed_and_added_entries_share_one_update_and_removed_entries_are_withdrawn() { +#[test_case::test_case(json!({}), None, None; "bundled")] +#[test_case::test_case(json!({"exec_tools_update_hint": null, "exec_tools_removed_definitions_header": null}), None, None; "null_fields")] +#[test_case::test_case(json!({"exec_tools_update_hint": "é".repeat(257), "exec_tools_removed_definitions_header": "Catalog removals:"}), None, Some("Catalog removals:".to_string()); "oversized_update_preserves_removal")] +#[test_case::test_case(json!({"exec_tools_update_hint": "Catalog update.", "exec_tools_removed_definitions_header": "é".repeat(257)}), Some("Catalog update.".to_string()), None; "oversized_removal_preserves_update")] +#[test_case::test_case(json!({"exec_tools_update_hint": "é".repeat(256), "exec_tools_removed_definitions_header": "🐈".repeat(128)}), Some("é".repeat(256)), Some("🐈".repeat(128)); "utf8_byte_limit")] +fn changed_and_added_entries_share_one_update_and_removed_entries_are_withdrawn( + overrides: Value, + expected_update_hint: Option, + expected_removal_header: Option, +) { let definition = exec_declaration("Shared MCP Types:", "grammar"); let original = TopLevelToolsState::new( /*messages*/ Default::default(), @@ -871,8 +883,11 @@ fn changed_and_added_entries_share_one_update_and_removed_entries_are_withdrawn( .0 .unwrap(); + let mut model = model_info_from_slug("test-model"); + model.model_messages = + Some(serde_json::from_value(json!({"tools": {"incremental_tools": overrides}})).unwrap()); let current = TopLevelToolsState::new( - /*messages*/ Default::default(), + ResolvedModelMessages::from_model(&model).incremental_tools(), vec![definition], /*metrics*/ None, ) @@ -890,15 +905,20 @@ fn changed_and_added_entries_share_one_update_and_removed_entries_are_withdrawn( }, ]))); let (snapshot, updates) = current.render_diff(PreviousSectionState::Known(&baseline)); + let defaults = ResolvedIncrementalToolMessages::default(); + let expected_update_hint = expected_update_hint.unwrap_or(defaults.exec_tools_update_hint); + let expected_removal_header = + expected_removal_header.unwrap_or(defaults.exec_tools_removed_definitions_header); assert_eq!( merge_world_state_updates(updates), vec![ - ContextualUserFragment::into(IncrementalToolsUpdate( - "Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier version in full. Unlisted tools keep their latest definitions and `tools` methods.\n\n## Namespace: functions\n### a\nChanged a.\ndeclare const tools: { a(value: number): Promise };\n\n### c\ndeclare const tools: { c(): Promise };".to_string(), - )), - ContextualUserFragment::into(RemovedExecDefinitions(vec![ - "Tool: tools.b".to_string(), - ])), + ContextualUserFragment::into(IncrementalToolsUpdate(format!( + "{expected_update_hint}\n\n## Namespace: functions\n### a\nChanged a.\ndeclare const tools: {{ a(value: number): Promise }};\n\n### c\ndeclare const tools: {{ c(): Promise }};", + ))), + ContextualUserFragment::into(RemovedExecDefinitions { + header: expected_removal_header, + names: vec!["Tool: tools.b".to_string()], + }), ] ); assert!( diff --git a/codex-rs/core/tests/suite/scenarios_exec_tools.rs b/codex-rs/core/tests/suite/scenarios_exec_tools.rs index 4d4baa0b7b..51a62ba101 100644 --- a/codex-rs/core/tests/suite/scenarios_exec_tools.rs +++ b/codex-rs/core/tests/suite/scenarios_exec_tools.rs @@ -2,6 +2,7 @@ use anyhow::Result; use codex_features::Feature; +use codex_protocol::openai_models::IncrementalToolMessages; use codex_protocol::openai_models::ToolMode; use codex_protocol::protocol::Op; use codex_protocol::protocol::ThreadSettingsOverrides; @@ -20,8 +21,12 @@ use std::collections::HashMap; use super::super::rmcp_client::remote_aware_environment_id; use super::super::rmcp_client::remote_aware_stdio_server_bin; +#[test_case::test_case(false; "bundled")] +#[test_case::test_case(true; "catalog_messages")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> Result<()> { +async fn changed_sections_and_entries_append_without_repeating_the_catalog( + catalog_messages: bool, +) -> Result<()> { skip_if_no_network!(Ok(())); skip_if_wine_exec!(Ok(()), "requires a Windows test_stdio_server fixture"); let server = responses::start_mock_server().await; @@ -39,38 +44,57 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> }; let original_instructions = "Echo service instructions."; let updated_instructions = "Echo service instructions. Durations are milliseconds."; + let update_hint = "Catalog exec updates: retain {{ literal }} text."; + let removal_header = "Catalog exec removals: retain {{ literal }} text."; let initial_server = server_config(&["echo"], original_instructions, &["deferred"])?; - let mut builder = test_codex() - .with_config(move |config| { - super::configure_scenario_catalog(config); - for feature in [ - Feature::IncrementalTools, - Feature::IncrementalExecTools, - Feature::DeferredToolWorldState, - ] { - config - .features - .enable(feature) - .expect("enable catalog updates"); - } + let mut builder = test_codex().with_config(move |config| { + super::configure_scenario_catalog(config); + for feature in [ + Feature::IncrementalTools, + Feature::IncrementalExecTools, + Feature::DeferredToolWorldState, + ] { config .features - .disable(Feature::CodeModeOnlyStrictThirdPartyTools) - .expect("allow eager MCP tools"); - config.code_mode.disable_in_process_fallback = true; - config - .mcp_servers - .set(HashMap::from([( - "echo_service".to_string(), - initial_server, - )])) - .expect("set MCP fixture"); - }) - .with_model_info_override("catalog-initial", |model| { + .enable(feature) + .expect("enable catalog updates"); + } + config + .features + .disable(Feature::CodeModeOnlyStrictThirdPartyTools) + .expect("allow eager MCP tools"); + config.code_mode.disable_in_process_fallback = true; + config + .mcp_servers + .set(HashMap::from([( + "echo_service".to_string(), + initial_server, + )])) + .expect("set MCP fixture"); + }); + for (slug, hint, header) in [ + ("catalog-initial", update_hint, removal_header), + ("catalog-empty-hints", "", ""), + ] { + builder = builder.with_model_info_override(slug, move |model| { model.use_responses_lite = true; model.tool_mode = Some(ToolMode::CodeModeOnly); model.supports_search_tool = true; + if catalog_messages { + model + .model_messages + .as_mut() + .expect("model messages") + .tools + .get_or_insert_default() + .incremental_tools = Some(IncrementalToolMessages { + exec_tools_update_hint: Some(hint.to_string()), + exec_tools_removed_definitions_header: Some(header.to_string()), + ..Default::default() + }); + } }); + } for (slug, description, deferred_guidance, shared_types) in [ ( "catalog-updated", @@ -96,6 +120,10 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> "deferred_nested_tools_guidance": deferred_guidance, "mcp_typescript_preamble": shared_types, }, + "incremental_tools": catalog_messages.then_some(json!({ + "exec_tools_update_hint": "", + "exec_tools_removed_definitions_header": "", + })), "mcp_resources": {"read_mcp_resource": { "description": "Read a selected resource revision.", "parameters": json!({ @@ -158,6 +186,22 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> .await?; test.submit_text_turn(prompt).await?; } + // The catalog variant changes only wording; bundled mode reapplies its current model. + core_test_support::submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + model: Some( + if catalog_messages { + "catalog-empty-hints" + } else { + "catalog-initial" + } + .to_string(), + ), + ..Default::default() + }, + ) + .await?; test.submit_text_turn("Unchanged catalog: emit no update.") .await?; @@ -236,6 +280,7 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> assert_eq!(requests.len(), 14); let mut previous = Vec::new(); let mut declarations = Vec::new(); + let mut previous_developer_count = 0; // Snapshots normalize IDs and metadata, so check the retained raw prefix separately. for (index, request) in requests.iter().enumerate() { let input = request.input(); @@ -257,6 +302,62 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> .cloned() .collect::>(), ); + if catalog_messages { + let texts = request.message_input_texts("developer"); + let start = if index == 13 { + 0 + } else { + previous_developer_count + }; + let updates = texts[start..] + .iter() + .map(String::as_str) + .filter(|text| text.contains("## Namespace: functions\n")) + .collect::>(); + let notices = texts[start..] + .iter() + .map(String::as_str) + .filter(|text| { + text.lines().any(|line| { + line.starts_with("- Tool: tools.") + || line.starts_with("- Namespace instructions: ") + }) + }) + .collect::>(); + if index == 4 { + assert!(updates.is_empty() && notices.is_empty() && declarations[index].is_empty()); + } + if matches!(index, 1 | 7 | 10) { + assert_eq!(updates.len(), 1); + } + if index == 7 { + assert!(updates[0].contains("mcp__echo_service__echo(args:")); + } + if index == 10 { + assert!(updates[0].contains("revision?: number")); + } + if matches!(index, 5 | 6 | 8) { + assert_eq!(notices.len(), 1); + } + let prefix = if index < 4 { + format!("{update_hint}\n\n## Namespace: functions\n") + } else { + "## Namespace: functions\n".to_string() + }; + for update in updates { + assert!(update.starts_with(&prefix)); + } + if index == 3 { + let expected = format!("{removal_header}\n- Tool: tools.mcp__echo_service__cwd"); + assert_eq!(notices, vec![expected.as_str()]); + } else if index >= 4 { + for notice in notices { + assert!(notice.lines().all(|line| line.starts_with("- Tool: tools.") + || line.starts_with("- Namespace instructions: "))); + } + } + previous_developer_count = texts.len(); + } previous = input; } // Native declarations are fingerprinted in snapshots; keep their invariants readable here. @@ -295,11 +396,22 @@ async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> ] { assert!(restored.contains(expected), "compaction omitted {expected}"); } + let (snapshot_name, scenario) = if catalog_messages { + ( + "incremental_exec_tools_catalog_messages", + "Configured exec updates and removals use the catalog wording. Empty overrides omit that wording while retaining the changed definitions and removal keys. Compaction restores the current catalog.", + ) + } else { + ( + "incremental_exec_tools", + "Code Mode appends complete changed tool entries and namespace instructions in combined developer messages. Namespace clearing, eager/deferred movement, remove-all and re-enabling preserve context. Common instructions and shared types stay frozen while nested methods update; compaction installs the latest full catalog.", + ) + }; let snapshot = context_snapshot::format_request_history_snapshot( - "Code Mode appends complete changed tool entries and namespace instructions in combined developer messages. Namespace clearing, eager/deferred movement, remove-all and re-enabling preserve context. Common instructions and shared types stay frozen while nested methods update; compaction installs the latest full catalog.", + scenario, &requests, &ContextSnapshotOptions::default().rewrite_known_segments(), ); - insta::assert_snapshot!("incremental_exec_tools", snapshot); + insta::assert_snapshot!(snapshot_name, snapshot); Ok(()) } diff --git a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs index 5f2b57b959..5c0a219cb4 100644 --- a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs +++ b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs @@ -508,6 +508,7 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history( removed_namespaces_header: Some("Unavailable namespaces:".to_string()), namespace_instructions_prefix: Some("Catalog {name}: ".to_string()), namespace_instructions_cleared: Some("Cleared {name}.".to_string()), + ..Default::default() }); } }) diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools_catalog_messages.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools_catalog_messages.snap new file mode 100644 index 0000000000..97d31cd25e --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools_catalog_messages.snap @@ -0,0 +1,475 @@ +--- +source: core/tests/suite/scenarios_exec_tools.rs +expression: snapshot +--- +Scenario: Configured exec updates and removals use the catalog wording. Empty overrides omit that wording while retaining the changed definitions and removal keys. Compaction restores the current catalog. + +## Window 1 +-- request 1 (turn) -- +00:additional_tools/developer (1; hash=b2c6697017b587f1): + - namespace/functions; hash=e9ce2d0830f744bb + - custom/exec + - function/wait + - function/request_user_input +01:message/developer: + +02:message/developer[2]: + [01] + [02] + Deferred tool namespaces: + - multi_agent_v1: Tools for spawning and managing sub-agents. + +03:message/user: + +04:message/user: + Initial eager catalog: echo. +-- request 2 (turn) -- +05:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` +06:message/user: + Nested tool addition: cwd. +-- request 3 (turn) -- +07:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +-- request 4 (turn) -- +09:message/developer: + Catalog exec removals: retain {{ literal }} text. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. + +## Window 2 (after request 4: settings changed (model)) +-- request 5 (turn) -- +00:additional_tools/developer (1; hash=b2c6697017b587f1): + - namespace/functions; hash=e9ce2d0830f744bb + - custom/exec + - function/wait + - function/request_user_input +01:message/developer: + +02:message/developer[2]: + [01] + [02] + Deferred tool namespaces: + - multi_agent_v1: Tools for spawning and managing sub-agents. + +03:message/user: + +04:message/user: + Initial eager catalog: echo. +05:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` +06:message/user: + Nested tool addition: cwd. +07:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +09:message/developer: + Catalog exec removals: retain {{ literal }} text. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +11:message/developer: + + The current model is catalog-empty-hints. + +12:message/user: + Unchanged catalog: emit no update. +-- request 6 (turn) -- +13:message/developer: + - Namespace instructions: mcp__echo_service +14:message/user: + Namespace instructions removal: keep echo. +-- request 7 (turn) -- +15:message/developer: + - Tool: tools.mcp__echo_service__echo +16:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +17:message/user: + Eager to deferred: echo. +-- request 8 (turn) -- +18:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +19:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +20:message/user: + Deferred to eager: echo. +-- request 9 (turn) -- +21:message/developer: + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +22:message/user: + Remove all echo service entries. +-- request 10 (turn) -- +23:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +24:message/user: + Re-enable echo service entries: echo and cwd. + +## Window 3 (after request 10: settings changed (model)) +-- request 11 (turn) -- +00:additional_tools/developer (1; hash=b2c6697017b587f1): + - namespace/functions; hash=e9ce2d0830f744bb + - custom/exec + - function/wait + - function/request_user_input +01:message/developer: + +02:message/developer[2]: + [01] + [02] + Deferred tool namespaces: + - multi_agent_v1: Tools for spawning and managing sub-agents. + +03:message/user: + +04:message/user: + Initial eager catalog: echo. +05:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` +06:message/user: + Nested tool addition: cwd. +07:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +09:message/developer: + Catalog exec removals: retain {{ literal }} text. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +11:message/developer: + + The current model is catalog-empty-hints. + +12:message/user: + Unchanged catalog: emit no update. +13:message/developer: + - Namespace instructions: mcp__echo_service +14:message/user: + Namespace instructions removal: keep echo. +15:message/developer: + - Tool: tools.mcp__echo_service__echo +16:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +17:message/user: + Eager to deferred: echo. +18:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +19:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +20:message/user: + Deferred to eager: echo. +21:message/developer: + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +22:message/user: + Remove all echo service entries. +23:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +24:message/user: + Re-enable echo service entries: echo and cwd. +25:message/developer: + ## Namespace: functions + ### `read_mcp_resource` + Read a selected resource revision. + + exec tool declaration: + ```ts + declare const tools: { read_mcp_resource(args: { revision?: number; server: string; uri: string; }): Promise; }; + ``` +26:message/developer: + + The current model is catalog-updated. + +27:message/user: + Combined redefinition: resource revision, common instructions, and shared types. + +## Window 4 (after request 11: settings changed (model)) +-- request 12 (turn) -- +00:additional_tools/developer (1; hash=b2c6697017b587f1): + - namespace/functions; hash=e9ce2d0830f744bb + - custom/exec + - function/wait + - function/request_user_input +01:message/developer: + +02:message/developer[2]: + [01] + [02] + Deferred tool namespaces: + - multi_agent_v1: Tools for spawning and managing sub-agents. + +03:message/user: + +04:message/user: + Initial eager catalog: echo. +05:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` +06:message/user: + Nested tool addition: cwd. +07:message/developer: + Catalog exec updates: retain {{ literal }} text. + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +09:message/developer: + Catalog exec removals: retain {{ literal }} text. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +11:message/developer: + + The current model is catalog-empty-hints. + +12:message/user: + Unchanged catalog: emit no update. +13:message/developer: + - Namespace instructions: mcp__echo_service +14:message/user: + Namespace instructions removal: keep echo. +15:message/developer: + - Tool: tools.mcp__echo_service__echo +16:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +17:message/user: + Eager to deferred: echo. +18:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +19:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +20:message/user: + Deferred to eager: echo. +21:message/developer: + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +22:message/user: + Remove all echo service entries. +23:message/developer: + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. + + ### `mcp__echo_service__cwd` + Return the current working directory of this test server process. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__cwd(args: {}): Promise>; }; + ``` + + ### `mcp__echo_service__echo` + Echo back the provided message and include environment data. + + exec tool declaration: + ```ts + declare const tools: { mcp__echo_service__echo(args: { env_var?: string; message: string; }): Promise>; }; + ``` +24:message/user: + Re-enable echo service entries: echo and cwd. +25:message/developer: + ## Namespace: functions + ### `read_mcp_resource` + Read a selected resource revision. + + exec tool declaration: + ```ts + declare const tools: { read_mcp_resource(args: { revision?: number; server: string; uri: string; }): Promise; }; + ``` +26:message/developer: + + The current model is catalog-updated. + +27:message/user: + Combined redefinition: resource revision, common instructions, and shared types. +28:message/developer: + + The current model is catalog-compacted. + +29:message/user: + Only common instructions and shared types change: wait for compaction. +-- request 13 (compaction) -- +30:compaction_trigger + +## Window 5 (after request 13: input diverged at item 00) +-- request 14 (turn) -- +00:additional_tools/developer (1; hash=c3cdfbca78ae91e2): + - namespace/functions; hash=36087e37fe329f5c + - custom/exec + - function/wait + - function/request_user_input +01:message/developer: + +02:message/user: + Initial eager catalog: echo. +03:message/user: + Nested tool addition: cwd. +04:message/user: + Namespace instructions redefinition: durations. +05:message/user: + Nested tool removal: cwd. +06:message/user: + Unchanged catalog: emit no update. +07:message/user: + Namespace instructions removal: keep echo. +08:message/user: + Eager to deferred: echo. +09:message/user: + Deferred to eager: echo. +10:message/user: + Remove all echo service entries. +11:message/user: + Re-enable echo service entries: echo and cwd. +12:message/user: + Combined redefinition: resource revision, common instructions, and shared types. +13:message/developer[2]: + [01] + [02] + Deferred tool namespaces: + - multi_agent_v1: Tools for spawning and managing sub-agents. + +14:message/user: + +15:message/user: + Only common instructions and shared types change: wait for compaction. +16:compaction:encrypted=true; chars=18; hash=1049063b3b4d3e47 +17:message/user: + After compaction: restore the full current catalog. diff --git a/codex-rs/prompts/src/model_messages/incremental_tools.rs b/codex-rs/prompts/src/model_messages/incremental_tools.rs index 5a05d8878e..57e76181b9 100644 --- a/codex-rs/prompts/src/model_messages/incremental_tools.rs +++ b/codex-rs/prompts/src/model_messages/incremental_tools.rs @@ -11,6 +11,8 @@ pub struct ResolvedIncrementalToolMessages { pub removed_namespaces_header: String, pub namespace_instructions_prefix: String, pub namespace_instructions_cleared: String, + pub exec_tools_update_hint: String, + pub exec_tools_removed_definitions_header: String, } impl Default for ResolvedIncrementalToolMessages { @@ -46,6 +48,14 @@ impl ResolvedIncrementalToolMessages { messages.and_then(|m| m.namespace_instructions_cleared.as_ref()), "The {name} namespace no longer has additional instructions.", ), + exec_tools_update_hint: resolve( + messages.and_then(|m| m.exec_tools_update_hint.as_ref()), + "Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier version in full. Unlisted tools keep their latest definitions and `tools` methods.", + ), + exec_tools_removed_definitions_header: resolve( + messages.and_then(|m| m.exec_tools_removed_definitions_header.as_ref()), + "Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged.", + ), } } } diff --git a/codex-rs/protocol/src/openai_models/incremental_tools.rs b/codex-rs/protocol/src/openai_models/incremental_tools.rs index f051318136..12c1cde715 100644 --- a/codex-rs/protocol/src/openai_models/incremental_tools.rs +++ b/codex-rs/protocol/src/openai_models/incremental_tools.rs @@ -21,4 +21,10 @@ pub struct IncrementalToolMessages { /// Message supporting `{name}` when a namespace's instructions are cleared. #[serde(default, skip_serializing_if = "Option::is_none")] pub namespace_instructions_cleared: Option, + /// Literal hint before changed Code Mode tool definitions and namespace instructions. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exec_tools_update_hint: Option, + /// Literal header before removed Code Mode tool and namespace-instruction identities. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub exec_tools_removed_definitions_header: Option, }