- Remove OperationalModel function and OpModel prompt layer (dead code) - Update session-9p.md: remove D-Bus preference mention - Update desktop file: remove X-DBUS-* entries - Update systemd service: remove dbus.service dependency - Rewrite route, process_output, process_signal tools to use 9P D-Bus is still used for: - Desktop notifications (org.freedesktop.Notifications) for elevation prompts - KDE tools (gui_windows, etc.) talking to KWin - not ollie's D-Bus |
||
|---|---|---|
| agent | ||
| backend | ||
| cmd | ||
| contrib/elisp | ||
| data | ||
| detach | ||
| doc | ||
| elevate | ||
| env | ||
| fs | ||
| fsedsl | ||
| kde@9f3f962567 | ||
| lib9p | ||
| log | ||
| paths | ||
| prompts | ||
| sandbox | ||
| session | ||
| skills | ||
| tools/lsp | ||
| toolsrv | ||
| .gitignore | ||
| .gitmodules | ||
| .plan.md | ||
| AGENTS.md | ||
| Containerfile | ||
| Ollie | ||
| README.md | ||
| go.mod | ||
| go.sum | ||
| justfile | ||
| lsp_definition | ||
| ollie-remote | ||
| pull.sh | ||
README.md
ollie
Ollie is an AI agent runtime. A single binary (olliesrv) exposes agent sessions
over a single surface: a 9P2000 filesystem. 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 (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 exposes the agent runtime over a single surface: a 9P2000 filesystem.
The KDE GUI, Kate plugin, and KRunner use 9P internally to communicate with the
server — there is no D-Bus interface.
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/dnspattern —/complete,/generate,/routeare stateless per-fid: write a request, read back the resultctlfiles — control operations (stop, kill, rename, compact) are writes to a control file, not method calls- Blocking reads —
statewaitblocks until state changes, replacing event subscriptions with a simplecat - Multiplexing — multiple clients mount the same server simultaneously, each with an independent view through per-fid state
KDE frontends
- 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
- 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