Skip to content
8 changes: 4 additions & 4 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1771,12 +1771,12 @@ pub enum ThreadItem {
/// 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<String>,
/// this corresponds to the newly spawned agent.
receiver_thread_ids: Vec<String>,
/// Prompt text sent as part of the collab tool call, when available.
prompt: Option<String>,
/// Last known status of the target agent, when available.
agent_state: Option<CollabAgentState>,
/// Last known status of the target agents, when available.
agents_states: HashMap<String, CollabAgentState>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
Expand Down
92 changes: 66 additions & 26 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ pub(crate) async fn apply_bespoke_event_handling(
tool: CollabAgentTool::SpawnAgent,
status: V2CollabToolCallStatus::InProgress,
sender_thread_id: begin_event.sender_thread_id.to_string(),
receiver_thread_id: None,
receiver_thread_ids: Vec::new(),
prompt: Some(begin_event.prompt),
agent_state: None,
agents_states: HashMap::new(),
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -301,19 +301,32 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::CollabAgentSpawnEnd(end_event) => {
let status = if end_event.new_thread_id.is_some() {
V2CollabToolCallStatus::Completed
} else {
V2CollabToolCallStatus::Failed
let has_receiver = end_event.new_thread_id.is_some();
let status = match &end_event.status {
codex_protocol::protocol::AgentStatus::Errored(_)
| codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed,
_ if has_receiver => V2CollabToolCallStatus::Completed,
_ => V2CollabToolCallStatus::Failed,
};
let (receiver_thread_ids, agents_states) = match end_event.new_thread_id {
Some(id) => {
let receiver_id = id.to_string();
let received_status = V2CollabAgentStatus::from(end_event.status.clone());
(
vec![receiver_id.clone()],
[(receiver_id, received_status)].into_iter().collect(),
)
}
None => (Vec::new(), HashMap::new()),
};
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()),
receiver_thread_ids,
prompt: Some(end_event.prompt),
agent_state: Some(V2CollabAgentStatus::from(end_event.status)),
agents_states,
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -325,14 +338,15 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::CollabAgentInteractionBegin(begin_event) => {
let receiver_thread_ids = vec![begin_event.receiver_thread_id.to_string()];
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()),
receiver_thread_ids,
prompt: Some(begin_event.prompt),
agent_state: None,
agents_states: HashMap::new(),
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -344,19 +358,21 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::CollabAgentInteractionEnd(end_event) => {
let status = match end_event.status {
let status = match &end_event.status {
codex_protocol::protocol::AgentStatus::Errored(_)
| codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed,
_ => V2CollabToolCallStatus::Completed,
};
let receiver_id = end_event.receiver_thread_id.to_string();
let received_status = V2CollabAgentStatus::from(end_event.status);
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()),
receiver_thread_ids: vec![receiver_id.clone()],
prompt: Some(end_event.prompt),
agent_state: Some(V2CollabAgentStatus::from(end_event.status)),
agents_states: [(receiver_id, received_status)].into_iter().collect(),
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -368,14 +384,19 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::CollabWaitingBegin(begin_event) => {
let receiver_thread_ids = begin_event
.receiver_thread_ids
.iter()
.map(ToString::to_string)
.collect();
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()),
receiver_thread_ids,
prompt: None,
agent_state: None,
agents_states: HashMap::new(),
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -387,19 +408,31 @@ pub(crate) async fn apply_bespoke_event_handling(
.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 status = if end_event.statuses.values().any(|status| {
matches!(
status,
codex_protocol::protocol::AgentStatus::Errored(_)
| codex_protocol::protocol::AgentStatus::NotFound
)
}) {
V2CollabToolCallStatus::Failed
} else {
V2CollabToolCallStatus::Completed
};
let receiver_thread_ids = end_event.statuses.keys().map(ToString::to_string).collect();
let agents_states = end_event
Comment on lines +422 to +423
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve waited-on IDs in wait completion

Here receiver_thread_ids is derived only from end_event.statuses keys, but the wait handler sends an empty statuses map when no agent reaches a final state before timeout. In that case the completed item will show an empty receiver list even though the call targeted specific agents, and clients treating item/completed as authoritative lose the ability to display which agents were waited on. Consider carrying the original receiver_thread_ids through the end event (or merging with the started item) so timeouts and partial completions still include the full target list.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope

.statuses
.iter()
.map(|(id, status)| (id.to_string(), V2CollabAgentStatus::from(status.clone())))
.collect();
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()),
receiver_thread_ids,
prompt: None,
agent_state: Some(V2CollabAgentStatus::from(end_event.status)),
agents_states,
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -416,9 +449,9 @@ pub(crate) async fn apply_bespoke_event_handling(
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()),
receiver_thread_ids: vec![begin_event.receiver_thread_id.to_string()],
prompt: None,
agent_state: None,
agents_states: HashMap::new(),
};
let notification = ItemStartedNotification {
thread_id: conversation_id.to_string(),
Expand All @@ -430,19 +463,26 @@ pub(crate) async fn apply_bespoke_event_handling(
.await;
}
EventMsg::CollabCloseEnd(end_event) => {
let status = match end_event.status {
let status = match &end_event.status {
codex_protocol::protocol::AgentStatus::Errored(_)
| codex_protocol::protocol::AgentStatus::NotFound => V2CollabToolCallStatus::Failed,
_ => V2CollabToolCallStatus::Completed,
};
let receiver_id = end_event.receiver_thread_id.to_string();
let agents_states = [(
receiver_id.clone(),
V2CollabAgentStatus::from(end_event.status),
)]
.into_iter()
.collect();
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()),
receiver_thread_ids: vec![receiver_id],
prompt: None,
agent_state: Some(V2CollabAgentStatus::from(end_event.status)),
agents_states,
};
let notification = ItemCompletedNotification {
thread_id: conversation_id.to_string(),
Expand Down
Loading
Loading