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

131 lines
4.4 KiB
Python

"""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},
)