Get started
Swarms — agent fan-out and orchestration in code mode
Swarms — agent fan-out and orchestration in code mode
Status: Shipped — superseded by docs/tools/swarm.md. This document remains as
the implementation design record.
1. What and why
A swarm is many subagents orchestrated deterministically from a code-mode
script: fan out N readers, verify findings adversarially, synthesize through a
stateful prioritizer, loop on decision gates. Control flow (Promise.all,
while, if) is the orchestration — there is deliberately no graph DSL,
no new mode, no new top-level tool surface.
OpenClaw code mode (QuickJS-WASI, snapshot/resume, bridge requests) is the substrate. A parked bridge call survives VM snapshot, gateway restart, and resumes exactly where it stopped — stronger than journal-replay designs, with no determinism constraints on scripts.
Naming: product/docs name is Swarm. Code identifiers stay literal:
agents.* guest API, tools.swarm config, swarm group columns.
2. Decisions (maintainer, 2026-07-17)
- Cost: enforced config caps; per-swarm token budget optional. No mandatory budget.
- Approvals: children run fail-closed / non-interactive. Approval-requiring actions are denied; the denial is reported in the child result; the script decides. No operator prompt spam from fan-out.
- v1 is model-written ad-hoc scripts only. Saved/named workflows, CLI/cron entry: later (headless code mode already exists for cron).
- Child identity: dedicated worker agent by default via
tools.swarm.defaultAgentIdconfig (validated against existing subagent target allowlist); per-spawnagentIdoverride. Core ships no bundled agent id; docs recommend a leanworkeragent config. - No Codex source changes. Codex harness uses the spawn/wait idiom (§8).
3. Architecture overview
code-mode script (QuickJS VM, gateway) Codex V8 script (codex process) agents.run(...) ── parked bridge call tools.sessions_spawn / tools.agents_wait │ │ item/tool/call RPC (≤600s each) ▼ ▼ CORE (harness-agnostic, this repo) sessions_spawn {collect:true, outputSchema, fastMode, groupId} agents_wait {ids, timeoutSeconds} │ subagent registry (SQLite): collector completion records, swarm group id │ children = ordinary subagent sessions (lane-capped, fail-closed approvals) │ sessions.changed SSE ──► Control UI dots / sidebar / channel status messageOne canonical owner of spawn/complete/settle semantics (core tools + registry).
Two await transports: QuickJS parks a bridge call indefinitely (snapshot);
Codex polls agents_wait in bounded RPCs.
4. Config gate (v1)
New tools.swarm (global + per-agent override, same merge pattern as
tools.codeMode):
"tools": { "swarm": { "enabled": false, // master gate, default OFF "maxConcurrent": 8, // children running at once (swarm lane cap) "maxChildrenPerGroup": 50, // live children per swarm group "maxTotalPerGroup": 200, // lifetime spawn count per group (runaway backstop) "waitTimeoutSecondsMax": 600, "defaultAgentId": "" // optional; child agent id when spawn omits agentId }}- Zod: union
boolean | strict objectlikeCodeModeSchema(src/config/zod-schema.agent-runtime.ts);swarm: true→{enabled: true}. - Types in
src/config/types.tools.ts(both per-agent and top-leveltools), labels inschema.labels.ts, help inschema.help.runtime.ts. - Resolution helper
resolveSwarmConfig(cfg, agentId)mirroringresolveCodeModeConfig(src/agents/code-mode.ts:215), clamping all numbers. - Gate effects when disabled:
agents_waittool absent from catalogs;collect/outputSchema/fastMode/groupIdparams onsessions_spawnrejected with a clear error naming the config key. No other behavior change. defaultAgentIdis validated throughresolveSubagentAllowedTargetIds(src/agents/subagent-target-policy.ts); unknown id → spawn error, not fallback.
5. Core: collector-mode spawn + agents_wait (v1)
5.1 sessions_spawn additions (all gated on swarm enabled)
collect: boolean— when true, the child run is registered withexpectsCompletionMessage: falseand a collector completion record instead of announce/steering delivery. Tool returns{ runId, sessionKey }immediately. No channel/thread binding.outputSchema: object— JSON Schema. Child gets a syntheticstructured_outputtool appended to its tool surface; system-prompt addendum instructs it to call it exactly once with its final result. On validation failure the child gets one nudge retry; after that the completion record carriesstructured: undefinedplus the raw text and aschemaError.fastMode: true | "auto" | false— threaded into the child session patch alongside model/thinking viaresolveSubagentModelAndThinkingPlan(src/agents/subagent-spawn-plan.ts), using the existingFastModeaxis (src/shared/fast-mode.ts). Omitted = inherit.groupId: string— swarm group stamp. Defaults toswarm:<requesterSessionKey>:<runId-of-requesting-run>. Persisted on the registry record and the child session row. Used for caps, listing, batch archive, and the dots.label: stringalready exists — surfaces in dots andsubagents list.- Child agent id:
params.agentId→ elsetools.swarm.defaultAgentId→ else requester agent (existing behavior).
5.2 Approvals fail-closed
Collector children run with a non-interactive approval context: any tool call
that would require operator approval resolves as a structured denial
(approval_required) visible to the child, which is expected to report the
blockage in its result. Implementation: reuse the existing exec/tool approval
policy plumbing with a forced deny resolver for collector-mode child runs.
No approval events are emitted to operator surfaces from collector children.
5.3 agents_wait tool (new, gated)
agents_wait({ ids: string[], timeoutSeconds?: number })→ { completed: [{ runId, status: "done"|"failed"|"killed"|"timeout", result: string, structured?: unknown, schemaError?: string, sessionKey, label?, usage?: {inputTokens, outputTokens} }], pending: string[] }- Returns as soon as at least one id completes (first-completion / race
semantics, enables pipelines), or on timeout with
completed: []. timeoutSecondsdefault 30, clamped towaitTimeoutSecondsMax.- Idempotent: already-completed ids return their records again (records are kept until group archive). Unknown id → per-id error entry, not a throw.
- Ownership: only the session that spawned a run (or its parent chain) may wait
on it — same ownership rule as
waitin code mode (code-mode.ts:1684). - Registry: completion records live in the existing subagent registry SQLite
store (
subagent-registry.store.sqlite.ts) — new fields, no new store, no schema-version bump (additive columns only; see §9 constraint).
5.4 Caps enforcement
maxConcurrent: collector children run on the existing subagent lane but counted per swarm group; spawns beyond the cap queue FIFO (host-side, in the spawn path — return runId immediately, run starts when a slot frees).maxChildrenPerGroup/maxTotalPerGroup: spawn rejects with a typed error once exceeded; the error text names the config key.- Depth: collector children keep
DEFAULT_SUBAGENT_MAX_SPAWN_DEPTHsemantics (children are leaves unless nesting explicitly configured).
6. Testing contract (v1, lane A)
- Unit: config resolution/clamping; gate rejections when disabled; groupId defaulting; cap enforcement (queue + reject); wait race semantics; wait idempotency; ownership denial; structured-output validation + nudge retry + schemaError path; fastMode plumbing into session patch; defaultAgentId validation.
- Integration (vitest, mock model runtime): spawn 3 collector children, wait in a loop, assert first-completion ordering and final drain; gateway-restart simulation: registry reload → wait resolves from persisted completion.
- All tests colocated
*.test.ts; no live model calls.
7. QuickJS guest surface (lane B, after core)
- Guest globals installed in
CONTROLLER_SOURCE(src/agents/code-mode.worker.ts:190-374), reserved names added incode-mode-namespaces.ts:agents.run(prompt, opts) → Promise<result|structured>— sugar: collector spawn + parked await on a dedicated bridge method (agentWait) that the host settles on completion (no polling; snapshot-safe).agents.session(system, opts) → Promise<handle>;handle.send(input, opts) → Promise<...>;handle.close(). (v1.1 — ships after run(); usesmode:"session"+ per-turn collector records.)phase(title),log(message)— fire-and-forget bridge notifications → swarm progress events.
- Bridge methods added to
CodeModeBridgeMethod(code-mode.ts:91):agentSpawn,agentWait,swarmNote.agentSpawn/agentWaitare replay-safe by construction: idempotency key(codeModeRunId, bridgeId)stored on the registry record; restart re-settles from persisted completions and never double-spawns. - Pending
agentWaitbridge calls extend the run's snapshot TTL (pending agent set is the signal; no flag). API.read("agents.d.ts")virtual file documents the typed surface + the fan-out / gate / cycle idioms (createCodeModeApiVirtualFiles,code-mode-namespaces.ts:876).
8. Codex harness projection (later lane)
sessions_spawn(with new params) andagents_waitflow through the existing dynamic-tool bridge; inside Codex code-mode scripts they appear astools.*automatically (verified:codex-rs/code-mode/src/runtime/globals.rs:14-65,codex-rs/core/src/tools/spec_plan.rs:448-507).agents_waitgets the long dynamic-tool timeout class (600s cap;extensions/codex/src/app-server/dynamic-tool-execution.ts:37-39) and is marked timeout/replay-safe.- Group key for Codex parents:
swarm:<parentSessionKey>:<turnId>. - Codex-native
spawn_agentsubagents coexist; their task-mirror rows feed the same progress surface.
9. Persistence and retention
- No new stores. Registry records extend the existing subagent registry
SQLite tables; children are ordinary
sessionsrows. Additive columns only — any change requiring a SQLite schema-version bump needs explicit maintainer sign-off first (repo policy). - Swarm group id on registry record + child session metadata.
- Retention: completed collector records survive until group archive:
when the parent run finishes (or TTL expires), the group's children archive
as a batch (extend the existing
DEFAULT_SUBAGENT_ARCHIVE_AFTER_MINUTESsweep to operate per group).
10. Progress surface ("the dots") — later lane
- Implicit, harness-driven. Derived from existing
sessions.changedSSE + registry;phase/lognotes add semantics. No agent-driven rendering. - Control UI:
swarmrenderer in the workspace widget family (ui/src/lib/workspace/widgets/) — dot grid grouped by phase, narrator line, per-dot status/label/model; sidebar child-tree unchanged. - Channels: one throttled edited status message per group (follow
docs/concepts/streaming.md; never per-child messages).
11. Labs page (Control UI, independent lane)
Settings → Labs: experimental feature toggles, first entries Code Mode
and Swarm. Each row: name, one-line description, docs link, toggle wired
via the existing config.patch RPC (RFC 7396 merge-patch — set
tools.codeMode.enabled / tools.swarm.enabled), plus a "restart required"
hint when applicable. Discoverable, but copy makes the experimental status
clear. i18n: all strings through the normal en.ts + sync pipeline.
12. Placement (later)
placementopt on spawn:"local"(default) |"cloud:<profile>"via existing worker-environment dispatch (sessions.dispatch); pooled placement later if shared-box SSH-sandbox children prove insufficient.- Orchestrator VM always stays on the gateway; settle/dots/budget are placement-blind.
13. Non-goals
- No graph DSL — control flow is the graph (deliberate, documented).
- No Codex source changes; no reuse of Codex Code Mode internals.
- No saved/named workflows in v1; no CLI entry point.
- No per-child operator approval bubbling.
- No 1:1 cloud provisioning at fan-out scale.
- No steady-state runtime compat shims; swarm is new surface, gated.
14. Build phases / PR slicing
- Lane A (core): §4 config + §5 spawn/wait/caps/approvals + §6 tests.
- Lane C (Labs page): §11 — independent, can land first.
- Lane B (QuickJS surface): §7 — after A contracts land.
- Dots renderer (§10), Codex projection (§8),
agents.session(§7 v1.1), placement (§12), user docs rewrite — follow-up PRs in that order.
Each PR: green CI, $autoreview clean, gated off by default, main shippable.