From 2f71969267d1b2466a20e338c701cf3b129630fa Mon Sep 17 00:00:00 2001 From: Roy Han Date: Wed, 11 Mar 2026 22:51:17 -0700 Subject: [PATCH] response wiring --- .../app-server/tests/suite/v2/compaction.rs | 1 + .../tests/suite/v2/thread_resume.rs | 2 + codex-rs/codex-api/tests/clients.rs | 1 + codex-rs/core/src/arc_monitor.rs | 7 ++++ codex-rs/core/src/codex.rs | 33 +++++++++++++++++ .../src/codex/rollout_reconstruction_tests.rs | 2 + codex-rs/core/src/codex_tests.rs | 35 +++++++++--------- codex-rs/core/src/compact.rs | 37 +++++++++++++++++++ .../core/src/context_manager/history_tests.rs | 23 ++++++++++++ .../core/src/context_manager/normalize.rs | 1 + codex-rs/core/src/context_manager/updates.rs | 1 + codex-rs/core/src/contextual_user_message.rs | 1 + codex-rs/core/src/event_mapping.rs | 10 +++++ codex-rs/core/src/features.rs | 8 ++++ codex-rs/core/src/guardian_tests.rs | 6 +++ codex-rs/core/src/memories/phase1.rs | 1 + codex-rs/core/src/rollout/tests.rs | 1 + codex-rs/core/src/rollout/truncation.rs | 2 + codex-rs/core/src/stream_events_utils.rs | 1 + codex-rs/core/src/tasks/mod.rs | 1 + codex-rs/core/src/tasks/review.rs | 2 + codex-rs/core/src/thread_manager.rs | 2 + .../core/src/tools/handlers/multi_agents.rs | 1 + codex-rs/core/src/turn_timing.rs | 2 + codex-rs/core/tests/common/responses.rs | 1 + codex-rs/core/tests/responses_headers.rs | 3 ++ codex-rs/core/tests/suite/client.rs | 5 +++ .../core/tests/suite/client_websockets.rs | 2 + codex-rs/core/tests/suite/compact.rs | 3 ++ codex-rs/core/tests/suite/compact_remote.rs | 3 ++ codex-rs/core/tests/suite/image_rollout.rs | 2 + codex-rs/core/tests/suite/items.rs | 34 ++++++++++++++++- codex-rs/core/tests/suite/review.rs | 2 + codex-rs/protocol/src/models.rs | 13 +++++++ codex-rs/protocol/src/protocol.rs | 1 + codex-rs/state/src/extract.rs | 1 + 36 files changed, 232 insertions(+), 19 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 09528c6a32..c1bd59b6da 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -132,6 +132,7 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() content: vec![ContentItem::OutputText { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 64e857fe43..0957014cbc 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -757,6 +757,7 @@ async fn thread_resume_rejects_history_when_thread_is_running() -> Result<()> { content: vec![ContentItem::InputText { text: "history override".to_string(), }], + metadata: None, end_turn: None, phase: None, }]), @@ -1494,6 +1495,7 @@ async fn thread_resume_supports_history_and_overrides() -> Result<()> { content: vec![ContentItem::InputText { text: history_text.to_string(), }], + metadata: None, end_turn: None, phase: None, }]; diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 63a033dea3..6a1b13a1ae 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -297,6 +297,7 @@ async fn azure_default_store_attaches_ids_and_headers() -> Result<()> { id: Some("msg_1".into()), role: "user".into(), content: vec![ContentItem::InputText { text: "hi".into() }], + metadata: None, end_turn: None, phase: None, }], diff --git a/codex-rs/core/src/arc_monitor.rs b/codex-rs/core/src/arc_monitor.rs index 030116b178..6727ae01bf 100644 --- a/codex-rs/core/src/arc_monitor.rs +++ b/codex-rs/core/src/arc_monitor.rs @@ -489,6 +489,7 @@ mod tests { content: vec![ContentItem::InputText { text: "first request".to_string(), }], + metadata: None, end_turn: None, phase: None, }], @@ -514,6 +515,7 @@ mod tests { content: vec![ContentItem::OutputText { text: "commentary".to_string(), }], + metadata: None, end_turn: None, phase: Some(MessagePhase::Commentary), }], @@ -528,6 +530,7 @@ mod tests { content: vec![ContentItem::OutputText { text: "final response".to_string(), }], + metadata: None, end_turn: None, phase: Some(MessagePhase::FinalAnswer), }], @@ -542,6 +545,7 @@ mod tests { content: vec![ContentItem::InputText { text: "latest request".to_string(), }], + metadata: None, end_turn: None, phase: None, }], @@ -695,6 +699,7 @@ mod tests { content: vec![ContentItem::InputText { text: "please run the tool".to_string(), }], + metadata: None, end_turn: None, phase: None, }], @@ -774,6 +779,7 @@ mod tests { content: vec![ContentItem::InputText { text: "please run the tool".to_string(), }], + metadata: None, end_turn: None, phase: None, }], @@ -843,6 +849,7 @@ mod tests { content: vec![ContentItem::InputText { text: "please run the tool".to_string(), }], + metadata: None, end_turn: None, phase: None, }], diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0b133cd1a6..d71fc9fe1d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3240,6 +3240,7 @@ impl Session { content: vec![ContentItem::InputText { text: format!("Warning: {}", message.into()), }], + metadata: None, end_turn: None, phase: None, }; @@ -3659,6 +3660,38 @@ impl Session { response_item: ResponseItem, user_message_type: Option, ) { + let user_message_type = if self.enabled(Feature::UserMessageTypeMetadata) { + user_message_type + } else { + None + }; + + let response_item = match (response_item, user_message_type.clone()) { + ( + ResponseItem::Message { + id, + role, + content, + metadata, + end_turn, + phase, + }, + Some(kind), + ) if role == "user" => { + let mut metadata = metadata.unwrap_or_default(); + metadata.user_message_type = Some(kind); + ResponseItem::Message { + id, + role, + content, + metadata: Some(metadata), + end_turn, + phase, + } + } + (response_item, _) => response_item, + }; + // Persist the user message to history, but emit the turn item from `UserInput` so // UI-only `text_elements` are preserved. `ResponseItem::Message` does not carry // those spans, and `record_response_item_and_emit_turn_item` would drop them. diff --git a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs index 6cc99a2907..2736905390 100644 --- a/codex-rs/core/src/codex/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/codex/rollout_reconstruction_tests.rs @@ -16,6 +16,7 @@ fn user_message(text: &str) -> ResponseItem { content: vec![ContentItem::InputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -28,6 +29,7 @@ fn assistant_message(text: &str) -> ResponseItem { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 9d3919be4b..305ee1f426 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -107,6 +107,7 @@ fn user_message(text: &str) -> ResponseItem { content: vec![ContentItem::InputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -119,6 +120,7 @@ fn assistant_message(text: &str) -> ResponseItem { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -131,6 +133,7 @@ fn skill_message(text: &str) -> ResponseItem { content: vec![ContentItem::InputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -764,6 +767,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -775,6 +779,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { content: vec![ContentItem::InputText { text: "stale developer instructions".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -3340,6 +3345,7 @@ async fn record_context_updates_and_set_reference_context_item_reinjects_full_co content: vec![ContentItem::InputText { text: format!("{}\nsummary", crate::compact::SUMMARY_PREFIX), }], + metadata: None, end_turn: None, phase: None, }; @@ -3716,6 +3722,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() content: vec![ContentItem::InputText { text: "late pending input".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -3737,19 +3744,12 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() assert!(matches!( second.msg, EventMsg::ItemStarted(ItemStartedEvent { - item: TurnItem::UserMessage(UserMessageItem { - content, - metadata, - .. - }), + item: TurnItem::UserMessage(UserMessageItem { content, .. }), .. }) if content == vec![UserInput::Text { text: "late pending input".to_string(), text_elements: Vec::new(), - }] && metadata - .as_ref() - .and_then(|item_metadata| item_metadata.user_message_type.as_ref()) - == Some(&codex_protocol::items::UserMessageType::PromptQueued) + }] )); let third = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) @@ -3759,19 +3759,12 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input() assert!(matches!( third.msg, EventMsg::ItemCompleted(ItemCompletedEvent { - item: TurnItem::UserMessage(UserMessageItem { - content, - metadata, - .. - }), + item: TurnItem::UserMessage(UserMessageItem { content, .. }), .. }) if content == vec![UserInput::Text { text: "late pending input".to_string(), text_elements: Vec::new(), - }] && metadata - .as_ref() - .and_then(|item_metadata| item_metadata.user_message_type.as_ref()) - == Some(&codex_protocol::items::UserMessageType::PromptQueued) + }] )); let fourth = tokio::time::timeout(std::time::Duration::from_secs(2), rx.recv()) @@ -4077,6 +4070,7 @@ async fn sample_rollout( content: vec![ContentItem::InputText { text: "first user".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -4092,6 +4086,7 @@ async fn sample_rollout( content: vec![ContentItem::OutputText { text: "assistant reply one".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -4119,6 +4114,7 @@ async fn sample_rollout( content: vec![ContentItem::InputText { text: "second user".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -4134,6 +4130,7 @@ async fn sample_rollout( content: vec![ContentItem::OutputText { text: "assistant reply two".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -4161,6 +4158,7 @@ async fn sample_rollout( content: vec![ContentItem::InputText { text: "third user".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -4176,6 +4174,7 @@ async fn sample_rollout( content: vec![ContentItem::OutputText { text: "assistant reply three".to_string(), }], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 42e338443d..d20cee6132 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -367,6 +367,7 @@ fn build_compacted_history_with_limit( content: vec![ContentItem::InputText { text: message.clone(), }], + metadata: None, end_turn: None, phase: None, }); @@ -382,6 +383,7 @@ fn build_compacted_history_with_limit( id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: summary_text }], + metadata: None, end_turn: None, phase: None, }); @@ -501,6 +503,7 @@ mod tests { content: vec![ContentItem::OutputText { text: "ignored".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -510,6 +513,7 @@ mod tests { content: vec![ContentItem::InputText { text: "first".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -535,6 +539,7 @@ do things "# .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -544,6 +549,7 @@ do things content: vec![ContentItem::InputText { text: "cwd=/tmp".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -553,6 +559,7 @@ do things content: vec![ContentItem::InputText { text: "real user message".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -636,6 +643,7 @@ do things content: vec![ContentItem::InputText { text: "stale permissions".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -645,6 +653,7 @@ do things content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -654,6 +663,7 @@ do things content: vec![ContentItem::InputText { text: "stale personality".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -666,6 +676,7 @@ do things content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }); @@ -680,6 +691,7 @@ do things content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -691,6 +703,7 @@ do things content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }); @@ -711,6 +724,7 @@ keep me updated "# .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -724,6 +738,7 @@ keep me updated "# .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -737,6 +752,7 @@ keep me updated "# .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -746,6 +762,7 @@ keep me updated content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -755,6 +772,7 @@ keep me updated content: vec![ContentItem::InputText { text: "stale developer instructions".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -767,6 +785,7 @@ keep me updated content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }); @@ -782,6 +801,7 @@ keep me updated content: vec![ContentItem::InputText { text: "older user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -791,6 +811,7 @@ keep me updated content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], + metadata: None, end_turn: None, phase: None, }, @@ -800,6 +821,7 @@ keep me updated content: vec![ContentItem::InputText { text: "latest user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -814,6 +836,7 @@ keep me updated content: vec![ContentItem::InputText { text: "older user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -823,6 +846,7 @@ keep me updated content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], + metadata: None, end_turn: None, phase: None, }, @@ -834,6 +858,7 @@ keep me updated content: vec![ContentItem::InputText { text: "latest user".to_string(), }], + metadata: None, end_turn: None, phase: None, }); @@ -848,6 +873,7 @@ keep me updated content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -878,6 +904,7 @@ keep me updated content: vec![ContentItem::InputText { text: "summary".to_string(), }], + metadata: None, end_turn: None, phase: None, }); @@ -893,6 +920,7 @@ keep me updated content: vec![ContentItem::InputText { text: "older user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -902,6 +930,7 @@ keep me updated content: vec![ContentItem::InputText { text: "latest user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -911,6 +940,7 @@ keep me updated content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], + metadata: None, end_turn: None, phase: None, }, @@ -921,6 +951,7 @@ keep me updated content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -936,6 +967,7 @@ keep me updated content: vec![ContentItem::InputText { text: "older user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -945,6 +977,7 @@ keep me updated content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -954,6 +987,7 @@ keep me updated content: vec![ContentItem::InputText { text: "latest user".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -963,6 +997,7 @@ keep me updated content: vec![ContentItem::InputText { text: format!("{SUMMARY_PREFIX}\nsummary text"), }], + metadata: None, end_turn: None, phase: None, }, @@ -981,6 +1016,7 @@ keep me updated content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -996,6 +1032,7 @@ keep me updated content: vec![ContentItem::InputText { text: "fresh permissions".to_string(), }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 104fedab06..6bfdf62aa1 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -33,6 +33,7 @@ fn assistant_msg(text: &str) -> ResponseItem { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -53,6 +54,7 @@ fn user_msg(text: &str) -> ResponseItem { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -65,6 +67,7 @@ fn user_input_text_msg(text: &str) -> ResponseItem { content: vec![ContentItem::InputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -120,6 +123,7 @@ fn filters_non_api_messages() { content: vec![ContentItem::OutputText { text: "ignored".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -151,6 +155,7 @@ fn filters_non_api_messages() { content: vec![ContentItem::OutputText { text: "hi".to_string() }], + metadata: None, end_turn: None, phase: None, }, @@ -160,6 +165,7 @@ fn filters_non_api_messages() { content: vec![ContentItem::OutputText { text: "hello".to_string() }], + metadata: None, end_turn: None, phase: None, } @@ -265,6 +271,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { text: "caption".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }, @@ -326,6 +333,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { text: "caption".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }, @@ -382,6 +390,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { image_url: "https://example.com/img.png".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }]); @@ -410,6 +419,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() { content: vec![ContentItem::InputText { text: "hi".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -435,6 +445,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() { image_url: "data:image/png;base64,Zm9v".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }, @@ -444,6 +455,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_supported() { content: vec![ContentItem::InputText { text: "hi".to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -460,6 +472,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() { content: vec![ContentItem::InputText { text: "generate a lobster".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -480,6 +493,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() { content: vec![ContentItem::InputText { text: "generate a lobster".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -501,6 +515,7 @@ fn for_prompt_rewrites_image_generation_calls_when_images_are_unsupported() { .to_string(), }, ], + metadata: None, end_turn: None, phase: None, }, @@ -648,6 +663,7 @@ fn replace_last_turn_images_does_not_touch_user_images() { content: vec![ContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -1409,6 +1425,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() { }, ContentItem::InputImage { image_url }, ], + metadata: None, end_turn: None, phase: None, }; @@ -1418,6 +1435,7 @@ fn image_data_url_payload_does_not_dominate_message_estimate() { content: vec![ContentItem::InputText { text: "Here is the screenshot".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -1490,6 +1508,7 @@ fn non_base64_image_urls_are_unchanged() { content: vec![ContentItem::InputImage { image_url: "https://example.com/foo.png".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -1521,6 +1540,7 @@ fn data_url_without_base64_marker_is_unchanged() { content: vec![ContentItem::InputImage { image_url: "data:image/svg+xml,".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -1559,6 +1579,7 @@ fn mixed_case_data_url_markers_are_adjusted() { id: None, role: "user".to_string(), content: vec![ContentItem::InputImage { image_url }], + metadata: None, end_turn: None, phase: None, }; @@ -1590,6 +1611,7 @@ fn multiple_inline_images_apply_multiple_fixed_costs() { image_url: image_url_two, }, ], + metadata: None, end_turn: None, phase: None, }; @@ -1673,6 +1695,7 @@ fn text_only_items_unchanged() { content: vec![ContentItem::OutputText { text: "Hello world, this is a response.".to_string(), }], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index a0009f18ab..d185704bd7 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -243,6 +243,7 @@ pub(crate) fn rewrite_image_generation_calls_for_stateless_input(items: &mut Vec }, ContentItem::InputImage { image_url }, ], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index 0d26c551ea..ba2a8c7bcc 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -178,6 +178,7 @@ fn build_text_message(role: &str, text_sections: Vec) -> Option\ntest_text\n".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -325,6 +329,7 @@ mod tests { content: vec![ContentItem::InputText { text: "test_text".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -334,6 +339,7 @@ mod tests { content: vec![ContentItem::InputText { text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -344,6 +350,7 @@ mod tests { text: "\ndemo\nskills/demo/SKILL.md\nbody\n" .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -353,6 +360,7 @@ mod tests { content: vec![ContentItem::InputText { text: "echo 42".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -369,6 +377,7 @@ mod tests { .to_string(), }, ], + metadata: None, end_turn: None, phase: None, }, @@ -388,6 +397,7 @@ mod tests { content: vec![ContentItem::OutputText { text: "Hello from Codex".to_string(), }], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index 7de8b8c7ed..991dccd24b 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -166,6 +166,8 @@ pub enum Feature { ToolCallMcpElicitation, /// Enable personality selection in the TUI. Personality, + /// Emit user_message_type metadata on user message turn items and request items. + UserMessageTypeMetadata, /// Enable native artifact tools. Artifact, /// Enable Fast mode selection in the TUI and request layer. @@ -784,6 +786,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::UserMessageTypeMetadata, + key: "user_message_type_metadata", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Artifact, key: "artifact", diff --git a/codex-rs/core/src/guardian_tests.rs b/codex-rs/core/src/guardian_tests.rs index 6deac9e777..031f363295 100644 --- a/codex-rs/core/src/guardian_tests.rs +++ b/codex-rs/core/src/guardian_tests.rs @@ -64,6 +64,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() { content: vec![ContentItem::InputText { text: "\n/tmp\n".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -73,6 +74,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() { content: vec![ContentItem::OutputText { text: "hello".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -99,6 +101,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() { content: vec![ContentItem::InputText { text: "check the repo".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -120,6 +123,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() { content: vec![ContentItem::OutputText { text: "I need to push a fix".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -313,6 +317,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() text: "Please check the repo visibility and push the docs fix if needed." .to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -335,6 +340,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot() text: "The repo is public; I now need approval to push the docs fix." .to_string(), }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/core/src/memories/phase1.rs b/codex-rs/core/src/memories/phase1.rs index ad4f29a0d2..96ee905e54 100644 --- a/codex-rs/core/src/memories/phase1.rs +++ b/codex-rs/core/src/memories/phase1.rs @@ -330,6 +330,7 @@ mod job { &rollout_contents, )?, }], + metadata: None, end_turn: None, phase: None, }], diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 12af36b783..9c8bfacfc6 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -1134,6 +1134,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { content: vec![ContentItem::OutputText { text: format!("reply-{idx}"), }], + metadata: None, end_turn: None, phase: None, }), diff --git a/codex-rs/core/src/rollout/truncation.rs b/codex-rs/core/src/rollout/truncation.rs index c50eacc48b..0b8081719d 100644 --- a/codex-rs/core/src/rollout/truncation.rs +++ b/codex-rs/core/src/rollout/truncation.rs @@ -85,6 +85,7 @@ mod tests { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -97,6 +98,7 @@ mod tests { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/src/stream_events_utils.rs b/codex-rs/core/src/stream_events_utils.rs index 26ec7cc6f7..1a758fcb6e 100644 --- a/codex-rs/core/src/stream_events_utils.rs +++ b/codex-rs/core/src/stream_events_utils.rs @@ -405,6 +405,7 @@ mod tests { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: Some(true), phase: None, } diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 848679eb3e..f098c28e74 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -433,6 +433,7 @@ impl Session { "{TURN_ABORTED_OPEN_TAG}\n{TURN_ABORTED_INTERRUPTED_GUIDANCE}\n" ), }], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 0a72355b5b..217f5e50ea 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -239,6 +239,7 @@ pub(crate) async fn exit_review_mode( id: Some(REVIEW_USER_MESSAGE_ID.to_string()), role: "user".to_string(), content: vec![ContentItem::InputText { text: user_message }], + metadata: None, end_turn: None, phase: None, }], @@ -260,6 +261,7 @@ pub(crate) async fn exit_review_mode( content: vec![ContentItem::OutputText { text: assistant_message, }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index cc6de47104..d0a40755aa 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -685,6 +685,7 @@ mod tests { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } @@ -696,6 +697,7 @@ mod tests { content: vec![ContentItem::OutputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index a2d4e39b99..26d7e681af 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -1717,6 +1717,7 @@ mod tests { content: vec![ContentItem::InputText { text: "materialized".to_string(), }], + metadata: None, end_turn: None, phase: None, })]), diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index 7ca67d10db..8004682894 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -258,6 +258,7 @@ mod tests { content: vec![ContentItem::OutputText { text: "hello".to_string(), }], + metadata: None, end_turn: None, phase: None, })); @@ -271,6 +272,7 @@ mod tests { content: vec![ContentItem::OutputText { text: String::new(), }], + metadata: None, end_turn: None, phase: None, })); diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index cf7c03f4df..f580b1d4ad 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -633,6 +633,7 @@ pub fn user_message_item(text: &str) -> ResponseItem { content: vec![ContentItem::InputText { text: text.to_string(), }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index d5376fcd7d..91f75d6a7b 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -105,6 +105,7 @@ async fn responses_stream_includes_subagent_header_on_review() { content: vec![ContentItem::InputText { text: "hello".into(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -218,6 +219,7 @@ async fn responses_stream_includes_subagent_header_on_other() { content: vec![ContentItem::InputText { text: "hello".into(), }], + metadata: None, end_turn: None, phase: None, }]; @@ -330,6 +332,7 @@ async fn responses_respects_model_info_overrides_from_config() { content: vec![ContentItem::InputText { text: "hello".into(), }], + metadata: None, end_turn: None, phase: None, }]; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index cfb84be836..c09498cce8 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -177,6 +177,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { content: vec![codex_protocol::models::ContentItem::InputText { text: "resumed user message".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -199,6 +200,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { content: vec![codex_protocol::models::ContentItem::OutputText { text: "resumed system instruction".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -221,6 +223,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { content: vec![codex_protocol::models::ContentItem::OutputText { text: "resumed assistant message".to_string(), }], + metadata: None, end_turn: None, phase: Some(MessagePhase::Commentary), }; @@ -400,6 +403,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() { content: vec![ContentItem::InputImage { image_url: legacy_image_url.to_string(), }], + metadata: None, end_turn: None, phase: None, }), @@ -1864,6 +1868,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { content: vec![ContentItem::OutputText { text: "message".into(), }], + metadata: None, end_turn: None, phase: None, }); diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 2c7b4d48e1..82c956c3fc 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -1470,6 +1470,7 @@ fn message_item(text: &str) -> ResponseItem { id: None, role: "user".into(), content: vec![ContentItem::InputText { text: text.into() }], + metadata: None, end_turn: None, phase: None, } @@ -1480,6 +1481,7 @@ fn assistant_message_item(id: &str, text: &str) -> ResponseItem { id: Some(id.to_string()), role: "assistant".into(), content: vec![ContentItem::OutputText { text: text.into() }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index bb34789499..de4280801e 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1586,6 +1586,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { content: vec![codex_protocol::models::ContentItem::OutputText { text: remote_summary.to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -2795,6 +2796,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { content: vec![codex_protocol::models::ContentItem::OutputText { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -2915,6 +2917,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { content: vec![codex_protocol::models::ContentItem::OutputText { text: "REMOTE_COMPACT_SUMMARY".to_string(), }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 683f8b945b..c448ffeff7 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -1101,6 +1101,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> content: vec![ContentItem::OutputText { text: "COMPACTED_ASSISTANT_NOTE".to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -1238,6 +1239,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res content: vec![ContentItem::InputText { text: stale_developer_message.to_string(), }], + metadata: None, end_turn: None, phase: None, }, @@ -1370,6 +1372,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume() content: vec![ContentItem::InputText { text: stale_developer_message.to_string(), }], + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 07b51c7629..dbbe1878bb 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -157,6 +157,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu text: "pasted image".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }; @@ -240,6 +241,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> text: "dropped image".to_string(), }, ], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index e18488b4c8..ef771df5b7 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -1,6 +1,7 @@ #![cfg(not(target_os = "windows"))] use anyhow::Ok; +use codex_core::features::Feature; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; @@ -35,6 +36,7 @@ use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use pretty_assertions::assert_eq; +use serde_json::Value; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn user_message_item_is_emitted() -> anyhow::Result<()> { @@ -42,7 +44,15 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { let server = start_mock_server().await; - let TestCodex { codex, .. } = test_codex().build(&server).await?; + let TestCodex { codex, .. } = test_codex() + .with_config(|config| { + config + .features + .enable(Feature::UserMessageTypeMetadata) + .expect("feature flag should be enabled for this test"); + }) + .build(&server) + .await?; let first_response = sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]); mount_sse_once(&server, first_response).await; @@ -105,6 +115,28 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { .await; assert_eq!(legacy_message.message, "please inspect sample.txt"); assert_eq!(legacy_message.text_elements, text_elements); + + let requests = server.received_requests().await.unwrap_or_default(); + assert_eq!(requests.len(), 1); + let body: Value = requests[0].body_json().expect("valid JSON request body"); + let input = body + .get("input") + .and_then(Value::as_array) + .expect("request input array"); + let user_message = input + .iter() + .find(|item| { + item.get("type").and_then(Value::as_str) == Some("message") + && item.get("role").and_then(Value::as_str) == Some("user") + }) + .expect("user message input item"); + assert_eq!( + user_message + .get("metadata") + .and_then(|metadata| metadata.get("user_message_type")) + .and_then(Value::as_str), + Some("prompt") + ); Ok(()) } diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 3a18cf157b..1676f6f8a9 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -530,6 +530,7 @@ async fn review_input_isolated_from_parent_history() { content: vec![codex_protocol::models::ContentItem::InputText { text: "parent: earlier user message".to_string(), }], + metadata: None, end_turn: None, phase: None, }; @@ -550,6 +551,7 @@ async fn review_input_isolated_from_parent_history() { content: vec![codex_protocol::models::ContentItem::OutputText { text: "parent: assistant reply".to_string(), }], + metadata: None, end_turn: None, phase: None, }; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index fd353b12c4..031a611f26 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -11,6 +11,7 @@ use ts_rs::TS; use crate::config_types::CollaborationMode; use crate::config_types::SandboxMode; +use crate::items::UserMessageType; use crate::protocol::AskForApproval; use crate::protocol::COLLABORATION_MODE_CLOSE_TAG; use crate::protocol::COLLABORATION_MODE_OPEN_TAG; @@ -250,6 +251,13 @@ pub enum ContentItem { OutputText { text: String }, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +pub struct ResponseItemMetadata { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub user_message_type: Option, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "lowercase")] pub enum ImageDetail { @@ -284,6 +292,9 @@ pub enum ResponseItem { id: Option, role: String, content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + metadata: Option, // Do not use directly, no available consistently across all providers. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -711,6 +722,7 @@ impl From for ResponseItem { content: vec![ContentItem::InputText { text: di.into_text(), }], + metadata: None, end_turn: None, phase: None, } @@ -870,6 +882,7 @@ impl From for ResponseItem { role, content, id: None, + metadata: None, end_turn: None, phase: None, }, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 081900ef14..8c4b8ca1ab 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2365,6 +2365,7 @@ impl From for ResponseItem { content: vec![ContentItem::OutputText { text: value.message, }], + metadata: None, end_turn: None, phase: None, } diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index ba425adbec..58ef3a2131 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -165,6 +165,7 @@ mod tests { content: vec![ContentItem::InputText { text: "hello from response item".to_string(), }], + metadata: None, end_turn: None, phase: None, });