GO AGENT DEVELOPMENT GUIDE
Build AI agents in Go, from the loop to multi-agent work
A production agent is more than one model request. It needs a controlled loop, validated tool calls, recoverable state, and explicit task ownership when several agents collaborate. This guide maps those responsibilities to Modu's actual package boundaries.
This guide is for Go developers evaluating an agent framework or moving beyond single LLM calls. If you only want to run the smallest working example, start with the Modu quickstart.
01 · ARCHITECTURE
Split the agent into four responsibilities
An agent framework is easy to mistake for a model client. The hard questions begin after the model asks for a tool: who validates the arguments, who owns the side effect, where execution resumes after a crash, and how multiple workers hand off a task. Combining those decisions in one function is difficult to test and even harder to recover.
pkg/providers handles protocols, streams, and provider registration without owning business state.
pkg/agent owns the ReAct-style loop, tools, events, interrupts, and queues.
pkg/runtime appends checkpoints for committed messages and supports resume and rewind.
pkg/mailbox tracks registrations, inboxes, tasks, projects, validation, and conversations.
The host application still owns prompts, its tool catalog, persistence policy, and deployment. The framework supplies execution mechanics; it should not decide business permissions or data retention.
02 · AGENT LOOP
Start with the smallest agent loop
Connect one model and run one prompt first. This verifies the provider, model ID, and event path before persistence or coordination enters the design. The structure below matches the repository's agent_demo; Ollama and LM Studio work through their OpenAI-compatible endpoints.
providers.Register(openai.New(
"ollama",
openai.WithBaseURL("http://localhost:11434/v1"),
))
model := &types.Model{
ID: "llama3.2", Name: "Llama 3.2", ProviderID: "ollama",
}
a := agent.NewAgent(types.Config{
InitialState: &types.State{
SystemPrompt: "You are helpful.",
Model: model,
},
})
err := a.Prompt(context.Background(), "Explain Modu in three sentences")
Use agent.Loop directly when the host owns message state and needs explicit input and output.
Use agent.Agent when you want prompt helpers, subscriptions, queues, and interrupt state.
03 · TOOLS
Treat every tool as a controlled side effect
A tool is more than a function registry. Each implementation supplies a stable name, a description,
parameter schema, and an execution method. The loop validates arguments before execution. File writes,
commands, and external sends should also pass through the host application's
ApproveTool boundary.
context.Context so timeouts and cancellation reach external calls.If a tool changes an external system and the process crashes before recording its result, replaying the conversation cannot undo that side effect. Operations that require exactly-once behavior need an idempotency key or transaction at the tool's API or database boundary.
04 · RECOVERY
Separate conversation recovery from business transactions
pkg/runtime writes a checkpoint after each committed message. After a restart,
Resume loads the latest state and repairs an interrupted tool call with no committed result.
Rewind makes an older checkpoint the head of a new branch without deleting later history.
store, err := runtime.NewFileStore("./checkpoints")
rt := runtime.New(agent.NewAgent(cfg), store, "session-123")
err = rt.Run(ctx, "finish this task")
resumed, err := rt.Resume(ctx)
The memory store is appropriate for tests. FileStore uses one append-only JSONL file per session and
calls fsync after appends. Implement the same Store interface for a database or object store.
05 · MULTI-AGENT
Add agents only when task boundaries are explicit
Multi-agent work is not an unstructured group chat. Define who assigns, who executes, who validates, and which state receives a failure. Mailbox does not call an LLM; it only maintains coordination state, so task transitions can be tested independently from model behavior.
mailbox.NewHub() uses process-local state by default. Choose the SQLite store when tasks,
projects, roles, and conversations must survive restarts, and define a policy for missing targets, full
inboxes, and retries.
06 · PRODUCTION
Check these seven boundaries before production
NEXT
Validate the design with one runnable example
Do not build the whole platform first. Pick the shortest example that matches your next decision:
# One agent and tool calls
go run ./examples/agent_demo
# Checkpoints, resume, and rewind
go run ./examples/runtime_demo
# Coordinator-led multi-agent work
go run ./examples/agent_teams