# ollie Architecture ## Philosophy ollie's design philosophy is Plan 9 / Acme / Unix: a small substrate that integrates with the surrounding system rather than trying to do everything itself. The Go runtime (`agent.Agent`) defines what an agent *is* — an event loop, a backend connection, a message history, and three built-in primitives (`shell`, tool registry, skill registry). Everything else — orchestration, scheduling, workflows, UIs — is pushed out to external tools, scripts, and frontends. The key difference from the Emacs model: Emacs extensibility means building capabilities *on top of* its Lisp interpreter and text primitives. Ollie's extensibility means **not** building capabilities into the core at all — instead, the core provides integration surfaces (9P filesystem, agent loop) and lets the surrounding system handle everything else. This is the Acme approach: a lightweight framework that delegates to external programs via a shared filesystem namespace. The primary integration philosophy is Plan 9's "everything is a file." `olliesrv` exposes agent state and behaviors as files in a 9P namespace. Any program that can read and write files can drive an agent: shell scripts, editors, web apps, cron, containers. All clients communicate via 9P exclusively. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only — this is the last remaining D-Bus dependency. Design principles: - **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts. - **Zero built-in tools.** No tools are compiled into the Go binary. Every tool — including `shell` and `reasoning_think` — is an external script loaded dynamically via the 9P filesystem. The core is pure coordination: stream LLM → dispatch tool calls → loop. - **One integration path: 9P filesystem. Streaming via blocking reads. No polling. - **9P namespace declared via EDSL.** The entire filesystem is a single recursive `FsNodeDecl` tree in `fs/spec.go`, validated and built by `BuildTree()` in `fs/builder.go`. Adding a file means adding a `Leaf()` call — no manual stat/readdir/write wiring. - **No framework lock-in.** Frontends are decoupled via whichever interface they prefer; the core doesn't know or care. ## Repository Structure Single Go module (`ollie`) with one Git submodule (`kde/`) for the KDE frontend: ``` ollie/ ├── agent/ Agent loop, history, hooks, prompt resolution, commands ├── backend/ LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer) ├── toolsrv/ Tool server: sandboxed execution, tool registry, skill management ├── session/ Session lifecycle (config, creation, persistence) ├── fs/ 9P filesystem: EDSL spec + handlers (flat package) │ ├── spec.go Namespace declaration (single source of truth) │ ├── fsnode.go FsNodeDecl type + Dir/Leaf/TemplateDir constructors │ ├── builder.go BuildTree — spec -> *Tree wiring │ ├── rootfiles.go Root-level handlers (backends, models, eventwait, complete, generate, route) │ ├── sessionfiles.go Session-level handlers (env, ctl, plan, agent/, ...) │ ├── agentfiles.go Agent-level handlers (prompt, chat, state, cfg, ...) │ ├── elevatefiles.go Elevation handlers (policy, pending) │ ├── procfiles.go Process handlers │ ├── lifecycle.go Session create/kill/rename/shutdown + event ring │ ├── newroot.go NewRoot — tree construction + persistence restore │ ├── persist.go Session persistence to disk │ ├── types.go Session/AgentLog types │ ├── tree.go 9P *Tree (from fs package) │ ├── fs.go 9P File/FileConfig implementations │ └── format.go Event formatting helpers ├── detach/ Background process management (ring buffer, signal) ├── elevate/ Elevation broker (privilege escalation daemon) ├── sandbox/ Landlock sandbox config YAML ├── env/ Environment variable loading ├── log/ Structured logging ├── paths/ XDG path resolution ├── cmd/ Binaries: │ ├── olliesrv/ 9P server │ ├── ollie-9p/ 9P client │ └── ollie-remote/ Remote execution server ├── kde/ KDE integration (submodule) — standalone GUI, Kate plugin, KRunner ├── data/agents/ Agent config JSONs (default, coding, orchestrator, worker, ...) ├── data/prompts/ Prompt templates (markdown) ├── data/tools/ Tool scripts + .meta sidecar files ├── data/skills/ Domain knowledge modules (markdown) ├── data/scripts/ Helper scripts (ollie-remount, o) ├── data/services/ Systemd/xdg-autostart service files ├── prompts/ Embedded prompt templates (compiled into binary) └── doc/ Documentation ``` ## System Overview ```mermaid flowchart TB subgraph Frontends["Frontends"] ACME["acme (Plan 9)"] ELLIE["ellie (Emacs)"] KDE["KDE GUI / Kate / KRunner"] SH["o (terminal CLI / tmux TUI)"] HTTP["curl / scripts"] end subgraph Integration["Integration Layer"] direction LR P9["9P Filesystem\n(ollie-9p client)"] end subgraph Server["olliesrv"] direction TB NS["9P Namespace\nEDSL-declared in fs/spec.go"] end subgraph Core["Agent Engine (per session)"] LOOP["Agent Loop\n(agent/loop.go)"] SESS["Session State\n(agent/history.go)"] TRV["toolsrv.Server\n· dynamic tool dispatch\n· landlock sandboxed execution\n· remote execution via SSH"] end subgraph Backends["LLM Backends"] OLLAMA["Ollama"] OPENAI["OpenAI / OpenRouter"] ANTHROPIC["Anthropic"] GEMINI["Gemini"] CW["CodeWhisperer"] end SH --> P9 ACME --> P9 ELLIE --> P9 KDE --> P9 HTTP --> P9 P9 --> NS NS --> LOOP LOOP <--> SESS LOOP --> TRV LOOP <--> Backends TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)\n· shell, reasoning_think\n· file_read, file_write\n· lsp_*, memory_*\n· gui_*, subagent_*"] TRV --> Remote["ollie-remote (SSH)\npure 9P RPC\nno embedded tools"] ``` ## Core Library The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`. ### Package Layout | Package | Purpose | |---|---| | `agent/` | Agent struct, loop, history, compaction, hooks, commands, prompt resolution, state | | `backend/` | Backend interface + LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer) | | `toolsrv/` | Tool server: dynamic tool dispatch, sandboxed execution, result tiering, remote execution (ollie-remote) | | `session/` | Session lifecycle, config, persistence | | `detach/` | Background process management (ring buffer, signal) | | `elevate/` | Elevation broker (privilege escalation daemon, policy) | | `sandbox/` | Landrun sandbox configuration and command wrapping | | `env/` | Session environment helpers | | `log/` | Structured logging | | `paths/` | XDG path resolution | ### Key Types **`agent.Agent`** — the concrete struct frontends drive: ```go type Agent struct { // unexported fields — no public interface } func New(cfg AgentConfig) *Agent func (a *Agent) Submit(ctx context.Context, input string) func (a *Agent) Interrupt(cause error) bool func (a *Agent) Queue(prompt string) func (a *Agent) State() string func (a *Agent) WaitChange(ctx context.Context, field, current string) (string, bool) func (a *Agent) Close() // ... model/backend info, CWD, usage, cost, etc. ``` **`backend.Backend`** — LLM provider contract: ```go type Backend interface { ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error) Name() string Model() string SetModel(model string) ContextLength(ctx context.Context) int Models(ctx context.Context) []string } ``` **`toolsrv.Runner`** — minimal tool execution interface: ```go type Runner interface { ListTools() ([]ToolInfo, error) CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) } ``` **`toolsrv.Server`** — the concrete tool server (implements Runner): ```go type Server struct { /* ... */ } ``` ### Agent Loop (`agent/loop.go`) The loop is the heart of the system. On each turn: 1. **Stream** — call `backend.ChatStream()` with messages + tool definitions 2. **Dispatch** — if the model returns tool calls, execute them via `toolsrv.Server` 3. **Update** — append assistant message + tool results to session history 4. **Repeat** — loop until the model produces a final text response (no tool calls) Safety mechanisms: - **Consecutive error limits**: soft nudge at 5, hard abort at 10 - **Replan gate**: after 8 rounds without a PLAN: block, inject a replan nudge - **Stall detection**: 5 rounds with unchanged LastAction triggers a nudge - **Max steps**: configurable per-agent; soft exit when reached - **Transient retry**: up to 3 retries with exponential backoff for rate limits and 5xx - **Context overflow**: triggers automatic compaction and retry ### Session & Context Management (`agent/history.go`) `History` owns the message history as a flat `[]backend.Message` slice. Key behaviors: - **TaskState**: structured JSON overlay (objective, plan_step, constraints, last_action, next_decision) injected at the top of every turn - **Compaction**: three-zone strategy when context reaches 50% of model limit: - **Cold zone**: structured task state summary - **Warm zone**: one-line decision index (10 messages) - **Hot zone**: last 8 messages verbatim - **Result tiers**: tool results classified as Hot (verbatim), Warm (summarized on compaction), or Cold (immediately collapsed) - **Persistence**: sessions saved as JSON to `{sessionsDir}/{id}.json` ### Hooks (`agent/hooks.go`) Lifecycle callbacks executed as shell commands with JSON payload on stdin: | Hook | When | Exit codes | |---|---|---| | `agentSpawn` | Session creation | 0=ok, 2=block | | `preTurn` | Before each turn (deprecated) | 0=inject stdout, 2=block | | `postTurn` | After each turn | 0=ok | | `preTool` | Before each tool call | 0=ok, 2=block execution | | `postTool` | After each tool call | 0=append, 2=replace result | | `preCompact` | Before compaction | 0=ok | | `postCompact` | After compaction | 0=ok | | `turnError` | On backend error | 0=handled (skip retries) | ### Prompt Resolution (`agent/prompt_resolver.go`) Agent configs declare prompts as a JSON array of shell commands. Each command is executed with `$OLLIE` and `$PWD` available; stdout is concatenated to form the system prompt. This enables dynamic, composable prompts assembled from template fragments. ## 9P Server (`cmd/olliesrv/`) `olliesrv` is the central daemon. It implements a 9P2000 file server that exposes the entire agent namespace: ### Namespace Layout ``` / ← root (owned by system user) ├── backends backends list ├── help help text ├── models model list (cached) ├── agents agent config list ├── tools global tool catalog (all discoverable tools on disk) ├── ctl root control (invalidate, kill) ├── eventwait global event stream (blocking read) ├── complete request-response: code completion ├── generate request-response: one-shot LLM generation ├── route request-response: model routing ├── elevate/ │ ├── policy global elevation policy │ └── pending/{id} pending elevation requests (approve/deny/persist) └── session/ ├── new write key=value to create session ├── idx session index (one line per agent) ├── {name}/ │ ├── env session environment variables │ ├── ctl session control (kill, save, invalidate) │ ├── plan session-scoped markdown checklist │ ├── id immutable session UUID │ ├── name mutable session name (write to rename) │ ├── elevate per-session elevation policy │ └── agent/ │ ├── new write config to create agent (rdwr) │ └── {aid}/ │ ├── prompt submit a prompt │ ├── prompt.prev last submitted prompt │ ├── fifo.in queue a prompt │ ├── fifo.out pop queued prompt │ ├── chat streaming chat log (blocking read) │ ├── log last 64KB of chat (non-blocking) │ ├── state current state (idle/thinking/calling) │ ├── statewait blocking read until state changes │ ├── cfg agent config (key=value) │ ├── ctl agent control (stop, compact, ...) │ ├── cwd working directory │ ├── id immutable agent UUID │ ├── name mutable agent name │ ├── offset byte offset after last user prompt │ ├── usage token usage stats │ ├── cost estimated cost │ ├── ctxsz context size │ ├── models available models │ ├── systemprompt rendered system prompt │ ├── context rendered context window │ ├── tail exec helper for tailing chat │ ├── tools tool management (read = list loaded, write name = load) │ └── proc/{pid} detached process output ``` ### 9P Interaction Model All agent interaction uses the `ollie-9p` client, which auto-discovers the server via `$NAMESPACE` and identifies the caller via `$OLLIE_UNAME`. The 9P filesystem exposes session state, control, tool management, and inter-agent communication as files: - Sessions are directories under `session/` - Session creation is a two-step process: `session/new` (name only) then `session/{name}/agent/new` (full config) - Sessions and agents have **immutable UUIDs** (`id` file) and **mutable human-readable names** (`name` file); writing to `name` renames the entry - **Tool loading is a file write** — writing a tool name to `agent/{id}/tools` loads it for the agent; reading it lists currently loaded tools - **Global tool catalog** at `/tools` lists all discoverable tools on disk - Prompts, state, chat history, and config are readable/writable files - Streaming via blocking reads: `chat`, `statewait`, `eventwait` block the 9P read until data arrives - Permission enforcement uses 9P user principals - The entire namespace is declared as a single EDSL tree in `fs/spec.go` — see `doc/edsl.md` ### Permission Model (`fs/spec.go`) Permissions are declared inline in the EDSL spec via `mode` and `GID()` options. There is no separate permission registry — the spec is the source of truth: - `prompt` is mode `0666` — group+other can write, owner (the agent) cannot - `chat` is mode `0444` — read-only for everyone - `ctl` is mode `0666` — anyone can send control commands - `plan` is mode `0666` — readable by peer agents - `eventwait` is `0444` — blocking read for global event stream - `session/new` is `0666` with `Request` handler — anyone can create sessions - Ownership inherits: empty `UID`/`GID` values inherit from parent node, all the way up to root ### Session Management (`fs/`) The `*fs.Tree` IS the session collection. Package functions manage lifecycle: - `NewRoot(cfg)` — create the root tree from the EDSL spec (`fs/spec.go` → `BuildTree` → wired `*Tree`) - `Lookup(tree, id)` — find a session by ID - `All(tree)` — list all sessions - `CreateFromRoot(tree, args)` — create a new session (two-step: session name, then agent creation) - `KillFromRoot(tree, id)` — kill a session - `RenameFromRoot(tree, old, new)` — rename a session - `Shutdown(tree)` — clean shutdown (interrupt all, wait for idle, persist, close) Event streaming uses a global ring buffer (`eventRing`) with a global `/eventwait` file. Events carry prefixes (`S` for session, `A` for agent) with structured descriptions (`new`, `kill`, `rename`). Frontends block on `/eventwait` and receive delta events without polling.