Files
galaxy/app/src/ai/agent/api/convert_from.rs
T

1068 lines
48 KiB
Rust

//! Conversions from MAA API types to application types.
use std::collections::HashMap;
use std::time::Duration;
use ai::agent::action::{LifecycleEventType as StartAgentLifecycleEventType, ReadSkillRequest};
use ai::agent::action_result::StartAgentVersion;
use ai::agent::convert::ToolToAIAgentActionError;
use ai::agent::UnknownCitationTypeError;
use ai::skills::{
skill_reference_from_api_skill_ref, skill_reference_from_read_skill_ref, SkillPathOrigin,
};
use api::ask_user_question::question::QuestionType;
use galaxy_core::channel::ChannelState;
use warp_multi_agent_api as api;
use crate::ai::agent::api::convert_conversation::{
convert_input_context, convert_tool_call_result_to_input,
};
use crate::ai::agent::api::is_internal_command_completion_assessment;
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;
use crate::ai::agent::{
runtime_activity, AIAgentAction, AIAgentActionType, AIAgentAttachment, AIAgentCitation,
AIAgentInput, AIAgentOutputMessage, AIAgentText, AIAgentTodo, ArtifactCreatedData,
CloneRepositoryURL, MessageId, RunAgentsAgentRunConfig, RunAgentsExecutionMode,
RunAgentsRequest, StartAgentExecutionMode, SubagentCall, SubagentType,
SuggestedAgentModeWorkflow, SuggestedRule, Suggestions, SummarizationType, TodoOperation,
UserQueryMode, WebFetchStatus, WebSearchStatus,
};
use crate::ai::artifact_download::sanitized_basename;
use crate::ai::document::ai_document_model::{AIDocumentId, AIDocumentVersion};
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"),
}
}
}
fn convert_read_skill(
read_skill: api::message::tool_call::ReadSkill,
skill_path_origin: &SkillPathOrigin,
) -> Result<AIAgentActionType, ToolToAIAgentActionError> {
let Some(reference) = read_skill.skill_reference else {
return Err(ToolToAIAgentActionError::MissingSkillReference);
};
let skill = skill_reference_from_read_skill_ref(reference, skill_path_origin)
.map_err(|_| ToolToAIAgentActionError::MissingSkillReference)?;
Ok(AIAgentActionType::ReadSkill(ReadSkillRequest { skill }))
}
/// 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())
}
/// Maps the proto `Harness` oneof to a client-side string identifier
/// (e.g. "oz", "claude"). Returns `None` for an unset variant.
pub(crate) fn convert_run_agents_harness(harness: Option<&api::Harness>) -> Option<String> {
let variant = harness?.variant.as_ref()?;
Some(
match variant {
api::harness::Variant::Oz(_) => "oz",
api::harness::Variant::ClaudeCode(_) => "claude",
api::harness::Variant::OpenCode(_) => "opencode",
api::harness::Variant::Gemini(_) => "gemini",
api::harness::Variant::Codex(_) => "codex",
}
.to_string(),
)
}
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_run_agents_execution_mode(
execution_mode: Option<api::run_agents::ExecutionMode>,
) -> RunAgentsExecutionMode {
match execution_mode {
Some(api::run_agents::ExecutionMode::Remote(remote)) => RunAgentsExecutionMode::Remote {
environment_id: remote.environment_id,
worker_host: remote.worker_host,
computer_use_enabled: remote.computer_use_enabled,
},
Some(api::run_agents::ExecutionMode::Local(_)) | None => RunAgentsExecutionMode::Local,
}
}
fn convert_run_agents(
run_agents: api::RunAgents,
skill_path_origin: &SkillPathOrigin,
) -> AIAgentActionType {
let api::RunAgents {
summary,
base_prompt,
skills,
model_id,
harness,
agent_run_configs,
execution_mode,
plan_id,
} = run_agents;
AIAgentActionType::RunAgents(RunAgentsRequest {
summary,
base_prompt,
skills: skills
.into_iter()
.filter_map(|skill| skill_reference_from_api_skill_ref(skill, skill_path_origin))
.collect(),
model_id,
harness_type: convert_run_agents_harness(harness.as_ref()).unwrap_or_default(),
execution_mode: convert_run_agents_execution_mode(execution_mode),
agent_run_configs: agent_run_configs
.into_iter()
.map(|config| RunAgentsAgentRunConfig {
name: config.name,
prompt: config.prompt,
title: config.title,
})
.collect(),
plan_id,
// Auth secret is a client-side dispatch concern populated by the
// confirmation card from `CloudAgentSettings.last_selected_auth_secret`
// before Accept. The proto does not carry it.
harness_auth_secret_name: None,
})
}
fn convert_start_agent_v2_execution_mode(
execution_mode: Option<api::start_agent_v2::ExecutionMode>,
skill_path_origin: &SkillPathOrigin,
) -> 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(|skill| {
skill_reference_from_api_skill_ref(skill, skill_path_origin)
})
.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,
// Auth secret is plumbed client-side via `RunAgentsRequest`;
// StartAgentV2 from the server never carries it.
auth_secret_name: None,
}
}
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(),
}
}
/// 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>,
pub skill_path_origin: &'a SkillPathOrigin,
}
/// 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) => {
let message = if let Some(activity) =
runtime_activity::decode(&self.server_message_data)
{
AIAgentOutputMessage::runtime_activity(MessageId::new(self.id), activity)
} else {
AIAgentOutputMessage::text(MessageId::new(self.id), output.into())
};
Ok(MaybeAIAgentOutputMessage::Message(
message.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(_)
// Stage 2 plan-card config snapshot: hydrated separately by the
// plan card's `AIDocumentModel` subscription, not via the
// exchange/output stream. No client output message representation.
| api::message::Message::OrchestrationConfigSnapshot(_) => {
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>;
}
/// Tries 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);
};
// Detect notebook tool name encoded in tool_call_id prefix.
let (effective_tool_call_id, tool_name) =
if let Some(stripped) = self.tool_call_id.strip_prefix("notebook::") {
(stripped.to_string(), Some("notebook".to_string()))
} else {
(self.tool_call_id.clone(), None)
};
let create_standard_action = |action: AIAgentActionType| {
Ok(MaybeAIAgentAction::Action(AIAgentAction {
id: effective_tool_call_id.clone().into(),
task_id: params.task_id.clone(),
action,
requires_result: true,
tool_name: tool_name.clone(),
}))
};
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::conversation_search_metadata::Target;
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, agent_run_id) = match cs_meta.target {
Some(Target::ConversationId(conversation_id))
if !conversation_id.is_empty() =>
{
(Some(conversation_id), None)
}
Some(Target::AgentRunId(agent_run_id)) if !agent_run_id.is_empty() => {
(None, Some(agent_run_id))
}
Some(Target::ConversationId(_))
| Some(Target::AgentRunId(_))
| None => (None, None),
};
SubagentType::ConversationSearch {
query,
conversation_id,
agent_run_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,
params.skill_path_origin,
),
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::RunAgents(orchestrate) => {
create_standard_action(convert_run_agents(orchestrate, params.skill_path_origin))
}
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(convert_read_skill(read_skill, params.skill_path_origin)?)
}
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)
}
api::message::tool_call::Tool::WaitForEvents(payload) => {
create_standard_action(AIAgentActionType::WaitForEvents {
tool_call_id: self.tool_call_id.clone(),
idle_timeout_seconds: payload.idle_timeout_seconds,
})
}
_ => 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) => {
if is_internal_command_completion_assessment(m) {
continue;
}
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;