The move protocol
This is the contract every participant’s handler must honor, regardless of what harness, skill, model, or CLI tool drives it. It is deliberately narrow: one primitive move per call, strictly typed, strictly validated. A participant can be as clever or as reckless as it wants inside this contract — the arena stays renderable either way.
Why one move at a time, not “here’s your sorted array”
Asking an LLM to just emit a fully-sorted array collapses the entire teaching point: you’d see only the end state, never the path — how many comparisons it took, how many swaps, whether it converged smoothly or thrashed. Comparison-sort algorithms are defined by their sequence of compares/swaps; visualizing that sequence is what makes harness differences legible on screen, not just in a benchmark table.
Round input (bridge → participant, on stdin, one JSON object)
{
"round": 7,
"array": [5, 3, 8, 1, 9, 2],
"history": [
{"round": 6, "action": "swap", "i": 2, "j": 4, "resultArray": [5, 3, 1, 8, 9, 2]}
],
"budgetRemaining": 43,
"mode": "solo",
"you": "algorithm-coached-claude"
}
array— the REAL current state. Nothing hidden, nothing pre-sorted for you.history— the last up-to-20 moves (not the full trace, to keep the payload bounded — see “Bounds” below). Inrelaymode this includes moves made by other participants.budgetRemaining— how many more rounds this run allows before it’s cut off (see “Bounds”).mode—"solo"(you own the whole array end to end) or"relay"(you get one move per tick, in rotation with every other currently-online participant, on a shared array).you— your own participant id, so inrelaymode you can tell your moves apart inhistory.
Your move (stdout, exactly one JSON object, nothing else)
{"action": "compare", "i": 2, "j": 4}
{"action": "swap", "i": 2, "j": 4}
{"action": "done"}
compare— reveals which ofarray[i]/array[j]is larger; does not change the array. Free of side effects, but still counts as a round (costs budget) — you can’t peek for free forever.swap— exchangesarray[i]andarray[j]. This is the only way the array state changes.done— you believe the array is fully sorted. The bridge checks; if you’re right, your run ends and your score is recorded. If you’re wrong, that’s a fault (see below) and you keep going.
No other keys, no prose, no markdown fences. i/j are 0-based integers, in bounds, and
i != j.
Validation and faults — no participant can ever crash the arena
A malformed response (bad JSON, unknown action, out-of-range/equal i/j, or no output at
all within the timeout) is a fault, not a crash:
- The bridge records the fault against that participant’s score.
- It re-sends the same round input, with one added field:
"correction": "<why the last reply was rejected>". - Up to 2 corrections per round. If still invalid, the bridge skips the round (array unchanged, budget still spent) and moves on.
A participant that never emits a single valid move for its entire budget still renders in the arena — as a flat line and a visibly high fault count. That is itself part of the lesson: a harness can fail to stay inside the contract just as easily as it can fail to sort well.
Bounds (why the arena can’t be griefed)
budgetRemainingstarts at a fixed per-run cap (default 200 rounds) — no participant can stall forever.- Each round’s LLM call is wrapped in a timeout (default 30s); a hang is a fault, not a hang.
historysent per round is capped at 20 entries — the payload doesn’t grow unbounded over a long run.arraylength is capped at a fixed max (default 24) — enough to be visually interesting, small enough that even a bad strategy finishes within budget.
Scoring (what the arena measures and shows per participant)
Computed by the bridge from the move trace, never self-reported by the participant:
| Metric | Meaning |
|---|---|
comparisons |
count of valid compare moves |
swaps |
count of valid swap moves |
faults |
count of rejected/corrected/skipped rounds |
roundsUsed |
rounds consumed out of the budget |
wallClockMs |
total real time across all LLM calls |
inversionsOverTime |
array-length-sized series: inversion count after each move (a standard “how far from sorted” measure — 0 means sorted) |
finishedCorrectly |
whether done was called AND the array was actually sorted at that point |
inversionsOverTime is what actually drives the on-screen animation and the little “signature”
sparkline per participant — no participant ever computes or reports it themselves.
Relay mode (the cooperative variant)
Same array, same move contract, but instead of one participant owning a full run, every
currently-online participant gets exactly one move per tick, in rotation. history shows the
whole team’s moves so far, including who made them. The point isn’t just “who sorts best solo” —
it’s watching a chaotic harness’s move get quietly cleaned up by a methodical one two ticks
later, or two mismatched strategies repeatedly undo each other. Same scoring table, computed
per participant across the shared trace.
Talking to a role over Agent-Fabric — inherited, not reinvented
This protocol only defines the JSON on stdin/stdout. Getting that stdin/stdout pair from a
real participant process, over a real Agent-Fabric channel, uses the same mechanism
CADS-flappy-demo and CADS-cookbook-demo already use — CT_AGENT_SERVICE_HANDLER_CMD /
CT_AGENT_SERVICES=text_generation, documented in CADS-Tunnel’s docs/agent-onboarding.md. See
Bring your own participant online for the
sort-arena-specific walkthrough of that mechanism, live.