mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Expose lifecycle timestamps on app-server thread items (#57172)
## What changed - Add nullable `startedAtMs` and `completedAtMs` fields to `ThreadItem` in live notifications and turn history. Values are Unix milliseconds when recorded. - Preserve recorded item times through history reconstruction, pagination, and resume, including command start times recorded during approval. - Separate the Rust item payload into `ThreadItemPayload` while keeping the JSON payload flat, and update consumers, schemas, and generated TypeScript and Python types. ## Testing Extend coverage for timestamp serialization, history reconstruction, paginated reads and resume, and persisted Python SDK items. GitOrigin-RevId: ecbcee95af52db7c807b46a6e9deb30e5fba1f80
This commit is contained in:
@@ -436,22 +436,29 @@ impl AnalyticsEventsClient {
|
||||
completed_at_ms: i64,
|
||||
) {
|
||||
let thread_id = item.sender_thread_id.to_string();
|
||||
let completed_item = TurnItem::CollabAgentToolCall(item.clone()).into();
|
||||
let completed_item = codex_app_server_protocol::ThreadItemPayload::from(
|
||||
TurnItem::CollabAgentToolCall(item.clone()),
|
||||
);
|
||||
item.status = CollabAgentToolCallStatus::InProgress;
|
||||
|
||||
self.track_notification(&ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: TurnItem::CollabAgentToolCall(item).into(),
|
||||
thread_id: thread_id.clone(),
|
||||
turn_id: turn_id.clone(),
|
||||
started_at_ms,
|
||||
}));
|
||||
self.track_notification(&ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
thread_id.clone(),
|
||||
turn_id.clone(),
|
||||
codex_app_server_protocol::ThreadItemPayload::from(TurnItem::CollabAgentToolCall(
|
||||
item,
|
||||
)),
|
||||
started_at_ms,
|
||||
),
|
||||
));
|
||||
self.track_notification(&ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: completed_item,
|
||||
ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
completed_item,
|
||||
Some(started_at_ms),
|
||||
completed_at_ms,
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -935,7 +942,7 @@ fn session_event_to_analytics_notification(
|
||||
match ¬ification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification { item, .. })
|
||||
| ServerNotification::ItemCompleted(ItemCompletedNotification { item, .. }) => {
|
||||
tracked_tool_item_id(item)?;
|
||||
tracked_tool_item_id(&item.payload)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_login::AuthManager;
|
||||
use codex_protocol::ThreadId;
|
||||
@@ -238,11 +238,10 @@ async fn buffered_tool_events_preserve_attribution_or_drop_it_on_queue_overflow(
|
||||
turn: started.turn,
|
||||
}));
|
||||
// Collaborator completions wait in the reducer for subsequent sampling evidence.
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 2,
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
let completed = ItemCompletedNotification::new(
|
||||
thread_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "item".to_string(),
|
||||
tool: CollabAgentTool::SendMessage,
|
||||
status: CollabAgentToolCallStatus::Failed,
|
||||
@@ -253,13 +252,17 @@ async fn buffered_tool_events_preserve_attribution_or_drop_it_on_queue_overflow(
|
||||
reasoning_effort: None,
|
||||
agents_states: Default::default(),
|
||||
},
|
||||
};
|
||||
client.track_notification(&ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1,
|
||||
item: completed.item.clone(),
|
||||
}));
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 2,
|
||||
);
|
||||
client.track_notification(&ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
thread_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
completed.item.payload.clone(),
|
||||
/*started_at_ms*/ 1,
|
||||
),
|
||||
));
|
||||
client.track_notification(&ServerNotification::ItemCompleted(completed));
|
||||
let measurement = PluginMeasurementsInput {
|
||||
thread_id: thread_id.to_string(),
|
||||
|
||||
@@ -154,7 +154,7 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ServerResponse;
|
||||
use codex_app_server_protocol::SubAgentActivityKind;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_app_server_protocol::WebSearchAction;
|
||||
@@ -535,36 +535,36 @@ struct TurnToolCounts {
|
||||
}
|
||||
|
||||
impl TurnToolCounts {
|
||||
fn record(&mut self, item: &ThreadItem) {
|
||||
fn record(&mut self, item: &ThreadItemPayload) {
|
||||
match item {
|
||||
ThreadItem::CommandExecution { .. } => self.shell_command += 1,
|
||||
ThreadItem::FileChange { .. } => self.file_change += 1,
|
||||
ThreadItem::McpToolCall { .. } => self.mcp_tool_call += 1,
|
||||
ThreadItem::DynamicToolCall { .. } => self.dynamic_tool_call += 1,
|
||||
ThreadItem::SubAgentActivity {
|
||||
ThreadItemPayload::CommandExecution { .. } => self.shell_command += 1,
|
||||
ThreadItemPayload::FileChange { .. } => self.file_change += 1,
|
||||
ThreadItemPayload::McpToolCall { .. } => self.mcp_tool_call += 1,
|
||||
ThreadItemPayload::DynamicToolCall { .. } => self.dynamic_tool_call += 1,
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
kind: SubAgentActivityKind::Completed,
|
||||
..
|
||||
} => return,
|
||||
ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::SubAgentActivity { id, .. } => {
|
||||
ThreadItemPayload::CollabAgentToolCall { id, .. }
|
||||
| ThreadItemPayload::SubAgentActivity { id, .. } => {
|
||||
if !self.subagent_tool_call_ids.insert(id.clone()) {
|
||||
return;
|
||||
}
|
||||
self.subagent_tool_call += 1;
|
||||
}
|
||||
ThreadItem::WebSearch(_) => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration(_) => self.image_generation += 1,
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
| ThreadItem::FunctionCallOutput { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::Sleep(_)
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
| ThreadItem::ContextCompaction { .. } => return,
|
||||
ThreadItemPayload::WebSearch(_) => self.web_search += 1,
|
||||
ThreadItemPayload::ImageGeneration(_) => self.image_generation += 1,
|
||||
ThreadItemPayload::UserMessage { .. }
|
||||
| ThreadItemPayload::HookPrompt { .. }
|
||||
| ThreadItemPayload::AgentMessage { .. }
|
||||
| ThreadItemPayload::FunctionCallOutput { .. }
|
||||
| ThreadItemPayload::Plan { .. }
|
||||
| ThreadItemPayload::Reasoning { .. }
|
||||
| ThreadItemPayload::ImageView { .. }
|
||||
| ThreadItemPayload::Sleep(_)
|
||||
| ThreadItemPayload::EnteredReviewMode { .. }
|
||||
| ThreadItemPayload::ExitedReviewMode { .. }
|
||||
| ThreadItemPayload::ContextCompaction { .. } => return,
|
||||
}
|
||||
self.total += 1;
|
||||
}
|
||||
@@ -2060,15 +2060,17 @@ impl AnalyticsReducer {
|
||||
}));
|
||||
}
|
||||
ServerNotification::ItemStarted(notification) => {
|
||||
let Some(item_id) = tracked_tool_item_id(¬ification.item) else {
|
||||
let Some(item_id) = tracked_tool_item_id(¬ification.item.payload) else {
|
||||
return;
|
||||
};
|
||||
let Some(started_at_ms) = option_i64_to_u64(Some(notification.started_at_ms))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let model_context = match ¬ification.item {
|
||||
ThreadItem::CommandExecution { model_context, .. } => model_context.clone(),
|
||||
let model_context = match ¬ification.item.payload {
|
||||
ThreadItemPayload::CommandExecution { model_context, .. } => {
|
||||
model_context.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
self.turn_state(notification.turn_id.clone())
|
||||
@@ -2079,7 +2081,7 @@ impl AnalyticsReducer {
|
||||
turn_id: notification.turn_id.clone(),
|
||||
item_id: item_id.to_string(),
|
||||
};
|
||||
let cua_identity = cua_mcp_tool_identity(¬ification.item);
|
||||
let cua_identity = cua_mcp_tool_identity(¬ification.item.payload);
|
||||
if cua_identity.is_some()
|
||||
&& self
|
||||
.turns
|
||||
@@ -2148,7 +2150,10 @@ impl AnalyticsReducer {
|
||||
));
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
if matches!(notification.item, ThreadItem::SubAgentActivity { .. }) {
|
||||
if matches!(
|
||||
notification.item.payload,
|
||||
ThreadItemPayload::SubAgentActivity { .. }
|
||||
) {
|
||||
let Some(turn_state) = self.turns.get_mut(¬ification.turn_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %notification.thread_id,
|
||||
@@ -2157,10 +2162,10 @@ impl AnalyticsReducer {
|
||||
);
|
||||
return;
|
||||
};
|
||||
turn_state.tool_counts.record(¬ification.item);
|
||||
turn_state.tool_counts.record(¬ification.item.payload);
|
||||
return;
|
||||
}
|
||||
let Some(item_id) = tracked_tool_item_id(¬ification.item) else {
|
||||
let Some(item_id) = tracked_tool_item_id(¬ification.item.payload) else {
|
||||
return;
|
||||
};
|
||||
let key = ToolItemKey {
|
||||
@@ -2168,7 +2173,7 @@ impl AnalyticsReducer {
|
||||
turn_id: notification.turn_id.clone(),
|
||||
item_id: item_id.to_string(),
|
||||
};
|
||||
let is_cua = cua_mcp_tool_identity(¬ification.item).is_some();
|
||||
let is_cua = cua_mcp_tool_identity(¬ification.item.payload).is_some();
|
||||
if is_cua
|
||||
&& self
|
||||
.turns
|
||||
@@ -2177,18 +2182,20 @@ impl AnalyticsReducer {
|
||||
{
|
||||
return;
|
||||
}
|
||||
let elicitation_type =
|
||||
if matches!(notification.item, ThreadItem::McpToolCall { .. }) {
|
||||
// Producers enqueue known classifications before completion on
|
||||
// the same analytics queue, so completed keys need no history.
|
||||
self.pending_mcp_tool_elicitations
|
||||
.iter()
|
||||
.position(|(pending, _)| pending == &key)
|
||||
.and_then(|index| self.pending_mcp_tool_elicitations.remove(index))
|
||||
.map(|(_, elicitation_type)| elicitation_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let elicitation_type = if matches!(
|
||||
notification.item.payload,
|
||||
ThreadItemPayload::McpToolCall { .. }
|
||||
) {
|
||||
// Producers enqueue known classifications before completion on
|
||||
// the same analytics queue, so completed keys need no history.
|
||||
self.pending_mcp_tool_elicitations
|
||||
.iter()
|
||||
.position(|(pending, _)| pending == &key)
|
||||
.and_then(|index| self.pending_mcp_tool_elicitations.remove(index))
|
||||
.map(|(_, elicitation_type)| elicitation_type)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let Some(turn_state) = self.turns.get_mut(¬ification.turn_id) else {
|
||||
tracing::warn!(
|
||||
thread_id = %notification.thread_id,
|
||||
@@ -2198,7 +2205,7 @@ impl AnalyticsReducer {
|
||||
);
|
||||
return;
|
||||
};
|
||||
turn_state.tool_counts.record(¬ification.item);
|
||||
turn_state.tool_counts.record(¬ification.item.payload);
|
||||
let Some((started_at_ms, model_context)) =
|
||||
self.tool_items_started_at_ms.remove(&key)
|
||||
else {
|
||||
@@ -2222,7 +2229,7 @@ impl AnalyticsReducer {
|
||||
if let Some(mut event) = tool_item_event(ToolItemEventInput {
|
||||
thread_id: ¬ification.thread_id,
|
||||
turn_id: ¬ification.turn_id,
|
||||
item: ¬ification.item,
|
||||
item: ¬ification.item.payload,
|
||||
model_context: model_context.as_ref(),
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
@@ -2245,12 +2252,14 @@ impl AnalyticsReducer {
|
||||
.and_then(|config| config.turn_metadata.root_turn_id());
|
||||
// Fast collaborator tools can complete before their sampling response.
|
||||
// Keep the event until that response can supply its originating ID.
|
||||
let emission =
|
||||
if matches!(¬ification.item, ThreadItem::CollabAgentToolCall { .. }) {
|
||||
ToolEventEmission::AwaitResponse
|
||||
} else {
|
||||
ToolEventEmission::ImmediateUnlessCorrelated
|
||||
};
|
||||
let emission = if matches!(
|
||||
¬ification.item.payload,
|
||||
ThreadItemPayload::CollabAgentToolCall { .. }
|
||||
) {
|
||||
ToolEventEmission::AwaitResponse
|
||||
} else {
|
||||
ToolEventEmission::ImmediateUnlessCorrelated
|
||||
};
|
||||
self.record_tool_event(
|
||||
¬ification.thread_id,
|
||||
¬ification.turn_id,
|
||||
@@ -2903,32 +2912,34 @@ fn warn_missing_analytics_context(
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> {
|
||||
pub(crate) fn tracked_tool_item_id(item: &ThreadItemPayload) -> Option<&str> {
|
||||
match item {
|
||||
ThreadItem::CommandExecution { id, .. }
|
||||
| ThreadItem::FileChange { id, .. }
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. } => Some(id),
|
||||
ThreadItem::WebSearch(item) => Some(&item.id),
|
||||
ThreadItem::ImageGeneration(item) => Some(&item.id),
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
| ThreadItem::FunctionCallOutput { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::SubAgentActivity { .. }
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::Sleep(_)
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
| ThreadItem::ContextCompaction { .. } => None,
|
||||
ThreadItemPayload::CommandExecution { id, .. }
|
||||
| ThreadItemPayload::FileChange { id, .. }
|
||||
| ThreadItemPayload::McpToolCall { id, .. }
|
||||
| ThreadItemPayload::DynamicToolCall { id, .. }
|
||||
| ThreadItemPayload::CollabAgentToolCall { id, .. } => Some(id),
|
||||
ThreadItemPayload::WebSearch(item) => Some(&item.id),
|
||||
ThreadItemPayload::ImageGeneration(item) => Some(&item.id),
|
||||
ThreadItemPayload::UserMessage { .. }
|
||||
| ThreadItemPayload::HookPrompt { .. }
|
||||
| ThreadItemPayload::AgentMessage { .. }
|
||||
| ThreadItemPayload::FunctionCallOutput { .. }
|
||||
| ThreadItemPayload::Plan { .. }
|
||||
| ThreadItemPayload::Reasoning { .. }
|
||||
| ThreadItemPayload::SubAgentActivity { .. }
|
||||
| ThreadItemPayload::ImageView { .. }
|
||||
| ThreadItemPayload::Sleep(_)
|
||||
| ThreadItemPayload::EnteredReviewMode { .. }
|
||||
| ThreadItemPayload::ExitedReviewMode { .. }
|
||||
| ThreadItemPayload::ContextCompaction { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cua_mcp_tool_identity(item: &ThreadItem) -> Option<(&str, &str, Option<&str>, Option<&str>)> {
|
||||
let ThreadItem::McpToolCall {
|
||||
fn cua_mcp_tool_identity(
|
||||
item: &ThreadItemPayload,
|
||||
) -> Option<(&str, &str, Option<&str>, Option<&str>)> {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
server,
|
||||
tool,
|
||||
plugin_id,
|
||||
@@ -3037,7 +3048,7 @@ fn item_review_summary_key(pending_review: &PendingReviewState) -> Option<ToolIt
|
||||
struct ToolItemEventInput<'a> {
|
||||
thread_id: &'a str,
|
||||
turn_id: &'a str,
|
||||
item: &'a ThreadItem,
|
||||
item: &'a ThreadItemPayload,
|
||||
model_context: Option<&'a ModelInvocationContext>,
|
||||
started_at_ms: u64,
|
||||
completed_at_ms: u64,
|
||||
@@ -3063,7 +3074,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
elicitation_type,
|
||||
} = input;
|
||||
match item {
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
sandbox_type,
|
||||
plugin_id,
|
||||
@@ -3122,7 +3133,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::FileChange {
|
||||
ThreadItemPayload::FileChange {
|
||||
id,
|
||||
changes,
|
||||
status,
|
||||
@@ -3160,7 +3171,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
}))
|
||||
}
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id,
|
||||
server,
|
||||
tool,
|
||||
@@ -3209,7 +3220,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::DynamicToolCall {
|
||||
ThreadItemPayload::DynamicToolCall {
|
||||
id,
|
||||
tool,
|
||||
status,
|
||||
@@ -3256,7 +3267,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id,
|
||||
tool,
|
||||
status,
|
||||
@@ -3323,7 +3334,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::WebSearch(item) => {
|
||||
ThreadItemPayload::WebSearch(item) => {
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
turn_id,
|
||||
@@ -3353,7 +3364,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
}))
|
||||
}
|
||||
ThreadItem::ImageGeneration(item) => {
|
||||
ThreadItemPayload::ImageGeneration(item) => {
|
||||
let (terminal_status, failure_kind) = image_generation_outcome(item.status.as_str());
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
@@ -4235,11 +4246,10 @@ mod tests {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: keys[2].thread_id.clone(),
|
||||
turn_id: keys[2].turn_id.clone(),
|
||||
completed_at_ms: 1_000,
|
||||
item: ThreadItem::McpToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
keys[2].thread_id.clone(),
|
||||
keys[2].turn_id.clone(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: keys[2].item_id.clone(),
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
@@ -4254,7 +4264,9 @@ mod tests {
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
|
||||
@@ -308,16 +308,16 @@ async fn completed_background_tool_item_emits_after_turn_event() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at_ms: 998,
|
||||
item: sample_command_execution_item(
|
||||
ItemStartedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ 998,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
@@ -343,16 +343,17 @@ async fn completed_background_tool_item_emits_after_turn_event() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
item: sample_command_execution_item(
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
sample_command_execution_item(
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(1),
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
@@ -430,16 +431,17 @@ async fn item_completed_without_turn_state_does_not_create_turn_state() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
item: sample_command_execution_item(
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
sample_command_execution_item(
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(1),
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
|
||||
@@ -830,16 +830,16 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-subagent".to_string(),
|
||||
turn_id: "turn-subagent".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
item: sample_command_execution_item(
|
||||
ItemStartedNotification::new(
|
||||
"thread-subagent".to_string(),
|
||||
"turn-subagent".to_string(),
|
||||
sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -847,16 +847,17 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-subagent".to_string(),
|
||||
turn_id: "turn-subagent".to_string(),
|
||||
completed_at_ms: 1_042,
|
||||
item: sample_command_execution_item(
|
||||
ItemCompletedNotification::new(
|
||||
"thread-subagent".to_string(),
|
||||
"turn-subagent".to_string(),
|
||||
sample_command_execution_item(
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(42),
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_042,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::McpToolCallAppContext;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
use codex_protocol::protocol::ThreadSource;
|
||||
use codex_protocol::sandbox::SandboxType;
|
||||
@@ -64,9 +64,9 @@ fn sample_command_execution_item_with_actions(
|
||||
plugin_id: Option<&str>,
|
||||
script_path: Option<&str>,
|
||||
sandbox_type: Option<SandboxType>,
|
||||
) -> ThreadItem {
|
||||
) -> ThreadItemPayload {
|
||||
let mut item = sample_command_execution_item(status, exit_code, duration_ms);
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
command_actions: item_command_actions,
|
||||
plugin_id: item_plugin_id,
|
||||
script_path: item_script_path,
|
||||
@@ -233,7 +233,7 @@ async fn cua_tool_call_lifecycle_requires_a_start_and_deduplicates_replay() {
|
||||
.await;
|
||||
events.clear();
|
||||
|
||||
let item = |status| ThreadItem::McpToolCall {
|
||||
let item = |status| ThreadItemPayload::McpToolCall {
|
||||
id: "cua-call".into(),
|
||||
server: "codex_apps".into(),
|
||||
tool: "js".into(),
|
||||
@@ -254,18 +254,19 @@ async fn cua_tool_call_lifecycle_requires_a_start_and_deduplicates_replay() {
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
let started = ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: "thread-2".into(),
|
||||
turn_id: "turn-2".into(),
|
||||
started_at_ms: 1_000,
|
||||
item: item(McpToolCallStatus::InProgress),
|
||||
});
|
||||
let completed = ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: "thread-2".into(),
|
||||
turn_id: "turn-2".into(),
|
||||
completed_at_ms: 1_100,
|
||||
item: item(McpToolCallStatus::Completed),
|
||||
});
|
||||
let started = ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
"thread-2".into(),
|
||||
"turn-2".into(),
|
||||
item(McpToolCallStatus::InProgress),
|
||||
/*started_at_ms*/ 1_000,
|
||||
));
|
||||
let completed = ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
"thread-2".into(),
|
||||
"turn-2".into(),
|
||||
item(McpToolCallStatus::Completed),
|
||||
Some(1_000),
|
||||
/*completed_at_ms*/ 1_100,
|
||||
));
|
||||
for notification in [
|
||||
completed.clone(), // Missing start must not poison later deduplication.
|
||||
started.clone(),
|
||||
@@ -330,17 +331,18 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
item: {
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
{
|
||||
let mut item = sample_command_execution_item(
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
);
|
||||
if let ThreadItem::CommandExecution { model_context, .. } = &mut item {
|
||||
if let ThreadItemPayload::CommandExecution { model_context, .. } =
|
||||
&mut item
|
||||
{
|
||||
*model_context =
|
||||
Some(codex_protocol::items::ModelInvocationContext {
|
||||
model_slug: model.to_string(),
|
||||
@@ -349,7 +351,8 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
}
|
||||
item
|
||||
},
|
||||
},
|
||||
/*started_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -363,11 +366,10 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 1_045,
|
||||
item: sample_command_execution_item_with_actions(
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
sample_command_execution_item_with_actions(
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(42),
|
||||
@@ -394,7 +396,9 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
|
||||
Some("scripts/run.py"),
|
||||
Some(SandboxType::WindowsMxc),
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_045,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -466,7 +470,7 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "call-1".into(),
|
||||
tool: CollabAgentTool::SendMessage,
|
||||
status: CollabAgentToolCallStatus::Failed,
|
||||
@@ -480,12 +484,12 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
started_at_ms: 1_000,
|
||||
item: item.clone(),
|
||||
},
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".into(),
|
||||
"turn-1".into(),
|
||||
item.clone(),
|
||||
/*started_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -494,12 +498,13 @@ async fn collaborator_tool_events_keep_response_ids_when_completion_races_sampli
|
||||
sampling_response("turn-1", "response-1", &["call-1"]),
|
||||
));
|
||||
let completion = AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-1".into(),
|
||||
turn_id: "turn-1".into(),
|
||||
completed_at_ms: 1_010,
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".into(),
|
||||
"turn-1".into(),
|
||||
item,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_010,
|
||||
),
|
||||
)));
|
||||
let facts = if response_first {
|
||||
[response, completion]
|
||||
@@ -785,7 +790,7 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
|
||||
("retry", "calendar", None),
|
||||
("denied", "drive", Some(ElicitationType::Approval)),
|
||||
] {
|
||||
let item = ThreadItem::McpToolCall {
|
||||
let item = ThreadItemPayload::McpToolCall {
|
||||
id: item_id.to_string(),
|
||||
server: "server".to_string(),
|
||||
tool: "search".to_string(),
|
||||
@@ -809,12 +814,12 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at_ms: 998,
|
||||
item: item.clone(),
|
||||
},
|
||||
ItemStartedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item.clone(),
|
||||
/*started_at_ms*/ 998,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -851,12 +856,13 @@ async fn mcp_elicitation_classification_survives_turn_completion_and_preserves_c
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
@@ -966,7 +972,7 @@ async fn image_generation_events_preserve_transparent_background_metadata() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
let item = ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: status.to_string(),
|
||||
revised_prompt: None,
|
||||
@@ -984,12 +990,12 @@ async fn image_generation_events_preserve_transparent_background_metadata() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at_ms: 998,
|
||||
item: item.clone(),
|
||||
},
|
||||
ItemStartedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item.clone(),
|
||||
/*started_at_ms*/ 998,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
@@ -997,12 +1003,13 @@ async fn image_generation_events_preserve_transparent_background_metadata() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::SubAgentActivityKind;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadRealtimeStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
use codex_app_server_protocol::WebSearchItem;
|
||||
@@ -474,7 +474,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mcp_tool_call_item = |status, duration_ms| ThreadItem::McpToolCall {
|
||||
let mcp_tool_call_item = |status, duration_ms| ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "server".to_string(),
|
||||
tool: "search".to_string(),
|
||||
@@ -497,13 +497,13 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
};
|
||||
let completed_tool_items = vec![
|
||||
sample_command_execution_item(CommandExecutionStatus::Completed, Some(0), Some(1)),
|
||||
ThreadItem::FileChange {
|
||||
ThreadItemPayload::FileChange {
|
||||
id: "file-change-1".to_string(),
|
||||
changes: Vec::new(),
|
||||
status: PatchApplyStatus::Completed,
|
||||
},
|
||||
mcp_tool_call_item(McpToolCallStatus::Completed, Some(2)),
|
||||
ThreadItem::DynamicToolCall {
|
||||
ThreadItemPayload::DynamicToolCall {
|
||||
id: "dynamic-1".to_string(),
|
||||
// A name collision with code-mode exec must not change the event boundary.
|
||||
namespace: Some("custom".to_string()),
|
||||
@@ -514,7 +514,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
success: Some(true),
|
||||
duration_ms: Some(3),
|
||||
},
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "collab-1".to_string(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: CollabAgentToolCallStatus::Completed,
|
||||
@@ -525,7 +525,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
reasoning_effort: None,
|
||||
agents_states: Default::default(),
|
||||
},
|
||||
ThreadItem::SubAgentActivity {
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
id: "sub-agent-activity-1".to_string(),
|
||||
@@ -533,7 +533,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
agent_thread_id: "thread-child".to_string(),
|
||||
agent_path: "/root/child".to_string(),
|
||||
},
|
||||
ThreadItem::SubAgentActivity {
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
id: "sub-agent-activity-completed".to_string(),
|
||||
@@ -541,13 +541,13 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
agent_thread_id: "thread-child".to_string(),
|
||||
agent_path: "/root/child".to_string(),
|
||||
},
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
ThreadItemPayload::WebSearch(WebSearchItem {
|
||||
id: "web-1".to_string(),
|
||||
query: "codex".to_string(),
|
||||
action: None,
|
||||
results: None,
|
||||
}),
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
@@ -567,12 +567,12 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
started_at_ms: 998,
|
||||
item: item.clone(),
|
||||
},
|
||||
ItemStartedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item.clone(),
|
||||
/*started_at_ms*/ 998,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
@@ -583,12 +583,13 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: 1_000,
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
item,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
&mut out,
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ use codex_app_server_protocol::SandboxPolicy as AppServerSandboxPolicy;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::SessionSource as AppServerSessionSource;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadSource as AppServerThreadSource;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
@@ -545,17 +545,17 @@ pub(super) async fn ingest_completed_command_execution_item(
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 1_000,
|
||||
item: sample_command_execution_item_with_id(
|
||||
ItemStartedNotification::new(
|
||||
thread_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
sample_command_execution_item_with_id(
|
||||
item_id,
|
||||
CommandExecutionStatus::InProgress,
|
||||
/*exit_code*/ None,
|
||||
/*duration_ms*/ None,
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ 1_000,
|
||||
),
|
||||
))),
|
||||
events,
|
||||
)
|
||||
@@ -563,17 +563,18 @@ pub(super) async fn ingest_completed_command_execution_item(
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Notification(Box::new(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 1_042,
|
||||
item: sample_command_execution_item_with_id(
|
||||
ItemCompletedNotification::new(
|
||||
thread_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
sample_command_execution_item_with_id(
|
||||
item_id,
|
||||
CommandExecutionStatus::Completed,
|
||||
Some(0),
|
||||
Some(42),
|
||||
),
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 1_042,
|
||||
),
|
||||
))),
|
||||
events,
|
||||
)
|
||||
@@ -640,7 +641,7 @@ pub(super) fn sample_command_execution_item(
|
||||
status: CommandExecutionStatus,
|
||||
exit_code: Option<i32>,
|
||||
duration_ms: Option<i64>,
|
||||
) -> ThreadItem {
|
||||
) -> ThreadItemPayload {
|
||||
sample_command_execution_item_with_id("item-1", status, exit_code, duration_ms)
|
||||
}
|
||||
|
||||
@@ -649,8 +650,8 @@ pub(super) fn sample_command_execution_item_with_id(
|
||||
status: CommandExecutionStatus,
|
||||
exit_code: Option<i32>,
|
||||
duration_ms: Option<i64>,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::CommandExecution {
|
||||
) -> ThreadItemPayload {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: id.to_string(),
|
||||
|
||||
@@ -1134,19 +1134,22 @@ mod tests {
|
||||
}
|
||||
|
||||
fn item_completed_notification(text: &str) -> ServerNotification {
|
||||
ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
|
||||
thread_id: "thread".to_string(),
|
||||
turn_id: "turn".to_string(),
|
||||
completed_at_ms: 0,
|
||||
item: codex_app_server_protocol::ThreadItem::AgentMessage {
|
||||
id: "item".to_string(),
|
||||
text: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
})
|
||||
ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification::new(
|
||||
"thread".to_string(),
|
||||
"turn".to_string(),
|
||||
codex_app_server_protocol::ThreadItemPayload::AgentMessage {
|
||||
id: "item".to_string(),
|
||||
text: text.to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn turn_completed_notification() -> ServerNotification {
|
||||
@@ -1882,8 +1885,8 @@ mod tests {
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification)
|
||||
if matches!(
|
||||
¬ification.item,
|
||||
codex_app_server_protocol::ThreadItem::AgentMessage { text, .. }
|
||||
¬ification.item.payload,
|
||||
codex_app_server_protocol::ThreadItemPayload::AgentMessage { text, .. }
|
||||
if text == "hello"
|
||||
) =>
|
||||
{
|
||||
|
||||
@@ -4643,6 +4643,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -5545,7 +5546,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadNameUpdatedNotification": {
|
||||
"properties": {
|
||||
|
||||
+23
-1
@@ -22316,6 +22316,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -23218,7 +23219,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItemEntry": {
|
||||
"properties": {
|
||||
|
||||
+23
-1
@@ -20030,6 +20030,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -20932,7 +20933,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItemEntry": {
|
||||
"properties": {
|
||||
|
||||
+23
-1
@@ -797,6 +797,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1699,7 +1700,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UserInput": {
|
||||
"oneOf": [
|
||||
|
||||
@@ -797,6 +797,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1699,7 +1700,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UserInput": {
|
||||
"oneOf": [
|
||||
|
||||
@@ -1001,6 +1001,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1903,7 +1904,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
|
||||
@@ -1615,6 +1615,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2517,7 +2518,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
@@ -797,6 +797,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1699,7 +1700,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItemEntry": {
|
||||
"properties": {
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadReadState": {
|
||||
"description": "The first unread position in a durable local thread. A null position means read. The opaque revision guards edits against activity or marks the caller has not seen.",
|
||||
|
||||
+23
-1
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadReadState": {
|
||||
"description": "The first unread position in a durable local thread. A null position means read. The opaque revision guards edits against activity or marks the caller has not seen.",
|
||||
|
||||
@@ -1667,6 +1667,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2569,7 +2570,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
@@ -1615,6 +1615,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2517,7 +2518,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
+23
-1
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
@@ -1001,6 +1001,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1903,7 +1904,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -2308,7 +2309,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSection": {
|
||||
"description": "An independently persisted, user-visible thread section.",
|
||||
|
||||
+23
-1
@@ -1001,6 +1001,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1903,7 +1904,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
|
||||
@@ -1001,6 +1001,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1903,7 +1904,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
|
||||
@@ -1001,6 +1001,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadItem": {
|
||||
"description": "A thread item and its recorded lifecycle times. The payload stays flat on the wire.",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -1903,7 +1904,28 @@
|
||||
"title": "ContextCompactionThreadItem",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
],
|
||||
"properties": {
|
||||
"completedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item completed, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAtMs": {
|
||||
"default": null,
|
||||
"description": "Unix timestamp in milliseconds when the item started, if recorded.",
|
||||
"format": "int64",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Turn": {
|
||||
"properties": {
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+13
-2
@@ -31,7 +31,18 @@ import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
import type { UserInput } from "./UserInput";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array<AsyncUserInputQuestion> | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* A thread item and its recorded lifecycle times. The payload stays flat on the wire.
|
||||
*/
|
||||
export type ThreadItem = {
|
||||
/**
|
||||
* Unix timestamp in milliseconds when the item started, if recorded.
|
||||
*/
|
||||
startedAtMs: number | null,
|
||||
/**
|
||||
* Unix timestamp in milliseconds when the item completed, if recorded.
|
||||
*/
|
||||
completedAtMs: number | null, } & ({ "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, delivery: AgentMessageDelivery | null, questions: Array<AsyncUserInputQuestion> | null, } | { "type": "functionCallOutput", id: string, name: string, namespace: string | null, output: FunctionCallOutputBody, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
* Trusted first-party plugin id when this command resolves to one plugin script.
|
||||
*/
|
||||
@@ -130,4 +141,4 @@ model: string | null,
|
||||
/**
|
||||
* Resolved reasoning effort at sub-agent creation, when known.
|
||||
*/
|
||||
reasoningEffort: ReasoningEffort | null, id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
reasoningEffort: ReasoningEffort | null, id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, });
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::protocol::v2::ReasoningSummaryPartAddedNotification;
|
||||
use crate::protocol::v2::ReasoningSummaryTextDeltaNotification;
|
||||
use crate::protocol::v2::ReasoningTextDeltaNotification;
|
||||
use crate::protocol::v2::TerminalInteractionNotification;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::ThreadItemPayload;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use std::collections::HashMap;
|
||||
@@ -42,7 +42,7 @@ pub fn item_event_to_server_notification(
|
||||
DynamicToolCallStatus::Failed
|
||||
};
|
||||
let duration_ms = i64::try_from(response.duration.as_millis()).ok();
|
||||
let item = ThreadItem::DynamicToolCall {
|
||||
let item = ThreadItemPayload::DynamicToolCall {
|
||||
id: response.call_id,
|
||||
namespace: response.namespace,
|
||||
tool: response.tool,
|
||||
@@ -68,15 +68,16 @@ pub fn item_event_to_server_notification(
|
||||
success: Some(response.success),
|
||||
duration_ms,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id: response.turn_id,
|
||||
response.turn_id,
|
||||
item,
|
||||
completed_at_ms: response.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
response.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabAgentSpawnBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -87,12 +88,12 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: Some(begin_event.reasoning_effort),
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
started_at_ms: begin_event.started_at_ms,
|
||||
})
|
||||
begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabAgentSpawnEnd(end_event) => {
|
||||
let has_receiver = end_event.new_thread_id.is_some();
|
||||
@@ -115,7 +116,7 @@ pub fn item_event_to_server_notification(
|
||||
}
|
||||
None => (Vec::new(), HashMap::new()),
|
||||
};
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status,
|
||||
@@ -126,16 +127,17 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: Some(end_event.reasoning_effort),
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabAgentInteractionBegin(begin_event) => {
|
||||
let receiver_thread_ids = vec![begin_event.receiver_thread_id.to_string()];
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -146,12 +148,12 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
started_at_ms: begin_event.started_at_ms,
|
||||
})
|
||||
begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabAgentInteractionEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
@@ -163,7 +165,7 @@ pub fn item_event_to_server_notification(
|
||||
};
|
||||
let receiver_id = end_event.receiver_thread_id.to_string();
|
||||
let received_status = CollabAgentState::from(end_event.status);
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status,
|
||||
@@ -174,15 +176,16 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states: [(receiver_id, received_status)].into_iter().collect(),
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::SubAgentActivity(activity) => {
|
||||
let item = ThreadItem::SubAgentActivity {
|
||||
let item = ThreadItemPayload::SubAgentActivity {
|
||||
model: activity.model,
|
||||
reasoning_effort: activity.reasoning_effort,
|
||||
id: activity.event_id,
|
||||
@@ -190,12 +193,13 @@ pub fn item_event_to_server_notification(
|
||||
agent_thread_id: activity.agent_thread_id.to_string(),
|
||||
agent_path: String::from(activity.agent_path),
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: activity.occurred_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
activity.occurred_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabWaitingBegin(begin_event) => {
|
||||
let receiver_thread_ids = begin_event
|
||||
@@ -203,7 +207,7 @@ pub fn item_event_to_server_notification(
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::Wait,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -214,12 +218,12 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
started_at_ms: begin_event.started_at_ms,
|
||||
})
|
||||
begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabWaitingEnd(end_event) => {
|
||||
let status = if end_event.statuses.values().any(|status| {
|
||||
@@ -239,7 +243,7 @@ pub fn item_event_to_server_notification(
|
||||
.iter()
|
||||
.map(|(id, status)| (id.to_string(), CollabAgentState::from(status.clone())))
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::Wait,
|
||||
status,
|
||||
@@ -250,15 +254,16 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabCloseBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -269,12 +274,12 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
started_at_ms: begin_event.started_at_ms,
|
||||
})
|
||||
begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabCloseEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
@@ -291,7 +296,7 @@ pub fn item_event_to_server_notification(
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::CloseAgent,
|
||||
status,
|
||||
@@ -302,15 +307,16 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabResumeBegin(begin_event) => {
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: begin_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -321,12 +327,12 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
};
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
started_at_ms: begin_event.started_at_ms,
|
||||
})
|
||||
begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::CollabResumeEnd(end_event) => {
|
||||
let status = match &end_event.status {
|
||||
@@ -343,7 +349,7 @@ pub fn item_event_to_server_notification(
|
||||
)]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let item = ThreadItem::CollabAgentToolCall {
|
||||
let item = ThreadItemPayload::CollabAgentToolCall {
|
||||
id: end_event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status,
|
||||
@@ -354,12 +360,13 @@ pub fn item_event_to_server_notification(
|
||||
reasoning_effort: None,
|
||||
agents_states,
|
||||
};
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item,
|
||||
completed_at_ms: end_event.completed_at_ms,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::AgentMessageContentDelta(event) => {
|
||||
let codex_protocol::protocol::AgentMessageContentDeltaEvent { item_id, delta, .. } =
|
||||
@@ -404,20 +411,21 @@ pub fn item_event_to_server_notification(
|
||||
})
|
||||
}
|
||||
EventMsg::ItemStarted(item_started_event) => {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: item_started_event.item.into(),
|
||||
started_at_ms: item_started_event.started_at_ms,
|
||||
})
|
||||
ThreadItemPayload::from(item_started_event.item),
|
||||
item_started_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::ItemCompleted(item_completed_event) => {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: item_completed_event.item.into(),
|
||||
completed_at_ms: item_completed_event.completed_at_ms,
|
||||
})
|
||||
ThreadItemPayload::from(item_completed_event.item),
|
||||
item_completed_event.started_at_ms,
|
||||
item_completed_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::PatchApplyUpdated(event) => {
|
||||
ServerNotification::FileChangePatchUpdated(FileChangePatchUpdatedNotification {
|
||||
@@ -428,12 +436,12 @@ pub fn item_event_to_server_notification(
|
||||
})
|
||||
}
|
||||
EventMsg::ExecCommandBegin(exec_command_begin_event) => {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
ServerNotification::ItemStarted(ItemStartedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: build_command_execution_begin_item(&exec_command_begin_event),
|
||||
started_at_ms: exec_command_begin_event.started_at_ms,
|
||||
})
|
||||
build_command_execution_begin_item(&exec_command_begin_event),
|
||||
exec_command_begin_event.started_at_ms,
|
||||
))
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let item_id = exec_command_output_delta_event.call_id;
|
||||
@@ -457,12 +465,13 @@ pub fn item_event_to_server_notification(
|
||||
})
|
||||
}
|
||||
EventMsg::ExecCommandEnd(exec_command_end_event) => {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
thread_id,
|
||||
turn_id,
|
||||
item: build_command_execution_end_item(&exec_command_end_event),
|
||||
completed_at_ms: exec_command_end_event.completed_at_ms,
|
||||
})
|
||||
build_command_execution_end_item(&exec_command_end_event),
|
||||
/*started_at_ms*/ None,
|
||||
exec_command_end_event.completed_at_ms,
|
||||
))
|
||||
}
|
||||
_ => unreachable!("unsupported item event"),
|
||||
}
|
||||
@@ -528,11 +537,10 @@ mod tests {
|
||||
);
|
||||
assert_item_started_server_notification(
|
||||
notification,
|
||||
ItemStartedNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: event.started_at_ms,
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -543,7 +551,8 @@ mod tests {
|
||||
reasoning_effort: None,
|
||||
agents_states: HashMap::new(),
|
||||
},
|
||||
},
|
||||
event.started_at_ms,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -567,11 +576,10 @@ mod tests {
|
||||
);
|
||||
assert_item_completed_server_notification(
|
||||
notification,
|
||||
ItemCompletedNotification {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
completed_at_ms: event.completed_at_ms,
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: event.call_id,
|
||||
tool: CollabAgentTool::ResumeAgent,
|
||||
status: CollabAgentToolCallStatus::Failed,
|
||||
@@ -587,7 +595,9 @@ mod tests {
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
event.completed_at_ms,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::protocol::v2::ItemGuardianApprovalReviewCompletedNotification;
|
||||
use crate::protocol::v2::ItemGuardianApprovalReviewStartedNotification;
|
||||
use crate::protocol::v2::PatchApplyStatus;
|
||||
use crate::protocol::v2::PatchChangeKind;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::ThreadItemPayload;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
@@ -69,34 +69,34 @@ pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String {
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
) -> ThreadItemPayload {
|
||||
ThreadItemPayload::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
pub fn build_file_change_begin_item(payload: &PatchApplyBeginEvent) -> ThreadItemPayload {
|
||||
ThreadItemPayload::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: PatchApplyStatus::InProgress,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem {
|
||||
ThreadItem::FileChange {
|
||||
pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItemPayload {
|
||||
ThreadItemPayload::FileChange {
|
||||
id: payload.call_id.clone(),
|
||||
changes: convert_patch_changes(&payload.changes),
|
||||
status: (&payload.status).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
|
||||
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItemPayload {
|
||||
let presentation =
|
||||
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: payload.call_id.clone(),
|
||||
@@ -114,7 +114,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItem {
|
||||
pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> ThreadItemPayload {
|
||||
let aggregated_output = if payload.aggregated_output.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -124,7 +124,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
|
||||
let presentation =
|
||||
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
|
||||
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: payload.call_id.clone(),
|
||||
@@ -193,7 +193,7 @@ fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) ->
|
||||
pub fn build_item_from_guardian_event(
|
||||
assessment: &GuardianAssessmentEvent,
|
||||
status: CommandExecutionStatus,
|
||||
) -> Option<ThreadItem> {
|
||||
) -> Option<ThreadItemPayload> {
|
||||
match &assessment.action {
|
||||
GuardianAssessmentAction::Command { command, cwd, .. } => {
|
||||
let id = assessment.target_item_id.as_ref()?;
|
||||
@@ -201,7 +201,7 @@ pub fn build_item_from_guardian_event(
|
||||
let command_actions = vec![CommandAction::Unknown {
|
||||
command: command.clone(),
|
||||
}];
|
||||
Some(ThreadItem::CommandExecution {
|
||||
Some(ThreadItemPayload::CommandExecution {
|
||||
id: id.clone(),
|
||||
model_context: assessment.model_context.clone(),
|
||||
sandbox_type: None,
|
||||
@@ -241,7 +241,7 @@ pub fn build_item_from_guardian_event(
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, cwd))
|
||||
.collect()
|
||||
};
|
||||
Some(ThreadItem::CommandExecution {
|
||||
Some(ThreadItemPayload::CommandExecution {
|
||||
id: id.clone(),
|
||||
model_context: assessment.model_context.clone(),
|
||||
sandbox_type: None,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::protocol::thread_history::build_turns_from_rollout_items;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
@@ -126,7 +127,11 @@ fn guardian_stdin_reviews_preserve_parent_command_history() {
|
||||
assert_eq!(expected.len(), 2);
|
||||
assert_eq!(
|
||||
expected[0].items,
|
||||
vec![build_command_execution_begin_item(&begin)],
|
||||
[ThreadItem {
|
||||
payload: build_command_execution_begin_item(&begin),
|
||||
started_at_ms: Some(begin.started_at_ms),
|
||||
completed_at_ms: None,
|
||||
}],
|
||||
);
|
||||
assert!(expected[1].items.is_empty());
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -78,9 +78,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet {
|
||||
RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: event.turn_id.clone(),
|
||||
item: ThreadItem::from(event.item.clone()),
|
||||
started_at_ms: event.started_at_ms,
|
||||
completed_at_ms: (event.completed_at_ms != 0).then_some(event.completed_at_ms),
|
||||
item: ThreadItem::from(event),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
use crate::protocol::v2::ThreadItem;
|
||||
use crate::protocol::v2::ThreadItemPayload;
|
||||
use crate::protocol::v2::TurnError;
|
||||
|
||||
#[test]
|
||||
@@ -145,18 +145,22 @@ fn projects_completed_canonical_turn_items() {
|
||||
user_changes.changed_items,
|
||||
vec![ThreadHistoryItemChange {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::from(user_item),
|
||||
started_at_ms: Some(100),
|
||||
completed_at_ms: Some(123),
|
||||
item: ThreadItem {
|
||||
payload: ThreadItemPayload::from(user_item),
|
||||
started_at_ms: Some(100),
|
||||
completed_at_ms: Some(123)
|
||||
}
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
agent_changes.changed_items,
|
||||
vec![ThreadHistoryItemChange {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::from(agent_item),
|
||||
started_at_ms: Some(100),
|
||||
completed_at_ms: Some(123),
|
||||
item: ThreadItem {
|
||||
payload: ThreadItemPayload::from(agent_item),
|
||||
started_at_ms: Some(100),
|
||||
completed_at_ms: Some(123)
|
||||
}
|
||||
}]
|
||||
);
|
||||
}
|
||||
@@ -187,9 +191,11 @@ fn projects_optional_completed_item_lifecycle_timestamps() {
|
||||
ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::from(item.clone()),
|
||||
started_at_ms,
|
||||
completed_at_ms: expected_completed_at_ms,
|
||||
item: ThreadItem {
|
||||
payload: ThreadItemPayload::from(item.clone()),
|
||||
started_at_ms,
|
||||
completed_at_ms: expected_completed_at_ms
|
||||
}
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
//! Projects a turn's items without copying payloads excluded by the requested view.
|
||||
|
||||
use super::v2::ThreadItem;
|
||||
use super::v2::ThreadItemPayload;
|
||||
use super::v2::TurnItemsView;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
impl TurnItemsView {
|
||||
/// Copies only the items included in this view, preserving summary selection order.
|
||||
pub fn project_items(self, items: &[ThreadItem]) -> Vec<ThreadItem> {
|
||||
pub fn project_items<T: Borrow<ThreadItemPayload> + Clone>(self, items: &[T]) -> Vec<T> {
|
||||
match self {
|
||||
Self::NotLoaded => Vec::new(),
|
||||
Self::Full => items.to_vec(),
|
||||
Self::Summary => {
|
||||
let first_user_message = items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::UserMessage { .. }));
|
||||
let final_agent_message = items
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|item| matches!(item, ThreadItem::AgentMessage { .. }));
|
||||
.find(|item| matches!((*item).borrow(), ThreadItemPayload::UserMessage { .. }));
|
||||
let final_agent_message = items.iter().rev().find(|item| {
|
||||
matches!((*item).borrow(), ThreadItemPayload::AgentMessage { .. })
|
||||
});
|
||||
match (first_user_message, final_agent_message) {
|
||||
(Some(user_message), Some(agent_message))
|
||||
if user_message.id() != agent_message.id() =>
|
||||
if user_message.borrow().id() != agent_message.borrow().id() =>
|
||||
{
|
||||
vec![user_message.clone(), agent_message.clone()]
|
||||
}
|
||||
|
||||
@@ -231,11 +231,72 @@ impl CommandAction {
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread item and its recorded lifecycle times. The payload stays flat on the wire.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadItem {
|
||||
#[serde(flatten)]
|
||||
pub payload: ThreadItemPayload,
|
||||
/// Unix timestamp in milliseconds when the item started, if recorded.
|
||||
#[serde(default)]
|
||||
#[ts(type = "number | null")]
|
||||
pub started_at_ms: Option<i64>,
|
||||
/// Unix timestamp in milliseconds when the item completed, if recorded.
|
||||
#[serde(default)]
|
||||
#[ts(type = "number | null")]
|
||||
pub completed_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl ThreadItem {
|
||||
/// Creates an item with no recorded start or completion time.
|
||||
pub fn untimed(payload: ThreadItemPayload) -> Self {
|
||||
Self {
|
||||
payload,
|
||||
started_at_ms: None,
|
||||
completed_at_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn completed(
|
||||
payload: ThreadItemPayload,
|
||||
started_at_ms: Option<i64>,
|
||||
completed_at_ms: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
payload,
|
||||
started_at_ms,
|
||||
// Older rollout events default a missing completion time to zero.
|
||||
completed_at_ms: (completed_at_ms != 0).then_some(completed_at_ms),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
self.payload.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&codex_protocol::protocol::ItemCompletedEvent> for ThreadItem {
|
||||
fn from(event: &codex_protocol::protocol::ItemCompletedEvent) -> Self {
|
||||
ThreadItem::completed(
|
||||
ThreadItemPayload::from(event.item.clone()),
|
||||
event.started_at_ms,
|
||||
event.completed_at_ms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::borrow::Borrow<ThreadItemPayload> for ThreadItem {
|
||||
fn borrow(&self) -> &ThreadItemPayload {
|
||||
&self.payload
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum ThreadItem {
|
||||
pub enum ThreadItemPayload {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
UserMessage {
|
||||
@@ -453,28 +514,28 @@ pub struct HookPromptFragment {
|
||||
pub hook_run_id: String,
|
||||
}
|
||||
|
||||
impl ThreadItem {
|
||||
impl ThreadItemPayload {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
ThreadItem::UserMessage { id, .. }
|
||||
| ThreadItem::HookPrompt { id, .. }
|
||||
| ThreadItem::AgentMessage { id, .. }
|
||||
| ThreadItem::FunctionCallOutput { id, .. }
|
||||
| ThreadItem::Plan { id, .. }
|
||||
| ThreadItem::Reasoning { id, .. }
|
||||
| ThreadItem::CommandExecution { id, .. }
|
||||
| ThreadItem::FileChange { id, .. }
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::SubAgentActivity { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::EnteredReviewMode { id, .. }
|
||||
| ThreadItem::ExitedReviewMode { id, .. }
|
||||
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||
ThreadItem::WebSearch(item) => &item.id,
|
||||
ThreadItem::Sleep(item) => &item.id,
|
||||
ThreadItem::ImageGeneration(item) => &item.id,
|
||||
Self::UserMessage { id, .. }
|
||||
| Self::HookPrompt { id, .. }
|
||||
| Self::AgentMessage { id, .. }
|
||||
| Self::FunctionCallOutput { id, .. }
|
||||
| Self::Plan { id, .. }
|
||||
| Self::Reasoning { id, .. }
|
||||
| Self::CommandExecution { id, .. }
|
||||
| Self::FileChange { id, .. }
|
||||
| Self::McpToolCall { id, .. }
|
||||
| Self::DynamicToolCall { id, .. }
|
||||
| Self::CollabAgentToolCall { id, .. }
|
||||
| Self::SubAgentActivity { id, .. }
|
||||
| Self::ImageView { id, .. }
|
||||
| Self::EnteredReviewMode { id, .. }
|
||||
| Self::ExitedReviewMode { id, .. }
|
||||
| Self::ContextCompaction { id, .. } => id,
|
||||
Self::WebSearch(item) => &item.id,
|
||||
Self::Sleep(item) => &item.id,
|
||||
Self::ImageGeneration(item) => &item.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -872,15 +933,15 @@ pub(crate) fn web_search_action_from_core(
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CoreTurnItem> for ThreadItem {
|
||||
impl From<CoreTurnItem> for ThreadItemPayload {
|
||||
fn from(value: CoreTurnItem) -> Self {
|
||||
match value {
|
||||
CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
|
||||
CoreTurnItem::UserMessage(user) => Self::UserMessage {
|
||||
id: user.id,
|
||||
client_id: user.client_id,
|
||||
content: user.content.into_iter().map(UserInput::from).collect(),
|
||||
},
|
||||
CoreTurnItem::HookPrompt(hook_prompt) => ThreadItem::HookPrompt {
|
||||
CoreTurnItem::HookPrompt(hook_prompt) => Self::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
@@ -896,7 +957,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
CoreAgentMessageContent::Text { text } => text,
|
||||
})
|
||||
.collect::<String>();
|
||||
ThreadItem::AgentMessage {
|
||||
Self::AgentMessage {
|
||||
id: agent.id,
|
||||
text,
|
||||
phase: agent.phase,
|
||||
@@ -905,17 +966,17 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
questions: agent.questions,
|
||||
}
|
||||
}
|
||||
CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput {
|
||||
CoreTurnItem::FunctionCallOutput(output) => Self::FunctionCallOutput {
|
||||
id: output.id,
|
||||
name: output.name,
|
||||
namespace: output.namespace,
|
||||
output: output.output,
|
||||
},
|
||||
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
|
||||
CoreTurnItem::Plan(plan) => Self::Plan {
|
||||
id: plan.id,
|
||||
text: plan.text,
|
||||
},
|
||||
CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning {
|
||||
CoreTurnItem::Reasoning(reasoning) => Self::Reasoning {
|
||||
id: reasoning.id,
|
||||
summary: reasoning.summary_text,
|
||||
content: reasoning.raw_content,
|
||||
@@ -926,7 +987,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
&command.parsed_cmd,
|
||||
&command.cwd,
|
||||
);
|
||||
ThreadItem::CommandExecution {
|
||||
Self::CommandExecution {
|
||||
id: command.id,
|
||||
model_context: command.model_context,
|
||||
sandbox_type: command.sandbox_type,
|
||||
@@ -947,7 +1008,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
.and_then(|duration| i64::try_from(duration.as_millis()).ok()),
|
||||
}
|
||||
}
|
||||
CoreTurnItem::DynamicToolCall(call) => ThreadItem::DynamicToolCall {
|
||||
CoreTurnItem::DynamicToolCall(call) => Self::DynamicToolCall {
|
||||
id: call.id,
|
||||
namespace: call.namespace,
|
||||
tool: call.tool,
|
||||
@@ -964,7 +1025,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
.duration
|
||||
.and_then(|duration| i64::try_from(duration.as_millis()).ok()),
|
||||
},
|
||||
CoreTurnItem::CollabAgentToolCall(call) => ThreadItem::CollabAgentToolCall {
|
||||
CoreTurnItem::CollabAgentToolCall(call) => Self::CollabAgentToolCall {
|
||||
id: call.id,
|
||||
tool: call.tool.into(),
|
||||
status: call.status.into(),
|
||||
@@ -983,7 +1044,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
.map(|(thread_id, status)| (thread_id.to_string(), status.into()))
|
||||
.collect(),
|
||||
},
|
||||
CoreTurnItem::SubAgentActivity(activity) => ThreadItem::SubAgentActivity {
|
||||
CoreTurnItem::SubAgentActivity(activity) => Self::SubAgentActivity {
|
||||
model: activity.model,
|
||||
reasoning_effort: activity.reasoning_effort,
|
||||
id: activity.id,
|
||||
@@ -991,46 +1052,44 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
agent_thread_id: activity.agent_thread_id.to_string(),
|
||||
agent_path: String::from(activity.agent_path),
|
||||
},
|
||||
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch(WebSearchItem {
|
||||
CoreTurnItem::WebSearch(search) => Self::WebSearch(WebSearchItem {
|
||||
id: search.id,
|
||||
query: search.query,
|
||||
action: Some(web_search_action_from_core(search.action)),
|
||||
results: search.results,
|
||||
}),
|
||||
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
|
||||
CoreTurnItem::ImageView(image) => Self::ImageView {
|
||||
id: image.id,
|
||||
path: image.path.into(),
|
||||
},
|
||||
CoreTurnItem::Extension(extension) => match extension {
|
||||
ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item),
|
||||
ExtensionItem::Sleep(item) => ThreadItem::Sleep(item),
|
||||
ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(item),
|
||||
ExtensionItem::ImageGeneration(item) => Self::ImageGeneration(item),
|
||||
ExtensionItem::Sleep(item) => Self::Sleep(item),
|
||||
ExtensionItem::WebSearch(item) => Self::WebSearch(item),
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
image: GeneratedImageRef::Inline {
|
||||
result: image.result,
|
||||
},
|
||||
transparent_background: None,
|
||||
failure: None,
|
||||
saved_path: image.saved_path,
|
||||
input_images: Vec::new(),
|
||||
imagegen_request_id: None,
|
||||
generation_id: None,
|
||||
})
|
||||
}
|
||||
CoreTurnItem::EnteredReviewMode(review) => ThreadItem::EnteredReviewMode {
|
||||
CoreTurnItem::ImageGeneration(image) => Self::ImageGeneration(ImageGenerationItem {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
image: GeneratedImageRef::Inline {
|
||||
result: image.result,
|
||||
},
|
||||
transparent_background: None,
|
||||
failure: None,
|
||||
saved_path: image.saved_path,
|
||||
input_images: Vec::new(),
|
||||
imagegen_request_id: None,
|
||||
generation_id: None,
|
||||
}),
|
||||
CoreTurnItem::EnteredReviewMode(review) => Self::EnteredReviewMode {
|
||||
id: review.id,
|
||||
review: review.user_facing_hint,
|
||||
},
|
||||
CoreTurnItem::ExitedReviewMode(review) => ThreadItem::ExitedReviewMode {
|
||||
CoreTurnItem::ExitedReviewMode(review) => Self::ExitedReviewMode {
|
||||
id: review.id,
|
||||
review: review_output_text(review.review_output.as_ref()),
|
||||
},
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
CoreTurnItem::FileChange(file_change) => Self::FileChange {
|
||||
id: file_change.id,
|
||||
changes: convert_patch_changes(&file_change.changes),
|
||||
status: file_change
|
||||
@@ -1044,7 +1103,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
.duration
|
||||
.and_then(|duration| i64::try_from(duration.as_millis()).ok());
|
||||
|
||||
ThreadItem::McpToolCall {
|
||||
Self::McpToolCall {
|
||||
id: mcp.id,
|
||||
server: mcp.server,
|
||||
tool: mcp.tool,
|
||||
@@ -1067,7 +1126,7 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
}
|
||||
}
|
||||
CoreTurnItem::ContextCompaction(compaction) => {
|
||||
ThreadItem::ContextCompaction { id: compaction.id }
|
||||
Self::ContextCompaction { id: compaction.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1353,6 +1412,25 @@ pub struct ItemStartedNotification {
|
||||
#[ts(type = "number")]
|
||||
pub started_at_ms: i64,
|
||||
}
|
||||
impl ItemStartedNotification {
|
||||
pub fn new(
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
payload: ThreadItemPayload,
|
||||
started_at_ms: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_id,
|
||||
turn_id,
|
||||
started_at_ms,
|
||||
item: ThreadItem {
|
||||
payload,
|
||||
started_at_ms: Some(started_at_ms),
|
||||
completed_at_ms: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -1431,6 +1509,22 @@ pub struct ItemCompletedNotification {
|
||||
#[ts(type = "number")]
|
||||
pub completed_at_ms: i64,
|
||||
}
|
||||
impl ItemCompletedNotification {
|
||||
pub fn new(
|
||||
thread_id: String,
|
||||
turn_id: String,
|
||||
payload: ThreadItemPayload,
|
||||
started_at_ms: Option<i64>,
|
||||
completed_at_ms: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread_id,
|
||||
turn_id,
|
||||
completed_at_ms,
|
||||
item: ThreadItem::completed(payload, started_at_ms, completed_at_ms),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -461,8 +461,12 @@ fn thread_items_list_round_trips() {
|
||||
let response = ThreadItemsListResponse {
|
||||
data: vec![ThreadItemEntry {
|
||||
turn_id: "turn_456".to_string(),
|
||||
item: ThreadItem::ContextCompaction {
|
||||
id: "item_1".to_string(),
|
||||
item: ThreadItem {
|
||||
payload: ThreadItemPayload::ContextCompaction {
|
||||
id: "item_1".to_string(),
|
||||
},
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
},
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
@@ -476,7 +480,7 @@ fn thread_items_list_round_trips() {
|
||||
json!({
|
||||
"data": [{
|
||||
"turnId": "turn_456",
|
||||
"item": {"type": "contextCompaction", "id": "item_1"},
|
||||
"item": {"type": "contextCompaction", "id": "item_1", "startedAtMs": started_at_ms, "completedAtMs": completed_at_ms},
|
||||
"startedAtMs": started_at_ms,
|
||||
"completedAtMs": completed_at_ms,
|
||||
}],
|
||||
@@ -3164,8 +3168,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(user_item),
|
||||
ThreadItem::UserMessage {
|
||||
ThreadItemPayload::from(user_item),
|
||||
ThreadItemPayload::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
client_id: Some("client-message-1".to_string()),
|
||||
content: vec![
|
||||
@@ -3218,8 +3222,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(agent_item),
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::from(agent_item),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "Hello world".to_string(),
|
||||
phase: None,
|
||||
@@ -3249,8 +3253,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(agent_item_with_phase),
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::from(agent_item_with_phase),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-2".to_string(),
|
||||
text: "final".to_string(),
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
@@ -3268,7 +3272,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}
|
||||
);
|
||||
|
||||
let async_item = ThreadItem::from(TurnItem::AgentMessage(AgentMessageItem {
|
||||
let async_item = ThreadItemPayload::from(TurnItem::AgentMessage(AgentMessageItem {
|
||||
id: "async-1".to_string(),
|
||||
content: vec![AgentMessageContent::Text {
|
||||
text: "Which?".to_string(),
|
||||
@@ -3288,13 +3292,13 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
"memoryCitation": null, "delivery": "async", "questions": [{"title": "Which?", "options": null}]
|
||||
})
|
||||
);
|
||||
let old_item: ThreadItem = serde_json::from_value(json!({
|
||||
let old_item: ThreadItemPayload = serde_json::from_value(json!({
|
||||
"type": "agentMessage", "id": "old-1", "text": "An old message"
|
||||
}))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
old_item,
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
questions: None,
|
||||
..
|
||||
}
|
||||
@@ -3307,8 +3311,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(reasoning_item),
|
||||
ThreadItem::Reasoning {
|
||||
ThreadItemPayload::from(reasoning_item),
|
||||
ThreadItemPayload::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: vec!["line one".to_string(), "line two".to_string()],
|
||||
content: vec![],
|
||||
@@ -3345,8 +3349,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(command_item),
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::from(command_item),
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "exec-1".to_string(),
|
||||
@@ -3391,8 +3395,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(dynamic_tool_call_item),
|
||||
ThreadItem::DynamicToolCall {
|
||||
ThreadItemPayload::from(dynamic_tool_call_item),
|
||||
ThreadItemPayload::DynamicToolCall {
|
||||
id: "dynamic-1".to_string(),
|
||||
namespace: Some("apps".to_string()),
|
||||
tool: "lookup".to_string(),
|
||||
@@ -3432,8 +3436,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(collab_item),
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
ThreadItemPayload::from(collab_item),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "collab-1".to_string(),
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status: CollabAgentToolCallStatus::Completed,
|
||||
@@ -3466,8 +3470,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(sub_agent_activity_item),
|
||||
ThreadItem::SubAgentActivity {
|
||||
ThreadItemPayload::from(sub_agent_activity_item),
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
model: Some("gpt-5".into()),
|
||||
reasoning_effort: Some(ReasoningEffort::High),
|
||||
id: "activity-1".to_string(),
|
||||
@@ -3484,7 +3488,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
"agent_path": "/root/worker",
|
||||
}))
|
||||
.unwrap();
|
||||
let old_wire_activity: ThreadItem = serde_json::from_value(json!({
|
||||
let old_wire_activity: ThreadItemPayload = serde_json::from_value(json!({
|
||||
"type": "subAgentActivity",
|
||||
"id": "old-activity",
|
||||
"kind": "started",
|
||||
@@ -3493,7 +3497,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ThreadItem::from(TurnItem::SubAgentActivity(old_activity)),
|
||||
ThreadItemPayload::from(TurnItem::SubAgentActivity(old_activity)),
|
||||
old_wire_activity
|
||||
);
|
||||
|
||||
@@ -3526,14 +3530,14 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(search_item),
|
||||
ThreadItem::WebSearch(expected_search_item.clone())
|
||||
ThreadItemPayload::from(search_item),
|
||||
ThreadItemPayload::WebSearch(expected_search_item.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
ThreadItem::from(TurnItem::Extension(
|
||||
ThreadItemPayload::from(TurnItem::Extension(
|
||||
codex_extension_items::ExtensionItem::WebSearch(expected_search_item.clone()),
|
||||
)),
|
||||
ThreadItem::WebSearch(expected_search_item)
|
||||
ThreadItemPayload::WebSearch(expected_search_item)
|
||||
);
|
||||
|
||||
let image_view_item = TurnItem::ImageView(ImageViewItem {
|
||||
@@ -3542,8 +3546,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(image_view_item),
|
||||
ThreadItem::ImageView {
|
||||
ThreadItemPayload::from(image_view_item),
|
||||
ThreadItemPayload::ImageView {
|
||||
id: "view-image-1".to_string(),
|
||||
path: LegacyAppPathString::from_abs_path(&test_path_buf("/tmp/view-image.png").abs()),
|
||||
}
|
||||
@@ -3566,8 +3570,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(file_change_item),
|
||||
ThreadItem::FileChange {
|
||||
ThreadItemPayload::from(file_change_item),
|
||||
ThreadItemPayload::FileChange {
|
||||
id: "patch-1".to_string(),
|
||||
changes: vec![FileUpdateChange {
|
||||
path: "README.md".to_string(),
|
||||
@@ -3598,8 +3602,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(mcp_tool_call_item),
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::from(mcp_tool_call_item),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
@@ -3647,8 +3651,8 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(completed_mcp_tool_call_item),
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::from(completed_mcp_tool_call_item),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-2".to_string(),
|
||||
server: "server".to_string(),
|
||||
tool: "tool".to_string(),
|
||||
@@ -3672,7 +3676,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_call_app_context_serializes_connector_id() {
|
||||
let item = ThreadItem::McpToolCall {
|
||||
let item = ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "codex_apps".to_string(),
|
||||
tool: "calendar.create_event".to_string(),
|
||||
|
||||
@@ -1845,6 +1845,16 @@ pub struct ThreadItemEntry {
|
||||
pub completed_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<ThreadItemEntry> for ThreadItem {
|
||||
fn from(entry: ThreadItemEntry) -> Self {
|
||||
Self {
|
||||
started_at_ms: entry.item.started_at_ms.or(entry.started_at_ms),
|
||||
completed_at_ms: entry.item.completed_at_ms.or(entry.completed_at_ms),
|
||||
..entry.item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::CodexErrorInfo;
|
||||
use super::ThreadEnvironment;
|
||||
use super::ThreadItem;
|
||||
use super::ThreadItemPayload;
|
||||
use super::ThreadStatus;
|
||||
use super::TurnStatus;
|
||||
use crate::JsonSchema;
|
||||
@@ -409,6 +410,15 @@ pub struct Turn {
|
||||
pub duration_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl Turn {
|
||||
/// Iterates over the item payloads in this turn.
|
||||
pub fn item_payloads(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = &ThreadItemPayload> + ExactSizeIterator {
|
||||
self.items.iter().map(|item| &item.payload)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -57,7 +57,7 @@ use codex_app_server_protocol::ThreadDecrementElicitationParams;
|
||||
use codex_app_server_protocol::ThreadDecrementElicitationResponse;
|
||||
use codex_app_server_protocol::ThreadIncrementElicitationParams;
|
||||
use codex_app_server_protocol::ThreadIncrementElicitationResponse;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
@@ -1456,13 +1456,13 @@ struct CodexClient {
|
||||
command_item_started: bool,
|
||||
helper_done_seen: bool,
|
||||
turn_completed_before_helper_done: bool,
|
||||
unexpected_items_before_helper_done: Vec<ThreadItem>,
|
||||
unexpected_items_before_helper_done: Vec<ThreadItemPayload>,
|
||||
last_turn_status: Option<TurnStatus>,
|
||||
last_turn_error_message: Option<String>,
|
||||
}
|
||||
|
||||
fn item_started_before_helper_done_is_unexpected(
|
||||
item: &ThreadItem,
|
||||
item: &ThreadItemPayload,
|
||||
command_item_started: bool,
|
||||
helper_done_seen: bool,
|
||||
) -> bool {
|
||||
@@ -1470,7 +1470,7 @@ fn item_started_before_helper_done_is_unexpected(
|
||||
return false;
|
||||
}
|
||||
|
||||
!matches!(item, ThreadItem::UserMessage { .. })
|
||||
!matches!(item, ThreadItemPayload::UserMessage { .. })
|
||||
}
|
||||
|
||||
impl CodexClient {
|
||||
@@ -1816,28 +1816,31 @@ impl CodexClient {
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
ServerNotification::ItemStarted(payload) => {
|
||||
if matches!(payload.item, ThreadItem::CommandExecution { .. }) {
|
||||
if matches!(
|
||||
payload.item.payload,
|
||||
ThreadItemPayload::CommandExecution { .. }
|
||||
) {
|
||||
if self.command_item_started && !self.helper_done_seen {
|
||||
self.unexpected_items_before_helper_done
|
||||
.push(payload.item.clone());
|
||||
.push(payload.item.payload.clone());
|
||||
}
|
||||
self.command_item_started = true;
|
||||
} else if item_started_before_helper_done_is_unexpected(
|
||||
&payload.item,
|
||||
&payload.item.payload,
|
||||
self.command_item_started,
|
||||
self.helper_done_seen,
|
||||
) {
|
||||
self.unexpected_items_before_helper_done
|
||||
.push(payload.item.clone());
|
||||
.push(payload.item.payload.clone());
|
||||
}
|
||||
println!("\n< item started: {:?}", payload.item);
|
||||
println!("\n< item started: {:?}", payload.item.payload);
|
||||
}
|
||||
ServerNotification::ItemCompleted(payload) => {
|
||||
if let ThreadItem::CommandExecution {
|
||||
if let ThreadItemPayload::CommandExecution {
|
||||
status,
|
||||
aggregated_output,
|
||||
..
|
||||
} = payload.item.clone()
|
||||
} = payload.item.payload.clone()
|
||||
{
|
||||
self.command_execution_statuses.push(status);
|
||||
if let Some(aggregated_output) = aggregated_output {
|
||||
@@ -1845,7 +1848,7 @@ impl CodexClient {
|
||||
self.command_execution_outputs.push(aggregated_output);
|
||||
}
|
||||
}
|
||||
println!("< item completed: {:?}", payload.item);
|
||||
println!("< item completed: {:?}", payload.item.payload);
|
||||
}
|
||||
ServerNotification::TurnCompleted(payload) => {
|
||||
if payload.turn.id == turn_id {
|
||||
|
||||
@@ -611,6 +611,11 @@ execution and sandbox policies.
|
||||
|
||||
# Item history anchors
|
||||
|
||||
Live notifications and turn history return the same `ThreadItem`. Each item has
|
||||
`startedAtMs` and `completedAtMs` beside `type` and `id`. Each timestamp is in
|
||||
Unix milliseconds, or `null` when the producer has not recorded it. Clients
|
||||
reading older servers may also encounter items that omit these fields.
|
||||
|
||||
`thread/items/list` accepts an optional nullable `cursor`: either an opaque string
|
||||
from a previous response or an item anchor such as
|
||||
`{"type":"item","itemId":"item-123"}`. An item anchor resumes exclusively after
|
||||
|
||||
@@ -54,6 +54,7 @@ use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::StrictReviewRequiredNotification;
|
||||
use codex_app_server_protocol::ThreadGoalUpdatedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadRealtimeClosedNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeErrorNotification;
|
||||
use codex_app_server_protocol::ThreadRealtimeItemAddedNotification;
|
||||
@@ -156,6 +157,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
id: event_turn_id,
|
||||
msg,
|
||||
} = event;
|
||||
let approval_starts_command = msg.command_approval_start().is_some();
|
||||
match msg {
|
||||
EventMsg::TurnStarted(payload) => {
|
||||
// While not technically necessary as it was already done on TurnComplete, be extra cautios and abort any pending server requests.
|
||||
@@ -165,7 +167,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
let turn = {
|
||||
let state = thread_state.lock().await;
|
||||
let mut turn = state.active_turn_snapshot().unwrap_or_else(|| Turn {
|
||||
let turn = state.active_turn_metadata_snapshot().map(Turn::from);
|
||||
turn.unwrap_or_else(|| Turn {
|
||||
id: payload.turn_id.clone(),
|
||||
root_turn_id: payload.root_turn_id.clone(),
|
||||
items: Vec::new(),
|
||||
@@ -175,10 +178,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
started_at: payload.started_at,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
});
|
||||
turn.items.clear();
|
||||
turn.items_view = TurnItemsView::NotLoaded;
|
||||
turn
|
||||
})
|
||||
};
|
||||
let notification = TurnStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
@@ -300,7 +300,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
&assessment,
|
||||
CommandExecutionStatus::InProgress,
|
||||
) {
|
||||
Some(ThreadItem::CommandExecution {
|
||||
Some(ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
model_context,
|
||||
plugin_id,
|
||||
@@ -327,20 +327,15 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
} else {
|
||||
assessment.turn_id.clone()
|
||||
};
|
||||
if assessment.status == codex_protocol::protocol::GuardianAssessmentStatus::InProgress
|
||||
if approval_starts_command
|
||||
&& let Some((target_item_id, completion_item)) = pending_command_execution.as_ref()
|
||||
{
|
||||
start_command_execution_item(
|
||||
&conversation_id,
|
||||
assessment_turn_id.clone(),
|
||||
target_item_id.clone(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
CommandExecutionSource::Agent,
|
||||
assessment.started_at_ms,
|
||||
completion_item,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -393,9 +388,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
assessment_turn_id,
|
||||
target_item_id,
|
||||
completion_item,
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
completion_status,
|
||||
assessment.completed_at_ms,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -760,20 +754,13 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
Some(completion_item),
|
||||
),
|
||||
};
|
||||
if approval_id.is_none()
|
||||
&& let Some(completion_item) = completion_item.as_ref()
|
||||
{
|
||||
if approval_starts_command && let Some(completion_item) = completion_item.as_ref() {
|
||||
start_command_execution_item(
|
||||
&conversation_id,
|
||||
event_turn_id.clone(),
|
||||
call_id.clone(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
CommandExecutionSource::Agent,
|
||||
started_at_ms,
|
||||
completion_item,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -1088,13 +1075,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
EventMsg::ItemStarted(event) => {
|
||||
let should_emit = match &event.item {
|
||||
// Approval and guardian flows can emit the command start notification before core
|
||||
// emits the canonical item. Reuse the same set to suppress that duplicate.
|
||||
// emits the canonical item. Keep the first observed start and suppress that duplicate.
|
||||
CoreTurnItem::CommandExecution(item) => thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.insert(item.id.clone()),
|
||||
.record_command_start(item.id.clone(), event.started_at_ms),
|
||||
_ => true,
|
||||
};
|
||||
let dynamic_tool_call_params = match &event.item {
|
||||
@@ -1129,12 +1115,12 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
});
|
||||
}
|
||||
}
|
||||
EventMsg::ItemCompleted(event) => {
|
||||
EventMsg::ItemCompleted(mut event) => {
|
||||
apply_canonical_item_completed_side_effects(
|
||||
&thread_manager,
|
||||
&thread_watch_manager,
|
||||
&thread_state,
|
||||
&event.item,
|
||||
&mut event,
|
||||
)
|
||||
.await;
|
||||
let mut notification = item_event_to_server_notification(
|
||||
@@ -1408,16 +1394,18 @@ async fn apply_canonical_item_completed_side_effects(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
item: &CoreTurnItem,
|
||||
event: &mut codex_protocol::protocol::ItemCompletedEvent,
|
||||
) {
|
||||
match item {
|
||||
match &event.item {
|
||||
CoreTurnItem::CommandExecution(item) => {
|
||||
thread_state
|
||||
let observed_start = thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.command_start_times
|
||||
.remove(&item.id);
|
||||
// A client can already be displaying the command's approval-time start.
|
||||
event.started_at_ms = observed_start.or(event.started_at_ms);
|
||||
}
|
||||
CoreTurnItem::SubAgentActivity(activity)
|
||||
if activity.kind == SubAgentActivityKind::Interrupted =>
|
||||
@@ -1450,50 +1438,42 @@ async fn remove_missing_thread_watch(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn start_command_execution_item(
|
||||
conversation_id: &ThreadId,
|
||||
turn_id: String,
|
||||
item_id: String,
|
||||
model_context: Option<ModelInvocationContext>,
|
||||
plugin_id: Option<String>,
|
||||
script_path: Option<String>,
|
||||
command: String,
|
||||
cwd: LegacyAppPathString,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
source: CommandExecutionSource,
|
||||
started_at_ms: i64,
|
||||
details: &CommandExecutionCompletionItem,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) -> bool {
|
||||
let first_start = {
|
||||
let mut state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.insert(item_id.clone())
|
||||
};
|
||||
let first_start = thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.record_command_start(item_id.clone(), started_at_ms);
|
||||
if first_start {
|
||||
let notification = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
let notification = ItemStartedNotification::new(
|
||||
conversation_id.to_string(),
|
||||
turn_id,
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
id: item_id,
|
||||
model_context,
|
||||
model_context: details.model_context.clone(),
|
||||
sandbox_type: None,
|
||||
plugin_id,
|
||||
script_path,
|
||||
command,
|
||||
cwd,
|
||||
plugin_id: details.plugin_id.clone(),
|
||||
script_path: details.script_path.clone(),
|
||||
command: details.command.clone(),
|
||||
cwd: details.cwd.clone(),
|
||||
process_id: None,
|
||||
source,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
command_actions: details.command_actions.clone(),
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
};
|
||||
started_at_ms,
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(notification))
|
||||
.await;
|
||||
@@ -1507,23 +1487,22 @@ async fn complete_command_execution_item(
|
||||
turn_id: String,
|
||||
item_id: String,
|
||||
completion_item: CommandExecutionCompletionItem,
|
||||
process_id: Option<String>,
|
||||
source: CommandExecutionSource,
|
||||
status: CommandExecutionStatus,
|
||||
completed_at_ms: Option<i64>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
let should_emit = thread_state
|
||||
let Some(started_at_ms) = thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&item_id);
|
||||
if !should_emit {
|
||||
.command_start_times
|
||||
.remove(&item_id)
|
||||
else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let item = ThreadItem::CommandExecution {
|
||||
let item = ThreadItemPayload::CommandExecution {
|
||||
id: item_id,
|
||||
model_context: completion_item.model_context,
|
||||
sandbox_type: None,
|
||||
@@ -1531,20 +1510,22 @@ async fn complete_command_execution_item(
|
||||
script_path: completion_item.script_path,
|
||||
command: completion_item.command,
|
||||
cwd: completion_item.cwd,
|
||||
process_id,
|
||||
source,
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status,
|
||||
command_actions: completion_item.command_actions,
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
};
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
let notification = ItemCompletedNotification::new(
|
||||
conversation_id.to_string(),
|
||||
turn_id,
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item,
|
||||
};
|
||||
Some(started_at_ms),
|
||||
// Guardian supplies the decision time used on resume. Manual approvals are timed here.
|
||||
completed_at_ms.unwrap_or_else(now_unix_timestamp_ms),
|
||||
);
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
@@ -2132,8 +2113,8 @@ async fn on_command_execution_request_approval_response(
|
||||
let state = thread_state.lock().await;
|
||||
state
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.contains(&item_id)
|
||||
.command_start_times
|
||||
.contains_key(&item_id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -2148,9 +2129,8 @@ async fn on_command_execution_request_approval_response(
|
||||
event_turn_id.clone(),
|
||||
item_id.clone(),
|
||||
completion_item,
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
status,
|
||||
/*completed_at_ms*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2551,13 +2531,8 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
CommandExecutionSource::Agent,
|
||||
/*started_at_ms*/ 1_000,
|
||||
&completion_item,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2567,11 +2542,12 @@ mod tests {
|
||||
let msg = recv_broadcast_notification(&mut rx).await?;
|
||||
match msg {
|
||||
ServerNotification::ItemStarted(payload) => {
|
||||
assert_eq!(payload.item.started_at_ms, Some(1_000));
|
||||
assert_eq!(payload.thread_id, conversation_id.to_string());
|
||||
assert_eq!(payload.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
payload.item,
|
||||
ThreadItem::CommandExecution {
|
||||
payload.item.payload,
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "cmd-1".to_string(),
|
||||
@@ -2596,13 +2572,8 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
CommandExecutionSource::Agent,
|
||||
/*started_at_ms*/ 1_100,
|
||||
&completion_item,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2633,13 +2604,8 @@ mod tests {
|
||||
&conversation_id,
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item.model_context.clone(),
|
||||
completion_item.plugin_id.clone(),
|
||||
completion_item.script_path.clone(),
|
||||
completion_item.command.clone(),
|
||||
completion_item.cwd.clone(),
|
||||
completion_item.command_actions.clone(),
|
||||
CommandExecutionSource::Agent,
|
||||
/*started_at_ms*/ 1_000,
|
||||
&completion_item,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2651,9 +2617,8 @@ mod tests {
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
completion_item,
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
CommandExecutionStatus::Declined,
|
||||
/*completed_at_ms*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2662,13 +2627,13 @@ mod tests {
|
||||
let completed = recv_broadcast_notification(&mut rx).await?;
|
||||
match completed {
|
||||
ServerNotification::ItemCompleted(payload) => {
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
plugin_id,
|
||||
script_path,
|
||||
status,
|
||||
..
|
||||
} = payload.item
|
||||
} = payload.item.payload
|
||||
else {
|
||||
bail!("expected command execution completion");
|
||||
};
|
||||
@@ -2685,9 +2650,8 @@ mod tests {
|
||||
"turn-1".to_string(),
|
||||
"cmd-1".to_string(),
|
||||
command_execution_completion_item("printf hi"),
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
CommandExecutionStatus::Declined,
|
||||
/*completed_at_ms*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -2750,13 +2714,13 @@ mod tests {
|
||||
match first {
|
||||
ServerNotification::ItemStarted(payload) => {
|
||||
assert_eq!(payload.turn_id, "turn-guardian-approved");
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
plugin_id,
|
||||
script_path,
|
||||
status,
|
||||
..
|
||||
} = payload.item
|
||||
} = payload.item.payload
|
||||
else {
|
||||
bail!("expected command execution item");
|
||||
};
|
||||
@@ -2822,13 +2786,13 @@ mod tests {
|
||||
match fourth {
|
||||
ServerNotification::ItemStarted(payload) => {
|
||||
assert_eq!(payload.turn_id, "turn-guardian-denied");
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
plugin_id,
|
||||
script_path,
|
||||
status,
|
||||
..
|
||||
} = payload.item
|
||||
} = payload.item.payload
|
||||
else {
|
||||
bail!("expected command execution item");
|
||||
};
|
||||
@@ -2878,13 +2842,14 @@ mod tests {
|
||||
let seventh = recv_broadcast_notification(&mut rx).await?;
|
||||
match seventh {
|
||||
ServerNotification::ItemCompleted(payload) => {
|
||||
let ThreadItem::CommandExecution {
|
||||
assert_eq!(payload.item.completed_at_ms, Some(1_042));
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
plugin_id,
|
||||
script_path,
|
||||
status,
|
||||
..
|
||||
} = payload.item
|
||||
} = payload.item.payload
|
||||
else {
|
||||
bail!("expected command execution completion");
|
||||
};
|
||||
@@ -3360,8 +3325,10 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
payload,
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::SubAgentActivity {
|
||||
ItemCompletedNotification::new(
|
||||
conversation_id.to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
id: "activity-1".to_string(),
|
||||
@@ -3369,10 +3336,9 @@ mod tests {
|
||||
agent_thread_id: child_thread_id_string,
|
||||
agent_path: "/root/worker".to_string(),
|
||||
},
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 42,
|
||||
}
|
||||
Some(42),
|
||||
/*completed_at_ms*/ 42
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3511,8 +3477,8 @@ mod tests {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
started_at_ms: Some(42_123),
|
||||
completed_at_ms: 42_200,
|
||||
}),
|
||||
);
|
||||
state.track_current_turn_event(
|
||||
@@ -3530,8 +3496,8 @@ mod tests {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}),
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: 0,
|
||||
started_at_ms: Some(42_321),
|
||||
completed_at_ms: 42_400,
|
||||
}),
|
||||
);
|
||||
state.track_current_turn_event(&event_turn_id, &EventMsg::TurnComplete(event.clone()));
|
||||
@@ -3554,7 +3520,7 @@ mod tests {
|
||||
assert_eq!(n.turn.items_view, TurnItemsView::Summary);
|
||||
assert!(matches!(
|
||||
&n.turn.items[..],
|
||||
[ThreadItem::AgentMessage { id, text, .. }]
|
||||
[ThreadItem { payload: ThreadItemPayload::AgentMessage { id, text, .. }, started_at_ms: Some(42_123), completed_at_ms: Some(42_200) }]
|
||||
if id == "msg-1" && text == "complete response"
|
||||
));
|
||||
assert_eq!(n.turn.error, None);
|
||||
|
||||
@@ -80,7 +80,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Result;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -120,8 +120,11 @@ fn server_notification_requires_delivery(notification: &ServerNotification) -> b
|
||||
| ServerNotification::ThreadAttachmentUpdated(_)
|
||||
| ServerNotification::ExternalAgentConfigImportCompleted(_)
|
||||
| ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
item: codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::AgentMessage {
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
..
|
||||
},
|
||||
..
|
||||
},
|
||||
..
|
||||
@@ -1084,8 +1087,10 @@ mod tests {
|
||||
})
|
||||
));
|
||||
assert!(server_notification_requires_delivery(
|
||||
&ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
&ServerNotification::ItemCompleted(ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "item-1".to_string(),
|
||||
text: "Still working".to_string(),
|
||||
phase: None,
|
||||
@@ -1093,10 +1098,9 @@ mod tests {
|
||||
delivery: Some(AgentMessageDelivery::Async),
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
})
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use codex_app_server_protocol::DynamicToolCallOutputContentItem;
|
||||
use codex_app_server_protocol::GeneratedImageRef;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -11,11 +11,11 @@ use codex_protocol::models::ResponseItem;
|
||||
pub(crate) fn without_notification_media(notification: ServerNotification) -> ServerNotification {
|
||||
match notification {
|
||||
ServerNotification::ItemStarted(mut notification) => {
|
||||
notification.item = without_thread_item_media(notification.item);
|
||||
notification.item.payload = without_thread_item_media(notification.item.payload);
|
||||
ServerNotification::ItemStarted(notification)
|
||||
}
|
||||
ServerNotification::ItemCompleted(mut notification) => {
|
||||
notification.item = without_thread_item_media(notification.item);
|
||||
notification.item.payload = without_thread_item_media(notification.item.payload);
|
||||
ServerNotification::ItemCompleted(notification)
|
||||
}
|
||||
ServerNotification::RawResponseItemCompleted(mut notification) => {
|
||||
@@ -142,9 +142,9 @@ pub(crate) fn without_notification_media(notification: ServerNotification) -> Se
|
||||
}
|
||||
}
|
||||
|
||||
fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem {
|
||||
fn without_thread_item_media(mut item: ThreadItemPayload) -> ThreadItemPayload {
|
||||
match &mut item {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
content.retain(|item| match item {
|
||||
UserInput::Image { .. } | UserInput::Audio { .. } => false,
|
||||
UserInput::Text { .. }
|
||||
@@ -154,7 +154,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem {
|
||||
| UserInput::Mention { .. } => true,
|
||||
});
|
||||
}
|
||||
ThreadItem::DynamicToolCall {
|
||||
ThreadItemPayload::DynamicToolCall {
|
||||
content_items: Some(content_items),
|
||||
..
|
||||
} => {
|
||||
@@ -164,7 +164,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem {
|
||||
DynamicToolCallOutputContentItem::InputText { .. } => true,
|
||||
});
|
||||
}
|
||||
ThreadItem::FunctionCallOutput {
|
||||
ThreadItemPayload::FunctionCallOutput {
|
||||
output: FunctionCallOutputBody::ContentItems(items),
|
||||
..
|
||||
} => {
|
||||
@@ -175,7 +175,7 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem {
|
||||
| FunctionCallOutputContentItem::EncryptedContent { .. } => true,
|
||||
});
|
||||
}
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
result: Some(result),
|
||||
..
|
||||
} => {
|
||||
@@ -191,34 +191,34 @@ fn without_thread_item_media(mut item: ThreadItem) -> ThreadItem {
|
||||
.is_none()
|
||||
});
|
||||
}
|
||||
ThreadItem::ImageGeneration(item) => {
|
||||
ThreadItemPayload::ImageGeneration(item) => {
|
||||
item.image = GeneratedImageRef::Inline {
|
||||
result: String::new(),
|
||||
};
|
||||
}
|
||||
ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::FunctionCallOutput {
|
||||
ThreadItemPayload::HookPrompt { .. }
|
||||
| ThreadItemPayload::AgentMessage { .. }
|
||||
| ThreadItemPayload::Plan { .. }
|
||||
| ThreadItemPayload::Reasoning { .. }
|
||||
| ThreadItemPayload::FunctionCallOutput {
|
||||
output: FunctionCallOutputBody::Text(_),
|
||||
..
|
||||
}
|
||||
| ThreadItem::CommandExecution { .. }
|
||||
| ThreadItem::FileChange { .. }
|
||||
| ThreadItem::McpToolCall { result: None, .. }
|
||||
| ThreadItem::DynamicToolCall {
|
||||
| ThreadItemPayload::CommandExecution { .. }
|
||||
| ThreadItemPayload::FileChange { .. }
|
||||
| ThreadItemPayload::McpToolCall { result: None, .. }
|
||||
| ThreadItemPayload::DynamicToolCall {
|
||||
content_items: None,
|
||||
..
|
||||
}
|
||||
| ThreadItem::CollabAgentToolCall { .. }
|
||||
| ThreadItem::SubAgentActivity { .. }
|
||||
| ThreadItem::WebSearch(_)
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::Sleep(_)
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
| ThreadItem::ContextCompaction { .. } => {}
|
||||
| ThreadItemPayload::CollabAgentToolCall { .. }
|
||||
| ThreadItemPayload::SubAgentActivity { .. }
|
||||
| ThreadItemPayload::WebSearch(_)
|
||||
| ThreadItemPayload::ImageView { .. }
|
||||
| ThreadItemPayload::Sleep(_)
|
||||
| ThreadItemPayload::EnteredReviewMode { .. }
|
||||
| ThreadItemPayload::ExitedReviewMode { .. }
|
||||
| ThreadItemPayload::ContextCompaction { .. } => {}
|
||||
}
|
||||
item
|
||||
}
|
||||
|
||||
@@ -229,8 +229,8 @@ use codex_app_server_protocol::ThreadIncrementElicitationParams;
|
||||
use codex_app_server_protocol::ThreadIncrementElicitationResponse;
|
||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||
use codex_app_server_protocol::ThreadInjectItemsResponse;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemEntry;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadItemsListParams;
|
||||
use codex_app_server_protocol::ThreadItemsListResponse;
|
||||
use codex_app_server_protocol::ThreadItemsReadParams;
|
||||
|
||||
@@ -271,6 +271,7 @@ pub(super) async fn ensure_listener_task_running(
|
||||
&conversation,
|
||||
watch_registration,
|
||||
thread_settings_baseline,
|
||||
config_snapshot.history_mode,
|
||||
);
|
||||
let Some(listener_command_tx) = thread_state.listener_command_tx() else {
|
||||
tracing::warn!(
|
||||
|
||||
@@ -19,6 +19,7 @@ use crate::thread_sample::SampleThread;
|
||||
use crate::thread_sample::generate_sample;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ThreadHistoryMode as ApiThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemsListAnchor;
|
||||
use codex_app_server_protocol::ThreadItemsListCursor;
|
||||
use codex_app_server_protocol::ThreadRevertParams;
|
||||
@@ -3515,8 +3516,8 @@ impl ThreadRequestProcessor {
|
||||
})
|
||||
.await
|
||||
.map_err(paginated_history_list_error)?;
|
||||
for item in page.items {
|
||||
items.push(deserialize_stored_thread_item(item)?);
|
||||
for item in &page.items {
|
||||
items.push(stored_thread_item_to_api_item(item)?);
|
||||
}
|
||||
let Some(next_cursor) = page.next_cursor else {
|
||||
return Ok(items);
|
||||
@@ -6366,29 +6367,31 @@ fn paginated_history_list_error(err: ThreadStoreError) -> JSONRPCErrorError {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_stored_thread_item(
|
||||
item: codex_thread_store::StoredThreadItem,
|
||||
fn stored_thread_item_to_api_item(
|
||||
stored_item: &codex_thread_store::StoredThreadItem,
|
||||
) -> Result<ThreadItem, JSONRPCErrorError> {
|
||||
serde_json::from_slice::<ThreadItem>(&item.item_json).map_err(|err| {
|
||||
let payload = serde_json::from_slice(&stored_item.item_json).map_err(|err| {
|
||||
internal_error(format!(
|
||||
"failed to deserialize stored thread item {}: {err}",
|
||||
item.item_id
|
||||
stored_item.item_id
|
||||
))
|
||||
})?;
|
||||
Ok(ThreadItem {
|
||||
payload,
|
||||
started_at_ms: stored_item.started_at_ms,
|
||||
completed_at_ms: stored_item.completed_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
fn stored_thread_item_to_entry(
|
||||
stored_item: codex_thread_store::StoredThreadItem,
|
||||
) -> Result<ThreadItemEntry, JSONRPCErrorError> {
|
||||
let turn_id = stored_item.turn_id.clone();
|
||||
let started_at_ms = stored_item.started_at_ms;
|
||||
let completed_at_ms = stored_item.completed_at_ms;
|
||||
let item = deserialize_stored_thread_item(stored_item)?;
|
||||
let item = stored_thread_item_to_api_item(&stored_item)?;
|
||||
Ok(ThreadItemEntry {
|
||||
turn_id,
|
||||
turn_id: stored_item.turn_id,
|
||||
started_at_ms: item.started_at_ms,
|
||||
completed_at_ms: item.completed_at_ms,
|
||||
item,
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6410,8 +6413,8 @@ fn stored_turn_to_api_turn(
|
||||
});
|
||||
let items = turn
|
||||
.items
|
||||
.into_iter()
|
||||
.map(deserialize_stored_thread_item)
|
||||
.iter()
|
||||
.map(stored_thread_item_to_api_item)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Turn {
|
||||
id: turn.turn_id,
|
||||
|
||||
@@ -153,6 +153,7 @@ mod thread_processor_behavior_tests {
|
||||
use chrono::Utc;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::LoaderOverrides;
|
||||
@@ -333,14 +334,14 @@ mod thread_processor_behavior_tests {
|
||||
let active_turn = Turn {
|
||||
id: "live-turn".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::UserMessage {
|
||||
id: "live-user-message".to_string(),
|
||||
client_id: None,
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "live".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}],
|
||||
})],
|
||||
items_view: TurnItemsView::Full,
|
||||
error: None,
|
||||
status: TurnStatus::InProgress,
|
||||
|
||||
@@ -112,8 +112,12 @@ pub(super) async fn publish(
|
||||
return None;
|
||||
}
|
||||
let has_error = summary.last_error.is_some();
|
||||
let final_text = match &summary.last_agent_message {
|
||||
Some(ThreadItem::AgentMessage { text, .. }) => Some(text.as_str()),
|
||||
let final_text = match summary
|
||||
.last_agent_message
|
||||
.as_ref()
|
||||
.map(|message| &message.payload)
|
||||
{
|
||||
Some(ThreadItemPayload::AgentMessage { text, .. }) => Some(text.as_str()),
|
||||
_ => None,
|
||||
};
|
||||
let visible = match &event.msg {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
use codex_app_server_protocol::GeneratedImageRef;
|
||||
use codex_app_server_protocol::McpToolCallResult;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
@@ -18,8 +18,8 @@ pub(super) fn should_redact_thread_resume_payloads(client_name: Option<&str>) ->
|
||||
|
||||
pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) {
|
||||
for turn in turns {
|
||||
turn.items.retain_mut(|item| match item {
|
||||
ThreadItem::McpToolCall {
|
||||
turn.items.retain_mut(|entry| match &mut entry.payload {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
arguments,
|
||||
result,
|
||||
error,
|
||||
@@ -34,7 +34,7 @@ pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) {
|
||||
}
|
||||
true
|
||||
}
|
||||
ThreadItem::ImageGeneration(_) => false,
|
||||
ThreadItemPayload::ImageGeneration(_) => false,
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
@@ -60,6 +60,7 @@ mod tests {
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::SessionSource;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadStatus;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
@@ -70,7 +71,7 @@ mod tests {
|
||||
#[test]
|
||||
fn redacts_mcp_success_result_and_removes_image_generation() {
|
||||
let mut thread = test_thread(vec![
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "kept".to_string(),
|
||||
phase: None,
|
||||
@@ -78,7 +79,7 @@ mod tests {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "docs".to_string(),
|
||||
tool: "lookup".to_string(),
|
||||
@@ -106,7 +107,7 @@ mod tests {
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
},
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
id: "ig-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("revised".to_string()),
|
||||
@@ -121,13 +122,16 @@ mod tests {
|
||||
generation_id: None,
|
||||
}),
|
||||
]);
|
||||
thread.turns[0].items[1].started_at_ms = Some(123);
|
||||
thread.turns[0].items[2].started_at_ms = Some(456);
|
||||
|
||||
redact_thread_resume_payloads(&mut thread.turns);
|
||||
|
||||
assert_eq!(thread.turns[0].items.len(), 2);
|
||||
assert_eq!(thread.turns[0].items[1].started_at_ms, Some(123));
|
||||
assert_eq!(
|
||||
thread.turns[0].items[0],
|
||||
ThreadItem::AgentMessage {
|
||||
thread.turns[0].items[0].payload,
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "kept".to_string(),
|
||||
phase: None,
|
||||
@@ -137,8 +141,8 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
thread.turns[0].items[1],
|
||||
ThreadItem::McpToolCall {
|
||||
thread.turns[0].items[1].payload,
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "docs".to_string(),
|
||||
tool: "lookup".to_string(),
|
||||
@@ -164,7 +168,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn redacts_mcp_error_message() {
|
||||
let mut thread = test_thread(vec![ThreadItem::McpToolCall {
|
||||
let mut thread = test_thread(vec![ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "docs".to_string(),
|
||||
tool: "lookup".to_string(),
|
||||
@@ -185,8 +189,8 @@ mod tests {
|
||||
redact_thread_resume_payloads(&mut thread.turns);
|
||||
|
||||
assert_eq!(
|
||||
thread.turns[0].items[0],
|
||||
ThreadItem::McpToolCall {
|
||||
thread.turns[0].items[0].payload,
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "docs".to_string(),
|
||||
tool: "lookup".to_string(),
|
||||
@@ -206,7 +210,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn test_thread(items: Vec<ThreadItem>) -> Thread {
|
||||
fn test_thread(items: Vec<ThreadItemPayload>) -> Thread {
|
||||
Thread {
|
||||
originator: None,
|
||||
environments: None,
|
||||
@@ -242,7 +246,7 @@ mod tests {
|
||||
turns: vec![Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items,
|
||||
items: items.into_iter().map(ThreadItem::untimed).collect(),
|
||||
items_view: TurnItemsView::Full,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_agent_extension::AgentInvocation;
|
||||
use codex_agent_extension::AgentRun;
|
||||
use codex_agent_extension::AgentRunner;
|
||||
use codex_app_server_protocol::ImageReference as V2ImageReference;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
@@ -1412,7 +1413,7 @@ impl TurnRequestProcessor {
|
||||
let items = if display_text.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![ThreadItem::UserMessage {
|
||||
vec![ThreadItem::untimed(ThreadItemPayload::UserMessage {
|
||||
id: turn_id.clone(),
|
||||
client_id: None,
|
||||
content: vec![V2UserInput::Text {
|
||||
@@ -1420,7 +1421,7 @@ impl TurnRequestProcessor {
|
||||
// Review prompt display text is synthesized; no UI element ranges to preserve.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
})]
|
||||
};
|
||||
|
||||
Turn {
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_rollout::RolloutItem;
|
||||
use codex_rollout::state_db::StateDbHandle;
|
||||
use codex_utils_path_uri::LegacyAppPathString;
|
||||
@@ -95,11 +96,24 @@ pub(crate) enum ThreadListenerCommand {
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct TurnSummary {
|
||||
pub(crate) started_at: Option<i64>,
|
||||
pub(crate) command_execution_started: HashSet<String>,
|
||||
pub(crate) command_start_times: HashMap<String, i64>,
|
||||
pub(crate) last_error: Option<TurnError>,
|
||||
pub(crate) last_agent_message: Option<ThreadItem>,
|
||||
}
|
||||
|
||||
impl TurnSummary {
|
||||
pub(crate) fn record_command_start(&mut self, item_id: String, started_at_ms: i64) -> bool {
|
||||
if let std::collections::hash_map::Entry::Vacant(entry) =
|
||||
self.command_start_times.entry(item_id)
|
||||
{
|
||||
entry.insert(started_at_ms);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ThreadState {
|
||||
goal_resume_lock: Arc<Mutex<()>>,
|
||||
@@ -115,6 +129,7 @@ pub(crate) struct ThreadState {
|
||||
last_thread_settings: Option<ThreadSettings>,
|
||||
listener_command_tx: Option<mpsc::UnboundedSender<ThreadListenerCommand>>,
|
||||
current_turn_history: ThreadHistoryBuilder,
|
||||
history_mode: ThreadHistoryMode,
|
||||
listener_thread: Option<Weak<CodexThread>>,
|
||||
watch_registration: WatchRegistration,
|
||||
}
|
||||
@@ -133,12 +148,14 @@ impl ThreadState {
|
||||
conversation: &Arc<CodexThread>,
|
||||
watch_registration: WatchRegistration,
|
||||
thread_settings_baseline: ThreadSettings,
|
||||
history_mode: ThreadHistoryMode,
|
||||
) -> (mpsc::UnboundedReceiver<ThreadListenerCommand>, u64) {
|
||||
if let Some(previous) = self.cancel_tx.replace(cancel_tx) {
|
||||
let _ = previous.send(());
|
||||
}
|
||||
self.listener_generation = self.listener_generation.wrapping_add(1);
|
||||
self.last_thread_settings = Some(thread_settings_baseline);
|
||||
self.history_mode = history_mode;
|
||||
let (listener_command_tx, listener_command_rx) = mpsc::unbounded_channel();
|
||||
self.listener_command_tx = Some(listener_command_tx);
|
||||
self.listener_thread = Some(Arc::downgrade(conversation));
|
||||
@@ -209,10 +226,10 @@ impl ThreadState {
|
||||
matches!(content, CoreAgentMessageContent::Text { text } if !text.trim().is_empty())
|
||||
})
|
||||
{
|
||||
self.turn_summary.last_agent_message =
|
||||
Some(ThreadItem::from(CoreTurnItem::AgentMessage(item.clone())));
|
||||
self.turn_summary.last_agent_message = Some(ThreadItem::from(payload));
|
||||
}
|
||||
self.current_turn_history.handle_event(event);
|
||||
self.current_turn_history
|
||||
.handle_event_for_history_mode(event, self.history_mode);
|
||||
if matches!(event, EventMsg::TurnAborted(_) | EventMsg::TurnComplete(_)) {
|
||||
self.last_terminal_turn_id = Some(event_turn_id.to_string());
|
||||
if !self.current_turn_history.has_active_turn() {
|
||||
|
||||
@@ -15,7 +15,7 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SandboxPolicy;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -439,8 +439,8 @@ async fn multi_agent_v2_tools_emit_collaborator_analytics() -> Result<()> {
|
||||
.items;
|
||||
let child_thread_id = items
|
||||
.iter()
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::SubAgentActivity {
|
||||
.find_map(|item| match &item.payload {
|
||||
ThreadItemPayload::SubAgentActivity {
|
||||
id,
|
||||
agent_thread_id,
|
||||
..
|
||||
@@ -451,12 +451,14 @@ async fn multi_agent_v2_tools_emit_collaborator_analytics() -> Result<()> {
|
||||
assert!(
|
||||
!items
|
||||
.iter()
|
||||
.any(|item| matches!(item, ThreadItem::CollabAgentToolCall { .. }))
|
||||
.any(|item| matches!(&item.payload, ThreadItemPayload::CollabAgentToolCall { .. }))
|
||||
);
|
||||
let activity_ids = items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ThreadItem::SubAgentActivity { id, .. } if id.starts_with("call-") => Some(id.as_str()),
|
||||
.filter_map(|item| match &item.payload {
|
||||
ThreadItemPayload::SubAgentActivity { id, .. } if id.starts_with("call-") => {
|
||||
Some(id.as_str())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -22,7 +22,7 @@ use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -427,10 +427,10 @@ async fn auto_compaction_emits_started_and_completed_items(route: CompactionRout
|
||||
let started = wait_for_context_compaction_started(&mut mcp).await?;
|
||||
let completed = wait_for_context_compaction_completed(&mut mcp).await?;
|
||||
|
||||
let ThreadItem::ContextCompaction { id: started_id } = started.item else {
|
||||
let ThreadItemPayload::ContextCompaction { id: started_id } = started.item.payload else {
|
||||
unreachable!("started item should be context compaction");
|
||||
};
|
||||
let ThreadItem::ContextCompaction { id: completed_id } = completed.item else {
|
||||
let ThreadItemPayload::ContextCompaction { id: completed_id } = completed.item.payload else {
|
||||
unreachable!("completed item should be context compaction");
|
||||
};
|
||||
|
||||
@@ -548,10 +548,10 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
|
||||
let completed = wait_for_context_compaction_completed(&mut mcp).await?;
|
||||
wait_for_turn_completed(&mut mcp, &started.turn_id).await?;
|
||||
|
||||
let ThreadItem::ContextCompaction { id: started_id } = started.item else {
|
||||
let ThreadItemPayload::ContextCompaction { id: started_id } = started.item.payload else {
|
||||
unreachable!("started item should be context compaction");
|
||||
};
|
||||
let ThreadItem::ContextCompaction { id: completed_id } = completed.item else {
|
||||
let ThreadItemPayload::ContextCompaction { id: completed_id } = completed.item.payload else {
|
||||
unreachable!("completed item should be context compaction");
|
||||
};
|
||||
|
||||
@@ -723,7 +723,7 @@ async fn wait_for_context_compaction_started(
|
||||
loop {
|
||||
let started: ItemStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??;
|
||||
if let ThreadItem::ContextCompaction { .. } = started.item {
|
||||
if let ThreadItemPayload::ContextCompaction { .. } = started.item.payload {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
@@ -738,7 +738,7 @@ async fn wait_for_context_compaction_completed(
|
||||
mcp.read_notification("item/completed"),
|
||||
)
|
||||
.await??;
|
||||
if let ThreadItem::ContextCompaction { .. } = completed.item {
|
||||
if let ThreadItemPayload::ContextCompaction { .. } = completed.item.payload {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -125,12 +125,12 @@ decision = "prompt"
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
app.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CommandExecution {
|
||||
if let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
status,
|
||||
exit_code,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
&& id == call_id
|
||||
{
|
||||
assert_eq!(
|
||||
|
||||
@@ -9,7 +9,7 @@ use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::McpServerToolCallParams;
|
||||
use codex_app_server_protocol::McpServerToolCallResponse;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -185,9 +185,9 @@ async fn daybreak_access_respects_plugin_provenance(case: AccessCall) -> Result<
|
||||
.await?;
|
||||
loop {
|
||||
let event: ItemCompletedNotification = app.read_notification("item/completed").await?;
|
||||
if let ThreadItem::McpToolCall {
|
||||
if let ThreadItemPayload::McpToolCall {
|
||||
result, plugin_id, ..
|
||||
} = event.item
|
||||
} = event.item.payload
|
||||
{
|
||||
assert_eq!(plugin_id.as_deref(), Some("selected-daybreak"));
|
||||
results.push((
|
||||
|
||||
@@ -20,7 +20,7 @@ use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -448,7 +448,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
let started = wait_for_dynamic_tool_started(&mut mcp, call_id).await?;
|
||||
assert_eq!(started.thread_id, thread_id);
|
||||
assert_eq!(started.turn_id, turn_id.clone());
|
||||
let ThreadItem::DynamicToolCall {
|
||||
let ThreadItemPayload::DynamicToolCall {
|
||||
id,
|
||||
namespace,
|
||||
tool,
|
||||
@@ -457,7 +457,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
content_items,
|
||||
success,
|
||||
duration_ms,
|
||||
} = started.item
|
||||
} = started.item.payload
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
@@ -504,7 +504,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, thread_id);
|
||||
assert_eq!(completed.turn_id, turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
let ThreadItemPayload::DynamicToolCall {
|
||||
id,
|
||||
namespace,
|
||||
tool,
|
||||
@@ -513,7 +513,7 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
content_items,
|
||||
success,
|
||||
duration_ms,
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
@@ -735,12 +735,12 @@ async fn dynamic_tool_call_round_trip_handles_content_items() -> Result<()> {
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, params.thread_id);
|
||||
assert_eq!(completed.turn_id, params.turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
let ThreadItemPayload::DynamicToolCall {
|
||||
status,
|
||||
content_items: completed_content_items,
|
||||
success,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
@@ -830,12 +830,12 @@ async fn dynamic_tool_remote_image_response_becomes_model_visible_error() -> Res
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, params.thread_id);
|
||||
assert_eq!(completed.turn_id, params.turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
let ThreadItemPayload::DynamicToolCall {
|
||||
status,
|
||||
content_items,
|
||||
success,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
@@ -886,12 +886,12 @@ async fn dynamic_tool_remote_audio_response_becomes_model_visible_error() -> Res
|
||||
let completed = wait_for_dynamic_tool_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.thread_id, params.thread_id);
|
||||
assert_eq!(completed.turn_id, params.turn_id);
|
||||
let ThreadItem::DynamicToolCall {
|
||||
let ThreadItemPayload::DynamicToolCall {
|
||||
status,
|
||||
content_items,
|
||||
success,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected dynamic tool call item");
|
||||
};
|
||||
@@ -978,7 +978,8 @@ async fn wait_for_dynamic_tool_started(
|
||||
continue;
|
||||
};
|
||||
let started: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) {
|
||||
if matches!(&started.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id)
|
||||
{
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
@@ -998,7 +999,8 @@ async fn wait_for_dynamic_tool_completed(
|
||||
continue;
|
||||
};
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(params)?;
|
||||
if matches!(&completed.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) {
|
||||
if matches!(&completed.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id)
|
||||
{
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
@@ -645,10 +645,10 @@ source = {:?}
|
||||
let response: ThreadReadResponse =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
assert_eq!(response.thread.turns.len(), 1);
|
||||
let imported_items = &response.thread.turns[0].items;
|
||||
let mut imported_items = response.thread.turns[0].item_payloads();
|
||||
assert_eq!(imported_items.len(), 3);
|
||||
match &imported_items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => assert_eq!(
|
||||
match imported_items.next() {
|
||||
Some(ThreadItemPayload::UserMessage { content, .. }) => assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
text: "first request".to_string(),
|
||||
@@ -657,8 +657,8 @@ source = {:?}
|
||||
),
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
match &imported_items[1] {
|
||||
ThreadItem::AgentMessage { text, .. } => assert_eq!(text, "first answer"),
|
||||
match imported_items.next() {
|
||||
Some(ThreadItemPayload::AgentMessage { text, .. }) => assert_eq!(text, "first answer"),
|
||||
other => panic!("expected agent message item, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -2108,10 +2108,10 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
let response: ThreadReadResponse =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
assert_eq!(response.thread.turns.len(), 2);
|
||||
let control_items = &response.thread.turns[0].items;
|
||||
let mut control_items = response.thread.turns[0].item_payloads();
|
||||
assert_eq!(control_items.len(), 1);
|
||||
match &control_items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match control_items.next() {
|
||||
Some(ThreadItemPayload::UserMessage { content, .. }) => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
@@ -2122,10 +2122,10 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
}
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
let imported_items = &response.thread.turns[1].items;
|
||||
let mut imported_items = response.thread.turns[1].item_payloads();
|
||||
assert_eq!(imported_items.len(), 3);
|
||||
match &imported_items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match imported_items.next() {
|
||||
Some(ThreadItemPayload::UserMessage { content, .. }) => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
@@ -2137,8 +2137,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
imported_items.last(),
|
||||
Some(&ThreadItem::AgentMessage {
|
||||
imported_items.next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
id: "item-4".into(),
|
||||
text: "<EXTERNAL SESSION IMPORTED>".into(),
|
||||
phase: None,
|
||||
@@ -2183,8 +2183,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
let response: ThreadReadResponse =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
|
||||
assert_eq!(response.thread.turns.len(), 3);
|
||||
match &response.thread.turns[2].items[1] {
|
||||
ThreadItem::AgentMessage { text, .. } => assert_eq!(text, "follow-up answer"),
|
||||
match response.thread.turns[2].item_payloads().nth(/*n*/ 1) {
|
||||
Some(ThreadItemPayload::AgentMessage { text, .. }) => assert_eq!(text, "follow-up answer"),
|
||||
other => panic!("expected agent message item, got {other:?}"),
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ use codex_app_server_protocol::ExternalAgentConfigDetectResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportCompletedNotification;
|
||||
use codex_app_server_protocol::ExternalAgentConfigImportResponse;
|
||||
use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -229,16 +230,18 @@ async fn start_app_server(codex_home: &Path) -> Result<TestAppServer> {
|
||||
fn assert_history(response: &ThreadReadResponse, expected: &[(&str, &str)]) {
|
||||
let mut actual = Vec::new();
|
||||
let mut markers = 0;
|
||||
for item in response.thread.turns.iter().flat_map(|turn| &turn.items) {
|
||||
for item in response.thread.turns.iter().flat_map(Turn::item_payloads) {
|
||||
match item {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
actual.extend(content.iter().filter_map(|input| match input {
|
||||
UserInput::Text { text, .. } => Some(("user", text.as_str())),
|
||||
_ => None,
|
||||
}));
|
||||
}
|
||||
ThreadItem::AgentMessage { text, .. } if text == IMPORT_MARKER => markers += 1,
|
||||
ThreadItem::AgentMessage { text, .. } => actual.push(("assistant", text.as_str())),
|
||||
ThreadItemPayload::AgentMessage { text, .. } if text == IMPORT_MARKER => markers += 1,
|
||||
ThreadItemPayload::AgentMessage { text, .. } => {
|
||||
actual.push(("assistant", text.as_str()))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -2650,12 +2650,12 @@ async fn user_approval_skips_async_guardian_without_changing_other_modes() -> Re
|
||||
while tool_results.len() < 3 {
|
||||
let completed: ItemCompletedNotification =
|
||||
timeout(TIMEOUT, app_server.read_notification("item/completed")).await??;
|
||||
if let ThreadItem::McpToolCall {
|
||||
if let ThreadItemPayload::McpToolCall {
|
||||
status,
|
||||
result,
|
||||
error,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
{
|
||||
tool_results.push((status, result.map(|result| result.content), error));
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ use codex_app_server_protocol::ImageGenerationInputImage;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::ImageReference as V2ImageReference;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
@@ -161,14 +162,14 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model(
|
||||
)
|
||||
.await??;
|
||||
|
||||
let ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
let ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
status,
|
||||
revised_prompt,
|
||||
image,
|
||||
transparent_background,
|
||||
saved_path: Some(saved_path),
|
||||
..
|
||||
}) = completed.item
|
||||
}) = completed.item.payload
|
||||
else {
|
||||
panic!("expected completed image generation item with saved path");
|
||||
};
|
||||
@@ -353,13 +354,13 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history(
|
||||
)
|
||||
.await??;
|
||||
let thread_id = completed.thread_id.clone();
|
||||
let ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
let ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
status,
|
||||
image,
|
||||
transparent_background,
|
||||
input_images,
|
||||
..
|
||||
}) = completed.item
|
||||
}) = completed.item.payload
|
||||
else {
|
||||
panic!("expected completed image-generation item");
|
||||
};
|
||||
@@ -390,9 +391,9 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history(
|
||||
let persisted_image = thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn.items.iter())
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::ImageGeneration(item) => Some(item),
|
||||
ThreadItemPayload::ImageGeneration(item) => Some(item),
|
||||
_ => None,
|
||||
})
|
||||
.context("persisted legacy history should contain the generated image")?;
|
||||
@@ -410,9 +411,9 @@ async fn transparent_image_preserves_output_metadata_and_persisted_history(
|
||||
let resumed_image = thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn.items.iter())
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::ImageGeneration(item) => Some(item),
|
||||
ThreadItemPayload::ImageGeneration(item) => Some(item),
|
||||
_ => None,
|
||||
})
|
||||
.context("resumed legacy history should contain the generated image")?;
|
||||
@@ -471,7 +472,7 @@ async fn automatic_image_background_preserves_unknown_transparency() -> Result<(
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
let ThreadItem::ImageGeneration(image) = completed.item else {
|
||||
let ThreadItemPayload::ImageGeneration(image) = completed.item.payload else {
|
||||
panic!("expected completed image-generation item");
|
||||
};
|
||||
assert_eq!(image.transparent_background, None);
|
||||
@@ -630,8 +631,8 @@ async fn standalone_image_generation_failure_emits_terminal_item(
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
completed.item,
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
completed.item.payload,
|
||||
ThreadItemPayload::ImageGeneration(ImageGenerationItem {
|
||||
id: call_id.to_string(),
|
||||
status: "failed".to_string(),
|
||||
revised_prompt: Some("paint a blue whale".to_string()),
|
||||
@@ -744,7 +745,7 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() ->
|
||||
)
|
||||
.await??;
|
||||
let thread_id = completed.thread_id.clone();
|
||||
let ThreadItem::ImageGeneration(image) = completed.item else {
|
||||
let ThreadItemPayload::ImageGeneration(image) = completed.item.payload else {
|
||||
panic!("expected failed image-generation item");
|
||||
};
|
||||
assert_eq!(image.status, "failed");
|
||||
@@ -772,9 +773,9 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() ->
|
||||
let persisted_failure = thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn.items.iter())
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::ImageGeneration(item) => item.failure.as_ref(),
|
||||
ThreadItemPayload::ImageGeneration(item) => item.failure.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -796,9 +797,9 @@ async fn image_generation_usage_limit_preserves_correlated_failure_metadata() ->
|
||||
let resumed_failure = thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn.items.iter())
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::ImageGeneration(item) => item.failure.as_ref(),
|
||||
ThreadItemPayload::ImageGeneration(item) => item.failure.as_ref(),
|
||||
_ => None,
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1157,7 +1158,7 @@ async fn run_image_edit_test(
|
||||
wait_for_image_generation_completed(&mut mcp),
|
||||
)
|
||||
.await??;
|
||||
let ThreadItem::ImageGeneration(completed_image) = completed.item else {
|
||||
let ThreadItemPayload::ImageGeneration(completed_image) = completed.item.payload else {
|
||||
panic!("expected completed image-generation item");
|
||||
};
|
||||
timeout(
|
||||
@@ -1240,7 +1241,10 @@ async fn wait_for_image_generation_completed(
|
||||
) -> Result<ItemCompletedNotification> {
|
||||
loop {
|
||||
let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?;
|
||||
if matches!(&completed.item, ThreadItem::ImageGeneration(_)) {
|
||||
if matches!(
|
||||
&completed.item.payload,
|
||||
ThreadItemPayload::ImageGeneration(_)
|
||||
) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnEnvironmentParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
@@ -388,10 +389,10 @@ async fn model_mcp_tool_call_uses_session_client_extensions() -> Result<()> {
|
||||
.await?;
|
||||
|
||||
let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?;
|
||||
let ThreadItem::McpToolCall {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
result: Some(result),
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected completed MCP tool call item");
|
||||
};
|
||||
@@ -1053,7 +1054,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result()
|
||||
let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.turn_id, turn.id);
|
||||
|
||||
let ThreadItem::McpToolCall {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
id,
|
||||
server,
|
||||
tool,
|
||||
@@ -1061,7 +1062,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result()
|
||||
result: Some(result),
|
||||
error,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
panic!("expected completed MCP tool call item");
|
||||
};
|
||||
@@ -1081,7 +1082,7 @@ async fn mcp_tool_call_completion_notification_contains_truncated_large_result()
|
||||
assert!(text.contains("truncated"));
|
||||
assert!(text.len() < DEFAULT_OUTPUT_BYTES_CAP + 1024);
|
||||
|
||||
let serialized_item = serde_json::to_string(&ThreadItem::McpToolCall {
|
||||
let serialized_item = serde_json::to_string(&ThreadItemPayload::McpToolCall {
|
||||
id,
|
||||
server,
|
||||
tool,
|
||||
@@ -1193,7 +1194,7 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result
|
||||
..
|
||||
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??;
|
||||
|
||||
let expected_item = ThreadItem::McpToolCall {
|
||||
let expected_item = ThreadItemPayload::McpToolCall {
|
||||
id: call_id.to_string(),
|
||||
server: TEST_SERVER_NAME.to_string(),
|
||||
tool: TEST_TOOL_NAME.to_string(),
|
||||
@@ -1211,8 +1212,8 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result
|
||||
let resumed_item = resumed_thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| &turn.items)
|
||||
.find(|item| matches!(item, ThreadItem::McpToolCall { id, .. } if id == call_id))
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find(|item| matches!(item, ThreadItemPayload::McpToolCall { id, .. } if id == call_id))
|
||||
.expect("resumed thread should include the in-progress MCP tool call");
|
||||
assert_eq!(resumed_item, &expected_item);
|
||||
|
||||
@@ -1228,11 +1229,11 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result
|
||||
|
||||
let completed = wait_for_mcp_tool_call_completed(&mut mcp, call_id).await?;
|
||||
assert_eq!(completed.turn_id, turn.id);
|
||||
let ThreadItem::McpToolCall {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
status,
|
||||
read_only_hint,
|
||||
..
|
||||
} = &completed.item
|
||||
} = &completed.item.payload
|
||||
else {
|
||||
panic!("expected the completed MCP tool call item");
|
||||
};
|
||||
@@ -1258,10 +1259,10 @@ async fn mcp_tool_call_hint_survives_mid_call_thread_read_and_resume() -> Result
|
||||
let persisted_item = completed_read
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| &turn.items)
|
||||
.find(|item| matches!(item, ThreadItem::McpToolCall { id, .. } if id == call_id))
|
||||
.flat_map(Turn::item_payloads)
|
||||
.find(|item| matches!(item, ThreadItemPayload::McpToolCall { id, .. } if id == call_id))
|
||||
.expect("completed thread history should include the persisted MCP tool call");
|
||||
assert_eq!(persisted_item, &completed.item);
|
||||
assert_eq!(persisted_item, &completed.item.payload);
|
||||
|
||||
mcp_server_handle.abort();
|
||||
let _ = mcp_server_handle.await;
|
||||
@@ -1608,7 +1609,8 @@ async fn wait_for_mcp_tool_call_completed(
|
||||
mcp.read_notification("item/completed"),
|
||||
)
|
||||
.await??;
|
||||
if matches!(&completed.item, ThreadItem::McpToolCall { id, .. } if id == call_id) {
|
||||
if matches!(&completed.item.payload, ThreadItemPayload::McpToolCall { id, .. } if id == call_id)
|
||||
{
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::PlanDeltaNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -63,7 +63,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> {
|
||||
assert_eq!(turn_completed.turn.id, turn.id);
|
||||
assert_eq!(turn_completed.turn.status, TurnStatus::Completed);
|
||||
|
||||
let expected_plan = ThreadItem::Plan {
|
||||
let expected_plan = ThreadItemPayload::Plan {
|
||||
id: format!("{}-plan", turn.id),
|
||||
text: "# Final plan\n- first\n- second\n".to_string(),
|
||||
};
|
||||
@@ -81,7 +81,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> {
|
||||
let plan_items = completed_items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
ThreadItem::Plan { .. } => Some(item.clone()),
|
||||
ThreadItemPayload::Plan { .. } => Some(item.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -89,7 +89,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> {
|
||||
assert!(
|
||||
completed_items
|
||||
.iter()
|
||||
.any(|item| matches!(item, ThreadItem::AgentMessage { .. })),
|
||||
.any(|item| matches!(item, ThreadItemPayload::AgentMessage { .. })),
|
||||
"agent message items should still be emitted alongside the plan item"
|
||||
);
|
||||
|
||||
@@ -123,7 +123,7 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()>
|
||||
|
||||
let has_plan_item = completed_items
|
||||
.iter()
|
||||
.any(|item| matches!(item, ThreadItem::Plan { .. }));
|
||||
.any(|item| matches!(item, ThreadItemPayload::Plan { .. }));
|
||||
assert!(!has_plan_item);
|
||||
assert!(plan_deltas.is_empty());
|
||||
|
||||
@@ -168,8 +168,8 @@ async fn start_plan_mode_turn(mcp: &mut TestAppServer) -> Result<codex_app_serve
|
||||
async fn collect_turn_notifications(
|
||||
mcp: &mut TestAppServer,
|
||||
) -> Result<(
|
||||
Vec<ThreadItem>,
|
||||
Vec<ThreadItem>,
|
||||
Vec<ThreadItemPayload>,
|
||||
Vec<ThreadItemPayload>,
|
||||
Vec<PlanDeltaNotification>,
|
||||
TurnCompletedNotification,
|
||||
)> {
|
||||
@@ -188,14 +188,14 @@ async fn collect_turn_notifications(
|
||||
.params
|
||||
.ok_or_else(|| anyhow!("item/started notifications must include params"))?;
|
||||
let payload: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
started_items.push(payload.item);
|
||||
started_items.push(payload.item.payload);
|
||||
}
|
||||
"item/completed" => {
|
||||
let params = notification
|
||||
.params
|
||||
.ok_or_else(|| anyhow!("item/completed notifications must include params"))?;
|
||||
let payload: ItemCompletedNotification = serde_json::from_value(params)?;
|
||||
completed_items.push(payload.item);
|
||||
completed_items.push(payload.item.payload);
|
||||
}
|
||||
"item/plan/delta" => {
|
||||
let params = notification
|
||||
|
||||
@@ -14,7 +14,7 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadItemsListParams;
|
||||
use codex_app_server_protocol::ThreadItemsListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
@@ -56,6 +56,7 @@ use codex_app_server_protocol::ThreadTimelineListParams;
|
||||
use codex_app_server_protocol::ThreadTimelineListResponse;
|
||||
use codex_app_server_protocol::ThreadTurnsListParams;
|
||||
use codex_app_server_protocol::ThreadTurnsListResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
@@ -1285,8 +1286,8 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa
|
||||
.read_notification::<ItemCompletedNotification>("item/completed")
|
||||
.await?;
|
||||
assert!(matches!(
|
||||
initial_message.item,
|
||||
ThreadItem::UserMessage { .. }
|
||||
initial_message.item.payload,
|
||||
ThreadItemPayload::UserMessage { .. }
|
||||
));
|
||||
|
||||
harness
|
||||
@@ -1397,8 +1398,8 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa
|
||||
item,
|
||||
..
|
||||
} if matches!(
|
||||
item.as_ref(),
|
||||
ThreadItem::UserMessage { client_id, .. }
|
||||
&item.payload,
|
||||
ThreadItemPayload::UserMessage { client_id, .. }
|
||||
if client_id.as_deref() == Some("accepted-steer")
|
||||
)
|
||||
)
|
||||
@@ -1411,7 +1412,7 @@ async fn realtime_timeline_splits_accepted_steering_and_persists_promoted_artifa
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
ThreadTimelineEntry::Item { item, .. }
|
||||
if matches!(item.as_ref(), ThreadItem::AgentMessage { id, .. } if id == "promoted-message") => Some("artifact"),
|
||||
if matches!(&item.payload, ThreadItemPayload::AgentMessage { id, .. } if id == "promoted-message") => Some("artifact"),
|
||||
ThreadTimelineEntry::Realtime {
|
||||
item: ThreadRealtimeItem {
|
||||
content: ThreadRealtimeItemContent::BemItemPromoted { item_id, .. },
|
||||
@@ -1883,7 +1884,7 @@ async fn realtime_conversation_stop_emits_closed_notification(
|
||||
let items: ThreadItemsListResponse =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request)).await??;
|
||||
let tails = items.data.iter().filter(|entry| matches!(
|
||||
&entry.item, ThreadItem::UserMessage { content, .. }
|
||||
&entry.item.payload, ThreadItemPayload::UserMessage { content, .. }
|
||||
if content.iter().any(|part| matches!(part, V2UserInput::Text { text, .. }
|
||||
if text.contains("<source>transcript_tail_flush</source>") && text.contains("user: final words")))
|
||||
)).collect::<Vec<_>>();
|
||||
@@ -1913,8 +1914,8 @@ async fn realtime_conversation_stop_emits_closed_notification(
|
||||
.await?;
|
||||
let history: ThreadReadResponse =
|
||||
timeout(DEFAULT_TIMEOUT, mcp.read_response(request)).await??;
|
||||
assert_eq!(history.thread.turns.iter().flat_map(|turn| &turn.items).filter(|item| matches!(
|
||||
item, ThreadItem::UserMessage { content, .. }
|
||||
assert_eq!(history.thread.turns.iter().flat_map(Turn::item_payloads).filter(|item| matches!(
|
||||
item, ThreadItemPayload::UserMessage { content, .. }
|
||||
if content.iter().any(|part| matches!(part, V2UserInput::Text { text, .. }
|
||||
if text.contains("<source>transcript_tail_flush</source>") && text.contains("user: final words")))
|
||||
)).count(), 1);
|
||||
@@ -3963,7 +3964,8 @@ async fn websocket_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Resul
|
||||
|
||||
// Phase 2: observe the delegated background agent turn executing the requested shell command.
|
||||
let started_command = wait_for_started_command_execution(&mut harness.mcp).await?;
|
||||
let ThreadItem::CommandExecution { id, status, .. } = started_command.item else {
|
||||
let ThreadItemPayload::CommandExecution { id, status, .. } = started_command.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution items");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -3972,12 +3974,12 @@ async fn websocket_v2_tool_call_delegated_turn_can_execute_shell_tool() -> Resul
|
||||
);
|
||||
|
||||
let completed_command = wait_for_completed_command_execution(&mut harness.mcp).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
status,
|
||||
aggregated_output,
|
||||
..
|
||||
} = completed_command.item
|
||||
} = completed_command.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution items");
|
||||
};
|
||||
@@ -4298,7 +4300,7 @@ async fn wait_for_started_command_execution(
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started = read_notification::<ItemStartedNotification>(mcp, "item/started").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = &started.item {
|
||||
if let ThreadItemPayload::CommandExecution { .. } = &started.item.payload {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
@@ -4310,7 +4312,7 @@ async fn wait_for_completed_command_execution(
|
||||
loop {
|
||||
let completed =
|
||||
read_notification::<ItemCompletedNotification>(mcp, "item/completed").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = &completed.item {
|
||||
if let ThreadItemPayload::CommandExecution { .. } = &completed.item.payload {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_app_server_protocol::ReviewTarget;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadStartedNotification;
|
||||
@@ -148,14 +149,14 @@ async fn review_start_runs_review_turn_and_emits_code_review_item(
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
[ThreadItem::untimed(ThreadItemPayload::UserMessage {
|
||||
id: turn_id.clone(),
|
||||
client_id: None,
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "commit 1234567: Tidy UI colors".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
})]
|
||||
);
|
||||
|
||||
// Confirm we see the EnteredReviewMode marker on the main thread.
|
||||
@@ -163,8 +164,8 @@ async fn review_start_runs_review_turn_and_emits_code_review_item(
|
||||
for _ in 0..10 {
|
||||
let started: ItemStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??;
|
||||
match started.item {
|
||||
ThreadItem::EnteredReviewMode { review, .. } => {
|
||||
match started.item.payload {
|
||||
ThreadItemPayload::EnteredReviewMode { review, .. } => {
|
||||
assert_eq!(started.turn_id, turn_id);
|
||||
assert_eq!(review, "commit 1234567: Tidy UI colors");
|
||||
saw_entered_review_mode = true;
|
||||
@@ -187,8 +188,8 @@ async fn review_start_runs_review_turn_and_emits_code_review_item(
|
||||
mcp.read_notification("item/completed"),
|
||||
)
|
||||
.await??;
|
||||
match completed.item {
|
||||
ThreadItem::ExitedReviewMode { review, .. } => {
|
||||
match completed.item.payload {
|
||||
ThreadItemPayload::ExitedReviewMode { review, .. } => {
|
||||
assert_eq!(completed.turn_id, turn_id);
|
||||
review_body = Some(review);
|
||||
break;
|
||||
@@ -257,14 +258,14 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() ->
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
[ThreadItem::untimed(ThreadItemPayload::UserMessage {
|
||||
id: turn_id.clone(),
|
||||
client_id: None,
|
||||
content: vec![V2UserInput::Text {
|
||||
text: "commit 1234567: Check review approvals".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
})]
|
||||
);
|
||||
|
||||
let server_req = timeout(
|
||||
@@ -282,7 +283,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() ->
|
||||
for _ in 0..10 {
|
||||
let started: ItemStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??;
|
||||
if let ThreadItem::CommandExecution { id, .. } = started.item {
|
||||
if let ThreadItemPayload::CommandExecution { id, .. } = started.item.payload {
|
||||
command_item_id = Some(id);
|
||||
break;
|
||||
}
|
||||
@@ -437,14 +438,14 @@ async fn review_start_with_detached_delivery_returns_new_thread_id() -> Result<(
|
||||
assert_eq!(turn.items_view, TurnItemsView::NotLoaded);
|
||||
assert_eq!(
|
||||
turn.items,
|
||||
vec![ThreadItem::UserMessage {
|
||||
[ThreadItem::untimed(ThreadItemPayload::UserMessage {
|
||||
id: turn.id.clone(),
|
||||
client_id: None,
|
||||
content: vec![V2UserInput::Text {
|
||||
text: expected_prompt.clone(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
})]
|
||||
);
|
||||
assert_ne!(
|
||||
review_thread_id, thread_id,
|
||||
|
||||
@@ -11,7 +11,7 @@ use codex_app_server_protocol::ModelRerouteReason;
|
||||
use codex_app_server_protocol::ModelReroutedNotification;
|
||||
use codex_app_server_protocol::ModelVerification;
|
||||
use codex_app_server_protocol::ModelVerificationNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnModerationMetadataNotification;
|
||||
@@ -365,14 +365,14 @@ async fn collect_turn_notifications_and_validate_no_warning_item(
|
||||
anyhow::anyhow!("item/started notifications must include params")
|
||||
})?;
|
||||
let payload: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
assert!(!is_warning_user_message_item(&payload.item.payload));
|
||||
}
|
||||
"item/completed" => {
|
||||
let params = notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("item/completed notifications must include params")
|
||||
})?;
|
||||
let payload: ItemCompletedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
assert!(!is_warning_user_message_item(&payload.item.payload));
|
||||
}
|
||||
"turn/completed" => {
|
||||
return rerouted.ok_or_else(|| {
|
||||
@@ -413,14 +413,14 @@ async fn collect_model_verification_notifications_and_validate_no_warning_item(
|
||||
anyhow::anyhow!("item/started notifications must include params")
|
||||
})?;
|
||||
let payload: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
assert!(!is_warning_user_message_item(&payload.item.payload));
|
||||
}
|
||||
"item/completed" => {
|
||||
let params = notification.params.ok_or_else(|| {
|
||||
anyhow::anyhow!("item/completed notifications must include params")
|
||||
})?;
|
||||
let payload: ItemCompletedNotification = serde_json::from_value(params)?;
|
||||
assert!(!is_warning_user_message_item(&payload.item));
|
||||
assert!(!is_warning_user_message_item(&payload.item.payload));
|
||||
}
|
||||
"turn/completed" => {
|
||||
let verification = verification.ok_or_else(|| {
|
||||
@@ -468,8 +468,8 @@ async fn collect_cyber_policy_error_and_validate_no_reroute(
|
||||
}
|
||||
}
|
||||
|
||||
fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
|
||||
let ThreadItem::UserMessage { content, .. } = item else {
|
||||
fn warning_text_from_item(item: &ThreadItemPayload) -> Option<&str> {
|
||||
let ThreadItemPayload::UserMessage { content, .. } = item else {
|
||||
return None;
|
||||
};
|
||||
|
||||
@@ -479,7 +479,7 @@ fn warning_text_from_item(item: &ThreadItem) -> Option<&str> {
|
||||
})
|
||||
}
|
||||
|
||||
fn is_warning_user_message_item(item: &ThreadItem) -> bool {
|
||||
fn is_warning_user_message_item(item: &ThreadItemPayload) -> bool {
|
||||
warning_text_from_item(item).is_some()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::SleepItem;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
@@ -286,28 +286,29 @@ clock_source = "external"
|
||||
);
|
||||
}
|
||||
|
||||
let expected_item = ThreadItem::Sleep(SleepItem {
|
||||
let expected_item = ThreadItemPayload::Sleep(SleepItem {
|
||||
id: CALL_ID.to_string(),
|
||||
duration_ms: DURATION_MS,
|
||||
});
|
||||
assert!(completed.completed_at_ms >= started.started_at_ms);
|
||||
assert_eq!(
|
||||
started,
|
||||
ItemStartedNotification {
|
||||
item: expected_item.clone(),
|
||||
thread_id: thread.id.clone(),
|
||||
turn_id: turn.id.clone(),
|
||||
started_at_ms: started.started_at_ms,
|
||||
}
|
||||
ItemStartedNotification::new(
|
||||
thread.id.clone(),
|
||||
turn.id.clone(),
|
||||
expected_item.clone(),
|
||||
started.started_at_ms
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
completed,
|
||||
ItemCompletedNotification {
|
||||
item: expected_item,
|
||||
thread_id: thread.id,
|
||||
turn_id: turn.id,
|
||||
completed_at_ms: completed.completed_at_ms,
|
||||
}
|
||||
ItemCompletedNotification::new(
|
||||
thread.id,
|
||||
turn.id,
|
||||
expected_item,
|
||||
Some(started.started_at_ms),
|
||||
completed.completed_at_ms
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -320,7 +321,7 @@ async fn wait_for_sleep_started(
|
||||
loop {
|
||||
let started: ItemStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("item/started")).await??;
|
||||
if matches!(&started.item, ThreadItem::Sleep(item) if item.id == call_id) {
|
||||
if matches!(&started.item.payload, ThreadItemPayload::Sleep(item) if item.id == call_id) {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
@@ -336,7 +337,7 @@ async fn wait_for_sleep_completed(
|
||||
mcp.read_notification("item/completed"),
|
||||
)
|
||||
.await??;
|
||||
if matches!(&completed.item, ThreadItem::Sleep(item) if item.id == call_id) {
|
||||
if matches!(&completed.item.payload, ThreadItemPayload::Sleep(item) if item.id == call_id) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ use codex_app_server_protocol::ThreadClosedNotification;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
@@ -250,8 +250,8 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
|
||||
let turn = &thread.turns[0];
|
||||
assert_eq!(turn.status, TurnStatus::Interrupted);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match &turn.items[0].payload {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
@@ -2102,8 +2102,8 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted(
|
||||
assert_eq!(turns[0].status, TurnStatus::Interrupted);
|
||||
assert_eq!(turns[0].items.len(), 1);
|
||||
assert!(matches!(
|
||||
&turns[0].items[0],
|
||||
ThreadItem::UserMessage { id, .. } if id == "before-fork"
|
||||
&turns[0].items[0].payload,
|
||||
ThreadItemPayload::UserMessage { id, .. } if id == "before-fork"
|
||||
));
|
||||
|
||||
let search: ThreadSearchOccurrencesResponse = mcp
|
||||
@@ -2486,8 +2486,8 @@ async fn assert_thread_fork_ephemeral_remains_pathless_and_omits_listing(
|
||||
let turn = &thread.turns[0];
|
||||
assert_eq!(turn.status, TurnStatus::Completed);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match &turn.items[0].payload {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
|
||||
@@ -11,6 +11,7 @@ use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||
use codex_app_server_protocol::ThreadInjectItemsResponse;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -330,10 +331,16 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history(
|
||||
assert_eq!(turn.id, turn_started.turn.id);
|
||||
assert_eq!(turn_started.turn.status, TurnStatus::InProgress);
|
||||
assert_eq!(item_completed.turn_id, turn_started.turn.id);
|
||||
assert_eq!(item_started.item, item_completed.item);
|
||||
assert_eq!(
|
||||
item_completed.item,
|
||||
ThreadItem {
|
||||
completed_at_ms: Some(item_completed.completed_at_ms),
|
||||
..item_started.item
|
||||
}
|
||||
);
|
||||
assert!(matches!(
|
||||
&item_completed.item,
|
||||
ThreadItem::FunctionCallOutput {
|
||||
&item_completed.item.payload,
|
||||
ThreadItemPayload::FunctionCallOutput {
|
||||
name,
|
||||
namespace,
|
||||
output: FunctionCallOutputBody::Text(output),
|
||||
|
||||
@@ -18,7 +18,7 @@ use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::QueuedSubmission;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
@@ -386,9 +386,9 @@ async fn idle_queue_dispatch_preserves_client_id() -> Result<()> {
|
||||
);
|
||||
let started: ItemStartedNotification =
|
||||
timeout(READ_TIMEOUT, app.read_notification("item/started")).await??;
|
||||
let ThreadItem::UserMessage {
|
||||
let ThreadItemPayload::UserMessage {
|
||||
client_id, content, ..
|
||||
} = started.item
|
||||
} = started.item.payload
|
||||
else {
|
||||
anyhow::bail!("queued turn did not begin with its user message");
|
||||
};
|
||||
@@ -538,9 +538,9 @@ async fn cold_thread_resume_dispatches_a_persisted_queued_submission() -> Result
|
||||
|
||||
let started: ItemStartedNotification =
|
||||
timeout(READ_TIMEOUT, resumed.read_notification("item/started")).await??;
|
||||
let ThreadItem::UserMessage {
|
||||
let ThreadItemPayload::UserMessage {
|
||||
client_id, content, ..
|
||||
} = started.item
|
||||
} = started.item.payload
|
||||
else {
|
||||
anyhow::bail!("cold resume did not start the persisted queued user message");
|
||||
};
|
||||
@@ -747,14 +747,14 @@ async fn queue_start_while_active_returns_busy_and_preserves_the_queue() -> Resu
|
||||
let started_item: ItemStartedNotification =
|
||||
timeout(READ_TIMEOUT, app.read_notification("item/started")).await??;
|
||||
if matches!(
|
||||
&started_item.item,
|
||||
ThreadItem::UserMessage { client_id, .. }
|
||||
&started_item.item.payload,
|
||||
ThreadItemPayload::UserMessage { client_id, .. }
|
||||
if client_id.as_deref() == Some("active-queued-client-id")
|
||||
) {
|
||||
break started_item;
|
||||
}
|
||||
};
|
||||
let ThreadItem::UserMessage { client_id, .. } = started_item.item else {
|
||||
let ThreadItemPayload::UserMessage { client_id, .. } = started_item.item.payload else {
|
||||
anyhow::bail!("queued message did not start after the active turn completed");
|
||||
};
|
||||
assert_eq!(client_id.as_deref(), Some("active-queued-client-id"));
|
||||
@@ -823,8 +823,8 @@ async fn queue_start_without_id_starts_the_head_when_idle() -> Result<()> {
|
||||
let started_item: ItemStartedNotification =
|
||||
timeout(READ_TIMEOUT, app.read_notification("item/started")).await??;
|
||||
if matches!(
|
||||
&started_item.item,
|
||||
ThreadItem::UserMessage { client_id, .. }
|
||||
&started_item.item.payload,
|
||||
ThreadItemPayload::UserMessage { client_id, .. }
|
||||
if client_id.as_deref() == Some(first.client_user_message_id.as_str())
|
||||
) {
|
||||
break started_item;
|
||||
|
||||
@@ -25,8 +25,8 @@ use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemEntry;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadItemsListAnchor;
|
||||
use codex_app_server_protocol::ThreadItemsListCursor;
|
||||
use codex_app_server_protocol::ThreadItemsListParams;
|
||||
@@ -222,8 +222,8 @@ async fn thread_read_can_include_turns() -> Result<()> {
|
||||
assert_eq!(turn.root_turn_id, None);
|
||||
assert_eq!(turn.items_view, TurnItemsView::Full);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match &turn.items[0].payload {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
@@ -297,7 +297,8 @@ async fn thread_read_preserves_file_id_and_root_from_completed_turn(
|
||||
thread: loaded_thread,
|
||||
..
|
||||
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??;
|
||||
let ThreadItem::UserMessage { content, .. } = &loaded_thread.turns[0].items[0] else {
|
||||
let ThreadItemPayload::UserMessage { content, .. } = &loaded_thread.turns[0].items[0].payload
|
||||
else {
|
||||
panic!("expected completed turn to start with a user message");
|
||||
};
|
||||
assert_eq!(content, &vec![image.clone()]);
|
||||
@@ -323,7 +324,9 @@ async fn thread_read_preserves_file_id_and_root_from_completed_turn(
|
||||
thread: restarted_thread,
|
||||
..
|
||||
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??;
|
||||
let ThreadItem::UserMessage { content, .. } = &restarted_thread.turns[0].items[0] else {
|
||||
let ThreadItemPayload::UserMessage { content, .. } =
|
||||
&restarted_thread.turns[0].items[0].payload
|
||||
else {
|
||||
panic!("expected persisted turn to start with a user message");
|
||||
};
|
||||
assert_eq!(content, &vec![image]);
|
||||
@@ -500,6 +503,7 @@ async fn thread_turns_list_can_page_backward_and_forward() -> Result<()> {
|
||||
data,
|
||||
next_cursor,
|
||||
backwards_cursor,
|
||||
..
|
||||
} = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(read_id)).await??;
|
||||
assert_eq!(turn_user_texts(&data), vec!["third", "second"]);
|
||||
assert!(
|
||||
@@ -1062,7 +1066,13 @@ async fn thread_read_loaded_include_turns_reads_store_history_without_rollout_pa
|
||||
let ThreadReadResponse { thread, .. } = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(turn_user_texts(&thread.turns), vec!["history from store"]);
|
||||
let [ThreadItem::UserMessage { content, .. }] = thread.turns[0].items.as_slice() else {
|
||||
let [
|
||||
codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::UserMessage { content, .. },
|
||||
..
|
||||
},
|
||||
] = thread.turns[0].items.as_slice()
|
||||
else {
|
||||
panic!("expected one user message item");
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -1834,17 +1844,17 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
ThreadItemPayload::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
client_id: None,
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::UserMessage {
|
||||
ThreadItemPayload::UserMessage {
|
||||
id: "steer-1".to_string(),
|
||||
client_id: Some("updated-steer".to_string()),
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -1852,7 +1862,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|payload| codex_app_server_protocol::ThreadItem {
|
||||
payload,
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1),
|
||||
})
|
||||
.collect(),
|
||||
items_view: TurnItemsView::Full,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -1863,11 +1880,18 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
let expected_turn_2_full = Turn {
|
||||
id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
items: vec![ThreadItemPayload::UserMessage {
|
||||
id: "user-2".to_string(),
|
||||
client_id: None,
|
||||
content: Vec::new(),
|
||||
}],
|
||||
}]
|
||||
.into_iter()
|
||||
.map(|payload| codex_app_server_protocol::ThreadItem {
|
||||
payload,
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1),
|
||||
})
|
||||
.collect(),
|
||||
items_view: TurnItemsView::Full,
|
||||
status: TurnStatus::Interrupted,
|
||||
error: None,
|
||||
@@ -2029,12 +2053,12 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
ThreadItemPayload::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
client_id: None,
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -2042,7 +2066,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|payload| codex_app_server_protocol::ThreadItem {
|
||||
payload,
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1)
|
||||
})
|
||||
.collect(),
|
||||
items_view: TurnItemsView::Summary,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
@@ -2051,6 +2082,24 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
duration_ms: Some(10_000),
|
||||
}]
|
||||
);
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
exclude_turns: true,
|
||||
initial_turns_page: Some(ThreadResumeInitialTurnsPageParams {
|
||||
limit: Some(1),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: Some(TurnItemsView::Summary),
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resumed: ThreadResumeResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(resume_id)).await??;
|
||||
assert_eq!(
|
||||
resumed.initial_turns_page.expect("summary page").data,
|
||||
first_page.data
|
||||
);
|
||||
let next_cursor = first_page.next_cursor.expect("next turn cursor");
|
||||
let second_page = read_turns_page(
|
||||
&mut mcp,
|
||||
@@ -2100,10 +2149,14 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
first_items_page.data,
|
||||
vec![ThreadItemEntry {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
client_id: None,
|
||||
content: Vec::new(),
|
||||
item: codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
client_id: None,
|
||||
content: Vec::new(),
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1)
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1),
|
||||
@@ -2232,23 +2285,31 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
vec![
|
||||
ThreadItemEntry {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::UserMessage {
|
||||
id: "steer-1".to_string(),
|
||||
client_id: Some("updated-steer".to_string()),
|
||||
content: Vec::new(),
|
||||
item: codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::UserMessage {
|
||||
id: "steer-1".to_string(),
|
||||
client_id: Some("updated-steer".to_string()),
|
||||
content: Vec::new(),
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1)
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1),
|
||||
},
|
||||
ThreadItemEntry {
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: ThreadItem::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
item: codex_app_server_protocol::ThreadItem {
|
||||
payload: ThreadItemPayload::AgentMessage {
|
||||
id: "agent-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1)
|
||||
},
|
||||
started_at_ms: Some(0),
|
||||
completed_at_ms: Some(1),
|
||||
@@ -2607,8 +2668,8 @@ fn paginated_completed_item(
|
||||
fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
turns
|
||||
.iter()
|
||||
.filter_map(|turn| match turn.items.first()? {
|
||||
ThreadItem::UserMessage { content, .. } => match content.first()? {
|
||||
.filter_map(|turn| match &turn.items.first()?.payload {
|
||||
ThreadItemPayload::UserMessage { content, .. } => match content.first()? {
|
||||
UserInput::Text { text, .. } => Some(text.as_str()),
|
||||
UserInput::Image { .. }
|
||||
| UserInput::LocalImage { .. }
|
||||
@@ -2625,9 +2686,9 @@ fn turn_user_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
fn turn_agent_texts(turns: &[codex_app_server_protocol::Turn]) -> Vec<&str> {
|
||||
turns
|
||||
.iter()
|
||||
.flat_map(|turn| &turn.items)
|
||||
.flat_map(Turn::item_payloads)
|
||||
.filter_map(|item| match item {
|
||||
ThreadItem::AgentMessage { text, .. } => Some(text.as_str()),
|
||||
ThreadItemPayload::AgentMessage { text, .. } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -52,7 +52,7 @@ use codex_app_server_protocol::ThreadGoalGetResponse;
|
||||
use codex_app_server_protocol::ThreadGoalSetResponse;
|
||||
use codex_app_server_protocol::ThreadGoalStatus;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
@@ -133,6 +133,7 @@ use core_test_support::streaming_sse::StreamingSseChunk;
|
||||
use core_test_support::streaming_sse::start_streaming_sse_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::FileTimes;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
@@ -2736,8 +2737,8 @@ async fn thread_resume_returns_rollout_history() -> Result<()> {
|
||||
let turn = &thread.turns[0];
|
||||
assert_eq!(turn.status, TurnStatus::Completed);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
match &turn.items[0].payload {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
@@ -2772,16 +2773,16 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<(
|
||||
let remote_mcp_item = remote_turn
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::McpToolCall { .. }))
|
||||
.find(|item| matches!(&item.payload, ThreadItemPayload::McpToolCall { .. }))
|
||||
.expect("remote resume should include redacted MCP item");
|
||||
let ThreadItem::McpToolCall {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
arguments,
|
||||
app_context,
|
||||
read_only_hint,
|
||||
result,
|
||||
error,
|
||||
..
|
||||
} = remote_mcp_item
|
||||
} = &remote_mcp_item.payload
|
||||
else {
|
||||
unreachable!("matched MCP item");
|
||||
};
|
||||
@@ -2812,7 +2813,7 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<(
|
||||
!remote_turn
|
||||
.items
|
||||
.iter()
|
||||
.any(|item| matches!(item, ThreadItem::ImageGeneration(_))),
|
||||
.any(|item| matches!(&item.payload, ThreadItemPayload::ImageGeneration(_))),
|
||||
"remote resume should drop image generation items for {client_name}"
|
||||
);
|
||||
}
|
||||
@@ -2827,15 +2828,15 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<(
|
||||
let normal_mcp_item = normal_turn
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::McpToolCall { .. }))
|
||||
.find(|item| matches!(&item.payload, ThreadItemPayload::McpToolCall { .. }))
|
||||
.expect("normal resume should include MCP item");
|
||||
let ThreadItem::McpToolCall {
|
||||
let ThreadItemPayload::McpToolCall {
|
||||
arguments,
|
||||
app_context,
|
||||
read_only_hint,
|
||||
result,
|
||||
..
|
||||
} = normal_mcp_item
|
||||
} = &normal_mcp_item.payload
|
||||
else {
|
||||
unreachable!("matched MCP item");
|
||||
};
|
||||
@@ -2866,8 +2867,8 @@ async fn thread_resume_redacts_payloads_for_chatgpt_remote_clients() -> Result<(
|
||||
assert_eq!(result.meta, Some(json!({"secret":"meta"})));
|
||||
assert!(
|
||||
normal_turn.items.iter().any(|item| matches!(
|
||||
item,
|
||||
ThreadItem::ImageGeneration(item)
|
||||
&item.payload,
|
||||
ThreadItemPayload::ImageGeneration(item)
|
||||
if matches!(
|
||||
&item.image,
|
||||
GeneratedImageRef::Inline { result }
|
||||
@@ -5364,6 +5365,8 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R
|
||||
gate: None,
|
||||
body: responses::sse(vec![
|
||||
responses::ev_response_created("resp-2"),
|
||||
responses::ev_message_item_added("progress", ""),
|
||||
responses::ev_assistant_message("progress", "Checking"),
|
||||
responses::ev_message_item_added("msg-2", ""),
|
||||
]),
|
||||
},
|
||||
@@ -5438,17 +5441,17 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R
|
||||
primary.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await??;
|
||||
// An assistant item starts after the user message has reached live history.
|
||||
// The gated remainder of the response keeps the turn running during resume.
|
||||
// Starting the gated reply means the prompt and the progress message have completed.
|
||||
let mut message_starts = HashMap::new();
|
||||
timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification =
|
||||
primary.read_notification("item/started").await?;
|
||||
if started.turn_id == running_turn.id
|
||||
&& matches!(started.item, ThreadItem::AgentMessage { .. })
|
||||
{
|
||||
assert_eq!(started.turn_id, running_turn.id);
|
||||
if started.item.id() == "msg-2" {
|
||||
return Ok::<(), anyhow::Error>(());
|
||||
}
|
||||
message_starts.insert(started.item.id().to_owned(), started.started_at_ms);
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
@@ -5550,7 +5553,18 @@ async fn thread_resume_rejoins_running_paginated_thread_with_initial_page() -> R
|
||||
page.data[0].items_view,
|
||||
items_view.unwrap_or(TurnItemsView::Summary)
|
||||
);
|
||||
assert!(!page.data[0].items.is_empty());
|
||||
assert!(matches!(
|
||||
page.data[0].items.as_slice(),
|
||||
[codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::UserMessage { .. }, .. }, codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::AgentMessage { text, .. }, .. }] if text == "Checking"
|
||||
));
|
||||
assert_eq!(
|
||||
page.data[0]
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| Some((item.id().to_owned(), item.started_at_ms?)))
|
||||
.collect::<std::collections::HashMap<_, _>>(),
|
||||
message_starts
|
||||
);
|
||||
if !exclude_turns {
|
||||
let full_turn = resumed.thread.turns.last().expect("full active turn");
|
||||
assert_eq!(full_turn.id, running_turn.id);
|
||||
@@ -5899,14 +5913,14 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result<
|
||||
.await?;
|
||||
let started: ItemStartedNotification =
|
||||
serde_json::from_value(notification.params.clone().expect("item/started params"))?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let expected_readme_path = workspace.join("README.md");
|
||||
let expected_file_change = ThreadItem::FileChange {
|
||||
let expected_file_change = ThreadItemPayload::FileChange {
|
||||
id: "patch-call".to_string(),
|
||||
changes: vec![codex_app_server_protocol::FileUpdateChange {
|
||||
path: expected_readme_path.to_string_lossy().into_owned(),
|
||||
|
||||
@@ -20,7 +20,7 @@ use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
@@ -86,9 +86,9 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
|
||||
.await?;
|
||||
|
||||
let started = wait_for_command_execution_started(&mut mcp, /*expected_id*/ None).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id, source, status, ..
|
||||
} = &started.item
|
||||
} = &started.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
@@ -103,14 +103,14 @@ async fn thread_shell_command_history_responses_exclude_persisted_command_execut
|
||||
);
|
||||
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
source,
|
||||
status,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
..
|
||||
} = &completed.item
|
||||
} = &completed.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
@@ -279,9 +279,9 @@ async fn check_thread_shell_command_in_active_turn(timeout_ms: Option<i64>) -> R
|
||||
let started: TurnStartedNotification = mcp.read_notification("turn/started").await?;
|
||||
assert_eq!(started.turn.id, turn.id);
|
||||
let agent_started = wait_for_command_execution_started(&mut mcp, Some("call-approve")).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
command, source, ..
|
||||
} = &agent_started.item
|
||||
} = &agent_started.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
@@ -315,19 +315,19 @@ async fn check_thread_shell_command_in_active_turn(timeout_ms: Option<i64>) -> R
|
||||
wait_for_command_execution_started_by_source(&mut mcp, CommandExecutionSource::UserShell)
|
||||
.await?;
|
||||
assert_eq!(started.turn_id, turn.id);
|
||||
let command_id = match &started.item {
|
||||
ThreadItem::CommandExecution { id, .. } => id.clone(),
|
||||
let command_id = match &started.item.payload {
|
||||
ThreadItemPayload::CommandExecution { id, .. } => id.clone(),
|
||||
_ => unreachable!("helper returns command execution item"),
|
||||
};
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
assert_eq!(completed.turn_id, turn.id);
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
source,
|
||||
aggregated_output,
|
||||
status,
|
||||
exit_code,
|
||||
..
|
||||
} = &completed.item
|
||||
} = &completed.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
@@ -443,12 +443,12 @@ async fn thread_shell_command_honors_optional_timeout() -> Result<()> {
|
||||
let completed =
|
||||
wait_for_command_execution_completed(&mut mcp, /*expected_id*/ None).await?;
|
||||
assert_eq!(completed.turn_id, started.turn.id);
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
status,
|
||||
exit_code,
|
||||
aggregated_output,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
@@ -544,11 +544,11 @@ fn current_shell_slow_command() -> Result<(String, String)> {
|
||||
Ok((format!("{command}{suffix}"), expected_output))
|
||||
}
|
||||
|
||||
fn assert_no_command_executions(items: &[ThreadItem], context: &str) {
|
||||
fn assert_no_command_executions(items: &[codex_app_server_protocol::ThreadItem], context: &str) {
|
||||
assert!(
|
||||
items
|
||||
.iter()
|
||||
.all(|item| !matches!(item, ThreadItem::CommandExecution { .. })),
|
||||
.all(|entry| !matches!(&entry.payload, ThreadItemPayload::CommandExecution { .. })),
|
||||
"{context} should always exclude command executions from returned turns"
|
||||
);
|
||||
}
|
||||
@@ -577,7 +577,7 @@ async fn wait_for_command_execution_started(
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &started.item else {
|
||||
let ThreadItemPayload::CommandExecution { id, .. } = &started.item.payload else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
@@ -592,7 +592,7 @@ async fn wait_for_command_execution_started_by_source(
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started = wait_for_command_execution_started(mcp, /*expected_id*/ None).await?;
|
||||
let ThreadItem::CommandExecution { source, .. } = &started.item else {
|
||||
let ThreadItemPayload::CommandExecution { source, .. } = &started.item.payload else {
|
||||
continue;
|
||||
};
|
||||
if source == &expected_source {
|
||||
@@ -607,7 +607,7 @@ async fn wait_for_command_execution_completed(
|
||||
) -> Result<ItemCompletedNotification> {
|
||||
loop {
|
||||
let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &completed.item else {
|
||||
let ThreadItemPayload::CommandExecution { id, .. } = &completed.item.payload else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
|
||||
@@ -4,7 +4,7 @@ use app_test_support::TestAppServer;
|
||||
use app_test_support::create_mock_responses_server_repeating_assistant;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadItemsListParams;
|
||||
use codex_app_server_protocol::ThreadItemsListResponse;
|
||||
use codex_app_server_protocol::ThreadRealtimeItemContent;
|
||||
@@ -164,7 +164,9 @@ async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() ->
|
||||
&page.data[0],
|
||||
ThreadTimelineEntry::Item { turn_id, item, .. }
|
||||
if turn_id == "turn-1"
|
||||
&& matches!(item.as_ref(), ThreadItem::UserMessage { .. })
|
||||
&& matches!(&item.payload, ThreadItemPayload::UserMessage { .. })
|
||||
&& item.started_at_ms == Some(0)
|
||||
&& item.completed_at_ms == Some(1)
|
||||
));
|
||||
assert!(matches!(
|
||||
&page.data[1],
|
||||
|
||||
@@ -12,7 +12,7 @@ use codex_app_server_protocol::DynamicToolSpec;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadClosedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
@@ -611,7 +611,8 @@ async fn wait_for_dynamic_tool_started(
|
||||
continue;
|
||||
};
|
||||
let started: ItemStartedNotification = serde_json::from_value(params)?;
|
||||
if matches!(&started.item, ThreadItem::DynamicToolCall { id, .. } if id == call_id) {
|
||||
if matches!(&started.item.payload, ThreadItemPayload::DynamicToolCall { id, .. } if id == call_id)
|
||||
{
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ use codex_app_server_protocol::ThreadDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadDeletedNotification;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadInjectItemsParams;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
use codex_app_server_protocol::ThreadMetadataUpdateParams;
|
||||
@@ -607,7 +607,7 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
|
||||
assert_eq!(completed.turn.items_view, TurnItemsView::Summary);
|
||||
assert!(matches!(
|
||||
&completed.turn.items[..],
|
||||
[ThreadItem::AgentMessage { text, .. }] if text == "Done"
|
||||
[codex_app_server_protocol::ThreadItem { payload: ThreadItemPayload::AgentMessage { text, .. }, .. }] if text == "Done"
|
||||
));
|
||||
|
||||
let event = wait_for_analytics_event(&server, DEFAULT_READ_TIMEOUT, "codex_turn_event").await?;
|
||||
@@ -972,15 +972,15 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> {
|
||||
loop {
|
||||
let item_started: ItemStartedNotification =
|
||||
mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::UserMessage { .. } = item_started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(item_started.item);
|
||||
if let ThreadItemPayload::UserMessage { .. } = item_started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(item_started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
match user_message_item {
|
||||
ThreadItem::UserMessage {
|
||||
ThreadItemPayload::UserMessage {
|
||||
client_id, content, ..
|
||||
} => {
|
||||
assert_eq!(client_id, Some("client-message-1".to_string()));
|
||||
@@ -2249,12 +2249,12 @@ allow_upstream_proxy = false
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
app_server.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CommandExecution {
|
||||
if let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
status,
|
||||
exit_code,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
&& id == call_id
|
||||
{
|
||||
assert_eq!(
|
||||
@@ -3030,6 +3030,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
|
||||
params.command.as_deref(),
|
||||
Some(expected_approval_command.as_str())
|
||||
);
|
||||
let approval_started_at_ms = params.started_at_ms;
|
||||
let resolved_request_id = request_id.clone();
|
||||
|
||||
// Approve and wait for task completion
|
||||
@@ -3051,14 +3052,15 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
|
||||
"item/completed" => {
|
||||
let completed: ItemCompletedNotification =
|
||||
serde_json::from_value(notification.params.expect("item/completed params"))?;
|
||||
match completed.item {
|
||||
ThreadItem::CommandExecution {
|
||||
match completed.item.payload {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
command,
|
||||
exit_code,
|
||||
aggregated_output,
|
||||
..
|
||||
} if id == "call1" => {
|
||||
assert_eq!(completed.item.started_at_ms, Some(approval_started_at_ms));
|
||||
assert_eq!(command, expected_display_command);
|
||||
assert_eq!(exit_code, Some(0));
|
||||
assert!(aggregated_output.is_some_and(|output| output.contains("True")));
|
||||
@@ -3236,13 +3238,13 @@ async fn run_turn_start_exec_approval_rejection_v2(
|
||||
let started_command_execution = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::CommandExecution { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
status,
|
||||
command,
|
||||
@@ -3283,13 +3285,13 @@ async fn run_turn_start_exec_approval_rejection_v2(
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = completed.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
if let ThreadItemPayload::CommandExecution { .. } = completed.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(completed.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
status,
|
||||
command,
|
||||
@@ -3494,13 +3496,16 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns(
|
||||
loop {
|
||||
let item_started: ItemStartedNotification =
|
||||
mcp.read_notification("item/started").await?;
|
||||
if matches!(item_started.item, ThreadItem::CommandExecution { .. }) {
|
||||
return Ok::<ThreadItem, anyhow::Error>(item_started.item);
|
||||
if matches!(
|
||||
item_started.item.payload,
|
||||
ThreadItemPayload::CommandExecution { .. }
|
||||
) {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(item_started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
cwd,
|
||||
command,
|
||||
status,
|
||||
@@ -3868,13 +3873,13 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
let started_file_change = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange {
|
||||
let ThreadItemPayload::FileChange {
|
||||
ref id,
|
||||
status,
|
||||
ref changes,
|
||||
@@ -3917,7 +3922,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
)
|
||||
.await?;
|
||||
let mut saw_resolved = false;
|
||||
let mut completed_file_change: Option<ThreadItem> = None;
|
||||
let mut completed_file_change: Option<ThreadItemPayload> = None;
|
||||
while completed_file_change.is_none() {
|
||||
let message = timeout(DEFAULT_READ_TIMEOUT, mcp.read_next_message()).await??;
|
||||
let JSONRPCMessage::Notification(notification) = message else {
|
||||
@@ -3939,9 +3944,9 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
notification.params.clone().expect("item/completed params"),
|
||||
)?;
|
||||
if let ThreadItem::FileChange { .. } = completed.item {
|
||||
if let ThreadItemPayload::FileChange { .. } = completed.item.payload {
|
||||
assert!(saw_resolved, "serverRequest/resolved should arrive first");
|
||||
completed_file_change = Some(completed.item);
|
||||
completed_file_change = Some(completed.item.payload);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -3949,7 +3954,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
|
||||
}
|
||||
let completed_file_change =
|
||||
completed_file_change.expect("file change completion should be observed");
|
||||
let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else {
|
||||
let ThreadItemPayload::FileChange { ref id, status, .. } = completed_file_change else {
|
||||
unreachable!("loop ensures we break on file change items");
|
||||
};
|
||||
assert_eq!(id, "patch-call");
|
||||
@@ -4333,17 +4338,17 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
|
||||
let spawn_started = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::CollabAgentToolCall { id, .. } = &started.item
|
||||
if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &started.item.payload
|
||||
&& id == SPAWN_CALL_ID
|
||||
{
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
assert_eq!(
|
||||
spawn_started,
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: SPAWN_CALL_ID.to_string(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: CollabAgentToolCallStatus::InProgress,
|
||||
@@ -4360,15 +4365,15 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CollabAgentToolCall { id, .. } = &completed.item
|
||||
if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &completed.item.payload
|
||||
&& id == SPAWN_CALL_ID
|
||||
{
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(completed.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CollabAgentToolCall {
|
||||
let ThreadItemPayload::CollabAgentToolCall {
|
||||
id,
|
||||
tool,
|
||||
status,
|
||||
@@ -4577,15 +4582,15 @@ async fn direct_input_to_multi_agent_v2_subagent_is_rejected(
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
assert!(!matches!(
|
||||
&completed.item,
|
||||
ThreadItem::CollabAgentToolCall { .. }
|
||||
&completed.item.payload,
|
||||
ThreadItemPayload::CollabAgentToolCall { .. }
|
||||
));
|
||||
if let ThreadItem::SubAgentActivity {
|
||||
if let ThreadItemPayload::SubAgentActivity {
|
||||
id,
|
||||
kind: SubAgentActivityKind::Started,
|
||||
agent_thread_id,
|
||||
..
|
||||
} = completed.item
|
||||
} = completed.item.payload
|
||||
&& id == SPAWN_CALL_ID
|
||||
{
|
||||
return Ok::<String, anyhow::Error>(agent_thread_id);
|
||||
@@ -4993,15 +4998,15 @@ config_file = "./custom-role.toml"
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CollabAgentToolCall { id, .. } = &completed.item
|
||||
if let ThreadItemPayload::CollabAgentToolCall { id, .. } = &completed.item.payload
|
||||
&& id == SPAWN_CALL_ID
|
||||
{
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(completed.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CollabAgentToolCall {
|
||||
let ThreadItemPayload::CollabAgentToolCall {
|
||||
id,
|
||||
tool,
|
||||
status,
|
||||
@@ -5128,13 +5133,13 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
|
||||
let started_file_change_1 = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange { id, status, .. } = started_file_change_1 else {
|
||||
let ThreadItemPayload::FileChange { id, status, .. } = started_file_change_1 else {
|
||||
unreachable!("loop ensures we break on file change items");
|
||||
};
|
||||
assert_eq!(id, "patch-call-1");
|
||||
@@ -5181,7 +5186,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
|
||||
"item/completed" => {
|
||||
let completed: ItemCompletedNotification =
|
||||
serde_json::from_value(notification.params.expect("item/completed params"))?;
|
||||
if matches!(completed.item, ThreadItem::FileChange { ref id, .. } if id == "patch-call-1")
|
||||
if matches!(completed.item.payload, ThreadItemPayload::FileChange { ref id, .. } if id == "patch-call-1")
|
||||
{
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn_id, turn_1.id);
|
||||
@@ -5220,13 +5225,13 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
|
||||
let started_file_change_2 = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange { id, status, .. } = started_file_change_2 else {
|
||||
let ThreadItemPayload::FileChange { id, status, .. } = started_file_change_2 else {
|
||||
unreachable!("loop ensures we break on file change items");
|
||||
};
|
||||
assert_eq!(id, "patch-call-2");
|
||||
@@ -5244,7 +5249,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
notification.params.expect("item/completed params"),
|
||||
)?;
|
||||
if matches!(completed.item, ThreadItem::FileChange { ref id, .. } if id == "patch-call-2")
|
||||
if matches!(completed.item.payload, ThreadItemPayload::FileChange { ref id, .. } if id == "patch-call-2")
|
||||
{
|
||||
return Ok::<ItemCompletedNotification, anyhow::Error>(completed);
|
||||
}
|
||||
@@ -5356,13 +5361,13 @@ async fn run_turn_start_file_change_approval_rejection_v2(
|
||||
let started_file_change = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::FileChange { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange {
|
||||
let ThreadItemPayload::FileChange {
|
||||
ref id,
|
||||
status,
|
||||
ref changes,
|
||||
@@ -5402,13 +5407,13 @@ async fn run_turn_start_file_change_approval_rejection_v2(
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
if let ThreadItem::FileChange { .. } = completed.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
if let ThreadItemPayload::FileChange { .. } = completed.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(completed.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::FileChange { ref id, status, .. } = completed_file_change else {
|
||||
let ThreadItemPayload::FileChange { ref id, status, .. } = completed_file_change else {
|
||||
unreachable!("loop ensures we break on file change items");
|
||||
};
|
||||
assert_eq!(id, "patch-call");
|
||||
@@ -5506,13 +5511,13 @@ async fn command_execution_notifications_include_process_id(
|
||||
let started_command = timeout(DEFAULT_READ_TIMEOUT, async {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = started.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(started.item);
|
||||
if let ThreadItemPayload::CommandExecution { .. } = started.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(started.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id,
|
||||
process_id: started_process_id,
|
||||
status,
|
||||
@@ -5529,13 +5534,13 @@ async fn command_execution_notifications_include_process_id(
|
||||
loop {
|
||||
let completed: ItemCompletedNotification =
|
||||
mcp.read_notification("item/completed").await?;
|
||||
if let ThreadItem::CommandExecution { .. } = completed.item {
|
||||
return Ok::<ThreadItem, anyhow::Error>(completed.item);
|
||||
if let ThreadItemPayload::CommandExecution { .. } = completed.item.payload {
|
||||
return Ok::<ThreadItemPayload, anyhow::Error>(completed.item.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let ThreadItem::CommandExecution {
|
||||
let ThreadItemPayload::CommandExecution {
|
||||
id: completed_id,
|
||||
process_id: completed_process_id,
|
||||
status: completed_status,
|
||||
@@ -5691,8 +5696,8 @@ async fn command_execution_notifications_include_trusted_plugin_id(
|
||||
let notification = mcp.read_stream_until_notification_message(method).await?;
|
||||
let params = notification.params.expect("item notification params");
|
||||
let item_json = params.get("item").expect("item notification item").clone();
|
||||
let item = serde_json::from_value::<ThreadItem>(item_json.clone())?;
|
||||
if let ThreadItem::CommandExecution { status, .. } = item {
|
||||
let item = serde_json::from_value::<ThreadItemPayload>(item_json.clone())?;
|
||||
if let ThreadItemPayload::CommandExecution { status, .. } = item {
|
||||
let emitted_script_path = item_json
|
||||
.get("scriptPath")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
|
||||
@@ -16,7 +16,7 @@ use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -318,9 +318,9 @@ async fn turn_steer_returns_active_turn_id() -> Result<()> {
|
||||
let params = notification.params.expect("item/started params");
|
||||
let item_started: ItemStartedNotification =
|
||||
serde_json::from_value(params).expect("deserialize item/started notification");
|
||||
let ThreadItem::UserMessage {
|
||||
let ThreadItemPayload::UserMessage {
|
||||
client_id, content, ..
|
||||
} = item_started.item
|
||||
} = item_started.item.payload
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::write_models_cache;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadResumeResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -143,7 +143,8 @@ async fn fresh_context_subagent_inherits_disabled_view_image_and_mcp_tools() ->
|
||||
mcp.read_notification("item/completed"),
|
||||
)
|
||||
.await??;
|
||||
if matches!(&completed.item, ThreadItem::McpToolCall { id, .. } if id == MCP_CALL_ID) {
|
||||
if matches!(&completed.item.payload, ThreadItemPayload::McpToolCall { id, .. } if id == MCP_CALL_ID)
|
||||
{
|
||||
assert_ne!(completed.thread_id, thread.id);
|
||||
break completed.thread_id;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ use app_test_support::TestAppServer;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
@@ -304,15 +305,15 @@ async fn assert_standalone_web_search_round_trips_output(
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
started.item,
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
started.item.payload,
|
||||
ThreadItemPayload::WebSearch(WebSearchItem {
|
||||
id: call_id.to_string(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
})
|
||||
);
|
||||
let expected_completed_item = ThreadItem::WebSearch(WebSearchItem {
|
||||
let expected_completed_item = ThreadItemPayload::WebSearch(WebSearchItem {
|
||||
id: call_id.to_string(),
|
||||
query: "standalone web search".to_string(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
@@ -328,7 +329,7 @@ async fn assert_standalone_web_search_round_trips_output(
|
||||
"future_field": {"preserved": true},
|
||||
})]),
|
||||
});
|
||||
assert_eq!(completed.item, expected_completed_item);
|
||||
assert_eq!(completed.item.payload, expected_completed_item);
|
||||
|
||||
drop(mcp);
|
||||
let mut reloaded_mcp = TestAppServer::builder()
|
||||
@@ -344,11 +345,11 @@ async fn assert_standalone_web_search_round_trips_output(
|
||||
.await?;
|
||||
let ThreadReadResponse { thread, .. } =
|
||||
timeout(DEFAULT_READ_TIMEOUT, reloaded_mcp.read_response(read_req)).await??;
|
||||
let persisted_web_searches: Vec<&ThreadItem> = thread
|
||||
let persisted_web_searches: Vec<&ThreadItemPayload> = thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| &turn.items)
|
||||
.filter(|item| matches!(item, ThreadItem::WebSearch(_)))
|
||||
.flat_map(Turn::item_payloads)
|
||||
.filter(|item| matches!(item, ThreadItemPayload::WebSearch(_)))
|
||||
.collect();
|
||||
assert_eq!(persisted_web_searches, vec![&expected_completed_item]);
|
||||
|
||||
@@ -358,7 +359,7 @@ async fn assert_standalone_web_search_round_trips_output(
|
||||
async fn wait_for_web_search_started(mcp: &mut TestAppServer) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started: ItemStartedNotification = mcp.read_notification("item/started").await?;
|
||||
if matches!(&started.item, ThreadItem::WebSearch(_)) {
|
||||
if matches!(&started.item.payload, ThreadItemPayload::WebSearch(_)) {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
@@ -369,7 +370,7 @@ async fn wait_for_web_search_completed(
|
||||
) -> Result<ItemCompletedNotification> {
|
||||
loop {
|
||||
let completed: ItemCompletedNotification = mcp.read_notification("item/completed").await?;
|
||||
if matches!(&completed.item, ThreadItem::WebSearch(_)) {
|
||||
if matches!(&completed.item.payload, ThreadItemPayload::WebSearch(_)) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2500,6 +2500,14 @@ impl Session {
|
||||
|
||||
/// Persist the event to rollout and send it to clients.
|
||||
pub(crate) async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg) {
|
||||
if let Some((item_id, started_at_ms)) = msg.command_approval_start() {
|
||||
// The client can see the command before execution. Keep its first start
|
||||
// for the canonical lifecycle events that will be saved in history.
|
||||
turn_context
|
||||
.turn_timing_state
|
||||
.record_item_started(item_id.to_owned(), started_at_ms)
|
||||
.await;
|
||||
}
|
||||
let legacy_source = msg.clone();
|
||||
if let EventMsg::Error(error) = &legacy_source
|
||||
&& error
|
||||
|
||||
@@ -308,9 +308,13 @@ async fn guardian_retry_executes_only_after_a_completed_approval() -> Result<()>
|
||||
.await?;
|
||||
let mut reviews = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut command_started_at_ms = None;
|
||||
loop {
|
||||
match test.codex.next_event().await?.msg {
|
||||
EventMsg::GuardianAssessment(review) => reviews.push(review.status),
|
||||
EventMsg::GuardianAssessment(review) => reviews.push(review),
|
||||
EventMsg::ItemCompleted(event) if event.item.id() == "write-marker" => {
|
||||
command_started_at_ms = event.started_at_ms;
|
||||
}
|
||||
EventMsg::GuardianWarning(warning) => warnings.push(warning.message),
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
@@ -338,12 +342,16 @@ async fn guardian_retry_executes_only_after_a_completed_approval() -> Result<()>
|
||||
assert_eq!(request.inputs_of_type("agent_message"), expected_messages);
|
||||
}
|
||||
assert_eq!(
|
||||
reviews,
|
||||
reviews
|
||||
.iter()
|
||||
.map(|review| review.status)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
GuardianAssessmentStatus::InProgress,
|
||||
GuardianAssessmentStatus::Approved,
|
||||
]
|
||||
);
|
||||
assert_eq!(command_started_at_ms, Some(reviews[0].started_at_ms));
|
||||
assert_eq!(
|
||||
warnings.len(),
|
||||
1,
|
||||
|
||||
@@ -3937,7 +3937,7 @@ async fn conversation_user_text_turn_is_not_sent_to_realtime(ephemeral: bool) ->
|
||||
ThreadTimelineEntry::Realtime { item, .. }
|
||||
if matches!(&item.content, ThreadRealtimeItemContent::TranscriptSegment { text, .. } if text == "spoken before typed input") => Some("speech"),
|
||||
ThreadTimelineEntry::Item { item, .. }
|
||||
if matches!(item.as_ref(), codex_app_server_protocol::ThreadItem::UserMessage { .. }) => Some("typed input"),
|
||||
if matches!(&item.payload, codex_app_server_protocol::ThreadItemPayload::UserMessage { .. }) => Some("typed input"),
|
||||
_ => None,
|
||||
}).collect::<Vec<_>>(),
|
||||
vec!["speech", "typed input"]
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::Duration;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_app_server_protocol::ThreadHistoryBuilder;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::test_support::CloudConfigBundleFixture;
|
||||
@@ -203,14 +203,15 @@ async fn interrupted_one_shot_launch_failure_completes_and_persists() -> Result<
|
||||
.context("interrupted turn in persisted history")?;
|
||||
assert_eq!(turn.status, TurnStatus::Interrupted);
|
||||
let commands = turn
|
||||
.items
|
||||
.into_iter()
|
||||
.item_payloads()
|
||||
.filter(|item| item.id() == CALL_ID)
|
||||
.collect::<Vec<_>>();
|
||||
// History reconstructs persisted fields; model_context is only present on live items.
|
||||
assert_eq!(
|
||||
commands,
|
||||
vec![ThreadItem::from(persisted_completions[0].item.clone())]
|
||||
vec![&ThreadItemPayload::from(
|
||||
persisted_completions[0].item.clone()
|
||||
)]
|
||||
);
|
||||
assert!(
|
||||
matches!(aborted, EventMsg::TurnAborted(event) if event.turn_id.as_deref() == Some(completed[0].turn_id.as_str()))
|
||||
|
||||
@@ -5,7 +5,7 @@ use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_core::config::Config;
|
||||
@@ -67,16 +67,16 @@ impl EventProcessorWithHumanOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_item_started(&self, item: &ThreadItem) {
|
||||
fn render_item_started(&self, item: &ThreadItemPayload) {
|
||||
match item {
|
||||
ThreadItem::CommandExecution { command, cwd, .. } => {
|
||||
ThreadItemPayload::CommandExecution { command, cwd, .. } => {
|
||||
eprintln!(
|
||||
"{}\n{} in {cwd}",
|
||||
"exec".style(self.italic).style(self.magenta),
|
||||
command.style(self.bold),
|
||||
);
|
||||
}
|
||||
ThreadItem::McpToolCall { server, tool, .. } => {
|
||||
ThreadItemPayload::McpToolCall { server, tool, .. } => {
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
"mcp:".style(self.bold),
|
||||
@@ -84,22 +84,22 @@ impl EventProcessorWithHumanOutput {
|
||||
"started".style(self.dimmed)
|
||||
);
|
||||
}
|
||||
ThreadItem::WebSearch(item) => {
|
||||
ThreadItemPayload::WebSearch(item) => {
|
||||
eprintln!("{} {}", "web search:".style(self.bold), item.query);
|
||||
}
|
||||
ThreadItem::FileChange { .. } => {
|
||||
ThreadItemPayload::FileChange { .. } => {
|
||||
eprintln!("{}", "apply patch".style(self.bold));
|
||||
}
|
||||
ThreadItem::CollabAgentToolCall { tool, .. } => {
|
||||
ThreadItemPayload::CollabAgentToolCall { tool, .. } => {
|
||||
eprintln!("{} {:?}", "collab:".style(self.bold), tool);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_item_completed(&mut self, item: ThreadItem) {
|
||||
fn render_item_completed(&mut self, item: ThreadItemPayload) {
|
||||
match item {
|
||||
ThreadItem::AgentMessage { text, .. } => {
|
||||
ThreadItemPayload::AgentMessage { text, .. } => {
|
||||
eprintln!(
|
||||
"{}\n{}",
|
||||
"codex".style(self.italic).style(self.magenta),
|
||||
@@ -108,7 +108,7 @@ impl EventProcessorWithHumanOutput {
|
||||
self.final_message = Some(text);
|
||||
self.final_message_rendered = true;
|
||||
}
|
||||
ThreadItem::Reasoning {
|
||||
ThreadItemPayload::Reasoning {
|
||||
summary, content, ..
|
||||
} => {
|
||||
if self.show_agent_reasoning
|
||||
@@ -119,7 +119,7 @@ impl EventProcessorWithHumanOutput {
|
||||
eprintln!("{}", text.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
command: _,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
@@ -163,7 +163,7 @@ impl EventProcessorWithHumanOutput {
|
||||
eprintln!("{output}");
|
||||
}
|
||||
}
|
||||
ThreadItem::FileChange {
|
||||
ThreadItemPayload::FileChange {
|
||||
changes, status, ..
|
||||
} => {
|
||||
let status_text = match status {
|
||||
@@ -177,7 +177,7 @@ impl EventProcessorWithHumanOutput {
|
||||
eprintln!("{}", change.path.style(self.dimmed));
|
||||
}
|
||||
}
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
server,
|
||||
tool,
|
||||
status,
|
||||
@@ -199,10 +199,10 @@ impl EventProcessorWithHumanOutput {
|
||||
eprintln!("{}", error.message.style(self.red));
|
||||
}
|
||||
}
|
||||
ThreadItem::WebSearch(item) => {
|
||||
ThreadItemPayload::WebSearch(item) => {
|
||||
eprintln!("{} {}", "web search:".style(self.bold), item.query);
|
||||
}
|
||||
ThreadItem::ContextCompaction { .. } => {
|
||||
ThreadItemPayload::ContextCompaction { .. } => {
|
||||
eprintln!("{}", "context compacted".style(self.dimmed));
|
||||
}
|
||||
_ => {}
|
||||
@@ -287,11 +287,11 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
CodexStatus::Running
|
||||
}
|
||||
ServerNotification::ItemStarted(notification) => {
|
||||
self.render_item_started(¬ification.item);
|
||||
self.render_item_started(¬ification.item.payload);
|
||||
CodexStatus::Running
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
self.render_item_completed(notification.item);
|
||||
self.render_item_completed(notification.item.payload);
|
||||
CodexStatus::Running
|
||||
}
|
||||
ServerNotification::ModelRerouted(notification) => {
|
||||
@@ -499,17 +499,19 @@ fn reasoning_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn final_message_from_turn_items(items: &[ThreadItem]) -> Option<String> {
|
||||
fn final_message_from_turn_items(
|
||||
items: &[impl std::borrow::Borrow<ThreadItemPayload>],
|
||||
) -> Option<String> {
|
||||
items
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::AgentMessage { text, .. } => Some(text.clone()),
|
||||
.find_map(|item| match item.borrow() {
|
||||
ThreadItemPayload::AgentMessage { text, .. } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.or_else(|| {
|
||||
items.iter().rev().find_map(|item| match item {
|
||||
ThreadItem::Plan { text, .. } => Some(text.clone()),
|
||||
items.iter().rev().find_map(|item| match item.borrow() {
|
||||
ThreadItemPayload::Plan { text, .. } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
@@ -352,7 +353,7 @@ async fn config_summary_entries_include_runtime_workspace_roots() {
|
||||
#[test]
|
||||
fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "first".to_string(),
|
||||
phase: None,
|
||||
@@ -360,11 +361,11 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
ThreadItemPayload::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "plan".to_string(),
|
||||
},
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-2".to_string(),
|
||||
text: "second".to_string(),
|
||||
phase: None,
|
||||
@@ -380,16 +381,16 @@ fn final_message_from_turn_items_uses_latest_agent_message() {
|
||||
#[test]
|
||||
fn final_message_from_turn_items_falls_back_to_latest_plan() {
|
||||
let message = final_message_from_turn_items(&[
|
||||
ThreadItem::Reasoning {
|
||||
ThreadItemPayload::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: vec!["inspect".to_string()],
|
||||
content: Vec::new(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
ThreadItemPayload::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "first plan".to_string(),
|
||||
},
|
||||
ThreadItem::Plan {
|
||||
ThreadItemPayload::Plan {
|
||||
id: "plan-2".to_string(),
|
||||
text: "final plan".to_string(),
|
||||
},
|
||||
@@ -426,14 +427,14 @@ fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -478,14 +479,14 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
|
||||
@@ -10,7 +10,7 @@ use codex_app_server_protocol::McpToolCallStatus;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction;
|
||||
@@ -141,15 +141,15 @@ impl EventProcessorWithJsonOutput {
|
||||
}
|
||||
|
||||
fn map_item_with_id(
|
||||
item: ThreadItem,
|
||||
item: ThreadItemPayload,
|
||||
make_id: impl FnOnce() -> String,
|
||||
) -> Option<ExecThreadItem> {
|
||||
match item {
|
||||
ThreadItem::AgentMessage { text, .. } => Some(ExecThreadItem {
|
||||
ThreadItemPayload::AgentMessage { text, .. } => Some(ExecThreadItem {
|
||||
id: make_id(),
|
||||
details: ThreadItemDetails::AgentMessage(AgentMessageItem { text }),
|
||||
}),
|
||||
ThreadItem::Reasoning { summary, .. } => {
|
||||
ThreadItemPayload::Reasoning { summary, .. } => {
|
||||
let text = summary.join("\n");
|
||||
if text.trim().is_empty() {
|
||||
return None;
|
||||
@@ -159,7 +159,7 @@ impl EventProcessorWithJsonOutput {
|
||||
details: ThreadItemDetails::Reasoning(ReasoningItem { text }),
|
||||
})
|
||||
}
|
||||
ThreadItem::CommandExecution {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
command,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
@@ -179,7 +179,7 @@ impl EventProcessorWithJsonOutput {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ThreadItem::FileChange {
|
||||
ThreadItemPayload::FileChange {
|
||||
changes, status, ..
|
||||
} => Some(ExecThreadItem {
|
||||
id: make_id(),
|
||||
@@ -204,7 +204,7 @@ impl EventProcessorWithJsonOutput {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ThreadItem::McpToolCall {
|
||||
ThreadItemPayload::McpToolCall {
|
||||
server,
|
||||
tool,
|
||||
status,
|
||||
@@ -233,7 +233,7 @@ impl EventProcessorWithJsonOutput {
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
tool,
|
||||
sender_thread_id,
|
||||
receiver_thread_ids,
|
||||
@@ -300,7 +300,7 @@ impl EventProcessorWithJsonOutput {
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ThreadItem::WebSearch(item) => Some(ExecThreadItem {
|
||||
ThreadItemPayload::WebSearch(item) => Some(ExecThreadItem {
|
||||
id: make_id(),
|
||||
details: ThreadItemDetails::WebSearch(WebSearchItem {
|
||||
id: item.id,
|
||||
@@ -340,9 +340,9 @@ impl EventProcessorWithJsonOutput {
|
||||
.unwrap_or_else(|| self.next_item_id())
|
||||
}
|
||||
|
||||
fn map_started_item(&mut self, item: ThreadItem) -> Option<ExecThreadItem> {
|
||||
fn map_started_item(&mut self, item: ThreadItemPayload) -> Option<ExecThreadItem> {
|
||||
match item {
|
||||
ThreadItem::AgentMessage { .. } | ThreadItem::Reasoning { .. } => None,
|
||||
ThreadItemPayload::AgentMessage { .. } | ThreadItemPayload::Reasoning { .. } => None,
|
||||
other => {
|
||||
let raw_id = other.id().to_string();
|
||||
Self::map_item_with_id(other, || self.started_item_id(&raw_id))
|
||||
@@ -350,14 +350,14 @@ impl EventProcessorWithJsonOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_completed_item_mut(&mut self, item: ThreadItem) -> Option<ExecThreadItem> {
|
||||
if let ThreadItem::Reasoning { summary, .. } = &item
|
||||
fn map_completed_item_mut(&mut self, item: ThreadItemPayload) -> Option<ExecThreadItem> {
|
||||
if let ThreadItemPayload::Reasoning { summary, .. } = &item
|
||||
&& summary.join("\n").trim().is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
match &item {
|
||||
ThreadItem::AgentMessage { .. } | ThreadItem::Reasoning { .. } => {
|
||||
ThreadItemPayload::AgentMessage { .. } | ThreadItemPayload::Reasoning { .. } => {
|
||||
Self::map_item_with_id(item, || self.next_item_id())
|
||||
}
|
||||
other => {
|
||||
@@ -369,7 +369,7 @@ impl EventProcessorWithJsonOutput {
|
||||
|
||||
fn reconcile_unfinished_started_items(
|
||||
&mut self,
|
||||
turn_items: &[ThreadItem],
|
||||
turn_items: &[codex_app_server_protocol::ThreadItem],
|
||||
) -> Vec<ThreadEvent> {
|
||||
turn_items
|
||||
.iter()
|
||||
@@ -378,23 +378,25 @@ impl EventProcessorWithJsonOutput {
|
||||
if !self.raw_to_exec_item_id.contains_key(&raw_id) {
|
||||
return None;
|
||||
}
|
||||
self.map_completed_item_mut(item.clone())
|
||||
self.map_completed_item_mut(item.payload.clone())
|
||||
.map(|item| ThreadEvent::ItemCompleted(ItemCompletedEvent { item }))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn final_message_from_turn_items(items: &[ThreadItem]) -> Option<String> {
|
||||
fn final_message_from_turn_items(
|
||||
items: &[impl std::borrow::Borrow<ThreadItemPayload>],
|
||||
) -> Option<String> {
|
||||
items
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(|item| match item {
|
||||
ThreadItem::AgentMessage { text, .. } => Some(text.clone()),
|
||||
.find_map(|item| match item.borrow() {
|
||||
ThreadItemPayload::AgentMessage { text, .. } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.or_else(|| {
|
||||
items.iter().rev().find_map(|item| match item {
|
||||
ThreadItem::Plan { text, .. } => Some(text.clone()),
|
||||
items.iter().rev().find_map(|item| match item.borrow() {
|
||||
ThreadItemPayload::Plan { text, .. } => Some(text.clone()),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
@@ -475,13 +477,13 @@ impl EventProcessorWithJsonOutput {
|
||||
CodexStatus::Running
|
||||
}
|
||||
ServerNotification::ItemStarted(notification) => {
|
||||
if let Some(item) = self.map_started_item(notification.item) {
|
||||
if let Some(item) = self.map_started_item(notification.item.payload) {
|
||||
events.push(ThreadEvent::ItemStarted(ItemStartedEvent { item }));
|
||||
}
|
||||
CodexStatus::Running
|
||||
}
|
||||
ServerNotification::ItemCompleted(notification) => {
|
||||
if let Some(item) = self.map_completed_item_mut(notification.item) {
|
||||
if let Some(item) = self.map_completed_item_mut(notification.item.payload) {
|
||||
if let ThreadItemDetails::AgentMessage(AgentMessageItem { text }) =
|
||||
&item.details
|
||||
{
|
||||
|
||||
@@ -12,8 +12,10 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(Some(output_path.clone()));
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
codex_app_server_protocol::ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
@@ -21,10 +23,9 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(collected.status, CodexStatus::Running);
|
||||
@@ -95,8 +96,10 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
codex_app_server_protocol::ItemCompletedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
codex_app_server_protocol::ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "search service".to_string(),
|
||||
tool: "web_run".to_string(),
|
||||
@@ -115,10 +118,9 @@ fn mcp_tool_call_result_preserves_meta_in_jsonl_event() {
|
||||
error: None,
|
||||
duration_ms: Some(42),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(collected.status, CodexStatus::Running);
|
||||
|
||||
@@ -39,7 +39,6 @@ use codex_app_server_protocol::Thread as AppServerThread;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem as AppServerThreadItem;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadListResponse;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
@@ -1793,7 +1792,7 @@ fn should_backfill_turn_completed_items(
|
||||
fn turn_items_for_thread(
|
||||
thread: &AppServerThread,
|
||||
turn_id: &str,
|
||||
) -> Option<Vec<AppServerThreadItem>> {
|
||||
) -> Option<Vec<codex_app_server_protocol::ThreadItem>> {
|
||||
thread
|
||||
.turns
|
||||
.iter()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::AuthRecoveryNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload as AppServerThreadItemPayload;
|
||||
use codex_otel::set_parent_from_w3c_trace_context;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
@@ -387,14 +389,16 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
items: vec![ThreadItem::untimed(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
)],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -405,10 +409,10 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![AppServerThreadItem::Plan {
|
||||
items: vec![ThreadItem::untimed(AppServerThreadItemPayload::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "ship it".to_string(),
|
||||
}],
|
||||
})],
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -420,14 +424,16 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
|
||||
assert_eq!(
|
||||
turn_items_for_thread(&thread, "turn-1"),
|
||||
Some(vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}])
|
||||
Some(vec![ThreadItem::untimed(
|
||||
AppServerThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
)])
|
||||
);
|
||||
assert_eq!(turn_items_for_thread(&thread, "missing-turn"), None);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_app_server_protocol::PatchApplyStatus as ApiPatchApplyStatus;
|
||||
use codex_app_server_protocol::PatchChangeKind as ApiPatchChangeKind;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::TokenUsageBreakdown;
|
||||
use codex_app_server_protocol::Turn;
|
||||
@@ -168,7 +169,7 @@ fn turn_started_emits_turn_started_event() {
|
||||
#[test]
|
||||
fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let command_item = ThreadItem::CommandExecution {
|
||||
let command_item = ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "cmd-1".to_string(),
|
||||
@@ -185,13 +186,14 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
duration_ms: None,
|
||||
};
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: command_item,
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
command_item,
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
assert_eq!(
|
||||
started,
|
||||
CollectedThreadEvents {
|
||||
@@ -211,8 +213,10 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
);
|
||||
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::CommandExecution {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "cmd-1".to_string(),
|
||||
@@ -228,10 +232,9 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
|
||||
exit_code: Some(0),
|
||||
duration_ms: Some(3),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
assert_eq!(
|
||||
completed,
|
||||
@@ -257,16 +260,17 @@ fn empty_reasoning_items_are_ignored() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::Reasoning {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: Vec::new(),
|
||||
content: vec!["raw reasoning".to_string()],
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -283,15 +287,16 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let ignored = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::Plan {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "ignored plan".to_string(),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -303,8 +308,10 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
|
||||
);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "message-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -312,10 +319,9 @@ fn unsupported_items_do_not_consume_synthetic_ids() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -339,16 +345,17 @@ fn reasoning_items_emit_summary_not_raw_content() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::Reasoning {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::Reasoning {
|
||||
id: "reasoning-1".to_string(),
|
||||
summary: vec!["safe summary".to_string()],
|
||||
content: vec!["raw reasoning".to_string()],
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -372,8 +379,10 @@ fn web_search_completion_preserves_query_and_action() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::WebSearch(ApiWebSearchItem {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::WebSearch(ApiWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "rust async await".to_string(),
|
||||
action: Some(ApiWebSearchAction::Search {
|
||||
@@ -382,10 +391,9 @@ fn web_search_completion_preserves_query_and_action() {
|
||||
}),
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -436,17 +444,18 @@ fn web_search_page_actions_and_results_survive_json_output() {
|
||||
] {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::WebSearch(ApiWebSearchItem {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::WebSearch(ApiWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: url.to_string(),
|
||||
action: Some(action.clone()),
|
||||
results: results.clone(),
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
let mut expected_item = json!({
|
||||
@@ -470,22 +479,25 @@ fn web_search_page_actions_and_results_survive_json_output() {
|
||||
fn web_search_start_and_completion_reuse_item_id() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::WebSearch(ApiWebSearchItem {
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::WebSearch(ApiWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::WebSearch(ApiWebSearchItem {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::WebSearch(ApiWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "rust async await".to_string(),
|
||||
action: Some(ApiWebSearchAction::Search {
|
||||
@@ -494,10 +506,9 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
}),
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -543,9 +554,11 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "server_a".to_string(),
|
||||
tool: "tool_x".to_string(),
|
||||
@@ -560,13 +573,14 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-1".to_string(),
|
||||
server: "server_a".to_string(),
|
||||
tool: "tool_x".to_string(),
|
||||
@@ -585,10 +599,9 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
|
||||
error: None,
|
||||
duration_ms: Some(1_000),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -640,8 +653,10 @@ fn mcp_tool_call_failure_sets_failed_status() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-2".to_string(),
|
||||
server: "server_b".to_string(),
|
||||
tool: "tool_y".to_string(),
|
||||
@@ -658,10 +673,9 @@ fn mcp_tool_call_failure_sets_failed_status() {
|
||||
}),
|
||||
duration_ms: Some(5),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -691,9 +705,11 @@ fn mcp_tool_call_failure_sets_failed_status() {
|
||||
fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-3".to_string(),
|
||||
server: "server_c".to_string(),
|
||||
tool: "tool_z".to_string(),
|
||||
@@ -708,13 +724,14 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
error: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::McpToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::McpToolCall {
|
||||
id: "mcp-3".to_string(),
|
||||
server: "server_c".to_string(),
|
||||
tool: "tool_z".to_string(),
|
||||
@@ -736,10 +753,9 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
error: None,
|
||||
duration_ms: Some(10),
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -793,9 +809,11 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
|
||||
fn collab_spawn_begin_and_end_emit_item_events() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-parent".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "collab-1".to_string(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: ApiCollabAgentToolCallStatus::InProgress,
|
||||
@@ -806,13 +824,14 @@ fn collab_spawn_begin_and_end_emit_item_events() {
|
||||
reasoning_effort: None,
|
||||
agents_states: std::collections::HashMap::new(),
|
||||
},
|
||||
thread_id: "thread-parent".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
let completed = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::CollabAgentToolCall {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-parent".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CollabAgentToolCall {
|
||||
id: "collab-1".to_string(),
|
||||
tool: CollabAgentTool::SpawnAgent,
|
||||
status: ApiCollabAgentToolCallStatus::Completed,
|
||||
@@ -829,10 +848,9 @@ fn collab_spawn_begin_and_end_emit_item_events() {
|
||||
},
|
||||
)]),
|
||||
},
|
||||
thread_id: "thread-parent".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -886,8 +904,10 @@ fn file_change_completion_maps_change_kinds() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::FileChange {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::FileChange {
|
||||
id: "patch-1".to_string(),
|
||||
changes: vec![
|
||||
ApiFileUpdateChange {
|
||||
@@ -908,10 +928,9 @@ fn file_change_completion_maps_change_kinds() {
|
||||
],
|
||||
status: ApiPatchApplyStatus::Completed,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -949,8 +968,10 @@ fn file_change_declined_maps_to_failed_status() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::FileChange {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::FileChange {
|
||||
id: "patch-2".to_string(),
|
||||
changes: vec![ApiFileUpdateChange {
|
||||
path: "file.txt".to_string(),
|
||||
@@ -959,10 +980,9 @@ fn file_change_declined_maps_to_failed_status() {
|
||||
}],
|
||||
status: ApiPatchApplyStatus::Declined,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -990,8 +1010,10 @@ fn agent_message_item_updates_final_message() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -999,10 +1021,9 @@ fn agent_message_item_updates_final_message() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -1026,9 +1047,11 @@ fn agent_message_item_updates_final_message() {
|
||||
fn agent_message_item_started_is_ignored() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "hello".to_string(),
|
||||
phase: None,
|
||||
@@ -1036,10 +1059,9 @@ fn agent_message_item_started_is_ignored() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
collected,
|
||||
@@ -1055,16 +1077,17 @@ fn reasoning_item_completed_uses_synthetic_id() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::Reasoning {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::Reasoning {
|
||||
id: "rs-1".to_string(),
|
||||
summary: vec!["thinking...".to_string()],
|
||||
content: vec!["raw".to_string()],
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
@@ -1384,14 +1407,14 @@ fn turn_completion_recovers_final_message_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -1417,9 +1440,11 @@ fn turn_completion_recovers_final_message_from_turn_items() {
|
||||
fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let started =
|
||||
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
item: ThreadItem::CommandExecution {
|
||||
let started = processor.collect_thread_events(ServerNotification::ItemStarted(
|
||||
ItemStartedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "cmd-1".to_string(),
|
||||
@@ -1435,10 +1460,9 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
exit_code: None,
|
||||
duration_ms: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
}));
|
||||
/*started_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
assert_eq!(
|
||||
started,
|
||||
CollectedThreadEvents {
|
||||
@@ -1464,7 +1488,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::CommandExecution {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::CommandExecution {
|
||||
model_context: None,
|
||||
sandbox_type: None,
|
||||
id: "cmd-1".to_string(),
|
||||
@@ -1479,7 +1503,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
aggregated_output: Some("a.txt\n".to_string()),
|
||||
exit_code: Some(0),
|
||||
duration_ms: Some(3),
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -1517,8 +1541,10 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let _ = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-stale".to_string(),
|
||||
text: "stale answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1526,10 +1552,9 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
|
||||
@@ -1539,14 +1564,14 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "final answer".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -1572,8 +1597,10 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let _ = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-streamed".to_string(),
|
||||
text: "streamed answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1581,10 +1608,9 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
let completed = processor.collect_thread_events(ServerNotification::TurnCompleted(
|
||||
@@ -1621,8 +1647,10 @@ fn failed_turn_clears_stale_final_message() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::AgentMessage {
|
||||
ItemCompletedNotification::new(
|
||||
"thread-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
ThreadItemPayload::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
text: "partial answer".to_string(),
|
||||
phase: None,
|
||||
@@ -1630,10 +1658,9 @@ fn failed_turn_clears_stale_final_message() {
|
||||
delivery: None,
|
||||
questions: None,
|
||||
},
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
/*started_at_ms*/ None,
|
||||
/*completed_at_ms*/ 0,
|
||||
),
|
||||
));
|
||||
|
||||
assert_eq!(collected.status, CodexStatus::Running);
|
||||
@@ -1676,10 +1703,10 @@ fn turn_completion_falls_back_to_final_plan_text() {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::Plan {
|
||||
items: vec![ThreadItem::untimed(ThreadItemPayload::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
text: "ship the typed adapter".to_string(),
|
||||
}],
|
||||
})],
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
|
||||
@@ -227,7 +227,7 @@ fn turn_complete_item(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::build_turns_from_rollout_items;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::path::Path;
|
||||
@@ -258,15 +258,15 @@ mod tests {
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(turns[1].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[1].items[1],
|
||||
ThreadItem::AgentMessage {
|
||||
turns[1].item_payloads().next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
id: "item-4".into(),
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
delivery: None,
|
||||
questions: None,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,8 +292,8 @@ mod tests {
|
||||
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(
|
||||
turns[0].items.last(),
|
||||
Some(&ThreadItem::AgentMessage {
|
||||
turns[0].item_payloads().next_back(),
|
||||
Some(&ThreadItemPayload::AgentMessage {
|
||||
id: "item-3".into(),
|
||||
text: EXTERNAL_SESSION_IMPORTED_MARKER.into(),
|
||||
phase: None,
|
||||
|
||||
@@ -1695,6 +1695,34 @@ pub enum EventMsg {
|
||||
SubAgentActivity(SubAgentActivityEvent),
|
||||
}
|
||||
|
||||
impl EventMsg {
|
||||
/// Returns the ID and start time of the command this approval opens.
|
||||
/// Stdin and network reviews have their own prompts for an existing operation.
|
||||
pub fn command_approval_start(&self) -> Option<(&str, i64)> {
|
||||
match self {
|
||||
Self::ExecApprovalRequest(request)
|
||||
if request.approval_id.is_none() && request.network_approval_context.is_none() =>
|
||||
{
|
||||
Some((&request.call_id, request.started_at_ms))
|
||||
}
|
||||
Self::GuardianAssessment(assessment)
|
||||
if assessment.status == GuardianAssessmentStatus::InProgress
|
||||
&& matches!(
|
||||
assessment.action,
|
||||
GuardianAssessmentAction::Command { .. }
|
||||
| GuardianAssessmentAction::Execve { .. }
|
||||
) =>
|
||||
{
|
||||
Some((
|
||||
assessment.target_item_id.as_deref()?,
|
||||
assessment.started_at_ms,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS, EnumIter)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HookEventName {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use codex_app_server_protocol::ThreadHistoryChangeSet;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
@@ -428,7 +428,8 @@ WHERE thread_id = ?
|
||||
.map_err(thread_history_error)?;
|
||||
}
|
||||
|
||||
for item in changes.changed_items {
|
||||
for change in changes.changed_items {
|
||||
let item = change.item;
|
||||
let created_at_ms =
|
||||
item.started_at_ms
|
||||
.or(fallback_created_at_ms)
|
||||
@@ -437,8 +438,8 @@ WHERE thread_id = ?
|
||||
"thread history projection for {thread_id} is missing an item creation timestamp"
|
||||
),
|
||||
})?;
|
||||
let item_id = item.item.id().to_string();
|
||||
let item_json = serde_json::to_string(&item.item).map_err(thread_history_error)?;
|
||||
let item_id = item.id().to_string();
|
||||
let item_json = serde_json::to_string(&item.payload).map_err(thread_history_error)?;
|
||||
// Completed items are immutable: local producers emit ItemCompleted exactly once per
|
||||
// item. Tolerate an unexpected duplicate defensively so it cannot poison materialization,
|
||||
// preserving the original creation ordinal and timestamp while updating its snapshot.
|
||||
@@ -465,7 +466,7 @@ ON CONFLICT(thread_id, turn_id, item_id) DO UPDATE SET
|
||||
"#,
|
||||
)
|
||||
.bind(thread_id)
|
||||
.bind(item.turn_id.as_str())
|
||||
.bind(change.turn_id.as_str())
|
||||
.bind(item_id.as_str())
|
||||
.bind(rollout_ordinal)
|
||||
.bind(rollout_ordinal)
|
||||
@@ -480,8 +481,8 @@ ON CONFLICT(thread_id, turn_id, item_id) DO UPDATE SET
|
||||
|
||||
// Keep summary item IDs on the turn row so reads do not need to scan every item in the
|
||||
// turn.
|
||||
match item.item {
|
||||
ThreadItem::UserMessage { .. } => {
|
||||
match item.payload {
|
||||
ThreadItemPayload::UserMessage { .. } => {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE thread_turns
|
||||
@@ -494,12 +495,12 @@ WHERE thread_id = ?
|
||||
)
|
||||
.bind(item_id.as_str())
|
||||
.bind(thread_id)
|
||||
.bind(item.turn_id.as_str())
|
||||
.bind(change.turn_id.as_str())
|
||||
.execute(&mut **transaction)
|
||||
.await
|
||||
.map_err(thread_history_error)?;
|
||||
}
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
phase: Some(MessagePhase::FinalAnswer),
|
||||
..
|
||||
} => {
|
||||
@@ -515,32 +516,32 @@ WHERE thread_id = ?
|
||||
)
|
||||
.bind(item_id.as_str())
|
||||
.bind(thread_id)
|
||||
.bind(item.turn_id.as_str())
|
||||
.bind(change.turn_id.as_str())
|
||||
.execute(&mut **transaction)
|
||||
.await
|
||||
.map_err(thread_history_error)?;
|
||||
}
|
||||
ThreadItem::AgentMessage {
|
||||
ThreadItemPayload::AgentMessage {
|
||||
phase: Some(MessagePhase::Commentary | MessagePhase::PartialAnswer) | None,
|
||||
..
|
||||
}
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::FunctionCallOutput { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::CommandExecution { .. }
|
||||
| ThreadItem::FileChange { .. }
|
||||
| ThreadItem::McpToolCall { .. }
|
||||
| ThreadItem::DynamicToolCall { .. }
|
||||
| ThreadItem::CollabAgentToolCall { .. }
|
||||
| ThreadItem::SubAgentActivity { .. }
|
||||
| ThreadItem::WebSearch(_)
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::Sleep(_)
|
||||
| ThreadItem::ImageGeneration(_)
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
| ThreadItem::ContextCompaction { .. } => {}
|
||||
| ThreadItemPayload::HookPrompt { .. }
|
||||
| ThreadItemPayload::FunctionCallOutput { .. }
|
||||
| ThreadItemPayload::Plan { .. }
|
||||
| ThreadItemPayload::Reasoning { .. }
|
||||
| ThreadItemPayload::CommandExecution { .. }
|
||||
| ThreadItemPayload::FileChange { .. }
|
||||
| ThreadItemPayload::McpToolCall { .. }
|
||||
| ThreadItemPayload::DynamicToolCall { .. }
|
||||
| ThreadItemPayload::CollabAgentToolCall { .. }
|
||||
| ThreadItemPayload::SubAgentActivity { .. }
|
||||
| ThreadItemPayload::WebSearch(_)
|
||||
| ThreadItemPayload::ImageView { .. }
|
||||
| ThreadItemPayload::Sleep(_)
|
||||
| ThreadItemPayload::ImageGeneration(_)
|
||||
| ThreadItemPayload::EnteredReviewMode { .. }
|
||||
| ThreadItemPayload::ExitedReviewMode { .. }
|
||||
| ThreadItemPayload::ContextCompaction { .. } => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -97,7 +97,18 @@ pub(in crate::local) async fn list_timeline(
|
||||
.map(|value| sqlite_integer(value, "rollout ordinal"))
|
||||
.transpose()?
|
||||
.unwrap_or(i64::MAX);
|
||||
let segment_rows = sqlx::query_as::<_, (i64, i64, String, Option<String>, String)>(
|
||||
let segment_rows = sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
i64,
|
||||
i64,
|
||||
String,
|
||||
Option<String>,
|
||||
String,
|
||||
Option<i64>,
|
||||
Option<i64>,
|
||||
),
|
||||
>(
|
||||
r#"
|
||||
WITH starts AS (
|
||||
SELECT rollout_ordinal, 0 AS kind, turn_id AS id, turn_id,
|
||||
@@ -109,7 +120,7 @@ WHERE thread_id = ?1 AND rollout_ordinal >= ?2
|
||||
AND (rollout_ordinal, 0, turn_id) < (?4, ?5, ?6)
|
||||
ORDER BY rollout_ordinal DESC, turn_id DESC LIMIT ?7
|
||||
), items AS (
|
||||
SELECT rollout_ordinal, 1 AS kind, item_id AS id, turn_id, item_json
|
||||
SELECT rollout_ordinal, 1 AS kind, item_id AS id, turn_id, item_json, started_at_ms, completed_at_ms
|
||||
FROM thread_items
|
||||
WHERE thread_id = ?1 AND rollout_ordinal >= ?2
|
||||
AND rollout_ordinal < ?3 AND rollout_ordinal <= ?4
|
||||
@@ -134,10 +145,10 @@ WHERE thread_id = ?1 AND rollout_end_ordinal >= ?2
|
||||
AND (rollout_end_ordinal, 3, turn_id) < (?4, ?5, ?6)
|
||||
ORDER BY rollout_end_ordinal DESC, turn_id DESC LIMIT ?7
|
||||
)
|
||||
SELECT * FROM starts
|
||||
SELECT *, NULL, NULL FROM starts
|
||||
UNION ALL SELECT * FROM items
|
||||
UNION ALL SELECT * FROM realtime
|
||||
UNION ALL SELECT * FROM ends
|
||||
UNION ALL SELECT *, NULL, NULL FROM realtime
|
||||
UNION ALL SELECT *, NULL, NULL FROM ends
|
||||
ORDER BY rollout_ordinal DESC, kind DESC, id DESC
|
||||
LIMIT ?7
|
||||
"#,
|
||||
@@ -153,7 +164,9 @@ LIMIT ?7
|
||||
.await
|
||||
.map_err(thread_history_error)?;
|
||||
|
||||
for (position, kind, _id, turn_id, item_json) in segment_rows {
|
||||
for (position, kind, _id, turn_id, item_json, started_at_ms, completed_at_ms) in
|
||||
segment_rows
|
||||
{
|
||||
let position = u64::try_from(position).map_err(thread_history_error)?;
|
||||
rows.push(match kind {
|
||||
0 | 3 => serde_json::from_str::<ThreadTimelineEntry>(&item_json)
|
||||
@@ -161,10 +174,11 @@ LIMIT ?7
|
||||
1 => ThreadTimelineEntry::Item {
|
||||
position,
|
||||
turn_id: turn_id.ok_or_else(|| thread_history_error("missing turn ID"))?,
|
||||
item: Box::new(
|
||||
serde_json::from_str::<ThreadItem>(&item_json)
|
||||
.map_err(thread_history_error)?,
|
||||
),
|
||||
item: Box::new(ThreadItem {
|
||||
payload: serde_json::from_str(&item_json).map_err(thread_history_error)?,
|
||||
started_at_ms,
|
||||
completed_at_ms,
|
||||
}),
|
||||
},
|
||||
2 => ThreadTimelineEntry::Realtime {
|
||||
position,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::strip_user_message_prefix;
|
||||
@@ -176,12 +176,11 @@ ORDER BY rollout_ordinal ASC
|
||||
let mut matching_occurrences = 0_usize;
|
||||
while let Some(row) = rows.try_next().await.map_err(thread_history_error)? {
|
||||
let row = candidate_row(row)?;
|
||||
let item =
|
||||
serde_json::from_str::<ThreadItem>(row.item_json.as_str()).map_err(|err| {
|
||||
ThreadStoreError::Internal {
|
||||
message: format!("failed to deserialize stored thread item: {err}"),
|
||||
}
|
||||
})?;
|
||||
let item = serde_json::from_str::<ThreadItemPayload>(row.item_json.as_str()).map_err(
|
||||
|err| ThreadStoreError::Internal {
|
||||
message: format!("failed to deserialize stored thread item: {err}"),
|
||||
},
|
||||
)?;
|
||||
let Some(text) = searchable_text(&item) else {
|
||||
continue;
|
||||
};
|
||||
@@ -309,9 +308,9 @@ fn invalid_cursor(cursor: &str) -> ThreadStoreError {
|
||||
}
|
||||
}
|
||||
|
||||
fn searchable_text(item: &ThreadItem) -> Option<Cow<'_, str>> {
|
||||
fn searchable_text(item: &ThreadItemPayload) -> Option<Cow<'_, str>> {
|
||||
match item {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
ThreadItemPayload::UserMessage { content, .. } => {
|
||||
let mut text_parts = content
|
||||
.iter()
|
||||
.filter_map(|input| match input {
|
||||
@@ -335,27 +334,27 @@ fn searchable_text(item: &ThreadItem) -> Option<Cow<'_, str>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadItem::AgentMessage { text, .. } => {
|
||||
ThreadItemPayload::AgentMessage { text, .. } => {
|
||||
let text = markdown_to_search_text(text);
|
||||
(!text.is_empty()).then_some(Cow::Owned(text))
|
||||
}
|
||||
ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::FunctionCallOutput { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. }
|
||||
| ThreadItem::CommandExecution { .. }
|
||||
| ThreadItem::FileChange { .. }
|
||||
| ThreadItem::McpToolCall { .. }
|
||||
| ThreadItem::DynamicToolCall { .. }
|
||||
| ThreadItem::CollabAgentToolCall { .. }
|
||||
| ThreadItem::SubAgentActivity { .. }
|
||||
| ThreadItem::WebSearch(_)
|
||||
| ThreadItem::ImageView { .. }
|
||||
| ThreadItem::Sleep(_)
|
||||
| ThreadItem::ImageGeneration(_)
|
||||
| ThreadItem::EnteredReviewMode { .. }
|
||||
| ThreadItem::ExitedReviewMode { .. }
|
||||
| ThreadItem::ContextCompaction { .. } => None,
|
||||
ThreadItemPayload::HookPrompt { .. }
|
||||
| ThreadItemPayload::FunctionCallOutput { .. }
|
||||
| ThreadItemPayload::Plan { .. }
|
||||
| ThreadItemPayload::Reasoning { .. }
|
||||
| ThreadItemPayload::CommandExecution { .. }
|
||||
| ThreadItemPayload::FileChange { .. }
|
||||
| ThreadItemPayload::McpToolCall { .. }
|
||||
| ThreadItemPayload::DynamicToolCall { .. }
|
||||
| ThreadItemPayload::CollabAgentToolCall { .. }
|
||||
| ThreadItemPayload::SubAgentActivity { .. }
|
||||
| ThreadItemPayload::WebSearch(_)
|
||||
| ThreadItemPayload::ImageView { .. }
|
||||
| ThreadItemPayload::Sleep(_)
|
||||
| ThreadItemPayload::ImageGeneration(_)
|
||||
| ThreadItemPayload::EnteredReviewMode { .. }
|
||||
| ThreadItemPayload::ExitedReviewMode { .. }
|
||||
| ThreadItemPayload::ContextCompaction { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ fn read_projection_batches(
|
||||
let fallback_created_at_ms = if changes
|
||||
.changed_items
|
||||
.iter()
|
||||
.any(|item| item.started_at_ms.is_none())
|
||||
.any(|change| change.item.started_at_ms.is_none())
|
||||
|| (!is_inherited_subagent_history
|
||||
&& matches!(&line.item, RolloutItem::RealtimeItem(_)))
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user