From 33b3f80a0935e51a49cbbda7d6473ef757d4241b Mon Sep 17 00:00:00 2001 From: Alex Kira Date: Sat, 7 Nov 2026 19:15:33 +0000 Subject: [PATCH] Wire incremental exec tool updates into Code Mode sessions (#60163) ## Why Changes to nested tools should update the model's catalog without repeating the full `functions.exec` declaration or rewriting retained context. ## What changed Pass structured exec definitions through the tool router into the session's tool world state. Enable granular updates for Responses Lite in `CodeModeOnly` when both `IncrementalTools` and `IncrementalExecTools` are enabled. Append changed tool definitions, namespace instructions, and removals as developer messages. Keep common exec instructions and shared types frozen until compaction, then install the latest full catalog. ## Testing Add feature-gating coverage and a scenario snapshot covering tool additions and removals, namespace instruction changes, eager/deferred transitions, re-enabling tools, retained context, and catalog restoration after compaction. GitOrigin-RevId: bbf850d4e5018388eeb4c09345d885d776962e09 --- .../core/src/agent/host/invocation_tests.rs | 1 + .../context/world_state/top_level_tools.rs | 4 - codex-rs/core/src/session/tests.rs | 1 + codex-rs/core/src/session/world_state.rs | 8 +- codex-rs/core/src/tools/code_mode/mod.rs | 1 + codex-rs/core/src/tools/parallel.rs | 2 + .../core/src/tools/parallel_trace_tests.rs | 1 + codex-rs/core/src/tools/router.rs | 7 + codex-rs/core/src/tools/spec_plan.rs | 43 +- .../tests/suite/code_mode_model_messages.rs | 85 ++++ codex-rs/core/tests/suite/scenarios.rs | 2 + .../core/tests/suite/scenarios_exec_tools.rs | 305 ++++++++++++ ...s__exec_tools__incremental_exec_tools.snap | 443 ++++++++++++++++++ 13 files changed, 882 insertions(+), 21 deletions(-) create mode 100644 codex-rs/core/tests/suite/scenarios_exec_tools.rs create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools.snap diff --git a/codex-rs/core/src/agent/host/invocation_tests.rs b/codex-rs/core/src/agent/host/invocation_tests.rs index b5b39f3aac..3e0e312fef 100644 --- a/codex-rs/core/src/agent/host/invocation_tests.rs +++ b/codex-rs/core/src/agent/host/invocation_tests.rs @@ -70,6 +70,7 @@ async fn dispatch_revokes_access_and_holds_call_id_through_callbacks() -> anyhow vec![], ToolMode::Direct, Default::default(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], ); 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 04978ef8cd..772df01768 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 @@ -107,10 +107,6 @@ impl TopLevelToolsState { } /// Track granular exec updates as part of this tool catalog. - #[cfg_attr( - not(test), - expect(dead_code, reason = "Activated by the follow-up runtime integration") - )] pub(crate) fn with_incremental_exec_tools( mut self, exec_definitions: Option>>, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index e1e60bdfd7..c332c8d920 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -328,6 +328,7 @@ impl StepContext { Vec::new(), ToolMode::Direct, BTreeMap::new(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], )), diff --git a/codex-rs/core/src/session/world_state.rs b/codex-rs/core/src/session/world_state.rs index 698f25b501..72944bcb7b 100644 --- a/codex-rs/core/src/session/world_state.rs +++ b/codex-rs/core/src/session/world_state.rs @@ -208,11 +208,15 @@ impl Session { .cloned() .collect::>(); let definitions = codex_tools::create_tools_json_for_responses_lite(&specs)?; - world_state.add_section(TopLevelToolsState::new( + let mut top_level = TopLevelToolsState::new( model_messages.incremental_tools(), definitions, Some(Arc::clone(&extension_metrics)), - )?); + )?; + if let Some(entries) = step_context.tool_router.exec_definitions() { + top_level = top_level.with_incremental_exec_tools(Some(entries)); + } + world_state.add_section(top_level); } world_state.add_section(model_state); let token_budget_enabled = turn_context.config.features.enabled(Feature::TokenBudget) diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index a8c9095480..0dc3655e2b 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -497,6 +497,7 @@ mod tests { Vec::new(), ToolMode::CodeModeOnly, BTreeMap::new(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], )); diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index f11e64547b..0af29fc333 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -831,6 +831,7 @@ mod tests { Vec::new(), ToolMode::Direct, BTreeMap::new(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], )); @@ -1019,6 +1020,7 @@ mod tests { Vec::new(), ToolMode::Direct, BTreeMap::new(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], )); diff --git a/codex-rs/core/src/tools/parallel_trace_tests.rs b/codex-rs/core/src/tools/parallel_trace_tests.rs index 9863fc4f0a..1814e9540a 100644 --- a/codex-rs/core/src/tools/parallel_trace_tests.rs +++ b/codex-rs/core/src/tools/parallel_trace_tests.rs @@ -43,6 +43,7 @@ async fn spawned_dispatch_preserves_caller_context(level: LevelFilter) -> anyhow Vec::new(), ToolMode::Direct, BTreeMap::new(), + /*exec_definitions*/ None, /*tool_namespaces_info*/ None, &[], )); diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index d21d1508e3..f1121af3b5 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -101,6 +101,7 @@ pub struct ToolRouter { model_visible_specs: Arc<[ToolSpec]>, tool_mode: ToolMode, code_mode_tool_names: BTreeMap, + exec_definitions: Option>>, tool_namespaces_info: Option, can_manage_children: bool, } @@ -141,6 +142,7 @@ impl ToolRouter { model_visible_specs: Vec, tool_mode: ToolMode, code_mode_tool_names: BTreeMap, + exec_definitions: Option>>, tool_namespaces_info: Option, child_management_tools: &[ToolName], ) -> Self { @@ -149,6 +151,7 @@ impl ToolRouter { model_visible_specs: model_visible_specs.into(), tool_mode, code_mode_tool_names, + exec_definitions, tool_namespaces_info, can_manage_children: false, }; @@ -163,6 +166,10 @@ impl ToolRouter { Arc::clone(&self.model_visible_specs) } + pub(crate) fn exec_definitions(&self) -> Option>> { + self.exec_definitions.clone() + } + pub(crate) fn tool_mode(&self) -> ToolMode { self.tool_mode } diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 8c736b191c..68ddcca8f7 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -497,7 +497,7 @@ pub(crate) fn finalize_tool_router( registry.mcp_namespaces(), ) .map_err(|error| CodexErrorDetails::InvalidRequest(error.to_string()))?; - let code_mode_tool_names = + let (code_mode_tool_names, exec_definitions) = register_code_mode_executors(turn_context, model_info, &mut registry, &indirect_prefixes); let include_tool_namespaces_info = turn_context .config @@ -580,6 +580,7 @@ pub(crate) fn finalize_tool_router( model_visible_specs, tool_mode, code_mode_tool_names, + exec_definitions, tool_namespaces_info, &child_management_tools, )) @@ -1052,10 +1053,13 @@ fn register_code_mode_executors( model_info: &ModelInfo, registry: &mut ToolRegistry, indirect_prefixes: &IndirectNamespacePrefixes<'_>, -) -> BTreeMap { +) -> ( + BTreeMap, + Option>>, +) { let tool_mode = effective_tool_mode(turn_context, model_info); if !matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) { - return BTreeMap::new(); + return (BTreeMap::new(), None); } let mut code_mode_tool_names = BTreeMap::new(); @@ -1109,21 +1113,30 @@ fn register_code_mode_executors( } enabled_tools .sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions)); - let (exec_description, _) = codex_code_mode::build_exec_tool_description_and_definitions( - &enabled_tools, - &deferred_tools, - &namespace_descriptions, - turn_context.config.code_mode.default_exec_yield_time_ms, - tool_mode == ToolMode::CodeModeOnly, + let image_detail_visibility = if unified_image_budget_enabled(&turn_context.config.features, model_info) { codex_code_mode::ImageDetailVisibility::Hidden } else { codex_code_mode::ImageDetailVisibility::Visible - }, - deferred_tool_discovery, - /*incremental_exec_enabled*/ false, - model_messages.code_mode(), - ); + }; + let features = &turn_context.config.features; + let incremental_exec_enabled = model_info.use_responses_lite + && features.enabled(Feature::IncrementalTools) + && features.enabled(Feature::IncrementalExecTools) + && tool_mode == ToolMode::CodeModeOnly; + let (exec_description, exec_definitions) = + codex_code_mode::build_exec_tool_description_and_definitions( + &enabled_tools, + &deferred_tools, + &namespace_descriptions, + turn_context.config.code_mode.default_exec_yield_time_ms, + tool_mode == ToolMode::CodeModeOnly, + image_detail_visibility, + deferred_tool_discovery, + incremental_exec_enabled, + model_messages.code_mode(), + ); + let exec_definitions = incremental_exec_enabled.then(|| Arc::new(exec_definitions)); let execute_handler = CodeModeExecuteHandler::new( create_code_mode_tool(exec_description), code_mode_nested_tool_specs, @@ -1135,7 +1148,7 @@ fn register_code_mode_executors( ))); registry.prepend_trusted(Arc::new(execute_handler)); - code_mode_tool_names + (code_mode_tool_names, exec_definitions) } #[instrument(level = "trace", skip_all, fields(tool_spec_count = specs.len()))] diff --git a/codex-rs/core/tests/suite/code_mode_model_messages.rs b/codex-rs/core/tests/suite/code_mode_model_messages.rs index 4a5cc517ca..eb6dcfff75 100644 --- a/codex-rs/core/tests/suite/code_mode_model_messages.rs +++ b/codex-rs/core/tests/suite/code_mode_model_messages.rs @@ -1,8 +1,11 @@ //! Wait descriptions and parameter schemas resolve independently without changing other tools. use anyhow::Result; +use codex_features::Feature; use codex_protocol::openai_models::ToolMessages; use codex_protocol::openai_models::ToolMode; +use codex_protocol::protocol::ThreadSettingsOverrides; +use core_test_support::responses; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse_completed; use core_test_support::responses::start_mock_server; @@ -71,3 +74,85 @@ async fn code_mode_wait_overrides_are_independent( assert_eq!(requests[1].body_json()["tools"], expected); Ok(()) } + +#[test_case(false; "disabled")] +#[test_case(true; "enabled")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn exec_tools_follow_incremental_exec_flag(incremental_exec_enabled: bool) -> Result<()> { + skip_if_no_network!(Ok(())); + let server = start_mock_server().await; + let mock = mount_sse_sequence( + &server, + vec![sse_completed("response-0"), sse_completed("response-1")], + ) + .await; + let mut builder = test_codex().with_config(move |config| { + config + .features + .enable(Feature::IncrementalTools) + .expect("enable incremental tools"); + config + .features + .set_enabled(Feature::IncrementalExecTools, incremental_exec_enabled) + .expect("set incremental exec tools"); + config.code_mode.disable_in_process_fallback = true; + }); + for (slug, instructions) in [ + ("gate-initial", "Initial code instructions."), + ("gate-updated", "Updated code instructions."), + ] { + builder = builder.with_model_info_override(slug, move |model| { + model.use_responses_lite = true; + model.tool_mode = Some(ToolMode::CodeModeOnly); + model.supports_search_tool = false; + model.model_messages.as_mut().expect("model messages").tools = Some( + serde_json::from_value(json!({ + "code_mode": {"exec": {"description": instructions}}, + })) + .expect("code mode messages"), + ); + }); + } + let test = builder + .with_model("gate-initial") + .build_with_auto_env(&server) + .await?; + test.submit_turn("Begin with the original instructions.") + .await?; + core_test_support::submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + model: Some("gate-updated".to_string()), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("Continue with changed instructions.") + .await?; + let definitions = mock + .requests() + .iter() + .map(|request| { + request + .inputs_of_type("additional_tools") + .iter() + .rev() + .find_map(|item| responses::namespace_child_tool(item, "functions", "exec")) + .expect("exec definition") + .clone() + }) + .collect::>(); + assert_eq!(definitions.len(), 2); + let initial_description = definitions[0]["description"].as_str().expect("description"); + assert!(initial_description.starts_with("Initial code instructions.")); + let mut expected = definitions[0].clone(); + if !incremental_exec_enabled { + expected["description"] = json!(initial_description.replacen( + "Initial code instructions.", + "Updated code instructions.", + 1 + )); + } + assert_eq!(definitions[1], expected); + Ok(()) +} diff --git a/codex-rs/core/tests/suite/scenarios.rs b/codex-rs/core/tests/suite/scenarios.rs index b0a929886e..45dca5128e 100644 --- a/codex-rs/core/tests/suite/scenarios.rs +++ b/codex-rs/core/tests/suite/scenarios.rs @@ -125,6 +125,8 @@ mod audio_file; #[path = "scenarios_retained_tool_outputs.rs"] mod retained_tool_outputs; +#[path = "scenarios_exec_tools.rs"] +mod exec_tools; #[path = "scenarios_incremental_tools.rs"] mod incremental_tools; #[path = "scenarios_incremental_tools_resume.rs"] diff --git a/codex-rs/core/tests/suite/scenarios_exec_tools.rs b/codex-rs/core/tests/suite/scenarios_exec_tools.rs new file mode 100644 index 0000000000..4d4baa0b7b --- /dev/null +++ b/codex-rs/core/tests/suite/scenarios_exec_tools.rs @@ -0,0 +1,305 @@ +//! Exec tools append changed entries and refresh frozen exec sections only at compaction. + +use anyhow::Result; +use codex_features::Feature; +use codex_protocol::openai_models::ToolMode; +use codex_protocol::protocol::Op; +use codex_protocol::protocol::ThreadSettingsOverrides; +use core_test_support::context_snapshot; +use core_test_support::context_snapshot::ContextSnapshotOptions; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use core_test_support::skip_if_wine_exec; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_mcp_server; +use core_test_support::wait_for_successful_turn; +use pretty_assertions::assert_eq; +use serde_json::json; +use std::collections::HashMap; + +use super::super::rmcp_client::remote_aware_environment_id; +use super::super::rmcp_client::remote_aware_stdio_server_bin; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn changed_sections_and_entries_append_without_repeating_the_catalog() -> 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; + let command = remote_aware_stdio_server_bin()?; + let environment_id = remote_aware_environment_id(); + let server_config = |tools: &[&str], instructions: &str, omitted: &[&str]| { + serde_json::from_value(json!({ + "command": command, + "environment_id": environment_id, + "env": {"MCP_TEST_SERVER_INSTRUCTIONS": instructions}, + "enabled_tools": tools, + "omit_tools_from": omitted, + "default_tools_approval_mode": "approve", + })) + }; + let original_instructions = "Echo service instructions."; + let updated_instructions = "Echo service instructions. Durations are milliseconds."; + 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"); + } + 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| { + model.use_responses_lite = true; + model.tool_mode = Some(ToolMode::CodeModeOnly); + model.supports_search_tool = true; + }); + for (slug, description, deferred_guidance, shared_types) in [ + ( + "catalog-updated", + "Run JavaScript. Await every tool call before returning.", + "Find deferred tools in ALL_TOOLS.", + "type CallToolResult = { content: unknown[]; structuredContent?: T; isError?: boolean };", + ), + ( + "catalog-compacted", + "Run JavaScript. Await every tool call and inspect its result before returning.", + "Find deferred tools by name in ALL_TOOLS.", + "type CallToolResult = { content: unknown[]; structuredContent?: T; isError?: boolean; _meta?: unknown };", + ), + ] { + 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; + model.model_messages.as_mut().expect("model messages").tools = Some( + serde_json::from_value(json!({ + "code_mode": { + "exec": {"description": description}, + "deferred_nested_tools_guidance": deferred_guidance, + "mcp_typescript_preamble": shared_types, + }, + "mcp_resources": {"read_mcp_resource": { + "description": "Read a selected resource revision.", + "parameters": json!({ + "type": "object", + "properties": {"server": {"type": "string"}, "uri": {"type": "string"}, "revision": {"type": "integer"}}, + "required": ["server", "uri"], + "additionalProperties": false, + }).to_string(), + }}, + })).expect("updated tool messages"), + ); + }); + } + let test = builder + .with_model("catalog-initial") + .build_with_auto_env(&server) + .await?; + wait_for_mcp_server(&test.codex, "echo_service").await?; + let mut events = (0..12) + .map(|index| responses::sse_completed(&format!("response-{index}"))) + .collect::>(); + events.push(responses::sse(vec![ + json!({"type": "response.output_item.done", "item": { + "type": "compaction", "encrypted_content": "CATALOG_CHECKPOINT", + }}), + responses::ev_completed("response-12"), + ])); + events.push(responses::sse_completed("response-13")); + let mock = responses::mount_sse_sequence(&server, events).await; + test.submit_text_turn("Initial eager catalog: echo.") + .await?; + // Refresh through the public MCP configuration and call boundaries, as production does. + let ready = Some(json!({"message": "catalog ready"})); + for (tools, instructions, prompt) in [ + ( + vec!["echo", "cwd"], + original_instructions, + "Nested tool addition: cwd.", + ), + ( + vec!["echo", "cwd"], + updated_instructions, + "Namespace instructions redefinition: durations.", + ), + ( + vec!["echo"], + updated_instructions, + "Nested tool removal: cwd.", + ), + ] { + let current = test.codex.config().await; + let mut updated = current.as_ref().clone(); + updated.mcp_servers.set(HashMap::from([( + "echo_service".to_string(), + server_config(&tools, instructions, &["deferred"])?, + )]))?; + let _ = test.codex.refresh_mcp_config(current, updated).await; + test.codex + .call_mcp_tool("echo_service", "echo", ready.clone(), /*meta*/ None) + .await?; + test.submit_text_turn(prompt).await?; + } + test.submit_text_turn("Unchanged catalog: emit no update.") + .await?; + + for (tools, instructions, omitted, prompt) in [ + ( + vec!["echo"], + "", + vec!["deferred"], + "Namespace instructions removal: keep echo.", + ), + (vec!["echo"], "", vec![], "Eager to deferred: echo."), + ( + vec!["echo"], + updated_instructions, + vec!["deferred"], + "Deferred to eager: echo.", + ), + (vec![], "", vec![], "Remove all echo service entries."), + ( + vec!["echo", "cwd"], + updated_instructions, + vec!["deferred"], + "Re-enable echo service entries: echo and cwd.", + ), + ] { + let current = test.codex.config().await; + let mut updated = current.as_ref().clone(); + updated.mcp_servers.set(if tools.is_empty() { + HashMap::new() + } else { + HashMap::from([( + "echo_service".to_string(), + server_config(&tools, instructions, &omitted)?, + )]) + })?; + let _ = test.codex.refresh_mcp_config(current, updated).await; + let result = test + .codex + .call_mcp_tool("echo_service", "echo", ready.clone(), /*meta*/ None) + .await; + if tools.is_empty() { + assert!(result.is_err(), "removed tools must not remain callable"); + } else { + result?; + } + test.submit_text_turn(prompt).await?; + } + core_test_support::submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + model: Some("catalog-updated".to_string()), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn( + "Combined redefinition: resource revision, common instructions, and shared types.", + ) + .await?; + core_test_support::submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + model: Some("catalog-compacted".to_string()), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("Only common instructions and shared types change: wait for compaction.") + .await?; + test.codex.submit(Op::Compact).await?; + wait_for_successful_turn(&test.codex).await; + test.submit_text_turn("After compaction: restore the full current catalog.") + .await?; + + let requests = mock.requests(); + assert_eq!(requests.len(), 14); + let mut previous = Vec::new(); + let mut declarations = Vec::new(); + // Snapshots normalize IDs and metadata, so check the retained raw prefix separately. + for (index, request) in requests.iter().enumerate() { + let input = request.input(); + let suffix = if index == 13 { + input.as_slice() + } else { + assert_eq!( + &input[..previous.len()], + previous.as_slice(), + "retained context changed" + ); + &input[previous.len()..] + }; + declarations.push( + suffix + .iter() + .filter(|item| item["type"] == "additional_tools") + .filter_map(|item| responses::namespace_child_tool(item, "functions", "exec")) + .cloned() + .collect::>(), + ); + previous = input; + } + // Native declarations are fingerprinted in snapshots; keep their invariants readable here. + let mut native_fields = declarations[0][0].clone(); + native_fields["description"] = json!(null); + for exec in declarations.iter().flatten() { + let mut actual = exec.clone(); + actual["description"] = json!(null); + assert_eq!(actual, native_fields, "compaction preserves native fields"); + } + for request in &requests[10..13] { + let retained_input = serde_json::to_string(&request.input())?; + for frozen in [ + "Run JavaScript. Await every tool call", + "Find deferred tools in ALL_TOOLS.", + "Find deferred tools by name in ALL_TOOLS.", + "content: unknown[]; structuredContent?: T; isError?: boolean", + ] { + assert!( + !retained_input.contains(frozen), + "frozen documentation appeared before compaction: {frozen}" + ); + } + } + let restored = declarations[13][0]["description"] + .as_str() + .expect("compacted catalog"); + for expected in [ + "Await every tool call and inspect its result", + "Find deferred tools by name in ALL_TOOLS.", + "isError?: boolean; _meta?: unknown", + "revision?: number", + "### `mcp__echo_service__echo`", + "### `mcp__echo_service__cwd`", + updated_instructions, + ] { + assert!(restored.contains(expected), "compaction omitted {expected}"); + } + 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.", + &requests, + &ContextSnapshotOptions::default().rewrite_known_segments(), + ); + insta::assert_snapshot!("incremental_exec_tools", snapshot); + Ok(()) +} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools.snap new file mode 100644 index 0000000000..d89e7b6ba8 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__exec_tools__incremental_exec_tools.snap @@ -0,0 +1,443 @@ +--- +source: core/tests/suite/scenarios_exec_tools.rs +expression: snapshot +--- +Scenario: 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. + +## 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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +-- request 4 (turn) -- +09:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +-- request 5 (turn) -- +11:message/user: + Unchanged catalog: emit no update. +-- request 6 (turn) -- +12:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service +13:message/user: + Namespace instructions removal: keep echo. +-- request 7 (turn) -- +14:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__echo +15:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +16:message/user: + Eager to deferred: echo. +-- request 8 (turn) -- +17:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +18:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +19:message/user: + Deferred to eager: echo. +-- request 9 (turn) -- +20:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +21:message/user: + Remove all echo service entries. +-- request 10 (turn) -- +22:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +23:message/user: + Re-enable echo service entries: echo and cwd. + +## Window 2 (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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +09:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +11:message/user: + Unchanged catalog: emit no update. +12:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service +13:message/user: + Namespace instructions removal: keep echo. +14:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__echo +15:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +16:message/user: + Eager to deferred: echo. +17:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +18:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +19:message/user: + Deferred to eager: echo. +20:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +21:message/user: + Remove all echo service entries. +22:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +23:message/user: + Re-enable echo service entries: echo and cwd. +24:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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; }; + ``` +25:message/developer: + + The current model is catalog-updated. + +26:message/user: + Combined redefinition: resource revision, common instructions, and shared types. + +## Window 3 (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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## Namespace: functions + ## mcp__echo_service + Echo service instructions. Durations are milliseconds. +08:message/user: + Namespace instructions redefinition: durations. +09:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__cwd +10:message/user: + Nested tool removal: cwd. +11:message/user: + Unchanged catalog: emit no update. +12:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service +13:message/user: + Namespace instructions removal: keep echo. +14:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Tool: tools.mcp__echo_service__echo +15:message/developer: + + Added deferred tool namespaces: + - mcp__echo_service + +16:message/user: + Eager to deferred: echo. +17:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +18:message/developer: + + Removed deferred tool namespaces: + - mcp__echo_service + +19:message/user: + Deferred to eager: echo. +20:message/developer: + Listed `Tool:` entries are unavailable until redefined or rediscovered. `Namespace instructions:` removes guidance only. Unlisted entries are unchanged. + - Namespace instructions: mcp__echo_service + - Tool: tools.mcp__echo_service__echo +21:message/user: + Remove all echo service entries. +22:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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>; }; + ``` +23:message/user: + Re-enable echo service entries: echo and cwd. +24:message/developer: + Use these definitions and namespace instructions with `functions.exec`. Each replaces its earlier versio...ls keep their latest definitions and `tools` methods. [hash=4d13008df632810d] + + ## 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; }; + ``` +25:message/developer: + + The current model is catalog-updated. + +26:message/user: + Combined redefinition: resource revision, common instructions, and shared types. +27:message/developer: + + The current model is catalog-compacted. + +28:message/user: + Only common instructions and shared types change: wait for compaction. +-- request 13 (compaction) -- +29:compaction_trigger + +## Window 4 (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.