ollie is a 9p-based generative agent surface
Go to file
Ollie Agent e3d388146f usage: make screenshots clickable to full size 2026-08-04 20:09:59 +02:00
agent session: pause/resume persistence, restore paused sessions without spawning processes 2026-08-04 16:05:22 +02:00
backend fix(backend): strip tool blocks when no tools defined 2026-08-03 15:58:29 +02:00
cmd Move mount/ package to ollie-9p 2026-08-04 16:38:44 +02:00
contrib/elisp doc/ellie.md: consolidate Emacs docs, update references 2026-08-04 20:00:38 +02:00
data fix resolve_context unbound variable when no args 2026-08-04 19:54:48 +02:00
detach flatten: absorb submodules into single Go module 2026-07-30 18:57:51 +02:00
doc usage: make screenshots clickable to full size 2026-08-04 20:09:59 +02:00
elevate elevate: broker credential support for privileged tool execution 2026-07-30 23:37:57 +02:00
env remove SUPERPOWERD and OLLIE_TRANSCRIPT_PATH references 2026-07-31 08:40:37 +02:00
fs fs: add trailing newlines to all static read/request handlers 2026-08-04 17:27:32 +02:00
fsedsl fsedsl: simplify README usage section 2026-08-04 12:48:39 +02:00
kde@9f3f962567 Update kde submodule: remove sidebar state dots 2026-08-04 16:33:41 +02:00
lib9p lib9p: remove test.c (confuses cgo) 2026-08-03 20:05:48 +02:00
log flatten: absorb submodules into single Go module 2026-07-30 18:57:51 +02:00
paths flatten: absorb submodules into single Go module 2026-07-30 18:57:51 +02:00
prompts docs: replace all session/{id} and {sid} with {sname} 2026-08-04 19:30:57 +02:00
sandbox multiplex toolsrv, wip zero-tool server 2026-08-02 19:18:39 +02:00
session session: pause/resume persistence, restore paused sessions without spawning processes 2026-08-04 16:05:22 +02:00
skills flatten: absorb submodules into single Go module 2026-07-30 18:57:51 +02:00
tools/lsp move LSP .meta files to live alongside their Go code 2026-08-03 01:03:27 +02:00
toolsrv session: pause/resume persistence, restore paused sessions without spawning processes 2026-08-04 16:05:22 +02:00
.gitignore remove no9p flag — 9P is the only interface now 2026-07-31 20:56:52 +02:00
.gitmodules remove httpgw and webui; update AGENTS.md to reflect current structure 2026-07-30 20:19:32 +02:00
.plan.md agent: refresh tool definitions from server on every turn 2026-07-30 20:37:35 +02:00
AGENTS.md docs: replace all session/{id} and {sid} with {sname} 2026-08-04 19:30:57 +02:00
Containerfile add remote access via TCP: Containerfile, mount subcommand, docs 2026-04-16 23:32:37 +02:00
Ollie session: replace timestamp-prefix IDs with UUIDv4; show first segment in acme GUI 2026-08-01 17:26:34 +02:00
README.md doc/ellie.md: consolidate Emacs docs, update references 2026-08-04 20:00:38 +02:00
go.mod Extract EDSL to fsedsl/ library 2026-08-04 12:02:32 +02:00
go.sum Revert "remove dead mount/ package and olliesrv mount subcommand" 2026-07-30 21:19:11 +02:00
justfile remove mount/ package references and ollie-watchdog 2026-08-04 16:58:44 +02:00
lsp_definition tools/lsp: port LSP tools from Python to Go 2026-07-30 22:10:56 +02:00
ollie-remote toolsrv: unified socket-based execution 2026-07-31 14:10:47 +02:00
pull.sh pull.sh: update for flattened repo (only . kde webui remain) 2026-07-30 19:42:21 +02:00

README.md

ollie

Ollie is an AI agent runtime. A single binary (olliesrv) exposes agent sessions over two surfaces: a 9P2000 filesystem (the canonical interface) and a D-Bus service (org.ollie.SessionManager). Both are thin adapters over the same core — the agent loop, tool dispatch, sandboxing, and multi-backend routing. Choice of surface is per-deployment: 9P for its Unix composability, D-Bus for desktop integration, or both.

The core is deliberately minimal. Capabilities come from composing small pieces — tool executables, skill files, metadata sidecars — rather than building a monolithic framework.

Getting started

git clone --recurse-submodules https://git.lneely.de/lkn/ollie.git
cd ollie
just

Requires just:

cargo install just

See doc/USAGE.md for usage instructions.

Configuration

Environment: ~/.config/ollie/env

OLLIE_BACKEND=openai           # ollama | openai | anthropic | copilot | kiro (default: ollama)
OLLIE_OLLAMA_URL=              # base URL for Ollama (default: http://localhost:11434)
OLLIE_OPENAI_URL=https://openrouter.ai/api
OLLIE_OPENAI_KEY=sk-or-...
OLLIE_ANTHROPIC_KEY=sk-ant-...
OLLIE_COPILOT_TOKEN=...
OLLIE_KIRO_TOKEN=...           # bearer token or sqlite:// path (auto-detected from Kiro CLI if unset)
OLLIE_MODEL=qwen/qwen3-235b-a22b
OLLIE_TOOLS_PATH=~/.config/ollie/tools    # directory for tool executables + .meta files
OLLIE_MEMORY_PATH=~/.config/ollie/memory  # directory for memory files
OLLIE_ELEVATE_SOCKET=${XDG_RUNTIME_DIR}/ollie/elevate.sock
OLLIE_COMPLETE_BACKEND=ollama             # backend for code completion
OLLIE_COMPLETE_MODEL=qwen3:latest         # model for code completion
OLLIE_ENABLED_BACKENDS=openrouter,kiro  # backends available for routing (comma-separated)
OLLIE_ROUTE_BACKEND=ollama               # backend for the /route classifier
OLLIE_ROUTE_MODEL=qwen3:8b               # model for task routing

Shell environment variables take precedence over the env file.

Repository layout

Single Go module with one Git submodule (kde).

Directory Language Description
agent/ Go Agent loop, history, hooks, prompt resolution, commands
backend/ Go LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer)
toolsrv/ Go Tool server, dynamic tool dispatch, sandboxed execution, remote execution
tools/lsp/ Go LSP bridge daemon + client library (gopls, clangd, intelephense)
session/ Go Session lifecycle, config, persistence
fs/ Go 9P filesystem tree for session namespace
detach/ Go Background process management
elevate/ Go Elevation broker (privilege escalation)
sandbox/ Go Landlock sandbox config
cmd/olliesrv/ Go The main binary
cmd/ollie-9p/ Go 9P client
cmd/ollie-remote/ Go Remote execution binary
kde/ C++/Qt6 KDE plasmoid, GUI, Kate plugin, KRunner, tray (submodule)
data/agents/ JSON Agent configs
data/prompts/ Markdown System prompt templates
data/tools/ Mixed Tool executables (scripts + compiled) + .meta sidecar files
data/skills/ Markdown Domain knowledge modules
doc/ Markdown Architecture docs, usage guide

Architecture

graph TB
    subgraph Frontends
        ACME[Plan 9 acme]
        EMACS[Emacs<br>ellie.el]
        KDE[KDE<br>GUI / Kate / KRunner]
    end

    subgraph "olliesrv"
        P9[9P Filesystem<br>session/ namespace]
        SM[Session Manager]
        AG[agent.Agent<br>loop · compaction<br>prompt templates]
        ROUTE[Router<br>complete · generate · route]
        TOOLS[toolsrv.Server<br>dynamic dispatch · sandbox]
    end

    subgraph "LLM Backends"
        OLL[Ollama]
        OAI[OpenAI / OpenRouter]
        ANT[Anthropic]
        COP[Copilot]
        KIRO[Kiro]
    end

    subgraph Execution
        LOCAL[Tool Scripts<br>loaded via 9P write<br>· shell, reasoning_think<br>· file_*, lsp_*, memory_*<br>· gui_*, subagent_*]
        REMOTE[ollie-remote<br>via SSH · no embedded tools]
    end

    ACME & EMACS & KDE --> P9
    P9 --> SM
    SM --> AG
    AG --> ROUTE
    AG --> TOOLS

    AG -->|streaming| OLL & OAI & ANT & COP & KIRO

    TOOLS --> LOCAL
    TOOLS -->|SSH| REMOTE

olliesrv is a single binary. Which surface it exposes is a flag:

Mode Flags Interface Frontends
+9p +dbus (default) 9P filesystem + D-Bus signals All
+9p -dbus -nodbus 9P filesystem only Terminal, acme, Emacs, web
-9p +dbus -no9p D-Bus only KDE (plasmoid, GUI, Kate, KRunner, tray)

9P: everything is a file

The 9P server exposes the agent runtime as a synthetic filesystem. Every session is a directory; every operation is a read or write. There is no client library, no SDK, no protocol buffer — echo, cat, tail, and rm are the API.

# Create a session
echo "name=worker" > /mnt/ollie/session/new

# Submit a prompt
echo "fix the bug in main.go" > /mnt/ollie/session/{sname}/agent/{aname}/prompt

# Stream the response
tail -f /mnt/ollie/session/{sname}/agent/{aname}/chat

# Check state
cat /mnt/ollie/session/{sname}/agent/{aname}/state

# One-shot generation (no session needed)
exec 3<>/mnt/ollie/generate; echo 'explain monads' >&3; cat <&3; exec 3>&-

# Route a task to the best model
exec 3<>/mnt/ollie/route; echo 'redesign the auth system' >&3; cat <&3; exec 3>&-
# → backend=kiro model=claude-sonnet-4-20250514

Network transparency

9P is a network protocol. Mount the agent from any machine:

ollie-9p mount tcp!server:5640 ~/mnt/ollie

No SSH tunneling, no port forwarding, no API gateway. One mount and every session, every tool, every config value is accessible as if local.

Pipe o: a composable CLI

The o script (data/scripts/o) wraps the 9P namespace into a composable CLI:

o read chat           # stream chat output
o prompt              # interactive multi-line REPL
o tail                # tail chat with block-marker filtering
o readloop statewait  # block until state changes

Context is set once, then every command addresses the right agent:

eval $(o env mysession myagent)
o read state      # → session/mysession/agent/myagent/state
o prompt          # REPL for session/mysession/agent/myagent

Paths resolve based on context — agent files (state, chat, prompt, etc.) use $session + $agent, session files (env, ctl, name) use just $session, root paths (models, agents, help) need no context. Full paths starting with session/ bypass resolution entirely.

o tui: an agent terminal in 20 lines of tmux

o tui composes o tail and o prompt into a single tmux layout — no framework, no widgets, no IPC. It's two shell commands in two panes:

┌──────────────────────────────────┐
│  o tail | grep                    │  ← chat stream
│                                   │
├──────────────────────────────────┤
│  o prompt                         │  ← input REPL
└──────────────────────────────────┘
Agent state shown in tmux status bar

The prompt REPL supports multi-line input (send with . on its own line), slash commands (/stop, /model, /quit, etc.), and readline editing. Agent state updates the tmux status bar in real time via statewait, with color-coded background — green for idle, orange for calling, blue for thinking.

This is the entire "frontend": two o subcommands, two tmux splits, one background statewait reader. No SDK, no widget toolkit, no event loop. It's the Plan 9 principle in miniature — shell composition over framework bloat.

Because operations are file I/O, they compose with the full Unix toolkit:

# Fan out a prompt to all agents
for s in /mnt/ollie/session/*/agent/*/prompt; do echo "run tests" > "$s"; done

# Wait for all agents to finish
for s in /mnt/ollie/session/*/agent/*/statewait; do cat "$s" > /dev/null; done

# Grep all agent plans
grep -r "TODO" /mnt/ollie/session/*/plan

# Monitor costs
paste /mnt/ollie/session/*/agent/*/cost

# Strip block markup from chat output (text-only frontends)
ollie-9p read session/{sname}/agent/{aname}/chat | grep -vE '^\[\[\[.*'
# → filters [[[type]]]/[[[end]]] block markers, leaving plain text

Pipe agents into awk. Filter with grep. Schedule with cron. Orchestrate with a 10-line shell script instead of a framework.

Plan 9 patterns

  • /net/dns pattern — /complete, /generate, /route are stateless per-fid: write a request, read back the result
  • ctl files — control operations (stop, kill, rename, compact) are writes to a control file, not method calls
  • Blocking reads — statewait blocks until state changes, replacing event subscriptions with a simple cat
  • Multiplexing — multiple clients mount the same server simultaneously, each with an independent view through per-fid state

D-Bus: desktop integration

The D-Bus interface (org.ollie.SessionManager) is embedded in olliesrv (enabled by default, or exclusively via -no9p). It provides typed method calls and real-time signals.

# Create a session
dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \
  /org/ollie/SessionManager org.ollie.SessionManager.CreateSession \
  string:"$PWD" string:"" string:"" string:"default" string:"" string:""

# Submit a prompt
dbus-send --session --dest=org.ollie.SessionManager --type=method_call \
  /org/ollie/SessionManager org.ollie.SessionManager.Submit \
  string:"SESSION_ID" string:"fix the bug"

# Read state
dbus-send --session --dest=org.ollie.SessionManager --type=method_call --print-reply \
  /org/ollie/SessionManager org.ollie.SessionManager.GetState string:"SESSION_ID"

Signals

D-Bus signals push events without polling:

  • ChatUpdated — new chat output (session_id, offset, text)
  • StateChanged — transition notifications
  • SessionCreated / SessionKilled / SessionRenamed — lifecycle events
  • ProcessDetached / ProcessExited — background process events

GUI frontends subscribe once and react — no timers, no file watches, no busy loops.

KDE integration

  • Plasmoid — embed an agent chat widget in the Plasma panel
  • Kate plugin — inline AI assistance in the text editor
  • KRunner — launch prompts from the desktop search bar
  • System tray — status indicator and quick actions
  • Standalone GUI — full-featured Qt6 chat application

Tool registry: zero built-in tools

No tools are compiled into the Go binary. Every tool — including shell and reasoning_think — is an external executable loaded dynamically through the 9P filesystem. Tool definitions, schemas, and execution logic live entirely in the filesystem.

Loading tools

Tools are loaded by writing their name to session/{sname}/agent/{aname}/tools:

# List currently loaded tools
ollie-9p read session/{sname}/agent/{aname}/tools

# Load a tool — becomes a native callable function
echo file_glob | ollie-9p write session/{sname}/agent/{aname}/tools

# Global catalog: all discoverable tools on disk
ollie-9p read tools

The tool_load tool is itself a bash script that does exactly this — it wraps the 9P write in a callable function so the LLM can load tools during a turn.

Agent autoLoad configuration

Agent configs declare which tools to load at startup via autoLoad:

{
  "autoLoad": ["file_read", "file_edit", "file_glob", "file_grep", "file_write", "tool_load"]
}

Each agent profile (default, copilot, explorer, librarian, navigator, taskmanager, theo) has its own autoLoad list. The allowTools field provides a secondary security gate.

Declarative registration

Tools are executables in ~/.config/ollie/tools/. Each one has a .meta sidecar file declaring its JSON schema, documentation, concurrency class, tier, and privilege requirements. The registry reads .meta files — it never inspects the executable itself.

This is declarative tool registration: write a .meta file, drop an executable in $OLLIE_TOOLS_PATH, and the tool is live. No server restart, no config file, no code change.

The .meta file (file_glob.meta):

{
  "description": "Find files by glob pattern, sorted by mtime (newest first).",
  "args": {"type":"object","properties":{"pattern":{"type":"string"}},"required":["pattern"]},
  "tier": "cold",
  "readOnly": true
}

Everything is sandboxed by default via Landlock. Tools that need to escape request elevation explicitly ("elevated": true).

Privileged tools: sudo

Tools that need root privileges declare "sudo": true in their .meta. The dispatch chain becomes a two-gate sequence: elevation approval (escape sandbox) → credential prompt → sudo -S execution. The tool itself has no knowledge of sudo — the privilege wrapping is entirely in the dispatch layer.

{
  "description": "Read system logs (dmesg).",
  "sudo": true,
  "cmd": "system_logs_dmesg",
  "args": {"type":"object","properties":{"lines":{"type":"string"}}},
  "readOnly": true
}

Credentials are prompted via kdialog/zenity on desktop, or forwarded over SSH for remote execution. See doc/WRITING_TOOLS.md for details.

Host-conditional variants

A single .meta file can describe a tool that works differently on different hosts. Variants are gated by match conditions (binary, file, os, arch, env, nenv). The first matching variant determines the tool's schema, documentation, and executable. If no variant matches, the tool is hidden from the registry — it doesn't exist on this host.

{
  "description": "Read system logs.",
  "variants": [
    {
      "match": {"binary": "journalctl"},
      "cmd": "system_logs_journald",
      "args": {"type":"object","properties":{"unit":{"type":"string"},"since":{"type":"string"}}}
    },
    {
      "match": {"file": "/var/log/syslog"},
      "cmd": "system_logs_syslog",
      "args": {"type":"object","properties":{"lines":{"type":"string"}}}
    }
  ]
}

On a systemd host the model sees unit filtering and time ranges. On Alpine it sees tail + grep. On a host with neither, the tool doesn't appear. One .meta deploys everywhere. See doc/WRITING_TOOLS.md for the full variant specification.

What you can do

Capability How
Run an agent olliesrv → any frontend (ellie, KDE, acme, o tui)
Terminal TUI o tui — tmux + two shell commands, no widgets
Remote execution Set remote=user@host in session config
Multi-agent subagent_spawn from within a session, or shell scripts
Code completion Read/write /complete for fill-in-the-middle
One-shot LLM Write to /generate, read back the response
Task routing Write to /route, read back backend=X model=Y
Background jobs Detach via shell({"elevated":true, "detach":true})
Load tools Write tool name to agent/{aname}/tools via 9P
Custom tools Drop an executable + .meta in $OLLIE_TOOLS_PATH
Privileged tools Declare "sudo": true in .meta — broker handles elevation + credentials
Custom agents Write a JSON config in agents/
Domain skills Write a markdown file in skills/

Credits

  • Plan 9 from Bell Labs — for an interesting system
  • @9fans — for the Plan 9 port
  • Suckless — for articulating good software development principles
  • @simonfxr — for a solid agent baseline to "borrow" from
  • @aws — for a solid open-source agent implementation

License

GPLv3