# 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.defaultAgentId` config (validated against existing subagent target allowlist); per-spawn `agentId` override. Core ships no bundled agent id; docs recommend a lean `worker` agent 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 message ``` One 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`): ```jsonc "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 object` like `CodeModeSchema` (`src/config/zod-schema.agent-runtime.ts`); `swarm: true` → `{enabled: true}`. - Types in `src/config/types.tools.ts` (both per-agent and top-level `tools`), labels in `schema.labels.ts`, help in `schema.help.runtime.ts`. - Resolution helper `resolveSwarmConfig(cfg, agentId)` mirroring `resolveCodeModeConfig` (`src/agents/code-mode.ts:215`), clamping all numbers. - Gate effects when disabled: `agents_wait` tool absent from catalogs; `collect`/`outputSchema`/`fastMode`/`groupId` params on `sessions_spawn` rejected with a clear error naming the config key. No other behavior change. - `defaultAgentId` is validated through `resolveSubagentAllowedTargetIds` (`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 with `expectsCompletionMessage: false` and 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 synthetic `structured_output` tool 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 carries `structured: undefined` plus the raw text and a `schemaError`. - `fastMode: true | "auto" | false` — threaded into the child session patch alongside model/thinking via `resolveSubagentModelAndThinkingPlan` (`src/agents/subagent-spawn-plan.ts`), using the existing `FastMode` axis (`src/shared/fast-mode.ts`). Omitted = inherit. - `groupId: string` — swarm group stamp. Defaults to `swarm::`. Persisted on the registry record and the child session row. Used for caps, listing, batch archive, and the dots. - `label: string` already exists — surfaces in dots and `subagents list`. - Child agent id: `params.agentId` → else `tools.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: []`. - `timeoutSeconds` default 30, clamped to `waitTimeoutSecondsMax`. - 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 `wait` in 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_DEPTH` semantics (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 in `code-mode-namespaces.ts`: - `agents.run(prompt, opts) → Promise` — 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.send(input, opts) → Promise<...>`; `handle.close()`. (v1.1 — ships after run(); uses `mode:"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`/`agentWait` are replay-safe **by construction**: idempotency key `(codeModeRunId, bridgeId)` stored on the registry record; restart re-settles from persisted completions and never double-spawns. - Pending `agentWait` bridge 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) and `agents_wait` flow through the existing dynamic-tool bridge; inside Codex code-mode scripts they appear as `tools.*` automatically (verified: `codex-rs/code-mode/src/runtime/globals.rs:14-65`, `codex-rs/core/src/tools/spec_plan.rs:448-507`). - `agents_wait` gets 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::`. - Codex-native `spawn_agent` subagents 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 `sessions` rows. 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_MINUTES` sweep to operate per group). ## 10. Progress surface ("the dots") — later lane - Implicit, harness-driven. Derived from existing `sessions.changed` SSE + registry; `phase`/`log` notes add semantics. No agent-driven rendering. - Control UI: `swarm` renderer 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) - `placement` opt on spawn: `"local"` (default) | `"cloud:"` 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 1. **Lane A (core)**: §4 config + §5 spawn/wait/caps/approvals + §6 tests. 2. **Lane C (Labs page)**: §11 — independent, can land first. 3. **Lane B (QuickJS surface)**: §7 — after A contracts land. 4. 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.