Files
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

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