use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use tokio_util::sync::CancellationToken;
use super::MAX_OUTPUT_BYTES;
use crate::tool::{AgentTool, AgentToolResult, ToolFuture, validated_schema_for};
pub struct ReadFileTool {
schema: Value,
}
impl ReadFileTool {
#[must_use]
pub fn new() -> Self {
Self {
schema: validated_schema_for::<Params>(),
}
}
}
impl Default for ReadFileTool {
fn default() -> Self {
Self::new()
}
}
#[derive(Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
struct Params {
path: String,
}
#[allow(clippy::unnecessary_literal_bound)]
impl AgentTool for ReadFileTool {
fn name(&self) -> &str {
"read_file"
}
fn label(&self) -> &str {
"Read File"
}
fn description(&self) -> &str {
"Read a file and return its contents."
}
fn parameters_schema(&self) -> &Value {
&self.schema
}
fn execute(
&self,
_tool_call_id: &str,
params: Value,
cancellation_token: CancellationToken,
_on_update: Option<Box<dyn Fn(AgentToolResult) + Send + Sync>>,
_state: std::sync::Arc<std::sync::RwLock<crate::SessionState>>,
_credential: Option<crate::credential::ResolvedCredential>,
) -> ToolFuture<'_> {
Box::pin(async move {
let parsed: Params = match serde_json::from_value(params) {
Ok(p) => p,
Err(e) => return AgentToolResult::error(format!("invalid parameters: {e}")),
};
if cancellation_token.is_cancelled() {
return AgentToolResult::error("cancelled");
}
match tokio::fs::read_to_string(&parsed.path).await {
Ok(mut content) => {
if content.len() > MAX_OUTPUT_BYTES {
content.truncate(MAX_OUTPUT_BYTES);
content.push_str("\n[truncated]");
}
AgentToolResult::text(content)
}
Err(e) => {
AgentToolResult::error(format!("failed to read file {}: {e}", parsed.path))
}
}
})
}
}