The layers
agent — Run the Loop
The agentic loop over pkg/ai: runs, event streams, hooks, and the built-in tools
- Moving from single calls to a tool-running agent
- Understanding what a run is and what its stream carries
- Deciding where hooks fit before reaching for middleware
You can run a tool-using conversation loop with pkg/agent.
A run builds the prompt, calls the model, runs the tools the model asked for,
records the results, and repeats. It stops when the model calls no tool, or when
WithMaxTurns stops it.
a := agent.New(model, agent.WithSystemPrompt("You are a release engineer."), agent.WithTools(weather, readFile), agent.WithMaxTurns(12),)
stream := a.Run(ctx, ai.UserMessage("Draft the release note."))
for event, err := range stream.Events() { // ...}
msgs, err := stream.Wait() // the new messages of this runpkg/ai makes one call. pkg/agent owns the loop around those calls, and the
in-memory history of one agent instance. agent.Prompt(ctx, a, "…") wraps
Run and Wait for the text-in, answer-out path.
The stream belongs to the run
switch event.Type {case agent.EventMessageUpdate: // event.AssistantEvent carries the ai.Event deltascase agent.EventToolExecutionStart: // event.ToolName, event.Result // ... agent_start, agent_end, turn_start, turn_end, message_start, // message_end, tool_execution_update, tool_execution_end}Read Events for live progress, or call Wait for the messages alone. Errors
reach the stream, not a separate return value. A second active run on the same
instance fails through its stream.
Hooks change the loop at named points
a := agent.New(model, agent.WithBeforeTool(denyWrites), agent.WithAfterTurn(summarize),)Each point has its own typed signature and its own option.
| Hook | What it can change |
|---|---|
BeforeCall | What the model sees, without changing history |
BeforeTool | Rewrite a tool call, or deny it |
AfterTool | Rewrite the tool result, or add blocks to it |
AfterTurn | Append messages after a turn |
BeforeStop | Add messages and continue the loop |
The history is append-only: no hook rewrites the past. A message that a hook adds crosses the stream once and says where it came from, so the model, the reader, and the store each apply their own rule to it.
Built-in tools plug into the same interface
box := sandbox.New("/repo/checkout", sandbox.Strict())
agent.WithTools( ai.DefineParallelTool( read.ToolName, read.Description, read.New(read.FS{FS: box}), // the runner, confined to the sandbox ), // ... bash, write, edit, grep, find, todowrite)Each package under pkg/tool returns a runner: bash, read, write, edit, grep,
find, and todowrite. ai.DefineTool gives the runner a name, a description, and
a schema. pkg/sandbox gives the file tools a path boundary, and sandbox.AllowDir
opens one more directory.