# ollie Architecture ## Philosophy ollie is a small, extensible core library for building agentic systems. It has no binary of its own — frontends and consumers live in separate repos and import ollie as a dependency. The internal surface area is kept minimal; everything a consumer needs is exported under `pkg/`. ## Overview ## Package Layout ``` pkg/ agent/ — Core interface, agent loop, session and context management backend/ — Backend interface + implementations config/ — Agent config struct and loader env/ — Environment variable loading (env file + shell) log/ — Structured logging paths/ — Config and data directory resolution skills/ — Skill file discovery and loading store/ — Store interfaces and implementations (FlatDir, Session, Batch, Skill) tools/ — Server and Dispatcher interfaces tools/execute/ — execute.Server: execute_code internal/ sandbox/ — landrun sandbox config and command wrapper ``` ### Why internal/sandbox? The sandbox package is an implementation detail of `pkg/tools/execute`. It has no stable public API and no reason to be imported by consumers directly. Keeping it internal prevents accidental coupling. ## Extension Points Consumers extend ollie by implementing or composing its interfaces: - **`backend.Backend`** — swap or add LLM backends - **`tools.Server`** — add a new tool server; all servers are equal - **`tools.Dispatcher`** — replace the tool router entirely (e.g. remote dispatcher, mock) - **`agent.Core`** — the agent's public API; frontends drive it without knowing internals. Includes `Close()` to release session resources (tmpdir under `/tmp/ollie/`). All tool servers implement the same `tools.Server` interface. There is no special "builtin" concept — `execute.Server` is registered by name, and is torn down and recreated on `/agent` switches. Each built-in server package exports a `Decl` function (`func Decl(...) func() tools.Server`) — a parameterized factory that produces a fresh server instance. `tools.NewDispatcherFunc` takes a map of name→Decl result and returns a `func() tools.Dispatcher` suitable for `agent.AgentCoreConfig.NewDispatcher`. `execute.Decl(cwd string)` accepts a working directory used as `cmd.Dir` for sandboxed commands. Pass `""` to fall back to `os.Getwd()`. **Adding a new tool server:** 1. Implement `tools.Server` in a new package under `pkg/tools/` 2. Export `func Decl(...) func() tools.Server` 3. Register the Decl result by name in `tools.NewDispatcherFunc` — no frontend changes needed ## Data Flow ``` Frontend └── agent.Core.Submit(prompt) └── loop.run() ├── backend.ChatStream() — LLM call └── tools.Dispatcher.Dispatch(...) — tool dispatch ├── "execute" → tools.Server │ execute_code └── "" → tools.Server ``` All tool calls go through one `tools.Dispatcher.Dispatch` path. Every registered server is a `tools.Server`; the dispatcher routes by name and is agnostic to how any server is implemented. ## execute_code `execute_code` is the only built-in tool. It is a unified primitive that handles simple execution, sequential composition, and parallel fan-out through a single `steps` array of pipeline stages. Each stage is one of: - `{code, language}` — inline code (default: bash) - `{tool, args}` — named script from `OLLIE_TOOLS_PATH`; language detected from shebang - `{parallel: [...]}` — concurrent fan-out; each element is a `{code}` or `{tool}` step; outputs concatenated in submission order Stages run sequentially, each stage's stdout fed to the next stage's stdin. A single simple stage is the degenerate case and returns raw output directly. ``` steps=[{code: "ls"}] — simple execution steps=[{code: "grep error log"}, {code:"wc"}] — sequential pipeline steps=[{parallel:[{code:"wc a"},{code:"wc b"}]}] — parallel fan-out steps=[{parallel:[...]}, {code:"sort"}] — fan-out then transform ``` `OLLIE_TOOLS_PATH` defaults to `~/.config/ollie/tools`. `{tool}` steps resolve scripts from this path. ## Script Namespaces User-facing scripts are organized by the 9P namespace they are hosted under and installed to `~/.config/ollie/scripts/{ns}/`: | Namespace | Purpose | Scripts | |---|---|---| | `s/` | Session management and one-shot queries | `sh`, `ls`, `kill`, `cleanup`, `b`, `bfg`, `bbg` | | `u/` | Utility scripts; compositions of `s/` primitives | `optimize` | `s/b` is the core one-shot runner. `s/bfg` and `s/bbg` are thin wrappers: `s/bfg` blocks until the agent finishes and writes the result to stdout; `s/bbg` returns immediately and prints the session path. `u/` scripts have no special server backing — they are executable scripts that compose other namespaces via the filesystem. ## Tool Scripts All capabilities beyond `execute_code` itself are implemented as tool scripts in `OLLIE_TOOLS_PATH`, invoked via `{tool}` steps. The agent discovers available scripts via `t/idx`. Examples from the default install: `file_read`, `file_write`, `file_edit`, `file_glob`, `file_grep`, `memory_remember`, `memory_recall`, `reasoning_think`, `task_plan`, `task_complete`, `subagent_spawn`. ## Sub-agents Sub-agents are implemented as a tool script (`subagent_spawn`) on top of the `s/` namespace. The script writes a session spec to `s/new`, blocks on `s/{id}/statewait` per job in background subshells, joins on all, and concatenates results. The `s/` namespace handles agent lifecycle, cancellation, and result storage. For heterogeneous parallel delegation (N agents, N different prompts), a `{parallel:[...]}` stage composes with `subagent_spawn` — each parallel step is one shell invocation with a distinct prompt: ``` execute_code {parallel} stage ├── subagent_spawn "prompt A" → s/{id-0}/chat ├── subagent_spawn "prompt B" → s/{id-1}/chat └── subagent_spawn "prompt C" → s/{id-2}/chat ↓ concatenated in submission order ``` The architecture boundary is clean: `execute_code` is the only built-in tool in `core`; everything else is a tool script; `s/` is a 9P namespace. No layer knows about the others. ## Typical Consumer Setup ```go newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{ "execute": execute.Decl(cwd), // "" falls back to os.Getwd() }) env := agent.BuildAgentEnv(cfg, newDispatcher(), cwd) core := agent.NewAgentCore(agent.AgentCoreConfig{ Backend: be, CWD: cwd, Env: env, NewDispatcher: newDispatcher, // ... }) ``` `BuildAgentEnv` wires together the pre-registered servers with the config. On `/agent` switches, `NewDispatcher` is called to produce a fresh dispatcher — all servers are torn down and recreated for the new agent config. `CWD` is preserved across switches. ## Session Lifecycle `NewAgentCore` creates `/tmp/ollie/{sessionID}` when a session starts. `Core.Close()` removes it recursively. olliesrv calls `Close()` from `killSession` (individual session removal) and `Shutdown` (server stop — iterates all sessions). `SetSessionID` renames the tmpdir atomically alongside the persisted session files. `!` shell commands run in the session's current working directory (`Core.CWD()`), not the process working directory. ## Session and Context `agent.Session` owns the message history. A `contextBuilder` enforces a rolling window to prevent unbounded prompt growth. When the window fills, older messages are either evicted (with a compaction notice injected) or proactively summarized via `/compact`. ## Sandboxing `internal/sandbox` wraps commands with [landrun](https://github.com/landlock-lsm/landrun) (Landlock LSM). Configuration is layered: 1. Named sandbox config (`~/.config/ollie/sandbox/.yaml`; `default` is used when unspecified) If `superpowerd` is running, commands are additionally wrapped with `superpowers run-session` for privilege scoping (optional, detected at runtime). ## HTTP Gateway `ollie-httpgw` is a thin translation layer between HTTP and 9P. It connects to `olliesrv` as a 9P client and exposes the same namespace over REST-style HTTP, so any HTTP client can interact with ollie without a 9P library. Each HTTP method maps to a 9P primitive: `GET` → read/stat, `POST` → write, `DELETE` → remove, `PATCH` → wstat (rename). The gateway serializes file content to JSON where the format is well-defined (key-value files, tab-separated records, session index); raw text falls through as-is. Sensitive internal paths (`tr/`, `help`, `statewait`, `systemprompt`, etc.) are blocked at the gateway layer and never exposed over HTTP. ``` HTTP client (browser, curl, script) │ HTTP/JSON :8080 ▼ ollie-httpgw │ 9P protocol ▼ olliesrv ($NAMESPACE/ollie or tcp:9564) ``` The gateway can connect to a local server via the Plan 9 namespace (default) or to a remote server via TCP (`-net tcp -addr host:9564`), making it equally useful for local setups and container/remote deployments. ## Frontends All frontends interact with the same 9P namespace, either directly or via httpgw: | Frontend | Interface | Talks to | |---|---|---| | `s/sh` | terminal (readline) | 9P filesystem directly | | ollie-tui | terminal (TUI) | `agent.Core` library | | ellie | Emacs buffer | 9P filesystem directly | | Web UI | browser (Preact SPA) | `ollie-httpgw` over HTTP | The web UI (`webui/`) is a Preact single-page application. It polls `/s/{id}/chat` every two seconds for new output and proxies all API paths to `ollie-httpgw`. It has no knowledge of 9P; the gateway is the only boundary. Frontends that use `agent.Core` directly (ollie-tui) embed the core library and manage sessions in-process. Frontends that use the filesystem (s/sh, ellie, web UI) are decoupled from the implementation entirely — they work equally well against a local or remote `olliesrv`.