Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,944 @@
|
||||
//! Conversions from MAA API types to application types.
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::ai::agent::api::convert_conversation::{
|
||||
convert_input_context, convert_tool_call_result_to_input,
|
||||
};
|
||||
use crate::ai::agent::comment::CodeReview;
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::todos::AIAgentTodoList;
|
||||
use crate::ai::agent::{
|
||||
util::parse_markdown_into_text_and_code_sections, AIAgentAction, AIAgentActionType,
|
||||
AIAgentCitation, AIAgentInput, AIAgentOutputMessage, AIAgentText, AIAgentTodo,
|
||||
ArtifactCreatedData, MessageId, StartAgentExecutionMode, SuggestedAgentModeWorkflow,
|
||||
SuggestedRule, Suggestions, TodoOperation,
|
||||
};
|
||||
use crate::ai::agent::{
|
||||
CloneRepositoryURL, SubagentCall, SubagentType, SummarizationType, WebFetchStatus,
|
||||
WebSearchStatus,
|
||||
};
|
||||
use crate::ai::artifact_download::sanitized_basename;
|
||||
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
|
||||
use ai::agent::action::LifecycleEventType as StartAgentLifecycleEventType;
|
||||
use ai::agent::action_result::StartAgentVersion;
|
||||
use ai::agent::convert::ToolToAIAgentActionError;
|
||||
use ai::agent::UnknownCitationTypeError;
|
||||
use ai::skills::SkillReference;
|
||||
use api::ask_user_question::question::QuestionType;
|
||||
use warp_core::channel::ChannelState;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::agent::{AIAgentAttachment, UserQueryMode};
|
||||
|
||||
impl TryFrom<api::Attachment> for AIAgentAttachment {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(attachment: api::Attachment) -> Result<Self, Self::Error> {
|
||||
match attachment.value {
|
||||
Some(api::attachment::Value::FilePathReference(fpr)) => {
|
||||
Ok(AIAgentAttachment::FilePathReference {
|
||||
file_id: String::new(),
|
||||
file_name: fpr
|
||||
.file_path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or(&fpr.file_path)
|
||||
.to_string(),
|
||||
file_path: fpr.file_path,
|
||||
})
|
||||
}
|
||||
_ => anyhow::bail!("Unsupported attachment type for conversion"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts proto UserQueryMode to the internal UserQueryMode type
|
||||
pub(crate) fn convert_user_query_mode(mode: Option<&api::UserQueryMode>) -> UserQueryMode {
|
||||
let Some(mode) = mode else {
|
||||
return UserQueryMode::default();
|
||||
};
|
||||
|
||||
match &mode.r#type {
|
||||
Some(api::user_query_mode::Type::Plan(_)) => UserQueryMode::Plan,
|
||||
Some(api::user_query_mode::Type::Orchestrate(_)) => UserQueryMode::Orchestrate,
|
||||
None => UserQueryMode::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_start_agent_lifecycle_event_type(
|
||||
event_type: i32,
|
||||
) -> Option<StartAgentLifecycleEventType> {
|
||||
let event_type = StartAgentLifecycleEventType::try_from(event_type).ok()?;
|
||||
(event_type != StartAgentLifecycleEventType::Unspecified).then_some(event_type)
|
||||
}
|
||||
|
||||
fn convert_start_agent_v2_harness_type(
|
||||
harness: Option<api::start_agent_v2::execution_mode::Harness>,
|
||||
) -> Option<String> {
|
||||
harness
|
||||
.map(|harness| harness.r#type)
|
||||
.filter(|harness_type| !harness_type.trim().is_empty())
|
||||
}
|
||||
|
||||
fn convert_start_agent_execution_mode(
|
||||
execution_mode: Option<api::start_agent::ExecutionMode>,
|
||||
) -> StartAgentExecutionMode {
|
||||
match execution_mode.and_then(|execution_mode| execution_mode.mode) {
|
||||
Some(api::start_agent::execution_mode::Mode::Remote(remote)) => {
|
||||
StartAgentExecutionMode::remote_with_defaults(remote.environment_id)
|
||||
}
|
||||
Some(api::start_agent::execution_mode::Mode::Local(_)) | None => {
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_start_agent_v2_execution_mode(
|
||||
execution_mode: Option<api::start_agent_v2::ExecutionMode>,
|
||||
) -> StartAgentExecutionMode {
|
||||
match execution_mode.and_then(|execution_mode| execution_mode.mode) {
|
||||
Some(api::start_agent_v2::execution_mode::Mode::Remote(remote)) => {
|
||||
StartAgentExecutionMode::Remote {
|
||||
environment_id: remote.environment_id,
|
||||
skill_references: remote
|
||||
.skills
|
||||
.into_iter()
|
||||
.filter_map(convert_skill_reference)
|
||||
.collect(),
|
||||
model_id: remote.model_id,
|
||||
computer_use_enabled: remote.computer_use_enabled,
|
||||
worker_host: remote.worker_host,
|
||||
harness_type: convert_start_agent_v2_harness_type(remote.harness)
|
||||
.unwrap_or_default(),
|
||||
title: remote.title,
|
||||
}
|
||||
}
|
||||
Some(api::start_agent_v2::execution_mode::Mode::Local(local)) => {
|
||||
convert_start_agent_v2_harness_type(local.harness)
|
||||
.map(StartAgentExecutionMode::local_harness)
|
||||
.unwrap_or_else(StartAgentExecutionMode::local_with_defaults)
|
||||
}
|
||||
None => StartAgentExecutionMode::local_with_defaults(),
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_skill_reference(skill_ref: api::SkillRef) -> Option<SkillReference> {
|
||||
match skill_ref.skill_reference {
|
||||
Some(api::skill_ref::SkillReference::Path(path)) => Some(SkillReference::Path(path.into())),
|
||||
Some(api::skill_ref::SkillReference::BundledSkillId(id)) => {
|
||||
Some(SkillReference::BundledSkillId(id))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unexpected errors when trying to convert an [`api::Message`] to an [`AIAgentOutputMessage`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MessageToAIAgentOutputMessageError {
|
||||
#[error("Missing expected message")]
|
||||
MissingMessage,
|
||||
#[error("Error converting tool to action: {0:?}")]
|
||||
ToolError(#[from] ToolToAIAgentActionError),
|
||||
#[error("Error converting citation: {0:?}")]
|
||||
CitationError(#[from] UnknownCitationTypeError),
|
||||
}
|
||||
|
||||
/// Successful result when trying to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum MaybeAIAgentOutputMessage {
|
||||
/// There is a mapping to a client output message.
|
||||
Message(AIAgentOutputMessage),
|
||||
/// We tried to parse a message that we don't care about.
|
||||
NoClientRepresentation,
|
||||
}
|
||||
|
||||
/// Successful result when trying to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum MaybeAIAgentAction {
|
||||
/// There is a mapping to a client action.
|
||||
Action(AIAgentAction),
|
||||
Subagent(SubagentCall),
|
||||
/// We tried to parse a tool call that we don't care about.
|
||||
NoClientRepresentation,
|
||||
}
|
||||
|
||||
pub struct ConversionParams<'a> {
|
||||
pub task_id: &'a TaskId,
|
||||
pub current_todo_list: Option<&'a AIAgentTodoList>,
|
||||
pub active_code_review: Option<&'a CodeReview>,
|
||||
}
|
||||
|
||||
/// Trait for converting an [`api::Message`] to an [`AIAgentOutputMessage`].
|
||||
pub trait ConvertAPIMessageToClientOutputMessage {
|
||||
fn to_client_output_message(
|
||||
self,
|
||||
params: ConversionParams,
|
||||
) -> Result<MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError>;
|
||||
}
|
||||
|
||||
impl ConvertAPIMessageToClientOutputMessage for api::Message {
|
||||
fn to_client_output_message(
|
||||
self,
|
||||
params: ConversionParams,
|
||||
) -> Result<MaybeAIAgentOutputMessage, MessageToAIAgentOutputMessageError> {
|
||||
let Some(message) = self.message else {
|
||||
// In shared-session streams we can receive skeleton placeholder task messages without payloads.
|
||||
// Treat them as having no client representation rather than erroring and aborting ingestion entirely.
|
||||
return Ok(MaybeAIAgentOutputMessage::NoClientRepresentation);
|
||||
};
|
||||
|
||||
let citations = self
|
||||
.citations
|
||||
.iter()
|
||||
.map(|citation| (*citation).clone().try_into())
|
||||
.collect::<Result<Vec<AIAgentCitation>, UnknownCitationTypeError>>()?;
|
||||
|
||||
match message {
|
||||
api::message::Message::AgentOutput(output) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::text(MessageId::new(self.id), output.into())
|
||||
.with_citations(citations),
|
||||
)),
|
||||
api::message::Message::AgentReasoning(reasoning) => {
|
||||
let duration = reasoning
|
||||
.finished_duration
|
||||
.map(|d| Duration::from_secs(d.seconds as u64));
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::reasoning(
|
||||
MessageId::new(self.id),
|
||||
reasoning.into(),
|
||||
duration,
|
||||
),
|
||||
))
|
||||
}
|
||||
api::message::Message::ToolCall(tool_call) => match tool_call.to_action(params)? {
|
||||
MaybeAIAgentAction::Action(action) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::action(MessageId::new(self.id), action)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
MaybeAIAgentAction::Subagent(subagent) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::subagent(MessageId::new(self.id), subagent)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
MaybeAIAgentAction::NoClientRepresentation => {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
},
|
||||
api::message::Message::WebSearch(web_search) => {
|
||||
let status = match &web_search.status {
|
||||
Some(api::message::web_search::Status {
|
||||
r#type: Some(api::message::web_search::status::Type::Searching(searching)),
|
||||
}) => WebSearchStatus::Searching {
|
||||
query: if searching.query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(searching.query.clone())
|
||||
},
|
||||
},
|
||||
Some(api::message::web_search::Status {
|
||||
r#type: Some(api::message::web_search::status::Type::Success(success)),
|
||||
}) => WebSearchStatus::Success {
|
||||
query: success.query.clone(),
|
||||
pages: success
|
||||
.pages
|
||||
.iter()
|
||||
.map(|p| (p.url.clone(), p.title.clone()))
|
||||
.collect(),
|
||||
},
|
||||
Some(api::message::web_search::Status {
|
||||
r#type: Some(api::message::web_search::status::Type::Error(_)),
|
||||
}) => {
|
||||
// Error type doesn't have a query field currently, use empty string
|
||||
WebSearchStatus::Error {
|
||||
query: String::new(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Unknown or missing status
|
||||
return Ok(MaybeAIAgentOutputMessage::NoClientRepresentation);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::web_search(MessageId::new(self.id), status)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
api::message::Message::WebFetch(web_fetch) => {
|
||||
let status = match &web_fetch.status {
|
||||
Some(api::message::web_fetch::Status {
|
||||
r#type: Some(api::message::web_fetch::status::Type::Fetching(fetching)),
|
||||
}) => WebFetchStatus::Fetching {
|
||||
urls: fetching.urls.clone(),
|
||||
},
|
||||
Some(api::message::web_fetch::Status {
|
||||
r#type: Some(api::message::web_fetch::status::Type::Success(success)),
|
||||
}) => WebFetchStatus::Success {
|
||||
pages: success
|
||||
.pages
|
||||
.iter()
|
||||
.map(|p| (p.url.clone(), p.title.clone(), p.success))
|
||||
.collect(),
|
||||
},
|
||||
Some(api::message::web_fetch::Status {
|
||||
r#type: Some(api::message::web_fetch::status::Type::Error(_)),
|
||||
}) => WebFetchStatus::Error,
|
||||
_ => {
|
||||
// Unknown or missing status
|
||||
return Ok(MaybeAIAgentOutputMessage::NoClientRepresentation);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::web_fetch(MessageId::new(self.id), status)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
api::message::Message::ModelUsed(_) => {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
api::message::Message::UpdateTodos(update_todos) => {
|
||||
if let Some(operation) = update_todos.operation {
|
||||
match operation {
|
||||
api::message::update_todos::Operation::CreateTodoList(create_todo_list) => {
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::todo_operation(
|
||||
MessageId::new(self.id),
|
||||
TodoOperation::UpdateTodos {
|
||||
todos: create_todo_list
|
||||
.initial_todos
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
api::message::update_todos::Operation::UpdatePendingTodos(
|
||||
update_pending_todos,
|
||||
) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::todo_operation(
|
||||
MessageId::new(self.id),
|
||||
TodoOperation::UpdateTodos {
|
||||
todos: params
|
||||
.current_todo_list
|
||||
.iter()
|
||||
.flat_map(|list| list.completed_items().iter().cloned())
|
||||
.chain(
|
||||
update_pending_todos
|
||||
.updated_pending_todos
|
||||
.into_iter()
|
||||
.map(Into::into),
|
||||
)
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
api::message::update_todos::Operation::MarkTodosCompleted(
|
||||
mark_todos_completed,
|
||||
) => {
|
||||
if mark_todos_completed.todo_ids.is_empty() {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
} else {
|
||||
// This is a mark as completed operation
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::todo_operation(
|
||||
MessageId::new(self.id),
|
||||
TodoOperation::MarkAsCompleted {
|
||||
completed_todos: mark_todos_completed
|
||||
.todo_ids
|
||||
.into_iter()
|
||||
.filter_map(|todo_id| {
|
||||
params.current_todo_list.and_then(|todo_list| {
|
||||
todo_list
|
||||
.completed_items()
|
||||
.iter()
|
||||
.find(|item| {
|
||||
item.id.as_ref() == todo_id.as_str()
|
||||
})
|
||||
.cloned()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
api::message::Message::Summarization(summarization) => {
|
||||
let duration = summarization
|
||||
.finished_duration
|
||||
.map(|d| Duration::from_secs(d.seconds as u64));
|
||||
let (text, summarization_type, token_count) = match summarization.summary_type {
|
||||
Some(api::message::summarization::SummaryType::ConversationSummary(
|
||||
conv_summary,
|
||||
)) => {
|
||||
let token_count = if conv_summary.token_count > 0 {
|
||||
Some(conv_summary.token_count as u32)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let text = if !conv_summary.summary.is_empty() {
|
||||
AIAgentText {
|
||||
sections: parse_markdown_into_text_and_code_sections(
|
||||
&conv_summary.summary,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
AIAgentText { sections: vec![] }
|
||||
};
|
||||
(text, SummarizationType::ConversationSummary, token_count)
|
||||
}
|
||||
Some(api::message::summarization::SummaryType::ToolCallResultSummary(_)) => (
|
||||
AIAgentText { sections: vec![] },
|
||||
SummarizationType::ToolCallResultSummary,
|
||||
None,
|
||||
),
|
||||
None => {
|
||||
// Default to ConversationSummary if not specified
|
||||
(
|
||||
AIAgentText { sections: vec![] },
|
||||
SummarizationType::ConversationSummary,
|
||||
None,
|
||||
)
|
||||
}
|
||||
};
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::summarization(
|
||||
MessageId::new(self.id),
|
||||
text,
|
||||
duration,
|
||||
summarization_type,
|
||||
token_count,
|
||||
),
|
||||
))
|
||||
}
|
||||
api::message::Message::UpdateReviewComments(update_comments) => {
|
||||
if let Some(operation) = update_comments.operation {
|
||||
match operation {
|
||||
api::message::update_review_comments::Operation::AddressReviewComments(
|
||||
address_comments,
|
||||
) => {
|
||||
if let Some(current_comments) = params.active_code_review {
|
||||
let addressed_comments = current_comments
|
||||
.addressed_comments
|
||||
.iter()
|
||||
.filter(|comment| {
|
||||
address_comments
|
||||
.comment_ids
|
||||
.iter()
|
||||
.any(|id| id == &comment.id.to_string())
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::comments_addressed(
|
||||
MessageId::new(self.id),
|
||||
addressed_comments,
|
||||
)
|
||||
.with_citations(citations),
|
||||
))
|
||||
} else {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
api::message::Message::DebugOutput(debug_output) => {
|
||||
if ChannelState::enable_debug_features() {
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::debug_output(
|
||||
MessageId::new(self.id),
|
||||
debug_output.text,
|
||||
),
|
||||
))
|
||||
} else {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
api::message::Message::ArtifactEvent(artifact_event) => match artifact_event.event {
|
||||
Some(api::message::artifact_event::Event::Created(artifact_created)) => {
|
||||
match artifact_created.artifact {
|
||||
Some(
|
||||
api::message::artifact_event::artifact_created::Artifact::PullRequest(
|
||||
pr,
|
||||
),
|
||||
) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::artifact_created(
|
||||
MessageId::new(self.id),
|
||||
ArtifactCreatedData::PullRequest {
|
||||
url: pr.url,
|
||||
branch: pr.branch,
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
Some(
|
||||
api::message::artifact_event::artifact_created::Artifact::Screenshot(
|
||||
screenshot,
|
||||
),
|
||||
) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::artifact_created(
|
||||
MessageId::new(self.id),
|
||||
ArtifactCreatedData::Screenshot {
|
||||
artifact_uid: screenshot.artifact_uid,
|
||||
mime_type: screenshot.mime_type,
|
||||
description: if screenshot.description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(screenshot.description)
|
||||
},
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
Some(api::message::artifact_event::artifact_created::Artifact::File(
|
||||
file,
|
||||
)) => Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::artifact_created(
|
||||
MessageId::new(self.id),
|
||||
ArtifactCreatedData::File {
|
||||
artifact_uid: file.artifact_uid,
|
||||
filename: sanitized_basename(&file.filepath)
|
||||
.unwrap_or_else(|| file.filepath.clone()),
|
||||
filepath: file.filepath,
|
||||
mime_type: file.mime_type,
|
||||
description: if file.description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(file.description)
|
||||
},
|
||||
size_bytes: file.size_bytes,
|
||||
},
|
||||
)
|
||||
.with_citations(citations),
|
||||
)),
|
||||
None => Ok(MaybeAIAgentOutputMessage::NoClientRepresentation),
|
||||
}
|
||||
}
|
||||
Some(api::message::artifact_event::Event::ForkArtifacts(_)) | None => {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
},
|
||||
api::message::Message::MessagesReceivedFromAgents(messages_received_from_agents) => {
|
||||
let messages = messages_received_from_agents
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(|msg| crate::ai::agent::ReceivedMessageDisplay {
|
||||
message_id: msg.message_id,
|
||||
sender_agent_id: msg.sender_agent_id,
|
||||
addresses: msg.addresses,
|
||||
subject: msg.subject,
|
||||
message_body: msg.message_body,
|
||||
})
|
||||
.collect();
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::messages_received_from_agents(
|
||||
MessageId::new(self.id),
|
||||
messages,
|
||||
)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
api::message::Message::EventsFromAgents(events) => {
|
||||
let event_ids = events
|
||||
.agent_events
|
||||
.iter()
|
||||
.map(|e| e.event_id.clone())
|
||||
.collect();
|
||||
Ok(MaybeAIAgentOutputMessage::Message(
|
||||
AIAgentOutputMessage::events_from_agents(MessageId::new(self.id), event_ids)
|
||||
.with_citations(citations),
|
||||
))
|
||||
}
|
||||
// These messages don't indicate an error but they don't translate to a client-side output message.
|
||||
api::message::Message::UserQuery(_)
|
||||
| api::message::Message::SystemQuery(_)
|
||||
| api::message::Message::ToolCallResult(_)
|
||||
| api::message::Message::CodeReview(_)
|
||||
| api::message::Message::ServerEvent(_)
|
||||
| api::message::Message::InvokeSkill(_)
|
||||
| api::message::Message::PassiveSuggestionResult(_) => {
|
||||
Ok(MaybeAIAgentOutputMessage::NoClientRepresentation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::AgentOutput> for AIAgentText {
|
||||
fn from(value: api::message::AgentOutput) -> Self {
|
||||
AIAgentText {
|
||||
sections: parse_markdown_into_text_and_code_sections(value.text.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::message::AgentReasoning> for AIAgentText {
|
||||
fn from(value: api::message::AgentReasoning) -> Self {
|
||||
AIAgentText {
|
||||
sections: parse_markdown_into_text_and_code_sections(value.reasoning.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for converting an [`api::Message`] to an [`AIAgentOutputMessage`].
|
||||
trait ConvertAPIToolCallToAIAgentAction {
|
||||
fn to_action(
|
||||
self,
|
||||
params: ConversionParams,
|
||||
) -> Result<MaybeAIAgentAction, ToolToAIAgentActionError>;
|
||||
}
|
||||
|
||||
/// Trys to convert an [`api::message::ToolCall`] to an [`AIAgentAction`].
|
||||
///
|
||||
/// A [`Result::Error`] indicates an unexpected problem, while [`Ok(None)`]
|
||||
/// indicates a tool call that we aren't expected to parse.
|
||||
impl ConvertAPIToolCallToAIAgentAction for api::message::ToolCall {
|
||||
fn to_action(
|
||||
self,
|
||||
params: ConversionParams,
|
||||
) -> Result<MaybeAIAgentAction, ToolToAIAgentActionError> {
|
||||
let Some(tool) = self.tool else {
|
||||
return Err(ToolToAIAgentActionError::MissingTool);
|
||||
};
|
||||
|
||||
let create_standard_action = |action: AIAgentActionType| {
|
||||
Ok(MaybeAIAgentAction::Action(AIAgentAction {
|
||||
id: self.tool_call_id.clone().into(),
|
||||
task_id: params.task_id.clone(),
|
||||
action,
|
||||
requires_result: true,
|
||||
}))
|
||||
};
|
||||
|
||||
match tool {
|
||||
api::message::tool_call::Tool::RunShellCommand(run_shell_command) => {
|
||||
create_standard_action(run_shell_command.into())
|
||||
}
|
||||
api::message::tool_call::Tool::WriteToLongRunningShellCommand(
|
||||
write_to_long_running_shell_command,
|
||||
) => create_standard_action(write_to_long_running_shell_command.into()),
|
||||
api::message::tool_call::Tool::ReadFiles(read_files) => {
|
||||
create_standard_action(read_files.into())
|
||||
}
|
||||
api::message::tool_call::Tool::UploadFileArtifact(upload_file_artifact) => {
|
||||
create_standard_action(upload_file_artifact.try_into()?)
|
||||
}
|
||||
api::message::tool_call::Tool::SearchCodebase(search_codebase) => {
|
||||
create_standard_action(search_codebase.into())
|
||||
}
|
||||
api::message::tool_call::Tool::Grep(grep) => create_standard_action(grep.into()),
|
||||
#[allow(deprecated)]
|
||||
api::message::tool_call::Tool::FileGlob(glob) => create_standard_action(glob.into()),
|
||||
api::message::tool_call::Tool::FileGlobV2(glob) => create_standard_action(glob.into()),
|
||||
api::message::tool_call::Tool::ApplyFileDiffs(apply_file_diffs) => {
|
||||
create_standard_action(apply_file_diffs.into())
|
||||
}
|
||||
api::message::tool_call::Tool::ReadMcpResource(read_mcp_resource) => {
|
||||
create_standard_action(read_mcp_resource.into())
|
||||
}
|
||||
api::message::tool_call::Tool::CallMcpTool(call_mcp_tool) => {
|
||||
match call_mcp_tool.try_into() {
|
||||
Ok(call_mcp_tool_action) => create_standard_action(call_mcp_tool_action),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
api::message::tool_call::Tool::SuggestNewConversation(suggest_new_conversation) => {
|
||||
create_standard_action(suggest_new_conversation.into())
|
||||
}
|
||||
api::message::tool_call::Tool::SuggestPrompt(suggest_prompt) => {
|
||||
match suggest_prompt.try_into() {
|
||||
Ok(suggest_prompt_action) => create_standard_action(suggest_prompt_action),
|
||||
Err(_) => Ok(MaybeAIAgentAction::NoClientRepresentation),
|
||||
}
|
||||
}
|
||||
api::message::tool_call::Tool::OpenCodeReview(_) => {
|
||||
create_standard_action(AIAgentActionType::OpenCodeReview)
|
||||
}
|
||||
api::message::tool_call::Tool::InitProject(_) => {
|
||||
create_standard_action(AIAgentActionType::InitProject)
|
||||
}
|
||||
api::message::tool_call::Tool::ReadDocuments(read_documents) => {
|
||||
create_standard_action(read_documents.into())
|
||||
}
|
||||
api::message::tool_call::Tool::EditDocuments(edit_documents) => {
|
||||
create_standard_action(edit_documents.into())
|
||||
}
|
||||
api::message::tool_call::Tool::CreateDocuments(create_documents) => {
|
||||
create_standard_action(create_documents.into())
|
||||
}
|
||||
api::message::tool_call::Tool::ReadShellCommandOutput(read_shell_command_output) => {
|
||||
create_standard_action(read_shell_command_output.into())
|
||||
}
|
||||
api::message::tool_call::Tool::TransferShellCommandControlToUser(
|
||||
transfer_shell_command_control_to_user,
|
||||
) => create_standard_action(transfer_shell_command_control_to_user.into()),
|
||||
api::message::tool_call::Tool::UseComputer(use_computer) => {
|
||||
create_standard_action(use_computer.try_into()?)
|
||||
}
|
||||
api::message::tool_call::Tool::RequestComputerUse(request_computer_use) => {
|
||||
create_standard_action(request_computer_use.into())
|
||||
}
|
||||
api::message::tool_call::Tool::Subagent(subagent) => {
|
||||
use api::message::tool_call::subagent::Metadata;
|
||||
let subagent_type = match subagent.metadata {
|
||||
Some(Metadata::Cli(_)) => SubagentType::Cli,
|
||||
Some(Metadata::Research(_)) => SubagentType::Research,
|
||||
Some(Metadata::Advice(_)) => SubagentType::Advice,
|
||||
Some(Metadata::ComputerUse(_)) => SubagentType::ComputerUse,
|
||||
Some(Metadata::Summarization(_)) => SubagentType::Summarization,
|
||||
Some(Metadata::ConversationSearch(cs_meta)) => {
|
||||
let query = if cs_meta.query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(cs_meta.query)
|
||||
};
|
||||
let conversation_id = if cs_meta.conversation_id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(cs_meta.conversation_id)
|
||||
};
|
||||
SubagentType::ConversationSearch {
|
||||
query,
|
||||
conversation_id,
|
||||
}
|
||||
}
|
||||
Some(Metadata::WarpDocumentationSearch(_)) => {
|
||||
SubagentType::WarpDocumentationSearch
|
||||
}
|
||||
None => SubagentType::Unknown,
|
||||
};
|
||||
Ok(MaybeAIAgentAction::Subagent(SubagentCall {
|
||||
task_id: subagent.task_id,
|
||||
subagent_type,
|
||||
}))
|
||||
}
|
||||
api::message::tool_call::Tool::StartAgent(start_agent) => {
|
||||
create_standard_action(AIAgentActionType::StartAgent {
|
||||
version: StartAgentVersion::V1,
|
||||
name: start_agent.name,
|
||||
prompt: start_agent.prompt,
|
||||
execution_mode: convert_start_agent_execution_mode(start_agent.execution_mode),
|
||||
lifecycle_subscription: start_agent.lifecycle_subscription.map(
|
||||
|subscription| {
|
||||
subscription
|
||||
.event_types
|
||||
.into_iter()
|
||||
.filter_map(convert_start_agent_lifecycle_event_type)
|
||||
.collect()
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
api::message::tool_call::Tool::StartAgentV2(start_agent) => {
|
||||
create_standard_action(AIAgentActionType::StartAgent {
|
||||
version: StartAgentVersion::V2,
|
||||
name: start_agent.name,
|
||||
prompt: start_agent.prompt,
|
||||
execution_mode: convert_start_agent_v2_execution_mode(
|
||||
start_agent.execution_mode,
|
||||
),
|
||||
lifecycle_subscription: start_agent.lifecycle_subscription.map(
|
||||
|subscription| {
|
||||
subscription
|
||||
.event_types
|
||||
.into_iter()
|
||||
.filter_map(convert_start_agent_lifecycle_event_type)
|
||||
.collect()
|
||||
},
|
||||
),
|
||||
})
|
||||
}
|
||||
api::message::tool_call::Tool::SendMessageToAgent(send_message) => {
|
||||
create_standard_action(AIAgentActionType::SendMessageToAgent {
|
||||
addresses: send_message.addresses,
|
||||
subject: send_message.subject,
|
||||
message: send_message.message,
|
||||
})
|
||||
}
|
||||
api::message::tool_call::Tool::InsertReviewComments(insert_review_comments) => {
|
||||
create_standard_action(insert_review_comments.into())
|
||||
}
|
||||
api::message::tool_call::Tool::ReadSkill(read_skill) => {
|
||||
create_standard_action(read_skill.try_into()?)
|
||||
}
|
||||
api::message::tool_call::Tool::FetchConversation(fetch_conversation) => {
|
||||
create_standard_action(fetch_conversation.into())
|
||||
}
|
||||
api::message::tool_call::Tool::AskUserQuestion(ask) => {
|
||||
let questions = ask
|
||||
.questions
|
||||
.into_iter()
|
||||
.filter_map(convert_api_question)
|
||||
.collect();
|
||||
create_standard_action(AIAgentActionType::AskUserQuestion { questions })
|
||||
}
|
||||
// Clients do not need to know how to parse server tool-calls but receiving
|
||||
// them is not an error.
|
||||
api::message::tool_call::Tool::Server(_) => {
|
||||
Ok(MaybeAIAgentAction::NoClientRepresentation)
|
||||
}
|
||||
_ => Err(ToolToAIAgentActionError::UnexpectedTool),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::Suggestions> for Suggestions {
|
||||
fn from(api_suggestions: api::Suggestions) -> Self {
|
||||
Self {
|
||||
rules: api_suggestions
|
||||
.rules
|
||||
.into_iter()
|
||||
.map(|rule| SuggestedRule {
|
||||
name: rule.name,
|
||||
content: rule.content,
|
||||
logging_id: rule.logging_id.into(),
|
||||
})
|
||||
.collect(),
|
||||
agent_mode_workflows: api_suggestions
|
||||
.workflows
|
||||
.into_iter()
|
||||
.map(|workflow| SuggestedAgentModeWorkflow {
|
||||
name: workflow.name,
|
||||
prompt: workflow.prompt,
|
||||
logging_id: workflow.logging_id.into(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<api::TodoItem> for AIAgentTodo {
|
||||
fn from(value: api::TodoItem) -> Self {
|
||||
AIAgentTodo {
|
||||
id: value.id.into(),
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct user inputs from the provided server messages
|
||||
/// (for use in shared agent exchanges where the input was not provided in this session)
|
||||
pub fn user_inputs_from_messages(messages: &[api::Message]) -> Vec<AIAgentInput> {
|
||||
let mut inputs = Vec::new();
|
||||
let mut document_versions: HashMap<AIDocumentId, AIDocumentVersion> = HashMap::new();
|
||||
for m in messages {
|
||||
let Some(inner) = &m.message else { continue };
|
||||
match inner {
|
||||
api::message::Message::UserQuery(uq) => {
|
||||
let context = convert_input_context(uq.context.as_ref());
|
||||
let referenced_attachments = uq
|
||||
.referenced_attachments
|
||||
.iter()
|
||||
.filter_map(|(key, attachment)| {
|
||||
AIAgentAttachment::try_from(attachment.clone())
|
||||
.ok()
|
||||
.map(|a| (key.clone(), a))
|
||||
})
|
||||
.collect();
|
||||
inputs.push(AIAgentInput::UserQuery {
|
||||
query: uq.query.clone(),
|
||||
context,
|
||||
static_query_type: None,
|
||||
referenced_attachments,
|
||||
user_query_mode: convert_user_query_mode(uq.mode.as_ref()),
|
||||
running_command: None,
|
||||
intended_agent: Some(uq.intended_agent()),
|
||||
});
|
||||
}
|
||||
api::message::Message::SystemQuery(sq) => {
|
||||
let ctx = convert_input_context(sq.context.as_ref());
|
||||
if let Some(t) = &sq.r#type {
|
||||
// These system queries appear as user inputs in ai blocks.
|
||||
match t {
|
||||
api::message::system_query::Type::CreateNewProject(p) => {
|
||||
inputs.push(AIAgentInput::CreateNewProject {
|
||||
query: p.query.clone(),
|
||||
context: ctx,
|
||||
});
|
||||
}
|
||||
api::message::system_query::Type::CloneRepository(p) => {
|
||||
inputs.push(AIAgentInput::CloneRepository {
|
||||
clone_repo_url: CloneRepositoryURL::new(p.url.clone()),
|
||||
context: ctx,
|
||||
});
|
||||
}
|
||||
api::message::system_query::Type::AutoCodeDiff(p) => {
|
||||
inputs.push(AIAgentInput::AutoCodeDiffQuery {
|
||||
query: p.query.clone(),
|
||||
context: ctx,
|
||||
});
|
||||
}
|
||||
api::message::system_query::Type::FetchReviewComments(fetch) => {
|
||||
inputs.push(AIAgentInput::FetchReviewComments {
|
||||
repo_path: fetch.repo_path.clone(),
|
||||
context: ctx,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
api::message::Message::ToolCallResult(tcr) => {
|
||||
let task_id = TaskId::new(m.task_id.clone());
|
||||
if let Some(input) = convert_tool_call_result_to_input(
|
||||
&task_id,
|
||||
tcr,
|
||||
&HashMap::new(),
|
||||
&mut document_versions,
|
||||
) {
|
||||
inputs.push(input);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
inputs
|
||||
}
|
||||
|
||||
fn convert_api_question(
|
||||
q: api::ask_user_question::Question,
|
||||
) -> Option<ai::agent::action::AskUserQuestionItem> {
|
||||
let Some(QuestionType::MultipleChoice(mc)) = q.question_type else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Server sends -1 when there is no recommendation.
|
||||
let recommended_idx = usize::try_from(mc.recommended_option_index)
|
||||
.ok()
|
||||
.filter(|idx| *idx < mc.options.len());
|
||||
let options = mc
|
||||
.options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, opt)| ai::agent::action::AskUserQuestionOption {
|
||||
label: opt.label.clone(),
|
||||
recommended: recommended_idx == Some(i),
|
||||
})
|
||||
.collect();
|
||||
Some(ai::agent::action::AskUserQuestionItem {
|
||||
question_id: q.question_id.clone(),
|
||||
question: q.question,
|
||||
question_type: ai::agent::action::AskUserQuestionType::MultipleChoice {
|
||||
is_multiselect: mc.is_multiselect,
|
||||
options,
|
||||
supports_other: mc.supports_other,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert_from_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,644 @@
|
||||
use super::{
|
||||
convert_api_question, ConversionParams, ConvertAPIMessageToClientOutputMessage,
|
||||
MaybeAIAgentOutputMessage,
|
||||
};
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionType, AIAgentOutputMessageType, LifecycleEventType, StartAgentExecutionMode,
|
||||
};
|
||||
use ai::agent::action::AskUserQuestionType;
|
||||
use ai::skills::SkillReference;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
fn start_agent_tool_call_message(
|
||||
name: &str,
|
||||
prompt: &str,
|
||||
execution_mode: Option<api::start_agent::ExecutionMode>,
|
||||
lifecycle_subscription_event_types: Option<Vec<i32>>,
|
||||
) -> api::Message {
|
||||
api::Message {
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
|
||||
tool_call_id: "tool-call-id".to_string(),
|
||||
tool: Some(api::message::tool_call::Tool::StartAgent(api::StartAgent {
|
||||
name: name.to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
execution_mode,
|
||||
lifecycle_subscription: lifecycle_subscription_event_types
|
||||
.map(|event_types| api::start_agent::LifecycleSubscription { event_types }),
|
||||
})),
|
||||
})),
|
||||
request_id: "request-id".to_string(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_start_agent_v2_execution_mode(harness_type: &str) -> api::start_agent_v2::ExecutionMode {
|
||||
api::start_agent_v2::ExecutionMode {
|
||||
mode: Some(api::start_agent_v2::execution_mode::Mode::Local(
|
||||
api::start_agent_v2::execution_mode::Local {
|
||||
harness: Some(api::start_agent_v2::execution_mode::Harness {
|
||||
r#type: harness_type.to_string(),
|
||||
}),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_start_agent_v2_execution_mode_without_harness() -> api::start_agent_v2::ExecutionMode {
|
||||
api::start_agent_v2::ExecutionMode {
|
||||
mode: Some(api::start_agent_v2::execution_mode::Mode::Local(
|
||||
api::start_agent_v2::execution_mode::Local { harness: None },
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_agent_v2_tool_call_message(
|
||||
name: &str,
|
||||
prompt: &str,
|
||||
execution_mode: Option<api::start_agent_v2::ExecutionMode>,
|
||||
lifecycle_subscription_event_types: Option<Vec<i32>>,
|
||||
) -> api::Message {
|
||||
api::Message {
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
|
||||
tool_call_id: "tool-call-id".to_string(),
|
||||
tool: Some(api::message::tool_call::Tool::StartAgentV2(
|
||||
api::StartAgentV2 {
|
||||
name: name.to_string(),
|
||||
prompt: prompt.to_string(),
|
||||
execution_mode,
|
||||
lifecycle_subscription: lifecycle_subscription_event_types.map(|event_types| {
|
||||
api::start_agent_v2::LifecycleSubscription { event_types }
|
||||
}),
|
||||
},
|
||||
)),
|
||||
})),
|
||||
request_id: "request-id".to_string(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_artifact_tool_call_message(path: &str, description: &str) -> api::Message {
|
||||
api::Message {
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
|
||||
tool_call_id: "tool-call-id".to_string(),
|
||||
tool: Some(api::message::tool_call::Tool::UploadFileArtifact(
|
||||
api::UploadFileArtifact {
|
||||
file: Some(api::FilePathReference {
|
||||
file_path: path.to_string(),
|
||||
}),
|
||||
description: description.to_string(),
|
||||
},
|
||||
)),
|
||||
})),
|
||||
request_id: "request-id".to_string(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_start_agent_v2_execution_mode(
|
||||
environment_id: &str,
|
||||
) -> api::start_agent_v2::ExecutionMode {
|
||||
api::start_agent_v2::ExecutionMode {
|
||||
mode: Some(api::start_agent_v2::execution_mode::Mode::Remote(
|
||||
api::start_agent_v2::execution_mode::Remote {
|
||||
environment_id: environment_id.to_string(),
|
||||
skills: vec![
|
||||
api::SkillRef {
|
||||
skill_reference: Some(api::skill_ref::SkillReference::Path(
|
||||
"/tmp/SKILL.md".to_string(),
|
||||
)),
|
||||
},
|
||||
api::SkillRef {
|
||||
skill_reference: Some(api::skill_ref::SkillReference::BundledSkillId(
|
||||
"review-comments".to_string(),
|
||||
)),
|
||||
},
|
||||
],
|
||||
model_id: "gpt-test".to_string(),
|
||||
computer_use_enabled: true,
|
||||
worker_host: "worker-host".to_string(),
|
||||
harness: Some(api::start_agent_v2::execution_mode::Harness {
|
||||
r#type: "claude-code".to_string(),
|
||||
}),
|
||||
title: "Remote child".to_string(),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn file_artifact_created_message(filepath: &str, description: &str) -> api::Message {
|
||||
api::Message {
|
||||
id: "message-id".to_string(),
|
||||
task_id: "task-id".to_string(),
|
||||
server_message_data: String::new(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::ArtifactEvent(
|
||||
api::message::ArtifactEvent {
|
||||
event: Some(api::message::artifact_event::Event::Created(
|
||||
api::message::artifact_event::ArtifactCreated {
|
||||
artifact: Some(
|
||||
api::message::artifact_event::artifact_created::Artifact::File(
|
||||
api::message::artifact_event::FileArtifact {
|
||||
artifact_uid: "artifact-uid".to_string(),
|
||||
filepath: filepath.to_string(),
|
||||
mime_type: "text/plain".to_string(),
|
||||
size_bytes: 42,
|
||||
description: description.to_string(),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
)),
|
||||
},
|
||||
)),
|
||||
request_id: "request-id".to_string(),
|
||||
timestamp: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_start_agent_execution_mode(environment_id: &str) -> api::start_agent::ExecutionMode {
|
||||
api::start_agent::ExecutionMode {
|
||||
mode: Some(api::start_agent::execution_mode::Mode::Remote(
|
||||
api::start_agent::execution_mode::Remote {
|
||||
environment_id: environment_id.to_string(),
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_multiple_choice_question(
|
||||
recommended_option_index: i32,
|
||||
) -> api::ask_user_question::Question {
|
||||
api::ask_user_question::Question {
|
||||
question_id: "q1".to_string(),
|
||||
question: "Which option should we prefer?".to_string(),
|
||||
question_type: Some(
|
||||
api::ask_user_question::question::QuestionType::MultipleChoice(
|
||||
api::ask_user_question::MultipleChoice {
|
||||
is_multiselect: false,
|
||||
options: vec![
|
||||
api::ask_user_question::Option {
|
||||
label: "First".to_string(),
|
||||
},
|
||||
api::ask_user_question::Option {
|
||||
label: "Second".to_string(),
|
||||
},
|
||||
],
|
||||
recommended_option_index,
|
||||
supports_other: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_api_question_treats_negative_recommended_index_as_no_recommendation() {
|
||||
let converted = convert_api_question(build_multiple_choice_question(-1))
|
||||
.expect("multiple choice questions should convert");
|
||||
|
||||
let AskUserQuestionType::MultipleChoice { options, .. } = converted.question_type;
|
||||
assert_eq!(options.len(), 2);
|
||||
assert!(options.iter().all(|option| !option.recommended));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_api_question_uses_zero_based_recommended_index_when_present() {
|
||||
let converted = convert_api_question(build_multiple_choice_question(0))
|
||||
.expect("multiple choice questions should convert");
|
||||
|
||||
let AskUserQuestionType::MultipleChoice { options, .. } = converted.question_type;
|
||||
assert_eq!(options.len(), 2);
|
||||
assert!(options[0].recommended);
|
||||
assert!(!options[1].recommended);
|
||||
}
|
||||
|
||||
fn extract_start_agent_action(
|
||||
output: MaybeAIAgentOutputMessage,
|
||||
) -> (
|
||||
String,
|
||||
String,
|
||||
StartAgentExecutionMode,
|
||||
Option<Vec<LifecycleEventType>>,
|
||||
) {
|
||||
let MaybeAIAgentOutputMessage::Message(output_message) = output else {
|
||||
panic!("expected output message");
|
||||
};
|
||||
let AIAgentOutputMessageType::Action(action) = output_message.message else {
|
||||
panic!("expected action output message");
|
||||
};
|
||||
let AIAgentActionType::StartAgent {
|
||||
version: _,
|
||||
name,
|
||||
prompt,
|
||||
execution_mode,
|
||||
lifecycle_subscription,
|
||||
} = action.action
|
||||
else {
|
||||
panic!("expected StartAgent action");
|
||||
};
|
||||
(name, prompt, execution_mode, lifecycle_subscription)
|
||||
}
|
||||
|
||||
fn extract_upload_artifact_action(output: MaybeAIAgentOutputMessage) -> (String, Option<String>) {
|
||||
let MaybeAIAgentOutputMessage::Message(output_message) = output else {
|
||||
panic!("expected output message");
|
||||
};
|
||||
let AIAgentOutputMessageType::Action(action) = output_message.message else {
|
||||
panic!("expected action output message");
|
||||
};
|
||||
let AIAgentActionType::UploadArtifact(request) = action.action else {
|
||||
panic!("expected UploadArtifact action");
|
||||
};
|
||||
(request.file_path, request.description)
|
||||
}
|
||||
|
||||
fn extract_file_artifact_created(
|
||||
output: MaybeAIAgentOutputMessage,
|
||||
) -> (String, String, Option<String>, i64) {
|
||||
let MaybeAIAgentOutputMessage::Message(output_message) = output else {
|
||||
panic!("expected output message");
|
||||
};
|
||||
let AIAgentOutputMessageType::ArtifactCreated(artifact) = output_message.message else {
|
||||
panic!("expected artifact created output message");
|
||||
};
|
||||
let crate::ai::agent::ArtifactCreatedData::File {
|
||||
filepath,
|
||||
filename,
|
||||
description,
|
||||
size_bytes,
|
||||
..
|
||||
} = artifact
|
||||
else {
|
||||
panic!("expected file artifact created output message");
|
||||
};
|
||||
(filepath, filename, description, size_bytes)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_start_agent_tool_call_to_action_with_prompt() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message =
|
||||
start_agent_tool_call_message("Agent 1", "run tests and report failures", None, None);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (name, prompt, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
|
||||
assert_eq!(name, "Agent 1");
|
||||
assert_eq!(prompt, "run tests and report failures");
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_local_start_agent_v2_without_harness_type_to_defaults() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_v2_tool_call_message(
|
||||
"Agent 7",
|
||||
"run in the default local harness",
|
||||
Some(local_start_agent_v2_execution_mode_without_harness()),
|
||||
None,
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (name, prompt, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
|
||||
assert_eq!(name, "Agent 7");
|
||||
assert_eq!(prompt, "run in the default local harness");
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_upload_artifact_tool_call_to_action() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = upload_artifact_tool_call_message(
|
||||
"/tmp/build/output.log",
|
||||
"Build output for the latest run",
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (file_path, description) = extract_upload_artifact_action(output);
|
||||
|
||||
assert_eq!(file_path, "/tmp/build/output.log");
|
||||
assert_eq!(
|
||||
description.as_deref(),
|
||||
Some("Build output for the latest run")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_file_artifact_created_message_with_filename() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message =
|
||||
file_artifact_created_message("outputs/report.txt", "Build output for the latest run");
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (filepath, filename, description, size_bytes) = extract_file_artifact_created(output);
|
||||
|
||||
assert_eq!(filepath, "outputs/report.txt");
|
||||
assert_eq!(filename, "report.txt");
|
||||
assert_eq!(
|
||||
description.as_deref(),
|
||||
Some("Build output for the latest run")
|
||||
);
|
||||
assert_eq!(size_bytes, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_start_agent_tool_calls_with_different_prompt_lengths() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let partial_message = start_agent_tool_call_message("Agent 2", "run tests", None, None);
|
||||
let updated_message = start_agent_tool_call_message(
|
||||
"Agent 2",
|
||||
"run tests and then summarize failures",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let partial_output = partial_message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("partial conversion should succeed");
|
||||
let updated_output = updated_message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("updated conversion should succeed");
|
||||
|
||||
let (_, partial_prompt, partial_execution_mode, _) = extract_start_agent_action(partial_output);
|
||||
let (_, updated_prompt, updated_execution_mode, _) = extract_start_agent_action(updated_output);
|
||||
|
||||
assert_eq!(partial_prompt, "run tests");
|
||||
assert_eq!(updated_prompt, "run tests and then summarize failures");
|
||||
assert_eq!(
|
||||
partial_execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
assert_eq!(
|
||||
updated_execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_start_agent_with_explicit_empty_lifecycle_subscription() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_tool_call_message("Agent 3", "run tests", None, Some(vec![]));
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (_, _, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, Some(vec![]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_start_agent_with_cancelled_and_blocked_lifecycle_subscription() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_tool_call_message(
|
||||
"Agent 4",
|
||||
"wait for approval",
|
||||
None,
|
||||
Some(vec![
|
||||
api::LifecycleEventType::Cancelled as i32,
|
||||
api::LifecycleEventType::Blocked as i32,
|
||||
]),
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (_, _, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::local_with_defaults()
|
||||
);
|
||||
assert_eq!(
|
||||
lifecycle_subscription,
|
||||
Some(vec![
|
||||
LifecycleEventType::Cancelled,
|
||||
LifecycleEventType::Blocked
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_remote_start_agent_with_environment_id() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_tool_call_message(
|
||||
"Agent 5",
|
||||
"run in the remote environment",
|
||||
Some(remote_start_agent_execution_mode("env-123")),
|
||||
None,
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (name, prompt, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
|
||||
assert_eq!(name, "Agent 5");
|
||||
assert_eq!(prompt, "run in the remote environment");
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::Remote {
|
||||
environment_id: "env-123".to_string(),
|
||||
skill_references: vec![],
|
||||
model_id: String::new(),
|
||||
computer_use_enabled: false,
|
||||
worker_host: String::new(),
|
||||
harness_type: String::new(),
|
||||
title: String::new(),
|
||||
}
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_remote_start_agent_v2_with_skill_references() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_v2_tool_call_message(
|
||||
"Agent 6",
|
||||
"run in the remote environment",
|
||||
Some(remote_start_agent_v2_execution_mode("env-123")),
|
||||
None,
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (name, prompt, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
|
||||
assert_eq!(name, "Agent 6");
|
||||
assert_eq!(prompt, "run in the remote environment");
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::Remote {
|
||||
environment_id: "env-123".to_string(),
|
||||
skill_references: vec![
|
||||
SkillReference::Path("/tmp/SKILL.md".into()),
|
||||
SkillReference::BundledSkillId("review-comments".to_string()),
|
||||
],
|
||||
model_id: "gpt-test".to_string(),
|
||||
computer_use_enabled: true,
|
||||
worker_host: "worker-host".to_string(),
|
||||
harness_type: "claude-code".to_string(),
|
||||
title: "Remote child".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_local_start_agent_v2_with_harness_type() {
|
||||
let task_id = TaskId::new("task-id".to_string());
|
||||
let message = start_agent_v2_tool_call_message(
|
||||
"Agent 6",
|
||||
"run in the local claude harness",
|
||||
Some(local_start_agent_v2_execution_mode("claude-code")),
|
||||
None,
|
||||
);
|
||||
|
||||
let output = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("conversion should succeed");
|
||||
|
||||
let (name, prompt, execution_mode, lifecycle_subscription) = extract_start_agent_action(output);
|
||||
|
||||
assert_eq!(name, "Agent 6");
|
||||
assert_eq!(prompt, "run in the local claude harness");
|
||||
assert_eq!(
|
||||
execution_mode,
|
||||
StartAgentExecutionMode::local_harness("claude-code".to_string())
|
||||
);
|
||||
assert_eq!(lifecycle_subscription, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_control_tool_call_converts_to_action_message() {
|
||||
let task_id = TaskId::new("task".to_string());
|
||||
let reason = "Please finish the interactive flow".to_string();
|
||||
let message = api::Message {
|
||||
id: "message".to_string(),
|
||||
task_id: "task".to_string(),
|
||||
server_message_data: String::new(),
|
||||
citations: vec![],
|
||||
message: Some(api::message::Message::ToolCall(api::message::ToolCall {
|
||||
tool_call_id: "tool_call".to_string(),
|
||||
tool: Some(
|
||||
api::message::tool_call::Tool::TransferShellCommandControlToUser(
|
||||
api::message::tool_call::TransferShellCommandControlToUser {
|
||||
reason: reason.clone(),
|
||||
},
|
||||
),
|
||||
),
|
||||
})),
|
||||
request_id: "req".to_string(),
|
||||
timestamp: None,
|
||||
};
|
||||
|
||||
let converted = message
|
||||
.to_client_output_message(ConversionParams {
|
||||
task_id: &task_id,
|
||||
current_todo_list: None,
|
||||
active_code_review: None,
|
||||
})
|
||||
.expect("transfer-control conversion should succeed");
|
||||
|
||||
match converted {
|
||||
MaybeAIAgentOutputMessage::Message(output) => match output.message {
|
||||
AIAgentOutputMessageType::Action(action) => {
|
||||
assert_eq!(action.task_id, task_id);
|
||||
assert_eq!(
|
||||
action.action,
|
||||
AIAgentActionType::TransferShellCommandControlToUser { reason }
|
||||
);
|
||||
assert!(action.requires_result);
|
||||
}
|
||||
other => panic!("Expected action message, got {other:?}"),
|
||||
},
|
||||
MaybeAIAgentOutputMessage::NoClientRepresentation => {
|
||||
panic!("Expected transfer-control tool call to produce a client action")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,977 @@
|
||||
//! Conversions from application types to MAA API types.
|
||||
|
||||
use ai::agent::convert::ConvertToAPITypeError;
|
||||
use anyhow::anyhow;
|
||||
use chrono::{DateTime, Local, Timelike};
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::ai::{
|
||||
agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, AIAgentAttachment, AIAgentContext,
|
||||
AIAgentInput, DriveObjectPayload, MCPContext, PassiveSuggestionResultType,
|
||||
PassiveSuggestionTrigger, RunningCommand, StaticQueryType, Suggestions, UserQueryMode,
|
||||
},
|
||||
block_context::BlockContext,
|
||||
};
|
||||
|
||||
fn local_datetime_to_timestamp(timestamp: DateTime<Local>) -> prost_types::Timestamp {
|
||||
prost_types::Timestamp {
|
||||
seconds: timestamp.timestamp(),
|
||||
nanos: timestamp.timestamp_subsec_nanos() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StaticQueryType> for api::request::input::query_with_canned_response::Type {
|
||||
type Error = ConvertToAPITypeError;
|
||||
|
||||
fn try_from(value: StaticQueryType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
StaticQueryType::Install => Ok(
|
||||
api::request::input::query_with_canned_response::Type::Install(
|
||||
api::request::input::query_with_canned_response::Install {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::Code => {
|
||||
Ok(api::request::input::query_with_canned_response::Type::Code(
|
||||
api::request::input::query_with_canned_response::Code {},
|
||||
))
|
||||
}
|
||||
StaticQueryType::Deploy => Ok(
|
||||
api::request::input::query_with_canned_response::Type::Deploy(
|
||||
api::request::input::query_with_canned_response::Deploy {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::SomethingElse => Ok(
|
||||
api::request::input::query_with_canned_response::Type::SomethingElse(
|
||||
api::request::input::query_with_canned_response::SomethingElse {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::CustomOnboardingRequest => Ok(
|
||||
api::request::input::query_with_canned_response::Type::CustomOnboardingRequest(
|
||||
api::request::input::query_with_canned_response::CustomOnboardingRequest {},
|
||||
),
|
||||
),
|
||||
StaticQueryType::EvaluationSuite => {
|
||||
Err(anyhow::anyhow!("EvaluationSuite StaticQueryType not yet supported").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn convert_input(
|
||||
mut inputs: Vec<AIAgentInput>,
|
||||
) -> Result<api::request::Input, ConvertToAPITypeError> {
|
||||
if inputs.is_empty() {
|
||||
return Err(anyhow!("Attempted to send multi-agent request with no input").into());
|
||||
}
|
||||
let api_context = inputs
|
||||
.iter()
|
||||
.rev()
|
||||
.find_map(AIAgentInput::context)
|
||||
.map(convert_context);
|
||||
|
||||
let mut api_inputs = vec![];
|
||||
if inputs.len() == 1 {
|
||||
match inputs.pop().expect("Input exists.") {
|
||||
AIAgentInput::UserQuery {
|
||||
query,
|
||||
context,
|
||||
static_query_type: Some(query_type),
|
||||
..
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::QueryWithCannedResponse(
|
||||
api::request::input::QueryWithCannedResponse {
|
||||
query,
|
||||
r#type: Some(query_type.try_into()?),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::AutoCodeDiffQuery { query, context } => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::AutoCodeDiffQuery(
|
||||
api::request::input::AutoCodeDiffQuery { query },
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::ResumeConversation { context } => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::ResumeConversation(
|
||||
api::request::input::ResumeConversation {},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::InitProjectRules { context, .. } => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::InitProjectRules(
|
||||
api::request::input::InitProjectRules {},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::CreateEnvironment {
|
||||
context,
|
||||
repo_paths,
|
||||
..
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::CreateEnvironment(
|
||||
api::request::input::CreateEnvironment { repo_paths },
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::TriggerPassiveSuggestion {
|
||||
context,
|
||||
attachments,
|
||||
trigger,
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::GeneratePassiveSuggestions(
|
||||
api::request::input::GeneratePassiveSuggestions {
|
||||
attachments: attachments
|
||||
.into_iter()
|
||||
.map(|attachment| attachment.into())
|
||||
.collect(),
|
||||
trigger: Some(trigger.into()),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::CreateNewProject { query, context } => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::CreateNewProject(
|
||||
api::request::input::CreateNewProject { query },
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::CloneRepository {
|
||||
clone_repo_url,
|
||||
context,
|
||||
..
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::CloneRepository(
|
||||
api::request::input::CloneRepository {
|
||||
url: clone_repo_url.into_url(),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::CodeReview {
|
||||
context,
|
||||
review_comments,
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::CodeReview(
|
||||
api::request::input::CodeReview {
|
||||
operation: Some(
|
||||
api::request::input::code_review::Operation::InitialReviewComments(
|
||||
api::request::input::code_review::InitialReviewComments {
|
||||
review_comments: review_comments
|
||||
.comments
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
diff_set: Some(api::DiffSet {
|
||||
hunks: review_comments
|
||||
.diff_set
|
||||
.into_iter()
|
||||
.flat_map(|(file_path, hunks)| {
|
||||
hunks.into_iter().map(move |hunk| {
|
||||
hunk.convert_to_api(file_path.clone())
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
curr_ref: None,
|
||||
base_ref: None,
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::FetchReviewComments { repo_path, context } => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::FetchReviewComments(
|
||||
api::request::input::FetchReviewComments { repo_path },
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::SummarizeConversation { prompt } => {
|
||||
return Ok(api::request::Input {
|
||||
context: None,
|
||||
r#type: Some(api::request::input::Type::SummarizeConversation(
|
||||
api::request::input::SummarizeConversation {
|
||||
prompt: prompt.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::InvokeSkill {
|
||||
context,
|
||||
skill,
|
||||
user_query,
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::InvokeSkill(
|
||||
api::request::input::InvokeSkill {
|
||||
skill: Some(skill.into()),
|
||||
user_query: user_query.map(|user_query| {
|
||||
api::request::input::UserQuery {
|
||||
query: user_query.query,
|
||||
referenced_attachments: user_query
|
||||
.referenced_attachments
|
||||
.into_iter()
|
||||
.map(|(k, attachment)| (k, attachment.into()))
|
||||
.collect(),
|
||||
mode: None,
|
||||
intended_agent: Default::default(),
|
||||
}
|
||||
}),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
AIAgentInput::StartFromAmbientRunPrompt {
|
||||
ambient_run_id,
|
||||
context,
|
||||
runtime_skill,
|
||||
attachments_dir,
|
||||
} => {
|
||||
return Ok(api::request::Input {
|
||||
context: Some(convert_context(context.as_ref())),
|
||||
r#type: Some(api::request::input::Type::StartFromAmbientRunPrompt(
|
||||
api::request::input::StartFromAmbientRunPrompt {
|
||||
ambient_run_id,
|
||||
// Deprecated, we always resolve base_prompt from the stored task config.
|
||||
runtime_base_prompt: String::new(),
|
||||
|
||||
runtime_skill: runtime_skill.map(|skill| skill.into()),
|
||||
attachments_dir: attachments_dir.unwrap_or_default(),
|
||||
},
|
||||
)),
|
||||
});
|
||||
}
|
||||
other_input => match convert_input_to_user_input(other_input) {
|
||||
Ok(api_input) => api_inputs.push(api_input),
|
||||
Err(ConvertToAPITypeError::Ignore) => (),
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for input in inputs.into_iter() {
|
||||
match convert_input_to_user_input(input) {
|
||||
Ok(api_input) => api_inputs.push(api_input),
|
||||
Err(ConvertToAPITypeError::Ignore) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(api::request::Input {
|
||||
context: api_context,
|
||||
r#type: Some(api::request::input::Type::UserInputs(
|
||||
api::request::input::UserInputs {
|
||||
inputs: api_inputs
|
||||
.into_iter()
|
||||
.map(|input| api::request::input::user_inputs::UserInput { input: Some(input) })
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_input_to_user_input(
|
||||
input: AIAgentInput,
|
||||
) -> Result<api::request::input::user_inputs::user_input::Input, ConvertToAPITypeError> {
|
||||
match input {
|
||||
AIAgentInput::UserQuery {
|
||||
query,
|
||||
static_query_type: None,
|
||||
referenced_attachments,
|
||||
user_query_mode,
|
||||
running_command: None,
|
||||
intended_agent,
|
||||
..
|
||||
} => Ok(
|
||||
api::request::input::user_inputs::user_input::Input::UserQuery(
|
||||
api::request::input::UserQuery {
|
||||
query,
|
||||
referenced_attachments: referenced_attachments.into_iter().map(|(k, attachment)| (k, attachment.into())).collect(),
|
||||
mode: Some(user_query_mode.into()),
|
||||
intended_agent: intended_agent.map(|agent| agent.into()).unwrap_or_default(),
|
||||
},
|
||||
),
|
||||
),
|
||||
AIAgentInput::UserQuery {
|
||||
query,
|
||||
static_query_type: None,
|
||||
referenced_attachments,
|
||||
user_query_mode,
|
||||
running_command: Some(RunningCommand{
|
||||
command,
|
||||
block_id,
|
||||
grid_contents: output,
|
||||
cursor,
|
||||
requested_command_id,
|
||||
is_alt_screen_active,
|
||||
}),
|
||||
..
|
||||
} => {
|
||||
Ok(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(
|
||||
api::request::input::CliAgentUserQuery {
|
||||
user_query: Some(api::request::input::UserQuery {
|
||||
query,
|
||||
referenced_attachments: referenced_attachments.into_iter().map(|(k, attachment)| (k, attachment.into())).collect(),
|
||||
mode: Some(user_query_mode.into()),
|
||||
intended_agent: api::AgentType::Cli.into(),
|
||||
}),
|
||||
running_command: Some(api::RunningShellCommand{
|
||||
command,
|
||||
snapshot: Some(api::LongRunningShellCommandSnapshot {
|
||||
output,
|
||||
cursor,
|
||||
command_id: block_id.as_str().to_owned(),
|
||||
is_alt_screen_active,
|
||||
is_preempted: false,
|
||||
}),
|
||||
}),
|
||||
run_shell_command_tool_call_id: requested_command_id.map(|id| id.to_string()).unwrap_or_default(),
|
||||
}
|
||||
))
|
||||
}
|
||||
AIAgentInput::ActionResult { result, .. } => result.try_into(),
|
||||
AIAgentInput::MessagesReceivedFromAgents { messages } => Ok(
|
||||
api::request::input::user_inputs::user_input::Input::MessagesReceivedFromAgents(
|
||||
api::request::input::user_inputs::MessagesReceivedFromAgents {
|
||||
messages: messages
|
||||
.into_iter()
|
||||
.map(
|
||||
|msg| api::request::input::user_inputs::messages_received_from_agents::ReceivedMessage {
|
||||
message_id: msg.message_id,
|
||||
sender_agent_id: msg.sender_agent_id,
|
||||
addresses: msg.addresses,
|
||||
subject: msg.subject,
|
||||
message_body: msg.message_body,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
},
|
||||
),
|
||||
),
|
||||
AIAgentInput::EventsFromAgents { events } => Ok(
|
||||
api::request::input::user_inputs::user_input::Input::EventsFromAgents(
|
||||
api::request::input::user_inputs::EventsFromAgents {
|
||||
agent_events: events,
|
||||
},
|
||||
),
|
||||
),
|
||||
AIAgentInput::PassiveSuggestionResult {
|
||||
trigger,
|
||||
suggestion,
|
||||
..
|
||||
} => {
|
||||
let api_trigger = match trigger {
|
||||
Some(PassiveSuggestionTrigger::ShellCommandCompleted(shell_trigger)) => Some(
|
||||
api::passive_suggestion_result_type::Trigger::ExecutedShellCommand(
|
||||
(*shell_trigger.executed_shell_command).into(),
|
||||
),
|
||||
),
|
||||
Some(PassiveSuggestionTrigger::AgentResponseCompleted { .. }) => Some(
|
||||
api::passive_suggestion_result_type::Trigger::AgentResponseCompleted(
|
||||
api::passive_suggestion_result_type::AgentResponseCompleted {},
|
||||
),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
let api_suggestion = match suggestion {
|
||||
PassiveSuggestionResultType::Prompt { prompt } => Some(
|
||||
api::passive_suggestion_result_type::Suggestion::Prompt(
|
||||
api::passive_suggestion_result_type::Prompt { prompt },
|
||||
),
|
||||
),
|
||||
PassiveSuggestionResultType::CodeDiff {
|
||||
diffs,
|
||||
summary,
|
||||
accepted,
|
||||
} => Some(
|
||||
api::passive_suggestion_result_type::Suggestion::CodeDiff(
|
||||
api::passive_suggestion_result_type::CodeDiff {
|
||||
diffs: diffs
|
||||
.into_iter()
|
||||
.map(|d| api::passive_suggestion_result_type::code_diff::Diff {
|
||||
file_path: d.file_path,
|
||||
search: d.search,
|
||||
replace: d.replace,
|
||||
})
|
||||
.collect(),
|
||||
summary,
|
||||
accepted,
|
||||
},
|
||||
),
|
||||
),
|
||||
};
|
||||
Ok(
|
||||
api::request::input::user_inputs::user_input::Input::PassiveSuggestionResult(
|
||||
api::request::input::user_inputs::PassiveSuggestionResultInput {
|
||||
result: Some(api::PassiveSuggestionResultType {
|
||||
trigger: api_trigger,
|
||||
suggestion: api_suggestion,
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
AIAgentInput::ResumeConversation { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::InitProjectRules { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::CodeReview { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::FetchReviewComments { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::CreateEnvironment { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
AIAgentInput::InvokeSkill { .. } => Err(ConvertToAPITypeError::Ignore),
|
||||
invalid_input => Err(anyhow!(
|
||||
"Cannot convert non user query or action result input into API UserInput: {invalid_input:?}"
|
||||
).into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PassiveSuggestionTrigger> for api::request::input::generate_passive_suggestions::Trigger {
|
||||
fn from(value: PassiveSuggestionTrigger) -> Self {
|
||||
match value {
|
||||
PassiveSuggestionTrigger::FilesChanged => {
|
||||
api::request::input::generate_passive_suggestions::Trigger::FilesChanged(())
|
||||
}
|
||||
PassiveSuggestionTrigger::CommandRun => {
|
||||
api::request::input::generate_passive_suggestions::Trigger::CommandRun(())
|
||||
}
|
||||
PassiveSuggestionTrigger::ShellCommandCompleted(shell_trigger) => {
|
||||
api::request::input::generate_passive_suggestions::Trigger::ShellCommandCompleted(
|
||||
api::request::input::generate_passive_suggestions::ShellCommandCompleted {
|
||||
executed_shell_command: Some(
|
||||
(*shell_trigger.executed_shell_command).into(),
|
||||
),
|
||||
relevant_files: shell_trigger
|
||||
.relevant_files
|
||||
.into_iter()
|
||||
.flat_map(|file| Vec::<api::AnyFileContent>::from(file).into_iter())
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
}
|
||||
PassiveSuggestionTrigger::AgentResponseCompleted { .. } => {
|
||||
api::request::input::generate_passive_suggestions::Trigger::AgentResponseCompleted(
|
||||
api::request::input::generate_passive_suggestions::AgentResponseCompleted {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserQueryMode> for warp_multi_agent_api::UserQueryMode {
|
||||
fn from(value: UserQueryMode) -> Self {
|
||||
match value {
|
||||
UserQueryMode::Normal => warp_multi_agent_api::UserQueryMode { r#type: None },
|
||||
UserQueryMode::Plan => warp_multi_agent_api::UserQueryMode {
|
||||
r#type: Some(warp_multi_agent_api::user_query_mode::Type::Plan(())),
|
||||
},
|
||||
UserQueryMode::Orchestrate => warp_multi_agent_api::UserQueryMode {
|
||||
r#type: Some(warp_multi_agent_api::user_query_mode::Type::Orchestrate(())),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AIAgentAttachment> for api::Attachment {
|
||||
fn from(attachment: AIAgentAttachment) -> Self {
|
||||
match attachment {
|
||||
AIAgentAttachment::PlainText(text) => api::Attachment {
|
||||
value: Some(api::attachment::Value::PlainText(text)),
|
||||
},
|
||||
AIAgentAttachment::Block(block) => api::Attachment {
|
||||
value: Some(api::attachment::Value::ExecutedShellCommand(block.into())),
|
||||
},
|
||||
AIAgentAttachment::DriveObject { uid, payload } => api::Attachment {
|
||||
value: Some(api::attachment::Value::DriveObject(api::DriveObject {
|
||||
uid,
|
||||
object_payload: payload.map(|p| match p {
|
||||
DriveObjectPayload::Workflow {
|
||||
name,
|
||||
description,
|
||||
command,
|
||||
} => api::drive_object::ObjectPayload::Workflow(api::Workflow {
|
||||
name,
|
||||
description,
|
||||
command,
|
||||
}),
|
||||
DriveObjectPayload::Notebook { title, content } => {
|
||||
api::drive_object::ObjectPayload::Notebook(api::Notebook {
|
||||
title,
|
||||
content,
|
||||
})
|
||||
}
|
||||
DriveObjectPayload::GenericStringObject {
|
||||
payload,
|
||||
object_type,
|
||||
} => api::drive_object::ObjectPayload::GenericStringObject(
|
||||
api::GenericStringObject {
|
||||
payload,
|
||||
object_type,
|
||||
},
|
||||
),
|
||||
}),
|
||||
})),
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
AIAgentAttachment::DiffHunk {
|
||||
file_path,
|
||||
line_range,
|
||||
diff_content,
|
||||
lines_added,
|
||||
lines_removed,
|
||||
current,
|
||||
base,
|
||||
} => api::Attachment {
|
||||
value: Some(api::attachment::Value::DiffHunk(api::DiffHunk {
|
||||
file_path,
|
||||
line_range: Some(api::FileContentLineRange {
|
||||
start: line_range.start.as_usize() as u32,
|
||||
end: line_range.end.as_usize() as u32,
|
||||
}),
|
||||
diff_content,
|
||||
lines_added,
|
||||
lines_removed,
|
||||
current: current.map(Into::into),
|
||||
base: Some(base.into()),
|
||||
})),
|
||||
},
|
||||
AIAgentAttachment::DocumentContent {
|
||||
document_id,
|
||||
content,
|
||||
line_range,
|
||||
// TODO: Add attachment source to API
|
||||
..
|
||||
} => api::Attachment {
|
||||
value: Some(api::attachment::Value::DocumentContent(
|
||||
api::DocumentContent {
|
||||
document_id,
|
||||
content,
|
||||
line_range: line_range.map(|range| api::FileContentLineRange {
|
||||
start: range.start.as_usize() as u32,
|
||||
end: range.end.as_usize() as u32,
|
||||
}),
|
||||
},
|
||||
)),
|
||||
},
|
||||
AIAgentAttachment::DiffSet {
|
||||
file_diffs,
|
||||
current,
|
||||
base,
|
||||
} => api::Attachment {
|
||||
value: Some(api::attachment::Value::DiffSet(api::DiffSet {
|
||||
hunks: file_diffs
|
||||
.into_iter()
|
||||
.flat_map(|(file_path, hunks)| {
|
||||
hunks
|
||||
.into_iter()
|
||||
.map(move |hunk| hunk.convert_to_api(file_path.clone()))
|
||||
})
|
||||
.collect(),
|
||||
curr_ref: current.map(Into::into),
|
||||
base_ref: Some(base.into()),
|
||||
})),
|
||||
},
|
||||
AIAgentAttachment::FilePathReference { file_path, .. } => api::Attachment {
|
||||
value: Some(api::attachment::Value::FilePathReference(
|
||||
api::FilePathReference { file_path },
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<AIAgentActionResult> for api::request::input::user_inputs::user_input::Input {
|
||||
type Error = ConvertToAPITypeError;
|
||||
|
||||
fn try_from(action_result: AIAgentActionResult) -> Result<Self, Self::Error> {
|
||||
let result = match action_result.result {
|
||||
AIAgentActionResultType::RequestCommandOutput(request_command_result) => {
|
||||
Some(request_command_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::WriteToLongRunningShellCommand(result) => {
|
||||
Some(result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::ReadFiles(read_files_result) => {
|
||||
Some(read_files_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::UploadArtifact(upload_artifact_result) => {
|
||||
Some(upload_artifact_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::SearchCodebase(search_codebase_result) => {
|
||||
Some(search_codebase_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::RequestFileEdits(request_file_edits_result) => {
|
||||
Some(request_file_edits_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::Grep(grep_result) => Some(grep_result.try_into()?),
|
||||
AIAgentActionResultType::FileGlob(file_glob_result) => {
|
||||
Some(file_glob_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::FileGlobV2(file_glob_result) => {
|
||||
Some(file_glob_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::ReadMCPResource(read_mcp_resource_result) => {
|
||||
Some(read_mcp_resource_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::CallMCPTool(call_mcp_tool_result) => {
|
||||
Some(call_mcp_tool_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::ReadSkill(read_skill_result) => {
|
||||
Some(read_skill_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::SuggestNewConversation(suggest_new_conversation_result) => {
|
||||
Some(suggest_new_conversation_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::SuggestPrompt(suggest_prompt_result) => {
|
||||
Some(suggest_prompt_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::OpenCodeReview => Some(
|
||||
warp_multi_agent_api::request::input::tool_call_result::Result::OpenCodeReview(
|
||||
warp_multi_agent_api::OpenCodeReviewResult {},
|
||||
),
|
||||
),
|
||||
AIAgentActionResultType::InsertReviewComments(insert_review_comments_result) => {
|
||||
Some(insert_review_comments_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::InitProject => Some(
|
||||
warp_multi_agent_api::request::input::tool_call_result::Result::InitProject(
|
||||
warp_multi_agent_api::InitProjectResult {},
|
||||
),
|
||||
),
|
||||
AIAgentActionResultType::ReadDocuments(read_documents_result) => {
|
||||
Some(read_documents_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::EditDocuments(edit_documents_result) => {
|
||||
Some(edit_documents_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::CreateDocuments(create_documents_result) => {
|
||||
Some(create_documents_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::ReadShellCommandOutput(read_shell_command_output_result) => {
|
||||
Some(read_shell_command_output_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::UseComputer(use_computer_result) => {
|
||||
Some(use_computer_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::RequestComputerUse(request_computer_use_result) => {
|
||||
Some(request_computer_use_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::FetchConversation(fetch_conversation_result) => {
|
||||
Some(fetch_conversation_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::StartAgent(start_agent_result) => {
|
||||
Some(start_agent_result.into())
|
||||
}
|
||||
AIAgentActionResultType::SendMessageToAgent(send_message_result) => {
|
||||
Some(send_message_result.into())
|
||||
}
|
||||
AIAgentActionResultType::TransferShellCommandControlToUser(transfer_control_result) => {
|
||||
Some(transfer_control_result.try_into()?)
|
||||
}
|
||||
AIAgentActionResultType::AskUserQuestion(ask_user_question_result) => {
|
||||
Some(ask_user_question_result.into())
|
||||
}
|
||||
};
|
||||
Ok(
|
||||
api::request::input::user_inputs::user_input::Input::ToolCallResult(
|
||||
api::request::input::ToolCallResult {
|
||||
tool_call_id: action_result.id.into(),
|
||||
result,
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_context(context: &[AIAgentContext]) -> api::InputContext {
|
||||
let mut api_context = api::InputContext::default();
|
||||
for context in context.iter().cloned() {
|
||||
match context {
|
||||
AIAgentContext::Block(block) => {
|
||||
#[allow(deprecated)]
|
||||
api_context.executed_shell_commands.push((*block).into());
|
||||
}
|
||||
AIAgentContext::Directory {
|
||||
pwd,
|
||||
home_dir,
|
||||
are_file_symbols_indexed,
|
||||
} => {
|
||||
api_context.directory = Some(api::input_context::Directory {
|
||||
pwd: pwd.unwrap_or_default(),
|
||||
home: home_dir.unwrap_or_default(),
|
||||
pwd_file_symbols_indexed: are_file_symbols_indexed,
|
||||
});
|
||||
}
|
||||
AIAgentContext::SelectedText(text) => {
|
||||
api_context
|
||||
.selected_text
|
||||
.push(api::input_context::SelectedText { text });
|
||||
}
|
||||
AIAgentContext::ExecutionEnvironment(execution_ctx) => {
|
||||
api_context.shell = Some(api::input_context::Shell {
|
||||
name: execution_ctx.shell_name,
|
||||
version: execution_ctx.shell_version.unwrap_or_default(),
|
||||
});
|
||||
|
||||
if execution_ctx.os.category.is_none() && execution_ctx.os.distribution.is_none() {
|
||||
continue;
|
||||
}
|
||||
api_context.operating_system = Some(api::input_context::OperatingSystem {
|
||||
platform: execution_ctx.os.category.unwrap_or_default(),
|
||||
distribution: execution_ctx.os.distribution.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
AIAgentContext::CurrentTime { current_time } => {
|
||||
let utc_time = current_time.to_utc();
|
||||
api_context.current_time = Some(prost_types::Timestamp {
|
||||
seconds: utc_time.timestamp(),
|
||||
nanos: utc_time.nanosecond() as i32,
|
||||
});
|
||||
}
|
||||
AIAgentContext::Image(image_context) => {
|
||||
api_context.images.push(api::input_context::Image {
|
||||
data: image_context.data.into(),
|
||||
mime_type: image_context.mime_type,
|
||||
});
|
||||
}
|
||||
AIAgentContext::Codebase { path, name } => {
|
||||
api_context
|
||||
.codebases
|
||||
.push(api::input_context::Codebase { path, name });
|
||||
}
|
||||
AIAgentContext::ProjectRules {
|
||||
root_path,
|
||||
active_rules,
|
||||
additional_rule_paths,
|
||||
} => {
|
||||
api_context
|
||||
.project_rules
|
||||
.push(api::input_context::ProjectRules {
|
||||
root_path,
|
||||
active_rule_files: active_rules
|
||||
.into_iter()
|
||||
.flat_map(|rule| {
|
||||
let file_contents: Vec<api::FileContent> = rule.into();
|
||||
file_contents.into_iter()
|
||||
})
|
||||
.collect(),
|
||||
additional_rule_file_paths: additional_rule_paths,
|
||||
});
|
||||
}
|
||||
AIAgentContext::File(file_context) => {
|
||||
let contents: Vec<api::FileContent> = file_context.into();
|
||||
|
||||
for content in contents {
|
||||
api_context.files.push(api::input_context::File {
|
||||
content: Some(content),
|
||||
});
|
||||
}
|
||||
}
|
||||
AIAgentContext::Git { head, branch } => {
|
||||
api_context.git = Some(api::input_context::Git {
|
||||
head,
|
||||
branch: branch.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
AIAgentContext::Skills { skills } => {
|
||||
api_context.updated_skills_context = Some(api::input_context::SkillsContext {
|
||||
available_skills: skills
|
||||
.into_iter()
|
||||
.map(|skill| api::SkillDescriptor {
|
||||
skill_reference: Some(skill.reference.into()),
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
provider: Some(skill.provider.into()),
|
||||
scope: Some(skill.scope.into()),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
api_context
|
||||
}
|
||||
|
||||
impl From<Suggestions> for api::Suggestions {
|
||||
fn from(value: Suggestions) -> Self {
|
||||
Self {
|
||||
rules: value
|
||||
.rules
|
||||
.into_iter()
|
||||
.map(|rule| api::SuggestedRule {
|
||||
name: rule.name,
|
||||
content: rule.content,
|
||||
logging_id: rule.logging_id.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
workflows: value
|
||||
.agent_mode_workflows
|
||||
.into_iter()
|
||||
.map(|workflow| api::SuggestedAgentModeWorkflow {
|
||||
name: workflow.name,
|
||||
prompt: workflow.prompt,
|
||||
logging_id: workflow.logging_id.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert rmcp resource to proto format.
|
||||
fn convert_mcp_resource(resource: rmcp::model::Resource) -> api::request::mcp_context::McpResource {
|
||||
let rmcp::model::RawResource {
|
||||
uri,
|
||||
name,
|
||||
description,
|
||||
mime_type,
|
||||
..
|
||||
} = resource.raw;
|
||||
api::request::mcp_context::McpResource {
|
||||
uri,
|
||||
name,
|
||||
description: description.unwrap_or_default(),
|
||||
mime_type: mime_type.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Convert rmcp tool to proto format, skipping tools with invalid schemas.
|
||||
fn convert_mcp_tool(tool: rmcp::model::Tool) -> Option<api::request::mcp_context::McpTool> {
|
||||
let Ok(prost_types::Value {
|
||||
kind: Some(prost_types::value::Kind::StructValue(input_schema)),
|
||||
}) = serde_json_to_prost(tool.input_schema.as_ref().clone().into())
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(api::request::mcp_context::McpTool {
|
||||
name: tool.name.to_string(),
|
||||
description: tool.description.map(|d| d.to_string()).unwrap_or_default(),
|
||||
input_schema: Some(input_schema),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<MCPContext> for api::request::McpContext {
|
||||
#[allow(deprecated)]
|
||||
fn from(value: MCPContext) -> Self {
|
||||
// Check if we're using the old flat structure (no servers)
|
||||
// or the new grouped structure (servers populated)
|
||||
if value.servers.is_empty() {
|
||||
// Old behavior: use deprecated flat resources and tools lists
|
||||
api::request::McpContext {
|
||||
#[allow(deprecated)]
|
||||
resources: value
|
||||
.resources
|
||||
.into_iter()
|
||||
.map(convert_mcp_resource)
|
||||
.collect(),
|
||||
#[allow(deprecated)]
|
||||
tools: value
|
||||
.tools
|
||||
.into_iter()
|
||||
.filter_map(convert_mcp_tool)
|
||||
.collect(),
|
||||
servers: vec![], // Empty for old behavior
|
||||
}
|
||||
} else {
|
||||
// New behavior: group by server
|
||||
let servers: Vec<_> = value
|
||||
.servers
|
||||
.into_iter()
|
||||
.map(|server| api::request::mcp_context::McpServer {
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
description: server.description,
|
||||
resources: server
|
||||
.resources
|
||||
.into_iter()
|
||||
.map(convert_mcp_resource)
|
||||
.collect(),
|
||||
tools: server
|
||||
.tools
|
||||
.into_iter()
|
||||
.filter_map(convert_mcp_tool)
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
api::request::McpContext {
|
||||
#[allow(deprecated)]
|
||||
resources: vec![], // Empty - everything is grouped by server
|
||||
#[allow(deprecated)]
|
||||
tools: vec![], // Empty - everything is grouped by server
|
||||
servers,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockContext> for api::ExecutedShellCommand {
|
||||
fn from(block: BlockContext) -> Self {
|
||||
api::ExecutedShellCommand {
|
||||
command: block.command,
|
||||
output: block.output,
|
||||
exit_code: block.exit_code.value(),
|
||||
command_id: block.id.into(),
|
||||
is_auto_attached: block.is_auto_attached,
|
||||
started_ts: block.started_ts.map(local_datetime_to_timestamp),
|
||||
finished_ts: block.finished_ts.map(local_datetime_to_timestamp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trys to convert a [`serde_json::Value`] to a [`prost_types::Value`].
|
||||
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
fn serde_json_to_prost(value: serde_json::Value) -> Result<prost_types::Value, String> {
|
||||
use prost_types::value::Kind::*;
|
||||
use serde_json::Value::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
Ok(prost_types::Value {
|
||||
kind: Some(match value {
|
||||
Null => NullValue(0),
|
||||
Bool(v) => BoolValue(v),
|
||||
Number(n) => NumberValue(
|
||||
n.as_f64()
|
||||
.ok_or_else(|| format!("float {n} is not valid JSON number"))?,
|
||||
),
|
||||
String(s) => StringValue(s),
|
||||
Array(a) => ListValue(prost_types::ListValue {
|
||||
values: a
|
||||
.into_iter()
|
||||
.map(serde_json_to_prost)
|
||||
.collect::<Result<Vec<_>, std::string::String>>()?,
|
||||
}),
|
||||
Object(v) => StructValue(prost_types::Struct {
|
||||
fields: v
|
||||
.into_iter()
|
||||
.map(|(k, v)| serde_json_to_prost(v).map(|v| (k, v)))
|
||||
.collect::<Result<BTreeMap<_, _>, std::string::String>>()?,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "convert_to_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::ai::agent::task::TaskId;
|
||||
use crate::ai::agent::{
|
||||
AIAgentActionResult, AIAgentActionResultType, TransferShellCommandControlToUserResult,
|
||||
};
|
||||
use crate::terminal::model::block::BlockId;
|
||||
use warp_core::command::ExitCode;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
#[test]
|
||||
fn transfer_control_snapshot_result_converts_to_tool_call_result_input() {
|
||||
let block_id = BlockId::default();
|
||||
let input =
|
||||
api::request::input::user_inputs::user_input::Input::try_from(AIAgentActionResult {
|
||||
id: "tool_call".to_string().into(),
|
||||
task_id: TaskId::new("task".to_string()),
|
||||
result: AIAgentActionResultType::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::Snapshot {
|
||||
block_id: block_id.clone(),
|
||||
grid_contents: "snapshot".to_string(),
|
||||
cursor: "<|cursor|>".to_string(),
|
||||
is_alt_screen_active: false,
|
||||
is_preempted: false,
|
||||
},
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match input {
|
||||
api::request::input::user_inputs::user_input::Input::ToolCallResult(result) => {
|
||||
assert_eq!(result.tool_call_id, "tool_call");
|
||||
match result.result {
|
||||
Some(api::request::input::tool_call_result::Result::TransferShellCommandControlToUser(
|
||||
api_result,
|
||||
)) => match api_result.result {
|
||||
Some(
|
||||
api::transfer_shell_command_control_to_user_result::Result::LongRunningCommandSnapshot(snapshot),
|
||||
) => {
|
||||
assert_eq!(snapshot.command_id, block_id.to_string());
|
||||
assert_eq!(snapshot.output, "snapshot");
|
||||
assert_eq!(snapshot.cursor, "<|cursor|>");
|
||||
}
|
||||
other => panic!("Expected snapshot result, got {other:?}"),
|
||||
},
|
||||
other => panic!("Expected transfer-control tool call result, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected tool-call-result input, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_control_finished_result_converts_to_tool_call_result_input() {
|
||||
let block_id = BlockId::default();
|
||||
let input =
|
||||
api::request::input::user_inputs::user_input::Input::try_from(AIAgentActionResult {
|
||||
id: "tool_call".to_string().into(),
|
||||
task_id: TaskId::new("task".to_string()),
|
||||
result: AIAgentActionResultType::TransferShellCommandControlToUser(
|
||||
TransferShellCommandControlToUserResult::CommandFinished {
|
||||
block_id: block_id.clone(),
|
||||
output: "done".to_string(),
|
||||
exit_code: ExitCode::from(17),
|
||||
},
|
||||
),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
match input {
|
||||
api::request::input::user_inputs::user_input::Input::ToolCallResult(result) => {
|
||||
assert_eq!(result.tool_call_id, "tool_call");
|
||||
match result.result {
|
||||
Some(api::request::input::tool_call_result::Result::TransferShellCommandControlToUser(
|
||||
api_result,
|
||||
)) => match api_result.result {
|
||||
Some(
|
||||
api::transfer_shell_command_control_to_user_result::Result::CommandFinished(finished),
|
||||
) => {
|
||||
assert_eq!(finished.command_id, block_id.to_string());
|
||||
assert_eq!(finished.output, "done");
|
||||
assert_eq!(finished.exit_code, 17);
|
||||
}
|
||||
other => panic!("Expected command-finished result, got {other:?}"),
|
||||
},
|
||||
other => panic!("Expected transfer-control tool call result, got {other:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected tool-call-result input, got {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{ai::agent::redaction, terminal::model::session::SessionType};
|
||||
use futures_util::StreamExt;
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use crate::server::server_api::ServerApi;
|
||||
|
||||
use super::{convert_to::convert_input, ConvertToAPITypeError, RequestParams, ResponseStream};
|
||||
|
||||
pub async fn generate_multi_agent_output(
|
||||
server_api: Arc<ServerApi>,
|
||||
mut params: RequestParams,
|
||||
cancellation_rx: futures::channel::oneshot::Receiver<()>,
|
||||
) -> Result<ResponseStream, ConvertToAPITypeError> {
|
||||
let supported_tools = params
|
||||
.supported_tools_override
|
||||
.take()
|
||||
.unwrap_or_else(|| get_supported_tools(¶ms));
|
||||
let supported_cli_agent_tools = get_supported_cli_agent_tools(¶ms);
|
||||
let mut logging_metadata = HashMap::new();
|
||||
if let Some(metadata) = params.metadata {
|
||||
logging_metadata.insert(
|
||||
"is_autodetected_user_query".to_owned(),
|
||||
prost_types::Value {
|
||||
kind: Some(prost_types::value::Kind::BoolValue(
|
||||
metadata.is_autodetected_user_query,
|
||||
)),
|
||||
},
|
||||
);
|
||||
logging_metadata.insert(
|
||||
"entrypoint".to_owned(),
|
||||
prost_types::Value {
|
||||
kind: Some(prost_types::value::Kind::StringValue(
|
||||
metadata.entrypoint.entrypoint(),
|
||||
)),
|
||||
},
|
||||
);
|
||||
logging_metadata.insert(
|
||||
"is_auto_resume_after_error".to_owned(),
|
||||
prost_types::Value {
|
||||
kind: Some(prost_types::value::Kind::BoolValue(
|
||||
metadata.is_auto_resume_after_error,
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if params.should_redact_secrets {
|
||||
redaction::redact_inputs(&mut params.input);
|
||||
}
|
||||
|
||||
let mut api_keys = params.api_keys;
|
||||
if let Some(api_keys) = &mut api_keys {
|
||||
api_keys.allow_use_of_warp_credits = params.allow_use_of_warp_credits_with_byok;
|
||||
}
|
||||
|
||||
let request = api::Request {
|
||||
task_context: Some(api::request::TaskContext {
|
||||
tasks: params.tasks,
|
||||
}),
|
||||
input: Some(convert_input(params.input)?),
|
||||
settings: Some(api::request::Settings {
|
||||
model_config: Some(api::request::settings::ModelConfig {
|
||||
base: params.model.into(),
|
||||
cli_agent: params.cli_agent_model.into(),
|
||||
computer_use_agent: params.computer_use_model.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
rules_enabled: params.is_memory_enabled,
|
||||
warp_drive_context_enabled: params.warp_drive_context_enabled,
|
||||
web_context_retrieval_enabled: true,
|
||||
supports_parallel_tool_calls: true,
|
||||
use_anthropic_text_editor_tools: false,
|
||||
planning_enabled: params.planning_enabled,
|
||||
supports_create_files: true,
|
||||
supported_tools: supported_tools.into_iter().map(Into::into).collect(),
|
||||
supports_long_running_commands: true,
|
||||
should_preserve_file_content_in_history: true,
|
||||
supports_todos_ui: true,
|
||||
supports_linked_code_blocks: FeatureFlag::LinkedCodeBlocks.is_enabled(),
|
||||
supports_started_child_task_message: true,
|
||||
supports_suggest_prompt: true,
|
||||
supports_read_image_files: FeatureFlag::ReadImageFiles.is_enabled(),
|
||||
supports_reasoning_message: true,
|
||||
api_keys,
|
||||
autonomy_level: params.autonomy_level.into(),
|
||||
isolation_level: params.isolation_level.into(),
|
||||
web_search_enabled: params.web_search_enabled,
|
||||
supported_cli_agent_tools: supported_cli_agent_tools
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
supports_v4a_file_diffs: FeatureFlag::V4AFileDiffs.is_enabled(),
|
||||
supports_summarization_via_message_replacement:
|
||||
FeatureFlag::SummarizationViaMessageReplacement.is_enabled(),
|
||||
supports_bundled_skills: FeatureFlag::BundledSkills.is_enabled(),
|
||||
supports_research_agent: params.research_agent_enabled,
|
||||
supports_orchestration_v2: FeatureFlag::OrchestrationV2.is_enabled(),
|
||||
}),
|
||||
metadata: Some(api::request::Metadata {
|
||||
logging: logging_metadata,
|
||||
conversation_id: params
|
||||
.conversation_token
|
||||
.as_ref()
|
||||
.map(|token| token.as_str().to_string())
|
||||
.unwrap_or_default(),
|
||||
ambient_agent_task_id: params
|
||||
.ambient_agent_task_id
|
||||
.map(|id| id.to_string())
|
||||
.unwrap_or_default(),
|
||||
forked_from_conversation_id: if params.conversation_token.is_none() {
|
||||
// We only include this param on our initial request to the server
|
||||
// (when the forked conversation has not been asigned a new id yet).
|
||||
params
|
||||
.forked_from_conversation_token
|
||||
.map(|token| token.as_str().to_string())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
parent_agent_id: params.parent_agent_id.unwrap_or_default(),
|
||||
agent_name: params.agent_name.unwrap_or_default(),
|
||||
}),
|
||||
existing_suggestions: params
|
||||
.existing_suggestions
|
||||
.map(|suggestions| suggestions.into()),
|
||||
mcp_context: params.mcp_context.map(Into::into),
|
||||
};
|
||||
|
||||
let response_stream = server_api.generate_multi_agent_output(&request).await;
|
||||
match response_stream {
|
||||
Ok(stream) => {
|
||||
let output_stream = stream.take_until(cancellation_rx);
|
||||
Ok(Box::pin(output_stream))
|
||||
}
|
||||
Err(e) => {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
let _ = tx.send(Err(e)).await;
|
||||
Ok(Box::pin(rx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_supported_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
let mut supported_tools = vec![
|
||||
api::ToolType::Grep,
|
||||
api::ToolType::FileGlob,
|
||||
api::ToolType::FileGlobV2,
|
||||
api::ToolType::ReadMcpResource,
|
||||
api::ToolType::CallMcpTool,
|
||||
api::ToolType::InitProject,
|
||||
api::ToolType::OpenCodeReview,
|
||||
api::ToolType::RunShellCommand,
|
||||
api::ToolType::SuggestNewConversation,
|
||||
api::ToolType::Subagent,
|
||||
api::ToolType::WriteToLongRunningShellCommand,
|
||||
api::ToolType::ReadShellCommandOutput,
|
||||
api::ToolType::ReadDocuments,
|
||||
api::ToolType::CreateDocuments,
|
||||
api::ToolType::EditDocuments,
|
||||
api::ToolType::SuggestPrompt,
|
||||
];
|
||||
|
||||
if FeatureFlag::ConversationsAsContext.is_enabled() {
|
||||
supported_tools.push(api::ToolType::FetchConversation);
|
||||
}
|
||||
|
||||
match params.session_context.session_type() {
|
||||
None | Some(SessionType::Local) => {
|
||||
supported_tools.extend(&[
|
||||
api::ToolType::ReadFiles,
|
||||
api::ToolType::ApplyFileDiffs,
|
||||
api::ToolType::SearchCodebase,
|
||||
]);
|
||||
|
||||
if FeatureFlag::ArtifactCommand.is_enabled() {
|
||||
supported_tools.push(api::ToolType::UploadFileArtifact);
|
||||
}
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
// Remote session with a known host — enable tools that route
|
||||
// through RemoteServerClient. The host_id is only populated
|
||||
// after a successful connection handshake, so its presence is a
|
||||
// sufficient proxy for client availability.
|
||||
// SearchCodebase remains disabled (follow-up work).
|
||||
supported_tools.extend(&[api::ToolType::ReadFiles, api::ToolType::ApplyFileDiffs]);
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {
|
||||
// Feature flag off or not yet connected — no remote tools.
|
||||
}
|
||||
}
|
||||
|
||||
if FeatureFlag::AgentModeComputerUse.is_enabled() && params.computer_use_enabled {
|
||||
supported_tools.extend(&[api::ToolType::UseComputer]);
|
||||
supported_tools.extend(&[api::ToolType::RequestComputerUse])
|
||||
}
|
||||
|
||||
if FeatureFlag::PRCommentsSlashCommand.is_enabled() {
|
||||
supported_tools.push(api::ToolType::InsertReviewComments);
|
||||
}
|
||||
|
||||
if FeatureFlag::ListSkills.is_enabled() {
|
||||
supported_tools.push(api::ToolType::ReadSkill);
|
||||
}
|
||||
|
||||
if params.orchestration_enabled {
|
||||
supported_tools.push(if FeatureFlag::OrchestrationV2.is_enabled() {
|
||||
api::ToolType::StartAgentV2
|
||||
} else {
|
||||
api::ToolType::StartAgent
|
||||
});
|
||||
supported_tools.push(api::ToolType::SendMessageToAgent);
|
||||
}
|
||||
|
||||
if FeatureFlag::AskUserQuestion.is_enabled() && params.ask_user_question_enabled {
|
||||
supported_tools.push(api::ToolType::AskUserQuestion);
|
||||
}
|
||||
|
||||
supported_tools
|
||||
}
|
||||
|
||||
fn get_supported_cli_agent_tools(params: &RequestParams) -> Vec<api::ToolType> {
|
||||
let mut supported_cli_agent_tools = vec![
|
||||
api::ToolType::WriteToLongRunningShellCommand,
|
||||
api::ToolType::ReadShellCommandOutput,
|
||||
api::ToolType::Grep,
|
||||
api::ToolType::FileGlob,
|
||||
api::ToolType::FileGlobV2,
|
||||
];
|
||||
|
||||
if FeatureFlag::TransferControlTool.is_enabled() {
|
||||
supported_cli_agent_tools.push(api::ToolType::TransferShellCommandControlToUser);
|
||||
}
|
||||
|
||||
match params.session_context.session_type() {
|
||||
None | Some(SessionType::Local) => {
|
||||
supported_cli_agent_tools
|
||||
.extend(&[api::ToolType::ReadFiles, api::ToolType::SearchCodebase]);
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: Some(_) }) => {
|
||||
supported_cli_agent_tools.push(api::ToolType::ReadFiles);
|
||||
}
|
||||
Some(SessionType::WarpifiedRemote { host_id: None }) => {}
|
||||
}
|
||||
|
||||
supported_cli_agent_tools
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "impl_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,81 @@
|
||||
use crate::ai::agent::api::RequestParams;
|
||||
use crate::ai::blocklist::SessionContext;
|
||||
use crate::ai::llms::LLMId;
|
||||
use warp_core::features::FeatureFlag;
|
||||
use warp_multi_agent_api as api;
|
||||
|
||||
use super::get_supported_tools;
|
||||
|
||||
fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool) -> RequestParams {
|
||||
let model = LLMId::from("test-model");
|
||||
|
||||
RequestParams {
|
||||
input: vec![],
|
||||
conversation_token: None,
|
||||
forked_from_conversation_token: None,
|
||||
ambient_agent_task_id: None,
|
||||
tasks: vec![],
|
||||
existing_suggestions: None,
|
||||
metadata: None,
|
||||
session_context: SessionContext::new_for_test(),
|
||||
model: model.clone(),
|
||||
coding_model: model.clone(),
|
||||
cli_agent_model: model.clone(),
|
||||
computer_use_model: model,
|
||||
is_memory_enabled: false,
|
||||
warp_drive_context_enabled: false,
|
||||
mcp_context: None,
|
||||
planning_enabled: true,
|
||||
should_redact_secrets: false,
|
||||
api_keys: None,
|
||||
allow_use_of_warp_credits_with_byok: false,
|
||||
autonomy_level: api::AutonomyLevel::Supervised,
|
||||
isolation_level: api::IsolationLevel::None,
|
||||
web_search_enabled: false,
|
||||
computer_use_enabled: false,
|
||||
ask_user_question_enabled,
|
||||
research_agent_enabled: false,
|
||||
orchestration_enabled: false,
|
||||
supported_tools_override: None,
|
||||
parent_agent_id: None,
|
||||
agent_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_omits_ask_user_question_when_disabled() {
|
||||
let params = request_params_with_ask_user_question_enabled(false);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::AskUserQuestion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_includes_ask_user_question_when_enabled_and_feature_flag_is_enabled() {
|
||||
if !FeatureFlag::AskUserQuestion.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = request_params_with_ask_user_question_enabled(true);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(supported_tools.contains(&api::ToolType::AskUserQuestion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_include_upload_artifact_when_feature_flag_is_enabled() {
|
||||
let _flag = FeatureFlag::ArtifactCommand.override_enabled(true);
|
||||
let params = request_params_with_ask_user_question_enabled(false);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(supported_tools.contains(&api::ToolType::UploadFileArtifact));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_tools_omit_upload_artifact_when_feature_flag_is_disabled() {
|
||||
let _flag = FeatureFlag::ArtifactCommand.override_enabled(false);
|
||||
let params = request_params_with_ask_user_question_enabled(false);
|
||||
let supported_tools = get_supported_tools(¶ms);
|
||||
|
||||
assert!(!supported_tools.contains(&api::ToolType::UploadFileArtifact));
|
||||
}
|
||||
Reference in New Issue
Block a user