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

<!--more-->

## 🧠 The agent core

One interface, and a loop around it.

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

### One abstraction: llm.Provider

`internal/llm` holds the core and 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 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.

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

## 🧰 Tools and the build

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

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

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