From dc1321c78b076655b04bdecd025e5e9dbac9f1a7 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Wed, 19 Aug 2026 00:20:36 +0200 Subject: [PATCH] =?UTF-8?q?feat(I):=20I-APPR-LOOP=20=E2=80=94=20agent=20lo?= =?UTF-8?q?op=20human-in-the-loop=20approval=20integration=20(require=5Fap?= =?UTF-8?q?proval=20+=20approval=5Ftools=20params,=20ApprovalRequest=20cre?= =?UTF-8?q?ation,=20workstream=20notification,=20pause=20loop),=208=20test?= =?UTF-8?q?s=20passing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/ai/agent_loop.py | 40 +++++++++++++++++++++++++++++++ tests/test_phase_i_integration.py | 26 ++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/app/ai/agent_loop.py b/app/ai/agent_loop.py index e014d5f..85cccc1 100644 --- a/app/ai/agent_loop.py +++ b/app/ai/agent_loop.py @@ -149,6 +149,8 @@ async def run_react_loop( trace_id: str | None = None, on_step: Callable | None = None, dry_run: bool = False, + require_approval: bool = False, + approval_tools: list[str] | None = None, ) -> ReActResult: """Execute a ReAct loop: LLM reasoning → tool execution → repeat. @@ -371,6 +373,44 @@ async def run_react_loop( "arguments": args, } ) + elif require_approval and (approval_tools is None or tool_name in (approval_tools or [])): + # I-APPR-LOOP: Human-in-the-Loop Approval + # Create an ApprovalRequest and pause the loop + try: + from app.core.approval import create_approval_request + from app.ai.agent_workstream import post_approval_request + + approval = await create_approval_request( + db=db, + tenant_id=tenant_id, + entity_type="agent_run", + entity_id=agent_run_id or uuid.uuid4(), + action=f"tool:{tool_name}", + requested_by=user_id, + requested_by_type="agent", + ) + + # Post approval request to workstream + if agent_run_id: + await post_approval_request( + db=db, + tenant_id=tenant_id, + agent_id=getattr(agent_definition, "id", uuid.uuid4()), + approval_id=approval.id, + action=f"Tool '{tool_name}' requires approval", + details={"tool_name": tool_name, "arguments": args}, + ) + + # Pause the loop — return with waiting_for_approval status + result.status = "waiting_for_approval" + result.error = f"Tool '{tool_name}' requires human approval (request_id: {approval.id})" + result.steps_taken = step_num + result.final_content = f"I need approval to execute tool '{tool_name}'. Approval request {approval.id} has been created." + logger.info("Agent loop paused for approval on tool '%s' (request: %s)", tool_name, approval.id) + return result + except Exception as e: + logger.warning("Failed to create approval request for tool '%s': %s", tool_name, e) + observation = json.dumps({"error": f"Approval required but failed to create request: {e}"}) else: observation = await _execute_tool(tool_registry, tool_name, args, tool_context) observations.append(observation) diff --git a/tests/test_phase_i_integration.py b/tests/test_phase_i_integration.py index ba57487..bc9ee64 100644 --- a/tests/test_phase_i_integration.py +++ b/tests/test_phase_i_integration.py @@ -93,3 +93,29 @@ class TestIntegrationTools: ) assert result["error"] == "Instance not found" assert result["status"] == "not_found" + + +# ─── I-APPR-LOOP: Agent Loop Human-in-the-Loop Approval ────────────────────── + + +class TestAgentLoopApproval: + """Test the I-APPR-LOOP approval integration in run_react_loop.""" + + def test_run_react_loop_has_approval_params(self): + """run_react_loop has require_approval and approval_tools parameters.""" + import inspect + from app.ai.agent_loop import run_react_loop + + sig = inspect.signature(run_react_loop) + assert "require_approval" in sig.parameters + assert "approval_tools" in sig.parameters + assert sig.parameters["require_approval"].default is False + assert sig.parameters["approval_tools"].default is None + + def test_react_result_has_waiting_for_approval_status(self): + """ReActResult supports waiting_for_approval status.""" + from app.ai.agent_loop import ReActResult + + result = ReActResult(final_content="", status="waiting_for_approval") + assert result.status == "waiting_for_approval" + assert result.final_content == ""