"""Agent controller — builds initial state and starts the run. This is the single entry point used by main.py. It owns: state construction, AuditLogger init, Executor init, final state save. The loop itself lives in agent_loop.py. """ from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path from core.agent_loop import run_loop from core.state import AgentState, RunStatus from planner.planner import PlannerError, build_plan from runtime.executor import Executor from runtime.tool_registry import ToolRegistry from telemetry.audit_logger import AuditLogger def run_agent(run_id: str, goal: str, workspace_root: Path, trace_id: str | None = None) -> AgentState: """Orchestrate a full agent run from goal string to final AgentState. Flow: build_plan → log plan_created → run_loop → save final state → log run_finished """ runs_dir = workspace_root / "runs" logger = AuditLogger(run_id=run_id, runs_dir=runs_dir, trace_id=trace_id) try: plan = build_plan(goal) except PlannerError as exc: now = datetime.now(timezone.utc).isoformat() from core.state import Plan failed_state = AgentState( run_id=run_id, goal=goal, plan=Plan(steps=[]), current_step_id=None, history=[], started_at=now, updated_at=now, status=RunStatus.FAILED, trace_id=trace_id, ) runs_dir = workspace_root / "runs" logger.log("run_failed", status="failed", details={"reason": str(exc)}) save_state(failed_state, runs_dir, logger=None) return failed_state now = datetime.now(timezone.utc).isoformat() state = AgentState( run_id=run_id, goal=goal, plan=plan, current_step_id=None, history=[], started_at=now, updated_at=now, status=RunStatus.RUNNING, trace_id=trace_id, ) logger.log( "run_started", status="running", details={"goal": goal}, ) logger.log( "plan_created", status="ok", details={"step_count": len(plan.steps), "steps": [s.to_dict() for s in plan.steps]}, ) save_state(state, runs_dir, logger) state = run_loop(state, executor=Executor(registry=ToolRegistry()), logger=logger, runs_dir=runs_dir) # Final save — loop already saves after each step, this captures terminal run status save_state(state, runs_dir, logger=None) # no event: loop already logged last state_saved # Validation: ensure search tasks included web_fetch search_keywords = ["search", "найди", "поиск", "news", "новости", "найти информацию", "find"] is_search_task = any(kw in goal.lower() for kw in search_keywords) if is_search_task and state.status == RunStatus.DONE: has_web_search = any(s.kind == "web_search" for s in plan.steps) has_web_fetch = any(s.kind == "web_fetch" for s in plan.steps) if has_web_search and not has_web_fetch: # This shouldn't happen if planner is configured correctly logger.log( "validation_warning", status="warning", details={ "reason": "search task completed without web_fetch", "goal": goal, "steps": [s.kind for s in plan.steps] } ) logger.log( "run_finished", status=state.status.value, details={ "total_steps": len(plan.steps), "completed": sum(1 for s in plan.steps if s.status.value == "done"), "failed": sum(1 for s in plan.steps if s.status.value == "failed"), }, ) return state def save_state(state: AgentState, runs_dir: Path, logger: "Optional[AuditLogger]" = None) -> None: """Persist current AgentState to state.json and emit state_saved event.""" run_dir = runs_dir / state.run_id run_dir.mkdir(parents=True, exist_ok=True) (run_dir / "state.json").write_text( json.dumps(state.to_dict(), indent=2, ensure_ascii=False), encoding="utf-8", ) if logger: logger.log( "state_saved", step_id=state.current_step_id, status="ok", details={"run_status": state.status.value, "current_step_id": state.current_step_id}, )