diff --git a/ai-hub/app/api/routes/agents.py b/ai-hub/app/api/routes/agents.py index 24401be..00006bc 100644 --- a/ai-hub/app/api/routes/agents.py +++ b/ai-hub/app/api/routes/agents.py @@ -163,8 +163,8 @@ db.refresh(instance) return instance - @router.post("/{id}/webhook", status_code=202) - def webhook_receiver(id: str, payload: dict, background_tasks: BackgroundTasks, token: str = None, db: Session = Depends(get_db)): + @router.post("/{id}/webhook") + async def webhook_receiver(id: str, payload: dict, background_tasks: BackgroundTasks, response: Response, token: str = None, sync: bool = False, db: Session = Depends(get_db)): instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() if not instance: raise HTTPException(status_code=404, detail="Instance not found") @@ -187,8 +187,18 @@ # Fallback to serialised payload prompt = f"Webhook Event: {json.dumps(payload)}" - background_tasks.add_task(AgentExecutor.run, instance.id, prompt, services.rag_service, services.user_service) - return {"message": "Accepted"} + if sync: + # Synchronous blocking mode + try: + answer = await AgentExecutor.run(db, instance.id, prompt, services.rag_service, services.user_service) + return {"status": "success", "response": answer} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Agent execution failed: {str(e)}") + else: + # Asynchronous background mode (Default) + background_tasks.add_task(AgentExecutor.run, instance.id, prompt, services.rag_service, services.user_service) + response.status_code = status.HTTP_202_ACCEPTED + return {"status": "accepted", "message": "Background task initiated"} @router.post("/{id}/run", status_code=202) def manual_trigger(id: str, payload: dict, background_tasks: BackgroundTasks, db: Session = Depends(get_db)): diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py index 66ecfba..065128f 100644 --- a/ai-hub/app/core/orchestration/agent_loop.py +++ b/ai-hub/app/core/orchestration/agent_loop.py @@ -92,6 +92,7 @@ final_tool_counts = {} final_input_tokens = 0 final_output_tokens = 0 + final_answer = "" # We consume the generator completely to let it execute all tools and generate reasoning async for event in rag_service.chat_with_rag( @@ -106,6 +107,7 @@ final_tool_counts = event.get("tool_counts", {}) final_input_tokens = event.get("input_tokens", 0) final_output_tokens = event.get("output_tokens", 0) + final_answer = event.get("full_answer", "") # Execution complete instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() @@ -129,13 +131,15 @@ if k not in current_counts: current_counts[k] = {"calls": 0, "successes": 0, "failures": 0} - current_counts[k]["calls"] = current_counts[k].get("calls", 0) + v.get("calls", 0) - current_counts[k]["successes"] = current_counts[k].get("successes", 0) + v.get("successes", 0) - current_counts[k]["failures"] = current_counts[k].get("failures", 0) + v.get("failures", 0) + current_counts[k]["calls"] += v.get("calls", 0) + current_counts[k]["successes"] += v.get("successes", 0) + current_counts[k]["failures"] += v.get("failures", 0) instance.tool_call_counts = current_counts db.commit() + + return final_answer except Exception as e: import traceback diff --git a/ai-hub/integration_tests/test_agents.py b/ai-hub/integration_tests/test_agents.py index cb76511..bf9b455 100644 --- a/ai-hub/integration_tests/test_agents.py +++ b/ai-hub/integration_tests/test_agents.py @@ -180,7 +180,21 @@ time.sleep(2) assert found, "The agent did not process the custom webhook prompt correctly." - print("[test] Webhook custom prompt processed successfully!") + print("[test] ASYNC Webhook custom prompt processed successfully!") + + # 6. Trigger the Webhook in SYNC Mode + print(f"\n[test] Triggering SYNC Webhook...") + sync_secret_msg = "SYNC-MODE-CONFIRMED-OK" + r_sync = client.post( + f"{BASE_URL}/agents/{instance_id}/webhook", + params={"token": secret, "sync": "true"}, + json={"prompt": f"Please reply with exactly: {sync_secret_msg}"} + ) + assert r_sync.status_code == 200, f"Sync Webhook failed: {r_sync.text}" + sync_data = r_sync.json() + assert sync_data["status"] == "success" + assert sync_secret_msg in (sync_data.get("response") or ""), f"Expected {sync_secret_msg} in sync response" + print("[test] SYNC Webhook verified successfully!") # 6. Cleanup client.delete(f"{BASE_URL}/agents/{instance_id}", headers=_headers()) diff --git a/ai-hub/test.db-shm b/ai-hub/test.db-shm index 63248ae..1247bd6 100644 --- a/ai-hub/test.db-shm +++ b/ai-hub/test.db-shm Binary files differ diff --git a/ai-hub/test.db-wal b/ai-hub/test.db-wal index 4875607..226b764 100644 --- a/ai-hub/test.db-wal +++ b/ai-hub/test.db-wal Binary files differ diff --git a/docs/features/harness_engineering/harness_engineering_design.md b/docs/features/harness_engineering/harness_engineering_design.md index ffe8f93..23ad5d6 100644 --- a/docs/features/harness_engineering/harness_engineering_design.md +++ b/docs/features/harness_engineering/harness_engineering_design.md @@ -94,13 +94,16 @@ 3. **Manual / Off-hand Requests (Play Button):** - *UI:* A prominent "Start/Pause" toggle or a manual "Trigger Now" button. - *Mechanics:* Kicks off the Agent loop. If triggered via API with a specific prompt, it uses that; otherwise, it falls back to the *Predefined Default Prompt*. -4. **Event Webhooks (Acknowledge-First Architecture):** +4. **Event Webhooks (Dual-Mode Architecture):** - *UI:* Clicking "Generate Webhook" produces a secure URL and secret token. Includes a JSON mapping field. + - *Modes:* + - **Async (Default):** The Hub returns `202 Accepted` immediately. The task runs in the background. Ideal for fire-and-forget integrations like GitHub Actions or Jira. + - **Sync:** Appending `?sync=true` to the URL blocks the HTTP request until the agent completes its run. It returns the final text response (`200 OK`). Perfect for using an Agent as a synchronous function or inter-agent communication. - *Mechanics:* - 1. The Hub receives the raw JSON webhook. + 1. The Hub receives the raw JSON webhook and validates the `token`. 2. If mapping is defined, it transforms payload fields into a formatted prompt (e.g., `Issue #{{payload.id}} was created: {{payload.content}}`). 3. If no payload mapping matches or the hit is empty, it uses the *Predefined Default Prompt*. - 4. Worker wakes up and processes the task. + 4. Worker wakes up and processes the task (either backgrounded or blocking). ### D. Dependency Graph (The "Orchestrator" View) As agents begin to natively Handoff tasks (passing JSON Manifests), they form a pipeline (e.g., *Frontend Dev* -> *Backend Dev* -> *QA Reviewer*). The UI provides a "Link View" visualizing these connections as edges between nodes. Real-time token flow and "Awaiting Dependencies" states are visualized here to help lead engineers spot pipeline bottlenecks instantly. diff --git a/frontend/src/features/agents/components/AgentDrillDown.js b/frontend/src/features/agents/components/AgentDrillDown.js index 61c96a8..199ed67 100644 --- a/frontend/src/features/agents/components/AgentDrillDown.js +++ b/frontend/src/features/agents/components/AgentDrillDown.js @@ -629,6 +629,20 @@
{JSON.stringify({ "prompt": "...custom prompt here..." }, null, 2)}
@@ -636,11 +650,11 @@