Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use uuid::Uuid;
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::{
|
||||
agent::{
|
||||
action::{
|
||||
AIAgentActionType, AIAgentPtyWriteMode, CommentSide, FileEdit, InsertReviewComment,
|
||||
InsertedCommentLine, InsertedCommentLocation, ReadFilesRequest, ReadSkillRequest,
|
||||
SearchCodebaseRequest, ShellCommandDelay, SuggestPromptRequest, UploadArtifactRequest,
|
||||
UseComputerRequest,
|
||||
},
|
||||
action_result::{AnyFileContent, FileContext},
|
||||
convert::ToolToAIAgentActionError,
|
||||
FileLocations,
|
||||
},
|
||||
diff_validation::{ParsedDiff, V4AHunk},
|
||||
document::AIDocumentId,
|
||||
skills::SkillReference,
|
||||
};
|
||||
|
||||
impl From<api::message::tool_call::RunShellCommand> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::RunShellCommand) -> Self {
|
||||
AIAgentActionType::RequestCommandOutput {
|
||||
command: value.command,
|
||||
is_read_only: Some(value.is_read_only),
|
||||
rationale: None,
|
||||
uses_pager: Some(value.uses_pager),
|
||||
is_risky: Some(value.is_risky),
|
||||
wait_until_completion: value.wait_until_complete_value.is_none_or(
|
||||
|api::message::tool_call::run_shell_command::WaitUntilCompleteValue::WaitUntilComplete(
|
||||
should_wait,
|
||||
)| should_wait,
|
||||
),
|
||||
citations: value
|
||||
.citations
|
||||
.iter()
|
||||
.filter_map(|citation| citation.clone().try_into().ok())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::WriteToLongRunningShellCommand> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::WriteToLongRunningShellCommand) -> Self {
|
||||
AIAgentActionType::WriteToLongRunningShellCommand {
|
||||
block_id: value.command_id.into(),
|
||||
input: value.input.into(),
|
||||
mode: value.mode.map(Into::into).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::write_to_long_running_shell_command::Mode>
|
||||
for AIAgentPtyWriteMode
|
||||
{
|
||||
fn from(value: api::message::tool_call::write_to_long_running_shell_command::Mode) -> Self {
|
||||
match value.mode {
|
||||
Some(mode) => {
|
||||
use warp_multi_agent_api::message::tool_call::write_to_long_running_shell_command::mode::Mode;
|
||||
match mode {
|
||||
Mode::Raw(_) => AIAgentPtyWriteMode::Raw,
|
||||
Mode::Line(_) => AIAgentPtyWriteMode::Line,
|
||||
Mode::Block(_) => AIAgentPtyWriteMode::Block,
|
||||
}
|
||||
}
|
||||
None => AIAgentPtyWriteMode::Raw,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::SuggestNewConversation> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::SuggestNewConversation) -> Self {
|
||||
AIAgentActionType::SuggestNewConversation {
|
||||
message_id: value.message_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::ApplyFileDiffs> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::ApplyFileDiffs) -> Self {
|
||||
let diff_edits = value.diffs.into_iter().map(|file_diff| {
|
||||
FileEdit::Edit(ParsedDiff::StrReplaceEdit {
|
||||
search: file_diff.search.none_if_default(),
|
||||
replace: file_diff.replace.none_if_default(),
|
||||
file: file_diff.file_path.none_if_default(),
|
||||
})
|
||||
});
|
||||
let v4a_updates = value.v4a_updates.into_iter().map(|v4a_update| {
|
||||
FileEdit::Edit(ParsedDiff::V4AEdit {
|
||||
file: v4a_update.file_path.none_if_default(),
|
||||
move_to: v4a_update.move_to.clone().none_if_default(),
|
||||
hunks: v4a_update
|
||||
.hunks
|
||||
.into_iter()
|
||||
.map(|hunk| V4AHunk {
|
||||
change_context: hunk.change_context,
|
||||
pre_context: hunk.pre_context,
|
||||
old: hunk.old,
|
||||
new: hunk.new,
|
||||
post_context: hunk.post_context,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
});
|
||||
let file_deletes = value
|
||||
.deleted_files
|
||||
.into_iter()
|
||||
.map(|file_delete| FileEdit::Delete {
|
||||
file: file_delete.file_path.none_if_default(),
|
||||
});
|
||||
let new_file_edits = value
|
||||
.new_files
|
||||
.into_iter()
|
||||
.map(|new_file| FileEdit::Create {
|
||||
file: new_file.file_path.none_if_default(),
|
||||
content: new_file.content.none_if_default(),
|
||||
});
|
||||
|
||||
AIAgentActionType::RequestFileEdits {
|
||||
file_edits: diff_edits
|
||||
.chain(v4a_updates)
|
||||
.chain(new_file_edits)
|
||||
.chain(file_deletes)
|
||||
.collect(),
|
||||
title: Some(value.summary),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::ReadFiles> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::ReadFiles) -> Self {
|
||||
AIAgentActionType::ReadFiles(ReadFilesRequest {
|
||||
locations: value.files.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::UploadFileArtifact> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::UploadFileArtifact) -> Result<Self, Self::Error> {
|
||||
let file = value
|
||||
.file
|
||||
.filter(|file| !file.file_path.is_empty())
|
||||
.ok_or(ToolToAIAgentActionError::MissingUploadArtifactFileReference)?;
|
||||
|
||||
Ok(AIAgentActionType::UploadArtifact(UploadArtifactRequest {
|
||||
file_path: file.file_path,
|
||||
description: value.description.none_if_default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::SearchCodebase> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::SearchCodebase) -> Self {
|
||||
AIAgentActionType::SearchCodebase(SearchCodebaseRequest {
|
||||
query: value.query,
|
||||
partial_paths: if !value.path_filters.is_empty() {
|
||||
Some(value.path_filters)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
codebase_path: if !value.codebase_path.is_empty() {
|
||||
Some(value.codebase_path)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::Grep> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::Grep) -> Self {
|
||||
AIAgentActionType::Grep {
|
||||
queries: value.queries,
|
||||
path: value.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::FileGlob> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::FileGlob) -> Self {
|
||||
AIAgentActionType::FileGlob {
|
||||
patterns: value.patterns,
|
||||
path: if value.path.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.path)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::FileGlobV2> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::FileGlobV2) -> Self {
|
||||
AIAgentActionType::FileGlobV2 {
|
||||
patterns: value.patterns,
|
||||
search_dir: if value.search_dir.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.search_dir)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::read_files::File> for FileLocations {
|
||||
fn from(value: api::message::tool_call::read_files::File) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
lines: value
|
||||
.line_ranges
|
||||
.into_iter()
|
||||
.map(|line_range| line_range.start as usize..line_range.end as usize)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::ReadMcpResource> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::ReadMcpResource) -> Self {
|
||||
let server_id = if FeatureFlag::MCPGroupedServerContext.is_enabled() {
|
||||
Uuid::parse_str(&value.server_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
AIAgentActionType::ReadMCPResource {
|
||||
server_id,
|
||||
uri: Some(value.uri),
|
||||
name: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::message::tool_call::CallMcpTool> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::message::tool_call::CallMcpTool) -> Result<Self, Self::Error> {
|
||||
let Some(args) = value.args else {
|
||||
return Err(ToolToAIAgentActionError::CallMCPToolArgsError(
|
||||
String::from("missing args"),
|
||||
));
|
||||
};
|
||||
|
||||
let input = prost_to_serde_json(prost_types::Value {
|
||||
kind: Some(prost_types::value::Kind::StructValue(args)),
|
||||
})
|
||||
.map_err(ToolToAIAgentActionError::CallMCPToolArgsError)?;
|
||||
|
||||
let server_id = if FeatureFlag::MCPGroupedServerContext.is_enabled() {
|
||||
Uuid::parse_str(&value.server_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(AIAgentActionType::CallMCPTool {
|
||||
server_id,
|
||||
name: value.name,
|
||||
input,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::message::tool_call::SuggestPrompt> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::message::tool_call::SuggestPrompt) -> Result<Self, Self::Error> {
|
||||
let request = match value.display_mode {
|
||||
Some(api::message::tool_call::suggest_prompt::DisplayMode::InlineQueryBanner(
|
||||
inline_query_banner,
|
||||
)) => SuggestPromptRequest::UnitTestsSuggestion {
|
||||
title: inline_query_banner.title,
|
||||
description: inline_query_banner.description,
|
||||
query: inline_query_banner.query,
|
||||
},
|
||||
Some(api::message::tool_call::suggest_prompt::DisplayMode::PromptChip(chip)) => {
|
||||
let label = chip.label.none_if_default();
|
||||
SuggestPromptRequest::PromptSuggestion {
|
||||
prompt: chip.prompt,
|
||||
label,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ToolToAIAgentActionError::SuggestPromptError(String::from(
|
||||
"unsupported display mode",
|
||||
)));
|
||||
}
|
||||
};
|
||||
Ok(AIAgentActionType::SuggestPrompt(request))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<warp_multi_agent_api::FileContent> for FileContext {
|
||||
fn from(content: warp_multi_agent_api::FileContent) -> Self {
|
||||
let line_range = content.line_range.map(|r| r.start as usize..r.end as usize);
|
||||
|
||||
FileContext::new(
|
||||
content.file_path,
|
||||
AnyFileContent::StringContent(content.content),
|
||||
line_range,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<warp_multi_agent_api::AnyFileContent> for FileContext {
|
||||
fn from(content: warp_multi_agent_api::AnyFileContent) -> Self {
|
||||
match content.content {
|
||||
Some(api::any_file_content::Content::BinaryContent(binary_content)) => {
|
||||
FileContext::new(
|
||||
binary_content.file_path,
|
||||
AnyFileContent::BinaryContent(binary_content.data),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
Some(api::any_file_content::Content::TextContent(text_content)) => {
|
||||
let line_range = text_content
|
||||
.line_range
|
||||
.map(|r| r.start as usize..r.end as usize);
|
||||
|
||||
FileContext::new(
|
||||
text_content.file_path,
|
||||
AnyFileContent::StringContent(text_content.content),
|
||||
line_range,
|
||||
None,
|
||||
)
|
||||
}
|
||||
None => unreachable!("AnyFileContent should always have a content"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::ReadDocuments> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::ReadDocuments) -> Self {
|
||||
use crate::agent::action::ReadDocumentsRequest;
|
||||
AIAgentActionType::ReadDocuments(ReadDocumentsRequest {
|
||||
document_ids: value
|
||||
.documents
|
||||
.into_iter()
|
||||
.filter_map(|doc| AIDocumentId::try_from(doc.document_id).ok())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::EditDocuments> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::EditDocuments) -> Self {
|
||||
use crate::agent::action::{DocumentDiff, EditDocumentsRequest};
|
||||
AIAgentActionType::EditDocuments(EditDocumentsRequest {
|
||||
diffs: value
|
||||
.diffs
|
||||
.into_iter()
|
||||
.filter_map(|diff| {
|
||||
AIDocumentId::try_from(diff.document_id)
|
||||
.map(|document_id| DocumentDiff {
|
||||
document_id,
|
||||
search: diff.search,
|
||||
replace: diff.replace,
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::CreateDocuments> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::CreateDocuments) -> Self {
|
||||
use crate::agent::action::{CreateDocumentsRequest, DocumentToCreate};
|
||||
AIAgentActionType::CreateDocuments(CreateDocumentsRequest {
|
||||
documents: value
|
||||
.new_documents
|
||||
.into_iter()
|
||||
.map(|doc| DocumentToCreate {
|
||||
content: doc.content,
|
||||
title: if doc.title.is_empty() {
|
||||
// DO NOT SUBMIT
|
||||
// crate::ai::ai_document_view::DEFAULT_PLANNING_DOCUMENT_TITLE.to_string()
|
||||
"".to_string()
|
||||
} else {
|
||||
doc.title
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::ReadShellCommandOutput> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::ReadShellCommandOutput) -> Self {
|
||||
let delay = match value.delay {
|
||||
Some(api::message::tool_call::read_shell_command_output::Delay::Duration(duration)) => {
|
||||
Some(ShellCommandDelay::Duration(Duration::from_secs(
|
||||
duration.seconds as u64,
|
||||
)))
|
||||
}
|
||||
Some(api::message::tool_call::read_shell_command_output::Delay::OnCompletion(_)) => {
|
||||
Some(ShellCommandDelay::OnCompletion)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
AIAgentActionType::ReadShellCommandOutput {
|
||||
block_id: value.command_id.into(),
|
||||
delay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::TransferShellCommandControlToUser> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::TransferShellCommandControlToUser) -> Self {
|
||||
AIAgentActionType::TransferShellCommandControlToUser {
|
||||
reason: value.reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::message::tool_call::UseComputer> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::message::tool_call::UseComputer) -> Result<Self, Self::Error> {
|
||||
use api::message::tool_call::use_computer;
|
||||
|
||||
let actions = value
|
||||
.actions
|
||||
.into_iter()
|
||||
.map(|action| {
|
||||
let Some(action_type) = action.r#type else {
|
||||
return Err(ToolToAIAgentActionError::MissingComputerUseActionType);
|
||||
};
|
||||
match action_type {
|
||||
use_computer::action::Type::MouseMove(mouse_move) => {
|
||||
Ok(computer_use::Action::MouseMove {
|
||||
to: coordinates_to_vec(mouse_move.to.as_ref())?,
|
||||
})
|
||||
}
|
||||
use_computer::action::Type::MouseDown(mouse_down) => {
|
||||
Ok(computer_use::Action::MouseDown {
|
||||
button: to_computer_use_button(mouse_down.button()),
|
||||
at: coordinates_to_vec(mouse_down.at.as_ref())?,
|
||||
})
|
||||
}
|
||||
use_computer::action::Type::MouseUp(mouse_up) => {
|
||||
Ok(computer_use::Action::MouseUp {
|
||||
button: to_computer_use_button(mouse_up.button()),
|
||||
})
|
||||
}
|
||||
use_computer::action::Type::MouseWheel(mouse_wheel) => {
|
||||
let direction = to_scroll_direction(mouse_wheel.direction());
|
||||
let distance = to_scroll_distance(mouse_wheel.distance)?;
|
||||
Ok(computer_use::Action::MouseWheel {
|
||||
at: coordinates_to_vec(mouse_wheel.at.as_ref())?,
|
||||
direction,
|
||||
distance,
|
||||
})
|
||||
}
|
||||
use_computer::action::Type::Wait(wait) => {
|
||||
let duration = wait.duration.unwrap_or_default();
|
||||
if duration.seconds < 0 || duration.nanos < 0 {
|
||||
return Err(ToolToAIAgentActionError::InvalidComputerUseWaitDuration);
|
||||
}
|
||||
let duration = Duration::from_secs(duration.seconds as u64)
|
||||
+ Duration::from_nanos(duration.nanos as u64);
|
||||
Ok(computer_use::Action::Wait(duration))
|
||||
}
|
||||
use_computer::action::Type::TypeText(type_text) => {
|
||||
Ok(computer_use::Action::TypeText {
|
||||
text: type_text.text,
|
||||
})
|
||||
}
|
||||
use_computer::action::Type::KeyDown(key_down) => {
|
||||
let key = convert_key(key_down.key)?;
|
||||
Ok(computer_use::Action::KeyDown { key })
|
||||
}
|
||||
use_computer::action::Type::KeyUp(key_up) => {
|
||||
let key = convert_key(key_up.key)?;
|
||||
Ok(computer_use::Action::KeyUp { key })
|
||||
}
|
||||
}
|
||||
})
|
||||
.try_collect()?;
|
||||
let screenshot_params = value
|
||||
.post_actions_screenshot_params
|
||||
.map(convert_screenshot_params);
|
||||
Ok(AIAgentActionType::UseComputer(UseComputerRequest {
|
||||
action_summary: value.action_summary,
|
||||
actions,
|
||||
screenshot_params,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::RequestComputerUse> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::RequestComputerUse) -> Self {
|
||||
use crate::agent::action::RequestComputerUseRequest;
|
||||
AIAgentActionType::RequestComputerUse(RequestComputerUseRequest {
|
||||
task_summary: value.task_summary,
|
||||
screenshot_params: value.screenshot_params.map(convert_screenshot_params),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<api::message::tool_call::ReadSkill> for AIAgentActionType {
|
||||
type Error = ToolToAIAgentActionError;
|
||||
|
||||
fn try_from(value: api::message::tool_call::ReadSkill) -> Result<Self, Self::Error> {
|
||||
match value.skill_reference {
|
||||
Some(reference) => Ok(AIAgentActionType::ReadSkill(ReadSkillRequest {
|
||||
skill: SkillReference::from(reference),
|
||||
})),
|
||||
None => Err(ToolToAIAgentActionError::MissingSkillReference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::FetchConversation> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::FetchConversation) -> Self {
|
||||
AIAgentActionType::FetchConversation {
|
||||
conversation_id: value.conversation_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::read_skill::SkillReference> for SkillReference {
|
||||
fn from(value: api::message::tool_call::read_skill::SkillReference) -> Self {
|
||||
use warp_multi_agent_api::message::tool_call::read_skill::SkillReference as ApiSkillReference;
|
||||
match value {
|
||||
ApiSkillReference::SkillPath(skill_path) => {
|
||||
SkillReference::Path(PathBuf::from(skill_path))
|
||||
}
|
||||
ApiSkillReference::BundledSkillId(id) => SkillReference::BundledSkillId(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts API ScreenshotParams to the internal computer_use type.
|
||||
fn convert_screenshot_params(
|
||||
params: api::message::tool_call::ScreenshotParams,
|
||||
) -> computer_use::ScreenshotParams {
|
||||
let region = params
|
||||
.region
|
||||
.and_then(|r| match (r.top_left.as_ref(), r.bottom_right.as_ref()) {
|
||||
(Some(tl), Some(br)) => Some(computer_use::ScreenshotRegion {
|
||||
top_left: computer_use::Vector2I::new(tl.x, tl.y),
|
||||
bottom_right: computer_use::Vector2I::new(br.x, br.y),
|
||||
}),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
computer_use::ScreenshotParams {
|
||||
max_long_edge_px: (params.max_long_edge_px > 0).then_some(params.max_long_edge_px as usize),
|
||||
max_total_px: (params.max_total_px > 0).then_some(params.max_total_px as usize),
|
||||
region,
|
||||
}
|
||||
}
|
||||
|
||||
fn coordinates_to_vec(
|
||||
coords: Option<&api::Coordinates>,
|
||||
) -> Result<computer_use::Vector2I, ToolToAIAgentActionError> {
|
||||
match coords {
|
||||
Some(coords) => Ok(computer_use::Vector2I::new(coords.x, coords.y)),
|
||||
None => Err(ToolToAIAgentActionError::MissingComputerUseCoordinates),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_computer_use_button(
|
||||
api_button: api::message::tool_call::use_computer::action::MouseButton,
|
||||
) -> computer_use::MouseButton {
|
||||
use api::message::tool_call::use_computer::action::MouseButton;
|
||||
match api_button {
|
||||
MouseButton::Left => computer_use::MouseButton::Left,
|
||||
MouseButton::Right => computer_use::MouseButton::Right,
|
||||
MouseButton::Middle => computer_use::MouseButton::Middle,
|
||||
MouseButton::Back => computer_use::MouseButton::Back,
|
||||
MouseButton::Forward => computer_use::MouseButton::Forward,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_scroll_direction(
|
||||
api_direction: api::message::tool_call::use_computer::action::mouse_wheel::Direction,
|
||||
) -> computer_use::ScrollDirection {
|
||||
use api::message::tool_call::use_computer::action::mouse_wheel::Direction;
|
||||
match api_direction {
|
||||
Direction::Up => computer_use::ScrollDirection::Up,
|
||||
Direction::Down => computer_use::ScrollDirection::Down,
|
||||
Direction::Left => computer_use::ScrollDirection::Left,
|
||||
Direction::Right => computer_use::ScrollDirection::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_scroll_distance(
|
||||
api_distance: Option<api::message::tool_call::use_computer::action::mouse_wheel::Distance>,
|
||||
) -> Result<computer_use::ScrollDistance, ToolToAIAgentActionError> {
|
||||
use api::message::tool_call::use_computer::action::mouse_wheel::Distance;
|
||||
match api_distance {
|
||||
Some(Distance::Pixels(pixels)) => Ok(computer_use::ScrollDistance::Pixels(pixels)),
|
||||
Some(Distance::Clicks(clicks)) => Ok(computer_use::ScrollDistance::Clicks(clicks)),
|
||||
None => Err(ToolToAIAgentActionError::MissingComputerUseScrollDistance),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_key(
|
||||
api_key: Option<api::message::tool_call::use_computer::action::Key>,
|
||||
) -> Result<computer_use::Key, ToolToAIAgentActionError> {
|
||||
use api::message::tool_call::use_computer::action::key::Data;
|
||||
let key = api_key.ok_or(ToolToAIAgentActionError::MissingComputerUseKey)?;
|
||||
match key.data {
|
||||
Some(Data::Keycode(keycode)) => Ok(computer_use::Key::Keycode(keycode)),
|
||||
Some(Data::Char(char_str)) => {
|
||||
let mut chars = char_str.chars();
|
||||
let ch = chars
|
||||
.next()
|
||||
.ok_or(ToolToAIAgentActionError::InvalidComputerUseCharKey)?;
|
||||
if chars.next().is_some() {
|
||||
return Err(ToolToAIAgentActionError::InvalidComputerUseCharKey);
|
||||
}
|
||||
Ok(computer_use::Key::Char(ch))
|
||||
}
|
||||
None => Err(ToolToAIAgentActionError::MissingComputerUseKey),
|
||||
}
|
||||
}
|
||||
|
||||
fn prost_to_serde_json(x: prost_types::Value) -> Result<serde_json::Value, String> {
|
||||
use prost_types::value::Kind::*;
|
||||
use serde_json::Value::*;
|
||||
|
||||
let Some(kind) = x.kind else {
|
||||
return Err("google.protobuf.Value kind was None".to_string());
|
||||
};
|
||||
|
||||
Ok(match kind {
|
||||
NullValue(_) => Null,
|
||||
BoolValue(v) => Bool(v),
|
||||
NumberValue(n) => Number(
|
||||
serde_json::Number::from_f64(n)
|
||||
.ok_or_else(|| format!("float {n} is not valid JSON number"))?,
|
||||
),
|
||||
StringValue(s) => String(s),
|
||||
ListValue(l) => Array(
|
||||
l.values
|
||||
.into_iter()
|
||||
.map(prost_to_serde_json)
|
||||
.collect::<Result<Vec<_>, std::string::String>>()?,
|
||||
),
|
||||
StructValue(v) => Object(
|
||||
v.fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| prost_to_serde_json(v).map(|v| (k, v)))
|
||||
.collect::<Result<serde_json::Map<_, _>, std::string::String>>()?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper trait to easily convert default values to `None`.
|
||||
/// With `prost`, scalar types are converted to their default values instead
|
||||
/// of `None` if the type is unset. This trait allows a more natural
|
||||
/// conversion to better denote if a value should be `Some` (indicating it was set)
|
||||
/// or `None` (indicating it was unset).
|
||||
///
|
||||
///
|
||||
/// NOTE: Consumers should use this with caution as it only makes sense where the default
|
||||
/// value of the type isn't a reasonable value and `None` makes more sense instead.
|
||||
trait NoneIfDefault
|
||||
where
|
||||
Self: Sized + Default,
|
||||
{
|
||||
fn none_if_default(self) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl NoneIfDefault for String {
|
||||
fn none_if_default(self) -> Option<Self> {
|
||||
if self == Self::default() {
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::InsertReviewComments> for AIAgentActionType {
|
||||
fn from(value: api::message::tool_call::InsertReviewComments) -> Self {
|
||||
AIAgentActionType::InsertCodeReviewComments {
|
||||
repo_path: PathBuf::from(value.repo_path),
|
||||
comments: value.comments.into_iter().map(Into::into).collect(),
|
||||
base_branch: value.base_branch.none_if_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::insert_review_comments::CommentSide> for CommentSide {
|
||||
fn from(value: api::message::tool_call::insert_review_comments::CommentSide) -> Self {
|
||||
match value {
|
||||
api::message::tool_call::insert_review_comments::CommentSide::New => CommentSide::Right,
|
||||
api::message::tool_call::insert_review_comments::CommentSide::Old => CommentSide::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::tool_call::insert_review_comments::Comment> for InsertReviewComment {
|
||||
fn from(value: api::message::tool_call::insert_review_comments::Comment) -> Self {
|
||||
let location = value.location.map(|loc| InsertedCommentLocation {
|
||||
relative_file_path: loc.file_path,
|
||||
line: loc.line.and_then(|comment_line_range| {
|
||||
let side = comment_line_range.side().into();
|
||||
let diff_hunk = comment_line_range.diff_hunk;
|
||||
comment_line_range.range.map(|r| InsertedCommentLine {
|
||||
comment_line_range: r.start as usize..r.end as usize,
|
||||
diff_hunk_line_range: r.start as usize..r.end as usize,
|
||||
diff_hunk_text: diff_hunk,
|
||||
side: Some(side),
|
||||
})
|
||||
}),
|
||||
});
|
||||
|
||||
InsertReviewComment {
|
||||
comment_id: value.comment_id,
|
||||
author: value.author,
|
||||
last_modified_timestamp: value.last_modified_timestamp,
|
||||
comment_body: value.comment_body,
|
||||
parent_comment_id: if value.parent_comment_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.parent_comment_id)
|
||||
},
|
||||
comment_location: location,
|
||||
html_url: value.html_url.none_if_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
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,
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
#[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>,
|
||||
},
|
||||
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 }
|
||||
}
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ask_user_question_skipped_by_auto_approve_converts_to_skipped_answers() {
|
||||
let result = api::request::input::tool_call_result::Result::from(
|
||||
AskUserQuestionResult::SkippedByAutoApprove {
|
||||
question_ids: vec!["q1".to_string(), "q2".to_string()],
|
||||
},
|
||||
);
|
||||
|
||||
let api::request::input::tool_call_result::Result::AskUserQuestion(result) = result else {
|
||||
panic!("expected ask_user_question result");
|
||||
};
|
||||
|
||||
let Some(api::ask_user_question_result::Result::Success(success)) = result.result else {
|
||||
panic!("expected success result");
|
||||
};
|
||||
|
||||
assert_eq!(success.answers.len(), 2);
|
||||
assert_eq!(success.answers[0].question_id, "q1");
|
||||
assert_eq!(success.answers[1].question_id, "q2");
|
||||
assert!(matches!(
|
||||
success.answers[0].answer,
|
||||
Some(AskUserQuestionAnswer::Skipped(()))
|
||||
));
|
||||
assert!(matches!(
|
||||
success.answers[1].answer,
|
||||
Some(AskUserQuestionAnswer::Skipped(()))
|
||||
));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
use super::{StartAgentResult, StartAgentVersion};
|
||||
|
||||
#[test]
|
||||
fn deserializes_legacy_start_agent_success_without_version_as_v1() {
|
||||
let result: StartAgentResult =
|
||||
serde_json::from_value(serde_json::json!({ "Success": { "agent_id": "agent-1" } }))
|
||||
.expect("legacy start-agent success should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
StartAgentResult::Success {
|
||||
agent_id: "agent-1".to_string(),
|
||||
version: StartAgentVersion::V1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_legacy_start_agent_error_without_version_as_v1() {
|
||||
let result: StartAgentResult =
|
||||
serde_json::from_value(serde_json::json!({ "Error": { "error": "boom" } }))
|
||||
.expect("legacy start-agent error should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
StartAgentResult::Error {
|
||||
error: "boom".to_string(),
|
||||
version: StartAgentVersion::V1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_legacy_start_agent_cancelled_without_version_as_v1() {
|
||||
let result: StartAgentResult = serde_json::from_value(serde_json::json!({ "Cancelled": {} }))
|
||||
.expect("legacy start-agent cancellation should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
StartAgentResult::Cancelled {
|
||||
version: StartAgentVersion::V1,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
/// A citation listed in an AI response.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum AIAgentCitation {
|
||||
WarpDriveObject { uid: String },
|
||||
WarpDocumentation { path: String },
|
||||
WebPage { url: String },
|
||||
}
|
||||
|
||||
impl Display for AIAgentCitation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AIAgentCitation::WarpDriveObject { uid } => {
|
||||
write!(f, "Warp Drive Object: {uid}")
|
||||
}
|
||||
AIAgentCitation::WarpDocumentation { path } => {
|
||||
write!(f, "Warp Documentation: {path}")
|
||||
}
|
||||
AIAgentCitation::WebPage { url } => {
|
||||
write!(f, "Web Page: {url}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for Citation conversion errors
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Unknown citation type")]
|
||||
pub struct UnknownCitationTypeError;
|
||||
|
||||
impl TryFrom<api::Citation> for AIAgentCitation {
|
||||
type Error = UnknownCitationTypeError;
|
||||
|
||||
fn try_from(citation: api::Citation) -> Result<Self, Self::Error> {
|
||||
let doc_type = api::DocumentType::try_from(citation.document_type)
|
||||
.unwrap_or(api::DocumentType::Unknown);
|
||||
|
||||
match doc_type {
|
||||
api::DocumentType::WarpDriveWorkflow
|
||||
| api::DocumentType::WarpDriveNotebook
|
||||
| api::DocumentType::WarpDriveEnvVar
|
||||
| api::DocumentType::Rule => Ok(AIAgentCitation::WarpDriveObject {
|
||||
uid: citation.document_id,
|
||||
}),
|
||||
api::DocumentType::WarpDocumentation => Ok(AIAgentCitation::WarpDocumentation {
|
||||
path: citation.document_id,
|
||||
}),
|
||||
api::DocumentType::WebPage => Ok(AIAgentCitation::WebPage {
|
||||
url: citation.document_id,
|
||||
}),
|
||||
api::DocumentType::Unknown => Err(UnknownCitationTypeError),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConvertToAPITypeError {
|
||||
/// There is no API type for the given value.
|
||||
///
|
||||
/// This means the value just be ignored during request construction.
|
||||
#[error("Ignoring value when constructing API type.")]
|
||||
Ignore,
|
||||
#[error("Conversion from type {0} is unimplemented.")]
|
||||
Unimplemented(String),
|
||||
#[error("Encountered error converting types for MultiAgentApi request: {0:?}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// Unexpected errors when trying to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ToolToAIAgentActionError {
|
||||
#[error("Missing tool")]
|
||||
MissingTool,
|
||||
#[error("Could not parse args for MCP tool call: {0}")]
|
||||
CallMCPToolArgsError(String),
|
||||
#[error("Error converting suggest prompt tool call: {0}")]
|
||||
SuggestPromptError(String),
|
||||
#[error("Required coordinates for computer use action were missing")]
|
||||
MissingComputerUseCoordinates,
|
||||
#[error("Required scroll distance for mouse wheel action was missing")]
|
||||
MissingComputerUseScrollDistance,
|
||||
#[error("Received missing computer use action type")]
|
||||
MissingComputerUseActionType,
|
||||
#[error("Wait duration must be non-negative")]
|
||||
InvalidComputerUseWaitDuration,
|
||||
#[error("Required key for KeyDown/KeyUp action was missing")]
|
||||
MissingComputerUseKey,
|
||||
#[error("Character key was empty")]
|
||||
InvalidComputerUseCharKey,
|
||||
#[error("Received unexpected tool")]
|
||||
UnexpectedTool,
|
||||
#[error("Missing required reference for read skill tool call")]
|
||||
MissingSkillReference,
|
||||
#[error("Missing required file reference for upload artifact tool call")]
|
||||
MissingUploadArtifactFileReference,
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use warp_terminal::shell::ShellLaunchData;
|
||||
|
||||
use crate::agent::action_result::FileContext;
|
||||
use crate::{index::locations::CodeContextLocation, paths::shell_native_absolute_path};
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct FileLocations {
|
||||
pub name: String,
|
||||
pub lines: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl FileLocations {
|
||||
/// Convert file locations to a user readable format.
|
||||
pub fn to_user_message(
|
||||
&self,
|
||||
shell_launch_data: Option<&ShellLaunchData>,
|
||||
current_working_directory: Option<&String>,
|
||||
file_line_count: Option<usize>,
|
||||
) -> String {
|
||||
let absolute_path =
|
||||
shell_native_absolute_path(&self.name, shell_launch_data, current_working_directory);
|
||||
|
||||
if self.lines.is_empty() {
|
||||
return absolute_path;
|
||||
}
|
||||
|
||||
let line_ranges = self
|
||||
.lines
|
||||
.iter()
|
||||
.filter_map(|range| {
|
||||
let (start, end) = match file_line_count {
|
||||
Some(line_count) => (
|
||||
std::cmp::min(range.start, line_count),
|
||||
std::cmp::min(range.end, line_count),
|
||||
),
|
||||
None => (range.start, range.end),
|
||||
};
|
||||
|
||||
if start == 1 && Some(end) == file_line_count {
|
||||
// don't show ranges that are just the entire file
|
||||
None
|
||||
} else {
|
||||
Some(format!("{start}-{end}"))
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
if line_ranges.is_empty() {
|
||||
absolute_path
|
||||
} else {
|
||||
format!("{} ({})", absolute_path, line_ranges.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Expands the line ranges (if any) in both directions by `context_line`. Then the line ranges are sorted
|
||||
/// and merged if there are overlaps.
|
||||
pub fn expand_surrounding_context(&mut self, context_line: usize) {
|
||||
if self.lines.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Expand each range by context_line in both directions
|
||||
let mut expanded: Vec<Range<usize>> = self
|
||||
.lines
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let start = r.start.saturating_sub(context_line);
|
||||
let end = r.end + context_line;
|
||||
start..end
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by start
|
||||
expanded.sort_by_key(|r| r.start);
|
||||
|
||||
// Merge overlapping or adjacent ranges
|
||||
let mut merged: Vec<Range<usize>> = Vec::with_capacity(expanded.len());
|
||||
for range in expanded {
|
||||
if let Some(last) = merged.last_mut() {
|
||||
if range.start <= last.end {
|
||||
last.end = last.end.max(range.end);
|
||||
} else {
|
||||
merged.push(range);
|
||||
}
|
||||
} else {
|
||||
merged.push(range);
|
||||
}
|
||||
}
|
||||
self.lines = merged;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CodeContextLocation> for FileLocations {
|
||||
fn from(location: &CodeContextLocation) -> Self {
|
||||
match location {
|
||||
CodeContextLocation::WholeFile(path) => Self {
|
||||
name: path.to_string_lossy().to_string(),
|
||||
lines: vec![],
|
||||
},
|
||||
CodeContextLocation::Fragment(fragment) => Self {
|
||||
name: fragment.path.to_string_lossy().to_string(),
|
||||
lines: fragment.line_ranges.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups a slice of [`FileContext`]s by file name, collecting line ranges from
|
||||
/// fragments of the same file. Returns one display string per unique file,
|
||||
/// preserving first-occurrence order.
|
||||
pub fn group_file_contexts_for_display(
|
||||
file_contexts: &[FileContext],
|
||||
shell_launch_data: Option<&ShellLaunchData>,
|
||||
current_working_directory: Option<&String>,
|
||||
) -> Vec<String> {
|
||||
let mut order: Vec<String> = Vec::new();
|
||||
let mut groups: HashMap<String, Vec<Range<usize>>> = HashMap::new();
|
||||
|
||||
for fc in file_contexts {
|
||||
let entry = groups.entry(fc.file_name.clone()).or_insert_with(|| {
|
||||
order.push(fc.file_name.clone());
|
||||
Vec::new()
|
||||
});
|
||||
if let Some(range) = &fc.line_range {
|
||||
entry.push(range.clone());
|
||||
}
|
||||
}
|
||||
|
||||
order
|
||||
.iter()
|
||||
.map(|file_name| {
|
||||
let ranges = groups.get(file_name).unwrap();
|
||||
let mut sorted = ranges.clone();
|
||||
sorted.sort_by_key(|r| (r.start, r.end));
|
||||
let locations = FileLocations {
|
||||
name: file_name.clone(),
|
||||
lines: sorted,
|
||||
};
|
||||
locations.to_user_message(shell_launch_data, current_working_directory, None)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod action;
|
||||
pub mod action_result;
|
||||
mod citation;
|
||||
pub mod convert;
|
||||
pub mod file_locations;
|
||||
|
||||
pub use citation::{AIAgentCitation, UnknownCitationTypeError};
|
||||
pub use file_locations::{group_file_contexts_for_display, FileLocations};
|
||||
Reference in New Issue
Block a user