diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index ae8e6fed34..2da898f5c7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -3116,6 +3116,16 @@ }, "type": "array" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "description": "Optional string metadata attached only to this turn.", + "type": [ + "object", + "null" + ] + }, "model": { "description": "Override the model for this turn and subsequent turns.", "type": [ 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 e20953a231..40e35afc47 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 @@ -13912,6 +13912,16 @@ }, "type": "array" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "description": "Optional string metadata attached only to this turn.", + "type": [ + "object", + "null" + ] + }, "model": { "description": "Override the model for this turn and subsequent turns.", "type": [ 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 cb7b2c3a0a..dc5f10f01d 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 @@ -11672,6 +11672,16 @@ }, "type": "array" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "description": "Optional string metadata attached only to this turn.", + "type": [ + "object", + "null" + ] + }, "model": { "description": "Override the model for this turn and subsequent turns.", "type": [ 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 cad1d8b5bc..8fc67ea731 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -545,6 +545,16 @@ }, "type": "array" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "description": "Optional string metadata attached only to this turn.", + "type": [ + "object", + "null" + ] + }, "model": { "description": "Override the model for this turn and subsequent turns.", "type": [ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 8f57a5e68b..76b8c054a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -13,6 +13,9 @@ import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; export type TurnStartParams = {threadId: string, input: Array, /** + * Optional string metadata attached only to this turn. + */ +metadata?: { [key in string]?: string } | null, /** * Override the working directory for this turn and subsequent turns. */ cwd?: string | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index b534510b7c..41bf18798e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3831,6 +3831,9 @@ pub enum TurnStatus { pub struct TurnStartParams { pub thread_id: String, pub input: Vec, + /// Optional string metadata attached only to this turn. + #[ts(optional = nullable)] + pub metadata: Option>, /// Override the working directory for this turn and subsequent turns. #[ts(optional = nullable)] pub cwd: Option, @@ -7906,6 +7909,7 @@ mod tests { let without_override = TurnStartParams { thread_id: "thread_123".to_string(), input: vec![], + metadata: None, cwd: None, approval_policy: None, approvals_reviewer: None, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 57798a99b0..8a9a48e0fd 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -69,7 +69,7 @@ Use the thread APIs to create, list, or archive conversations. Drive a conversat - Initialize once per connection: Immediately after opening a transport connection, send an `initialize` request with your client metadata, then emit an `initialized` notification. Any other request on that connection before this handshake gets rejected. - Start (or resume) a thread: Call `thread/start` to open a fresh conversation. The response returns the thread object and you’ll also get a `thread/started` notification. If you’re continuing an existing conversation, call `thread/resume` with its ID instead. If you want to branch from an existing conversation, call `thread/fork` to create a new thread id with copied history. Like `thread/start`, `thread/fork` also accepts `ephemeral: true` for an in-memory temporary thread. The returned `thread.ephemeral` flag tells you whether the session is intentionally in-memory only; when it is `true`, `thread.path` is `null`. -- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, approval policy, approvals reviewer, etc. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. +- Begin a turn: To send user input, call `turn/start` with the target `threadId` and the user's input. Optional fields let you override model, cwd, sandbox policy, approval policy, approvals reviewer, etc. `turn/start.metadata` also lets callers attach arbitrary string metadata to that turn. This immediately returns the new turn object. The app-server emits `turn/started` when that turn actually begins running. - Stream events: After `turn/start`, keep reading JSON-RPC notifications on stdout. You’ll see `item/started`, `item/completed`, deltas like `item/agentMessage/delta`, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes). - Finish the turn: When the model is done (or the turn is interrupted via making the `turn/interrupt` call), the server sends `turn/completed` with the final turn state and token usage. @@ -139,7 +139,7 @@ Example with notification opt-out: - `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream. - `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted. - `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success. -- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". +- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. `metadata` accepts arbitrary string key/value pairs for turn-scoped observability and request forwarding. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode". - `turn/steer` — add user input to an already in-flight turn without starting a new turn; returns the active `turnId` that accepted the input. - `turn/interrupt` — request cancellation of an in-flight turn by `(thread_id, turn_id)`; success is an empty `{}` response and the turn finishes with `status: "interrupted"`. - `thread/realtime/start` — start a thread-scoped realtime session (experimental); returns `{}` and streams `thread/realtime/*` notifications. diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b6ece83ddf..0a54b9346a 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6018,6 +6018,7 @@ impl CodexMessageProcessor { Op::UserInput { items: mapped_items, final_output_json_schema: params.output_schema, + metadata: params.metadata, }, ) .await; diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index 4288d15393..5ff3e4e95c 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -857,6 +857,176 @@ mod tests { .expect("in-process runtime should shutdown cleanly"); } + #[tokio::test] + async fn in_process_start_forwards_turn_metadata_headers_on_turn_requests() { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new().expect("tempdir should create"); + let mut client = start(InProcessStartArgs { + arg0_paths: Arg0DispatchPaths::default(), + config: Arc::new( + ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("test config should load"), + ), + cli_overrides: vec![ + ( + "model_provider".to_string(), + TomlValue::String("mock_provider".to_string()), + ), + ( + "model_providers.mock_provider.name".to_string(), + TomlValue::String("Mock provider for test".to_string()), + ), + ( + "model_providers.mock_provider.base_url".to_string(), + TomlValue::String(format!("{}/v1", server.uri())), + ), + ( + "model_providers.mock_provider.wire_api".to_string(), + TomlValue::String("responses".to_string()), + ), + ( + "model_providers.mock_provider.request_max_retries".to_string(), + TomlValue::Integer(0), + ), + ( + "model_providers.mock_provider.stream_max_retries".to_string(), + TomlValue::Integer(0), + ), + ], + loader_overrides: LoaderOverrides::default(), + cloud_requirements: CloudRequirementsLoader::default(), + auth_manager: None, + thread_manager: None, + feedback: CodexFeedback::new(), + config_warnings: Vec::new(), + session_source: SessionSource::Cli, + enable_codex_api_key_env: false, + initialize: InitializeParams { + client_info: ClientInfo { + name: "codex-in-process-test".to_string(), + title: None, + version: "0.0.0".to_string(), + }, + capabilities: None, + }, + channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY, + }) + .await + .expect("in-process runtime should start"); + + let thread_start_response: ThreadStartResponse = serde_json::from_value( + client + .request(ClientRequest::ThreadStart { + request_id: RequestId::Integer(5), + params: ThreadStartParams { + model: Some("mock-model".to_string()), + ephemeral: Some(true), + ..ThreadStartParams::default() + }, + }) + .await + .expect("thread/start request should work") + .expect("thread/start should succeed"), + ) + .expect("thread/start response should parse"); + + let _turn_start_response: TurnStartResponse = serde_json::from_value( + client + .request(ClientRequest::TurnStart { + request_id: RequestId::Integer(6), + params: TurnStartParams { + thread_id: thread_start_response.thread.id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + metadata: Some(std::collections::BTreeMap::from([ + ( + "parentConversationId".to_string(), + "parent-conversation-123".to_string(), + ), + ( + "parentMessageId".to_string(), + "parent-message-123".to_string(), + ), + ("parentTurnId".to_string(), "parent-turn-123".to_string()), + ])), + ..TurnStartParams::default() + }, + }) + .await + .expect("turn/start request should work") + .expect("turn/start should succeed"), + ) + .expect("turn/start response should parse"); + + let turn_completed = loop { + let event = timeout(std::time::Duration::from_secs(10), client.next_event()) + .await + .expect("timed out waiting for turn completion"); + match event { + Some(InProcessServerEvent::ServerNotification( + ServerNotification::TurnCompleted(notification), + )) => break notification, + Some(_) => continue, + None => panic!("in-process runtime exited before turn completion"), + } + }; + assert_eq!( + turn_completed.turn.status, + TurnStatus::Completed, + "turn error: {:?}", + turn_completed.turn.error + ); + assert_eq!(turn_completed.turn.error, None); + + let requests = timeout(std::time::Duration::from_secs(5), async { + loop { + let requests = server + .received_requests() + .await + .expect("received requests should be available"); + if !requests.is_empty() { + break requests; + } + tokio::task::yield_now().await; + } + }) + .await + .expect("timed out waiting for outbound requests"); + assert!(!requests.is_empty()); + for request in requests { + assert_eq!( + request + .headers + .get("x-openai-parent-conversation-id") + .and_then(|value| value.to_str().ok()), + Some("parent-conversation-123") + ); + assert_eq!( + request + .headers + .get("x-openai-parent-message-id") + .and_then(|value| value.to_str().ok()), + Some("parent-message-123") + ); + assert_eq!( + request + .headers + .get("x-openai-parent-turn-id") + .and_then(|value| value.to_str().ok()), + Some("parent-turn-123") + ); + } + + client + .shutdown() + .await + .expect("in-process runtime should shutdown cleanly"); + } #[test] fn guaranteed_delivery_helpers_cover_terminal_notifications() { assert!(server_notification_requires_delivery( diff --git a/codex-rs/app-server/src/message_processor/tracing_tests.rs b/codex-rs/app-server/src/message_processor/tracing_tests.rs index e39484cedb..7d86c86e90 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -601,6 +601,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { personality: None, output_schema: None, collaboration_mode: None, + metadata: None, }, }, Some(remote_trace), 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 e235ee6b61..2a6faf5788 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1395,6 +1395,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { personality: None, output_schema: None, collaboration_mode: None, + metadata: None, }) .await?; timeout( @@ -1428,6 +1429,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { personality: None, output_schema: None, collaboration_mode: None, + metadata: None, }) .await?; timeout( diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 83af6258fb..1b76cce5fd 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -424,6 +424,7 @@ impl AgentControl { Op::UserInput { items, final_output_json_schema: None, + metadata: None, }, ) .await; diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 7c2c46b2f3..50660a739e 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -372,6 +372,7 @@ async fn send_input_submits_user_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + metadata: None, }, ); let captured = harness @@ -403,6 +404,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() { text_elements: Vec::new(), }], final_output_json_schema: None, + metadata: None, }, ); let captured = harness @@ -479,6 +481,7 @@ async fn spawn_agent_can_fork_parent_thread_history() { text_elements: Vec::new(), }], final_output_json_schema: None, + metadata: None, }, ); let captured = harness diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ba71033c3b..70c4bfa44c 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -108,6 +108,7 @@ use crate::response_debug_context::extract_response_debug_context_from_api_error use crate::response_debug_context::telemetry_api_error_message; use crate::response_debug_context::telemetry_transport_error_message; use crate::tools::spec::create_tools_json_for_responses_api; +use crate::turn_metadata::extend_known_request_headers; use crate::util::FeedbackRequestTags; use crate::util::emit_feedback_auth_recovery_tags; use crate::util::emit_feedback_request_tags_with_auth_env; @@ -469,6 +470,9 @@ impl ModelClient { extra_headers } + fn build_session_request_headers(&self) -> ApiHeaderMap { + self.build_subagent_headers() + } /// Builds request telemetry for unary API calls (e.g., Compact endpoint). fn build_request_telemetry( session_telemetry: &SessionTelemetry, @@ -1384,6 +1388,10 @@ fn build_responses_headers( turn_metadata_header: Option<&HeaderValue>, ) -> ApiHeaderMap { let mut headers = ApiHeaderMap::new(); + extend_known_request_headers( + &mut headers, + turn_metadata_header.and_then(|value| value.to_str().ok()), + ); if let Some(value) = beta_features_header && !value.is_empty() && let Ok(header_value) = HeaderValue::from_str(value) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 931300cf4e..3e9832977d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::fmt::Debug; @@ -590,6 +591,7 @@ impl Codex { persist_extended_history, inherited_shell_snapshot, user_shell_override, + next_turn_metadata: BTreeMap::new(), }; // Generate a unique ID for the lifetime of this Codex session. @@ -1055,6 +1057,8 @@ pub(crate) struct SessionConfiguration { persist_extended_history: bool, inherited_shell_snapshot: Option>, user_shell_override: Option, + /// One-shot metadata consumed by the next turn created from this session. + next_turn_metadata: BTreeMap, } impl SessionConfiguration { @@ -1097,6 +1101,9 @@ impl SessionConfiguration { if let Some(personality) = updates.personality { next_configuration.personality = Some(personality); } + if let Some(next_turn_metadata) = updates.next_turn_metadata.clone() { + next_configuration.next_turn_metadata = next_turn_metadata; + } if let Some(approval_policy) = updates.approval_policy { next_configuration.approval_policy.set(approval_policy)?; } @@ -1147,6 +1154,8 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) final_output_json_schema: Option>, pub(crate) personality: Option, pub(crate) app_server_client_name: Option, + /// One-shot metadata for the next turn created from this update. + pub(crate) next_turn_metadata: Option>, } impl Session { @@ -1341,6 +1350,7 @@ impl Session { cwd.clone(), session_configuration.sandbox_policy.get(), session_configuration.windows_sandbox_level, + session_configuration.next_turn_metadata.clone(), )); let (current_date, timezone) = local_time_context(); TurnContext { @@ -2036,6 +2046,7 @@ impl Session { text_elements: Vec::new(), }], final_output_json_schema: None, + metadata: None, }, ) .await; @@ -2305,7 +2316,9 @@ impl Session { state.session_configuration.sandbox_policy != next.sandbox_policy; let codex_home = next.codex_home.clone(); let session_source = next.session_source.clone(); - state.session_configuration = next.clone(); + let mut persisted_configuration = next.clone(); + persisted_configuration.next_turn_metadata.clear(); + state.session_configuration = persisted_configuration; ( next, sandbox_policy_changed, @@ -4491,6 +4504,7 @@ mod handlers { reasoning_summary: summary, service_tier, final_output_json_schema: Some(final_output_json_schema), + next_turn_metadata: None, personality, app_server_client_name: None, }, @@ -4499,10 +4513,12 @@ mod handlers { Op::UserInput { items, final_output_json_schema, + metadata, } => ( items, SessionSettingsUpdate { final_output_json_schema: Some(final_output_json_schema), + next_turn_metadata: Some(metadata.unwrap_or_default()), ..Default::default() }, ), @@ -5225,6 +5241,7 @@ async fn spawn_review_thread( parent_turn_context.cwd.clone(), parent_turn_context.sandbox_policy.get(), parent_turn_context.windows_sandbox_level, + parent_turn_context.turn_metadata_state.metadata().clone(), )); let review_turn_context = TurnContext { diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index e560cd9c7f..54261d6bcd 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -170,6 +170,7 @@ pub(crate) async fn run_codex_thread_one_shot( io.submit(Op::UserInput { items: input, final_output_json_schema, + metadata: None, }) .await?; diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index c7cbcf2252..440b240366 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1660,6 +1660,7 @@ async fn set_rate_limits_retains_previous_credits() { persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), }; let mut state = SessionState::new(session_configuration); @@ -1758,6 +1759,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), }; let mut state = SessionState::new(session_configuration); @@ -2102,6 +2104,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), } } @@ -2333,6 +2336,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), }; let (tx_event, _rx_event) = async_channel::unbounded(); @@ -2428,6 +2432,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), }; let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_info = ModelsManager::construct_model_info_offline_for_tests( @@ -2865,6 +2870,7 @@ fn op_kind_distinguishes_turn_ops() { Op::UserInput { items: vec![], final_output_json_schema: None, + metadata: None, } .kind(), "user_input" @@ -3223,6 +3229,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( persist_extended_history: false, inherited_shell_snapshot: None, user_shell_override: None, + next_turn_metadata: std::collections::BTreeMap::new(), }; let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_info = ModelsManager::construct_model_info_offline_for_tests( diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 16a05df957..ac76f7c9a8 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -33,6 +33,7 @@ use crate::protocol::McpInvocation; use crate::protocol::McpToolCallBeginEvent; use crate::protocol::McpToolCallEndEvent; use crate::state_db; +use crate::turn_metadata::extend_known_request_headers; use codex_protocol::mcp::CallToolResult; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::AskForApproval; @@ -389,7 +390,6 @@ pub(crate) struct McpToolApprovalMetadata { } const MCP_TOOL_CODEX_APPS_META_KEY: &str = "_codex_apps"; - fn build_mcp_tool_call_request_meta( turn_context: &TurnContext, server: &str, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index be34a15707..e51f71caa3 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -557,6 +557,7 @@ async fn send_input_accepts_structured_items() { }, ], final_output_json_schema: None, + metadata: None, }; let captured = manager .captured_ops() diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 3a4bac011d..fd3f1e3522 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -5,6 +5,9 @@ use std::sync::Arc; use std::sync::Mutex; use std::sync::RwLock; +use http::HeaderMap; +use http::HeaderValue; +use serde::Deserialize; use serde::Serialize; use tokio::task::JoinHandle; @@ -16,6 +19,14 @@ use crate::sandbox_tags::sandbox_tag; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::protocol::SandboxPolicy; +pub(crate) const PARENT_CONVERSATION_ID_METADATA_KEY: &str = "parentConversationId"; +pub(crate) const PARENT_MESSAGE_ID_METADATA_KEY: &str = "parentMessageId"; +pub(crate) const PARENT_TURN_ID_METADATA_KEY: &str = "parentTurnId"; + +const X_OPENAI_PARENT_CONVERSATION_ID_HEADER: &str = "x-openai-parent-conversation-id"; +const X_OPENAI_PARENT_MESSAGE_ID_HEADER: &str = "x-openai-parent-message-id"; +const X_OPENAI_PARENT_TURN_ID_HEADER: &str = "x-openai-parent-turn-id"; + #[derive(Clone, Debug, Default)] struct WorkspaceGitMetadata { associated_remote_urls: Option>, @@ -31,7 +42,7 @@ impl WorkspaceGitMetadata { } } -#[derive(Clone, Debug, Serialize, Default)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] struct TurnMetadataWorkspace { #[serde(default, skip_serializing_if = "Option::is_none")] associated_remote_urls: Option>, @@ -51,7 +62,7 @@ impl From for TurnMetadataWorkspace { } } -#[derive(Clone, Debug, Serialize, Default)] +#[derive(Clone, Debug, Deserialize, Serialize, Default)] pub(crate) struct TurnMetadataBag { #[serde(default, skip_serializing_if = "Option::is_none")] session_id: Option, @@ -61,6 +72,8 @@ pub(crate) struct TurnMetadataBag { workspaces: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] sandbox: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + metadata: BTreeMap, } impl TurnMetadataBag { @@ -75,6 +88,7 @@ fn build_turn_metadata_bag( sandbox: Option, repo_root: Option, workspace_git_metadata: Option, + metadata: BTreeMap, ) -> TurnMetadataBag { let mut workspaces = BTreeMap::new(); if let (Some(repo_root), Some(workspace_git_metadata)) = (repo_root, workspace_git_metadata) @@ -88,6 +102,7 @@ fn build_turn_metadata_bag( turn_id, workspaces, sandbox, + metadata, } } @@ -117,6 +132,7 @@ pub async fn build_turn_metadata_header(cwd: &Path, sandbox: Option<&str>) -> Op latest_git_commit_hash, has_changes, }), + BTreeMap::new(), ) .to_header_value() } @@ -138,6 +154,7 @@ impl TurnMetadataState { cwd: PathBuf, sandbox_policy: &SandboxPolicy, windows_sandbox_level: WindowsSandboxLevel, + metadata: BTreeMap, ) -> Self { let repo_root = get_git_repo_root(&cwd).map(|root| root.to_string_lossy().into_owned()); let sandbox = Some(sandbox_tag(sandbox_policy, windows_sandbox_level).to_string()); @@ -147,6 +164,7 @@ impl TurnMetadataState { sandbox, /*repo_root*/ None, /*workspace_git_metadata*/ None, + metadata, ); let base_header = base_metadata .to_header_value() @@ -180,6 +198,10 @@ impl TurnMetadataState { .and_then(|header| serde_json::from_str(&header).ok()) } + pub(crate) fn metadata(&self) -> &BTreeMap { + &self.base_metadata.metadata + } + pub(crate) fn spawn_git_enrichment_task(&self) { if self.repo_root.is_none() { return; @@ -206,6 +228,7 @@ impl TurnMetadataState { state.base_metadata.sandbox.clone(), Some(repo_root), Some(workspace_git_metadata), + state.base_metadata.metadata.clone(), ); if enriched_metadata.workspaces.is_empty() { return; @@ -245,6 +268,51 @@ impl TurnMetadataState { } } +pub(crate) fn extend_known_request_headers( + headers: &mut HeaderMap, + turn_metadata_header: Option<&str>, +) { + let Some(turn_metadata_header) = turn_metadata_header else { + return; + }; + let Ok(turn_metadata) = serde_json::from_str::(turn_metadata_header) else { + return; + }; + + insert_if_valid( + headers, + X_OPENAI_PARENT_CONVERSATION_ID_HEADER, + turn_metadata + .metadata + .get(PARENT_CONVERSATION_ID_METADATA_KEY) + .map(String::as_str), + ); + insert_if_valid( + headers, + X_OPENAI_PARENT_MESSAGE_ID_HEADER, + turn_metadata + .metadata + .get(PARENT_MESSAGE_ID_METADATA_KEY) + .map(String::as_str), + ); + insert_if_valid( + headers, + X_OPENAI_PARENT_TURN_ID_HEADER, + turn_metadata + .metadata + .get(PARENT_TURN_ID_METADATA_KEY) + .map(String::as_str), + ); +} + +fn insert_if_valid(headers: &mut HeaderMap, name: &'static str, value: Option<&str>) { + if let Some(value) = value + && let Ok(header_value) = HeaderValue::from_str(value) + { + headers.insert(name, header_value); + } +} + #[cfg(test)] #[path = "turn_metadata_tests.rs"] mod tests; diff --git a/codex-rs/core/src/turn_metadata_tests.rs b/codex-rs/core/src/turn_metadata_tests.rs index 5da26563f6..69df990b41 100644 --- a/codex-rs/core/src/turn_metadata_tests.rs +++ b/codex-rs/core/src/turn_metadata_tests.rs @@ -1,5 +1,7 @@ use super::*; +use std::collections::BTreeMap; + use serde_json::Value; use tempfile::TempDir; use tokio::process::Command; @@ -72,6 +74,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { cwd, &sandbox_policy, WindowsSandboxLevel::Disabled, + BTreeMap::new(), ); let header = state.current_header_value().expect("header"); @@ -83,3 +86,38 @@ fn turn_metadata_state_uses_platform_sandbox_tag() { assert_eq!(sandbox_name, Some(expected_sandbox)); assert_eq!(session_id, Some("session-a")); } + +#[test] +fn extend_known_request_headers_reads_parent_ids_from_turn_metadata() { + let mut headers = http::HeaderMap::new(); + let turn_metadata = serde_json::json!({ + "turn_id": "turn-a", + "metadata": { + PARENT_CONVERSATION_ID_METADATA_KEY: "conv-123", + PARENT_MESSAGE_ID_METADATA_KEY: "msg-123", + PARENT_TURN_ID_METADATA_KEY: "turn-123", + }, + }); + let turn_metadata = serde_json::to_string(&turn_metadata).expect("turn metadata json"); + + extend_known_request_headers(&mut headers, Some(turn_metadata.as_str())); + + assert_eq!( + headers + .get("x-openai-parent-conversation-id") + .and_then(|value| value.to_str().ok()), + Some("conv-123") + ); + assert_eq!( + headers + .get("x-openai-parent-message-id") + .and_then(|value| value.to_str().ok()), + Some("msg-123") + ); + assert_eq!( + headers + .get("x-openai-parent-turn-id") + .and_then(|value| value.to_str().ok()), + Some("turn-123") + ); +} diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 823057797c..12b6173fbd 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -20,6 +20,7 @@ use core_test_support::responses; use core_test_support::test_codex::test_codex; use futures::StreamExt; use pretty_assertions::assert_eq; +use serde_json::json; use tempfile::TempDir; use wiremock::matchers::header; @@ -546,3 +547,129 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() Some(false) ); } + +#[tokio::test] +async fn responses_stream_includes_parent_headers_from_turn_metadata() { + core_test_support::skip_if_no_network!(); + + let server = responses::start_mock_server().await; + let request_recorder = responses::mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + + let provider = ModelProviderInfo { + name: "mock".into(), + base_url: Some(format!("{}/v1", server.uri())), + env_key: None, + env_key_instructions: None, + experimental_bearer_token: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: Some(0), + stream_max_retries: Some(0), + stream_idle_timeout_ms: Some(5_000), + websocket_connect_timeout_ms: None, + requires_openai_auth: false, + supports_websockets: false, + }; + + let codex_home = TempDir::new().expect("failed to create TempDir"); + let mut config = load_default_config_for_test(&codex_home).await; + config.model_provider_id = provider.name.clone(); + config.model_provider = provider.clone(); + let effort = config.model_reasoning_effort; + let summary = config.model_reasoning_summary; + let model = codex_core::test_support::get_model_offline(config.model.as_deref()); + config.model = Some(model.clone()); + let config = Arc::new(config); + + let conversation_id = ThreadId::new(); + let auth_mode = TelemetryAuthMode::Chatgpt; + let session_source = SessionSource::Exec; + let model_info = + codex_core::test_support::construct_model_info_offline(model.as_str(), &config); + let session_telemetry = SessionTelemetry::new( + conversation_id, + model.as_str(), + model_info.slug.as_str(), + None, + Some("test@test.com".to_string()), + Some(auth_mode), + "test_originator".to_string(), + false, + "test".to_string(), + session_source.clone(), + ); + + let client = ModelClient::new( + None, + conversation_id, + provider, + session_source, + config.model_verbosity, + false, + false, + false, + None, + ); + let mut client_session = client.new_session(); + + let mut prompt = Prompt::default(); + prompt.input = vec![ResponseItem::Message { + id: None, + role: "user".into(), + content: vec![ContentItem::InputText { + text: "hello".into(), + }], + end_turn: None, + phase: None, + }]; + let turn_metadata_header = serde_json::to_string(&json!({ + "turn_id": "turn-123", + "metadata": { + "parentConversationId": "conv-123", + "parentMessageId": "msg-123", + "parentTurnId": "turn-123", + }, + })) + .expect("turn metadata json"); + + let mut stream = client_session + .stream( + &prompt, + &model_info, + &session_telemetry, + effort, + summary.unwrap_or(model_info.default_reasoning_summary), + None, + Some(turn_metadata_header.as_str()), + ) + .await + .expect("stream failed"); + while let Some(event) = stream.next().await { + if matches!(event, Ok(ResponseEvent::Completed { .. })) { + break; + } + } + + let request = request_recorder.single_request(); + assert_eq!( + request.header("x-openai-parent-conversation-id").as_deref(), + Some("conv-123") + ); + assert_eq!( + request.header("x-openai-parent-message-id").as_deref(), + Some("msg-123") + ); + assert_eq!( + request.header("x-openai-parent-turn-id").as_deref(), + Some("turn-123") + ); +} diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index af3c70b74e..839568e85d 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -51,6 +51,8 @@ async fn interrupt_long_running_tool_emits_turn_aborted() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -105,6 +107,8 @@ async fn interrupt_tool_records_history_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -123,6 +127,8 @@ async fn interrupt_tool_records_history_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -203,6 +209,8 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -221,6 +229,8 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 35dee3fa70..ef24b90437 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -277,6 +277,8 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -639,6 +641,8 @@ async fn includes_conversation_id_and_model_headers_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -687,6 +691,8 @@ async fn includes_base_instructions_override_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -739,6 +745,8 @@ async fn chatgpt_auth_sends_correct_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -841,6 +849,8 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -877,6 +887,8 @@ async fn includes_user_instructions_message_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -962,6 +974,8 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1050,6 +1064,8 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1120,6 +1136,8 @@ async fn skills_append_to_developer_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1171,6 +1189,8 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1213,6 +1233,8 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1253,6 +1275,8 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1364,6 +1388,8 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1478,6 +1504,8 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1534,6 +1562,8 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1570,6 +1600,8 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1615,6 +1647,8 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1659,6 +1693,8 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1708,6 +1744,8 @@ async fn includes_developer_instructions_message_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1992,6 +2030,8 @@ async fn token_count_includes_rate_limits_snapshot() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2157,6 +2197,8 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submission should succeed while emitting usage limit error events"); @@ -2231,6 +2273,8 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -2243,6 +2287,8 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -2325,6 +2371,8 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -2423,6 +2471,8 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2508,6 +2558,8 @@ async fn env_var_overrides_loaded_auth() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2569,6 +2621,8 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2582,6 +2636,8 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2595,6 +2651,8 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 4416ff1083..c283332c86 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -996,6 +996,8 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submission should succeed while emitting usage limit error events"); @@ -1081,6 +1083,8 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submission should succeed while emitting invalid request events"); diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 81d0678cad..7ff0195655 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -84,6 +84,8 @@ async fn no_collaboration_instructions_by_default() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -143,6 +145,8 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -240,6 +244,8 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -362,6 +368,8 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -389,6 +397,8 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -445,6 +455,8 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -472,6 +484,8 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -530,6 +544,8 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -560,6 +576,8 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -619,6 +637,8 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -649,6 +669,8 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -712,6 +734,8 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -725,6 +749,8 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -781,6 +807,8 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index f02ab65743..619a325bd0 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -242,6 +242,8 @@ async fn summarize_context_three_requests_and_instructions() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -264,6 +266,8 @@ async fn summarize_context_three_requests_and_instructions() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -440,6 +444,8 @@ async fn manual_compact_uses_custom_prompt() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit first user turn"); @@ -584,6 +590,8 @@ async fn manual_compact_emits_context_compaction_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -747,6 +755,8 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit user input"); @@ -1246,6 +1256,8 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1259,6 +1271,8 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1272,6 +1286,8 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1440,6 +1456,8 @@ async fn auto_compact_emits_context_compaction_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1518,6 +1536,8 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1530,6 +1550,8 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1542,6 +1564,8 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2029,6 +2053,8 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2041,6 +2067,8 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2053,6 +2081,8 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2139,6 +2169,8 @@ async fn manual_compact_retries_after_context_window_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2250,6 +2282,8 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit user input"); @@ -2342,6 +2376,8 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2357,6 +2393,8 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2372,6 +2410,8 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2533,6 +2573,8 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2635,6 +2677,8 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2829,6 +2873,8 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -2946,6 +2992,8 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -3005,6 +3053,8 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit user input"); @@ -3040,6 +3090,8 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess }, ], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit user input"); @@ -3250,6 +3302,8 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit first user"); @@ -3262,6 +3316,8 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit second user"); @@ -3332,6 +3388,8 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit follow-up user input"); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 96ade23c87..73668a80fb 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -235,6 +235,8 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -249,6 +251,8 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -379,6 +383,8 @@ async fn remote_compact_runs_automatically() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -453,6 +459,8 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -464,6 +472,8 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -573,6 +583,8 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -584,6 +596,8 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -601,6 +615,8 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -698,6 +714,8 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -709,6 +727,8 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -800,6 +820,8 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&baseline_codex, |event| { @@ -814,6 +836,8 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&baseline_codex, |event| { @@ -899,6 +923,8 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&override_codex, |event| { @@ -913,6 +939,8 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&override_codex, |event| { @@ -981,6 +1009,8 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -1059,6 +1089,8 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -1141,6 +1173,8 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1282,6 +1316,8 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1297,6 +1333,8 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1319,6 +1357,8 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1413,6 +1453,8 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1427,6 +1469,8 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1496,6 +1540,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1507,6 +1553,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1571,6 +1619,8 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() - text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1629,6 +1679,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1642,6 +1694,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1714,6 +1768,8 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1728,6 +1784,8 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1808,6 +1866,8 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1821,6 +1881,8 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1909,6 +1971,8 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1936,6 +2000,8 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2029,6 +2095,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2113,6 +2181,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2139,6 +2209,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2255,6 +2327,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2266,6 +2340,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; let error_message = wait_for_event_match(&codex, |event| match event { @@ -2348,6 +2424,8 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2423,6 +2501,8 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2506,6 +2586,8 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2520,6 +2602,8 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2599,6 +2683,8 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index fafbced0b7..03d54b2081 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -651,6 +651,8 @@ async fn user_turn(conversation: &Arc, text: &str) { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .expect("submit user turn"); diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index 96fef440c8..d17e94f033 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -52,6 +52,8 @@ async fn fork_thread_twice_drops_to_first_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 113a946019..703fd4ea6e 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -60,6 +60,8 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![expected_input.clone()], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -116,6 +118,8 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -174,6 +178,8 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -233,6 +239,8 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -288,6 +296,8 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -341,6 +351,8 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -394,6 +406,8 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -1036,6 +1050,8 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -1095,6 +1111,8 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 587436c83b..37b4990cb7 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -318,6 +318,8 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -416,6 +418,8 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -461,6 +465,8 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |event| { diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index c96988d18b..e36990024b 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -107,6 +107,8 @@ async fn responses_api_emits_api_request_event() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -150,6 +152,8 @@ async fn process_sse_emits_tracing_for_output_item() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -193,6 +197,8 @@ async fn process_sse_emits_failed_event_on_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -237,6 +243,8 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -301,6 +309,8 @@ async fn process_sse_failed_event_records_response_error_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -363,6 +373,8 @@ async fn process_sse_failed_event_logs_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -412,6 +424,8 @@ async fn process_sse_failed_event_logs_missing_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -470,6 +484,8 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -522,6 +538,8 @@ async fn process_sse_emits_completed_telemetry() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -594,6 +612,8 @@ async fn handle_responses_span_records_response_kind_and_tool_name() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -678,6 +698,8 @@ async fn record_responses_sets_span_fields_for_response_events() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -762,6 +784,8 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -835,6 +859,8 @@ async fn handle_response_item_records_tool_result_for_function_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -918,6 +944,8 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -985,6 +1013,8 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1093,6 +1123,8 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1144,6 +1176,8 @@ async fn handle_container_exec_user_approved_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1210,6 +1244,8 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1276,6 +1312,8 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1342,6 +1380,8 @@ async fn handle_container_exec_user_denies_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1408,6 +1448,8 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -1475,6 +1517,8 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 30b0d9d50b..dbd28caeaf 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -104,6 +104,8 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -120,6 +122,8 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index cdf69acdc0..430c4e1939 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -66,6 +66,8 @@ async fn permissions_message_sent_once_on_start() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -107,6 +109,8 @@ async fn permissions_message_added_on_override_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -134,6 +138,8 @@ async fn permissions_message_added_on_override_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -181,6 +187,8 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -192,6 +200,8 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -250,6 +260,8 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -279,6 +291,8 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -292,6 +306,8 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -351,6 +367,8 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -380,6 +398,8 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -401,6 +421,8 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -429,6 +451,8 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -483,6 +507,8 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index 0eba6e3234..cd6b19fe80 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -213,6 +213,8 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<() text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -288,6 +290,8 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> { path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -360,6 +364,8 @@ async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> { path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 96b7f64570..1360cb538a 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -151,6 +151,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -162,6 +164,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -247,6 +251,8 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -258,6 +264,8 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -320,6 +328,8 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -331,6 +341,8 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -412,6 +424,8 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -448,6 +462,8 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -530,6 +546,8 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -681,6 +699,8 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/quota_exceeded.rs b/codex-rs/core/tests/suite/quota_exceeded.rs index 466680e6a8..8c3cbe89df 100644 --- a/codex-rs/core/tests/suite/quota_exceeded.rs +++ b/codex-rs/core/tests/suite/quota_exceeded.rs @@ -46,6 +46,8 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index 06cb31630a..6509d61801 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -2036,6 +2036,8 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; diff --git a/codex-rs/core/tests/suite/request_compression.rs b/codex-rs/core/tests/suite/request_compression.rs index 7f8b996c08..3fb3dbbae1 100644 --- a/codex-rs/core/tests/suite/request_compression.rs +++ b/codex-rs/core/tests/suite/request_compression.rs @@ -45,6 +45,8 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -92,6 +94,8 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() -> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index a77ad60b5a..a4e9f14d3d 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -91,6 +91,8 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> { text_elements: text_elements.clone(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -176,6 +178,8 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; @@ -265,6 +269,8 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -305,6 +311,8 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |event| { @@ -320,6 +328,8 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |event| { @@ -390,6 +400,8 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -433,6 +445,8 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&resumed.codex, |event| { diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 81d3d1e6da..ba37377f2c 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -717,6 +717,8 @@ async fn review_history_surfaces_in_parent_session() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index dd182befb5..09c8e59bb0 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -380,6 +380,8 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() - text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index 23ffc4afb0..4fdb62f414 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -97,6 +97,8 @@ async fn continue_after_stream_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); @@ -116,6 +118,8 @@ async fn continue_after_stream_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs index 5d1b214811..dece9f2579 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -81,6 +81,8 @@ async fn retries_on_early_close() { text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index a2303c0ecf..bcf54484c7 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -62,6 +62,8 @@ mv "${tmp_path}" "${payload_path}""#, text_elements: Vec::new(), }], final_output_json_schema: None, + + metadata: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index beccedb781..51001828bf 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3,6 +3,7 @@ //! Uses a SQ (Submission Queue) / EQ (Event Queue) pattern to asynchronously communicate //! between user and agent. +use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::ffi::OsStr; @@ -238,6 +239,9 @@ pub enum Op { /// Optional JSON Schema used to constrain the final assistant message for this turn. #[serde(skip_serializing_if = "Option::is_none")] final_output_json_schema: Option, + /// Optional metadata attached only to this turn. + #[serde(default, skip_serializing_if = "Option::is_none")] + metadata: Option>, }, /// Similar to [`Op::UserInput`], but contains additional context required @@ -4256,6 +4260,7 @@ mod tests { let op = Op::UserInput { items: Vec::new(), final_output_json_schema: None, + metadata: None, }; let json_op = serde_json::to_value(op)?; @@ -4273,6 +4278,7 @@ mod tests { Op::UserInput { items: Vec::new(), final_output_json_schema: None, + metadata: None, } ); @@ -4292,6 +4298,7 @@ mod tests { let op = Op::UserInput { items: Vec::new(), final_output_json_schema: Some(schema.clone()), + metadata: None, }; let json_op = serde_json::to_value(op)?;