Skip to content

Commit 7dd231b

Browse files
authored
Merge pull request #31 from Sewer56/split/task-tool-core-only
Split task-tool: extract core module refactor + docs
2 parents bf3a29d + 137afcc commit 7dd231b

27 files changed

Lines changed: 1058 additions & 286 deletions

src/Cargo.lock

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/llm-coding-tools-core/Cargo.toml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ readme = "README.md"
1010

1111
[features]
1212
default = ["tokio"]
13-
# Base async signatures - requires a runtime, do not enable directly
14-
async = ["dep:async-trait"]
15-
# Async with tokio runtime (default)
13+
# Base async support (enabled by runtimes like tokio). PRs for other runtimes (smol, async-std) are welcome!
14+
async = []
15+
# Async with tokio runtime (default). Enables async feature and tokio-specific implementations.
1616
tokio = ["async", "dep:tokio", "dep:reqwest", "process-wrap/tokio1", "process-wrap/job-object", "process-wrap/process-group", "process-wrap/kill-on-drop"]
1717
# Blocking/sync mode - mutually exclusive with async
1818
blocking = ["maybe-async/is_sync", "dep:reqwest", "reqwest?/blocking", "process-wrap/std", "process-wrap/job-object", "process-wrap/process-group"]
@@ -48,9 +48,6 @@ reqwest = { version = "0.13", default-features = false, features = [
4848
# Unifies async/sync code via procedural macros
4949
maybe-async = "0.2"
5050

51-
# TaskExecutor trait requires async methods
52-
async-trait = { version = "0.1", optional = true }
53-
5451
# Async file I/O, process execution, and timeouts
5552
tokio = { version = "1.49", features = ["fs", "io-util", "process", "time"], optional = true }
5653

@@ -62,3 +59,4 @@ tempfile = "3.24"
6259
# For async tests (when async feature enabled)
6360
tokio = { version = "1.49", features = ["rt", "macros"] }
6461
wiremock = "0.6"
62+
indoc = "2"
Lines changed: 218 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,240 @@
11
# llm-coding-tools-core
22

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.
44

5-
## Overview
5+
`llm-coding-tools-core` provides reviewed, production-grade implementations of common coding-agent tools, plus shared safety, prompt, and policy primitives.
66

7-
This crate provides the foundational building blocks for coding tool implementations:
7+
## Table of contents
88

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)
1414

15-
## Features
15+
## Install
1616

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"
2020

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+
```
2224

23-
Future runtimes (smol, async-std) can be added following the same pattern as `tokio`.
25+
## Feature flags
2426

25-
## Usage
27+
- `tokio` (default): async runtime support
28+
- `blocking`: sync/blocking mode
29+
- `async`: internal base async feature (enabled by runtimes, not directly)
2630

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+
}
3071
```
3172

32-
## Context Module
73+
### Context and wrapper mapping
3374

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.
3676

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`]:
4078

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;
4385
44-
// Non-path tools have a single variant
45-
println!("{}", BASH);
86+
impl ReadTool {
87+
fn new() -> Self {
88+
Self
89+
}
90+
}
4691
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();
50101
```
51102

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+
```
59131

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+
```
61202

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

src/llm-coding-tools-core/examples/system_prompt_preview.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ fn main() {
5555
}
5656

5757
// Mock tools implementing ToolContext for demonstration.
58-
// In real usage, these would be actual tool structs from a framework integration.
58+
// In real usage, these would be actual tool structs from llm-coding-tools-serdesai.
5959

6060
struct MockReadTool;
6161
impl ToolContext for MockReadTool {

src/llm-coding-tools-core/src/context/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ pub const GREP_ALLOWED: &str = include_str!("grep_allowed.txt");
7979

8080
/// Trait for tools that provide usage context for LLM system prompts.
8181
///
82-
/// Implement this trait on tool types to enable automatic system prompt
82+
/// Implement this trait on tool types (for frameworks like serdesAI) to enable automatic system prompt
8383
/// generation via [`SystemPromptBuilder`](crate::SystemPromptBuilder).
8484
///
8585
/// # Example

0 commit comments

Comments
 (0)