|
1 | 1 | # llm-coding-tools-core |
2 | 2 |
|
3 | | -Lightweight, high-performance core types and utilities for coding tools - framework agnostic. |
| 3 | +Framework-agnostic core library of standard tools used by coding agents - headless, TUI, or anything in between. |
4 | 4 |
|
5 | | -## Overview |
| 5 | +`llm-coding-tools-core` provides reviewed, production-grade implementations of common coding-agent tools, plus shared safety, prompt, and policy primitives. |
6 | 6 |
|
7 | | -This crate provides the foundational building blocks for coding tool implementations: |
| 7 | +## Table of contents |
8 | 8 |
|
9 | | -- `ToolError` - Unified error type for all tool operations |
10 | | -- `ToolResult<T>` - Result type alias using ToolError |
11 | | -- `ToolOutput` - Wrapper for tool responses with truncation metadata |
12 | | -- Utility functions for text processing and formatting |
13 | | -- `context` module - LLM guidance strings for tool usage |
| 9 | +- [Install](#install) |
| 10 | +- [Feature flags](#feature-flags) |
| 11 | +- [Tools, context, and integration](#tools-context-and-integration) |
| 12 | +- [System prompt builder](#system-prompt-builder) |
| 13 | +- [Permissions](#permissions) |
14 | 14 |
|
15 | | -## Features |
| 15 | +## Install |
16 | 16 |
|
17 | | -- `tokio` (default): Async mode with tokio runtime. Enables async function signatures. |
18 | | -- `blocking`: Sync/blocking mode. Mutually exclusive with `tokio`/`async`. |
19 | | -- `async`: Base async signatures (internal). Requires a runtime; use `tokio` instead. |
| 17 | +```toml |
| 18 | +# Async (default) |
| 19 | +llm-coding-tools-core = "0.2" |
20 | 20 |
|
21 | | -The `async` and `blocking` features are mutually exclusive - enabling both causes a compile error. |
| 21 | +# Sync/blocking |
| 22 | +llm-coding-tools-core = { version = "0.2", default-features = false, features = ["blocking"] } |
| 23 | +``` |
22 | 24 |
|
23 | | -Future runtimes (smol, async-std) can be added following the same pattern as `tokio`. |
| 25 | +## Feature flags |
24 | 26 |
|
25 | | -## Usage |
| 27 | +- `tokio` (default): async runtime support |
| 28 | +- `blocking`: sync/blocking mode |
| 29 | +- `async`: internal base async feature (enabled by runtimes, not directly) |
26 | 30 |
|
27 | | -```rust |
28 | | -use llm_coding_tools_core::{ToolError, ToolResult, ToolOutput}; |
29 | | -use llm_coding_tools_core::util::{truncate_text, format_numbered_line}; |
| 31 | +`tokio` and `blocking` are mutually exclusive. |
| 32 | + |
| 33 | +## Tools, context, and integration |
| 34 | + |
| 35 | +Canonical tool names are defined in [`tool_names`] ([`Read`], [`Write`], [`Edit`], [`Glob`], [`Grep`], [`Bash`], [`WebFetch`], [`TodoRead`], [`TodoWrite`], [`Task`]). |
| 36 | + |
| 37 | +### Standard tools |
| 38 | + |
| 39 | +- [`Read`] ([`read_file`]) - Read a file window (`offset`/`limit`) with const-generic line numbers (`read_file::<_, true>` or `read_file::<_, false>`). |
| 40 | +- [`Write`] ([`write_file`]) - Create or overwrite a file at a resolved path. |
| 41 | +- [`Edit`] ([`edit_file`]) - Apply exact text replacements with structured edit errors. |
| 42 | +- [`Glob`] ([`glob_files`]) - Match filesystem paths by glob pattern. |
| 43 | +- [`Grep`] ([`grep_search`]) - Search file contents by regex with match metadata. |
| 44 | +- [`Bash`] ([`execute_command`]) - Execute shell commands with timeout and captured output. |
| 45 | +- [`WebFetch`] ([`fetch_url`]) - Fetch URL content as text, markdown, or html (requires `tokio` or `blocking`). |
| 46 | +- [`TodoRead`] ([`read_todos`]) - Read shared todo state. |
| 47 | +- [`TodoWrite`] ([`write_todos`]) - Write and validate shared todo state. |
| 48 | +- [`Task`] ([`TaskInput`], [`TaskOutput`]) - Standard task payload types used by delegation wrappers. |
| 49 | + |
| 50 | +### Path safety and sandboxing |
| 51 | + |
| 52 | +Path-based tools are generic over [`PathResolver`], so wrappers can choose unrestricted access or sandboxed access. |
| 53 | + |
| 54 | +- [`AbsolutePathResolver`] enforces absolute-path inputs (unrestricted mode). |
| 55 | +- [`AllowedPathResolver`] constrains operations to configured directories (sandbox mode). |
| 56 | +- Failed resolution rejects traversal and out-of-sandbox paths before tool execution. |
| 57 | + |
| 58 | +```rust,no_run |
| 59 | +use llm_coding_tools_core::{AbsolutePathResolver, AllowedPathResolver, PathResolver, ToolResult}; |
| 60 | +
|
| 61 | +fn demo() -> ToolResult<()> { |
| 62 | + // Unrestricted mode: any absolute path is allowed. |
| 63 | + let any_path = AbsolutePathResolver; |
| 64 | + let _hosts = any_path.resolve("/etc/hosts")?; |
| 65 | +
|
| 66 | + // Sandboxed mode: only configured directories are allowed. |
| 67 | + let sandbox = AllowedPathResolver::new(["/workspace/project", "/tmp"])?; |
| 68 | + let _lib = sandbox.resolve("src/lib.rs")?; |
| 69 | + Ok(()) |
| 70 | +} |
30 | 71 | ``` |
31 | 72 |
|
32 | | -## Context Module |
| 73 | +### Context and wrapper mapping |
33 | 74 |
|
34 | | -The `context` module provides embedded strings containing usage guidance for LLM agents. |
35 | | -These can be appended to tool descriptions or system prompts. |
| 75 | +[`context`] provides reusable guidance constants. |
36 | 76 |
|
37 | | -Path-based tools have two variants: |
38 | | -- `*_ABSOLUTE`: For unrestricted filesystem access (absolute paths required) |
39 | | -- `*_ALLOWED`: For sandboxed access (paths relative to allowed directories) |
| 77 | +Wrappers usually bind a tool's canonical name and guidance through [`ToolContext`]: |
40 | 78 |
|
41 | | -```rust |
42 | | -use llm_coding_tools_core::context::{BASH, READ_ABSOLUTE, READ_ALLOWED}; |
| 79 | +Any-path read tool: |
| 80 | + |
| 81 | +```rust,no_run |
| 82 | +use llm_coding_tools_core::{ToolContext, context, tool_names}; |
| 83 | +
|
| 84 | +struct ReadTool; |
43 | 85 |
|
44 | | -// Non-path tools have a single variant |
45 | | -println!("{}", BASH); |
| 86 | +impl ReadTool { |
| 87 | + fn new() -> Self { |
| 88 | + Self |
| 89 | + } |
| 90 | +} |
46 | 91 |
|
47 | | -// Path-based tools have absolute and allowed variants |
48 | | -println!("{}", READ_ABSOLUTE); |
49 | | -println!("{}", READ_ALLOWED); |
| 92 | +impl ToolContext for ReadTool { |
| 93 | + const NAME: &'static str = tool_names::READ; |
| 94 | +
|
| 95 | + fn context(&self) -> &'static str { |
| 96 | + context::READ_ABSOLUTE |
| 97 | + } |
| 98 | +} |
| 99 | +
|
| 100 | +let _tool = ReadTool::new(); |
50 | 101 | ``` |
51 | 102 |
|
52 | | -Available context strings: |
53 | | -- `BASH`, `TASK`, `TODO_READ`, `TODO_WRITE`, `WEBFETCH` - standalone tools |
54 | | -- `READ_ABSOLUTE`, `READ_ALLOWED` - file reading |
55 | | -- `WRITE_ABSOLUTE`, `WRITE_ALLOWED` - file writing |
56 | | -- `EDIT_ABSOLUTE`, `EDIT_ALLOWED` - file editing |
57 | | -- `GLOB_ABSOLUTE`, `GLOB_ALLOWED` - pattern matching |
58 | | -- `GREP_ABSOLUTE`, `GREP_ALLOWED` - content search |
| 103 | +Sandboxed read tool: |
| 104 | + |
| 105 | +```rust,no_run |
| 106 | +use llm_coding_tools_core::{AllowedPathResolver, ToolContext, context, tool_names}; |
| 107 | +
|
| 108 | +struct ReadTool { |
| 109 | + _resolver: AllowedPathResolver, |
| 110 | +} |
| 111 | +
|
| 112 | +impl ReadTool { |
| 113 | + fn new(resolver: AllowedPathResolver) -> Self { |
| 114 | + Self { |
| 115 | + _resolver: resolver, |
| 116 | + } |
| 117 | + } |
| 118 | +} |
| 119 | +
|
| 120 | +impl ToolContext for ReadTool { |
| 121 | + const NAME: &'static str = tool_names::READ; |
| 122 | +
|
| 123 | + fn context(&self) -> &'static str { |
| 124 | + context::READ_ALLOWED |
| 125 | + } |
| 126 | +} |
| 127 | +
|
| 128 | +let resolver = AllowedPathResolver::new(["/workspace/project"]).expect("valid allowed path"); |
| 129 | +let _tool = ReadTool::new(resolver); |
| 130 | +``` |
59 | 131 |
|
60 | | -## Design Principles |
| 132 | +Core tool functions are generic over [`PathResolver`], but wrappers usually expose separate absolute/allowed tool types for simpler ergonomics (to avoid extra generic parameters). |
| 133 | + |
| 134 | +This keeps registration name (`Read`) and prompt guidance in sync. |
| 135 | + |
| 136 | +## System prompt builder |
| 137 | + |
| 138 | +[`SystemPromptBuilder`] builds one prompt string for agent runtimes. |
| 139 | + |
| 140 | +- [`track(&mut self, tool: T)`] records tool guidance and returns the tool unchanged. |
| 141 | +- [`working_directory(self, path)`] and [`allowed_paths(self, resolver)`] add environment metadata. |
| 142 | +- [`add_context(self, name, context)`] appends supplemental sections (for example `GIT_WORKFLOW`). |
| 143 | +- [`system_prompt(self, prompt)`] prepends custom instructions; [`build(self)`] renders the final prompt. |
| 144 | + |
| 145 | +You usually build framework wrappers from these primitives (`ToolContext` + `SystemPromptBuilder`). |
| 146 | + |
| 147 | +### Typical wrapper integration (serdesAI) |
| 148 | + |
| 149 | +For example with `llm-coding-tools-serdesai`, wrappers are built from these primitives. |
| 150 | + |
| 151 | +```rust,no_run |
| 152 | +# #[cfg(any())] |
| 153 | +# { |
| 154 | +use llm_coding_tools_serdesai::absolute::{GlobTool, GrepTool, ReadTool}; |
| 155 | +use llm_coding_tools_serdesai::{BashTool, SystemPromptBuilder}; |
| 156 | +use serdes_ai::prelude::*; |
| 157 | +
|
| 158 | +let mut pb = SystemPromptBuilder::new() |
| 159 | + .working_directory(std::env::current_dir()?.display().to_string()); |
| 160 | +
|
| 161 | +let agent = AgentBuilder::<(), String>::new(model) |
| 162 | + .tool(pb.track(ReadTool::<true>::new())) |
| 163 | + .tool(pb.track(GlobTool::new())) |
| 164 | + .tool(pb.track(GrepTool::<true>::new())) |
| 165 | + .tool(pb.track(BashTool::new())) |
| 166 | + .system_prompt(pb.build()) |
| 167 | + .build(); |
| 168 | +# } |
| 169 | +``` |
| 170 | + |
| 171 | +## Permissions |
| 172 | + |
| 173 | +[`permissions`] provides ordered allow/deny rules for tool access and delegation. |
| 174 | + |
| 175 | +- [`Rule`] stores `(permission_key, subject_pattern, action)`. |
| 176 | +- [`Ruleset`] uses last-match-wins; no match defaults to [`PermissionAction::Deny`]. |
| 177 | +- Permission keys are exact-match; wildcard matching (`*`, `?`) applies to subject patterns. |
| 178 | + |
| 179 | +Frontmatter-style config is typically translated into this model: |
| 180 | + |
| 181 | +```yaml |
| 182 | +permission: |
| 183 | + bash: allow |
| 184 | + task: |
| 185 | + orchestrator-*: allow |
| 186 | + "*": deny |
| 187 | +``` |
| 188 | +
|
| 189 | +With last-match-wins, the final `"*": deny` rule overrides earlier `task` matches. |
| 190 | + |
| 191 | +```rust |
| 192 | +use llm_coding_tools_core::permissions::{PermissionAction, Rule, Ruleset}; |
| 193 | +
|
| 194 | +let mut rules = Ruleset::new(); |
| 195 | +rules.push(Rule::new("bash", "*", PermissionAction::Allow)); |
| 196 | +rules.push(Rule::new("task", "orchestrator-*", PermissionAction::Allow)); |
| 197 | +rules.push(Rule::new("task", "*", PermissionAction::Deny)); |
| 198 | +
|
| 199 | +assert_eq!(rules.evaluate("bash", "any-agent"), PermissionAction::Allow); |
| 200 | +assert_eq!(rules.evaluate("task", "orchestrator-review"), PermissionAction::Deny); // last-match-wins |
| 201 | +``` |
61 | 202 |
|
62 | | -- No framework-specific dependencies, plug and play into any LLM framework/library |
63 | | -- Minimal dependency footprint |
64 | | -- Performance-oriented (optimized) with zero-cost abstractions |
| 203 | +[`tool_names`]: crate::tool_names |
| 204 | +[`Read`]: crate::tool_names::READ |
| 205 | +[`Write`]: crate::tool_names::WRITE |
| 206 | +[`Edit`]: crate::tool_names::EDIT |
| 207 | +[`Glob`]: crate::tool_names::GLOB |
| 208 | +[`Grep`]: crate::tool_names::GREP |
| 209 | +[`Bash`]: crate::tool_names::BASH |
| 210 | +[`WebFetch`]: crate::tool_names::WEBFETCH |
| 211 | +[`TodoRead`]: crate::tool_names::TODO_READ |
| 212 | +[`TodoWrite`]: crate::tool_names::TODO_WRITE |
| 213 | +[`Task`]: crate::tool_names::TASK |
| 214 | +[`read_file`]: crate::read_file |
| 215 | +[`write_file`]: crate::write_file |
| 216 | +[`edit_file`]: crate::edit_file |
| 217 | +[`glob_files`]: crate::glob_files |
| 218 | +[`grep_search`]: crate::grep_search |
| 219 | +[`execute_command`]: crate::execute_command |
| 220 | +[`fetch_url`]: crate::fetch_url |
| 221 | +[`read_todos`]: crate::read_todos |
| 222 | +[`write_todos`]: crate::write_todos |
| 223 | +[`TaskInput`]: crate::TaskInput |
| 224 | +[`TaskOutput`]: crate::TaskOutput |
| 225 | +[`SystemPromptBuilder`]: crate::SystemPromptBuilder |
| 226 | +[`track(&mut self, tool: T)`]: crate::SystemPromptBuilder::track |
| 227 | +[`working_directory(self, path)`]: crate::SystemPromptBuilder::working_directory |
| 228 | +[`allowed_paths(self, resolver)`]: crate::SystemPromptBuilder::allowed_paths |
| 229 | +[`add_context(self, name, context)`]: crate::SystemPromptBuilder::add_context |
| 230 | +[`system_prompt(self, prompt)`]: crate::SystemPromptBuilder::system_prompt |
| 231 | +[`build(self)`]: crate::SystemPromptBuilder::build |
| 232 | +[`context`]: crate::context |
| 233 | +[`ToolContext`]: crate::context::ToolContext |
| 234 | +[`PathResolver`]: crate::PathResolver |
| 235 | +[`AbsolutePathResolver`]: crate::AbsolutePathResolver |
| 236 | +[`AllowedPathResolver`]: crate::AllowedPathResolver |
| 237 | +[`permissions`]: crate::permissions |
| 238 | +[`Rule`]: crate::permissions::Rule |
| 239 | +[`Ruleset`]: crate::permissions::Ruleset |
| 240 | +[`PermissionAction::Deny`]: crate::permissions::PermissionAction::Deny |
0 commit comments