Files

56 lines
2.4 KiB
Python
Raw Permalink Normal View History

2026-09-22 23:21:59 +03:00
# 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)