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

139 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22:07 +0000

1"""Domain types for the Mission goal-directed iteration loop. 

2 

3All structured types live in one module so the engine, validators, sampler, 

4and tool wrappers share the same shape. ``TypedDict`` (not ``dataclass``) so 

5``json.dumps`` / ``json.loads`` round-trip without any custom serialization. 

6The ``version`` field on :class:`SessionState` is checked on every load 

7against :data:`mcp.mission.SCHEMA_VERSION` (re-exported here as 

8:data:`SCHEMA_VERSION` for callers that import only this module). 

9 

10Optional keys use :data:`typing.NotRequired` so that ``mypy --strict`` accepts 

11absence on dict literals while still rejecting an unknown key. 

12""" 

13 

14from __future__ import annotations 

15 

16from typing import Any, Literal, NotRequired, TypedDict 

17 

18# Re-exported here so callers that import only ``mcp.mission.types`` can read 

19# the schema version without an extra import. The canonical value lives on 

20# the package ``__init__``. 

21from . import SCHEMA_VERSION as SCHEMA_VERSION 

22 

23# --------------------------------------------------------------------------- 

24# Literal type aliases 

25# --------------------------------------------------------------------------- 

26 

27VerdictLabel = Literal["continue", "adjust", "complete", "terminate"] 

28"""The four possible Decide_Phase outputs. 

29 

30``continue`` keeps the loop running with the current strategy. ``adjust`` 

31runs another iteration with a Strategy_Revision. ``complete`` ends the 

32session as success. ``terminate`` ends the session as give-up. 

33""" 

34 

35VerdictReason = Literal[ 

36 "in_progress", 

37 "cadence_skip", 

38 "criteria_met", 

39 "forced_complete", 

40 "heuristic_unproductive", 

41 "max_iterations", 

42 "max_wall_clock", 

43 "no_progress", 

44 "user_abort", 

45] 

46"""The exhaustive set of reasons that pair with a :data:`VerdictLabel`.""" 

47 

48StatusLabel = Literal["pending", "running", "paused", "completed", "terminated", "failed"] 

49"""The lifecycle states of a :class:`SessionState`.""" 

50 

51CriterionKind = Literal[ 

52 "metric_threshold", "event", "predicate", "tool_call_succeeded", "metric_trend" 

53] 

54"""The five Criterion evaluator kinds. 

55 

56``metric_trend`` is the history-aware kind: rather than comparing a single 

57point-in-time value to a fixed target (``metric_threshold``), it evaluates the 

58direction of a metric across iterations using the cumulative metric history the 

59engine accumulates in :meth:`MissionEngine._build_cumulative_observation`. 

60""" 

61 

62MetricTrendDirection = Literal["decreasing", "increasing", "non_increasing", "non_decreasing"] 

63"""The four trend directions a ``metric_trend`` criterion can require. 

64 

65``decreasing`` / ``increasing`` require a strict net change across the window 

66(last < first / last > first); ``non_increasing`` / ``non_decreasing`` allow a 

67flat series (last <= first / last >= first). 

68""" 

69 

70SamplingStatus = Literal["used", "rejected", "fallback", "unavailable", "disabled"] 

71"""The terminal status of a single sampling attempt on an iteration.""" 

72 

73CadenceKind = Literal["every_iteration", "every_n_iterations", "every_t_seconds", "on_event"] 

74"""The four supported Checkpoint_Cadence kinds.""" 

75 

76SessionRole = Literal["orchestrator", "child"] 

77"""The two swarm roles a session can carry. 

78 

79A session with no ``role`` field is a standalone session — every session 

80that predates swarm supervision, with behavior identical to before the 

81field existed. ``orchestrator`` sessions hold a :class:`SwarmConfig` and a 

82child registry and are the only sessions whose engine receives the 

83in-process supervisor tools. ``child`` sessions carry 

84``parent_session_id`` and are otherwise ordinary sessions. 

85""" 

86 

87RestartPolicy = Literal["never", "on_failure", "on_failure_with_revision"] 

88"""The supervision policy fixed on a child slot at spawn time. 

89 

90``never`` — one shot; the slot is done when its session ends. ``on_failure`` 

91— a child that ends ``failed`` or ``terminated`` without meeting its 

92criteria is respawned with the same directive, up to ``max_respawns``. 

93``on_failure_with_revision`` — same, except the replacement directive may be 

94revised from the failed child's Final_Report lessons (advisory sampling; 

95falls back to the verbatim directive). The respawn *decision* is always 

96deterministic policy evaluation — never a sampler output. 

97""" 

98 

99 

100# --------------------------------------------------------------------------- 

101# Terminal-state sets 

102# --------------------------------------------------------------------------- 

103 

104TERMINAL_STATES: frozenset[StatusLabel] = frozenset({"completed", "terminated", "failed"}) 

105"""The :data:`StatusLabel` values from which a session cannot transition. 

106 

107A session in any of these states refuses further ``mission_iterate`` calls 

108with ``session_terminal``. The engine consults this set on every iteration 

109entry to short-circuit before performing any work. 

110""" 

111 

112TERMINAL_VERDICTS: frozenset[VerdictLabel] = frozenset({"complete", "terminate"}) 

113"""The :data:`VerdictLabel` values that end a session. 

114 

115When the Decide_Phase emits a verdict in this set, the engine writes a 

116Final_Report and transitions the session to ``completed`` or ``terminated`` 

117(matching the verdict). 

118""" 

119 

120 

121# --------------------------------------------------------------------------- 

122# Criterion and CriterionResult 

123# --------------------------------------------------------------------------- 

124 

125 

126class Criterion(TypedDict): 

127 """A single machine-checkable success condition. 

128 

129 The kind-specific keys (``metric``/``op``/``target`` for 

130 ``metric_threshold``, ``event_name`` for ``event``, ``expression`` for 

131 ``predicate``, ``tool_name``/``min_count`` for ``tool_call_succeeded``, 

132 ``metric``/``direction``/``window``/``min_points`` for ``metric_trend``) 

133 are not declared on the base ``TypedDict`` because they are mutually 

134 exclusive per ``kind``. Validators in ``mcp.mission.validation`` verify 

135 the right keys are present for each ``kind`` and may attach a private 

136 cached AST under ``_parsed_ast`` for ``predicate`` entries. 

137 """ 

138 

139 criterion_id: str 

140 kind: CriterionKind 

141 required: bool 

142 # Kind-specific keys (validator-enforced): 

143 metric: NotRequired[str] 

144 op: NotRequired[Literal["<", "<=", ">", ">=", "==", "!="]] 

145 target: NotRequired[float] 

146 event_name: NotRequired[str] 

147 expression: NotRequired[str] 

148 tool_name: NotRequired[str] 

149 min_count: NotRequired[int] 

150 # metric_trend keys: ``direction`` is required for the kind; ``window`` 

151 # bounds how many of the most-recent points are considered (default: all 

152 # available); ``min_points`` is the minimum number of numeric points 

153 # required before the criterion decides met/unmet rather than inconclusive. 

154 direction: NotRequired[MetricTrendDirection] 

155 window: NotRequired[int] 

156 min_points: NotRequired[int] 

157 # Cached parsed AST attached by ``validate_criteria`` for predicate entries. 

158 _parsed_ast: NotRequired[Any] 

159 

160 

161class CriterionResult(TypedDict): 

162 """The outcome of evaluating one :class:`Criterion` at a checkpoint.""" 

163 

164 criterion_id: str 

165 status: Literal["met", "unmet", "inconclusive"] 

166 evidence: Any 

167 evaluated_at: str # ISO 8601 UTC 

168 

169 

170# --------------------------------------------------------------------------- 

171# Budget controls and cadence 

172# --------------------------------------------------------------------------- 

173 

174 

175class BudgetControls(TypedDict): 

176 """Loop-control caps every Mission_Session declares at start time. 

177 

178 These are **loop-control** caps — not financial budgets. Mission 

179 enforces only the caps the loop has direct visibility into: 

180 iteration count and wall-clock seconds. Cost guardrails live 

181 out-of-band; configure AWS Budgets and Cost Anomaly Detection at 

182 the account level for those. 

183 

184 Both ``max_iterations`` and ``max_wall_clock_seconds`` accept 

185 either a strictly-positive integer cap or the explicit sentinel 

186 ``-1`` to opt out of that axis. The validator rejects every other 

187 shape (zero, other negatives, non-integer types, missing keys), 

188 and additionally rejects both caps being ``-1`` simultaneously 

189 (with ``reason="at_least_one_cap_required"``) since that would 

190 leave the loop with no axis-driven termination — a runaway-loop 

191 config error. 

192 """ 

193 

194 max_iterations: int 

195 max_wall_clock_seconds: int 

196 

197 

198class Cadence(TypedDict): 

199 """The Checkpoint_Cadence configuration on a session. 

200 

201 ``n`` is required for ``every_n_iterations``. ``t`` is required for 

202 ``every_t_seconds``. ``event_name`` is required for ``on_event``. The 

203 base ``every_iteration`` requires no extra keys. 

204 """ 

205 

206 kind: CadenceKind 

207 n: NotRequired[int] 

208 t: NotRequired[int] 

209 event_name: NotRequired[str] 

210 

211 

212# --------------------------------------------------------------------------- 

213# Swarm supervision 

214# --------------------------------------------------------------------------- 

215 

216 

217class SwarmConfig(TypedDict): 

218 """Swarm-level rails persisted on an orchestrator session. 

219 

220 These are **loop-control** rails in the same sense as 

221 :class:`BudgetControls`: they cap what the supervisor can directly 

222 observe (fleet size, pooled child iterations, concurrency), never 

223 money. Cost guardrails live out-of-band (AWS Budgets / Cost Anomaly 

224 Detection), exactly as documented for Mission budgets. 

225 

226 The validator normalizes defaults, so a persisted config always 

227 carries all four keys. ``max_children`` bounds the number of live 

228 (non-settled) child slots. ``child_iteration_pool`` is the pooled 

229 iteration budget every spawn reserves from — child budgets reject 

230 the ``-1`` uncapped sentinel, so the pool is always meaningful. 

231 ``max_concurrent_children`` bounds how many children advance 

232 simultaneously. ``allow_overlapping_mutating_tools`` opts out of the 

233 reject-by-default rule against two live children sharing a 

234 non-``safe``-tagged tool. 

235 """ 

236 

237 max_children: int 

238 child_iteration_pool: int 

239 max_concurrent_children: int 

240 allow_overlapping_mutating_tools: bool 

241 

242 

243class ChildRegistryEntry(TypedDict): 

244 """One supervised slot in an orchestrator session's child registry. 

245 

246 A **slot** is the stable supervision identity; the ``session_id`` it 

247 points at changes on respawn (lineage is kept under 

248 ``prior_session_ids``). Pool accounting reads two fields: 

249 ``reserved_iterations`` counts against the pool while the entry is 

250 live, and ``consumed_iterations`` accumulates the actually-recorded 

251 iterations of settled (terminal) sessions. ``settled`` marks that the 

252 current session's consumption has been folded into 

253 ``consumed_iterations`` — the settle step is what refunds the unused 

254 remainder of a reservation back to the pool. 

255 """ 

256 

257 slot: str 

258 session_id: str 

259 spawned_at: str # ISO 8601 UTC 

260 reserved_iterations: int 

261 restart_policy: RestartPolicy 

262 max_respawns: int 

263 respawn_count: int 

264 consumed_iterations: int 

265 settled: NotRequired[bool] 

266 prior_session_ids: NotRequired[list[str]] 

267 

268 

269# --------------------------------------------------------------------------- 

270# Tool calls and strategy 

271# --------------------------------------------------------------------------- 

272 

273 

274class ToolCallRecord(TypedDict): 

275 """A single tool invocation recorded during an Iteration's Execute_Phase. 

276 

277 Used both for direct ``tool_calls`` strategies and for in-script calls 

278 captured by the Mission_Sandbox under ``IterationRecord.script_call_log``. 

279 """ 

280 

281 tool_name: str 

282 args: dict[str, Any] 

283 status: Literal["ok", "failed", "skipped_not_allowed"] 

284 result_summary: Any 

285 duration_ms: int 

286 error_message: NotRequired[str] 

287 

288 

289class Strategy(TypedDict, total=False): 

290 """The Propose_Phase output. Carries one of ``tool_calls`` or ``script``. 

291 

292 ``total=False`` because every key is optional in isolation; the 

293 ``validate_strategy`` validator enforces the mutual-exclusivity rule 

294 (exactly one of ``tool_calls`` or ``script`` must be present and 

295 non-empty). 

296 """ 

297 

298 tool_calls: list[dict[str, Any]] 

299 script: str 

300 expected_observation_keys: list[str] 

301 rationale: str 

302 

303 

304# --------------------------------------------------------------------------- 

305# Observation, Phase, Iteration, Session 

306# --------------------------------------------------------------------------- 

307 

308 

309class Observation(TypedDict): 

310 """The Observe_Phase output — a normalized view of Execute_Phase results.""" 

311 

312 tool_results: list[Any] 

313 metrics: dict[str, Any] 

314 events: list[dict[str, Any]] 

315 errors: NotRequired[list[dict[str, Any]]] 

316 # Cumulative, history-aware view of every numeric metric seen across the 

317 # session, keyed by metric name and ordered oldest→newest. Present only on 

318 # the *cumulative* observation the Evaluate_Phase builds (see 

319 # :meth:`MissionEngine._build_cumulative_observation`); the per-iteration 

320 # Observation written to ``record["observation"]`` keeps ``metrics`` 

321 # strictly point-in-time and does not carry this key. Consumed by the 

322 # ``metric_trend`` criterion and available to predicates. 

323 metric_history: NotRequired[dict[str, list[float]]] 

324 # Present only on orchestrator sessions: the deterministic, slot-ordered 

325 # snapshot of supervised child states merged by the swarm observation 

326 # augmenter at the end of the Observe_Phase. Standalone and child 

327 # sessions never carry this key. Predicates read it via 

328 # ``obs['children']``; the paired aggregate counts land as ordinary 

329 # numeric metrics under ``metrics`` (``children_completed``, ...). 

330 children: NotRequired[list[dict[str, Any]]] 

331 phase_started_at: str 

332 phase_ended_at: str 

333 

334 

335class PhaseRecord(TypedDict): 

336 """One row in :attr:`IterationRecord.phases`. One per phase regardless of outcome.""" 

337 

338 phase: Literal["propose", "execute", "observe", "evaluate", "decide"] 

339 status: Literal["succeeded", "failed"] 

340 started_at: str 

341 ended_at: str 

342 error_message: NotRequired[str] 

343 

344 

345class IterationRecord(TypedDict): 

346 """The complete record of one pass through the five-phase cycle. 

347 

348 Sampling-related fields (``sampling_status``, ``sampling_output``, 

349 ``sampling_rejection_reason``) are present only when the iteration 

350 triggered an advisory-path sampling call. ``script_call_log`` is 

351 present only when the strategy carried a ``script``. 

352 """ 

353 

354 iteration_index: int 

355 started_at: str 

356 ended_at: str 

357 phases: list[PhaseRecord] 

358 strategy: Strategy 

359 observation: Observation 

360 criteria_evaluation: list[CriterionResult] 

361 verdict: VerdictLabel 

362 verdict_reason: VerdictReason 

363 revision_rationale: NotRequired[str] 

364 checkpoint_evaluated: bool 

365 sampling_status: NotRequired[SamplingStatus] 

366 sampling_output: NotRequired[str] 

367 sampling_rejection_reason: NotRequired[str] 

368 script_call_log: NotRequired[list[ToolCallRecord]] 

369 # Set by ``_execute_script`` when the sandbox runner raises 

370 # :class:`mcp.mission.sandbox.SandboxTerminated`. The Decide_Phase's 

371 # cascade reads this sentinel before any other branch and emits 

372 # ``("terminate", <reason>)`` so a sandbox cap propagates up to the 

373 # budget-cap path rather than failing the iteration as a phase 

374 # exception. Carries the wall-clock :data:`VerdictReason` 

375 # ``max_wall_clock`` for duration / memory / runtime caps. 

376 sandbox_terminated_reason: NotRequired[VerdictReason] 

377 

378 

379class SessionState(TypedDict): 

380 """The durable Mission_Session payload persisted by Mission_State_Backend. 

381 

382 The ``version`` field carries :data:`SCHEMA_VERSION`; loaders compare it 

383 against the current value and reject mismatches. Optional fields are 

384 populated as the session progresses (``started_at`` on first iteration, 

385 ``ended_at`` and ``final_report_path`` on terminal verdict, etc.). 

386 """ 

387 

388 version: int 

389 session_id: str 

390 directive_text: str 

391 criteria: list[Criterion] 

392 budget: BudgetControls 

393 tool_allowlist: list[str] 

394 checkpoint_cadence: Cadence 

395 stagnation_threshold: int 

396 use_sampling: bool 

397 sampling_backend_resolved: NotRequired[Literal["bedrock", "none"]] 

398 bedrock_model_id: NotRequired[str] 

399 allow_scripted_strategies: bool 

400 status: StatusLabel 

401 created_at: str 

402 started_at: NotRequired[str] 

403 ended_at: NotRequired[str] 

404 iterations: list[IterationRecord] 

405 no_progress_counter: int 

406 last_checkpoint_at: NotRequired[str] 

407 final_verdict: NotRequired[VerdictLabel] 

408 final_report_path: NotRequired[str] 

409 # Swarm supervision fields. All NotRequired so pre-swarm session files 

410 # load unchanged (loaders reject only on ``version`` mismatch, and the 

411 # schema version is deliberately NOT bumped for these additive keys). 

412 # ``role`` absent means standalone. ``parent_session_id`` is set on 

413 # child sessions only; ``swarm`` and ``children`` on orchestrators only. 

414 role: NotRequired[SessionRole] 

415 parent_session_id: NotRequired[str] 

416 swarm: NotRequired[SwarmConfig] 

417 children: NotRequired[list[ChildRegistryEntry]]