byo-coding-agent

command module
v0.0.0-...-77aa4db Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: MIT Imports: 22 Imported by: 0

README ΒΆ

Build Your Own Coding Agent

ci

πŸ“– Read the book online: byoharness.dev Β· byoharness.dev/es

🌐 Languages: English · Español

A hands-on introduction to harness engineering β€” the discipline of building the scaffolding around an LLM that turns it into a useful agent. You'll build a working AI coding agent in Go, then experiment with the parts that matter: providers, tools, compaction strategies, and permissions.

What is harness engineering?

The model is the engine. The harness is everything else: the loop that calls it, the tools it can use, how its conversation is shaped over time, what it's allowed to do, how the user talks to it.

Get the harness right and a mid-tier model feels great. Get it wrong and a frontier model feels broken. Most of the interesting decisions in tools like Claude Code, OpenCode, and Aider live in their harnesses, not their models.

Harness engineering happens at three layers β€” building the loop and abstractions, extending them with new tools or integrations, and configuring the behavior via files like AGENTS.md and mcp.json. Most practitioners spend the bulk of their time in the top layer; this book focuses on building because that's where the mental model is formed. Once you've built one harness, you read every config file with new eyes.

This project is a stripped-down, readable version of those tools, designed to be poked at.

What you'll build

A terminal-based coding agent (~600 lines of Go) that:

  • Talks to Claude (or any LLM you plug in)
  • Calls tools β€” bash, read_file, write_file β€” to act on your filesystem
  • Asks for approval before each tool call
  • Compacts long conversations using pluggable strategies
  • Supports slash commands (/help, /model, /compact, /verbose, …)
  • Has a TUI input with history, line editing, and a styled prompt

Prerequisites

  • Go 1.21+ (the project uses generics and the max builtin)
  • An Anthropic API key β€” console.anthropic.com β†’ Settings β†’ API Keys

Quick start

git clone git@github.com:betta-tech/byo-coding-agent.git
cd byo-coding-agent
export ANTHROPIC_API_KEY=sk-ant-...
go run .

To run against OpenAI as well, export the key once:

export OPENAI_API_KEY=sk-...
go run .

Then switch providers mid-session with the slash command β€” no restart needed:

/provider              # show current provider + the available choices
/provider openai       # swap to OpenAI (defaults to gpt-5-codex)
/provider openai gpt-4o-mini
/provider anthropic
/model gpt-5           # change just the model on the current provider

Both providers implement the same Provider interface; the rest of the harness is unchanged when you swap. Setting LLM_PROVIDER/LLM_MODEL env vars still works as a startup default if you'd rather not type the command every session.

Type /help to see commands. Try:

  • list the files here
  • write a hello.txt with a haiku in it
  • read main.go and tell me how the agent loop works

Architecture in 60 seconds

The harness is built around three orthogonal extension points. Each lives in its own internal/ package, with a small interface and an in-tree default implementation. Swapping is one line.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  main.go        wiring Β· REPL loop Β· agent loop     β”‚
β”‚  commands.go    /help Β· /model Β· /compact Β· …       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β”œβ”€β”€ internal/api/             shared types (Message, Block, ToolDef, …)
        β”‚
        β”œβ”€β”€ internal/provider/        Provider interface + Anthropic impl
        β”‚     Send messages β†’ get response. Swap to add OpenAI etc.
        β”‚
        β”œβ”€β”€ internal/tool/            Tool interface + Registry + one file per tool
        β”‚     Self-registers via init() β€” drop a file in, it appears.
        β”‚
        β”œβ”€β”€ internal/compact/         CompactionStrategy interface + strategies
        β”‚     SlidingWindow, Summarize, NoCompaction, WithLogging decorator.
        β”‚
        └── internal/ui/              Banner, spinner, Bubble Tea input, styles
              TUI affordances. Readable but less interesting.

Project layout

.
β”œβ”€β”€ main.go              wiring + REPL + agent loop + executeTool wrapper
β”œβ”€β”€ commands.go          slash command registry
└── internal/
    β”œβ”€β”€ api/             Message, Block, ToolDef, Response, RenderTranscript
    β”œβ”€β”€ provider/        Provider interface + AnthropicProvider
    β”œβ”€β”€ tool/            Tool interface + Registry + bash / readfile / writefile
    β”œβ”€β”€ compact/         CompactionStrategy + SlidingWindow / Summarize / Logging
    └── ui/              banner, spinner, input (Bubble Tea), styling helpers

internal/ is enforced by the Go compiler β€” packages in there can only be imported by code in this same module, which is the right signal for "these aren't meant to be reused as a library."

The three extension points

1. Providers

Want to use OpenAI, Bedrock, or a local model? Implement Provider:

type Provider interface {
    Send(ctx context.Context, messages []Message, tools []ToolDef) (Response, error)
    Model() string
    SetModel(name string)
}

Then change one line in main.go:

llm = provider.NewOpenAIProvider(...)  // instead of provider.NewAnthropicProvider

The adapter is the only place that knows the SDK's wire format. The rest of the harness deals in generic Message / Block / ToolDef types. See internal/provider/anthropic.go for the reference.

2. Tools

Want to add git_diff, web_search, kubectl? Create one file under internal/tool/:

// internal/tool/gitdiff.go
package tool

import (
    "os/exec"

    "github.com/betta-tech/byo-coding-agent/internal/api"
)

type GitDiffTool struct{}

func init() { Default.Register(&GitDiffTool{}) }

func (GitDiffTool) Definition() api.ToolDef {
    return api.ToolDef{
        Name:        "git_diff",
        Description: "Show uncommitted changes in the current repo.",
        InputSchema: map[string]any{},
        Required:    []string{},
    }
}

func (GitDiffTool) Execute(_ string) (string, bool) {
    out, err := exec.Command("git", "diff").CombinedOutput()
    if err != nil { return string(out), true }
    return string(out), false
}

Drop the file in. Run go run .. Type /tools β€” git_diff is in the list, the model can call it. No edits to main.go β€” main already imports internal/tool, so the new file's init() runs when the package loads.

3. Compaction strategies

Want to test different ways to handle long conversations? Implement CompactionStrategy:

type CompactionStrategy interface {
    Compact(ctx context.Context, messages []Message) ([]Message, error)
}

Three are included:

Strategy What it does
compact.NoCompaction{} Default β€” never modifies messages
&compact.SlidingWindow{KeepLast: 10} Keeps the last N messages, drops older
&compact.Summarize{Provider: llm, Threshold: 20, KeepRecent: 6} Asks the model to summarize old turns once history hits Threshold

Wrap any of them with compact.WithLogging(inner, "compactions.log") to record before/after diffs to a file β€” useful for comparing strategies.

Swap by changing one line in main.go:

compactor = &compact.Summarize{Provider: llm, Threshold: 20, KeepRecent: 6}

There's a subtle bit: a naive truncation can leave a tool_use block without its matching tool_result, and the API will 400. The SafeSplitPoint helper in internal/compact/strategy.go walks back until it finds a "clean" boundary. All strategies route through it.

Commands

Command Effect
/help List all commands
/provider [anthropic|openai] [model] Show or swap the LLM provider
/model [name] Show current model or change it (provider-aware suggestions)
/tokens Show cumulative input/output tokens and estimated cost
/debug [on|off|clear] Toggle the live debug panel (provider calls, tool dispatch, compaction)
/clear Wipe conversation history
/tools List registered tools
/subagents List registered and in-flight subagents
/compact [sliding|summarize|none] Run compaction (configured strategy, or ad-hoc one)
/verbose [on|off] Toggle before/after printing on compaction
/exit Quit

Now try

In rough order of difficulty:

  1. Add a git_diff tool. Read tool_bash.go for the pattern, write a new tool_git_diff.go. Verify /tools lists it after.
  2. Add a TokenBudget compaction strategy. Drop oldest messages until estimated token count is under a configurable threshold. Start with a byte-count approximation; later swap in a real count_tokens call.
  3. Add a PermissionPolicy abstraction. Currently every tool call goes through confirm. Refactor so a policy decides β€” AlwaysAllow, AlwaysAsk, AllowList{names}. The policy slots into main.go like the other extension points.
  4. Add a second provider. OpenAI, a local Ollama, or a MockProvider that records calls (most useful for the next exercise).
  5. Add tests. With MockProvider you can test the agent loop end-to-end without an API call. Compaction strategies are easy to test on synthetic message histories.

What's not in here yet

  • Streaming. The model returns a full response before we render anything. Real coding agents stream tokens as they arrive.
  • Tests. Nothing's automated yet β€” see exercise 5.
  • Prompt caching. Every turn re-sends the full history at full price.
  • Multi-line input. Bubble Tea's textarea would unlock Shift-Enter for newlines.
  • Permission policies. Approval is hardcoded as "ask every time" β€” see exercise 3.
  • MCP support. No external tool servers.

Each one is a worthwhile next chapter.

Documentation

Three flavors of docs, for the three reasons people read this repo:

  • examples/minimal/ β€” a single-file (~130 lines) version of the agent loop, no abstractions. The fastest way to see "the essence" before any harness machinery shows up. Run with go run ./examples/minimal.
  • follow_along/ β€” chapter-length narrative on the why of every layer in the harness, in the order it was built. Read in order; about an hour total. Available in English and Spanish.
  • how-to/ β€” short recipe-style references for the extension tasks people actually do: add a tool, add a provider, add a permission policy. Also bilingual.

If you want a quick taste, start with examples/minimal/. If you want the full story, read follow_along/ in order. If you've already built it and want to extend it, jump to how-to/.

Acknowledgments

The structure draws on architectural decisions visible in Claude Code, OpenCode, and Aider. The "build your own X" framing comes from Build Your Own Redis, Crafting Interpreters, and Thorsten Ball's Writing An Interpreter In Go.

Documentation ΒΆ

Overview ΒΆ

Package main wires the harness together. The interesting code lives in the internal/ packages β€” this file constructs the root Agent, registers any subagents and their delegate tools, and launches the Bubble Tea TUI.

Directories ΒΆ

Path Synopsis
cmd
sitegen command
Command sitegen renders the follow_along/ chapters, how-to/ recipes, and exercises/ extension prompts into a static website that matches the Claude-Design landing-page mockup.
Command sitegen renders the follow_along/ chapters, how-to/ recipes, and exercises/ extension prompts into a static website that matches the Claude-Design landing-page mockup.
examples
minimal command
Package main is the bare-bones coding agent from follow_along chapter 01 β€” the inner loop, the outer REPL, three tools dispatched by a switch, and nothing else.
Package main is the bare-bones coding agent from follow_along chapter 01 β€” the inner loop, the outer REPL, three tools dispatched by a switch, and nothing else.
internal
agent
Package agent encapsulates the agent loop.
Package agent encapsulates the agent loop.
api
Package api defines the provider-agnostic message types used everywhere else in the harness.
Package api defines the provider-agnostic message types used everywhere else in the harness.
compact
Package compact holds the CompactionStrategy interface, helpers for safe truncation, and the strategies that ship with the harness.
Package compact holds the CompactionStrategy interface, helpers for safe truncation, and the strategies that ship with the harness.
debug
Package debug is the lightweight in-memory event log surfaced by the TUI's /debug panel.
Package debug is the lightweight in-memory event log surfaced by the TUI's /debug panel.
mcp
Package mcp adapts external Model Context Protocol servers into the harness's local Tool interface.
Package mcp adapts external Model Context Protocol servers into the harness's local Tool interface.
memory
Package memory adds persistent context to the harness.
Package memory adds persistent context to the harness.
provider
Package provider defines the LLM-backend interface and houses each concrete implementation.
Package provider defines the LLM-backend interface and houses each concrete implementation.
subagent
Package subagent defines the Subagent abstraction and a registry that holds them by name.
Package subagent defines the Subagent abstraction and a registry that holds them by name.
tool
Package tool defines the Tool interface and a Registry that holds tools by name.
Package tool defines the Tool interface and a Registry that holds tools by name.
ui
Package ui holds everything the user sees: the banner, the loading spinner, the chat-input TUI, the confirmation prompt, and the styling helpers used by the rest of the harness when printing to stdout.
Package ui holds everything the user sees: the banner, the loading spinner, the chat-input TUI, the confirmation prompt, and the styling helpers used by the rest of the harness when printing to stdout.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL