From bad4c12b9da59f3d73e9c73e6602004412e0bbc6 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 15 Jan 2026 09:03:26 +0000 Subject: [PATCH] feat: collab tools app-server event mapping (#9213) --- .../app-server-protocol/src/protocol/v2.rs | 90 +++++++++ codex-rs/app-server/README.md | 1 + .../app-server/src/bespoke_event_handling.rs | 175 ++++++++++++++++++ 3 files changed, 266 insertions(+) diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index d8a0e8a6e..e7adf6586 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -16,6 +16,7 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; +use codex_protocol::protocol::AgentStatus as CoreAgentStatus; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; @@ -1681,6 +1682,25 @@ pub enum ThreadItem { }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] + CollabAgentToolCall { + /// Unique identifier for this collab tool call. + id: String, + /// Name of the collab tool that was invoked. + tool: CollabAgentTool, + /// Current status of the collab tool call. + status: CollabAgentToolCallStatus, + /// Thread ID of the agent issuing the collab request. + sender_thread_id: String, + /// Thread ID of the receiving agent, when applicable. In case of spawn operation, + /// this correspond to the newly spawned agent. + receiver_thread_id: Option, + /// Prompt text sent as part of the collab tool call, when available. + prompt: Option, + /// Last known status of the target agent, when available. + agent_state: Option, + }, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] WebSearch { id: String, query: String }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -1733,6 +1753,16 @@ pub enum CommandExecutionStatus { Declined, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentTool { + SpawnAgent, + SendInput, + Wait, + CloseAgent, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1771,6 +1801,66 @@ pub enum McpToolCallStatus { Failed, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentToolCallStatus { + InProgress, + Completed, + Failed, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum CollabAgentStatus { + PendingInit, + Running, + Completed, + Errored, + Shutdown, + NotFound, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct CollabAgentState { + pub status: CollabAgentStatus, + pub message: Option, +} + +impl From for CollabAgentState { + fn from(value: CoreAgentStatus) -> Self { + match value { + CoreAgentStatus::PendingInit => Self { + status: CollabAgentStatus::PendingInit, + message: None, + }, + CoreAgentStatus::Running => Self { + status: CollabAgentStatus::Running, + message: None, + }, + CoreAgentStatus::Completed(message) => Self { + status: CollabAgentStatus::Completed, + message, + }, + CoreAgentStatus::Errored(message) => Self { + status: CollabAgentStatus::Errored, + message: Some(message), + }, + CoreAgentStatus::Shutdown => Self { + status: CollabAgentStatus::Shutdown, + message: None, + }, + CoreAgentStatus::NotFound => Self { + status: CollabAgentStatus::NotFound, + message: None, + }, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 26c2dee9c..63f4dea96 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -375,6 +375,7 @@ Today both notifications carry an empty `items` array even when item events were - `commandExecution` — `{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}` for sandboxed commands; `status` is `inProgress`, `completed`, `failed`, or `declined`. - `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`. - `mcpToolCall` — `{id, server, tool, status, arguments, result?, error?}` describing MCP calls; `status` is `inProgress`, `completed`, or `failed`. +- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`. - `webSearch` — `{id, query}` for a web search request issued by the agent. - `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool. - `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 0870191ec..f14dd2ddc 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -14,6 +14,9 @@ use codex_app_server_protocol::AgentMessageDeltaNotification; use codex_app_server_protocol::ApplyPatchApprovalParams; use codex_app_server_protocol::ApplyPatchApprovalResponse; use codex_app_server_protocol::CodexErrorInfo as V2CodexErrorInfo; +use codex_app_server_protocol::CollabAgentState as V2CollabAgentStatus; +use codex_app_server_protocol::CollabAgentTool; +use codex_app_server_protocol::CollabAgentToolCallStatus as V2CollabToolCallStatus; use codex_app_server_protocol::CommandAction as V2ParsedCommand; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionOutputDeltaNotification; @@ -278,6 +281,178 @@ pub(crate) async fn apply_bespoke_event_handling( .send_server_notification(ServerNotification::ItemCompleted(notification)) .await; } + EventMsg::CollabAgentSpawnBegin(begin_event) => { + let item = ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::SpawnAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_id: None, + prompt: Some(begin_event.prompt), + agent_state: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabAgentSpawnEnd(end_event) => { + let status = if end_event.new_thread_id.is_some() { + V2CollabToolCallStatus::Completed + } else { + V2CollabToolCallStatus::Failed + }; + let item = ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::SpawnAgent, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_id: end_event.new_thread_id.map(|id| id.to_string()), + prompt: Some(end_event.prompt), + agent_state: Some(V2CollabAgentStatus::from(end_event.status)), + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } + EventMsg::CollabAgentInteractionBegin(begin_event) => { + let item = ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::SendInput, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_id: Some(begin_event.receiver_thread_id.to_string()), + prompt: Some(begin_event.prompt), + agent_state: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabAgentInteractionEnd(end_event) => { + let status = match end_event.status { + codex_protocol::protocol::AgentStatus::Errored(_) + | codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed, + _ => V2CollabToolCallStatus::Completed, + }; + let item = ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::SendInput, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_id: Some(end_event.receiver_thread_id.to_string()), + prompt: Some(end_event.prompt), + agent_state: Some(V2CollabAgentStatus::from(end_event.status)), + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } + EventMsg::CollabWaitingBegin(begin_event) => { + let item = ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::Wait, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_id: Some(begin_event.receiver_thread_id.to_string()), + prompt: None, + agent_state: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabWaitingEnd(end_event) => { + let status = match end_event.status { + codex_protocol::protocol::AgentStatus::Errored(_) + | codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed, + _ => V2CollabToolCallStatus::Completed, + }; + let item = ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::Wait, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_id: Some(end_event.receiver_thread_id.to_string()), + prompt: None, + agent_state: Some(V2CollabAgentStatus::from(end_event.status)), + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } + EventMsg::CollabCloseBegin(begin_event) => { + let item = ThreadItem::CollabAgentToolCall { + id: begin_event.call_id, + tool: CollabAgentTool::CloseAgent, + status: V2CollabToolCallStatus::InProgress, + sender_thread_id: begin_event.sender_thread_id.to_string(), + receiver_thread_id: Some(begin_event.receiver_thread_id.to_string()), + prompt: None, + agent_state: None, + }; + let notification = ItemStartedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemStarted(notification)) + .await; + } + EventMsg::CollabCloseEnd(end_event) => { + let status = match end_event.status { + codex_protocol::protocol::AgentStatus::Errored(_) + | codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed, + _ => V2CollabToolCallStatus::Completed, + }; + let item = ThreadItem::CollabAgentToolCall { + id: end_event.call_id, + tool: CollabAgentTool::CloseAgent, + status, + sender_thread_id: end_event.sender_thread_id.to_string(), + receiver_thread_id: Some(end_event.receiver_thread_id.to_string()), + prompt: None, + agent_state: Some(V2CollabAgentStatus::from(end_event.status)), + }; + let notification = ItemCompletedNotification { + thread_id: conversation_id.to_string(), + turn_id: event_turn_id.clone(), + item, + }; + outgoing + .send_server_notification(ServerNotification::ItemCompleted(notification)) + .await; + } EventMsg::AgentMessageContentDelta(event) => { let notification = AgentMessageDeltaNotification { thread_id: conversation_id.to_string(),