Enhance tool documentation and add new features
- Added module-level documentation for memory tools (`remember`, `recall`, `forget`) to clarify their purpose. - Improved documentation in `recall.rs` and `remember.rs` to describe the functionality and flow of memory entry operations. - Updated `mod.rs` to include descriptions for the tool trait and execution context. - Enhanced `plan.rs` with detailed comments on plan-mode signaling tools. - Documented text search tools in `search.rs` to explain their functionality. - Improved sequential-thinking tool documentation in `seqthink.rs`. - Added safety filter documentation in `shell_filter` for credential and git operations. - Enhanced utility tools documentation, including `cd`, `dir_cache_update`, and `todowrite`. - Improved rendering documentation in view modules (`chat`, `markdown`, `status`, `workflow`) to clarify rendering flows and purposes.
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
//! Tool implementations for interacting with background bash jobs: `bash_output`
|
||||
//! and `bash_kill`. Both take a `job_id` produced by `bash` with `run_in_background=true`.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool: fetch buffered output from a background bash job by `job_id`.
|
||||
pub struct BashOutput;
|
||||
|
||||
impl Tool for BashOutput {
|
||||
@@ -39,6 +43,7 @@ impl Tool for BashOutput {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool: terminate a running background bash job by `job_id`.
|
||||
pub struct BashKill;
|
||||
|
||||
impl Tool for BashKill {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Tool: `delete` — remove a file or empty directory relative to a workspace root.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
@@ -7,6 +9,7 @@ use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
use super::helpers::arg_str;
|
||||
|
||||
/// Tool: delete a file or empty directory. Refuses non-empty directories.
|
||||
pub struct Delete;
|
||||
|
||||
impl Tool for Delete {
|
||||
@@ -31,6 +34,10 @@ impl Tool for Delete {
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete a file or empty directory. Returns success message or errors on failure.
|
||||
///
|
||||
/// Flow: resolve path → check existence → check dir/file → remove.
|
||||
/// Only empty directories are deletable (non-empty returns an error).
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Tool: `edit` — replace a substring in a file with a new string.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
@@ -8,6 +10,7 @@ use super::super::resolve_path;
|
||||
use super::super::check_graduated_checks;
|
||||
use super::helpers::arg_str;
|
||||
|
||||
/// Tool: replace text in a file. Requires the old string to be unique unless `replace_all` is true.
|
||||
pub struct Edit;
|
||||
|
||||
impl Tool for Edit {
|
||||
@@ -48,6 +51,13 @@ impl Tool for Edit {
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform the in-file string replacement.
|
||||
///
|
||||
/// Flow: validate args → resolve path → read file → count occurrences →
|
||||
/// replace one or all → write back → report byte delta (+ optional graduated checks).
|
||||
///
|
||||
/// Why: requires a non-empty `reason` and a non-empty `old` string to prevent
|
||||
/// accidental identity edits. Enforces uniqueness unless `replace_all` is set.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let old = arg_str(args, "old")?;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
//! Shared helpers for filesystem tools: extracting string arguments from JSON
|
||||
//! and producing user-friendly "not found" diagnostics.
|
||||
|
||||
use std::path::Path;
|
||||
use serde_json::Value;
|
||||
use anyhow::{Result, anyhow};
|
||||
|
||||
/// Extract a required string argument from a JSON args map.
|
||||
///
|
||||
/// Return: the value as `String` if present and a string type; `Err` if missing
|
||||
/// or of a different JSON type (null, number, boolean, array, object).
|
||||
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
||||
args.get(name)
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -9,6 +16,12 @@ pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
||||
.ok_or_else(|| anyhow!("missing required argument: {}", name))
|
||||
}
|
||||
|
||||
/// Produce a user-friendly diagnostic string when a path doesn't resolve or exist.
|
||||
///
|
||||
/// Checks whether the resolved path canonically falls inside any workspace root
|
||||
/// and reports either "path outside workspaces" or "path does not exist" accordingly.
|
||||
///
|
||||
/// Return: a one-line description of the resolution failure.
|
||||
pub fn not_found_help(ctx: &super::super::ToolCtx, path: &Path, rel: &str) -> String {
|
||||
let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
let in_ws = ctx.workspaces.iter().any(|w| {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
//! Filesystem tool implementations: read, write, edit, and delete operations
|
||||
//! on workspace-rooted paths.
|
||||
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod helpers;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Tool: `read` — display file contents with line numbers.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
@@ -7,6 +9,7 @@ use super::super::ToolCtx;
|
||||
use super::super::resolve_path;
|
||||
use super::helpers::{arg_str, not_found_help};
|
||||
|
||||
/// Tool: read a file and display it with line numbers, optionally truncated to `limit` lines.
|
||||
pub struct Read;
|
||||
|
||||
impl Tool for Read {
|
||||
@@ -35,6 +38,13 @@ impl Tool for Read {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read and display a file with line numbers.
|
||||
///
|
||||
/// Flow: resolve path → if not found, call `not_found_help` for diagnostic →
|
||||
/// read entire file → enumerate and format lines → optionally truncate by `limit`.
|
||||
///
|
||||
/// Return: line-numbered content; `not_found_help` message if the path doesn't
|
||||
/// exist; a "is a directory" message if the path points at a directory.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let limit = args.get("limit").and_then(|v| v.as_u64()).map(|v| v as usize);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Tool: `write` — write content to a file, creating parent directories on demand.
|
||||
|
||||
use std::fs;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -7,6 +9,7 @@ use super::super::resolve_path;
|
||||
use super::super::check_graduated_checks;
|
||||
use super::helpers::arg_str;
|
||||
|
||||
/// Tool: write content to a file, auto-creating parent directories as needed.
|
||||
pub struct Write;
|
||||
|
||||
impl Tool for Write {
|
||||
@@ -39,6 +42,13 @@ impl Tool for Write {
|
||||
})
|
||||
}
|
||||
|
||||
/// Write content to a file, creating parent directories as needed.
|
||||
///
|
||||
/// Flow: validate args (non-empty reason) → resolve path → create parent
|
||||
/// dirs → write file → report byte count (+ optional graduated checks).
|
||||
///
|
||||
/// Why: requires a non-empty `reason` to discourage stray writes; parent
|
||||
/// directories are created silently so the tool works for new paths.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = arg_str(args, "path")?;
|
||||
let content = arg_str(args, "content")?;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Tool wrapper around `git credential` for store/get/erase operations.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that shells out to `git credential <op>` to store, retrieve, or erase credentials.
|
||||
pub struct GitCred;
|
||||
|
||||
impl Tool for GitCred {
|
||||
@@ -29,6 +32,15 @@ impl Tool for GitCred {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run `git credential <operation>`, forwarding stdin-less invocation to the git binary.
|
||||
///
|
||||
/// Flow: extract `operation` arg → spawn `git credential <operation>` → capture output.
|
||||
///
|
||||
/// Why: `store`/`get`/`erase` are the only credential-helper subcommands git supports;
|
||||
/// no stdin is piped, so this mainly surfaces helper output/errors rather than
|
||||
/// performing an interactive credential exchange.
|
||||
///
|
||||
/// Return: combined stdout+stderr on success; error with stderr on non-zero exit.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let operation = args.get("operation")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Generic tool for running arbitrary git subcommands.
|
||||
|
||||
use std::process::Command;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that runs `git <operation> [args...]` and returns combined stdout/stderr.
|
||||
pub struct GitOperator;
|
||||
|
||||
impl Tool for GitOperator {
|
||||
@@ -33,6 +36,16 @@ impl Tool for GitOperator {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run `git <operation> [args...]` and return its combined output.
|
||||
///
|
||||
/// Flow: extract `operation` + `args` → spawn `git <operation> <args>` → trim and
|
||||
/// join stdout/stderr.
|
||||
///
|
||||
/// Why: no allowlist here — the model may run any git subcommand; destructive
|
||||
/// operations are blocked upstream by `shell_filter::git`, not by this tool.
|
||||
///
|
||||
/// Return: trimmed combined output on success; error including exit code and
|
||||
/// stderr on failure.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let operation = args.get("operation")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Tool for creating git worktrees under the session's worktrees directory.
|
||||
|
||||
use std::process::Command;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that creates a new git worktree (`git worktree add`) from a given base ref.
|
||||
pub struct GitWorktree;
|
||||
|
||||
impl Tool for GitWorktree {
|
||||
@@ -32,6 +35,13 @@ impl Tool for GitWorktree {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create the worktree directory and run `git worktree add --checkout <path> <base_ref>`.
|
||||
///
|
||||
/// Flow: extract name/base_ref → create worktree dir under `ctx.worktrees_dir` →
|
||||
/// spawn `git worktree add` → combine stdout/stderr.
|
||||
///
|
||||
/// Return: success message with combined output on success; error including exit
|
||||
/// code and stderr on failure.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let name = args.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Tool for deleting a persisted memory entry by name.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use crate::model::memory::Memory;
|
||||
|
||||
/// Tool that removes a single memory entry from `ctx.memory_dir` by exact name.
|
||||
pub struct Forget;
|
||||
|
||||
impl Tool for Forget {
|
||||
@@ -28,6 +31,12 @@ impl Tool for Forget {
|
||||
})
|
||||
}
|
||||
|
||||
/// Delete the memory file matching `name` from disk.
|
||||
///
|
||||
/// Flow: extract `name` → `Memory::remove` → confirmation string.
|
||||
///
|
||||
/// Return: confirmation message on success; error if the memory does not exist
|
||||
/// or the file could not be removed.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let name = args.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Memory tools: `remember`, `recall`, and `forget` for persisted project memory entries.
|
||||
|
||||
pub mod forget;
|
||||
pub mod recall;
|
||||
pub mod remember;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Tool for reading a single memory entry or listing the whole memory index.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use crate::model::memory::Memory;
|
||||
|
||||
/// Tool that reads one memory entry by name, or lists all entries when name is omitted.
|
||||
pub struct Recall;
|
||||
|
||||
impl Tool for Recall {
|
||||
@@ -27,6 +30,12 @@ impl Tool for Recall {
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a specific memory entry, or fall back to listing all entries.
|
||||
///
|
||||
/// Flow: if `name` present and non-empty → `Memory::read` and format as frontmatter
|
||||
/// + body; otherwise → `list_all`.
|
||||
///
|
||||
/// Return: formatted memory content, or the full index listing.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
|
||||
if name.is_empty() {
|
||||
@@ -48,6 +57,12 @@ impl Tool for Recall {
|
||||
}
|
||||
}
|
||||
|
||||
/// List every memory entry in `ctx.memory_dir` as a one-line summary index.
|
||||
///
|
||||
/// Flow: `Memory::list` names → for each, try `Memory::read` for kind/description →
|
||||
/// fall back to bare name if the file can't be parsed.
|
||||
///
|
||||
/// Return: `Ok` with the formatted index (never fails; missing dir yields "(no memory entries)").
|
||||
fn list_all(ctx: &ToolCtx) -> Result<String> {
|
||||
let names = Memory::list(&ctx.memory_dir);
|
||||
if names.is_empty() {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
//! Tool for saving a new memory entry to persistent project memory.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
use crate::model::memory::Memory;
|
||||
|
||||
/// Tool that writes a new `Memory` entry (name/description/content/kind) to disk.
|
||||
pub struct Remember;
|
||||
|
||||
impl Tool for Remember {
|
||||
@@ -41,6 +44,16 @@ impl Tool for Remember {
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a `Memory` from the given args and persist it to `ctx.memory_dir`.
|
||||
///
|
||||
/// Flow: extract name/description/content/kind → validate name via `Memory::slugify`
|
||||
/// → construct `Memory` with `lifecycle: "new"` and current timestamps →
|
||||
/// `memory.write`.
|
||||
///
|
||||
/// Why: name must slugify to a valid filename (alphanumeric + hyphens, 1-80 chars)
|
||||
/// since it's used directly as the on-disk file identifier.
|
||||
///
|
||||
/// Return: confirmation string on success; error if name is invalid or the write fails.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let name = args.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Tool trait, execution context, and the registry of all 28 built-in tools.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use serde_json::Value;
|
||||
use anyhow::Result;
|
||||
@@ -16,6 +18,7 @@ pub mod shell_filter;
|
||||
pub mod utility;
|
||||
pub mod workflow;
|
||||
|
||||
/// Common interface every agent-invocable tool implements: name, JSON schema, and execution.
|
||||
pub trait Tool: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
fn description(&self) -> &'static str;
|
||||
@@ -23,6 +26,7 @@ pub trait Tool: Send + Sync {
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String>;
|
||||
}
|
||||
|
||||
/// A project-defined rule that flags a matching file path or content pattern for review.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GraduatedCheck {
|
||||
pub name: String,
|
||||
@@ -30,6 +34,8 @@ pub struct GraduatedCheck {
|
||||
pub rule: String,
|
||||
}
|
||||
|
||||
/// Shared execution context passed to every `Tool::run` call: workspace roots, session
|
||||
/// paths, and cached directory state.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolCtx {
|
||||
pub workspaces: Vec<PathBuf>,
|
||||
@@ -42,6 +48,12 @@ pub struct ToolCtx {
|
||||
pub graduated_checks: Vec<GraduatedCheck>,
|
||||
}
|
||||
|
||||
/// Find which graduated checks apply to a given file path/content pair.
|
||||
///
|
||||
/// Flow: for each check, match its `pattern` against `path` or its `rule` against
|
||||
/// `content` (substring match) → collect matching check names.
|
||||
///
|
||||
/// Return: names of all matching checks; empty if none match.
|
||||
pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedCheck]) -> Vec<String> {
|
||||
let mut matches = Vec::new();
|
||||
for check in checks {
|
||||
@@ -53,11 +65,13 @@ pub fn check_graduated_checks(path: &str, content: &str, checks: &[GraduatedChec
|
||||
}
|
||||
|
||||
impl ToolCtx {
|
||||
/// Start building a `ToolCtx` with `ToolCtxBuilder`'s defaults.
|
||||
pub fn builder() -> ToolCtxBuilder {
|
||||
ToolCtxBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for `ToolCtx`; fields default to empty paths and a fresh `DirCache`.
|
||||
pub struct ToolCtxBuilder {
|
||||
pub workspaces: Vec<PathBuf>,
|
||||
pub session_dir: PathBuf,
|
||||
@@ -85,8 +99,11 @@ impl Default for ToolCtxBuilder {
|
||||
}
|
||||
|
||||
impl ToolCtxBuilder {
|
||||
/// Set the session directory.
|
||||
pub fn session_dir(mut self, v: PathBuf) -> Self { self.session_dir = v; self }
|
||||
/// Set the origin (main process vs. daemon-attached).
|
||||
pub fn origin(mut self, v: crate::app::state::types::Origin) -> Self { self.origin = v; self }
|
||||
/// Consume the builder and produce the final `ToolCtx`.
|
||||
pub fn build(self) -> ToolCtx {
|
||||
ToolCtx {
|
||||
workspaces: self.workspaces,
|
||||
@@ -101,6 +118,10 @@ impl ToolCtxBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct one instance of every built-in tool, in the fixed order exposed to the LLM.
|
||||
///
|
||||
/// Return: boxed trait objects for all 28 tools (fs, search, bash, git, memory, plan,
|
||||
/// workflow, utility).
|
||||
pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
||||
vec![
|
||||
Box::new(super::tool::fs::read::Read),
|
||||
@@ -131,10 +152,16 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Whether a tool by name can mutate the filesystem or run arbitrary shell commands.
|
||||
///
|
||||
/// Why: used by the harness to decide which tool calls need user confirmation/guard checks.
|
||||
pub fn tool_is_risky(name: &str) -> bool {
|
||||
matches!(name, "write" | "delete" | "edit" | "bash" | "git_operator")
|
||||
}
|
||||
|
||||
/// Convert a list of tools into the provider-facing `ToolDef` request schema.
|
||||
///
|
||||
/// Return: one `ToolDef` per tool, in the same order as `tools`.
|
||||
pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::ToolDef> {
|
||||
tools
|
||||
.iter()
|
||||
@@ -149,6 +176,18 @@ pub fn tool_defs(tools: &[Box<dyn Tool>]) -> Vec<crate::dto::provider::request::
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve a tool-supplied relative path to an absolute path within a workspace root,
|
||||
/// rejecting escapes.
|
||||
///
|
||||
/// Flow: parse optional `[N]` workspace-index prefix (defaults to workspace 0) → join
|
||||
/// remainder onto that workspace root → canonicalize → verify the canonical path
|
||||
/// still starts with one of `workspaces`.
|
||||
///
|
||||
/// Why: canonicalizing and re-checking containment (rather than trusting the join)
|
||||
/// prevents `../` traversal from escaping the sandboxed workspace roots.
|
||||
///
|
||||
/// Return: the canonical absolute path, or an error if the workspace index is invalid
|
||||
/// or the resolved path falls outside all workspace roots.
|
||||
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
||||
let _parts: Vec<&str> = rel.splitn(2, '/').collect();
|
||||
let (ws_idx, path) = if rel.starts_with('[') {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Plan-mode signaling tools: entering plan mode with a proposal, and confirming readiness.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool the model calls to present a step-by-step plan and enter plan mode.
|
||||
pub struct PlanEnter;
|
||||
|
||||
impl Tool for PlanEnter {
|
||||
@@ -31,6 +34,10 @@ impl Tool for PlanEnter {
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate that both `plan` and `sign_off` are present; the actual plan text is
|
||||
/// surfaced to the user by the harness rather than returned here.
|
||||
///
|
||||
/// Return: fixed acknowledgement string on success; error if either arg is missing.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let _plan = args.get("plan")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -42,6 +49,7 @@ impl Tool for PlanEnter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool the model calls to confirm it will follow the approved plan before executing it.
|
||||
pub struct PlanReady;
|
||||
|
||||
impl Tool for PlanReady {
|
||||
@@ -66,6 +74,9 @@ impl Tool for PlanReady {
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate that a `confirmation` argument was supplied before exiting plan mode.
|
||||
///
|
||||
/// Return: fixed "ready to execute" string on success; error if `confirmation` is missing.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let _confirmation = args.get("confirmation")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Text search tools: `grep` (line matching) and `glob` (filename pattern matching).
|
||||
|
||||
use std::fs;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -7,6 +9,7 @@ use super::Tool;
|
||||
use super::ToolCtx;
|
||||
use super::resolve_path;
|
||||
|
||||
/// Tool that recursively searches text files under a directory for a literal substring.
|
||||
pub struct Grep;
|
||||
|
||||
impl Tool for Grep {
|
||||
@@ -35,6 +38,16 @@ impl Tool for Grep {
|
||||
})
|
||||
}
|
||||
|
||||
/// Recursively walk the resolved directory and collect matching lines.
|
||||
///
|
||||
/// Flow: extract `pattern` + `path` → `resolve_path` (workspace-scoped) → bail if
|
||||
/// missing/not a dir → `ignore::Walk` the tree → for each file, `read_to_string`
|
||||
/// and substring-match each line → emit `<rel_path>:<line_no>:<text>` rows.
|
||||
///
|
||||
/// Why: `ignore::Walk` respects `.gitignore` and skips heavy dirs (e.g. `.git/`)
|
||||
/// which is what the agent expects when running in real repos.
|
||||
///
|
||||
/// Return: "no matches found" if empty, else a header + `path:line:text` rows.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let pattern = args.get("pattern")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -80,6 +93,7 @@ impl Tool for Grep {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that lists files under a directory matching a glob pattern.
|
||||
pub struct Glob;
|
||||
|
||||
impl Tool for Glob {
|
||||
@@ -108,6 +122,17 @@ impl Tool for Glob {
|
||||
})
|
||||
}
|
||||
|
||||
/// Walk the resolved directory and collect entries matching the glob pattern.
|
||||
///
|
||||
/// Flow: extract pattern + path → `resolve_path` → build a `GlobSet` from the
|
||||
/// joined absolute pattern → `ignore::Walk` the tree → keep entries that
|
||||
/// match → sort → join with newlines, appending `/` for directories.
|
||||
///
|
||||
/// Why: joining the workspace-relative pattern onto the resolved root lets users
|
||||
/// supply familiar glob shapes (`**/*.rs`) while the sandbox still controls the
|
||||
/// boundary.
|
||||
///
|
||||
/// Return: sorted newline-joined matches; "no files match" sentinel if empty.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let pat_str = args.get("pattern")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Sequential-thinking tool: a no-side-effect echo that records reasoning steps.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::Result;
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that accepts a reasoning step and returns it verbatim, giving the model a
|
||||
/// structured way to surface its thought chain to the TUI.
|
||||
pub struct SeqThink;
|
||||
|
||||
impl Tool for SeqThink {
|
||||
@@ -27,6 +31,12 @@ impl Tool for SeqThink {
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the `thought` string verbatim (or empty if missing).
|
||||
///
|
||||
/// Why: there's no I/O or state mutation — the harness surfaces the text in the
|
||||
/// TUI's reasoning pane.
|
||||
///
|
||||
/// Return: the thought text, possibly empty; never an error.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let thought = args.get("thought").and_then(|v| v.as_str()).unwrap_or("");
|
||||
Ok(thought.to_string())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Bash-shell execution tool with safety filters and optional timeout.
|
||||
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
use serde_json::{json, Value};
|
||||
@@ -5,6 +7,8 @@ use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that runs `bash -c <command>`, optionally in the background, with safety
|
||||
/// filters applied before spawning.
|
||||
pub struct Bash;
|
||||
|
||||
impl Tool for Bash {
|
||||
@@ -41,6 +45,19 @@ impl Tool for Bash {
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a bash command (foreground or background) with safety filters and a timeout.
|
||||
///
|
||||
/// Flow: extract args → run `check_credential_read` then `check_git_destructive`
|
||||
/// (bail if either rejects) → branch on `run_in_background`: if true, hand off
|
||||
/// to the bg-bash subsystem and return the job ID; else spawn `bash -c`,
|
||||
/// poll with `try_wait`, kill on timeout, format combined stdout+stderr.
|
||||
///
|
||||
/// Why: the safety filters run unconditionally so background jobs are also gated;
|
||||
/// the timeout is enforced by polling the child rather than relying on a libc alarm
|
||||
/// so cleanup stays in Rust.
|
||||
///
|
||||
/// Return: exit-code + elapsed-seconds summary line (plus captured output) for
|
||||
/// foreground runs, or the job ID for background runs.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let cmd = args.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
//! Block shell commands that try to read common credential files or secrets.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Reject shell commands whose lowercased form contains any known credential-read pattern.
|
||||
///
|
||||
/// Flow: lowercase the command → for each pattern, substring-match → bail with the
|
||||
/// matching pattern on the first hit.
|
||||
///
|
||||
/// Why: catches `cat ~/.ssh/id_rsa`, `grep token= foo.txt`, `.git-credentials`,
|
||||
/// cloud-CLI credential paths, etc., before the bash tool spawns anything.
|
||||
///
|
||||
/// Return: `Ok(())` if no pattern matches; error naming the offending pattern otherwise.
|
||||
pub fn check_credential_read(cmd: &str) -> Result<()> {
|
||||
let patterns = [
|
||||
"cat ~/.ssh",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
//! Block shell commands that perform destructive or hard-to-reverse git operations.
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Reject shell commands whose lowercased form contains any known destructive git pattern.
|
||||
///
|
||||
/// Flow: lowercase the command → for each pattern, substring-match → bail with the
|
||||
/// matching pattern on the first hit.
|
||||
///
|
||||
/// Why: hard-resets, force-pushes, `clean -fdx`, `filter-branch`, etc. can destroy
|
||||
/// uncommitted work or rewrite shared history; the bash tool refuses to run them.
|
||||
///
|
||||
/// Return: `Ok(())` if no pattern matches; error naming the offending pattern otherwise.
|
||||
pub fn check_git_destructive(cmd: &str) -> Result<()> {
|
||||
let patterns = [
|
||||
"force-push",
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
//! Pre-execution safety filters applied to shell commands before they're spawned.
|
||||
|
||||
pub mod credentials;
|
||||
pub mod git;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! `cd` tool: verify and resolve a workspace-relative directory path.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
/// Tool that resolves a workspace-relative path and reports whether it exists and is a dir.
|
||||
pub struct Cd;
|
||||
|
||||
impl Tool for Cd {
|
||||
@@ -27,6 +30,16 @@ impl Tool for Cd {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve `path` against the workspace roots and report its status.
|
||||
///
|
||||
/// Flow: extract `path` → `resolve_path` (sandboxed to `ctx.workspaces`) →
|
||||
/// check `exists()` and `is_dir()` → canonicalize → return canonical path.
|
||||
///
|
||||
/// Why: the agent has no persistent cwd between tool calls; "cd" here is purely a
|
||||
/// verification + canonicalization helper rather than a state change.
|
||||
///
|
||||
/// Return: canonical path on success; explicit "does not exist" / "not a directory"
|
||||
/// message (still `Ok`) so the model can react without treating it as an error.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
//! Tool for refreshing the shared workspace directory cache.
|
||||
//!
|
||||
//! Flow: resolve the requested path against the workspace roots →
|
||||
//! non-recursively walk it → spin up a one-shot Tokio runtime (the agent
|
||||
//! turn runs on a plain `std::thread` with no async context) → write the
|
||||
//! entries into the shared `dir_cache` behind an async `RwLock`.
|
||||
//!
|
||||
//! Why: other tools rely on this cache for faster path resolution, so
|
||||
//! it must be kept fresh on demand rather than only populated at startup.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
/// Tool that refreshes the shared directory cache for a given path.
|
||||
pub struct DirCacheUpdate;
|
||||
|
||||
impl Tool for DirCacheUpdate {
|
||||
@@ -27,6 +38,19 @@ impl Tool for DirCacheUpdate {
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve `path`, walk its immediate entries, and store them in the shared cache.
|
||||
///
|
||||
/// Flow: extract `path` argument → resolve against workspace roots →
|
||||
/// bail early with a plain message (not an error) if it doesn't exist
|
||||
/// → `walk_directory` collects direct children → spawn a temporary
|
||||
/// Tokio runtime to acquire the async `RwLock` write guard and call
|
||||
/// `cache.set(entries)`.
|
||||
///
|
||||
/// Why: uses a fresh one-shot runtime instead of `ctx`'s own executor
|
||||
/// because this tool can be invoked from a non-async thread.
|
||||
///
|
||||
/// Return: a confirmation string with the entry count, or an error if
|
||||
/// the `path` argument is missing or the temp runtime fails to start.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -55,6 +79,14 @@ impl Tool for DirCacheUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-recursively list the immediate entries of `path`.
|
||||
///
|
||||
/// Flow: read_dir → flatten Ok entries → collect their paths.
|
||||
///
|
||||
/// Why: silently skips unreadable entries (e.g. permission errors)
|
||||
/// rather than failing the whole cache update.
|
||||
///
|
||||
/// Return: paths of direct children; empty vec if `path` can't be read.
|
||||
fn walk_directory(path: &std::path::Path) -> Vec<std::path::PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(path) {
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
//! Tool for listing the immediate contents of a workspace directory.
|
||||
//!
|
||||
//! Flow: resolve the requested path against workspace roots → validate
|
||||
//! it exists and is a directory → read its direct children with
|
||||
//! `fs::read_dir`, tagging subdirectories with a trailing `/` → format
|
||||
//! into a header + newline-joined listing.
|
||||
//!
|
||||
//! Why: gives the agent a quick, one-level view of the workspace
|
||||
//! structure without pulling in the full recursive directory cache.
|
||||
|
||||
use std::fs;
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
/// Tool that lists the immediate contents of a workspace directory.
|
||||
pub struct DirList;
|
||||
|
||||
impl Tool for DirList {
|
||||
@@ -28,6 +39,19 @@ impl Tool for DirList {
|
||||
})
|
||||
}
|
||||
|
||||
/// List the immediate entries of the requested workspace directory.
|
||||
///
|
||||
/// Flow: extract `path` argument → resolve against workspace roots →
|
||||
/// short-circuit with a plain message if the path doesn't exist or
|
||||
/// isn't a directory → `read_dir` → map each entry to its name
|
||||
/// (appending `/` for subdirectories) → join into a formatted listing
|
||||
/// with an entry-count header showing the canonicalized path.
|
||||
///
|
||||
/// Why: entries whose metadata fails to read (`e.ok()` filter) are
|
||||
/// silently skipped rather than aborting the whole listing.
|
||||
///
|
||||
/// Return: header + newline-joined entry names, or an error if the
|
||||
/// `path` argument is missing or `read_dir` fails outright.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let rel = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Small standalone utility tools (cd, dir listing/caching, pong, todowrite).
|
||||
|
||||
pub mod cd;
|
||||
pub mod dir_cache_update;
|
||||
pub mod dir_list;
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
//! Trivial connectivity-check tool.
|
||||
//!
|
||||
//! Flow: read the optional `message` argument → echo it back prefixed
|
||||
//! with `"pong: "`, defaulting to `"pong"` when no message is supplied.
|
||||
//!
|
||||
//! Why: gives callers a cheap, dependency-free way to verify the tool
|
||||
//! harness is reachable and responding before running real work.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::Result;
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
/// Tool that echoes back a message; used for connectivity/latency checks.
|
||||
pub struct Pong;
|
||||
|
||||
impl Tool for Pong {
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
//! Tool for appending timestamped tasks to the session's todo list.
|
||||
//!
|
||||
//! Flow: extract the `task` argument → format a Markdown checkbox line
|
||||
//! with a UTC timestamp → open `todo.md` in the session directory
|
||||
//! (creating it if needed) in append mode → write the line.
|
||||
//!
|
||||
//! Why: the file lives under `ctx.session_dir` so it persists per
|
||||
//! session and is picked up by the TUI's Todo panel; appending (rather
|
||||
//! than rewriting) keeps prior tasks intact.
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde_json::{json, Value};
|
||||
@@ -5,6 +15,7 @@ use anyhow::{Result, anyhow};
|
||||
use super::super::Tool;
|
||||
use super::super::ToolCtx;
|
||||
|
||||
/// Tool that appends a timestamped task line to the session's todo.md.
|
||||
pub struct Todowrite;
|
||||
|
||||
impl Tool for Todowrite {
|
||||
@@ -29,6 +40,17 @@ impl Tool for Todowrite {
|
||||
})
|
||||
}
|
||||
|
||||
/// Append a timestamped, unchecked task line to the session's `todo.md`.
|
||||
///
|
||||
/// Flow: extract `task` argument → build `- [ ] <task> (<timestamp>)`
|
||||
/// line with a UTC `%Y-%m-%d %H:%M:%S` timestamp → open (create if
|
||||
/// missing) `<session_dir>/todo.md` in append mode → write the line.
|
||||
///
|
||||
/// Why: append-only so the file acts as a running log rather than
|
||||
/// requiring the agent to track and rewrite existing content.
|
||||
///
|
||||
/// Return: confirmation string echoing the added task, or an error
|
||||
/// if the `task` argument is missing or the file can't be opened/written.
|
||||
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let task = args.get("task")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
//! Tools for orchestrating multi-agent workflow runs.
|
||||
//!
|
||||
//! Flow: the LLM emits a `workflow_run` tool call with a JSON-encoded
|
||||
//! `WorkflowScript` (Agent/Parallel/Pipeline/Phase primitives) which is
|
||||
//! deserialized and handed to `app::workflow::engine::run_workflow` for
|
||||
//! execution. Sibling agents spawned within the same run can share
|
||||
//! ephemeral text via the `note_finding` tool, which forwards to
|
||||
//! `app::workflow::engine::note_finding`.
|
||||
//!
|
||||
//! Why: decomposing a task into a workflow script lets the harness fan
|
||||
//! out independent subtasks (parallel/pipeline/phased) instead of the
|
||||
//! agent handling everything inline; simple tasks should skip this tool
|
||||
//! entirely per its own description string.
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use anyhow::{Result, anyhow};
|
||||
use super::Tool;
|
||||
use super::ToolCtx;
|
||||
|
||||
/// Tool that parses and executes a JSON-encoded workflow script (Agent/Parallel/Pipeline/Phase).
|
||||
pub struct WorkflowRun;
|
||||
|
||||
impl Tool for WorkflowRun {
|
||||
@@ -31,6 +46,18 @@ impl Tool for WorkflowRun {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the `script`/`args` tool arguments and execute the workflow.
|
||||
///
|
||||
/// Flow: extract `script` string → deserialize into `WorkflowScript` →
|
||||
/// collect optional `args` object into a `HashMap<String, String>` for
|
||||
/// `{{key}}` template substitution → delegate to
|
||||
/// `app::workflow::engine::run_workflow`.
|
||||
///
|
||||
/// Why: template args are silently filtered to string values only
|
||||
/// (non-string values are dropped rather than erroring).
|
||||
///
|
||||
/// Return: the workflow engine's output string, or an error if the
|
||||
/// script argument is missing or fails to parse as JSON.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let script_str = args.get("script")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -53,6 +80,7 @@ impl Tool for WorkflowRun {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that shares a text finding with sibling agents in the current workflow run.
|
||||
pub struct NoteFinding;
|
||||
|
||||
impl Tool for NoteFinding {
|
||||
@@ -77,6 +105,17 @@ impl Tool for NoteFinding {
|
||||
})
|
||||
}
|
||||
|
||||
/// Record `text` as a finding visible to sibling agents in the run.
|
||||
///
|
||||
/// Flow: extract `text` argument → forward to
|
||||
/// `app::workflow::engine::note_finding` → return a truncated
|
||||
/// confirmation echo.
|
||||
///
|
||||
/// Why: findings are ephemeral (not persisted to memory) and are
|
||||
/// meant to be prepended to sibling agents' next tool-round context.
|
||||
///
|
||||
/// Return: confirmation string containing up to the first 80 chars
|
||||
/// of the recorded text.
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let text = args.get("text")
|
||||
.and_then(|v| v.as_str())
|
||||
|
||||
Reference in New Issue
Block a user