Writing

Building Your Own Coding Agent on Top of zot

Jun 16, 20263 min read

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.

What zot gives you

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.

The whole harness

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 = 20

Four building blocks: a provider client, a tool registry, a system prompt, and an agent that ties them together.

Picking a provider

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 and the sandbox

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 rejected

Adding 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.

Running the loop

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.

Why build your own instead of using the TUI

zot has a full interactive TUI, so why write a harness at all? Because a custom harness lets you:

  • Hard-code a workflow (one-shot prompts, batch jobs, CI checks) instead of an interactive session.
  • Lock down the tool set and sandbox to exactly what a task needs.
  • Embed agent behavior inside a larger Go program.
  • Pin a system prompt and persona without user-visible configuration.

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.

Getting started

go mod init yourharness
go get github.com/patriceckhart/zot

Then 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.

References