use std::path::Path;
use agent_client_protocol::schema::v1::{ToolCallLocation, ToolKind};
use rho_sdk::tool::{OperationKind, ToolFileChange, ToolMetadata, ToolOutput};
use rho_tools::{
apply_patch::{parse_patch, Hunk},
EditFormat,
};
use serde::Deserialize;
use serde_json::Value;
use crate::tools::{plan_exit, todo};
const READ_FILE_TOOL: &str = "read_file";
const MAX_GREP_LOCATIONS: usize = 20;
pub(super) fn kind(name: &str, arguments: Option<&Value>, metadata: &ToolMetadata) -> ToolKind {
match metadata.operation_kind() {
Some(OperationKind::Read) => ToolKind::Read,
Some(OperationKind::Search) => ToolKind::Search,
Some(OperationKind::Write) => match (EditFormat::from_tool_name(name), arguments) {
(Some(EditFormat::ApplyPatch), Some(arguments)) => patch_kind(arguments),
_ => ToolKind::Edit,
},
Some(OperationKind::Execute) => ToolKind::Execute,
Some(OperationKind::Network) => ToolKind::Fetch,
Some(OperationKind::Other(_)) | None => match name {
todo::TOOL_NAME => ToolKind::Think,
plan_exit::TOOL_NAME => ToolKind::SwitchMode,
_ => ToolKind::Other,
},
_ => ToolKind::Other,
}
}
fn patch_kind(arguments: &Value) -> ToolKind {
let Some(Ok(hunks)) = arguments
.get("input")
.and_then(Value::as_str)
.map(parse_patch)
else {
return ToolKind::Edit;
};
if hunks.iter().all(|hunk| matches!(hunk, Hunk::Delete { .. })) {
ToolKind::Delete
} else if hunks.iter().all(|hunk| {
matches!(
hunk,
Hunk::Update {
move_path: Some(_),
..
}
)
}) {
ToolKind::Move
} else {
ToolKind::Edit
}
}
pub(super) fn started_locations(
name: &str,
arguments: Option<&Value>,
metadata: &ToolMetadata,
cwd: &Path,
) -> Vec<ToolCallLocation> {
let line = arguments
.filter(|_| name == READ_FILE_TOOL)
.and_then(|arguments| arguments.get("offset")?.as_u64())
.and_then(row);
metadata
.affected_paths()
.iter()
.map(|path| ToolCallLocation::new(cwd.join(path)).line(line))
.collect()
}
pub(super) fn finished_locations(output: &ToolOutput, cwd: &Path) -> Option<Vec<ToolCallLocation>> {
let metadata = output.presentation();
let locations = if !metadata.file_changes().is_empty() {
metadata
.file_changes()
.iter()
.map(|change| ToolCallLocation::new(cwd.join(change.path())).line(changed_line(change)))
.collect()
} else if metadata.operation_kind() == Some(&OperationKind::Search) {
GrepHits::deserialize(output.structured_content()?)
.ok()?
.files
.into_iter()
.take(MAX_GREP_LOCATIONS)
.map(|file| {
let line = file.lines.first().and_then(|line| row(line.line));
ToolCallLocation::new(cwd.join(file.path)).line(line)
})
.collect()
} else {
return None;
};
Some(locations).filter(|locations: &Vec<_>| !locations.is_empty())
}
fn changed_line(change: &ToolFileChange) -> Option<u32> {
let mut old_lines = change.old_text()?.lines();
let mut rows = 0usize;
for (index, new_line) in change.new_text().lines().enumerate() {
if old_lines.next() != Some(new_line) {
return u32::try_from(index).ok();
}
rows = index + 1;
}
(rows > 0 && old_lines.next().is_some())
.then(|| u32::try_from(rows - 1).ok())
.flatten()
}
fn row(line: u64) -> Option<u32> {
u32::try_from(line.checked_sub(1)?).ok()
}
#[derive(Deserialize)]
struct GrepHits {
files: Vec<GrepFile>,
}
#[derive(Deserialize)]
struct GrepFile {
path: std::path::PathBuf,
#[serde(default)]
lines: Vec<GrepLine>,
}
#[derive(Deserialize)]
struct GrepLine {
line: u64,
}
#[cfg(test)]
#[path = "events_tools_tests.rs"]
mod tests;