From 45f603302c45269737db97443612bb4876365798 Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 17 Jun 2026 13:48:56 +0100 Subject: [PATCH] Add join key for MAv2 inter-agent messages (#28561) ## Summary This keeps inter-agent communication on the existing raw response item path and adds a join key for MAv2 tool calls. MAv2 `spawn_agent`, `send_message`, and `followup_task` now stamp the originating tool call id into `ResponseItemMetadata.source_call_id` on the raw `ResponseItem::AgentMessage`. App-server clients can join that raw item back to the existing tool/activity event by call id, while using the raw agent message's existing sender, receiver, and content fields. No new app-server `ThreadItem` or notification type is added. ## Tests - `just fmt` - `just write-app-server-schema` - `just test -p codex-protocol` - `just test -p codex-app-server-protocol` - `just test -p codex-core multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_path` - `just test -p codex-core multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn` - `just fix -p codex-protocol` - `just fix -p codex-app-server-protocol` - `just fix -p codex-core` --- .../schema/json/ClientRequest.json | 6 +++++ .../codex_app_server_protocol.schemas.json | 6 +++++ .../codex_app_server_protocol.v2.schemas.json | 6 +++++ .../RawResponseItemCompletedNotification.json | 6 +++++ .../schema/json/v2/ThreadResumeParams.json | 6 +++++ .../schema/typescript/ResponseItemMetadata.ts | 2 +- codex-rs/core/src/compact_tests.rs | 1 + .../core/src/context_manager/history_tests.rs | 1 + codex-rs/core/src/session/tests.rs | 1 + .../src/tools/handlers/multi_agents_tests.rs | 27 +++++++++++++++++++ .../handlers/multi_agents_v2/message_tool.rs | 7 ++++- .../tools/handlers/multi_agents_v2/spawn.rs | 7 ++++- .../tests/suite/subagent_notifications.rs | 3 +++ codex-rs/protocol/src/models.rs | 4 +++ 14 files changed, 80 insertions(+), 3 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 3358ea9d6..cb1d594ff 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2940,6 +2940,12 @@ }, "ResponseItemMetadata": { "properties": { + "source_call_id": { + "type": [ + "string", + "null" + ] + }, "turn_id": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 1e11c76ae..bd894898d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -15424,6 +15424,12 @@ }, "ResponseItemMetadata": { "properties": { + "source_call_id": { + "type": [ + "string", + "null" + ] + }, "turn_id": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 326bfdfc9..fdc3a342c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -11877,6 +11877,12 @@ }, "ResponseItemMetadata": { "properties": { + "source_call_id": { + "type": [ + "string", + "null" + ] + }, "turn_id": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index fdd40dae6..3efca0322 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -1035,6 +1035,12 @@ }, "ResponseItemMetadata": { "properties": { + "source_call_id": { + "type": [ + "string", + "null" + ] + }, "turn_id": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 7ba2a0a64..093c27044 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1106,6 +1106,12 @@ }, "ResponseItemMetadata": { "properties": { + "source_call_id": { + "type": [ + "string", + "null" + ] + }, "turn_id": { "type": [ "string", diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts index f7b69a6df..365c561fd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMetadata.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ResponseItemMetadata = { turn_id?: string, }; +export type ResponseItemMetadata = { turn_id?: string, source_call_id?: string, }; diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index a52e70a7b..c1004004a 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -238,6 +238,7 @@ fn build_compacted_history_preserves_user_message_metadata() { message: "first user message".to_string(), metadata: Some(ResponseItemMetadata { turn_id: Some("turn-1".to_string()), + ..Default::default() }), }], "summary text", diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 908cb286f..bf5bfc3b7 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -1054,6 +1054,7 @@ fn record_items_truncates_function_call_output_content() { }, metadata: Some(ResponseItemMetadata { turn_id: Some("turn-1".to_string()), + ..Default::default() }), }; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 473adaa83..a26fb6599 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1606,6 +1606,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() { phase: None, metadata: Some(ResponseItemMetadata { turn_id: Some("compact-turn".to_string()), + ..Default::default() }), }; let replacement_history = vec![ diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 4608862ca..557164f4d 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1195,6 +1195,11 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat && communication.other_recipients.is_empty() && communication.content.is_empty() && communication.encrypted_content.as_deref() == Some("encrypted-spawn-message") + && communication + .metadata + .as_ref() + .and_then(|metadata| metadata.source_call_id.as_deref()) + == Some("call-1") && communication.trigger_turn ) })); @@ -1222,6 +1227,11 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat && communication.other_recipients.is_empty() && communication.content.is_empty() && communication.encrypted_content.as_deref() == Some("encrypted-send-message") + && communication + .metadata + .as_ref() + .and_then(|metadata| metadata.source_call_id.as_deref()) + == Some("call-1") && !communication.trigger_turn ) })); @@ -2027,6 +2037,23 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn() .await .expect("followup_task should succeed"); + assert!(manager.captured_ops().iter().any(|(id, op)| { + *id == agent_id + && matches!( + op, + Op::InterAgentCommunication { communication } + if communication.author == AgentPath::root() + && communication.recipient == worker_path + && communication.encrypted_content.as_deref() == Some("continue") + && communication + .metadata + .as_ref() + .and_then(|metadata| metadata.source_call_id.as_deref()) + == Some("call-1") + && communication.trigger_turn + ) + })); + let second_turn = thread.codex.session.new_default_turn().await; thread .codex diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 7fdb80e8b..81eebed4a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -6,6 +6,7 @@ use super::*; use crate::tools::context::FunctionToolOutput; use crate::turn_timing::now_unix_timestamp_ms; +use codex_protocol::models::ResponseItemMetadata; use codex_protocol::protocol::InterAgentCommunication; #[derive(Clone, Copy, PartialEq, Eq)] @@ -99,8 +100,12 @@ pub(crate) async fn handle_message_string_tool( .session_source .get_agent_path() .unwrap_or_else(AgentPath::root); - let communication = + let mut communication = communication_from_tool_message(author, receiver_agent_path.clone(), message); + communication + .metadata + .get_or_insert_with(ResponseItemMetadata::default) + .source_call_id = Some(call_id.clone()); let result = session .services .agent_control diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 2186a4a6d..51813c5c1 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -8,6 +8,7 @@ use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2; use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::AgentPath; +use codex_protocol::models::ResponseItemMetadata; use codex_protocol::protocol::Op; use codex_tools::ToolSpec; @@ -117,8 +118,12 @@ async fn handle_spawn_agent( .session_source .get_agent_path() .unwrap_or_else(AgentPath::root); - let communication = + let mut communication = communication_from_tool_message(author, new_agent_path.clone(), message); + communication + .metadata + .get_or_insert_with(ResponseItemMetadata::default) + .source_call_id = Some(call_id.clone()); Op::InterAgentCommunication { communication } } initial_operation => initial_operation, diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 764380be7..2501aa0dd 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -1088,6 +1088,9 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result "type": "agent_message", "author": "/root", "recipient": "/root/worker", + "metadata": { + "source_call_id": SPAWN_CALL_ID, + }, "content": [ { "type": "input_text", diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 0e7e9bb44..9443866c6 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -909,6 +909,9 @@ pub struct ResponseItemMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub turn_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub source_call_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] @@ -2072,6 +2075,7 @@ mod tests { fn response_item_metadata(turn_id: &str) -> ResponseItemMetadata { ResponseItemMetadata { turn_id: Some(turn_id.to_string()), + ..Default::default() } }