# LlmToolkit
Base code tools for agentic LLM execution — a self-contained, composable tool framework for building AI agents in Elixir.
Provides 12 file/shell/web tools, a `ToolResolver` behaviour, composable resolver architecture, session-scoped tool filtering, and an Ecto trace schema for audit logging.
## Tools
| Tool | Purpose |
|---|---|
| `read_file` | Read file contents with offset/limit |
| `write_file` | Write or overwrite a file |
| `edit_file` | Exact-match single edit (pi safety model) |
| `multi_edit` | Multiple edits in one call with rollback |
| `append_to_file` | Append content to a file |
| `bash` | Execute shell commands |
| `grep` | Search file contents (ripgrep) |
| `glob` | Find files by name pattern |
| `list_directory` | List directory contents |
| `tree` | Visual directory tree with sizes |
| `file_info` | File metadata (size, type, mtime) |
| `http_get` | Fetch a URL |
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:llm_toolkit, "~> 0.1"}
]
end
```
## Usage
### Standalone
```elixir
alias LlmToolkit.CodeTools
alias LlmToolkit.Tool.Call
# Use with default cwd (".")
{:ok, content} = CodeTools.resolve(%Call{name: "read_file", arguments: %{"path" => "README.md"}})
# Use with specific cwd
{:ok, content} = CodeTools.resolve(
%Call{name: "read_file", arguments: %{"path" => "README.md"}},
"/path/to/project"
)
```
### With Your Agent Loop
```elixir
# As the sole tool resolver
MyAgent.Loop.run(task, LlmToolkit.CodeTools, opts)
# Via resolver tuple (binds working directory)
MyAgent.Loop.run(task, {LlmToolkit.CodeTools, "/project"}, opts)
```
### Composed with Domain Tools
```elixir
# Base tools + your own tools
resolver = LlmToolkit.Composition.new([
{LlmToolkit.CodeTools, "/project"},
MyApp.DomainTools
])
tools = LlmToolkit.Composition.available_tools(resolver)
{:ok, result} = LlmToolkit.Composition.resolve(resolver, call)
```
### Configurable Resolver with `use AgentResolver`
```elixir
defmodule MyApp.Tools.Resolver do
use LlmToolkit.AgentResolver, tools: [
MyApp.Tools.Search,
MyApp.Tools.Analyze
]
end
# Each tool module implements:
# definition/0 → %LlmToolkit.Tool{}
# execute/2 → (args, context) → {:ok, string} | {:error, string}
# sensitive_fields/0 → ["api_key"] (optional, for telemetry scrubbing)
```
### Session-Scoped Tool Filtering
```elixir
# Prepare only the tools declared for a session turn
{tools, resolver_fn} = LlmToolkit.SessionTools.prepare(
MyApp.Tools.Resolver,
["read_file", "search"],
%{user_id: "abc", project: "/repo"}
)
# resolver_fn is a fresh closure — thread-safe, no process dictionary
{:ok, result} = resolver_fn.(%Call{name: "read_file", arguments: %{"path" => "README.md"}})
```
## Architecture
| Module | Role |
|---|---|
| `LlmToolkit.ToolResolver` | Behaviour — `resolve/1`, `available_tools/0`, optional `dispatch_recipe/1` |
| `LlmToolkit.Tool` | Provider-neutral tool definition (name, description, JSON Schema params) |
| `LlmToolkit.Tool.Call` | An LLM's request to invoke a tool |
| `LlmToolkit.Tool.Result` | The outcome of executing a tool call |
| `LlmToolkit.CodeTools` | The 12 base tools implementing `ToolResolver` |
| `LlmToolkit.AgentResolver` | `use`-based macro — list your tool modules, get a full resolver |
| `LlmToolkit.Composition` | Merge multiple resolvers into one (first match wins) |
| `LlmToolkit.SessionTools` | Filter tools by declaration, build context-bound closures |
| `LlmToolkit.Trace` | Ecto schema for audit logging tool invocations |
## Safety Model
**edit_file:** Uses exact string matching with uniqueness validation. `oldText` must match exactly once. No silent corruption.
**multi_edit:** Transactional — applies edits sequentially in memory. If any edit fails, the file is never written. All-or-nothing.
## Dependencies
- **Req** (~> 0.5) — HTTP client for the `http_get` tool
- **Ecto** (~> 3.12) — Schema/changesets for the `Trace` audit schema
Both are lightweight and runtime-only.