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:
@@ -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', '')
|
||||
|
||||
Reference in New Issue
Block a user