Files
galaxy/app/src/ai/provider/convert_request.rs
T

1149 lines
52 KiB
Rust

#![allow(dead_code, unused_imports, unused_variables, deprecated)]
use warp_multi_agent_api as api;
use super::convert::{ConversationMessage, ContentPart, MessageContent, MessageRole, ToolDefinition};
/// Extract new input messages from the current request and convert them directly
/// to ConversationMessage format for the Bedrock message history.
/// This extracts UserQuery and ToolCallResult from request.input only.
pub fn extract_new_input_messages(request: &api::Request) -> Vec<ConversationMessage> {
let mut results = Vec::new();
let Some(input) = &request.input else {
return results;
};
let Some(input_type) = &input.r#type else {
return results;
};
match input_type {
api::request::input::Type::UserInputs(user_inputs) => {
let mut tool_results: Vec<ConversationMessage> = Vec::new();
for user_input in &user_inputs.inputs {
match &user_input.input {
Some(api::request::input::user_inputs::user_input::Input::ToolCallResult(
result,
)) => {
if !result.tool_call_id.is_empty() {
let content = extract_tool_result_content(result);
tool_results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: result.tool_call_id.clone(),
content,
is_error: false,
},
});
}
}
Some(api::request::input::user_inputs::user_input::Input::UserQuery(
query,
)) => {
if !query.query.is_empty() {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(query.query.clone()),
});
}
}
Some(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(
cli_query,
)) => {
if let Some(user_query) = &cli_query.user_query {
if !user_query.query.is_empty() {
// Include running command context as part of the user message
let query_text = if let Some(running_cmd) = &cli_query.running_command {
let mut context = format!(
"[Running command: {}]\n",
running_cmd.command
);
if let Some(snapshot) = &running_cmd.snapshot {
if !snapshot.output.is_empty() {
context.push_str(&format!(
"[Terminal output:\n{}\n]\n",
snapshot.output
));
}
}
context.push_str(&user_query.query);
context
} else {
user_query.query.clone()
};
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(query_text),
});
}
}
}
_ => {}
}
}
if !tool_results.is_empty() {
if tool_results.len() == 1 {
results.extend(tool_results);
} else {
let parts: Vec<ContentPart> = tool_results
.into_iter()
.map(|tr| match tr.content {
MessageContent::ToolResult { tool_use_id, content, is_error } => {
ContentPart::ToolResult { tool_use_id, content, is_error }
}
_ => unreachable!(),
})
.collect();
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::MultiPart(parts),
});
}
}
}
#[allow(deprecated)]
api::request::input::Type::UserQuery(query) => {
if !query.query.is_empty() {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(query.query.clone()),
});
}
}
api::request::input::Type::InitProjectRules(_) => {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Initialize this project. Analyze the codebase structure and files, \
generate an AGENTS.md file documenting project conventions and setup \
instructions, and offer to create a development environment configuration. \
Use the available tools to inspect the project before responding."
.to_string(),
),
});
}
api::request::input::Type::CreateEnvironment(env) => {
let repo_info = if env.repo_paths.is_empty() {
String::new()
} else {
format!(" Repositories: {}", env.repo_paths.join(", "))
};
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Create a development environment for this project. \
Set up necessary dependencies, configuration files, and tooling.{repo_info}"
)),
});
}
api::request::input::Type::CreateNewProject(project) => {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Create a new project: {}",
project.query,
)),
});
}
api::request::input::Type::CloneRepository(repo) => {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Clone the repository at {} and set it up for development.",
repo.url,
)),
});
}
api::request::input::Type::ResumeConversation(_) => {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Continue where we left off. Review the conversation history and proceed with the next steps."
.to_string(),
),
});
}
api::request::input::Type::CodeReview(_) => {
results.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Review the following code changes and provide detailed feedback on correctness, style, and potential issues."
.to_string(),
),
});
}
_ => {}
}
for msg in &results {
let desc = match &msg.content {
MessageContent::Text(t) => format!("Text({}chars)", t.len()),
MessageContent::ToolResult { tool_use_id, .. } => format!("ToolResult({})", tool_use_id),
MessageContent::MultiPart(parts) => format!("MultiPart({} parts)", parts.len()),
_ => "Other".to_string(),
};
log::info!("[bedrock] New input message: role={:?}, content={}", msg.role, desc);
}
results
}
/// For the Bedrock direct path: inject all input messages (user queries and tool call
/// results) into the task's messages so they persist in conversation history for future
/// requests. Without this, inputs are lost after the current request cycle because they
/// only exist in `request.input` and are never stored in `task_context.tasks[].messages`.
pub fn inject_input_messages_into_task(request: &mut api::Request) {
let input_messages: Vec<api::Message> = extract_input_messages(request);
if input_messages.is_empty() {
return;
}
log::info!(
"[bedrock] Injecting {} input messages into task history",
input_messages.len()
);
if let Some(task_context) = &mut request.task_context {
if let Some(task) = task_context.tasks.first_mut() {
task.messages.extend(input_messages);
} else {
// No task exists yet — create one to hold the messages
let task_id = uuid::Uuid::new_v4().to_string();
task_context.tasks.push(api::Task {
id: task_id,
messages: input_messages,
..Default::default()
});
}
} else {
let task_id = uuid::Uuid::new_v4().to_string();
request.task_context = Some(api::request::TaskContext {
tasks: vec![api::Task {
id: task_id,
messages: input_messages,
..Default::default()
}],
});
}
}
fn extract_input_messages(request: &api::Request) -> Vec<api::Message> {
let mut results = Vec::new();
let Some(input) = &request.input else {
return results;
};
let Some(input_type) = &input.r#type else {
return results;
};
let task_id = request
.task_context
.as_ref()
.and_then(|tc| tc.tasks.first())
.map(|t| t.id.clone())
.unwrap_or_default();
match input_type {
api::request::input::Type::UserInputs(user_inputs) => {
for user_input in &user_inputs.inputs {
match &user_input.input {
Some(api::request::input::user_inputs::user_input::Input::ToolCallResult(
result,
)) => {
if !result.tool_call_id.is_empty() {
let content = extract_tool_result_content(result);
results.push(api::Message {
id: uuid::Uuid::new_v4().to_string(),
task_id: task_id.clone(),
request_id: String::new(),
timestamp: None,
server_message_data: String::new(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: result.tool_call_id.clone(),
context: None,
result: Some(
api::message::tool_call_result::Result::Server(
api::message::tool_call_result::ServerResult {
serialized_result: content,
},
),
),
},
)),
});
}
}
Some(api::request::input::user_inputs::user_input::Input::UserQuery(
query,
)) => {
if !query.query.is_empty() {
results.push(api::Message {
id: uuid::Uuid::new_v4().to_string(),
task_id: task_id.clone(),
request_id: String::new(),
timestamp: None,
server_message_data: String::new(),
citations: vec![],
message: Some(api::message::Message::UserQuery(
api::message::UserQuery {
query: query.query.clone(),
..Default::default()
},
)),
});
}
}
Some(api::request::input::user_inputs::user_input::Input::CliAgentUserQuery(
cli_query,
)) => {
if let Some(user_query) = &cli_query.user_query {
if !user_query.query.is_empty() {
let query_text = if let Some(running_cmd) = &cli_query.running_command {
let mut context = format!(
"[Running command: {}]\n",
running_cmd.command
);
if let Some(snapshot) = &running_cmd.snapshot {
if !snapshot.output.is_empty() {
context.push_str(&format!(
"[Terminal output:\n{}\n]\n",
snapshot.output
));
}
}
context.push_str(&user_query.query);
context
} else {
user_query.query.clone()
};
results.push(api::Message {
id: uuid::Uuid::new_v4().to_string(),
task_id: task_id.clone(),
request_id: String::new(),
timestamp: None,
server_message_data: String::new(),
citations: vec![],
message: Some(api::message::Message::UserQuery(
api::message::UserQuery {
query: query_text,
..Default::default()
},
)),
});
}
}
}
_ => {}
}
}
}
api::request::input::Type::UserQuery(query) => {
if !query.query.is_empty() {
results.push(api::Message {
id: uuid::Uuid::new_v4().to_string(),
task_id: task_id.clone(),
request_id: String::new(),
timestamp: None,
server_message_data: String::new(),
citations: vec![],
message: Some(api::message::Message::UserQuery(
api::message::UserQuery {
query: query.query.clone(),
..Default::default()
},
)),
});
}
}
api::request::input::Type::ToolCallResult(result) => {
if !result.tool_call_id.is_empty() {
let content = extract_tool_result_content(result);
results.push(api::Message {
id: uuid::Uuid::new_v4().to_string(),
task_id: task_id.clone(),
request_id: String::new(),
timestamp: None,
server_message_data: String::new(),
citations: vec![],
message: Some(api::message::Message::ToolCallResult(
api::message::ToolCallResult {
tool_call_id: result.tool_call_id.clone(),
context: None,
result: Some(
api::message::tool_call_result::Result::Server(
api::message::tool_call_result::ServerResult {
serialized_result: content,
},
),
),
},
)),
});
}
}
_ => {}
}
results
}
pub fn extract_messages_from_request(request: &api::Request) -> Vec<ConversationMessage> {
let mut messages = Vec::new();
if let Some(task_context) = &request.task_context {
log::info!(
"[bedrock-debug] extract_messages: {} tasks in task_context",
task_context.tasks.len()
);
for task in &task_context.tasks {
log::info!(
"[bedrock-debug] extract_messages: task '{}' has {} messages",
task.id,
task.messages.len()
);
for msg in &task.messages {
let msg_type = msg.message.as_ref().map(|m| match m {
api::message::Message::UserQuery(_) => "UserQuery",
api::message::Message::AgentOutput(_) => "AgentOutput",
api::message::Message::ToolCall(_) => "ToolCall",
api::message::Message::ToolCallResult(_) => "ToolCallResult",
api::message::Message::AgentReasoning(_) => "AgentReasoning",
_ => "Other",
}).unwrap_or("None");
log::info!(
"[bedrock-debug] extract_messages: msg id='{}' type={}",
msg.id,
msg_type
);
if let Some(converted) = convert_proto_message(msg) {
messages.push(converted);
}
}
}
} else {
log::warn!("[bedrock-debug] extract_messages: NO task_context in request!");
}
if let Some(input) = &request.input {
if let Some(input_type) = &input.r#type {
#[allow(deprecated)]
match input_type {
// UserInputs (UserQuery + ToolCallResult) are already injected
// into task messages by inject_input_messages_into_task().
api::request::input::Type::UserInputs(_) => {}
api::request::input::Type::UserQuery(_) => {}
api::request::input::Type::ToolCallResult(_) => {}
api::request::input::Type::InitProjectRules(_) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Initialize this project. Analyze the codebase structure and files, \
generate an AGENTS.md file documenting project conventions and setup \
instructions, and offer to create a development environment configuration. \
Use the available tools to inspect the project before responding."
.to_string(),
),
});
}
api::request::input::Type::CreateEnvironment(env) => {
let repo_info = if env.repo_paths.is_empty() {
String::new()
} else {
format!(" Repositories: {}", env.repo_paths.join(", "))
};
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Create a development environment for this project. \
Set up necessary dependencies, configuration files, and tooling.{}",
repo_info
)),
});
}
api::request::input::Type::CreateNewProject(project) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Create a new project: {}",
project.query
)),
});
}
api::request::input::Type::CloneRepository(repo) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Clone the repository at {} and set it up for development.",
repo.url
)),
});
}
api::request::input::Type::AutoCodeDiffQuery(diff) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(format!(
"Apply code changes: {}",
diff.query
)),
});
}
api::request::input::Type::ResumeConversation(_) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Continue where we left off. Review the conversation history and proceed with the next steps."
.to_string(),
),
});
}
api::request::input::Type::QueryWithCannedResponse(canned) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(canned.query.clone()),
});
}
api::request::input::Type::CodeReview(_) => {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Review the following code changes and provide detailed feedback on correctness, style, and potential issues."
.to_string(),
),
});
}
_ => {}
}
}
}
ensure_starts_with_user_message(&mut messages);
ensure_tool_results_paired(&mut messages);
log::info!(
"[bedrock-debug] extract_messages: TOTAL {} messages to send to Bedrock (User={}, Assistant={}, ToolResult={}, ToolUse={})",
messages.len(),
messages.iter().filter(|m| m.role == MessageRole::User && matches!(&m.content, MessageContent::Text(_))).count(),
messages.iter().filter(|m| m.role == MessageRole::Assistant && matches!(&m.content, MessageContent::Text(_))).count(),
messages.iter().filter(|m| matches!(&m.content, MessageContent::ToolResult { .. })).count(),
messages.iter().filter(|m| matches!(&m.content, MessageContent::ToolUse { .. })).count(),
);
messages
}
fn ensure_starts_with_user_message(messages: &mut Vec<ConversationMessage>) {
if messages.is_empty() {
messages.push(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text("Please proceed with the requested task.".to_string()),
});
return;
}
if messages[0].role != MessageRole::User {
messages.insert(
0,
ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(
"Please proceed with the requested task.".to_string(),
),
},
);
}
}
fn ensure_tool_results_paired(messages: &mut Vec<ConversationMessage>) {
let mut tool_use_ids: Vec<String> = Vec::new();
let mut tool_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
for msg in messages.iter() {
match &msg.content {
MessageContent::ToolUse { tool_use_id, .. } => {
tool_use_ids.push(tool_use_id.clone());
}
MessageContent::ToolResult { tool_use_id, .. } => {
tool_result_ids.insert(tool_use_id.clone());
}
MessageContent::MultiPart(parts) => {
for part in parts {
match part {
super::convert::ContentPart::ToolUse { tool_use_id, .. } => {
tool_use_ids.push(tool_use_id.clone());
}
super::convert::ContentPart::ToolResult { tool_use_id, .. } => {
tool_result_ids.insert(tool_use_id.clone());
}
_ => {}
}
}
}
_ => {}
}
}
let orphaned: Vec<String> = tool_use_ids
.into_iter()
.filter(|id| !tool_result_ids.contains(id))
.collect();
if orphaned.is_empty() {
return;
}
log::debug!(
"[bedrock] Synthesizing {} missing toolResult messages for orphaned tool calls",
orphaned.len()
);
for orphaned_id in &orphaned {
let insert_idx = messages
.iter()
.rposition(|m| match &m.content {
MessageContent::ToolUse { tool_use_id, .. } => tool_use_id == orphaned_id,
MessageContent::MultiPart(parts) => parts.iter().any(|p| matches!(
p,
super::convert::ContentPart::ToolUse { tool_use_id, .. } if tool_use_id == orphaned_id
)),
_ => false,
})
.map(|i| i + 1)
.unwrap_or(messages.len());
messages.insert(
insert_idx,
ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: orphaned_id.clone(),
content: "Tool executed successfully.".to_string(),
is_error: false,
},
},
);
}
}
pub fn extract_system_prompt(request: &api::Request) -> Option<String> {
let mut prompt = String::with_capacity(2048);
prompt.push_str("You are Galaxy, an AI coding assistant embedded in a terminal application. You help users with software engineering tasks including writing code, debugging, explaining concepts, and navigating codebases.\n\n");
if let Some(input) = &request.input {
if let Some(context) = &input.context {
prompt.push_str("## Environment\n");
if let Some(dir) = &context.directory {
if !dir.pwd.is_empty() {
prompt.push_str(&format!("- Working directory: {}\n", dir.pwd));
}
if !dir.home.is_empty() {
prompt.push_str(&format!("- Home directory: {}\n", dir.home));
}
}
if let Some(os) = &context.operating_system {
if !os.platform.is_empty() {
prompt.push_str(&format!("- OS: {}\n", os.platform));
}
}
if let Some(shell) = &context.shell {
if !shell.name.is_empty() {
prompt.push_str(&format!("- Shell: {}", shell.name));
if !shell.version.is_empty() {
prompt.push_str(&format!(" {}", shell.version));
}
prompt.push('\n');
}
}
if let Some(git) = &context.git {
if !git.branch.is_empty() {
prompt.push_str(&format!("- Git branch: {}\n", git.branch));
}
}
prompt.push('\n');
if !context.project_rules.is_empty() {
prompt.push_str("## Project Rules\n");
for rules in &context.project_rules {
if !rules.root_path.is_empty() {
prompt.push_str(&format!("### Rules from {}\n", rules.root_path));
}
for file in &rules.active_rule_files {
if !file.content.is_empty() {
prompt.push_str(&file.content);
prompt.push('\n');
}
}
}
prompt.push('\n');
}
}
}
prompt.push_str("## Tools\nYou have access to the following tools. Use them proactively to explore codebases and complete tasks:\n");
prompt.push_str("- `run_shell_command`: Execute shell commands. Use absolute paths based on the working directory.\n");
prompt.push_str("- `read_files`: Read file contents. Pass all files you need in a single call.\n");
prompt.push_str("- `apply_file_diffs`: Apply search/replace edits to files.\n");
prompt.push_str("- `grep`: Search for patterns in files. Pass all patterns in one call.\n");
prompt.push_str("- `file_glob`: Find files matching glob patterns. Pass all patterns in one call.\n");
prompt.push_str("- `get_tool_documentation`: Get detailed documentation for any tool or system capabilities.\n");
prompt.push_str("- `suggest_next_prompt`: After completing a task, suggest a follow-up action the user might want.\n\n");
prompt.push_str("## Guidelines\n");
prompt.push_str("- ALWAYS use tools to explore the codebase before answering questions about code.\n");
prompt.push_str("- Use absolute paths based on the working directory shown above.\n");
prompt.push_str("- When asked about a project, start by listing files with `file_glob` or `run_shell_command`.\n");
prompt.push_str("- Read relevant files before making claims about code structure or behavior.\n");
prompt.push_str("- Be concise and direct in responses.\n");
prompt.push_str("- IMPORTANT: After EVERY response, you MUST call `suggest_next_prompt` to suggest a relevant follow-up action or question the user might want to take next.\n");
Some(prompt)
}
pub fn extract_tools(_request: &api::Request) -> Vec<ToolDefinition> {
// Always provide the full set of tools to the model. Previously this function
// tried to reconstruct the tool list from conversation history, which meant
// tools that hadn't been used yet (like apply_file_diffs) would be missing on
// subsequent turns — causing the model to believe it lacked access to them.
default_tool_definitions()
}
fn tool_definition_for_name(name: &str) -> ToolDefinition {
match name {
"run_shell_command" => ToolDefinition {
name: "run_shell_command".to_string(),
description: "Execute a shell command and return its output.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"command": { "type": "string", "description": "The shell command to execute" }
},
"required": ["command"]
}),
},
"read_files" => ToolDefinition {
name: "read_files".to_string(),
description: "Read the contents of one or more files. ALWAYS pass all files you need in a single call rather than making multiple separate calls.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"files": { "type": "array", "items": { "type": "string" }, "description": "File paths to read. Include ALL files you need in one call for efficiency." }
},
"required": ["files"]
}),
},
"apply_file_diffs" => ToolDefinition {
name: "apply_file_diffs".to_string(),
description: "Apply search/replace diffs to files.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"summary": { "type": "string", "description": "A brief summary of what these edits accomplish" },
"diffs": { "type": "array", "items": { "type": "object", "properties": { "file_path": { "type": "string" }, "search": { "type": "string" }, "replace": { "type": "string" } }, "required": ["file_path", "search", "replace"] } }
},
"required": ["summary", "diffs"]
}),
},
"grep" => ToolDefinition {
name: "grep".to_string(),
description: "Search for patterns in files. Returns up to 1,000 matching line locations and reports when additional matches were omitted. Pass all search patterns in one call.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"queries": { "type": "array", "items": { "type": "string" }, "description": "Search patterns. Include ALL patterns you need in one call." },
"path": { "type": "string", "description": "Directory to search in" }
},
"required": ["queries"]
}),
},
"file_glob" => ToolDefinition {
name: "file_glob".to_string(),
description: "Find files matching glob patterns. Pass all patterns in one call.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"patterns": { "type": "array", "items": { "type": "string" }, "description": "Glob patterns to match" }
},
"required": ["patterns"]
}),
},
"suggest_next_prompt" => ToolDefinition {
name: "suggest_next_prompt".to_string(),
description: "After completing a task, suggest a relevant follow-up prompt the user might want to try next. Use this to suggest a natural next step based on what was just accomplished.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"prompt": { "type": "string", "description": "The suggested prompt text that will be sent to the agent if the user accepts" },
"label": { "type": "string", "description": "Short display label for the suggestion chip (keep under 40 chars)" }
},
"required": ["prompt", "label"]
}),
},
"recall_tool_history" => ToolDefinition {
name: "recall_tool_history".to_string(),
description: "Retrieve the full output of a past tool execution from conversation history. Use when you need details from a tool call that is no longer in your current context window.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"search_query": {
"type": "string",
"description": "Search text to match against tool names, inputs, or outputs"
},
"tool_name": {
"type": "string",
"description": "Filter by tool name (run_shell_command, read_files, grep, file_glob, apply_file_diffs)"
},
"tool_use_id": {
"type": "string",
"description": "Exact tool_use_id to look up (most precise)"
},
"offset_from_end": {
"type": "integer",
"description": "How many matching tool calls back from most recent (0 = most recent match)"
}
}
}),
},
_ => ToolDefinition {
name: name.to_string(),
description: format!("Tool: {}", name),
input_schema: serde_json::json!({
"type": "object",
"properties": {}
}),
},
}
}
fn default_tool_definitions() -> Vec<ToolDefinition> {
vec![
tool_definition_for_name("run_shell_command"),
tool_definition_for_name("read_files"),
tool_definition_for_name("apply_file_diffs"),
tool_definition_for_name("grep"),
tool_definition_for_name("file_glob"),
tool_definition_for_name("suggest_next_prompt"),
tool_definition_for_name("recall_tool_history"),
]
}
fn convert_proto_message(msg: &api::Message) -> Option<ConversationMessage> {
let message_content = msg.message.as_ref()?;
match message_content {
api::message::Message::UserQuery(query) => Some(ConversationMessage {
role: MessageRole::User,
content: MessageContent::Text(query.query.clone()),
}),
api::message::Message::AgentOutput(output) => Some(ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::Text(output.text.clone()),
}),
api::message::Message::ToolCall(tool_call) => {
let (name, input) = extract_tool_call_info(tool_call);
Some(ConversationMessage {
role: MessageRole::Assistant,
content: MessageContent::ToolUse {
tool_use_id: tool_call.tool_call_id.clone(),
name,
input,
},
})
}
api::message::Message::ToolCallResult(result) => {
let content = format_tool_call_result(result);
Some(ConversationMessage {
role: MessageRole::User,
content: MessageContent::ToolResult {
tool_use_id: result.tool_call_id.clone(),
content,
is_error: false,
},
})
}
api::message::Message::AgentReasoning(_) => None,
_ => None,
}
}
fn extract_tool_call_info(tool_call: &api::message::ToolCall) -> (String, serde_json::Value) {
if let Some(tool) = &tool_call.tool {
match tool {
api::message::tool_call::Tool::RunShellCommand(cmd) => (
"run_shell_command".to_string(),
serde_json::json!({ "command": cmd.command }),
),
api::message::tool_call::Tool::ReadFiles(read) => (
"read_files".to_string(),
serde_json::json!({ "files": read.files.iter().map(|f| &f.name).collect::<Vec<_>>() }),
),
api::message::tool_call::Tool::ApplyFileDiffs(diffs) => (
"apply_file_diffs".to_string(),
serde_json::json!({ "diffs": diffs.diffs.iter().map(|d| {
serde_json::json!({
"file_path": d.file_path,
"search": d.search,
"replace": d.replace
})
}).collect::<Vec<_>>() }),
),
api::message::tool_call::Tool::Grep(grep) => (
"grep".to_string(),
serde_json::json!({ "queries": grep.queries, "path": grep.path }),
),
#[allow(deprecated)]
api::message::tool_call::Tool::FileGlob(glob) => (
"file_glob".to_string(),
serde_json::json!({ "patterns": glob.patterns }),
),
api::message::tool_call::Tool::SuggestPrompt(sp) => {
let (prompt, label) = match &sp.display_mode {
Some(api::message::tool_call::suggest_prompt::DisplayMode::PromptChip(chip)) => {
(chip.prompt.clone(), chip.label.clone())
}
_ => (String::new(), String::new()),
};
(
"suggest_next_prompt".to_string(),
serde_json::json!({ "prompt": prompt, "label": label }),
)
}
_ => ("unknown_tool".to_string(), serde_json::json!({})),
}
} else {
("unknown_tool".to_string(), serde_json::json!({}))
}
}
fn extract_tool_result_content(result: &api::request::input::ToolCallResult) -> String {
if let Some(result_type) = &result.result {
match result_type {
api::request::input::tool_call_result::Result::RunShellCommand(cmd_result) => {
match &cmd_result.result {
Some(api::run_shell_command_result::Result::CommandFinished(finished)) => {
if finished.output.is_empty() {
format!("Exit code: {}\n(no output)", finished.exit_code)
} else {
format!("Exit code: {}\n{}", finished.exit_code, finished.output)
}
}
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(
snapshot,
)) => snapshot.output.clone(),
_ => "Command completed.".to_string(),
}
}
api::request::input::tool_call_result::Result::ReadFiles(read_result) => {
match &read_result.result {
Some(api::read_files_result::Result::TextFilesSuccess(success)) => success
.files
.iter()
.map(|f| format!("{}:\n{}", f.file_path, f.content))
.collect::<Vec<_>>()
.join("\n\n"),
Some(api::read_files_result::Result::AnyFilesSuccess(success)) => success
.files
.iter()
.filter_map(|f| match &f.content {
Some(api::any_file_content::Content::TextContent(t)) => {
Some(format!("{}:\n{}", t.file_path, t.content))
}
_ => None,
})
.collect::<Vec<_>>()
.join("\n\n"),
_ => "Failed to read files.".to_string(),
}
}
api::request::input::tool_call_result::Result::Grep(grep_result) => {
match &grep_result.result {
Some(api::grep_result::Result::Success(success)) => {
if success.matched_files.is_empty() {
"No matches found.".to_string()
} else {
success
.matched_files
.iter()
.map(|f| {
let lines: String = f
.matched_lines
.iter()
.map(|l| format!(" line {}", l.line_number))
.collect::<Vec<_>>()
.join(", ");
format!("{} (matches at: {})", f.file_path, lines)
})
.collect::<Vec<_>>()
.join("\n")
}
}
Some(api::grep_result::Result::Error(error)) => {
format!("Grep error: {}", error.message)
}
None => "Grep completed (no result).".to_string(),
}
}
api::request::input::tool_call_result::Result::FileGlobV2(glob_result) => {
match &glob_result.result {
Some(api::file_glob_v2_result::Result::Success(success)) => {
if success.matched_files.is_empty() {
"No files matched.".to_string()
} else {
success
.matched_files
.iter()
.map(|f| f.file_path.as_str())
.collect::<Vec<_>>()
.join("\n")
}
}
Some(api::file_glob_v2_result::Result::Error(error)) => {
format!("File glob error: {}", error.message)
}
None => "File glob completed (no result).".to_string(),
}
}
api::request::input::tool_call_result::Result::ApplyFileDiffs(diff_result) => {
match &diff_result.result {
Some(api::apply_file_diffs_result::Result::Success(success)) => {
let mut parts = Vec::new();
for f in &success.updated_files_v2 {
if let Some(file) = &f.file {
parts.push(format!("Updated: {}", file.file_path));
}
}
for f in &success.deleted_files {
parts.push(format!("Deleted: {}", f.file_path));
}
if parts.is_empty() {
"Diffs applied successfully.".to_string()
} else {
parts.join("\n")
}
}
Some(api::apply_file_diffs_result::Result::Error(error)) => {
format!("Apply diffs error: {}", error.message)
}
None => "Apply diffs completed.".to_string(),
}
}
_ => "Tool completed successfully.".to_string(),
}
} else {
"Tool completed.".to_string()
}
}
fn format_tool_call_result(result: &api::message::ToolCallResult) -> String {
if let Some(result_type) = &result.result {
match result_type {
api::message::tool_call_result::Result::RunShellCommand(cmd_result) => {
match &cmd_result.result {
Some(api::run_shell_command_result::Result::CommandFinished(finished)) => {
if finished.output.is_empty() {
format!("Exit code: {}\n(no output)", finished.exit_code)
} else {
format!(
"Exit code: {}\n{}",
finished.exit_code, finished.output
)
}
}
Some(api::run_shell_command_result::Result::LongRunningCommandSnapshot(
snapshot,
)) => {
format!("Output (running): {}", snapshot.output)
}
_ => "Command completed.".to_string(),
}
}
api::message::tool_call_result::Result::ReadFiles(read_result) => {
match &read_result.result {
Some(api::read_files_result::Result::TextFilesSuccess(success)) => success
.files
.iter()
.map(|f| format!("{}:\n{}", f.file_path, f.content))
.collect::<Vec<_>>()
.join("\n\n"),
_ => "Read files completed.".to_string(),
}
}
api::message::tool_call_result::Result::Grep(grep_result) => {
match &grep_result.result {
Some(api::grep_result::Result::Success(success)) => {
if success.matched_files.is_empty() {
"No matches found.".to_string()
} else {
success
.matched_files
.iter()
.map(|f| {
let lines: String = f
.matched_lines
.iter()
.map(|l| format!(" line {}", l.line_number))
.collect::<Vec<_>>()
.join(", ");
format!("{} (matches at: {})", f.file_path, lines)
})
.collect::<Vec<_>>()
.join("\n")
}
}
Some(api::grep_result::Result::Error(error)) => {
format!("Grep error: {}", error.message)
}
None => "Grep completed (no result).".to_string(),
}
}
api::message::tool_call_result::Result::FileGlobV2(glob_result) => {
match &glob_result.result {
Some(api::file_glob_v2_result::Result::Success(success)) => {
if success.matched_files.is_empty() {
"No files matched.".to_string()
} else {
success
.matched_files
.iter()
.map(|f| f.file_path.as_str())
.collect::<Vec<_>>()
.join("\n")
}
}
Some(api::file_glob_v2_result::Result::Error(error)) => {
format!("File glob error: {}", error.message)
}
None => "File glob completed (no result).".to_string(),
}
}
api::message::tool_call_result::Result::ApplyFileDiffs(diff_result) => {
match &diff_result.result {
Some(api::apply_file_diffs_result::Result::Success(success)) => {
let mut parts = Vec::new();
for f in &success.updated_files_v2 {
if let Some(file) = &f.file {
parts.push(format!("Updated: {}", file.file_path));
}
}
for f in &success.deleted_files {
parts.push(format!("Deleted: {}", f.file_path));
}
if parts.is_empty() {
"Diffs applied successfully.".to_string()
} else {
parts.join("\n")
}
}
Some(api::apply_file_diffs_result::Result::Error(error)) => {
format!("Apply diffs error: {}", error.message)
}
None => "Apply diffs completed.".to_string(),
}
}
api::message::tool_call_result::Result::Server(server_result) => {
server_result.serialized_result.clone()
}
_ => "Tool completed successfully.".to_string(),
}
} else {
"Tool completed.".to_string()
}
}