387 lines
14 KiB
Markdown
387 lines
14 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
git clone --recurse-submodules https://git.lneely.de/lkn/ollie.git
|
|
cd ollie
|
|
just
|
|
```
|
|
|
|
Requires [just](https://github.com/casey/just):
|
|
|
|
```sh
|
|
cargo install just
|
|
```
|
|
|
|
See [doc/USAGE.md](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
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
# 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
|
|
|
|
### 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`:
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```json
|
|
{
|
|
"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`):
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```json
|
|
{
|
|
"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](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.
|
|
|
|
```json
|
|
{
|
|
"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](doc/WRITING_TOOLS.md) for the
|
|
full variant specification.
|
|
|
|
## What you can do
|
|
|
|
| Capability | How |
|
|
|---|---|
|
|
| **Run an agent** | `olliesrv` → any frontend ([ellie](doc/ellie.md), 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](https://9fans.net) — for an interesting system
|
|
- [@9fans](https://github.com/9fans) — for the Plan 9 port
|
|
- [Suckless](https://suckless.org) — for articulating good software development principles
|
|
- [@simonfxr](https://github.com/simonfxr) — for a solid agent baseline to "borrow" from
|
|
- [@aws](https://github.com/aws/amazon-q-developer-cli) — for a solid open-source agent implementation
|
|
|
|
## License
|
|
|
|
GPLv3 |