Lots of changes... not done yet.

This commit is contained in:
Ryan Ward
2026-08-17 18:19:37 -05:00
parent b5f3290d1a
commit 56e3b51d48
55 changed files with 4494 additions and 1098 deletions
@@ -94,6 +94,25 @@ impl TryFrom<RequestCommandOutputResult> for api::request::input::tool_call_resu
},
),
),
RequestCommandOutputResult::ExecutionError { command, message } => Ok(
api::request::input::tool_call_result::Result::RunShellCommand(
#[allow(deprecated)]
api::RunShellCommandResult {
command,
output: Default::default(),
exit_code: Default::default(),
result: Some(api::run_shell_command_result::Result::CommandFinished(
api::ShellCommandFinished {
command_id: String::new(),
output: format!("Command was not executed: {message}"),
exit_code: 1,
start_ts: None,
finish_ts: None,
},
)),
},
),
),
RequestCommandOutputResult::Denylisted { command } =>
{
#[allow(deprecated)]
@@ -1551,6 +1570,14 @@ impl From<RunAgentsAgentOutcome> for api::run_agents_result::AgentOutcome {
api::run_agents_result::LaunchedAgent { agent_id },
)
}
// The legacy wire schema has no completed-child shape. Preserve the child identity;
// direct-provider history retains the richer local result and output.
RunAgentsAgentOutcomeKind::Completed {
agent_id,
output: _,
} => api::run_agents_result::agent_outcome::Result::Launched(
api::run_agents_result::LaunchedAgent { agent_id },
),
RunAgentsAgentOutcomeKind::Failed { error } => {
api::run_agents_result::agent_outcome::Result::Failed(
api::run_agents_result::FailedAgent { error },
@@ -28,3 +28,22 @@ fn ask_user_question_skipped_by_auto_approve_converts_to_skipped_answers() {
Some(AskUserQuestionAnswer::Skipped(()))
));
}
#[test]
fn completed_run_agents_child_converts_to_legacy_launched_wire_outcome() {
let outcome = RunAgentsAgentOutcome {
name: "research".to_string(),
kind: RunAgentsAgentOutcomeKind::Completed {
agent_id: "child-1".to_string(),
output: "local output".to_string(),
},
};
let converted = api::run_agents_result::AgentOutcome::from(outcome);
assert!(matches!(
converted.result,
Some(api::run_agents_result::agent_outcome::Result::Launched(
api::run_agents_result::LaunchedAgent { agent_id }
)) if agent_id == "child-1"
));
}
+54 -7
View File
@@ -165,6 +165,7 @@ impl AIAgentActionResultType {
None,
),
RequestCommandOutputResult::CancelledBeforeExecution
| RequestCommandOutputResult::ExecutionError { .. }
| RequestCommandOutputResult::Denylisted { .. } => result.to_string(),
},
Self::WriteToLongRunningShellCommand(result) => match result {
@@ -418,6 +419,8 @@ pub enum RequestCommandOutputResult {
/// A running command canceled via ctrl-c
/// would have Completed result with exit code 130.
CancelledBeforeExecution,
/// The command could not start because the terminal was unavailable for execution.
ExecutionError { command: String, message: String },
/// The command was denied because it was present on the denylist.
Denylisted { command: String },
}
@@ -427,14 +430,16 @@ impl RequestCommandOutputResult {
match self {
Self::Completed { exit_code, .. } => exit_code.was_successful(),
Self::LongRunningCommandSnapshot { .. } => true,
Self::CancelledBeforeExecution | Self::Denylisted { .. } => false,
Self::CancelledBeforeExecution
| Self::ExecutionError { .. }
| Self::Denylisted { .. } => false,
}
}
pub fn failed(&self) -> bool {
match self {
Self::Completed { exit_code, .. } => !exit_code.was_successful(),
Self::Denylisted { .. } => true,
Self::ExecutionError { .. } | Self::Denylisted { .. } => true,
Self::CancelledBeforeExecution | Self::LongRunningCommandSnapshot { .. } => false,
}
}
@@ -444,6 +449,7 @@ impl RequestCommandOutputResult {
match self {
Self::Completed { command, .. }
| Self::LongRunningCommandSnapshot { command, .. }
| Self::ExecutionError { command, .. }
| Self::Denylisted { command } => command.clone(),
Self::CancelledBeforeExecution => "cancelled".to_string(),
}
@@ -473,6 +479,9 @@ impl Display for RequestCommandOutputResult {
RequestCommandOutputResult::CancelledBeforeExecution => {
write!(f, "Command output cancelled")
}
RequestCommandOutputResult::ExecutionError { command, message } => {
write!(f, "Command '{command}' could not be executed: {message}")
}
RequestCommandOutputResult::Denylisted { .. } => {
write!(f, "Command output was on denylist")
}
@@ -1042,7 +1051,9 @@ impl AIAgentActionResultType {
| TransferShellCommandControlToUserResult::CommandFinished { .. },
) => true,
Self::AskUserQuestion(AskUserQuestionResult::Success { .. }) => true,
Self::RunAgents(RunAgentsResult::Launched { .. }) => true,
Self::RunAgents(RunAgentsResult::Launched { agents, .. }) => agents
.iter()
.any(|agent| !matches!(agent.kind, RunAgentsAgentOutcomeKind::Failed { .. })),
Self::WaitForEvents(WaitForEventsResult::Completed) => true,
_ => false,
}
@@ -1076,6 +1087,12 @@ impl AIAgentActionResultType {
| Self::RunAgents(RunAgentsResult::Failure { .. } | RunAgentsResult::Denied { .. }) => {
true
}
Self::RunAgents(RunAgentsResult::Launched { agents, .. }) => {
!agents.is_empty()
&& agents
.iter()
.all(|agent| matches!(agent.kind, RunAgentsAgentOutcomeKind::Failed { .. }))
}
_ => false,
}
}
@@ -1627,6 +1644,7 @@ pub struct RunAgentsAgentOutcome {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunAgentsAgentOutcomeKind {
Launched { agent_id: String },
Completed { agent_id: String, output: String },
Failed { error: String },
}
@@ -1654,6 +1672,17 @@ impl RunAgentsResult {
"computer_use_enabled": computer_use_enabled,
}),
};
let children_completed = agents.iter().all(|agent| {
matches!(
agent.kind,
RunAgentsAgentOutcomeKind::Completed { .. }
| RunAgentsAgentOutcomeKind::Failed { .. }
)
});
let all_failed = !agents.is_empty()
&& agents.iter().all(|agent| {
matches!(agent.kind, RunAgentsAgentOutcomeKind::Failed { .. })
});
let agents = agents
.iter()
.map(|agent| match &agent.kind {
@@ -1662,6 +1691,14 @@ impl RunAgentsResult {
"status": "launched",
"agent_id": agent_id,
}),
RunAgentsAgentOutcomeKind::Completed { agent_id, output } => {
serde_json::json!({
"name": agent.name,
"status": "completed",
"agent_id": agent_id,
"output": output,
})
}
RunAgentsAgentOutcomeKind::Failed { error } => serde_json::json!({
"name": agent.name,
"status": "failed",
@@ -1670,9 +1707,13 @@ impl RunAgentsResult {
})
.collect::<Vec<_>>();
serde_json::json!({
"status": "launched",
"completion_state": "children_running",
"instruction": "Child agents have only been launched, not completed. Wait for child-agent updates before repeating this work or reporting final results.",
"status": if all_failed { "failure" } else { "launched" },
"completion_state": if children_completed { "children_completed" } else { "children_running" },
"instruction": if children_completed {
"Child agents reached terminal states. Use their structured outputs and errors to complete the task."
} else {
"Child agents have only been launched, not completed. Wait for child-agent updates before repeating this work or reporting final results."
},
"model_id": model_id,
"harness_type": harness_type,
"execution_mode": execution_mode,
@@ -1701,7 +1742,13 @@ impl Display for RunAgentsResult {
RunAgentsResult::Launched { agents, .. } => {
let launched = agents
.iter()
.filter(|a| matches!(a.kind, RunAgentsAgentOutcomeKind::Launched { .. }))
.filter(|a| {
matches!(
a.kind,
RunAgentsAgentOutcomeKind::Launched { .. }
| RunAgentsAgentOutcomeKind::Completed { .. }
)
})
.count();
write!(
f,
+85 -2
View File
@@ -1,8 +1,22 @@
use super::{
AIAgentActionResultType, RunAgentsAgentOutcome, RunAgentsAgentOutcomeKind,
RunAgentsLaunchedExecutionMode, RunAgentsResult, StartAgentResult, StartAgentVersion,
AIAgentActionResultType, RequestCommandOutputResult, RunAgentsAgentOutcome,
RunAgentsAgentOutcomeKind, RunAgentsLaunchedExecutionMode, RunAgentsResult, StartAgentResult,
StartAgentVersion,
};
#[test]
fn shell_execution_error_is_failed_but_not_cancelled() {
let result =
AIAgentActionResultType::RequestCommandOutput(RequestCommandOutputResult::ExecutionError {
command: "cargo test".to_string(),
message: "terminal is busy".to_string(),
});
assert!(result.is_failed());
assert!(!result.is_cancelled());
assert!(result.model_content().contains("terminal is busy"));
}
#[test]
fn deserializes_legacy_start_agent_success_without_version_as_v1() {
let result: StartAgentResult =
@@ -134,3 +148,72 @@ fn run_agents_model_content_serializes_terminal_non_launch_outcomes() {
assert_eq!(content, expected);
}
}
#[test]
fn completed_local_run_agents_preserves_outputs_and_terminal_state() {
let result = AIAgentActionResultType::RunAgents(RunAgentsResult::Launched {
model_id: "model".to_string(),
harness_type: "codex".to_string(),
execution_mode: RunAgentsLaunchedExecutionMode::Local,
agents: vec![RunAgentsAgentOutcome {
name: "research".to_string(),
kind: RunAgentsAgentOutcomeKind::Completed {
agent_id: "child-1".to_string(),
output: "Found the root cause".to_string(),
},
}],
});
let content: serde_json::Value = serde_json::from_str(&result.model_content()).unwrap();
assert_eq!(content["status"], "launched");
assert_eq!(content["completion_state"], "children_completed");
assert_eq!(content["agents"][0]["status"], "completed");
assert_eq!(content["agents"][0]["output"], "Found the root cause");
assert!(result.is_successful());
assert!(!result.is_failed());
}
#[test]
fn all_failed_run_agents_is_failure_but_mixed_batch_is_successful() {
let failed = |name: &str, error: &str| RunAgentsAgentOutcome {
name: name.to_string(),
kind: RunAgentsAgentOutcomeKind::Failed {
error: error.to_string(),
},
};
let result = AIAgentActionResultType::RunAgents(RunAgentsResult::Launched {
model_id: "model".to_string(),
harness_type: "codex".to_string(),
execution_mode: RunAgentsLaunchedExecutionMode::Local,
agents: vec![failed("one", "first error"), failed("two", "second error")],
});
let content: serde_json::Value = serde_json::from_str(&result.model_content()).unwrap();
assert_eq!(content["status"], "failure");
assert_eq!(content["agents"][0]["error"], "first error");
assert_eq!(content["agents"][1]["error"], "second error");
assert!(result.is_failed());
assert!(!result.is_successful());
let mixed = AIAgentActionResultType::RunAgents(RunAgentsResult::Launched {
model_id: "model".to_string(),
harness_type: "codex".to_string(),
execution_mode: RunAgentsLaunchedExecutionMode::Local,
agents: vec![
failed("one", "first error"),
RunAgentsAgentOutcome {
name: "two".to_string(),
kind: RunAgentsAgentOutcomeKind::Completed {
agent_id: "child-2".to_string(),
output: "useful output".to_string(),
},
},
],
});
let mixed_content: serde_json::Value = serde_json::from_str(&mixed.model_content()).unwrap();
assert_eq!(mixed_content["status"], "launched");
assert_eq!(mixed_content["agents"][0]["error"], "first error");
assert_eq!(mixed_content["agents"][1]["output"], "useful output");
assert!(mixed.is_successful());
assert!(!mixed.is_failed());
}
+5 -1
View File
@@ -820,7 +820,8 @@ impl ProviderRun {
call_id: call.call.id.clone(),
})
}
PendingToolCallState::PermissionPending { .. } | PendingToolCallState::Executing => {
PendingToolCallState::Executing => Ok(()),
PendingToolCallState::PermissionPending { .. } => {
Err(invalid_tool_transition(call, "tool start"))
}
}
@@ -840,6 +841,9 @@ impl ProviderRun {
call.state = PendingToolCallState::Resolved { result };
Ok(())
}
PendingToolCallState::Resolved {
result: completed_result,
} if completed_result == &result => Ok(()),
PendingToolCallState::Resolved { .. } => {
Err(ProviderRunProtocolError::DuplicateToolUpdate {
call_id: call.call.id.clone(),
@@ -358,6 +358,89 @@ fn parallel_tool_results_commit_atomically_in_original_call_order() {
assert_eq!(ids, vec!["first", "second"]);
}
#[test]
fn duplicate_tool_start_is_idempotent() {
let mut run = run();
let batch = accept_tool_turn(
&mut run,
tool_turn(vec![tool_call("read", "read_files")], &["read_files"]),
);
run.start_tool(&batch.work_id, "read").unwrap();
let started = run.clone();
run.start_tool(&batch.work_id, "read").unwrap();
assert_eq!(run, started);
}
#[test]
fn identical_tool_completion_is_idempotent() {
let mut run = run();
let batch = accept_tool_turn(
&mut run,
tool_turn(vec![tool_call("read", "read_files")], &["read_files"]),
);
let result = successful_result("read", "contents");
run.complete_tool(&batch.work_id, result.clone()).unwrap();
let completed = run.clone();
run.complete_tool(&batch.work_id, result).unwrap();
assert_eq!(run, completed);
}
#[test]
fn conflicting_tool_completion_is_rejected_without_mutation() {
let mut run = run();
let batch = accept_tool_turn(
&mut run,
tool_turn(vec![tool_call("read", "read_files")], &["read_files"]),
);
run.complete_tool(&batch.work_id, successful_result("read", "contents"))
.unwrap();
let completed = run.clone();
assert_eq!(
run.complete_tool(&batch.work_id, successful_result("read", "different"))
.unwrap_err(),
ProviderRunProtocolError::DuplicateToolUpdate {
call_id: "read".to_string(),
}
);
assert_eq!(run, completed);
}
#[test]
fn duplicate_tool_callbacks_do_not_prevent_eventual_batch_completion() {
let mut run = run();
let batch = accept_tool_turn(
&mut run,
tool_turn(
vec![
tool_call("first", "read_files"),
tool_call("second", "grep"),
],
&["read_files", "grep"],
),
);
let first_result = successful_result("first", "one");
run.start_tool(&batch.work_id, "first").unwrap();
run.start_tool(&batch.work_id, "first").unwrap();
run.complete_tool(&batch.work_id, first_result.clone())
.unwrap();
run.complete_tool(&batch.work_id, first_result).unwrap();
run.complete_tool(&batch.work_id, successful_result("second", "two"))
.unwrap();
let ProviderRunState::AwaitingTools { batch: completed } = run.state() else {
panic!("expected completed tool batch");
};
assert!(completed.is_complete());
run.commit_tool_batch(&batch.work_id).unwrap();
assert_eq!(run.state().phase(), ProviderRunPhase::ReadyToCallModel);
}
#[test]
fn exact_batch_submission_rejects_missing_duplicate_and_unknown_results_without_mutation() {
let mut run = run();