ollie/doc/ARCHITECTURE.md

12 KiB

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. All clients communicate via 9P. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only. 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.
  • **One integration path: 9P filesystem. Streaming via blocking reads. No polling.
  • 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
├── 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

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"]
    end
    subgraph Server["olliesrv"]
        direction TB
        NS["9P Namespace\nsession/ · backends · models"]
    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:

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:

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:

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):

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 The adapter is best-effort: if no session bus is available (e.g., headless/container), olliesrv continues functioning as a pure 9P server.