Featured image of post An agent is a loop with real tools and a fence

An agent is a loop with real tools and a fence

An agent is a model in a loop with tools it can really call and results it can really see. Taking the ReAct pattern apart in go-tool-base.

Ask a chatbot to fix a bug and it hands you back something that looks completely right. Confident, well-formatted, plausible. Will it compile? The chatbot has not the faintest idea, because it never ran it. It can’t. It produced text that resembles a fix, and whether that text actually builds is a question it is structurally incapable of answering. It’s guessing, beautifully.

The whole distance between that and something genuinely useful comes down to one thing: can it do things, and can it see what happened when it did. That’s what internal/agent in go-tool-base is, and it’s worth pulling apart because “agent” is a word doing a lot of vague work in the industry right now, and underneath it the idea is refreshingly concrete.

The pattern has a name, ReAct, which is just “reasoning and acting” mashed together, and the concept is simpler than the paper it came from. Instead of one prompt in and one answer out, you give the model a set of tools it’s allowed to call, and you run it in a loop. It reasons about what to do next. It calls a tool. It gets the real result back. It reasons again with that result in hand. Reason, act, observe, repeat, until the job’s done or it’s out of road. The model isn’t narrating what it would do; it’s doing it, a step at a time, and reacting to what actually happened.

The “actually happened” is the load-bearing part, and it’s all in the tools. The agent’s toolbox in go-tool-base is the everyday stuff a developer does all day, handed to the model as callable functions through pkg/chat’s tool-calling: read_file, write_file, list_dir, a tree for the layout, a batched read_files for grabbing several at once. And then the ones that matter most, the Go toolchain itself: go build ./..., go test, the linter, go get. These aren’t simulations. When the model writes a fix and then calls the build tool, what comes back is the real compiler’s output. A real error on a real line, or a real green pass.

That’s the difference, and it’s the whole difference. An LLM is extraordinary at producing code that looks correct and genuinely poor at knowing whether it is correct, because those are different skills and it only really has the first. Real tools fix that by outsourcing the judgment. The model proposes; the compiler disposes. It can’t declare victory and move on, because the next thing that happens is go build runs and either agrees or doesn’t, and the test suite either passes or hands back a failure it now has to deal with. You’ve replaced “the model thinks it’s done” with “the toolchain confirms it’s done,” and the toolchain cannot be charmed by a confident tone.

Which raises the obvious worry, and it’s the right one to have. A loop that can write files and run commands on its own, unattended, is exactly the kind of thing that should make you nervous. Hand an autonomous model write_file and a shell and walk away, and you’re one bad reasoning step from it deciding the fix involves rewriting something three directories up that it had no business touching.

So there’s a fence. Every file tool resolves the path it’s been asked for and checks that it lands inside the project root it was given, and if it doesn’t, the tool refuses with a flat ErrPathInvalid, “path is outside of allowed directory.” Try to climb out with .., try to hand it an absolute path pointing somewhere else on the machine, and you get nothing. The model can do real, consequential things, read, write, build, test, but only within one fenced directory. That single constraint is what turns “let an AI write files and run commands by itself” from a reckless idea into one you can actually leave running, because the worst it can do is make a mess in the one place you pointed it at.

There’s a small concession tucked in there too, the kind you only meet when you build the thing rather than describe it. read_file doesn’t always hand over the whole file. For a large one it sends the head and the tail and trims the middle, because the model’s context is finite and a 4,000-line file would swallow it whole. It’s a reminder that these loops don’t run in some abstract space of pure reasoning; they run inside a token budget, and you make engineering trade-offs to fit.

If the fixed “generate, then run check, clippy and test” pipeline from the rtb scaffolder is the special case, this is the general one: there, the steps are wired in a fixed order; here, the model itself decides which tool to reach for next. Same instinct, more rope.

So when someone says “agent” and it sounds like magic, this is what’s under it, and it’s deliberately not magic. A loop. A set of tools that do real things and report real results. And a fence that keeps the real things contained. The model supplies the reasoning, which is the part it’s good at, and the tools supply the truth, which is the part it isn’t. Wire those two together, point them at one directory, and “write me some code” becomes “go and make this actually pass.”

Built with Hugo · Theme Stack designed by Jimmy