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:
jif
2026-06-12 13:50:04 -07:00
committed by GitHub
parent 3e2ee1da3f
commit 8f2d6416ce
44 changed files with 716 additions and 113 deletions
+41 -5
View File
@@ -405,12 +405,15 @@ mod job {
) -> codex_protocol::error::Result<String> {
let filtered = items
.iter()
.filter_map(|item| {
if let RolloutItem::ResponseItem(item) = item {
sanitize_response_item_for_memories(item)
} else {
None
.filter_map(|item| match item {
RolloutItem::ResponseItem(item) => sanitize_response_item_for_memories(item),
RolloutItem::InterAgentCommunication(communication) => {
Some(communication.to_model_input_item())
}
RolloutItem::SessionMeta(_)
| RolloutItem::Compacted(_)
| RolloutItem::TurnContext(_)
| RolloutItem::EventMsg(_) => None,
})
.collect::<Vec<_>>();
let serialized = serde_json::to_string(&filtered).map_err(|err| {
@@ -656,6 +659,8 @@ fn emit_metrics(context: &StageOneRequestContext, counts: &Stats) {
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::AgentPath;
use codex_protocol::protocol::InterAgentCommunication;
use pretty_assertions::assert_eq;
#[test]
@@ -745,6 +750,37 @@ mod tests {
assert!(serialized.contains("[REDACTED_SECRET]"));
}
#[test]
fn serializes_inter_agent_communications_for_memory() {
let plaintext = InterAgentCommunication::new(
AgentPath::root().join("worker").expect("worker path"),
AgentPath::root(),
Vec::new(),
"child done".to_string(),
/*trigger_turn*/ false,
);
let encrypted = InterAgentCommunication::new_encrypted(
AgentPath::root(),
AgentPath::root().join("worker").expect("worker path"),
Vec::new(),
"encrypted payload".to_string(),
/*trigger_turn*/ true,
);
let expected = vec![
plaintext.to_model_input_item(),
encrypted.to_model_input_item(),
];
let serialized = job::serialize_filtered_rollout_response_items(&[
RolloutItem::InterAgentCommunication(plaintext),
RolloutItem::InterAgentCommunication(encrypted),
])
.expect("serialize");
let parsed: Vec<ResponseItem> = serde_json::from_str(&serialized).expect("parse");
assert_eq!(parsed, expected);
}
#[test]
fn count_outcomes_sums_token_usage_across_all_jobs() {
let counts = aggregate_stats(vec![