151 lines
4.0 KiB
Python
151 lines
4.0 KiB
Python
"""Readable runtime values shared by Workflow orchestration modules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
|
|
class WorkflowStatus(StrEnum):
|
|
"""The small set of states useful to operators and future debug tooling."""
|
|
|
|
STARTING = "starting"
|
|
WAITING_USER = "waiting_user"
|
|
ROUTING = "routing"
|
|
RUNNING_AGENT = "running_agent"
|
|
RUNNING_ACTION = "running_action"
|
|
HANDOFF = "handoff"
|
|
ENDED = "ended"
|
|
|
|
|
|
class RouteStatus(StrEnum):
|
|
"""A routing error is deliberately different from a valid no-match."""
|
|
|
|
MATCHED = "matched"
|
|
NO_MATCH = "no_match"
|
|
ERROR = "error"
|
|
|
|
|
|
class ActionStatus(StrEnum):
|
|
"""Stable Action outcomes used by routing and future debug tooling."""
|
|
|
|
SUCCESS = "success"
|
|
FAILURE = "failure"
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ActionError:
|
|
"""Machine-readable failure details without losing the operator message."""
|
|
|
|
code: str
|
|
message: str
|
|
retryable: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ActionOutcome:
|
|
"""One completed Action invocation.
|
|
|
|
``result`` remains an in-memory value because a tool response may contain
|
|
private business data. Trace events publish only its shape and variable
|
|
names, never the raw response.
|
|
"""
|
|
|
|
invocation_id: str
|
|
status: ActionStatus
|
|
duration_ms: int
|
|
result: dict[str, Any] | None = None
|
|
updated_variables: tuple[str, ...] = ()
|
|
error: ActionError | None = None
|
|
|
|
@property
|
|
def should_route(self) -> bool:
|
|
"""Cancellation is a lifecycle outcome, not a failure branch."""
|
|
|
|
return self.status != ActionStatus.CANCELLED
|
|
|
|
def trace_payload(self) -> dict[str, Any]:
|
|
"""Return a persistence-safe summary of the execution result."""
|
|
|
|
payload: dict[str, Any] = {
|
|
"invocationId": self.invocation_id,
|
|
"status": self.status.value,
|
|
"durationMs": self.duration_ms,
|
|
"updatedVariables": list(self.updated_variables),
|
|
}
|
|
if self.result is not None:
|
|
payload["resultKeys"] = sorted(str(key) for key in self.result)
|
|
if self.error is not None:
|
|
payload["error"] = {
|
|
"code": self.error.code,
|
|
"message": self.error.message,
|
|
"retryable": self.error.retryable,
|
|
}
|
|
return payload
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class UserTurn:
|
|
"""One committed user turn that may cross automatic Workflow nodes."""
|
|
|
|
id: int
|
|
text: str
|
|
|
|
|
|
@dataclass
|
|
class WorkflowRuntimeState:
|
|
"""Mutable per-call state; graph definitions remain immutable."""
|
|
|
|
current_node_id: str
|
|
status: WorkflowStatus = WorkflowStatus.STARTING
|
|
pending_user_turn: UserTurn | None = None
|
|
transition_id: int = 0
|
|
automatic_hops: int = 0
|
|
ended: bool = False
|
|
_next_turn_id: int = 1
|
|
|
|
def begin_user_turn(self, text: str) -> UserTurn:
|
|
turn = UserTurn(id=self._next_turn_id, text=text)
|
|
self._next_turn_id += 1
|
|
self.pending_user_turn = turn
|
|
self.automatic_hops = 0
|
|
return turn
|
|
|
|
def enter(self, node_id: str, status: WorkflowStatus) -> None:
|
|
self.current_node_id = node_id
|
|
self.status = status
|
|
|
|
def begin_transition(self) -> int:
|
|
self.transition_id += 1
|
|
return self.transition_id
|
|
|
|
def consume_user_turn(self) -> UserTurn | None:
|
|
turn = self.pending_user_turn
|
|
self.pending_user_turn = None
|
|
return turn
|
|
|
|
def finish(self) -> None:
|
|
self.ended = True
|
|
self.status = WorkflowStatus.ENDED
|
|
self.pending_user_turn = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LLMRouteResult:
|
|
"""Control-plane result returned by the small routing LLM."""
|
|
|
|
status: RouteStatus
|
|
function_name: str | None = None
|
|
error: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EdgeEvaluation:
|
|
"""Final graph-level decision after expression, LLM and default handling."""
|
|
|
|
status: RouteStatus
|
|
edge: dict[str, Any] | None = None
|
|
error: str | None = None
|