Files
openclaw/docs/plan/swarms.md
T
2026-07-19 05:33:00 -07:00

261 lines
13 KiB
Markdown

# 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:<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: 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<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(); 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:<parentSessionKey>:<turnId>`.
- 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:<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
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.