From c6ac258a132aec9c746be7fbb1ebd4539da1bea1 Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 7 Apr 2026 14:05:04 -0700 Subject: [PATCH 1/4] [codex-analytics] add token usage metadata --- .../analytics/src/analytics_client_tests.rs | 60 +++++++++++++++++++ codex-rs/analytics/src/client.rs | 7 +++ codex-rs/analytics/src/events.rs | 5 ++ codex-rs/analytics/src/facts.rs | 9 +++ codex-rs/analytics/src/lib.rs | 1 + codex-rs/analytics/src/reducer.rs | 46 ++++++++++++++ .../app-server/tests/suite/v2/turn_start.rs | 5 ++ codex-rs/core/src/tasks/mod.rs | 8 +++ 8 files changed, 141 insertions(+) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 1fe77760ee..af7d957ea9 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -30,6 +30,7 @@ use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnTokenUsageFact; use crate::reducer::AnalyticsReducer; use crate::reducer::normalize_path_for_skill_id; use crate::reducer::skill_id_for_local_skill; @@ -66,6 +67,7 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::TokenUsage; use pretty_assertions::assert_eq; use serde_json::json; use std::collections::HashSet; @@ -181,6 +183,20 @@ fn sample_turn_started_notification(thread_id: &str, turn_id: &str) -> ServerNot }) } +fn sample_turn_token_usage_fact(thread_id: &str, turn_id: &str) -> TurnTokenUsageFact { + TurnTokenUsageFact { + thread_id: thread_id.to_string(), + turn_id: turn_id.to_string(), + token_usage: TokenUsage { + total_tokens: 321, + input_tokens: 123, + cached_input_tokens: 45, + output_tokens: 140, + reasoning_output_tokens: 13, + }, + } +} + fn sample_turn_completed_notification( thread_id: &str, turn_id: &str, @@ -232,6 +248,7 @@ async fn ingest_turn_prerequisites( include_initialize: bool, include_resolved_config: bool, include_started: bool, + include_token_usage: bool, ) { if include_initialize { reducer @@ -301,6 +318,17 @@ async fn ingest_turn_prerequisites( ) .await; } + + if include_token_usage { + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnTokenUsage(Box::new( + sample_turn_token_usage_fact("thread-2", "turn-2"), + ))), + out, + ) + .await; + } } fn expected_absolute_path(path: &PathBuf) -> String { @@ -1045,6 +1073,11 @@ fn turn_event_serializes_expected_shape() { subagent_tool_call_count: None, web_search_count: None, image_generation_count: None, + input_tokens: None, + cached_input_tokens: None, + output_tokens: None, + reasoning_output_tokens: None, + total_tokens: None, duration_ms: Some(1234), started_at: Some(455), completed_at: Some(456), @@ -1086,6 +1119,11 @@ fn turn_event_serializes_expected_shape() { "subagent_tool_call_count": null, "web_search_count": null, "image_generation_count": null, + "input_tokens": null, + "cached_input_tokens": null, + "output_tokens": null, + "reasoning_output_tokens": null, + "total_tokens": null, "duration_ms": 1234, "started_at": 455, "completed_at": 456 @@ -1105,6 +1143,7 @@ async fn turn_lifecycle_emits_turn_event() { /*include_initialize*/ true, /*include_resolved_config*/ true, /*include_started*/ true, + /*include_token_usage*/ true, ) .await; reducer @@ -1133,6 +1172,14 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["started_at"], json!(455)); assert_eq!(payload["event_params"]["completed_at"], json!(456)); assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); + assert_eq!(payload["event_params"]["input_tokens"], json!(123)); + assert_eq!(payload["event_params"]["cached_input_tokens"], json!(45)); + assert_eq!(payload["event_params"]["output_tokens"], json!(140)); + assert_eq!( + payload["event_params"]["reasoning_output_tokens"], + json!(13) + ); + assert_eq!(payload["event_params"]["total_tokens"], json!(321)); } #[tokio::test] @@ -1146,6 +1193,7 @@ async fn turn_does_not_emit_without_required_prerequisites() { /*include_initialize*/ false, /*include_resolved_config*/ true, /*include_started*/ false, + /*include_token_usage*/ false, ) .await; reducer @@ -1175,6 +1223,7 @@ async fn turn_does_not_emit_without_required_prerequisites() { /*include_initialize*/ true, /*include_resolved_config*/ false, /*include_started*/ false, + /*include_token_usage*/ false, ) .await; reducer @@ -1202,6 +1251,7 @@ async fn turn_lifecycle_emits_failed_turn_event() { /*include_initialize*/ true, /*include_resolved_config*/ true, /*include_started*/ true, + /*include_token_usage*/ false, ) .await; reducer @@ -1233,6 +1283,7 @@ async fn turn_lifecycle_emits_interrupted_turn_event_without_error() { /*include_initialize*/ true, /*include_resolved_config*/ true, /*include_started*/ true, + /*include_token_usage*/ false, ) .await; reducer @@ -1264,6 +1315,7 @@ async fn turn_completed_without_started_notification_emits_null_started_at() { /*include_initialize*/ true, /*include_resolved_config*/ true, /*include_started*/ false, + /*include_token_usage*/ false, ) .await; reducer @@ -1281,6 +1333,14 @@ async fn turn_completed_without_started_notification_emits_null_started_at() { let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); assert_eq!(payload["event_params"]["started_at"], json!(null)); assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); + assert_eq!(payload["event_params"]["input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["cached_input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["output_tokens"], json!(null)); + assert_eq!( + payload["event_params"]["reasoning_output_tokens"], + json!(null) + ); + assert_eq!(payload["event_params"]["total_tokens"], json!(null)); } fn sample_plugin_metadata() -> PluginTelemetryMetadata { diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index e61affe76c..41802bdb0e 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -14,6 +14,7 @@ use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; +use crate::facts::TurnTokenUsageFact; use crate::reducer::AnalyticsReducer; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponse; @@ -196,6 +197,12 @@ impl AnalyticsEventsClient { )); } + pub fn track_turn_token_usage(&self, fact: TurnTokenUsageFact) { + self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnTokenUsage( + Box::new(fact), + ))); + } + pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) { self.record_fact(AnalyticsFact::Custom( CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput { diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 1947fd506d..48914d9de9 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -156,6 +156,11 @@ pub(crate) struct CodexTurnEventParams { pub(crate) subagent_tool_call_count: Option, pub(crate) web_search_count: Option, pub(crate) image_generation_count: Option, + pub(crate) input_tokens: Option, + pub(crate) cached_input_tokens: Option, + pub(crate) output_tokens: Option, + pub(crate) reasoning_output_tokens: Option, + pub(crate) total_tokens: Option, pub(crate) duration_ms: Option, pub(crate) started_at: Option, pub(crate) completed_at: Option, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index fdc45cb9b3..ed5d96bb25 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -16,6 +16,7 @@ use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::TokenUsage; use serde::Serialize; use std::path::PathBuf; @@ -58,6 +59,13 @@ pub struct TurnResolvedConfigFact { pub is_first_turn: bool, } +#[derive(Clone)] +pub struct TurnTokenUsageFact { + pub turn_id: String, + pub thread_id: String, + pub token_usage: TokenUsage, +} + #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum TurnSubmissionType { @@ -133,6 +141,7 @@ pub(crate) enum AnalyticsFact { pub(crate) enum CustomAnalyticsFact { SubAgentThreadStarted(SubAgentThreadStartedInput), TurnResolvedConfig(Box), + TurnTokenUsage(Box), SkillInvoked(SkillInvokedInput), AppMentioned(AppMentionedInput), AppUsed(AppUsedInput), diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index d3193c6fd1..5a4630ff47 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -13,6 +13,7 @@ pub use facts::TrackEventsContext; pub use facts::TurnResolvedConfigFact; pub use facts::TurnStatus; pub use facts::TurnSubmissionType; +pub use facts::TurnTokenUsageFact; pub use facts::build_track_events_context; #[cfg(test)] diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 80e6786af5..e24900ef04 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -30,6 +30,7 @@ use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnTokenUsageFact; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponse; use codex_app_server_protocol::CodexErrorInfo; @@ -46,6 +47,7 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; +use codex_protocol::protocol::TokenUsage; use sha1::Digest; use std::collections::HashMap; use std::path::Path; @@ -85,6 +87,7 @@ struct TurnState { num_input_images: Option, resolved_config: Option, started_at: Option, + token_usage: Option, completed: Option, } @@ -129,6 +132,9 @@ impl AnalyticsReducer { CustomAnalyticsFact::TurnResolvedConfig(input) => { self.ingest_turn_resolved_config(*input, out); } + CustomAnalyticsFact::TurnTokenUsage(input) => { + self.ingest_turn_token_usage(*input, out); + } CustomAnalyticsFact::SkillInvoked(input) => { self.ingest_skill_invoked(input, out).await; } @@ -221,6 +227,7 @@ impl AnalyticsReducer { num_input_images: None, resolved_config: None, started_at: None, + token_usage: None, completed: None, }); turn_state.thread_id = Some(thread_id); @@ -229,6 +236,26 @@ impl AnalyticsReducer { self.maybe_emit_turn_event(&turn_id, out); } + fn ingest_turn_token_usage( + &mut self, + input: TurnTokenUsageFact, + out: &mut Vec, + ) { + let turn_id = input.turn_id.clone(); + let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState { + connection_id: None, + thread_id: None, + num_input_images: None, + resolved_config: None, + started_at: None, + token_usage: None, + completed: None, + }); + turn_state.thread_id = Some(input.thread_id); + turn_state.token_usage = Some(input.token_usage); + self.maybe_emit_turn_event(&turn_id, out); + } + async fn ingest_skill_invoked( &mut self, input: SkillInvokedInput, @@ -373,6 +400,7 @@ impl AnalyticsReducer { num_input_images: None, resolved_config: None, started_at: None, + token_usage: None, completed: None, }); turn_state.connection_id = Some(connection_id); @@ -397,6 +425,7 @@ impl AnalyticsReducer { num_input_images: None, resolved_config: None, started_at: None, + token_usage: None, completed: None, }); turn_state.started_at = notification @@ -414,6 +443,7 @@ impl AnalyticsReducer { num_input_images: None, resolved_config: None, started_at: None, + token_usage: None, completed: None, }); turn_state.completed = Some(CompletedTurnState { @@ -532,6 +562,7 @@ fn codex_turn_event_params( personality, is_first_turn, } = resolved_config; + let token_usage = turn_state.token_usage.clone(); CodexTurnEventParams { thread_id, turn_id, @@ -563,6 +594,21 @@ fn codex_turn_event_params( subagent_tool_call_count: None, web_search_count: None, image_generation_count: None, + input_tokens: token_usage + .as_ref() + .map(|token_usage| token_usage.input_tokens), + cached_input_tokens: token_usage + .as_ref() + .map(|token_usage| token_usage.cached_input_tokens), + output_tokens: token_usage + .as_ref() + .map(|token_usage| token_usage.output_tokens), + reasoning_output_tokens: token_usage + .as_ref() + .map(|token_usage| token_usage.reasoning_output_tokens), + total_tokens: token_usage + .as_ref() + .map(|token_usage| token_usage.total_tokens), duration_ms: completed.duration_ms, started_at, completed_at: Some(completed.completed_at), 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 a658811eb4..d878e33cf5 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -424,6 +424,11 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { assert!(event["event_params"]["started_at"].as_u64().is_some()); assert!(event["event_params"]["completed_at"].as_u64().is_some()); assert!(event["event_params"]["duration_ms"].as_u64().is_some()); + assert_eq!(event["event_params"]["input_tokens"], 0); + assert_eq!(event["event_params"]["cached_input_tokens"], 0); + assert_eq!(event["event_params"]["output_tokens"], 0); + assert_eq!(event["event_params"]["reasoning_output_tokens"], 0); + assert_eq!(event["event_params"]["total_tokens"], 0); Ok(()) } diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 5da3650bf8..d246e69cac 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -30,6 +30,7 @@ use crate::hook_runtime::record_pending_input; use crate::state::ActiveTurn; use crate::state::RunningTask; use crate::state::TaskKind; +use codex_analytics::TurnTokenUsageFact; use codex_login::AuthManager; use codex_models_manager::manager::ModelsManager; use codex_otel::SessionTelemetry; @@ -485,6 +486,13 @@ impl Session { - token_usage_at_turn_start.total_tokens) .max(0), }; + self.services + .analytics_events_client + .track_turn_token_usage(TurnTokenUsageFact { + turn_id: turn_context.sub_id.clone(), + thread_id: self.conversation_id.to_string(), + token_usage: turn_token_usage.clone(), + }); self.services.session_telemetry.histogram( TURN_TOKEN_USAGE_METRIC, turn_token_usage.total_tokens, From cf986a9bc583e8a04b4537b31c42eb78f83b1959 Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 7 Apr 2026 14:49:00 -0700 Subject: [PATCH 2/4] [codex-analytics] add queued submission metadata --- .../analytics/src/analytics_client_tests.rs | 90 ++++++++++++++- .../schema/json/ClientRequest.json | 18 +++ .../codex_app_server_protocol.schemas.json | 18 +++ .../codex_app_server_protocol.v2.schemas.json | 18 +++ .../schema/json/v2/TurnStartParams.json | 18 +++ .../schema/typescript/SubmissionType.ts | 5 + .../schema/typescript/index.ts | 1 + .../schema/typescript/v2/TurnStartParams.ts | 4 + .../app-server-protocol/src/protocol/v2.rs | 7 ++ .../app-server/src/codex_message_processor.rs | 1 + .../src/message_processor/tracing_tests.rs | 1 + .../app-server/tests/suite/v2/analytics.rs | 35 ++++++ .../app-server/tests/suite/v2/turn_start.rs | 107 ++++++++++++++++++ codex-rs/core/src/agent/control_tests.rs | 3 + codex-rs/core/src/codex.rs | 31 ++++- codex-rs/core/src/codex_delegate.rs | 1 + codex-rs/core/src/codex_tests.rs | 6 + codex-rs/core/src/guardian/review_session.rs | 1 + .../src/tools/handlers/multi_agents_tests.rs | 1 + codex-rs/core/tests/common/test_codex.rs | 1 + codex-rs/core/tests/suite/abort_tasks.rs | 5 + codex-rs/core/tests/suite/apply_patch_cli.rs | 7 ++ codex-rs/core/tests/suite/approvals.rs | 1 + codex-rs/core/tests/suite/client.rs | 34 ++++++ .../core/tests/suite/client_websockets.rs | 2 + codex-rs/core/tests/suite/code_mode.rs | 1 + .../tests/suite/collaboration_instructions.rs | 16 +++ codex-rs/core/tests/suite/compact.rs | 36 ++++++ codex-rs/core/tests/suite/compact_remote.rs | 43 +++++++ .../core/tests/suite/compact_resume_fork.rs | 1 + codex-rs/core/tests/suite/exec_policy.rs | 2 + codex-rs/core/tests/suite/fork_thread.rs | 1 + codex-rs/core/tests/suite/hooks.rs | 2 + codex-rs/core/tests/suite/image_rollout.rs | 2 + codex-rs/core/tests/suite/items.rs | 14 +++ codex-rs/core/tests/suite/json_result.rs | 1 + codex-rs/core/tests/suite/live_reload.rs | 1 + codex-rs/core/tests/suite/model_switching.rs | 14 +++ .../core/tests/suite/model_visible_layout.rs | 8 ++ codex-rs/core/tests/suite/models_cache_ttl.rs | 1 + .../core/tests/suite/models_etag_responses.rs | 1 + codex-rs/core/tests/suite/otel.rs | 22 ++++ codex-rs/core/tests/suite/pending_input.rs | 3 + .../core/tests/suite/permissions_messages.rs | 15 +++ codex-rs/core/tests/suite/personality.rs | 13 +++ codex-rs/core/tests/suite/plugins.rs | 3 + codex-rs/core/tests/suite/prompt_caching.rs | 15 +++ codex-rs/core/tests/suite/quota_exceeded.rs | 1 + .../core/tests/suite/realtime_conversation.rs | 1 + codex-rs/core/tests/suite/remote_models.rs | 4 + .../core/tests/suite/request_compression.rs | 2 + .../core/tests/suite/request_permissions.rs | 1 + .../tests/suite/request_permissions_tool.rs | 1 + .../core/tests/suite/request_user_input.rs | 2 + codex-rs/core/tests/suite/resume.rs | 7 ++ codex-rs/core/tests/suite/review.rs | 1 + codex-rs/core/tests/suite/rmcp_client.rs | 6 + .../tests/suite/safety_check_downgrade.rs | 4 + codex-rs/core/tests/suite/search_tool.rs | 1 + codex-rs/core/tests/suite/shell_snapshot.rs | 4 + codex-rs/core/tests/suite/skill_approval.rs | 1 + codex-rs/core/tests/suite/skills.rs | 1 + codex-rs/core/tests/suite/sqlite_state.rs | 1 + .../suite/stream_error_allows_next_turn.rs | 2 + .../core/tests/suite/stream_no_completed.rs | 1 + codex-rs/core/tests/suite/tool_harness.rs | 5 + codex-rs/core/tests/suite/tool_parallelism.rs | 2 + codex-rs/core/tests/suite/truncation.rs | 1 + codex-rs/core/tests/suite/unified_exec.rs | 6 + .../core/tests/suite/user_notification.rs | 1 + codex-rs/core/tests/suite/user_shell_cmd.rs | 1 + codex-rs/core/tests/suite/view_image.rs | 14 +++ .../core/tests/suite/websocket_fallback.rs | 1 + codex-rs/core/tests/suite/window_headers.rs | 1 + codex-rs/exec/src/lib.rs | 1 + codex-rs/mcp-server/src/codex_tool_runner.rs | 2 + codex-rs/protocol/src/protocol.rs | 39 +++++++ codex-rs/tui/src/app.rs | 2 + codex-rs/tui/src/app_command.rs | 6 + codex-rs/tui/src/app_server_session.rs | 3 + codex-rs/tui/src/chatwidget.rs | 17 ++- .../chatwidget/tests/composer_submission.rs | 23 ++++ 82 files changed, 792 insertions(+), 5 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index af7d957ea9..d026aeaac8 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -30,6 +30,7 @@ use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSubmissionType; use crate::facts::TurnTokenUsageFact; use crate::reducer::AnalyticsReducer; use crate::reducer::normalize_path_for_skill_id; @@ -226,7 +227,7 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { turn_id: turn_id.to_string(), thread_id: "thread-2".to_string(), num_input_images: 1, - submission_type: None, + submission_type: Some(TurnSubmissionType::Default), model: "gpt-5".to_string(), model_provider: "openai".to_string(), sandbox_policy: SandboxPolicy::new_read_only_policy(), @@ -1048,7 +1049,7 @@ fn turn_event_serializes_expected_shape() { thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), product_client_id: "codex-tui".to_string(), - submission_type: None, + submission_type: Some(TurnSubmissionType::Default), model: Some("gpt-5".to_string()), model_provider: "openai".to_string(), sandbox_policy: Some("read_only"), @@ -1094,7 +1095,7 @@ fn turn_event_serializes_expected_shape() { "thread_id": "thread-2", "turn_id": "turn-2", "product_client_id": "codex-tui", - "submission_type": null, + "submission_type": "default", "model": "gpt-5", "model_provider": "openai", "sandbox_policy": "read_only", @@ -1182,6 +1183,89 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["total_tokens"], json!(321)); } +#[tokio::test] +async fn queued_submission_type_emits_queued_turn_event() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Initialize { + connection_id: 7, + params: InitializeParams { + client_info: ClientInfo { + name: "codex-tui".to_string(), + title: None, + version: "1.0.0".to_string(), + }, + capabilities: None, + }, + product_client_id: "codex-tui".to_string(), + runtime: CodexRuntimeMetadata { + codex_rs_version: "0.1.0".to_string(), + runtime_os: "macos".to_string(), + runtime_os_version: "15.3.1".to_string(), + runtime_arch: "aarch64".to_string(), + }, + rpc_transport: AppServerRpcTransport::Stdio, + }, + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Request { + connection_id: 7, + request_id: RequestId::Integer(3), + request: Box::new(sample_turn_start_request("thread-2", /*request_id*/ 3)), + }, + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Response { + connection_id: 7, + response: Box::new(sample_turn_start_response("turn-2", /*request_id*/ 3)), + }, + &mut out, + ) + .await; + let mut resolved_config = sample_turn_resolved_config("turn-2"); + resolved_config.submission_type = Some(TurnSubmissionType::Queued); + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new( + resolved_config, + ))), + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_started_notification( + "thread-2", "turn-2", + ))), + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_completed_notification( + "thread-2", + "turn-2", + AppServerTurnStatus::Completed, + /*codex_error_info*/ None, + ))), + &mut out, + ) + .await; + + assert_eq!(out.len(), 1); + let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); + assert_eq!(payload["event_params"]["submission_type"], json!("queued")); +} + #[tokio::test] async fn turn_does_not_emit_without_required_prerequisites() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 5039a0bfe7..323311e1bc 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2515,6 +2515,13 @@ }, "type": "object" }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TextElement": { "properties": { "byteRange": { @@ -3283,6 +3290,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { 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 da21699875..deb9619f3a 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 @@ -12202,6 +12202,13 @@ } ] }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -14763,6 +14770,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/v2/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { 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 a66c34e14d..7195b658ac 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 @@ -10057,6 +10057,13 @@ } ] }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TerminalInteractionNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12618,6 +12625,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { 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..982cf96634 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -354,6 +354,13 @@ ], "type": "object" }, + "SubmissionType": { + "enum": [ + "prompt", + "prompt_queued" + ], + "type": "string" + }, "TextElement": { "properties": { "byteRange": { @@ -595,6 +602,17 @@ ], "description": "Override the service tier for this turn and subsequent turns." }, + "submissionType": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionType" + }, + { + "type": "null" + } + ], + "description": "Metadata describing how the prompt was submitted." + }, "summary": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts b/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts new file mode 100644 index 0000000000..7c32c07052 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SubmissionType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SubmissionType = "prompt" | "prompt_queued"; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 7ffc15e83d..a15d949c89 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -65,6 +65,7 @@ export type { ServiceTier } from "./ServiceTier"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; export type { SubAgentSource } from "./SubAgentSource"; +export type { SubmissionType } from "./SubmissionType"; export type { ThreadId } from "./ThreadId"; export type { Tool } from "./Tool"; export type { Verbosity } from "./Verbosity"; 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..9697838d9c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -6,6 +6,7 @@ import type { Personality } from "../Personality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; import type { ServiceTier } from "../ServiceTier"; +import type { SubmissionType } from "../SubmissionType"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; @@ -45,6 +46,9 @@ personality?: Personality | null, /** * this turn. */ outputSchema?: JsonValue | null, /** + * Metadata describing how the prompt was submitted. + */ +submissionType?: SubmissionType | null, /** * EXPERIMENTAL - Set a pre-set collaboration mode. * Takes precedence over model, reasoning_effort, and developer instructions if set. * diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e0d22bfa24..ac3f15c59c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -80,6 +80,7 @@ use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; use codex_protocol::protocol::SkillScope as CoreSkillScope; use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TokenUsage as CoreTokenUsage; use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; use codex_protocol::request_permissions::PermissionGrantScope as CorePermissionGrantScope; @@ -4058,6 +4059,11 @@ pub struct TurnStartParams { #[ts(optional = nullable)] pub output_schema: Option, + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub submission_type: Option, + /// EXPERIMENTAL - Set a pre-set collaboration mode. /// Takes precedence over model, reasoning_effort, and developer instructions if set. /// @@ -8420,6 +8426,7 @@ mod tests { service_tier: None, effort: None, summary: None, + submission_type: None, output_schema: None, collaboration_mode: None, personality: None, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 8a05de6f54..759cc60017 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -6650,6 +6650,7 @@ impl CodexMessageProcessor { Op::UserInput { items: mapped_items, final_output_json_schema: params.output_schema, + submission_type: params.submission_type, }, ) .await; 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 ef88364a3a..fbab5a7726 100644 --- a/codex-rs/app-server/src/message_processor/tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor/tracing_tests.rs @@ -615,6 +615,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { effort: None, summary: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }, diff --git a/codex-rs/app-server/tests/suite/v2/analytics.rs b/codex-rs/app-server/tests/suite/v2/analytics.rs index a3ecdbc1f4..d0fe9bbeb9 100644 --- a/codex-rs/app-server/tests/suite/v2/analytics.rs +++ b/codex-rs/app-server/tests/suite/v2/analytics.rs @@ -173,6 +173,41 @@ pub(crate) async fn wait_for_analytics_event( .await? } +pub(crate) async fn wait_for_analytics_turn_event( + server: &MockServer, + read_timeout: Duration, + turn_id: &str, +) -> Result { + timeout(read_timeout, async { + loop { + let Some(requests) = server.received_requests().await else { + tokio::time::sleep(Duration::from_millis(25)).await; + continue; + }; + for request in &requests { + if request.method != "POST" + || request.url.path() != "/codex/analytics-events/events" + { + continue; + } + let payload: Value = serde_json::from_slice(&request.body) + .map_err(|err| anyhow::anyhow!("invalid analytics payload: {err}"))?; + let Some(events) = payload["events"].as_array() else { + continue; + }; + if let Some(event) = events.iter().find(|event| { + event["event_type"] == "codex_turn_event" + && event["event_params"]["turn_id"] == turn_id + }) { + return Ok::(event.clone()); + } + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + }) + .await? +} + pub(crate) fn thread_initialized_event(payload: &Value) -> Result<&Value> { let events = payload["events"] .as_array() 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 d878e33cf5..5358a2752f 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -55,6 +55,7 @@ use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::protocol::SubmissionType; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use core_test_support::responses; use core_test_support::skip_if_no_network; @@ -69,6 +70,7 @@ use tokio::time::timeout; use super::analytics::enable_analytics_capture; use super::analytics::wait_for_analytics_event; use super::analytics::wait_for_analytics_payload; +use super::analytics::wait_for_analytics_turn_event; #[cfg(windows)] const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25); @@ -419,7 +421,9 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { assert_eq!(event["event_params"]["model"], "mock-model"); assert_eq!(event["event_params"]["model_provider"], "mock_provider"); assert_eq!(event["event_params"]["sandbox_policy"], "read_only"); + assert_eq!(event["event_params"]["submission_type"], "default"); assert_eq!(event["event_params"]["num_input_images"], 1); + assert_eq!(event["event_params"]["is_first_turn"], true); assert_eq!(event["event_params"]["status"], "completed"); assert!(event["event_params"]["started_at"].as_u64().is_some()); assert!(event["event_params"]["completed_at"].as_u64().is_some()); @@ -496,6 +500,107 @@ async fn turn_start_does_not_track_turn_event_analytics_without_feature() -> Res payload.is_err(), "turn analytics should be gated off when general_analytics is disabled" ); + + Ok(()) +} + +#[tokio::test] +async fn turn_start_tracks_second_turn_as_not_first_and_queued_submission_analytics() -> Result<()> +{ + let responses = vec![ + create_final_assistant_message_sse_response("Done 1")?, + create_final_assistant_message_sse_response("Done 2")?, + ]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + + let codex_home = TempDir::new()?; + write_mock_responses_config_toml_with_chatgpt_base_url( + codex_home.path(), + &server.uri(), + &server.uri(), + )?; + enable_analytics_capture(&server, codex_home.path()).await?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams { + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let first_turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "first turn".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + let first_turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(first_turn_req)), + ) + .await??; + let TurnStartResponse { turn: first_turn } = to_response::(first_turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let first_turn_event = + wait_for_analytics_turn_event(&server, DEFAULT_READ_TIMEOUT, &first_turn.id).await?; + assert_eq!( + first_turn_event["event_params"]["submission_type"], + "default" + ); + assert_eq!(first_turn_event["event_params"]["is_first_turn"], true); + + let second_turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id, + input: vec![V2UserInput::Text { + text: "second turn".to_string(), + text_elements: Vec::new(), + }], + submission_type: Some(SubmissionType::PromptQueued), + ..Default::default() + }) + .await?; + let second_turn_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(second_turn_req)), + ) + .await??; + let TurnStartResponse { turn: second_turn } = + to_response::(second_turn_resp)?; + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let second_turn_event = + wait_for_analytics_turn_event(&server, DEFAULT_READ_TIMEOUT, &second_turn.id).await?; + assert_eq!( + second_turn_event["event_params"]["submission_type"], + "queued" + ); + assert_eq!(second_turn_event["event_params"]["is_first_turn"], false); + Ok(()) } @@ -1653,6 +1758,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { summary: Some(ReasoningSummary::Auto), service_tier: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }) @@ -1686,6 +1792,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { summary: Some(ReasoningSummary::Auto), service_tier: None, personality: None, + submission_type: None, output_schema: None, collaboration_mode: None, }) diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 54693228fa..17e620e6ba 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -425,6 +425,7 @@ async fn send_input_submits_user_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }, ); let captured = harness @@ -571,6 +572,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }, ); let captured = harness @@ -678,6 +680,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }, ); let captured = harness diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 283a165aa3..769f27db57 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -53,6 +53,7 @@ use codex_analytics::AppInvocation; use codex_analytics::InvocationType; use codex_analytics::SubAgentThreadStartedInput; use codex_analytics::TurnResolvedConfigFact; +use codex_analytics::TurnSubmissionType; use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; @@ -119,6 +120,7 @@ use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; @@ -885,6 +887,7 @@ pub(crate) struct TurnContext { pub(crate) current_date: Option, pub(crate) timezone: Option, pub(crate) app_server_client_name: Option, + pub(crate) submission_type: Option, pub(crate) developer_instructions: Option, pub(crate) compact_prompt: Option, pub(crate) user_instructions: Option, @@ -1003,6 +1006,7 @@ impl TurnContext { current_date: self.current_date.clone(), timezone: self.timezone.clone(), app_server_client_name: self.app_server_client_name.clone(), + submission_type: self.submission_type, developer_instructions: self.developer_instructions.clone(), compact_prompt: self.compact_prompt.clone(), user_instructions: self.user_instructions.clone(), @@ -1088,6 +1092,13 @@ impl TurnContext { } } +fn turn_submission_type(submission_type: SubmissionType) -> TurnSubmissionType { + match submission_type { + SubmissionType::Prompt => TurnSubmissionType::Default, + SubmissionType::PromptQueued => TurnSubmissionType::Queued, + } +} + fn local_time_context() -> (String, String) { match iana_time_zone::get_timezone() { Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), @@ -1265,6 +1276,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) personality: Option, pub(crate) app_server_client_name: Option, pub(crate) app_server_client_version: Option, + pub(crate) submission_type: Option, } pub(crate) struct AppServerClientMetadata { @@ -1433,6 +1445,7 @@ impl Session { network: Option, environment: Option>, sub_id: String, + submission_type: Option, js_repl: Arc, skills_outcome: Arc, ) -> TurnContext { @@ -1497,6 +1510,7 @@ impl Session { current_date: Some(current_date), timezone: Some(timezone), app_server_client_name: session_configuration.app_server_client_name.clone(), + submission_type, developer_instructions: session_configuration.developer_instructions.clone(), compact_prompt: session_configuration.compact_prompt.clone(), user_instructions: session_configuration.user_instructions.clone(), @@ -2209,6 +2223,7 @@ impl Session { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }, ) .await; @@ -2512,6 +2527,7 @@ impl Session { sub_id, session_configuration, updates.final_output_json_schema, + updates.submission_type, sandbox_policy_changed, ) .await) @@ -2522,6 +2538,7 @@ impl Session { sub_id: String, session_configuration: SessionConfiguration, final_output_json_schema: Option>, + submission_type: Option, sandbox_policy_changed: bool, ) -> Arc { let per_turn_config = Self::build_per_turn_config(&session_configuration); @@ -2587,6 +2604,7 @@ impl Session { .map(StartedNetworkProxy::proxy), self.services.environment.clone(), sub_id, + submission_type, Arc::clone(&self.js_repl), skills_outcome, ); @@ -2699,6 +2717,7 @@ impl Session { sub_id, session_configuration, /*final_output_json_schema*/ None, + /*submission_type*/ None, /*sandbox_policy_changed*/ false, ) .await @@ -4891,6 +4910,7 @@ mod handlers { items, collaboration_mode, personality, + submission_type, } => { let collaboration_mode = collaboration_mode.or_else(|| { Some(CollaborationMode { @@ -4917,16 +4937,19 @@ mod handlers { personality, app_server_client_name: None, app_server_client_version: None, + submission_type, }, ) } Op::UserInput { items, final_output_json_schema, + submission_type, } => ( items, SessionSettingsUpdate { final_output_json_schema: Some(final_output_json_schema), + submission_type, ..Default::default() }, ), @@ -5733,6 +5756,7 @@ async fn spawn_review_thread( current_date: parent_turn_context.current_date.clone(), timezone: parent_turn_context.timezone.clone(), app_server_client_name: parent_turn_context.app_server_client_name.clone(), + submission_type: None, developer_instructions: None, user_instructions: None, compact_prompt: parent_turn_context.compact_prompt.clone(), @@ -6371,7 +6395,12 @@ async fn track_turn_resolved_config_analytics( matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. }) }) .count(), - submission_type: None, + submission_type: Some( + turn_context + .submission_type + .map(turn_submission_type) + .unwrap_or(TurnSubmissionType::Default), + ), model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(), sandbox_policy: turn_context.sandbox_policy.get().clone(), diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 9aea3e5594..82b78e9030 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -186,6 +186,7 @@ pub(crate) async fn run_codex_thread_one_shot( io.submit(Op::UserInput { items: input, final_output_json_schema, + submission_type: None, }) .await?; diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index a5edfba863..f58dea067b 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1197,6 +1197,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1252,6 +1253,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2811,6 +2813,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { /*network*/ None, Some(environment), "turn_id".to_string(), + /*submission_type*/ None, Arc::clone(&js_repl), skills_outcome, ); @@ -3149,6 +3152,7 @@ fn op_kind_distinguishes_turn_ops() { Op::UserInput { items: vec![], final_output_json_schema: None, + submission_type: None, } .kind(), "user_input" @@ -3179,6 +3183,7 @@ async fn user_turn_updates_approvals_reviewer() { final_output_json_schema: None, collaboration_mode: None, personality: config.personality, + submission_type: None, }, ) .await; @@ -3653,6 +3658,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( /*network*/ None, Some(environment), "turn_id".to_string(), + /*submission_type*/ None, Arc::clone(&js_repl), skills_outcome, )); diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 368b812e56..fe472eb8be 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -523,6 +523,7 @@ async fn run_review_on_session( final_output_json_schema: Some(params.schema.clone()), collaboration_mode: None, personality: params.personality, + submission_type: None, }) .await }), 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 5f09c248a4..517bdf2a53 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2004,6 +2004,7 @@ async fn send_input_accepts_structured_items() { }, ], final_output_json_schema: None, + submission_type: None, }; let captured = manager .captured_ops() diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index a6f3f0c72b..52f05e1caa 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -760,6 +760,7 @@ impl TestCodex { service_tier, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index af3c70b74e..797c3cf37b 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -51,6 +51,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -105,6 +106,7 @@ async fn interrupt_tool_records_history_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -123,6 +125,7 @@ async fn interrupt_tool_records_history_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -203,6 +206,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -221,6 +225,7 @@ async fn interrupt_persists_turn_aborted_marker_in_next_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index a8b429c0dd..c6a1b22970 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -368,6 +368,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -978,6 +979,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1060,6 +1062,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1212,6 +1215,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1277,6 +1281,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1350,6 +1355,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1423,6 +1429,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index f7874eaf45..837c1c331c 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -597,6 +597,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index a4336bd7d0..13517bcc9a 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -387,6 +387,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -749,6 +750,7 @@ async fn includes_conversation_id_and_model_headers_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -947,6 +949,7 @@ async fn includes_base_instructions_override_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -993,6 +996,7 @@ async fn omits_explicit_null_base_instructions_from_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1041,6 +1045,7 @@ async fn chatgpt_auth_sends_correct_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1154,6 +1159,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1190,6 +1196,7 @@ async fn includes_user_instructions_message_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1275,6 +1282,7 @@ async fn includes_apps_guidance_as_developer_message_for_chatgpt_auth() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1335,6 +1343,7 @@ async fn omits_apps_guidance_for_api_key_auth_even_when_feature_enabled() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1391,6 +1400,7 @@ async fn omits_apps_guidance_when_configured_off() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1430,6 +1440,7 @@ async fn omits_environment_context_when_configured_off() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1484,6 +1495,7 @@ async fn skills_append_to_developer_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1535,6 +1547,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1577,6 +1590,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1617,6 +1631,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1687,6 +1702,7 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; @@ -1729,6 +1745,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1801,6 +1818,7 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await .unwrap(); @@ -1844,6 +1862,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1900,6 +1919,7 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1936,6 +1956,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1981,6 +2002,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2025,6 +2047,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2074,6 +2097,7 @@ async fn includes_developer_instructions_message_in_request() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2364,6 +2388,7 @@ async fn token_count_includes_rate_limits_snapshot() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2529,6 +2554,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submission should succeed while emitting usage limit error events"); @@ -2603,6 +2629,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -2615,6 +2642,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -2697,6 +2725,7 @@ async fn incomplete_response_emits_content_filter_error_message() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -2804,6 +2833,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2889,6 +2919,7 @@ async fn env_var_overrides_loaded_auth() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2950,6 +2981,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2963,6 +2995,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2976,6 +3009,7 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 7ff4a9f88d..4ca6b04e6b 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1003,6 +1003,7 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submission should succeed while emitting usage limit error events"); @@ -1088,6 +1089,7 @@ async fn responses_websocket_invalid_request_error_with_status_is_forwarded() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submission should succeed while emitting invalid request events"); diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 9ddc169a60..94b799a6af 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -2368,6 +2368,7 @@ text( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index df8d6e4fe4..f6d82bd7ca 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -84,6 +84,7 @@ async fn no_collaboration_instructions_by_default() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -143,6 +144,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -191,6 +193,7 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> { collaboration_mode: Some(collaboration_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -241,6 +244,7 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -307,6 +311,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu collaboration_mode: Some(turn_mode), final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -364,6 +369,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -391,6 +397,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -447,6 +454,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -474,6 +482,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -532,6 +541,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -562,6 +572,7 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -621,6 +632,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -651,6 +663,7 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -714,6 +727,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -727,6 +741,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -783,6 +798,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: 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 df8fad124d..ebdd6babd1 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -245,6 +245,7 @@ async fn summarize_context_three_requests_and_instructions() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -267,6 +268,7 @@ async fn summarize_context_three_requests_and_instructions() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -443,6 +445,7 @@ async fn manual_compact_uses_custom_prompt() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit first user turn"); @@ -587,6 +590,7 @@ async fn manual_compact_emits_context_compaction_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -750,6 +754,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit user input"); @@ -1249,6 +1254,7 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1262,6 +1268,7 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1275,6 +1282,7 @@ async fn auto_compact_runs_after_token_limit_hit() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1443,6 +1451,7 @@ async fn auto_compact_emits_context_compaction_items() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1521,6 +1530,7 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1533,6 +1543,7 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1545,6 +1556,7 @@ async fn auto_compact_starts_after_turn_started() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1668,6 +1680,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .unwrap(); @@ -1759,6 +1772,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit first user turn"); @@ -1784,6 +1798,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit second user turn"); @@ -1895,6 +1910,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit pre-resume turn"); @@ -1944,6 +1960,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit resumed user turn"); @@ -2037,6 +2054,7 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2049,6 +2067,7 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2061,6 +2080,7 @@ async fn auto_compact_persists_rollout_entries() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2147,6 +2167,7 @@ async fn manual_compact_retries_after_context_window_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2258,6 +2279,7 @@ async fn manual_compact_non_context_failure_retries_then_emits_task_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit user input"); @@ -2350,6 +2372,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2365,6 +2388,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2380,6 +2404,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2541,6 +2566,7 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2643,6 +2669,7 @@ async fn snapshot_request_shape_mid_turn_continuation_compaction() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2839,6 +2866,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -2956,6 +2984,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -3015,6 +3044,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit user input"); @@ -3050,6 +3080,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess }, ], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit user input"); @@ -3149,6 +3180,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit first user turn"); @@ -3174,6 +3206,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await .expect("submit second user turn"); @@ -3260,6 +3293,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit first user"); @@ -3272,6 +3306,7 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit second user"); @@ -3342,6 +3377,7 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: 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 dd99e0f391..dbea43e643 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -235,6 +235,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -249,6 +250,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -379,6 +381,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -453,6 +456,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -464,6 +468,7 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -579,6 +584,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -590,6 +596,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -607,6 +614,7 @@ async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -704,6 +712,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -715,6 +724,7 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -806,6 +816,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&baseline_codex, |event| { @@ -820,6 +831,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&baseline_codex, |event| { @@ -905,6 +917,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&override_codex, |event| { @@ -919,6 +932,7 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&override_codex, |event| { @@ -987,6 +1001,7 @@ async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -1065,6 +1080,7 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -1147,6 +1163,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1288,6 +1305,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1303,6 +1321,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1325,6 +1344,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1419,6 +1439,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1433,6 +1454,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1502,6 +1524,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1513,6 +1536,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_sta text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1577,6 +1601,7 @@ async fn remote_request_uses_custom_experimental_realtime_start_instructions() - text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1635,6 +1660,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1648,6 +1674,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_end text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1720,6 +1747,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1734,6 +1762,7 @@ async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1814,6 +1843,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1827,6 +1857,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_real text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1915,6 +1946,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -1942,6 +1974,7 @@ async fn snapshot_request_shape_remote_compact_resume_restates_realtime_end() -> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2035,6 +2068,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2119,6 +2153,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2145,6 +2180,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2261,6 +2297,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2272,6 +2309,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceed text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; let error_message = wait_for_event_match(&codex, |event| match event { @@ -2354,6 +2392,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2429,6 +2468,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2512,6 +2552,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2526,6 +2567,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -2605,6 +2647,7 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: 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 8f4f2ae755..4cfa7c1cb0 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -808,6 +808,7 @@ async fn user_turn(conversation: &Arc, text: &str) { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .expect("submit user turn"); diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index fb055c970f..6de4141e6c 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -59,6 +59,7 @@ async fn submit_user_turn( service_tier: None, collaboration_mode, personality: None, + submission_type: None, }) .await?; Ok(()) @@ -140,6 +141,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index e24cb74d7c..0b1c645be1 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -53,6 +53,7 @@ async fn fork_thread_twice_drops_to_first_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index c6e8a48d27..07479cd9b7 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -904,6 +904,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -920,6 +921,7 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; } diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 8195bd0a86..0594e48f31 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -130,6 +130,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -214,6 +215,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index f949cd4b97..3b2bc50644 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -86,6 +86,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![expected_input.clone()], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -142,6 +143,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -200,6 +202,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -259,6 +262,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -323,6 +327,7 @@ async fn image_generation_call_event_is_emitted() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -385,6 +390,7 @@ async fn image_generation_call_event_is_emitted_when_image_save_fails() -> anyho text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -438,6 +444,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -530,6 +537,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result< service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -607,6 +615,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> { service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -716,6 +725,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -903,6 +913,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -1017,6 +1028,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> { service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; @@ -1085,6 +1097,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -1144,6 +1157,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 3b6f3e3f9c..027e34edbd 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -88,6 +88,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/live_reload.rs b/codex-rs/core/tests/suite/live_reload.rs index 6ab001383f..a0d46f2072 100644 --- a/codex-rs/core/tests/suite/live_reload.rs +++ b/codex-rs/core/tests/suite/live_reload.rs @@ -69,6 +69,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str) service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b14267af49..0ebd5e20b0 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -134,6 +134,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -171,6 +172,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -231,6 +233,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -268,6 +271,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -410,6 +414,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -431,6 +436,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -539,6 +545,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -560,6 +567,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -671,6 +679,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -692,6 +701,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -805,6 +815,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -834,6 +845,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history() summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -989,6 +1001,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1048,6 +1061,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: 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 13e2819141..02e0d81dd6 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -129,6 +129,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -153,6 +154,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> { service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -232,6 +234,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -256,6 +259,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { @@ -322,6 +326,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -366,6 +371,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |event| { @@ -421,6 +427,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -466,6 +473,7 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |event| { diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 94f2424db4..15cbc82d4b 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -105,6 +105,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index daf3e89462..9fbbcdb93c 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -110,6 +110,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index 96df3fd2f1..936b50ae2a 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -107,6 +107,7 @@ async fn responses_api_emits_api_request_event() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -150,6 +151,7 @@ async fn process_sse_emits_tracing_for_output_item() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -193,6 +195,7 @@ async fn process_sse_emits_failed_event_on_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -237,6 +240,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -301,6 +305,7 @@ async fn process_sse_failed_event_records_response_error_message() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -363,6 +368,7 @@ async fn process_sse_failed_event_logs_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -412,6 +418,7 @@ async fn process_sse_failed_event_logs_missing_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -470,6 +477,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -522,6 +530,7 @@ async fn process_sse_emits_completed_telemetry() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -594,6 +603,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -678,6 +688,7 @@ async fn record_responses_sets_span_fields_for_response_events() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -762,6 +773,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -835,6 +847,7 @@ async fn handle_response_item_records_tool_result_for_function_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -918,6 +931,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -986,6 +1000,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1094,6 +1109,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1145,6 +1161,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1211,6 +1228,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1277,6 +1295,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1343,6 +1362,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1409,6 +1429,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -1476,6 +1497,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 25a08120ae..0467c27250 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -96,6 +96,7 @@ async fn submit_user_input(codex: &CodexThread, text: &str) { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap_or_else(|err| panic!("submit user input: {err}")); @@ -240,6 +241,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -256,6 +258,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index d1738db902..53960bd728 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -53,6 +53,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -90,6 +91,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -117,6 +119,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -160,6 +163,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -171,6 +175,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -214,6 +219,7 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -241,6 +247,7 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -297,6 +304,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -326,6 +334,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -339,6 +348,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -396,6 +406,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -425,6 +436,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -444,6 +456,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -476,6 +489,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -528,6 +542,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 8a0cbdcb63..ea2507c766 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -111,6 +111,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -162,6 +163,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -220,6 +222,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -284,6 +287,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -336,6 +340,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -374,6 +379,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -441,6 +447,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -479,6 +486,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -559,6 +567,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -597,6 +606,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -714,6 +724,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow service_tier: None, collaboration_mode: None, personality: Some(Personality::Friendly), + submission_type: None, }) .await?; @@ -833,6 +844,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -871,6 +883,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index cd0ad330ae..7bc2532cbe 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -213,6 +213,7 @@ async fn capability_sections_render_in_developer_message_in_order() -> Result<() text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -288,6 +289,7 @@ async fn explicit_plugin_mentions_inject_plugin_guidance() -> Result<()> { path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -360,6 +362,7 @@ async fn explicit_plugin_mentions_track_plugin_used_analytics() -> Result<()> { path: format!("plugin://{SAMPLE_PLUGIN_CONFIG_NAME}"), }], final_output_json_schema: None, + submission_type: 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 4d34e691de..764f72b60b 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -151,6 +151,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -162,6 +163,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -247,6 +249,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -258,6 +261,7 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -320,6 +324,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -331,6 +336,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -412,6 +418,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -448,6 +455,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -530,6 +538,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -681,6 +690,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -712,6 +722,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -825,6 +836,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -846,6 +858,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -951,6 +964,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -972,6 +986,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu collaboration_mode: None, final_output_json_schema: None, personality: None, + submission_type: 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..24cdd5c64f 100644 --- a/codex-rs/core/tests/suite/quota_exceeded.rs +++ b/codex-rs/core/tests/suite/quota_exceeded.rs @@ -46,6 +46,7 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/realtime_conversation.rs b/codex-rs/core/tests/suite/realtime_conversation.rs index b169e229a5..de0332c340 100644 --- a/codex-rs/core/tests/suite/realtime_conversation.rs +++ b/codex-rs/core/tests/suite/realtime_conversation.rs @@ -2062,6 +2062,7 @@ async fn inbound_handoff_request_steers_active_turn() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 9559397b7e..5d08e69a29 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -184,6 +184,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -247,6 +248,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning() service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -407,6 +409,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -630,6 +633,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/request_compression.rs b/codex-rs/core/tests/suite/request_compression.rs index fb0125e66e..2f4fe36217 100644 --- a/codex-rs/core/tests/suite/request_compression.rs +++ b/codex-rs/core/tests/suite/request_compression.rs @@ -45,6 +45,7 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -92,6 +93,7 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() -> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 2cfd1cf6f7..6e25adb228 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -202,6 +202,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 14506f4a41..d44f47d931 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -153,6 +153,7 @@ async fn submit_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/request_user_input.rs b/codex-rs/core/tests/suite/request_user_input.rs index 8e30b37c21..952c696653 100644 --- a/codex-rs/core/tests/suite/request_user_input.rs +++ b/codex-rs/core/tests/suite/request_user_input.rs @@ -153,6 +153,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul }, }), personality: None, + submission_type: None, }) .await?; @@ -264,6 +265,7 @@ where service_tier: None, collaboration_mode: Some(collaboration_mode), personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index a77ad60b5a..155482cd60 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -91,6 +91,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> { text_elements: text_elements.clone(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -176,6 +177,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()> text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; @@ -265,6 +267,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -305,6 +308,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |event| { @@ -320,6 +324,7 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&resumed.codex, |event| { @@ -390,6 +395,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; @@ -433,6 +439,7 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: 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 503bc8745e..cf7ef74293 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -726,6 +726,7 @@ async fn review_history_surfaces_in_parent_session() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 68c2bc0106..cb38a90919 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -138,6 +138,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -309,6 +310,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -514,6 +516,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -630,6 +633,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -793,6 +797,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1041,6 +1046,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/safety_check_downgrade.rs b/codex-rs/core/tests/suite/safety_check_downgrade.rs index 51a88ef16a..ef8c086275 100644 --- a/codex-rs/core/tests/suite/safety_check_downgrade.rs +++ b/codex-rs/core/tests/suite/safety_check_downgrade.rs @@ -53,6 +53,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() -> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -152,6 +153,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -238,6 +240,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -288,6 +291,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index d41785bd4e..9e03afdc4c 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -427,6 +427,7 @@ async fn tool_search_returns_deferred_tools_without_follow_up_tool_injection() - text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 0e044f3922..960ebdb945 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -172,6 +172,7 @@ async fn run_snapshot_command_with_options( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -263,6 +264,7 @@ async fn run_shell_command_snapshot_with_options( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -334,6 +336,7 @@ async fn run_tool_turn_on_harness( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -568,6 +571,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/skill_approval.rs b/codex-rs/core/tests/suite/skill_approval.rs index d77d736f94..15d096268a 100644 --- a/codex-rs/core/tests/suite/skill_approval.rs +++ b/codex-rs/core/tests/suite/skill_approval.rs @@ -59,6 +59,7 @@ async fn submit_turn_with_policies( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; Ok(()) diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 388618b56a..690de66893 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -82,6 +82,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index d99c3865c7..3dcd754bcf 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -405,6 +405,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result< service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; wait_for_event(&test.codex, |event| { 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 c5aa57ce74..1f4283dd3e 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 @@ -98,6 +98,7 @@ async fn continue_after_stream_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); @@ -117,6 +118,7 @@ async fn continue_after_stream_error() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: 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 0c4d7cb49d..bfaa123fcc 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -82,6 +82,7 @@ async fn retries_on_early_close() { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index 9594195a5b..296b5c4c09 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -93,6 +93,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -164,6 +165,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -245,6 +247,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -341,6 +344,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -445,6 +449,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index faff0b2e09..16b0a2edd7 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -50,6 +50,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -367,6 +368,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 3f4a9e5a64..5e4c980228 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -496,6 +496,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 7470717a04..66cf9a427c 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -179,6 +179,7 @@ async fn submit_unified_exec_turn( service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -259,6 +260,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1747,6 +1749,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1840,6 +1843,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2312,6 +2316,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -2421,6 +2426,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index a2303c0ecf..dc383f00dd 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -62,6 +62,7 @@ mv "${tmp_path}" "${payload_path}""#, text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index f6abb5d621..c3ff787392 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -185,6 +185,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index b213e305e4..36f5d28541 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -164,6 +164,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -273,6 +274,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -414,6 +416,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5 summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -520,6 +523,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -612,6 +616,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -712,6 +717,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -823,6 +829,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -925,6 +932,7 @@ await codex.emitImage(out); service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1045,6 +1053,7 @@ console.log(out.type); summary: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1138,6 +1147,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1214,6 +1224,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1295,6 +1306,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1425,6 +1437,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; @@ -1500,6 +1513,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()> service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index c55e72ec64..60db8b4bcf 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -165,6 +165,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result service_tier: None, collaboration_mode: None, personality: None, + submission_type: None, }) .await?; diff --git a/codex-rs/core/tests/suite/window_headers.rs b/codex-rs/core/tests/suite/window_headers.rs index 76f135d205..3d9fffbef5 100644 --- a/codex-rs/core/tests/suite/window_headers.rs +++ b/codex-rs/core/tests/suite/window_headers.rs @@ -109,6 +109,7 @@ async fn submit_user_turn(codex: &Arc, text: &str) -> Result<()> { text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await?; wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 8c849ca806..9baba93215 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -723,6 +723,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> { effort: default_effort, summary: None, personality: None, + submission_type: None, output_schema, collaboration_mode: None, }, diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 64d553bbff..8ab7af20fe 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -114,6 +114,7 @@ pub async fn run_codex_tool_session( text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }, trace: None, }; @@ -161,6 +162,7 @@ pub async fn run_codex_tool_session_reply( text_elements: Vec::new(), }], final_output_json_schema: None, + submission_type: None, }) .await { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f2e90196fc..447038ee82 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -103,6 +103,13 @@ pub const REALTIME_CONVERSATION_OPEN_TAG: &str = ""; pub const REALTIME_CONVERSATION_CLOSE_TAG: &str = ""; pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:"; +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +pub enum SubmissionType { + Prompt, + PromptQueued, +} + /// Submission Queue Entry - requests from user #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct Submission { @@ -244,6 +251,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, + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + submission_type: Option, }, /// Similar to [`Op::UserInput`], but contains additional context required @@ -301,6 +311,10 @@ pub enum Op { /// Optional personality override for this turn. #[serde(skip_serializing_if = "Option::is_none")] personality: Option, + + /// Metadata describing how the prompt was submitted. + #[serde(default, skip_serializing_if = "Option::is_none")] + submission_type: Option, }, /// Inter-agent communication that should be recorded as assistant history @@ -517,6 +531,7 @@ impl From> for Op { Op::UserInput { items: value, final_output_json_schema: None, + submission_type: None, } } } @@ -4484,6 +4499,7 @@ mod tests { let op = Op::UserInput { items: Vec::new(), final_output_json_schema: None, + submission_type: None, }; let json_op = serde_json::to_value(op)?; @@ -4501,6 +4517,7 @@ mod tests { Op::UserInput { items: Vec::new(), final_output_json_schema: None, + submission_type: None, } ); @@ -4520,6 +4537,7 @@ mod tests { let op = Op::UserInput { items: Vec::new(), final_output_json_schema: Some(schema.clone()), + submission_type: None, }; let json_op = serde_json::to_value(op)?; @@ -4535,6 +4553,27 @@ mod tests { Ok(()) } + #[test] + fn user_input_serialization_includes_submission_type_when_some() -> Result<()> { + let op = Op::UserInput { + items: Vec::new(), + final_output_json_schema: None, + submission_type: Some(SubmissionType::PromptQueued), + }; + + let json_op = serde_json::to_value(op)?; + assert_eq!( + json_op, + json!({ + "type": "user_input", + "items": [], + "submission_type": "prompt_queued", + }) + ); + + Ok(()) + } + #[test] fn user_input_text_serializes_empty_text_elements() -> Result<()> { let input = UserInput::Text { diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index dad10fa766..13bacdff82 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2235,6 +2235,7 @@ impl App { final_output_json_schema, collaboration_mode, personality, + submission_type, } => { let mut should_start_turn = true; if let Some(turn_id) = self.active_turn_id_for_thread(thread_id).await { @@ -2317,6 +2318,7 @@ impl App { *service_tier, collaboration_mode.clone(), *personality, + *submission_type, final_output_json_schema.clone(), ) .await?; diff --git a/codex-rs/tui/src/app_command.rs b/codex-rs/tui/src/app_command.rs index 0646cc297d..4a83d1ab95 100644 --- a/codex-rs/tui/src/app_command.rs +++ b/codex-rs/tui/src/app_command.rs @@ -17,6 +17,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SubmissionType; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; use codex_protocol::user_input::UserInput; @@ -51,6 +52,7 @@ pub(crate) enum AppCommandView<'a> { final_output_json_schema: &'a Option, collaboration_mode: &'a Option, personality: &'a Option, + submission_type: &'a Option, }, OverrideTurnContext { cwd: &'a Option, @@ -152,6 +154,7 @@ impl AppCommand { final_output_json_schema: Option, collaboration_mode: Option, personality: Option, + submission_type: Option, ) -> Self { Self(Op::UserTurn { items, @@ -166,6 +169,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, }) } @@ -311,6 +315,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, } => AppCommandView::UserTurn { items, cwd, @@ -324,6 +329,7 @@ impl AppCommand { final_output_json_schema, collaboration_mode, personality, + submission_type, }, Op::OverrideTurnContext { cwd, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 03072632bc..13ec3a393d 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -84,6 +84,7 @@ use codex_protocol::protocol::ReviewRequest; use codex_protocol::protocol::ReviewTarget as CoreReviewTarget; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionNetworkProxyRuntime; +use codex_protocol::protocol::SubmissionType; use color_eyre::eyre::ContextCompat; use color_eyre::eyre::Result; use color_eyre::eyre::WrapErr; @@ -427,6 +428,7 @@ impl AppServerSession { service_tier: Option>, collaboration_mode: Option, personality: Option, + submission_type: Option, output_schema: Option, ) -> Result { let request_id = self.next_request_id(); @@ -445,6 +447,7 @@ impl AppServerSession { effort, summary, personality, + submission_type, output_schema, collaboration_mode, }, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 86e11b49c4..9c511e398f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -193,6 +193,7 @@ use codex_protocol::protocol::ReviewTarget; use codex_protocol::protocol::SkillMetadata as ProtocolSkillMetadata; #[cfg(test)] use codex_protocol::protocol::StreamErrorEvent; +use codex_protocol::protocol::SubmissionType; use codex_protocol::protocol::TerminalInteractionEvent; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; @@ -5559,6 +5560,14 @@ impl ChatWidget { } fn submit_user_message(&mut self, user_message: UserMessage) { + self.submit_user_message_with_type(user_message, /*submission_type*/ None); + } + + fn submit_user_message_with_type( + &mut self, + user_message: UserMessage, + submission_type: Option, + ) { if !self.is_session_configured() { tracing::warn!("cannot submit user message before session is configured; queueing"); self.queued_user_messages.push_front(user_message); @@ -5774,6 +5783,7 @@ impl ChatWidget { /*final_output_json_schema*/ None, collaboration_mode, personality, + submission_type, ); if !self.submit_op(op) { @@ -7235,8 +7245,13 @@ impl ChatWidget { if self.bottom_pane.is_task_running() { return; } + let submission_type = if self.rejected_steers_queue.is_empty() { + Some(SubmissionType::PromptQueued) + } else { + None + }; if let Some(user_message) = self.pop_next_queued_user_message() { - self.submit_user_message(user_message); + self.submit_user_message_with_type(user_message, submission_type); } // Update the list to reflect the remaining queued messages (if any). self.refresh_pending_input_preview(); diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 1bae5d2aa8..f272ceb81e 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -642,6 +642,29 @@ async fn interrupted_turn_restore_keeps_active_mode_for_resubmission() { assert_eq!(chat.active_collaboration_mode_kind(), expected_mode); } +#[tokio::test] +async fn queued_user_message_marks_prompt_queued_submission_type() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.thread_id = Some(ThreadId::new()); + chat.queued_user_messages.push_back(UserMessage { + text: "queued follow up".to_string(), + local_images: Vec::new(), + remote_image_urls: Vec::new(), + text_elements: Vec::new(), + mention_bindings: Vec::new(), + }); + + chat.maybe_send_next_queued_input(); + + match next_submit_op(&mut op_rx) { + Op::UserTurn { + submission_type: Some(SubmissionType::PromptQueued), + .. + } => {} + other => panic!("expected queued prompt submission, got {other:?}"), + } +} + #[tokio::test] async fn remap_placeholders_uses_attachment_labels() { let placeholder_one = "[Image #1]"; From 7973184a406a75f096d72efa39cc6f2437cfc51e Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 7 Apr 2026 14:49:00 -0700 Subject: [PATCH 3/4] [codex-analytics] add steering metadata --- .../analytics/src/analytics_client_tests.rs | 292 +++++++++++++++++- codex-rs/analytics/src/client.rs | 11 + codex-rs/analytics/src/events.rs | 42 +++ codex-rs/analytics/src/facts.rs | 33 ++ codex-rs/analytics/src/lib.rs | 3 + codex-rs/analytics/src/reducer.rs | 75 ++++- .../app-server/tests/suite/v2/turn_steer.rs | 77 +++-- codex-rs/core/src/codex.rs | 186 +++++++++-- 8 files changed, 671 insertions(+), 48 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index d026aeaac8..d68167e55c 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -19,6 +19,7 @@ use crate::facts::AnalyticsFact; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; +use crate::facts::CodexTurnSteerEvent; use crate::facts::CustomAnalyticsFact; use crate::facts::InvocationType; use crate::facts::PluginState; @@ -30,6 +31,9 @@ use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSteerInput; +use crate::facts::TurnSteerRejectionReason; +use crate::facts::TurnSteerResult; use crate::facts::TurnSubmissionType; use crate::facts::TurnTokenUsageFact; use crate::reducer::AnalyticsReducer; @@ -243,6 +247,25 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { } } +fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { + CodexAppServerClientMetadata { + product_client_id: "codex-tui".to_string(), + client_name: Some("codex-tui".to_string()), + client_version: Some("1.0.0".to_string()), + rpc_transport: AppServerRpcTransport::Stdio, + experimental_api_enabled: None, + } +} + +fn sample_runtime_metadata() -> CodexRuntimeMetadata { + CodexRuntimeMetadata { + codex_rs_version: "0.1.0".to_string(), + runtime_os: "macos".to_string(), + runtime_os_version: "15.3.1".to_string(), + runtime_arch: "aarch64".to_string(), + } +} + async fn ingest_turn_prerequisites( reducer: &mut AnalyticsReducer, out: &mut Vec, @@ -1065,7 +1088,7 @@ fn turn_event_serializes_expected_shape() { is_first_turn: true, status: Some(TurnStatus::Completed), turn_error: None, - steer_count: None, + steer_count: Some(0), total_tool_call_count: None, shell_command_count: None, file_change_count: None, @@ -1111,7 +1134,7 @@ fn turn_event_serializes_expected_shape() { "is_first_turn": true, "status": "completed", "turn_error": null, - "steer_count": null, + "steer_count": 0, "total_tool_call_count": null, "shell_command_count": null, "file_change_count": null, @@ -1133,6 +1156,172 @@ fn turn_event_serializes_expected_shape() { ); } +#[tokio::test] +async fn accepted_turn_steer_emits_expected_event() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + ingest_turn_prerequisites( + &mut reducer, + &mut out, + /*include_initialize*/ true, + /*include_resolved_config*/ false, + /*include_started*/ false, + /*include_token_usage*/ false, + ) + .await; + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 2, + result: TurnSteerResult::Accepted, + rejection_reason: None, + started_at: 1_716_000_125, + }, + })), + &mut out, + ) + .await; + + assert_eq!(out.len(), 1); + let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event"); + assert_eq!(payload["event_type"], json!("codex_turn_steer_event")); + assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!(payload["event_params"]["expected_turn_id"], json!("turn-2")); + assert_eq!(payload["event_params"]["accepted_turn_id"], json!("turn-2")); + assert_eq!(payload["event_params"]["num_input_images"], json!(2)); + assert_eq!(payload["event_params"]["result"], json!("accepted")); + assert_eq!(payload["event_params"]["rejection_reason"], json!(null)); + assert_eq!(payload["event_params"]["started_at"], json!(1_716_000_125)); + assert_eq!( + payload["event_params"]["app_server_client"]["product_client_id"], + json!("codex-tui") + ); + assert_eq!( + payload["event_params"]["runtime"]["codex_rs_version"], + json!("0.1.0") + ); + assert!(payload["event_params"].get("product_client_id").is_none()); +} + +#[tokio::test] +async fn rejected_turn_steer_uses_thread_connection_metadata() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Initialize { + connection_id: 7, + params: InitializeParams { + client_info: ClientInfo { + name: "codex-tui".to_string(), + title: None, + version: "1.0.0".to_string(), + }, + capabilities: None, + }, + product_client_id: "codex-tui".to_string(), + runtime: sample_runtime_metadata(), + rpc_transport: AppServerRpcTransport::Stdio, + }, + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Response { + connection_id: 7, + response: Box::new(sample_thread_start_response( + "thread-2", /*ephemeral*/ false, "gpt-5", + )), + }, + &mut out, + ) + .await; + out.clear(); + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: String::new(), + thread_id: "thread-2".to_string(), + turn_id: String::new(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: None, + accepted_turn_id: None, + num_input_images: 1, + result: TurnSteerResult::Rejected, + rejection_reason: Some(TurnSteerRejectionReason::NoActiveTurn), + started_at: 1_716_000_126, + }, + })), + &mut out, + ) + .await; + + assert_eq!(out.len(), 1); + let payload = serde_json::to_value(&out[0]).expect("serialize turn steer event"); + assert_eq!(payload["event_type"], json!("codex_turn_steer_event")); + assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!(payload["event_params"]["expected_turn_id"], json!(null)); + assert_eq!(payload["event_params"]["accepted_turn_id"], json!(null)); + assert_eq!(payload["event_params"]["num_input_images"], json!(1)); + assert_eq!( + payload["event_params"]["app_server_client"]["product_client_id"], + json!("codex-tui") + ); + assert_eq!( + payload["event_params"]["runtime"]["codex_rs_version"], + json!("0.1.0") + ); + assert_eq!(payload["event_params"]["result"], json!("rejected")); + assert_eq!( + payload["event_params"]["rejection_reason"], + json!("no_active_turn") + ); + assert_eq!(payload["event_params"]["started_at"], json!(1_716_000_126)); +} + +#[tokio::test] +async fn turn_steer_does_not_emit_without_connection_metadata() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: None, + num_input_images: 1, + result: TurnSteerResult::Rejected, + rejection_reason: Some(TurnSteerRejectionReason::NoActiveTurn), + started_at: 1_716_000_126, + }, + })), + &mut out, + ) + .await; + + assert!(out.is_empty()); +} + #[tokio::test] async fn turn_lifecycle_emits_turn_event() { let mut reducer = AnalyticsReducer::default(); @@ -1170,6 +1359,7 @@ async fn turn_lifecycle_emits_turn_event() { ); assert_eq!(payload["event_params"]["num_input_images"], json!(1)); assert_eq!(payload["event_params"]["status"], json!("completed")); + assert_eq!(payload["event_params"]["steer_count"], json!(0)); assert_eq!(payload["event_params"]["started_at"], json!(455)); assert_eq!(payload["event_params"]["completed_at"], json!(456)); assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); @@ -1183,6 +1373,104 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["total_tokens"], json!(321)); } +#[tokio::test] +async fn accepted_steers_increment_turn_steer_count() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + ingest_turn_prerequisites( + &mut reducer, + &mut out, + /*include_initialize*/ true, + /*include_resolved_config*/ true, + /*include_started*/ true, + /*include_token_usage*/ false, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 0, + result: TurnSteerResult::Accepted, + rejection_reason: None, + started_at: 1, + }, + })), + &mut out, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: None, + accepted_turn_id: None, + num_input_images: 0, + result: TurnSteerResult::Rejected, + rejection_reason: Some(TurnSteerRejectionReason::NoActiveTurn), + started_at: 2, + }, + })), + &mut out, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer(TurnSteerInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-2".to_string(), + turn_id: "turn-2".to_string(), + }, + turn_steer: CodexTurnSteerEvent { + expected_turn_id: Some("turn-2".to_string()), + accepted_turn_id: Some("turn-2".to_string()), + num_input_images: 1, + result: TurnSteerResult::Accepted, + rejection_reason: None, + started_at: 3, + }, + })), + &mut out, + ) + .await; + + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_completed_notification( + "thread-2", + "turn-2", + AppServerTurnStatus::Completed, + /*codex_error_info*/ None, + ))), + &mut out, + ) + .await; + + let turn_event = out + .iter() + .find(|event| matches!(event, TrackEventRequest::TurnEvent(_))) + .expect("turn event should be emitted"); + let payload = serde_json::to_value(turn_event).expect("serialize turn event"); + assert_eq!(payload["event_params"]["steer_count"], json!(2)); +} + #[tokio::test] async fn queued_submission_type_emits_queued_turn_event() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index 41802bdb0e..5a5d2f30b5 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -6,6 +6,7 @@ use crate::facts::AnalyticsFact; use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; +use crate::facts::CodexTurnSteerEvent; use crate::facts::CustomAnalyticsFact; use crate::facts::PluginState; use crate::facts::PluginStateChangedInput; @@ -14,6 +15,7 @@ use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; +use crate::facts::TurnSteerInput; use crate::facts::TurnTokenUsageFact; use crate::reducer::AnalyticsReducer; use codex_app_server_protocol::ClientRequest; @@ -203,6 +205,15 @@ impl AnalyticsEventsClient { ))); } + pub fn track_turn_steer(&self, tracking: TrackEventsContext, turn_steer: CodexTurnSteerEvent) { + self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnSteer( + TurnSteerInput { + tracking, + turn_steer, + }, + ))); + } + pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) { self.record_fact(AnalyticsFact::Custom( CustomAnalyticsFact::PluginStateChanged(PluginStateChangedInput { diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 48914d9de9..b03cf74619 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -1,9 +1,12 @@ use crate::facts::AppInvocation; +use crate::facts::CodexTurnSteerEvent; use crate::facts::InvocationType; use crate::facts::PluginState; use crate::facts::SubAgentThreadStartedInput; use crate::facts::TrackEventsContext; use crate::facts::TurnStatus; +use crate::facts::TurnSteerRejectionReason; +use crate::facts::TurnSteerResult; use crate::facts::TurnSubmissionType; use codex_app_server_protocol::CodexErrorInfo; use codex_login::default_client::originator; @@ -41,6 +44,7 @@ pub(crate) enum TrackEventRequest { AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), TurnEvent(Box), + TurnSteer(CodexTurnSteerEventRequest), PluginUsed(CodexPluginUsedEventRequest), PluginInstalled(CodexPluginEventRequest), PluginUninstalled(CodexPluginEventRequest), @@ -172,6 +176,25 @@ pub(crate) struct CodexTurnEventRequest { pub(crate) event_params: CodexTurnEventParams, } +#[derive(Serialize)] +pub(crate) struct CodexTurnSteerEventParams { + pub(crate) thread_id: String, + pub(crate) expected_turn_id: Option, + pub(crate) accepted_turn_id: Option, + pub(crate) app_server_client: CodexAppServerClientMetadata, + pub(crate) runtime: CodexRuntimeMetadata, + pub(crate) num_input_images: usize, + pub(crate) result: TurnSteerResult, + pub(crate) rejection_reason: Option, + pub(crate) started_at: u64, +} + +#[derive(Serialize)] +pub(crate) struct CodexTurnSteerEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexTurnSteerEventParams, +} + #[derive(Serialize)] pub(crate) struct CodexPluginMetadata { pub(crate) plugin_id: Option, @@ -263,6 +286,25 @@ pub(crate) fn codex_plugin_used_metadata( } } +pub(crate) fn codex_turn_steer_event_params( + app_server_client: CodexAppServerClientMetadata, + runtime: CodexRuntimeMetadata, + tracking: &TrackEventsContext, + turn_steer: CodexTurnSteerEvent, +) -> CodexTurnSteerEventParams { + CodexTurnSteerEventParams { + thread_id: tracking.thread_id.clone(), + expected_turn_id: turn_steer.expected_turn_id, + accepted_turn_id: turn_steer.accepted_turn_id, + app_server_client, + runtime, + num_input_images: turn_steer.num_input_images, + result: turn_steer.result, + rejection_reason: turn_steer.rejection_reason, + started_at: turn_steer.started_at, + } +} + pub(crate) fn thread_source_name(thread_source: &SessionSource) -> Option<&'static str> { match thread_source { SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec => Some("user"), diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index ed5d96bb25..34d158d8bd 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -81,6 +81,33 @@ pub enum TurnStatus { Interrupted, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerResult { + Accepted, + Rejected, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum TurnSteerRejectionReason { + NoActiveTurn, + ExpectedTurnMismatch, + NonSteerableReview, + NonSteerableCompact, + EmptyInput, +} + +#[derive(Clone)] +pub struct CodexTurnSteerEvent { + pub expected_turn_id: Option, + pub accepted_turn_id: Option, + pub num_input_images: usize, + pub result: TurnSteerResult, + pub rejection_reason: Option, + pub started_at: u64, +} + #[derive(Clone, Debug)] pub struct SkillInvocation { pub skill_name: String, @@ -142,6 +169,7 @@ pub(crate) enum CustomAnalyticsFact { SubAgentThreadStarted(SubAgentThreadStartedInput), TurnResolvedConfig(Box), TurnTokenUsage(Box), + TurnSteer(TurnSteerInput), SkillInvoked(SkillInvokedInput), AppMentioned(AppMentionedInput), AppUsed(AppUsedInput), @@ -149,6 +177,11 @@ pub(crate) enum CustomAnalyticsFact { PluginStateChanged(PluginStateChangedInput), } +pub(crate) struct TurnSteerInput { + pub tracking: TrackEventsContext, + pub turn_steer: CodexTurnSteerEvent, +} + pub(crate) struct SkillInvokedInput { pub tracking: TrackEventsContext, pub invocations: Vec, diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index 5a4630ff47..b3e829ea03 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -6,12 +6,15 @@ mod reducer; pub use client::AnalyticsEventsClient; pub use events::AppServerRpcTransport; pub use facts::AppInvocation; +pub use facts::CodexTurnSteerEvent; pub use facts::InvocationType; pub use facts::SkillInvocation; pub use facts::SubAgentThreadStartedInput; pub use facts::TrackEventsContext; pub use facts::TurnResolvedConfigFact; pub use facts::TurnStatus; +pub use facts::TurnSteerRejectionReason; +pub use facts::TurnSteerResult; pub use facts::TurnSubmissionType; pub use facts::TurnTokenUsageFact; pub use facts::build_track_events_context; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index e24900ef04..d6f3ebd5e5 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -7,6 +7,7 @@ use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; use crate::events::CodexTurnEventParams; use crate::events::CodexTurnEventRequest; +use crate::events::CodexTurnSteerEventRequest; use crate::events::SkillInvocationEventParams; use crate::events::SkillInvocationEventRequest; use crate::events::ThreadInitializationMode; @@ -16,20 +17,25 @@ use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; +use crate::events::codex_turn_steer_event_params; use crate::events::plugin_state_event_type; use crate::events::subagent_thread_started_event_request; use crate::events::thread_source_name; use crate::facts::AnalyticsFact; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; +use crate::facts::CodexTurnSteerEvent; use crate::facts::CustomAnalyticsFact; use crate::facts::PluginState; use crate::facts::PluginStateChangedInput; use crate::facts::PluginUsedInput; use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; +use crate::facts::TurnSteerInput; +use crate::facts::TurnSteerResult; use crate::facts::TurnTokenUsageFact; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ClientResponse; @@ -57,6 +63,7 @@ pub(crate) struct AnalyticsReducer { requests: HashMap<(u64, RequestId), RequestState>, turns: HashMap, connections: HashMap, + thread_connections: HashMap, } struct ConnectionState { @@ -89,6 +96,7 @@ struct TurnState { started_at: Option, token_usage: Option, completed: Option, + steer_count: usize, } impl AnalyticsReducer { @@ -135,6 +143,9 @@ impl AnalyticsReducer { CustomAnalyticsFact::TurnTokenUsage(input) => { self.ingest_turn_token_usage(*input, out); } + CustomAnalyticsFact::TurnSteer(input) => { + self.ingest_turn_steer(input, out); + } CustomAnalyticsFact::SkillInvoked(input) => { self.ingest_skill_invoked(input, out).await; } @@ -198,6 +209,8 @@ impl AnalyticsReducer { let ClientRequest::TurnStart { params, .. } = request else { return; }; + self.thread_connections + .insert(params.thread_id.clone(), connection_id); self.requests.insert( (connection_id, request_id), RequestState::TurnStart(PendingTurnStartState { @@ -229,6 +242,7 @@ impl AnalyticsReducer { started_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.thread_id = Some(thread_id); turn_state.num_input_images = Some(num_input_images); @@ -250,6 +264,7 @@ impl AnalyticsReducer { started_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.thread_id = Some(input.thread_id); turn_state.token_usage = Some(input.token_usage); @@ -402,6 +417,7 @@ impl AnalyticsReducer { started_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.connection_id = Some(connection_id); turn_state.thread_id = Some(pending_request.thread_id); @@ -427,6 +443,7 @@ impl AnalyticsReducer { started_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.started_at = notification .turn @@ -445,6 +462,7 @@ impl AnalyticsReducer { started_at: None, token_usage: None, completed: None, + steer_count: 0, }); turn_state.completed = Some(CompletedTurnState { status: analytics_turn_status(notification.turn.status), @@ -478,6 +496,8 @@ impl AnalyticsReducer { out: &mut Vec, ) { let thread_source: SessionSource = thread.source.into(); + self.thread_connections + .insert(thread.id.clone(), connection_id); let Some(connection_state) = self.connections.get(&connection_id) else { return; }; @@ -500,6 +520,59 @@ impl AnalyticsReducer { )); } + fn ingest_turn_steer(&mut self, input: TurnSteerInput, out: &mut Vec) { + let TurnSteerInput { + tracking, + turn_steer, + } = input; + if matches!(turn_steer.result, TurnSteerResult::Accepted) + && let Some(accepted_turn_id) = turn_steer.accepted_turn_id.as_ref() + && let Some(turn_state) = self.turns.get_mut(accepted_turn_id) + { + turn_state.steer_count += 1; + } + let Some((app_server_client, runtime)) = + self.connection_metadata_for_turn_steer(&tracking, &turn_steer) + else { + return; + }; + out.push(TrackEventRequest::TurnSteer(CodexTurnSteerEventRequest { + event_type: "codex_turn_steer_event", + event_params: codex_turn_steer_event_params( + app_server_client, + runtime, + &tracking, + turn_steer, + ), + })); + } + + fn connection_metadata_for_turn_steer( + &self, + tracking: &TrackEventsContext, + turn_steer: &CodexTurnSteerEvent, + ) -> Option<(CodexAppServerClientMetadata, CodexRuntimeMetadata)> { + let turn_connection_id = turn_steer + .accepted_turn_id + .as_ref() + .or(turn_steer.expected_turn_id.as_ref()) + .or(Some(&tracking.turn_id)) + .and_then(|turn_id| self.turns.get(turn_id)) + .and_then(|turn_state| turn_state.connection_id); + + let connection_id = turn_connection_id + .or_else(|| self.thread_connections.get(&tracking.thread_id).copied()); + + connection_id + .and_then(|connection_id| self.connections.get(&connection_id)) + .map(|connection_state| { + ( + connection_state.app_server_client.clone(), + connection_state.runtime.clone(), + ) + }) + } + fn maybe_emit_turn_event(&mut self, turn_id: &str, out: &mut Vec) { let Some(turn_state) = self.turns.get(turn_id) else { return; @@ -585,7 +658,7 @@ fn codex_turn_event_params( is_first_turn, status: completed.status, turn_error: completed.turn_error, - steer_count: None, + steer_count: Some(turn_state.steer_count), total_tool_call_count: None, shell_command_count: None, file_change_count: None, diff --git a/codex-rs/app-server/tests/suite/v2/turn_steer.rs b/codex-rs/app-server/tests/suite/v2/turn_steer.rs index 5d1b3cc22e..76da7efb78 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_steer.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_steer.rs @@ -6,6 +6,7 @@ use app_test_support::create_mock_responses_server_sequence; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::create_shell_command_sse_response; use app_test_support::to_response; +use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url; use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::JSONRPCError; @@ -23,6 +24,9 @@ use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use tempfile::TempDir; use tokio::time::timeout; +use super::analytics::enable_analytics_capture; +use super::analytics::wait_for_analytics_event; + const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); #[tokio::test] @@ -32,7 +36,12 @@ async fn turn_steer_requires_active_turn() -> Result<()> { std::fs::create_dir(&codex_home)?; let server = create_mock_responses_server_sequence(vec![]).await; - create_config_toml(&codex_home, &server.uri())?; + write_mock_responses_config_toml_with_chatgpt_base_url( + &codex_home, + &server.uri(), + &server.uri(), + )?; + enable_analytics_capture(&server, &codex_home).await?; let mut mcp = McpProcess::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -52,7 +61,7 @@ async fn turn_steer_requires_active_turn() -> Result<()> { let steer_req = mcp .send_turn_steer_request(TurnSteerParams { - thread_id: thread.id, + thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "steer".to_string(), text_elements: Vec::new(), @@ -67,6 +76,21 @@ async fn turn_steer_requires_active_turn() -> Result<()> { .await??; assert_eq!(steer_err.error.code, -32600); + let event = + wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_steer_event").await?; + assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["result"], "rejected"); + assert_eq!(event["event_params"]["num_input_images"], 0); + assert_eq!( + event["event_params"]["expected_turn_id"], + "turn-does-not-exist" + ); + assert_eq!( + event["event_params"]["accepted_turn_id"], + serde_json::Value::Null + ); + assert_eq!(event["event_params"]["rejection_reason"], "no_active_turn"); + Ok(()) } @@ -95,7 +119,12 @@ async fn turn_steer_rejects_oversized_text_input() -> Result<()> { "call_sleep", )?]) .await; - create_config_toml(&codex_home, &server.uri())?; + write_mock_responses_config_toml_with_chatgpt_base_url( + &codex_home, + &server.uri(), + &server.uri(), + )?; + enable_analytics_capture(&server, &codex_home).await?; let mut mcp = McpProcess::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -198,7 +227,12 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { "call_sleep", )?]) .await; - create_config_toml(&codex_home, &server.uri())?; + write_mock_responses_config_toml_with_chatgpt_base_url( + &codex_home, + &server.uri(), + &server.uri(), + )?; + enable_analytics_capture(&server, &codex_home).await?; let mut mcp = McpProcess::new(&codex_home).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; @@ -258,31 +292,20 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> { let steer: TurnSteerResponse = to_response::(steer_resp)?; assert_eq!(steer.turn_id, turn.id); + let event = + wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_steer_event").await?; + assert_eq!(event["event_params"]["thread_id"], thread.id); + assert_eq!(event["event_params"]["result"], "accepted"); + assert_eq!(event["event_params"]["num_input_images"], 0); + assert_eq!(event["event_params"]["expected_turn_id"], turn.id); + assert_eq!(event["event_params"]["accepted_turn_id"], turn.id); + assert_eq!( + event["event_params"]["rejection_reason"], + serde_json::Value::Null + ); + mcp.interrupt_turn_and_wait_for_aborted(thread.id, steer.turn_id, DEFAULT_READ_TIMEOUT) .await?; Ok(()) } - -fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> { - let config_toml = codex_home.join("config.toml"); - std::fs::write( - config_toml, - format!( - r#" -model = "mock-model" -approval_policy = "never" -sandbox_mode = "danger-full-access" - -model_provider = "mock_provider" - -[model_providers.mock_provider] -name = "Mock provider for test" -base_url = "{server_uri}/v1" -wire_api = "responses" -request_max_retries = 0 -stream_max_retries = 0 -"# - ), - ) -} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 769f27db57..77eb802ad9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -50,9 +50,13 @@ use chrono::Local; use chrono::Utc; use codex_analytics::AnalyticsEventsClient; use codex_analytics::AppInvocation; +use codex_analytics::CodexTurnSteerEvent; use codex_analytics::InvocationType; use codex_analytics::SubAgentThreadStartedInput; +use codex_analytics::TrackEventsContext; use codex_analytics::TurnResolvedConfigFact; +use codex_analytics::TurnSteerRejectionReason; +use codex_analytics::TurnSteerResult; use codex_analytics::TurnSubmissionType; use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; @@ -240,6 +244,32 @@ impl SteerInputError { }, } } + + fn to_turn_steer_rejection_reason(&self) -> TurnSteerRejectionReason { + match self { + Self::NoActiveTurn(_) => TurnSteerRejectionReason::NoActiveTurn, + Self::ExpectedTurnMismatch { .. } => TurnSteerRejectionReason::ExpectedTurnMismatch, + Self::ActiveTurnNotSteerable { turn_kind } => match turn_kind { + NonSteerableTurnKind::Review => TurnSteerRejectionReason::NonSteerableReview, + NonSteerableTurnKind::Compact => TurnSteerRejectionReason::NonSteerableCompact, + }, + Self::EmptyInput => TurnSteerRejectionReason::EmptyInput, + } + } +} + +struct AcceptedSteerInput { + turn_id: String, + tracking: TrackEventsContext, + expected_turn_id: String, + num_input_images: usize, +} + +struct RejectedSteerInput { + error: SteerInputError, + tracking: TrackEventsContext, + expected_turn_id: Option, + num_input_images: usize, } /// Notes from the previous real user turn. @@ -4108,47 +4138,163 @@ impl Session { input: Vec, expected_turn_id: Option<&str>, ) -> Result { - if input.is_empty() { - return Err(SteerInputError::EmptyInput); + let started_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + match self + .try_append_input_to_active_turn(input, expected_turn_id) + .await + { + Ok(accepted) => { + self.services.analytics_events_client.track_turn_steer( + accepted.tracking, + CodexTurnSteerEvent { + expected_turn_id: Some(accepted.expected_turn_id), + accepted_turn_id: Some(accepted.turn_id.clone()), + num_input_images: accepted.num_input_images, + result: TurnSteerResult::Accepted, + rejection_reason: None, + started_at, + }, + ); + Ok(accepted.turn_id) + } + Err(rejected) => { + self.services.analytics_events_client.track_turn_steer( + rejected.tracking, + CodexTurnSteerEvent { + expected_turn_id: rejected.expected_turn_id, + accepted_turn_id: None, + num_input_images: rejected.num_input_images, + result: TurnSteerResult::Rejected, + rejection_reason: Some(rejected.error.to_turn_steer_rejection_reason()), + started_at, + }, + ); + Err(rejected.error) + } } + } + + async fn try_append_input_to_active_turn( + &self, + input: Vec, + expected_turn_id: Option<&str>, + ) -> Result { + let thread_id = self.conversation_id.to_string(); + let fallback_tracking = || { + build_track_events_context( + String::new(), + thread_id.clone(), + expected_turn_id.unwrap_or_default().to_string(), + ) + }; + + if input.is_empty() { + return Err(RejectedSteerInput { + error: SteerInputError::EmptyInput, + tracking: fallback_tracking(), + expected_turn_id: expected_turn_id.map(str::to_string), + num_input_images: 0, + }); + } + + let num_input_images = input + .iter() + .filter(|item| matches!(item, UserInput::Image { .. } | UserInput::LocalImage { .. })) + .count(); let mut active = self.active_turn.lock().await; let Some(active_turn) = active.as_mut() else { - return Err(SteerInputError::NoActiveTurn(input)); + return Err(RejectedSteerInput { + error: SteerInputError::NoActiveTurn(input), + tracking: fallback_tracking(), + expected_turn_id: expected_turn_id.map(str::to_string), + num_input_images, + }); }; - let Some((active_turn_id, _)) = active_turn.tasks.first() else { - return Err(SteerInputError::NoActiveTurn(input)); + let Some((active_turn_id, task)) = active_turn.tasks.first() else { + return Err(RejectedSteerInput { + error: SteerInputError::NoActiveTurn(input), + tracking: fallback_tracking(), + expected_turn_id: expected_turn_id.map(str::to_string), + num_input_images, + }); }; + let active_turn_id = active_turn_id.clone(); + let tracking = build_track_events_context( + task.turn_context.model_info.slug.clone(), + thread_id.clone(), + task.turn_context.sub_id.clone(), + ); if let Some(expected_turn_id) = expected_turn_id && expected_turn_id != active_turn_id { - return Err(SteerInputError::ExpectedTurnMismatch { - expected: expected_turn_id.to_string(), - actual: active_turn_id.clone(), + return Err(RejectedSteerInput { + error: SteerInputError::ExpectedTurnMismatch { + expected: expected_turn_id.to_string(), + actual: active_turn_id, + }, + tracking, + expected_turn_id: Some(expected_turn_id.to_string()), + num_input_images, }); } match active_turn.tasks.first().map(|(_, task)| task.kind) { Some(crate::state::TaskKind::Regular) => {} Some(crate::state::TaskKind::Review) => { - return Err(SteerInputError::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Review, + return Err(RejectedSteerInput { + error: SteerInputError::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Review, + }, + tracking, + expected_turn_id: expected_turn_id + .map(str::to_string) + .or(Some(active_turn_id)), + num_input_images, }); } Some(crate::state::TaskKind::Compact) => { - return Err(SteerInputError::ActiveTurnNotSteerable { - turn_kind: NonSteerableTurnKind::Compact, + return Err(RejectedSteerInput { + error: SteerInputError::ActiveTurnNotSteerable { + turn_kind: NonSteerableTurnKind::Compact, + }, + tracking, + expected_turn_id: expected_turn_id + .map(str::to_string) + .or(Some(active_turn_id)), + num_input_images, + }); + } + None => { + return Err(RejectedSteerInput { + error: SteerInputError::NoActiveTurn(input), + tracking: fallback_tracking(), + expected_turn_id: expected_turn_id.map(str::to_string), + num_input_images, }); } - None => return Err(SteerInputError::NoActiveTurn(input)), } let mut turn_state = active_turn.turn_state.lock().await; turn_state.push_pending_input(input.into()); turn_state.accept_mailbox_delivery_for_current_turn(); - Ok(active_turn_id.clone()) + drop(turn_state); + + let expected_turn_id = expected_turn_id + .map(str::to_string) + .unwrap_or_else(|| active_turn_id.clone()); + + Ok(AcceptedSteerInput { + turn_id: active_turn_id, + tracking, + expected_turn_id, + num_input_images, + }) } /// Returns the input if there was no task running to inject into. @@ -4813,6 +4959,7 @@ fn submission_dispatch_span(sub: &Submission) -> tracing::Span { /// Operation handlers mod handlers { + use crate::codex::RejectedSteerInput; use crate::codex::Session; use crate::codex::SessionSettingsUpdate; use crate::codex::SteerInputError; @@ -4963,11 +5110,14 @@ mod handlers { sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref()) .await; match sess - .steer_input(items.clone(), /*expected_turn_id*/ None) + .try_append_input_to_active_turn(items.clone(), /*expected_turn_id*/ None) .await { Ok(_) => current_context.session_telemetry.user_prompt(&items), - Err(SteerInputError::NoActiveTurn(items)) => { + Err(RejectedSteerInput { + error: SteerInputError::NoActiveTurn(items), + .. + }) => { current_context.session_telemetry.user_prompt(&items); sess.refresh_mcp_servers_if_requested(¤t_context) .await; @@ -4978,10 +5128,10 @@ mod handlers { ) .await; } - Err(err) => { + Err(RejectedSteerInput { error, .. }) => { sess.send_event_raw(Event { id: sub_id, - msg: EventMsg::Error(err.to_error_event()), + msg: EventMsg::Error(error.to_error_event()), }) .await; } From 979a31754125f3f52e652f9c91d97b02495be2ae Mon Sep 17 00:00:00 2001 From: rhan-oai Date: Tue, 7 Apr 2026 14:49:00 -0700 Subject: [PATCH 4/4] [codex-analytics] denormalize thread metadata onto turn events --- .../analytics/src/analytics_client_tests.rs | 249 ++++++++++++++---- codex-rs/analytics/src/events.rs | 49 +++- codex-rs/analytics/src/facts.rs | 12 + codex-rs/analytics/src/lib.rs | 1 + codex-rs/analytics/src/reducer.rs | 38 ++- .../app-server/src/codex_message_processor.rs | 1 + .../app-server/tests/suite/v2/turn_start.rs | 13 +- codex-rs/core/src/codex.rs | 21 ++ codex-rs/core/src/codex_tests.rs | 7 + codex-rs/core/src/codex_thread.rs | 2 + 10 files changed, 315 insertions(+), 78 deletions(-) diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index d68167e55c..639d5ba96a 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -7,7 +7,6 @@ use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; use crate::events::CodexTurnEventRequest; -use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; @@ -28,6 +27,7 @@ use crate::facts::PluginUsedInput; use crate::facts::SkillInvocation; use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; @@ -71,6 +71,7 @@ use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; use pretty_assertions::assert_eq; @@ -232,6 +233,9 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { thread_id: "thread-2".to_string(), num_input_images: 1, submission_type: Some(TurnSubmissionType::Default), + ephemeral: false, + session_source: SessionSource::Exec, + initialization_mode: ThreadInitializationMode::New, model: "gpt-5".to_string(), model_provider: "openai".to_string(), sandbox_policy: SandboxPolicy::new_read_only_policy(), @@ -250,8 +254,8 @@ fn sample_turn_resolved_config(turn_id: &str) -> TurnResolvedConfigFact { fn sample_app_server_client_metadata() -> CodexAppServerClientMetadata { CodexAppServerClientMetadata { product_client_id: "codex-tui".to_string(), - client_name: Some("codex-tui".to_string()), - client_version: Some("1.0.0".to_string()), + client_name: "codex-tui".to_string(), + client_version: "1.0.0".to_string(), rpc_transport: AppServerRpcTransport::Stdio, experimental_api_enabled: None, } @@ -533,8 +537,8 @@ fn thread_initialized_event_serializes_expected_shape() { thread_id: "thread-0".to_string(), app_server_client: CodexAppServerClientMetadata { product_client_id: DEFAULT_ORIGINATOR.to_string(), - client_name: Some("codex-tui".to_string()), - client_version: Some("1.0.0".to_string()), + client_name: "codex-tui".to_string(), + client_version: "1.0.0".to_string(), rpc_transport: AppServerRpcTransport::Stdio, experimental_api_enabled: Some(true), }, @@ -1071,8 +1075,14 @@ fn turn_event_serializes_expected_shape() { event_params: crate::events::CodexTurnEventParams { thread_id: "thread-2".to_string(), turn_id: "turn-2".to_string(), - product_client_id: "codex-tui".to_string(), + app_server_client: sample_app_server_client_metadata(), + runtime: sample_runtime_metadata(), submission_type: Some(TurnSubmissionType::Default), + ephemeral: false, + thread_source: Some("user".to_string()), + initialization_mode: ThreadInitializationMode::New, + subagent_source: None, + parent_thread_id: None, model: Some("gpt-5".to_string()), model_provider: "openai".to_string(), sandbox_policy: Some("read_only"), @@ -1109,51 +1119,97 @@ fn turn_event_serializes_expected_shape() { })); let payload = serde_json::to_value(&event).expect("serialize turn event"); - + assert_eq!(payload["event_type"], json!("codex_turn_event")); + assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); + assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( - payload, + payload["event_params"]["app_server_client"], json!({ - "event_type": "codex_turn_event", - "event_params": { - "thread_id": "thread-2", - "turn_id": "turn-2", - "product_client_id": "codex-tui", - "submission_type": "default", - "model": "gpt-5", - "model_provider": "openai", - "sandbox_policy": "read_only", - "reasoning_effort": "high", - "reasoning_summary": "detailed", - "service_tier": "flex", - "approval_policy": "on-request", - "approvals_reviewer": "guardian_subagent", - "sandbox_network_access": true, - "collaboration_mode": "plan", - "personality": "pragmatic", - "num_input_images": 2, - "is_first_turn": true, - "status": "completed", - "turn_error": null, - "steer_count": 0, - "total_tool_call_count": null, - "shell_command_count": null, - "file_change_count": null, - "mcp_tool_call_count": null, - "dynamic_tool_call_count": null, - "subagent_tool_call_count": null, - "web_search_count": null, - "image_generation_count": null, - "input_tokens": null, - "cached_input_tokens": null, - "output_tokens": null, - "reasoning_output_tokens": null, - "total_tokens": null, - "duration_ms": 1234, - "started_at": 455, - "completed_at": 456 - } + "product_client_id": "codex-tui", + "client_name": "codex-tui", + "client_version": "1.0.0", + "rpc_transport": "stdio", + "experimental_api_enabled": null, }) ); + assert_eq!( + payload["event_params"]["runtime"], + json!({ + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64", + }) + ); + assert!(payload["event_params"].get("product_client_id").is_none()); + assert_eq!(payload["event_params"]["submission_type"], json!("default")); + assert_eq!(payload["event_params"]["ephemeral"], json!(false)); + assert_eq!(payload["event_params"]["thread_source"], json!("user")); + assert_eq!(payload["event_params"]["initialization_mode"], json!("new")); + assert_eq!(payload["event_params"]["subagent_source"], json!(null)); + assert_eq!(payload["event_params"]["parent_thread_id"], json!(null)); + assert_eq!(payload["event_params"]["model"], json!("gpt-5")); + assert_eq!(payload["event_params"]["model_provider"], json!("openai")); + assert_eq!( + payload["event_params"]["sandbox_policy"], + json!("read_only") + ); + assert_eq!(payload["event_params"]["reasoning_effort"], json!("high")); + assert_eq!( + payload["event_params"]["reasoning_summary"], + json!("detailed") + ); + assert_eq!(payload["event_params"]["service_tier"], json!("flex")); + assert_eq!( + payload["event_params"]["approval_policy"], + json!("on-request") + ); + assert_eq!( + payload["event_params"]["approvals_reviewer"], + json!("guardian_subagent") + ); + assert_eq!( + payload["event_params"]["sandbox_network_access"], + json!(true) + ); + assert_eq!(payload["event_params"]["collaboration_mode"], json!("plan")); + assert_eq!(payload["event_params"]["personality"], json!("pragmatic")); + assert_eq!(payload["event_params"]["num_input_images"], json!(2)); + assert_eq!(payload["event_params"]["is_first_turn"], json!(true)); + assert_eq!(payload["event_params"]["status"], json!("completed")); + assert_eq!(payload["event_params"]["turn_error"], json!(null)); + assert_eq!(payload["event_params"]["steer_count"], json!(0)); + assert_eq!( + payload["event_params"]["total_tool_call_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["shell_command_count"], json!(null)); + assert_eq!(payload["event_params"]["file_change_count"], json!(null)); + assert_eq!(payload["event_params"]["mcp_tool_call_count"], json!(null)); + assert_eq!( + payload["event_params"]["dynamic_tool_call_count"], + json!(null) + ); + assert_eq!( + payload["event_params"]["subagent_tool_call_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["web_search_count"], json!(null)); + assert_eq!( + payload["event_params"]["image_generation_count"], + json!(null) + ); + assert_eq!(payload["event_params"]["input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["cached_input_tokens"], json!(null)); + assert_eq!(payload["event_params"]["output_tokens"], json!(null)); + assert_eq!( + payload["event_params"]["reasoning_output_tokens"], + json!(null) + ); + assert_eq!(payload["event_params"]["total_tokens"], json!(null)); + assert_eq!(payload["event_params"]["duration_ms"], json!(1234)); + assert_eq!(payload["event_params"]["started_at"], json!(455)); + assert_eq!(payload["event_params"]["completed_at"], json!(456)); } #[tokio::test] @@ -1354,9 +1410,30 @@ async fn turn_lifecycle_emits_turn_event() { assert_eq!(payload["event_params"]["thread_id"], json!("thread-2")); assert_eq!(payload["event_params"]["turn_id"], json!("turn-2")); assert_eq!( - payload["event_params"]["product_client_id"], - json!("codex-tui") + payload["event_params"]["app_server_client"], + json!({ + "product_client_id": "codex-tui", + "client_name": "codex-tui", + "client_version": "1.0.0", + "rpc_transport": "stdio", + "experimental_api_enabled": null, + }) ); + assert_eq!( + payload["event_params"]["runtime"], + json!({ + "codex_rs_version": "0.1.0", + "runtime_os": "macos", + "runtime_os_version": "15.3.1", + "runtime_arch": "aarch64", + }) + ); + assert!(payload["event_params"].get("product_client_id").is_none()); + assert_eq!(payload["event_params"]["ephemeral"], json!(false)); + assert_eq!(payload["event_params"]["thread_source"], json!("user")); + assert_eq!(payload["event_params"]["initialization_mode"], json!("new")); + assert_eq!(payload["event_params"]["subagent_source"], json!(null)); + assert_eq!(payload["event_params"]["parent_thread_id"], json!(null)); assert_eq!(payload["event_params"]["num_input_images"], json!(1)); assert_eq!(payload["event_params"]["status"], json!("completed")); assert_eq!(payload["event_params"]["steer_count"], json!(0)); @@ -1554,6 +1631,73 @@ async fn queued_submission_type_emits_queued_turn_event() { assert_eq!(payload["event_params"]["submission_type"], json!("queued")); } +#[tokio::test] +async fn turn_event_includes_subagent_thread_metadata() { + let mut reducer = AnalyticsReducer::default(); + let mut out = Vec::new(); + + ingest_turn_prerequisites( + &mut reducer, + &mut out, + /*include_initialize*/ true, + /*include_resolved_config*/ false, + /*include_started*/ true, + /*include_token_usage*/ false, + ) + .await; + + let mut resolved_config = sample_turn_resolved_config("turn-2"); + resolved_config.ephemeral = true; + let parent_thread_id = + codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111") + .expect("valid thread id"); + resolved_config.session_source = SessionSource::SubAgent(SubAgentSource::ThreadSpawn { + parent_thread_id, + depth: 1, + agent_path: None, + agent_nickname: Some("worker".to_string()), + agent_role: None, + }); + resolved_config.initialization_mode = ThreadInitializationMode::Forked; + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::TurnResolvedConfig(Box::new( + resolved_config, + ))), + &mut out, + ) + .await; + reducer + .ingest( + AnalyticsFact::Notification(Box::new(sample_turn_completed_notification( + "thread-2", + "turn-2", + AppServerTurnStatus::Completed, + /*codex_error_info*/ None, + ))), + &mut out, + ) + .await; + + assert_eq!(out.len(), 1); + let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); + assert_eq!(payload["event_params"]["ephemeral"], json!(true)); + assert_eq!(payload["event_params"]["thread_source"], json!("subagent")); + assert_eq!( + payload["event_params"]["initialization_mode"], + json!("forked") + ); + assert_eq!( + payload["event_params"]["subagent_source"], + json!("thread_spawn") + ); + assert_eq!( + payload["event_params"]["parent_thread_id"], + json!("11111111-1111-1111-1111-111111111111") + ); +} + #[tokio::test] async fn turn_does_not_emit_without_required_prerequisites() { let mut reducer = AnalyticsReducer::default(); @@ -1579,12 +1723,7 @@ async fn turn_does_not_emit_without_required_prerequisites() { &mut out, ) .await; - assert_eq!(out.len(), 1); - let payload = serde_json::to_value(&out[0]).expect("serialize turn event"); - assert_eq!( - payload["event_params"]["product_client_id"], - json!(originator().value) - ); + assert!(out.is_empty()); let mut reducer = AnalyticsReducer::default(); let mut out = Vec::new(); diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index b03cf74619..6df74c26ca 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -3,6 +3,7 @@ use crate::facts::CodexTurnSteerEvent; use crate::facts::InvocationType; use crate::facts::PluginState; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnStatus; use crate::facts::TurnSteerRejectionReason; @@ -23,14 +24,6 @@ pub enum AppServerRpcTransport { InProcess, } -#[derive(Clone, Copy, Debug, Serialize)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ThreadInitializationMode { - New, - Forked, - Resumed, -} - #[derive(Serialize)] pub(crate) struct TrackEventsRequest { pub(crate) events: Vec, @@ -73,8 +66,8 @@ pub(crate) struct SkillInvocationEventParams { #[derive(Clone, Serialize)] pub(crate) struct CodexAppServerClientMetadata { pub(crate) product_client_id: String, - pub(crate) client_name: Option, - pub(crate) client_version: Option, + pub(crate) client_name: String, + pub(crate) client_version: String, pub(crate) rpc_transport: AppServerRpcTransport, pub(crate) experimental_api_enabled: Option, } @@ -134,8 +127,14 @@ pub(crate) struct CodexAppUsedEventRequest { pub(crate) struct CodexTurnEventParams { pub(crate) thread_id: String, pub(crate) turn_id: String, - pub(crate) product_client_id: String, + pub(crate) app_server_client: CodexAppServerClientMetadata, + pub(crate) runtime: CodexRuntimeMetadata, pub(crate) submission_type: Option, + pub(crate) ephemeral: bool, + pub(crate) thread_source: Option, + pub(crate) initialization_mode: ThreadInitializationMode, + pub(crate) subagent_source: Option, + pub(crate) parent_thread_id: Option, pub(crate) model: Option, pub(crate) model_provider: String, pub(crate) sandbox_policy: Option<&'static str>, @@ -330,8 +329,8 @@ pub(crate) fn subagent_thread_started_event_request( thread_id: input.thread_id, app_server_client: CodexAppServerClientMetadata { product_client_id: input.product_client_id, - client_name: Some(input.client_name), - client_version: Some(input.client_version), + client_name: input.client_name, + client_version: input.client_version, rpc_transport: AppServerRpcTransport::InProcess, experimental_api_enabled: None, }, @@ -368,3 +367,27 @@ fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Option _ => None, } } + +pub(crate) fn turn_subagent_source_name(thread_source: &SessionSource) -> Option { + match thread_source { + SessionSource::SubAgent(subagent_source) => Some(subagent_source_name(subagent_source)), + SessionSource::Cli + | SessionSource::VSCode + | SessionSource::Exec + | SessionSource::Mcp + | SessionSource::Custom(_) + | SessionSource::Unknown => None, + } +} + +pub(crate) fn turn_parent_thread_id(thread_source: &SessionSource) -> Option { + match thread_source { + SessionSource::SubAgent(subagent_source) => subagent_parent_thread_id(subagent_source), + SessionSource::Cli + | SessionSource::VSCode + | SessionSource::Exec + | SessionSource::Mcp + | SessionSource::Custom(_) + | SessionSource::Unknown => None, + } +} diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 34d158d8bd..86fafc8c37 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -14,6 +14,7 @@ use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TokenUsage; @@ -45,6 +46,9 @@ pub struct TurnResolvedConfigFact { pub thread_id: String, pub num_input_images: usize, pub submission_type: Option, + pub ephemeral: bool, + pub session_source: SessionSource, + pub initialization_mode: ThreadInitializationMode, pub model: String, pub model_provider: String, pub sandbox_policy: SandboxPolicy, @@ -73,6 +77,14 @@ pub enum TurnSubmissionType { Queued, } +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ThreadInitializationMode { + New, + Forked, + Resumed, +} + #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "snake_case")] pub enum TurnStatus { diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index b3e829ea03..8fa4da8228 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -10,6 +10,7 @@ pub use facts::CodexTurnSteerEvent; pub use facts::InvocationType; pub use facts::SkillInvocation; pub use facts::SubAgentThreadStartedInput; +pub use facts::ThreadInitializationMode; pub use facts::TrackEventsContext; pub use facts::TurnResolvedConfigFact; pub use facts::TurnStatus; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index d6f3ebd5e5..eead6f0309 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -10,7 +10,6 @@ use crate::events::CodexTurnEventRequest; use crate::events::CodexTurnSteerEventRequest; use crate::events::SkillInvocationEventParams; use crate::events::SkillInvocationEventRequest; -use crate::events::ThreadInitializationMode; use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; @@ -21,6 +20,8 @@ use crate::events::codex_turn_steer_event_params; use crate::events::plugin_state_event_type; use crate::events::subagent_thread_started_event_request; use crate::events::thread_source_name; +use crate::events::turn_parent_thread_id; +use crate::events::turn_subagent_source_name; use crate::facts::AnalyticsFact; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; @@ -31,6 +32,7 @@ use crate::facts::PluginStateChangedInput; use crate::facts::PluginUsedInput; use crate::facts::SkillInvokedInput; use crate::facts::SubAgentThreadStartedInput; +use crate::facts::ThreadInitializationMode; use crate::facts::TrackEventsContext; use crate::facts::TurnResolvedConfigFact; use crate::facts::TurnStatus; @@ -178,8 +180,8 @@ impl AnalyticsReducer { ConnectionState { app_server_client: CodexAppServerClientMetadata { product_client_id, - client_name: Some(params.client_info.name), - client_version: Some(params.client_info.version), + client_name: params.client_info.name, + client_version: params.client_info.version, rpc_transport, experimental_api_enabled: params .capabilities @@ -584,16 +586,24 @@ impl AnalyticsReducer { { return; } - let product_client_id = turn_state + let connection_metadata = turn_state .connection_id .and_then(|connection_id| self.connections.get(&connection_id)) - .map(|connection_state| connection_state.app_server_client.product_client_id.clone()) - .unwrap_or_else(|| originator().value); + .map(|connection_state| { + ( + connection_state.app_server_client.clone(), + connection_state.runtime.clone(), + ) + }); + let Some((app_server_client, runtime)) = connection_metadata else { + return; + }; out.push(TrackEventRequest::TurnEvent(Box::new( CodexTurnEventRequest { event_type: "codex_turn_event", event_params: codex_turn_event_params( - product_client_id, + app_server_client, + runtime, turn_id.to_string(), turn_state, ), @@ -604,7 +614,8 @@ impl AnalyticsReducer { } fn codex_turn_event_params( - product_client_id: String, + app_server_client: CodexAppServerClientMetadata, + runtime: CodexRuntimeMetadata, turn_id: String, turn_state: &TurnState, ) -> CodexTurnEventParams { @@ -622,6 +633,9 @@ fn codex_turn_event_params( thread_id: _resolved_thread_id, num_input_images: _resolved_num_input_images, submission_type, + ephemeral, + session_source, + initialization_mode, model, model_provider, sandbox_policy, @@ -639,8 +653,14 @@ fn codex_turn_event_params( CodexTurnEventParams { thread_id, turn_id, - product_client_id, + app_server_client, + runtime, submission_type, + ephemeral, + thread_source: thread_source_name(&session_source).map(str::to_string), + initialization_mode, + subagent_source: turn_subagent_source_name(&session_source), + parent_thread_id: turn_parent_thread_id(&session_source), model: Some(model), model_provider, sandbox_policy: Some(sandbox_policy_mode(&sandbox_policy)), diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 759cc60017..4c31ebdfb8 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -9240,6 +9240,7 @@ mod tests { reasoning_effort: None, personality: None, session_source: SessionSource::Cli, + initialization_mode: codex_analytics::ThreadInitializationMode::New, }; assert_eq!( 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 5358a2752f..3e9e3fafaf 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -415,13 +415,24 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> { assert_eq!(event["event_params"]["thread_id"], thread.id); assert_eq!(event["event_params"]["turn_id"], turn.id); assert_eq!( - event["event_params"]["product_client_id"], + event["event_params"]["app_server_client"]["product_client_id"], DEFAULT_CLIENT_NAME ); assert_eq!(event["event_params"]["model"], "mock-model"); assert_eq!(event["event_params"]["model_provider"], "mock_provider"); assert_eq!(event["event_params"]["sandbox_policy"], "read_only"); assert_eq!(event["event_params"]["submission_type"], "default"); + assert_eq!(event["event_params"]["ephemeral"], false); + assert_eq!(event["event_params"]["thread_source"], "user"); + assert_eq!(event["event_params"]["initialization_mode"], "new"); + assert_eq!( + event["event_params"]["subagent_source"], + serde_json::Value::Null + ); + assert_eq!( + event["event_params"]["parent_thread_id"], + serde_json::Value::Null + ); assert_eq!(event["event_params"]["num_input_images"], 1); assert_eq!(event["event_params"]["is_first_turn"], true); assert_eq!(event["event_params"]["status"], "completed"); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 77eb802ad9..ce08fad223 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -53,6 +53,7 @@ use codex_analytics::AppInvocation; use codex_analytics::CodexTurnSteerEvent; use codex_analytics::InvocationType; use codex_analytics::SubAgentThreadStartedInput; +use codex_analytics::ThreadInitializationMode; use codex_analytics::TrackEventsContext; use codex_analytics::TurnResolvedConfigFact; use codex_analytics::TurnSteerRejectionReason; @@ -695,6 +696,7 @@ impl Codex { app_server_client_name: None, app_server_client_version: None, session_source, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools, persist_extended_history, inherited_shell_snapshot, @@ -1129,6 +1131,14 @@ fn turn_submission_type(submission_type: SubmissionType) -> TurnSubmissionType { } } +fn thread_initialization_mode(initial_history: &InitialHistory) -> ThreadInitializationMode { + match initial_history { + InitialHistory::New => ThreadInitializationMode::New, + InitialHistory::Forked(_) => ThreadInitializationMode::Forked, + InitialHistory::Resumed(_) => ThreadInitializationMode::Resumed, + } +} + fn local_time_context() -> (String, String) { match iana_time_zone::get_timezone() { Ok(timezone) => (Local::now().format("%Y-%m-%d").to_string(), timezone), @@ -1194,6 +1204,7 @@ pub(crate) struct SessionConfiguration { app_server_client_version: Option, /// Source of the session (cli, vscode, exec, mcp, ...) session_source: SessionSource, + thread_initialization_mode: ThreadInitializationMode, dynamic_tools: Vec, persist_extended_history: bool, inherited_shell_snapshot: Option>, @@ -1218,6 +1229,7 @@ impl SessionConfiguration { reasoning_effort: self.collaboration_mode.reasoning_effort(), personality: self.personality, session_source: self.session_source.clone(), + initialization_mode: self.thread_initialization_mode, } } @@ -1594,6 +1606,8 @@ impl Session { session_configuration.collaboration_mode.model(), session_configuration.provider ); + session_configuration.thread_initialization_mode = + thread_initialization_mode(&initial_history); let forked_from_id = initial_history.forked_from_id(); let (conversation_id, rollout_params) = match &initial_history { @@ -6530,6 +6544,10 @@ async fn track_turn_resolved_config_analytics( return; } + let thread_config = { + let state = sess.state.lock().await; + state.session_configuration.thread_config_snapshot() + }; let is_first_turn = { let mut state = sess.state.lock().await; state.take_next_turn_is_first() @@ -6551,6 +6569,9 @@ async fn track_turn_resolved_config_analytics( .map(turn_submission_type) .unwrap_or(TurnSubmissionType::Default), ), + ephemeral: thread_config.ephemeral, + session_source: thread_config.session_source, + initialization_mode: thread_config.initialization_mode, model: turn_context.model_info.slug.clone(), model_provider: turn_context.config.model_provider_id.clone(), sandbox_policy: turn_context.sandbox_policy.get().clone(), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index f58dea067b..8635590b5c 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -13,6 +13,7 @@ use crate::function_tool::FunctionCallError; use crate::shell::default_user_shell; use crate::tools::format_exec_output_str; +use codex_analytics::ThreadInitializationMode; use codex_features::Features; use codex_login::CodexAuth; use codex_mcp::ToolInfo; @@ -1879,6 +1880,7 @@ async fn set_rate_limits_retains_previous_credits() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -1982,6 +1984,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2333,6 +2336,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2597,6 +2601,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -2702,6 +2707,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools: Vec::new(), persist_extended_history: false, inherited_shell_snapshot: None, @@ -3547,6 +3553,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( app_server_client_name: None, app_server_client_version: None, session_source: SessionSource::Exec, + thread_initialization_mode: ThreadInitializationMode::New, dynamic_tools, persist_extended_history: false, inherited_shell_snapshot: None, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 9727cc208a..4a2f5dae24 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -3,6 +3,7 @@ use crate::codex::Codex; use crate::codex::SteerInputError; use crate::config::ConstraintResult; use crate::file_watcher::WatchRegistration; +use codex_analytics::ThreadInitializationMode; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::Personality; @@ -42,6 +43,7 @@ pub struct ThreadConfigSnapshot { pub reasoning_effort: Option, pub personality: Option, pub session_source: SessionSource, + pub initialization_mode: ThreadInitializationMode, } pub struct CodexThread {