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:
@@ -2,6 +2,7 @@ use core_test_support::test_codex::local_selections;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_core::CodexThread;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -206,6 +207,78 @@ async fn wait_for_turn_complete(codex: &CodexThread) {
|
||||
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn steer_interrupts_wait_agent_and_is_sent_in_follow_up_request() {
|
||||
const WAIT_CALL_ID: &str = "wait-call";
|
||||
const INITIAL_PROMPT: &str = "wait for an agent";
|
||||
const STEER_PROMPT: &str = "stop waiting and continue";
|
||||
|
||||
let first_chunks = vec![
|
||||
chunk(ev_response_created("resp-1")),
|
||||
chunk(ev_function_call(
|
||||
WAIT_CALL_ID,
|
||||
"wait_agent",
|
||||
r#"{"timeout_ms":10000}"#,
|
||||
)),
|
||||
chunk(ev_completed("resp-1")),
|
||||
];
|
||||
let (server, _completions) =
|
||||
start_streaming_sse_server(vec![first_chunks, response_completed_chunks("resp-2")]).await;
|
||||
let codex = test_codex()
|
||||
.with_model("gpt-5.4")
|
||||
.with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::MultiAgentV2)
|
||||
.expect("test config should allow feature update");
|
||||
})
|
||||
.build_with_streaming_server(&server)
|
||||
.await
|
||||
.expect("build Codex test session")
|
||||
.codex;
|
||||
|
||||
submit_user_input(&codex, INITIAL_PROMPT).await;
|
||||
wait_for_event(&codex, |event| {
|
||||
matches!(event, EventMsg::CollabWaitingBegin(_))
|
||||
})
|
||||
.await;
|
||||
|
||||
steer_user_input(&codex, STEER_PROMPT).await;
|
||||
wait_for_turn_complete(&codex).await;
|
||||
|
||||
let requests = server.requests().await;
|
||||
assert_eq!(requests.len(), 2);
|
||||
let second: Value = from_slice(&requests[1]).expect("parse second request");
|
||||
let relevant_user_input = message_input_texts(&second, "user")
|
||||
.into_iter()
|
||||
.filter(|text| text == INITIAL_PROMPT || text == STEER_PROMPT)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
relevant_user_input,
|
||||
vec![INITIAL_PROMPT.to_string(), STEER_PROMPT.to_string()]
|
||||
);
|
||||
let wait_output = second["input"]
|
||||
.as_array()
|
||||
.expect("second request input")
|
||||
.iter()
|
||||
.find(|item| {
|
||||
item.get("type").and_then(Value::as_str) == Some("function_call_output")
|
||||
&& item.get("call_id").and_then(Value::as_str) == Some(WAIT_CALL_ID)
|
||||
})
|
||||
.and_then(|item| item.get("output"))
|
||||
.and_then(Value::as_str)
|
||||
.expect("wait_agent output");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(wait_output).expect("parse wait_agent output"),
|
||||
json!({
|
||||
"message": "Wait interrupted by new input.",
|
||||
"timed_out": false,
|
||||
})
|
||||
);
|
||||
|
||||
server.shutdown().await;
|
||||
}
|
||||
|
||||
fn assert_two_responses_input_snapshot(snapshot_name: &str, requests: &[Vec<u8>]) {
|
||||
assert_eq!(requests.len(), 2);
|
||||
let options = ContextSnapshotOptions::default().strip_capability_instructions();
|
||||
|
||||
Reference in New Issue
Block a user