Preserve assistant phase for replayed messages (#19832)

This commit is contained in:
friel-openai
2026-04-28 08:46:13 -07:00
committed by GitHub
Unverified
parent 21e19912e0
commit 598bbcdb58
8 changed files with 64 additions and 5 deletions
+7 -1
View File
@@ -456,9 +456,15 @@ impl CodexThread {
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {
match message {
ResponseItem::Message { role, content, .. } => Ok(ResponseInputItem::Message {
ResponseItem::Message {
role,
content,
phase,
..
} => Ok(ResponseInputItem::Message {
role: role.clone(),
content: content.clone(),
phase: phase.clone(),
}),
_ => Err(CodexErr::InvalidRequest(
"append_message only supports ResponseItem::Message".to_string(),
+2
View File
@@ -1307,6 +1307,7 @@ impl Session {
content: vec![ContentItem::InputText {
text: continuation_prompt(&goal),
}],
phase: None,
}],
})
}
@@ -1452,6 +1453,7 @@ fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
content: vec![ContentItem::InputText {
text: budget_limit_prompt(goal),
}],
phase: None,
}
}
+1
View File
@@ -1205,6 +1205,7 @@ Approved action:
let items = vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
phase: None,
}];
if let Err(items) = sess.inject_response_items(items).await {
+2
View File
@@ -1699,6 +1699,7 @@ impl Session {
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
phase: None,
}])
.await
.is_err()
@@ -1795,6 +1796,7 @@ impl Session {
.inject_response_items(vec![ResponseInputItem::Message {
role: "developer".to_string(),
content: vec![ContentItem::InputText { text }],
phase: None,
}])
.await
.is_err()
+8 -1
View File
@@ -6432,6 +6432,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
content: vec![ContentItem::InputText {
text: "late pending input".to_string(),
}],
phase: None,
}])
.await
.expect("inject pending input into active turn");
@@ -6677,18 +6678,21 @@ async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() {
content: vec![ContentItem::InputText {
text: "blocked queued prompt".to_string(),
}],
phase: None,
};
let later = ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "later queued prompt".to_string(),
}],
phase: None,
};
let newer = ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "newer queued prompt".to_string(),
}],
phase: None,
};
sess.inject_response_items(vec![blocked.clone(), later.clone()])
@@ -6719,6 +6723,7 @@ async fn queued_response_items_for_next_turn_move_into_next_active_turn() {
content: vec![ContentItem::InputText {
text: "queued before wake".to_string(),
}],
phase: None,
};
sess.queue_response_items_for_next_turn(vec![queued_item.clone()])
@@ -6745,6 +6750,7 @@ async fn idle_interrupt_does_not_wake_queued_next_turn_items() {
content: vec![ContentItem::InputText {
text: "queued before interrupt".to_string(),
}],
phase: None,
};
sess.queue_response_items_for_next_turn(vec![queued_item])
@@ -6764,6 +6770,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
content: vec![ContentItem::InputText {
text: "late pending input".to_string(),
}],
phase: None,
};
let turn_state = {
let mut active = sess.active_turn.lock().await;
@@ -6969,7 +6976,7 @@ async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyh
.await?;
let pending_input = sess.get_pending_input().await;
let [ResponseInputItem::Message { role, content }] = pending_input.as_slice() else {
let [ResponseInputItem::Message { role, content, .. }] = pending_input.as_slice() else {
panic!("expected one budget-limit steering message, got {pending_input:#?}");
};
assert_eq!("developer", role);
+10 -1
View File
@@ -354,7 +354,16 @@ async fn persist_user_shell_output(
}
let response_input_item = match output_item {
ResponseItem::Message { role, content, .. } => ResponseInputItem::Message { role, content },
ResponseItem::Message {
role,
content,
phase,
..
} => ResponseInputItem::Message {
role,
content,
phase,
},
_ => unreachable!("user shell command output record should always be a message"),
};
+33 -2
View File
@@ -609,6 +609,9 @@ pub enum ResponseInputItem {
Message {
role: String,
content: Vec<ContentItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
phase: Option<MessagePhase>,
},
FunctionCallOutput {
call_id: String,
@@ -1034,11 +1037,15 @@ pub fn local_image_content_items_with_label_number(
impl From<ResponseInputItem> for ResponseItem {
fn from(item: ResponseInputItem) -> Self {
match item {
ResponseInputItem::Message { role, content } => Self::Message {
ResponseInputItem::Message {
role,
content,
phase,
} => Self::Message {
role,
content,
id: None,
phase: None,
phase,
},
ResponseInputItem::FunctionCallOutput { call_id, output } => {
Self::FunctionCallOutput { call_id, output }
@@ -1176,6 +1183,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
UserInput::Skill { .. } | UserInput::Mention { .. } => Vec::new(), // Tool bodies are injected later in core
})
.collect::<Vec<ContentItem>>(),
phase: None,
}
}
}
@@ -1581,6 +1589,29 @@ mod tests {
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn response_input_message_conversion_preserves_phase() {
let item = ResponseItem::from(ResponseInputItem::Message {
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "still working".to_string(),
}],
phase: Some(MessagePhase::Commentary),
});
assert_eq!(
item,
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "still working".to_string(),
}],
phase: Some(MessagePhase::Commentary),
}
);
}
#[test]
fn sandbox_permissions_helpers_match_documented_semantics() {
let cases = [
+1
View File
@@ -855,6 +855,7 @@ impl InterAgentCommunication {
content: vec![ContentItem::OutputText {
text: serde_json::to_string(self).unwrap_or_default(),
}],
phase: Some(MessagePhase::Commentary),
}
}