mirror of
https://github.com/openai/codex.git
synced 2026-09-17 12:23:33 +00:00
Move spawned-agent interruption rules into AgentControl (#45676)
## What changed Extract V2 interruption validation and dispatch into `AgentControl::interrupt_spawned_agent`, returning the agent path and previous status with typed validation and runtime errors. Update the `interrupt_agent` tool handler to delegate to it while retaining tool error mapping and activity emission. Preserve rejection of root and self targets and successful handling of unloaded or already-dead runtimes without reloading them. GitOrigin-RevId: 893bbc4f464596d0fee032a71702a4a4f6becf5c
This commit is contained in:
@@ -72,10 +72,13 @@ pub(crate) use self::delivery::MessageDeliveryError;
|
||||
pub(crate) use self::delivery::MessageDeliveryMode;
|
||||
pub(crate) use self::execution::AgentExecutionGuard;
|
||||
use self::execution::AgentExecutionLimiter;
|
||||
pub(crate) use self::interrupt::AgentInterruptError;
|
||||
pub(crate) use self::interrupt::AgentInterruptOutcome;
|
||||
use self::residency::V2Residency;
|
||||
|
||||
mod delivery;
|
||||
mod execution;
|
||||
mod interrupt;
|
||||
mod legacy;
|
||||
mod residency;
|
||||
mod service_tier;
|
||||
|
||||
68
codex-rs/core/src/agent/control/interrupt.rs
Normal file
68
codex-rs/core/src/agent/control/interrupt.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Applies V2 interruption rules to a registered agent without loading its runtime.
|
||||
//!
|
||||
//! Root and self targets are rejected. An unloaded or already-dead runtime is a successful
|
||||
//! interruption; this operation never reloads it.
|
||||
|
||||
use super::AgentControl;
|
||||
use crate::agent::AgentStatus;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
|
||||
pub(crate) struct AgentInterruptOutcome {
|
||||
pub(crate) agent_path: AgentPath,
|
||||
pub(crate) previous_status: AgentStatus,
|
||||
}
|
||||
|
||||
/// Keeps request validation distinct from runtime failures for the tool adapter's error mapping.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AgentInterruptError {
|
||||
InvalidRequest(String),
|
||||
Agent(CodexErr),
|
||||
}
|
||||
|
||||
impl AgentControl {
|
||||
/// Interrupts a spawned agent's current task, preserving the status observed before dispatch.
|
||||
pub(crate) async fn interrupt_spawned_agent(
|
||||
&self,
|
||||
caller: ThreadId,
|
||||
target: ThreadId,
|
||||
) -> Result<AgentInterruptOutcome, AgentInterruptError> {
|
||||
let receiver_agent = self
|
||||
.ensure_agent_known(target)
|
||||
.map_err(AgentInterruptError::Agent)?;
|
||||
if receiver_agent
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.is_some_and(AgentPath::is_root)
|
||||
{
|
||||
return Err(AgentInterruptError::InvalidRequest(
|
||||
"root is not a spawned agent".to_string(),
|
||||
));
|
||||
}
|
||||
if target == caller {
|
||||
return Err(AgentInterruptError::InvalidRequest(
|
||||
"an agent cannot interrupt itself; return your result and let the parent interrupt you if needed"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||
AgentInterruptError::InvalidRequest("target agent is missing an agent_path".to_string())
|
||||
})?;
|
||||
let previous_status = self.get_status(target).await;
|
||||
match self.interrupt_agent(target).await {
|
||||
Ok(_) => {}
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(_) | CodexErrorDetails::InternalAgentDied
|
||||
) => {}
|
||||
Err(err) => return Err(AgentInterruptError::Agent(err)),
|
||||
}
|
||||
Ok(AgentInterruptOutcome {
|
||||
agent_path,
|
||||
previous_status,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use crate::tools::handlers::multi_agents_common::*;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::CollabAgentTool;
|
||||
use codex_protocol::items::CollabAgentToolCallItem;
|
||||
use codex_protocol::items::CollabAgentToolCallStatus;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::analytics::ToolCallAnalytics;
|
||||
use super::*;
|
||||
use crate::agent::control::AgentInterruptError;
|
||||
use crate::agent::control::AgentInterruptOutcome;
|
||||
use crate::tools::handlers::multi_agents_spec::create_interrupt_agent_tool_v2;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
pub(crate) struct Handler;
|
||||
@@ -44,63 +45,33 @@ async fn handle_interrupt_agent(
|
||||
let args: InterruptAgentArgs = parse_arguments(&arguments)?;
|
||||
let agent_id = resolve_agent_target(&session, &turn, &args.target).await?;
|
||||
analytics.set_receiver(agent_id);
|
||||
let receiver_agent = session
|
||||
let AgentInterruptOutcome {
|
||||
agent_path,
|
||||
previous_status,
|
||||
} = session
|
||||
.services
|
||||
.agent_control
|
||||
.ensure_agent_known(agent_id)
|
||||
.map_err(|err| collab_agent_error(agent_id, err))?;
|
||||
if receiver_agent
|
||||
.agent_path
|
||||
.as_ref()
|
||||
.is_some_and(AgentPath::is_root)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"root is not a spawned agent".to_string(),
|
||||
));
|
||||
}
|
||||
if agent_id == session.thread_id {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"an agent cannot interrupt itself; return your result and let the parent interrupt you if needed"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let receiver_agent_path = receiver_agent.agent_path.clone().ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel("target agent is missing an agent_path".to_string())
|
||||
})?;
|
||||
let status = session.services.agent_control.get_status(agent_id).await;
|
||||
let result = match session
|
||||
.services
|
||||
.agent_control
|
||||
.interrupt_agent(agent_id)
|
||||
.interrupt_spawned_agent(session.thread_id, agent_id)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::ThreadNotFound(_) | CodexErrorDetails::InternalAgentDied
|
||||
) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(collab_agent_error(agent_id, err)),
|
||||
};
|
||||
result?;
|
||||
.map_err(|err| match err {
|
||||
AgentInterruptError::InvalidRequest(message) => {
|
||||
FunctionCallError::RespondToModel(message)
|
||||
}
|
||||
AgentInterruptError::Agent(err) => collab_agent_error(agent_id, err),
|
||||
})?;
|
||||
emit_sub_agent_activity(
|
||||
&session,
|
||||
&turn,
|
||||
SubAgentActivityItem {
|
||||
id: call_id,
|
||||
agent_thread_id: agent_id,
|
||||
agent_path: receiver_agent_path,
|
||||
agent_path,
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(InterruptAgentResult {
|
||||
previous_status: status,
|
||||
})
|
||||
Ok(InterruptAgentResult { previous_status })
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
|
||||
Reference in New Issue
Block a user