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
« 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.
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).
10Optional keys use :data:`typing.NotRequired` so that ``mypy --strict`` accepts
11absence on dict literals while still rejecting an unknown key.
12"""
14from __future__ import annotations
16from typing import Any, Literal, NotRequired, TypedDict
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
23# ---------------------------------------------------------------------------
24# Literal type aliases
25# ---------------------------------------------------------------------------
27VerdictLabel = Literal["continue", "adjust", "complete", "terminate"]
28"""The four possible Decide_Phase outputs.
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"""
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`."""
48StatusLabel = Literal["pending", "running", "paused", "completed", "terminated", "failed"]
49"""The lifecycle states of a :class:`SessionState`."""
51CriterionKind = Literal[
52 "metric_threshold", "event", "predicate", "tool_call_succeeded", "metric_trend"
53]
54"""The five Criterion evaluator kinds.
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"""
62MetricTrendDirection = Literal["decreasing", "increasing", "non_increasing", "non_decreasing"]
63"""The four trend directions a ``metric_trend`` criterion can require.
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"""
70SamplingStatus = Literal["used", "rejected", "fallback", "unavailable", "disabled"]
71"""The terminal status of a single sampling attempt on an iteration."""
73CadenceKind = Literal["every_iteration", "every_n_iterations", "every_t_seconds", "on_event"]
74"""The four supported Checkpoint_Cadence kinds."""
76SessionRole = Literal["orchestrator", "child"]
77"""The two swarm roles a session can carry.
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"""
87RestartPolicy = Literal["never", "on_failure", "on_failure_with_revision"]
88"""The supervision policy fixed on a child slot at spawn time.
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"""
100# ---------------------------------------------------------------------------
101# Terminal-state sets
102# ---------------------------------------------------------------------------
104TERMINAL_STATES: frozenset[StatusLabel] = frozenset({"completed", "terminated", "failed"})
105"""The :data:`StatusLabel` values from which a session cannot transition.
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"""
112TERMINAL_VERDICTS: frozenset[VerdictLabel] = frozenset({"complete", "terminate"})
113"""The :data:`VerdictLabel` values that end a session.
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"""
121# ---------------------------------------------------------------------------
122# Criterion and CriterionResult
123# ---------------------------------------------------------------------------
126class Criterion(TypedDict):
127 """A single machine-checkable success condition.
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 """
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]
161class CriterionResult(TypedDict):
162 """The outcome of evaluating one :class:`Criterion` at a checkpoint."""
164 criterion_id: str
165 status: Literal["met", "unmet", "inconclusive"]
166 evidence: Any
167 evaluated_at: str # ISO 8601 UTC
170# ---------------------------------------------------------------------------
171# Budget controls and cadence
172# ---------------------------------------------------------------------------
175class BudgetControls(TypedDict):
176 """Loop-control caps every Mission_Session declares at start time.
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.
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 """
194 max_iterations: int
195 max_wall_clock_seconds: int
198class Cadence(TypedDict):
199 """The Checkpoint_Cadence configuration on a session.
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 """
206 kind: CadenceKind
207 n: NotRequired[int]
208 t: NotRequired[int]
209 event_name: NotRequired[str]
212# ---------------------------------------------------------------------------
213# Swarm supervision
214# ---------------------------------------------------------------------------
217class SwarmConfig(TypedDict):
218 """Swarm-level rails persisted on an orchestrator session.
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.
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 """
237 max_children: int
238 child_iteration_pool: int
239 max_concurrent_children: int
240 allow_overlapping_mutating_tools: bool
243class ChildRegistryEntry(TypedDict):
244 """One supervised slot in an orchestrator session's child registry.
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 """
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]]
269# ---------------------------------------------------------------------------
270# Tool calls and strategy
271# ---------------------------------------------------------------------------
274class ToolCallRecord(TypedDict):
275 """A single tool invocation recorded during an Iteration's Execute_Phase.
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 """
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]
289class Strategy(TypedDict, total=False):
290 """The Propose_Phase output. Carries one of ``tool_calls`` or ``script``.
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 """
298 tool_calls: list[dict[str, Any]]
299 script: str
300 expected_observation_keys: list[str]
301 rationale: str
304# ---------------------------------------------------------------------------
305# Observation, Phase, Iteration, Session
306# ---------------------------------------------------------------------------
309class Observation(TypedDict):
310 """The Observe_Phase output — a normalized view of Execute_Phase results."""
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
335class PhaseRecord(TypedDict):
336 """One row in :attr:`IterationRecord.phases`. One per phase regardless of outcome."""
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]
345class IterationRecord(TypedDict):
346 """The complete record of one pass through the five-phase cycle.
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 """
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]
379class SessionState(TypedDict):
380 """The durable Mission_Session payload persisted by Mission_State_Backend.
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 """
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]]