How EONYX Works: The Agent Architecture from the Inside
EONYX is written in Go and builds into a single static binary. Inside is a tidy set of packages built around one central abstraction. Let’s walk through how it is put together — useful both for contributors and for understanding the agent’s behaviour.
1 The agent core
At the heart of it there is a single interface and a loop around it; let’s look at how the packages, the provider and the agent itself are connected.
1.1 Dependency flow
At a high level: cmd → (tui | agent) → (provider → llm) + tools + session + permission + prompt. The default command brings up tui; the run subcommand drives the same agent headless. Everything below the llm.Provider interface is hidden behind it.
1.2 One abstraction: llm.Provider
The core is the internal/llm package, with no concrete backends. A provider has one method:
Stream(ctx, []Message, []Tool) (<-chan Event, error)Event is a closed sum type: EventTextDelta, EventReasoningDelta, EventToolCall, EventUsage, EventDone, EventError. All communication with the model is reduced to a stream of events. Because of that, adding a backend means implementing a single interface.
The only concrete implementation is internal/llm/openai: an OpenAI-compatible client on top of net/http + SSE, with no vendor SDKs. internal/provider.For(...) is the single place where a model is mapped to a concrete provider.
1.3 The agent loop
internal/agent is the loop itself. Send records the user message, then repeats: stream the response, execute tool calls, feed the results back, and stop when the model no longer calls tools (the limit is maxSteps, 50 by default).
It is configured through Options:
History— seed/resume;Persist— write every message (an error aborts the turn — the store is the source of truth);Authorize— the permission gate (a denial becomes an error result, it does not kill the turn);Events— streaming callbacks;ContextTokens— enables auto-summarization.
Consecutive read-only calls (read, ls, glob, grep, git statuses) run in parallel; results are always returned in the original order.
2 Tools and the build
Around the core sit the tool registry, the built-in shell, the remaining packages and, finally, the build into a single binary.
2.1 Tools
internal/tools is the registry of executable tools plus DefaultSet(...), which registers the 15 standard ones (read, write, edit, multi_edit, ls, picker, glob, grep, bash, fetch, git_status, git_diff, git_log, git_commit, todo_write). A tool’s declaration (what the model sees) is separate from its Handler (what actually runs). When the sandbox is enabled, file tools are wrapped in sandboxed(...).
internal/shell runs bash-like scripts in-process via mvdan.cc/sh/v3 — which is why bash works cross-platform without a system shell.
2.2 The remaining packages
| Package | Role |
|---|---|
internal/session | Store with a JSONL implementation (sessions/<id>.jsonl + index) |
internal/prompt | system prompt assembly and AGENTS.md lookup up the tree |
internal/agent/summarize.go | summarization once ~0.8·context_window is filled (in memory) |
internal/permission | static permission policy |
internal/tui | interactive chat on Bubble Tea / Bubbles / Lipgloss |
internal/subagent | the task tool — a child agent with a read-only tool set |
internal/skills | local Agent Skills (SKILL.md, progressive disclosure) |
internal/mcp | MCP client (stdio + http) on the official Go SDK |
internal/hooks | shell hooks around tools |
internal/version | single source of the build version |
2.3 Build and version
The version is a git tag, hardcoded nowhere: make build bakes git describe --tags --always --dirty into the binary, and eonyx --version prints it. A release is a v* tag push; from there GoReleaser in GitHub Actions builds binaries for Linux, macOS and Windows.
eonyx, Go 1.26. Dependencies are pure Go and free of vendor LLM SDKs: cobra, doublestar, mvdan/sh, golang.org/x/net, the official modelcontextprotocol/go-sdk, alecthomas/chroma (code highlighting in the TUI) and the Charm libraries (bubbletea/bubbles/lipgloss) purely as a UI toolkit.3 Summary
- One abstraction
llm.Provider.Stream(...)→ a stream ofEvent; a backend = one implementation- the only provider is an OpenAI-compatible client on
net/http+ SSE, no vendor SDKs
- The agent loop
- stream → tools → results, up to
maxSteps - consecutive read-only calls run in parallel
- stream → tools → results, up to
- Tools
- 15 standard ones via
DefaultSet, sandbox for file tools - in-process shell via
mvdan/sh, no system bash
- 15 standard ones via
- Build
- a single binary, Go 1.26
- version from a git tag, releases via GoReleaser