22 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"]
TUI["s/sh (terminal)"]
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:
- Stream — call
backend.ChatStream()with messages + tool definitions - Dispatch — if the model returns tool calls, execute them via
toolsrv.Server - Update — append assistant message + tool results to session history
- 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:
promptis mode0666— group+other can write, owner (the agent) cannotchatis mode0444— read-only for everyonectlis mode0666— anyone can send control commandsplanis mode0666— readable by peer agents
Session Management (fs/session/)
The *fs.Tree IS the session collection. Package functions manage lifecycle:
NewRoot(cfg)— create the root treeLookup(tree, id)— find a session by IDAll(tree)— list all sessionsCreateFromRoot(tree, args)— create a new sessionKillFromRoot(tree, id)— kill a sessionRenameFromRoot(tree, old, new)— rename a sessionShutdown(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,ChatUpdatedsignals- 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),
olliesrvcontinues 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 structured header comment declaring their name, description, parameters, and parallelism annotation:
file_read,file_write,file_edit,file_glob,file_grep— filesystem I/Olsp_definition,lsp_references,lsp_rename,lsp_symbols,lsp_diagnostics,lsp_hover,lsp_completion— LSP bridge (Python)memory_remember,memory_recall— persistent memoryreasoning_think— scratchpad for externalized reasoningsubagent_spawn,subagent_generate— sub-agent lifecycleroute— orchestrator dispatchgui_*— KDE desktop automation (screenshot, windows, clipboard, input, ...) Scripts annotatedollie:parallel readcan 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 classificationRateLimitError,TransientError,ContextOverflowError,ToolUnsupportedError— typed errors for loop controlGenerationParams— unified sampling parameters across all backends
Multi-Agent System
Multi-agent coordination is built entirely on the filesystem primitive:
Mechanism
- Spawn: write session spec to
session/new→ new session directory appears - Prompt: write task to
session/{id}/prompt - Observe: read
session/{id}/stateor block onsession/{id}/statewait - Result: child writes to
session/{parent_id}/promptwhen 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
SH["s/sh (terminal)"]
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
SH --> P9
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 | 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
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
- Two integration surfaces — 9P filesystem (canonical, Unix philosophy) and D-Bus (conventional, for GUI/web consumers). Both are thin adapters over the same core.
- 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. - Mandatory sandboxing — no unsandboxed fallback. Security is not optional.
- Streaming-only backends — simplifies the interface; blocking APIs wrap as single-event streams.
- Fire-and-forget sub-agents — no active waiting, no polling. Results arrive as filesystem writes.
- Prompt-as-code — system prompts are assembled by running shell commands, not by concatenating static strings.
- Per-agent principals — 9P permission enforcement prevents self-prompting loops and enforces access boundaries without application-level checks.