Repository navigation
Tools
A tools.Registry holds tool definitions and their handlers; the agent offers registry.Definitions() to the model and runs registry.Dispatch for every call. The built-in file tools and the plan tool are registered into the same registry.
From tools/example_test.go (ExampleRegistry):
registry := tools.NewRegistry()
err := registry.Register(llm.ToolDefinition{
Name: "greet",
Description: "Greet a person by name.",
Parameters: tools.Object(map[string]any{"name": tools.String("Who to greet.")}, "name"),
}, func(_ context.Context, arguments json.RawMessage, _ tools.Environment) (tools.Result, error) {
var parsed struct {
Name string `json:"name"`
}
if err := json.Unmarshal(arguments, &parsed); err != nil {
return tools.Result{}, fmt.Errorf("greet arguments: %w", err)
}
return tools.Result{Content: "Hello, " + parsed.Name + "!"}, nil
})-
Registerfails for an empty name, a nil handler, or a taken name (tools.ErrDuplicateTool).Definitionsreturns the tools in registration order;Has(name)checks one. - A
tools.Handlerisfunc(ctx, arguments json.RawMessage, environment tools.Environment) (tools.Result, error).Result.Contentis JSON-encoded for the model (nil isnull). A returned error is shown to the model as the tool's failure, so write it for the model: what was wrong and what to do instead. -
tools.Environmentcarries what changes per call:SessionIDandTurnID(one UUID per model reply). Everything else (storage, clock, database) the handler captures by closure when you register it (ADR-021). - Honour
ctx: it carries the session'sToolCallTimeoutas a deadline.
Schemas are explicit JSON Schema: tools.Object(properties, required...) with tools.String, tools.Integer, tools.Number, tools.Boolean, tools.Enum(description, values...) and tools.Array(items, description). Output is deterministic (keys sorted). Object panics on a value encoding/json cannot encode, since schemas are literals written at registration. There is no reflection helper; struct-driven schemas come from github.com/invopop/jsonschema (json.Marshal(jsonschema.Reflect(&Arguments{}))) passed as Parameters.
Before the handler runs, Dispatch checks the arguments: empty arguments count as {}, anything else must be a JSON object (a bare null is refused) holding every field the schema lists as required (a JSON null counts as missing). Only the top-level required list is enforced; types, enums and nested objects are not, so the handler still parses and validates. A Parameters value that is not an object schema enforces nothing.
From ExampleRegistry_Dispatch:
_, err = registry.Dispatch(context.Background(), llm.ToolCall{Name: "divide", Arguments: `{}`}, tools.Environment{})
fmt.Println(errors.Is(err, tools.ErrInvalidArguments))
_, err = registry.Dispatch(context.Background(), llm.ToolCall{Name: "divide", Arguments: `{"by":0}`}, tools.Environment{})
fmt.Println(errors.Is(err, tools.ErrToolPanicked))
_, err = registry.Dispatch(context.Background(), llm.ToolCall{Name: "multiply"}, tools.Environment{})
fmt.Println(errors.Is(err, tools.ErrUnknownTool))| Error | When | Message |
|---|---|---|
tools.ErrUnknownTool |
no tool of that name | names the tool |
tools.ErrInvalidArguments |
not a JSON object, or a required field missing | names the tool and the field |
tools.ErrToolPanicked |
the handler panicked | names the tool and the panic value |
In an agent run all three are tool errors: the model sees them and the run continues.
agent.RegisterVFSTools(registry, &agent.VFSToolOptions{Storage: storage}) registers tools over any vfs.Storage (Files). With every option at its default the registry holds, in this order: read_file, list_files, write_file, delete_file, grep, glob, edit_file, multi_edit, move_file, and search when the storage is vfs.Searchable.
| Tool | Parameters (required in bold) | Result and rules |
|---|---|---|
read_file |
path, offset (1-based line), limit (lines, default 2000), column (1-based byte in the offset line) |
content with every line prefixed by its number and a tab, plus content_type, size, offset, total_lines, truncated. Stops at 2000 lines or at 40 KiB of content as JSON-encoded (escapes counted), so the result always fits the default 64 KiB tool result cap; continue at the next line number. A line too long for one call is cut and ends in a notice [line N is cut here; read on with offset N and column C]. An offset past the end, or a column past the end of its line, is an error. The whole file is read, whatever the window (the Storage has no ranged read; Security) |
list_files |
none |
files: path, size, updated_at, sorted by path |
write_file |
path, content, content_type
|
creates or replaces; the content type defaults from the extension, text/markdown for unknown ones |
delete_file |
path |
fails for a missing file |
grep |
pattern (Go RE2), path_glob, case_insensitive, context (0-5), output_mode (content, files_with_matches, count) |
path:line:text matches, path-line-text context; at most 200 entries, lines cut at 500 characters, a scan stops before a file that would pass 32 MiB read; truncated says so. Binary files are skipped, and so are files listed larger than MaximumFileBytes (unread, truncated is then true). Each scanned file is read whole. $ anchors at the end of each line |
glob |
pattern (* in a segment, ** across, ?, {a,b}) |
matching paths, sorted, at most 500 |
edit_file |
path, old_string, new_string, replace_all
|
exact replacement; old_string must start at exactly one position unless replace_all (overlapping matches, aa in aaa, are ambiguous too), and differ from new_string. Without the line-number prefix of read_file
|
multi_edit |
path, edits (array of old_string, new_string, replace_all) |
the edits apply in order, each on the previous result; all or nothing. Every edit needs old_string and new_string (an explicit "" deletes, a missing or null one is an argument error); an edit that would grow the file past MaximumFileBytes fails before it is applied |
move_file |
source, destination, overwrite
|
writes the destination, then deletes the source (the Storage has no rename; a failed delete leaves both and says so). Fails on an existing destination unless overwrite. A rename that only changes case (Readme.md to README.md) goes through a hidden staging file in the same directory, so it is safe on case-insensitive disks (macOS, Windows); its cleanup after a failed step ignores cancellation but gives up after 30 s per step. The destination gets the store's default mode, so a 0600 file comes out 0644 on vfs/osfs (Security) |
search |
query |
paths matching the backend's query syntax; the description embeds SearchSyntax()
|
Options:
VFSToolOptions |
Effect |
|---|---|
Storage |
required; multi-tenant callers pass a store already bound with Scope
|
ReadOnly |
drops write_file, delete_file, edit_file, multi_edit, move_file
|
DisableExtended |
drops grep, glob, edit_file, multi_edit, move_file: the v0.3 set |
HideHiddenPaths |
paths with a segment starting with _ or . disappear from listings and are refused as arguments |
AllowedExtensions |
e.g. []string{".md"}, case-insensitive; other files disappear from listings and are refused as arguments of every tool, reads included. Nil allows all |
MaximumFileBytes |
cap for written (and moved) files, default 1 MiB (vfs.ErrFileTooLarge) |
From ExampleRegisterVFSTools, a read-only set over a searchable store:
err := agent.RegisterVFSTools(registry, &agent.VFSToolOptions{
Storage: vfsmemory.New(),
ReadOnly: true,
})registers read_file, list_files, grep, glob, search.
Every path argument goes through vfs.ValidatePath and the policy above before the storage is called. Argument errors match tools.ErrInvalidArguments and name the tool and the argument (... for read_file: argument "offset" must be an integer, got string), so the model can correct the call.
agent.RegisterPlanTool(registry) adds todo_write: the model sends the whole task list (todos: array of content, status pending, in_progress or completed) and gets it back validated as {"todos": [...]}. Each call replaces the previous list. The list lives in the conversation only, so it needs no storage; show progress from EventToolResult. It is not registered by RegisterVFSTools.
A host that filters or audits tool names must know the full default set above. To keep the v0.3 set (read_file, write_file, list_files, delete_file, search) set DisableExtended: true.
Mirror of docs/wiki at v1.2.0 (dd94d84). Edit the repository, not this wiki.