mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Support plaintext agent messages (#27830)
## Why Multi-agent v2 `send_message` deliveries already reach the receiving model as typed `agent_message` items with encrypted content. Child-completion notifications are generated by Codex itself, so their content is plaintext and previously fell back to a serialized JSON envelope inside an assistant message. With plaintext `input_text` supported for `agent_message`, both delivery paths can use the same model-visible type while preserving explicit author and recipient metadata. ## What changed - add plaintext `input_text` support to `AgentMessageInputContent` and regenerate the affected app-server schemas - preserve `InterAgentCommunication` as structured mailbox input instead of converting it to assistant text - record delivered communications as typed `agent_message` history items - persist a dedicated rollout item so local delivery metadata such as `trigger_turn` remains available without leaking into the Responses request - reconstruct typed agent messages on resume and preserve fork-turn truncation behavior - remove request-time assistant-content parsing - preserve plaintext and encrypted inter-agent deliveries in stage-one memory inputs - normalize and link plaintext and encrypted agent messages in rollout traces without treating inbound messages as child results - cover the real MultiAgent V2 child-completion path end to end with deterministic mailbox synchronization ## Verification - `just test -p codex-core plaintext_multi_agent_v2_completion_sends_agent_message` - `just test -p codex-core input_queue_drains_mailbox_in_delivery_order record_initial_history_reconstructs_typed_inter_agent_message fork_turn_positions_use_inter_agent_delivery_metadata` - `just test -p codex-memories-write serializes_inter_agent_communications_for_memory` - `just test -p codex-rollout-trace agent_messages_preserve_routing_and_content sub_agent_started_activity_creates_spawn_edge` - `just test -p codex-rollout-trace agent_result_edge_falls_back_to_child_thread_without_result_message` - `just test -p codex-protocol -p codex-rollout -p codex-app-server-protocol`
This commit is contained in:
@@ -3,6 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::payload::RawPayloadId;
|
||||
|
||||
use super::AgentPath;
|
||||
use super::AgentThreadId;
|
||||
use super::CodeCellId;
|
||||
use super::CodexTurnId;
|
||||
@@ -32,6 +33,9 @@ pub struct ConversationItem {
|
||||
/// Codex channel for assistant/tool content, when the item is channel-specific.
|
||||
pub channel: Option<ConversationChannel>,
|
||||
pub kind: ConversationItemKind,
|
||||
/// Routing metadata carried by a Responses `agent_message` item.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_message: Option<AgentMessageMetadata>,
|
||||
pub body: ConversationBody,
|
||||
/// Protocol/model `call_id` for function/custom tool call and output items.
|
||||
pub call_id: Option<ModelVisibleCallId>,
|
||||
@@ -39,6 +43,15 @@ pub struct ConversationItem {
|
||||
pub produced_by: Vec<ProducerRef>,
|
||||
}
|
||||
|
||||
/// Sender and destination identities attached to a model-visible agent message.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AgentMessageMetadata {
|
||||
/// Agent path that authored the message.
|
||||
pub author: AgentPath,
|
||||
/// Agent path that received the message.
|
||||
pub recipient: AgentPath,
|
||||
}
|
||||
|
||||
/// Model-visible role assigned to a conversation item.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
@@ -322,6 +322,7 @@ impl TraceReducer {
|
||||
role: ConversationRole::Assistant,
|
||||
channel: None,
|
||||
kind: ConversationItemKind::CompactionMarker,
|
||||
agent_message: None,
|
||||
// The summary is a separate model/provider-visible item. Keep the marker body
|
||||
// empty so transcript renderers cannot mistake the boundary for prompt content.
|
||||
body: ConversationBody { parts: Vec::new() },
|
||||
@@ -419,6 +420,7 @@ impl TraceReducer {
|
||||
role: item.role,
|
||||
channel: item.channel,
|
||||
kind: item.kind,
|
||||
agent_message: item.agent_message,
|
||||
body: item.body,
|
||||
call_id: item.call_id,
|
||||
produced_by,
|
||||
@@ -579,6 +581,7 @@ fn conversation_item_matches(
|
||||
item.role == normalized.role
|
||||
&& item.channel == normalized.channel
|
||||
&& item.kind == normalized.kind
|
||||
&& item.agent_message == normalized.agent_message
|
||||
&& body_matches
|
||||
&& item.call_id == normalized.call_id
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
//! Normalization from Responses-shaped JSON items into conversation item data.
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_protocol::models::AgentMessageInputContent;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::model::AgentMessageMetadata;
|
||||
use crate::model::ConversationBody;
|
||||
use crate::model::ConversationChannel;
|
||||
use crate::model::ConversationItemKind;
|
||||
@@ -22,6 +26,7 @@ pub(super) struct NormalizedConversationItem {
|
||||
pub(super) role: ConversationRole,
|
||||
pub(super) channel: Option<ConversationChannel>,
|
||||
pub(super) kind: ConversationItemKind,
|
||||
pub(super) agent_message: Option<AgentMessageMetadata>,
|
||||
pub(super) body: ConversationBody,
|
||||
pub(super) call_id: Option<String>,
|
||||
}
|
||||
@@ -58,11 +63,13 @@ fn normalize_model_item(
|
||||
};
|
||||
match item_type {
|
||||
"message" => normalize_message_item(item, raw_payload),
|
||||
"agent_message" => normalize_agent_message_item(item, raw_payload),
|
||||
"reasoning" => normalize_reasoning_item(item, raw_payload),
|
||||
"function_call" => Ok(NormalizedConversationItem {
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::FunctionCall,
|
||||
agent_message: None,
|
||||
body: raw_text_or_json_body(item.get("arguments"), raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -73,6 +80,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Tool,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::FunctionCallOutput,
|
||||
agent_message: None,
|
||||
body: tool_output_body(item.get("output"), raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -83,6 +91,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::CustomToolCall,
|
||||
agent_message: None,
|
||||
body: custom_tool_call_body(item, raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -93,6 +102,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Tool,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::CustomToolCallOutput,
|
||||
agent_message: None,
|
||||
body: tool_output_body(item.get("output"), raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -104,6 +114,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::FunctionCall,
|
||||
agent_message: None,
|
||||
body: json_body(item, raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -115,6 +126,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Tool,
|
||||
channel: Some(ConversationChannel::Commentary),
|
||||
kind: ConversationItemKind::FunctionCallOutput,
|
||||
agent_message: None,
|
||||
body: json_body(item, raw_payload),
|
||||
call_id: item
|
||||
.get("call_id")
|
||||
@@ -126,6 +138,7 @@ fn normalize_model_item(
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Summary),
|
||||
kind: ConversationItemKind::Message,
|
||||
agent_message: None,
|
||||
body: compaction_body(item, raw_payload)?,
|
||||
call_id: None,
|
||||
})
|
||||
@@ -160,6 +173,7 @@ fn normalize_message_item(
|
||||
.and_then(Value::as_str)
|
||||
.and_then(channel_from_phase),
|
||||
kind: ConversationItemKind::Message,
|
||||
agent_message: None,
|
||||
body: ConversationBody {
|
||||
parts: content_parts(item.get("content"), raw_payload),
|
||||
},
|
||||
@@ -167,6 +181,49 @@ fn normalize_message_item(
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_agent_message_item(
|
||||
item: &Value,
|
||||
raw_payload: &RawPayloadRef,
|
||||
) -> Result<NormalizedConversationItem> {
|
||||
let raw_payload_id = &raw_payload.raw_payload_id;
|
||||
let response_item =
|
||||
serde_json::from_value::<ResponseItem>(item.clone()).with_context(|| {
|
||||
format!("failed to parse agent_message item in payload {raw_payload_id}")
|
||||
})?;
|
||||
let ResponseItem::AgentMessage {
|
||||
author,
|
||||
recipient,
|
||||
content,
|
||||
} = response_item
|
||||
else {
|
||||
bail!("item in payload {raw_payload_id} was not an agent_message");
|
||||
};
|
||||
let parts = content
|
||||
.into_iter()
|
||||
.map(|content| match content {
|
||||
AgentMessageInputContent::InputText { text } => ConversationPart::Text { text },
|
||||
AgentMessageInputContent::EncryptedContent { encrypted_content } => {
|
||||
ConversationPart::Encoded {
|
||||
label: "encrypted_content".to_string(),
|
||||
value: encrypted_content,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if parts.is_empty() {
|
||||
bail!("agent_message item in payload {raw_payload_id} contained no content");
|
||||
}
|
||||
|
||||
Ok(NormalizedConversationItem {
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Analysis),
|
||||
kind: ConversationItemKind::Message,
|
||||
agent_message: Some(AgentMessageMetadata { author, recipient }),
|
||||
body: ConversationBody { parts },
|
||||
call_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_reasoning_item(
|
||||
item: &Value,
|
||||
raw_payload: &RawPayloadRef,
|
||||
@@ -217,6 +274,7 @@ fn normalize_reasoning_item(
|
||||
role: ConversationRole::Assistant,
|
||||
channel: Some(ConversationChannel::Analysis),
|
||||
kind: ConversationItemKind::Reasoning,
|
||||
agent_message: None,
|
||||
body: ConversationBody { parts },
|
||||
call_id: None,
|
||||
})
|
||||
|
||||
@@ -2,9 +2,12 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::model::AgentMessageMetadata;
|
||||
use crate::model::ConversationBody;
|
||||
use crate::model::ConversationChannel;
|
||||
use crate::model::ConversationItemKind;
|
||||
use crate::model::ConversationPart;
|
||||
use crate::model::ConversationRole;
|
||||
use crate::model::ExecutionStatus;
|
||||
use crate::model::ProducerRef;
|
||||
use crate::model::ToolCallKind;
|
||||
@@ -107,6 +110,90 @@ fn response_outputs_enter_thread_conversation_on_completion() -> anyhow::Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_messages_preserve_routing_and_content() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let writer = create_started_writer(&temp)?;
|
||||
start_turn(&writer, "turn-1")?;
|
||||
|
||||
let request = writer.write_json_payload(
|
||||
RawPayloadKind::InferenceRequest,
|
||||
&json!({
|
||||
"input": [
|
||||
{
|
||||
"type": "agent_message",
|
||||
"author": "/root/worker",
|
||||
"recipient": "/root",
|
||||
"content": [{"type": "input_text", "text": "done"}]
|
||||
},
|
||||
{
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/worker",
|
||||
"content": [{
|
||||
"type": "encrypted_content",
|
||||
"encrypted_content": "encrypted-task"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}),
|
||||
)?;
|
||||
append_inference_start(&writer, "inference-1", "turn-1", request)?;
|
||||
|
||||
let rollout = replay_bundle(temp.path())?;
|
||||
let actual = rollout.inference_calls["inference-1"]
|
||||
.request_item_ids
|
||||
.iter()
|
||||
.map(|item_id| {
|
||||
let item = &rollout.conversation_items[item_id];
|
||||
(
|
||||
item.role.clone(),
|
||||
item.channel.clone(),
|
||||
item.kind.clone(),
|
||||
item.agent_message.clone(),
|
||||
item.body.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
(
|
||||
ConversationRole::Assistant,
|
||||
Some(ConversationChannel::Analysis),
|
||||
ConversationItemKind::Message,
|
||||
Some(AgentMessageMetadata {
|
||||
author: "/root/worker".to_string(),
|
||||
recipient: "/root".to_string(),
|
||||
}),
|
||||
ConversationBody {
|
||||
parts: vec![ConversationPart::Text {
|
||||
text: "done".to_string(),
|
||||
}],
|
||||
},
|
||||
),
|
||||
(
|
||||
ConversationRole::Assistant,
|
||||
Some(ConversationChannel::Analysis),
|
||||
ConversationItemKind::Message,
|
||||
Some(AgentMessageMetadata {
|
||||
author: "/root".to_string(),
|
||||
recipient: "/root/worker".to_string(),
|
||||
}),
|
||||
ConversationBody {
|
||||
parts: vec![ConversationPart::Encoded {
|
||||
label: "encrypted_content".to_string(),
|
||||
value: "encrypted-task".to_string(),
|
||||
}],
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_full_request_reuses_prior_json_tool_call_by_position() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(in crate::reducer) struct PendingAgentInteractionEdge {
|
||||
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>,
|
||||
@@ -254,6 +255,7 @@ impl TraceReducer {
|
||||
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 {
|
||||
@@ -263,6 +265,7 @@ impl TraceReducer {
|
||||
tool_call_id: tool_call_id.to_string(),
|
||||
},
|
||||
target_thread_id,
|
||||
message_author,
|
||||
message_content,
|
||||
unresolved_spawn_thread_id,
|
||||
started_at_unix_ms,
|
||||
@@ -354,6 +357,7 @@ impl TraceReducer {
|
||||
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,
|
||||
@@ -362,6 +366,7 @@ impl TraceReducer {
|
||||
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,
|
||||
@@ -395,6 +400,7 @@ impl TraceReducer {
|
||||
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,
|
||||
@@ -402,6 +408,7 @@ impl TraceReducer {
|
||||
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,
|
||||
@@ -469,6 +476,7 @@ impl TraceReducer {
|
||||
&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,
|
||||
@@ -492,6 +500,7 @@ impl TraceReducer {
|
||||
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,
|
||||
@@ -508,14 +517,21 @@ impl TraceReducer {
|
||||
&mut self,
|
||||
item_id: &str,
|
||||
) -> Result<()> {
|
||||
let Some((thread_id, message_content)) = self.inter_agent_message_item(item_id) else {
|
||||
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_content == message_content
|
||||
pending.target_thread_id == thread_id
|
||||
&& pending.message_author == message_author
|
||||
&& pending.message_content == message_content
|
||||
})
|
||||
else {
|
||||
return Ok(());
|
||||
@@ -530,6 +546,7 @@ impl TraceReducer {
|
||||
) -> 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);
|
||||
@@ -543,6 +560,7 @@ impl TraceReducer {
|
||||
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
|
||||
{
|
||||
@@ -661,6 +679,7 @@ impl TraceReducer {
|
||||
fn find_unlinked_inter_agent_message_item(
|
||||
&self,
|
||||
thread_id: &str,
|
||||
message_author: &str,
|
||||
message_content: &str,
|
||||
) -> Option<String> {
|
||||
self.rollout
|
||||
@@ -672,19 +691,30 @@ impl TraceReducer {
|
||||
!self.is_interaction_edge_target_item(item_id)
|
||||
&& self
|
||||
.inter_agent_message_item(item_id)
|
||||
.is_some_and(|(_, content)| content == message_content)
|
||||
.is_some_and(|(_, author, content)| {
|
||||
author == message_author && content == message_content
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn inter_agent_message_item(&self, item_id: &str) -> Option<(String, String)> {
|
||||
fn inter_agent_message_item(&self, item_id: &str) -> Option<(String, String, String)> {
|
||||
let item = self.rollout.conversation_items.get(item_id)?;
|
||||
let (recipient_agent_path, message_content) = inter_agent_message_fields(item)?;
|
||||
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(), message_content))
|
||||
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 {
|
||||
@@ -707,6 +737,7 @@ impl TraceReducer {
|
||||
&& 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())
|
||||
@@ -735,19 +766,36 @@ fn push_unique(items: &mut Vec<String>, item: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn inter_agent_message_fields(item: &ConversationItem) -> Option<(String, String)> {
|
||||
// Multi-agent v2 injects mailbox 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.
|
||||
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 [content] = item.body.parts.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let message_content = match content {
|
||||
ConversationPart::Text { text } => 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
|
||||
|
||||
@@ -246,18 +246,17 @@ fn sub_agent_started_activity_creates_spawn_edge() -> anyhow::Result<()> {
|
||||
)?;
|
||||
start_thread(&writer, child_thread_id, "/root/reviewer")?;
|
||||
start_turn_for_thread(&writer, child_thread_id, "turn-child-1")?;
|
||||
let delivered = inter_agent_message(
|
||||
"/root",
|
||||
"/root/reviewer",
|
||||
"review this",
|
||||
/*trigger_turn*/ true,
|
||||
);
|
||||
append_inference_request(
|
||||
&writer,
|
||||
child_thread_id,
|
||||
"turn-child-1",
|
||||
"inference-child-1",
|
||||
vec![message("assistant", &delivered)],
|
||||
vec![json!({
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/reviewer",
|
||||
"content": [{"type": "input_text", "text": "review this"}]
|
||||
})],
|
||||
)?;
|
||||
|
||||
let replayed = replay_bundle(temp.path())?;
|
||||
@@ -773,10 +772,9 @@ fn agent_result_edge_falls_back_to_child_thread_without_result_message() -> anyh
|
||||
let temp = TempDir::new()?;
|
||||
let writer = create_started_agent_writer(&temp)?;
|
||||
|
||||
// The child thread and turn exist, but there is intentionally no completed
|
||||
// assistant message for this turn. Failed child tasks can still notify the
|
||||
// parent through AgentStatus, so the result edge must not require a final
|
||||
// transcript item from the child.
|
||||
// The child received its task but produced no assistant output. Failed
|
||||
// child tasks can still notify the parent through AgentStatus, so the
|
||||
// inbound task must not be mistaken for the child's result.
|
||||
start_thread(
|
||||
&writer,
|
||||
"019d0000-0000-7000-8000-000000000002",
|
||||
@@ -787,6 +785,18 @@ fn agent_result_edge_falls_back_to_child_thread_without_result_message() -> anyh
|
||||
"019d0000-0000-7000-8000-000000000002",
|
||||
"turn-child-1",
|
||||
)?;
|
||||
append_inference_request(
|
||||
&writer,
|
||||
"019d0000-0000-7000-8000-000000000002",
|
||||
"turn-child-1",
|
||||
"inference-child-1",
|
||||
vec![json!({
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/child",
|
||||
"content": [{"type": "input_text", "text": "do the task"}]
|
||||
})],
|
||||
)?;
|
||||
|
||||
let notification = r#"<subagent_notification>{"agent_path":"/root/child","status":{"failed":"boom"}}</subagent_notification>"#;
|
||||
let carried_payload = writer.write_json_payload(
|
||||
|
||||
Reference in New Issue
Block a user