153 lines
4.3 KiB
Python
153 lines
4.3 KiB
Python
"""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",
|
|
]
|