ollie/doc/ARCHITECTURE.md

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

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:

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

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 (Landlock LSM). Configuration is YAML-based:

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: <result>
ARTIFACTS: <paths>

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:

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

{
  "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

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.