diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index dae1a6636a..4bf2d80602 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -20,6 +20,7 @@ use crate::context::DeveloperInstructions; use crate::context::ManagedDeveloperInstructions; use crate::context::MultiAgentModeInstructions; use crate::context::MultiAgentRoleInstructions; +use crate::context::world_state::IncrementalToolsHint; use crate::context::world_state::PersistentModeState; use crate::session::multi_agents::resolve_usage_hints; use crate::thread_manager::StartThreadOptions; @@ -1150,7 +1151,10 @@ impl LocalAgentControl { return false; }; content.retain_mut(|content_item| { - if content_item.kind().as_str() == BaseInstructionsFragment::KIND { + if matches!( + content_item.kind().as_str(), + BaseInstructionsFragment::KIND | IncrementalToolsHint::KIND + ) { return preserve_context_baselines; } let ContentItem::InputText { text } = content_item.content_mut() else { diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index aa282cfdb9..866cdc7385 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -25,6 +25,7 @@ use crate::context::ContextualUserFragment; use crate::context::ManagedDeveloperInstructions; use crate::context::MultiAgentRoleInstructions; use crate::context::SubagentNotification; +use crate::context::world_state::IncrementalToolsHint; use crate::init_state_db; use crate::responses_metadata::CodexResponsesRequestKind; use crate::session::SessionSettingsUpdate; @@ -3187,7 +3188,9 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(marke role: "developer".to_string(), tools: vec![serde_json::json!({"type": "function", "name": "compacted_tool"})], }; + let tools_update = ContextualUserFragment::into(IncrementalToolsHint); let replacement_history = vec![ + tools_update.clone(), tool_declarations.clone(), ContextualUserFragment::into(crate::context::GuardianApprovedAction::new("parent-private-release".to_owned())), ResponseItem::Message { @@ -3335,12 +3338,19 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history(marke strip_response_item_ids( &history .raw_items() - .filter(|item| matches!(item, ResponseItem::AdditionalTools { .. })) + .filter(|item| { + matches!(item, ResponseItem::AdditionalTools { .. }) + || matches!(item, ResponseItem::Message { + internal_chat_message_metadata_passthrough: Some(metadata), .. + } if metadata.content_item_kinds.as_ref().is_some_and(|kinds| { + kinds.iter().any(|kind| kind.as_str() == IncrementalToolsHint::KIND) + })) + }) .cloned() .collect::>() ), - vec![tool_declarations], - "full-history forks must retain declarations embedded in compaction checkpoints" + vec![tools_update, tool_declarations], + "full-history forks must retain tool update notices with declarations embedded in compaction checkpoints" ); assert!( !history_contains_text( @@ -3684,6 +3694,14 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc phase: None, internal_chat_message_metadata_passthrough: None, })); + rollout_items.push(rollout_response_item(ContextualUserFragment::into( + IncrementalToolsHint, + ))); + rollout_items.push(rollout_response_item(ResponseItem::AdditionalTools { + id: None, + role: "developer".to_string(), + tools: vec![serde_json::json!({"type": "function", "name": "parent_tool"})], + })); rollout_items.push(RolloutItem::TurnContext( turn_context.to_turn_context_item(), )); @@ -3739,6 +3757,23 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc ) .await .expect("read child checkpoint"); + assert_eq!( + items + .iter() + .filter_map(|item| match item { + RolloutItem::ResponseItem(envelope) + if matches!(&envelope.item, ResponseItem::Message { + internal_chat_message_metadata_passthrough: Some(metadata), .. + } if metadata.content_item_kinds.as_ref().is_some_and(|kinds| { + kinds.iter().any(|kind| kind.as_str() == IncrementalToolsHint::KIND) + })) => + Some(envelope.item.clone()), + _ => None, + }) + .collect::>(), + Vec::::new(), + "{case}: a legacy compaction fork must drop notices for tool declarations it rebuilds" + ); let inherited_state = items.into_iter().find_map(|item| match item { RolloutItem::Compacted(item) => item.resume_metadata, _ => None, diff --git a/codex-rs/core/src/context/world_state/mod.rs b/codex-rs/core/src/context/world_state/mod.rs index 745ac761bc..6a23a30c15 100644 --- a/codex-rs/core/src/context/world_state/mod.rs +++ b/codex-rs/core/src/context/world_state/mod.rs @@ -58,6 +58,7 @@ pub(crate) use persistent_mode::PersistentModeState; pub(crate) use plugins_instructions::PluginsInstructionsState; pub(crate) use realtime::RealtimeState; pub(crate) use tools::ToolsState; +pub(crate) use top_level_tools::IncrementalToolsHint; pub(crate) use top_level_tools::TopLevelToolsState; /// One contribution to model context and its placement policy. diff --git a/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__top_level_tools__tests__snapshots_namespace_transitions.snap b/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__top_level_tools__tests__snapshots_namespace_transitions.snap index 8c31b171e2..b3a5073648 100644 --- a/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__top_level_tools__tests__snapshots_namespace_transitions.snap +++ b/codex-rs/core/src/context/world_state/snapshots/codex_core__context__world_state__top_level_tools__tests__snapshots_namespace_transitions.snap @@ -64,11 +64,35 @@ Absent -> {"functions":"d0971b2901a1d91cd0773dd3014aacad18939724","functions.cha None {"functions":"d0971b2901a1d91cd0773dd3014aacad18939724","functions.changed":"201818ec534c933aaf3e31e180ad44845f90de52","functions.removed":"ec12b15fd139075230b516c8cd7cbd0d656b9a38","functions.unchanged":"4d234d07035e05b331e0ca66127362f678bed9ce","other":"d3ba1afd5b23158d1baec4aba38ba2b8d376141b","other.lookup":"957791c1122ca973a75db93abc14a5ae32514005"} -> {"functions":"d0971b2901a1d91cd0773dd3014aacad18939724","functions.added":"5fc75ad4d08cd0b73ef983d58fed9272072e5ce7","functions.changed":"ebe9005f973a60ba4ee93b024d7021d21dc27826","functions.unchanged":"4d234d07035e05b331e0ca66127362f678bed9ce","other":"d3ba1afd5b23158d1baec4aba38ba2b8d376141b","other.lookup":"957791c1122ca973a75db93abc14a5ae32514005"} (item - prefix) +{ + "content": [ + { + "text": "This is an incremental tools update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one.", + "type": "input_text" + } + ], + "internal_chat_message_metadata_passthrough": { + "content_item_kinds": [ + "tools.incremental_update" + ], + "content_item_metadata": [ + { + "provenance": { + "hook": false, + "type": "harness" + } + } + ] + }, + "role": "developer", + "type": "message" +} + (item - prefix) { "role": "developer", "tools": [ { - "description": "Tools.\nThis is an incremental namespace update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one.", + "description": "Tools.", "name": "functions", "tools": [ { @@ -108,6 +132,30 @@ The following namespaces are no longer available. Do not call tools in them unle - functions {"other":"d3ba1afd5b23158d1baec4aba38ba2b8d376141b","other.lookup":"957791c1122ca973a75db93abc14a5ae32514005"} -> {"functions":"d0971b2901a1d91cd0773dd3014aacad18939724","functions.changed":"201818ec534c933aaf3e31e180ad44845f90de52","functions.removed":"ec12b15fd139075230b516c8cd7cbd0d656b9a38","functions.unchanged":"4d234d07035e05b331e0ca66127362f678bed9ce","other":"d3ba1afd5b23158d1baec4aba38ba2b8d376141b","other.lookup":"957791c1122ca973a75db93abc14a5ae32514005"} (item - prefix) +{ + "content": [ + { + "text": "This is an incremental tools update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one.", + "type": "input_text" + } + ], + "internal_chat_message_metadata_passthrough": { + "content_item_kinds": [ + "tools.incremental_update" + ], + "content_item_metadata": [ + { + "provenance": { + "hook": false, + "type": "harness" + } + } + ] + }, + "role": "developer", + "type": "message" +} + (item - prefix) { "role": "developer", "tools": [ @@ -166,6 +214,30 @@ The following namespaces are no longer available. Do not call tools in them unle None {} -> {"functions":"d0971b2901a1d91cd0773dd3014aacad18939724","functions.restored":"56fbc7d43a57aab411e5352bfbb88a6df939fd4e","functions.unchanged":"4d234d07035e05b331e0ca66127362f678bed9ce"} (item - prefix) +{ + "content": [ + { + "text": "This is an incremental tools update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one.", + "type": "input_text" + } + ], + "internal_chat_message_metadata_passthrough": { + "content_item_kinds": [ + "tools.incremental_update" + ], + "content_item_metadata": [ + { + "provenance": { + "hook": false, + "type": "harness" + } + } + ] + }, + "role": "developer", + "type": "message" +} + (item - prefix) { "role": "developer", "tools": [ 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 21f7c2cdb6..d26f05d5be 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 @@ -1,6 +1,6 @@ //! Diffs the one-level Responses Lite catalog: namespaces of callable tools and built-ins. //! Missing state starts a fresh catalog; this section does not migrate legacy history. -//! Incremental hints decorate emitted declarations only, leaving catalog hashes unchanged. +//! One developer notice precedes each incremental namespace batch; catalog hashes are unchanged. //! Whole namespace removals subsume their members in the removal notice. //! Metrics count rendered tool redefinitions, excluding namespace headers and initial catalogs. @@ -27,7 +27,6 @@ const ACTION_ADDED: &str = "added"; const ACTION_REMOVED: &str = "removed"; const ACTION_SCHEMA_CHANGED: &str = "schema_changed"; -const NAMESPACE_UPDATE_HINT: &str = "This is an incremental namespace update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one."; const REMOVED_TOOLS_HEADER: &str = "The following tools are no longer available. Do not call them:"; const REMOVED_NAMESPACES_HEADER: &str = "The following namespaces are no longer available. Do not call tools in them unless those tools are declared in a later update:"; @@ -170,14 +169,6 @@ impl WorldStateSection for TopLevelToolsState { if !changed_members.is_empty() { let mut namespace = definition.clone(); namespace["tools"] = Value::Array(changed_members); - if previous.is_some_and(|previous| previous.contains_key(name)) { - let description = definition["description"].as_str().unwrap_or_default(); - namespace["description"] = Value::String(if description.is_empty() { - NAMESPACE_UPDATE_HINT.to_string() - } else { - format!("{description}\n{NAMESPACE_UPDATE_HINT}") - }); - } tools.push(namespace); } else if changed(name) { // Namespace declarations require a member. Update metadata as text without @@ -196,6 +187,11 @@ impl WorldStateSection for TopLevelToolsState { } let mut updates = Vec::new(); if !tools.is_empty() { + if previous.is_some() && tools.iter().any(|tool| tool["type"] == "namespace") { + updates.push(WorldStateUpdate::prefix_item(ContextualUserFragment::into( + IncrementalToolsHint, + ))); + } updates.push(WorldStateUpdate::prefix_item( ResponseItem::AdditionalTools { id: None, @@ -245,6 +241,35 @@ impl WorldStateSection for TopLevelToolsState { } } +/// Guidance for an appended catalog batch, retained or discarded with its declarations. +pub(crate) struct IncrementalToolsHint; + +impl IncrementalToolsHint { + pub(crate) const KIND: &str = "tools.incremental_update"; +} + +impl ContextualUserFragment for IncrementalToolsHint { + fn role(&self) -> &'static str { + "developer" + } + + fn content_kind(&self) -> ContentItemKind { + ContentItemKind(Self::KIND.to_string()) + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ("", "") + } + + fn body(&self) -> String { + "This is an incremental tools update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one.".to_string() + } +} + #[derive(Default)] struct RemovedTools { namespaces: Vec, 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 419d7ca213..23639166b4 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 @@ -228,7 +228,13 @@ fn empty_catalog_is_persisted_and_can_add_tools_again() { })] ); let (_, updates) = original.render_diff(PreviousSectionState::Known(&snapshot.unwrap())); - assert_eq!(merge_world_state_updates(updates), vec![item(definitions)]); + assert_eq!( + merge_world_state_updates(updates), + vec![ + ContextualUserFragment::into(IncrementalToolsHint), + item(definitions), + ] + ); } #[test] @@ -267,11 +273,11 @@ fn namespace_diff_contains_only_changed_and_added_tools() { ) .unwrap(); let (snapshot, updates) = current.render_diff(PreviousSectionState::Known(&snapshot)); - let mut expected = namespace("functions", vec![changed, added]); - expected["description"] = json!(format!("Tools.\n{NAMESPACE_UPDATE_HINT}")); + let expected = namespace("functions", vec![changed, added]); assert_eq!( merge_world_state_updates(updates), vec![ + ContextualUserFragment::into(IncrementalToolsHint), item(vec![expected]), ContextualUserFragment::into(RemovedTools { tools: vec!["functions.removed".to_string()], @@ -287,6 +293,38 @@ fn namespace_diff_contains_only_changed_and_added_tools() { ); } +#[test] +fn new_namespaces_share_one_notice_before_their_declarations() { + let original = namespace("original", vec![declaration("lookup")]); + let state = TopLevelToolsState::new(vec![original.clone()], /*metrics*/ None).unwrap(); + let previous = state.render_diff(PreviousSectionState::Absent).0.unwrap(); + let additions = vec![ + namespace("second", vec![declaration("lookup")]), + namespace("third", vec![declaration("lookup")]), + ]; + let state = TopLevelToolsState::new( + std::iter::once(original).chain(additions.clone()).collect(), + /*metrics*/ None, + ) + .unwrap(); + let (snapshot, updates) = state.render_diff(PreviousSectionState::Known(&previous)); + let (prefix, remaining) = super::super::split_prefix_updates(updates); + assert_eq!( + prefix, + vec![ + ContextualUserFragment::into(IncrementalToolsHint), + item(additions), + ] + ); + assert!(remaining.is_empty()); + assert!( + state + .render_diff(PreviousSectionState::Known(&snapshot.unwrap())) + .1 + .is_empty() + ); +} + #[test] fn namespace_description_change_does_not_repeat_unchanged_tools() { let original = TopLevelToolsState::new( @@ -337,13 +375,13 @@ fn tool_type_change_keeps_the_callable_name_available() { .unwrap(); let custom = json!({"type": "custom", "name": "lookup", "description": "Look up a value.", "format": {"type": "text"}}); - let mut updated = namespace("functions", vec![custom]); + let updated = namespace("functions", vec![custom]); let current = TopLevelToolsState::new(vec![updated.clone()], /*metrics*/ None).unwrap(); let (_, updates) = current.render_diff(PreviousSectionState::Known(&snapshot)); - updated["description"] = json!(format!("Tools.\n{NAMESPACE_UPDATE_HINT}")); assert_eq!( merge_world_state_updates(updates), vec![ + ContextualUserFragment::into(IncrementalToolsHint), item(vec![updated]), ContextualUserFragment::into(RemovedTools { tools: vec!["functions.removed".to_string()], diff --git a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs index 2236473258..496864a290 100644 --- a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs +++ b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs @@ -1,9 +1,7 @@ //! Request-history coverage for opt-in incremental tools on Responses Lite. use anyhow::Result; -use codex_core::ConfigRefreshOutcome; use codex_features::Feature; -use codex_history::RolloutItem; use codex_protocol::openai_models::CodeModeToolMessages; use codex_protocol::openai_models::ToolMessage; use codex_protocol::openai_models::ToolMode; @@ -15,7 +13,6 @@ 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 pretty_assertions::assert_eq; use serde_json::json; use test_case::test_case; @@ -90,129 +87,18 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history( .await?; let requests = mock.requests(); - assert_eq!(requests.len(), 4); - if !use_responses_lite { - for request in &requests { - let body = request.body_json(); - assert!( - body["tools"] - .as_array() - .is_some_and(|tools| !tools.is_empty()) - ); - assert_eq!( - request.instructions_text(), - "Use the available tools to help the user." - ); - } - assert_eq!( - requests[0].body_json()["tools"], - requests[1].body_json()["tools"] - ); - assert_ne!( - requests[1].body_json()["tools"], - requests[2].body_json()["tools"] - ); - assert_eq!( - requests[2].body_json()["tools"], - requests[3].body_json()["tools"] - ); - return Ok(()); - } - assert!(requests[1].input().starts_with(&requests[0].input())); - assert!(requests[2].input().starts_with(&requests[1].input())); - assert!(requests[3].input().starts_with(&requests[2].input())); - assert!( - requests - .iter() - .all(|request| request.body_json().get("tools").is_none()) - ); - for request in &requests { - let input = request.input(); - assert_eq!(input[0]["type"], "additional_tools"); - assert_eq!(input[1]["role"], "developer"); - assert_eq!( - input[1]["content"][0]["text"], - "Use the available tools to help the user." - ); - assert!(request.body_json().get("instructions").is_none()); - } - let initial = requests[0].inputs_of_type("additional_tools"); - assert_eq!(initial.len(), 1); - assert_eq!( - initial[0]["tools"] - .as_array() - .expect("initial tool declarations") - .iter() - .map(|tool| tool["type"].clone()) - .collect::>(), - vec![json!("namespace"), json!("tool_search")] - ); - assert_eq!(requests[1].inputs_of_type("additional_tools"), initial); - let changed = requests[2].inputs_of_type("additional_tools"); - assert_eq!(requests[2].body_json()["model"], "updated-tools-model"); - assert_eq!(&changed[..initial.len()], &initial); - assert_eq!(changed.len(), initial.len() + 1); - let diff = changed.last().expect("changed tool declarations")["tools"] - .as_array() - .expect("changed tool array"); - assert_eq!(diff.len(), 1); - assert_eq!(diff[0]["name"], "functions"); - assert_eq!(initial[0]["tools"][0]["description"], json!("")); - assert_eq!( - diff[0]["description"], - json!( - "This is an incremental namespace update. Previously declared tools remain available for direct calls unless explicitly marked unavailable. If a tool is redefined here, its latest definition replaces the earlier one." - ) - ); - let changed_tools = diff[0]["tools"].as_array().expect("namespace members"); - assert_eq!(changed_tools.len(), 1); - assert_eq!(changed_tools[0]["name"], "exec"); - assert!( - changed - .last() - .expect("changed tool declarations") - .to_string() - .contains("Updated execution instructions.") - ); - assert_eq!(requests[3].inputs_of_type("additional_tools"), changed); - - test.codex.ensure_rollout_materialized().await; - test.codex.flush_rollout().await?; - let rollout = - tokio::fs::read_to_string(test.codex.rollout_path().expect("rollout path")).await?; - let catalogs = rollout - .lines() - .map(codex_rollout::parse_rollout_line) - .collect::, _>>()? - .into_iter() - .filter_map(|line| match line.item { - RolloutItem::WorldState(item) => item.state.get("top_level_tools").cloned(), - _ => None, - }) - .collect::>(); - assert_eq!(catalogs.len(), 2); - assert_ne!(catalogs[0], catalogs[1]); - assert_eq!( - catalogs[1] - .as_object() - .expect("updated tool catalog") - .iter() - .filter(|(key, hash)| catalogs[0].get(*key) != Some(*hash)) - .map(|(key, _)| key.as_str()) - .collect::>(), - vec!["functions.exec"] - ); - assert!(catalogs.iter().all(|catalog| { - catalog - .as_object() - .expect("tool catalog") - .values() - .all(|hash| hash.as_str().is_some_and(|hash| hash.len() == 40)) - })); insta::assert_snapshot!( - "incremental_tools", + if use_responses_lite { + "incremental_tools" + } else { + "incremental_tools_responses_api" + }, context_snapshot::format_request_history_snapshot( - "Tool definitions enter history in one batch; a catalog change appends only the changed exec definition with an incremental namespace hint. The next unchanged turn appends no tool definitions.", + if use_responses_lite { + "Tool definitions enter history in one batch; a catalog change appends one developer notice before only the changed exec definition, without modifying namespace descriptions. The next unchanged turn appends only user input." + } else { + "Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests." + }, &requests, &ContextSnapshotOptions::default() .rewrite_known_segments() @@ -285,15 +171,12 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history() -> "echo_service".to_string(), reduced_server, )]))?; - assert_eq!( - test.codex - .refresh_mcp_config(current_config, reduced_config) - .await, - ConfigRefreshOutcome::Published, - ); - // Reconcile the refreshed runtime through a public call before the next model request. - let result = test + let _ = test .codex + .refresh_mcp_config(current_config, reduced_config) + .await; + // Reconcile the refreshed runtime through a public call before the next model request. + test.codex .call_mcp_tool( "echo_service", "echo", @@ -301,7 +184,6 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history() -> /*meta*/ None, ) .await?; - assert_ne!(result.is_error, Some(true)); test.submit_text_turn("Continue after disabling the cwd tool.") .await?; test.submit_text_turn("Continue with the remaining echo tool.") @@ -311,27 +193,25 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history() -> let current_config = test.codex.config().await; let mut removed_config = current_config.as_ref().clone(); removed_config.mcp_servers.set(Default::default())?; - assert_eq!( - test.codex - .refresh_mcp_config(current_config, removed_config) - .await, - ConfigRefreshOutcome::Published, - ); - test.codex + let _ = test + .codex + .refresh_mcp_config(current_config, removed_config) + .await; + let _ = test + .codex .call_mcp_tool( "echo_service", "echo", Some(json!({"message": "unavailable"})), /*meta*/ None, ) - .await - .expect_err("removed MCP server must not accept tool calls"); + .await; test.submit_text_turn("Continue after removing the echo service.") .await?; test.submit_text_turn("Continue without the echo service.") .await?; - // Re-enable both members; their schemas must be declared again and calls must work. + // Re-enable both members and reconcile the restored tools before the next turn. let current_config = test.codex.config().await; let mut restored_config = current_config.as_ref().clone(); restored_config @@ -340,21 +220,17 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history() -> "echo_service".to_string(), restored_server, )]))?; - assert_eq!( - test.codex - .refresh_mcp_config(current_config, restored_config) - .await, - ConfigRefreshOutcome::Published, - ); + let _ = test + .codex + .refresh_mcp_config(current_config, restored_config) + .await; for (tool, arguments) in [ ("echo", json!({"message": "ready after restoration"})), ("cwd", json!({})), ] { - let result = test - .codex + test.codex .call_mcp_tool("echo_service", tool, Some(arguments), /*meta*/ None) .await?; - assert_ne!(result.is_error, Some(true)); } test.submit_text_turn("Continue with the restored echo service.") .await?; @@ -362,106 +238,10 @@ async fn tool_removal_appends_a_standalone_notice_without_rewriting_history() -> .await?; let requests = mock.requests(); - assert_eq!(requests.len(), 7); - assert!(requests[1].has_content_kinds(&["tools.removed_definition"])); - assert!(requests[3].has_content_kinds(&["tools.removed_definition"])); - let initial_tools = requests[0].inputs_of_type("additional_tools"); - for pair in requests.windows(2) { - assert!(pair[1].input().starts_with(&pair[0].input())); - } - for request in &requests[1..5] { - assert_eq!(request.inputs_of_type("additional_tools"), initial_tools); - } - let removed_input = requests[1].input(); - let updates = &removed_input[requests[0].input().len()..]; - assert_eq!( - updates.len(), - 2, - "removal appends a standalone notice and user input" - ); - assert_eq!(updates[0]["role"], "developer"); - assert_eq!( - updates[0]["content"], - json!([{ - "type": "input_text", - "text": "The following tools are no longer available. Do not call them:\n- mcp__echo_service.cwd", - }]) - ); - let removed_namespace_input = requests[3].input(); - let namespace_updates = &removed_namespace_input[requests[2].input().len()..]; - assert_eq!( - namespace_updates.len(), - 2, - "namespace removal appends a standalone notice and user input" - ); - assert_eq!(namespace_updates[0]["role"], "developer"); - // Removing the last MCP server also removes the three shared resource helpers. - assert_eq!( - namespace_updates[0]["content"], - json!([{ - "type": "input_text", - "text": "The following namespaces are no longer available. Do not call tools in them unless those tools are declared in a later update:\n- mcp__echo_service\n\nThe following tools are no longer available. Do not call them:\n- functions.list_mcp_resource_templates\n- functions.list_mcp_resources\n- functions.read_mcp_resource", - }]) - ); - let restored_tools = requests[5].inputs_of_type("additional_tools"); - assert_eq!(restored_tools.len(), initial_tools.len() + 1); - assert_eq!(&restored_tools[..initial_tools.len()], &initial_tools); - let original_namespace = initial_tools - .iter() - .flat_map(|item| item["tools"].as_array().expect("initial tools")) - .find(|tool| tool["name"] == "mcp__echo_service") - .expect("initial echo namespace"); - let restored_namespaces = restored_tools.last().expect("restored tools")["tools"] - .as_array() - .expect("restored namespaces"); - assert_eq!(restored_namespaces.len(), 2); - assert_eq!( - restored_namespaces - .iter() - .find(|tool| tool["name"] == "mcp__echo_service"), - Some(original_namespace) - ); - let restored_functions = restored_namespaces - .iter() - .find(|tool| tool["name"] == "functions") - .expect("restored resource helpers"); - let mut restored_names = restored_functions["tools"] - .as_array() - .expect("restored functions") - .iter() - .map(|tool| tool["name"].as_str().expect("restored tool name")) - .collect::>(); - restored_names.sort_unstable(); - assert_eq!( - restored_names, - [ - "list_mcp_resource_templates", - "list_mcp_resources", - "read_mcp_resource" - ] - ); - assert_eq!( - requests[6].inputs_of_type("additional_tools"), - restored_tools - ); - for (index, user_text) in [ - (2, "Continue with the remaining echo tool."), - (4, "Continue without the echo service."), - (6, "Continue with the same restored tools."), - ] { - let unchanged_input = requests[index].input(); - let follow_up = &unchanged_input[requests[index - 1].input().len()..]; - assert_eq!(follow_up.len(), 1, "unchanged turn appends only user input"); - assert_eq!(follow_up[0]["role"], "user"); - assert_eq!( - follow_up[0]["content"], - json!([{"type": "input_text", "text": user_text}]) - ); - } insta::assert_snapshot!( "incremental_tool_removals", context_snapshot::format_request_history_snapshot( - "MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Standalone removal notices and the restored namespace append to history; unchanged turns do not repeat updates.", + "MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Removal notices append alone; restoration appends one developer notice before both namespace declarations. Unchanged turns do not repeat updates.", &requests, &ContextSnapshotOptions::default() .rewrite_known_segments() diff --git a/codex-rs/core/tests/suite/scenarios_incremental_tools_resume.rs b/codex-rs/core/tests/suite/scenarios_incremental_tools_resume.rs index ccaa42afa1..9bfb5c80bb 100644 --- a/codex-rs/core/tests/suite/scenarios_incremental_tools_resume.rs +++ b/codex-rs/core/tests/suite/scenarios_incremental_tools_resume.rs @@ -69,7 +69,7 @@ async fn legacy_resume_switches_only_after_window_replacement(token_budget: bool ])); } replies.extend( - (1..=4).map(|i| responses::sse(vec![responses::ev_completed(&format!("after-{i}"))])), + (1..=5).map(|i| responses::sse(vec![responses::ev_completed(&format!("after-{i}"))])), ); let mock = responses::mount_sse_sequence(&server, replies).await; let initial = builder(/*incremental_tools*/ false, token_budget) @@ -118,6 +118,19 @@ async fn legacy_resume_switches_only_after_window_replacement(token_budget: bool .submit_turn("Enable planning and remove shell tools after migration.") .await?; changed.submit_turn("Continue with the same tools.").await?; + let resumed_update = builder(/*incremental_tools*/ true, token_budget) + .with_config(|config| { + config.update_plan_enabled = true; + config + .features + .disable(Feature::ShellTool) + .expect("disable shell"); + }) + .restart_with_auto_env(&server, &changed) + .await?; + resumed_update + .submit_turn("Resume with the same updated tools.") + .await?; let requests = mock.requests(); insta::assert_snapshot!( if token_budget { @@ -126,7 +139,7 @@ async fn legacy_resume_switches_only_after_window_replacement(token_budget: bool "legacy_resume_remote_compaction" }, context_snapshot::format_request_history_snapshot( - "A resumed legacy window retains its generated prefix; the next window records tools and instructions once, then emits only tool changes after resume.", + "A resumed legacy window retains its generated prefix; rebuilding the next window emits no incremental notice. A later update appends one developer notice, changed definitions, then the removal notice. Unchanged turns and another restart preserve that pair without duplication.", &requests, &ContextSnapshotOptions::default() .rewrite_known_segments() @@ -166,6 +179,7 @@ async fn disable_incremental_tools_at_next_window() -> Result<()> { initial.submit_turn("Begin with all tools.").await?; let disabled_builder = || { session_builder(/*enabled*/ false).with_config(|config| { + config.update_plan_enabled = true; config .features .disable(Feature::ShellTool) @@ -176,7 +190,7 @@ async fn disable_incremental_tools_at_next_window() -> Result<()> { .restart_with_auto_env(&server, &initial) .await?; disabled - .submit_turn("Disable incremental tools and remove shell tools in the existing window.") + .submit_turn("Disable incremental tools, enable planning, and remove shell tools in the existing window.") .await?; disabled.codex.submit(Op::Compact).await?; wait_for_event(&disabled.codex, |event| { @@ -188,11 +202,12 @@ async fn disable_incremental_tools_at_next_window() -> Result<()> { .restart_with_auto_env(&server, &disabled) .await?; resumed.submit_turn("Resume with the same tools.").await?; + let requests = mock.requests(); insta::assert_snapshot!( "disable_incremental_tools", context_snapshot::format_request_history_snapshot( "Disabling incremental tools preserves updates in the existing window; after compaction the generated catalog survives resume.", - &mock.requests(), + &requests, &ContextSnapshotOptions::default() .rewrite_known_segments() .include_request_settings(), diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tool_removals.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tool_removals.snap index a57a69ff96..6353959e7e 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tool_removals.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tool_removals.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/scenarios_incremental_tools.rs -expression: "context_snapshot::format_request_history_snapshot(\"MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Standalone removal notices and the restored namespace append to history; unchanged turns do not repeat updates.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +expression: "context_snapshot::format_request_history_snapshot(\"MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Removal notices append alone; restoration appends one developer notice before both namespace declarations. Unchanged turns do not repeat updates.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" --- -Scenario: MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Standalone removal notices and the restored namespace append to history; unchanged turns do not repeat updates. +Scenario: MCP refreshes remove one tool, then its whole namespace, before restoring both tools. Removal notices append alone; restoration appends one developer notice before both namespace declarations. Unchanged turns do not repeat updates. ## Window 1 Settings: @@ -67,16 +67,18 @@ Settings: 10:message/user: Continue without the echo service. -- request 6 (turn) -- -11:additional_tools/developer (2; hash=817ef381d2e3ba83): - - namespace/functions: This is an incremental namespace update. Previously declared tools remain available for direct ca...; hash=b54767a7496e2119 +11:message/developer: + This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c] +12:additional_tools/developer (2; hash=ba0bceb82aa883e0): + - namespace/functions; hash=88d1efdc7cb932a8 - function/list_mcp_resources - function/list_mcp_resource_templates - function/read_mcp_resource - namespace/mcp__echo_service: Echo service instructions.; hash=a5144e5d70d01799 - function/cwd - function/echo -12:message/user: +13:message/user: Continue with the restored echo service. -- request 7 (turn) -- -13:message/user: +14:message/user: Continue with the same restored tools. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools.snap index 64702ea68e..73d233a689 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/scenarios_incremental_tools.rs -expression: "context_snapshot::format_request_history_snapshot(\"Tool definitions enter history in one batch; a catalog change appends only the changed exec definition with an incremental namespace hint. The next unchanged turn appends no tool definitions.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +expression: "context_snapshot::format_request_history_snapshot(\"Tool definitions enter history in one batch; a catalog change appends one developer notice before only the changed exec definition, without modifying namespace descriptions. The next unchanged turn appends only user input.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" --- -Scenario: Tool definitions enter history in one batch; a catalog change appends only the changed exec definition with an incremental namespace hint. The next unchanged turn appends no tool definitions. +Scenario: Tool definitions enter history in one batch; a catalog change appends one developer notice before only the changed exec definition, without modifying namespace descriptions. The next unchanged turn appends only user input. ## Window 1 Settings: @@ -62,13 +62,15 @@ Settings: relative to window 1 Begin work. 05:message/user: Continue with the same tools. -06:additional_tools/developer (1; hash=fd00dfa0a0c11da2): - - namespace/functions: This is an incremental namespace update. Previously declared tools remain available for direct ca...; hash=dd09638a0d29d0ca +06:message/developer: + This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c] +07:additional_tools/developer (1; hash=26cd263a29587cc7): + - namespace/functions; hash=771cad2ff250d2f3 - custom/exec -07:message/developer: +08:message/developer: -08:message/user: +09:message/user: Continue with the updated execution instructions. -- request 4 (turn) -- -09:message/user: +10:message/user: Continue with the same updated tools. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools_responses_api.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools_responses_api.snap new file mode 100644 index 0000000000..1a2a3c8593 --- /dev/null +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools__incremental_tools_responses_api.snap @@ -0,0 +1,63 @@ +--- +source: core/tests/suite/scenarios_incremental_tools.rs +expression: "context_snapshot::format_request_history_snapshot(if use_responses_lite\n{\n \"Tool definitions enter history in one batch; a catalog change appends one developer notice before only the changed exec definition, without modifying namespace descriptions. The next unchanged turn appends only user input.\"\n} else\n{\n \"Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests.\"\n}, &requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +--- +Scenario: Regular Responses requests carry the full current tool catalog without an incremental notice; changed execution instructions replace the catalog on subsequent requests. + +## Window 1 +Settings: + include: ["reasoning.encrypted_content"] + model: "gpt-5.4" + parallel_tool_calls: true + prompt_cache_key: "" + reasoning: {"effort":"medium"} + store: false + stream: true + text: {"verbosity":"low"} + tool_choice: "auto" + tools (9; hash=b0011e3ae8ca7fd2): + - custom/exec: Run JavaScript code to orchestrate/compose tool calls - Evaluates the provided JavaScript code in...; hash=3b60611486072e76 + - function/wait: Waits on a yielded `exec` cell and returns new output or completion. - Use `wait` only after `exe...; args=[cell_id, max_tokens, terminate, yield_time_ms]; hash=6692d8f6c55be489 + - function/exec_command: Runs a command in a PTY, returning output or a session ID for ongoing interaction. exec tool dec...; args=[cmd, justification, login, max_output_tokens, prefix_rule, sandbox_permissions, shell, tty, workdir, yield_time_ms]; hash=759843fc2a7d41a0 + - function/write_stdin: Writes characters to an existing unified exec session and returns recent output. exec tool decla...; args=[chars, max_output_tokens, session_id, yield_time_ms]; hash=e6405481b30a43c1 + - function/request_user_input: Request user input for one to three short questions and wait for the response. This tool is only ...; args=[questions]; hash=64a729b25c424fc6 + - custom/apply_patch: The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the pat...; hash=11c5b5ed4fb972e8 + - function/view_image: View a local image file from the filesystem when visual inspection is needed. Use this for images...; args=[detail, path]; hash=1b87dd8874f6fa10 + - tool_search: # Tool discovery Searches over deferred tool metadata with BM25 and exposes matching tools for t...; args=[limit, query]; hash=400cd6ac61d524a9 + - web_search; hash=e214d4db1bc49e89 +-- request 1 (turn) -- +00:message/developer: + +01:message/developer: + +02:message/user: + +03:message/user: + Begin work. +-- request 2 (turn) -- +04:message/user: + Continue with the same tools. + +## Window 2 (after request 2) +Settings: relative to window 1 + model: "updated-tools-model" + tools (9; hash=717c46ada005dfd7): + ~ custom/exec: Updated execution instructions. Some deferred nested tools may be omitted from this description....; hash=7af6961848c10849 +-- request 3 (turn) -- +00:message/developer: + +01:message/developer: + +02:message/user: + +03:message/user: + Begin work. +04:message/user: + Continue with the same tools. +05:message/developer: + +06:message/user: + Continue with the updated execution instructions. +-- request 4 (turn) -- +07:message/user: + Continue with the same updated tools. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__disable_incremental_tools.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__disable_incremental_tools.snap index d68b5a7977..54f4bd41dd 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__disable_incremental_tools.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__disable_incremental_tools.snap @@ -1,6 +1,6 @@ --- source: core/tests/suite/scenarios_incremental_tools_resume.rs -expression: "context_snapshot::format_request_history_snapshot(\"Disabling incremental tools preserves updates in the existing window; after compaction the generated catalog survives resume.\",\n&mock.requests(),\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +expression: "context_snapshot::format_request_history_snapshot(\"Disabling incremental tools preserves updates in the existing window; after compaction the generated catalog survives resume.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" --- Scenario: Disabling incremental tools preserves updates in the existing window; after compaction the generated catalog survives resume. @@ -34,21 +34,27 @@ Settings: Begin with all tools. -- request 2 (turn) -- 05:message/developer: + This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c] +06:additional_tools/developer (1; hash=b7fd89abae4195ad): + - namespace/functions; hash=174a13d856a6951d + - function/update_plan +07:message/developer: The following tools are no longer available. Do not call them: - functions.exec_command - functions.write_stdin -06:message/user: +08:message/user: -07:message/user: - Disable incremental tools and remove shell tools in the existing window. +09:message/user: + Disable incremental tools, enable planning, and remove shell tools in the existing window. -- request 3 (compaction) -- -08:compaction_trigger +10:compaction_trigger ## Window 2 (after request 3: input diverged at item 00) Settings: same as window 1 -- request 4 (turn) -- -00:additional_tools/developer (2; hash=2155c287c22bb8e8): - - namespace/functions; hash=cc7ea9ff03477840 +00:additional_tools/developer (2; hash=434a400f3708d1bc): + - namespace/functions; hash=36c9912d971c5be8 + - function/update_plan - function/request_user_input - custom/apply_patch - function/view_image @@ -62,7 +68,7 @@ Settings: same as window 1 04:message/user: 05:message/user: - Disable incremental tools and remove shell tools in the existing window. + Disable incremental tools, enable planning, and remove shell tools in the existing window. 06:compaction:encrypted=true; chars=17; hash=6c10f5e81c1f8e16 07:message/user: Continue in the new window. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_remote_compaction.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_remote_compaction.snap index a7112fc0dc..4f7dde808a 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_remote_compaction.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_remote_compaction.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/scenarios_incremental_tools_resume.rs -expression: "context_snapshot::format_request_history_snapshot(\"A resumed legacy window retains its generated prefix; the next window records tools and instructions once, then emits only tool changes after resume.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +expression: "context_snapshot::format_request_history_snapshot(\"A resumed legacy window retains its generated prefix; rebuilding the next window emits no incremental notice. A later update appends one developer notice, changed definitions, then the removal notice. Unchanged turns and another restart preserve that pair without duplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" --- -Scenario: A resumed legacy window retains its generated prefix; the next window records tools and instructions once, then emits only tool changes after resume. +Scenario: A resumed legacy window retains its generated prefix; rebuilding the next window emits no incremental notice. A later update appends one developer notice, changed definitions, then the removal notice. Unchanged turns and another restart preserve that pair without duplication. ## Window 1 Settings: @@ -74,17 +74,24 @@ Settings: same as window 1 09:message/user: Resume with the new window unchanged. -- request 6 (turn) -- -10:additional_tools/developer (1; hash=6334fc838872158e): - - namespace/functions: This is an incremental namespace update. Previously declared tools remain available for direct ca...; hash=1099660be7ddd67e +10:message/developer: + This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c] +11:additional_tools/developer (1; hash=845cf7662dc8512b): + - namespace/functions; hash=0c787fbffa4d7ddf - function/update_plan -11:message/developer: +12:message/developer: The following tools are no longer available. Do not call them: - functions.exec_command - functions.write_stdin -12:message/user: - 13:message/user: + +14:message/user: Enable planning and remove shell tools after migration. -- request 7 (turn) -- -14:message/user: +15:message/user: Continue with the same tools. +-- request 8 (turn) -- +16:message/user: + +17:message/user: + Resume with the same updated tools. diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_window_reset.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_window_reset.snap index 9a84c3198f..20833f9832 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_window_reset.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__incremental_tools_resume__legacy_resume_window_reset.snap @@ -1,8 +1,8 @@ --- source: core/tests/suite/scenarios_incremental_tools_resume.rs -expression: "context_snapshot::format_request_history_snapshot(\"A resumed legacy window retains its generated prefix; the next window records tools and instructions once, then emits only tool changes after resume.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" +expression: "context_snapshot::format_request_history_snapshot(\"A resumed legacy window retains its generated prefix; rebuilding the next window emits no incremental notice. A later update appends one developer notice, changed definitions, then the removal notice. Unchanged turns and another restart preserve that pair without duplication.\",\n&requests,\n&ContextSnapshotOptions::default().rewrite_known_segments().include_request_settings(),)" --- -Scenario: A resumed legacy window retains its generated prefix; the next window records tools and instructions once, then emits only tool changes after resume. +Scenario: A resumed legacy window retains its generated prefix; rebuilding the next window emits no incremental notice. A later update appends one developer notice, changed definitions, then the removal notice. Unchanged turns and another restart preserve that pair without duplication. ## Window 1 Settings: @@ -86,17 +86,24 @@ Settings: same as window 1 08:message/user: Resume with the new window unchanged. -- request 5 (turn) -- -09:additional_tools/developer (1; hash=6334fc838872158e): - - namespace/functions: This is an incremental namespace update. Previously declared tools remain available for direct ca...; hash=1099660be7ddd67e +09:message/developer: + This is an incremental tools update. Previously declared tools remain available for direct calls unless ...here, its latest definition replaces the earlier one. [hash=bceb65fad1afdf9c] +10:additional_tools/developer (1; hash=845cf7662dc8512b): + - namespace/functions; hash=0c787fbffa4d7ddf - function/update_plan -10:message/developer: +11:message/developer: The following tools are no longer available. Do not call them: - functions.exec_command - functions.write_stdin -11:message/user: - 12:message/user: + +13:message/user: Enable planning and remove shell tools after migration. -- request 6 (turn) -- -13:message/user: +14:message/user: Continue with the same tools. +-- request 7 (turn) -- +15:message/user: + +16:message/user: + Resume with the same updated tools.