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
131 lines
4.4 KiB
Python
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},
|
|
)
|