File size: 5,297 Bytes
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8835ca1
28a08e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
"""
backend/api/agent_checkpoint.py β€” Simplified checkpoint endpoints (ARCH-K2.3)

Aggiunge alias /api/agent/checkpoint (senza task_id nella path) per uso diretto dal frontend:
  GET    /api/agent/checkpoint             β€” lista tutti i checkpoint attivi in memoria
  POST   /api/agent/checkpoint             β€” salva checkpoint (taskId opzionale nel body)
  GET    /api/agent/checkpoint/{task_id}   β€” recupera checkpoint specifico
  DELETE /api/agent/checkpoint/{task_id}   β€” elimina checkpoint

I checkpoint per-task esistono giΓ  su /api/agent/tasks/{id}/checkpoint (agent.py).
Questi alias sono piΓΉ comodi quando il frontend non ha un task_id esplicito
(es. salvataggio periodico dello stato dell'agente, resume dopo refresh).

ROUTING CF PAGES: /api/agent/* β†’ HANDS (Space B) via HANDS_PATTERNS[0].
Nessuna modifica a [[catchall]].ts necessaria.

NOTA: Import da api.agent e api.persistence sono LAZY (dentro le funzioni)
per evitare import circolari β€” agent.py importa giΓ  molti altri moduli.
"""
import time
import asyncio
import logging
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

from .auth_guard import require_role, AuthRole

_logger = logging.getLogger("api.agent_checkpoint")

router = APIRouter(dependencies=[Depends(require_role(AuthRole.MACHINE))])


class CheckpointBody(BaseModel):
    taskId:     Optional[str]  = None   # se omesso β†’ usa "default"
    step:       int            = 0
    goal:       str            = ""
    plan:       list           = []
    logs:       list[str]      = []
    artifacts:  list[str]      = []
    retryCount: int            = 0
    extra:      dict           = {}


# ── GET /api/agent/checkpoint ─────────────────────────────────────────────────
@router.get("/api/agent/checkpoint")
async def list_checkpoints_alias():
    """
    Lista tutti i checkpoint attivi in memoria.
    Alias leggero per /api/agent/checkpoints (agent.py).
    """
    # Import lazy β€” evita circolaritΓ 
    from api.agent import _task_checkpoints, _prune_checkpoints  # type: ignore[import]

    _prune_checkpoints()
    now = int(time.time() * 1000)
    return {
        "count": len(_task_checkpoints),
        "checkpoints": [
            {
                "taskId": k,
                "step":   v.get("step", 0),
                "goal":   v.get("goal", "")[:300],
                "age_ms": now - v.get("savedAt", now),
            }
            for k, v in _task_checkpoints.items()
        ],
    }


# ── POST /api/agent/checkpoint ────────────────────────────────────────────────
@router.post("/api/agent/checkpoint")
async def save_checkpoint_alias(body: CheckpointBody):
    """
    Salva un checkpoint. taskId opzionale: se omesso usa 'default'.
    Replica la logica di /api/agent/tasks/{id}/checkpoint con Supabase persist.
    """
    from api.agent import _task_checkpoints, _prune_checkpoints  # type: ignore[import]
    from api.persistence import sb_save_checkpoint               # type: ignore[import]

    _prune_checkpoints()
    task_id = body.taskId or "default"

    cp: dict = {
        "taskId":     task_id,
        "step":       body.step,
        "goal":       body.goal,
        "plan":       body.plan,
        "logs":       body.logs[-50:],   # mantieni solo gli ultimi 50 log
        "artifacts":  body.artifacts,
        "retryCount": body.retryCount,
        "extra":      body.extra,
        "savedAt":    int(time.time() * 1000),
    }
    _task_checkpoints[task_id] = cp
    # Persist su Supabase β€” fire-and-forget (stesso pattern di agent.py)
    asyncio.create_task(sb_save_checkpoint(task_id, body.step, cp))
    return {"saved": True, "taskId": task_id, "step": body.step}


# ── GET /api/agent/checkpoint/{task_id} ──────────────────────────────────────
@router.get("/api/agent/checkpoint/{task_id}")
async def get_checkpoint_alias(task_id: str):
    """
    Recupera il checkpoint per un task specifico.
    Cerca prima in memoria (_task_checkpoints), poi su Supabase via sb_get_checkpoint.
    """
    from api.agent import _task_checkpoints, _prune_checkpoints  # type: ignore[import]
    from api.persistence import sb_get_checkpoint                # type: ignore[import]

    _prune_checkpoints()
    cp = _task_checkpoints.get(task_id)
    if not cp:
        cp = await sb_get_checkpoint(task_id)
    if not cp:
        raise HTTPException(
            status_code=404,
            detail={"error": "checkpoint_not_found", "taskId": task_id},
        )
    return cp


# ── DELETE /api/agent/checkpoint/{task_id} ───────────────────────────────────
@router.delete("/api/agent/checkpoint/{task_id}")
async def delete_checkpoint_alias(task_id: str):
    """Rimuove il checkpoint da memoria in-process (non elimina da Supabase)."""
    from api.agent import _task_checkpoints  # type: ignore[import]

    _task_checkpoints.pop(task_id, None)
    return {"deleted": task_id}