Skip to main content

Using tools and the built-ins

When the model returns tool calls, the Agent takes over. It validates every call, executes the batch, writes one result message per call, and sends the next request when the batch does not terminate.

model returns calls
→ Agent checks IDs, names, JSON, and schemas
→ Agent runs the batch
→ Agent records ordered result messages
→ Agent requests the next turn when the batch continues

Registering a tool and watching it run

The lookupTool from Defining tools is registered here, and its lifecycle is observed through events.

assistant, err := agent.New(agent.Config{
Generator: generator,
Model: model,
Tools: []tool.Tool{lookupTool},
})
if err != nil {
return err
}

run, err := assistant.Start(ctx, agent.Input{
Prompts: []message.UserMessage{
message.NewUserMessage("Which plan does C-100 use?"),
},
})
if err != nil {
return err
}

for event := range run.Events() {
switch event := event.(type) {
case agent.ToolExecutionStartEvent:
fmt.Printf("[start] %s %s\n", event.Call.Name, event.Call.Arguments)
case agent.ToolExecutionUpdateEvent:
fmt.Printf("[update] %s parts=%d\n", event.Call.Name, len(event.PartialResult.Content))
case agent.ToolExecutionEndEvent:
fmt.Printf("[end] %s error=%t result=%q\n", event.Call.Name, event.Result.IsError, event.Result.Text())
}
}

result, err := run.Wait()
if err != nil {
return err
}
fmt.Println(result.Final.Text())

The end event's Result is the message.ToolResultMessage sent to the next model request. Event ordering, including why invalid calls still close with an end event, is covered in Observing Agent events.

Knowing the validation sequence

Each call passes these checks in order:

  1. its ID is present and unique within the assistant batch, and its name is non-blank.
  2. its name is registered.
  3. its arguments are a JSON object.
  4. its arguments satisfy the schema compiled at agent.New.
  5. its function runs.
  6. its returned content is a valid tool result.

An unknown tool, invalid argument, function error, invalid final result, invalid update, or panic becomes an error result with IsError: true. The model can respond to that result, and the run does not fail for that reason alone.

Running the batch

Calls from one assistant response run in parallel by default. agent.Config.ToolExecution accepts tool.ExecutionSequential to serialize every batch, and a registered tool with ExecutionSequential also makes the whole batch sequential. The built-in edit and write tools use that mode.

Parallel calls can finish in any order, and transcript tool-result messages stay in assistant source order. Event ordering across the batch, including start and end event order, is covered in Observing Agent events. Separate runs on the same Agent execute concurrently.

All functions receive the run context. Golato waits for started functions to return, and a function that ignores cancellation can delay the run.

Deciding when the loop stops

A final tool.Result{Terminate: true} asks the Agent to stop after the current batch. Every call in the batch must return a successful terminating result. An error result or an ordinary sibling keeps the loop running, and siblings are not skipped or canceled because one call requested termination.

If assistant output stops with message.StopReasonLength while it contains calls, no function runs. The Agent creates error results for those calls and continues when possible, because their arguments may be incomplete.

Using the built-in tools

These tools are not a sandbox

The built-ins run with the hosting process's file, process, environment, and network permissions. Bash inherits the process environment, including exported credentials. The built-ins belong to trusted work only, and anything else needs process isolation through a container, virtual machine, or isolated host.

tool/builtin.New returns the default read, bash, edit, and write tools in that order. Custom tools join them, and the returned slice goes to agent.Config.Tools.

tools, err := builtin.New(builtin.Config{
WorkingDirectory: workspace,
CustomTools: []tool.Tool{lookupTool},
})
if err != nil {
return err
}

assistant, err := agent.New(agent.Config{
Generator: generator,
Model: model,
Tools: tools,
})
if err != nil {
return err
}

result, err := assistant.Run(ctx, agent.Input{
Prompts: []message.UserMessage{
message.NewUserMessage("What files are in this project?"),
},
})
if err != nil {
return err
}
fmt.Println(result.Final.Text())
ToolInputsResult
readpath, optional offset, limittext or a supported image attachment
writepath, contentcreates parents, overwrites, reports byte count
editpath, edits[{oldText,newText}]unique replacements against the original file
bashcommand, optional timeoutnon-interactive combined output and command errors

Relative paths use the working directory captured by builtin.New, while absolute and parent paths stay usable. An empty working directory uses the process directory.

read returns text up to 2,000 logical lines or 50 KiB and supports one-indexed paging with offset and limit. PNG, JPEG, GIF, and WebP files are returned as images. Oversized images are resized or re-encoded to fit 2,000 by 2,000 pixels and a 4 MiB Base64 payload.

write creates missing parent directories and writes content verbatim. edit validates all replacements against the original file, rejects duplicates and overlaps, tries a fuzzy fallback for whitespace, quotes, and dashes when exact text is absent, and preserves the file BOM and first newline convention.

bash invokes the bash executable non-interactively with combined standard output and error. It keeps the tail up to 2,000 lines or 50 KiB, publishes progress updates, and has no default timeout. On Darwin, Linux, and the BSD targets with process-group support, an explicit timeout kills the command and its descendants. Other targets kill the direct child and bound pipe draining with WaitDelay.

Within one slice returned by builtin.New, same-path edit and write operations are serialized across concurrent runs. Another call to builtin.New creates a separate set with separate coordination.

Selecting only some tools

builtin.Config applies static selection while constructing the slice.

tools, err := builtin.New(builtin.Config{
WorkingDirectory: workspace,
AllowedToolNames: []string{"read", "lookup_customer"},
ExcludedToolNames: []string{"bash"},
CustomTools: []tool.Tool{lookupTool},
})
if err != nil {
return err
}
validateErr := tool.Validate(tools)
if validateErr != nil {
return validateErr
}
  • nil AllowedToolNames selects every candidate.
  • a non-nil empty allowlist selects none.
  • exclusions apply last and always win.
  • repeated names are ignored, and every listed name must exist.
  • DisableBuiltinTools: true removes the four defaults but keeps custom tools.

Selection is fixed before Agent construction. A new set is needed when the working directory or the tool list must change.

Verifying effects

A tool result is a report, not proof that an external change happened. Checking files, running tests, or diffing after a run shows the actual result. Tool events describe attempts and results, and they do not grant authorization or form an audit record.

API reference