12 KiB
ollie Architecture
Philosophy
ollie is a small, extensible core library for building agentic systems. It has no binary of its own — frontends and consumers live in separate repos and import ollie as a dependency. The internal surface area is kept minimal; everything a consumer needs is exported under pkg/.
Overview
Package Layout
pkg/
agent/ — Core interface, agent loop, session and context management
backend/ — Backend interface + implementations
config/ — Agent config struct and loader
env/ — Environment variable loading (env file + shell)
log/ — Structured logging
paths/ — Config and data directory resolution
skills/ — Skill file discovery and loading
store/ — Store interfaces and implementations (FlatDir, Session, Batch, Skill)
tools/ — Server and Dispatcher interfaces
tools/execute/ — execute.Server: execute_code
internal/
sandbox/ — landrun sandbox config and command wrapper
Why internal/sandbox?
The sandbox package is an implementation detail of pkg/tools/execute. It has no stable public API and no reason to be imported by consumers directly. Keeping it internal prevents accidental coupling.
Agent Configuration
Agent configs are JSON files in ~/.config/ollie/agents/. The key fields are:
prompt — an array of shell commands whose stdout is concatenated to form the system prompt. Each command runs in the session's working directory with access to $OLLIE (mount path) and $PWD. Commands that exit non-zero are skipped; exit code 2 blocks the turn entirely.
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-reasoning"
]
}
hooks — lifecycle callbacks executed at specific points:
| Hook | When | Use case |
|---|---|---|
agentSpawn |
Session creation | Start elevation adapter, initialize state |
preTurn |
Before each turn (deprecated; use prompt) |
Legacy prompt injection |
postTurn |
After each turn | Task list display, logging |
preCompact |
Before context compaction | Save state |
postCompact |
After context compaction | Notify user |
turnError |
On turn error (rate limit, transient failure) | Fallback handling, model switching |
trustedTools — array of tool names allowed to run outside the sandbox.
mcpServers — MCP server configurations for external tool providers:
{
"mcpServers": {
"server-name": {
"command": "server-binary",
"args": [],
"env": {"KEY": "value"}
}
}
}
Generation parameters — maxTokens, temperature, frequencyPenalty, presencePenalty.
Extension Points
Consumers extend ollie by implementing or composing its interfaces:
backend.Backend— swap or add LLM backendstools.Server— add a new tool server; all servers are equaltools.Dispatcher— replace the tool router entirely (e.g. remote dispatcher, mock)agent.Core— the agent's public API; frontends drive it without knowing internals. IncludesClose()to release session resources (tmpdir under/tmp/ollie/).
All tool servers implement the same tools.Server interface. There is no special "builtin" concept — execute.Server is registered by name, and is torn down and recreated on /agent switches.
Each built-in server package exports a Decl function (func Decl(...) func() tools.Server) — a parameterized factory that produces a fresh server instance. tools.NewDispatcherFunc takes a map of name→Decl result and returns a func() tools.Dispatcher suitable for agent.AgentCoreConfig.NewDispatcher.
execute.Decl(cwd string) accepts a working directory used as cmd.Dir for sandboxed commands. Pass "" to fall back to os.Getwd().
Adding a new tool server:
- Implement
tools.Serverin a new package underpkg/tools/ - Export
func Decl(...) func() tools.Server - Register the Decl result by name in
tools.NewDispatcherFunc— no frontend changes needed
Data Flow
Frontend
└── agent.Core.Submit(prompt)
└── loop.run()
├── backend.ChatStream() — LLM call
└── tools.Dispatcher.Dispatch(...) — tool dispatch
├── "execute" → tools.Server
│ execute_code
└── "<servername>" → tools.Server
All tool calls go through one tools.Dispatcher.Dispatch path. Every registered server is a tools.Server; the dispatcher routes by name and is agnostic to how any server is implemented.
execute_code
execute_code is the only built-in tool. It is a unified primitive that handles simple execution, sequential composition, and parallel fan-out through a single steps array of pipeline stages.
Each stage is one of:
{code, language}— inline code (default: bash){tool, args}— named script fromOLLIE_TOOLS_PATH; language detected from shebang{parallel: [...]}— concurrent fan-out; each element is a{code}or{tool}step; outputs concatenated in submission order
Stages run sequentially, each stage's stdout fed to the next stage's stdin. A single simple stage is the degenerate case and returns raw output directly.
steps=[{code: "ls"}] — simple execution
steps=[{code: "grep error log"}, {code:"wc"}] — sequential pipeline
steps=[{parallel:[{code:"wc a"},{code:"wc b"}]}] — parallel fan-out
steps=[{parallel:[...]}, {code:"sort"}] — fan-out then transform
OLLIE_TOOLS_PATH defaults to ~/.config/ollie/tools. {tool} steps resolve scripts from this path.
Script Namespaces
User-facing scripts are organized by the 9P namespace they are hosted under and installed to ~/.config/ollie/scripts/{ns}/:
| Namespace | Purpose | Scripts |
|---|---|---|
s/ |
Session management and one-shot queries | sh, ls, kill, cleanup, b, bfg, bbg |
u/ |
Utility scripts; compositions of s/ primitives |
optimize |
x/ |
System exec and plugins | elevate, elevate-adapter-*, bd, prime, freeloader, task |
s/b is the core one-shot runner. s/bfg and s/bbg are thin wrappers: s/bfg blocks until the agent finishes and writes the result to stdout; s/bbg returns immediately and prints the session path. u/ scripts have no special server backing — they are executable scripts that compose other namespaces via the filesystem.
Tool Scripts
All capabilities beyond execute_code itself are implemented as tool scripts in OLLIE_TOOLS_PATH, invoked via {tool} steps. The agent discovers available scripts via t/idx.
Examples from the default install: file_read, file_write, file_edit, file_glob, file_grep, memory_remember, memory_recall, reasoning_think, task_add, task_check, task_clear, denote_view, browser_screencap, subagent_spawn.
Sub-agents
Sub-agents are implemented as a tool script (subagent_spawn) on top of the s/ namespace. The script writes a session spec to s/new, blocks on s/{id}/statewait per job in background subshells, joins on all, and concatenates results. The s/ namespace handles agent lifecycle, cancellation, and result storage.
For heterogeneous parallel delegation (N agents, N different prompts), a {parallel:[...]} stage composes with subagent_spawn — each parallel step is one shell invocation with a distinct prompt:
execute_code {parallel} stage
├── subagent_spawn "prompt A" → s/{id-0}/chat
├── subagent_spawn "prompt B" → s/{id-1}/chat
└── subagent_spawn "prompt C" → s/{id-2}/chat
↓
concatenated in submission order
The architecture boundary is clean: execute_code is the only built-in tool in core; everything else is a tool script; s/ is a 9P namespace. No layer knows about the others.
Typical Consumer Setup
newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{
"execute": execute.Decl(cwd), // "" falls back to os.Getwd()
})
env := agent.BuildAgentEnv(cfg, newDispatcher(), cwd)
core := agent.NewAgentCore(agent.AgentCoreConfig{
Backend: be,
CWD: cwd,
Env: env,
NewDispatcher: newDispatcher,
// ...
})
BuildAgentEnv wires together the pre-registered servers with the config. On /agent switches, NewDispatcher is called to produce a fresh dispatcher — all servers are torn down and recreated for the new agent config. CWD is preserved across switches.
Session Lifecycle
NewAgentCore creates /tmp/ollie/{sessionID} when a session starts. Core.Close() removes it recursively. olliesrv calls Close() from killSession (individual session removal) and Shutdown (server stop — iterates all sessions). SetSessionID renames the tmpdir atomically alongside the persisted session files.
!<cmd> shell commands run in the session's current working directory (Core.CWD()), not the process working directory.
Session and Context
agent.Session owns the message history as a flat []backend.Message slice. There is no rolling window or automatic eviction — the history grows until compaction
runs.
Compaction runs automatically before a turn when the estimated token count reaches
75% of the model's context length (defaulting to 128k if the backend doesn't report
one). It can also be triggered manually via /compact (from s/sh or the ctl file).
Either way it makes a single LLM call to summarize the conversation, then replaces the
history with a compact representation:
- All
systemmessages are preserved verbatim. - Recent
usermessages are preserved up to a ~20 000-token budget (newest first, estimated at 4 chars/token). - The LLM-generated summary is appended as a
usermessage with a handoff prefix so the next turn can resume seamlessly.
Tool call/result pairs are flattened to plain text before the compaction request so no
tool schemas are needed for the summarization call. Before compaction runs, a full
snapshot of the pre-compaction history is written to
{sessionsDir}/{sessionID}.compaction.jsonl for recovery.
/context and /usage commands report the current message breakdown and token usage
without modifying history.
Sandboxing
Sandboxing is applied specifically to execute_code. Each code step is wrapped with
landrun (Landlock LSM) via
internal/sandbox. The sandbox config is loaded by name from
~/.config/ollie/sandbox/<name>.yaml; if no sandbox argument is passed in the
execute_code call, the default config is used.
Steps marked elevated: true run outside the sandbox via the elevation backend
(x/elevate) and are the only exception. All other steps are sandboxed unconditionally.
HTTP Gateway
ollie-httpgw is a thin translation layer between HTTP and 9P. It connects to
olliesrv as a 9P client and exposes the same namespace over REST-style HTTP,
so any HTTP client can interact with ollie without a 9P library.
Each HTTP method maps to a 9P primitive: GET → read/stat, POST → write,
DELETE → remove, PATCH → wstat (rename). The gateway serializes file content
to JSON where the format is well-defined (key-value files, tab-separated records,
session index); raw text falls through as-is.
Sensitive internal paths (tr/, help, statewait, systemprompt, etc.) are
blocked at the gateway layer and never exposed over HTTP.
HTTP client (browser, curl, script)
│ HTTP/JSON :8080
▼
ollie-httpgw
│ 9P protocol
▼
olliesrv ($NAMESPACE/ollie or tcp:9564)
The gateway can connect to a local server via the Plan 9 namespace (default) or
to a remote server via TCP (-net tcp -addr host:9564), making it equally useful
for local setups and container/remote deployments.
Frontends
All frontends interact with the same 9P namespace, either directly or via httpgw:
| Frontend | Interface | Talks to |
|---|---|---|
s/sh |
terminal (readline) | 9P filesystem directly |
| ellie | Emacs buffer | 9P filesystem directly |
| Web UI | browser (Preact SPA) | ollie-httpgw over HTTP |
The web UI (webui/) is a Preact single-page application. It polls
/s/{id}/chat every two seconds for new output and proxies all API paths to
ollie-httpgw. It has no knowledge of 9P; the gateway is the only boundary.
Frontends that use the filesystem (s/sh, ellie, web
UI) are decoupled from the implementation entirely — they work equally well
against a local or remote olliesrv.