Goa’s tool system provides the agent with interfaces to the filesystem, shell, network, and more. Tools implement the agentic.Tool interface and are registered in a ToolRegistry that wraps the SDK’s registry with documentation support.
// internal/agentic/tool.go
type Tool interface {
Schema() ToolSchema
Execute(input string) (string, error)
}
type ToolSchema struct {
Name string
Description string
Schema map[string]interface{} // JSON Schema
}
Goa extends this with the Documentable interface for self-documenting tools:
// tools/documentable.go
type Documentable interface {
ShortDoc() string
LongDoc() string
Examples() []ToolExample
}
type ToolExample struct {
Description string
Input string
Output string
}
// tools/registry.go
type ToolRegistry struct {
tools map[string]agentic.Tool
docTools map[string]Documentable
}
func NewToolRegistry() *ToolRegistry
func (r *ToolRegistry) Register(tool agentic.Tool)
func (r *ToolRegistry) Get(name string) (agentic.Tool, bool)
func (r *ToolRegistry) All() []agentic.Tool
func (r *ToolRegistry) Schemas() []agentic.ToolSchema
func (r *ToolRegistry) AllDocumented() []DocumentedTool
read — Read file contentsReads files from the project directory (or worktree, if isolated).
A leading @ expands to the current working directory, and a
fuzzy_match fallback finds the closest filename when the exact path does
not exist.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| path | string | Path to file (required) |
| start_line | number | First line to read, 1-indexed (optional) |
| end_line | number | Last line to read, 1-indexed (optional) |
| max_lines | number | Max lines (optional, default 500) |
| max_bytes | number | Max bytes (optional, default 50000) |
Example:
{
"file_path": "src/main.go",
"offset": 0,
"limit": 50
}
write — Write file contentsCreates or overwrites a file. A leading @ expands to the current working
directory, and fuzzy_match can resolve a misspelled filename to an existing
file. In review mode, writes are queued for approval.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| path | string | Path to file (required) |
| content | string | File content (required) |
| create_dirs | boolean | Create parent directories (default: true) |
Example:
{
"file_path": "src/hello.go",
"content": "package main\n\nfunc main() {\n\tprintln(\"hello\")\n}\n"
}
edit — Edit a filePerforms targeted search/replace edits on an existing file. Like read and
write, a leading @ expands to the current working directory and
fuzzy_match resolves misspelled filenames. The search/replace content uses
3-tier fuzzy matching controlled by tools.edit.allow_fuzz_on_edits.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| path | string | Path to file (required) |
| old_string | string | Text to search for (required) |
| new_string | string | Replacement text (required) |
Fuzzy matching (enabled by default via tools.edit.allow_fuzz_on_edits):
| Tier | Strategy | Example |
|——|———-|———|
| 1 | Exact match (after CRLF normalization) | old_string matches byte-for-byte |
| 2 | Trailing whitespace normalized | "func foo() { " matches "func foo() {" |
| 3 | Full fuzzy + auto-reindent | Indentation differences are auto-corrected |
When fuzzy matching is disabled (tools.edit.allow_fuzz_on_edits: false), only
exact match (tier 1) is attempted.
Example:
{
"path": "src/main.go",
"old_string": "fmt.Println(\"hello\")",
"new_string": "fmt.Println(\"world\")"
}
search — Search filesFull-text regex search across the project with concurrent file scanning. Results are ordered by file extension priority (source code first), then by match count per file (most matches first).
Parameters:
| Field | Type | Description |
|——-|——|————-|
| pattern | string | Regex pattern (required) |
| path | string | Root directory (default: project root) |
| glob | string | File glob filter (e.g. "*.go", "**/*.test.js") |
| recursive | boolean | Search subdirectories (default: true) |
| case_sensitive | boolean | Case-sensitive search (default: false) |
| max_results | number | Max results to show (default: 30) |
| context_lines | number | Context lines per match (default: 1) |
| exclude | array | Additional directory exclude patterns |
Glob patterns: Supports ** (match any number of directories),
* (match filename component), and standard filepath.Match patterns.
Fuzzy fallback: When a pattern contains | and returns no results,
the tool automatically splits on | and searches each term separately.
Priority ordering: Built-in search_priority.json maps file extensions
to priority tiers. Source code (.go, .py, .rs) sorts first, then config
files (.json, .yaml), then data/doc files (.md, .csv, .html), then media.
Users can override via ~/.goa/search_priority.json.
Example:
{
"pattern": "func.*Handler",
"glob": "*.go",
"max_results": 10
}
bash — Execute shell commandsRuns shell commands with security controls (blocked/allowed command filtering, env masking, output truncation, and an optional project-directory jail).
Parameters:
| Field | Type | Description |
|——-|——|————-|
| command | string | Shell command (required) |
| timeout | number | Timeout in seconds (default: 60, max: 300) |
| workdir | string | Working directory (default: project root) |
| env | object | Extra environment variables (optional) |
Security:
blocked_commands — Never execute these (configurable)allowed_commands — Whitelist (empty = allow all except blocked)env_mask_patterns — Mask sensitive values in outputjail — In SOLO mode, reject commands that escape the project directoryOutput: Long output is truncated to the tail and a truncation notice is included. The full output is saved to a temp file when truncated.
Tool output compression reduces verbose command output into a compact form before returning it to the agent. This saves tokens and is especially beneficial for local models with tighter context windows.
| Command | Compression | Example |
|---|---|---|
ls -la |
Strips permissions/owner/group/size — filenames only | file.go vs -rw-r--r-- user group 1024 Jan 1 file.go |
git status |
One line per changed file | M file.go vs full porcelain |
git diff |
Condensed per-file diff with only changed lines | --- file.go + changed lines |
git log |
Deduplicated, author email stripped | feat: add foo vs full commit metadata |
grep / rg |
Grouped by file, long lines truncated at 200 chars | file.go: func foo() |
cat / head / tail |
Line-numbered output | ` 1 package main` |
go test |
PASS lines stripped, stack traces compressed, pass/fail summary | 2 passed, 0 failed |
Compression is disabled by default for cloud providers. For local providers (LM Studio, Ollama, and any provider with a localhost endpoint) it is enabled by default since local models benefit most from token savings.
You can override the default in three ways:
1. Per-model (highest precedence) — add compress_output to a model definition:
models:
- id: my-local-model
model: qwen/qwen3.5-9b
provider: lmstudio
compress_output: true # enable for this model
2. Global config — set tools.bash.compress_output:
tools:
bash:
compress_output: false # disable globally
3. Provider auto-detect (default) — local providers (lm-studio, ollama, endpoints with localhost/127.0.0.1) get compression enabled automatically; remote providers get it disabled.
Example:
{
"command": "go build ./...",
"timeout": 60
}
ssh_bash — Execute commands on remote hostsRuns shell commands on SSH hosts using the system ssh binary.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| host | string | Host ID from config (required) |
| command | string | Command to run (required) |
| timeout | number | Timeout in seconds (optional) |
Example:
{
"host": "server1",
"command": "systemctl status app",
"timeout": 30
}
bg_exec — Background process executionStarts a background process with pipe I/O (stdin/stdout/stderr).
Parameters:
| Field | Type | Description |
|——-|——|————-|
| command | string | Command to run (required) |
| args | array | Command arguments (optional) |
| stdin | string | Stdin content (optional) |
| timeout | number | Timeout in seconds (optional) |
Example:
{
"command": "npm",
"args": ["test"],
"timeout": 120
}
memento — Read/write thinking artifactsPersists agent thoughts as markdown files in .goa/memory/. The agent can
also read the full content of memory files via read when memory summaries
are injected into the system prompt.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| action | string | read | write | append | list | delete (required) |
| name | string | Memory name (required for read/write/append/delete) |
| content | string | Content (required for write/append) |
Example:
{
"action": "write",
"name": "architecture-notes",
"content": "## Database Schema\n\nThe users table needs a unique constraint on email."
}
goa_command — Execute Goa commands from LLMAllows the LLM to invoke Goa commands programmatically.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| command | string | Command string (e.g., mode confirm) (required) |
Example:
{
"command": "mode confirm"
}
ask_user_question — Ask clarifying questionsLets the LLM ask the user one or more clarifying questions when requirements
are ambiguous. Each question is shown as a card in the conversation
(title / summary / question / numbered options) and answered through the
main input line; the card never captures input. Registered by default;
disable with tools.enabled.clarify_disabled: true.
Parameters:
| Field | Type | Description |
|——-|——|————-|
| questions | array | One or more questions; each asked separately (required) |
| question | string | The question (required) |
| title | string | Short label for the card / input title |
| summary | string | Optional context |
| options | string[] | Up to 6 choices (type number or text) |
| required | bool | If true, cancellation is an error |
| allow_free_text | bool | If false with options, restrict to listed options |
Example:
{
"questions": [
{
"title": "Target branch",
"summary": "Two release branches are active",
"question": "Which branch should I target?",
"options": ["main", "release-2.x"]
}
]
}
To make a tool self-documenting, implement the Documentable interface:
type MyTool struct{}
func (t *MyTool) Schema() agentic.ToolSchema { ... }
func (t *MyTool) Execute(input string) (string, error) { ... }
// Documentable interface
func (t *MyTool) ShortDoc() string { return "Does something useful" }
func (t *MyTool) LongDoc() string { return "Detailed explanation..." }
func (t *MyTool) Examples() []ToolExample {
return []ToolExample{
{Description: "Basic usage", Input: `{"key": "value"}`, Output: "result"},
}
}
Tools are registered in main.go:
func registerTools(reg *tools.ToolRegistry, wm *internal.WorktreeManager, projectDir string, cfg *config.Config) {
gitStager := tools.NewGitStager(projectDir)
reg.Register(&tools.ReadFileTool{WorktreeMgr: wm})
reg.Register(&tools.WriteFileTool{WorktreeMgr: wm, ProjectDir: projectDir, GitStager: gitStager})
reg.Register(&tools.EditFileTool{WorktreeMgr: wm, ProjectDir: projectDir, GitStager: gitStager})
reg.Register(&tools.SearchTool{WorktreeMgr: wm, Threads: cfg.Tools.Search.Threads, ...})
reg.Register(&tools.BashTool{WorktreeMgr: wm, Blocked: cfg.Tools.Bash.BlockedCommands, ...})
reg.Register(&tools.SSHBashTool{Hosts: sshHosts(cfg)})
reg.Register(tools.NewBGExecTool())
reg.Register(&tools.MementoTool{ProjectDir: projectDir, GlobalDir: cfg.ConfigDir})
}
Goa supports concurrent execution of non-conflicting tool calls. When the LLM issues multiple tool calls in a single turn, tools with independent resource accesses run in parallel, while conflicting tools are serialized.
Each tool implements the Accessor interface to declare what resources it
accesses:
type Accessor interface {
Access(input string) toolaccess.Access
}
type Access struct {
ReadPaths []string // file paths this tool reads
WritePaths []string // file paths this tool writes to
Category string // "shell", "network", "memory" for broad conflict
}
| Scenario | Executes |
|---|---|
| Two reads on different files | ✅ Parallel |
| Two reads on the same file | ✅ Parallel (reads are safe) |
| Read + write on different files | ✅ Parallel |
| Read + write on the same file | ❌ Serialized |
| Two writes on the same file | ❌ Serialized |
| Two shell commands | ❌ Serialized (same category) |
| Shell + file read | ✅ Parallel (different categories) |
The ToolScheduler dispatches non-conflicting tools in goroutines and queues
conflicting ones until their dependencies complete. Results are returned in
provider order (the order the LLM issued them), regardless of completion order.
This is transparent to the LLM — it sees the same tool result sequence as sequential execution, but with lower latency for multi-tool turns. ```