From aeb9cc1a5b43924a37c8723bb121833b722e9b3b Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 22 Jan 2026 18:06:59 -0800 Subject: [PATCH] Include elapsedMs in app-server protocol --- .../src/protocol/thread_history.rs | 31 +++++++-- .../app-server-protocol/src/protocol/v2.rs | 65 +++++++++++++++++-- 2 files changed, 85 insertions(+), 11 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index e6c679b418..79e07578a8 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -70,7 +70,11 @@ impl ThreadHistoryBuilder { let mut turn = self.new_turn(); let id = self.next_item_id(); let content = self.build_user_inputs(payload); - turn.items.push(ThreadItem::UserMessage { id, content }); + turn.items.push(ThreadItem::UserMessage { + id, + content, + elapsed_ms: None, + }); self.current_turn = Some(turn); } @@ -80,9 +84,11 @@ impl ThreadHistoryBuilder { } let id = self.next_item_id(); - self.ensure_turn() - .items - .push(ThreadItem::AgentMessage { id, text }); + self.ensure_turn().items.push(ThreadItem::AgentMessage { + id, + text, + elapsed_ms: None, + }); } fn handle_agent_reasoning(&mut self, payload: &AgentReasoningEvent) { @@ -102,6 +108,7 @@ impl ThreadHistoryBuilder { id, summary: vec![payload.text.clone()], content: Vec::new(), + elapsed_ms: None, }); } @@ -122,6 +129,7 @@ impl ThreadHistoryBuilder { id, summary: Vec::new(), content: vec![payload.text.clone()], + elapsed_ms: None, }); } @@ -296,6 +304,7 @@ mod tests { url: "https://example.com/one.png".into(), } ], + elapsed_ms: None, } ); assert_eq!( @@ -303,6 +312,7 @@ mod tests { ThreadItem::AgentMessage { id: "item-2".into(), text: "Hi there".into(), + elapsed_ms: None, } ); assert_eq!( @@ -311,6 +321,7 @@ mod tests { id: "item-3".into(), summary: vec!["thinking".into()], content: vec!["full reasoning".into()], + elapsed_ms: None, } ); @@ -325,6 +336,7 @@ mod tests { text: "Second turn".into(), text_elements: Vec::new(), }], + elapsed_ms: None, } ); assert_eq!( @@ -332,6 +344,7 @@ mod tests { ThreadItem::AgentMessage { id: "item-5".into(), text: "Reply two".into(), + elapsed_ms: None, } ); } @@ -370,6 +383,7 @@ mod tests { id: "item-2".into(), summary: vec!["first summary".into()], content: vec!["first content".into()], + elapsed_ms: None, } ); assert_eq!( @@ -378,6 +392,7 @@ mod tests { id: "item-4".into(), summary: vec!["second summary".into()], content: Vec::new(), + elapsed_ms: None, } ); } @@ -422,6 +437,7 @@ mod tests { text: "Please do the thing".into(), text_elements: Vec::new(), }], + elapsed_ms: None, } ); assert_eq!( @@ -429,6 +445,7 @@ mod tests { ThreadItem::AgentMessage { id: "item-2".into(), text: "Working...".into(), + elapsed_ms: None, } ); @@ -443,6 +460,7 @@ mod tests { text: "Let's try again".into(), text_elements: Vec::new(), }], + elapsed_ms: None, } ); assert_eq!( @@ -450,6 +468,7 @@ mod tests { ThreadItem::AgentMessage { id: "item-4".into(), text: "Second attempt complete.".into(), + elapsed_ms: None, } ); } @@ -500,10 +519,12 @@ mod tests { text: "First".into(), text_elements: Vec::new(), }], + elapsed_ms: None, }, ThreadItem::AgentMessage { id: "item-2".into(), text: "A1".into(), + elapsed_ms: None, }, ], }, @@ -518,10 +539,12 @@ mod tests { text: "Third".into(), text_elements: Vec::new(), }], + elapsed_ms: None, }, ThreadItem::AgentMessage { id: "item-4".into(), text: "A3".into(), + elapsed_ms: None, }, ], }, diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 66654e0517..8904e2d2d7 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1789,10 +1789,20 @@ impl From for UserInput { pub enum ThreadItem { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - UserMessage { id: String, content: Vec }, + UserMessage { + id: String, + content: Vec, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - AgentMessage { id: String, text: String }, + AgentMessage { + id: String, + text: String, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] Reasoning { @@ -1801,6 +1811,8 @@ pub enum ThreadItem { summary: Vec, #[serde(default)] content: Vec, + #[ts(type = "number | null")] + elapsed_ms: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -1824,6 +1836,8 @@ pub enum ThreadItem { /// The duration of the command execution in milliseconds. #[ts(type = "number | null")] duration_ms: Option, + #[ts(type = "number | null")] + elapsed_ms: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -1831,6 +1845,8 @@ pub enum ThreadItem { id: String, changes: Vec, status: PatchApplyStatus, + #[ts(type = "number | null")] + elapsed_ms: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -1845,6 +1861,8 @@ pub enum ThreadItem { /// The duration of the MCP tool call in milliseconds. #[ts(type = "number | null")] duration_ms: Option, + #[ts(type = "number | null")] + elapsed_ms: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -1864,19 +1882,41 @@ pub enum ThreadItem { prompt: Option, /// Last known status of the target agents, when available. agents_states: HashMap, + #[ts(type = "number | null")] + elapsed_ms: Option, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - WebSearch { id: String, query: String }, + WebSearch { + id: String, + query: String, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - ImageView { id: String, path: String }, + ImageView { + id: String, + path: String, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - EnteredReviewMode { id: String, review: String }, + EnteredReviewMode { + id: String, + review: String, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] - ExitedReviewMode { id: String, review: String }, + ExitedReviewMode { + id: String, + review: String, + #[ts(type = "number | null")] + elapsed_ms: Option, + }, } impl From for ThreadItem { @@ -1885,6 +1925,7 @@ impl From for ThreadItem { CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage { id: user.id, content: user.content.into_iter().map(UserInput::from).collect(), + elapsed_ms: None, }, CoreTurnItem::AgentMessage(agent) => { let text = agent @@ -1894,16 +1935,22 @@ impl From for ThreadItem { CoreAgentMessageContent::Text { text } => text, }) .collect::(); - ThreadItem::AgentMessage { id: agent.id, text } + ThreadItem::AgentMessage { + id: agent.id, + text, + elapsed_ms: None, + } } CoreTurnItem::Reasoning(reasoning) => ThreadItem::Reasoning { id: reasoning.id, summary: reasoning.summary_text, content: reasoning.raw_content, + elapsed_ms: None, }, CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch { id: search.id, query: search.query, + elapsed_ms: None, }, } } @@ -2548,6 +2595,7 @@ mod tests { path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"), }, ], + elapsed_ms: None, } ); @@ -2568,6 +2616,7 @@ mod tests { ThreadItem::AgentMessage { id: "agent-1".to_string(), text: "Hello world".to_string(), + elapsed_ms: None, } ); @@ -2583,6 +2632,7 @@ mod tests { id: "reasoning-1".to_string(), summary: vec!["line one".to_string(), "line two".to_string()], content: vec![], + elapsed_ms: None, } ); @@ -2596,6 +2646,7 @@ mod tests { ThreadItem::WebSearch { id: "search-1".to_string(), query: "docs".to_string(), + elapsed_ms: None, } ); }