use std::sync::Arc;
use async_trait::async_trait;
use foundation_nativeapis::shared::vfs::AsyncVfsFileSystem;
use crate::agentic::tool_impl::{ToolArgs, ToolCallResult, ToolError, ToolImpl};
use crate::types::base_types::Args;
use crate::types::{Tool, ToolArguments, ToolDefinition};
fn exec_err(tool: &str, reason: impl Into<String>) -> ToolError {
ToolError::Execution {
tool: tool.into(),
reason: reason.into(),
}
}
pub struct ReadTool<F> {
fs: Arc<F>,
}
impl<F: AsyncVfsFileSystem + 'static> ReadTool<F> {
#[must_use]
pub fn new(fs: Arc<F>) -> Self {
Self { fs }
}
}
#[async_trait]
impl<F: AsyncVfsFileSystem + 'static> ToolImpl for ReadTool<F> {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "read".into(),
description: "Read a text file. Args: path (required), optional offset (1-indexed \
start line) and limit (max lines)."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("path", foundation_jsonschema::scheme::string().min_len(1))
.optional("offset", foundation_jsonschema::scheme::integer())
.optional("limit", foundation_jsonschema::scheme::integer())
.build(),
),
category: "read".into(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let args = ToolArgs::new("read", &arguments);
let path = args.str("path")?.to_string();
let offset = args.opt_usize("offset")?;
let limit = args.opt_usize("limit")?;
let bytes = self
.fs
.read_file_async(path.clone())
.await
.map_err(|e| exec_err("read", format!("cannot read '{path}': {e}")))?;
let text = String::from_utf8(bytes)
.map_err(|_| exec_err("read", format!("'{path}' is not valid UTF-8 text")))?;
if offset.is_none() && limit.is_none() {
return Ok(ToolCallResult::text(text));
}
let start = offset.unwrap_or(1).saturating_sub(1);
let selected: String = text
.lines()
.skip(start)
.take(limit.unwrap_or(usize::MAX))
.collect::<Vec<_>>()
.join("\n");
Ok(ToolCallResult::text(selected))
}
}
pub struct WriteTool<F> {
fs: Arc<F>,
}
impl<F: AsyncVfsFileSystem + 'static> WriteTool<F> {
#[must_use]
pub fn new(fs: Arc<F>) -> Self {
Self { fs }
}
}
#[async_trait]
impl<F: AsyncVfsFileSystem + 'static> ToolImpl for WriteTool<F> {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "write".into(),
description: "Create or overwrite a text file. Args: path (required), content \
(required). Returns the number of bytes written."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("path", foundation_jsonschema::scheme::string().min_len(1))
.required("content", foundation_jsonschema::scheme::string())
.build(),
),
category: "write".into(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let args = ToolArgs::new("write", &arguments);
let path = args.str("path")?.to_string();
let content = args.str("content")?.to_string();
let n = content.len();
self.fs
.write_file_async(path.clone(), content.into_bytes())
.await
.map_err(|e| exec_err("write", format!("cannot write '{path}': {e}")))?;
Ok(ToolCallResult::text(format!("wrote {n} bytes to {path}")))
}
}
#[derive(serde::Deserialize)]
struct EditArgs {
path: String,
old_string: String,
new_string: String,
#[serde(default, deserialize_with = "bool_or_text")]
replace_all: bool,
}
fn bool_or_text<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
use serde::de::{Deserialize, Error};
match serde_json::Value::deserialize(deserializer)? {
serde_json::Value::Bool(b) => Ok(b),
serde_json::Value::String(s) if s == "true" => Ok(true),
serde_json::Value::String(s) if s == "false" => Ok(false),
other => Err(D::Error::custom(format!("expected a boolean, got {other}"))),
}
}
pub struct EditTool<F> {
fs: Arc<F>,
}
impl<F: AsyncVfsFileSystem + 'static> EditTool<F> {
#[must_use]
pub fn new(fs: Arc<F>) -> Self {
Self { fs }
}
}
#[async_trait]
impl<F: AsyncVfsFileSystem + 'static> ToolImpl for EditTool<F> {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "edit".into(),
description: "Replace an exact string in a file. Args: path, old_string, new_string, \
optional replace_all (default false). old_string must be unique unless \
replace_all is set."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("path", foundation_jsonschema::scheme::string().min_len(1))
.required("old_string", foundation_jsonschema::scheme::string())
.required("new_string", foundation_jsonschema::scheme::string())
.optional("replace_all", foundation_jsonschema::scheme::boolean())
.build(),
),
category: "edit".into(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let EditArgs {
path,
old_string: old,
new_string: new,
replace_all,
} = ToolArgs::new("edit", &arguments).parse()?;
let bytes = self
.fs
.read_file_async(path.clone())
.await
.map_err(|e| exec_err("edit", format!("cannot read '{path}': {e}")))?;
let text = String::from_utf8(bytes)
.map_err(|_| exec_err("edit", format!("'{path}' is not valid UTF-8 text")))?;
let count = text.matches(&old).count();
if count == 0 {
return Err(exec_err("edit", format!("old_string not found in '{path}'")));
}
if count > 1 && !replace_all {
return Err(exec_err(
"edit",
format!("old_string is not unique in '{path}' ({count} matches); set replace_all"),
));
}
let updated = if replace_all {
text.replace(&old, &new)
} else {
text.replacen(&old, &new, 1)
};
let replaced = if replace_all { count } else { 1 };
self.fs
.write_file_async(path.clone(), updated.into_bytes())
.await
.map_err(|e| exec_err("edit", format!("cannot write '{path}': {e}")))?;
Ok(ToolCallResult::text(format!(
"replaced {replaced} occurrence(s) in {path}"
)))
}
}
pub struct BashTool {
timeout: std::time::Duration,
}
impl Default for BashTool {
fn default() -> Self {
Self {
timeout: std::time::Duration::from_secs(120),
}
}
}
impl BashTool {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_timeout(timeout: std::time::Duration) -> Self {
Self { timeout }
}
}
#[async_trait]
impl ToolImpl for BashTool {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "bash".into(),
description: "Run a shell command. Args: command (required), optional cwd. Returns \
stdout, stderr and the exit code."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("command", foundation_jsonschema::scheme::string().min_len(1))
.optional("cwd", foundation_jsonschema::scheme::string())
.build(),
),
category: "shell".into(),
returns: None,
})
}
#[cfg(not(target_family = "wasm"))]
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let args = ToolArgs::new("bash", &arguments);
let command = args.str("command")?.to_string();
let cwd = args.opt_str("cwd")?.map(str::to_owned);
let timeout = self.timeout;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c").arg(&command);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
let _ = tx.send(cmd.output());
});
let output = match rx.recv_timeout(timeout) {
Ok(Ok(o)) => o,
Ok(Err(e)) => return Err(exec_err("bash", format!("spawn failed: {e}"))),
Err(_) => {
return Err(exec_err(
"bash",
format!("command timed out after {}s", timeout.as_secs()),
))
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let code = output.status.code().unwrap_or(-1);
Ok(ToolCallResult::text(format!(
"exit_code: {code}\nstdout:\n{stdout}\nstderr:\n{stderr}"
)))
}
#[cfg(target_family = "wasm")]
async fn execute(&self, _arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
Err(exec_err(
"bash",
"shell execution is not supported on this target",
))
}
}
pub fn register_file_tools<F: AsyncVfsFileSystem + 'static>(
manager: &crate::agentic::tool_impl::ToolCallManager,
fs: Arc<F>,
) {
manager.register(Arc::new(ReadTool::new(fs.clone())));
manager.register(Arc::new(WriteTool::new(fs.clone())));
manager.register(Arc::new(EditTool::new(fs)));
}
pub fn register_shell_tool(manager: &crate::agentic::tool_impl::ToolCallManager) {
manager.register(Arc::new(BashTool::new()));
}