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