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
205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
"""Main agent execution loop: select → resolve → execute → retry? → update → persist → repeat."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from core.classifier import FailureClass, classify
|
|
from core.resolver import has_templates, resolve_payload
|
|
from core.retry_policy import should_retry
|
|
from core.state import AgentState, RunStatus, StepResult, StepStatus
|
|
from planner.step_selector import select_next_step
|
|
from runtime.executor import Executor
|
|
from telemetry.audit_logger import AuditLogger
|
|
|
|
MAX_STEPS = 20
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def run_loop(
|
|
state: AgentState,
|
|
executor: Executor,
|
|
logger: AuditLogger,
|
|
runs_dir: Path,
|
|
) -> AgentState:
|
|
"""Main loop: select → resolve → execute → retry-or-continue → save → repeat.
|
|
|
|
Terminates when:
|
|
- no pending steps remain (DONE)
|
|
- template resolution fails and no retry allowed (FAILED)
|
|
- a step fails and no retry allowed (FAILED)
|
|
- a 'finish' step completes (DONE)
|
|
- MAX_STEPS is exceeded (FAILED)
|
|
"""
|
|
from core.controller import save_state # avoid circular import
|
|
|
|
for _ in range(MAX_STEPS):
|
|
step = select_next_step(state.plan)
|
|
if step is None:
|
|
state.status = RunStatus.DONE
|
|
break
|
|
|
|
# Track attempt number (0 = first time through)
|
|
is_retry = step.attempts > 0
|
|
step.attempts += 1
|
|
current_attempt = step.attempts
|
|
|
|
# pending → running
|
|
step.status = StepStatus.RUNNING
|
|
state.current_step_id = step.step_id
|
|
state.updated_at = _now()
|
|
|
|
if is_retry:
|
|
logger.log(
|
|
"retry_attempt_started",
|
|
step_id=step.step_id,
|
|
status="running",
|
|
details={"kind": step.kind, "attempt": current_attempt},
|
|
)
|
|
else:
|
|
logger.log(
|
|
"step_started",
|
|
step_id=step.step_id,
|
|
status="running",
|
|
details={"kind": step.kind, "description": step.description},
|
|
)
|
|
|
|
# --- Template resolution (before executor) ---
|
|
if has_templates(step.payload):
|
|
resolved_payload, ok, err = resolve_payload(step.payload, state)
|
|
logger.log(
|
|
"resolution_performed",
|
|
step_id=step.step_id,
|
|
status="ok" if ok else "failed",
|
|
details={"templates_found": True, "success": ok, "error": err if not ok else None},
|
|
)
|
|
if not ok:
|
|
result = StepResult(
|
|
step_id=step.step_id,
|
|
status=StepStatus.FAILED,
|
|
error=f"Template resolution failed: {err}",
|
|
started_at=_now(),
|
|
finished_at=_now(),
|
|
attempt=current_attempt,
|
|
failure_class=FailureClass.TEMPLATE_ERROR.value,
|
|
)
|
|
step.status = StepStatus.FAILED
|
|
state.history.append(result)
|
|
state.updated_at = _now()
|
|
logger.log(
|
|
"retry_attempt_finished" if is_retry else "step_finished",
|
|
step_id=step.step_id,
|
|
status="failed",
|
|
details={"error": result.error, "attempt": current_attempt},
|
|
)
|
|
logger.log(
|
|
"step_failed",
|
|
step_id=step.step_id,
|
|
status="failed",
|
|
details={
|
|
"failure_class": FailureClass.TEMPLATE_ERROR.value,
|
|
"attempt": current_attempt,
|
|
"retry_scheduled": False,
|
|
"reason": "template_error is never retried",
|
|
},
|
|
)
|
|
state.status = RunStatus.FAILED
|
|
state.current_step_id = None
|
|
save_state(state, runs_dir, logger)
|
|
break
|
|
step.payload = resolved_payload
|
|
else:
|
|
if step.kind not in ("finish",):
|
|
logger.log(
|
|
"resolution_performed",
|
|
step_id=step.step_id,
|
|
status="ok",
|
|
details={"templates_found": False, "success": True},
|
|
)
|
|
|
|
# --- Execute ---
|
|
started = _now()
|
|
result = executor.execute(step)
|
|
result.started_at = started
|
|
result.finished_at = _now()
|
|
result.attempt = current_attempt
|
|
|
|
logger.log(
|
|
"retry_attempt_finished" if is_retry else "step_finished",
|
|
step_id=step.step_id,
|
|
status=result.status.value,
|
|
details={
|
|
"output_text": (result.output_text or "")[:300],
|
|
"error": result.error,
|
|
"attempt": current_attempt,
|
|
},
|
|
)
|
|
|
|
# --- Retry / fail decision ---
|
|
if result.status == StepStatus.FAILED:
|
|
failure_class = classify(step.kind, result.error or "")
|
|
result.failure_class = failure_class.value
|
|
retry_ok = should_retry(failure_class, step.attempts)
|
|
|
|
logger.log(
|
|
"step_failed",
|
|
step_id=step.step_id,
|
|
status="failed",
|
|
details={
|
|
"failure_class": failure_class.value,
|
|
"attempt": current_attempt,
|
|
"retry_scheduled": retry_ok,
|
|
},
|
|
)
|
|
|
|
if retry_ok:
|
|
step.status = StepStatus.PENDING # reset for retry
|
|
state.history.append(result)
|
|
state.updated_at = _now()
|
|
logger.log(
|
|
"retry_scheduled",
|
|
step_id=step.step_id,
|
|
status="ok",
|
|
details={
|
|
"attempt_just_failed": current_attempt,
|
|
"next_attempt": current_attempt + 1,
|
|
"failure_class": failure_class.value,
|
|
},
|
|
)
|
|
save_state(state, runs_dir, logger)
|
|
# Loop continues — select_next_step will pick up PENDING step again
|
|
else:
|
|
step.status = StepStatus.FAILED
|
|
state.history.append(result)
|
|
state.updated_at = _now()
|
|
state.status = RunStatus.FAILED
|
|
state.current_step_id = None
|
|
save_state(state, runs_dir, logger)
|
|
break
|
|
|
|
else:
|
|
# Step succeeded
|
|
step.status = StepStatus.DONE
|
|
state.history.append(result)
|
|
state.updated_at = _now()
|
|
save_state(state, runs_dir, logger)
|
|
|
|
if step.kind == "finish":
|
|
state.status = RunStatus.DONE
|
|
state.current_step_id = None
|
|
break
|
|
|
|
else:
|
|
state.status = RunStatus.FAILED
|
|
state.current_step_id = None
|
|
logger.log(
|
|
"run_aborted",
|
|
status="failed",
|
|
details={"reason": f"Exceeded MAX_STEPS ({MAX_STEPS})"},
|
|
)
|
|
|
|
return state
|