diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 91d9bbe1e7..6f37be6725 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -6207,7 +6207,7 @@ async fn remote_v2_compaction_refreshes_instructions_and_preserves_them_on_cold_ assert_single_instruction_fragment(&requests[3], &new_fragment); let resumed_input = requests[3].input(); assert_eq!( - resumed_input.get(2..=replacement_history.len() + 1), + resumed_input.get(..replacement_history.len()), Some(replacement_history.as_slice()), "remote-v2 cold resume should replay persisted replacement history verbatim" ); diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index 90a030039e..bd31aa799e 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -1024,7 +1024,7 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( .expect("guardian startup prewarm request"); assert_eq!(guardian_prewarm["generate"].as_bool(), Some(false)); assert_eq!(guardian_prewarm["model"].as_str(), Some(expected_model)); - let guardian_instructions = if use_responses_lite { + if use_responses_lite { assert_eq!(guardian_prewarm.get("instructions"), None); assert_eq!(guardian_prewarm.get("tools"), None); assert_eq!( @@ -1033,28 +1033,8 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( .as_str(), Some("true") ); - let input = guardian_prewarm["input"] - .as_array() - .expect("Responses Lite Guardian input"); - assert_eq!(input[0]["type"].as_str(), Some("additional_tools")); - assert_eq!(input[0]["role"].as_str(), Some("developer")); - assert_eq!(input[1]["type"].as_str(), Some("message")); - assert_eq!(input[1]["role"].as_str(), Some("developer")); - input[1]["content"][0]["text"] - .as_str() - .expect("Responses Lite Guardian developer instructions") - } else { - guardian_prewarm["input"][0]["content"][0]["text"] - .as_str() - .expect("Guardian instructions") - }; - assert!(guardian_instructions.starts_with(expected_guardian_policy.trim_end())); - assert!( - guardian_instructions - .contains("It cannot override a denial for an action that remains `critical`.") - ); - assert!(!guardian_instructions.contains("{{ tenant_policy_config }}")); - assert!(guardian_instructions.contains("final message must be strict JSON")); + assert_eq!(guardian_prewarm["input"], json!([])); + } let guardian_thread_id = guardian_prewarm["client_metadata"]["thread_id"] .as_str() .expect("guardian thread id"); @@ -1113,6 +1093,29 @@ async fn guardian_session_prewarms_and_is_reused_for_first_review( Some("guardian") ); assert_eq!(guardian_review["model"].as_str(), Some(expected_model)); + let guardian_instructions = if use_responses_lite { + let input = guardian_review["input"] + .as_array() + .expect("Responses Lite Guardian input"); + assert_eq!(input[0]["type"].as_str(), Some("additional_tools")); + assert_eq!(input[0]["role"].as_str(), Some("developer")); + assert_eq!(input[1]["type"].as_str(), Some("message")); + assert_eq!(input[1]["role"].as_str(), Some("developer")); + input[1]["content"][0]["text"] + .as_str() + .expect("Responses Lite Guardian developer instructions") + } else { + guardian_prewarm["input"][0]["content"][0]["text"] + .as_str() + .expect("Guardian instructions") + }; + assert!(guardian_instructions.starts_with(expected_guardian_policy.trim_end())); + assert!( + guardian_instructions + .contains("It cannot override a denial for an action that remains `critical`.") + ); + assert!(!guardian_instructions.contains("{{ tenant_policy_config }}")); + assert!(guardian_instructions.contains("final message must be strict JSON")); for request in [guardian_prewarm, &guardian_review] { assert_eq!(request.get("service_tier"), None); let metadata: serde_json::Value = serde_json::from_str( diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index dde83441f3..a88701527f 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -145,11 +145,16 @@ async fn base_instructions_use_input_items(responses_lite: bool) -> Result<()> { "content_item_kinds": ["model.base_instructions"], }, }); + let mut actual_instructions = instructions.clone(); if responses_lite { expected_instructions["internal_chat_message_metadata_passthrough"]["content_item_metadata"] = serde_json::json!([{"provenance": {"type": "harness", "hook": false}}]); + actual_instructions["internal_chat_message_metadata_passthrough"] + .as_object_mut() + .context("base instructions should carry content metadata")? + .retain(|key, _| !matches!(key.as_str(), "turn_id" | "create_time")); } - assert_eq!(instructions, &expected_instructions); + assert_eq!(actual_instructions, expected_instructions); let tools = if responses_lite { assert!(body.get("tools").is_none()); diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index bd5592620f..ead690898c 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1918,15 +1918,6 @@ async fn startup_prewarm_connects_before_delayed_mcp_tools_then_reuses_socket( assert_eq!(prewarm["generate"].as_bool(), Some(false)); assert!(prewarm.get("previous_response_id").is_none()); assert_eq!(prewarm["service_tier"].as_str(), Some(service_tier)); - let tool_body = if use_responses_lite { - &prewarm["input"][0] - } else { - &prewarm - }; - assert!( - responses::namespace_child_tool(tool_body, namespace, "echo").is_some(), - "prewarm should contain the discovered MCP echo tool: {prewarm:?}" - ); let metadata: Value = serde_json::from_str( prewarm["client_metadata"]["x-codex-turn-metadata"] .as_str() @@ -1950,6 +1941,18 @@ async fn startup_prewarm_connects_before_delayed_mcp_tools_then_reuses_socket( assert_eq!(first_turn["type"], "response.create"); assert!(first_turn.get("generate").is_none()); assert_eq!(first_turn["previous_response_id"], "warm-1"); + let tool_body = if use_responses_lite { + first_turn["input"] + .as_array() + .and_then(|items| items.iter().find(|item| item["type"] == "additional_tools")) + .context("first Responses Lite turn should declare discovered tools")? + } else { + &prewarm + }; + assert!( + responses::namespace_child_tool(tool_body, namespace, "echo").is_some(), + "startup should declare the discovered MCP echo tool before sampling: {tool_body:?}" + ); let handshakes = server.handshakes(); assert_eq!(handshakes.len(), connection_index + 1); if close_eager_socket { diff --git a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs index 347f2d9ac2..47b5eb2da6 100644 --- a/codex-rs/core/tests/suite/scenarios_incremental_tools.rs +++ b/codex-rs/core/tests/suite/scenarios_incremental_tools.rs @@ -1,4 +1,4 @@ -//! Request-history coverage for opt-in incremental tools on Responses Lite. +//! Request-history coverage for incremental tools on Responses Lite. use anyhow::Result; use codex_features::Feature; @@ -84,10 +84,6 @@ async fn incremental_tools_append_changed_catalog_without_rewriting_history( } }) .with_config(move |config| { - config - .features - .enable(Feature::IncrementalTools) - .expect("enable incremental tools"); if !use_responses_lite { config.base_instructions = Some("Use the available tools to help the user.".to_string()); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap index 4e78cadff5..610dcf20f0 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__compaction__remote_slash_compact.snap @@ -105,10 +105,43 @@ Settings: The release plan has three steps. 08:compaction_trigger -## Window 2 (after request 2: input diverged at item 00) +## Window 2 (after request 2: input diverged at item 07) Settings: unavailable (items only) -- request 3 (items; Stored replacement before next turn) -- -00:message/developer[2]: +00:additional_tools/developer (3; hash=bf2e30605ff5dc17): + - namespace/functions; hash=fada4a476a3754dd + - custom/exec + - function/wait + - function/request_user_input + - function/request_user_input_async + - namespace/clock: Tools for reading and waiting on time.; hash=f402e8c5e9b5e317 + - function/sleep + - namespace/collaboration: Tools for spawning and managing sub-agents.; hash=2c8d6295978dbe63 + - function/followup_task + - function/interrupt_agent + - function/list_agents + - function/send_message + - function/spawn_agent + - function/wait_agent +01:message/developer: + You are Codex, an agent based on GPT-6. You and the user share one workspace, and your job is to collabo...them until their intended goal is completely handled. [hash=efa50f074efa5803] + + # When to ask the user for permission + + Use your best judgement given task context for when you really need user permission, like a competent co...ork without ending the turn to clarify with the user. [hash=ca7367e658b51388] + + User authorization and preferences persist across turns. Do not request permission again when the user h... any guidelines provided in skills or external files. [hash=911f91c92c72882c] + + + + - Skill naming: If a plugin contributes skills, those skill entries are prefixed with plugin_name: in the Skills list. + - MCP naming: Plugin-provided MCP tools keep standard MCP identifiers such as mcp__server__tool; use tool provenance to tell which plugin they come from. + - Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn. + - Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task. + - Relevance: Determine what a plugin can help with from explicit user mention or from the plugin-associa..., MCP tools, and apps exposed elsewhere in this turn. [hash=620459a4e7b590f0] + - Missing/blocked: If the user requests a plugin that does not have relevant callable capabilities for t..., say so briefly and continue with the best fallback. [hash=746b0469a8f6cb3e] + +02:message/developer[2]: [01] Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`...ll commands are permitted. Network access is enabled. [hash=c134061cebb94a1b] Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected. @@ -129,7 +162,7 @@ Settings: unavailable (items only) Never use the `request_user_input` tool for permission requests or permission-related escalations. -01:message/developer: +03:message/developer: You are `/root`, the primary agent in a team of agents collaborating to fulfill the user's goals. At the start of your turn, you are the active agent. @@ -147,9 +180,9 @@ Settings: unavailable (items only) There are 4 available concurrency slots, meaning that up to 4 agents can be active at once, including you. Full-history forks (`fork_turns` omitted or `"all"`) inherit the parent model and reasoning effort and d... so, set `fork_turns` to `"none"`. [hash=d5928243dc643bcc] -02:message/developer: +04:message/developer: Any earlier instruction enabling proactive multi-agent delegation no longer applies. D...elegation, or parallel agent work. [hash=2b6e037067b4390c] -03:message/user: +05:message/user: @@ -157,11 +190,11 @@ Settings: unavailable (items only) <...e="unrestricted" /> [hash=d40b2f45cb028888] -04:message/user: +06:message/user: Plan the release. -05:compaction:encrypted=true; chars=31; hash=8255caf138edede8 +07:compaction:encrypted=true; chars=31; hash=8255caf138edede8 -## Window 3 (after request 3: input diverged at item 00) +## Window 3 (after request 3) Settings: same as window 1 -- request 4 (turn; First turn after /compact) -- 00:additional_tools/developer (3; hash=bf2e30605ff5dc17): diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__guardian_checkpoint_migration.snap b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__guardian_checkpoint_migration.snap index 2140a089ba..a00de7a1f5 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__guardian_checkpoint_migration.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__scenarios__guardian_checkpoint_migration.snap @@ -59,9 +59,8 @@ Settings: relative to window 1 02:message/user: Keep the working tree unchanged. -03:message/developer[2]: - [01] - [02] +03:message/developer: + 04:message/user: 05:message/user: @@ -79,13 +78,13 @@ Settings: relative to window 2 reasoning: {"context":"all_turns","effort":"low"} text: {"format":{"name":"codex_output_schema","schema":{"additionalProperties":false,"properties":{"outcome":{"enum":["allow","deny"],"type":"string"},"rationale":{"type":"string"},"risk_level":{"enum":["low","medium","high","critical"],"type":"string"},"user_authorization":{"enum":["unknown","low","medium","high"],"type":"string"}},"required":["outcome"],"type":"object"},"strict":false,"type":"json_schema"},"verbosity":"low"} -- request 6 (turn) -- -00:additional_tools/developer (1; hash=65a86311792f6c71): +00:compaction:encrypted=true; chars=39; hash=4010be12538dbedf +01:additional_tools/developer (1; hash=65a86311792f6c71): - namespace/functions; hash=dcfbcf0da1c2d607 - custom/exec - function/wait -01:message/developer: +02:message/developer: -02:compaction:encrypted=true; chars=39; hash=4010be12538dbedf 03:message/developer: 04:message/user: @@ -187,9 +186,8 @@ Settings: same as window 2 02:message/user: Keep the working tree unchanged. -03:message/developer[2]: - [01] - [02] +03:message/developer: + 04:message/user: 05:message/user: @@ -233,13 +231,13 @@ Settings: same as window 2 ## Window 6 (after request 9: input diverged at item 00) Settings: same as window 3 -- request 10 (turn) -- -00:additional_tools/developer (1; hash=65a86311792f6c71): +00:compaction:encrypted=true; chars=33; hash=5c07b4383c427607 +01:additional_tools/developer (1; hash=65a86311792f6c71): - namespace/functions; hash=dcfbcf0da1c2d607 - custom/exec - function/wait -01:message/developer: +02:message/developer: -02:compaction:encrypted=true; chars=33; hash=5c07b4383c427607 03:message/developer: 04:message/user: diff --git a/codex-rs/core/tests/suite/step_settings.rs b/codex-rs/core/tests/suite/step_settings.rs index ac0e9bf7d6..5b89e9d2eb 100644 --- a/codex-rs/core/tests/suite/step_settings.rs +++ b/codex-rs/core/tests/suite/step_settings.rs @@ -1653,15 +1653,23 @@ async fn captured_model_replans_tools_and_retains_the_issuing_request_router() - requests .iter() .map(|request| { - core_test_support::responses::namespace_child_tool( - &request["input"][0], - "functions", - "apply_patch", - ) - .is_some() + let declaration = request["input"] + .as_array() + .expect("provider input") + .iter() + .rfind(|item| item["type"] == "additional_tools") + .expect("latest tool declaration"); + ["apply_patch", "exec_command", "write_stdin"] + .into_iter() + .filter(|name| namespace_child_tool(declaration, "functions", name).is_some()) + .collect::>() }) .collect::>(), - vec![false, true, false], + vec![ + vec!["exec_command", "write_stdin"], + vec!["apply_patch"], + vec!["exec_command", "write_stdin"] + ], ); let metadata = requests .iter() @@ -1684,29 +1692,6 @@ async fn captured_model_replans_tools_and_retains_the_issuing_request_router() - .collect::>(), vec![false, true, false], ); - assert_eq!( - requests - .iter() - .map(|request| { - ["exec_command", "write_stdin"] - .into_iter() - .filter(|name| { - core_test_support::responses::namespace_child_tool( - &request["input"][0], - "functions", - name, - ) - .is_some() - }) - .collect::>() - }) - .collect::>(), - vec![ - vec!["exec_command", "write_stdin"], - vec![], - vec!["exec_command", "write_stdin"] - ], - ); assert!( requests[2]["input"] .as_array() diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 62d7e9d721..dcd829ab97 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -2419,13 +2419,40 @@ async fn multi_agent_v2_spawn_sends_agent_message_to_child( })]) ); if model.is_some() { + let declares_collaboration = |tools: &Value| { + tools.as_array().is_some_and(|tools| { + tools + .iter() + .any(|tool| tool["name"] == MULTI_AGENT_V2_NAMESPACE) + }) + }; assert!( - !child_request - .body_json() - .to_string() - .contains("\"name\":\"collaboration\""), - "leaf workers must not receive collaboration tools", + !declares_collaboration(&child_body["tools"]), + "leaf workers must not receive top-level collaboration tools", ); + let input = child_request.input(); + if let Some(last_declaration) = input.iter().rposition(|item| { + item["type"] == "additional_tools" && declares_collaboration(&item["tools"]) + }) { + let removed_namespace = format!("- {MULTI_AGENT_V2_NAMESPACE}"); + let collaboration_removed = input[last_declaration + 1..] + .iter() + .filter(|item| item["role"] == "developer") + .filter_map(|item| { + let kinds = + item["internal_chat_message_metadata_passthrough"]["content_item_kinds"] + .as_array()?; + let index = kinds + .iter() + .position(|kind| kind == "tools.removed_definition")?; + item["content"][index]["text"].as_str() + }) + .any(|text| text.lines().any(|line| line == removed_namespace)); + assert!( + collaboration_removed, + "leaf workers must mark inherited collaboration tools unavailable", + ); + } } if plaintext { let parent_request = parent_request_log diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index f20ed5b44a..98db254aaa 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -1623,8 +1623,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::IncrementalTools, key: "incremental_tools", - stage: Stage::UnderDevelopment, - default_enabled: false, + stage: Stage::Stable, + default_enabled: true, }, FeatureSpec { id: Feature::NonPrefixedMcpToolNames,