Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Changed
- **BREAKING**: `ToolDef` schema field replaced `Vec<ParamDef>` with `schemars::Schema` auto-derived from Rust structs via `#[derive(JsonSchema)]`
- **BREAKING**: `ParamDef` and `ParamType` removed from `zeph-tools` public API
- **BREAKING**: `ToolRegistry::new()` replaced with `ToolRegistry::from_definitions()`; registry no longer hardcodes built-in tools — each executor owns its definitions via `tool_definitions()`
- `ToolDef` now includes `InvocationHint` (FencedBlock/ToolCall) so LLM prompt shows exact invocation format per tool
- `web_scrape` tool definition includes all parameters (`url`, `select`, `extract`, `limit`) auto-derived from `ScrapeInstruction`
- `ShellExecutor` and `WebScrapeExecutor` now implement `tool_definitions()` for single source of truth

## [0.9.5] - 2026-02-14

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ reqwest = { version = "0.13", default-features = false }
rmcp = "0.14"
scrape-core = "0.2.2"
subtle = "2.6"
schemars = "1.2"
serde = "1.0"
serde_json = "1.0"
serial_test = "3.3"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ zeph (binary)
├── zeph-memory — SQLite + Qdrant, semantic recall, summarization
├── zeph-index — AST-based code indexing, semantic retrieval, repo map (optional)
├── zeph-channels — Telegram adapter (teloxide) with streaming
├── zeph-tools — ToolRegistry with 7 built-in tools (shell, file read/write/edit/glob/grep, web scrape), composite dispatch
├── zeph-tools — schemars-driven tool registry (shell, file ops, web scrape), composite dispatch
├── zeph-mcp — MCP client, multi-server lifecycle, unified tool matching
├── zeph-a2a — A2A client + server, agent discovery, JSON-RPC 2.0
└── zeph-tui — ratatui TUI dashboard with live agent metrics (optional)
Expand Down
2 changes: 1 addition & 1 deletion crates/zeph-core/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1739,7 +1739,7 @@ impl<P: LlmProvider + Clone + 'static, C: Channel, T: ToolExecutor> Agent<P, C,
if defs.is_empty() {
None
} else {
let reg = zeph_tools::ToolRegistry::new();
let reg = zeph_tools::ToolRegistry::from_definitions(defs);
Some(reg.format_for_prompt_filtered(&self.permission_policy))
}
};
Expand Down
1 change: 1 addition & 0 deletions crates/zeph-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ glob.workspace = true
regex.workspace = true
uuid = { workspace = true, features = ["v4"] }
reqwest = { workspace = true, features = ["rustls"] }
schemars.workspace = true
scrape-core.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
Expand Down
178 changes: 96 additions & 82 deletions crates/zeph-tools/src/file.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,64 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};

use schemars::JsonSchema;

use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
use crate::registry::{ParamDef, ParamType, ToolDef};
use crate::registry::{InvocationHint, ToolDef};

// Schema-only: fields are read by schemars derive, not by Rust code directly.
#[derive(JsonSchema)]
#[allow(dead_code)]
pub(crate) struct ReadParams {
/// File path
path: String,
/// Line offset
offset: Option<u32>,
/// Max lines
limit: Option<u32>,
}

// Schema-only: fields are read by schemars derive, not by Rust code directly.
#[derive(JsonSchema)]
#[allow(dead_code)]
struct WriteParams {
/// File path
path: String,
/// Content to write
content: String,
}

// Schema-only: fields are read by schemars derive, not by Rust code directly.
#[derive(JsonSchema)]
#[allow(dead_code)]
struct EditParams {
/// File path
path: String,
/// Text to find
old_string: String,
/// Replacement text
new_string: String,
}

// Schema-only: fields are read by schemars derive, not by Rust code directly.
#[derive(JsonSchema)]
#[allow(dead_code)]
struct GlobParams {
/// Glob pattern
pattern: String,
}

// Schema-only: fields are read by schemars derive, not by Rust code directly.
#[derive(JsonSchema)]
#[allow(dead_code)]
struct GrepParams {
/// Regex pattern
pattern: String,
/// Search path
path: Option<String>,
/// Case sensitive
case_sensitive: Option<bool>,
}

/// File operations executor sandboxed to allowed paths.
#[derive(Debug)]
Expand Down Expand Up @@ -219,108 +275,37 @@ impl ToolExecutor for FileExecutor {
self.execute_file_tool(&call.tool_id, &call.params)
}

#[allow(clippy::too_many_lines)]
fn tool_definitions(&self) -> Vec<ToolDef> {
vec![
ToolDef {
id: "read",
description: "Read file contents with optional offset/limit",
parameters: vec![
ParamDef {
name: "path",
description: "File path",
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "offset",
description: "Line offset",
required: false,
param_type: ParamType::Integer,
},
ParamDef {
name: "limit",
description: "Max lines",
required: false,
param_type: ParamType::Integer,
},
],
schema: schemars::schema_for!(ReadParams),
invocation: InvocationHint::ToolCall,
},
ToolDef {
id: "write",
description: "Write content to a file",
parameters: vec![
ParamDef {
name: "path",
description: "File path",
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "content",
description: "Content to write",
required: true,
param_type: ParamType::String,
},
],
schema: schemars::schema_for!(WriteParams),
invocation: InvocationHint::ToolCall,
},
ToolDef {
id: "edit",
description: "Replace a string in a file",
parameters: vec![
ParamDef {
name: "path",
description: "File path",
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "old_string",
description: "Text to find",
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "new_string",
description: "Replacement text",
required: true,
param_type: ParamType::String,
},
],
schema: schemars::schema_for!(EditParams),
invocation: InvocationHint::ToolCall,
},
ToolDef {
id: "glob",
description: "Find files matching a glob pattern",
parameters: vec![ParamDef {
name: "pattern",
description: "Glob pattern",
required: true,
param_type: ParamType::String,
}],
schema: schemars::schema_for!(GlobParams),
invocation: InvocationHint::ToolCall,
},
ToolDef {
id: "grep",
description: "Search file contents with regex",
parameters: vec![
ParamDef {
name: "pattern",
description: "Regex pattern",
required: true,
param_type: ParamType::String,
},
ParamDef {
name: "path",
description: "Search path",
required: false,
param_type: ParamType::String,
},
ParamDef {
name: "case_sensitive",
description: "Case sensitive",
required: false,
param_type: ParamType::Boolean,
},
],
schema: schemars::schema_for!(GrepParams),
invocation: InvocationHint::ToolCall,
},
]
}
Expand Down Expand Up @@ -623,4 +608,33 @@ mod tests {
let result = exec.execute_file_tool("grep", &params);
assert!(matches!(result, Err(ToolError::SandboxViolation { .. })));
}

#[test]
fn tool_definitions_returns_five_tools() {
let exec = FileExecutor::new(vec![]);
let defs = exec.tool_definitions();
assert_eq!(defs.len(), 5);
let ids: Vec<&str> = defs.iter().map(|d| d.id).collect();
assert_eq!(ids, vec!["read", "write", "edit", "glob", "grep"]);
}

#[test]
fn tool_definitions_all_use_tool_call() {
let exec = FileExecutor::new(vec![]);
for def in exec.tool_definitions() {
assert_eq!(def.invocation, InvocationHint::ToolCall);
}
}

#[test]
fn tool_definitions_read_schema_has_params() {
let exec = FileExecutor::new(vec![]);
let defs = exec.tool_definitions();
let read = defs.iter().find(|d| d.id == "read").unwrap();
let obj = read.schema.as_object().unwrap();
let props = obj["properties"].as_object().unwrap();
assert!(props.contains_key("path"));
assert!(props.contains_key("offset"));
assert!(props.contains_key("limit"));
}
}
Loading
Loading