mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: render typed envelopes for multi-agent v2 messages (#28368)
## Why
Multi-agent v2 messages need a consistent, model-visible envelope that
identifies what kind of interaction occurred, who sent it, and which
agent it targets. Previously, encrypted deliveries exposed only
`encrypted_content`, while child completion used the legacy
`<subagent_notification>` shape. That meant the client could not
consistently present `NEW_TASK`, `MESSAGE`, and `FINAL_ANSWER` using the
same format.
This change adds the routing envelope as plaintext while keeping task
and message payloads encrypted. No new Responses API field is required:
an encrypted delivery is represented as an `input_text` header
immediately followed by its existing `encrypted_content` item.
Every envelope now follows this shape:
```text
Message Type: <NEW_TASK | MESSAGE | FINAL_ANSWER>
Task name: <recipient agent path>
Sender: <author agent path>
Payload:
<message payload>
```
## Message types
### `NEW_TASK`
`NEW_TASK` is used when the recipient should begin a new turn, including
an initial `spawn_agent` task and a later `followup_task`.
For a root agent spawning `/root/worker`, the request contains a
plaintext envelope followed by the encrypted task:
```json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/worker",
"content": [
{
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "<encrypted task payload>"
}
]
}
```
Conceptually, the model receives:
```text
Message Type: NEW_TASK
Task name: /root/worker
Sender: /root
Payload:
Review the authentication changes and report any regressions.
```
### `MESSAGE`
`MESSAGE` is used for a queued `send_message` delivery. It communicates
with an existing agent without starting a new turn.
For `/root/worker` reporting progress to the root agent, the request
contains:
```json
{
"type": "agent_message",
"author": "/root/worker",
"recipient": "/root",
"content": [
{
"type": "input_text",
"text": "Message Type: MESSAGE\nTask name: /root\nSender: /root/worker\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "<encrypted message payload>"
}
]
}
```
Conceptually, the model receives:
```text
Message Type: MESSAGE
Task name: /root
Sender: /root/worker
Payload:
The protocol tests pass; I am checking the resume path now.
```
### `FINAL_ANSWER`
`FINAL_ANSWER` is emitted when a child agent reaches a terminal state
and reports its result to its parent. Completion payloads are already
available locally, so the complete envelope is represented as plaintext
rather than as a plaintext header plus encrypted content.
For `/root/worker` completing work for the root agent, the request
contains:
```json
{
"type": "agent_message",
"author": "/root/worker",
"recipient": "/root",
"content": [
{
"type": "input_text",
"text": "Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/worker\nPayload:\nNo regressions found."
}
]
}
```
The model-visible form is:
```text
Message Type: FINAL_ANSWER
Task name: /root
Sender: /root/worker
Payload:
No regressions found.
```
Errored, shut down, and missing agents also use `FINAL_ANSWER`, with a
terminal-status description in the payload.
## What changed
- Render `NEW_TASK` or `MESSAGE` in
`InterAgentCommunication::to_model_input_item`, based on whether the
encrypted delivery starts a turn.
- Replace the multi-agent v2 `<subagent_notification>` completion
payload with a model-visible `FINAL_ANSWER` envelope.
- Document `Task name`, `Sender`, and `Payload` consistently in the
multi-agent developer instructions.
- Prevent local-only history projections from treating an encrypted
message's plaintext header as the complete assistant message.
- Preserve rollout-trace interaction edges when an agent message
contains both plaintext and encrypted content.
Legacy multi-agent behavior remains unchanged.
## Verification
- `just test -p codex-protocol`
- `just test -p codex-rollout-trace`
- `just test -p codex-web-search-extension`
- `just test -p codex-core
encrypted_multi_agent_v2_spawn_sends_agent_message_to_child`
- `just test -p codex-core
plaintext_multi_agent_v2_completion_sends_agent_message`
- `just test -p codex-core
multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn`
- `just test -p codex-core
multi_agent_v2_completion_queues_message_for_direct_parent`
This commit is contained in:
@@ -8,6 +8,7 @@ use crate::codex_thread::ThreadConfigSnapshot;
|
||||
use crate::config::Config;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use crate::session::emit_subagent_session_started;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use crate::session_prefix::format_subagent_context_line;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::thread_manager::ResumeThreadWithHistoryOptions;
|
||||
@@ -434,7 +435,6 @@ impl AgentControl {
|
||||
return;
|
||||
};
|
||||
let child_thread = state.get_thread(child_thread_id).await.ok();
|
||||
let message = format_subagent_notification_message(child_reference.as_str(), &status);
|
||||
let child_uses_multi_agent_v2 = match child_thread.as_ref() {
|
||||
Some(child_thread) => {
|
||||
child_thread.multi_agent_version() == Some(MultiAgentVersion::V2)
|
||||
@@ -452,6 +452,13 @@ impl AgentControl {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(message) = format_inter_agent_completion_message(
|
||||
parent_agent_path.clone(),
|
||||
child_agent_path.clone(),
|
||||
&status,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
let communication = InterAgentCommunication::new(
|
||||
child_agent_path,
|
||||
parent_agent_path,
|
||||
@@ -464,6 +471,7 @@ impl AgentControl {
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
let message = format_subagent_notification_message(child_reference.as_str(), &status);
|
||||
let Ok(parent_thread) = state.get_thread(parent_thread_id).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2000,10 +2000,12 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let expected_message = crate::session_prefix::format_subagent_notification_message(
|
||||
tester_path.as_str(),
|
||||
let expected_message = crate::session_prefix::format_inter_agent_completion_message(
|
||||
worker_path.clone(),
|
||||
tester_path.clone(),
|
||||
&AgentStatus::Completed(Some("done".to_string())),
|
||||
);
|
||||
)
|
||||
.expect("completed status should render");
|
||||
let expected = (
|
||||
worker_thread_id,
|
||||
Op::InterAgentCommunication {
|
||||
|
||||
@@ -210,6 +210,7 @@ You can decide how much context you want to propagate to your sub-agents with th
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
Message Type: MESSAGE | FINAL_ANSWER
|
||||
Task name: <recipient>
|
||||
Sender: <author>
|
||||
Payload:
|
||||
<payload text>
|
||||
@@ -228,7 +229,7 @@ When you provide a response in the final channel, that content is immediately de
|
||||
You will receive messages in the analysis channel in the form:
|
||||
```
|
||||
Message Type: NEW_TASK | MESSAGE | FINAL_ANSWER
|
||||
Task name: <recipient> # only for NEW_TASK -- this determines your identity
|
||||
Task name: <recipient>
|
||||
Sender: <author>
|
||||
Payload:
|
||||
<payload text>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use codex_protocol::AgentPath;
|
||||
|
||||
use super::ContextualUserFragment;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct InterAgentCompletionMessage {
|
||||
task_name: AgentPath,
|
||||
sender: AgentPath,
|
||||
payload: String,
|
||||
}
|
||||
|
||||
impl InterAgentCompletionMessage {
|
||||
pub(crate) fn new(task_name: AgentPath, sender: AgentPath, payload: impl Into<String>) -> Self {
|
||||
Self {
|
||||
task_name,
|
||||
sender,
|
||||
payload: payload.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for InterAgentCompletionMessage {
|
||||
fn role(&self) -> &'static str {
|
||||
"assistant"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
Self::type_markers()
|
||||
}
|
||||
|
||||
fn type_markers() -> (&'static str, &'static str) {
|
||||
("", "")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
format!(
|
||||
"Message Type: FINAL_ANSWER\nTask name: {}\nSender: {}\nPayload:\n{}",
|
||||
self.task_name, self.sender, self.payload,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod environment_context;
|
||||
mod guardian_followup_review_reminder;
|
||||
mod hook_additional_context;
|
||||
mod image_generation_instructions;
|
||||
mod inter_agent_completion_message;
|
||||
mod internal_model_context;
|
||||
mod legacy_apply_patch_exec_command_warning;
|
||||
mod legacy_model_mismatch_warning;
|
||||
@@ -46,6 +47,7 @@ pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder
|
||||
pub(crate) use hook_additional_context::HookAdditionalContext;
|
||||
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
|
||||
pub use image_generation_instructions::extension_image_generation_output_hint;
|
||||
pub(crate) use inter_agent_completion_message::InterAgentCompletionMessage;
|
||||
pub use internal_model_context::InternalContextSource;
|
||||
pub use internal_model_context::InternalModelContextFragment;
|
||||
pub use internal_model_context::InvalidInternalContextSource;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_protocol::models::AgentMessageInputContent;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::plaintext_agent_message_content;
|
||||
use codex_protocol::protocol::GuardianRiskLevel;
|
||||
use codex_protocol::protocol::GuardianUserAuthorization;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
@@ -455,20 +455,10 @@ pub(crate) fn collect_guardian_transcript_entries(
|
||||
}
|
||||
ResponseItem::AgentMessage {
|
||||
author, content, ..
|
||||
} => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|content| match content {
|
||||
AgentMessageInputContent::InputText { text } => Some(text.as_str()),
|
||||
AgentMessageInputContent::EncryptedContent { .. } => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
(!text.trim().is_empty()).then(|| GuardianTranscriptEntry {
|
||||
kind: GuardianTranscriptEntryKind::Assistant,
|
||||
text: format!("Agent message from {author}:\n{text}"),
|
||||
})
|
||||
}
|
||||
} => plaintext_agent_message_content(content).map(|text| GuardianTranscriptEntry {
|
||||
kind: GuardianTranscriptEntryKind::Assistant,
|
||||
text: format!("Agent message from {author}:\n{text}"),
|
||||
}),
|
||||
ResponseItem::LocalShellCall { action, .. } => serialized_entry(
|
||||
GuardianTranscriptEntryKind::Tool("tool shell call".to_string()),
|
||||
serde_json::to_string(action).ok(),
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::session::session::Session;
|
||||
use chrono::Utc;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_protocol::models::AgentMessageInputContent;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::plaintext_agent_message_content;
|
||||
use codex_thread_store::ListThreadsParams;
|
||||
use codex_thread_store::SortDirection;
|
||||
use codex_thread_store::StoredThread;
|
||||
@@ -243,16 +243,10 @@ fn build_current_thread_section(items: &[ResponseItem]) -> Option<String> {
|
||||
ResponseItem::AgentMessage {
|
||||
author, content, ..
|
||||
} => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|content| match content {
|
||||
AgentMessageInputContent::InputText { text } => Some(text.as_str()),
|
||||
AgentMessageInputContent::EncryptedContent { .. } => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if text.trim().is_empty() || current_user.is_empty() && current_assistant.is_empty()
|
||||
{
|
||||
let Some(text) = plaintext_agent_message_content(content) else {
|
||||
continue;
|
||||
};
|
||||
if current_user.is_empty() && current_assistant.is_empty() {
|
||||
continue;
|
||||
}
|
||||
current_assistant.push(format!("Agent message from {author}:\n{text}"));
|
||||
|
||||
@@ -37,7 +37,7 @@ use crate::image_preparation::prepare_response_items;
|
||||
use crate::parse_turn_item;
|
||||
use crate::realtime_conversation::RealtimeConversationManager;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use crate::skills::SkillRenderSideEffects;
|
||||
use crate::skills_load_input_from_config;
|
||||
use crate::turn_metadata::TurnMetadataState;
|
||||
@@ -1737,7 +1737,13 @@ impl Session {
|
||||
return;
|
||||
};
|
||||
|
||||
let message = format_subagent_notification_message(child_agent_path.as_str(), &status);
|
||||
let Some(message) = format_inter_agent_completion_message(
|
||||
parent_agent_path.clone(),
|
||||
child_agent_path.clone(),
|
||||
&status,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
// `communication` owns the message. Keep a second copy only when the
|
||||
// recorder will actually need it after parent delivery succeeds.
|
||||
let trace_message = self
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::InterAgentCompletionMessage;
|
||||
use crate::context::SubagentNotification;
|
||||
|
||||
// Helpers for model-visible session state markers that are stored in user-role
|
||||
@@ -14,6 +16,22 @@ pub(crate) fn format_subagent_notification_message(
|
||||
SubagentNotification::new(agent_reference, status.clone()).render()
|
||||
}
|
||||
|
||||
pub(crate) fn format_inter_agent_completion_message(
|
||||
task_name: AgentPath,
|
||||
sender: AgentPath,
|
||||
status: &AgentStatus,
|
||||
) -> Option<String> {
|
||||
let payload = match status {
|
||||
AgentStatus::Completed(Some(message)) => message.clone(),
|
||||
AgentStatus::Completed(None) => String::new(),
|
||||
AgentStatus::Errored(error) => format!("Agent errored: {error}"),
|
||||
AgentStatus::Shutdown => "Agent shut down.".to_string(),
|
||||
AgentStatus::NotFound => "Agent was not found.".to_string(),
|
||||
AgentStatus::PendingInit | AgentStatus::Running | AgentStatus::Interrupted => return None,
|
||||
};
|
||||
Some(InterAgentCompletionMessage::new(task_name, sender, payload).render())
|
||||
}
|
||||
|
||||
pub(crate) fn format_subagent_context_line(
|
||||
agent_reference: &str,
|
||||
agent_nickname: Option<&str>,
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::config::DEFAULT_AGENT_MAX_DEPTH;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::init_state_db;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session_prefix::format_subagent_notification_message;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use crate::thread_manager::thread_store_from_config;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::handlers::multi_agents_v2::FollowupTaskHandler as FollowupTaskHandlerV2;
|
||||
@@ -2043,14 +2043,18 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
|
||||
)
|
||||
.await;
|
||||
|
||||
let first_notification = format_subagent_notification_message(
|
||||
worker_path.as_str(),
|
||||
let first_notification = format_inter_agent_completion_message(
|
||||
AgentPath::root(),
|
||||
worker_path.clone(),
|
||||
&AgentStatus::Completed(Some("first done".to_string())),
|
||||
);
|
||||
let second_notification = format_subagent_notification_message(
|
||||
worker_path.as_str(),
|
||||
)
|
||||
.expect("completed status should render");
|
||||
let second_notification = format_inter_agent_completion_message(
|
||||
AgentPath::root(),
|
||||
worker_path.clone(),
|
||||
&AgentStatus::Completed(Some("second done".to_string())),
|
||||
);
|
||||
)
|
||||
.expect("completed status should render");
|
||||
|
||||
let notifications = timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
|
||||
@@ -1087,10 +1087,16 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/worker",
|
||||
"content": [{
|
||||
"type": "encrypted_content",
|
||||
"encrypted_content": encrypted_message,
|
||||
}],
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n",
|
||||
},
|
||||
{
|
||||
"type": "encrypted_content",
|
||||
"encrypted_content": encrypted_message,
|
||||
},
|
||||
],
|
||||
})]
|
||||
);
|
||||
|
||||
@@ -1128,7 +1134,7 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message() -> Result<()>
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| {
|
||||
body_contains(req, SPAWN_CALL_ID) && !body_contains(req, "<subagent_notification>")
|
||||
body_contains(req, SPAWN_CALL_ID) && !body_contains(req, "Message Type: FINAL_ANSWER")
|
||||
},
|
||||
sse(vec![
|
||||
ev_response_created("resp-parent-2"),
|
||||
@@ -1137,14 +1143,15 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message() -> Result<()>
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let notification = "<subagent_notification>\n{\"agent_path\":\"/root/worker\",\"status\":{\"completed\":\"child done\"}}\n</subagent_notification>";
|
||||
let notification =
|
||||
"Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/worker\nPayload:\nchild done";
|
||||
// If the child is still running when the parent turn starts, wait_agent blocks
|
||||
// until mailbox delivery. The follow-up request must then contain that delivery.
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| {
|
||||
body_contains(req, TURN_2_NO_WAIT_PROMPT)
|
||||
&& !body_contains(req, "<subagent_notification>")
|
||||
&& !body_contains(req, "Message Type: FINAL_ANSWER")
|
||||
},
|
||||
sse(vec![
|
||||
ev_response_created("resp-parent-3"),
|
||||
@@ -1157,7 +1164,7 @@ async fn plaintext_multi_agent_v2_completion_sends_agent_message() -> Result<()>
|
||||
&server,
|
||||
|req: &wiremock::Request| {
|
||||
body_contains(req, TURN_2_NO_WAIT_PROMPT)
|
||||
&& body_contains(req, "<subagent_notification>")
|
||||
&& body_contains(req, "Message Type: FINAL_ANSWER")
|
||||
},
|
||||
sse(vec![
|
||||
ev_response_created("resp-parent-4"),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use codex_api::SearchInput;
|
||||
use codex_core::parse_turn_item;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::AgentMessageInputContent;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::plaintext_agent_message_content;
|
||||
use codex_tools::retain_tail_from_last_n_user_messages;
|
||||
use codex_tools::truncate_assistant_output_text_to_token_budget;
|
||||
|
||||
@@ -37,15 +37,7 @@ fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) {
|
||||
metadata,
|
||||
..
|
||||
} => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|content| match content {
|
||||
AgentMessageInputContent::InputText { text } => Some(text.as_str()),
|
||||
AgentMessageInputContent::EncryptedContent { .. } => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if !text.trim().is_empty() {
|
||||
if let Some(text) = plaintext_agent_message_content(content) {
|
||||
messages.push(ResponseItem::Message {
|
||||
id: None,
|
||||
role: ASSISTANT_ROLE.to_string(),
|
||||
|
||||
@@ -863,6 +863,20 @@ pub enum AgentMessageInputContent {
|
||||
EncryptedContent { encrypted_content: String },
|
||||
}
|
||||
|
||||
/// Returns the locally readable text when an agent message is entirely plaintext.
|
||||
pub fn plaintext_agent_message_content(content: &[AgentMessageInputContent]) -> Option<String> {
|
||||
let mut text_parts = Vec::with_capacity(content.len());
|
||||
for part in content {
|
||||
match part {
|
||||
AgentMessageInputContent::InputText { text } => text_parts.push(text.as_str()),
|
||||
AgentMessageInputContent::EncryptedContent { .. } => return None,
|
||||
}
|
||||
}
|
||||
|
||||
let text = text_parts.join("\n");
|
||||
(!text.trim().is_empty()).then_some(text)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImageDetail {
|
||||
@@ -1970,6 +1984,20 @@ mod tests {
|
||||
1, 122, 94, 171, 63, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn plaintext_agent_message_content_rejects_mixed_encrypted_content() {
|
||||
let content = vec![
|
||||
AgentMessageInputContent::InputText {
|
||||
text: "Message Type: MESSAGE\nPayload:\n".to_string(),
|
||||
},
|
||||
AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: "encrypted-payload".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(plaintext_agent_message_content(&content), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_input_message_conversion_preserves_phase() {
|
||||
let item = ResponseItem::from(ResponseInputItem::Message {
|
||||
|
||||
@@ -739,17 +739,32 @@ impl InterAgentCommunication {
|
||||
|
||||
pub fn to_model_input_item(&self) -> ResponseItem {
|
||||
let content = match &self.encrypted_content {
|
||||
Some(encrypted_content) => AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
},
|
||||
None => AgentMessageInputContent::InputText {
|
||||
Some(encrypted_content) => {
|
||||
let message_type = if self.trigger_turn {
|
||||
"NEW_TASK"
|
||||
} else {
|
||||
"MESSAGE"
|
||||
};
|
||||
vec![
|
||||
AgentMessageInputContent::InputText {
|
||||
text: format!(
|
||||
"Message Type: {message_type}\nTask name: {}\nSender: {}\nPayload:\n",
|
||||
self.recipient, self.author
|
||||
),
|
||||
},
|
||||
AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: encrypted_content.clone(),
|
||||
},
|
||||
]
|
||||
}
|
||||
None => vec![AgentMessageInputContent::InputText {
|
||||
text: self.content.clone(),
|
||||
},
|
||||
}],
|
||||
};
|
||||
ResponseItem::AgentMessage {
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content: vec![content],
|
||||
content,
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
@@ -4256,6 +4271,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_encrypted_inter_agent_communication_renders_message_envelope() {
|
||||
let communication = InterAgentCommunication::new_encrypted(
|
||||
AgentPath::root().join("worker").expect("author path"),
|
||||
AgentPath::root(),
|
||||
Vec::new(),
|
||||
"encrypted payload".to_string(),
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
communication.to_model_input_item(),
|
||||
ResponseItem::AgentMessage {
|
||||
author: "/root/worker".to_string(),
|
||||
recipient: "/root".to_string(),
|
||||
content: vec![
|
||||
AgentMessageInputContent::InputText {
|
||||
text: "Message Type: MESSAGE\nTask name: /root\nSender: /root/worker\nPayload:\n"
|
||||
.to_string(),
|
||||
},
|
||||
AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: "encrypted payload".to_string(),
|
||||
},
|
||||
],
|
||||
metadata: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_source_from_startup_arg_normalizes_custom_values() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -771,12 +771,13 @@ fn inter_agent_message_fields(item: &ConversationItem) -> Option<(String, String
|
||||
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,
|
||||
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((
|
||||
|
||||
@@ -255,7 +255,13 @@ fn sub_agent_started_activity_creates_spawn_edge() -> anyhow::Result<()> {
|
||||
"type": "agent_message",
|
||||
"author": "/root",
|
||||
"recipient": "/root/reviewer",
|
||||
"content": [{"type": "input_text", "text": "review this"}]
|
||||
"content": [
|
||||
{
|
||||
"type": "input_text",
|
||||
"text": "Message Type: NEW_TASK\nTask name: /root/reviewer\nSender: /root\nPayload:\n"
|
||||
},
|
||||
{"type": "encrypted_content", "encrypted_content": "review this"}
|
||||
]
|
||||
})],
|
||||
)?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user