sgr-agent-tools 0.2.0

14 reusable file-system tools for sgr-agent based AI agents
Documentation

sgr-agent-tools

Crates.io License: MIT

11 reusable file-system tools for sgr-agent based AI agents.

Generic over FileBackend trait — implement it once for your runtime (RPC, local fs, in-memory mock) and get battle-tested tools out of the box.

Tools

# Tool Type Description
1 ReadTool observe Read file with trust metadata header
2 WriteTool act Write file with JSON auto-repair (llm_json)
3 DeleteTool act Delete files — single or batch via paths[]
4 SearchTool observe Smart search: query expansion, fuzzy regex, Levenshtein fallback, auto-expand ≤10 files
5 ListTool observe List directory contents
6 TreeTool observe Directory tree structure
7 EvalTool compute JavaScript via Boa engine, file glob, workspace_date (feature eval)
8 ReadAllTool observe Batch read all files in directory
9 MkDirTool act Create directory (deferred)
10 MoveTool act Move/rename file (deferred)
11 FindTool observe Find files by name pattern (deferred)

Quick start

Via sgr-agent (recommended)

sgr-agent = { version = "0.7", features = ["tools"] }
# with JS eval:
sgr-agent = { version = "0.7", features = ["tools-eval"] }
use sgr_agent::tools::{FileBackend, ReadTool, SearchTool, WriteTool};

Standalone

sgr-agent-tools = "0.1"
# with JS eval:
sgr-agent-tools = { version = "0.1", features = ["eval"] }

Usage

use std::sync::Arc;
use sgr_agent_tools::{FileBackend, ReadTool, WriteTool, SearchTool, TreeTool};

// 1. Implement FileBackend for your runtime
struct MyBackend;

#[async_trait::async_trait]
impl FileBackend for MyBackend {
    async fn read(&self, path: &str, number: bool, start_line: i32, end_line: i32) -> anyhow::Result<String> {
        todo!("read from your storage")
    }
    async fn write(&self, path: &str, content: &str, start_line: i32, end_line: i32) -> anyhow::Result<()> {
        todo!()
    }
    async fn delete(&self, path: &str) -> anyhow::Result<()> { todo!() }
    async fn search(&self, root: &str, pattern: &str, limit: i32) -> anyhow::Result<String> { todo!() }
    async fn list(&self, path: &str) -> anyhow::Result<String> { todo!() }
    async fn tree(&self, root: &str, level: i32) -> anyhow::Result<String> { todo!() }
    async fn context(&self) -> anyhow::Result<String> { todo!() }
    async fn mkdir(&self, path: &str) -> anyhow::Result<()> { todo!() }
    async fn move_file(&self, from: &str, to: &str) -> anyhow::Result<()> { todo!() }
    async fn find(&self, root: &str, name: &str, file_type: &str, limit: i32) -> anyhow::Result<String> { todo!() }
}

// 2. Create tools
let backend = Arc::new(MyBackend);
let read = ReadTool(backend.clone());
let write = WriteTool(backend.clone());
let search = SearchTool(backend.clone());
let tree = TreeTool(backend.clone());

Adding your own tools

Build custom tools using sgr-agent-core types:

use std::sync::Arc;
use sgr_agent_core::{Tool, ToolOutput, ToolError, parse_args, AgentContext, json_schema_for};
use schemars::JsonSchema;
use serde::Deserialize;

use sgr_agent_tools::FileBackend;

#[derive(Deserialize, JsonSchema)]
struct WordCountArgs {
    /// File path to count words in
    path: String,
}

struct WordCountTool<B: FileBackend>(pub Arc<B>);

#[async_trait::async_trait]
impl<B: FileBackend> Tool for WordCountTool<B> {
    fn name(&self) -> &str { "word_count" }
    fn description(&self) -> &str { "Count words in a file" }
    fn is_read_only(&self) -> bool { true }
    fn parameters_schema(&self) -> serde_json::Value { json_schema_for::<WordCountArgs>() }

    async fn execute(&self, args: serde_json::Value, _ctx: &mut AgentContext) -> Result<ToolOutput, ToolError> {
        let a: WordCountArgs = parse_args(&args)?;
        let content = self.0.read(&a.path, false, 0, 0).await
            .map_err(|e| ToolError::Execution(e.to_string()))?;
        let count = content.split_whitespace().count();
        Ok(ToolOutput::text(format!("{count} words")))
    }
}

Pattern: struct YourTool<B: FileBackend>(pub Arc<B>) — generic over backend, reusable across projects.

Middleware pattern (extending tools)

When you need project-specific behavior on top of base tools (workflow guards, hooks, annotations), use the middleware wrapper pattern instead of forking the tool:

use sgr_agent_tools::{ReadTool, FileBackend};
use sgr_agent_core::{Tool, ToolOutput, ToolError, AgentContext};

/// Middleware: adds workflow tracking + content security scanning to ReadTool.
struct Pac1ReadTool<B: FileBackend> {
    inner: ReadTool<B>,
    workflow: Arc<Mutex<WorkflowState>>,
}

#[async_trait::async_trait]
impl<B: FileBackend> Tool for Pac1ReadTool<B> {
    fn name(&self) -> &str { self.inner.name() }          // delegate name
    fn description(&self) -> &str { self.inner.description() } // delegate schema
    fn parameters_schema(&self) -> serde_json::Value { self.inner.parameters_schema() }

    async fn execute(&self, args: serde_json::Value, ctx: &mut AgentContext)
        -> Result<ToolOutput, ToolError>
    {
        // 1. Base read (trust metadata, auto line numbers)
        let result = self.inner.execute(args, ctx).await?;
        let mut output = result.content;

        // 2. Middleware: security content scan
        output = scan_for_injection(output);

        // 3. Middleware: workflow phase tracking
        let path = extract_path_from_header(&output);
        for msg in self.workflow.lock().unwrap().post_action("read", &path) {
            output.push_str(&format!("\n{}", msg));
        }

        Ok(ToolOutput::text(output))
    }
}

When to use middleware vs custom tool:

Scenario Approach
Add pre/post hooks to existing tool Middleware wrapper
Same schema, different behavior Middleware wrapper
Completely different schema/logic Custom tool from scratch
Project-specific annotations (CRM, etc.) Middleware wrapper

Real example from PAC1 agent — 3 tools use middleware:

  • ReadTool + security scan + workflow tracking
  • WriteTool + outbox injection + JSON schema validation + hooks
  • DeleteTool + workflow guards (block delete before write)

Design principles

Based on building a PAC1 benchmark agent (16→11 tools, 7 models, 40+ tasks) and studying Codex CLI / Claude Code:

  • 7 core tools max in prompt schema — models degrade on long tool lists
  • Deferred loading for rarely-used tools (mkdir, move, find)
  • Trust metadata on every read: [path | trusted/untrusted]
  • Batch tools justified only when saving 3+ round-trips (read_all: 48→4 calls)
  • Smart search — don't fail silently, try name variants and fuzzy matching
  • JSON auto-repair — LLMs produce broken JSON, fix it before writing

Crate architecture

sgr-agent-core    ← Tool trait, AgentContext, schema (5 lightweight deps)
    ↑         ↑
sgr-agent-tools   sgr-agent
(this crate)      (framework, re-exports tools via feature "tools")

Features

Feature Default What
(none) yes 10 tools without JS eval
eval no Adds EvalTool — Boa JS engine (~5MB binary size)