Run it

A scripted agent loop, no model needed

Run a complete agent loop that fixes a failing Go test, driven by a script of replies recorded from a real model. No API key and no network, with tests that run in milliseconds.

Draws on chapter 1 of the bookUpdated

This page works on all four stages of the loop

  1. 1Build the request (this page)
  2. 2Call the model (this page)
  3. 3Run the tools (this page)
  4. 4Decide (this page)

This program runs the whole agent loop, with four tools and a small Go project whose tests fail, against a model that is a script. The script is a copy of what a real model, anthropic/claude-haiku-5.5, replied when it fixed the bug in October 2026, so the run has the shape of a real one while nothing leaves your machine. It is the smallest complete agent in the companion code, and every later example grows from it.

What it shows

  • The loop’s four stages in about 30 lines of Go: assemble a request, send it to the model, execute any tool calls, and choose whether to go around again. The agent loop in Go walks through them.
  • A Model interface with one method, which a script satisfies as well as a real client does.
  • Tools as a definition the model sees plus a Go function it never sees.
  • Errors from tools going back to the model as messages, so they never stop the run.

Run it

From the code directory of a clone of go-agents-book-code, with Go 1.27 or later:

$ go run ./ch01

The output, turn by turn:

go run ./ch01
task: The tests in this project fail. Find out why and fix it.

turn 1
  model asks:  run_tests {}
  model asks:  list_files {}
  tool result: --- FAIL: TestParse (0.00s) (5 lines)
  tool result: duration.go, duration_test.go

turn 2
  model asks:  read_file {"path": "duration.go"}
  model asks:  read_file {"path": "duration_test.go"}
  tool result: // Package duration parses durations suc... (44 lines)
  tool result: package duration (36 lines)

turn 3
  model asks:  edit_file
                 path: "duration.go"
                 old: "'m': time.Second,"
                 new: "'m': time.Minute,"
  model asks:  run_tests {}
  tool result: edited duration.go
  tool result: ok  	duration	0.002s

turn 4
  model says:  The tests now pass. The cause was a typo in the unit table in
               duration.go: the 'm' (minute) entry mapped to time.Second. So
               "45m" parsed as 45 seconds, and "1h30m" parsed as 1h + 30
               seconds. I changed it to time.Minute.

Read the middle of each turn as requests: every model asks line is a tool call, and every tool result line is your program doing the work. In turns 1 to 3 the model asks for two things at once, and the loop runs them in the order asked, which matters in turn 3, where the edit has to land before the tests run. Turn 4 has no tool calls, so the loop stops.

Nothing touched your disk. The project lives in an in-memory file system, a map from names to contents, and run_tests is a stand-in that checks for the fixed line; the ch04 example swaps in a real go test.

The script

The replies are plain Go values, built with three small helpers:

ch01/script.go
// script holds the replies from the run at the start of the chapter,
// recorded from claude-haiku-5.5, so the loop can replay it without a
// model. The model's IDs are shortened, and its text before tool calls
// is left out.
var script = []agent.Message{
	agent.Calls(
		agent.Call("call_1", "run_tests", `{}`),
		agent.Call("call_2", "list_files", `{}`),
	),
	agent.Calls(
		agent.Call("call_3", "read_file", `{"path": "duration.go"}`),
		agent.Call("call_4", "read_file",
			`{"path": "duration_test.go"}`),
	),
	agent.Calls(
		agent.Call("call_5", "edit_file", `{
			"path": "duration.go",
			"old": "'m': time.Second,",
			"new": "'m': time.Minute,"
		}`),
		agent.Call("call_6", "run_tests", `{}`),
	),
	agent.Says("The tests now pass. The cause was a typo in the unit " +
		"table in duration.go: the 'm' (minute) entry mapped to " +
		"time.Second. So \"45m\" parsed as 45 seconds, and \"1h30m\" " +
		"parsed as 1h + 30 seconds. I changed it to time.Minute."),
}

Calls builds a reply that requests tools, Call builds a single tool call whose arguments are JSON, and Says builds a reply that only contains text; a reply without tool calls counts as the answer. To decide which reply comes next, the scripted model counts how many assistant turns the conversation already holds. A loop that lost a message or put one in the wrong place would knock the script out of step, and the tests would fail.

Run the tests

$ go test ./ch01/...

Each test in ch01/agent pins down a single rule of the loop. An answer ends the run, and so does reaching the turn limit. Each tool call is answered by exactly one tool message, even when one reply holds several calls. A tool that fails sends its error back flagged as an error, and a call to a tool that doesn’t exist is told which tools do. The whole suite finishes in milliseconds without touching the network, and that is the reason the book writes the loop before it adds a model.

Things to try

Each of these is a one-line change; run go run ./ch01 after it.

Misspell a tool. In script.go, change the name in the call_3 call from read_file to read_files. Turn 2 now prints a tool error whose text starts with no tool named "read_files" (the printer cuts long results short, so the list of real tools is cut off on screen), and the run keeps going, because the loop hands the error to the model and carries on. With a real model, the next reply would usually correct the name.

Lower the turn limit. In main.go, set MaxTurns to 3. The run prints three turns, then ends with run agent: agent: max turns reached. Run still returns the conversation so far, which is how the program prints those three turns.

Cancel before it starts. Wrap the context in main.go with a deadline that has already passed, context.WithTimeout(ctx, 0). Run checks ctx at the top of every turn, so it stops before the first model call, and the error matches context.DeadlineExceeded under errors.Is.

Key files

File What it holds
ch01/agent/types.go Model, Request, Message, ToolDef, ToolCall, Tool, Limits, Result
ch01/agent/loop.go Run, the loop, and runTool
ch01/agent/fake.go The scripted model and its helpers
ch01/agent/tools.go read_file, list_files, edit_file, run_tests, and the argument decoder
ch01/agent/memfs.go The in-memory project
ch01/main.go, ch01/script.go The program: Ctrl-C handling, the task, and the script
ch01/testdata/duration The duration package with its bug

Next

A real agent that fixes a failing test runs the same Run against a real model’s recorded replies, with a real go test behind run_tests.