feat: add workflow action outcomes and tracing
This commit is contained in:
@@ -27,6 +27,65 @@ class RouteStatus(StrEnum):
|
||||
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."""
|
||||
@@ -89,4 +148,3 @@ class EdgeEvaluation:
|
||||
status: RouteStatus
|
||||
edge: dict[str, Any] | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from pipecat.frames.frames import OutputTransportMessageUrgentFrame, TTSSpeakFrame
|
||||
from pipecat.utils.time import time_now_iso8601
|
||||
@@ -67,6 +68,29 @@ class WorkflowOutput:
|
||||
if node_id:
|
||||
await self.emit({"type": "node-active", "nodeId": node_id})
|
||||
|
||||
async def emit_trace(
|
||||
self,
|
||||
event: str,
|
||||
*,
|
||||
revision: str,
|
||||
transition_id: int,
|
||||
**details: Any,
|
||||
) -> None:
|
||||
"""Publish one ordered, machine-readable Workflow runtime event."""
|
||||
|
||||
await self.emit(
|
||||
{
|
||||
"type": "workflow-event",
|
||||
"eventId": f"wfe_{uuid4().hex[:20]}",
|
||||
"event": event,
|
||||
"timestamp": time_now_iso8601(),
|
||||
"sessionId": self._runtime.session_id,
|
||||
"workflowRevision": revision,
|
||||
"transitionId": transition_id,
|
||||
**details,
|
||||
}
|
||||
)
|
||||
|
||||
async def emit_variables(
|
||||
self,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user