Coordination model
What
Castellan agents coordinate through a shared coordination field, not through conversation. A typed blackboard, decaying pheromone zones, and a pressure-driven scheduler decide who wakes and when — there is no router LLM picking the next agent, no natural-language handoff, and no agent-to-agent chat.
Why
Coordination geometry (topology, zones, wake thresholds) is the thing that gets tuned and evolved, not prompt text. Vocabulary: Conventions.
If you are wiring up a plugin, debugging why an agent never wakes, or trying to understand what castellan run --mux actually does under the hood, this page is the mental model you need.
Substrate primitives
| Primitive | Type | Role |
|---|---|---|
| Blackboard | Typed JSON slots | Structured read/write state, scoped per plugin manifest ([blackboard] slots = [...]) |
| Pheromone zones | PheromoneField | Named signal maps with exponential decay; agents perceive within a radius |
| Perception radius | Scalar per zone | Bounds how far an agent reads field signals — no global broadcast |
| Pressure field | PressureScheduler queue | Wakes tasks when dependency signals + base pressure clear a threshold |
| Completion signal | Deposit | NextAction::Complete deposits a completion signal into the zone; dependents wake |
| Quorum gate (H1) | QuorumSensor | Phase unlock when density ≥ Q and ≥ k distinct contributors deposit within window W — colony consensus without a router LLM |
| Trail corridors (H4/H9) | CorridorField | Optional (from,to,signal) edges with per-edge λ; Physarum conductance c ← c + α·flux − β·c prunes low-flow paths |
| Cement (H3) | cement signal | Successful verify deposits cement; morphogenesis may birth topology nodes under regulatory caps |
Wake pressure is computed as min(base_pressure, min(dependency signals)), optionally boosted by multiplexer pane activity (pane_status_wake_boost). A task with unmet dependencies stays deferred no matter how high its own base pressure is — the field, not an individual agent, decides readiness.
Quorum = colony phase gate. Unlike task wake (deps ∩ pressure), quorum locks a phase transition until multi-contributor density clears — ablate to a single depositor and the phase stays locked (FalseQuorum immune when mass lacks diversity).
Response thresholds (H2). Topology nodes carry private θ maps; among ready tasks the scheduler can prefer the node with highest stimulus−θ surplus — specialization emerges without hardcoded roles.
Allowed vs forbidden coordination patterns
| Pattern | Status | Why |
|---|---|---|
| Deposit completion → dependent wakes | Allowed | Core substrate mechanism |
| Blackboard slot handoff (typed JSON) | Allowed | Structured, auditable, replayable |
NextAction::Delegate { task } | Allowed | Explicit sub-task spawn, still substrate-mediated |
Multiplexer pane deposits (pheromone_deposits) | Allowed | Pane activity feeds the pane zone |
| Agent-to-agent chat | Forbidden | Re-introduces router-LLM coordination cost and non-determinism |
| Router LLM choosing next agent | Forbidden | Defeats the “coordination is geometry, not prompts” thesis |
| Re-encoding substrate state as natural language for another agent to parse | Forbidden | Loses structure, adds token cost, adds failure surface |
Anatomy: one scheduler tick
flowchart TB
G[Goal] --> P[encode_task]
P --> Q[Pending queue]
Q --> RP[refresh_pressure]
RP --> DEP{deps met?}
DEP -->|no| DEFER[deferred]
DEP -->|yes| DISPATCH[dispatch]
DISPATCH --> HOST[RunHost]
HOST --> OBS[observation]
OBS --> BB[Blackboard]
OBS --> FIELD[PheromoneField]
OBS --> V[verify_goal]
V --> COMP[deposit completion]
COMP --> FIELD
FIELD --> RP
classDef runtime stroke:#0969da,stroke-width:2px
classDef substrate stroke:#5b6b8c,stroke-width:2px
classDef gate stroke:#1a7f37,stroke-width:2px
class G,P,Q,RP,DISPATCH,HOST runtime
class OBS,BB,FIELD substrate
class V,COMP gate
CastellanEngine::run_goal walks this loop until the goal is satisfied, exhausted, or a step budget expires:
- Encode — the plugin’s
encode_task(goal)produces the initialTaskDescriptor. - Decay + refresh — the field decays one tick;
refresh_pressurerecomputes wake pressure for every pending task. - Dispatch — the ready batch (dependencies met, pressure over threshold) goes to the
RunHost(in-process, mux panes, or remote). - Observe — host output updates the blackboard and deposits into zone(s);
coordinate_from_observationapplies any explicit deposits from the plugin. - Verify —
verify_goalchecks the observation against the goal; on success it deposits acompletionsignal so dependents wake next tick. - Snapshot — field state, topology, and wake stats are written into the episode JSON under
.castellan/episodes/.
How operators use it
You rarely touch the substrate directly — you drive it through castellan run and read its effects back out of episode JSON and live MCP tools.
1. Run a goal and let the scheduler coordinate:
castellan run --goal "reach target" --plugin gridworld
2. Force scheduler-led multi-pane coordination (topology + pane tree recorded in the episode):
castellan run --goal "reach target" --plugin gridworld --mux
3. Inspect live substrate state while a run is active (via MCP tools, from an editor or castellan mcp client):
4. Replay a past run to see the coordination trace tick-by-tick:
castellan replay --episode .castellan/episodes/<goal-id>.json
5. Watch multiplexer pane activity feed the field — pane observations can carry pheromone_deposits:
{"pheromone_deposits": [{"zone": "pane", "signal": "activity", "amount": 0.5}]}
These deposits feed pane_status_wake_boost, which nudges wake pressure for stalled tasks whenever panes report activity — useful when a long-running shell pane is making progress but hasn’t hit a dependency signal yet.
Recipes
When a plugin has independent sub-tasks that must merge before completion, use NextAction::Delegate to spawn them and let the blackboard carry results back — don’t have the parent poll or message the children in natural language.
When you want a run to react to external pane activity (e.g. a human working in another tab), enable --mux so pane deposits land in the pane zone and boost wake pressure for dependent tasks.
When debugging “my task never wakes,” dump the episode JSON and check pending/deferred tasks against their declared dependencies — the field will not dispatch a task early no matter how urgent it looks to a human reader.
When you need deterministic, replayable coordination for CI, avoid --rlm/LLM-driven verify and stick to plugin-native verify_goal — the scheduler loop itself never calls an LLM.
Failure paths / troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Task stuck in deferred forever | Dependency never deposits completion (upstream task failed or plugin didn’t verify) | Check upstream task’s verify_goal result in episode JSON; fix the plugin’s completion condition |
| Pane activity doesn’t seem to speed anything up | --mux not set, so pane deposits never reach the scheduler’s field | Re-run with --mux, or confirm the multiplexer socket is up (castellan multiplexer ensure) |
| Two plugins fight over the same zone | Zones overlap in plugin.toml [zones] without intent | Give each plugin a distinct primary zone; share default zones deliberately |
| Wake pressure looks “stuck” below threshold | Perception radius too small to see the depositing zone | Widen the zone’s perception radius or deposit into a zone the waiting task actually watches |
| Coordination trace missing from episode | Run used --no-mux / in-process host without topology tracking | Re-run with --mux if you need pane_tree and per-zone deposit history |
See also
- Memory architecture — where coordination state gets persisted
- Organism model — how coordination parameters get evolved
- Plugin architecture — how plugins declare zones and blackboard slots
- Multiplexer overview — pane deposits and
--mux - Quickstart — first episode