ollie/doc/ARCHITECTURE.md

23 KiB

ollie Architecture

Philosophy

ollie's design philosophy is Emacs: a small, extensible core. The Go runtime (agent.Core) defines what an agent is — an event loop, a backend connection, a message history, and seven built-in primitives (shell, tool_*, skill_*). 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.
  • Seven built-in primitives. shell, tool_load/tool_list/tool_active, 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

The project is a monorepo with Git submodules:

ollie/
├── core/           → ollie-core      Go library: agent loop, backends, tools, config
├── 9p/             → ollie-9p         9P filesystem server (olliesrv)
├── kde/            → ollie-kde        KDE GUI, plasmoid, Kate plugin, KRunner, tray
├── httpgw/         → ollie-httpgw     HTTP-to-D-Bus gateway
├── webui/          → ollie-webui      Preact SPA (browser frontend)
├── acme/           → ollie-acme       Plan 9 acme editor frontend
├── el/             → ollie-el         Emacs frontend (ellie.el)
├── skills/         → agent-skills     Domain knowledge files
├── agents/         Agent config JSONs (default, coding, orchestrator, worker, ...)
├── prompts/        Prompt templates (markdown)
├── tools/          Tool scripts (file_read, lsp_*, memory_*, subagent_*, ...)
├── scripts/        User-facing scripts organized by namespace (s/, u/, x/)
├── sandbox/        Sandbox config YAML
└── doc/            Documentation

System Overview

┌───────────────────────────────────────────────────────────────────────────┐
│                              Frontends                                    │
│  s/sh (terminal) │ acme │ ellie (Emacs) │ KDE GUI/plasmoid │ Web UI      │
└───────┬──────────┴───┬──┴───────┬───────┴────────┬─────────┴──────┬──────┘
        │ 9P           │ 9P       │ 9P             │ D-Bus          │ D-Bus
        │              │          │                ▼                ▼
        │              │          │     ┌──────────────────────────────────┐
        │              │          │     │  org.ollie.SessionManager (D-Bus)│
        │              │          │     └──────────────┬───────────────────┘
        ▼              ▼          ▼                    │ embedded adapter
┌───────────────────────────────────────────────────────▼───────────────────┐
│                    olliesrv (9P server + D-Bus adapter)                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                                │
│  │ Sessions │  │ backends │  │  models  │                                │
│  │  s/      │  │ (file)   │  │  (file)  │                                │
│  └────┬─────┘  └──────────┘  └──────────┘                                │
└───────┼───────────────────────────────────────────────────────────────────┘
        │ per-session
        ▼

D-Bus-only mode: olliesrv -no9p (same interface, no filesystem listener)
┌────────────────────────────────────────────────────────────────┐
│                    agent.Core (per session)                     │
│  ┌─────────────┐  ┌──────────────┐  ┌────────────────────────┐ │
│  │ Agent Loop  │  │   Session    │  │  tools.Dispatcher      │ │
│  │ (loop.go)   │  │ (messages,   │  │  ┌──────────────────┐  │ │
│  │             │  │  task state,  │  │  │ execute.Server   │  │ │
│  │ stream ←────┼──┤  compaction)  │  │  │ (shell,          │  │ │
│  │ dispatch ───┼──┼───────────────┼──┤  │  tool_*, skill_*)│  │ │
│  └─────────────┘  └──────────────┘  │  └────────┬─────────┘  │ │
│                                      └───────────┼────────────┘ │
└──────────────────────────────────────────────────┼──────────────┘
                                                   │
        ┌──────────────────────────────────────────┼──────┐
        │              Tool Execution               │      │
        │  ┌─────────────┐  ┌───────────────────┐  │      │
        │  │  landrun     │  │  Tool Scripts     │  │      │
        │  │  (sandbox)   │  │  (OLLIE_TOOLS_PATH)│ │      │
        │  └─────────────┘  └───────────────────┘  │      │
        └──────────────────────────────────────────────────┘
                                                   │
                                                   ▼
                                          ┌─────────────────┐
                                          │  LLM Backends   │
                                          │ Ollama│OpenAI│  │
                                          │ Anthropic│...   │
                                          └─────────────────┘

Core Library (core/)

The core is a Go library (ollie/) with no binary. Frontends import it as a dependency.

Package Layout

Package Purpose
agent/ Core interface, agent loop, compaction, hooks, session persistence, config loading
backend/ Backend interface + LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer)
tools/ Server/Dispatcher interfaces, tool registry, discovery, schema parsing
execute/ Sandboxed shell execution, promoted tool dispatch, skill dispatch, remote SSH
detach/ Background process management (ring buffer, signal)
skills/ Skill metadata parsing and per-session registry
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 Interfaces

agent.Core — the public API frontends drive:

type Core interface {
    Submit(ctx context.Context, input string)
    Interrupt(cause error) bool
    Queue(prompt string)
    State() string           // "idle" | "thinking" | "calling: <tool>"
    WaitChange(ctx context.Context, field, current string) (string, bool)
    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
}

tools.Server — any tool provider:

type Server interface {
    ListTools() ([]ToolInfo, error)
    CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error)
}

tools.Dispatcher — routes tool calls to servers:

type Dispatcher interface {
    AddServer(name string, s Server)
    ListTools() ([]ToolInfo, error)
    Dispatch(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error)
}

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 tools.Dispatcher
  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/session.go)

Session 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 (9p/)

olliesrv is the central daemon. It implements a 9P2000 file server that exposes the entire agent namespace:

Namespace Layout

Path Contents
s/ Sessions — each session is a directory
s/{id}/chat Full chat history (streamable)
s/{id}/prompt Write to submit a prompt
s/{id}/ctl Control commands (stop, kill, compact, model, ...)
s/{id}/cfg Session config (key=value)
s/{id}/state Current state (idle/thinking/calling)
s/{id}/statewait Blocking read until state changes
s/{id}/plan Session-scoped markdown checklist

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 s/
  • Prompts, state, chat history, and config are readable/writable files
  • Permission enforcement uses 9P user principals

Permission Model (9p/fs/perm.go)

All permissions are declared in a single registry (fs.Perms). Key design:

  • prompt is mode 0466 — 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 Manager (9p/session/)

The Manager handles session lifecycle:

  • Creating sessions (write to s/new)
  • Killing sessions (delete s/{id} or write "kill" to s/{id}/ctl)
  • Renaming sessions
  • Assigning unique numeric UIDs for 9P principals
  • Wiring up agent.Core with backends and tool servers
  • Persisting/restoring sessions across restarts

Embedded D-Bus Adapter (9p/dbus.go)

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
  • Allows KDE GUI, plasmoid, and web UI (via httpgw) to interact with sessions without speaking 9P
  • 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 Daemon (dbus/)

For D-Bus-only deployments (no 9P), run olliesrv -no9p. This disables the filesystem listener and per-session FUSE mounts 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 (execute.Server) that exposes three executors:

Primitive Purpose
shell Run a bash command in a sandbox

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 structured header comment declaring their name, description, parameters, and parallelism annotation:

  • 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 (Python)
  • memory_remember, memory_recall — persistent memory
  • reasoning_think — scratchpad for externalized reasoning
  • subagent_spawn, subagent_generate — sub-agent lifecycle
  • route — orchestrator dispatch

Scripts annotated ollie:parallel read can be fanned out concurrently by the agent loop.

Sandboxing (internal/sandbox/)

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

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 s/new → new session directory appears
  2. Prompt: write task to s/{id}/prompt
  3. Observe: read s/{id}/state or block on s/{id}/statewait
  4. Result: child writes to s/{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.

HTTP Gateway (httpgw/)

ollie-httpgw bridges HTTP clients (like the web UI) to the D-Bus session manager. It translates REST-style requests into org.ollie.SessionManager method calls and streams ChatUpdated signals back as Server-Sent Events.

This is the secondary integration path for non-LLM consumers (web UIs, dashboards, external tools) that want a conventional HTTP API without speaking 9P or D-Bus directly.

Frontends

Frontends connect via either 9P (filesystem) or D-Bus, depending on preference:

Frontend Technology Connection
s/sh Terminal (readline) 9P
ollie-acme Plan 9 acme editor 9P
ellie Emacs Lisp 9P
KDE GUI Qt/QML standalone app D-Bus
Plasmoid Plasma widget D-Bus
Kate plugin KTextEditor plugin D-Bus
KRunner Runner plugin D-Bus
Web UI Preact SPA D-Bus (via httpgw)

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 sh (interactive), b (one-shot), bfg (blocking), bbg (background)
u/ Utility compositions complete (code completion), optimize (prompt optimization), cascade, escalate
x/ System plugins elevate (privilege escalation), prime (prompt template renderer), freeloader

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

User input (via any frontend)
  │
  ▼ write to s/{id}/prompt
olliesrv
  │
  ▼ agent.Core.Submit()
agent loop (loop.go)
  │
  ├── backend.ChatStream()  →  LLM API call (streaming)
  │
  ├── tools.Dispatcher.Dispatch()  →  execute.Server
  │       │
  │       ├── {code} steps  →  landrun sandbox  →  subprocess
  │       └── {tool} steps  →  landrun sandbox  →  tool script
  │
  ├── session.update()  →  append to message history
  │
  └── loop (until no tool calls)
  │
  ▼ event bus → session log
Frontend reads s/{id}/chat (streaming)

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 tools.Server, register in NewDispatcherFunc
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. Seven built-in primitives — shell, tool_*, skill_* are the only compiled-in capabilities. 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.