deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/workflows/errors.py
2026-09-07 18:24:55 +08:00

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",
]