Plugin architecture
A plugin is a goal environment adapter: it encodes goals into runnable tasks, verifies observations against goals, and scores outcomes for evolution. Plugins are what make Castellan’s coordination and evolution loops generic — the scheduler, substrate, and evolve pipeline never know anything about gridworlds or shell commands specifically; they only know the CastellanPlugin trait.
If you’re integrating a new environment, debugging why fitness looks wrong for your plugin, or deciding between install and link while iterating, this page is the reference.
The trait
#![allow(unused)]
fn main() {
#[async_trait]
pub trait CastellanPlugin: Send + Sync {
fn name(&self) -> &'static str;
fn encode_task(&self, goal: &Goal) -> TaskDescriptor;
async fn verify_goal(&self, goal: &Goal, observation: &serde_json::Value) -> AgentReport;
fn fitness(&self, goal: &Goal, report: &AgentReport) -> FitnessVector;
fn tools(&self) -> Vec<ToolSpec> { vec![] }
}
}
| Method | Called when | Produces |
|---|---|---|
name() | Registry lookup, episode environment_fingerprint | Stable plugin identifier |
encode_task(goal) | Run start | Initial TaskDescriptor — what the scheduler dispatches first |
verify_goal(goal, observation) | Every tick after an observation lands | AgentReport with a NextAction (complete / retry / delegate / fail) |
fitness(goal, report) | Episode end | FitnessVector — plugin-grounded score consumed by castellan evolve |
tools() | Registry init | Optional ToolSpec list the plugin exposes to MCP-connected agents |
Supporting traits, for narrower integrations:
GoalVerifier— verify-only surface, used by RLM loops (castellan run --rlm) that don’t need the full plugin lifecycle.
Coordination hooks (optional, provided via context at runtime — not part of the trait itself):
PluginContext— blackboard read/write, budget snapshot.CoordinationContext— zone list, perception radius,deposit_signal/read_signal.
Anatomy: lifecycle
flowchart LR
REG[register] --> ENC[encode_task]
ENC --> SCHED[scheduler run]
SCHED --> VER[verify_goal]
VER --> EP[episode snapshot]
EP --> EVOLVE[evolve fitness]
classDef runtime stroke:#0969da,stroke-width:2px
classDef evolve stroke:#d97706,stroke-width:2px
class REG,ENC,SCHED,VER runtime
class EP,EVOLVE evolve
- Register —
PluginRegistry::builtin()loads built-ins plus any manifestplugin.tomlon disk. - Encode —
encode_task(goal)produces the initialTaskDescriptorpayload the scheduler dispatches. - Run — the scheduler enqueues the task in the plugin’s primary zone; the host executes it; observations flow back into the blackboard/field.
- Verify —
verify_goalreturns anAgentReportcarrying aNextAction(complete, retry, delegate, or fail). - Episode — the runtime writes
.castellan/episodes/<goal-id>.json, including the plugin’sfitness()output. - Evolve —
castellan evolvereads the episode corpus; the plugin name seeds the archive’senvironment_fingerprintlookup for future--seed-archiveruns.
Current built-in plugins
| Plugin | Kind | LLM required | Description |
|---|---|---|---|
| gridworld | Simulator | No | Deterministic 5×5 grid; agents navigate to a target; boids-style coordination deposits |
| shell | Executor | No | Bounded subprocess (argv, cwd, timeout); exit code + output verification |
Plugin manifest (plugin.toml)
name = "gridworld"
description = "Deterministic 5x5 grid navigation simulator"
version = "0.1.0"
[zones]
primary = "grid"
default = ["grid", "goal"]
[blackboard]
slots = ["state", "last_observation"]
Manifests live at crates/castellan-plugins/<name>/plugin.toml. The registry scans that tree at init — you never hand-edit a central registry.rs to add a plugin.
How operators use it
1. List what’s registered:
castellan plugin list
2. Inspect a plugin’s manifest, zones, and blackboard schema:
castellan plugin info gridworld
3. Scaffold a new plugin from the template:
castellan plugin new myplugin
castellan plugin list
castellan run --goal "your goal" --plugin myplugin
4. Iterate on a plugin without a full reinstall — symlink instead of copy:
castellan plugin link --from ~/src/my-plugin
cargo build --release -p castellan-cli # rebuild to register
# edit ~/src/my-plugin/src/lib.rs, rebuild — no re-link needed
5. Install a plugin from git (for sharing/distribution):
castellan plugin install --git https://github.com/example/castellan-gridworld-extra.git
cargo build --release -p castellan-cli # rebuild to register
6. Uninstall:
castellan plugin uninstall myplugin
install deletes the copied source tree; link only removes the symlink and leaves your source directory intact.
Link vs install
install | link | |
|---|---|---|
| Source location | Copied into crates/castellan-plugins/<name> | Symlinked — edits in the original directory are live |
| Iteration | Re-install after every change | Rebuild only (cargo build) |
| Uninstall | Deletes the copy | Removes the symlink; source directory intact |
| Best for | Distribution, committing to the repo | Active plugin development |
castellan plugin list reports "source": "builtin" | "install" | "link" per plugin so you can tell at a glance how each one got there.
Extension points
| Extension | Location |
|---|---|
| Plugins | castellan-plugin-api, castellan-plugins |
RunHost backends | castellan-runtime::RunHost, castellan-rah, castellan-herdr |
| Evolve hooks | castellan-evolve — GepaHook, EpisodeLogProposer |
| Governance | castellan-governance — VERIFICATION_MANIFEST |
| Multiplexer | castellan-multiplexer — pane deposits → observations |
Recipes
When building a new environment adapter, start from castellan plugin new myplugin rather than hand-rolling the manifest — the template wires up [zones]/[blackboard] correctly and registers automatically on rebuild.
When your plugin’s fitness always comes back flat, check fitness(goal, report) against the actual AgentReport.next_action values you’re returning from verify_goal — a fitness function that ignores NextAction variants will look identical across very different outcomes.
When two plugins need to share coordination state, give them overlapping default zones deliberately in each plugin.toml, but keep primary zones distinct so scheduling doesn’t collide.
When distributing a plugin to teammates, publish it as a git repo and have them castellan plugin install --git <url> rather than sharing a local path — link is a dev-loop convenience, not a distribution mechanism.
Failure paths / troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
unknown plugin: <name> from castellan run | Plugin not registered (missing plugin.toml, or not rebuilt after install/link) | castellan plugin list to confirm registration; rebuild after install/link |
castellan plugin new output doesn’t show up in plugin list | Registry discovery happens at build time via build.rs, not at runtime | Run cargo build --release -p castellan-cli after scaffolding |
| Linked plugin edits don’t take effect | Forgot to rebuild after editing source | cargo build — link makes the source live, not the compiled binary |
| Plugin’s episodes never seed an archive entry | environment_fingerprint mismatch between evolve and run (different plugin name or goal shape) | Confirm name() is stable and goal text matches what evolve saw |
verify_goal never returns Complete | Plugin logic bug, or observation shape doesn’t match what verify_goal expects | Add tracing/log output inside verify_goal; replay the episode with castellan replay to inspect observations tick-by-tick |
Related
- Coordination model — zones, blackboard, and how plugin deposits reach the scheduler
- Topology evolution — how plugin fitness feeds mutation gating
castellan pluginreference- CLI reference