# ollie Architecture ## Philosophy ollie's design philosophy is Emacs: a small, extensible core. 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 the surrounding environment. 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. For desktop-native integration, `olliesrv` embeds a D-Bus adapter (`org.ollie.SessionManager`) alongside its 9P interface. The KDE GUI, plasmoid, and web UI connect this way. For D-Bus-only deployments, `-no9p` disables the 9P listener entirely. Design principles: - **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts. - **Three built-in primitives.** `shell`, tool registry (`tool_load`/`tool_list`/`tool_active`), and skill registry (`skill_load`/`skill_list`/`skill_active`) are the only tools compiled into the core. Everything else is a script. - **Two integration paths.** 9P filesystem (canonical) and/or D-Bus (conventional). Same core, different surfaces, selectable via flags. - **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 two Git submodules for decoupled frontends: ``` 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/session/ 9P filesystem tree for sessions ├── dbus/ D-Bus adapter (org.ollie.SessionManager) ├── 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 ├── mount/ 9P FUSE mount client ├── cmd/ Binaries: │ ├── olliesrv/ 9P server │ ├── ollie-9p/ 9P client │ └── ollie-remote/ Remote execution server ├── kde/ KDE integration (submodule) — plasmoid, GUI, Kate plugin, KRunner, tray ├── agents/ Agent config JSONs (default, coding, orchestrator, worker, ...) ├── prompts/ Prompt templates (markdown) ├── tools/ Tool scripts (file_read, lsp_*, memory_*, subagent_*, ...) ├── skills/ Domain knowledge files (markdown) ├── scripts/ User-facing scripts organized by namespace (s/, u/, x/) ├── sandbox/ Sandbox config YAML files └── doc/ Documentation ``` ## System Overview ```mermaid flowchart TB subgraph Frontends["Frontends"] ACME["acme (Plan 9)"] ELLIE["ellie (Emacs)"] KDE["KDE GUI / Plasmoid"] end subgraph Integration["Integration Layer"] direction LR P9["9P Filesystem"] DBUS["org.ollie.SessionManager (D-Bus)"] end subgraph Server["olliesrv"] direction TB NS["9P Namespace\nsession/ · backends · models"] DBA["Embedded D-Bus Adapter"] 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· shell execution\n· tool registry\n· landlock sandbox"] end subgraph Backends["LLM Backends"] OLLAMA["Ollama"] OPENAI["OpenAI / OpenRouter"] ANTHROPIC["Anthropic"] GEMINI["Gemini"] CW["CodeWhisperer"] end TUI --> P9 ACME --> P9 ELLIE --> P9 KDE --> DBUS WEB --> DBUS P9 --> NS DBUS --> DBA NS --> LOOP DBA --> LOOP LOOP <--> SESS LOOP --> TRV LOOP <--> Backends TRV --> Shell["sandboxed shell\n(landrun)"] TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)"] ``` ## 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: sandboxed shell execution, named tool dispatch, skill management, result tiering, remote execution | | `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 ``` session/ ├── new write key=value to create session ├── idx session index (tab-separated) ├── ls exec to list sessions ├── kill exec to kill a session ├── sh interactive shell ├── b batch one-shot (create, submit, wait, print, kill) ├── bfg batch foreground (submit to existing, wait, print) ├── bbg batch background (submit, return path) ├── cleanup kill idle sessions ├── {sid}/ │ ├── plan session-scoped markdown checklist │ ├── env session environment │ └── agent/ │ └── {aid}/ │ ├── cfg session config (key=value) │ ├── ctl control commands (stop, kill, compact, model, ...) │ ├── state current state (idle/thinking/calling) │ ├── statewait blocking read until state changes │ ├── chat full chat history (streamable) │ ├── offset byte offset after last user prompt │ ├── prompt write to submit a prompt │ ├── prompt.prev last submitted prompt │ ├── fifo.in queue a prompt (FIFO input) │ ├── fifo.out pop queued prompt (FIFO output) │ ├── context rendered context window │ ├── systemprompt rendered system prompt │ ├── usage token usage stats │ ├── cost estimated cost │ ├── ctxsz context size │ ├── models available models │ ├── tools tool management (read all, write name to load) │ ├── toolsrv.loaded currently loaded tools │ ├── toolsrv.rev revision counter │ ├── tail exec helper │ └── proc/ detached background processes ``` ### 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, and inter-agent communication as files: - Sessions are directories under `session/` - Prompts, state, chat history, and config are readable/writable files - Permission enforcement uses 9P user principals ### Permission Model (`fs/session/perm.go`) All permissions are declared in a single registry (`Perms`). Key design: - `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 ### Session Management (`fs/session/`) The `*fs.Tree` IS the session collection. Package functions manage lifecycle: - `NewRoot(cfg)` — create the root tree - `Lookup(tree, id)` — find a session by ID - `All(tree)` — list all sessions - `CreateFromRoot(tree, args)` — create a new session - `KillFromRoot(tree, id)` — kill a session - `RenameFromRoot(tree, old, new)` — rename a session - `Shutdown(tree)` — clean shutdown ### Embedded D-Bus Adapter `olliesrv` includes an embedded D-Bus adapter that claims `org.ollie.SessionManager` on the session bus. This provides: - The same method/signal interface used by KDE frontends - `SessionCreated`, `SessionKilled`, `StateChanged`, `ChatUpdated` signals - Lifecycle watchers that bridge session state changes to D-Bus signals The adapter is best-effort: if no session bus is available (e.g., headless/container), `olliesrv` continues functioning as a pure 9P server. ## D-Bus Adapter (`dbus/`) The D-Bus adapter is embedded in `olliesrv` (no separate daemon). For D-Bus-only deployments, run `olliesrv -no9p`. This disables the filesystem listener while keeping the full D-Bus interface. Session persistence uses the same `~/.local/share/ollie/sessions/active/` directory. ## KDE Integration (`kde/`) The KDE submodule provides several Qt/QML components that connect to `org.ollie.SessionManager` over D-Bus: | Component | Path | Purpose | |---|---|---| | GUI | `kde/gui/` | Standalone Qt window for chat interaction | | Plasmoid | `kde/plasmoid/` | Plasma widget with `OllieDBusClient` | | Kate plugin | `kde/kate/` | In-editor chat pane and ghost completion | | KRunner | `kde/krunner/` | Quick-launch sessions from KRunner | | System tray | `kde/tray/` | Tray icon with session status | All KDE components use `QDBusServiceWatcher` to handle daemon restarts gracefully — recreating the `QDBusInterface` when the service reappears and refreshing the session list. ## Tool System ### Architecture The tool system has exactly one built-in server (`toolsrv.Server`) that exposes three primitives: | Primitive | Purpose | |---|---| | `shell` | Run a bash command in a sandbox | | `tool_load`/`tool_list`/`tool_active` | Dynamic tool registry | | `skill_load`/`skill_list`/`skill_active` | Dynamic skill registry | All other capabilities are **tool scripts** — executable files discovered from `OLLIE_TOOLS_PATH` (default: `~/.config/ollie/tools/`). ### Tool Scripts Tool scripts are plain executables with a `.meta` sidecar JSON file declaring description, parameters, tier, and parallelism class: - `file_read`, `file_write`, `file_edit`, `file_glob`, `file_grep` — filesystem I/O - `lsp_definition`, `lsp_references`, `lsp_rename`, `lsp_symbols`, `lsp_diagnostics`, `lsp_hover`, `lsp_completion` — LSP bridge (Go, `tools/lsp/`) - `memory_remember`, `memory_recall` — persistent memory - `reasoning_think` — scratchpad for externalized reasoning - `subagent_spawn`, `subagent_generate` — sub-agent lifecycle - `route` — orchestrator dispatch - `gui_*` — KDE desktop automation (screenshot, windows, clipboard, input, ...) Tools marked `"readOnly": true` in their `.meta` can be fanned out concurrently by the agent loop. ### Sandboxing Every code execution step is wrapped with [landrun](https://github.com/landlock-lsm/landrun) (Landlock LSM). Configuration is YAML-based: ```yaml general: best_effort: true filesystem: ro: ["/usr", "/lib", "/etc", "{HOME}/.config/ollie"] rox: ["/usr/bin", "/usr/local/bin"] rw: ["{CWD}", "{TMPDIR}"] network: enabled: true unrestricted: true ``` Template variables (`{CWD}`, `{HOME}`, `{XDG_*}`, `{OLLIE_*}`) are expanded at runtime. **landrun is mandatory** — there is no unsandboxed fallback. Steps marked `elevated: true` bypass the sandbox via the elevation backend (`x/elevate`). ## LLM Backends (`backend/`) All backends implement the streaming-only `Backend` interface: | Backend | Wire Format | Notes | |---|---|---| | `OllamaBackend` | `/api/chat` | Local models, context length from `/api/show` | | `OpenAIBackend` | `/v1/chat/completions` | OpenAI, OpenRouter, any compatible API | | `AnthropicBackend` | `/v1/messages` | Extended thinking, prompt caching | | `GeminiBackend` | Gemini API | Google Gemini models | | `CopilotBackend` | GitHub Copilot API | Copilot Chat | | `CodeWhispererBackend` | CodeWhisperer API | AWS CodeWhisperer | Shared infrastructure: - `streamRequest()` — common HTTP streaming with error classification - `RateLimitError`, `TransientError`, `ContextOverflowError`, `ToolUnsupportedError` — typed errors for loop control - `GenerationParams` — unified sampling parameters across all backends ## Multi-Agent System Multi-agent coordination is built entirely on the filesystem primitive: ### Mechanism 1. **Spawn**: write session spec to `session/new` → new session directory appears 2. **Prompt**: write task to `session/{id}/prompt` 3. **Observe**: read `session/{id}/state` or block on `session/{id}/statewait` 4. **Result**: child writes to `session/{parent_id}/prompt` when done ### Delegation Tools | Tool | Semantics | |---|---| | `route` | Orchestrator dispatch; selects backend+model for a task | | `subagent_spawn` | Raw session creation with full control | | `subagent_generate` | JIT agent identity generation | ### Callback Protocol ``` [from={session_id}] STATUS: done|error SUMMARY: ARTIFACTS: ``` The parent never actively waits — results arrive as queued prompts. This is the secondary integration path for non-LLM consumers (web UIs, dashboards, external tools) that want a conventional HTTP API without speaking 9P directly. ## Frontends Frontends connect via either 9P (filesystem) or D-Bus, depending on preference: ```mermaid flowchart LR subgraph Core["agent.Agent"] AG["Agent Engine"] end subgraph Surfaces["Integration Surfaces"] P9["9P Filesystem\n(session/ namespace)"] DB["D-Bus\n(org.ollie.SessionManager)"] end subgraph Frontends ACME["acme (Plan 9)"] EL["ellie (Emacs)"] KG["KDE GUI"] KP["KDE Plasmoid"] KK["Kate Plugin"] KR["KRunner"] HTTP["curl / scripts"] end AG --> P9 AG --> DB ACME --> P9 EL --> P9 HTTP --> P9 KG --> DB KP --> DB KK --> DB KR --> DB WEB --> DB ``` Frontends are fully decoupled — the core doesn't know which interface they use. ## Script Namespaces User-facing scripts installed to `~/.config/ollie/scripts/{ns}/`: | Namespace | Purpose | Key Scripts | |---|---|---| | `s/` | Session management | `ls`, `kill`, `cleanup` | | `u/` | Utility compositions | `complete` (code completion), `optimize` (prompt optimization) | | `x/` | System plugins | `elevate` (privilege escalation), `prime` (prompt template renderer) | ## Agent Configuration Agent configs are JSON files in `~/.config/ollie/agents/`: ```json { "prompt": [ "$OLLIE_CFG_PATH/scripts/x/prime SYSTEM_PROMPT", "$OLLIE_CFG_PATH/scripts/x/prime session", "$OLLIE_CFG_PATH/scripts/x/prime agent-coding", "$OLLIE_CFG_PATH/scripts/x/prime tools-file", "$OLLIE_CFG_PATH/scripts/x/prime tools-reasoning" ], "hooks": { "agentSpawn": [], "turnError": ["$OLLIE_CFG_PATH/scripts/x/freeloader $OLLIE_SESSION_ID"] }, "maxTokens": 8192, "temperature": 0.7, "maxSteps": 50 } ``` The `prompt` array is the key innovation: each element is a shell command whose stdout is concatenated to form the system prompt. This makes prompts composable, dynamic, and environment-aware. ## Data Flow ```mermaid sequenceDiagram participant F as Frontend participant S as olliesrv participant A as agent.Agent participant B as LLM Backend participant T as toolsrv.Server participant SH as sandbox (landrun) F->>S: write to session/{id}/prompt S->>A: Submit() activate A loop Agent Loop A->>B: ChatStream(messages, tools) activate B B-->>A: stream events deactivate B alt has tool calls A->>T: Dispatch(tool, args) activate T T->>SH: sandboxed execution SH-->>T: result T-->>A: ToolResult deactivate T A->>A: append to history else no tool calls A->>A: turn complete break end end A-->>S: turn complete deactivate A S-->>F: readable via session/{id}/chat ``` ## Extension Points | What | How | |---|---| | Add an LLM backend | Implement `backend.Backend` | | Add a tool | Drop an executable into `OLLIE_TOOLS_PATH` | | Add a tool server | Implement `toolsrv.Runner`, register in `NewToolServer` | | Add a frontend | Connect via 9P (filesystem) or D-Bus (method calls) | | Add a skill | Drop a markdown file into `skills/` | | Add an agent persona | Create a JSON config in `agents/` | | Customize sandbox | Create a YAML in `~/.config/ollie/sandbox/` | ## Key Design Decisions 1. **Two integration surfaces** — 9P filesystem (canonical, Unix philosophy) and D-Bus (conventional, for GUI/web consumers). Both are thin adapters over the same core. 2. **Three built-in primitives** — `shell`, tool registry, skill registry. Everything else is a script. This keeps the core small and makes all other tools equal citizens. 3. **Mandatory sandboxing** — no unsandboxed fallback. Security is not optional. 4. **Streaming-only backends** — simplifies the interface; blocking APIs wrap as single-event streams. 5. **Fire-and-forget sub-agents** — no active waiting, no polling. Results arrive as filesystem writes. 6. **Prompt-as-code** — system prompts are assembled by running shell commands, not by concatenating static strings. 7. **Per-agent principals** — 9P permission enforcement prevents self-prompting loops and enforces access boundaries without application-level checks.