Contents

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.

One interface, and a loop around it.

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.

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.

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.

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.

PackageRole
internal/sessionStore with a JSONL implementation (sessions/<id>.jsonl + index)
internal/promptsystem prompt assembly and AGENTS.md lookup up the tree
internal/agent/summarize.gosummarization once ~0.8ยทcontext_window is filled (in memory)
internal/permissionstatic permission policy
internal/tuiinteractive chat on Bubble Tea / Bubbles / Lipgloss
internal/subagentthe task tool โ€” a child agent with a read-only tool set
internal/skillslocal Agent Skills (SKILL.md, progressive disclosure)
internal/mcpMCP client (stdio + http) on the official Go SDK
internal/hooksshell hooks around tools
internal/versionsingle source of the build 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.

Dependencies are deliberately modest
Module 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.
Cheat sheet
  • One abstraction
    • llm.Provider.Stream(...) โ†’ a stream of Event; 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
  • Tools
    • 15 standard ones via DefaultSet, sandbox for file tools
    • in-process shell via mvdan/sh, no system bash
  • Build
    • a single binary, Go 1.26
    • version from a git tag, releases via GoReleaser