TL;DR: An AI that can only produce text can describe your system. An AI that can call your Go functions can operate it. The gap between those two is tool-calling and the ReAct loop, and the loop is the fiddly part. go-tool-base’s chat package owns the loop: you define chat.Tool values (a name, a description, a parameter struct, a handler) and the framework runs Reason → Act → Observe until the AI produces a final answer.
Talking about the system versus operating it
Wire an AI provider into a CLI command and you get something that can talk. Ask it a question, get a paragraph back. Useful, up to a point.
But notice the ceiling. An AI that can only generate text can describe things. It can tell you what it would do. What it cannot do is look at the actual current state of your system, or take an actual action, because it has no hands. It’s reasoning in a vacuum about a world it can’t touch.
The thing that gives it hands is tool-calling. You hand the AI a set of functions it’s allowed to call. Now, mid-conversation, it can decide it needs to read that file before it can answer, or run that query, or check that status, and actually do it, and reason about the real result. The AI stops describing your system and starts operating it.
The loop is the hard part
Tool-calling has a shape, and the shape is a loop. The literature calls it ReAct: Reason, Act, Observe.
- The AI reasons about the prompt and decides whether it needs a tool.
- If it does, it acts — it asks for a specific tool with specific arguments.
- Your code runs the tool and feeds the result back. The AI observes that result.
- Round again. Reason about the new information, maybe call another tool, maybe several. Keep going until the AI has what it needs and produces a final text answer with no more tool calls.
Conceptually simple. Tedious and error-prone to implement by hand every time: parsing the model’s tool-call requests, dispatching to the right function, marshalling arguments in and results out, feeding observations back in the format the provider expects, knowing when to stop, and not looping forever if the model gets stuck.
That orchestration is pure plumbing, identical for every tool and every command. So go-tool-base’s chat package owns it. You don’t write the loop. You write the tools.
Defining a tool
A chat.Tool is four things: a name, a description, a parameter schema, and a handler. The description is what the AI reads to decide whether to use the tool, so it’s worth writing well. The schema describes the arguments, and you don’t hand-write it. You write a tagged Go struct and let it generate:
type ReadFileParams struct {
Path string `json:"path" jsonschema_description:"Relative path to the file"`
}
The struct is the contract. The framework derives the JSON Schema the AI is given from those tags, so the schema and the Go type the handler receives cannot drift apart, because they have one source. The handler is then an ordinary Go function that takes those parameters and returns a result.
You register your tools with SetTools, call Chat, and that’s the whole of your involvement. The framework runs the ReAct loop and Chat returns the AI’s final text answer once the loop settles.
Two details that show it was built for real use
A couple of decisions in the loop tell you it’s meant for production, not a demo.
Tool errors don’t abort the conversation. When a handler returns an error, the framework doesn’t crash the loop. It hands the error back to the AI as a string, as just another observation. That’s deliberate and it’s right. A real agent should be able to call a tool, see it fail, and react: try different arguments, take a different route, or tell the user it couldn’t. A loop that aborted on the first tool error would be far more brittle than the model it’s driving.
The loop is bounded. There’s a MaxSteps limit, default 20. An AI that gets confused could otherwise call tools forever, and a CLI command that never returns is a worse failure than a wrong answer. The cap guarantees the command terminates. The agent gets room to genuinely work a problem across many steps, but not infinite room.
There’s also parallel tool execution: when the model asks for several tools in a single step (three independent file reads, say) the framework runs them concurrently rather than one after another, because there’s no reason to make the AI wait out a sequence of things that don’t depend on each other.
In short
A text-only AI can describe your system; an AI that can call your functions can operate it. Bridging that gap means tool-calling, and tool-calling means the ReAct loop (reason, act, observe, repeat) whose orchestration is fiddly, identical every time, and not your problem worth solving twice.
go-tool-base’s chat package runs the loop for you. You define chat.Tool values (name, description, a tagged parameter struct that generates its own schema, a handler), call SetTools and Chat, and get the final answer. Tool errors go back to the AI as observations so it can recover, and a MaxSteps cap guarantees the command always terminates. You write Go functions. The framework makes them things an agent can reach for.