Files
agent-maxim/bridge/controller/controller_loop.py
T
Anton 052591281d Initial clean version - Agent Maxim
- 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
2026-04-09 18:07:43 +03:00

259 lines
9.6 KiB
Python

"""Controller Loop V1.
Minimal autonomous orchestration:
goal → decision → bridge run (adapter=agent) → observe → decision → stop
Stop conditions:
1. decision.action == "stop"
2. iteration >= MAX_ITERATIONS
3. bridge run status == done AND agent_final_status == done (natural success)
4. bridge run failed twice in a row (repeated failure guard)
"""
from __future__ import annotations
import json
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
import adapters as adapter_registry
from controller.controller_state import ControllerState, save as save_state, state_path
from controller.decision_model import DECISION_MODE, decide
from core.run_manager import RunManager
from policy.guard import check as policy_check
MAX_ITERATIONS: int = 3
RUNS_DIR: Path = Path(__file__).resolve().parents[1] / "runs"
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
# ---------------------------------------------------------------------------
# Controller-level audit logger
# ---------------------------------------------------------------------------
class _CtrlAudit:
def __init__(self, ctrl_id: str, runs_dir: Path) -> None:
self._path = runs_dir / ctrl_id / "controller_audit.jsonl"
self._ctrl_id = ctrl_id
self._path.parent.mkdir(parents=True, exist_ok=True)
def log(self, event: str, *, status: str = "ok", details: Optional[dict] = None) -> None:
record = {
"timestamp": _now(),
"trace_id": self._ctrl_id, # trace_id == controller_run_id
"run_id": self._ctrl_id,
"layer": "controller",
"controller_run_id": self._ctrl_id,
"event_type": event,
"status": status,
"details": details or {},
}
with self._path.open("a", encoding="utf-8") as f:
f.write(json.dumps(record) + "\n")
# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------
def run_controller(
goal: str,
run_id: Optional[str] = None,
) -> ControllerState:
"""Execute the full controller loop and return final ControllerState."""
ctrl_id = run_id or f"ctrl-{str(uuid.uuid4())[:8]}"
if state_path(RUNS_DIR, ctrl_id).exists():
raise ValueError(f"controller run_id '{ctrl_id}' already exists")
state = ControllerState(
controller_run_id=ctrl_id,
original_goal=goal,
decision_mode=DECISION_MODE,
)
audit = _CtrlAudit(ctrl_id, RUNS_DIR)
manager = RunManager(RUNS_DIR)
save_state(state, RUNS_DIR)
audit.log("controller_started", details={"goal": goal, "decision_mode": DECISION_MODE,
"max_iterations": MAX_ITERATIONS})
_t_total = time.time()
last_result: Optional[dict[str, Any]] = None
consecutive_failures = 0
recovery_attempted = False
for i in range(MAX_ITERATIONS):
state.iteration_count = i + 1
state.touch()
# 1. DECIDE
decision = decide(goal, i, last_result, MAX_ITERATIONS, recovery_attempted, trace_id=ctrl_id)
# Log recovery audit events based on decision
if last_result is not None and (
last_result.get("status") == "failed"
or last_result.get("agent_final_status") == "failed"
) and not recovery_attempted:
audit.log("recovery_considered", details={
"iteration": i,
"original_goal": goal,
"recovery_applied": decision.recovery_applied,
"reason": decision.reason,
})
if decision.recovery_applied:
audit.log("recovery_applied", details={
"iteration": i,
"original_goal": goal,
"rewritten_goal": decision.goal,
"rule_reason": decision.reason,
})
state.recovery_attempted = True
state.recovery_reason = decision.reason
state.rewritten_goal = decision.goal
recovery_attempted = True
# Update running goal to rewritten goal
goal = decision.goal
else:
audit.log("recovery_skipped", details={
"iteration": i,
"original_goal": goal,
"reason": decision.reason,
})
state.decisions.append({**decision.to_dict(), "iteration": i, "decided_at": _now()})
save_state(state, RUNS_DIR)
# Hard stop guard — layer 2: if last run succeeded, never run agent again
terminal_enforced = False
if (
decision.action == "run_agent"
and last_result is not None
and last_result.get("status") == "done"
and last_result.get("agent_final_status") == "done"
):
decision.action = "stop"
terminal_enforced = True
audit.log("decision_made", details={
"iteration": i,
"action": decision.action,
"goal": decision.goal,
"reason": decision.reason,
"mode": decision.mode,
"parsed_success": decision.parsed_success,
"fallback_used": decision.mode in ("stub", "ollama-fallback", "real-fallback"),
"fallback_reason": decision.fallback_reason,
"corrected": decision.corrected,
"correction_reason": decision.correction_reason,
"terminal_enforced": terminal_enforced,
})
if decision.action == "stop":
# Determine final status from last result
if last_result is None:
state.final_status = "stopped"
elif last_result.get("status") == "done":
state.final_status = "done"
else:
state.final_status = "failed"
break
# 2. EXECUTE BRIDGE RUN
bridge_run_id = f"bri-{ctrl_id}-i{i}"
agent_mod = adapter_registry.get("agent")
audit.log("bridge_run_started", details={
"iteration": i,
"bridge_run_id": bridge_run_id,
"goal": decision.goal,
})
_t_bridge = time.time()
try:
bridge_result = manager.execute_run(
goal=decision.goal,
adapter_name="agent",
adapter_fn=agent_mod.execute, # type: ignore[attr-defined]
guard_fn=policy_check,
run_id=bridge_run_id,
trace_id=ctrl_id,
)
except Exception as exc: # noqa: BLE001
error_msg = f"{type(exc).__name__}: {exc}"
audit.log("bridge_run_error", status="failed", details={
"iteration": i,
"bridge_run_id": bridge_run_id,
"error": error_msg,
})
bridge_result = {
"run_id": bridge_run_id,
"status": "failed",
"error": error_msg,
"agent_final_status": "failed",
"exit_code": 1,
}
print(f"[TIMING] bridge.execute (iter {i}): {time.time() - _t_bridge:.3f}s")
state.linked_bridge_run_ids.append(bridge_run_id)
state.bridge_run_results.append({
"iteration": i,
"bridge_run_id": bridge_run_id,
"status": bridge_result.get("status"),
"agent_final_status": bridge_result.get("agent_final_status"),
"exit_code": bridge_result.get("exit_code"),
"error": bridge_result.get("error"),
})
last_result = bridge_result
state.touch()
save_state(state, RUNS_DIR)
audit.log("bridge_run_finished", details={
"iteration": i,
"bridge_run_id": bridge_run_id,
"status": bridge_result.get("status"),
"agent_final_status": bridge_result.get("agent_final_status"),
"exit_code": bridge_result.get("exit_code"),
})
# 3. IMMEDIATE STOP — natural success, no further decision call
if (bridge_result.get("status") == "done"
and bridge_result.get("agent_final_status") == "done"):
state.final_status = "done"
save_state(state, RUNS_DIR)
audit.log("controller_finished", status="done",
details={"reason": "agent success", "iterations": state.iteration_count,
"linked_runs": state.linked_bridge_run_ids})
print(f"[TIMING] run_controller total: {time.time() - _t_total:.3f}s")
return state
# Repeated failure guard
consecutive_failures += 1
if consecutive_failures >= 2:
state.final_status = "failed"
audit.log("controller_finished", status="failed",
details={"reason": "Repeated consecutive failures", "iterations": state.iteration_count})
save_state(state, RUNS_DIR)
print(f"[TIMING] run_controller total: {time.time() - _t_total:.3f}s")
return state
else:
# MAX_ITERATIONS exhausted without explicit stop
if last_result and last_result.get("status") == "done":
state.final_status = "done"
else:
state.final_status = "failed"
save_state(state, RUNS_DIR)
audit.log("controller_finished", status=state.final_status,
details={"iterations": state.iteration_count,
"linked_runs": state.linked_bridge_run_ids})
print(f"[TIMING] run_controller total: {time.time() - _t_total:.3f}s")
return state