Coverage for gco_mcp / mission / swarm.py: 100.00%

174 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""Pure swarm-supervision primitives: validation, pool accounting, policy. 

2 

3A swarm is one **orchestrator** Mission session supervising N **child** 

4Mission sessions. This module holds everything about that relationship 

5that can be expressed as pure functions — no I/O, no clocks, no 

6environment lookups, no FastMCP imports — so the whole admission and 

7accounting surface is unit- and property-testable in isolation: 

8 

9* :func:`validate_swarm_config` — the swarm-level rails 

10 (:class:`~.types.SwarmConfig`). 

11* :func:`validate_spawn` — the full spawn-admission pipeline, run for 

12 every ``mission_spawn`` dispatch and for every scaffolded plan entry. 

13 First failure wins; every rejection is a 

14 :class:`~.validation.MissionValidationError` with a stable 

15 ``details.reason`` token, so a sampled decomposition gets precise 

16 feedback and a retry prompt can quote the exact rule it broke. 

17* Pool accounting — :func:`compute_pool_balance`, :func:`settle_entry`, 

18 :func:`respawn_entry`, :func:`new_registry_entry`. Reservation model: 

19 a spawn reserves the child's ``max_iterations`` from the pool; the 

20 settle step on a terminal child folds the actually-recorded iteration 

21 count into ``consumed_iterations`` and thereby refunds the unused 

22 remainder. 

23* :func:`should_respawn` — the deterministic restart-policy table. The 

24 respawn *decision* never depends on a sampler; only the optional 

25 replacement-directive text does (elsewhere). 

26 

27The supervisor tools themselves (``mission_spawn`` / ``children_status`` 

28/ ``child_abort``) are **in-process dispatcher entries** wired by the 

29runner for orchestrator sessions only. They are never registered with 

30the MCP server, and — together with the operator-facing ``swarm_*`` MCP 

31tools and the ``mission_*`` control tools — they are excluded from every 

32resolvable session allowlist (:data:`SWARM_EXCLUDED_TOOLS`), so no loop 

33can drive loops except through this validated seam. 

34""" 

35 

36from __future__ import annotations 

37 

38import re 

39from collections.abc import Collection, Mapping, Sequence 

40from typing import Any, Final, TypedDict, cast 

41 

42from .types import ( 

43 BudgetControls, 

44 Cadence, 

45 ChildRegistryEntry, 

46 Criterion, 

47 RestartPolicy, 

48 SwarmConfig, 

49) 

50from .validation import ( 

51 SUPERVISOR_TOOLS, 

52 SWARM_EXCLUDED_TOOLS, 

53 SWARM_MCP_TOOLS, 

54 MissionValidationError, 

55 resolve_effective_allowlist, 

56 validate_cadence, 

57 validate_criteria, 

58 validate_directive, 

59) 

60 

61__all__ = [ 

62 "DEFAULT_MAX_CONCURRENT_CHILDREN", 

63 "DEFAULT_RESPAWNS_BY_POLICY", 

64 "RESTART_POLICIES", 

65 "SUPERVISOR_TOOLS", 

66 "SUPERVISOR_TOOL_DOCSTRINGS", 

67 "SUPERVISOR_TOOL_SCHEMAS", 

68 "SWARM_EXCLUDED_TOOLS", 

69 "SWARM_MCP_TOOLS", 

70 "PoolBalance", 

71 "SpawnSpec", 

72 "build_orchestrator_session", 

73 "compute_pool_balance", 

74 "new_registry_entry", 

75 "respawn_entry", 

76 "settle_entry", 

77 "should_respawn", 

78 "validate_spawn", 

79 "validate_swarm_config", 

80] 

81 

82 

83# --------------------------------------------------------------------------- 

84# Constants 

85# --------------------------------------------------------------------------- 

86 

87RESTART_POLICIES: Final[frozenset[str]] = frozenset( 

88 {"never", "on_failure", "on_failure_with_revision"} 

89) 

90"""The valid ``restart_policy`` values on a spawn request.""" 

91 

92DEFAULT_MAX_CONCURRENT_CHILDREN: Final[int] = 3 

93"""Default concurrency bound on simultaneously advancing children. 

94 

95Deliberately small: it is the swarm's primary throughput control (tool 

96fan-out and, when children opt into sampling, concurrent Bedrock calls). 

97""" 

98 

99DEFAULT_RESPAWNS_BY_POLICY: Final[dict[str, int]] = { 

100 "never": 0, 

101 "on_failure": 1, 

102 "on_failure_with_revision": 1, 

103} 

104"""Default ``max_respawns`` per restart policy when the request omits it.""" 

105 

106_SAFE_TAG: Final[str] = "safe" 

107"""The risk-tier tag marking a tool read-only for the overlap check.""" 

108 

109_SLOT_RE: Final[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") 

110"""Slot names: 1-64 chars, alphanumeric plus ``. _ -``, no whitespace. 

111 

112Slots become file-name fragments (task-status heartbeats) and audit keys, 

113so the charset is deliberately conservative. 

114""" 

115 

116 

117# --------------------------------------------------------------------------- 

118# Result shapes 

119# --------------------------------------------------------------------------- 

120 

121 

122class PoolBalance(TypedDict): 

123 """A point-in-time view of the child-iteration pool. 

124 

125 ``reserved`` counts live (non-settled) entries' reservations; 

126 ``consumed`` sums settled consumption; ``remaining`` is what a new 

127 spawn may draw from. The admission pipeline keeps ``remaining`` 

128 non-negative by construction. 

129 """ 

130 

131 pool: int 

132 reserved: int 

133 consumed: int 

134 remaining: int 

135 

136 

137class SpawnSpec(TypedDict): 

138 """A fully validated, normalized child specification. 

139 

140 Everything a runner needs to persist a child session and register the 

141 slot. Produced only by :func:`validate_spawn`; consuming code may 

142 trust every field. 

143 """ 

144 

145 slot: str 

146 directive: str 

147 criteria: list[Criterion] 

148 budget: BudgetControls 

149 tool_allowlist: list[str] 

150 checkpoint_cadence: Cadence 

151 restart_policy: RestartPolicy 

152 max_respawns: int 

153 use_sampling: bool 

154 

155 

156# --------------------------------------------------------------------------- 

157# Local helpers 

158# --------------------------------------------------------------------------- 

159 

160 

161def _is_positive_int(value: Any) -> bool: 

162 """Return True iff ``value`` is an int (not bool) and strictly > 0.""" 

163 return isinstance(value, int) and not isinstance(value, bool) and value > 0 

164 

165 

166def _is_non_negative_int(value: Any) -> bool: 

167 """Return True iff ``value`` is an int (not bool) and >= 0.""" 

168 return isinstance(value, int) and not isinstance(value, bool) and value >= 0 

169 

170 

171def _reject(field: str, reason: str, **extra: Any) -> MissionValidationError: 

172 """Build the standard structured rejection for this module.""" 

173 details: dict[str, Any] = {"field": field, "reason": reason} 

174 details.update(extra) 

175 return MissionValidationError("validation_error", details=details) 

176 

177 

178def _is_safe_tool(name: str, registered_tags: Mapping[str, Collection[str]]) -> bool: 

179 """Return True iff the tool is known and carries the ``safe`` tag. 

180 

181 Unknown names are treated as **not** safe: the overlap rail fails 

182 closed when tag information is missing. 

183 """ 

184 tags = registered_tags.get(name) 

185 return tags is not None and _SAFE_TAG in tags 

186 

187 

188# --------------------------------------------------------------------------- 

189# Swarm config 

190# --------------------------------------------------------------------------- 

191 

192 

193def validate_swarm_config(config: dict[str, Any]) -> SwarmConfig: 

194 """Validate the swarm-level rails and normalize defaults. 

195 

196 Required: ``max_children`` and ``child_iteration_pool``, each a 

197 strictly-positive int — there is deliberately **no** ``-1`` uncapped 

198 sentinel at the swarm level; an unbounded fleet or pool is exactly 

199 the runaway shape these rails exist to prevent. Optional: 

200 ``max_concurrent_children`` (default 

201 :data:`DEFAULT_MAX_CONCURRENT_CHILDREN`) and 

202 ``allow_overlapping_mutating_tools`` (default ``False``). 

203 

204 Returns a normalized :class:`~.types.SwarmConfig` carrying all four 

205 keys. 

206 """ 

207 if not isinstance(config, dict): 

208 raise _reject("swarm", "not_a_dict") 

209 max_children = config.get("max_children") 

210 if not _is_positive_int(max_children): 

211 raise _reject("swarm", "missing_or_not_positive_int", subfield="max_children") 

212 pool = config.get("child_iteration_pool") 

213 if not _is_positive_int(pool): 

214 raise _reject("swarm", "missing_or_not_positive_int", subfield="child_iteration_pool") 

215 concurrency = config.get("max_concurrent_children", DEFAULT_MAX_CONCURRENT_CHILDREN) 

216 if not _is_positive_int(concurrency): 

217 raise _reject("swarm", "not_positive_int", subfield="max_concurrent_children") 

218 allow_overlap = config.get("allow_overlapping_mutating_tools", False) 

219 if not isinstance(allow_overlap, bool): 

220 raise _reject("swarm", "not_a_bool", subfield="allow_overlapping_mutating_tools") 

221 normalized: dict[str, Any] = { 

222 "max_children": max_children, 

223 "child_iteration_pool": pool, 

224 "max_concurrent_children": concurrency, 

225 "allow_overlapping_mutating_tools": allow_overlap, 

226 } 

227 return cast("SwarmConfig", normalized) 

228 

229 

230# --------------------------------------------------------------------------- 

231# Pool accounting 

232# --------------------------------------------------------------------------- 

233 

234 

235def compute_pool_balance( 

236 child_iteration_pool: int, 

237 children: Sequence[ChildRegistryEntry], 

238) -> PoolBalance: 

239 """Compute the pool view from the registry alone. 

240 

241 Live (non-settled) entries hold their full ``reserved_iterations`` 

242 against the pool; settled entries contribute only their 

243 ``consumed_iterations`` (the settle step already folded the refund). 

244 """ 

245 reserved = sum(e["reserved_iterations"] for e in children if not e.get("settled")) 

246 consumed = sum(e["consumed_iterations"] for e in children) 

247 return { 

248 "pool": child_iteration_pool, 

249 "reserved": reserved, 

250 "consumed": consumed, 

251 "remaining": child_iteration_pool - reserved - consumed, 

252 } 

253 

254 

255def new_registry_entry(spec: SpawnSpec, session_id: str, spawned_at: str) -> ChildRegistryEntry: 

256 """Build the registry entry for a freshly spawned slot.""" 

257 entry: ChildRegistryEntry = { 

258 "slot": spec["slot"], 

259 "session_id": session_id, 

260 "spawned_at": spawned_at, 

261 "reserved_iterations": spec["budget"]["max_iterations"], 

262 "restart_policy": spec["restart_policy"], 

263 "max_respawns": spec["max_respawns"], 

264 "respawn_count": 0, 

265 "consumed_iterations": 0, 

266 } 

267 return entry 

268 

269 

270def settle_entry(entry: ChildRegistryEntry, iterations_recorded: int) -> ChildRegistryEntry: 

271 """Fold a terminal session's consumption into the slot; refund the rest. 

272 

273 Consumption is clamped to ``[0, reserved_iterations]`` — the engine's 

274 own budget cap guarantees a child never records more iterations than 

275 its reservation, and the clamp keeps the pool arithmetic sound even 

276 against a corrupted count. Settling an already-settled entry is a 

277 no-op (idempotent), so a crash between settle and persist cannot 

278 double-count on replay. 

279 

280 Returns a new entry; the input is not mutated. 

281 """ 

282 if entry.get("settled"): 

283 return entry 

284 reserved = entry["reserved_iterations"] 

285 folded = min(max(iterations_recorded, 0), reserved) 

286 updated = dict(entry) 

287 updated["consumed_iterations"] = entry["consumed_iterations"] + folded 

288 updated["reserved_iterations"] = 0 

289 updated["settled"] = True 

290 return cast("ChildRegistryEntry", updated) 

291 

292 

293def respawn_entry( 

294 entry: ChildRegistryEntry, 

295 *, 

296 new_session_id: str, 

297 reserved_iterations: int, 

298 spawned_at: str, 

299) -> ChildRegistryEntry: 

300 """Point a settled slot at its replacement session. 

301 

302 The prior session id moves into the lineage list, the respawn count 

303 increments, and the new reservation goes live. Respawning an 

304 unsettled entry is a supervision bug, rejected loudly rather than 

305 silently corrupting the pool. 

306 

307 Returns a new entry; the input is not mutated. 

308 """ 

309 if not entry.get("settled"): 

310 raise _reject("spawn", "respawn_before_settle", slot=entry["slot"]) 

311 updated = dict(entry) 

312 lineage = list(entry.get("prior_session_ids", [])) 

313 lineage.append(entry["session_id"]) 

314 updated["prior_session_ids"] = lineage 

315 updated["session_id"] = new_session_id 

316 updated["spawned_at"] = spawned_at 

317 updated["reserved_iterations"] = reserved_iterations 

318 updated["respawn_count"] = entry["respawn_count"] + 1 

319 updated.pop("settled", None) 

320 return cast("ChildRegistryEntry", updated) 

321 

322 

323# --------------------------------------------------------------------------- 

324# Restart policy 

325# --------------------------------------------------------------------------- 

326 

327 

328def should_respawn(entry: ChildRegistryEntry, final_status: str) -> tuple[bool, str]: 

329 """Deterministic restart-policy table for a slot whose session ended. 

330 

331 ``final_status`` is the child session's terminal 

332 :data:`~.types.StatusLabel`. Returns ``(decision, reason)``; the reason 

333 token names which row of the table fired, so tests can assert the policy 

334 rather than just the boolean (the runner acts on the decision alone): 

335 

336 * non-terminal status → ``(False, "not_terminal")`` (caller bug guard) 

337 * ``completed`` → ``(False, "completed_no_respawn")`` 

338 * policy ``never`` → ``(False, "policy_never")`` 

339 * respawn budget exhausted → ``(False, "max_respawns_reached")`` 

340 * otherwise (``failed`` / ``terminated``) → ``(True, "respawn")`` 

341 

342 The pool and fleet-cap checks still apply at respawn time — a ``True`` 

343 here is a policy decision, not an admission. 

344 """ 

345 if final_status not in ("completed", "terminated", "failed"): 

346 return (False, "not_terminal") 

347 if final_status == "completed": 

348 return (False, "completed_no_respawn") 

349 if entry["restart_policy"] == "never": 

350 return (False, "policy_never") 

351 if entry["respawn_count"] >= entry["max_respawns"]: 

352 return (False, "max_respawns_reached") 

353 return (True, "respawn") 

354 

355 

356# --------------------------------------------------------------------------- 

357# Spawn admission 

358# --------------------------------------------------------------------------- 

359 

360 

361def _validate_child_budget(budget: Any) -> BudgetControls: 

362 """Validate a child budget: both caps required, strictly positive. 

363 

364 Children deliberately reject the ``-1`` uncapped sentinel Mission 

365 budgets accept: a supervised worker must be self-terminating on both 

366 axes even if its supervisor dies, and the iteration cap doubles as 

367 the slot's pool reservation, which must be a finite number. 

368 """ 

369 if not isinstance(budget, dict): 

370 raise _reject("budget", "not_a_dict") 

371 max_iterations = budget.get("max_iterations") 

372 if not _is_positive_int(max_iterations): 

373 raise _reject("budget", "missing_or_not_positive_int", subfield="max_iterations") 

374 max_wall = budget.get("max_wall_clock_seconds") 

375 if not _is_positive_int(max_wall): 

376 raise _reject("budget", "missing_or_not_positive_int", subfield="max_wall_clock_seconds") 

377 normalized: dict[str, Any] = { 

378 "max_iterations": max_iterations, 

379 "max_wall_clock_seconds": max_wall, 

380 } 

381 return cast("BudgetControls", normalized) 

382 

383 

384def validate_spawn( 

385 *, 

386 parent_role: str | None, 

387 config: SwarmConfig, 

388 children: Sequence[ChildRegistryEntry], 

389 request: Mapping[str, Any], 

390 registered_tools: dict[str, Any], 

391 registered_tags: Mapping[str, Collection[str]], 

392 sibling_allowlists: Mapping[str, Sequence[str]], 

393 flag_lookup: dict[str, str] | None = None, 

394 respawn_of_slot: str | None = None, 

395) -> SpawnSpec: 

396 """Run the full spawn-admission pipeline; first failure wins. 

397 

398 Pure: the caller supplies every piece of live state — the parent's 

399 role, the persisted registry, the registered tool names and tag map, 

400 and the allowlists of **live** sibling children keyed by slot (the 

401 overlap rail checks only siblings that can still act). 

402 

403 Admission order (each rejection carries its own ``details.reason``): 

404 

405 1. depth — the dispatching session must be an orchestrator 

406 2. slot shape and uniqueness (``respawn_of_slot`` exempts its own slot) 

407 3. child budget shape (strictly positive; ``-1`` rejected) 

408 4. restart policy and ``max_respawns`` 

409 5. ``use_sampling`` shape 

410 6. fleet cap over live (non-settled) slots 

411 7. iteration-pool balance 

412 8. directive, criteria, cadence via the shared Mission validators 

413 9. allowlist resolution (control/supervisor/swarm names unreachable) 

414 10. mutating-tool overlap against live siblings (unless opted out) 

415 

416 Returns the normalized :class:`SpawnSpec`. 

417 """ 

418 # 1. Depth: only orchestrators spawn. Children never receive the 

419 # supervisor tools in the first place (structural guard); this role 

420 # check is the second, independent layer of the same rule. 

421 if parent_role != "orchestrator": 

422 raise _reject( 

423 "spawn", 

424 "spawn_depth_exceeded", 

425 parent_role=parent_role, 

426 ) 

427 

428 # 2. Slot. 

429 slot = request.get("slot") 

430 if not isinstance(slot, str) or not _SLOT_RE.match(slot): 

431 raise _reject("spawn", "slot_missing_or_invalid") 

432 existing_slots = {entry["slot"] for entry in children} 

433 if slot in existing_slots and slot != respawn_of_slot: 

434 raise _reject("spawn", "duplicate_slot", slot=slot) 

435 

436 # 3. Child budget (also the pool reservation). 

437 budget = _validate_child_budget(request.get("budget")) 

438 

439 # 4. Restart policy. 

440 restart_policy = request.get("restart_policy", "never") 

441 if restart_policy not in RESTART_POLICIES: 

442 raise _reject("spawn", "restart_policy_invalid", restart_policy=restart_policy) 

443 max_respawns = request.get("max_respawns") 

444 if max_respawns is None: 

445 max_respawns = DEFAULT_RESPAWNS_BY_POLICY[restart_policy] 

446 elif not _is_non_negative_int(max_respawns): 

447 raise _reject("spawn", "max_respawns_not_a_non_negative_int") 

448 if restart_policy == "never": 

449 max_respawns = 0 

450 

451 # 5. Sampling default: deterministic leaves unless explicitly opted in. 

452 use_sampling = request.get("use_sampling", False) 

453 if not isinstance(use_sampling, bool): 

454 raise _reject("spawn", "use_sampling_not_a_bool") 

455 

456 # 6. Fleet cap over live slots. A respawn follows settle, so its old 

457 # entry is no longer live and counts itself naturally. 

458 live = [entry for entry in children if not entry.get("settled")] 

459 if len(live) + 1 > config["max_children"]: 

460 raise _reject( 

461 "spawn", 

462 "fleet_cap_exceeded", 

463 max_children=config["max_children"], 

464 live_children=len(live), 

465 ) 

466 

467 # 7. Pool balance. 

468 balance = compute_pool_balance(config["child_iteration_pool"], children) 

469 if budget["max_iterations"] > balance["remaining"]: 

470 raise _reject( 

471 "spawn", 

472 "iteration_pool_exhausted", 

473 requested=budget["max_iterations"], 

474 remaining=balance["remaining"], 

475 ) 

476 

477 # 8. Directive / criteria / cadence via the shared validators. 

478 directive = validate_directive(cast("str", request.get("directive", ""))) 

479 criteria = validate_criteria(cast("list[dict[str, Any]]", request.get("criteria"))) 

480 cadence = validate_cadence( 

481 cast("dict[str, Any]", request.get("cadence") or {"kind": "every_iteration"}) 

482 ) 

483 

484 # 9. Allowlist. An explicit list naming a control-plane tool is a 

485 # loud rejection (precise sampler feedback beats silent stripping); 

486 # the all-tools expansion excludes them via the control set. 

487 explicit = request.get("tool_allowlist") 

488 allow_all = bool(request.get("allow_all_tools", False)) 

489 if isinstance(explicit, list): 

490 for name in explicit: 

491 if isinstance(name, str) and name in SWARM_EXCLUDED_TOOLS: 

492 raise _reject("tool_allowlist", "control_tool_not_allowed", tool_name=name) 

493 tool_allowlist = resolve_effective_allowlist( 

494 allow_all_tools=allow_all, 

495 explicit_allowlist=cast("list[str] | None", explicit), 

496 registered_tools=registered_tools, 

497 control_tools=SWARM_EXCLUDED_TOOLS, 

498 flag_lookup=flag_lookup, 

499 ) 

500 

501 # 10. Mutating-tool overlap against live siblings, slot-ordered so 

502 # the first rejection is deterministic. 

503 if not config["allow_overlapping_mutating_tools"]: 

504 mutating = {name for name in tool_allowlist if not _is_safe_tool(name, registered_tags)} 

505 if mutating: 

506 for sibling_slot in sorted(sibling_allowlists): 

507 if sibling_slot == respawn_of_slot: 

508 continue 

509 sibling_mutating = { 

510 name 

511 for name in sibling_allowlists[sibling_slot] 

512 if not _is_safe_tool(name, registered_tags) 

513 } 

514 overlap = sorted(mutating & sibling_mutating) 

515 if overlap: 

516 raise _reject( 

517 "spawn", 

518 "mutating_tool_overlap", 

519 tools=overlap, 

520 sibling_slot=sibling_slot, 

521 ) 

522 

523 spec: SpawnSpec = { 

524 "slot": slot, 

525 "directive": directive, 

526 "criteria": criteria, 

527 "budget": budget, 

528 "tool_allowlist": tool_allowlist, 

529 "checkpoint_cadence": cadence, 

530 "restart_policy": cast("RestartPolicy", restart_policy), 

531 "max_respawns": max_respawns, 

532 "use_sampling": use_sampling, 

533 } 

534 return spec 

535 

536 

537# --------------------------------------------------------------------------- 

538# Supervisor tool schemas 

539# --------------------------------------------------------------------------- 

540 

541SUPERVISOR_TOOL_SCHEMAS: Final[dict[str, dict[str, Any]]] = { 

542 "mission_spawn": { 

543 "type": "object", 

544 "properties": { 

545 "slot": {"type": "string", "description": "Unique slot name for the child."}, 

546 "directive": {"type": "string", "description": "Child goal, natural language."}, 

547 "criteria": { 

548 "type": "array", 

549 "items": {"type": "object"}, 

550 "description": "Mission criteria array for the child.", 

551 }, 

552 "budget": { 

553 "type": "object", 

554 "properties": { 

555 "max_iterations": {"type": "integer", "minimum": 1}, 

556 "max_wall_clock_seconds": {"type": "integer", "minimum": 1}, 

557 }, 

558 "required": ["max_iterations", "max_wall_clock_seconds"], 

559 "description": "Finite child budget; -1 is rejected on children.", 

560 }, 

561 "tool_allowlist": {"type": "array", "items": {"type": "string"}}, 

562 "allow_all_tools": {"type": "boolean"}, 

563 "restart_policy": { 

564 "type": "string", 

565 "enum": ["never", "on_failure", "on_failure_with_revision"], 

566 }, 

567 "max_respawns": {"type": "integer", "minimum": 0}, 

568 "use_sampling": {"type": "boolean"}, 

569 "cadence": {"type": "object"}, 

570 }, 

571 "required": ["slot", "directive", "criteria", "budget"], 

572 }, 

573 "children_status": {"type": "object", "properties": {}}, 

574 "child_abort": { 

575 "type": "object", 

576 "properties": {"slot": {"type": "string"}}, 

577 "required": ["slot"], 

578 }, 

579} 

580"""JSON schemas for the in-process supervisor tools. 

581 

582The supervisor tools are never registered with FastMCP, so the sampled 

583strategy validator (``validate_strategy_against_catalog``) cannot learn 

584their shapes from the live registry. Callers that build a sampled 

585orchestrator extend the sampler's catalog with these schemas so spawn 

586proposals validate at proposal time; the spawn tool itself re-validates 

587every dispatch through :func:`validate_spawn` regardless. 

588""" 

589 

590SUPERVISOR_TOOL_DOCSTRINGS: Final[dict[str, str]] = { 

591 "mission_spawn": ( 

592 "Spawn one supervised child Mission session. Validated against the " 

593 "swarm rails (fleet cap, iteration pool, finite child budget, " 

594 "allowlist exclusions, mutating-tool overlap)." 

595 ), 

596 "children_status": ( 

597 "Return the deterministic fleet snapshot: slot-ordered child rows " 

598 "plus aggregate children_* metrics and the remaining iteration pool." 

599 ), 

600 "child_abort": "Abort one live child slot by name, settling its reservation.", 

601} 

602"""Prompt-facing docstrings for the supervisor tools (allowlist rendering).""" 

603 

604 

605# --------------------------------------------------------------------------- 

606# Orchestrator session construction 

607# --------------------------------------------------------------------------- 

608 

609 

610def build_orchestrator_session( 

611 *, 

612 session_id: str, 

613 directive: str, 

614 criteria: list[Criterion], 

615 budget: BudgetControls, 

616 swarm_config: SwarmConfig, 

617 cadence: Cadence, 

618 extra_allowlist: Sequence[str] = (), 

619 stagnation_threshold: int = 3, 

620 use_sampling: bool = False, 

621 sampling_backend_resolved: str = "none", 

622 created_at: str, 

623) -> dict[str, Any]: 

624 """Assemble a new orchestrator session dict from validated inputs. 

625 

626 Pure: every argument is already validated by its own validator; this 

627 function only fixes the shape shared by the MCP tool and the CLI so 

628 the two surfaces cannot drift. The effective allowlist brackets the 

629 caller's extras with the supervisor tools — ``children_status`` 

630 first (the deterministic strategy's target), spawn/abort last — 

631 which is the only place those names may enter a session allowlist. 

632 """ 

633 effective = ["children_status"] 

634 effective.extend(name for name in extra_allowlist if name not in SUPERVISOR_TOOLS) 

635 effective.extend(["mission_spawn", "child_abort"]) 

636 return { 

637 "version": _schema_version(), 

638 "session_id": session_id, 

639 "directive_text": directive, 

640 "criteria": [ 

641 {k: v for k, v in criterion.items() if not str(k).startswith("_")} 

642 for criterion in criteria 

643 ], 

644 "budget": budget, 

645 "tool_allowlist": effective, 

646 "checkpoint_cadence": cadence, 

647 "stagnation_threshold": stagnation_threshold, 

648 "use_sampling": use_sampling, 

649 "sampling_backend_resolved": sampling_backend_resolved, 

650 "allow_scripted_strategies": False, 

651 "status": "pending", 

652 "created_at": created_at, 

653 "iterations": [], 

654 "no_progress_counter": 0, 

655 "role": "orchestrator", 

656 "swarm": swarm_config, 

657 "children": [], 

658 } 

659 

660 

661def _schema_version() -> int: 

662 """Late import so this pure module keeps its type-only dependency.""" 

663 from .types import SCHEMA_VERSION 

664 

665 return int(SCHEMA_VERSION)