ollie/doc/ARCHITECTURE.md

233 lines
12 KiB
Markdown

# ollie Architecture
## Philosophy
ollie's design philosophy is Emacs: a small, extensible core. The Go runtime (`agent.Agent`) defines what an agent *is* — an event loop, a backend connection, a message history, and three built-in primitives (`shell`, tool registry, skill registry). Everything else — orchestration, scheduling, workflows, UIs — is pushed out to the surrounding environment.
The primary integration philosophy is Plan 9's "everything is a file." `olliesrv` exposes agent state and behaviors as files in a 9P namespace. Any program that can read and write files can drive an agent: shell scripts, editors, web apps, cron, containers.
All clients communicate via 9P. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only.
Design principles:
- **Small extensible core.** The agent runtime is minimal; capabilities come from composing external scripts.
- **Three built-in primitives.** `shell`, tool registry (`tool_load`/`tool_list`/`tool_active`), and skill registry (`skill_load`/`skill_list`/`skill_active`) are the only tools compiled into the core. Everything else is a script.
- **One integration path: 9P filesystem. Streaming via blocking reads. No polling.
- **No framework lock-in.** Frontends are decoupled via whichever interface they prefer; the core doesn't know or care.
## Repository Structure
Single Go module (`ollie`) with two Git submodules for decoupled frontends:
```
ollie/
├── agent/ Agent loop, history, hooks, prompt resolution, commands
├── backend/ LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer)
├── toolsrv/ Tool server: sandboxed execution, tool registry, skill management
├── session/ Session lifecycle (config, creation, persistence)
├── fs/session/ 9P filesystem tree for sessions
├── detach/ Background process management (ring buffer, signal)
├── elevate/ Elevation broker (privilege escalation daemon)
├── sandbox/ Landlock sandbox config YAML
├── env/ Environment variable loading
├── log/ Structured logging
├── paths/ XDG path resolution
├── mount/ 9P FUSE mount client
├── cmd/ Binaries:
│ ├── olliesrv/ 9P server
│ ├── ollie-9p/ 9P client
│ └── ollie-remote/ Remote execution server
├── kde/ KDE integration (submodule) — plasmoid, GUI, Kate plugin, KRunner, tray
├── agents/ Agent config JSONs (default, coding, orchestrator, worker, ...)
├── prompts/ Prompt templates (markdown)
├── tools/ Tool scripts (file_read, lsp_*, memory_*, subagent_*, ...)
├── skills/ Domain knowledge files (markdown)
├── scripts/ User-facing scripts organized by namespace (s/, u/, x/)
├── sandbox/ Sandbox config YAML files
└── doc/ Documentation
```
## System Overview
```mermaid
flowchart TB
subgraph Frontends["Frontends"]
ACME["acme (Plan 9)"]
ELLIE["ellie (Emacs)"]
KDE["KDE GUI / Plasmoid"]
end
subgraph Integration["Integration Layer"]
direction LR
P9["9P Filesystem"]
end
subgraph Server["olliesrv"]
direction TB
NS["9P Namespace\nsession/ · backends · models"]
end
subgraph Core["Agent Engine (per session)"]
LOOP["Agent Loop\n(agent/loop.go)"]
SESS["Session State\n(agent/history.go)"]
TRV["toolsrv.Server\n· shell execution\n· tool registry\n· landlock sandbox"]
end
subgraph Backends["LLM Backends"]
OLLAMA["Ollama"]
OPENAI["OpenAI / OpenRouter"]
ANTHROPIC["Anthropic"]
GEMINI["Gemini"]
CW["CodeWhisperer"]
end
TUI --> P9
ACME --> P9
ELLIE --> P9
KDE --> DBUS
WEB --> DBUS
P9 --> NS
DBUS --> DBA
NS --> LOOP
DBA --> LOOP
LOOP <--> SESS
LOOP --> TRV
LOOP <--> Backends
TRV --> Shell["sandboxed shell\n(landrun)"]
TRV --> Tools["tool scripts\n(OLLIE_TOOLS_PATH)"]
```
## Core Library
The core is a single Go module (`ollie`) with no binary. Binaries live in `cmd/`.
### Package Layout
| Package | Purpose |
|---|---|
| `agent/` | Agent struct, loop, history, compaction, hooks, commands, prompt resolution, state |
| `backend/` | Backend interface + LLM providers (Anthropic, OpenAI, Ollama, Gemini, Copilot, CodeWhisperer) |
| `toolsrv/` | Tool server: sandboxed shell execution, named tool dispatch, skill management, result tiering, remote execution |
| `session/` | Session lifecycle, config, persistence |
| `detach/` | Background process management (ring buffer, signal) |
| `elevate/` | Elevation broker (privilege escalation daemon, policy) |
| `sandbox/` | Landrun sandbox configuration and command wrapping |
| `env/` | Session environment helpers |
| `log/` | Structured logging |
| `paths/` | XDG path resolution |
### Key Types
**`agent.Agent`** — the concrete struct frontends drive:
```go
type Agent struct {
// unexported fields — no public interface
}
func New(cfg AgentConfig) *Agent
func (a *Agent) Submit(ctx context.Context, input string)
func (a *Agent) Interrupt(cause error) bool
func (a *Agent) Queue(prompt string)
func (a *Agent) State() string
func (a *Agent) WaitChange(ctx context.Context, field, current string) (string, bool)
func (a *Agent) Close()
// ... model/backend info, CWD, usage, cost, etc.
```
**`backend.Backend`** — LLM provider contract:
```go
type Backend interface {
ChatStream(ctx context.Context, messages []Message, tools []Tool, params GenerationParams) (<-chan StreamEvent, error)
Name() string
Model() string
SetModel(model string)
ContextLength(ctx context.Context) int
Models(ctx context.Context) []string
}
```
**`toolsrv.Runner`** — minimal tool execution interface:
```go
type Runner interface {
ListTools() ([]ToolInfo, error)
CallTool(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error)
}
```
**`toolsrv.Server`** — the concrete tool server (implements Runner):
```go
type Server struct { /* ... */ }
```
### Agent Loop (`agent/loop.go`)
The loop is the heart of the system. On each turn:
1. **Stream** — call `backend.ChatStream()` with messages + tool definitions
2. **Dispatch** — if the model returns tool calls, execute them via `toolsrv.Server`
3. **Update** — append assistant message + tool results to session history
4. **Repeat** — loop until the model produces a final text response (no tool calls)
Safety mechanisms:
- **Consecutive error limits**: soft nudge at 5, hard abort at 10
- **Replan gate**: after 8 rounds without a PLAN: block, inject a replan nudge
- **Stall detection**: 5 rounds with unchanged LastAction triggers a nudge
- **Max steps**: configurable per-agent; soft exit when reached
- **Transient retry**: up to 3 retries with exponential backoff for rate limits and 5xx
- **Context overflow**: triggers automatic compaction and retry
### Session & Context Management (`agent/history.go`)
`History` owns the message history as a flat `[]backend.Message` slice. Key behaviors:
- **TaskState**: structured JSON overlay (objective, plan_step, constraints, last_action, next_decision) injected at the top of every turn
- **Compaction**: three-zone strategy when context reaches 50% of model limit:
- **Cold zone**: structured task state summary
- **Warm zone**: one-line decision index (10 messages)
- **Hot zone**: last 8 messages verbatim
- **Result tiers**: tool results classified as Hot (verbatim), Warm (summarized on compaction), or Cold (immediately collapsed)
- **Persistence**: sessions saved as JSON to `{sessionsDir}/{id}.json`
### Hooks (`agent/hooks.go`)
Lifecycle callbacks executed as shell commands with JSON payload on stdin:
| Hook | When | Exit codes |
|---|---|---|
| `agentSpawn` | Session creation | 0=ok, 2=block |
| `preTurn` | Before each turn (deprecated) | 0=inject stdout, 2=block |
| `postTurn` | After each turn | 0=ok |
| `preTool` | Before each tool call | 0=ok, 2=block execution |
| `postTool` | After each tool call | 0=append, 2=replace result |
| `preCompact` | Before compaction | 0=ok |
| `postCompact` | After compaction | 0=ok |
| `turnError` | On backend error | 0=handled (skip retries) |
### Prompt Resolution (`agent/prompt_resolver.go`)
Agent configs declare prompts as a JSON array of shell commands. Each command is executed with `$OLLIE` and `$PWD` available; stdout is concatenated to form the system prompt. This enables dynamic, composable prompts assembled from template fragments.
## 9P Server (`cmd/olliesrv/`)
`olliesrv` is the central daemon. It implements a 9P2000 file server that exposes the entire agent namespace:
### Namespace Layout
```
session/
├── new write key=value to create session
├── idx session index (tab-separated)
├── ls exec to list sessions
├── kill exec to kill a session
├── sh interactive shell
├── b batch one-shot (create, submit, wait, print, kill)
├── bfg batch foreground (submit to existing, wait, print)
├── bbg batch background (submit, return path)
├── cleanup kill idle sessions
├── {sid}/
│ ├── plan session-scoped markdown checklist
│ ├── env session environment
│ └── agent/
│ └── {aid}/
│ ├── cfg session config (key=value)
│ ├── ctl control commands (stop, kill, compact, model, ...)
│ ├── state current state (idle/thinking/calling)
│ ├── statewait blocking read until state changes
│ ├── chat full chat history (streamable)
│ ├── offset byte offset after last user prompt
│ ├── prompt write to submit a prompt
│ ├── prompt.prev last submitted prompt
│ ├── fifo.in queue a prompt (FIFO input)
│ ├── fifo.out pop queued prompt (FIFO output)
│ ├── context rendered context window
│ ├── systemprompt rendered system prompt
│ ├── usage token usage stats
│ ├── cost estimated cost
│ ├── ctxsz context size
│ ├── models available models
│ ├── tools tool management (read all, write name to load)
│ ├── toolsrv.loaded currently loaded tools
│ ├── toolsrv.rev revision counter
│ ├── tail exec helper
│ └── proc/ detached background processes
```
### 9P Interaction Model
All agent interaction uses the `ollie-9p` client, which auto-discovers the server via `$NAMESPACE` and identifies the caller via `$OLLIE_UNAME`. The 9P filesystem exposes session state, control, and inter-agent communication as files:
- Sessions are directories under `session/`
- Prompts, state, chat history, and config are readable/writable files
- Permission enforcement uses 9P user principals
### Permission Model (`fs/session/perm.go`)
All permissions are declared in a single registry (`Perms`). Key design:
- `prompt` is mode `0666` — group+other can write, owner (the agent) cannot
- `chat` is mode `0444` — read-only for everyone
- `ctl` is mode `0666` — anyone can send control commands
- `plan` is mode `0666` — readable by peer agents
### Session Management (`fs/session/`)
The `*fs.Tree` IS the session collection. Package functions manage lifecycle:
- `NewRoot(cfg)` — create the root tree
- `Lookup(tree, id)` — find a session by ID
- `All(tree)` — list all sessions
- `CreateFromRoot(tree, args)` — create a new session
- `KillFromRoot(tree, id)` — kill a session
- `RenameFromRoot(tree, old, new)` — rename a session
- `Shutdown(tree)` — clean shutdown
The adapter is best-effort: if no session bus is available (e.g., headless/container), `olliesrv` continues functioning as a pure 9P server.