052591281d
- Complete agent system with planning and execution - Bridge API with FastAPI server - WhatsApp-style mobile dashboard - Enhanced web search with query improvement - Security guards and policy checks - Comprehensive documentation
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""Agent adapter — executes the real local agent via subprocess.
|
|
|
|
Calls agent/main.py with the given instruction as --goal.
|
|
The planner keyword-matches on goal text, so the instruction
|
|
(which contains the original goal as a substring) routes correctly.
|
|
|
|
Run ID mapping:
|
|
bridge run_id : <uuid> (e.g. "a1b2c3d4")
|
|
agent run_id : "agent-<bridge_run_id>" (e.g. "agent-a1b2c3d4")
|
|
|
|
This makes every bridge run traceable to a unique agent workspace dir:
|
|
agent/workspace/runs/agent-<bridge_run_id>/state.json
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Callable, Optional
|
|
|
|
from adapters.base import AdapterResult
|
|
|
|
MODE = "real"
|
|
|
|
# Resolve paths relative to this file: bridge/adapters/ → bridge/ → localagent/ → agent/
|
|
_BRIDGE_DIR: Path = Path(__file__).resolve().parents[1]
|
|
_AGENT_DIR: Path = _BRIDGE_DIR.parent / "agent"
|
|
_AGENT_MAIN: Path = _AGENT_DIR / "main.py"
|
|
|
|
|
|
def execute(prompt: str, bridge_run_id: Optional[str] = None, trace_id: Optional[str] = None, is_cancelled: Optional[Callable[[], bool]] = None) -> AdapterResult:
|
|
"""Run the agent with prompt as --goal, return its stdout + parsed state."""
|
|
print("=== THIS ADAPTER IS USED: agent_adapter ===")
|
|
if not _AGENT_MAIN.exists():
|
|
return AdapterResult(
|
|
response=None,
|
|
error=f"agent/main.py not found at {_AGENT_MAIN}",
|
|
exit_code=1,
|
|
mode=MODE,
|
|
)
|
|
|
|
agent_run_id = f"agent-{bridge_run_id}" if bridge_run_id else "agent-unlinked"
|
|
|
|
# Check if run is cancelled via callback
|
|
if is_cancelled and is_cancelled():
|
|
return AdapterResult(
|
|
response=None,
|
|
error="user_cancelled",
|
|
exit_code=130, # Exit code for cancelled
|
|
mode=MODE,
|
|
metadata={"agent_run_id": agent_run_id, "agent_final_status": "cancelled"},
|
|
)
|
|
|
|
cmd = [sys.executable, str(_AGENT_MAIN), "--goal", prompt, "--run-id", agent_run_id]
|
|
if trace_id:
|
|
cmd += ["--trace-id", trace_id]
|
|
|
|
_t_sub = time.time()
|
|
print("=== BEFORE AGENT SUBPROCESS ===")
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
shell=False,
|
|
cwd=str(_AGENT_DIR),
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
print("=== AFTER AGENT SUBPROCESS ===")
|
|
print(f"[TIMING] subprocess: {time.time() - _t_sub:.3f}s [TIMEOUT]")
|
|
return AdapterResult(
|
|
response=None,
|
|
error="agent subprocess timed out after 60s",
|
|
exit_code=1,
|
|
mode=MODE,
|
|
metadata={"agent_run_id": agent_run_id},
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
print("=== AFTER AGENT SUBPROCESS ===")
|
|
print(f"[TIMING] subprocess: {time.time() - _t_sub:.3f}s [ERROR: {exc}]")
|
|
return AdapterResult(
|
|
response=None,
|
|
error=f"{type(exc).__name__}: {exc}",
|
|
exit_code=1,
|
|
mode=MODE,
|
|
metadata={"agent_run_id": agent_run_id},
|
|
)
|
|
|
|
print("=== AFTER AGENT SUBPROCESS ===")
|
|
print(f"[TIMING] subprocess: {time.time() - _t_sub:.3f}s")
|
|
stdout = proc.stdout.strip()
|
|
stderr = proc.stderr.strip()
|
|
print(f"[STDOUT]: {stdout[:300]}")
|
|
if stderr:
|
|
print(f"[STDERR]: {stderr[:300]}")
|
|
combined = stdout + ("\n" + stderr if stderr else "")
|
|
|
|
# Read agent's persisted state.json for structured result
|
|
agent_state_path = _AGENT_DIR / "workspace" / "runs" / agent_run_id / "state.json"
|
|
agent_final_status: Optional[str] = None
|
|
if agent_state_path.exists():
|
|
try:
|
|
agent_state = json.loads(agent_state_path.read_text(encoding="utf-8"))
|
|
agent_final_status = agent_state.get("status")
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
metadata = {
|
|
"agent_run_id": agent_run_id,
|
|
"agent_final_status": agent_final_status,
|
|
"agent_state_path": str(agent_state_path),
|
|
}
|
|
|
|
if proc.returncode != 0:
|
|
return AdapterResult(
|
|
response=combined or None,
|
|
error=f"agent exited {proc.returncode}" + (f": {stderr}" if stderr else ""),
|
|
exit_code=proc.returncode,
|
|
mode=MODE,
|
|
metadata=metadata,
|
|
)
|
|
|
|
# exit_code=0 but agent state missing or unreadable → treat as failure
|
|
if agent_final_status is None:
|
|
return AdapterResult(
|
|
response=combined or None,
|
|
error="agent state missing or invalid (exit 0 but state.json unreadable)",
|
|
exit_code=1,
|
|
mode=MODE,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return AdapterResult(
|
|
response=combined,
|
|
error=None,
|
|
exit_code=0,
|
|
mode=MODE,
|
|
metadata=metadata,
|
|
)
|
|
|
|
|
|
def mode() -> str:
|
|
return MODE if _AGENT_MAIN.exists() else "unavailable"
|