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

174 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +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)`` where the 

333 reason token lands in the child-lifecycle audit event: 

334 

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

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

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

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

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

340 

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

342 here is a policy decision, not an admission. 

343 """ 

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

345 return (False, "not_terminal") 

346 if final_status == "completed": 

347 return (False, "completed_no_respawn") 

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

349 return (False, "policy_never") 

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

351 return (False, "max_respawns_reached") 

352 return (True, "respawn") 

353 

354 

355# --------------------------------------------------------------------------- 

356# Spawn admission 

357# --------------------------------------------------------------------------- 

358 

359 

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

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

362 

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

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

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

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

367 """ 

368 if not isinstance(budget, dict): 

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

370 max_iterations = budget.get("max_iterations") 

371 if not _is_positive_int(max_iterations): 

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

373 max_wall = budget.get("max_wall_clock_seconds") 

374 if not _is_positive_int(max_wall): 

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

376 normalized: dict[str, Any] = { 

377 "max_iterations": max_iterations, 

378 "max_wall_clock_seconds": max_wall, 

379 } 

380 return cast("BudgetControls", normalized) 

381 

382 

383def validate_spawn( 

384 *, 

385 parent_role: str | None, 

386 config: SwarmConfig, 

387 children: Sequence[ChildRegistryEntry], 

388 request: Mapping[str, Any], 

389 registered_tools: dict[str, Any], 

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

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

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

393 respawn_of_slot: str | None = None, 

394) -> SpawnSpec: 

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

396 

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

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

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

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

401 

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

403 

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

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

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

407 4. restart policy and ``max_respawns`` 

408 5. ``use_sampling`` shape 

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

410 7. iteration-pool balance 

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

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

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

414 

415 Returns the normalized :class:`SpawnSpec`. 

416 """ 

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

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

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

420 if parent_role != "orchestrator": 

421 raise _reject( 

422 "spawn", 

423 "spawn_depth_exceeded", 

424 parent_role=parent_role, 

425 ) 

426 

427 # 2. Slot. 

428 slot = request.get("slot") 

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

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

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

432 if slot in existing_slots and slot != respawn_of_slot: 

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

434 

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

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

437 

438 # 4. Restart policy. 

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

440 if restart_policy not in RESTART_POLICIES: 

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

442 max_respawns = request.get("max_respawns") 

443 if max_respawns is None: 

444 max_respawns = DEFAULT_RESPAWNS_BY_POLICY[restart_policy] 

445 elif not _is_non_negative_int(max_respawns): 

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

447 if restart_policy == "never": 

448 max_respawns = 0 

449 

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

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

452 if not isinstance(use_sampling, bool): 

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

454 

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

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

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

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

459 raise _reject( 

460 "spawn", 

461 "fleet_cap_exceeded", 

462 max_children=config["max_children"], 

463 live_children=len(live), 

464 ) 

465 

466 # 7. Pool balance. 

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

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

469 raise _reject( 

470 "spawn", 

471 "iteration_pool_exhausted", 

472 requested=budget["max_iterations"], 

473 remaining=balance["remaining"], 

474 ) 

475 

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

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

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

479 cadence = validate_cadence( 

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

481 ) 

482 

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

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

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

486 explicit = request.get("tool_allowlist") 

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

488 if isinstance(explicit, list): 

489 for name in explicit: 

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

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

492 tool_allowlist = resolve_effective_allowlist( 

493 allow_all_tools=allow_all, 

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

495 registered_tools=registered_tools, 

496 control_tools=SWARM_EXCLUDED_TOOLS, 

497 flag_lookup=flag_lookup, 

498 ) 

499 

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

501 # the first rejection is deterministic. 

502 if not config["allow_overlapping_mutating_tools"]: 

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

504 if mutating: 

505 for sibling_slot in sorted(sibling_allowlists): 

506 if sibling_slot == respawn_of_slot: 

507 continue 

508 sibling_mutating = { 

509 name 

510 for name in sibling_allowlists[sibling_slot] 

511 if not _is_safe_tool(name, registered_tags) 

512 } 

513 overlap = sorted(mutating & sibling_mutating) 

514 if overlap: 

515 raise _reject( 

516 "spawn", 

517 "mutating_tool_overlap", 

518 tools=overlap, 

519 sibling_slot=sibling_slot, 

520 ) 

521 

522 spec: SpawnSpec = { 

523 "slot": slot, 

524 "directive": directive, 

525 "criteria": criteria, 

526 "budget": budget, 

527 "tool_allowlist": tool_allowlist, 

528 "checkpoint_cadence": cadence, 

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

530 "max_respawns": max_respawns, 

531 "use_sampling": use_sampling, 

532 } 

533 return spec 

534 

535 

536# --------------------------------------------------------------------------- 

537# Supervisor tool schemas 

538# --------------------------------------------------------------------------- 

539 

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

541 "mission_spawn": { 

542 "type": "object", 

543 "properties": { 

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

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

546 "criteria": { 

547 "type": "array", 

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

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

550 }, 

551 "budget": { 

552 "type": "object", 

553 "properties": { 

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

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

556 }, 

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

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

559 }, 

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

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

562 "restart_policy": { 

563 "type": "string", 

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

565 }, 

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

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

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

569 }, 

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

571 }, 

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

573 "child_abort": { 

574 "type": "object", 

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

576 "required": ["slot"], 

577 }, 

578} 

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

580 

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

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

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

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

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

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

587""" 

588 

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

590 "mission_spawn": ( 

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

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

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

594 ), 

595 "children_status": ( 

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

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

598 ), 

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

600} 

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

602 

603 

604# --------------------------------------------------------------------------- 

605# Orchestrator session construction 

606# --------------------------------------------------------------------------- 

607 

608 

609def build_orchestrator_session( 

610 *, 

611 session_id: str, 

612 directive: str, 

613 criteria: list[Criterion], 

614 budget: BudgetControls, 

615 swarm_config: SwarmConfig, 

616 cadence: Cadence, 

617 extra_allowlist: Sequence[str] = (), 

618 stagnation_threshold: int = 3, 

619 use_sampling: bool = False, 

620 sampling_backend_resolved: str = "none", 

621 created_at: str, 

622) -> dict[str, Any]: 

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

624 

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

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

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

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

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

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

631 """ 

632 effective = ["children_status"] 

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

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

635 return { 

636 "version": _schema_version(), 

637 "session_id": session_id, 

638 "directive_text": directive, 

639 "criteria": [ 

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

641 for criterion in criteria 

642 ], 

643 "budget": budget, 

644 "tool_allowlist": effective, 

645 "checkpoint_cadence": cadence, 

646 "stagnation_threshold": stagnation_threshold, 

647 "use_sampling": use_sampling, 

648 "sampling_backend_resolved": sampling_backend_resolved, 

649 "allow_scripted_strategies": False, 

650 "status": "pending", 

651 "created_at": created_at, 

652 "iterations": [], 

653 "no_progress_counter": 0, 

654 "role": "orchestrator", 

655 "swarm": swarm_config, 

656 "children": [], 

657 } 

658 

659 

660def _schema_version() -> int: 

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

662 from .types import SCHEMA_VERSION 

663 

664 return int(SCHEMA_VERSION)