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:
- its ID is present and unique within the assistant batch, and its name is non-blank.
- its name is registered.
- its arguments are a JSON object.
- its arguments satisfy the schema compiled at
agent.New. - its function runs.
- 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
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())
| Tool | Inputs | Result |
|---|---|---|
read | path, optional offset, limit | text or a supported image attachment |
write | path, content | creates parents, overwrites, reports byte count |
edit | path, edits[{oldText,newText}] | unique replacements against the original file |
bash | command, optional timeout | non-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
AllowedToolNamesselects 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: trueremoves 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.