How zot ships its internals as importable Go packages, so you can write a working coding agent harness in about a hundred lines.
Most "AI coding agent" projects spend 90% of their code on plumbing: streaming protocol parsing, provider auth, tool schemas, a file sandbox, a system prompt. The interesting 10% (what your agent actually does) gets buried. zot flips that ratio. It hands you the plumbing as importable Go packages so you can write a working harness in about a hundred lines.
This article walks through coil, a tiny harness built on zot, and shows how the pieces fit together.
zot ships its internals as ordinary Go packages under
github.com/patriceckhart/zot/packages/...:
provider: clients for Anthropic, OpenAI (Chat Completions and Responses),
Gemini, and more, all behind one provider.Client interface.core: the agent loop, tool registry, and event stream.agent: helpers like a sane default system prompt builder.agent/tools: ready-made read, write, edit, and bash tools plus a path
sandbox.You import what you need and ignore the rest. There is no daemon, no config format you have to adopt, no TUI you are forced to render.
Here is the core of coil. The shape is the same for any harness you build.
prov := env("COIL_PROVIDER", "anthropic")
model := env("COIL_MODEL", defaultModel(prov))
client := newClient(prov, apiKey(prov))
sb := tools.NewSandbox(cwd)
sb.Lock() // confine file tools to the current directory
reg := core.NewRegistry(
&tools.ReadTool{CWD: cwd, Sandbox: sb},
&tools.WriteTool{CWD: cwd, Sandbox: sb},
&tools.EditTool{CWD: cwd, Sandbox: sb},
&tools.BashTool{CWD: cwd, Sandbox: sb},
)
system := zotagent.BuildSystemPrompt(zotagent.SystemPromptOpts{
CWD: cwd,
Custom: "You are coil, a small coding agent harness. Be concise.",
})
ag := core.NewAgent(client, model, system, reg)
ag.MaxSteps = 20Four building blocks: a provider client, a tool registry, a system prompt, and an agent that ties them together.
Every provider is constructed the same way and returns the same interface, so swapping models is a one-line change:
func newClient(name, key string) provider.Client {
switch name {
case "anthropic":
return provider.NewAnthropic(key, "")
case "openai":
return provider.NewOpenAI(key, "")
case "gemini", "google":
return provider.NewGemini(key, "")
default:
panic("unknown provider: " + name)
}
}Because the agent loop only talks to provider.Client, your harness does not
care whether the bytes on the wire are Anthropic's Messages format or OpenAI's
Chat Completions format. zot normalizes streaming, tool calls, and usage for you.
Tools are just values that implement zot's tool interface. The built-in ones cover the common cases, and the sandbox keeps file access honest:
sb := tools.NewSandbox(cwd)
sb.Lock() // reads and writes outside cwd are rejectedAdding your own tool is the same pattern as the built-ins: implement the interface, register it. The model sees its JSON schema and can call it like any other.
zot's agent emits a stream of typed events. You decide how to render them. A CLI just prints; a TUI would draw. coil prints:
ag.Prompt(ctx, prompt, nil, func(ev core.AgentEvent) {
switch e := ev.(type) {
case core.EvTextDelta:
fmt.Print(e.Delta)
case core.EvToolCall:
fmt.Printf("\n[tool] %s %s\n", e.Name, string(e.Args))
case core.EvError:
fmt.Fprintf(os.Stderr, "\nerror: %v\n", e.Err)
}
})That callback is the entire UI layer. The agent handles the request, parses tool
calls, runs the registered tools, feeds results back, and loops until the model
is done or MaxSteps is reached.
zot has a full interactive TUI, so why write a harness at all? Because a custom harness lets you:
You get zot's battle-tested provider and tool layer while keeping full control of the surface your users (or your CI pipeline) actually touch.
Want your own vibe? Build your own TUI on top of it and make it yours.
go mod init yourharness
go get github.com/patriceckhart/zotThen copy the four-block skeleton above, register the tools you want, write your system prompt, and run. You will have a working agent before you finish your coffee, and every line you add from there is about your product, not about reimplementing streaming parsers.
That is the point of building on zot: spend your effort on the 10% that makes your agent yours.