use anyhow::Context;use anyhow::Result;use anyhow::bail;use codex_protocol::protocol::CollabAgentInteractionBeginEvent;use codex_protocol::protocol::CollabAgentInteractionEndEvent;use codex_protocol::protocol::CollabAgentSpawnEndEvent;use codex_protocol::protocol::CollabCloseBeginEvent;use codex_protocol::protocol::CollabCloseEndEvent;use codex_protocol::protocol::InterAgentCommunication;use codex_protocol::protocol::SubAgentActivityEvent;use codex_protocol::protocol::SubAgentActivityKind;use serde::Deserialize;use serde_json::Value;use super::super::TraceReducer;use crate::model::ConversationItem;use crate::model::ConversationItemKind;use crate::model::ConversationPart;use crate::model::ConversationRole;use crate::model::InteractionEdge;use crate::model::InteractionEdgeKind;use crate::model::ToolCallKind;use crate::model::TraceAnchor;use crate::payload::RawPayloadRef;/// Agent delivery edge waiting for the recipient-side conversation item.////// Multi-agent v2 records the sender tool before the target thread necessarily/// includes the delivered mailbox message in a model-visible request. The edge/// stays pending so it can target that exact conversation item when possible.pub(in crate::reducer) struct PendingAgentInteractionEdge { pub(in crate::reducer) edge_id: String, pub(in crate::reducer) kind: InteractionEdgeKind, pub(in crate::reducer) source: TraceAnchor, pub(in crate::reducer) target_thread_id: String, pub(in crate::reducer) message_author: String, pub(in crate::reducer) message_content: String, /// Spawn-only fallback for children that fail before their task message is model-visible. pub(in crate::reducer) unresolved_spawn_thread_id: Option<String>, pub(in crate::reducer) started_at_unix_ms: i64, pub(in crate::reducer) ended_at_unix_ms: Option<i64>, pub(in crate::reducer) carried_raw_payload_ids: Vec<String>,}/// Typed reducer input for a multi-agent v2 child completion notification.////// Child results are observed outside the normal tool lifecycle, but they still/// carry a parent-thread notification. This wrapper keeps the dispatcher from/// passing a positional bundle of thread and turn ids.pub(in crate::reducer) struct ObservedAgentResultEdge { pub(in crate::reducer) wall_time_unix_ms: i64, pub(in crate::reducer) edge_id: String, pub(in crate::reducer) child_thread_id: String, pub(in crate::reducer) child_codex_turn_id: String, pub(in crate::reducer) parent_thread_id: String, pub(in crate::reducer) message: String, pub(in crate::reducer) carried_payload: Option<RawPayloadRef>,}#[derive(Deserialize)]struct AgentMessageInvocationArgs { message: String,}/// Builds the stable edge id for the spawn relationship between two threads.pub(in crate::reducer) fn spawn_edge_id(parent_thread_id: &str, child_thread_id: &str) -> String { format!("edge:spawn:{parent_thread_id}:{child_thread_id}")}impl TraceReducer { /// Starts a multi-agent edge from a runtime begin payload, when the tool kind supports one. pub(super) fn start_agent_interaction_from_runtime( &mut self, tool_call_id: &str, runtime_payload: &RawPayloadRef, ) -> Result<()> { let kind = self .rollout .tool_calls .get(tool_call_id) .with_context(|| format!("agent edge referenced unknown tool call {tool_call_id}"))? .kind .clone(); match kind { ToolCallKind::AssignAgentTask => { let payload: CollabAgentInteractionBeginEvent = serde_json::from_value(self.read_payload_json(runtime_payload)?)?; self.queue_message_agent_interaction( tool_call_id, InteractionEdgeKind::AssignAgentTask, payload.receiver_thread_id.to_string(), payload.prompt, /*ended_at_unix_ms*/ None, ) } ToolCallKind::SendMessage => { let payload: CollabAgentInteractionBeginEvent = serde_json::from_value(self.read_payload_json(runtime_payload)?)?; self.queue_message_agent_interaction( tool_call_id, InteractionEdgeKind::SendMessage, payload.receiver_thread_id.to_string(), payload.prompt, /*ended_at_unix_ms*/ None, ) } ToolCallKind::CloseAgent => { let payload: CollabCloseBeginEvent = serde_json::from_value(self.read_payload_json(runtime_payload)?)?; self.upsert_close_agent_interaction( tool_call_id, payload.receiver_thread_id.to_string(), /*ended_at_unix_ms*/ None, ) } ToolCallKind::ExecCommand | ToolCallKind::WriteStdin | ToolCallKind::ApplyPatch | ToolCallKind::Mcp { .. } | ToolCallKind::Web | ToolCallKind::ImageGeneration | ToolCallKind::SpawnAgent | ToolCallKind::WaitAgent | ToolCallKind::Other { .. } => Ok(()), } } /// Ends or enriches a multi-agent edge from a runtime end payload. pub(super) fn end_agent_interaction_from_runtime( &mut self, wall_time_unix_ms: i64, tool_call_id: &str, runtime_payload: &RawPayloadRef, ) -> Result<()> { let kind = self.rollout.tool_calls[tool_call_id].kind.clone(); let runtime_payload_json = self.read_payload_json(runtime_payload)?; if runtime_payload_json.get("agent_thread_id").is_some() { let payload: SubAgentActivityEvent = serde_json::from_value(runtime_payload_json)?; return self.end_sub_agent_activity(wall_time_unix_ms, tool_call_id, &kind, &payload); } match kind { ToolCallKind::SpawnAgent => { let payload: CollabAgentSpawnEndEvent = serde_json::from_value(runtime_payload_json)?; self.end_spawn_agent_interaction(wall_time_unix_ms, tool_call_id, &payload) } ToolCallKind::AssignAgentTask => { let payload: CollabAgentInteractionEndEvent = serde_json::from_value(runtime_payload_json)?; self.end_message_agent_interaction( wall_time_unix_ms, tool_call_id, InteractionEdgeKind::AssignAgentTask, &payload, ) } ToolCallKind::SendMessage => { let payload: CollabAgentInteractionEndEvent = serde_json::from_value(runtime_payload_json)?; self.end_message_agent_interaction( wall_time_unix_ms, tool_call_id, InteractionEdgeKind::SendMessage, &payload, ) } ToolCallKind::CloseAgent => { let payload: CollabCloseEndEvent = serde_json::from_value(runtime_payload_json)?; self.upsert_close_agent_interaction( tool_call_id, payload.receiver_thread_id.to_string(), Some(wall_time_unix_ms), ) } ToolCallKind::ExecCommand | ToolCallKind::WriteStdin | ToolCallKind::ApplyPatch | ToolCallKind::Mcp { .. } | ToolCallKind::Web | ToolCallKind::ImageGeneration | ToolCallKind::WaitAgent | ToolCallKind::Other { .. } => Ok(()), } } fn end_sub_agent_activity( &mut self, wall_time_unix_ms: i64, tool_call_id: &str, tool_kind: &ToolCallKind, payload: &SubAgentActivityEvent, ) -> Result<()> { let target_thread_id = payload.agent_thread_id.to_string(); match (tool_kind, &payload.kind) { (ToolCallKind::SpawnAgent, SubAgentActivityKind::Started) => { let parent_thread_id = self .rollout .tool_calls .get(tool_call_id) .with_context(|| { format!("agent activity referenced unknown tool call {tool_call_id}") })? .thread_id .clone(); self.queue_sub_agent_activity_message_edge( wall_time_unix_ms, tool_call_id, spawn_edge_id(&parent_thread_id, &target_thread_id), InteractionEdgeKind::SpawnAgent, target_thread_id.clone(), Some(target_thread_id), ) } (ToolCallKind::AssignAgentTask, SubAgentActivityKind::Interacted) => self .queue_sub_agent_activity_message_edge( wall_time_unix_ms, tool_call_id, tool_edge_id(tool_call_id), InteractionEdgeKind::AssignAgentTask, target_thread_id, /*unresolved_spawn_thread_id*/ None, ), (ToolCallKind::SendMessage, SubAgentActivityKind::Interacted) => self .queue_sub_agent_activity_message_edge( wall_time_unix_ms, tool_call_id, tool_edge_id(tool_call_id), InteractionEdgeKind::SendMessage, target_thread_id, /*unresolved_spawn_thread_id*/ None, ), (ToolCallKind::CloseAgent, SubAgentActivityKind::Interrupted) => self .upsert_close_agent_interaction( tool_call_id, target_thread_id, Some(wall_time_unix_ms), ), _ => bail!( "sub-agent activity {:?} does not match tool call kind {tool_kind:?}", payload.kind ), } } fn queue_sub_agent_activity_message_edge( &mut self, wall_time_unix_ms: i64, tool_call_id: &str, edge_id: String, edge_kind: InteractionEdgeKind, target_thread_id: String, unresolved_spawn_thread_id: Option<String>, ) -> Result<()> { let tool_call = self.rollout.tool_calls.get(tool_call_id).with_context(|| { format!("agent activity referenced unknown tool call {tool_call_id}") })?; let started_at_unix_ms = tool_call.execution.started_at_unix_ms; let message_author = self.agent_path_for_thread(&tool_call.thread_id)?; let message_content = self.agent_message_content_from_invocation(tool_call_id)?; let carried_raw_payload_ids = self.agent_tool_payload_ids(tool_call_id)?; self.queue_or_resolve_agent_interaction_edge(PendingAgentInteractionEdge { edge_id, kind: edge_kind, source: TraceAnchor::ToolCall { tool_call_id: tool_call_id.to_string(), }, target_thread_id, message_author, message_content, unresolved_spawn_thread_id, started_at_unix_ms, ended_at_unix_ms: Some(wall_time_unix_ms), carried_raw_payload_ids, }) } fn agent_message_content_from_invocation(&self, tool_call_id: &str) -> Result<String> { let tool_call = self.rollout.tool_calls.get(tool_call_id).with_context(|| { format!("agent activity referenced unknown tool call {tool_call_id}") })?; let invocation_payload_id = tool_call .raw_invocation_payload_id .as_deref() .with_context(|| { format!("agent activity tool call {tool_call_id} missing invocation payload") })?; let invocation_payload = self .rollout .raw_payloads .get(invocation_payload_id) .with_context(|| { format!( "agent activity tool call {tool_call_id} referenced missing invocation payload {invocation_payload_id}" ) })?; let invocation = self.read_payload_json(invocation_payload)?; let arguments = invocation .get("payload") .and_then(|payload| payload.get("arguments")) .and_then(Value::as_str) .with_context(|| { format!("agent activity tool call {tool_call_id} missing function arguments") })?; let args: AgentMessageInvocationArgs = serde_json::from_str(arguments) .with_context(|| format!("parse agent activity tool call {tool_call_id} arguments"))?; Ok(args.message) } /// Adds the canonical tool result payload to an already reduced multi-agent edge. pub(super) fn attach_agent_interaction_tool_result( &mut self, tool_call_id: &str, result_payload: Option<&RawPayloadRef>, ) -> Result<()> { let Some(result_payload) = result_payload else { return Ok(()); }; if let Some(edge) = self .rollout .interaction_edges .values_mut() .find(|edge| tool_call_source_matches(&edge.source, tool_call_id)) { push_unique( &mut edge.carried_raw_payload_ids, &result_payload.raw_payload_id, ); return Ok(()); } // Agent delivery edges intentionally wait for the recipient-side // conversation item. Tool end can arrive before that item is // reduced, so preserve the response payload on the pending edge rather // than dropping evidence until the delivery materializes. if let Some(pending) = self .pending_agent_interaction_edges .iter_mut() .find(|pending| tool_call_source_matches(&pending.source, tool_call_id)) { push_unique( &mut pending.carried_raw_payload_ids, &result_payload.raw_payload_id, ); } Ok(()) } fn end_spawn_agent_interaction( &mut self, wall_time_unix_ms: i64, tool_call_id: &str, payload: &CollabAgentSpawnEndEvent, ) -> Result<()> { let Some(child_thread_id) = payload.new_thread_id else { return Ok(()); }; let tool_call = &self.rollout.tool_calls[tool_call_id]; let child_thread_id = child_thread_id.to_string(); let edge_id = spawn_edge_id(&payload.sender_thread_id.to_string(), &child_thread_id); let message_author = self.agent_path_for_thread(&tool_call.thread_id)?; self.queue_or_resolve_agent_interaction_edge(PendingAgentInteractionEdge { edge_id, kind: InteractionEdgeKind::SpawnAgent, source: TraceAnchor::ToolCall { tool_call_id: tool_call_id.to_string(), }, target_thread_id: child_thread_id.clone(), message_author, message_content: payload.prompt.clone(), unresolved_spawn_thread_id: Some(child_thread_id), started_at_unix_ms: tool_call.execution.started_at_unix_ms, ended_at_unix_ms: Some(wall_time_unix_ms), carried_raw_payload_ids: self.agent_tool_payload_ids(tool_call_id)?, }) } fn end_message_agent_interaction( &mut self, wall_time_unix_ms: i64, tool_call_id: &str, edge_kind: InteractionEdgeKind, payload: &CollabAgentInteractionEndEvent, ) -> Result<()> { self.queue_message_agent_interaction( tool_call_id, edge_kind, payload.receiver_thread_id.to_string(), payload.prompt.clone(), Some(wall_time_unix_ms), ) } fn queue_message_agent_interaction( &mut self, tool_call_id: &str, kind: InteractionEdgeKind, target_thread_id: String, message_content: String, ended_at_unix_ms: Option<i64>, ) -> Result<()> { let tool_call = &self.rollout.tool_calls[tool_call_id]; let message_author = self.agent_path_for_thread(&tool_call.thread_id)?; self.queue_or_resolve_agent_interaction_edge(PendingAgentInteractionEdge { edge_id: tool_edge_id(tool_call_id), kind, source: TraceAnchor::ToolCall { tool_call_id: tool_call_id.to_string(), }, target_thread_id, message_author, message_content, unresolved_spawn_thread_id: None, started_at_unix_ms: tool_call.execution.started_at_unix_ms, ended_at_unix_ms, carried_raw_payload_ids: self.agent_tool_payload_ids(tool_call_id)?, }) } fn agent_tool_payload_ids(&self, tool_call_id: &str) -> Result<Vec<String>> { let tool_call = self.rollout.tool_calls.get(tool_call_id).with_context(|| { format!("agent edge referenced unknown tool call {tool_call_id}") })?; let mut payload_ids = Vec::new(); if let Some(payload_id) = &tool_call.raw_invocation_payload_id { push_unique(&mut payload_ids, payload_id); } for payload_id in &tool_call.raw_runtime_payload_ids { push_unique(&mut payload_ids, payload_id); } if let Some(payload_id) = &tool_call.raw_result_payload_id { push_unique(&mut payload_ids, payload_id); } Ok(payload_ids) } fn upsert_close_agent_interaction( &mut self, tool_call_id: &str, target_thread_id: String, ended_at_unix_ms: Option<i64>, ) -> Result<()> { if !self.rollout.threads.contains_key(&target_thread_id) { // A failed close can name a thread that never participated in this // trace. Keep that evidence on the ToolCall raw payloads rather // than creating an anchor to a non-existent reduced object. return Ok(()); } let started_at_unix_ms = self .rollout .tool_calls .get(tool_call_id) .with_context(|| format!("close edge referenced unknown tool call {tool_call_id}"))? .execution .started_at_unix_ms; let carried_raw_payload_ids = self.agent_tool_payload_ids(tool_call_id)?; self.upsert_interaction_edge(InteractionEdge { edge_id: tool_edge_id(tool_call_id), kind: InteractionEdgeKind::CloseAgent, source: TraceAnchor::ToolCall { tool_call_id: tool_call_id.to_string(), }, target: TraceAnchor::Thread { thread_id: target_thread_id, }, started_at_unix_ms, ended_at_unix_ms, carried_item_ids: Vec::new(), carried_raw_payload_ids, }) } /// Queues or resolves the edge from a child completion to its parent notification. pub(in crate::reducer) fn queue_agent_result_interaction_edge( &mut self, observed: ObservedAgentResultEdge, ) -> Result<()> { let message_author = self.agent_path_for_thread(&observed.child_thread_id)?; let source = if let Some(source_item_id) = self.latest_assistant_message_item_for_turn( &observed.child_thread_id, &observed.child_codex_turn_id, ) { TraceAnchor::ConversationItem { item_id: source_item_id, } } else { // Child completion is delivered from AgentStatus, not from transcript // content. Failed or cancelled children can therefore notify the parent // without producing a final assistant message. Anchor those edges to // the child thread so the trace keeps the valid delivery instead of // inventing a missing conversation item. TraceAnchor::Thread { thread_id: observed.child_thread_id, } }; self.queue_or_resolve_agent_interaction_edge(PendingAgentInteractionEdge { edge_id: observed.edge_id, kind: InteractionEdgeKind::AgentResult, source, target_thread_id: observed.parent_thread_id, message_author, message_content: observed.message, unresolved_spawn_thread_id: None, started_at_unix_ms: observed.wall_time_unix_ms, ended_at_unix_ms: Some(observed.wall_time_unix_ms), carried_raw_payload_ids: observed .carried_payload .map(|payload| vec![payload.raw_payload_id]) .unwrap_or_default(), }) } /// Resolves pending agent edges whose target is the newly reduced conversation item. pub(in crate::reducer) fn resolve_pending_agent_edges_for_item( &mut self, item_id: &str, ) -> Result<()> { if self.is_interaction_edge_target_item(item_id) { return Ok(()); } let Some((thread_id, message_author, message_content)) = self.inter_agent_message_item(item_id) else { return Ok(()); }; let Some(pending_index) = self .pending_agent_interaction_edges .iter() .position(|pending| { pending.target_thread_id == thread_id && pending.message_author == message_author && pending.message_content == message_content }) else { return Ok(()); }; let pending = self.pending_agent_interaction_edges.remove(pending_index); self.upsert_agent_interaction_edge_for_item(pending, item_id.to_string()) } fn queue_or_resolve_agent_interaction_edge( &mut self, pending: PendingAgentInteractionEdge, ) -> Result<()> { if let Some(item_id) = self.find_unlinked_inter_agent_message_item( &pending.target_thread_id, &pending.message_author, &pending.message_content, ) { return self.upsert_agent_interaction_edge_for_item(pending, item_id); } if let Some(existing) = self .pending_agent_interaction_edges .iter_mut() .find(|existing| existing.edge_id == pending.edge_id) { if existing.kind != pending.kind || existing.source != pending.source || existing.target_thread_id != pending.target_thread_id || existing.message_author != pending.message_author || existing.message_content != pending.message_content || existing.unresolved_spawn_thread_id != pending.unresolved_spawn_thread_id { bail!( "pending interaction edge {} was observed with conflicting delivery data", pending.edge_id ); } existing.started_at_unix_ms = existing.started_at_unix_ms.min(pending.started_at_unix_ms); existing.ended_at_unix_ms = match (existing.ended_at_unix_ms, pending.ended_at_unix_ms) { (Some(existing_ended), Some(pending_ended)) => { Some(existing_ended.max(pending_ended)) } (None, ended) | (ended, None) => ended, }; extend_unique( &mut existing.carried_raw_payload_ids, pending.carried_raw_payload_ids, ); return Ok(()); } self.pending_agent_interaction_edges.push(pending); Ok(()) } /// Materializes unresolved spawn edges that have a valid child-thread fallback target. pub(in crate::reducer) fn resolve_pending_spawn_edge_fallbacks(&mut self) -> Result<()> { let pending_edges = std::mem::take(&mut self.pending_agent_interaction_edges); for pending in pending_edges { let Some(child_thread_id) = pending.unresolved_spawn_thread_id else { continue; }; if pending.kind != InteractionEdgeKind::SpawnAgent { bail!( "non-spawn interaction edge {} carried a spawn fallback target", pending.edge_id ); } if !self.rollout.threads.contains_key(&child_thread_id) { continue; } // Spawn normally resolves to the child task message because that is // where the delegated work first becomes model-visible. A child can // fail before that transcript item exists, but the spawned thread is // still real and the spawning tool still created it. Preserve that // relationship with the thread fallback instead of dropping the edge. self.upsert_interaction_edge(InteractionEdge { edge_id: pending.edge_id, kind: pending.kind, source: pending.source, target: TraceAnchor::Thread { thread_id: child_thread_id, }, started_at_unix_ms: pending.started_at_unix_ms, ended_at_unix_ms: pending.ended_at_unix_ms, carried_item_ids: Vec::new(), carried_raw_payload_ids: pending.carried_raw_payload_ids, })?; } Ok(()) } fn upsert_agent_interaction_edge_for_item( &mut self, pending: PendingAgentInteractionEdge, target_item_id: String, ) -> Result<()> { self.upsert_interaction_edge(InteractionEdge { edge_id: pending.edge_id, kind: pending.kind, source: pending.source, target: TraceAnchor::ConversationItem { item_id: target_item_id.clone(), }, started_at_unix_ms: pending.started_at_unix_ms, ended_at_unix_ms: pending.ended_at_unix_ms, carried_item_ids: vec![target_item_id], carried_raw_payload_ids: pending.carried_raw_payload_ids, }) } fn upsert_interaction_edge(&mut self, edge: InteractionEdge) -> Result<()> { if let Some(existing) = self.rollout.interaction_edges.get_mut(&edge.edge_id) { if existing.kind != edge.kind || existing.source != edge.source || existing.target != edge.target { bail!( "interaction edge {} was observed with conflicting endpoints", edge.edge_id ); } existing.started_at_unix_ms = existing.started_at_unix_ms.min(edge.started_at_unix_ms); existing.ended_at_unix_ms = match (existing.ended_at_unix_ms, edge.ended_at_unix_ms) { (Some(existing_ended), Some(edge_ended)) => Some(existing_ended.max(edge_ended)), (None, ended) | (ended, None) => ended, }; extend_unique(&mut existing.carried_item_ids, edge.carried_item_ids); extend_unique( &mut existing.carried_raw_payload_ids, edge.carried_raw_payload_ids, ); return Ok(()); } self.rollout .interaction_edges .insert(edge.edge_id.clone(), edge); Ok(()) } fn find_unlinked_inter_agent_message_item( &self, thread_id: &str, message_author: &str, message_content: &str, ) -> Option<String> { self.rollout .threads .get(thread_id)? .conversation_item_ids .iter() .find(|item_id| { !self.is_interaction_edge_target_item(item_id) && self .inter_agent_message_item(item_id) .is_some_and(|(_, author, content)| { author == message_author && content == message_content }) }) .cloned() } fn inter_agent_message_item(&self, item_id: &str) -> Option<(String, String, String)> { let item = self.rollout.conversation_items.get(item_id)?; let (author_agent_path, recipient_agent_path, message_content) = inter_agent_message_fields(item)?; let thread = self.rollout.threads.get(&item.thread_id)?; if recipient_agent_path != thread.agent_path { return None; } Some((item.thread_id.clone(), author_agent_path, message_content)) } fn agent_path_for_thread(&self, thread_id: &str) -> Result<String> { self.rollout .threads .get(thread_id) .map(|thread| thread.agent_path.clone()) .with_context(|| format!("agent edge referenced unknown thread {thread_id}")) } fn is_interaction_edge_target_item(&self, item_id: &str) -> bool { self.rollout .interaction_edges .values() .any(|edge| matches!(&edge.target, TraceAnchor::ConversationItem { item_id: target } if target == item_id)) } fn latest_assistant_message_item_for_turn( &self, thread_id: &str, codex_turn_id: &str, ) -> Option<String> { self.rollout .conversation_items .values() .filter(|item| { item.thread_id == thread_id && item.codex_turn_id.as_deref() == Some(codex_turn_id) && item.role == ConversationRole::Assistant && item.kind == ConversationItemKind::Message && item.agent_message.is_none() }) .max_by_key(|item| item.first_seen_at_unix_ms) .map(|item| item.item_id.clone()) }}fn extend_unique(items: &mut Vec<String>, new_items: Vec<String>) { for item in new_items { if !items.iter().any(|existing| existing == &item) { items.push(item); } }}fn tool_edge_id(tool_call_id: &str) -> String { format!("edge:tool:{tool_call_id}")}fn tool_call_source_matches(anchor: &TraceAnchor, tool_call_id: &str) -> bool { matches!(anchor, TraceAnchor::ToolCall { tool_call_id: source } if source == tool_call_id)}fn push_unique(items: &mut Vec<String>, item: &str) { if !items.iter().any(|existing| existing == item) { items.push(item.to_string()); }}fn inter_agent_message_fields(item: &ConversationItem) -> Option<(String, String, String)> { if item.role != ConversationRole::Assistant || item.kind != ConversationItemKind::Message { return None; } if let Some(agent_message) = &item.agent_message { let message_content = match item.body.parts.as_slice() { [ConversationPart::Text { text }] => text, [ConversationPart::Encoded { label, value }] if label == "encrypted_content" => value, [ ConversationPart::Text { .. }, ConversationPart::Encoded { label, value }, ] if label == "encrypted_content" => value, _ => return None, }; return Some(( agent_message.author.clone(), agent_message.recipient.clone(), message_content.clone(), )); } // Older traces store multi-agent v2 deliveries as assistant messages whose // text is serialized `InterAgentCommunication`. Treat only that exact // transport shape as an edge target; ordinary assistant JSON must not be // mistaken for cross-thread delivery. let [ConversationPart::Text { text }] = item.body.parts.as_slice() else { return None; }; let communication = serde_json::from_str::<InterAgentCommunication>(text).ok()?; Some(( communication.author.to_string(), communication.recipient.to_string(), communication .encrypted_content .unwrap_or(communication.content), ))}#[cfg(test)]#[path = "agents_tests.rs"]mod tests;