How EONYX Works: The Agent Architecture from the Inside
EONYX is written in Go.
It builds into a single static binary.
Inside sits a small set of packages around one central abstraction.
Here is how it fits together.
1 ๐ง The agent core
One interface, and a loop around it.
1.1 Dependency flow
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 llm.Provider is hidden behind that interface.
1.2 One abstraction: llm.Provider
internal/llm holds the core and 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 traffic with the model collapses into a stream of events.
So adding a backend means implementing one interface.
The only implementation is internal/llm/openai.
It is an OpenAI-compatible client on net/http and SSE, with no vendor SDKs.internal/provider.For(...) is the single place that maps a model onto a provider.
1.3 The agent loop
internal/agent is the loop.
Send records the user message.
Then it repeats: stream the response, execute tool calls, feed results back.
It stops when the model calls no more tools.
The ceiling is maxSteps, 50 by default.
Options configures it:
Historyโ seed or resume.Persistโ write every message. An error aborts the turn, because the store is the source of truth.Authorizeโ the permission gate. A denial becomes an error result and does not kill the turn.Eventsโ streaming callbacks.ContextTokensโ enables auto-summarization.
Consecutive read-only calls run in parallel: read, ls, glob, grep, git statuses.
Results always come back in the original order.
2 ๐งฐ Tools and the build
2.1 Tools
internal/tools is the registry, plus DefaultSet(...).
It registers the 15 standard tools: 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 is what the model sees.
Its Handler is what actually runs.
They are separate on purpose.
With the sandbox on, file tools are wrapped in sandboxed(...).
internal/shell runs bash-like scripts in-process via mvdan.cc/sh/v3.
That is why bash works everywhere 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 and is hardcoded nowhere.make build bakes git describe --tags --always --dirty into the binary.eonyx --version prints it.
A release is a v* tag push.
GoReleaser then builds Linux, macOS and Windows in GitHub Actions.
github.com/eonyx-core/eonyx, Go 1.26.Everything is pure Go, with no vendor LLM SDKs.
cobra, doublestar,
mvdan/sh, golang.org/x/net, the official modelcontextprotocol/go-sdk, alecthomas/chroma for highlighting.Plus 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