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

<!--more-->

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

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

### One abstraction: llm.Provider

The core is the `internal/llm` package, with no concrete backends. A provider has **one method**:

```go
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.

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

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

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

### 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                                   |

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

{{< admonition type="note" title="Dependencies are deliberately modest" >}}
Module `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.
{{< /admonition >}}

## Summary

{{< admonition type="abstract" title="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
{{< /admonition >}}

