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`
This commit is contained in:
jif
2026-06-17 13:48:56 +01:00
committed by GitHub
Unverified
parent a5229e0686
commit 45f603302c
14 changed files with 80 additions and 3 deletions
@@ -2940,6 +2940,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -15424,6 +15424,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -11877,6 +11877,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -1035,6 +1035,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -1106,6 +1106,12 @@
},
"ResponseItemMetadata": {
"properties": {
"source_call_id": {
"type": [
"string",
"null"
]
},
"turn_id": {
"type": [
"string",
@@ -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, };
+1
View File
@@ -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",
@@ -1054,6 +1054,7 @@ fn record_items_truncates_function_call_output_content() {
},
metadata: Some(ResponseItemMetadata {
turn_id: Some("turn-1".to_string()),
..Default::default()
}),
};
+1
View File
@@ -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![
@@ -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
@@ -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
@@ -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,
@@ -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",
+4
View File
@@ -909,6 +909,9 @@ pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub turn_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub source_call_id: Option<String>,
}
#[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()
}
}