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.

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.

Model access pkg/providers handles protocols, streams, and provider registration without owning business state.
Execution loop pkg/agent owns the ReAct-style loop, tools, events, interrupts, and queues.
Session recovery pkg/runtime appends checkpoints for committed messages and supports resume and rewind.
Coordination state 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.

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.

main.go
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.

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.

DescriptionTell the model exactly when the tool applies; keep names stable across sessions.
ParametersUse JSON Schema for required fields, types, and enums instead of ad hoc parsing.
ExecutionAccept context.Context so timeouts and cancellation reach external calls.
ApprovalAsk the host before risky actions; never delegate authorization to the model.

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.

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.

runtime.go
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.

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.

Agent Teams Use named roles and one coordinator that assigns work and combines results.
Independent validation Use a worker to submit and a separate validator to accept or request a retry.

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.

Check these seven boundaries before production

TerminationSet a maximum step count and distinguish cancellation, model errors, and tool errors.
Tool permissionsDefault to least privilege and require approval for writes, commands, and sends.
ValidationSchema validates structure; business code must still validate ownership and allowed scope.
ObservabilitySubscribe to Agent, Turn, Message, and Tool Execution events to locate latency and failures.
Recovery semanticsDocument which state can replay and which external effects require idempotency.
Context costCheckpoints contain full history; long sessions need compaction and retention policies.
BackpressureChoose retry, drop, or fallback behavior for full queues instead of assuming delivery.

Validate the design with one runnable example

Do not build the whole platform first. Pick the shortest example that matches your next decision:

terminal
# 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
Complete Modu quickstartInstallation, providers, Runtime, and Mailbox Agent Core referenceBoundaries between Loop, Agent, and the host application Runtime recovery semanticsCheckpoints, resume, rewind, and external side effects
Copied to clipboard