197 lines
6.5 KiB
Markdown
197 lines
6.5 KiB
Markdown
# Multi-Agent Coordination
|
|
|
|
ollie exposes agents as filesystem objects via the 9P protocol. This means
|
|
multi-agent coordination requires no framework, SDK, or message bus — it is
|
|
ordinary shell scripting against a mounted filesystem.
|
|
|
|
## The Namespace
|
|
|
|
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 an empty session (write `name=...`, read back name) |
|
|
| `/session/{name}/agent/new` | Create an agent within a session (write `cwd=... backend=...`) |
|
|
| `/session/{name}/env` | Session environment variables |
|
|
| `/session/{name}/id` | Immutable session UUID |
|
|
| `/session/{name}/name` | Mutable session name (write to rename) |
|
|
| `/session/{name}/agent/{aname}/prompt` | Submit a prompt |
|
|
| `/session/{name}/agent/{aname}/chat` | Streaming chat (filtered text) |
|
|
| `/session/{name}/agent/{aname}/plan` | Agent-scoped markdown checklist |
|
|
| `/session/{name}/agent/{aname}/cfg` | Agent config KV: backend, model, agent, cwd, params |
|
|
| `/session/{name}/agent/{aname}/statewait` | **Blocking read** — returns when state changes |
|
|
| `/session/{name}/agent/{aname}/ctl` | Control commands: `stop`, `kill`, `compact`, `rn <name>` |
|
|
| `/session/{name}/agent/{aname}/id` | Immutable agent ID (uname) |
|
|
| `/session/{name}/agent/{aname}/name` | Mutable display name (write to rename) |
|
|
|
|
Global request-response files provide stateless operations without sessions:
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `/generate` | Write prompt, read response (one-shot generation) |
|
|
|
|
Global namespaces are shared across all sessions:
|
|
|
|
| Path | Purpose |
|
|
|------|---------|
|
|
| `/session/idx` | Session index (id, state, cwd, backend, model) |
|
|
| `/session/{name}/agent/{aname}/plan` | Agent checklist |
|
|
| `/session/{name}/agent/{aname}/chat` | Streaming filtered text |
|
|
|
|
## Spawning Agents
|
|
|
|
From within an agent session, use `subagent_spawn`:
|
|
|
|
```sh
|
|
subagent_spawn [-name NAME] [-agent AGENT] [-backend BACKEND] [-model MODEL] [-cwd DIR] <prompt>
|
|
```
|
|
|
|
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
|
|
|
|
### Hub-and-Spoke (Conductor)
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
ROOT["Root Agent\n(reasoning)"]
|
|
W1["Worker A\n(cheap model)"]
|
|
W2["Worker B\n(cheap model)"]
|
|
|
|
ROOT -- "subagent_spawn" --> W1
|
|
ROOT -- "subagent_spawn" --> W2
|
|
W1 -- "statewait → read chat" --> ROOT
|
|
W2 -- "statewait → read chat" --> ROOT
|
|
```
|
|
|
|
A root agent reasons about a task and delegates subtasks to workers. Workers
|
|
report back via their log; the conductor reads `/session/{name}/agent/{aname}/log` after
|
|
`statewait` unblocks.
|
|
|
|
```sh
|
|
# Conductor spawns two workers
|
|
worker_a=$(subagent_spawn -model haiku "analyse /src/a.go")
|
|
worker_b=$(subagent_spawn -model haiku "analyse /src/b.go")
|
|
|
|
# Wait for both
|
|
until [ "$(cat $worker_a/state)" = "idle" ]; do cat $worker_a/statewait; done
|
|
until [ "$(cat $worker_b/state)" = "idle" ]; do cat $worker_b/statewait; done
|
|
|
|
# Collect results
|
|
cat $worker_a/chat $worker_b/chat
|
|
```
|
|
|
|
The conductor and workers can run on different models. Mechanical analysis goes
|
|
to cheaper models; reasoning stays on the expensive one.
|
|
|
|
### Pipeline
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A["Step 1\nExtract TODOs"]
|
|
B["Step 2\nPrioritise"]
|
|
A -->|"/tmp/todos.txt"| B
|
|
```
|
|
|
|
Agent A produces output; agent B consumes it. Composition via `/tmp/` or direct
|
|
path passing.
|
|
|
|
```sh
|
|
step1=$(subagent_spawn "extract all TODOs from /src/ and write to /tmp/todos.txt")
|
|
until [ "$(cat $step1/state)" = "idle" ]; do cat $step1/statewait; done
|
|
subagent_spawn "prioritise the TODOs in /tmp/todos.txt and produce a plan"
|
|
```
|
|
|
|
### Scatter-Gather (Swarm)
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
P["Parent"]
|
|
S1["Worker 1"]
|
|
S2["Worker 2"]
|
|
S3["Worker N"]
|
|
P --> S1
|
|
P --> S2
|
|
P --> S3
|
|
S1 --> P
|
|
S2 --> P
|
|
S3 --> P
|
|
```
|
|
|
|
Spawn N workers in parallel, collect when all reach `idle`. Because `statewait`
|
|
blocks until state changes, no polling loop is needed.
|
|
|
|
```sh
|
|
# Scatter
|
|
for f in /src/*.go; do
|
|
subagent_spawn "review $f for security issues" &
|
|
done
|
|
wait # wait for all subagent_spawn calls to return session paths
|
|
```
|
|
|
|
Or from within `shell`:
|
|
|
|
```json
|
|
{
|
|
"steps": [
|
|
{ "parallel": [
|
|
{ "code": "subagent_spawn 'task A'" },
|
|
{ "code": "subagent_spawn 'task B'" },
|
|
{ "code": "subagent_spawn 'task C'" }
|
|
]}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Peer-to-Peer
|
|
|
|
Any agent can read any other agent's session directory. There is no privileged
|
|
orchestrator role — agents coordinate by reading each other's `chat` and `state`
|
|
files directly. A peer can wait on another peer's `statewait`, then act on what
|
|
it reads in `chat`.
|
|
|
|
### Reactive / Event-Driven
|
|
|
|
`statewait` is a proper synchronization primitive. A shell script (or another
|
|
agent) blocks on it and wakes exactly when the target agent transitions state.
|
|
This enables event-driven pipelines without polling:
|
|
|
|
```sh
|
|
# Block until agent finishes, then trigger downstream work
|
|
cat /session/worker-123/statewait
|
|
# agent is now idle — proceed
|
|
```
|
|
|
|
### Self-Directed Spawning
|
|
|
|
Agents can spawn sub-agents mid-task without human direction. If an agent
|
|
determines that parallelism or specialization would help, it calls
|
|
`subagent_spawn` on its own. No human prompt is required to initiate a
|
|
multi-agent workflow — the model decides.
|
|
|
|
## Design Principle
|
|
|
|
ollie imposes no coordination model. There is no built-in conductor, swarm
|
|
layer, or workflow engine. Orchestration is handled by the surrounding
|
|
environment — shell scripts, cron, systemd, containers, or another agent.
|
|
|
|
This is intentional. The filesystem namespace is the API. Any tool that can
|
|
read and write files can participate in a multi-agent workflow, which means the
|
|
full Unix toolchain is available as an orchestration layer. |