Skip to content

Commit b2a6f1c

Browse files
friel-openaicodex
andcommitted
Preserve assistant phase for replayed messages
ResponseInputItem::Message can contain assistant-authored history that is later converted into ResponseItem::Message. Preserve phase across that boundary instead of inferring phase from message content. InterAgentCommunication::to_response_input_item records inter-agent mailbox messages as commentary because they are assistant-authored in-between updates, not completed final answers. This follows the assistant phase guidance in https://developers.openai.com/api/docs/guides/deployment-checklist#set-up-the-assistant-phase-parameter. Co-authored-by: Codex <noreply@openai.com>
1 parent 4ed22fc commit b2a6f1c

8 files changed

Lines changed: 64 additions & 5 deletions

File tree

codex-rs/core/src/codex_thread.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,15 @@ impl CodexThread {
437437

438438
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {
439439
match message {
440-
ResponseItem::Message { role, content, .. } => Ok(ResponseInputItem::Message {
440+
ResponseItem::Message {
441+
role,
442+
content,
443+
phase,
444+
..
445+
} => Ok(ResponseInputItem::Message {
441446
role: role.clone(),
442447
content: content.clone(),
448+
phase: phase.clone(),
443449
}),
444450
_ => Err(CodexErr::InvalidRequest(
445451
"append_message only supports ResponseItem::Message".to_string(),

codex-rs/core/src/goals.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,7 @@ impl Session {
13071307
content: vec![ContentItem::InputText {
13081308
text: continuation_prompt(&goal),
13091309
}],
1310+
phase: None,
13101311
}],
13111312
})
13121313
}
@@ -1452,6 +1453,7 @@ fn budget_limit_steering_item(goal: &ThreadGoal) -> ResponseInputItem {
14521453
content: vec![ContentItem::InputText {
14531454
text: budget_limit_prompt(goal),
14541455
}],
1456+
phase: None,
14551457
}
14561458
}
14571459

codex-rs/core/src/session/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,7 @@ Approved action:
12701270
let items = vec![ResponseInputItem::Message {
12711271
role: "developer".to_string(),
12721272
content: vec![ContentItem::InputText { text }],
1273+
phase: None,
12731274
}];
12741275

12751276
if let Err(items) = sess.inject_response_items(items).await {

codex-rs/core/src/session/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,7 @@ impl Session {
16871687
.inject_response_items(vec![ResponseInputItem::Message {
16881688
role: "developer".to_string(),
16891689
content: vec![ContentItem::InputText { text }],
1690+
phase: None,
16901691
}])
16911692
.await
16921693
.is_err()
@@ -1783,6 +1784,7 @@ impl Session {
17831784
.inject_response_items(vec![ResponseInputItem::Message {
17841785
role: "developer".to_string(),
17851786
content: vec![ContentItem::InputText { text }],
1787+
phase: None,
17861788
}])
17871789
.await
17881790
.is_err()

codex-rs/core/src/session/tests.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6250,6 +6250,7 @@ async fn task_finish_emits_turn_item_lifecycle_for_leftover_pending_user_input()
62506250
content: vec![ContentItem::InputText {
62516251
text: "late pending input".to_string(),
62526252
}],
6253+
phase: None,
62536254
}])
62546255
.await
62556256
.expect("inject pending input into active turn");
@@ -6495,18 +6496,21 @@ async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() {
64956496
content: vec![ContentItem::InputText {
64966497
text: "blocked queued prompt".to_string(),
64976498
}],
6499+
phase: None,
64986500
};
64996501
let later = ResponseInputItem::Message {
65006502
role: "user".to_string(),
65016503
content: vec![ContentItem::InputText {
65026504
text: "later queued prompt".to_string(),
65036505
}],
6506+
phase: None,
65046507
};
65056508
let newer = ResponseInputItem::Message {
65066509
role: "user".to_string(),
65076510
content: vec![ContentItem::InputText {
65086511
text: "newer queued prompt".to_string(),
65096512
}],
6513+
phase: None,
65106514
};
65116515

65126516
sess.inject_response_items(vec![blocked.clone(), later.clone()])
@@ -6537,6 +6541,7 @@ async fn queued_response_items_for_next_turn_move_into_next_active_turn() {
65376541
content: vec![ContentItem::InputText {
65386542
text: "queued before wake".to_string(),
65396543
}],
6544+
phase: None,
65406545
};
65416546

65426547
sess.queue_response_items_for_next_turn(vec![queued_item.clone()])
@@ -6563,6 +6568,7 @@ async fn idle_interrupt_does_not_wake_queued_next_turn_items() {
65636568
content: vec![ContentItem::InputText {
65646569
text: "queued before interrupt".to_string(),
65656570
}],
6571+
phase: None,
65666572
};
65676573

65686574
sess.queue_response_items_for_next_turn(vec![queued_item])
@@ -6582,6 +6588,7 @@ async fn abort_empty_active_turn_preserves_pending_input() {
65826588
content: vec![ContentItem::InputText {
65836589
text: "late pending input".to_string(),
65846590
}],
6591+
phase: None,
65856592
};
65866593
let turn_state = {
65876594
let mut active = sess.active_turn.lock().await;
@@ -6787,7 +6794,7 @@ async fn budget_limited_accounting_steers_active_turn_without_aborting() -> anyh
67876794
.await?;
67886795

67896796
let pending_input = sess.get_pending_input().await;
6790-
let [ResponseInputItem::Message { role, content }] = pending_input.as_slice() else {
6797+
let [ResponseInputItem::Message { role, content, .. }] = pending_input.as_slice() else {
67916798
panic!("expected one budget-limit steering message, got {pending_input:#?}");
67926799
};
67936800
assert_eq!("developer", role);

codex-rs/core/src/tasks/user_shell.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,16 @@ async fn persist_user_shell_output(
354354
}
355355

356356
let response_input_item = match output_item {
357-
ResponseItem::Message { role, content, .. } => ResponseInputItem::Message { role, content },
357+
ResponseItem::Message {
358+
role,
359+
content,
360+
phase,
361+
..
362+
} => ResponseInputItem::Message {
363+
role,
364+
content,
365+
phase,
366+
},
358367
_ => unreachable!("user shell command output record should always be a message"),
359368
};
360369

codex-rs/protocol/src/models.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,9 @@ pub enum ResponseInputItem {
675675
Message {
676676
role: String,
677677
content: Vec<ContentItem>,
678+
#[serde(default, skip_serializing_if = "Option::is_none")]
679+
#[ts(optional)]
680+
phase: Option<MessagePhase>,
678681
},
679682
FunctionCallOutput {
680683
call_id: String,
@@ -1106,11 +1109,15 @@ pub fn local_image_content_items_with_label_number(
11061109
impl From<ResponseInputItem> for ResponseItem {
11071110
fn from(item: ResponseInputItem) -> Self {
11081111
match item {
1109-
ResponseInputItem::Message { role, content } => Self::Message {
1112+
ResponseInputItem::Message {
1113+
role,
1114+
content,
1115+
phase,
1116+
} => Self::Message {
11101117
role,
11111118
content,
11121119
id: None,
1113-
phase: None,
1120+
phase,
11141121
},
11151122
ResponseInputItem::FunctionCallOutput { call_id, output } => {
11161123
Self::FunctionCallOutput { call_id, output }
@@ -1248,6 +1255,7 @@ impl From<Vec<UserInput>> for ResponseInputItem {
12481255
UserInput::Skill { .. } | UserInput::Mention { .. } => Vec::new(), // Tool bodies are injected later in core
12491256
})
12501257
.collect::<Vec<ContentItem>>(),
1258+
phase: None,
12511259
}
12521260
}
12531261
}
@@ -1652,6 +1660,29 @@ mod tests {
16521660
use pretty_assertions::assert_eq;
16531661
use tempfile::tempdir;
16541662

1663+
#[test]
1664+
fn response_input_message_conversion_preserves_phase() {
1665+
let item = ResponseItem::from(ResponseInputItem::Message {
1666+
role: "assistant".to_string(),
1667+
content: vec![ContentItem::OutputText {
1668+
text: "still working".to_string(),
1669+
}],
1670+
phase: Some(MessagePhase::Commentary),
1671+
});
1672+
1673+
assert_eq!(
1674+
item,
1675+
ResponseItem::Message {
1676+
id: None,
1677+
role: "assistant".to_string(),
1678+
content: vec![ContentItem::OutputText {
1679+
text: "still working".to_string(),
1680+
}],
1681+
phase: Some(MessagePhase::Commentary),
1682+
}
1683+
);
1684+
}
1685+
16551686
#[test]
16561687
fn sandbox_permissions_helpers_match_documented_semantics() {
16571688
let cases = [

codex-rs/protocol/src/protocol.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,7 @@ impl InterAgentCommunication {
858858
content: vec![ContentItem::OutputText {
859859
text: serde_json::to_string(self).unwrap_or_default(),
860860
}],
861+
phase: Some(MessagePhase::Commentary),
861862
}
862863
}
863864

0 commit comments

Comments
 (0)