ollie/doc/ARCHITECTURE.md

8.0 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/.

Package Layout

pkg/
  agent/         — Core interface, agent loop, session and context management
  backend/       — Backend interface + implementations
  config/        — Agent config struct and loader
  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.

Extension Points

Consumers extend ollie by implementing or composing its interfaces:

  • backend.Backend — swap or add LLM backends
  • tools.Server — add a new tool server; all servers are equal
  • tools.Dispatcher — replace the tool router entirely (e.g. remote dispatcher, mock)
  • agent.Core — the agent's public API; frontends drive it without knowing internals. Includes Close() 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:

  1. Implement tools.Server in a new package under pkg/tools/
  2. Export func Decl(...) func() tools.Server
  3. 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 from OLLIE_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 sh, ls, kill
b/ Batch job submission and execution job, q, sched
u/ Utility scripts; compositions of b/ primitives optimize

b/job is the core batch runner. b/q and b/sched are thin wrappers that add foreground-wait and background-submit semantics respectively. 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_plan, task_complete, subagent_spawn.

Sub-agents

Sub-agents are implemented as a tool script (subagent_spawn) on top of the b/ batch namespace. The script writes a job spec to b/new, blocks on b/{id}/statewait per job in background subshells, joins on all, and concatenates results. The b/ namespace (backed by BatchStore) 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"  →  b/{id-0}/result
  ├── subagent_spawn "prompt B"  →  b/{id-1}/result
  └── subagent_spawn "prompt C"  →  b/{id-2}/result
                                        ↓
                               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; b/ 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. A contextBuilder enforces a rolling window to prevent unbounded prompt growth. When the window fills, older messages are either evicted (with a compaction notice injected) or proactively summarized via /compact.

Sandboxing

internal/sandbox wraps commands with landrun (Landlock LSM). Configuration is layered:

  1. Global defaults (~/.config/ollie/sandbox.yaml)
  2. Named sandbox overlay (~/.config/ollie/sandbox/<name>.yaml)

If superpowerd is running, commands are additionally wrapped with superpowers run-session for privilege scoping (optional, detected at runtime).

Frontends

The reference frontend is ollie-tui: a readline-based terminal UI that imports ollie as a library. It is the only consumer of agent.Core today, but the interface is intentionally frontend-agnostic.