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

@@ -311,6 +311,25 @@ class AgentRuntime:
except (json.JSONDecodeError, TypeError):
targs = {}
# 工具执行前审批检查
if tname in self.config.tools.require_approval:
from app.services.approval_manager import approval_manager as _am
logger.info("Agent 工具需审批 [%s]: %s", tname, targs)
approval_req = await _am.submit(
tool_name=tname, args=targs,
timeout_ms=self.config.tools.approval_timeout_ms,
)
decision = approval_req.decision
if decision == "denied":
result = f"[审批拒绝] 工具 {tname} 需要人工审批但被拒绝。"
self.context.add_tool_result(tcid, tname, result)
continue
elif decision == "skip":
result = f"[审批跳过] 工具 {tname} 被跳过。"
self.context.add_tool_result(tcid, tname, result)
continue
# decision == "approved" → 继续执行
logger.info("Agent 执行工具 [%s]: %s", tname, targs)
result = await self.tool_manager.execute(tname, targs)
@@ -591,6 +610,34 @@ class AgentRuntime:
"iteration": self.context.iteration,
}
# 工具执行前审批检查(流式:先 create → yield 事件带 ID → 等待决定)
if tname in self.config.tools.require_approval:
from app.services.approval_manager import approval_manager as _am
logger.info("Agent 工具需审批 [%s]: %s", tname, targs)
approval_req = _am.create(tool_name=tname, args=targs)
yield {
"type": "approval_required",
"approval_id": approval_req.approval_id,
"tool_name": tname,
"args": targs,
"iteration": self.context.iteration,
}
decision = await _am.wait_for_decision(
approval_req.approval_id,
timeout_ms=self.config.tools.approval_timeout_ms,
)
if decision == "denied":
result = f"[审批拒绝] 工具 {tname} 需要人工审批但被拒绝。"
yield {"type": "tool_result", "name": tname, "result": result, "iteration": self.context.iteration}
self.context.add_tool_result(tcid, tname, result)
continue
elif decision == "skip":
result = f"[审批跳过] 工具 {tname} 被跳过。"
yield {"type": "tool_result", "name": tname, "result": result, "iteration": self.context.iteration}
self.context.add_tool_result(tcid, tname, result)
continue
# decision == "approved" → 继续执行
logger.info("Agent 执行工具 [%s]: %s", tname, targs)
result = await self.tool_manager.execute(tname, targs)