19 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 workspace-scoped navigator sessions
- 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/)
Conditional variants for network transparency
Tools can declare multiple variants gated by host conditions (binary, file,
os, arch, env, nenv). First match determines the tool's schema and
executable. Enables one .meta to work across heterogeneous hosts — the
contract adapts to capabilities. Critical for ollie-remote deployments where
the remote host may have different tools, package managers, or init systems.
Sudo credential broker
Tools declare "sudo": true in their .meta to require root privileges.
Dispatch becomes a two-gate chain: elevation approval → credential prompt → sudo -S.
Credentials are requested via kdialog/zenity on the local desktop, or forwarded
over SSH for remote execution. The tool itself has no knowledge of sudo — the
privilege wrapping is entirely in the dispatch layer.
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 |