Tool System

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.

Tool Interface

// 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
}

Tool Registry

// 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

Some tools are deferred: they stay registered and executable, but their call schemas are withheld from every request to save context. Deferral activates when the session's tool set contains the tool_search loader and at least 8 deferred-eligible tools; otherwise all schemas ship eagerly.

The model discovers deferred tools from two surfaces:

  • The system prompt <deferred_tools> block lists every withheld tool with a one-line description, plus the load instruction.
  • The tool_search schema itself carries a compact name-only list (fallback for sub-agents that do not receive goa's app system prompt).

Loading is explicit and on demand:

tool_search {"query": "select:lsp,verify"}   // load exact tools
tool_search {"query": "language server"}     // discovery only (no load)

select: returns the full schemas and exposes the tools on the next round; calling an unloaded deferred tool is rejected with an error naming the exact select: remedy. Runtime toggles (/tools:*, /config, MCP connect / disconnect, plugin load/unload) are announced to the model as one batched [goa-tools] user message listing what was enabled/disabled.

Tool Reference

read — Read file contents

Reads 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 contents

Creates 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 file

Performs 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 files

Full-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 commands

Runs 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 output
  • jail — In SOLO mode, reject commands that escape the project directory

Output: Long output is truncated to the tail and a truncation notice is included. The full output is saved to a temp file when truncated.

Output Compression

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
}

python — Execute Python code in an embedded interpreter

Runs Python code using the embedded gpython interpreter. Each call gets a fresh isolated interpreter context with stdout and stderr captured. Supports Python 3.4 language subset and stdlib modules provided by gpython.

Deferred: like other opt-in-heavy tools, the python schema loads on demand via tool_search select:python (see "Deferred loading" above); it stays registered and executable once loaded.

Parameters:

Field Type Description
code string Python code to execute (required)
timeout number Timeout in seconds (default: 60, max: 300)

Security:

  • Runs in-process with an isolated gpython context per call
  • No filesystem or network access unless the Python code explicitly uses available stdlib modules
  • Disable via /tools python:off or /config → Tools

Example:

{
  "code": "print(sum(range(10)))"
}

ssh_bash — Execute commands on remote hosts

Runs 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 execution

Starts 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 artifacts

Persists 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
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 LLM

Allows 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 questions

Lets 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. Not registered in headless mode (goa -p, --goal, --orchestrate) — there is no user at the input line to answer.

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"]
    }
  ]
}

verify — Run project test suite

Runs the project's test suite and returns a structured report with pass/fail status and failure summaries. Supports Go (go test), Node (npm test), and Python (pytest) projects. Framework auto-detection from project root.

Parameters:

Field Type Description
command string Optional explicit test command
args array Optional extra arguments to pass to the test command
timeout_seconds number Maximum time in seconds (default: 60)

Example:

{
  "command": "go test ./...",
  "timeout_seconds": 120
}

todo_list — Session todo list

Tracks work items as a checklist — a single tool with action parameters (keeps the tool array small). Todos are lightweight items, not goals: no lifecycle, turns, budget, or completion criterion. Use them to break the current task into visible steps; use goals for autonomous multi-turn work.

Parameters:

Field Type Description
action string add | update | complete | remove | list | clear (required)
id string Todo ID for update/complete/remove
description string Todo text for add
status string pending | in_progress | done (for update)

Goal linkage (bugs.md):

  • No goal active — items live in the session list (conversation scratchpad).
  • Goal active — the list is linked to the goal: blank at goal start, items added belong to the goal (the framework surfaces them each turn and the stall watchdog counts their transitions as progress), and they are contained by it — when the goal ends, its todos end with it and the session list resurfaces unchanged. remove/clear are refused while goal-linked. A goal completed with open todos reminds the model of them so unfinished work is not silently dropped.

Enabled via tools.enabled.todo_list (default on); toggle at runtime with /tools:todo_list:on|off.

terminal — Sandboxed shell execution

Executes shell commands in a hardened sandbox environment with process group isolation, credential stripping, and a command-position blocklist. Unlike the bash tool, terminal runs in a per-session sandbox directory with repointed HOME/TMPDIR and stricter security defaults.

Parameters:

Field Type Description
command string Shell command to execute (required)
timeout number Timeout in seconds (optional)

Security:

  • Runs in a per-session sandbox directory with repointed HOME/TMPDIR
  • Command-position blocklist (rm, sudo, curl, ssh, etc.)
  • Credential stripping from child environment
  • Process group isolation and timeout enforcement

read_media — Read media files for multimodal models

Reads image and video files and returns base64-encoded content for vision- and multimodal-capable models. Only available when the active model supports image inputs (input_types: [text, image] in model config).

Parameters:

Field Type Description
path string Path to the media file (required)

Performs relevance-ranked code search using BM25Okapi ranking. Unlike the regex-based search tool, smartsearch accepts natural language queries and returns files ranked by topical relevance. It builds and maintains a persistent BM25 index under .goa/smartsearch/ that refreshes incrementally as files change (via edit/write tools).

Parameters:

Field Type Description
query string Natural language query (required)
glob string File glob filter (e.g. "*.go")
path string Root directory to search (default: project root)
max_results number Maximum results to return (default: 20)
min_score float Minimum relevance score threshold (0.0–1.0)

Configuration:

tools:
  smartsearch:
    enabled: true             # Enable smartsearch (default: false)
    max_results: 20           # Max results per query
    min_score: 0.0            # Minimum relevance score threshold
    exclude_dirs: []          # Additional exclusion dirs
    k1: 1.5                   # BM25 k1 parameter (term frequency saturation)
    b: 0.75                   # BM25 b parameter (length normalisation)

Best for:

  • Finding code by what it does, not by an exact pattern
  • Broad concept queries ("database migration", "error handling")
  • Exploring unfamiliar codebases

For exact pattern matching (regex, function names), use search instead.

lsp — Language-server code navigation

Queries the language server for precise, compiler-grade code navigation: definition, references, hover, and document symbols. Replaces grep-and-guess with exact locations. Works for every language with a configured server — the manager selects the right server per file by extension (Go/gopls, Python/pyright, TypeScript/typescript-language-server, Rust, Java, C/C++, Ruby, Zig, Lua, Bash, Terraform, Dockerfile, and more from the embedded 34-server registry).

Parameters:

Field Type Description
op string definition | references | hover | symbols (required)
path string Source file path, any supported language (required)
line integer 0-indexed line of the symbol (not needed for symbols)
character integer 0-indexed column of the symbol (not needed for symbols)

Diagnostics on edit/write: every edit/write also notifies the file's language server (any supported type, not just Go) and appends a Diagnostics (<server>): block to the tool result when the server reports problems — the label names the actual analyzer (gopls, pyright, typescript-language-server, …).

Non-blocking guarantee: language servers spawn asynchronously and file touches never wait on a server start (a cold npx download can take a minute — reads/edits used to block for the full duration). While a server is starting, touches are skipped and the next touch re-opens the document once it is up; the first edit on a cold server may therefore miss diagnostics — they appear from the next one onward. Explicit lsp navigation queries wait for the server (bounded by the turn context / 3-minute handshake cap).

Configuration:

tools:
  enabled:
    lsp: true        # master switch for the WHOLE LSP integration
                     # (navigation tool + edit/write/read linking + server
                     # spawns). Off means off — no background servers.
                     # Toggle live at runtime: /tools:lsp:on | /tools:lsp:off
lsp:                 # optional schema-level tuning (OpenCode parity)
  disable_download: false   # only use servers already installed
  servers:
    pyright: { disabled: true }          # disable a builtin server
    myls:   { command: ["myls","--stdio"], extensions: [".my"],
              markers: ["my.toml"], language_id: "mylang" }
# `lsp: false` disables LSP globally (blocks even /tools:lsp:on).

Servers resolve in order: PATH binary → npx --yes fallback (Node servers, downloaded on first run) → optional installer into ~/.goa/bin (lsp.disable_download skips both downloads).

webfetch — Fetch web pages as Markdown

Fetches any URL and converts the HTML page to structured Markdown. Acts like a remote read tool with session-scoped caching, line-range requests, and optional AI summarization via sub-agent.

Parameters:

Field Type Description
url string Absolute URL to fetch (required)
action string fetch (default) or summarize
start_line number First line to return, 1-indexed
end_line number Last line to return, 1-indexed
max_lines number Maximum lines to return
prompt string Steering prompt for summarize action

Actions:

  • fetch (default): download URL, convert HTML to Markdown, cache, return lines
  • summarize: ask configured sub-agent to summarize cached Markdown

Example:

{"url": "https://example.com", "start_line": 1, "end_line": 50}

See docs/tools/webfetch.md for full configuration reference.

agent — Spawn a sub-agent

Spawns a focused sub-agent in an isolated context to execute a specific task. Returns the sub-agent's final result. Supports running in background mode for concurrent task processing.

Parameters:

Field Type Description
description string Short task summary (3-5 words) for UI display
prompt string Complete task description for the sub-agent
subagent_type string coder (default), explore, or plan
run_in_background boolean If true, return immediately with a task_id
resume string Optional task ID to resume a background agent

agent_swarm — Spawn parallel sub-agents

Spawns a swarm of sub-agents to process multiple items in parallel. Each item launches one sub-agent, and all run concurrently. The prompt_template uses {{item}} as a placeholder replaced with each item value.

Parameters:

Field Type Description
task string Overall swarm task description (required)
items array List of items to process (required)
subagent_type string coder (default), explore, or plan
prompt_template string Template with {{item}} placeholder

Example:

{
  "task": "Review files for issues",
  "items": ["src/main.go", "src/utils.go"],
  "subagent_type": "reviewer"
}

Documentable Tool Update

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"},
    }
}

Tool Registration

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.PythonTool{TimeoutSeconds: cfg.Tools.Python.TimeoutSeconds})
    reg.Register(&tools.SSHBashTool{Hosts: sshHosts(cfg)})
    reg.Register(tools.NewBGExecTool())
    reg.Register(&tools.MementoTool{ProjectDir: projectDir, GlobalDir: cfg.ConfigDir})
}

Concurrent Tool Execution

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.

Resource Access Declaration

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
}

Conflict Rules

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)

Scheduling

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.