| """Autonomais aģents — plāno un izpilda uzdevumus.""" |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import logging |
| import uuid |
| from datetime import UTC, datetime |
| from typing import Any |
|
|
| from fastapi import APIRouter |
| from pydantic import BaseModel, Field |
|
|
| from maris_core.autonomous.executor import TaskExecutionError, task_executor |
| from maris_core.autonomous.planner import Planner |
| from maris_core.autonomous.session_store import session_store |
| from maris_core.memory_context import MemoryMatch, memory_store |
| from maris_core.orchestrator.routing import build_system_prompt |
| from maris_core.personas import resolve_persona |
| from maris_core.text.generate import call_generation_pipeline, get_pipeline |
|
|
| logger = logging.getLogger(__name__) |
| router = APIRouter() |
| CODE_GENERATION_KEYWORDS = ("kod", "api", "script", "python", "rust") |
| WEB_RESEARCH_KEYWORDS = ("meklē", "research", "search", "salīdzini") |
| WEB_AUTOMATION_KEYWORDS = ("browser", "pārlūk", "form", "klikš", "scrape", "web") |
| VALIDATION_KEYWORDS = ("test", "verify", "pārbaud") |
| AUTONOMOUS_BACKGROUND_LOOP_DELAY_SECONDS = 0.05 |
|
|
| |
| _sessions: dict[str, dict[str, Any]] = {} |
| _session_runners: dict[str, asyncio.Task[None]] = {} |
| _session_runner_lock: asyncio.Lock | None = None |
| _session_runner_lock_loop: asyncio.AbstractEventLoop | None = None |
| planner = Planner() |
| _AUTONOMOUS_AGENT_ROLES = [ |
| { |
| "id": "planner", |
| "title": "Planner", |
| "responsibility": "Sadala mērķi izpildāmā plānā ar atkarībām un checkpointiem.", |
| }, |
| { |
| "id": "executor", |
| "title": "Executor", |
| "responsibility": "Izpilda nākamo gatavo uzdevumu un straumē rezultātus.", |
| }, |
| { |
| "id": "reviewer", |
| "title": "Reviewer", |
| "responsibility": "Pārbauda riskus, validāciju un sagatavo approval signālus.", |
| }, |
| { |
| "id": "operator", |
| "title": "Operator", |
| "responsibility": "Saņem interruptus, approval pieprasījumus un var atjaunot sesiju no checkpointa.", |
| }, |
| ] |
|
|
|
|
| class StartRequest(BaseModel): |
| session_id: str |
| goal: str |
| max_steps: int = 10 |
| persona_id: str | None = None |
|
|
|
|
| class StatusRequest(BaseModel): |
| session_id: str |
|
|
|
|
| class TaskModel(BaseModel): |
| id: str |
| description: str |
| status: str |
| result: str | None = None |
| created_at: str |
| tool: str |
| depends_on: list[str] = Field(default_factory=list) |
| attempts: int = 0 |
| max_attempts: int = 2 |
| last_error: str | None = None |
|
|
|
|
| class TimelineEventModel(BaseModel): |
| id: str |
| event_type: str |
| title: str |
| detail: str |
| agent_role: str |
| level: str = "info" |
| created_at: str |
| task_id: str | None = None |
| interruptible: bool = False |
|
|
|
|
| class CheckpointModel(BaseModel): |
| id: str |
| label: str |
| status: str |
| summary: str |
| created_at: str |
| task_id: str | None = None |
|
|
|
|
| class ApprovalModel(BaseModel): |
| id: str |
| kind: str |
| status: str |
| title: str |
| summary: str |
| created_at: str |
| task_id: str | None = None |
| resolution_note: str | None = None |
|
|
|
|
| class AgentRoleModel(BaseModel): |
| id: str |
| title: str |
| responsibility: str |
| status: str |
|
|
|
|
| class SessionResponse(BaseModel): |
| session_id: str |
| goal: str |
| status: str |
| tasks: list[TaskModel] |
| progress_percent: int = 0 |
| persona_id: str = "assistant" |
| persona_title: str = "Core Assistant" |
| persona_summary: str = "" |
| events: list[TimelineEventModel] = Field(default_factory=list) |
| checkpoints: list[CheckpointModel] = Field(default_factory=list) |
| approvals: list[ApprovalModel] = Field(default_factory=list) |
| agent_roles: list[AgentRoleModel] = Field(default_factory=list) |
| replay_cursor: int = 0 |
| resume_token: str = "" |
| failover_mode: str = "checkpoint_resume" |
|
|
|
|
| def _sanitize_for_storage(value: Any) -> Any: |
| if isinstance(value, dict): |
| sanitized: dict[str, Any] = {} |
| for key, item in value.items(): |
| if str(key).startswith("_"): |
| continue |
| sanitized[str(key)] = _sanitize_for_storage(item) |
| return sanitized |
| if isinstance(value, list): |
| return [_sanitize_for_storage(item) for item in value] |
| if isinstance(value, tuple): |
| return [_sanitize_for_storage(item) for item in value] |
| if isinstance(value, set): |
| return sorted(_sanitize_for_storage(item) for item in value) |
| return value |
|
|
|
|
| def _now_iso() -> str: |
| return datetime.now(tz=UTC).isoformat() |
|
|
|
|
| def _build_task( |
| description: str, |
| *, |
| tool: str, |
| depends_on: list[str] | None = None, |
| max_attempts: int = 2, |
| ) -> dict[str, Any]: |
| return { |
| "id": str(uuid.uuid4()), |
| "description": description, |
| "status": "pending", |
| "result": None, |
| "created_at": _now_iso(), |
| "tool": tool, |
| "depends_on": depends_on or [], |
| "attempts": 0, |
| "max_attempts": max_attempts, |
| "last_error": None, |
| } |
|
|
|
|
| def _build_agent_roles() -> list[dict[str, str]]: |
| return [{**role, "status": "ready"} for role in _AUTONOMOUS_AGENT_ROLES] |
|
|
|
|
| def _append_event( |
| session: dict[str, Any], |
| *, |
| event_type: str, |
| title: str, |
| detail: str, |
| agent_role: str, |
| level: str = "info", |
| task_id: str | None = None, |
| interruptible: bool = False, |
| ) -> None: |
| event = { |
| "id": str(uuid.uuid4()), |
| "event_type": event_type, |
| "title": title, |
| "detail": detail, |
| "agent_role": agent_role, |
| "level": level, |
| "created_at": _now_iso(), |
| "task_id": task_id, |
| "interruptible": interruptible, |
| } |
| session.setdefault("events", []).append(event) |
| session["replay_cursor"] = len(session["events"]) |
| telemetry = session.setdefault("telemetry", {}) |
| telemetry["last_event_type"] = event_type |
| telemetry["last_event_at"] = event["created_at"] |
|
|
|
|
| def _ensure_checkpoint( |
| session: dict[str, Any], *, label: str, summary: str, status: str, task_id: str | None = None |
| ) -> None: |
| checkpoint_key = (label, task_id or "") |
| existing = session.setdefault("_checkpoint_keys", set()) |
| if checkpoint_key in existing: |
| return |
| existing.add(checkpoint_key) |
| session.setdefault("checkpoints", []).append( |
| { |
| "id": str(uuid.uuid4()), |
| "label": label, |
| "status": status, |
| "summary": summary, |
| "created_at": _now_iso(), |
| "task_id": task_id, |
| } |
| ) |
| telemetry = session.setdefault("telemetry", {}) |
| telemetry["checkpoint_count"] = len(session.get("checkpoints", [])) |
|
|
|
|
| def _set_agent_role_status(session: dict[str, Any], role_id: str, status: str) -> None: |
| for role in session.get("agent_roles", []): |
| if role["id"] == role_id: |
| role["status"] = status |
| break |
|
|
|
|
| def _upsert_approval( |
| session: dict[str, Any], |
| *, |
| task_id: str | None, |
| kind: str, |
| status: str, |
| title: str, |
| summary: str, |
| resolution_note: str | None = None, |
| ) -> None: |
| approvals = session.setdefault("approvals", []) |
| for approval in approvals: |
| if approval.get("task_id") == task_id and approval.get("kind") == kind: |
| approval.update( |
| { |
| "status": status, |
| "title": title, |
| "summary": summary, |
| "resolution_note": resolution_note, |
| } |
| ) |
| return |
|
|
| approvals.append( |
| { |
| "id": str(uuid.uuid4()), |
| "kind": kind, |
| "status": status, |
| "title": title, |
| "summary": summary, |
| "created_at": _now_iso(), |
| "task_id": task_id, |
| "resolution_note": resolution_note, |
| } |
| ) |
| telemetry = session.setdefault("telemetry", {}) |
| telemetry["approval_count"] = len(approvals) |
|
|
|
|
| async def _persist_session(session_id: str, session: dict[str, Any]) -> None: |
| await session_store.save_session(session_id, _sanitize_for_storage(session)) |
|
|
|
|
| async def _record_audit(session: dict[str, Any], record_type: str, payload: dict[str, Any]) -> None: |
| session_id = str(session.get("session_id", "")).strip() |
| if not session_id: |
| return |
| await session_store.append_audit_record( |
| session_id, |
| record_type=record_type, |
| payload=_sanitize_for_storage(payload), |
| ) |
|
|
|
|
| async def _load_session(session_id: str) -> dict[str, Any]: |
| cached = _sessions.get(session_id) |
| if cached is not None: |
| return cached |
| restored = await session_store.load_session(session_id) |
| if restored is None: |
| return {} |
| restored.setdefault( |
| "_checkpoint_keys", |
| { |
| (checkpoint.get("label", ""), checkpoint.get("task_id", "") or "") |
| for checkpoint in restored.get("checkpoints", []) |
| }, |
| ) |
| _sessions[session_id] = restored |
| return restored |
|
|
|
|
| async def _run_session_until_terminal(session_id: str) -> None: |
| try: |
| while True: |
| session = await _load_session(session_id) |
| if not session or session.get("status") in {"completed", "failed"}: |
| return |
|
|
| await _advance_session(session_id) |
|
|
| session = await _load_session(session_id) |
| if not session or session.get("status") in {"completed", "failed"}: |
| return |
|
|
| await asyncio.sleep(AUTONOMOUS_BACKGROUND_LOOP_DELAY_SECONDS) |
| except asyncio.CancelledError: |
| raise |
| except Exception as exc: |
| logger.exception("Autonomous background runner neizdevās sesijai %s: %s", session_id, exc) |
| session = await _load_session(session_id) |
| if session and session.get("status") not in {"completed", "failed"}: |
| session["status"] = "failed" |
| _set_agent_role_status(session, "reviewer", "attention") |
| _append_event( |
| session, |
| event_type="session.runtime_failed", |
| title="Autonomous runtime failed", |
| detail=f"Fona izpildītājs apstājās ar kļūdu: {exc}", |
| agent_role="operator", |
| level="warning", |
| interruptible=True, |
| ) |
| _ensure_checkpoint( |
| session, |
| label="Runtime failure checkpoint", |
| summary="Sesija apstājās fona runtime kļūdas dēļ un ir atjaunojama no checkpointa.", |
| status="recoverable", |
| ) |
| await _persist_session(session_id, session) |
| finally: |
| current_task = asyncio.current_task() |
| async with _get_session_runner_lock(): |
| if current_task is not None and _session_runners.get(session_id) is current_task: |
| _session_runners.pop(session_id, None) |
|
|
|
|
| def _get_session_runner_lock() -> asyncio.Lock: |
| global _session_runner_lock |
| global _session_runner_lock_loop |
|
|
| loop = asyncio.get_running_loop() |
| if _session_runner_lock is None or _session_runner_lock_loop is not loop: |
| _session_runner_lock = asyncio.Lock() |
| _session_runner_lock_loop = loop |
| return _session_runner_lock |
|
|
|
|
| async def _ensure_session_runner(session_id: str) -> None: |
| session = await _load_session(session_id) |
| if not session or session.get("status") in {"completed", "failed"}: |
| return |
|
|
| async with _get_session_runner_lock(): |
| existing = _session_runners.get(session_id) |
| if existing is not None and not existing.done(): |
| return |
| _session_runners[session_id] = asyncio.create_task( |
| _run_session_until_terminal(session_id), |
| name=f"maris-autonomous-{session_id}", |
| ) |
|
|
|
|
| def _infer_tool(description: str) -> str: |
| lowered = description.lower() |
| if any(token in lowered for token in CODE_GENERATION_KEYWORDS): |
| return "code_generation" |
| if any(token in lowered for token in WEB_AUTOMATION_KEYWORDS): |
| return "browser_automation" |
| if any(token in lowered for token in WEB_RESEARCH_KEYWORDS): |
| return "web_research" |
| if any(token in lowered for token in VALIDATION_KEYWORDS): |
| return "validation" |
| return "reasoning" |
|
|
|
|
| def _fallback_descriptions(goal: str, max_steps: int) -> list[str]: |
| stages = [ |
| f"Izanalizēt mērķi un ierobežojumus: {goal}", |
| "Sadalīt darbu prioritārās izpildes daļās un noteikt atkarības", |
| "Izpildīt galveno risinājuma soli ar piemērotāko rīku", |
| "Pārbaudīt rezultātu, apkopot riskus un nākamos soļus", |
| ] |
| return stages[: max(1, max_steps)] |
|
|
|
|
| def _extract_step_descriptions(content: str, goal: str, max_steps: int) -> list[str]: |
| descriptions = [ |
| line.split(".", 1)[-1].strip() |
| for line in content.split("\n") |
| if line.strip() and line.lstrip()[0].isdigit() |
| ] |
| if descriptions[:max_steps]: |
| return descriptions[:max_steps] |
| return planner.describe(goal, max_steps=max_steps) |
|
|
|
|
| def _load_memory_context(session_id: str, goal: str, *, limit: int = 4) -> list[MemoryMatch]: |
| return memory_store.retrieve_relevant_context(session_id, goal, limit=limit) |
|
|
|
|
| async def _plan_tasks( |
| goal: str, |
| max_steps: int, |
| persona_id: str | None = None, |
| *, |
| memory_context: list[MemoryMatch] | None = None, |
| ) -> list[dict[str, Any]]: |
| """Izmanto LLM lai sadalītu mērķi uzdevumos.""" |
| pipe = get_pipeline() |
| persona = resolve_persona(persona_id) |
| memory_context = memory_context or [] |
| memory_overlay = "" |
| if memory_context: |
| memory_lines = "\n".join( |
| f"- ({match.role}/{match.source}) {match.content}" for match in memory_context |
| ) |
| memory_overlay = f" Saistītā sesijas atmiņa:\n{memory_lines}\n" |
|
|
| if pipe is not None: |
| try: |
| messages = [ |
| { |
| "role": "system", |
| "content": ( |
| f"{build_system_prompt('planner', persona_id=persona.id)} " |
| "Tu esi Maris AI plānotājs. Dod mērķi un sadalī to " |
| f"maksimāli {max_steps} konkrētos soļos. " |
| "Katru soli ievietojiet jaunā rindā ar numuru. " |
| "Sakārto soļus tā, lai katrs nākamais būtu atkarīgs no iepriekšējā. " |
| f"Plāno ar aktīvo personu '{persona.title}' un tās prioritātēm." |
| f"{memory_overlay}" |
| ), |
| }, |
| {"role": "user", "content": f"Mērķis: {goal}"}, |
| ] |
| out = call_generation_pipeline( |
| pipe, |
| messages, |
| max_new_tokens=512, |
| temperature=0.3, |
| ) |
| content = out[0]["generated_text"][-1]["content"] |
| descriptions = _extract_step_descriptions(content, goal, max_steps) |
| tasks: list[dict[str, Any]] = [] |
| for description in descriptions: |
| dependency_ids = [tasks[-1]["id"]] if tasks else [] |
| tasks.append( |
| _build_task( |
| description, |
| tool=_infer_tool(description), |
| depends_on=dependency_ids, |
| ) |
| ) |
| return tasks |
| except Exception as exc: |
| logger.error("Plānošanas kļūda: %s", exc) |
|
|
| planned = planner.decompose(goal, max_steps=max_steps, memory_context=memory_context) |
| if planned: |
| tasks: list[dict[str, Any]] = [] |
| id_by_step: dict[int, str] = {} |
| for index, step in enumerate(planned, start=1): |
| depends_on_steps = [ |
| id_by_step[dependency_step] |
| for dependency_step in step.get("depends_on_steps", []) |
| if dependency_step in id_by_step |
| ] |
| task = _build_task( |
| str(step["action"]), |
| tool=str(step.get("tool", _infer_tool(str(step["action"])))), |
| depends_on=depends_on_steps, |
| max_attempts=int(step.get("max_attempts", 2)), |
| ) |
| task["execution_policy"] = step.get("execution_policy", "sequential") |
| task["risk_level"] = step.get("risk_level", "medium") |
| task["approval_required"] = bool(step.get("approval_required", False)) |
| task["observability_tags"] = step.get("observability_tags", []) |
| tasks.append(task) |
| id_by_step[index] = task["id"] |
| return tasks |
|
|
| tasks: list[dict[str, Any]] = [] |
| for description in planner.describe(goal, max_steps=max_steps): |
| tasks.append( |
| _build_task( |
| description, |
| tool=_infer_tool(description), |
| depends_on=[tasks[-1]["id"]] if tasks else [], |
| ) |
| ) |
| return tasks |
|
|
|
|
| async def _execute_task( |
| task: dict[str, Any], |
| goal: str, |
| tasks: list[dict[str, Any]], |
| *, |
| persona_id: str | None = None, |
| ) -> dict[str, Any]: |
| try: |
| result = await task_executor.execute( |
| task, |
| goal, |
| tasks, |
| persona_id=persona_id, |
| session_id=str(task.get("session_id", "") or ""), |
| ) |
| except TaskExecutionError: |
| raise |
| except Exception as exc: |
| raise TaskExecutionError( |
| f"Izpilde neizdevās: {exc}", |
| failure_class="unexpected_runtime_error", |
| ) from exc |
| return { |
| "summary": result.summary, |
| "artifacts": result.artifacts, |
| "metrics": result.metrics, |
| } |
|
|
|
|
| def _refresh_session_status(session: dict[str, Any]) -> None: |
| previous_status = session.get("status") |
| tasks = session.get("tasks", []) |
| if tasks and all(task["status"] == "completed" for task in tasks): |
| session["status"] = "completed" |
| elif any(task["status"] == "failed" for task in tasks) and not any( |
| task["status"] in {"pending", "retrying", "running"} for task in tasks |
| ): |
| session["status"] = "failed" |
| else: |
| session["status"] = "running" |
|
|
| if session["status"] == previous_status: |
| return |
|
|
| if session["status"] == "completed": |
| _set_agent_role_status(session, "reviewer", "completed") |
| _append_event( |
| session, |
| event_type="session.completed", |
| title="Session replay sealed", |
| detail="Sesija ir pabeigta un replay timeline ir gatavs operatora pārskatam.", |
| agent_role="operator", |
| ) |
| _ensure_checkpoint( |
| session, |
| label="Final replay checkpoint", |
| summary="Sesiju var atjaunot no pēdējā veiksmīgā stāvokļa.", |
| status="sealed", |
| ) |
| elif session["status"] == "failed": |
| _set_agent_role_status(session, "reviewer", "attention") |
| _append_event( |
| session, |
| event_type="session.interrupt", |
| title="Operator intervention required", |
| detail="Sesija apstājās kļūdas dēļ un gaida operatora lēmumu vai resume no checkpointa.", |
| agent_role="operator", |
| level="warning", |
| interruptible=True, |
| ) |
| _ensure_checkpoint( |
| session, |
| label="Recovery checkpoint", |
| summary="Pēdējais drošais checkpoint automātiskai failover atjaunošanai.", |
| status="recoverable", |
| ) |
|
|
|
|
| def _progress_percent(tasks: list[dict[str, Any]]) -> int: |
| total = max(len(tasks), 1) |
| completed = sum(1 for task in tasks if task["status"] == "completed") |
| return int(completed / total * 100) |
|
|
|
|
| def _build_session_response(session_id: str, session: dict[str, Any]) -> SessionResponse: |
| tasks_raw = session.get("tasks", []) |
| return SessionResponse( |
| session_id=session_id, |
| goal=session.get("goal", ""), |
| status=session.get("status", "unknown"), |
| tasks=[TaskModel(**task) for task in tasks_raw], |
| progress_percent=_progress_percent(tasks_raw), |
| persona_id=str(session.get("persona_id", "assistant")), |
| persona_title=str(session.get("persona_title", "Core Assistant")), |
| persona_summary=str(session.get("persona_summary", "")), |
| events=[TimelineEventModel(**event) for event in session.get("events", [])], |
| checkpoints=[ |
| CheckpointModel(**checkpoint) for checkpoint in session.get("checkpoints", []) |
| ], |
| approvals=[ApprovalModel(**approval) for approval in session.get("approvals", [])], |
| agent_roles=[AgentRoleModel(**role) for role in session.get("agent_roles", [])], |
| replay_cursor=int(session.get("replay_cursor", len(session.get("events", [])))), |
| resume_token=str(session.get("resume_token", f"resume:{session_id}")), |
| failover_mode=str(session.get("failover_mode", "checkpoint_resume")), |
| ) |
|
|
|
|
| async def _advance_session(session_id: str) -> None: |
| session = await _load_session(session_id) |
| if not session or session.get("status") in {"completed", "failed"}: |
| return |
|
|
| tasks = session["tasks"] |
| completed_ids = {task["id"] for task in tasks if task["status"] == "completed"} |
| ready_task = next( |
| ( |
| task |
| for task in tasks |
| if task["status"] in {"pending", "retrying"} |
| and all(dep in completed_ids for dep in task["depends_on"]) |
| ), |
| None, |
| ) |
|
|
| if ready_task is None: |
| _refresh_session_status(session) |
| return |
|
|
| _set_agent_role_status(session, "executor", "running") |
| if ready_task["tool"] in {"browser_automation", "validation", "code_generation"}: |
| _upsert_approval( |
| session, |
| task_id=ready_task["id"], |
| kind="operator_review", |
| status="pending_review", |
| title="Human-in-the-loop gate", |
| summary=f"Uzdevums '{ready_task['description']}' izmanto rīku {ready_task['tool']} un tiek izsekots operatora panelī.", |
| ) |
| _append_event( |
| session, |
| event_type="approval.requested", |
| title="Approval queued", |
| detail=f"Reviewer atzīmēja uzdevumu '{ready_task['description']}' kā operatoram redzamu darbību.", |
| agent_role="reviewer", |
| level="warning", |
| task_id=ready_task["id"], |
| interruptible=True, |
| ) |
|
|
| ready_task["status"] = "running" |
| ready_task["attempts"] += 1 |
| ready_task["session_id"] = session_id |
| _append_event( |
| session, |
| event_type="task.started", |
| title="Task started", |
| detail=f"Executor sāka '{ready_task['description']}' ar rīku {ready_task['tool']}.", |
| agent_role="executor", |
| task_id=ready_task["id"], |
| interruptible=True, |
| ) |
| await _record_audit(session, "task.started", ready_task) |
| await _persist_session(session_id, session) |
| try: |
| execution = await _execute_task( |
| ready_task, |
| session["goal"], |
| tasks, |
| persona_id=session.get("persona_id"), |
| ) |
| ready_task["result"] = execution["summary"] |
| ready_task["artifacts"] = execution.get("artifacts", {}) |
| ready_task["metrics"] = execution.get("metrics", {}) |
| ready_task["status"] = "completed" |
| ready_task["last_error"] = None |
| telemetry = session.setdefault("telemetry", {}) |
| telemetry["completed_tasks"] = telemetry.get("completed_tasks", 0) + 1 |
| telemetry["last_completed_task_id"] = ready_task["id"] |
| _append_event( |
| session, |
| event_type="task.completed", |
| title="Task completed", |
| detail=ready_task["result"] or "Uzdevums pabeigts.", |
| agent_role="executor", |
| task_id=ready_task["id"], |
| ) |
| _append_event( |
| session, |
| event_type="reviewer.summary", |
| title="Reviewer checkpointed result", |
| detail=f"Reviewer apstiprināja uzdevuma '{ready_task['description']}' rezultātu replay timeline.", |
| agent_role="reviewer", |
| task_id=ready_task["id"], |
| ) |
| await _record_audit( |
| session, |
| "task.completed", |
| { |
| "task_id": ready_task["id"], |
| "result": ready_task["result"], |
| "artifacts": ready_task.get("artifacts", {}), |
| "metrics": ready_task.get("metrics", {}), |
| }, |
| ) |
| _ensure_checkpoint( |
| session, |
| label=f"Checkpoint after {ready_task['description']}", |
| summary="Drošs stāvoklis ar pilnu task graph un timeline replay metadatiem.", |
| status="ready", |
| task_id=ready_task["id"], |
| ) |
| if ready_task["tool"] in {"browser_automation", "validation", "code_generation"}: |
| _upsert_approval( |
| session, |
| task_id=ready_task["id"], |
| kind="operator_review", |
| status="auto_approved", |
| title="Human-in-the-loop gate", |
| summary=f"Uzdevums '{ready_task['description']}' tika izpildīts bez blokējošas iejaukšanās.", |
| resolution_note="Auto-approved for this local runtime; production should require explicit operator action.", |
| ) |
| except TaskExecutionError as exc: |
| ready_task["last_error"] = str(exc) |
| ready_task["result"] = f"Mēģinājums {ready_task['attempts']} neizdevās: {exc}" |
| ready_task["failure_class"] = exc.failure_class |
| ready_task.setdefault("metrics", {})["failure_class"] = exc.failure_class |
| session.setdefault("telemetry", {}).setdefault("failure_classes", []).append( |
| exc.failure_class |
| ) |
| _append_event( |
| session, |
| event_type="task.failed_attempt", |
| title="Task attempt failed", |
| detail=ready_task["result"], |
| agent_role="executor", |
| level="warning", |
| task_id=ready_task["id"], |
| interruptible=True, |
| ) |
| await _record_audit( |
| session, |
| "task.failed_attempt", |
| { |
| "task_id": ready_task["id"], |
| "failure_class": exc.failure_class, |
| "retryable": exc.retryable, |
| "error": str(exc), |
| }, |
| ) |
| _set_agent_role_status(session, "reviewer", "attention") |
| if exc.retryable and ready_task["attempts"] < ready_task["max_attempts"]: |
| ready_task["status"] = "retrying" |
| _append_event( |
| session, |
| event_type="task.retrying", |
| title="Retry scheduled", |
| detail=f"Reviewer piešķīra atkārtotu mēģinājumu uzdevumam '{ready_task['description']}'.", |
| agent_role="reviewer", |
| level="warning", |
| task_id=ready_task["id"], |
| ) |
| _ensure_checkpoint( |
| session, |
| label=f"Retry checkpoint for {ready_task['description']}", |
| summary="Saglabāts stāvoklis pirms nākamā mēģinājuma.", |
| status="retry_pending", |
| task_id=ready_task["id"], |
| ) |
| else: |
| ready_task["status"] = "failed" |
| _upsert_approval( |
| session, |
| task_id=ready_task["id"], |
| kind="operator_review", |
| status="needs_intervention", |
| title="Operator intervention required", |
| summary=f"Uzdevums '{ready_task['description']}' izsmēla mēģinājumus un gaida resume no checkpointa.", |
| resolution_note=str(exc), |
| ) |
| except Exception as exc: |
| ready_task["last_error"] = str(exc) |
| ready_task["result"] = f"Mēģinājums {ready_task['attempts']} neizdevās: {exc}" |
| _append_event( |
| session, |
| event_type="task.failed_attempt", |
| title="Task attempt failed", |
| detail=ready_task["result"], |
| agent_role="executor", |
| level="warning", |
| task_id=ready_task["id"], |
| interruptible=True, |
| ) |
| _set_agent_role_status(session, "reviewer", "attention") |
| if ready_task["attempts"] < ready_task["max_attempts"]: |
| ready_task["status"] = "retrying" |
| _append_event( |
| session, |
| event_type="task.retrying", |
| title="Retry scheduled", |
| detail=f"Reviewer piešķīra atkārtotu mēģinājumu uzdevumam '{ready_task['description']}'.", |
| agent_role="reviewer", |
| level="warning", |
| task_id=ready_task["id"], |
| ) |
| _ensure_checkpoint( |
| session, |
| label=f"Retry checkpoint for {ready_task['description']}", |
| summary="Saglabāts stāvoklis pirms nākamā mēģinājuma.", |
| status="retry_pending", |
| task_id=ready_task["id"], |
| ) |
| else: |
| ready_task["status"] = "failed" |
| _upsert_approval( |
| session, |
| task_id=ready_task["id"], |
| kind="operator_review", |
| status="needs_intervention", |
| title="Operator intervention required", |
| summary=f"Uzdevums '{ready_task['description']}' izsmēla mēģinājumus un gaida resume no checkpointa.", |
| resolution_note=str(exc), |
| ) |
|
|
| _refresh_session_status(session) |
| await _persist_session(session_id, session) |
| if session["status"] == "running": |
| _set_agent_role_status(session, "planner", "completed") |
| _set_agent_role_status(session, "executor", "ready") |
| if all( |
| approval["status"] != "needs_intervention" for approval in session.get("approvals", []) |
| ): |
| _set_agent_role_status(session, "reviewer", "ready") |
|
|
|
|
| @router.post("/start", response_model=SessionResponse) |
| async def start_session(req: StartRequest) -> SessionResponse: |
| """Sāk autonomo sesiju.""" |
| persona = resolve_persona(req.persona_id) |
| memory_context = _load_memory_context(req.session_id, req.goal) |
| tasks_raw = await _plan_tasks( |
| req.goal, |
| req.max_steps, |
| req.persona_id, |
| memory_context=memory_context, |
| ) |
| created_at = _now_iso() |
| for task in tasks_raw: |
| task["session_id"] = req.session_id |
|
|
| _sessions[req.session_id] = { |
| "session_id": req.session_id, |
| "goal": req.goal, |
| "status": "running", |
| "created_at": created_at, |
| "tasks": tasks_raw, |
| "persona_id": persona.id, |
| "persona_title": persona.title, |
| "persona_summary": persona.summary, |
| "events": [], |
| "checkpoints": [], |
| "approvals": [], |
| "agent_roles": _build_agent_roles(), |
| "replay_cursor": 0, |
| "resume_token": f"resume:{req.session_id}", |
| "failover_mode": "checkpoint_resume", |
| "telemetry": { |
| "planned_task_count": len(tasks_raw), |
| "completed_tasks": 0, |
| "failure_classes": [], |
| }, |
| } |
| session = _sessions[req.session_id] |
| _append_event( |
| session, |
| event_type="session.started", |
| title="Session created", |
| detail=f"Planner saņēma mērķi '{req.goal}' un sāka veidot task graph.", |
| agent_role="planner", |
| ) |
| _append_event( |
| session, |
| event_type="task_graph.ready", |
| title="Task graph published", |
| detail=f"Plānā ir {len(tasks_raw)} soļi ar secīgām atkarībām un replay cursor atbalstu.", |
| agent_role="planner", |
| ) |
| if memory_context: |
| _append_event( |
| session, |
| event_type="memory.context_loaded", |
| title="Session context restored", |
| detail=f"Planner ielādēja {len(memory_context)} saistītus sesijas atmiņas ierakstus pirms plānošanas.", |
| agent_role="planner", |
| ) |
| _ensure_checkpoint( |
| session, |
| label="Planning checkpoint", |
| summary="Task graph ir publicēts un sesiju var atsākt no plānošanas posma.", |
| status="ready", |
| ) |
| memory_store.remember_message(req.session_id, "user", req.goal, source="autonomous_goal") |
| await _record_audit( |
| session, |
| "session.started", |
| { |
| "goal": req.goal, |
| "persona_id": persona.id, |
| "planned_task_count": len(tasks_raw), |
| }, |
| ) |
| await _persist_session(req.session_id, session) |
| await _advance_session(req.session_id) |
| await _ensure_session_runner(req.session_id) |
|
|
| return _build_session_response(req.session_id, session) |
|
|
|
|
| @router.post("/status", response_model=SessionResponse) |
| async def get_status(req: StatusRequest) -> SessionResponse: |
| """Atgriež sesijas statusu.""" |
| session = await _load_session(req.session_id) |
| if session: |
| await _ensure_session_runner(req.session_id) |
| return _build_session_response(req.session_id, session) |
|
|