Skip to content

Commit e357fc7

Browse files
authored
[app-server] add item started/completed events for turn items (#6517)
This one should be quite straightforward, as it's just a translation of TurnItem events we already emit to ThreadItem that app-server exposes to customers. To test, cp my change to owen/app_server_test_client and do the following: ``` cargo build -p codex-cli RUST_LOG=codex_app_server=info CODEX_BIN=target/debug/codex cargo run -p codex-app-server-test-client -- send-message-v2 "hello" ``` example event before (still kept there for backward compatibility): ``` { < "method": "codex/event/item_completed", < "params": { < "conversationId": "019a74cc-fad9-7ab3-83a3-f42827b7b074", < "id": "0", < "msg": { < "item": { < "Reasoning": { < "id": "rs_03d183492e07e20a016913a936eb8c81a1a7671a103fee8afc", < "raw_content": [], < "summary_text": [ < "Hey! What would you like to work on? I can explore the repo, run specific tests, or implement a change. Let's keep it short and straightforward. There's no need for a lengthy introduction or elaborate planning, just a friendly greeting and an open offer to help. I want to make sure the user feels welcomed and understood right from the start. It's all about keeping the tone friendly and concise!" < ] < } < }, < "thread_id": "019a74cc-fad9-7ab3-83a3-f42827b7b074", < "turn_id": "0", < "type": "item_completed" < } < } < } ``` after (v2): ``` < { < "method": "item/completed", < "params": { < "item": { < "id": "rs_03d183492e07e20a016913a936eb8c81a1a7671a103fee8afc", < "text": "Hey! What would you like to work on? I can explore the repo, run specific tests, or implement a change. Let's keep it short and straightforward. There's no need for a lengthy introduction or elaborate planning, just a friendly greeting and an open offer to help. I want to make sure the user feels welcomed and understood right from the start. It's all about keeping the tone friendly and concise!", < "type": "reasoning" < } < } < } ```
1 parent 807e2c2 commit e357fc7

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use codex_protocol::ConversationId;
66
use codex_protocol::account::PlanType;
77
use codex_protocol::config_types::ReasoningEffort;
88
use codex_protocol::config_types::ReasoningSummary;
9+
use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent;
10+
use codex_protocol::items::TurnItem as CoreTurnItem;
911
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
1012
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
1113
use codex_protocol::user_input::UserInput as CoreUserInput;
@@ -457,6 +459,17 @@ impl UserInput {
457459
}
458460
}
459461

462+
impl From<CoreUserInput> for UserInput {
463+
fn from(value: CoreUserInput) -> Self {
464+
match value {
465+
CoreUserInput::Text { text } => UserInput::Text { text },
466+
CoreUserInput::Image { image_url } => UserInput::Image { url: image_url },
467+
CoreUserInput::LocalImage { path } => UserInput::LocalImage { path },
468+
_ => unreachable!("unsupported user input variant"),
469+
}
470+
}
471+
}
472+
460473
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
461474
#[serde(tag = "type", rename_all = "camelCase")]
462475
#[ts(tag = "type")]
@@ -514,6 +527,42 @@ pub enum ThreadItem {
514527
},
515528
}
516529

530+
impl From<CoreTurnItem> for ThreadItem {
531+
fn from(value: CoreTurnItem) -> Self {
532+
match value {
533+
CoreTurnItem::UserMessage(user) => ThreadItem::UserMessage {
534+
id: user.id,
535+
content: user.content.into_iter().map(UserInput::from).collect(),
536+
},
537+
CoreTurnItem::AgentMessage(agent) => {
538+
let text = agent
539+
.content
540+
.into_iter()
541+
.map(|entry| match entry {
542+
CoreAgentMessageContent::Text { text } => text,
543+
})
544+
.collect::<String>();
545+
ThreadItem::AgentMessage { id: agent.id, text }
546+
}
547+
CoreTurnItem::Reasoning(reasoning) => {
548+
let text = if !reasoning.summary_text.is_empty() {
549+
reasoning.summary_text.join("\n")
550+
} else {
551+
reasoning.raw_content.join("\n")
552+
};
553+
ThreadItem::Reasoning {
554+
id: reasoning.id,
555+
text,
556+
}
557+
}
558+
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
559+
id: search.id,
560+
query: search.query,
561+
},
562+
}
563+
}
564+
}
565+
517566
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
518567
#[serde(rename_all = "camelCase")]
519568
#[ts(export_to = "v2/")]
@@ -708,3 +757,100 @@ pub struct AccountLoginCompletedNotification {
708757
pub success: bool,
709758
pub error: Option<String>,
710759
}
760+
761+
#[cfg(test)]
762+
mod tests {
763+
use super::*;
764+
use codex_protocol::items::AgentMessageContent;
765+
use codex_protocol::items::AgentMessageItem;
766+
use codex_protocol::items::ReasoningItem;
767+
use codex_protocol::items::TurnItem;
768+
use codex_protocol::items::UserMessageItem;
769+
use codex_protocol::items::WebSearchItem;
770+
use codex_protocol::user_input::UserInput as CoreUserInput;
771+
use pretty_assertions::assert_eq;
772+
use std::path::PathBuf;
773+
774+
#[test]
775+
fn core_turn_item_into_thread_item_converts_supported_variants() {
776+
let user_item = TurnItem::UserMessage(UserMessageItem {
777+
id: "user-1".to_string(),
778+
content: vec![
779+
CoreUserInput::Text {
780+
text: "hello".to_string(),
781+
},
782+
CoreUserInput::Image {
783+
image_url: "https://example.com/image.png".to_string(),
784+
},
785+
CoreUserInput::LocalImage {
786+
path: PathBuf::from("local/image.png"),
787+
},
788+
],
789+
});
790+
791+
assert_eq!(
792+
ThreadItem::from(user_item),
793+
ThreadItem::UserMessage {
794+
id: "user-1".to_string(),
795+
content: vec![
796+
UserInput::Text {
797+
text: "hello".to_string(),
798+
},
799+
UserInput::Image {
800+
url: "https://example.com/image.png".to_string(),
801+
},
802+
UserInput::LocalImage {
803+
path: PathBuf::from("local/image.png"),
804+
},
805+
],
806+
}
807+
);
808+
809+
let agent_item = TurnItem::AgentMessage(AgentMessageItem {
810+
id: "agent-1".to_string(),
811+
content: vec![
812+
AgentMessageContent::Text {
813+
text: "Hello ".to_string(),
814+
},
815+
AgentMessageContent::Text {
816+
text: "world".to_string(),
817+
},
818+
],
819+
});
820+
821+
assert_eq!(
822+
ThreadItem::from(agent_item),
823+
ThreadItem::AgentMessage {
824+
id: "agent-1".to_string(),
825+
text: "Hello world".to_string(),
826+
}
827+
);
828+
829+
let reasoning_item = TurnItem::Reasoning(ReasoningItem {
830+
id: "reasoning-1".to_string(),
831+
summary_text: vec!["line one".to_string(), "line two".to_string()],
832+
raw_content: vec![],
833+
});
834+
835+
assert_eq!(
836+
ThreadItem::from(reasoning_item),
837+
ThreadItem::Reasoning {
838+
id: "reasoning-1".to_string(),
839+
text: "line one\nline two".to_string(),
840+
}
841+
);
842+
843+
let search_item = TurnItem::WebSearch(WebSearchItem {
844+
id: "search-1".to_string(),
845+
query: "docs".to_string(),
846+
});
847+
848+
assert_eq!(
849+
ThreadItem::from(search_item),
850+
ThreadItem::WebSearch {
851+
id: "search-1".to_string(),
852+
query: "docs".to_string(),
853+
}
854+
);
855+
}
856+
}

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ use codex_app_server_protocol::GitDiffToRemoteResponse;
4646
use codex_app_server_protocol::InputItem as WireInputItem;
4747
use codex_app_server_protocol::InterruptConversationParams;
4848
use codex_app_server_protocol::InterruptConversationResponse;
49+
use codex_app_server_protocol::ItemCompletedNotification;
50+
use codex_app_server_protocol::ItemStartedNotification;
4951
use codex_app_server_protocol::JSONRPCErrorError;
5052
use codex_app_server_protocol::ListConversationsParams;
5153
use codex_app_server_protocol::ListConversationsResponse;
@@ -2609,6 +2611,20 @@ async fn apply_bespoke_event_handling(
26092611
.await;
26102612
}
26112613
}
2614+
EventMsg::ItemStarted(item_started_event) => {
2615+
let item: ThreadItem = item_started_event.item.clone().into();
2616+
let notification = ItemStartedNotification { item };
2617+
outgoing
2618+
.send_server_notification(ServerNotification::ItemStarted(notification))
2619+
.await;
2620+
}
2621+
EventMsg::ItemCompleted(item_completed_event) => {
2622+
let item: ThreadItem = item_completed_event.item.clone().into();
2623+
let notification = ItemCompletedNotification { item };
2624+
outgoing
2625+
.send_server_notification(ServerNotification::ItemCompleted(notification))
2626+
.await;
2627+
}
26122628
// If this is a TurnAborted, reply to any pending interrupt requests.
26132629
EventMsg::TurnAborted(turn_aborted_event) => {
26142630
let pending = {

0 commit comments

Comments
 (0)