ollie/doc/ARCHITECTURE.md

8.8 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
  mcp/           — MCP client (concrete)
  tools/         — Server and Dispatcher interfaces
  tools/execute/   — execute.Server: execute_code, execute_tool, execute_pipe
  tools/file/      — file.Server: file_read, file_write, file_edit, file_glob, file_grep
  tools/memory/    — memory.Server: memory_remember, memory_recall
  tools/reasoning/ — reasoning.Server: reasoning_think
  tools/task/      — task.Server: task_plan, task_complete

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.

Why pkg/mcp is concrete (not interfaced)

pkg/mcp exposes a concrete Client rather than an interface. MCP is a well-defined protocol; the client is a thin transport layer and there is currently no need for consumers to swap implementations. This may be revisited.

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 (built-in or MCP-backed); 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 regardless of whether they are built-in or backed by MCP. There is no special "builtin" concept — execute.Server and file.Server are registered by name the same way MCP servers are, and are torn down and recreated on /agent switches just like MCP connections.

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. tools.NewServer(client) wraps an mcp.Client as a tools.Server.

execute.Decl(cwd string) and file.Decl(cwd string) accept a working directory. Pass "" to fall back to os.Getwd().

task.Decl(planDir string) accepts the directory where plan files are written.

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 / execute_tool / execute_pipe
                    ├── "file"      → tools.Server
                    │       file_read / file_write / file_edit / file_glob / file_grep
                    ├── "memory"    → tools.Server
                    │       memory_remember / memory_recall
                    ├── "reasoning" → tools.Server
                    │       reasoning_think
                    ├── "task"      → tools.Server
                    │       task_plan / task_complete
                    └── "<servername>"  → tools.Server (MCP)

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.

Built-in Tools

Twelve tools across five servers:

Server Tool What it does
execute execute_code Runs inline bash in a landrun sandbox
execute execute_tool Reads a named script from OLLIE_TOOLS_PATH and runs it sandboxed
execute execute_pipe Chains steps, piping stdout of each into stdin of the next
file file_read Reads a file from the filesystem
file file_write Writes a file to the filesystem
file file_edit Edits a file with precise string replacement
file file_glob Finds files by glob pattern
file file_grep Searches file contents by regex
memory memory_remember Saves a persistent memory to a flat markdown file
memory memory_recall Searches saved memories by keyword
reasoning reasoning_think Externalizes intermediate reasoning (no-op, recorded in history)
task task_plan Decomposes a goal into ordered steps; saves a markdown checklist to pl/
task task_complete Marks a plan step complete; returns remaining steps

OLLIE_TOOLS_PATH defaults to ~/.config/ollie/tools. The directory can be a symlink or a mountpoint — execute_tool treats it as an ordinary filesystem path.

OLLIE_MEMORY_PATH defaults to ~/.config/ollie/memory. Memory files use denote naming: YYYYMMDDTHHmmss__{slug}--{tag1}-{tag2}.md.

Planning

task_plan saves a markdown checklist to the plan directory (pl/ in ollie-9p) as a __todo.md file. The agent then:

  1. Renames __todo → __wip when work begins (file_edit or shell)
  2. Calls task_complete after each step finishes — marks [ ] → [x], returns remaining steps
  3. Uses file_* tools to add, remove, or revise steps as the plan evolves
  4. Renames __wip → __done when the goal is realized

task_complete takes the plan filename and a step title (substring match, case-insensitive). It errors if no matching unchecked step is found, so silent skips are caught.

Everything else — listing plans, inspecting a specific step, bulk operations — goes through file_* or execute_code. The task server handles only creation and step completion.

Typical Consumer Setup

newDispatcher := tools.NewDispatcherFunc(map[string]func() tools.Server{
    "execute":   execute.Decl(cwd),  // "" falls back to os.Getwd()
    "file":      file.Decl(cwd),
    "memory":    memory.Decl(),
    "reasoning": reasoning.Decl(),
    "task":      task.Decl(planDir),
})

env := agent.BuildAgentEnv(cfg, newDispatcher(), cwd)  // also connects MCP servers from cfg

core := agent.NewAgentCore(agent.AgentCoreConfig{
    Backend:       be,
    CWD:           cwd,
    Env:           env,
    NewDispatcher: newDispatcher,
    // ...
})

BuildAgentEnv adds MCP servers from the config on top of the pre-registered servers. On /agent switches, NewDispatcher is called to produce a fresh dispatcher — all servers (built-in and MCP) are torn down and recreated for the new agent config. CWD is preserved across switches.

Core.ListServers() returns all registered tool servers and their tools, grouped by server name. Accessible via the /mcp command or ollie/s/{sid}/mcp in ollie-9p.

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.