Documentation
¶
Overview ¶
Package files is a configurable file toolset an agent host mounts: read, edit, write, grep and list (delete and move opt-in), rooted at one directory. read also does the work of the other two: on a directory it lists, and with grep it searches a file or a tree, so a host can mount read, edit and write alone.
Each enabled tool is an llm.ToolDef plus a branch of one dispatcher. The host adds Defs() to its tool list and wraps its dispatcher with Wrap — the same decorator shape as every other agentkit dispatcher.
Lessons carried over from aw4's file tools, where each one was measured:
- Reads are capped, and a read that names no range is capped harder. The bound is for the context window, not memory: one call must not be able to fill a window the agent has no way to know is full. A capped result says the file's length and the exact range to ask for next.
- Line numbers are the shared coordinate: read prints them, grep reports them, offset takes them, an edit miss quotes them.
- A refusal names what WOULD work: a missing path lists the nearest directory and same-named files elsewhere; an edit miss shows the closest region and the lines that differ; a bad regex is searched literally and the result says so.
- Writes never land inside .git (a hook written there runs on the next commit), and generated files ("Code generated … DO NOT EDIT") are refused because the next generator run overwrites the edit.
stdlib only, like the rest of the module. `rg` and `git` are used when they are on PATH and the package works without either.
Example ¶
A host mounts the toolset next to its own tools: Defs go in the tool list, Wrap decorates the dispatcher.
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/iodesystems/agentkit/files"
"github.com/iodesystems/agentkit/llm"
)
func main() {
root, _ := os.MkdirTemp("", "files-example")
defer func() { _ = os.RemoveAll(root) }()
_ = os.WriteFile(filepath.Join(root, "hello.txt"), []byte("hello\nworld\n"), 0o644)
ts, err := files.New(files.Config{Root: root})
if err != nil {
panic(err)
}
var tools []llm.ToolDef
tools = append(tools, ts.Defs()...)
host := func(ctx context.Context, tc llm.ToolCall) (string, error) {
return "the host's own tools", nil
}
dispatch := ts.Wrap(host)
var tc llm.ToolCall
tc.Function.Name = "file_read"
tc.Function.Arguments = `{"path":"hello.txt"}`
out, _ := dispatch(context.Background(), tc)
fmt.Println(len(tools))
fmt.Println(out)
}
Output: 5 hello.txt: 2 lines 1 hello 2 world
Index ¶
- Constants
- Variables
- func Under(root, rel string) (string, error)
- type Config
- type Dispatcher
- type Toolset
- func (t *Toolset) Bare(name string) string
- func (t *Toolset) Defs() []llm.ToolDef
- func (t *Toolset) Dispatch(ctx context.Context, name, argsJSON string) string
- func (t *Toolset) Handles(name string) bool
- func (t *Toolset) Name(bare string) string
- func (t *Toolset) Names() []string
- func (t *Toolset) Root() string
- func (t *Toolset) Run(ctx context.Context, bare string, a map[string]any) (string, error)
- func (t *Toolset) Wrap(inner Dispatcher) Dispatcher
Examples ¶
Constants ¶
const ( Read = "read" Edit = "edit" Write = "write" Grep = "grep" List = "list" Delete = "delete" Move = "move" )
Tool names, before the prefix.
const ( DefaultReadMax = 64 * 1024 DefaultUnrangedMax = 8 * 1024 DefaultGrepMax = 100 DefaultListMax = 200 )
Default caps. See read.go for the measurements behind the read caps.
const DefaultPrefix = "file_"
DefaultPrefix makes `file_read`, `file_edit`, …
Not `file.`: OpenAI and Anthropic both require function names to match ^[a-zA-Z0-9_-]+$, and a dot is refused by the provider, not by agentkit.
Variables ¶
var ( // ErrEscape: the path resolves outside Root (by `..`, an absolute path, or // a symlink whose target is outside). ErrEscape = errors.New("outside the root") // ErrNotFound: the path does not exist. ErrNotFound = errors.New("does not exist") )
Errors a Refusal hook can test with errors.Is.
DefaultTools is the set enabled when Config.Tools is nil. Delete and move are opt-in: neither is needed to make a change, and both lose work when wrong.
Functions ¶
func Under ¶
Under resolves rel inside root and refuses anything that escapes it — by `..`, by an absolute path elsewhere, or by a symlink whose target is outside. The returned path is the REAL one (symlinks resolved), which is what a write must target: renaming a temp file over a symlink would replace the link.
Symlinks inside root that point inside root are followed. Ones that point out are refused rather than followed (aw4 checked the path lexically only, which a link defeats): an agent's file tool is the one place nobody looks when a write lands outside the tree, and a dependency's source has its own way in through Config.Resolve.
An absolute rel that already lies inside root is accepted: the model is often told the root's absolute path, and refusing the path it was given teaches nothing.
Types ¶
type Config ¶
type Config struct {
// Root is the directory every path is relative to. Required.
Root string
// Tools enables tools by bare name (Read, Edit, …). nil → DefaultTools.
Tools []string
// ReadOnly keeps only read, grep and list, whatever Tools says.
ReadOnly bool
// NamePrefix is prepended to each tool name. "" → DefaultPrefix. It must
// keep names inside [a-zA-Z0-9_-].
NamePrefix string
// Caps. Zero → the Default* value.
ReadMax int // bytes one ranged read returns
UnrangedMax int // bytes a read with no offset/limit returns
GrepMax int // matches one grep returns
ListMax int // entries one list returns
// ExtraIgnore are gitignore-syntax patterns, relative to Root, hidden from
// grep and list on top of .gitignore.
ExtraIgnore []string
// AllowGenerated lets edit/write change files whose header says they are
// generated. Off: such an edit is overwritten by the next generator run.
AllowGenerated bool
// OverwriteUnread lets write replace a file this toolset has not read,
// without the model passing overwrite:true.
OverwriteUnread bool
// NoRipgrep makes grep use the Go engine even when rg is on PATH.
NoRipgrep bool
// Resolve replaces path resolution for path arguments: rel in, absolute
// path out. A host that reads another tree (a branch, a dependency) plugs
// in here. It owns its own safety; Under is exported to fall back on.
Resolve func(rel string) (abs string, err error)
// Refusal builds the text for a path refusal (reason wraps ErrEscape or
// ErrNotFound and carries the default text). nil → reason.Error().
Refusal func(path string, reason error) string
// AfterWrite runs after edit/write/delete/move with the root-relative
// paths changed (both sides of a move).
AfterWrite func(paths []string)
}
Config is one mounted toolset.
type Dispatcher ¶
Dispatcher is agent.ToolDispatcher's shape, restated so this package needs only llm. A value of either type converts to the other.
type Toolset ¶
type Toolset struct {
// contains filtered or unexported fields
}
Toolset is a mounted Config. One per agent session: it tracks which files that session has read, which is what write's overwrite guard consults.
func (*Toolset) Dispatch ¶
Dispatch runs one call by its model-facing name with JSON arguments. The result is always for the model: a refusal starts with "ERROR: ".
func (*Toolset) Run ¶
Run is Dispatch with decoded arguments, returning a model-facing refusal as the error. bare is the tool's bare name.
func (*Toolset) Wrap ¶
func (t *Toolset) Wrap(inner Dispatcher) Dispatcher
Wrap routes this toolset's calls and passes every other call to inner.