feat: structured multi-agent output (#15515)

Send input now sends messages as assistant message and with this format:

```
author: /root/worker_a
recipient: /root/worker_a/tester
other_recipients: []
Content: bla bla bla. Actual content. Only text for now
```
This commit is contained in:
jif-oai
2026-03-23 18:53:54 +00:00
committed by GitHub
parent e838645fa2
commit 37ac0c093c
17 changed files with 994 additions and 66 deletions
+11 -4
View File
@@ -1,3 +1,4 @@
use crate::agent::inter_agent_instruction::InterAgentInstruction;
use crate::codex::TurnContext;
use crate::context_manager::normalize;
use crate::event_mapping::is_contextual_user_message_content;
@@ -204,9 +205,10 @@ impl ContextManager {
}
}
/// Drop the last `num_turns` user turns from this history.
/// Drop the last `num_turns` instruction turns from this history.
///
/// "User turns" are identified as `ResponseItem::Message` entries whose role is `"user"`.
/// Instruction turns are history messages that should behave like a new prompt boundary:
/// ordinary user messages and structured assistant inter-agent instructions.
///
/// This mirrors thread-rollback semantics:
/// - `num_turns == 0` is a no-op
@@ -248,7 +250,7 @@ impl ContextManager {
}
fn get_non_last_reasoning_items_tokens(&self) -> i64 {
// Get reasoning items excluding all the ones after the last user message.
// Get reasoning items excluding all the ones after the last instruction boundary.
let Some(last_user_index) = self.items.iter().rposition(is_user_turn_boundary) else {
return 0;
};
@@ -631,7 +633,12 @@ pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool {
return false;
};
role == "user" && !is_contextual_user_message_content(content)
(role == "user" && !is_contextual_user_message_content(content))
|| (role == "assistant" && is_inter_agent_instruction_content(content))
}
fn is_inter_agent_instruction_content(content: &[ContentItem]) -> bool {
InterAgentInstruction::is_message_content(content)
}
fn user_message_positions(items: &[ResponseItem]) -> Vec<usize> {
@@ -38,6 +38,18 @@ fn assistant_msg(text: &str) -> ResponseItem {
}
}
fn inter_agent_assistant_msg(text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: text.to_string(),
}],
end_turn: None,
phase: None,
}
}
fn create_history_with_items(items: Vec<ResponseItem>) -> ContextManager {
let mut h = ContextManager::new();
// Use a generous but fixed token budget; tests only rely on truncation
@@ -225,6 +237,35 @@ fn items_after_last_model_generated_tokens_are_zero_without_model_generated_item
);
}
#[test]
fn inter_agent_assistant_messages_are_turn_boundaries() {
let item = inter_agent_assistant_msg(
"author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue",
);
assert!(is_user_turn_boundary(&item));
}
#[test]
fn drop_last_n_user_turns_treats_inter_agent_assistant_messages_as_instruction_turns() {
let first_turn = user_input_text_msg("first");
let first_reply = assistant_msg("done");
let inter_agent_turn = inter_agent_assistant_msg(
"author: /root\nrecipient: /root/worker\nother_recipients: []\nContent: continue",
);
let inter_agent_reply = assistant_msg("worker reply");
let mut history = create_history_with_items(vec![
first_turn.clone(),
first_reply.clone(),
inter_agent_turn,
inter_agent_reply,
]);
history.drop_last_n_user_turns(1);
assert_eq!(history.raw_items(), &vec![first_turn, first_reply]);
}
#[test]
fn total_token_usage_includes_all_items_after_last_model_generated_item() {
let mut history = create_history_with_items(vec![assistant_msg("already counted by API")]);