diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index e3623b2b03..fbd74dc548 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -5928,6 +5928,10 @@ }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" + }, + "retain": { + "description": "Requests retention of this output in the thread's model history.", + "type": "boolean" } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5801d70f38..fb2356184d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -26301,6 +26301,10 @@ }, "output": { "$ref": "#/definitions/v2/FunctionCallOutputBody" + }, + "retain": { + "description": "Requests retention of this output in the thread's model history.", + "type": "boolean" } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index edc42d3cf4..307f9a8b5c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -24015,6 +24015,10 @@ }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" + }, + "retain": { + "description": "Requests retention of this output in the thread's model history.", + "type": "boolean" } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index ff77ccec85..ad4194a511 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -533,6 +533,10 @@ }, "output": { "$ref": "#/definitions/FunctionCallOutputBody" + }, + "retain": { + "description": "Requests retention of this output in the thread's model history.", + "type": "boolean" } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 79aaee89b1..e96b44ac65 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index bf9172bec2..8f0ae35f22 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnToolOutput.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnToolOutput.ts index 77df888c83..72955a7dd2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnToolOutput.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnToolOutput.ts @@ -3,4 +3,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { FunctionCallOutputBody } from "../FunctionCallOutputBody"; -export type TurnToolOutput = { name: string, namespace: string | null, output: FunctionCallOutputBody, }; +export type TurnToolOutput = { name: string, namespace: string | null, output: FunctionCallOutputBody, +/** + * Requests retention of this output in the thread's model history. + */ +retain?: boolean, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index 6b784ada66..b09a191289 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -157,6 +157,9 @@ pub struct TurnToolOutput { pub name: String, pub namespace: Option, pub output: FunctionCallOutputBody, + /// Requests retention of this output in the thread's model history. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub retain: bool, } #[derive( diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 9316aa3f0b..1b9a253bec 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -15,6 +15,8 @@ use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind use codex_protocol::protocol::TurnEnvironmentRequests; use codex_protocol::protocol::TurnSettingsUpdate; use codex_protocol::protocol::TurnSettingsUpdateOutcome; +use codex_protocol::turn_input::AnnotatedResponseItem; +use codex_protocol::turn_input::ResponseItemAnnotations; use codex_skills::system_cache_root_dir; use crate::image_url::REMOTE_IMAGE_URL_ERROR; @@ -608,7 +610,14 @@ impl TurnRequestProcessor { internal_chat_message_metadata_passthrough: None, }; validate_response_item_image_urls(std::slice::from_ref(&item))?; - TurnInput::ResponseItem(item) + if tool_output.retain { + TurnInput::AnnotatedResponseItem(AnnotatedResponseItem { + item, + annotations: ResponseItemAnnotations { retain: true }, + }) + } else { + TurnInput::ResponseItem(item) + } } else { TurnInput::UserInput { content: params 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 972cf70347..a07a8dd936 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 @@ -297,6 +297,7 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history( thread_id: thread.id.clone(), input: Vec::new(), tool_output: Some(Box::new(TurnToolOutput { + retain: false, name: "send_message_to_thread".to_string(), namespace: Some("codex_app".to_string()), output: FunctionCallOutputBody::Text("Start a delegated turn.".to_string()), @@ -655,3 +656,89 @@ fn response_item_text_position(items: &[Value], needle: &str) -> Option { }) }) } + +#[tokio::test] +async fn turn_tool_output_retention_preserves_sender_metadata() -> Result<()> { + let server = responses::start_mock_server().await; + let response_mock = responses::mount_sse_once( + &server, + responses::sse(vec![responses::ev_completed("delegated-turn")]), + ) + .await; + let codex_home = TempDir::new()?; + MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; + let mut app = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build() + .await?; + app.initialize().await?; + let request = app + .send_thread_start_request_with_auto_env(ThreadStartParams::default()) + .await?; + let ThreadStartResponse { thread, .. } = app.read_response(request).await?; + let source_thread_id = ThreadId::new(); + let output = format!( + "\n {source_thread_id}\n Investigate the failing tests.\n" + ); + let request = app + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: Vec::new(), + tool_output: Some(Box::new(TurnToolOutput { + name: "send_message_to_thread".to_string(), + namespace: Some("codex_app".to_string()), + output: FunctionCallOutputBody::Text(output.clone()), + retain: true, + })), + ..Default::default() + }) + .await?; + let _: TurnStartResponse = app.read_response(request).await?; + app.read_stream_until_notification_message("turn/completed") + .await?; + let model_outputs = response_mock + .single_request() + .inputs_of_type("function_call_output") + .into_iter() + .map(responses::strip_metadata_from_json) + .map(strip_response_item_ids_from_json) + .collect::>(); + assert_eq!( + model_outputs, + vec![json!({ + "type": "function_call_output", + "name": "send_message_to_thread", + "namespace": "codex_app", + "output": output, + })] + ); + let rollout_path = thread.path.as_ref().context("thread path missing")?; + let InitialHistory::Resumed(history) = + RolloutRecorder::get_rollout_history(rollout_path).await? + else { + panic!("expected persisted history"); + }; + let envelope = history + .history + .iter() + .find_map(|item| match item { + RolloutItem::ResponseItem(envelope) + if matches!(envelope.item, ResponseItem::FunctionCallOutput { .. }) => + { + Some(envelope) + } + _ => None, + }) + .context("delegation output should be saved")?; + let metadata = envelope.metadata.as_ref().context("output metadata")?; + assert!(metadata.client_authored); + let sender_messages = metadata + .sender_user_messages + .as_ref() + .context("sender context must be preserved")?; + assert_eq!( + sender_messages.receiver_message_id, + envelope.item.id().unwrap().to_string() + ); + Ok(()) +} diff --git a/codex-rs/tui/src/dynamic_tools.rs b/codex-rs/tui/src/dynamic_tools.rs index d042db2580..bf4c95d62b 100644 --- a/codex-rs/tui/src/dynamic_tools.rs +++ b/codex-rs/tui/src/dynamic_tools.rs @@ -1243,6 +1243,7 @@ async fn start_turn( // Older app-server/TUI versions are intentionally unsupported: // preserving tool authority takes precedence over legacy fallback. tool_output: Some(Box::new(TurnToolOutput { + retain: false, name: tool.to_string(), namespace: Some(NAMESPACE.to_string()), output: FunctionCallOutputBody::Text(prompt), diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 012abd9e1e..20ce047fb5 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -11832,6 +11832,10 @@ class TurnToolOutput(BaseModel): name: str namespace: str | None = None output: FunctionCallOutputBody + retain: Annotated[ + bool | None, + Field(description="Requests retention of this output in the thread's model history."), + ] = None class TurnsPage(BaseModel):