# orchestrator/step_result.py from __future__ import annotations from dataclasses import dataclass from typing import Any, Literal # Terminal reasons are strings, not an enum, so you can add domain-specific # reasons (e.g. "escalated:ci_infra") without touching this module. TerminalReason = str AwaitState = str # must be one of the AWAITING_* names, or "ABANDONED" @dataclass(frozen=True) class StepResult: """ The outcome of a step handler. This is control flow, not data. The step's contract *output* lives in `output`; the fields here tell the runner what to do next. The runner is the only thing that reads these fields; handlers only construct them. """ kind: Literal["continue", "await", "terminal"] next_step: str | None = None # set iff kind == "continue" await_state: AwaitState | None = None # set iff kind == "await" terminal_reason: TerminalReason | None = None # set iff kind == "terminal" output: Any = None # the step's contract output # ── constructors ───────────────────────────────────────────── @classmethod def continue_to(cls, step: str, output: Any = None) -> "StepResult": """Advance to another step in the pipeline.""" return cls(kind="continue", next_step=step, output=output) @classmethod def awaiting(cls, state: AwaitState, output: Any = None) -> "StepResult": """Stop this call; resume when the await trigger fires.""" if not (state.startswith("AWAITING_") or state == "ABANDONED"): raise ValueError(f"not an await state: {state!r}") return cls(kind="await", await_state=state, output=output) @classmethod def complete(cls, output: Any = None) -> "StepResult": """Terminal: MR merged and task closed.""" return cls(kind="terminal", terminal_reason="completed", output=output) @classmethod def abandoned(cls, output: Any = None) -> "StepResult": """Terminal: no response within the timeout window.""" return cls(kind="terminal", terminal_reason="abandoned", output=output) @classmethod def escalated(cls, reason: str, output: Any = None) -> "StepResult": """Terminal: stopping because a human must intervene.""" return cls(kind="terminal", terminal_reason=f"escalated:{reason}", output=output)