feat: make interrupt state not final for multi-agents (#13850)
Make `interrupted` an agent state and make it not final. As a result, a `wait` won't return on an interrupted agent and no notification will be send to the parent agent. The rationals are: * If a user interrupt a sub-agent for any reason, you don't want the parent agent to instantaneously ask the sub-agent to restart * If a parent agent interrupt a sub-agent, no need to add a noisy notification in the parent agen
This commit is contained in:
parent
18ad67549c
commit
3f266bcd68
29 changed files with 151 additions and 4 deletions
|
|
@ -535,6 +535,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -5691,6 +5691,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -2292,6 +2292,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@
|
|||
"enum": [
|
||||
"pendingInit",
|
||||
"running",
|
||||
"interrupted",
|
||||
"completed",
|
||||
"errored",
|
||||
"shutdown",
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CollabAgentStatus = "pendingInit" | "running" | "completed" | "errored" | "shutdown" | "notFound";
|
||||
export type CollabAgentStatus = "pendingInit" | "running" | "interrupted" | "completed" | "errored" | "shutdown" | "notFound";
|
||||
|
|
|
|||
|
|
@ -2417,6 +2417,74 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_interrupted_send_input_as_completed_collab_call() {
|
||||
// `send_input(interrupt=true)` first stops the child's active turn, then redirects it with
|
||||
// new input. The transient interrupted status should remain visible in agent state, but the
|
||||
// collab tool call itself is still a successful redirect rather than a failed operation.
|
||||
let sender = ThreadId::try_from("00000000-0000-0000-0000-000000000001")
|
||||
.expect("valid sender thread id");
|
||||
let receiver = ThreadId::try_from("00000000-0000-0000-0000-000000000002")
|
||||
.expect("valid receiver thread id");
|
||||
let events = vec![
|
||||
EventMsg::UserMessage(UserMessageEvent {
|
||||
message: "redirect".into(),
|
||||
images: None,
|
||||
text_elements: Vec::new(),
|
||||
local_images: Vec::new(),
|
||||
}),
|
||||
EventMsg::CollabAgentInteractionBegin(
|
||||
codex_protocol::protocol::CollabAgentInteractionBeginEvent {
|
||||
call_id: "send-1".into(),
|
||||
sender_thread_id: sender,
|
||||
receiver_thread_id: receiver,
|
||||
prompt: "new task".into(),
|
||||
},
|
||||
),
|
||||
EventMsg::CollabAgentInteractionEnd(
|
||||
codex_protocol::protocol::CollabAgentInteractionEndEvent {
|
||||
call_id: "send-1".into(),
|
||||
sender_thread_id: sender,
|
||||
receiver_thread_id: receiver,
|
||||
receiver_agent_nickname: None,
|
||||
receiver_agent_role: None,
|
||||
prompt: "new task".into(),
|
||||
status: AgentStatus::Interrupted,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].items.len(), 2);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
id: "send-1".into(),
|
||||
tool: CollabAgentTool::SendInput,
|
||||
status: CollabAgentToolCallStatus::Completed,
|
||||
sender_thread_id: sender.to_string(),
|
||||
receiver_thread_ids: vec![receiver.to_string()],
|
||||
prompt: Some("new task".into()),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
agents_states: [(
|
||||
receiver.to_string(),
|
||||
CollabAgentState {
|
||||
status: crate::protocol::v2::CollabAgentStatus::Interrupted,
|
||||
message: None,
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_failed_error_does_not_mark_turn_failed() {
|
||||
let events = vec![
|
||||
|
|
|
|||
|
|
@ -4542,6 +4542,7 @@ pub enum CollabAgentToolCallStatus {
|
|||
pub enum CollabAgentStatus {
|
||||
PendingInit,
|
||||
Running,
|
||||
Interrupted,
|
||||
Completed,
|
||||
Errored,
|
||||
Shutdown,
|
||||
|
|
@ -4567,6 +4568,10 @@ impl From<CoreAgentStatus> for CollabAgentState {
|
|||
status: CollabAgentStatus::Running,
|
||||
message: None,
|
||||
},
|
||||
CoreAgentStatus::Interrupted => Self {
|
||||
status: CollabAgentStatus::Interrupted,
|
||||
message: None,
|
||||
},
|
||||
CoreAgentStatus::Completed(message) => Self {
|
||||
status: CollabAgentStatus::Completed,
|
||||
message,
|
||||
|
|
@ -5886,6 +5891,17 @@ mod tests {
|
|||
absolute_path("readable")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_agent_state_maps_interrupted_status() {
|
||||
assert_eq!(
|
||||
CollabAgentState::from(CoreAgentStatus::Interrupted),
|
||||
CollabAgentState {
|
||||
status: CollabAgentStatus::Interrupted,
|
||||
message: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_request_approval_rejects_relative_additional_permission_paths() {
|
||||
let err = serde_json::from_value::<CommandExecutionRequestApprovalParams>(json!({
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ async fn on_event_updates_status_from_turn_aborted() {
|
|||
reason: TurnAbortReason::Interrupted,
|
||||
}));
|
||||
|
||||
let expected = AgentStatus::Errored("Interrupted".to_string());
|
||||
let expected = AgentStatus::Interrupted;
|
||||
assert_eq!(status, Some(expected));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ pub(crate) fn agent_status_from_event(msg: &EventMsg) -> Option<AgentStatus> {
|
|||
match msg {
|
||||
EventMsg::TurnStarted(_) => Some(AgentStatus::Running),
|
||||
EventMsg::TurnComplete(ev) => Some(AgentStatus::Completed(ev.last_agent_message.clone())),
|
||||
EventMsg::TurnAborted(ev) => Some(AgentStatus::Errored(format!("{:?}", ev.reason))),
|
||||
EventMsg::TurnAborted(ev) => match ev.reason {
|
||||
codex_protocol::protocol::TurnAbortReason::Interrupted => {
|
||||
Some(AgentStatus::Interrupted)
|
||||
}
|
||||
_ => Some(AgentStatus::Errored(format!("{:?}", ev.reason))),
|
||||
},
|
||||
EventMsg::Error(ev) => Some(AgentStatus::Errored(ev.message.clone())),
|
||||
EventMsg::ShutdownComplete => Some(AgentStatus::Shutdown),
|
||||
_ => None,
|
||||
|
|
@ -15,5 +20,8 @@ pub(crate) fn agent_status_from_event(msg: &EventMsg) -> Option<AgentStatus> {
|
|||
}
|
||||
|
||||
pub(crate) fn is_final(status: &AgentStatus) -> bool {
|
||||
!matches!(status, AgentStatus::PendingInit | AgentStatus::Running)
|
||||
!matches!(
|
||||
status,
|
||||
AgentStatus::PendingInit | AgentStatus::Running | AgentStatus::Interrupted
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1260,6 +1260,7 @@ fn format_collab_status(status: &AgentStatus) -> String {
|
|||
match status {
|
||||
AgentStatus::PendingInit => "pending init".to_string(),
|
||||
AgentStatus::Running => "running".to_string(),
|
||||
AgentStatus::Interrupted => "interrupted".to_string(),
|
||||
AgentStatus::Completed(Some(message)) => {
|
||||
let preview = truncate_preview(message.trim(), 120);
|
||||
if preview.is_empty() {
|
||||
|
|
@ -1289,6 +1290,7 @@ fn style_for_agent_status(
|
|||
match status {
|
||||
AgentStatus::PendingInit | AgentStatus::Shutdown => processor.dimmed,
|
||||
AgentStatus::Running => processor.cyan,
|
||||
AgentStatus::Interrupted => processor.yellow,
|
||||
AgentStatus::Completed(_) => processor.green,
|
||||
AgentStatus::Errored(_) | AgentStatus::NotFound => processor.red,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -815,6 +815,10 @@ impl From<CoreAgentStatus> for CollabAgentState {
|
|||
status: CollabAgentStatus::Running,
|
||||
message: None,
|
||||
},
|
||||
CoreAgentStatus::Interrupted => Self {
|
||||
status: CollabAgentStatus::Interrupted,
|
||||
message: None,
|
||||
},
|
||||
CoreAgentStatus::Completed(message) => Self {
|
||||
status: CollabAgentStatus::Completed,
|
||||
message,
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ pub enum CollabTool {
|
|||
pub enum CollabAgentStatus {
|
||||
PendingInit,
|
||||
Running,
|
||||
Interrupted,
|
||||
Completed,
|
||||
Errored,
|
||||
Shutdown,
|
||||
|
|
|
|||
|
|
@ -1518,6 +1518,8 @@ pub enum AgentStatus {
|
|||
PendingInit,
|
||||
/// Agent is currently running.
|
||||
Running,
|
||||
/// Agent's current turn was interrupted and it may receive more input.
|
||||
Interrupted,
|
||||
/// Agent is done. Contains the final assistant message.
|
||||
Completed(Option<String>),
|
||||
/// Agent encountered an error.
|
||||
|
|
|
|||
|
|
@ -537,10 +537,13 @@ fn status_summary_line(status: &AgentStatus) -> Line<'static> {
|
|||
status_summary_spans(status).into()
|
||||
}
|
||||
|
||||
// Allow `.yellow()`
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
fn status_summary_spans(status: &AgentStatus) -> Vec<Span<'static>> {
|
||||
match status {
|
||||
AgentStatus::PendingInit => vec![Span::from("Pending init").cyan()],
|
||||
AgentStatus::Running => vec![Span::from("Running").cyan().bold()],
|
||||
AgentStatus::Interrupted => vec![Span::from("Interrupted").yellow()],
|
||||
AgentStatus::Completed(message) => {
|
||||
let mut spans = vec![Span::from("Completed").green()];
|
||||
if let Some(message) = message.as_ref() {
|
||||
|
|
@ -762,6 +765,25 @@ mod tests {
|
|||
assert_eq!(title.spans[6].style.fg, Some(Color::Magenta));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collab_resume_interrupted_snapshot() {
|
||||
let sender_thread_id = ThreadId::from_string("00000000-0000-0000-0000-000000000001")
|
||||
.expect("valid sender thread id");
|
||||
let robie_id = ThreadId::from_string("00000000-0000-0000-0000-000000000002")
|
||||
.expect("valid robie thread id");
|
||||
|
||||
let cell = resume_end(CollabResumeEndEvent {
|
||||
call_id: "call-resume".to_string(),
|
||||
sender_thread_id,
|
||||
receiver_thread_id: robie_id,
|
||||
receiver_agent_nickname: Some("Robie".to_string()),
|
||||
receiver_agent_role: Some("explorer".to_string()),
|
||||
status: AgentStatus::Interrupted,
|
||||
});
|
||||
|
||||
assert_snapshot!("collab_resume_interrupted", cell_to_text(&cell));
|
||||
}
|
||||
|
||||
fn cell_to_text(cell: &PlainHistoryCell) -> String {
|
||||
cell.display_lines(200)
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
source: tui/src/multi_agents.rs
|
||||
expression: cell_to_text(&cell)
|
||||
---
|
||||
• Resumed Robie [explorer]
|
||||
└ Interrupted
|
||||
Loading…
Add table
Reference in a new issue