Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion codex-rs/core/src/agent/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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;
};
Expand Down
8 changes: 5 additions & 3 deletions codex-rs/core/src/agent/control_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
41 changes: 41 additions & 0 deletions codex-rs/core/src/context/inter_agent_completion_message.rs
Original file line number Diff line number Diff line change
@@ -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,
Comment thread
jif-oai marked this conversation as resolved.
)
}
}
2 changes: 2 additions & 0 deletions codex-rs/core/src/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 5 additions & 15 deletions codex-rs/core/src/guardian/prompt.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
16 changes: 5 additions & 11 deletions codex-rs/core/src/realtime_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}"));
Expand Down
10 changes: 8 additions & 2 deletions codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/core/src/session_prefix.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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>,
Expand Down
18 changes: 11 additions & 7 deletions codex-rs/core/src/tools/handlers/multi_agents_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
23 changes: 15 additions & 8 deletions codex-rs/core/tests/suite/subagent_notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
],
})]
);

Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand Down
12 changes: 2 additions & 10 deletions codex-rs/ext/web-search/src/history.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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(),
Expand Down
Loading
Loading