diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 95ad84b2c4..384137b95a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -228,24 +228,37 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( .iter() .filter_map(|item| match item { RolloutItem::ResponseItem(envelope) => { - let ResponseItem::Message { role, content, .. } = &envelope.item else { - return None; + let context_kind = match &envelope.item { + ResponseItem::Message { role, content, .. } => { + content.iter().find_map(|item| { + let ContentItem::InputText { text } = item else { + return None; + }; + text.contains(application_context_text) + .then_some(role.as_str()) + }) + } + ResponseItem::FunctionCallOutput { + name, + namespace, + output, + .. + } if name.as_deref() == Some("untrusted_context") + && namespace.as_deref() == Some("agent") + && output.text_content() == Some(untrusted_context_text) => + { + Some("function_call_output") + } + _ => None, }; - content.iter().find_map(|item| { - let ContentItem::InputText { text } = item else { - return None; - }; - (text.contains(application_context_text) - || text.contains(untrusted_context_text)) - .then(|| { - ( - role.as_str(), - envelope - .metadata - .as_ref() - .map(|metadata| metadata.client_authored), - ) - }) + context_kind.map(|kind| { + ( + kind, + envelope + .metadata + .as_ref() + .map(|metadata| metadata.client_authored), + ) }) } _ => None, @@ -253,7 +266,10 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( .collect::>(); assert_eq!( persisted_additional_context, - vec![("developer", Some(true)), ("user", Some(false))] + vec![ + ("developer", Some(true)), + ("function_call_output", Some(false)) + ] ); let injected_value = serde_json::to_value(&injected_item)?; diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index dfa244926f..34e9d2dc45 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -815,10 +815,9 @@ async fn turn_start_additional_context_flows_to_model_input( let ThreadStartResponse { thread, .. } = mcp .start_thread(ThreadStartParams { model: Some("mock-model".to_string()), - config: Some(HashMap::from([( - "features.untrusted_as_tool_output".to_owned(), - json!(tool_outputs), - )])), + config: (!tool_outputs).then(|| { + HashMap::from([("features.untrusted_as_tool_output".to_owned(), json!(false))]) + }), ..Default::default() }) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index 38f5cace78..477497b9c2 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -557,11 +557,7 @@ async fn turn_steer_rejects_context_only_input_without_merging_context() -> Resu let body = response_requests[1] .body_json::() .context("request body should be JSON")?; - assert!( - !body - .to_string() - .contains("tab one") - ); + assert!(!body.to_string().contains("tab one")); Ok(()) } diff --git a/codex-rs/core/tests/suite/additional_context.rs b/codex-rs/core/tests/suite/additional_context.rs index 8f4b4518f3..be86847741 100644 --- a/codex-rs/core/tests/suite/additional_context.rs +++ b/codex-rs/core/tests/suite/additional_context.rs @@ -34,6 +34,8 @@ use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::received_responses_requests; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; +use core_test_support::responses::strip_metadata_from_json; +use core_test_support::responses::strip_response_item_ids_from_json; use core_test_support::skip_if_no_network; use core_test_support::skip_if_wine_exec; use core_test_support::test_codex::test_codex; @@ -113,7 +115,9 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re let request = request.single_request(); assert!(request.has_content_kinds(&["additional_content.automation_info"])); - assert!(request.has_content_kinds(&["additional_content.browser_info"])); + let outputs = request.inputs_of_type("function_call_output"); + assert_eq!(outputs.len(), 1); + assert_browser_context_actor_output(&outputs[0]); assert!(request.has_content_kinds(&["user.text"])); insta::assert_snapshot!( "additional_context_simple_input", @@ -134,10 +138,7 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re ); assert_eq!( request.message_input_texts("user"), - vec![ - "tab one", - "inspect the active tab", - ] + vec!["inspect the active tab"] ); Ok(()) @@ -639,20 +640,47 @@ async fn additional_context_is_deduplicated_between_turns_while_retained( }) .await; + let first_request = first_request.single_request(); + let second_request = second_request.single_request(); assert_eq!( - first_request.single_request().message_input_texts("user"), - vec![ - "same tab", - "first turn", - ] + first_request.message_input_texts("user"), + vec!["first turn"] ); assert_eq!( - second_request.single_request().message_input_texts("user"), - vec![ - "same tab", - "first turn", - "second turn", - ] + second_request.message_input_texts("user"), + vec!["first turn", "second turn"] + ); + let mut expected_outputs = vec![json!({ + "type": "function_call_output", + "namespace": "agent", + "name": first_name.unwrap_or("browser_info"), + "output": "same tab", + })]; + assert_eq!( + first_request + .inputs_of_type("function_call_output") + .into_iter() + .map(strip_response_item_ids_from_json) + .map(strip_metadata_from_json) + .collect::>(), + expected_outputs, + ); + if first_name != second_name { + expected_outputs.push(json!({ + "type": "function_call_output", + "namespace": "agent", + "name": second_name.unwrap_or("browser_info"), + "output": "same tab", + })); + } + assert_eq!( + second_request + .inputs_of_type("function_call_output") + .into_iter() + .map(strip_response_item_ids_from_json) + .map(strip_metadata_from_json) + .collect::>(), + expected_outputs, ); Ok(()) diff --git a/codex-rs/core/tests/suite/context_annotations.rs b/codex-rs/core/tests/suite/context_annotations.rs index fc982c9b12..7e47cb0cc4 100644 --- a/codex-rs/core/tests/suite/context_annotations.rs +++ b/codex-rs/core/tests/suite/context_annotations.rs @@ -214,7 +214,7 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()> message developer ["multi_agent.mode_instructions"] message user ["environments.environment_context"] message developer ["additional_content.automation_info"] - message user ["additional_content.browser_info"] + function_call_output - null message user ["user.text","user.image","user.audio"] message developer ["rollout_budget.remaining_tokens"] message developer ["current_time.reminder"] diff --git a/codex-rs/core/tests/suite/scenarios.rs b/codex-rs/core/tests/suite/scenarios.rs index b5b8074c8c..727e806a93 100644 --- a/codex-rs/core/tests/suite/scenarios.rs +++ b/codex-rs/core/tests/suite/scenarios.rs @@ -572,10 +572,12 @@ async fn additional_context_tool_outputs(tool_outputs: bool) -> Result<()> { .with_config(move |config| { configure_scenario_catalog(config); config.include_environment_context = false; - config - .features - .set_enabled(Feature::UntrustedAsToolOutput, tool_outputs) - .expect("test config should allow feature update"); + if !tool_outputs { + config + .features + .disable(Feature::UntrustedAsToolOutput) + .expect("test config should allow feature update"); + } }) .build_with_auto_env(&server) .await?; diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap b/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap index 00b33de5ba..bdfe03a4fa 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap @@ -16,7 +16,6 @@ Scenario: additional context is inserted before the user turn input. 02:message/developer: run one -03:message/user: - tab one +03:function_call_output/agent.browser_info:tab one 04:message/user: inspect the active tab diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index b3edf15ae1..ad9c53372a 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -1283,8 +1283,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::UntrustedAsToolOutput, key: "untrusted_as_tool_output", - stage: Stage::UnderDevelopment, - default_enabled: false, + stage: Stage::Stable, + default_enabled: true, }, FeatureSpec { id: Feature::ExecutedToolCallMetadata, diff --git a/codex-rs/tui/src/app/tests/safety_buffering.rs b/codex-rs/tui/src/app/tests/safety_buffering.rs index 62f376ddda..15d4059907 100644 --- a/codex-rs/tui/src/app/tests/safety_buffering.rs +++ b/codex-rs/tui/src/app/tests/safety_buffering.rs @@ -1441,9 +1441,16 @@ goals = true retry_request_index + 1 }; assert!( - user_input_texts(&request_bodies[goal_continuation_request_index]) + request_bodies[goal_continuation_request_index]["input"] + .as_array() + .expect("goal continuation model input") .iter() - .any(|text| text.contains(RETRY_GOAL)), + .any(|item| { + item["type"] == "function_call_output" + && item["namespace"] == "agent" + && item["name"] == "goal_objective" + && item["output"] == RETRY_GOAL + }), "inherited goal continuation should resume after the explicit retry" ); diff --git a/sdk/python/tests/test_app_server_goal_operations.py b/sdk/python/tests/test_app_server_goal_operations.py index 388aada8b7..ee064dcb25 100644 --- a/sdk/python/tests/test_app_server_goal_operations.py +++ b/sdk/python/tests/test_app_server_goal_operations.py @@ -74,9 +74,12 @@ def test_private_goal_operation_coalesces_runtime_continuations(tmp_path) -> Non "result": (result.id, result.status, result.final_response), "messages": agent_message_texts(events), "request_count": len(requests), - "objective_reached_model": ( - "Improve benchmark coverage" - in requests[0].message_input_texts("user") + "objective_reached_model": any( + item.get("type") == "function_call_output" + and item.get("namespace") == "agent" + and item.get("name") == "goal_objective" + and item.get("output") == "Improve benchmark coverage" + for item in requests[0].input() ), "steering_reached_model": any( "Continue working toward the active thread goal." in text