Files
galaxy/crates/ai/src/agent/action_result/mod.rs
T

1804 lines
65 KiB
Rust

mod convert;
use std::fmt::Display;
use std::ops::Range;
use std::time::SystemTime;
use chrono::{DateTime, Local};
use galaxy_core::command::ExitCode;
use galaxy_terminal::model::BlockId;
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use warp_multi_agent_api::apply_file_diffs_result::success::UpdatedFileContent;
use crate::agent::FileLocations;
use crate::document::{AIDocumentId, AIDocumentVersion};
#[derive(Debug, Clone, PartialEq)]
pub enum AIAgentActionResultType {
/// The output of a requested command.
RequestCommandOutput(RequestCommandOutputResult),
/// The result of sending some input to a long-running command.
WriteToLongRunningShellCommand(WriteToLongRunningShellCommandResult),
/// The output of a requested file edits.
RequestFileEdits(RequestFileEditsResult),
/// The output of a read files action.
ReadFiles(ReadFilesResult),
/// The output of an upload artifact action.
UploadArtifact(UploadArtifactResult),
/// The output of a search codebase action.
SearchCodebase(SearchCodebaseResult),
/// The output of a grep action.
Grep(GrepResult),
/// The output of a file glob action.
FileGlob(FileGlobResult),
/// The output of a file glob V2 action.
FileGlobV2(FileGlobV2Result),
/// The output of reading an MCP resource.
ReadMCPResource(ReadMCPResourceResult),
/// The output of calling an MCP tool.
CallMCPTool(CallMCPToolResult),
/// The output of reading a skill.
ReadSkill(ReadSkillResult),
/// The output of suggesting a new conversation.
SuggestNewConversation(SuggestNewConversationResult),
/// The result of suggesting a prompt.
SuggestPrompt(SuggestPromptResult),
OpenCodeReview,
InitProject,
/// The output of a read documents action.
ReadDocuments(ReadDocumentsResult),
/// The output of an edit documents action.
EditDocuments(EditDocumentsResult),
/// The output of a create documents action.
CreateDocuments(CreateDocumentsResult),
/// The output of reading shell command output.
ReadShellCommandOutput(ReadShellCommandOutputResult),
/// The output of using computer.
UseComputer(UseComputerResult),
/// The result of inserting code review comments.
InsertReviewComments(InsertReviewCommentsResult),
/// The output of requesting computer use.
RequestComputerUse(RequestComputerUseResult),
/// The result of fetching a conversation's tasks.
FetchConversation(FetchConversationResult),
/// The result of starting a child agent.
StartAgent(StartAgentResult),
/// The result of sending a message to another agent.
SendMessageToAgent(SendMessageToAgentResult),
/// The output of transferring shell command control to the user.
TransferShellCommandControlToUser(TransferShellCommandControlToUserResult),
/// The result of asking the user a question.
AskUserQuestion(AskUserQuestionResult),
/// The result of an orchestrate tool call: launched (with per-agent
/// outcomes), launch denied (Stage 2), failure, or cancelled.
RunAgents(RunAgentsResult),
/// Result of the client-side wait_for_events watchdog or inbound
/// resume.
WaitForEvents(WaitForEventsResult),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum StartAgentVersion {
#[default]
V1,
V2,
}
impl AIAgentActionResultType {
/// Returns the effective command string for command-related results, if any.
///
/// This is used by UIs (e.g. requested command views) that want to display the
/// final executed command rather than the original suggestion.
pub fn command_str(&self) -> Option<&str> {
match self {
AIAgentActionResultType::RequestCommandOutput(
RequestCommandOutputResult::Completed { command, .. },
)
| AIAgentActionResultType::RequestCommandOutput(
RequestCommandOutputResult::LongRunningCommandSnapshot { command, .. },
)
| AIAgentActionResultType::ReadShellCommandOutput(
ReadShellCommandOutputResult::CommandFinished { command, .. },
)
| AIAgentActionResultType::ReadShellCommandOutput(
ReadShellCommandOutputResult::LongRunningCommandSnapshot { command, .. },
) => Some(command.as_str()),
_ => None,
}
}
/// Returns the authoritative result content to send back to a model.
///
/// `Display` is intentionally concise for UI summaries, so content-bearing
/// results must not use it directly when constructing the next model turn.
pub fn model_content(&self) -> String {
match self {
Self::RequestCommandOutput(result) => match result {
RequestCommandOutputResult::Completed {
command,
output,
exit_code,
..
} => command_result_content(Some(command), output, exit_code.value()),
RequestCommandOutputResult::LongRunningCommandSnapshot {
block_id,
command,
grid_contents,
cursor,
is_alt_screen_active,
..
} => shell_snapshot_content(
block_id,
Some(command),
grid_contents,
cursor,
*is_alt_screen_active,
None,
),
RequestCommandOutputResult::CancelledBeforeExecution
| RequestCommandOutputResult::Denylisted { .. } => result.to_string(),
},
Self::WriteToLongRunningShellCommand(result) => match result {
WriteToLongRunningShellCommandResult::Snapshot {
block_id,
grid_contents,
cursor,
is_alt_screen_active,
is_preempted,
..
} => shell_snapshot_content(
block_id,
None,
grid_contents,
cursor,
*is_alt_screen_active,
Some(*is_preempted),
),
WriteToLongRunningShellCommandResult::CommandFinished {
output, exit_code, ..
} => command_result_content(None, output, exit_code.value()),
WriteToLongRunningShellCommandResult::Cancelled
| WriteToLongRunningShellCommandResult::Error(_) => result.to_string(),
},
Self::ReadFiles(result) => match result {
ReadFilesResult::Success { files } => file_contexts_content(files),
ReadFilesResult::Error(_) | ReadFilesResult::Cancelled => result.to_string(),
},
Self::SearchCodebase(result) => match result {
SearchCodebaseResult::Success { files } => file_contexts_content(files),
SearchCodebaseResult::Failed { .. } | SearchCodebaseResult::Cancelled => {
result.to_string()
}
},
Self::ReadSkill(result) => match result {
ReadSkillResult::Success { content } => file_context_content(content),
ReadSkillResult::Error(_) | ReadSkillResult::Cancelled => result.to_string(),
},
Self::ReadDocuments(result) => match result {
ReadDocumentsResult::Success { documents } => document_contexts_content(documents),
ReadDocumentsResult::Error(_) | ReadDocumentsResult::Cancelled => {
result.to_string()
}
},
Self::EditDocuments(result) => match result {
EditDocumentsResult::Success { updated_documents } => {
document_contexts_content(updated_documents)
}
EditDocumentsResult::Error(_) | EditDocumentsResult::Cancelled => {
result.to_string()
}
},
Self::CreateDocuments(result) => match result {
CreateDocumentsResult::Success { created_documents } => {
document_contexts_content(created_documents)
}
CreateDocumentsResult::Error(_) | CreateDocumentsResult::Cancelled => {
result.to_string()
}
},
Self::ReadShellCommandOutput(result) => match result {
ReadShellCommandOutputResult::CommandFinished {
command,
output,
exit_code,
..
} => command_result_content(Some(command), output, exit_code.value()),
ReadShellCommandOutputResult::LongRunningCommandSnapshot {
block_id,
command,
grid_contents,
cursor,
is_alt_screen_active,
is_preempted,
..
} => shell_snapshot_content(
block_id,
Some(command),
grid_contents,
cursor,
*is_alt_screen_active,
Some(*is_preempted),
),
ReadShellCommandOutputResult::Cancelled
| ReadShellCommandOutputResult::Error(_) => result.to_string(),
},
Self::TransferShellCommandControlToUser(result) => match result {
TransferShellCommandControlToUserResult::Snapshot {
block_id,
grid_contents,
cursor,
is_alt_screen_active,
is_preempted,
..
} => format!(
"{}\nControl has been transferred to the user. Do not write to the command until control is returned.",
shell_snapshot_content(
block_id,
None,
grid_contents,
cursor,
*is_alt_screen_active,
Some(*is_preempted),
)
),
TransferShellCommandControlToUserResult::CommandFinished {
output, exit_code, ..
} => command_result_content(None, output, exit_code.value()),
TransferShellCommandControlToUserResult::Cancelled
| TransferShellCommandControlToUserResult::Error(_) => result.to_string(),
},
Self::RequestFileEdits(_)
| Self::UploadArtifact(_)
| Self::Grep(_)
| Self::FileGlob(_)
| Self::FileGlobV2(_)
| Self::ReadMCPResource(_)
| Self::CallMCPTool(_)
| Self::SuggestNewConversation(_)
| Self::SuggestPrompt(_)
| Self::OpenCodeReview
| Self::InitProject
| Self::UseComputer(_)
| Self::InsertReviewComments(_)
| Self::RequestComputerUse(_)
| Self::FetchConversation(_)
| Self::StartAgent(_)
| Self::SendMessageToAgent(_)
| Self::AskUserQuestion(_)
| Self::RunAgents(_)
| Self::WaitForEvents(_) => self.to_string(),
}
}
}
fn command_result_content(command: Option<&str>, output: &str, exit_code: i32) -> String {
let command = command
.map(|command| format!("Command: {command}\n"))
.unwrap_or_default();
let output = if output.is_empty() {
"(no output)"
} else {
output
};
format!("{command}Command finished with exit code {exit_code}.\nOutput:\n{output}")
}
fn shell_snapshot_content(
block_id: &BlockId,
command: Option<&str>,
grid_contents: &str,
cursor: &str,
is_alt_screen_active: bool,
is_preempted: Option<bool>,
) -> String {
let command = command
.map(|command| format!("Command: {command}\n"))
.unwrap_or_default();
let preempted = is_preempted
.map(|is_preempted| format!("\nPreempted: {is_preempted}"))
.unwrap_or_default();
format!(
"{command}Command is still running.\nCommand ID: {block_id}\nCurrent output:\n{grid_contents}\nCursor: {cursor}\nAlt screen active: {is_alt_screen_active}{preempted}\nContinue monitoring with `read_shell_command_output` using command_id `{block_id}` and a short wait. Do not report completion while the command is still running."
)
}
fn file_contexts_content(files: &[FileContext]) -> String {
files
.iter()
.map(file_context_content)
.collect::<Vec<_>>()
.join("\n\n")
}
fn file_context_content(file: &FileContext) -> String {
match &file.content {
AnyFileContent::StringContent(content) => format!("{file}:\n{content}"),
AnyFileContent::BinaryContent(content) => {
format!("{file}:\n[binary file, {} bytes]", content.len())
}
}
}
fn document_contexts_content(documents: &[DocumentContext]) -> String {
documents
.iter()
.map(|document| format!("{document}:\n{}", document.content))
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;
impl Display for AIAgentActionResultType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AIAgentActionResultType::RequestCommandOutput(result) => result.fmt(f),
AIAgentActionResultType::WriteToLongRunningShellCommand(result) => result.fmt(f),
AIAgentActionResultType::RequestFileEdits(result) => result.fmt(f),
AIAgentActionResultType::ReadFiles(result) => result.fmt(f),
AIAgentActionResultType::UploadArtifact(result) => result.fmt(f),
AIAgentActionResultType::SearchCodebase(result) => result.fmt(f),
AIAgentActionResultType::Grep(result) => result.fmt(f),
AIAgentActionResultType::FileGlob(result) => result.fmt(f),
AIAgentActionResultType::FileGlobV2(result) => result.fmt(f),
AIAgentActionResultType::ReadMCPResource(result) => result.fmt(f),
AIAgentActionResultType::CallMCPTool(result) => result.fmt(f),
AIAgentActionResultType::ReadSkill(result) => result.fmt(f),
AIAgentActionResultType::SuggestNewConversation(result) => result.fmt(f),
AIAgentActionResultType::SuggestPrompt(result) => result.fmt(f),
AIAgentActionResultType::ReadDocuments(result) => result.fmt(f),
AIAgentActionResultType::EditDocuments(result) => result.fmt(f),
AIAgentActionResultType::CreateDocuments(result) => result.fmt(f),
AIAgentActionResultType::ReadShellCommandOutput(result) => result.fmt(f),
AIAgentActionResultType::UseComputer(result) => result.fmt(f),
AIAgentActionResultType::InsertReviewComments(result) => result.fmt(f),
AIAgentActionResultType::RequestComputerUse(result) => result.fmt(f),
AIAgentActionResultType::FetchConversation(result) => result.fmt(f),
AIAgentActionResultType::StartAgent(result) => result.fmt(f),
AIAgentActionResultType::SendMessageToAgent(result) => result.fmt(f),
AIAgentActionResultType::TransferShellCommandControlToUser(result) => result.fmt(f),
AIAgentActionResultType::AskUserQuestion(result) => result.fmt(f),
AIAgentActionResultType::RunAgents(result) => result.fmt(f),
AIAgentActionResultType::WaitForEvents(result) => result.fmt(f),
AIAgentActionResultType::OpenCodeReview | AIAgentActionResultType::InitProject => {
Ok(())
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum RequestCommandOutputResult {
Completed {
block_id: BlockId,
command: String,
output: String,
exit_code: ExitCode,
start_ts: Option<DateTime<Local>>,
completed_ts: Option<DateTime<Local>>,
},
LongRunningCommandSnapshot {
block_id: BlockId,
command: String,
grid_contents: String,
cursor: String,
is_alt_screen_active: bool,
},
/// A running command canceled via ctrl-c
/// would have Completed result with exit code 130.
CancelledBeforeExecution,
/// The command was denied because it was present on the denylist.
Denylisted { command: String },
}
impl RequestCommandOutputResult {
pub fn is_successful(&self) -> bool {
match self {
Self::Completed { exit_code, .. } => exit_code.was_successful(),
Self::LongRunningCommandSnapshot { .. } => true,
Self::CancelledBeforeExecution | Self::Denylisted { .. } => false,
}
}
pub fn failed(&self) -> bool {
match self {
Self::Completed { exit_code, .. } => !exit_code.was_successful(),
Self::Denylisted { .. } => true,
Self::CancelledBeforeExecution | Self::LongRunningCommandSnapshot { .. } => false,
}
}
/// Returns a stable identity string for loop detection (command input only, no output).
pub fn loop_description(&self) -> String {
match self {
Self::Completed { command, .. }
| Self::LongRunningCommandSnapshot { command, .. }
| Self::Denylisted { command } => command.clone(),
Self::CancelledBeforeExecution => "cancelled".to_string(),
}
}
}
impl Display for RequestCommandOutputResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequestCommandOutputResult::Completed {
command,
output,
exit_code,
..
} => {
write!(
f,
"Command '{}' completed with exit code {}:\n{}",
command,
exit_code.value(),
output
)
}
RequestCommandOutputResult::LongRunningCommandSnapshot { command, .. } => {
write!(f, "Command '{command}' is long-running")
}
RequestCommandOutputResult::CancelledBeforeExecution => {
write!(f, "Command output cancelled")
}
RequestCommandOutputResult::Denylisted { .. } => {
write!(f, "Command output was on denylist")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum ShellCommandError {
BlockNotFound,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum WriteToLongRunningShellCommandResult {
Snapshot {
block_id: BlockId,
grid_contents: String,
cursor: String,
is_alt_screen_active: bool,
is_preempted: bool,
},
CommandFinished {
block_id: BlockId,
output: String,
exit_code: ExitCode,
start_ts: Option<DateTime<Local>>,
completed_ts: Option<DateTime<Local>>,
},
Cancelled,
Error(ShellCommandError),
}
impl Display for WriteToLongRunningShellCommandResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Snapshot { .. } => {
write!(f, "Sent snapshot of long-running shell command to agent")
}
Self::CommandFinished {
output, exit_code, ..
} => write!(
f,
"Long-running shell command finished with exit code {}:\n{output}",
exit_code.value()
),
Self::Cancelled => write!(f, "Writing to long-running shell command cancelled"),
Self::Error(e) => write!(f, "Write to long-running shell command failed: {e:?}"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum SuggestNewConversationResult {
Accepted { message_id: String },
Rejected,
Cancelled,
}
impl Display for SuggestNewConversationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Accepted { message_id } => {
write!(
f,
"Suggest new conversation accepted for message {message_id}"
)
}
Self::Rejected => {
write!(f, "Suggest new conversation rejected for message")
}
Self::Cancelled => write!(f, "Suggest new conversation cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum AnyFileContent {
StringContent(String),
BinaryContent(Vec<u8>),
}
impl AnyFileContent {
pub fn len(&self) -> usize {
match self {
Self::StringContent(content) => content.len(),
Self::BinaryContent(content) => content.len(),
}
}
pub fn line_count(&self) -> usize {
match self {
Self::StringContent(content) => content.lines().count(),
Self::BinaryContent(_) => 0,
}
}
pub fn is_empty(&self) -> bool {
match self {
Self::StringContent(content) => content.is_empty(),
Self::BinaryContent(content) => content.is_empty(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct FileContext {
pub file_name: String,
pub content: AnyFileContent,
pub line_range: Option<Range<usize>>,
pub last_modified: Option<SystemTime>,
pub line_count: usize,
}
impl FileContext {
// create a new FileContext and autocalculate number of lines in the given file
pub fn new(
file_name: String,
content: AnyFileContent,
line_range: Option<Range<usize>>,
last_modified: Option<SystemTime>,
) -> Self {
let string_content = if let AnyFileContent::StringContent(content) = content.clone() {
content
} else {
return Self {
file_name,
content,
line_range,
last_modified,
line_count: 0,
};
};
let line_count = string_content.lines().count();
Self {
file_name,
content: AnyFileContent::StringContent(string_content),
line_range,
last_modified,
line_count,
}
}
}
impl Display for FileContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.line_range {
None => write!(f, "{}", self.file_name),
Some(range) => write!(f, "{} ({}-{})", self.file_name, range.start, range.end),
}
}
}
impl From<&FileContext> for FileLocations {
fn from(context: &FileContext) -> Self {
FileLocations {
name: context.file_name.clone(),
lines: context.line_range.clone().into_iter().collect(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ReadFilesResult {
Success { files: Vec<FileContext> },
Error(String),
Cancelled,
}
impl Display for ReadFilesResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadFilesResult::Success { files } => {
write!(f, "Read files: {}", files.iter().format(", "))
}
ReadFilesResult::Error(error) => write!(f, "Read files error: {error}"),
ReadFilesResult::Cancelled => write!(f, "Read files cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum UploadArtifactResult {
Success {
artifact_uid: String,
filepath: Option<String>,
mime_type: String,
description: Option<String>,
size_bytes: i64,
},
Error(String),
Cancelled,
}
impl Display for UploadArtifactResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UploadArtifactResult::Success {
artifact_uid,
filepath,
..
} => match filepath {
Some(filepath) => write!(f, "Uploaded artifact {artifact_uid} from {filepath}"),
None => write!(f, "Uploaded artifact {artifact_uid}"),
},
UploadArtifactResult::Error(error) => write!(f, "Upload artifact error: {error}"),
UploadArtifactResult::Cancelled => write!(f, "Upload artifact cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DocumentContext {
pub document_id: AIDocumentId,
pub document_version: AIDocumentVersion,
pub content: String,
pub line_ranges: Vec<Range<usize>>,
}
impl Display for DocumentContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.line_ranges.is_empty() {
return write!(f, "Document {}", self.document_id);
}
let line_ranges = self
.line_ranges
.iter()
.map(|range| format!("{}-{}", range.start, range.end))
.collect_vec();
write!(
f,
"Document {} ({})",
self.document_id,
line_ranges.join(", ")
)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ReadDocumentsResult {
Success { documents: Vec<DocumentContext> },
Error(String),
Cancelled,
}
impl Display for ReadDocumentsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadDocumentsResult::Success { documents } => {
write!(f, "Read documents: {}", documents.iter().format(", "))
}
ReadDocumentsResult::Error(error) => write!(f, "Read documents error: {error}"),
ReadDocumentsResult::Cancelled => write!(f, "Read documents cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum EditDocumentsResult {
Success {
updated_documents: Vec<DocumentContext>,
},
Error(String),
Cancelled,
}
impl Display for EditDocumentsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EditDocumentsResult::Success { updated_documents } => {
write!(
f,
"Edited documents: {}",
updated_documents.iter().format(", ")
)
}
EditDocumentsResult::Error(error) => write!(f, "Edit documents error: {error}"),
EditDocumentsResult::Cancelled => write!(f, "Edit documents cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum CreateDocumentsResult {
Success {
created_documents: Vec<DocumentContext>,
},
Error(String),
Cancelled,
}
impl Display for CreateDocumentsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CreateDocumentsResult::Success { created_documents } => {
write!(
f,
"Created documents: {}",
created_documents.iter().format(", ")
)
}
CreateDocumentsResult::Error(error) => write!(f, "Create documents error: {error}"),
CreateDocumentsResult::Cancelled => write!(f, "Create documents cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ReadShellCommandOutputResult {
CommandFinished {
command: String,
block_id: BlockId,
output: String,
exit_code: ExitCode,
start_ts: Option<DateTime<Local>>,
completed_ts: Option<DateTime<Local>>,
},
LongRunningCommandSnapshot {
command: String,
block_id: BlockId,
grid_contents: String,
cursor: String,
is_alt_screen_active: bool,
is_preempted: bool,
},
Cancelled,
Error(ShellCommandError),
}
impl Display for ReadShellCommandOutputResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadShellCommandOutputResult::CommandFinished {
output, exit_code, ..
} => {
write!(
f,
"Shell command output finished with exit code{}:\n{output}",
exit_code.value()
)
}
ReadShellCommandOutputResult::LongRunningCommandSnapshot { .. } => {
write!(f, "Sent snapshot of long-running shell command to agent")
}
ReadShellCommandOutputResult::Cancelled => {
write!(f, "Read shell command output cancelled")
}
ReadShellCommandOutputResult::Error(e) => {
write!(f, "Read shell command output failed: {e:?}")
}
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SearchCodebaseFailureReason {
CodebaseNotIndexed,
InvalidFilePaths,
GetRelevantFilesError,
ClientError,
MissingCurrentWorkingDirectory,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum SearchCodebaseResult {
Success {
files: Vec<FileContext>,
},
Failed {
reason: SearchCodebaseFailureReason,
/// The message to be sent back to the LLM to inform it why the search failed.
message: String,
},
Cancelled,
}
impl Display for SearchCodebaseResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SearchCodebaseResult::Success { files } => {
write!(f, "Codebase search found: {}", files.iter().format(", "))
}
SearchCodebaseResult::Failed { reason, message } => {
write!(f, "Codebase search failed ({reason:?}): {message}")
}
SearchCodebaseResult::Cancelled => write!(f, "Codebase search cancelled"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum RequestFileEditsResult {
Success {
diff: String,
updated_files: Vec<UpdatedFileContext>,
deleted_files: Vec<String>,
lines_added: usize,
lines_removed: usize,
},
Cancelled,
/// Diff application failed.
DiffApplicationFailed {
error: String,
},
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UpdatedFileContext {
pub was_edited_by_user: bool,
pub file_context: FileContext,
}
impl Display for UpdatedFileContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"user_edited {}, file {}",
self.was_edited_by_user, self.file_context
)
}
}
impl From<UpdatedFileContext> for Vec<UpdatedFileContent> {
fn from(value: UpdatedFileContext) -> Self {
// Note: This method only makes sense for FileContexts that have a string content.
// TODO: How do we gracefully fail binary files here?
let file_content: Vec<warp_multi_agent_api::FileContent> = value.file_context.into();
file_content
.into_iter()
.map(|content| UpdatedFileContent {
was_edited_by_user: value.was_edited_by_user,
file: Some(content),
})
.collect()
}
}
impl Display for RequestFileEditsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequestFileEditsResult::Success {
diff,
updated_files,
..
} => {
write!(
f,
"File edits completed:\n\tDiff:\n{diff}\n\tUpdatedFiles: [{}]",
updated_files.iter().format(", ")
)
}
RequestFileEditsResult::Cancelled => write!(f, "File edits cancelled"),
RequestFileEditsResult::DiffApplicationFailed { error } => {
write!(f, "File edits failed: {error}")
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum SuggestPromptResult {
Accepted { query: String },
Cancelled,
}
impl Display for SuggestPromptResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SuggestPromptResult::Accepted { query } => {
write!(f, "Suggest prompt accepted: {query}")
}
SuggestPromptResult::Cancelled => write!(f, "Suggest prompt cancelled"),
}
}
}
impl AIAgentActionResultType {
/// A user visible description of what the result contains.
/// Used to display error messages when the content is too large for
/// the LLM context window.
pub fn description(&self) -> &str {
match self {
AIAgentActionResultType::RequestCommandOutput(_) => {
"The output of your last command executed by Agent Mode"
}
AIAgentActionResultType::WriteToLongRunningShellCommand(_) => {
"A snapshot of the command currently being executed by Agent Mode"
}
AIAgentActionResultType::RequestFileEdits(_) => {
"The diff from editing the last file in Agent Mode"
}
AIAgentActionResultType::ReadFiles(_) => "The requested file content",
AIAgentActionResultType::UploadArtifact(_) => "The uploaded artifact metadata",
AIAgentActionResultType::SearchCodebase(_) => "The codebase search results",
AIAgentActionResultType::Grep(_) => "The results of the grep operation",
AIAgentActionResultType::FileGlob(_) => "The results of the file glob operation",
AIAgentActionResultType::FileGlobV2(_) => "The results of the file glob operation",
AIAgentActionResultType::CallMCPTool(_) => "The MCP tool call",
AIAgentActionResultType::ReadSkill(_) => "The results of reading a skill from file",
AIAgentActionResultType::ReadMCPResource(_) => "The MCP resource",
AIAgentActionResultType::SuggestNewConversation(_) => {
"Your decision on whether to start a new conversation"
}
AIAgentActionResultType::SuggestPrompt(_) => "The suggested prompt",
AIAgentActionResultType::OpenCodeReview => "Open code review",
AIAgentActionResultType::InsertReviewComments(_) => "Insert code review comments",
AIAgentActionResultType::InitProject => "Initialize project",
AIAgentActionResultType::ReadDocuments(_) => "The requested document content",
AIAgentActionResultType::EditDocuments(_) => "The edited document content",
AIAgentActionResultType::CreateDocuments(_) => "The newly created documents",
AIAgentActionResultType::ReadShellCommandOutput(_) => "The shell command output",
AIAgentActionResultType::UseComputer(_) => "The computer use result",
AIAgentActionResultType::RequestComputerUse(_) => "The computer use request result",
AIAgentActionResultType::FetchConversation(_) => "The fetched conversation tasks",
AIAgentActionResultType::StartAgent(_) => "The result of starting a child agent",
AIAgentActionResultType::SendMessageToAgent(_) => "The result of sending a message",
AIAgentActionResultType::TransferShellCommandControlToUser(_) => {
"The result of transferring shell command control to user"
}
AIAgentActionResultType::AskUserQuestion(_) => {
"The user's answers to clarifying questions"
}
AIAgentActionResultType::RunAgents(_) => {
"The result of an orchestrate batch of child agents"
}
AIAgentActionResultType::WaitForEvents(_) => {
"The local watchdog timed out while waiting for inbound events"
}
}
}
pub fn is_successful(&self) -> bool {
match self {
Self::RequestCommandOutput(r) => r.is_successful(),
Self::RequestFileEdits(RequestFileEditsResult::Success { .. })
| Self::ReadFiles(ReadFilesResult::Success { .. })
| Self::UploadArtifact(UploadArtifactResult::Success { .. })
| Self::SearchCodebase(SearchCodebaseResult::Success { .. })
| Self::Grep(GrepResult::Success { .. })
| Self::FileGlob(FileGlobResult::Success { .. })
| Self::FileGlobV2(FileGlobV2Result::Success { .. })
| Self::ReadMCPResource(ReadMCPResourceResult::Success { .. })
| Self::CallMCPTool(CallMCPToolResult::Success { .. })
| Self::SuggestNewConversation(SuggestNewConversationResult::Accepted { .. })
| Self::SuggestPrompt(SuggestPromptResult::Accepted { .. })
| Self::ReadDocuments(ReadDocumentsResult::Success { .. })
| Self::EditDocuments(EditDocumentsResult::Success { .. })
| Self::CreateDocuments(CreateDocumentsResult::Success { .. })
| Self::ReadShellCommandOutput(
ReadShellCommandOutputResult::CommandFinished { .. }
| ReadShellCommandOutputResult::LongRunningCommandSnapshot { .. },
)
| Self::UseComputer(UseComputerResult::Success(_))
| Self::InsertReviewComments(InsertReviewCommentsResult::Success { .. })
| Self::RequestComputerUse(RequestComputerUseResult::Approved { .. })
| Self::OpenCodeReview
| Self::ReadSkill(ReadSkillResult::Success { .. })
| Self::FetchConversation(FetchConversationResult::Success { .. })
| Self::StartAgent(StartAgentResult::Success { .. })
| Self::SendMessageToAgent(SendMessageToAgentResult::Success { .. })
| Self::TransferShellCommandControlToUser(
TransferShellCommandControlToUserResult::Snapshot { .. }
| TransferShellCommandControlToUserResult::CommandFinished { .. },
) => true,
Self::AskUserQuestion(AskUserQuestionResult::Success { .. }) => true,
Self::RunAgents(RunAgentsResult::Launched { .. }) => true,
Self::WaitForEvents(WaitForEventsResult::Completed) => true,
_ => false,
}
}
pub fn is_failed(&self) -> bool {
match self {
Self::RequestCommandOutput(r) => r.failed(),
Self::RequestFileEdits(RequestFileEditsResult::DiffApplicationFailed { .. })
| Self::ReadFiles(ReadFilesResult::Error(_))
| Self::UploadArtifact(UploadArtifactResult::Error(_))
| Self::SearchCodebase(SearchCodebaseResult::Failed { .. })
| Self::Grep(GrepResult::Error(_))
| Self::FileGlob(FileGlobResult::Error(_))
| Self::FileGlobV2(FileGlobV2Result::Error(_))
| Self::ReadMCPResource(ReadMCPResourceResult::Error(_))
| Self::CallMCPTool(CallMCPToolResult::Error(_))
| Self::ReadDocuments(ReadDocumentsResult::Error(_))
| Self::EditDocuments(EditDocumentsResult::Error(_))
| Self::CreateDocuments(CreateDocumentsResult::Error(_))
| Self::UseComputer(UseComputerResult::Error(_))
| Self::InsertReviewComments(InsertReviewCommentsResult::Error { .. })
| Self::RequestComputerUse(RequestComputerUseResult::Error(_))
| Self::FetchConversation(FetchConversationResult::Error(_))
| Self::StartAgent(StartAgentResult::Error { .. })
| Self::SendMessageToAgent(SendMessageToAgentResult::Error(_))
| Self::AskUserQuestion(AskUserQuestionResult::Error(_))
| Self::TransferShellCommandControlToUser(
TransferShellCommandControlToUserResult::Error(_),
)
| Self::RunAgents(RunAgentsResult::Failure { .. } | RunAgentsResult::Denied { .. }) => {
true
}
_ => false,
}
}
/// Stable identity for loop detection: tool type + input, no variable output.
pub fn loop_description(&self) -> String {
match self {
Self::RequestCommandOutput(r) => {
format!("run_shell_command: {}", r.loop_description())
}
Self::ReadFiles(ReadFilesResult::Success { files }) => {
format!(
"read_files: [{}]",
files
.iter()
.map(|f| f.file_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
Self::ReadFiles(ReadFilesResult::Error(error)) => {
format!("read_files: error={error}")
}
Self::ReadFiles(ReadFilesResult::Cancelled) => "read_files: cancelled".to_string(),
Self::RequestFileEdits(RequestFileEditsResult::Success {
updated_files,
deleted_files,
..
}) => {
let mut names: Vec<&str> = updated_files
.iter()
.map(|f| f.file_context.file_name.as_str())
.collect();
names.extend(deleted_files.iter().map(|s| s.as_str()));
format!("apply_file_diffs: [{}]", names.join(", "))
}
Self::RequestFileEdits(RequestFileEditsResult::DiffApplicationFailed { error }) => {
format!("apply_file_diffs: failed={error}")
}
Self::RequestFileEdits(RequestFileEditsResult::Cancelled) => {
"apply_file_diffs: cancelled".to_string()
}
Self::Grep(GrepResult::Success { matched_files }) => {
format!(
"grep: [{}]",
matched_files
.iter()
.map(|f| f.file_path.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
Self::Grep(GrepResult::Error(error)) => format!("grep: error={error}"),
Self::Grep(GrepResult::Cancelled) => "grep: cancelled".to_string(),
Self::FileGlobV2(FileGlobV2Result::Success { matched_files, .. }) => {
format!(
"file_glob: [{}]",
matched_files
.iter()
.map(|f| f.file_path.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
Self::FileGlobV2(FileGlobV2Result::Error(error)) => format!("file_glob: error={error}"),
Self::FileGlobV2(FileGlobV2Result::Cancelled) => "file_glob: cancelled".to_string(),
Self::SearchCodebase(SearchCodebaseResult::Success { files }) => {
format!(
"search_codebase: [{}]",
files
.iter()
.map(|f| f.file_name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
Self::SearchCodebase(SearchCodebaseResult::Failed { reason, message }) => {
format!("search_codebase: failed={reason:?} {message}")
}
Self::SearchCodebase(SearchCodebaseResult::Cancelled) => {
"search_codebase: cancelled".to_string()
}
other => format!("{other}"),
}
}
pub fn is_cancelled(&self) -> bool {
match self {
Self::RequestCommandOutput(RequestCommandOutputResult::CancelledBeforeExecution) => {
true
}
Self::RequestCommandOutput(RequestCommandOutputResult::Completed {
exit_code, ..
}) if exit_code.value() == 130 => true,
Self::RequestFileEdits(RequestFileEditsResult::Cancelled)
| Self::ReadFiles(ReadFilesResult::Cancelled)
| Self::UploadArtifact(UploadArtifactResult::Cancelled)
| Self::SearchCodebase(SearchCodebaseResult::Cancelled)
| Self::Grep(GrepResult::Cancelled)
| Self::FileGlob(FileGlobResult::Cancelled)
| Self::FileGlobV2(FileGlobV2Result::Cancelled)
| Self::ReadMCPResource(ReadMCPResourceResult::Cancelled)
| Self::CallMCPTool(CallMCPToolResult::Cancelled)
| Self::SuggestNewConversation(SuggestNewConversationResult::Cancelled)
| Self::SuggestPrompt(SuggestPromptResult::Cancelled)
| Self::ReadDocuments(ReadDocumentsResult::Cancelled)
| Self::EditDocuments(EditDocumentsResult::Cancelled)
| Self::CreateDocuments(CreateDocumentsResult::Cancelled)
| Self::ReadShellCommandOutput(ReadShellCommandOutputResult::Cancelled)
| Self::UseComputer(UseComputerResult::Cancelled)
| Self::InsertReviewComments(InsertReviewCommentsResult::Cancelled)
| Self::RequestComputerUse(RequestComputerUseResult::Cancelled)
| Self::TransferShellCommandControlToUser(
TransferShellCommandControlToUserResult::Cancelled,
)
| Self::WriteToLongRunningShellCommand(
WriteToLongRunningShellCommandResult::Cancelled,
)
| Self::ReadSkill(ReadSkillResult::Cancelled)
| Self::FetchConversation(FetchConversationResult::Cancelled)
| Self::StartAgent(StartAgentResult::Cancelled { .. })
| Self::SendMessageToAgent(SendMessageToAgentResult::Cancelled)
// SkippedByAutoApprove is intentionally excluded: the agent should continue.
| Self::AskUserQuestion(AskUserQuestionResult::Cancelled)
| Self::RunAgents(RunAgentsResult::Cancelled)
| Self::WaitForEvents(WaitForEventsResult::Cancelled) => true,
_ => false,
}
}
pub fn is_cancelled_during_requested_command_execution(&self) -> bool {
matches!(self, Self::RequestCommandOutput(RequestCommandOutputResult::Completed {
exit_code, ..
}) if exit_code.value() == 130)
}
/// Returns `true` if this completion of this action result should trigger a follow-up request.
pub fn should_trigger_request_upon_completion(&self) -> bool {
!self.is_cancelled()
}
pub fn is_requested_command(&self) -> bool {
matches!(self, AIAgentActionResultType::RequestCommandOutput(_))
}
pub fn is_call_mcp_tool(&self) -> bool {
matches!(self, AIAgentActionResultType::CallMCPTool(_))
}
/// Returns `true` if this result will cause the server to route the next
/// turn to a subagent (e.g. the CLI subagent) rather than the orchestrator.
/// LRC snapshot variants are the current indicators of this.
pub fn triggers_server_subagent(&self) -> bool {
matches!(
self,
Self::RequestCommandOutput(
RequestCommandOutputResult::LongRunningCommandSnapshot { .. }
) | Self::WriteToLongRunningShellCommand(
WriteToLongRunningShellCommandResult::Snapshot { .. }
) | Self::ReadShellCommandOutput(
ReadShellCommandOutputResult::LongRunningCommandSnapshot { .. }
) | Self::TransferShellCommandControlToUser(
TransferShellCommandControlToUserResult::Snapshot { .. }
)
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GrepResult {
Success { matched_files: Vec<GrepFileMatch> },
Error(String),
Cancelled,
}
impl Display for GrepResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GrepResult::Success { matched_files } => {
write!(
f,
"Grep found matches in: [{}]",
matched_files.iter().format(", ")
)
}
GrepResult::Error(error) => write!(f, "Grep error: {error}"),
GrepResult::Cancelled => write!(f, "Grep cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GrepFileMatch {
/// The absolute path to the file that was matched.
pub file_path: String,
/// The lines that matched the query.
pub matched_lines: Vec<GrepLineMatch>,
}
impl Display for GrepFileMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} on lines [{}]",
self.file_path,
self.matched_lines.iter().format(", ")
)
}
}
/// Info about a line that matched the grep query. This only contains the line
/// number for now, but can be extended in the future to include more info, e.g.
/// line contents.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GrepLineMatch {
/// The line number of the line that matched the query.
pub line_number: usize,
}
impl Display for GrepLineMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.line_number)
}
}
/// The result of a file globbing operation
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileGlobResult {
Success { matched_files: String },
Error(String),
Cancelled,
}
impl Display for FileGlobResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileGlobResult::Success { matched_files } => {
write!(f, "File glob completed: {matched_files}")
}
FileGlobResult::Error(error) => write!(f, "File glob error: {error}"),
FileGlobResult::Cancelled => write!(f, "File glob cancelled"),
}
}
}
// The result of a v2 file globbing operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileGlobV2Result {
Success {
matched_files: Vec<FileGlobV2Match>,
warnings: Option<String>,
},
Error(String),
Cancelled,
}
impl Display for FileGlobV2Result {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileGlobV2Result::Success {
matched_files,
warnings,
} => {
write!(
f,
"File glob V2 completed: [{}] warnings: {:?}",
matched_files.iter().format(", "),
warnings
)
}
FileGlobV2Result::Error(error) => write!(f, "File glob V2 error: {error}"),
FileGlobV2Result::Cancelled => write!(f, "File glob V2 cancelled"),
}
}
}
// A match of a single file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileGlobV2Match {
pub file_path: String,
}
impl Display for FileGlobV2Match {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.file_path)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CallMCPToolResult {
Success { result: rmcp::model::CallToolResult },
Error(String),
Cancelled,
}
impl Display for CallMCPToolResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CallMCPToolResult::Success { result } => {
write!(
f,
"MCP tool call completed: [{result:?}]",
// results.iter().format(", ")
)
}
CallMCPToolResult::Error(error) => write!(f, "MCP tool call error: {error}"),
CallMCPToolResult::Cancelled => write!(f, "MCP tool call cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReadMCPResourceResult {
Success {
resource_contents: Vec<rmcp::model::ResourceContents>,
},
Error(String),
Cancelled,
}
impl Display for ReadMCPResourceResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadMCPResourceResult::Success { resource_contents } => {
write!(f, "MCP resource read completed: [{resource_contents:?}]",)
}
ReadMCPResourceResult::Error(error) => write!(f, "MCP resource error: {error}"),
ReadMCPResourceResult::Cancelled => write!(f, "MCP resource read cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReadSkillResult {
Success { content: FileContext },
Error(String),
Cancelled,
}
impl Display for ReadSkillResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadSkillResult::Success { content } => {
write!(f, "Skill read successfully: {}", content.file_name)
}
ReadSkillResult::Error(error) => write!(f, "Skill read error: {error}"),
ReadSkillResult::Cancelled => write!(f, "Skill read cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum UseComputerResult {
/// Computer use succeeded, with one result per requested action.
Success(computer_use::ActionResult),
Error(String),
Cancelled,
}
impl Display for UseComputerResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UseComputerResult::Success(_) => write!(f, "Use computer completed"),
UseComputerResult::Error(error) => write!(f, "Use computer error: {error}"),
UseComputerResult::Cancelled => write!(f, "Use computer cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InsertReviewCommentsResult {
Success { repo_path: String },
Error { repo_path: String, message: String },
Cancelled,
}
impl Display for InsertReviewCommentsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InsertReviewCommentsResult::Success { repo_path } => {
write!(f, "Inserted code review comments for {repo_path}")
}
InsertReviewCommentsResult::Error { repo_path, message } => {
write!(
f,
"Error inserting code review comments for {repo_path}: {message}"
)
}
InsertReviewCommentsResult::Cancelled => {
write!(f, "Cancelled inserting code review comments")
}
}
}
}
/// Screen dimensions for computer use.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScreenDimensions {
pub width_px: i32,
pub height_px: i32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestComputerUseResult {
/// Request was accepted, with the screen dimensions.
Approved {
screenshot: computer_use::Screenshot,
platform: computer_use::Platform,
/// The on-screen windows the agent may target.
windows: Vec<computer_use::WindowInfo>,
},
/// Request errored.
Error(String),
/// Request was cancelled or rejected by the user.
Cancelled,
}
impl Display for RequestComputerUseResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RequestComputerUseResult::Approved { screenshot, .. } => {
write!(
f,
"Request computer use accepted ({}x{})",
screenshot.original_width, screenshot.original_height
)
}
RequestComputerUseResult::Error(error) => {
write!(f, "Request computer use error: {error}")
}
RequestComputerUseResult::Cancelled => write!(f, "Request computer use cancelled"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FetchConversationResult {
Success { directory_path: String },
Error(String),
Cancelled,
}
impl Display for FetchConversationResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchConversationResult::Success { directory_path } => {
write!(f, "Fetched conversation to {directory_path}")
}
FetchConversationResult::Error(error) => {
write!(f, "Fetch conversation error: {error}")
}
FetchConversationResult::Cancelled => write!(f, "Fetch conversation cancelled"),
}
}
}
// TODO(QUALITY-788): Delete legacy start_agent/start_agent_v2 result support once
// old preview orchestration history no longer needs parse/display/result compatibility.
// Linear issue: QUALITY-788.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum StartAgentResult {
Success {
agent_id: String,
#[serde(default)]
version: StartAgentVersion,
},
Error {
error: String,
#[serde(default)]
version: StartAgentVersion,
},
Cancelled {
#[serde(default)]
version: StartAgentVersion,
},
}
impl StartAgentResult {
/// Returns which start-agent tool schema version produced this result.
pub fn version(&self) -> StartAgentVersion {
match self {
StartAgentResult::Success { version, .. }
| StartAgentResult::Error { version, .. }
| StartAgentResult::Cancelled { version } => *version,
}
}
}
impl Display for StartAgentResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StartAgentResult::Success { agent_id, .. } => {
write!(f, "Started agent with id {agent_id}")
}
StartAgentResult::Error { error, .. } => write!(f, "Start agent error: {error}"),
StartAgentResult::Cancelled { .. } => write!(f, "Start agent cancelled"),
}
}
}
/// The terminal outcome of an orchestrate tool call.
///
/// Mirrors the proto `RunAgentsResult` oneof, with an additional
/// `Cancelled` variant used internally by the action machinery when the
/// user clicks Reject. The proto wire form for cancellation is the
/// generic `ToolCallResult.Cancel` marker; the conversion code emits
/// `ConvertToAPITypeError::Ignore` for `Cancelled` so the input
/// interceptor can synthesize the marker on the next outbound input.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunAgentsResult {
/// Orchestration launched. Carries the resolved configuration and one
/// `AgentOutcome` per `agent_run_configs[]` entry, in input order.
Launched {
model_id: String,
harness_type: String,
execution_mode: RunAgentsLaunchedExecutionMode,
agents: Vec<RunAgentsAgentOutcome>,
},
/// Declined for a non-error reason (currently disapproval).
Denied { reason: String },
/// Actual error path: server-side validation rejected the call, or the
/// client could not begin the launch sequence at all.
Failure { error: String },
/// User rejected via the Reject button. Wire form is the generic
/// `ToolCallResult.Cancel` marker, synthesized by the server's input
/// interceptor on the next user input.
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunAgentsLaunchedExecutionMode {
Local,
Remote {
environment_id: String,
worker_host: String,
computer_use_enabled: bool,
},
}
/// Per-agent outcome reported in `RunAgentsResult::Launched.agents`.
/// Order mirrors the input order of `RunAgents.agent_run_configs[]`,
/// regardless of which `CreateAgentTask` call returned first.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunAgentsAgentOutcome {
pub name: String,
pub kind: RunAgentsAgentOutcomeKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum RunAgentsAgentOutcomeKind {
Launched { agent_id: String },
Failed { error: String },
}
impl Display for RunAgentsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RunAgentsResult::Launched { agents, .. } => {
let launched = agents
.iter()
.filter(|a| matches!(a.kind, RunAgentsAgentOutcomeKind::Launched { .. }))
.count();
write!(
f,
"Orchestrate launched ({launched}/{} agents started)",
agents.len()
)
}
RunAgentsResult::Denied { reason } => {
write!(f, "Orchestrate launch denied: {reason}")
}
RunAgentsResult::Failure { error } => write!(f, "Orchestrate failure: {error}"),
RunAgentsResult::Cancelled => write!(f, "Orchestrate cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SendMessageToAgentResult {
Success { message_id: String },
Error(String),
Cancelled,
}
impl Display for SendMessageToAgentResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SendMessageToAgentResult::Success { message_id } => {
write!(f, "Sent message with id {message_id}")
}
SendMessageToAgentResult::Error(error) => write!(f, "Send message error: {error}"),
SendMessageToAgentResult::Cancelled => write!(f, "Send message cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub enum TransferShellCommandControlToUserResult {
Snapshot {
block_id: BlockId,
grid_contents: String,
cursor: String,
is_alt_screen_active: bool,
is_preempted: bool,
},
CommandFinished {
block_id: BlockId,
output: String,
exit_code: ExitCode,
start_ts: Option<DateTime<Local>>,
completed_ts: Option<DateTime<Local>>,
},
Cancelled,
Error(ShellCommandError),
}
impl Display for TransferShellCommandControlToUserResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Snapshot { .. } => {
write!(f, "Transferred control to user, sent snapshot")
}
Self::CommandFinished {
output, exit_code, ..
} => write!(
f,
"Command finished while user had control, exit code {}:\n{output}",
exit_code.value()
),
Self::Cancelled => write!(f, "Transfer shell command control to user cancelled"),
Self::Error(e) => write!(f, "Transfer shell command control to user failed: {e:?}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AskUserQuestionAnswerItem {
Answered {
question_id: String,
selected_options: Vec<String>,
other_text: String,
},
Skipped {
question_id: String,
},
}
impl AskUserQuestionAnswerItem {
pub fn is_skipped(&self) -> bool {
matches!(self, Self::Skipped { .. })
}
pub fn display_text(&self) -> String {
match self {
Self::Answered {
selected_options,
other_text,
..
} => {
let mut parts = selected_options.clone();
if !other_text.is_empty() {
parts.push(format!("Other: {other_text}"));
}
parts.join(", ")
}
Self::Skipped { .. } => "Skipped".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AskUserQuestionResult {
Success {
answers: Vec<AskUserQuestionAnswerItem>,
},
Error(String),
Cancelled,
/// The question was skipped automatically because the conversation is in auto-approve mode.
SkippedByAutoApprove {
question_ids: Vec<String>,
},
}
impl Display for AskUserQuestionResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AskUserQuestionResult::Success { answers } => {
write!(
f,
"Ask user question completed with {} answer(s)",
answers.len()
)
}
AskUserQuestionResult::Error(msg) => write!(f, "Ask user question error: {msg}"),
AskUserQuestionResult::Cancelled => write!(f, "Ask user question cancelled"),
AskUserQuestionResult::SkippedByAutoApprove { question_ids } => {
write!(
f,
"Ask user question skipped (auto-approve) with {} skipped question(s)",
question_ids.len()
)
}
}
}
}
/// Result of a client-side wait_for_events action.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum WaitForEventsResult {
/// Watchdog fired or an inbound resume signal closed the wait. The
/// agent's next turn observes an empty WaitForEvents result on the
/// wire and decides how to proceed.
Completed,
/// User cancelled the conversation while waiting. Mirrors
/// RunAgents::Cancelled: no tool-call result is sent on the wire.
Cancelled,
}
impl Display for WaitForEventsResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Completed => write!(f, "Wait for events completed"),
Self::Cancelled => write!(f, "Wait for events cancelled"),
}
}
}