Support plaintext collaboration tool messages (#35845)

## What changed

- Preserve `encrypted_function_args` on function calls so an empty list can mark plaintext collaboration arguments across request replay.
- Deliver `spawn_agent`, `send_message`, and `followup_task` payloads as structured plaintext agent messages when that marker is present; retain encrypted delivery otherwise.
- Redact plaintext collaboration arguments from tool and communication logs, and omit the metadata when sending requests to non-OpenAI providers.

## Testing

- Cover serialization of empty encrypted-argument metadata and plaintext versus encrypted subagent message delivery.
- Verify plaintext tool arguments are redacted and provider-specific metadata is removed from non-OpenAI requests.

GitOrigin-RevId: 64db98ff0b61a3af2f04ed609292363f2e2362a8
This commit is contained in:
jif authored and copyberry committed 2026-07-28 23:49:37 +00:00
1 parent 166658a34a
commit 03edf16f0b
36 files changed
+390 -76

No files matched your search

@@ -2854,6 +2854,15 @@
"call_id": { "call_id": {
"type": "string" "type": "string"
}, },
"encrypted_function_args": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"id": { "id": {
"type": [ "type": [
"string", "string",
@@ -16337,6 +16337,15 @@
"call_id": { "call_id": {
"type": "string" "type": "string"
}, },
"encrypted_function_args": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"id": { "id": {
"type": [ "type": [
"string", "string",
@@ -12679,6 +12679,15 @@
"call_id": { "call_id": {
"type": "string" "type": "string"
}, },
"encrypted_function_args": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"id": { "id": {
"type": [ "type": [
"string", "string",
@@ -632,6 +632,15 @@
"call_id": { "call_id": {
"type": "string" "type": "string"
}, },
"encrypted_function_args": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"id": { "id": {
"type": [ "type": [
"string", "string",
@@ -702,6 +702,15 @@
"call_id": { "call_id": {
"type": "string" "type": "string"
}, },
"encrypted_function_args": {
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"id": { "id": {
"type": [ "type": [
"string", "string",
@@ -21,4 +21,4 @@ id?: ResponseItemId,
/** /**
* Set when using the Responses API. * Set when using the Responses API.
*/ */
call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: ResponseItemId, name: string, namespace?: string, arguments: string, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: ResponseItemId, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: ResponseItemId, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: ResponseItemId, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: ResponseItemId, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: ResponseItemId, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: ResponseItemId, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: ResponseItemId, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: ResponseItemId, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: ResponseItemId, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" }; call_id: string | null, status: LocalShellStatus, action: LocalShellAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call", id?: ResponseItemId, name: string, namespace?: string, arguments: string, encrypted_function_args?: Array<string>, call_id: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_call", id?: ResponseItemId, call_id: string | null, status?: string, execution: string, arguments: unknown, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "function_call_output", id?: ResponseItemId, call_id: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call", id?: ResponseItemId, status?: string, call_id: string, name: string, namespace?: string, input: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "custom_tool_call_output", id?: ResponseItemId, call_id: string, name?: string, output: FunctionCallOutputBody, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "tool_search_output", id?: ResponseItemId, call_id: string | null, status: string, execution: string, tools: unknown[], internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "web_search_call", id?: ResponseItemId, status?: string, action?: WebSearchAction, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "image_generation_call", id?: ResponseItemId, status: string, revised_prompt?: string, result: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction", id?: ResponseItemId, encrypted_content: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "compaction_trigger", } | { "type": "context_compaction", id?: ResponseItemId, encrypted_content?: string, internal_chat_message_metadata_passthrough?: InternalChatMessageMetadataPassthrough, } | { "type": "other" };
+1
View File
@@ -120,6 +120,7 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: call_id.to_string(), call_id: call_id.to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
} }
} }
+4 -5
View File
@@ -56,11 +56,10 @@ pub(crate) fn emit_agent_communication_send(
state = "send", state = "send",
sender_thread_id = %context.sender_thread_id, sender_thread_id = %context.sender_thread_id,
receiver_thread_id = %receiver_thread_id, receiver_thread_id = %receiver_thread_id,
content = if communication.content.is_empty() { content = communication
communication.encrypted_content.as_deref().unwrap_or_default() .encrypted_content
} else { .as_deref()
communication.content.as_str() .unwrap_or("[plaintext]"),
},
}, },
"agent communication" "agent communication"
); );
+10 -3
View File
@@ -848,9 +848,16 @@ impl ModelClient {
let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite);
let is_openai = self.state.provider.info().is_openai(); let is_openai = self.state.provider.info().is_openai();
if !is_openai { if !is_openai {
input for item in &mut input {
.iter_mut() item.clear_internal_chat_message_metadata_passthrough();
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough); if let ResponseItem::FunctionCall {
encrypted_function_args,
..
} = item
{
*encrypted_function_args = None;
}
}
} }
let (instructions, tools) = if model_info.use_responses_lite { let (instructions, tools) = if model_info.use_responses_lite {
let tools = create_tools_json_for_responses_api(&prompt.tools)?; let tools = create_tools_json_for_responses_api(&prompt.tools)?;
+1
View File
@@ -620,6 +620,7 @@ mod tests {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call_1".to_string(), call_id: "call_1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::Compaction { ResponseItem::Compaction {
@@ -0,0 +1,66 @@
use codex_protocol::AgentPath;
use super::ContextualUserFragment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InterAgentMessageType {
Message,
NewTask,
}
impl InterAgentMessageType {
fn as_str(self) -> &'static str {
match self {
Self::Message => "MESSAGE",
Self::NewTask => "NEW_TASK",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct InterAgentMessage {
message_type: InterAgentMessageType,
task_name: AgentPath,
sender: AgentPath,
payload: String,
}
impl InterAgentMessage {
pub(crate) fn new(
message_type: InterAgentMessageType,
task_name: AgentPath,
sender: AgentPath,
payload: impl Into<String>,
) -> Self {
Self {
message_type,
task_name,
sender,
payload: payload.into(),
}
}
}
impl ContextualUserFragment for InterAgentMessage {
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: {}\nTask name: {}\nSender: {}\nPayload:\n{}",
self.message_type.as_str(),
self.task_name,
self.sender,
self.payload,
)
}
}
+3
View File
@@ -11,6 +11,7 @@ mod environments_instructions;
mod guardian_followup_review_reminder; mod guardian_followup_review_reminder;
mod hook_additional_context; mod hook_additional_context;
mod inter_agent_completion_message; mod inter_agent_completion_message;
mod inter_agent_message;
mod internal_model_context; mod internal_model_context;
mod legacy_apply_patch_exec_command_warning; mod legacy_apply_patch_exec_command_warning;
mod legacy_model_mismatch_warning; mod legacy_model_mismatch_warning;
@@ -51,6 +52,8 @@ pub(crate) use environments_instructions::EnvironmentsInstructions;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder; pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext; pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use inter_agent_completion_message::InterAgentCompletionMessage; pub(crate) use inter_agent_completion_message::InterAgentCompletionMessage;
pub(crate) use inter_agent_message::InterAgentMessage;
pub(crate) use inter_agent_message::InterAgentMessageType;
pub use internal_model_context::InternalContextSource; pub use internal_model_context::InternalContextSource;
pub use internal_model_context::InternalModelContextFragment; pub use internal_model_context::InternalModelContextFragment;
pub use internal_model_context::InvalidInternalContextSource; pub use internal_model_context::InvalidInternalContextSource;
@@ -550,6 +550,7 @@ fn for_prompt_strips_media_when_model_does_not_support_it() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -631,6 +632,7 @@ fn for_prompt_strips_media_when_model_does_not_support_it() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -851,6 +853,7 @@ fn remove_first_item_removes_matching_output_for_function_call() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -880,6 +883,7 @@ fn remove_first_item_removes_matching_call_for_output() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-2".to_string(), call_id: "call-2".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
]; ];
@@ -1392,6 +1396,7 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-x".to_string(), call_id: "call-x".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}]; }];
let mut h = create_history_with_items(items); let mut h = create_history_with_items(items);
@@ -1407,6 +1412,7 @@ fn normalize_adds_missing_output_for_function_call() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-x".to_string(), call_id: "call-x".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -1548,6 +1554,7 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "c1".to_string(), call_id: "c1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
// Orphan output that should be removed // Orphan output that should be removed
@@ -1595,6 +1602,7 @@ fn normalize_mixed_inserts_and_removals() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "c1".to_string(), call_id: "c1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -1650,6 +1658,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-x".to_string(), call_id: "call-x".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}]; }];
let mut h = create_history_with_items(items); let mut h = create_history_with_items(items);
@@ -1663,6 +1672,7 @@ fn normalize_adds_missing_output_for_function_call_inserts_output() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-x".to_string(), call_id: "call-x".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -1684,6 +1694,7 @@ fn for_prompt_assigns_stable_id_to_synthetic_output_without_reordering_history()
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-x".to_string(), call_id: "call-x".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::Message { ResponseItem::Message {
@@ -1894,6 +1905,7 @@ fn normalize_mixed_inserts_and_removals_panics_in_debug() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "c1".to_string(), call_id: "c1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
+2
View File
@@ -308,6 +308,7 @@ async fn seed_guardian_parent_history(session: &Arc<Session>, turn: &Arc<TurnCon
namespace: None, namespace: None,
arguments: "{\"repo\":\"openai/codex\"}".to_string(), arguments: "{\"repo\":\"openai/codex\"}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -830,6 +831,7 @@ fn collect_guardian_transcript_entries_includes_recent_tool_calls_and_output() {
namespace: None, namespace: None,
arguments: "{\"path\":\"README.md\"}".to_string(), arguments: "{\"path\":\"README.md\"}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
+2
View File
@@ -10983,6 +10983,7 @@ async fn tool_calls_reopen_mailbox_delivery_for_current_turn() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
let mut ctx = HandleOutputCtx { let mut ctx = HandleOutputCtx {
@@ -11446,6 +11447,7 @@ while :; do sleep 1; done"#,
}) })
.to_string(), .to_string(),
call_id: "shell-cleanup-call".to_string(), call_id: "shell-cleanup-call".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}; };
let call = ToolRouter::build_tool_call(item)? let call = ToolRouter::build_tool_call(item)?
+2 -1
View File
@@ -14,6 +14,7 @@ use crate::session::session::Session;
use crate::session::turn_context::TurnContext; use crate::session::turn_context::TurnContext;
use crate::tools::parallel::ToolCallRuntime; use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolRouter; use crate::tools::router::ToolRouter;
use crate::tools::router::tool_log_payload;
use codex_memories_read::citations::parse_memory_citation; use codex_memories_read::citations::parse_memory_citation;
use codex_memories_read::citations::thread_ids_from_memory_citation; use codex_memories_read::citations::thread_ids_from_memory_citation;
use codex_protocol::error::CodexErr; use codex_protocol::error::CodexErr;
@@ -303,7 +304,7 @@ pub(crate) async fn handle_output_item_done(
) )
.await; .await;
let payload_preview = call.payload.log_payload().into_owned(); let payload_preview = tool_log_payload(&call.payload, &call.direct_source());
tracing::info!( tracing::info!(
thread_id = %ctx.sess.thread_id, thread_id = %ctx.sess.thread_id,
"ToolCall: {} {}", "ToolCall: {} {}",
@@ -100,6 +100,7 @@ fn external_context_pollution_items_exclude_local_tool_calls() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
@@ -203,6 +203,7 @@ fn truncates_before_requested_user_message() {
name: "tool".to_string(), name: "tool".to_string(),
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
assistant_msg("a4"), assistant_msg("a4"),
@@ -233,6 +233,7 @@ fn truncates_rollout_from_start_before_nth_user_only() {
name: "tool".to_string(), name: "tool".to_string(),
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
assistant_msg("a4"), assistant_msg("a4"),
+1
View File
@@ -318,6 +318,7 @@ async fn call_nested_tool(
tool_name, tool_name,
call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()), call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()),
payload, payload,
encrypted_function_args: None,
}; };
let result = tool_runtime let result = tool_runtime
.handle_tool_call_with_source( .handle_tool_call_with_source(
+1
View File
@@ -45,6 +45,7 @@ pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum ToolCallSource { pub enum ToolCallSource {
Direct, Direct,
DirectPlaintextMessage,
CodeMode { CodeMode {
/// Runtime cell that issued the nested tool request. /// Runtime cell that issued the nested tool request.
cell_id: String, cell_id: String,
@@ -2,6 +2,9 @@
use crate::agent::AgentStatus; use crate::agent::AgentStatus;
use crate::agent::agent_resolver::resolve_agent_target; use crate::agent::agent_resolver::resolve_agent_target;
use crate::context::ContextualUserFragment;
use crate::context::InterAgentMessage;
use crate::context::InterAgentMessageType;
use crate::function_tool::FunctionCallError; use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation; use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput; use crate::tools::context::ToolOutput;
@@ -51,16 +54,31 @@ pub(crate) async fn emit_sub_agent_activity(
session.emit_turn_item_completed(turn, item).await; session.emit_turn_item_completed(turn, item).await;
} }
pub(super) fn communication_from_tool_message( fn communication_from_tool_message(
author: AgentPath, author: AgentPath,
recipient: AgentPath, recipient: AgentPath,
message: String, message: String,
source: &crate::tools::context::ToolCallSource,
trigger_turn: bool,
) -> InterAgentCommunication { ) -> InterAgentCommunication {
InterAgentCommunication::new_encrypted( if !matches!(
author, source,
recipient, crate::tools::context::ToolCallSource::DirectPlaintextMessage
Vec::new(), ) {
message, return InterAgentCommunication::new_encrypted(
/*trigger_turn*/ true, author,
) recipient,
Vec::new(),
message,
trigger_turn,
);
}
let message_type = if trigger_turn {
InterAgentMessageType::NewTask
} else {
InterAgentMessageType::Message
};
let content =
InterAgentMessage::new(message_type, recipient.clone(), author.clone(), message).render();
InterAgentCommunication::new(author, recipient, Vec::new(), content, trigger_turn)
} }
@@ -7,7 +7,6 @@ use super::*;
use crate::agent_communication::AgentCommunicationContext; use crate::agent_communication::AgentCommunicationContext;
use crate::agent_communication::AgentCommunicationKind; use crate::agent_communication::AgentCommunicationKind;
use crate::tools::context::FunctionToolOutput; use crate::tools::context::FunctionToolOutput;
use codex_protocol::protocol::InterAgentCommunication;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum MessageDeliveryMode { pub(crate) enum MessageDeliveryMode {
@@ -16,17 +15,10 @@ pub(crate) enum MessageDeliveryMode {
} }
impl MessageDeliveryMode { impl MessageDeliveryMode {
/// Returns whether the produced communication should start a turn immediately. fn trigger_turn(self) -> bool {
fn apply(self, communication: InterAgentCommunication) -> InterAgentCommunication {
match self { match self {
Self::QueueOnly => InterAgentCommunication { Self::QueueOnly => false,
trigger_turn: false, Self::TriggerTurn => true,
..communication
},
Self::TriggerTurn => InterAgentCommunication {
trigger_turn: true,
..communication
},
} }
} }
} }
@@ -68,6 +60,7 @@ pub(crate) async fn handle_message_string_tool(
session, session,
turn, turn,
call_id, call_id,
source,
.. ..
} = invocation; } = invocation;
let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?; let receiver_thread_id = resolve_agent_target(&session, &turn, &target).await?;
@@ -100,8 +93,13 @@ pub(crate) async fn handle_message_string_tool(
.session_source .session_source
.get_agent_path() .get_agent_path()
.unwrap_or_else(AgentPath::root); .unwrap_or_else(AgentPath::root);
let communication = let communication = communication_from_tool_message(
communication_from_tool_message(author, receiver_agent_path.clone(), message); author,
receiver_agent_path.clone(),
message,
&source,
mode.trigger_turn(),
);
let kind = match mode { let kind = match mode {
MessageDeliveryMode::QueueOnly => AgentCommunicationKind::Message, MessageDeliveryMode::QueueOnly => AgentCommunicationKind::Message,
MessageDeliveryMode::TriggerTurn => AgentCommunicationKind::Followup, MessageDeliveryMode::TriggerTurn => AgentCommunicationKind::Followup,
@@ -112,12 +110,7 @@ pub(crate) async fn handle_message_string_tool(
let result = session let result = session
.services .services
.agent_control .agent_control
.send_inter_agent_communication( .send_inter_agent_communication(receiver_thread_id, communication, context, parent_turn_id)
receiver_thread_id,
mode.apply(communication),
context,
parent_turn_id,
)
.await .await
.map_err(|err| collab_agent_error(receiver_thread_id, err)); .map_err(|err| collab_agent_error(receiver_thread_id, err));
result?; result?;
@@ -44,18 +44,19 @@ async fn handle_spawn_agent(
turn, turn,
payload, payload,
call_id, call_id,
source,
.. ..
} = invocation; } = invocation;
let arguments = function_arguments(payload)?; let arguments = function_arguments(payload)?;
let args: SpawnAgentArgs = parse_arguments(&arguments)?; let args: SpawnAgentArgs = parse_arguments(&arguments)?;
let fork_mode = args.fork_mode()?; let fork_mode = args.fork_mode()?;
let message = message_content(args.message)?;
let role_name = args let role_name = args
.agent_type .agent_type
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.filter(|role| !role.is_empty()); .filter(|role| !role.is_empty());
let message = message_content(args.message)?;
let session_source = turn.session_source.clone(); let session_source = turn.session_source.clone();
let child_depth = next_thread_spawn_depth(&session_source); let child_depth = next_thread_spawn_depth(&session_source);
let mut config = let mut config =
@@ -103,7 +104,13 @@ async fn handle_spawn_agent(
.session_source .session_source
.get_agent_path() .get_agent_path()
.unwrap_or_else(AgentPath::root); .unwrap_or_else(AgentPath::root);
let communication = communication_from_tool_message(author, new_agent_path.clone(), message); let communication = communication_from_tool_message(
author,
new_agent_path.clone(),
message,
&source,
/*trigger_turn*/ true,
);
let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id); let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id);
let spawned_agent = Box::pin( let spawned_agent = Box::pin(
session session
+3 -1
View File
@@ -86,7 +86,9 @@ async fn notify_tool_finish_parts(
fn extension_tool_call_source(source: ToolCallSource) -> ExtensionToolCallSource { fn extension_tool_call_source(source: ToolCallSource) -> ExtensionToolCallSource {
match source { match source {
ToolCallSource::Direct => ExtensionToolCallSource::Direct, ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
ExtensionToolCallSource::Direct
}
ToolCallSource::CodeMode { ToolCallSource::CodeMode {
cell_id, cell_id,
runtime_tool_call_id, runtime_tool_call_id,
+11 -3
View File
@@ -78,8 +78,8 @@ impl ToolCallRuntime {
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> impl std::future::Future<Output = Result<ResponseInputItem, CodexErr>> { ) -> impl std::future::Future<Output = Result<ResponseInputItem, CodexErr>> {
let error_call = call.clone(); let error_call = call.clone();
let future = let source = call.direct_source();
self.handle_tool_call_with_source(call, ToolCallSource::Direct, cancellation_token); let future = self.handle_tool_call_with_source(call, source, cancellation_token);
async move { async move {
match future.await { match future.await {
Ok(response) => Ok(response.into_response()), Ok(response) => Ok(response.into_response()),
@@ -270,7 +270,11 @@ impl ToolCallTimingGuard {
// Code-mode calls are nested within a direct code-mode tool call whose // Code-mode calls are nested within a direct code-mode tool call whose
// timing already includes them. Suppress nested guards so consumers do // timing already includes them. Suppress nested guards so consumers do
// not mistake overlapping events for independent tool-call latency. // not mistake overlapping events for independent tool-call latency.
if !matches!(source, ToolCallSource::Direct) || !tracing::enabled!(tracing::Level::INFO) { if !matches!(
source,
ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage
) || !tracing::enabled!(tracing::Level::INFO)
{
return None; return None;
} }
@@ -373,6 +377,7 @@ mod tests {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
let direct_guard = ToolCallTimingGuard::capture( let direct_guard = ToolCallTimingGuard::capture(
Instant::now(), Instant::now(),
@@ -449,6 +454,7 @@ mod tests {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
let response_task = let response_task =
tokio::spawn(runtime.handle_tool_call(call, cancellation_token.clone())); tokio::spawn(runtime.handle_tool_call(call, cancellation_token.clone()));
@@ -692,6 +698,7 @@ mod tests {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
let response_task = let response_task =
@@ -765,6 +772,7 @@ mod tests {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
let response_task = let response_task =
+4 -3
View File
@@ -24,6 +24,7 @@ use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::hook_names::HookToolName; use crate::tools::hook_names::HookToolName;
use crate::tools::lifecycle::notify_tool_finish; use crate::tools::lifecycle::notify_tool_finish;
use crate::tools::lifecycle::notify_tool_start; use crate::tools::lifecycle::notify_tool_start;
use crate::tools::router::tool_log_payload;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace; use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::util::error_or_panic; use crate::util::error_or_panic;
use codex_extension_api::ToolCallOutcome; use codex_extension_api::ToolCallOutcome;
@@ -465,7 +466,7 @@ impl ToolRegistry {
Some(tool) => tool, Some(tool) => tool,
None => { None => {
let message = unsupported_tool_call_message(&invocation.payload, &tool_name); let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
let log_payload = invocation.payload.log_payload(); let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
otel.tool_result_with_tags( otel.tool_result_with_tags(
tool_name_flat.as_ref(), tool_name_flat.as_ref(),
&call_id_owned, &call_id_owned,
@@ -496,7 +497,7 @@ impl ToolRegistry {
} }
if !tool.matches_kind(&invocation.payload) { if !tool.matches_kind(&invocation.payload) {
let message = format!("tool {tool_name} invoked with incompatible payload"); let message = format!("tool {tool_name} invoked with incompatible payload");
let log_payload = invocation.payload.log_payload(); let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
otel.tool_result_with_tags( otel.tool_result_with_tags(
tool_name_flat.as_ref(), tool_name_flat.as_ref(),
&call_id_owned, &call_id_owned,
@@ -578,7 +579,7 @@ impl ToolRegistry {
let response_cell = tokio::sync::Mutex::new(None); let response_cell = tokio::sync::Mutex::new(None);
let invocation_for_tool = invocation.clone(); let invocation_for_tool = invocation.clone();
let log_payload = invocation.payload.log_payload(); let log_payload = tool_log_payload(&invocation.payload, &invocation.source);
let result = otel let result = otel
.log_tool_result_with_tags( .log_tool_result_with_tags(
+36
View File
@@ -20,6 +20,7 @@ use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolExecutor; use codex_tools::ToolExecutor;
use codex_tools::ToolName; use codex_tools::ToolName;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use std::borrow::Cow;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
@@ -33,6 +34,36 @@ pub struct ToolCall {
pub tool_name: ToolName, pub tool_name: ToolName,
pub call_id: String, pub call_id: String,
pub payload: ToolPayload, pub payload: ToolPayload,
pub encrypted_function_args: Option<Vec<String>>,
}
impl ToolCall {
pub(crate) fn direct_source(&self) -> ToolCallSource {
if self.tool_name.namespace.as_deref() == Some("collaboration")
&& matches!(
self.tool_name.name.as_str(),
"spawn_agent" | "send_message" | "followup_task"
)
&& self
.encrypted_function_args
.as_ref()
.is_some_and(Vec::is_empty)
{
ToolCallSource::DirectPlaintextMessage
} else {
ToolCallSource::Direct
}
}
}
pub(crate) fn tool_log_payload<'a>(
payload: &'a ToolPayload,
source: &ToolCallSource,
) -> Cow<'a, str> {
if matches!(source, ToolCallSource::DirectPlaintextMessage) {
return Cow::Borrowed("[plaintext arguments]");
}
payload.log_payload()
} }
pub struct ToolRouter { pub struct ToolRouter {
@@ -131,6 +162,7 @@ impl ToolRouter {
name, name,
namespace, namespace,
arguments, arguments,
encrypted_function_args,
call_id, call_id,
.. ..
} => { } => {
@@ -139,6 +171,7 @@ impl ToolRouter {
tool_name, tool_name,
call_id, call_id,
payload: ToolPayload::Function { arguments }, payload: ToolPayload::Function { arguments },
encrypted_function_args,
})) }))
} }
ResponseItem::ToolSearchCall { ResponseItem::ToolSearchCall {
@@ -157,6 +190,7 @@ impl ToolRouter {
tool_name: ToolName::plain("tool_search"), tool_name: ToolName::plain("tool_search"),
call_id, call_id,
payload: ToolPayload::ToolSearch { arguments }, payload: ToolPayload::ToolSearch { arguments },
encrypted_function_args: None,
})) }))
} }
ResponseItem::ToolSearchCall { .. } => Ok(None), ResponseItem::ToolSearchCall { .. } => Ok(None),
@@ -170,6 +204,7 @@ impl ToolRouter {
tool_name: ToolName::new(namespace, name), tool_name: ToolName::new(namespace, name),
call_id, call_id,
payload: ToolPayload::Custom { input }, payload: ToolPayload::Custom { input },
encrypted_function_args: None,
})), })),
_ => Ok(None), _ => Ok(None),
} }
@@ -237,6 +272,7 @@ impl ToolRouter {
tool_name, tool_name,
call_id, call_id,
payload, payload,
..
} = call; } = call;
// Keep the legacy ToolInvocation.turn field tied to the same request state until handlers migrate. // Keep the legacy ToolInvocation.turn field tied to the same request state until handlers migrate.
+26
View File
@@ -37,9 +37,25 @@ use super::ToolCallSource;
use super::ToolRouter; use super::ToolRouter;
use super::ToolRouterParams; use super::ToolRouterParams;
use super::extension_tool_executors; use super::extension_tool_executors;
use super::tool_log_payload;
struct ExtensionEchoContributor; struct ExtensionEchoContributor;
#[test]
fn tool_log_payload_redacts_plaintext_multi_agent_messages() {
let payload = ToolPayload::Function {
arguments: json!({"target": "/root/worker", "message": "secret message"}).to_string(),
};
assert_eq!(
tool_log_payload(&payload, &ToolCallSource::DirectPlaintextMessage),
"[plaintext arguments]"
);
assert_eq!(
tool_log_payload(&payload, &ToolCallSource::Direct),
payload.log_payload()
);
}
impl codex_extension_api::ToolContributor for ExtensionEchoContributor { impl codex_extension_api::ToolContributor for ExtensionEchoContributor {
fn tools( fn tools(
&self, &self,
@@ -135,6 +151,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}) })
}) })
.expect("test session should expose a parallel shell-like tool"); .expect("test session should expose a parallel shell-like tool");
@@ -145,6 +162,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
})); }));
Ok(()) Ok(())
@@ -159,6 +177,7 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
name: tool_name.clone(), name: tool_name.clone(),
namespace: Some("mcp__codex_apps__calendar".to_string()), namespace: Some("mcp__codex_apps__calendar".to_string()),
arguments: "{}".to_string(), arguments: "{}".to_string(),
encrypted_function_args: Some(Vec::new()),
call_id: "call-namespace".to_string(), call_id: "call-namespace".to_string(),
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
})? })?
@@ -169,6 +188,8 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
ToolName::namespaced("mcp__codex_apps__calendar", tool_name) ToolName::namespaced("mcp__codex_apps__calendar", tool_name)
); );
assert_eq!(call.call_id, "call-namespace"); assert_eq!(call.call_id, "call-namespace");
assert_eq!(call.encrypted_function_args, Some(Vec::new()));
assert_eq!(call.direct_source(), ToolCallSource::Direct);
match call.payload { match call.payload {
ToolPayload::Function { arguments } => { ToolPayload::Function { arguments } => {
assert_eq!(arguments, "{}"); assert_eq!(arguments, "{}");
@@ -202,6 +223,7 @@ async fn build_custom_tool_call_uses_namespace_for_registry_name() -> anyhow::Re
payload: ToolPayload::Custom { payload: ToolPayload::Custom {
input: "print('hello')".to_string(), input: "print('hello')".to_string(),
}, },
encrypted_function_args: None,
} }
); );
@@ -246,6 +268,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
assert!(router.tool_supports_parallel(&call)); assert!(router.tool_supports_parallel(&call));
@@ -255,6 +278,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
}; };
assert!(!router.tool_supports_parallel(&different_server_call)); assert!(!router.tool_supports_parallel(&different_server_call));
@@ -286,6 +310,7 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
payload: ToolPayload::Function { payload: ToolPayload::Function {
arguments: "{}".to_string(), arguments: "{}".to_string(),
}, },
encrypted_function_args: None,
})); }));
Ok(()) Ok(())
@@ -438,6 +463,7 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
namespace: Some("extension/".to_string()), namespace: Some("extension/".to_string()),
arguments: json!({ "message": "hello" }).to_string(), arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(), call_id: "call-extension".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
})? })?
.expect("function_call should produce a tool call"); .expect("function_call should produce a tool call");
+10 -6
View File
@@ -61,9 +61,11 @@ impl ToolDispatchTrace {
fn tool_dispatch_invocation(invocation: &ToolInvocation) -> Option<ToolDispatchInvocation> { fn tool_dispatch_invocation(invocation: &ToolInvocation) -> Option<ToolDispatchInvocation> {
let requester = match &invocation.source { let requester = match &invocation.source {
ToolCallSource::Direct => ToolDispatchRequester::Model { ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
model_visible_call_id: invocation.call_id.clone(), ToolDispatchRequester::Model {
}, model_visible_call_id: invocation.call_id.clone(),
}
}
ToolCallSource::CodeMode { ToolCallSource::CodeMode {
cell_id, cell_id,
runtime_tool_call_id, runtime_tool_call_id,
@@ -91,9 +93,11 @@ fn tool_dispatch_result(
result: &dyn ToolOutput, result: &dyn ToolOutput,
) -> Option<ToolDispatchResult> { ) -> Option<ToolDispatchResult> {
match invocation.source { match invocation.source {
ToolCallSource::Direct => Some(ToolDispatchResult::DirectResponse { ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
response_item: result.to_response_item(call_id, payload), Some(ToolDispatchResult::DirectResponse {
}), response_item: result.to_response_item(call_id, payload),
})
}
ToolCallSource::CodeMode { .. } => Some(ToolDispatchResult::CodeModeResponse { ToolCallSource::CodeMode { .. } => Some(ToolDispatchResult::CodeModeResponse {
value: result.code_mode_result(payload), value: result.code_mode_result(payload),
}), }),
+1
View File
@@ -150,6 +150,7 @@ fn response_item_records_turn_ttft_for_first_output_signals() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
} }
)); ));
+23 -3
View File
@@ -254,9 +254,18 @@ async fn openai_stateless_responses_requests_preserve_item_turn_metadata_across_
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn non_openai_responses_requests_include_item_ids_without_passthrough_metadata() { async fn non_openai_responses_requests_include_item_ids_without_passthrough_metadata() {
let server = MockServer::start().await; let server = MockServer::start().await;
let response_mock = mount_sse_once( let mut private_function_call = ev_function_call("private-call", "unsupported_tool", "{}");
private_function_call["item"]["encrypted_function_args"] = json!(["message"]);
let response_mock = mount_sse_sequence(
&server, &server,
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]), vec![
sse(vec![
ev_response_created("resp1"),
private_function_call,
ev_completed("resp1"),
]),
sse(vec![ev_response_created("resp2"), ev_completed("resp2")]),
],
) )
.await; .await;
let mut provider = let mut provider =
@@ -289,7 +298,11 @@ async fn non_openai_responses_requests_include_item_ids_without_passthrough_meta
.unwrap(); .unwrap();
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let body = response_mock.single_request().body_json(); let body = response_mock
.requests()
.pop()
.expect("follow-up request")
.body_json();
let input = body["input"] let input = body["input"]
.as_array() .as_array()
.expect("request should include input items"); .expect("request should include input items");
@@ -300,6 +313,10 @@ async fn non_openai_responses_requests_include_item_ids_without_passthrough_meta
.is_none(), .is_none(),
"input item should omit internal chat message metadata passthrough: {item}" "input item should omit internal chat message metadata passthrough: {item}"
); );
assert!(
item.get("encrypted_function_args").is_none(),
"input item should omit private encrypted function metadata: {item}"
);
assert!( assert!(
item.get("id").and_then(serde_json::Value::as_str).is_some(), item.get("id").and_then(serde_json::Value::as_str).is_some(),
"input item should include a generated ID: {item}" "input item should include a generated ID: {item}"
@@ -508,6 +525,7 @@ async fn synthetic_call_output_id_is_stable_across_resumes() -> anyhow::Result<(
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: function_call_id.to_string(), call_id: function_call_id.to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}), }),
}, },
@@ -1169,6 +1187,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
namespace: None, namespace: None,
arguments: "{\"path\":\"/tmp/example.png\"}".to_string(), arguments: "{\"path\":\"/tmp/example.png\"}".to_string(),
call_id: function_call_id.to_string(), call_id: function_call_id.to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}), }),
}, },
@@ -3276,6 +3295,7 @@ async fn azure_responses_request_includes_store_and_prefixed_item_ids() {
namespace: None, namespace: None,
arguments: "{}".into(), arguments: "{}".into(),
call_id: "function-call-id".into(), call_id: "function-call-id".into(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}); });
prompt.input.push(ResponseItem::FunctionCallOutput { prompt.input.push(ResponseItem::FunctionCallOutput {
@@ -1400,8 +1400,10 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res
Ok(()) Ok(())
} }
#[test_case(false; "encrypted")]
#[test_case(true; "plaintext")]
#[tokio::test] #[tokio::test]
async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result<()> { async fn multi_agent_v2_spawn_sends_agent_message_to_child(plaintext: bool) -> Result<()> {
let output: &'static Mutex<Vec<u8>> = Box::leak(Box::new(Mutex::new(Vec::new()))); let output: &'static Mutex<Vec<u8>> = Box::leak(Box::new(Mutex::new(Vec::new())));
let subscriber = tracing_subscriber::fmt() let subscriber = tracing_subscriber::fmt()
.with_ansi(false) .with_ansi(false)
@@ -1411,22 +1413,30 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
let _guard = tracing::subscriber::set_default(subscriber); let _guard = tracing::subscriber::set_default(subscriber);
let server = start_mock_server().await; let server = start_mock_server().await;
let encrypted_message = "opaque-encrypted-message"; let message = if plaintext {
"plaintext delegated task"
} else {
"opaque-encrypted-message"
};
let spawn_args = serde_json::to_string(&json!({ let spawn_args = serde_json::to_string(&json!({
"message": encrypted_message, "message": message,
"task_name": "worker", "task_name": "worker",
}))?; }))?;
let mut spawn_event = ev_function_call_with_namespace(
SPAWN_CALL_ID,
MULTI_AGENT_V2_NAMESPACE,
"spawn_agent",
&spawn_args,
);
if plaintext {
spawn_event["item"]["encrypted_function_args"] = json!([]);
}
mount_sse_once_match( mount_sse_once_match(
&server, &server,
|req: &wiremock::Request| body_contains(req, TURN_1_PROMPT), |req: &wiremock::Request| body_contains(req, TURN_1_PROMPT),
sse(vec![ sse(vec![
ev_response_created("resp-parent-1"), ev_response_created("resp-parent-1"),
ev_function_call_with_namespace( spawn_event,
SPAWN_CALL_ID,
MULTI_AGENT_V2_NAMESPACE,
"spawn_agent",
&spawn_args,
),
ev_completed("resp-parent-1"), ev_completed("resp-parent-1"),
]), ]),
) )
@@ -1440,7 +1450,7 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
]), ]),
) )
.await; .await;
mount_sse_once_match( let parent_request_log = mount_sse_once_match(
&server, &server,
|req: &wiremock::Request| { |req: &wiremock::Request| {
body_contains(req, SPAWN_CALL_ID) && !request_has_input_type(req, "agent_message") body_contains(req, SPAWN_CALL_ID) && !request_has_input_type(req, "agent_message")
@@ -1484,6 +1494,25 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
} }
sleep(Duration::from_millis(10)).await; sleep(Duration::from_millis(10)).await;
}; };
let content = if plaintext {
vec![json!({
"type": "input_text",
"text": format!(
"Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n{message}"
),
})]
} else {
vec![
json!({
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n",
}),
json!({
"type": "encrypted_content",
"encrypted_content": message,
}),
]
};
assert_eq!( assert_eq!(
strip_response_item_ids_from_json(strip_metadata_from_json(Value::Array( strip_response_item_ids_from_json(strip_metadata_from_json(Value::Array(
child_request.inputs_of_type("agent_message"), child_request.inputs_of_type("agent_message"),
@@ -1492,18 +1521,20 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
"type": "agent_message", "type": "agent_message",
"author": "/root", "author": "/root",
"recipient": "/root/worker", "recipient": "/root/worker",
"content": [ "content": content,
{
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n",
},
{
"type": "encrypted_content",
"encrypted_content": encrypted_message,
},
],
})]) })])
); );
if plaintext {
assert!(
parent_request_log.requests().into_iter().any(|request| {
request.input().iter().any(|item| {
item["call_id"].as_str() == Some(SPAWN_CALL_ID)
&& item["encrypted_function_args"] == json!([])
})
}),
"plaintext function-call metadata should survive replay"
);
}
let child_thread_id = test let child_thread_id = test
.thread_manager .thread_manager
@@ -1530,7 +1561,8 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result
.expect("spawn send event"); .expect("spawn send event");
assert!(send.contains(&format!("sender_thread_id={root_thread_id}"))); assert!(send.contains(&format!("sender_thread_id={root_thread_id}")));
assert!(send.contains(&format!("receiver_thread_id={child_thread_id}"))); assert!(send.contains(&format!("receiver_thread_id={child_thread_id}")));
assert!(send.contains(&format!("content=\"{encrypted_message}\""))); let logged_message = if plaintext { "[plaintext]" } else { message };
assert!(send.contains(&format!("content=\"{logged_message}\"")));
let communication_id = log_field(send, "communication_id").expect("communication ID"); let communication_id = log_field(send, "communication_id").expect("communication ID");
logs.lines() logs.lines()
@@ -100,6 +100,7 @@ async fn recent_image_fallback_selects_newest_images_in_chronological_order() {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "mcp-call".to_string(), call_id: "mcp-call".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
ResponseItem::FunctionCallOutput { ResponseItem::FunctionCallOutput {
+1
View File
@@ -129,6 +129,7 @@ mod tests {
namespace: None, namespace: None,
arguments: "{}".to_string(), arguments: "{}".to_string(),
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
encrypted_function_args: None,
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
}, },
previous_assistant, previous_assistant,
+20
View File
@@ -882,6 +882,9 @@ pub enum ResponseItem {
// JSON, not as an already‑parsed object. We keep it as a raw string here and let // JSON, not as an already‑parsed object. We keep it as a raw string here and let
// Session::handle_function_call parse it into a Value. // Session::handle_function_call parse it into a Value.
arguments: String, arguments: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
encrypted_function_args: Option<Vec<String>>,
call_id: String, call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
@@ -2892,12 +2895,29 @@ mod tests {
name: "mcp__codex_apps__gmail_get_recent_emails".to_string(), name: "mcp__codex_apps__gmail_get_recent_emails".to_string(),
namespace: Some("mcp__codex_apps__gmail".to_string()), namespace: Some("mcp__codex_apps__gmail".to_string()),
arguments: "{\"top_k\":5}".to_string(), arguments: "{\"top_k\":5}".to_string(),
encrypted_function_args: None,
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
internal_chat_message_metadata_passthrough: None, internal_chat_message_metadata_passthrough: None,
} }
); );
} }
#[test]
fn function_call_preserves_empty_encrypted_function_args() {
let value = serde_json::json!({
"type": "function_call",
"name": "spawn_agent",
"namespace": "collaboration",
"arguments": "{\"message\":\"hello\",\"task_name\":\"worker\"}",
"encrypted_function_args": [],
"call_id": "call-1",
});
let item: ResponseItem =
serde_json::from_value(value.clone()).expect("function_call should deserialize");
assert_eq!(serde_json::to_value(item).expect("serialize item"), value);
}
#[test] #[test]
fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() { fn render_command_prefix_list_sorts_by_len_then_total_len_then_alphabetical() {
let prefixes = vec![ let prefixes = vec![