18 KiB
Architectural Evolution
How the Ollie system-of-systems emerged and evolved. ~1300 commits over ~3.5 months (Apr 11 – Jul 30, 2026).
Timeline
gantt
title Ollie Evolution
dateFormat YYYY-MM-DD
axisFormat %b %d
section Foundation
Monorepo + submodules :done, 2026-04-11, 14d
9P filesystem :done, 2026-04-14, 28d
section Core Primitives
Session lifecycle (session/) :done, 2026-04-20, 30d
Tool scripts :done, 2026-05-01, 20d
Skills system :done, 2026-05-10, 14d
section Execution
execute_code / shell :done, 2026-05-08, 20d
Batch jobs (b/ → session/bfg) :done, 2026-05-15, 25d
Sandbox (landrun) :done, 2026-05-20, 40d
section Multi-Agent
Subagent spawn :done, 2026-05-25, 15d
Cascade orchestrator :done, 2026-06-05, 20d
JIT agent generation :done, 2026-07-10, 10d
section Frontends
TUI + Emacs (ellie) :done, 2026-04-15, 45d
Acme (Plan 9) :done, 2026-05-20, 30d
KDE/Plasma :done, 2026-06-10, 40d
section Control Planes
D-Bus adapter (ollied) :done, 2026-06-25, 20d
D-Bus embedded in srv :done, 2026-07-15, 10d
section Late Evolution
Remote execution :done, 2026-07-05, 15d
Elevation broker :done, 2026-07-10, 15d
Tool registry (lazy) :done, 2026-07-20, 10d
Reactions / feedback :done, 2026-07-22, 8d
FUSE → 9P client :done, 2026-07-25, 5d
The Great Flattening :done, 2026-07-29, 2d
Phase 1: Monorepo Bootstrap (Apr 11)
Started as independent git repos unified under a monorepo with submodules. Initial components:
- agent — agent loop (Go): backend dispatch, tool execution, session state
- fs/session — 9P file server exposing agent sessions as a synthetic filesystem
- tui — terminal UI (later removed)
- el — Emacs integration (ellie.el)
Build system was
mkfile(Plan 9 make), laterMakefile, finallyjustfile.
Phase 2: The 9P Decision (Apr 14–28)
The defining architectural choice: every agent primitive is a file.
Sessions are directories. Sending a prompt is writing to session/{id}/prompt.
Reading state is cat session/{id}/state. This made the control plane
protocol-agnostic — any program that can read/write files can be a frontend.
Key files crystallized: prompt, chat, state, ctl, cfg, statewait.
The session/new file (write key=value pairs, read back session ID) became the
session factory.
Phase 3: Tool System (Apr 28 – May 10)
Tools evolved through several incarnations:
- MCP servers (denote-mcp, 9beads-mcp) — external processes, heavyweight
- Shell/Python scripts in
~/.config/ollie/tools/— lightweight, sandboxed - execute_code as the single built-in tool — all scripts invoked through it
- call_tool/pipe — named tool dispatch and cross-tool pipelines
- Tool registry (final form) — dynamic lazy-loading via
tool_list/tool_load/tool_active;execute_coderenamed toshell; tools hot-reloadable without server restart The file tools (file_read,file_write,file_edit,file_grep,file_glob) were extracted into standalone Python scripts. Memory (memory_remember,memory_recall) and reasoning (reasoning_think) followed the same pattern. MCP servers were removed early. Simple scripts won over complex daemons.
Phase 4: Planning System Churn (May – Jul)
Planning went through the most iterations of any subsystem:
pl/directory namespace with flat file-per-taskplan_create/plan_completetools- Beads integration (external issue tracker) — made optional, then dropped
task_add/task_checktools- Inline markdown plans forbidden
- Final form: a single
session/{id}/planfile (markdown checklist), written by the agent, persisted across context compaction The lesson: planning needed to be simple, agent-controlled, and not bureaucratic.
Phase 5: Multi-Agent Coordination (May 25 – Jun 15)
Three patterns emerged:
- subagent_spawn — fire-and-forget session creation, parent never blocks
- subagent_generate — JIT agent config generation (role, constraints, tools)
- cascade — script-driven fan-out with
-max-workers,-retries, optional synthesis step
flowchart TB
UP["User prompt"]
PARENT["Parent Session"]
W1["Worker 1"]
W2["Worker 2"]
W3["Worker 3"]
CASCADE["u/cascade\n(spawn + throttle)"]
CW1["Cascade Worker"]
CW2["Cascade Worker"]
UP --> PARENT
PARENT -- subagent_spawn --> W1
PARENT -- subagent_spawn --> W2
PARENT -- subagent_spawn --> W3
W1 --> PARENT
W2 --> PARENT
W3 --> PARENT
PARENT -- cascade --> CASCADE
CASCADE --> CW1
CASCADE --> CW2
Inter-agent communication uses the filesystem: agents write to each other's
prompt file.
Phase 6: Frontend Proliferation
flowchart TB
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
- s/sh — shell script frontend (bash, briefly rc, back to bash)
- acme — Plan 9 editor integration with
Oi(quick query),Kmpl(completion) - ellie.el — Emacs with ghost-text completion
- ollie-kde — full Plasma integration: plasmoid, KRunner, Kate plugin, standalone GUI, GUI automation tools
- TUI — removed (Jul) in favor of s/sh and richer GUIs
Phase 7: Security Model (May – Jul)
flowchart LR
subgraph Permissions["9P Permissions"]
OWNER["Owner → rw"]
AGENT["Agent → restricted"]
PEERS["Peers → read-only"]
end
subgraph Execution["Execution Sandbox"]
AGT["Agent"]
SANDBOX["landrun sandbox\n(restricted fs)"]
TOOLS["Tool scripts"]
ELEV["x/elevate\n(socket broker)"]
PRIV["Privileged Action"]
end
AGT --> SANDBOX --> TOOLS
TOOLS -- needs escape --> ELEV --> PRIV
Evolution:
- No sandboxing initially
- YAML-based sandbox configs (landrun) for execute_code
- Per-session 9P identity — agents can't read other sessions' tools
- Elevation broker: socket-based, user-confirmed privilege escalation
- SSH agent proxy added then removed (too much attack surface)
- Final: integrated elevation broker replaces separate superpowerd adapter
Phase 8: D-Bus as Second Control Plane (Jun 25 – Jul)
Originally everything was 9P-only. D-Bus was added for desktop integration:
- Separate
ollie-dbusdaemon (ollied) - Embedded directly into
olliesrv(9P server got-no9pflag) - KDE uses D-Bus exclusively; acme/sh/el use 9P
- 9P and D-Bus do not share sessions — independent stores, same core
Phase 9: Remote Execution (Jul 5–19)
Split-brain architecture: orchestration stays local, code execution on a remote machine via SSH.
ollie-remotebinary auto-deployed via SSH bootstrap- Tools embedded in remote binary
- Streaming output notifications back to local session
- Session persistence across remote restarts
Phase 10: Tool Registry (Jul 20–29)
The final major architectural shift — from "agent knows all tools upfront" to lazy discovery:
- Tools embed their own
ollie:promptmetadata blocks tool_list— discover available tools and descriptionstool_load— promote a tool to a native callabletool_active— introspect loaded tools- Skills get the same treatment:
skill_list,skill_load,skill_activeThis solved system prompt bloat — only load what's needed per task.
Phase 11: The Great Flattening (Jul 29–30)
Largest single-day structural change: −4,573 lines net across the codebase. Eliminated accumulated abstractions that no longer served a purpose.
Core (−4,400 lines)
- Killed
agent.Coreinterface — concrete*agent.Agentused directly. Nobody else implemented Core; the interface just added indirection. - Killed
Dispatcherindirection in toolsrv —toolsrv.Servercalled directly. - Merged
execute/intotools/— separate package for 3 functions was pointless overhead. - Renamed for clarity:
agent.Session→agent.History,session.Core→session.Session,NewAgentCore→New. - Extracted
agent/package from the monolithic session package. - Threaded context.Context properly through daemon → session → agent (replaced ad-hoc interrupt mechanisms with cancellation).
- Dead code removal:
GlobalToolNames,ExtractReturnSchema.
9P server (−214 lines)
- Killed
mgr/package entirely — replacedManagerstruct with package functions infs/session/. The*fs.TreeIS the session collection;rootState(unexported) lives intree.Data. CRUD viaNewRoot,Lookup,Create,Kill,Rename,Shutdown. - Fixed infinite recursion in
session/{sid}/agent/directory —pathType()didn't recognize agent subdirectories, causing walk to succeed at any depth. - Fixed phantom session descent — walk into non-existent sessions now fails immediately.
- Fixed
/agentsempty read —makeStatwasn't computing Length for root files, FUSE kernel saw 0 bytes and never issued a read. - Fixed
session.All()— was looking atroot.Children()(empty) instead ofrootState.sessions. Broke D-Bus ListSessions and GUI session restore.
Frontends
- acme: paths updated
s/→session/, agent files route throughsession/{id}/agent/{aid}/. - ellie.el: same path migration, added
ellie--agent-dirfor agent ID discovery. - KDE GUI: no changes needed (D-Bus operates on Go objects, not paths).
New filesystem layout
session/
├── new write key=value to create session
├── idx session index (tab-separated)
├── {sid}/
│ ├── plan session-scoped markdown checklist
│ ├── env session environment
│ └── agent/
│ └── {aid}/
│ ├── cfg session configuration (key=value)
│ ├── ctl control commands
│ ├── state current state
│ ├── statewait blocks until state changes
│ ├── chat conversation log
│ ├── prompt submit prompts
│ ├── prompt.prev last submitted prompt
│ ├── offset byte offset after last user 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 stats
│ ├── cost cost estimate
│ ├── ctxsz context size
│ ├── models available models
│ ├── tools tool management
│ ├── toolsrv.loaded currently loaded tools
│ ├── toolsrv.rev revision counter
│ ├── tail exec helper
│ └── proc/ detached process output
Architecture after (single Go module, no submodules for core/9p)
*fs.Tree (root) ← IS the session collection
└── .Data = *rootState ← sessions map, config, nextUID
└── sessions[id] = *Session ← owns Agent, context, cancel
Package functions (no Manager):
session.NewRoot(cfg) → *fs.Tree
session.Lookup(tree, id) → *Session
session.All(tree) → []*Session
session.CreateFromRoot(tree, args) → (id, error)
session.KillFromRoot(tree, id)
session.RenameFromRoot(tree, old, new)
session.Shutdown(tree)
SLOC after flattening
| Component | Lines |
|---|---|
| agent + backend + toolsrv + session | 10,437 |
| fs/session + cmd/olliesrv | 7,925 |
| kde gui | 12,331 |
| Total | 32,538 |
| Zero dead exported functions remain (verified via LSP + grep across all repos). |
Phase 12: Second Flattening & .meta Sidecar (Jul 30)
Another aggressive structural pass. Eliminated the entire script namespace
layer (s/, u/, x/), flattened contrib/ into data/, and replaced
header-comment-based tool metadata with JSON sidecar files.
Scripts removed (−1,100 lines)
- s/{sh,b,bfg,bbg,ls,kill,cleanup} — shell-based session frontends replaced
by
ollie-9p rdwr generate/complete/routeand GUI frontends - u/{optimize,complete,cascade,escalate} — utility scripts replaced by
generate,complete,routefiles in the 9P namespace - x/{bd,prime,freeloader,task} — internal plumbing scripts; PATH prepend
(
prependOlliePath) removed from toolsrv - 9P session root trimmed to just
newandidx; script-serving code removed install-scriptstarget now empty
Tool metadata: .meta sidecar files
Header comment parsing (ollie:prompt, ollie:tier, ollie:parallel read,
args_json:) eliminated entirely. All tool metadata now lives in a JSON
sidecar file alongside the executable:
tools/
file_edit ← executable (any language)
file_edit.meta ← JSON metadata
{
"description": "Replace text in a file.",
"prompt": "## file_edit\n\n...",
"args": {"type":"object", ...},
"tier": "cold",
"readOnly": true
}
This decouples metadata from implementation language — compiled Go binaries, Python scripts, and bash tools all use the same discovery mechanism.
LSP tools ported to Go
The Python LSP bridge (_lib/lsp/) replaced with a pure Go implementation:
tools/
├── builtin/ ← in-process handlers (shell, reasoning, tool/skill registry)
└── lsp/ ← shared LSP client library
└── cmd/ ← individual binaries (lsp_definition, lsp_hover, ...)
Architecture: each LSP tool binary embeds a bridge daemon (started on first
invocation via --bridge flag, persists via Unix socket, auto-exits after 5min
idle). No Python. No _lib. No external dependencies beyond the LSP servers
themselves (gopls, clangd, intelephense).
Repo reorganization
contrib/{prompts,scripts,services,tools,skills,agents}→data/contrib/elispstays (Emacs frontend contribution)tools/builtin/— built-in tool handlers (moved fromtools/)tools/lsp/— Go LSP implementation (replaceddata/tools/_lib/lsp/)
Current Topology
ollie/ ← single Go module
├── agent/ ← agent loop, history, hooks, commands
├── backend/ ← LLM providers (6 backends)
├── toolsrv/ ← tool server, registry, remote execution
├── session/ ← session lifecycle, config
├── fs/session/ ← 9P filesystem tree
├── dbus/ ← D-Bus adapter
├── detach/ ← background process management
├── elevate/ ← elevation broker
├── sandbox/ ← landrun sandbox config
├── env/ ← environment helpers
├── log/ ← structured logging
├── paths/ ← XDG path resolution
├── mount/ ← 9P FUSE mount client (network transparency)
├── cmd/ ← binaries (olliesrv, ollie-9p, ollie-remote)
├── kde/ ← KDE Plasma (submodule)
├── data/tools/ ← tool executables + .meta sidecar files
├── data/agents/ ← agent configs (JSON)
├── data/prompts/ ← system prompt fragments (markdown)
├── data/skills/ ← domain knowledge modules (markdown)
├── data/scripts/ ← ollie-remount, ollie-watchdog
├── data/services/ ← systemd, xdg-autostart
├── prompts/ ← embedded prompt templates
├── sandbox/ ← landrun sandbox profiles (YAML)
└── doc/ ← documentation
Principles That Emerged
- Filesystem-as-API — everything is read/write on synthetic files. No custom protocols needed. Monorepo coordinates versions.
- Scripts over servers — MCP servers removed early. Simple scripts won.
- Progressive disclosure — lazy tool/skill loading keeps system prompts small until complexity is needed.
- Plan simplicity — after 5 iterations, planning settled on one markdown checklist file per session.
- Dual control plane — 9P for power users/editors, D-Bus for desktop. Same core, different surfaces.
- Security by default — sandboxed execution with explicit elevation, per-session identity.
Dead Ends and Reversals
| What | Why it was removed |
|---|---|
| MCP servers | Too heavyweight; scripts are simpler and faster |
| TUI frontend | Replaced by KDE GUI and Emacs (ellie) |
| s/sh, s/bfg, s/bbg | Replaced by generate/complete/route 9P files |
pl/ planning namespace |
Over-engineered; plan file is sufficient |
| Beads integration | External dependency for something a checklist file handles |
| SSH agent proxy | Attack surface not worth the convenience |
| Separate ollie-dbus daemon | Consolidated into olliesrv |
| FUSE mounts | Replaced by standalone 9P client binary |
| Tier routing (FAST/POWER) | Replaced by /route endpoint with real model discovery |
| call_tool / pipe | Replaced by native tool registry with tool_load |
| execute_code (multi-lang) | Simplified to shell-only; tool scripts handle language choice |
| chatwait | Reverted; acme tails chat directly |
| Header comment metadata | Replaced by .meta sidecar JSON; decouples metadata from language |
contrib/ directory |
Renamed to data/; nothing was community-contributed |
| Script namespaces (s/, u/, x/) | Replaced by 9P request-response files |