mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
core: let steer interrupt wait_agent (#28341)
## Why `wait_agent` can block for a long timeout while waiting for sub-agent mailbox activity. Although same-turn user steer is accepted during that tool call, the input remains pending until the wait returns, so an explicit request to change direction can appear unresponsive. ## What changed - Notify active `wait_agent` calls when user input is steered into the current turn. - Check for already-pending steer input when subscribing so input that races with tool startup is not missed. - Distinguish mailbox activity, steered input, and timeout outcomes, returning `Wait interrupted by new input.` for the steer path. - Update the `wait_agent` tool description to document the early-return behavior. ## Testing - `just test -p codex-core input_queue_` - `just test -p codex-core wait_agent` The coverage includes steer notification before and after subscription, plus an end-to-end test that verifies the interrupted wait result and steered user input are both included exactly once in the follow-up model request.
This commit is contained in:
@@ -255,7 +255,7 @@ pub fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec {
|
||||
pub fn create_wait_agent_tool_v2(options: WaitAgentTimeoutOptions) -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "wait_agent".to_string(),
|
||||
description: "Wait for a mailbox update from any live agent, including queued messages and final-status notifications. Does not return the content; returns either a summary of which agents have updates (if any), or a timeout summary if no mailbox update arrives before the deadline."
|
||||
description: "Wait for a mailbox update from any live agent, including queued messages and final-status notifications. The wait also ends early when new user input is steered into the active turn. Does not return the content; returns either a summary of which agents have updates (if any), an interruption summary for steered input, or a timeout summary if no activity arrives before the deadline."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::session::InputQueueActivity;
|
||||
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
|
||||
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
@@ -65,7 +66,14 @@ impl Handler {
|
||||
None => default_timeout_ms,
|
||||
};
|
||||
|
||||
let mut mailbox_rx = session.input_queue.subscribe_mailbox().await;
|
||||
let turn_state = session
|
||||
.input_queue
|
||||
.turn_state_for_sub_id(&session.active_turn, &turn.sub_id)
|
||||
.await;
|
||||
let (mut activity_rx, pending_activity) = session
|
||||
.input_queue
|
||||
.subscribe_activity(turn_state.as_deref())
|
||||
.await;
|
||||
|
||||
session
|
||||
.send_event(
|
||||
@@ -82,8 +90,8 @@ impl Handler {
|
||||
.await;
|
||||
|
||||
let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
|
||||
let timed_out = !wait_for_mailbox_change(&mut mailbox_rx, deadline).await;
|
||||
let result = WaitAgentResult::from_timed_out(timed_out);
|
||||
let outcome = wait_for_activity(&mut activity_rx, pending_activity, deadline).await;
|
||||
let result = WaitAgentResult::from_outcome(outcome);
|
||||
|
||||
session
|
||||
.send_event(
|
||||
@@ -122,15 +130,15 @@ pub(crate) struct WaitAgentResult {
|
||||
}
|
||||
|
||||
impl WaitAgentResult {
|
||||
fn from_timed_out(timed_out: bool) -> Self {
|
||||
let message = if timed_out {
|
||||
"Wait timed out."
|
||||
} else {
|
||||
"Wait completed."
|
||||
fn from_outcome(outcome: WaitOutcome) -> Self {
|
||||
let message = match outcome {
|
||||
WaitOutcome::MailboxActivity => "Wait completed.",
|
||||
WaitOutcome::Steered => "Wait interrupted by new input.",
|
||||
WaitOutcome::TimedOut => "Wait timed out.",
|
||||
};
|
||||
Self {
|
||||
message: message.to_string(),
|
||||
timed_out,
|
||||
timed_out: outcome == WaitOutcome::TimedOut,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,12 +161,29 @@ impl ToolOutput for WaitAgentResult {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_mailbox_change(
|
||||
mailbox_rx: &mut tokio::sync::watch::Receiver<()>,
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum WaitOutcome {
|
||||
MailboxActivity,
|
||||
Steered,
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
async fn wait_for_activity(
|
||||
activity_rx: &mut tokio::sync::watch::Receiver<InputQueueActivity>,
|
||||
pending_activity: Option<InputQueueActivity>,
|
||||
deadline: Instant,
|
||||
) -> bool {
|
||||
match timeout_at(deadline, mailbox_rx.changed()).await {
|
||||
Ok(Ok(())) => true,
|
||||
Ok(Err(_)) | Err(_) => false,
|
||||
) -> WaitOutcome {
|
||||
if let Some(activity) = pending_activity {
|
||||
return match activity {
|
||||
InputQueueActivity::Mailbox => WaitOutcome::MailboxActivity,
|
||||
InputQueueActivity::Steer => WaitOutcome::Steered,
|
||||
};
|
||||
}
|
||||
match timeout_at(deadline, activity_rx.changed()).await {
|
||||
Ok(Ok(())) => match *activity_rx.borrow_and_update() {
|
||||
InputQueueActivity::Mailbox => WaitOutcome::MailboxActivity,
|
||||
InputQueueActivity::Steer => WaitOutcome::Steered,
|
||||
},
|
||||
Ok(Err(_)) | Err(_) => WaitOutcome::TimedOut,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user