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

1092 lines
45 KiB
Rust

//! Conversions from application types to MAA API types.
use ai::agent::convert::ConvertToAPITypeError;
use anyhow::anyhow;
use base64::engine::general_purpose;
use base64::Engine as _;
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,
};
use crate::ai::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::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, context } => {
return Ok(api::request::Input {
context: Some(convert_context(context.as_ref())),
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::CommandCompletionAssessment {
prompt,
completed_command:
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: prompt,
referenced_attachments: Default::default(),
mode: Some(UserQueryMode::Normal.into()),
intended_agent: api::AgentType::Primary.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::OrchestrationConfigUpdate {
plan_id,
config,
status,
} => Ok(
api::request::input::user_inputs::user_input::Input::OrchestrationConfigUpdate(
api::OrchestrationConfigUpdate {
plan_id,
config: Some(config.to_proto()),
status: status.to_proto(),
},
),
),
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())
}
AIAgentActionResultType::RunAgents(orchestrate_result) => {
Some(orchestrate_result.try_into()?)
}
AIAgentActionResultType::WaitForEvents(wait_for_events_result) => {
Some(wait_for_events_result.try_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();
let mut git_context = None;
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) => {
let Ok(data) = general_purpose::STANDARD.decode(&image_context.data) else {
log::warn!(
"Skipping AI image context with invalid base64 data (mime_type={})",
image_context.mime_type
);
continue;
};
api_context.images.push(api::input_context::Image {
data,
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 } => {
let api_git_context =
git_context.get_or_insert_with(api::input_context::Git::default);
api_git_context.head = head;
api_git_context.branch = branch.unwrap_or_default();
}
AIAgentContext::Repository { name, owner } => {
let api_git_context =
git_context.get_or_insert_with(api::input_context::Git::default);
api_git_context.repository = Some(api::input_context::git::Repository {
name,
owner: owner.unwrap_or_default(),
});
}
AIAgentContext::PullRequest {
number,
state,
draft,
base_branch,
} => {
if number <= 0 {
continue;
}
let Some(state) = api_pull_request_state(&state, draft) else {
continue;
};
let pull_request = api::input_context::git::PullRequest {
number,
state: state as i32,
base_branch,
};
let api_git_context =
git_context.get_or_insert_with(api::input_context::Git::default);
api_git_context.pull_request = Some(pull_request);
}
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.git = git_context;
api_context
}
/// Maps a GitHub PR state plus draft flag to the proto `State` enum.
///
/// Returns `None` for unknown states so the caller can skip emitting a
/// `pull_request` sub-message rather than sending `STATE_UNSPECIFIED` to the
/// server.
fn api_pull_request_state(
state: &str,
draft: bool,
) -> Option<api::input_context::git::pull_request::State> {
use api::input_context::git::pull_request::State;
match state.to_ascii_uppercase().as_str() {
"OPEN" => {
if draft {
Some(State::OpenDraft)
} else {
Some(State::Open)
}
}
"CLOSED" => Some(State::Closed),
"MERGED" => Some(State::Merged),
_ => None,
}
}
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),
}
}
}
/// Tries 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 std::collections::BTreeMap;
use prost_types::value::Kind::*;
use serde_json::Value::*;
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;