Skip to content

The layers

agent — Run the Loop

The agentic loop over pkg/ai: runs, event streams, hooks, and the built-in tools

Read this when
  • 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.

pkg/agentpkg/aiyour code
fig. 01 — One run of the default loop

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 run

pkg/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 deltas
case 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.

HookWhat it can change
BeforeCallWhat the model sees, without changing history
BeforeToolRewrite a tool call, or deny it
AfterToolRewrite the tool result, or add blocks to it
AfterTurnAppend messages after a turn
BeforeStopAdd 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.

Go deeper