response wiring

This commit is contained in:
Roy Han
2026-03-11 22:51:17 -07:00
parent 5d63c86d53
commit 2f71969267
36 changed files with 232 additions and 19 deletions
@@ -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,
},
@@ -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,
}];
+1
View File
@@ -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,
}],
+7
View File
@@ -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,
}],
+33
View File
@@ -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<UserMessageType>,
) {
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.
@@ -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,
}
+17 -18
View File
@@ -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,
};
+37
View File
@@ -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
</INSTRUCTIONS>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -544,6 +549,7 @@ do things
content: vec![ContentItem::InputText {
text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".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
</INSTRUCTIONS>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -724,6 +738,7 @@ keep me updated
</environment_context>"#
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -737,6 +752,7 @@ keep me updated
</turn_aborted>"#
.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,
},
@@ -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,<svg xmlns='http://www.w3.org/2000/svg'/>".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,
};
@@ -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,
}
@@ -178,6 +178,7 @@ fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<Response
id: None,
role: role.to_string(),
content,
metadata: None,
end_turn: None,
phase: None,
})
@@ -57,6 +57,7 @@ impl ContextualUserFragmentDefinition {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text }],
metadata: None,
end_turn: None,
phase: None,
}
+10
View File
@@ -201,6 +201,7 @@ mod tests {
image_url: img2.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -244,6 +245,7 @@ mod tests {
text: user_text.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -286,6 +288,7 @@ mod tests {
text: user_text.clone(),
},
],
metadata: None,
end_turn: None,
phase: None,
};
@@ -316,6 +319,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -325,6 +329,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "<environment_context>test_text</environment_context>".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<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -344,6 +350,7 @@ mod tests {
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
.to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
},
@@ -353,6 +360,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "<user_shell_command>echo 42</user_shell_command>".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,
};
+8
View File
@@ -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",
+6
View File
@@ -64,6 +64,7 @@ fn collect_guardian_transcript_entries_skips_contextual_user_messages() {
content: vec![ContentItem::InputText {
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".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,
},
+1
View File
@@ -330,6 +330,7 @@ mod job {
&rollout_contents,
)?,
}],
metadata: None,
end_turn: None,
phase: None,
}],
+1
View File
@@ -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,
}),
+2
View File
@@ -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,
}
+1
View File
@@ -405,6 +405,7 @@ mod tests {
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
metadata: None,
end_turn: Some(true),
phase: None,
}
+1
View File
@@ -433,6 +433,7 @@ impl Session {
"{TURN_ABORTED_OPEN_TAG}\n{TURN_ABORTED_INTERRUPTED_GUIDANCE}\n</turn_aborted>"
),
}],
metadata: None,
end_turn: None,
phase: None,
};
+2
View File
@@ -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,
},
+2
View File
@@ -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,
}
@@ -1717,6 +1717,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "materialized".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
})]),
+2
View File
@@ -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,
}));
+1
View File
@@ -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,
}
+3
View File
@@ -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,
}];
+5
View File
@@ -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,
});
@@ -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,
}
+3
View File
@@ -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,
},
@@ -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,
},
@@ -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,
};
+33 -1
View File
@@ -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(())
}
+2
View File
@@ -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,
};
+13
View File
@@ -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<UserMessageType>,
}
#[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<String>,
role: String,
content: Vec<ContentItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
metadata: Option<ResponseItemMetadata>,
// 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<DeveloperInstructions> for ResponseItem {
content: vec![ContentItem::InputText {
text: di.into_text(),
}],
metadata: None,
end_turn: None,
phase: None,
}
@@ -870,6 +882,7 @@ impl From<ResponseInputItem> for ResponseItem {
role,
content,
id: None,
metadata: None,
end_turn: None,
phase: None,
},
+1
View File
@@ -2365,6 +2365,7 @@ impl From<CompactedItem> for ResponseItem {
content: vec![ContentItem::OutputText {
text: value.message,
}],
metadata: None,
end_turn: None,
phase: None,
}
+1
View File
@@ -165,6 +165,7 @@ mod tests {
content: vec![ContentItem::InputText {
text: "hello from response item".to_string(),
}],
metadata: None,
end_turn: None,
phase: None,
});