docs: update architecture docs for Aug 1-2 refactors

Covers the major changes from the past two days:

- 9P Declarative EDSL (fs/spec.go + fs/builder.go, fs/session/ → fs/ flattening)
- 9P Server streamlining (fid map, GroupTable, method extraction)
- Multi-Agent Deepening (ID/Name split, UUIDv4, two-step creation, empty sessions)
- Eventwait redesign (global /eventwait ring buffer, structured delta events)
- Chat log [[[type]]]/[[[end]]] block format
- KDE submodule evolution (StreamFsm, responsive tree, emoji states, eventwait)

Updates: AGENTS.md, ARCHITECTURE.md, EVOLUTION.md, MULTI_AGENT.md,
         CORE.md, edsl.md
This commit is contained in:
Ollie Agent 2026-08-02 16:43:13 +02:00
parent bb152f401d
commit 6c6c133d76
6 changed files with 425 additions and 116 deletions

View File

@ -9,7 +9,10 @@ ollie/ ← you are here
├── agent/ (Go) Agent loop, history, hooks, commands
├── toolsrv/ (Go) Tool server, registry, sandboxed execution
├── session/ (Go) Session lifecycle, config, persistence
├── fs/session/ (Go) 9P filesystem tree
├── fs/ (Go) 9P filesystem tree (EDSL-declared, flat package)
│ ├── spec.go Single source of truth — entire namespace declared here
│ ├── fsnode.go Core FsNodeDecl type + Dir/Leaf/TemplateDir constructors
│ └── builder.go BuildTree — walks spec, validates, wires handlers
├── cmd/ (Go) Binaries (olliesrv, ollie-9p, ollie-remote)
├── tools/ (Go) Tool implementations:
│ ├── builtin/ Built-in handlers (shell, reasoning, tool/skill registry)
@ -87,6 +90,7 @@ just test-remote
4. **Sandbox** (`sandbox/`): Landlock-based. Config in `sandbox/*.yaml` defines filesystem access per profile.
5. **Backends** (`backend/`): Ollama, OpenAI-compatible, Anthropic, Copilot, Kiro, Gemini, CodeWhisperer. Selectable per-session.
6. **Prompts assembled at runtime**: Agent JSON `prompt` array specifies which prompt files to concatenate. Static prompt files can be included directly; the base system prompt is embedded in the binary and always prepended.
7. **9P namespace declared via EDSL**: The entire filesystem is a single recursive `FsNodeDecl` tree in `fs/spec.go`, built by `BuildTree()` in `fs/builder.go`. The `fs/` package is flat — no sub-package — handler files (`rootfiles.go`, `sessionfiles.go`, `agentfiles.go`) are organized by scope. See `doc/edsl.md` for the full reference.
## Key Files
| What | Where |
|------|-------|
@ -94,7 +98,8 @@ just test-remote
| Tool server | `toolsrv/server.go` |
| Remote execution | `toolsrv/remote.go`, `cmd/ollie-remote/` |
| Sandbox enforcement | `sandbox/` |
| 9P filesystem | `fs/` |
| 9P filesystem (EDSL spec) | `fs/spec.go` |
| 9P filesystem (builder) | `fs/builder.go` |
| Session management | `session/session.go` |
| System prompt template | Embedded in binary |
| Agent configs | `agents/*.json` |

View File

@ -2,21 +2,37 @@
## 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.
All clients communicate via 9P exclusively. Desktop notifications use D-Bus directly (org.freedesktop.Notifications) for elevation prompts only — this is the last remaining D-Bus dependency.
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.
- **9P namespace declared via EDSL.** The entire filesystem is a single recursive `FsNodeDecl` tree in `fs/spec.go`, validated and built by `BuildTree()` in `fs/builder.go`. Adding a file means adding a `Leaf()` call — no manual stat/readdir/write wiring.
- **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:
Single Go module (`ollie`) with one Git submodule (`kde/`) for the KDE frontend:
```
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
├── fs/ 9P filesystem: EDSL spec + handlers (flat package)
│ ├── spec.go Namespace declaration (single source of truth)
│ ├── fsnode.go FsNodeDecl type + Dir/Leaf/TemplateDir constructors
│ ├── builder.go BuildTree — spec -> *Tree wiring
│ ├── rootfiles.go Root-level handlers (backends, models, eventwait, complete, generate, route)
│ ├── sessionfiles.go Session-level handlers (env, ctl, plan, agent/, ...)
│ ├── agentfiles.go Agent-level handlers (prompt, chat, state, cfg, ...)
│ ├── elevatefiles.go Elevation handlers (policy, pending)
│ ├── procfiles.go Process handlers
│ ├── lifecycle.go Session create/kill/rename/shutdown + event ring
│ ├── newroot.go NewRoot — tree construction + persistence restore
│ ├── persist.go Session persistence to disk
│ ├── types.go Session/AgentLog types
│ ├── tree.go 9P *Tree (from fs package)
│ ├── fs.go 9P File/FileConfig implementations
│ └── format.go Event formatting helpers
├── detach/ Background process management (ring buffer, signal)
├── elevate/ Elevation broker (privilege escalation daemon)
├── sandbox/ Landlock sandbox config YAML
@ -28,13 +44,14 @@ ollie/
│ ├── 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
├── kde/ KDE integration (submodule) — standalone GUI, Kate plugin, KRunner
├── data/agents/ Agent config JSONs (default, coding, orchestrator, worker, ...)
├── data/prompts/ Prompt templates (markdown)
├── data/tools/ Tool scripts + .meta sidecar files
├── data/skills/ Domain knowledge modules (markdown)
├── data/scripts/ Helper scripts (ollie-remount, ollie-watchdog)
├── data/services/ Systemd/xdg-autostart service files
├── prompts/ Embedded prompt templates (compiled into binary)
└── doc/ Documentation
```
## System Overview
@ -43,15 +60,17 @@ flowchart TB
subgraph Frontends["Frontends"]
ACME["acme (Plan 9)"]
ELLIE["ellie (Emacs)"]
KDE["KDE GUI / Plasmoid"]
KDE["KDE GUI / Kate / KRunner"]
SH["s/sh (terminal)"]
HTTP["curl / scripts"]
end
subgraph Integration["Integration Layer"]
direction LR
P9["9P Filesystem"]
P9["9P Filesystem\n(ollie-9p client)"]
end
subgraph Server["olliesrv"]
direction TB
NS["9P Namespace\nsession/ · backends · models"]
NS["9P Namespace\nEDSL-declared in fs/spec.go"]
end
subgraph Core["Agent Engine (per session)"]
LOOP["Agent Loop\n(agent/loop.go)"]
@ -65,15 +84,13 @@ flowchart TB
GEMINI["Gemini"]
CW["CodeWhisperer"]
end
TUI --> P9
SH --> P9
ACME --> P9
ELLIE --> P9
KDE --> DBUS
WEB --> DBUS
KDE --> P9
HTTP --> P9
P9 --> NS
DBUS --> DBA
NS --> LOOP
DBA --> LOOP
LOOP <--> SESS
LOOP --> TRV
LOOP <--> Backends
@ -172,61 +189,84 @@ Agent configs declare prompts as a JSON array of shell commands. Each command is
`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
/ ← root (owned by system user)
├── backends backends list
├── help help text
├── models model list (cached)
├── agents agent config list
├── ctl root control (invalidate, kill)
├── eventwait global event stream (blocking read)
├── complete request-response: code completion
├── generate request-response: one-shot LLM generation
├── route request-response: model routing
├── elevate/
│ ├── policy global elevation policy
│ └── pending/{id} pending elevation requests (approve/deny/persist)
└── session/
├── new write key=value to create session
├── idx session index (one line per agent)
├── {name}/
│ ├── env session environment variables
│ ├── ctl session control (kill, save, invalidate)
│ ├── plan session-scoped markdown checklist
│ ├── id immutable session UUID
│ ├── name mutable session name (write to rename)
│ ├── elevate per-session elevation policy
│ └── agent/
│ ├── new write config to create agent (rdwr)
│ └── {aid}/
│ ├── prompt submit a prompt
│ ├── prompt.prev last submitted prompt
│ ├── fifo.in queue a prompt
│ ├── fifo.out pop queued prompt
│ ├── chat streaming chat log (blocking read)
│ ├── log last 64KB of chat (non-blocking)
│ ├── state current state (idle/thinking/calling)
│ ├── statewait blocking read until state changes
│ ├── cfg agent config (key=value)
│ ├── ctl agent control (stop, compact, ...)
│ ├── cwd working directory
│ ├── id immutable agent UUID
│ ├── name mutable agent name
│ ├── offset byte offset after last user prompt
│ ├── usage token usage stats
│ ├── cost estimated cost
│ ├── ctxsz context size
│ ├── models available models
│ ├── systemprompt rendered system prompt
│ ├── context rendered context window
│ ├── tail exec helper for tailing chat
│ ├── tools tool management (read all, write name to load)
│ ├── toolsrv.loaded currently loaded tools
│ ├── toolsrv.rev revision counter
│ └── proc/{pid} detached process output
```
### 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/`
- Session creation is a two-step process: `session/new` (name only) then `session/{name}/agent/new` (full config)
- Sessions and agents have **immutable UUIDs** (`id` file) and **mutable human-readable names** (`name` file); writing to `name` renames the entry
- Prompts, state, chat history, and config are readable/writable files
- Streaming via blocking reads: `chat`, `statewait`, `eventwait` block the 9P read until data arrives
- Permission enforcement uses 9P user principals
### Permission Model (`fs/perm.go`)
All permissions are declared in a single registry (`Perms`). Key design:
- The entire namespace is declared as a single EDSL tree in `fs/spec.go` — see `doc/edsl.md`
### Permission Model (`fs/spec.go`)
Permissions are declared inline in the EDSL spec via `mode` and `GID()` options. There is no separate permission registry — the spec is the source of truth:
- `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/`)
- `eventwait` is `0444` — blocking read for global event stream
- `session/new` is `0666` with `Request` handler — anyone can create sessions
- Ownership inherits: empty `UID`/`GID` values inherit from parent node, all the way up to root
### Session Management (`fs/`)
The `*fs.Tree` IS the session collection. Package functions manage lifecycle:
- `NewRoot(cfg)` — create the root tree
- `NewRoot(cfg)` — create the root tree from the EDSL spec (`fs/spec.go` → `BuildTree` → wired `*Tree`)
- `Lookup(tree, id)` — find a session by ID
- `All(tree)` — list all sessions
- `CreateFromRoot(tree, args)` — create a new session
- `CreateFromRoot(tree, args)` — create a new session (two-step: session name, then agent creation)
- `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.
- `Shutdown(tree)` — clean shutdown (interrupt all, wait for idle, persist, close)
Event streaming uses a global ring buffer (`eventRing`) with a global `/eventwait` file. Events carry prefixes (`S` for session, `A` for agent) with structured descriptions (`new`, `kill`, `rename`). Frontends block on `/eventwait` and receive delta events without polling.

View File

@ -57,17 +57,22 @@ flowchart TB
SESS["session.go\nSession, Config"]
end
subgraph FsSession["fs/session/ package"]
TREE["tree.go\n*fs.Tree"]
ROOT["root.go\nNewRoot()"]
CREATE["create.go\nCreateFromRoot()"]
FILES["files.go\nfile operations"]
PERM["perm.go\npermission registry"]
subgraph Fs["fs/ package"]
TREE["tree.go\n*Tree"]
NEWROOT["newroot.go\nNewRoot()"]
LIFECYCLE["lifecycle.go\nCreate/Kill/Rename"]
SESSFILES["sessionfiles.go\nsession file handlers"]
AGENTFILES["agentfiles.go\nagent file handlers"]
ROOTFILES["rootfiles.go\nroot file handlers"]
ELEVATEFILES["elevatefiles.go\nelevation handlers"]
PROCFILES["procfiles.go\nprocess handlers"]
PERSIST["persist.go\nsession persistence"]
FSGO["fs.go\n9P filesystem"]
FS_GO["fs.go\n9P File/FileConfig"]
FORMAT["format.go\nevent formatting"]
SYNTH["synth.go\nsynthetic files"]
TYPES["types.go\nSession struct"]
SPEC["spec.go\nEDSL namespace declaration"]
BUILDER["builder.go\nBuildTree"]
FSNODE["fsnode.go\nFsNodeDecl"]
end
subgraph Support["Supporting Packages"]
@ -82,7 +87,7 @@ flowchart TB
Agent --> Backend
Agent --> Toolsrv
Agent --> Session
FsSession --> Session
Fs --> Session
Toolsrv --> SANDBOX
Toolsrv --> DETACH
Toolsrv --> ELEVATE

View File

@ -350,34 +350,6 @@ Credentials are requested via `kdialog`/`zenity` on the local desktop, or forwar
over SSH for remote execution. The tool itself has no knowledge of sudo — the
privilege wrapping is entirely in the dispatch layer.
## Current Topology
```
ollie/ ← single Go module
├── agent/ ← agent loop, history, hooks, commands
├── backend/ ← LLM providers (6 backends)
├── toolsrv/ ← tool server, registry, remote execution
├── session/ ← session lifecycle, config
├── **fs**/ ← 9P filesystem tree
├── dbus/ ← D-Bus adapter
├── detach/ ← background process management
├── elevate/ ← elevation broker
├── sandbox/ ← landrun sandbox config
├── env/ ← environment helpers
├── log/ ← structured logging
├── paths/ ← XDG path resolution
├── mount/ ← 9P FUSE mount client (network transparency)
├── cmd/ ← binaries (olliesrv, ollie-9p, ollie-remote)
├── kde/ ← KDE Plasma (submodule)
├── data/tools/ ← tool executables + .meta sidecar files
├── data/agents/ ← agent configs (JSON)
├── data/prompts/ ← system prompt fragments (markdown)
├── data/skills/ ← domain knowledge modules (markdown)
├── data/scripts/ ← ollie-remount, ollie-watchdog
├── data/services/ ← systemd, xdg-autostart
├── prompts/ ← embedded prompt templates
├── sandbox/ ← landrun sandbox profiles (YAML)
└── doc/ ← documentation
```
## Principles That Emerged
1. **Filesystem-as-API** — everything is read/write on synthetic files. No
custom protocols needed.
@ -444,3 +416,270 @@ Lessons:
QTextDocument relayout is O(n) on the full content.
- The `loadEarlier` scroll-triggered cascade was creating 500 delegates
on startup. Bounded initial window + explicit scroll-back is correct.
## Phase 14: Multi-Agent Deepening — ID/Name Split & Empty Sessions (Aug 1)
The multi-agent data structures introduced during the Great Flattening
required further refinement to support true multi-agent sessions.
### ID/Name Split
Sessions and agents now have **two identities**:
- **`id`** (immutable UUIDv4, set at creation, never changes)
- **`name`** (mutable human-readable string, defaults to first UUID segment)
The sessions map is keyed by Name, not ID. Renaming a session or agent moves
the directory in the 9P namespace — `mv` on the session directory calls
`RenameFromRoot`, and `write` to the agent's `name` file calls `SetName`.
Both entities expose `id` and `name` files in their 9P directories.
### Two-Step Session Creation
Creation split into two phases:
1. `session/new` — write `name=<name>` to create an empty session (no agent yet)
2. `session/{name}/agent/new` — write `cwd=<dir> backend=<backend> model=<model> agent=<agent>` to create the agent
This enables creating sessions in advance and attaching agents later. Empty
sessions (`Core=nil`) are valid — they appear in `session/idx` with empty
fields and are fully killable/renameable.
### Multi-Agent Data Structures
- `session.Session` supports multiple agents (`Agents()` returns `[]*agent.Agent`)
- `Session.AgentLogs` maps agent IDs to `*AgentLog` instances
- Active agent is selectable, each agent has its own prompt/chat/state/log
- `session/idx` emits one line per agent, not one line per session
Key commits: 92 commits across Aug 1 touching session/, agent/, fs/, and kde/.
## Phase 15: 9P Declarative EDSL (Aug 2 — morning)
The most significant structural change to the 9P server since its inception:
**replaced the imperative filesystem with a declarative EDSL**.
### Before: Imperative
Every 9P operation (stat, walk, read, write, open, create, remove) was
hand-coded per node. The `fs/session/` sub-package contained ~3,100 lines
of manual tree-building code split across 6 files (`files.go`, `root.go`,
`perm.go`, `synth.go`, `create.go`, `persist.go`). Adding a new file meant
updating stat, readdir, open, and permission code paths.
### After: Declarative EDSL
The entire namespace is declared in a single `FsNodeDecl` tree in `fs/spec.go`:
```go
var treeSpec = Dir("/",
Leaf("backends", 0444, Read(readBackends), GID("agent")),
Leaf("eventwait", 0444, Read(readEventwait), Stream(blockEventwait), GID("agent")),
Leaf("complete", 0666, Request(requestComplete), GID("agent")),
// ...
Dir("session", GID("agent"),
Leaf("new", 0666, Request(requestSessionNew)),
Leaf("idx", 0444, Read(readSessionIdx)),
TemplateDir("{id}", listSessions),
),
)
```
`BuildTree()` in `fs/builder.go` walks the spec, validates invariants
(directories can't have leaf handlers, at most one blocking variant,
template nodes must have `List`, ownership inheritance), and produces
a fully-wired `*fs.Tree`.
### Package consolidation
- Entire `fs/session/` sub-package flattened into `fs/` — 6 files deleted
- `cmd/olliesrv/elevate_tree.go` — elevation tree moved to `fs/elevatefiles.go`
- `cmd/olliesrv/server.go` simplified from ~820+ lines to ~200 lines of thin
9P protocol handling; all filesystem logic is now in `fs/`
- Handler files organized by scope: `rootfiles.go`, `sessionfiles.go`,
`agentfiles.go`, `elevatefiles.go`, `procfiles.go`
### Net result
- **−3,152 lines** deleted, **+2,086 lines** added across 24 files
- Adding a file = adding one `Leaf()` call. No manual stat/readdir/write.
- Validation at `BuildTree` time catches structural errors before serving.
- Permission model inlined in the spec via `mode` and `GID()` — no separate `perm.go`.
## Phase 16: 9P Server Streamlining (Aug 2 — afternoon)
Following the EDSL conversion, the 9P protocol server itself
(`cmd/olliesrv/server.go`) received a focused cleanup over 13 commits:
### Fid management
- **Server owns the fid map**: validates newfids (not already in use, not stale),
garbage-collects orphaned fids. No more leaking fids on client disconnect.
- **Store opened file on fid**: read/write use `f.entry` directly instead of
re-resolving the path on every operation. Eliminates a class of TOCTOU bugs.
### Method extraction
- `attach` and `open` extracted from `*Server` — pass groups and log explicitly
- `GroupTable` extracted from `*Server` — permission checking uses bitmask OR
instead of a boolean dance (`checkPermBits` → `hasPermBits`)
- `checkPerm` extracted to package-level, permissions resolved in `open`
- 9 `rootTree`-only methods extracted to package-level functions; switch
statement replaced with handler map
- Three thin wrapper methods inlined (`readFile`, `writeFile`, `FileTree` alias)
- `handle` inlined into `Start`; renamed `Serve` → `Start`, `Shutdown` → `Kill`
### Result
- `server.go` reduced from ~820 to ~200 lines
- `main.go` simplified as elevation tree wiring moved to `fs/`
- Server is now a thin 9P2000 protocol translator, not a filesystem
## Phase 17: Eventwait Redesign & Chat Log Format (Aug 2)
### Global /eventwait
Replaced per-session event polling with a global event ring buffer and a
single `/eventwait` file. Events carry structured prefixes and delta descriptions:
- `S new session/<name>` — session created
- `S kill session/<name>` — session destroyed
- `S rename session/<old> session/<new>` — session renamed
- `A kill session/<name>/agent/<aid>` — agent killed
The event ring is a fixed-size circular buffer (100 slots) with a `sync.Cond`
for blocking reads. Frontends block on `/eventwait` and receive deltas since
their last known offset. No polling, no timer, no D-Bus.
Fixes along the way:
- Dangling pointer in `eventRing` cond initialization (caused panics under load)
- Double-unlock panic in `WaitEvent`
- Spurious `[[[end]]]` markers on reasoning blocks (reasoning events were
silently suppressed from chat but still advanced the role-transition state)
### Chat log: [[[type]]]/[[[end]]] block format
The chat log switched from plain text to a structured block format:
```
[[[assistant:resp-abc123]]]
Hello! How can I help?
[[[end]]]
[[[tool:file_read]]]
File contents...
[[[end]]]
```
This enables reliable parsing of multi-turn, multi-role conversations
from the flat log. Each event type (`assistant`, `tool`, `call`, `retry`,
`error`, `usage`, `info`, `exec`) opens a block and `[[[end]]]` closes it.
`reasoning` and `reasoning_think` events are suppressed from the log
entirely — they're included in the LLM context but invisible to the user.
### KDE submodule evolution
The KDE frontend received extensive updates across both days:
- **StreamFsm refactor** — chat block FSM with proper fence-post handling
- **Responsive session tree** — selection/statewait stream management
- **Explicit agent selection** — per-agent statewait/chat streams,
only `switchAgent` starts streams; session click clears agent selection
- **Emoji-labeled states** — visual state indicators (❌ for errors, ⚠️ for warnings)
- **Double-click rename** — inline renaming of agent nodes
- **Robust session loading** — loads sessions on startup, handles empty state
- **Context menu** — kill session, per-session operations
- **Eventwait-driven updates** — replaced timer-based polling with blocking `/eventwait`
- **Fence fix** — fixed a bug where code fences could eat surrounding content
- Removed D-Bus entirely; all communication via `9p` plan9port binary
### Updated Filesystem Layout (post-refactor)
```
/ ← root (owned by system user)
├── backends backends list
├── help help text
├── models model list (cached)
├── agents agent config list
├── ctl root control (invalidate, kill)
├── eventwait global event stream (blocking read)
├── complete request-response: code completion
├── generate request-response: one-shot LLM generation
├── route request-response: model routing
├── elevate/
│ ├── policy global elevation policy
│ └── pending/{id} pending elevation requests
└── session/
├── new write key=value to create session
├── idx session index (one line per agent)
├── {name}/
│ ├── env session environment variables
│ ├── ctl session control (kill, save, invalidate)
│ ├── plan session-scoped markdown checklist
│ ├── id immutable session UUID
│ ├── name mutable session name (write to rename)
│ ├── elevate per-session elevation policy
│ └── agent/
│ ├── new write config to create agent (rdwr)
│ └── {aid}/
│ ├── prompt, prompt.prev, fifo.in, fifo.out
│ ├── chat (streaming), log (64KB window)
│ ├── state, statewait (blocking)
│ ├── cfg, ctl, cwd
│ ├── id (immutable), name (mutable)
│ ├── offset, usage, cost, ctxsz, models
│ ├── systemprompt, context, tail
│ ├── tools, toolsrv.loaded, toolsrv.rev
│ └── proc/{pid}
```
## Updated Current Topology
```
ollie/ ← single Go module
├── agent/ ← agent loop, history, hooks, commands
├── backend/ ← LLM providers (6 backends)
├── toolsrv/ ← tool server, registry, remote execution
├── session/ ← session lifecycle, config
├── fs/ ← 9P filesystem (EDSL-declared, flat package)
│ ├── spec.go Namespace declaration (single source of truth)
│ ├── fsnode.go FsNodeDecl type + Dir/Leaf/TemplateDir
│ ├── builder.go BuildTree — spec → wired *Tree
│ ├── rootfiles.go Root-level handlers
│ ├── sessionfiles.go Session-level handlers
│ ├── agentfiles.go Agent-level handlers
│ ├── elevatefiles.go Elevation handlers
│ ├── procfiles.go Process handlers
│ ├── lifecycle.go Create/kill/rename + event ring
│ ├── newroot.go NewRoot constructor
│ ├── persist.go Session persistence
│ ├── types.go Session/AgentLog types
│ ├── tree.go 9P *Tree
│ ├── fs.go 9P File/FileConfig
│ └── format.go Event formatting
├── detach/ ← background process management
├── elevate/ ← elevation broker
├── sandbox/ ← landrun sandbox config
├── env/ ← environment helpers
├── log/ ← structured logging
├── paths/ ← XDG path resolution
├── mount/ ← 9P FUSE mount client (network transparency)
├── cmd/ ← binaries (olliesrv, ollie-9p, ollie-remote)
├── kde/ ← KDE Plasma (submodule)
├── data/tools/ ← tool executables + .meta sidecar files
├── data/agents/ ← agent configs (JSON)
├── data/prompts/ ← system prompt fragments (markdown)
├── data/skills/ ← domain knowledge modules (markdown)
├── data/scripts/ ← ollie-remount, ollie-watchdog
├── data/services/ ← systemd, xdg-autostart
├── prompts/ ← embedded prompt templates
├── sandbox/ ← landrun sandbox profiles (YAML)
└── doc/ ← documentation
```
## Dead Ends and Reversals (updated)
| What | Why it was removed |
|------|-------------------|
| MCP servers | Too heavyweight; scripts are simpler and faster |
| TUI frontend | Replaced by KDE GUI and Emacs (ellie) |
| s/sh, s/bfg, s/bbg | Replaced by `generate`/`complete`/`route` 9P files |
| `pl/` planning namespace | Over-engineered; plan file is sufficient |
| Beads integration | External dependency for something a checklist file handles |
| SSH agent proxy | Attack surface not worth the convenience |
| Separate ollie-dbus daemon | Consolidated into olliesrv |
| FUSE mounts | Replaced by standalone 9P client binary |
| Tier routing (FAST/POWER) | Replaced by /route endpoint with real model discovery |
| call_tool / pipe | Replaced by native tool registry with tool_load |
| execute_code (multi-lang) | Simplified to shell-only; tool scripts handle language choice |
| chatwait | Reverted; acme tails chat directly |
| Header comment metadata | Replaced by .meta sidecar JSON; decouples metadata from language |
| `contrib/` directory | Renamed to `data/`; nothing was community-contributed |
| Script namespaces (s/, u/, x/) | Replaced by 9P request-response files |
| D-Bus adapter | 9P streaming is superior; no polling, no offset tracking, no frozen GUIs |
| `fs/session/` sub-package | Flattened into `fs/` — no more sub-package indirection |
| `cmd/olliesrv/elevate_tree.go` | Elevation tree handlers moved to `fs/elevatefiles.go` |
| Per-session/agent timestamp IDs | Replaced by UUIDv4 — immutable `id` + mutable `name` |
| Single-step session creation | Split into `session/new` + `session/{name}/agent/new` |
| D-Bus-driven KDE frontends | Rewritten on pure 9P via plan9port `9p` binary |
| Plasmoid/tray KDE components | Deleted — not useful enough to maintain |

View File

@ -6,18 +6,26 @@ ordinary shell scripting against a mounted filesystem.
## The Namespace
Every session lives under `/session/{id}/`:
Every session lives under `/session/{name}/` where `{name}` is the session's
**mutable human-readable name** (not the immutable UUID — see `id` and `name`
files). Session directories are keyed by Name; renaming a session via writing
to its `name` file moves the entire directory subtree.
| Path | Purpose |
|------|---------|
| `/session/new` | Create a session (write `name`, `backend`, `model`, `agent`, `cwd`) |
| `/session/{id}/plan` | Session-scoped markdown checklist |
| `/session/{id}/env` | Session environment variables |
| `/session/{id}/agent/{aid}/prompt` | Submit a prompt (dispatched async on close) |
| `/session/{id}/agent/{aid}/chat` | Cumulative conversation history (append-only read) |
| `/session/{id}/agent/{aid}/cfg` | Agent config (KV): state, backend, model, agent, cwd, params |
| `/session/{id}/agent/{aid}/statewait` | **Blocking read** — returns when state changes |
| `/session/{id}/agent/{aid}/ctl` | Control commands: `stop`, `kill`, `compact`, `rn <name>` |
| `/session/new` | Create an empty session (write `name=...`, read back name) |
| `/session/{name}/agent/new` | Create an agent within a session (write `cwd=... backend=...`) |
| `/session/{name}/plan` | Session-scoped markdown checklist |
| `/session/{name}/env` | Session environment variables |
| `/session/{name}/id` | Immutable session UUID |
| `/session/{name}/name` | Mutable session name (write to rename) |
| `/session/{name}/agent/{aid}/prompt` | Submit a prompt |
| `/session/{name}/agent/{aid}/chat` | Streaming chat log (blocking read) |
| `/session/{name}/agent/{aid}/cfg` | Agent config KV: backend, model, agent, cwd, params |
| `/session/{name}/agent/{aid}/statewait` | **Blocking read** — returns when state changes |
| `/session/{name}/agent/{aid}/ctl` | Control commands: `stop`, `kill`, `compact`, `rn <name>` |
| `/session/{name}/agent/{aid}/id` | Immutable agent UUID |
| `/session/{name}/agent/{aid}/name` | Mutable agent name (write to rename) |
Global request-response files provide stateless operations without sessions:
@ -32,8 +40,8 @@ Global namespaces are shared across all sessions:
| Path | Purpose |
|------|---------|
| `/session/idx` | Session index (id, state, cwd, backend, model) |
| `/session/{id}/plan` | Shared session-scoped checklist |
| `/session/{id}/agent/{aid}/chat` | Full chat history (searchable) |
| `/session/{name}/plan` | Shared session-scoped checklist |
| `/session/{name}/agent/{aid}/chat` | Full chat history (searchable) |
## Spawning Agents
@ -43,9 +51,21 @@ From within an agent session, use `subagent_spawn`:
subagent_spawn [-name NAME] [-agent AGENT] [-backend BACKEND] [-model MODEL] [-cwd DIR] <prompt>
```
This creates a child session, writes the prompt, and returns the session path
(e.g. `session/my-session`). Child sessions carry the parent session ID in their name,
so the spawning tree is recoverable from session IDs alone.
This creates a child session + agent atomically, writes the prompt, and returns
the session path (e.g. `session/my-session`). Child sessions carry the parent
session ID in their name, so the spawning tree is recoverable from session IDs alone.
### Explicit Two-Step Creation
Session creation can be split into two phases for advanced orchestration
scenarios:
1. **Create empty session**: `echo "name=my-session" | ollie-9p rdwr session/new`
2. **Add agent**: `echo "cwd=/home/user agent=default backend=openai model=gpt-4o" | ollie-9p rdwr session/my-session/agent/new`
Empty sessions (`Core=nil`) are valid — they appear in `session/idx`, have
no running agent, and can be killed or renamed. This enables pre-provisioning
session directories before the agent environment is ready.
## Coordination Patterns
@ -64,7 +84,7 @@ flowchart TB
```
A root agent reasons about a task and delegates subtasks to workers. Workers
report back via their chat history; the conductor reads `/session/{id}/agent/{aid}/chat` after
report back via their chat history; the conductor reads `/session/{name}/agent/{aid}/chat` after
`statewait` unblocks.
```sh

View File

@ -76,7 +76,7 @@ Leaf("chat", 0444, Read(readAgentChat), Stream(blockAgentChat))
| `Stream(fn)` | `func(HandlerCtx, ctx, base) (data, base, err)` | `d.Stream` |
| `BlockOnce(fn)` | `func(HandlerCtx, ctx, base) (data, base, err)` | `d.BlockOnce` |
| `Request(fn)` | `func(HandlerCtx, []byte) ([]byte, error)` | `d.Request` |
| `Create(fn)` | `func(HandlerCtx, string) error` | `d.Create` |
| `CreateFile(fn)` | `func(HandlerCtx, string) error` | `d.Create` |
| `Remove(fn)` | `func(HandlerCtx) error` | `d.Remove` |
| `Stat(fn)` | `func() os.FileInfo` | `d.Stat` |
| `UID(s)` | string | `d.UID` |