Files
warp/crates/ai/src/agent/action/mod.rs
Matthew Albright 888c30278e [QUALITY-569] Stage 1: orchestrate tool (client) (#9628)
## Description

Client-side implementation of the `RunAgents` orchestration tool, paired
with server PR https://github.com/warpdotdev/warp-server/pull/10809.

### What
- **RunAgentsCardView**: confirmation card UI with inline editor for
model, harness, environment, host, and execution mode (Local/Cloud)
- **RunAgentsExecutor**: executor that dispatches accepted requests,
spawning child agent conversations via `StartAgentExecutor`
- **Cancel handling**: card subscribes to `FinishedAction` events so
Ctrl+C (both card-level and terminal-level) shows the cancelled state
- **Harness support**: Oz, Claude, Gemini, Codex with brand icons;
local-dev host gated on `Channel::Local`
- **Spawning card**: in-flight "Spawning N agents..." status while
children are being launched
- **Proto adoption**: `RunAgents` request/result types, `Harness` oneof
shape, `StartAgentV2` advertised alongside `Orchestrate`

### Demo
https://www.loom.com/share/dacad55f436b42d29191bf54461ab3cf

## Testing
- Manual validation of accept, reject (Ctrl+C), edit/discard, picker
interactions, local and cloud execution modes
- Existing tests updated for new harness list and import ordering

## Server API dependencies
- [x] Does this change rely on a new server API?
  - Server PR: https://github.com/warpdotdev/warp-server/pull/10809

## Agent Mode
- [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode

Co-Authored-By: Oz <oz-agent@warp.dev>

Co-authored-by: Oz <oz-agent@warp.dev>
2026-05-04 16:16:34 -04:00

897 lines
31 KiB
Rust

mod convert;
use std::{fmt::Display, ops::Range, path::PathBuf, time::Duration};
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use strum_macros::EnumDiscriminants;
use uuid::Uuid;
use warp_terminal::model::BlockId;
use crate::{
agent::{
action_result::{
AIAgentActionResultType, AskUserQuestionResult, CallMCPToolResult,
CreateDocumentsResult, EditDocumentsResult, FetchConversationResult, FileGlobResult,
FileGlobV2Result, GrepResult, InsertReviewCommentsResult, ReadDocumentsResult,
ReadFilesResult, ReadMCPResourceResult, ReadShellCommandOutputResult, ReadSkillResult,
RequestCommandOutputResult, RequestComputerUseResult, RequestFileEditsResult,
RunAgentsResult, SearchCodebaseResult, SendMessageToAgentResult, StartAgentResult,
StartAgentVersion, SuggestNewConversationResult, SuggestPromptResult,
TransferShellCommandControlToUserResult, UploadArtifactResult, UseComputerResult,
WriteToLongRunningShellCommandResult,
},
AIAgentCitation, FileLocations,
},
diff_validation::ParsedDiff,
document::AIDocumentId,
skills::SkillReference,
};
pub use warp_multi_agent_api::LifecycleEventType;
#[derive(Debug, Clone, Eq, PartialEq, EnumDiscriminants)]
pub enum AIAgentActionType {
/// The AI requested the output for a given command to be retrieved as context in responding to
/// a user's query.
RequestCommandOutput {
command: String,
/// [`Some(true)`] iff the LLM thinks that the `command` is readonly and doesn't produce side-effects.
is_read_only: Option<bool>,
/// [`Some(true)`] iff the LLM thinks that the `command` is risky and should require user confirmation.
is_risky: Option<bool>,
/// `true` if the client should wait until the command is completed and report the finish output as the result.
///
/// If `false` _and_ the command is long-running, a snapshot of the command output is taken and reported as the
/// result instead.
wait_until_completion: bool,
/// [`Some(true)`] iff the LLM thinks that the `command` might invoke pager.
uses_pager: Option<bool>,
/// The AI's rationale for requesting a command.
rationale: Option<String>,
/// The citations for the command.
citations: Vec<AIAgentCitation>,
},
WriteToLongRunningShellCommand {
block_id: BlockId,
input: bytes::Bytes,
mode: AIAgentPtyWriteMode,
},
/// AI requested getting the content of some files.
ReadFiles(ReadFilesRequest),
/// AI requested uploading a local file as a conversation artifact.
UploadArtifact(UploadArtifactRequest),
SearchCodebase(SearchCodebaseRequest),
/// AI requested a vector of edits. Each edit holds a list of diffs on a single code file.
RequestFileEdits {
file_edits: Vec<FileEdit>,
title: Option<String>,
},
Grep {
queries: Vec<String>,
path: String,
},
FileGlob {
patterns: Vec<String>,
path: Option<String>,
},
FileGlobV2 {
patterns: Vec<String>,
search_dir: Option<String>,
// TODO(matthew): Maybe implement client side depth and result limits.
},
ReadMCPResource {
server_id: Option<Uuid>,
name: String,
/// The unique URI for the resource. Prefer using this to identify
/// a resource over [`ReadMCPResource::name`], when available.
///
/// We should phase out `name` eventually and make this non-optional.
uri: Option<String>,
},
CallMCPTool {
server_id: Option<Uuid>,
name: String,
input: serde_json::Value,
},
SuggestNewConversation {
message_id: String,
},
SuggestPrompt(SuggestPromptRequest),
InitProject,
OpenCodeReview,
ReadDocuments(ReadDocumentsRequest),
EditDocuments(EditDocumentsRequest),
CreateDocuments(CreateDocumentsRequest),
ReadShellCommandOutput {
block_id: BlockId,
delay: Option<ShellCommandDelay>,
},
UseComputer(UseComputerRequest),
InsertCodeReviewComments {
repo_path: PathBuf,
comments: Vec<InsertReviewComment>,
base_branch: Option<String>,
},
RequestComputerUse(RequestComputerUseRequest),
// AI requested to read a skill.
ReadSkill(ReadSkillRequest),
FetchConversation {
conversation_id: String,
},
StartAgent {
version: StartAgentVersion,
name: String,
prompt: String,
execution_mode: StartAgentExecutionMode,
lifecycle_subscription: Option<Vec<LifecycleEventType>>,
},
SendMessageToAgent {
addresses: Vec<String>,
subject: String,
message: String,
},
/// Transfer control of a running shell command to the user.
TransferShellCommandControlToUser {
/// The reason provided by the agent for transferring control.
reason: String,
},
AskUserQuestion {
questions: Vec<AskUserQuestionItem>,
},
/// AI requested batched orchestration of one-or-more child agents that
/// share run-wide configuration (model, harness, execution mode).
/// The full per-child prompt is computed at dispatch time as
/// `base_prompt + "\n\n" + agent_run_configs[i].prompt` (or just
/// `base_prompt` when the per-agent `prompt` is empty).
RunAgents(RunAgentsRequest),
}
/// Run-wide + per-agent configuration for a `RunAgents` tool call.
///
/// Mirrors the proto `RunAgents` message. Server-resolved fields
/// (`model_id`, `harness_type`, `execution_mode`'s remote details) are
/// folded in by the server's final tool-call re-emission once the
/// payload is complete; the client renders the full layout from a
/// fully-resolved instance only.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RunAgentsRequest {
pub summary: String,
pub base_prompt: String,
pub skills: Vec<SkillReference>,
pub model_id: String,
pub harness_type: String,
pub execution_mode: RunAgentsExecutionMode,
pub agent_run_configs: Vec<RunAgentsAgentRunConfig>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum RunAgentsExecutionMode {
Local,
Remote {
environment_id: String,
worker_host: String,
computer_use_enabled: bool,
},
}
impl RunAgentsExecutionMode {
pub fn is_remote(&self) -> bool {
matches!(self, Self::Remote { .. })
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RunAgentsAgentRunConfig {
pub name: String,
pub prompt: String,
pub title: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum StartAgentExecutionMode {
Local {
/// `None` selects the legacy embedded local child-agent flow.
/// `Some(...)` selects a third-party CLI harness to launch locally.
harness_type: Option<String>,
/// `None` inherits the parent agent's preferred LLM (legacy behavior).
/// `Some(_)` overrides the child's preferred LLM with the supplied
/// model id (used by the orchestrate confirmation card so the user's
/// model selection is honored on local launches).
model_id: Option<String>,
},
Remote {
environment_id: String,
skill_references: Vec<SkillReference>,
model_id: String,
computer_use_enabled: bool,
worker_host: String,
harness_type: String,
title: String,
},
}
impl StartAgentExecutionMode {
/// Constructs a local execution mode using the legacy v1 default harness.
pub fn local_with_defaults() -> Self {
Self::Local {
harness_type: None,
model_id: None,
}
}
/// Constructs a local execution mode for a specific third-party harness.
pub fn local_harness(harness_type: String) -> Self {
Self::Local {
harness_type: Some(harness_type),
model_id: None,
}
}
/// Constructs a remote execution mode using the legacy v1 defaults for
/// fields that were added later in StartAgentV2.
pub fn remote_with_defaults(environment_id: String) -> Self {
Self::Remote {
environment_id,
skill_references: Vec::new(),
model_id: String::new(),
computer_use_enabled: false,
worker_host: String::new(),
harness_type: String::new(),
title: String::new(),
}
}
}
impl AIAgentActionType {
pub fn is_request_command_output(&self) -> bool {
matches!(self, Self::RequestCommandOutput { .. })
}
pub fn is_read_files(&self) -> bool {
matches!(self, Self::ReadFiles(..))
}
pub fn is_search_codebase(&self) -> bool {
matches!(self, Self::SearchCodebase(..))
}
pub fn is_grep(&self) -> bool {
matches!(self, Self::Grep { .. })
}
pub fn is_file_glob(&self) -> bool {
matches!(self, Self::FileGlob { .. } | Self::FileGlobV2 { .. })
}
pub fn is_write_to_shell_command(&self) -> bool {
matches!(self, Self::WriteToLongRunningShellCommand { .. })
}
pub fn cancelled_result(&self) -> AIAgentActionResultType {
match self {
Self::RequestCommandOutput { .. } => AIAgentActionResultType::RequestCommandOutput(
RequestCommandOutputResult::CancelledBeforeExecution,
),
Self::RequestFileEdits { .. } => {
AIAgentActionResultType::RequestFileEdits(RequestFileEditsResult::Cancelled)
}
Self::ReadFiles(..) => AIAgentActionResultType::ReadFiles(ReadFilesResult::Cancelled),
Self::UploadArtifact(..) => {
AIAgentActionResultType::UploadArtifact(UploadArtifactResult::Cancelled)
}
Self::SearchCodebase(..) => {
AIAgentActionResultType::SearchCodebase(SearchCodebaseResult::Cancelled)
}
Self::Grep { .. } => AIAgentActionResultType::Grep(GrepResult::Cancelled),
Self::FileGlob { .. } => AIAgentActionResultType::FileGlob(FileGlobResult::Cancelled),
Self::FileGlobV2 { .. } => {
AIAgentActionResultType::FileGlobV2(FileGlobV2Result::Cancelled)
}
Self::WriteToLongRunningShellCommand { .. } => {
AIAgentActionResultType::WriteToLongRunningShellCommand(
WriteToLongRunningShellCommandResult::Cancelled,
)
}
Self::CallMCPTool { .. } => {
AIAgentActionResultType::CallMCPTool(CallMCPToolResult::Cancelled)
}
Self::ReadMCPResource { .. } => {
AIAgentActionResultType::ReadMCPResource(ReadMCPResourceResult::Cancelled)
}
Self::SuggestNewConversation { .. } => AIAgentActionResultType::SuggestNewConversation(
SuggestNewConversationResult::Cancelled,
),
Self::SuggestPrompt { .. } => {
AIAgentActionResultType::SuggestPrompt(SuggestPromptResult::Cancelled)
}
Self::OpenCodeReview => AIAgentActionResultType::OpenCodeReview,
Self::InitProject => AIAgentActionResultType::InitProject,
Self::ReadDocuments(_) => {
AIAgentActionResultType::ReadDocuments(ReadDocumentsResult::Cancelled)
}
Self::EditDocuments(_) => {
AIAgentActionResultType::EditDocuments(EditDocumentsResult::Cancelled)
}
Self::CreateDocuments(_) => {
AIAgentActionResultType::CreateDocuments(CreateDocumentsResult::Cancelled)
}
Self::ReadShellCommandOutput { .. } => AIAgentActionResultType::ReadShellCommandOutput(
ReadShellCommandOutputResult::Cancelled,
),
Self::UseComputer(_) => {
AIAgentActionResultType::UseComputer(UseComputerResult::Cancelled)
}
Self::InsertCodeReviewComments { .. } => {
AIAgentActionResultType::InsertReviewComments(InsertReviewCommentsResult::Cancelled)
}
Self::RequestComputerUse(_) => {
AIAgentActionResultType::RequestComputerUse(RequestComputerUseResult::Cancelled)
}
Self::ReadSkill(_) => AIAgentActionResultType::ReadSkill(ReadSkillResult::Cancelled),
Self::FetchConversation { .. } => {
AIAgentActionResultType::FetchConversation(FetchConversationResult::Cancelled)
}
Self::StartAgent { version, .. } => {
AIAgentActionResultType::StartAgent(StartAgentResult::Cancelled {
version: *version,
})
}
Self::SendMessageToAgent { .. } => {
AIAgentActionResultType::SendMessageToAgent(SendMessageToAgentResult::Cancelled)
}
Self::TransferShellCommandControlToUser { .. } => {
AIAgentActionResultType::TransferShellCommandControlToUser(
TransferShellCommandControlToUserResult::Cancelled,
)
}
Self::AskUserQuestion { .. } => {
AIAgentActionResultType::AskUserQuestion(AskUserQuestionResult::Cancelled)
}
Self::RunAgents(_) => AIAgentActionResultType::RunAgents(RunAgentsResult::Cancelled),
}
}
pub fn user_friendly_name(&self) -> String {
match self {
Self::RequestCommandOutput { command, .. } => {
format!("Run command: {command}")
}
Self::WriteToLongRunningShellCommand { .. } => {
"Write to long running shell command".to_string()
}
Self::ReadFiles(_) => "Read files".to_string(),
Self::UploadArtifact(_) => "Upload artifact".to_string(),
Self::SearchCodebase(_) => "Search codebase".to_string(),
Self::RequestFileEdits { file_edits, .. } => {
let file_names = file_edits.iter().filter_map(|edit| edit.file()).join(", ");
format!("Edit {file_names}")
}
Self::Grep { .. } => "Grep".to_string(),
Self::FileGlob { .. } | Self::FileGlobV2 { .. } => "File glob".to_string(),
Self::ReadMCPResource { .. } => "Read mcp resource".to_string(),
Self::CallMCPTool { .. } => "Call mcp tool".to_string(),
Self::SuggestNewConversation { .. } => "Suggest new conversation".to_string(),
Self::SuggestPrompt { .. } => "Suggest prompt".to_string(),
Self::InitProject => "Init project".to_string(),
Self::OpenCodeReview => "Open code review".to_string(),
Self::ReadDocuments(_) => "Read documents".to_string(),
Self::EditDocuments(_) => "Edit documents".to_string(),
Self::CreateDocuments(_) => "Create documents".to_string(),
Self::ReadShellCommandOutput { .. } => "Read shell command output".to_string(),
Self::UseComputer(_) => "Use computer".to_string(),
Self::InsertCodeReviewComments { comments, .. } => {
format!("Insert {} code review comments", comments.len())
}
Self::RequestComputerUse(_) => "Request computer use".to_string(),
Self::ReadSkill(_) => "Read skill".to_string(),
Self::FetchConversation { .. } => "Fetch conversation".to_string(),
Self::StartAgent { name, .. } => format!("Start agent: {name}"),
Self::SendMessageToAgent { subject, .. } => format!("Send message: {subject}"),
Self::TransferShellCommandControlToUser { .. } => {
"Transfer shell command control to user".to_string()
}
Self::AskUserQuestion { questions } => {
format!("Ask user {} question(s)", questions.len())
}
Self::RunAgents(req) => {
format!("Orchestrate {} agent(s)", req.agent_run_configs.len())
}
}
}
}
impl Display for AIAgentActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AIAgentActionType::RequestCommandOutput {
command,
is_read_only,
uses_pager,
..
} => {
write!(
f,
"RequestCommandOutput: {command} (read_only: {is_read_only:?}, pager: {uses_pager:?})"
)
}
AIAgentActionType::WriteToLongRunningShellCommand {
block_id,
input,
mode,
} => {
write!(
f,
"WriteToLongRunningShellCommand (block id: {block_id}): {input:?}, {mode:?}",
)
}
AIAgentActionType::ReadFiles(request) => {
write!(f, "{request}")
}
AIAgentActionType::UploadArtifact(request) => {
write!(f, "{request}")
}
AIAgentActionType::SearchCodebase(request) => {
write!(f, "{request}")
}
AIAgentActionType::RequestFileEdits { file_edits, title } => {
let file_names = file_edits
.iter()
.filter_map(|edit| edit.file())
.collect::<Vec<_>>()
.join(", ");
if let Some(title) = title {
write!(f, "RequestFileEdits '{title}': [{file_names}]")
} else {
write!(f, "RequestFileEdits: [{file_names}]")
}
}
AIAgentActionType::Grep { queries, path } => {
write!(f, "Grep: [{}] in {}", queries.join(", "), path)
}
AIAgentActionType::FileGlob { patterns, path } => {
let path_str = path.as_deref().unwrap_or(".");
write!(f, "FileGlob: [{}] in {}", patterns.join(", "), path_str)
}
AIAgentActionType::FileGlobV2 {
patterns,
search_dir,
} => {
let path_str = search_dir.as_deref().unwrap_or(".");
write!(f, "FileGlobV2: [{}] in {}", patterns.join(", "), path_str)
}
AIAgentActionType::ReadMCPResource {
server_id: _,
name,
uri,
} => {
if let Some(uri) = uri {
write!(f, "ReadMCPResource: {name} ({uri})")
} else {
write!(f, "ReadMCPResource: {name}")
}
}
AIAgentActionType::CallMCPTool {
server_id: _,
name,
input,
} => {
write!(f, "CallMCPTool: {name} with input {input:?}")
}
AIAgentActionType::SuggestNewConversation { message_id } => {
write!(f, "SuggestNewConversation: {message_id}")
}
AIAgentActionType::SuggestPrompt(request) => {
write!(f, "SuggestPrompt: {request:?}")
}
AIAgentActionType::InitProject => {
write!(f, "InitProject")
}
AIAgentActionType::OpenCodeReview => {
write!(f, "OpenCodeReview")
}
AIAgentActionType::ReadDocuments(request) => {
let ids: Vec<String> = request
.document_ids
.iter()
.map(|id| id.to_string())
.collect();
write!(f, "ReadDocuments: [{}]", ids.join(", "))
}
AIAgentActionType::EditDocuments(request) => {
write!(f, "EditDocuments: {} diffs", request.diffs.len())
}
AIAgentActionType::CreateDocuments(request) => {
write!(f, "CreateDocuments: {} documents", request.documents.len())
}
AIAgentActionType::ReadShellCommandOutput { delay, block_id } => {
let delay = match delay {
Some(ShellCommandDelay::Duration(duration)) => {
format!("{} seconds", duration.as_secs())
}
Some(ShellCommandDelay::OnCompletion) => "on completion".to_string(),
None => "no".to_string(),
};
write!(
f,
"ReadShellCommandOutput (block id: {block_id}): with {delay} delay"
)
}
AIAgentActionType::UseComputer(req) => {
write!(
f,
"UseComputer: {} actions, screenshot_params={:?}",
req.actions.len(),
req.screenshot_params
)
}
AIAgentActionType::InsertCodeReviewComments { comments, .. } => {
let file_paths = comments
.iter()
.filter_map(|c| {
c.comment_location
.as_ref()
.map(|loc| loc.relative_file_path.as_str())
})
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"InsertCodeReviewComments: {} comments on [{}]",
comments.len(),
file_paths
)
}
AIAgentActionType::RequestComputerUse(req) => {
write!(f, "RequestComputerUse: {}", req.task_summary)
}
AIAgentActionType::ReadSkill(req) => {
write!(f, "ReadSkill: {}", req.skill)
}
AIAgentActionType::FetchConversation { conversation_id } => {
write!(f, "FetchConversation: {conversation_id}")
}
AIAgentActionType::StartAgent { name, .. } => {
write!(f, "StartAgent: {name}")
}
AIAgentActionType::SendMessageToAgent {
addresses, subject, ..
} => {
write!(
f,
"SendMessageToAgent: to=[{}] subject={subject}",
addresses.join(", ")
)
}
AIAgentActionType::TransferShellCommandControlToUser { reason } => {
write!(f, "TransferShellCommandControlToUser: {reason}")
}
AIAgentActionType::AskUserQuestion { questions } => {
write!(f, "AskUserQuestion: {} question(s)", questions.len())
}
AIAgentActionType::RunAgents(req) => {
let names = req
.agent_run_configs
.iter()
.map(|c| c.name.as_str())
.collect::<Vec<_>>()
.join(", ");
write!(f, "Orchestrate: summary='{}' agents=[{names}]", req.summary,)
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum AskUserQuestionType {
MultipleChoice {
is_multiselect: bool,
options: Vec<AskUserQuestionOption>,
supports_other: bool,
},
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct AskUserQuestionOption {
pub label: String,
pub recommended: bool,
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct AskUserQuestionItem {
pub question_id: String,
pub question: String,
pub question_type: AskUserQuestionType,
}
impl AskUserQuestionItem {
pub fn is_multiselect(&self) -> bool {
match &self.question_type {
AskUserQuestionType::MultipleChoice { is_multiselect, .. } => *is_multiselect,
}
}
pub fn multiple_choice_options(&self) -> Option<&[AskUserQuestionOption]> {
match &self.question_type {
AskUserQuestionType::MultipleChoice { options, .. } => Some(options),
}
}
pub fn supports_other(&self) -> bool {
match &self.question_type {
AskUserQuestionType::MultipleChoice { supports_other, .. } => *supports_other,
}
}
pub fn numbered_option_count(&self) -> usize {
self.multiple_choice_options()
.map_or(0, |options| options.len())
+ usize::from(self.supports_other())
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ReadFilesRequest {
pub locations: Vec<FileLocations>,
}
impl Display for ReadFilesRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let file_names = self
.locations
.iter()
.map(|loc| loc.name.as_str())
.collect::<Vec<_>>()
.join(", ");
write!(f, "ReadFiles: [{file_names}]")
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UploadArtifactRequest {
pub file_path: String,
pub description: Option<String>,
}
impl Display for UploadArtifactRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "UploadArtifact: {}", self.file_path)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SearchCodebaseRequest {
pub query: String,
/// Optional list of file paths to search through. This is used to narrow down the search scope.
/// Files are searched if any of the partial paths are a substring of the file path.
pub partial_paths: Option<Vec<String>>,
/// Optional absolute path to the codebase that we want to search. If not
/// provided, we will use the codebase in the user's current directory.
pub codebase_path: Option<String>,
}
impl Display for SearchCodebaseRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SearchCodebase: {}", self.query)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ReadDocumentsRequest {
pub document_ids: Vec<AIDocumentId>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DocumentDiff {
pub document_id: AIDocumentId,
pub search: String,
pub replace: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct EditDocumentsRequest {
pub diffs: Vec<DocumentDiff>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DocumentToCreate {
pub content: String,
pub title: String,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct CreateDocumentsRequest {
pub documents: Vec<DocumentToCreate>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UseComputerRequest {
pub action_summary: String,
pub actions: Vec<computer_use::Action>,
/// If set, a screenshot will be captured after the actions are executed.
pub screenshot_params: Option<computer_use::ScreenshotParams>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RequestComputerUseRequest {
/// A short summary of the task.
pub task_summary: String,
/// If set, a screenshot will be captured after the actions are executed.
pub screenshot_params: Option<computer_use::ScreenshotParams>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ReadSkillRequest {
pub skill: SkillReference,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ShellCommandDelay {
Duration(Duration),
OnCompletion,
}
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, EnumDiscriminants)]
pub enum AIAgentPtyWriteMode {
#[default]
Raw,
Line,
Block,
}
impl AIAgentPtyWriteMode {
/// Decorates input bytes according to the write mode.
pub fn decorate_bytes(
self,
bytes: impl Into<Vec<u8>>,
is_bracketed_paste_enabled: bool,
) -> Vec<u8> {
use warp_terminal::model::escape_sequences;
let bytes = bytes.into();
match self {
AIAgentPtyWriteMode::Raw => bytes,
AIAgentPtyWriteMode::Line => {
// Move to beginning of line, write input, then submit (Enter).
let mut v = Vec::with_capacity(bytes.len() + 2);
// ^A (SOH) is "beginning of line" for readline/prompt-toolkit style editors.
v.push(escape_sequences::C0::SOH);
v.extend_from_slice(&bytes);
cfg_if::cfg_if! {
if #[cfg(target_os = "windows")] {
// Use CR to submit on Windows hosts.
v.push(escape_sequences::C0::CR);
} else {
// Use LF to submit on POSIX.
v.push(escape_sequences::C0::LF);
}
}
v
}
AIAgentPtyWriteMode::Block => {
if is_bracketed_paste_enabled {
escape_sequences::BRACKETED_PASTE_START
.iter()
.copied()
.chain(bytes)
.chain(escape_sequences::BRACKETED_PASTE_END.iter().copied())
.collect()
} else {
bytes
}
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct InsertReviewComment {
pub comment_id: String,
pub author: String,
pub last_modified_timestamp: String,
pub comment_body: String,
pub parent_comment_id: Option<String>,
/// The file and line range the comment is attached to.
/// If None, the comment applies to the whole diff set.
pub comment_location: Option<InsertedCommentLocation>,
pub html_url: Option<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct InsertedCommentLocation {
/// Repo-relative path of the file the comment is attached to.
pub relative_file_path: String,
/// The specific line range the comment is attached to.
/// If None, the comment applies to the whole file.
pub line: Option<InsertedCommentLine>,
}
/// The side of a diff that a comment is attached to.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CommentSide {
/// The right side of the diff (new file / additions).
Right,
/// The left side of the diff (old file / deletions).
Left,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct InsertedCommentLine {
pub comment_line_range: Range<usize>,
/// The diff hunk line range overlaps with the comment line range
/// but may not match it exactly. We need this in order to be able
/// to find the full diff hunk this comment is attached to.
pub diff_hunk_line_range: Range<usize>,
/// The diff hunk text is needed to find where to attach comments
/// when line numbers on the local and remote branches have diverged.
pub diff_hunk_text: String,
/// The side of the diff the comment is attached to.
pub side: Option<CommentSide>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SuggestPromptRequest {
UnitTestsSuggestion {
query: String,
title: String,
description: String,
},
PromptSuggestion {
prompt: String,
label: Option<String>,
},
}
/// A file-editing request from the agent.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum FileEdit {
/// Edit an existing file by applying a diff.
Edit(ParsedDiff),
/// Create a new file.
Create {
file: Option<String>,
content: Option<String>,
},
/// Delete an existing file.
Delete { file: Option<String> },
}
impl FileEdit {
/// The path to the file this edit applies to.
pub fn file(&self) -> Option<&str> {
match self {
Self::Edit(diff) => diff.file().map(|s| s.as_str()),
Self::Create { file, .. } => file.as_deref(),
Self::Delete { file } => file.as_deref(),
}
}
}