"""Stable workflow error codes and exception types (protocol v1). User-visible ``message`` must never include stack traces, SQL passwords, full response bodies, or internal host paths. Full exceptions go to controlled server logs only. """ from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, Field WorkflowErrorCode = Literal[ # graph / publish "WORKFLOW_SCHEMA_INVALID", "WORKFLOW_GRAPH_CYCLE", "WORKFLOW_NODE_NOT_FOUND", "WORKFLOW_EDGE_INVALID", "WORKFLOW_EXPRESSION_INVALID", "WORKFLOW_EXPRESSION_UPSTREAM_REQUIRED", "WORKFLOW_RESOURCE_MISSING", "WORKFLOW_RESOURCE_FORBIDDEN", "WORKFLOW_LIMIT_EXCEEDED", "WORKFLOW_OUTPUT_SCHEMA_MISMATCH", "WORKFLOW_DRAFT_CONFLICT", "WORKFLOW_VERSION_NOT_FOUND", # run lifecycle "WORKFLOW_RUN_NOT_FOUND", "WORKFLOW_RUN_CONFLICT", "WORKFLOW_RUN_NOT_RESUMABLE", "WORKFLOW_RESUME_TOKEN_INVALID", "WORKFLOW_CANCELLED", "WORKFLOW_TIMEOUT", "WORKFLOW_MAX_STEPS", "WORKFLOW_MAX_LOOP_ITERATIONS", "WORKFLOW_BUDGET_EXCEEDED", # node categories "WORKFLOW_INPUT_INVALID", "WORKFLOW_AGENT_FAILED", "WORKFLOW_SKILL_FAILED", "WORKFLOW_HTTP_POLICY_DENIED", "WORKFLOW_HTTP_FAILED", "WORKFLOW_SQL_POLICY_DENIED", "WORKFLOW_SQL_FAILED", "WORKFLOW_CODE_SANDBOX_FAILED", "WORKFLOW_TRANSFORM_FAILED", "WORKFLOW_DEEP_RESEARCH_FAILED", "WORKFLOW_CONDITION_FAILED", "WORKFLOW_MERGE_FAILED", "WORKFLOW_HUMAN_INPUT_INVALID", "WORKFLOW_SUBWORKFLOW_FAILED", "WORKFLOW_LOOP_FAILED", # auth / embed "WORKFLOW_FORBIDDEN", "WORKFLOW_EMBED_TICKET_INVALID", "WORKFLOW_EMBED_ORIGIN_DENIED", # generic "WORKFLOW_INTERNAL", ] ALL_WORKFLOW_ERROR_CODES: tuple[WorkflowErrorCode, ...] = ( "WORKFLOW_SCHEMA_INVALID", "WORKFLOW_GRAPH_CYCLE", "WORKFLOW_NODE_NOT_FOUND", "WORKFLOW_EDGE_INVALID", "WORKFLOW_EXPRESSION_INVALID", "WORKFLOW_EXPRESSION_UPSTREAM_REQUIRED", "WORKFLOW_RESOURCE_MISSING", "WORKFLOW_RESOURCE_FORBIDDEN", "WORKFLOW_LIMIT_EXCEEDED", "WORKFLOW_OUTPUT_SCHEMA_MISMATCH", "WORKFLOW_DRAFT_CONFLICT", "WORKFLOW_VERSION_NOT_FOUND", "WORKFLOW_RUN_NOT_FOUND", "WORKFLOW_RUN_CONFLICT", "WORKFLOW_RUN_NOT_RESUMABLE", "WORKFLOW_RESUME_TOKEN_INVALID", "WORKFLOW_CANCELLED", "WORKFLOW_TIMEOUT", "WORKFLOW_MAX_STEPS", "WORKFLOW_MAX_LOOP_ITERATIONS", "WORKFLOW_BUDGET_EXCEEDED", "WORKFLOW_INPUT_INVALID", "WORKFLOW_AGENT_FAILED", "WORKFLOW_SKILL_FAILED", "WORKFLOW_HTTP_POLICY_DENIED", "WORKFLOW_HTTP_FAILED", "WORKFLOW_SQL_POLICY_DENIED", "WORKFLOW_SQL_FAILED", "WORKFLOW_CODE_SANDBOX_FAILED", "WORKFLOW_TRANSFORM_FAILED", "WORKFLOW_DEEP_RESEARCH_FAILED", "WORKFLOW_CONDITION_FAILED", "WORKFLOW_MERGE_FAILED", "WORKFLOW_HUMAN_INPUT_INVALID", "WORKFLOW_SUBWORKFLOW_FAILED", "WORKFLOW_LOOP_FAILED", "WORKFLOW_FORBIDDEN", "WORKFLOW_EMBED_TICKET_INVALID", "WORKFLOW_EMBED_ORIGIN_DENIED", "WORKFLOW_INTERNAL", ) class WorkflowErrorBody(BaseModel): """Wire-format error body shared by API responses and ``run.failed`` events.""" code: WorkflowErrorCode message: str retryable: bool = False node_id: str | None = Field(default=None, alias="nodeId") details: dict[str, Any] = Field(default_factory=dict) model_config = {"extra": "forbid", "populate_by_name": True} class WorkflowError(Exception): """Raised by validators / node executors; convertible to :class:`WorkflowErrorBody`.""" def __init__( self, code: WorkflowErrorCode, message: str, *, retryable: bool = False, node_id: str | None = None, details: dict[str, Any] | None = None, ) -> None: super().__init__(message) self.code = code self.message = message self.retryable = retryable self.node_id = node_id self.details = details or {} def to_body(self) -> WorkflowErrorBody: return WorkflowErrorBody( code=self.code, message=self.message, retryable=self.retryable, node_id=self.node_id, details=self.details, ) __all__ = [ "ALL_WORKFLOW_ERROR_CODES", "WorkflowError", "WorkflowErrorBody", "WorkflowErrorCode", ]