feat: Phase 2 - Orchestrator workflow node + tool-level human approval

2.1 Orchestrator in workflow:
- New run_orchestrator_node() in workflow_integration.py loads agents from DB,
  supports route/sequential/debate/pipeline modes
- New 'orchestrator' node type in workflow_engine.py execute_node dispatch

2.2 Tool-level human approval:
- AgentToolConfig extended with require_approval, approval_timeout_ms,
  approval_default fields
- New ApprovalManager (approval_manager.py) with asyncio.Event-based
  create/wait_for_decision/resolve pattern
- AgentRuntime run() and run_stream() intercept tool execution,
  wait for approval decision before executing
- New POST /api/v1/approval/{id}/resolve REST endpoint
- Frontend: approval_required SSE event handling, approval dialog UI
  with approve/deny/skip buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-04 23:17:59 +08:00
parent d895922438
commit f3cb35c460
8 changed files with 437 additions and 1 deletions

View File

@@ -1966,6 +1966,53 @@ class WorkflowEngine:
"error": f"Agent 执行失败: {e}",
}
elif node_type == 'orchestrator':
# 多 Agent 编排节点route / sequential / debate / pipeline
if self.logger:
self.logger.info(
"Orchestrator 节点开始执行",
data={"node_id": node_id, "input": input_data},
)
try:
from app.agent_runtime.workflow_integration import run_orchestrator_node
_agent_on_tool = None
if hasattr(self, '_on_tool_executed_budget'):
_agent_on_tool = self._on_tool_executed_budget
# Orchestrator 内各 Agent 的 LLM 调用计入工作流预算
def _on_agent_llm():
self._llm_invocations += 1
if self._llm_invocations > self._cap_llm:
raise WorkflowExecutionError(
detail=f"已超过 LLM 节点调用预算({self._cap_llm} 次)",
)
result = await run_orchestrator_node(
node_data=node.get("data", {}),
input_data=input_data,
execution_logger=self.logger,
user_id=self.trusted_model_config_user_id,
on_tool_executed=_agent_on_tool,
on_llm_invocation=_on_agent_llm,
)
if self.logger:
duration = int((time.time() - start_time) * 1000)
self.logger.log_node_complete(
node_id, node_type, result.get("output"), duration,
)
return result
except Exception as e:
if self.logger:
duration = int((time.time() - start_time) * 1000)
self.logger.log_node_error(node_id, node_type, e, duration)
logger.error(f"Orchestrator 节点执行失败: {e}", exc_info=True)
return {
"output": None,
"status": "failed",
"error": f"Orchestrator 执行失败: {e}",
}
elif node_type == 'condition':
# 条件节点判断分支output 必须透传上游 dict否则 sourceHandle true/false 下游只收到布尔值,丢失 reply/memory
condition = node.get('data', {}).get('condition', '')