use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
pub struct ReadSymbolTool;
#[derive(Deserialize)]
struct ReadSymbolArgs {
file_path: String,
symbol: String,
}
#[async_trait]
impl Tool for ReadSymbolTool {
fn definition(&self) -> ToolDef {
ToolDef {
name: "read_symbol",
description: "Read the complete source code of a specific function, class, or struct by name.\n\
More precise than read_file — returns exactly the symbol you need with line numbers.\n\
Use list_symbols first to discover available symbols, then read_symbol to get the code.\n\
Examples:\n\
- {\"file_path\": \"/path/to/main.rs\", \"symbol\": \"process_data\"}\n\
- {\"file_path\": \"/path/to/app.py\", \"symbol\": \"UserService\"}".to_string(),
parameters: json!({
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Absolute path to the source file" },
"symbol": { "type": "string", "description": "Name of the function, class, or struct to read" }
},
"required": ["file_path", "symbol"]
}),
}
}
fn approval(&self, _args: &str) -> ApprovalRequirement {
ApprovalRequirement::AutoApprove
}
fn approval_with_context(&self, args: &str, ctx: &ToolContext) -> ApprovalRequirement {
let parsed = match serde_json::from_str::<ReadSymbolArgs>(args) {
Ok(parsed) => parsed,
Err(_) => return self.approval(args),
};
let working_dir = match ctx.working_dir.try_read() {
Ok(wd) => wd.clone(),
Err(_) => return self.approval(args),
};
match super::approval_for_path(
&parsed.file_path,
&working_dir,
super::ExternalPathAction::Read,
) {
Ok(approval) => approval,
Err(_) => self.approval(args),
}
}
async fn execute(&self, args: &str, ctx: &ToolContext) -> Result<ToolResult> {
let parsed: ReadSymbolArgs = serde_json::from_str(args)?;
let working_dir = ctx.working_dir.read().await.clone();
let path = match super::inspect_path_access(&parsed.file_path, &working_dir) {
Ok(access) => access.path,
Err(err) => {
return Ok(ToolResult {
call_id: String::new(),
output: err.to_string(),
success: false,
});
}
};
if !path.exists() {
return Ok(ToolResult {
call_id: String::new(),
output: format!("File not found: {}", parsed.file_path),
success: false,
});
}
let mut searcher = ctx.semantic.lock().await;
match searcher.extract_symbol(&path, &parsed.symbol) {
Some(slice) => {
let mut out = format!(
"{} ({}, lines {}-{})\n\n",
slice.name, slice.kind, slice.start_line, slice.end_line
);
for (i, line) in slice.text.lines().enumerate() {
out.push_str(&format!("{:4}| {}\n", slice.start_line + i, line));
}
Ok(ToolResult {
call_id: String::new(),
output: out,
success: true,
})
}
None => {
let hint = match searcher.list_symbols(&path) {
Some(symbols) => {
let names: Vec<String> = symbols.iter().map(|s| s.name.clone()).collect();
format!(
"Symbol '{}' not found in {}.\nAvailable symbols: {}",
parsed.symbol,
parsed.file_path,
names.join(", ")
)
}
None => format!(
"Symbol '{}' not found in {}",
parsed.symbol, parsed.file_path
),
};
Ok(ToolResult {
call_id: String::new(),
output: hint,
success: false,
})
}
}
}
}