mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Move child completion routing into the agent controller (#46552)
## What changed Have `Session` capture terminal child state in an `AgentTurnOutcome` and delegate delivery to `LocalAgentControl`. Move parent notification and completion activity routing into `agent/control/completion.rs`, so routing no longer takes the child's live `TurnContext`. Preserve best-effort, queue-only parent delivery, completion activity for the initiating agent, and result tracing only after successful delivery. GitOrigin-RevId: e06871d92c9ca8660de29f50356cf7c37f7e5199
This commit is contained in:
@@ -70,6 +70,7 @@ pub(crate) use self::interrupt::AgentInterruptError;
|
||||
pub(crate) use self::interrupt::AgentInterruptOutcome;
|
||||
use self::residency::V2Residency;
|
||||
|
||||
mod completion;
|
||||
mod delivery;
|
||||
mod execution;
|
||||
mod interrupt;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Delivers terminal child results and completion activity to the agent tree.
|
||||
//!
|
||||
//! Sessions capture terminal state; the controller owns routing and queue-only delivery.
|
||||
//! Delivery remains best effort, with tracing recorded only after the parent accepts it.
|
||||
|
||||
use super::LocalAgentControl;
|
||||
use crate::TurnStartOptions;
|
||||
use crate::agent::api::AgentTurnOutcome;
|
||||
use crate::agent_communication::AgentCommunicationContext;
|
||||
use crate::agent_communication::AgentCommunicationKind;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_rollout_trace::AgentResultTracePayload;
|
||||
use codex_rollout_trace::ThreadTraceContext;
|
||||
use tracing::debug;
|
||||
|
||||
impl LocalAgentControl {
|
||||
/// Routes a captured terminal outcome without retaining the child's live turn context.
|
||||
pub(crate) async fn notify_parent_of_terminal_turn(
|
||||
&self,
|
||||
outcome: AgentTurnOutcome,
|
||||
trace: &ThreadTraceContext,
|
||||
) {
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
agent_path: Some(child_agent_path),
|
||||
..
|
||||
}) = &outcome.source
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let parent_thread_id = *parent_thread_id;
|
||||
let status = outcome.status;
|
||||
let Some(parent_agent_path) = child_agent_path
|
||||
.as_str()
|
||||
.rsplit_once('/')
|
||||
.and_then(|(parent, _)| AgentPath::try_from(parent).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if matches!(status, AgentStatus::Completed(_))
|
||||
&& let Some(parent_turn_id) = outcome.parent_turn_id
|
||||
{
|
||||
let initiating_thread_id = match outcome.initiating_agent_path.as_ref() {
|
||||
Some(initiating_agent_path) if initiating_agent_path != &parent_agent_path => {
|
||||
self.resolve_agent_reference(
|
||||
outcome.thread_id,
|
||||
&outcome.source,
|
||||
initiating_agent_path.as_str(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"failed to resolve completed activity initiator {initiating_agent_path}: {err}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
_ => Some(parent_thread_id),
|
||||
};
|
||||
if let Some(initiating_thread_id) = initiating_thread_id
|
||||
&& let Err(err) = self
|
||||
.emit_sub_agent_activity(
|
||||
initiating_thread_id,
|
||||
parent_turn_id,
|
||||
SubAgentActivityItem {
|
||||
id: format!("subagent-completed-{}", outcome.turn_id),
|
||||
kind: SubAgentActivityKind::Completed,
|
||||
agent_thread_id: outcome.thread_id,
|
||||
agent_path: child_agent_path.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
"failed to emit completed activity to initiating thread {initiating_thread_id}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 = trace.is_enabled().then(|| message.clone());
|
||||
let communication = InterAgentCommunication::new(
|
||||
child_agent_path.clone(),
|
||||
parent_agent_path,
|
||||
Vec::new(),
|
||||
message,
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
let context =
|
||||
AgentCommunicationContext::new(AgentCommunicationKind::Result, outcome.thread_id);
|
||||
if let Err(err) = self
|
||||
.send_inter_agent_communication(
|
||||
parent_thread_id,
|
||||
communication,
|
||||
context,
|
||||
TurnStartOptions::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to notify parent thread {parent_thread_id}: {err}");
|
||||
return;
|
||||
}
|
||||
if let Some(message) = trace_message {
|
||||
trace.record_agent_result_interaction(
|
||||
outcome.turn_id.as_str(),
|
||||
parent_thread_id,
|
||||
&AgentResultTracePayload {
|
||||
child_agent_path: child_agent_path.as_str(),
|
||||
message: &message,
|
||||
status: &status,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,8 @@ use std::time::UNIX_EPOCH;
|
||||
use crate::agent::AgentStatus;
|
||||
use crate::agent::LocalAgentControl;
|
||||
use crate::agent::agent_status_from_event;
|
||||
use crate::agent::api::AgentTurnOutcome;
|
||||
use crate::agent::status::is_final;
|
||||
use crate::agent_communication::AgentCommunicationContext;
|
||||
use crate::agent_communication::AgentCommunicationKind;
|
||||
use crate::agents_md_manager::SessionInstructions;
|
||||
use crate::attestation::AttestationProvider;
|
||||
use crate::compact;
|
||||
@@ -47,7 +46,6 @@ use crate::session::step_context::StepContext;
|
||||
use crate::session::step_settings::ResolvedStepSettings;
|
||||
use crate::session::step_settings::StepSettings;
|
||||
use crate::session::turn_context::TurnEnvironment;
|
||||
use crate::session_prefix::format_inter_agent_completion_message;
|
||||
use crate::shell_snapshot::SnapshotCredentialBrokerState;
|
||||
use crate::skills_load_input_from_config;
|
||||
use crate::stream_events_utils::mark_thread_memory_mode_polluted_if_external_context;
|
||||
@@ -113,7 +111,6 @@ use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::items::EnteredReviewModeItem;
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
@@ -141,7 +138,6 @@ use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::RawResponseItemEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
@@ -163,7 +159,6 @@ use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rollout::state_db;
|
||||
use codex_rollout_trace::AgentResultTracePayload;
|
||||
use codex_rollout_trace::ThreadStartedTraceMetadata;
|
||||
use codex_rollout_trace::ThreadTraceContext;
|
||||
use codex_sandboxing::SandboxType;
|
||||
@@ -2399,8 +2394,7 @@ impl Session {
|
||||
}
|
||||
|
||||
let SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
|
||||
parent_thread_id,
|
||||
agent_path: Some(child_agent_path),
|
||||
agent_path: Some(_),
|
||||
..
|
||||
}) = &turn_context.session_source
|
||||
else {
|
||||
@@ -2424,127 +2418,23 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
|
||||
self.forward_child_completion_to_parent(
|
||||
turn_context,
|
||||
*parent_thread_id,
|
||||
child_agent_path,
|
||||
status,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Sends the standard completion envelope from a spawned MultiAgentV2 child to its parent.
|
||||
async fn forward_child_completion_to_parent(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
parent_thread_id: ThreadId,
|
||||
child_agent_path: &codex_protocol::AgentPath,
|
||||
status: AgentStatus,
|
||||
) {
|
||||
let Some(parent_agent_path) = child_agent_path
|
||||
.as_str()
|
||||
.rsplit_once('/')
|
||||
.and_then(|(parent, _)| codex_protocol::AgentPath::try_from(parent).ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if matches!(status, AgentStatus::Completed(_))
|
||||
&& let Some(parent_turn_id) = turn_context.turn_metadata_state.parent_turn_id()
|
||||
{
|
||||
let initiating_thread_id = match turn_context
|
||||
.turn_metadata_state
|
||||
.initiating_agent_path()
|
||||
{
|
||||
Some(initiating_agent_path) if initiating_agent_path != &parent_agent_path => self
|
||||
.services
|
||||
.agent_control
|
||||
.resolve_agent_reference(
|
||||
self.thread_id,
|
||||
&turn_context.session_source,
|
||||
initiating_agent_path.as_str(),
|
||||
)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
debug!(
|
||||
"failed to resolve completed activity initiator {initiating_agent_path}: {err}"
|
||||
);
|
||||
})
|
||||
.ok(),
|
||||
_ => Some(parent_thread_id),
|
||||
};
|
||||
if let Some(initiating_thread_id) = initiating_thread_id
|
||||
&& let Err(err) = self
|
||||
.services
|
||||
.agent_control
|
||||
.emit_sub_agent_activity(
|
||||
initiating_thread_id,
|
||||
parent_turn_id,
|
||||
SubAgentActivityItem {
|
||||
id: format!("subagent-completed-{}", turn_context.sub_id),
|
||||
kind: SubAgentActivityKind::Completed,
|
||||
agent_thread_id: self.thread_id,
|
||||
agent_path: child_agent_path.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
"failed to emit completed activity to initiating thread {initiating_thread_id}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.services
|
||||
.rollout_thread_trace
|
||||
.is_enabled()
|
||||
.then(|| message.clone());
|
||||
let communication = InterAgentCommunication::new(
|
||||
child_agent_path.clone(),
|
||||
parent_agent_path,
|
||||
Vec::new(),
|
||||
message,
|
||||
/*trigger_turn*/ false,
|
||||
);
|
||||
let context =
|
||||
AgentCommunicationContext::new(AgentCommunicationKind::Result, self.thread_id);
|
||||
if let Err(err) = self
|
||||
.services
|
||||
self.services
|
||||
.agent_control
|
||||
.send_inter_agent_communication(
|
||||
parent_thread_id,
|
||||
communication,
|
||||
context,
|
||||
TurnStartOptions::default(),
|
||||
.notify_parent_of_terminal_turn(
|
||||
AgentTurnOutcome {
|
||||
thread_id: self.thread_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
source: turn_context.session_source.clone(),
|
||||
parent_turn_id: turn_context.turn_metadata_state.parent_turn_id(),
|
||||
initiating_agent_path: turn_context
|
||||
.turn_metadata_state
|
||||
.initiating_agent_path()
|
||||
.cloned(),
|
||||
status,
|
||||
},
|
||||
&self.services.rollout_thread_trace,
|
||||
)
|
||||
.await
|
||||
{
|
||||
debug!("failed to notify parent thread {parent_thread_id}: {err}");
|
||||
return;
|
||||
}
|
||||
if let Some(message) = trace_message {
|
||||
self.services
|
||||
.rollout_thread_trace
|
||||
.record_agent_result_interaction(
|
||||
turn_context.sub_id.as_str(),
|
||||
parent_thread_id,
|
||||
&AgentResultTracePayload {
|
||||
child_agent_path: child_agent_path.as_str(),
|
||||
message: &message,
|
||||
status: &status,
|
||||
},
|
||||
);
|
||||
}
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn maybe_mirror_event_text_to_realtime(&self, msg: &EventMsg) {
|
||||
|
||||
Reference in New Issue
Block a user