Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+944
View File
@@ -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;
+644
View File
@@ -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")
}
}
}
+977
View File
@@ -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;
+89
View File
@@ -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:?}"),
}
}
+253
View File
@@ -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(&params));
let supported_cli_agent_tools = get_supported_cli_agent_tools(&params);
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;
+81
View File
@@ -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(&params);
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(&params);
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(&params);
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(&params);
assert!(!supported_tools.contains(&api::ToolType::UploadFileArtifact));
}