files

package
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 22 Imported by: 0

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.

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

Examples

Constants

View Source
const (
	Read   = "read"
	Edit   = "edit"
	Write  = "write"
	Grep   = "grep"
	List   = "list"
	Delete = "delete"
	Move   = "move"
)

Tool names, before the prefix.

View Source
const (
	DefaultReadMax     = 64 * 1024
	DefaultUnrangedMax = 8 * 1024
	DefaultGrepMax     = 100
	DefaultListMax     = 200
)

Default caps. See read.go for the measurements behind the read caps.

View Source
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

View Source
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.

View Source
var DefaultTools = []string{Read, Edit, Write, Grep, List}

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

func Under(root, rel string) (string, error)

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

type Dispatcher = func(ctx context.Context, tc llm.ToolCall) (string, error)

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 New

func New(cfg Config) (*Toolset, error)

New mounts cfg.

func (*Toolset) Bare

func (t *Toolset) Bare(name string) string

Bare maps a model-facing name back to its bare tool name, "" if not ours.

func (*Toolset) Defs

func (t *Toolset) Defs() []llm.ToolDef

Defs are the tool definitions for the enabled tools.

func (*Toolset) Dispatch

func (t *Toolset) Dispatch(ctx context.Context, name, argsJSON string) string

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

func (t *Toolset) Handles(name string) bool

Handles reports whether name is one of this toolset's enabled tools.

func (*Toolset) Name

func (t *Toolset) Name(bare string) string

Name is the model-facing name of a bare tool name ("read" → "file_read").

func (*Toolset) Names

func (t *Toolset) Names() []string

Names are the model-facing names of the enabled tools, in a fixed order.

func (*Toolset) Root

func (t *Toolset) Root() string

Root is the absolute root, symlinks resolved.

func (*Toolset) Run

func (t *Toolset) Run(ctx context.Context, bare string, a map[string]any) (string, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL