Files
deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/workflows/errors.py
T
董宏凯 1706a24182 feat(workflow): 增加会话级多智能体任务规划运行能力
提交范围聚焦于工作流编排新增:新增 workflow_planning 路由与前后端运行入口(workflow_planning.py、workflow_proposal_planner.py、workflow_deep_research_adapter.py),支持基于会话的提案-规划-执行流程,满足任务驱动式多智能体协作。

完成网关层运行时适配:扩展 workflow_resource_checks、workflow_executor、workflow_runs、workflow_agent_runner 与依赖注入,新增/更新配置(config.yaml、workflow_config),并补充 lead_agent 路径与节点实现,确保 Planner 与普通工作流运行态衔接一致。

补齐持久化与建模:新增 workflow_planning 持久化模块与 MySQL migration(20260831_01_workflow_planning_sessions),注册 SQL 与模型、内存 store,并扩展错误/表达式/validator/节点语义以承载新会话态输入输出。

增强文档与测试:新增会话式多智能体编排中文说明、更新开发文档与 AGENTS 指南、补充 workflow_engine/workflow_runs 回归测试,提高新路径的行为可验证性;同时留存 seed 资产(workflow-planner)便于一体化部署。
2026-09-01 11:47:30 +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",
]