21 KiB
ollie Architecture
Philosophy
ollie asks: What if an AI agent were a Plan 9 process? The answer is a small Go runtime that defines what an agent is, then exposes its state and behaviors as files in a 9P namespace. Orchestration, scheduling, workflows, and UIs are pushed out to the surrounding environment — shell scripts, editors, web apps, cron, containers — rather than being built into the agent itself.
Design principles:
- Everything is a file. Agent state, control, chat history, tools, memory — all accessed via filesystem reads and writes.
- Subtract, don't add. The core is minimal; capabilities come from composing external scripts.
- One built-in tool.
execute_codeis the only tool compiled into the core. Everything else is a script. - No framework lock-in. Frontends are decoupled via the filesystem interface; any program that can read/write files can drive an agent.
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)
├── httpgw/ → ollie-httpgw HTTP-to-9P 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) │ Web UI (Preact)│
└────────┬──────────┴────┬───┴────────┬─────────┴───────┬─────────┘
│ 9P direct │ 9P direct │ 9P direct │ HTTP
│ │ │ ▼
│ │ │ ┌─────────────┐
│ │ │ │ ollie-httpgw│
│ │ │ └──────┬──────┘
▼ ▼ ▼ │ 9P
┌────────────────────────────────────────────────────────▼────────┐
│ olliesrv (9P server) │
│ ┌──────────┐ ┌──────────┐ ┌──────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ Sessions │ │ Memory │ │Skills│ │Tmp │ │ Tr │ │ ...│ │
│ │ s/ │ │ m/ │ │ sk/ │ │tmp/│ │tr/ │ │ │ │
│ └────┬─────┘ └──────────┘ └──────┘ └────┘ └────┘ └────┘ │
└───────┼─────────────────────────────────────────────────────────┘
│ per-session
▼
┌────────────────────────────────────────────────────────────────┐
│ agent.Core (per session) │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Agent Loop │ │ Session │ │ tools.Dispatcher │ │
│ │ (loop.go) │ │ (messages, │ │ ┌──────────────────┐ │ │
│ │ │ │ task state, │ │ │ execute.Server │ │ │
│ │ stream ←────┼──┤ compaction) │ │ │ (execute_code, │ │ │
│ │ dispatch ───┼──┼───────────────┼──┤ │ call_tool, pipe)│ │ │
│ └─────────────┘ └──────────────┘ │ └────────┬─────────┘ │ │
│ └───────────┼────────────┘ │
└──────────────────────────────────────────────────┼──────────────┘
│
┌──────────────────────────────────────────┼──────┐
│ Tool Execution │ │
│ ┌─────────────┐ ┌───────────────────┐ │ │
│ │ landrun │ │ Tool Scripts │ │ │
│ │ (sandbox) │ │ (OLLIE_TOOLS_PATH)│ │ │
│ └─────────────┘ └───────────────────┘ │ │
└──────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ LLM Backends │
│ Ollama│OpenAI│ │
│ Anthropic│... │
└─────────────────┘
Core Library (core/)
The core is a Go library (ollie/pkg/) with no binary. Frontends import it as a dependency.
Package Layout
| Package | Purpose |
|---|---|
pkg/agent/ |
Agent loop, Core interface, Runtime, Session, TaskState, hooks, prompt resolution |
pkg/backend/ |
Backend interface + implementations (Ollama, OpenAI, Anthropic) |
pkg/config/ |
Agent config struct and JSON loader |
pkg/env/ |
Environment variable loading (env file + shell) |
pkg/log/ |
Structured logging |
pkg/paths/ |
Config and data directory resolution |
pkg/tools/ |
Server and Dispatcher interfaces |
pkg/tools/execute/ |
The execute.Server (execute_code, call_tool, pipe) |
pkg/tools/execute/tool.go |
Named tool script discovery and execution |
internal/sandbox/ |
Landlock sandbox config and command wrapper |
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 (pkg/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
tools.Dispatcher - 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 (pkg/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 (pkg/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 (pkg/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 |
s/{id}/t/ |
Tool scripts visible to this session |
m/ |
Persistent memory files |
sk/ |
Domain skill files |
a/ |
Agent definitions (JSON) |
p/ |
Prompt templates |
u/ |
Utility scripts |
x/ |
System plugins and exec helpers |
tr/ |
Saved transcripts |
tmp/ |
Temporary storage |
Per-Agent 9P Principals
Each session gets its own FUSE mount at /tmp/ollie-mount-{uid} via ollie-9p-mount -u {uid}. The session's numeric UID is used as the 9P user principal, enabling permission enforcement:
- An agent cannot write to its own
promptfile (mode0466— owner has no write) - Cross-session access is controlled by standard Unix permission semantics
- The
$OLLIEenv var points to the session's private mount
Permission Model (9p/fs/perm.go)
All permissions are declared in a single registry (fs.Perms). Key design:
promptis mode0466— 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 Manager (9p/session/)
The Manager handles session lifecycle:
- Creating sessions (write to
s/new) - Killing sessions (delete
s/{id}or write "kill" tos/{id}/ctl) - Renaming sessions
- Assigning unique numeric UIDs for 9P principals
- Wiring up
agent.Corewith backends and tool servers
Tool System
Architecture
The tool system has exactly one built-in server (execute.Server) that exposes three executors:
| Executor | Purpose |
|---|---|
execute_code |
Run inline code steps (bash, python3, perl, lua, ...) |
call_tool |
Run named scripts from OLLIE_TOOLS_PATH |
pipe |
Sequential pipeline chaining stdout→stdin across steps |
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,cascade,escalate— tiered delegation
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 (pkg/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 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
s/new→ new session directory appears - Prompt: write task to
s/{id}/prompt - Observe: read
s/{id}/stateor block ons/{id}/statewait - Result: child writes to
s/{parent_id}/promptwhen done
Delegation Tools
| Tool | Semantics |
|---|---|
route |
Orchestrator dispatch; accepts --tier=worker|expert and --backend=<name> |
cascade |
Cheap worker delegation (fixed tier) |
escalate |
Strong expert delegation (fixed tier) |
subagent_spawn |
Raw session creation with full control |
subagent_generate |
JIT agent identity generation |
Tier resolution uses OLLIE_TIER_{WORKER|EXPERT}_{backend} env vars to map tiers to specific models per backend.
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 is a stateless translation layer:
| HTTP Method | 9P Operation |
|---|---|
| GET | read / stat / readdir |
| PUT | write (truncate) |
| POST | create |
| DELETE | remove |
| HEAD | stat (metadata in headers) |
| PATCH | wstat (rename) |
Connects to olliesrv via Plan 9 namespace (local) or TCP (remote). Sensitive paths (tr/, statewait, systemprompt) are blocked at the gateway layer.
Frontends
All frontends interact with the same 9P namespace:
| Frontend | Technology | Connection |
|---|---|---|
s/sh |
Terminal (readline) | 9P filesystem directly |
ollie-acme |
Plan 9 acme editor | 9P filesystem directly |
ellie |
Emacs Lisp | 9P filesystem directly |
| Web UI | Preact SPA | HTTP via ollie-httpgw |
Frontends are fully decoupled — they work identically against local or remote olliesrv instances.
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/x/prime SYSTEM_PROMPT",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-reasoning"
],
"hooks": {
"agentSpawn": ["$OLLIE/x/elevate start"],
"turnError": ["$OLLIE/x/freeloader"]
},
"trustedTools": ["escalate"],
"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 | Read/write the 9P namespace (directly or via httpgw) |
| 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
- 9P as the integration layer — not REST, not gRPC, not a library API. Files are the universal interface.
- Single built-in tool —
execute_codeis the only compiled-in capability. 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.