use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use openpaw_core::{
Error, Result, ToolCallOutcome, ToolExecutor, ToolHost, ToolHostValidator, ToolSpec,
};
use crate::exec::{bash, clipboard, sleep};
use crate::fs::{edit, glob, grep, notebook_edit, read, write};
use crate::meta::{clarify, plan_mode, session_search, structured_output, think, todo};
use crate::web::{web_extract, web_fetch, web_search};
pub struct ToolRegistry {
workspace: PathBuf,
specs: Vec<ToolSpec>,
}
impl ToolRegistry {
pub fn new(workspace: impl Into<PathBuf>) -> Self {
let mut specs = vec![
read::spec(),
write::spec(),
edit::spec(),
grep::spec(),
glob::spec(),
notebook_edit::spec(),
think::spec(),
];
specs.extend(todo::specs());
specs.push(clarify::spec());
specs.extend(plan_mode::specs());
specs.push(structured_output::spec());
specs.push(session_search::spec());
specs.push(bash::spec());
specs.push(clipboard::spec());
specs.push(sleep::spec());
specs.push(web_fetch::spec());
specs.push(web_search::spec());
specs.push(web_extract::spec());
Self {
workspace: workspace.into(),
specs,
}
}
pub fn tool_names(&self) -> Vec<String> {
self.specs.iter().map(|s| s.name.clone()).collect()
}
}
#[async_trait]
impl ToolExecutor for ToolRegistry {
async fn list_tools(&self) -> Result<Vec<ToolSpec>> {
Ok(self.specs.clone())
}
async fn call(&self, name: &str, args: serde_json::Value) -> Result<ToolCallOutcome> {
let ws = self.workspace.clone();
match name {
"read_file" => read::run(&ws, args).await,
"write_file" => write::run(&ws, args).await,
"edit_file" => edit::run(&ws, args).await,
"notebook_edit" => notebook_edit::run(&ws, args).await,
"grep" => grep::run(&ws, args).await,
"glob" => glob::run(&ws, args).await,
"bash" => bash::run(&ws, args).await,
"clipboard" => clipboard::run(&ws, args).await,
"sleep" => sleep::run(&ws, args).await,
"web_fetch" => web_fetch::run(&ws, args).await,
"web_search" => web_search::run(&ws, args).await,
"web_extract" => web_extract::run(&ws, args).await,
"think" => think::run(&ws, args).await,
"todo_add" | "todo_update" | "todo_list" => todo::run(&ws, name, args).await,
"clarify" => clarify::run(&ws, args).await,
"enter_plan_mode" | "exit_plan_mode" => plan_mode::run(&ws, name).await,
"structured_output" => structured_output::run(&ws, args).await,
"session_search" => session_search::run(&ws, args).await,
other => Err(Error::ToolHost(format!("unknown tool: {other}"))),
}
}
}
#[async_trait]
impl ToolHostValidator for ToolRegistry {}
pub fn arc_registry(workspace: impl Into<PathBuf>) -> Arc<dyn ToolHost> {
Arc::new(ToolRegistry::new(workspace))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn registry_lists_and_dispatches() {
let dir = tempfile::tempdir().unwrap();
let host = ToolRegistry::new(dir.path());
assert_eq!(host.tool_names().len(), 21);
let listed = host.list_tools().await.unwrap();
assert_eq!(listed.len(), 21);
host.call(
"write_file",
json!({"path": "hello.txt", "content": "hi\n"}),
)
.await
.unwrap();
let out = host
.call("read_file", json!({"path": "hello.txt"}))
.await
.unwrap();
assert!(out.content.contains("hi"));
assert!(!out.is_error);
}
#[tokio::test]
async fn unknown_tool_errors() {
let dir = tempfile::tempdir().unwrap();
let host = ToolRegistry::new(dir.path());
let err = host.call("vault_http_fetch", json!({})).await.unwrap_err();
assert!(err.to_string().contains("unknown tool"));
}
#[tokio::test]
async fn bad_args_surface_as_error_outcomes() {
let dir = tempfile::tempdir().unwrap();
let host = ToolRegistry::new(dir.path());
let out = host.call("read_file", json!({"nope": 1})).await.unwrap();
assert!(out.is_error);
assert!(out.content.contains("bad args"));
}
}