mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Add async delivery metadata to agent messages (#39312)
## What changed - Add an optional `delivery` field to agent message events and app-server `agentMessage` items, with `"async"` identifying a user-visible message sent without ending the current turn. - Preserve the marker through legacy event conversion, thread history materialization, replay, and generated JSON and TypeScript schemas. - Require delivery of async agent message completion notifications on the in-process app-server transport. ## Testing - Cover preservation of async delivery metadata in thread history. - Cover delivery-required classification for async agent messages. GitOrigin-RevId: f92ba25de1293ee271404badfdf0d117d6530329
This commit is contained in:
@@ -28,7 +28,7 @@ use crate::protocol::v2::WebSearchItem;
|
||||
use crate::protocol::v2::web_search_action_from_core;
|
||||
use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
@@ -331,11 +331,7 @@ impl ThreadHistoryBuilder {
|
||||
pub fn handle_event(&mut self, event: &EventMsg) {
|
||||
match event {
|
||||
EventMsg::UserMessage(payload) => self.handle_user_message(payload),
|
||||
EventMsg::AgentMessage(payload) => self.handle_agent_message(
|
||||
payload.message.clone(),
|
||||
payload.phase.clone(),
|
||||
payload.memory_citation.clone().map(Into::into),
|
||||
),
|
||||
EventMsg::AgentMessage(payload) => self.handle_agent_message(payload),
|
||||
EventMsg::AgentReasoning(payload) => self.handle_agent_reasoning(payload),
|
||||
EventMsg::AgentReasoningRawContent(payload) => {
|
||||
self.handle_agent_reasoning_raw_content(payload)
|
||||
@@ -491,22 +487,18 @@ impl ThreadHistoryBuilder {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_agent_message(
|
||||
&mut self,
|
||||
text: String,
|
||||
phase: Option<MessagePhase>,
|
||||
memory_citation: Option<crate::protocol::v2::MemoryCitation>,
|
||||
) {
|
||||
if text.is_empty() {
|
||||
fn handle_agent_message(&mut self, payload: &AgentMessageEvent) {
|
||||
if payload.message.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::AgentMessage {
|
||||
id,
|
||||
text,
|
||||
phase,
|
||||
memory_citation,
|
||||
text: payload.message.clone(),
|
||||
phase: payload.phase.clone(),
|
||||
memory_citation: payload.memory_citation.clone().map(Into::into),
|
||||
delivery: payload.delivery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1628,6 +1620,7 @@ impl From<&PendingTurn> for Turn {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::AgentMessageDelivery;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
use codex_extension_items::sleep::SleepItem as CoreSleepItem;
|
||||
@@ -1646,7 +1639,6 @@ mod tests {
|
||||
use codex_protocol::models::MessagePhase as CoreMessagePhase;
|
||||
use codex_protocol::models::WebSearchAction as CoreWebSearchAction;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::AgentMessageEvent;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
use codex_protocol::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
@@ -1693,6 +1685,7 @@ mod tests {
|
||||
message: "Hi there".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "thinking".into(),
|
||||
@@ -1712,6 +1705,7 @@ mod tests {
|
||||
message: "Reply two".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1750,6 +1744,7 @@ mod tests {
|
||||
text: "Hi there".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1783,6 +1778,7 @@ mod tests {
|
||||
text: "Reply two".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2330,11 +2326,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_agent_message_phase_in_history() {
|
||||
fn preserves_agent_message_phase_and_delivery_in_history() {
|
||||
let events = vec![EventMsg::AgentMessage(AgentMessageEvent {
|
||||
message: "Final reply".into(),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
})];
|
||||
|
||||
let items = events
|
||||
@@ -2348,8 +2345,9 @@ mod tests {
|
||||
ThreadItem::AgentMessage {
|
||||
id: "item-1".into(),
|
||||
text: "Final reply".into(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
phase: Some(CoreMessagePhase::FinalAnswer),
|
||||
memory_citation: None,
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2458,6 +2456,7 @@ mod tests {
|
||||
message: "interlude".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::AgentReasoning(AgentReasoningEvent {
|
||||
text: "second summary".into(),
|
||||
@@ -2506,6 +2505,7 @@ mod tests {
|
||||
message: "Working...".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
@@ -2526,6 +2526,7 @@ mod tests {
|
||||
message: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2557,6 +2558,7 @@ mod tests {
|
||||
text: "Working...".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2581,6 +2583,7 @@ mod tests {
|
||||
text: "Second attempt complete.".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2600,6 +2603,7 @@ mod tests {
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2613,6 +2617,7 @@ mod tests {
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 1 }),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
@@ -2627,6 +2632,7 @@ mod tests {
|
||||
message: "A3".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -2657,6 +2663,7 @@ mod tests {
|
||||
text: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2676,6 +2683,7 @@ mod tests {
|
||||
text: "A3".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -2696,6 +2704,7 @@ mod tests {
|
||||
message: "A1".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
client_id: None,
|
||||
@@ -2709,6 +2718,7 @@ mod tests {
|
||||
message: "A2".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::ThreadRolledBack(ThreadRolledBackEvent { num_turns: 99 }),
|
||||
];
|
||||
@@ -3741,6 +3751,7 @@ mod tests {
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-b".into(),
|
||||
@@ -3917,6 +3928,7 @@ mod tests {
|
||||
message: "still in b".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4180,6 +4192,7 @@ mod tests {
|
||||
message: "done".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message: "rollback failed".into(),
|
||||
|
||||
@@ -122,6 +122,7 @@ fn projects_completed_canonical_turn_items() {
|
||||
}],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
let user_changes = project(item_completed(thread_id, "turn-1", user_item.clone()));
|
||||
|
||||
@@ -24,6 +24,7 @@ use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmen
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
pub use codex_protocol::items::AgentMessageDelivery;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus as CoreCollabAgentToolCallStatus;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
@@ -250,6 +251,8 @@ pub enum ThreadItem {
|
||||
phase: Option<MessagePhase>,
|
||||
#[serde(default)]
|
||||
memory_citation: Option<MemoryCitation>,
|
||||
#[serde(default)]
|
||||
delivery: Option<AgentMessageDelivery>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
@@ -833,6 +836,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
text,
|
||||
phase: agent.phase,
|
||||
memory_citation: agent.memory_citation.map(Into::into),
|
||||
delivery: agent.delivery,
|
||||
}
|
||||
}
|
||||
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
|
||||
|
||||
@@ -2942,6 +2942,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
],
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -2951,6 +2952,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
text: "Hello world".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2969,6 +2971,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}],
|
||||
rollout_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
@@ -2986,6 +2989,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}],
|
||||
thread_ids: vec!["rollout-1".to_string()],
|
||||
}),
|
||||
delivery: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user