diff --git a/docs/superpowers/plans/2026-05-09-agentforge.md b/docs/superpowers/plans/2026-05-09-agentforge.md index 3137e10..ac731f1 100644 --- a/docs/superpowers/plans/2026-05-09-agentforge.md +++ b/docs/superpowers/plans/2026-05-09-agentforge.md @@ -2728,6 +2728,13 @@ git commit -m "feat(policies): añade política default v1 con guardrails comple ## Phase E — LangGraph Runtime (Tasks 17–20) +> **Nota de implementación (desviación del plan):** los nodos del grafo son `async def`, así que se invoca con `await graph.ainvoke(...)`, que el `SqliteSaver` síncrono no soporta. El checkpointing se cambió a **`AsyncSqliteSaver`** expuesto como context manager asíncrono (`async with build_checkpointer(data_dir) as cp: ...`). Consecuencias: +> - `core/requirements.txt` fija `aiosqlite>=0.20,<0.21` (la 0.21 eliminó `Connection.is_alive()`, usado por `langgraph-checkpoint-sqlite` 2.x). +> - `AgentOrchestrator` no guarda el checkpointer en `__init__`; abre uno por llamada sobre `self._data_dir`. El estado igual persiste en `data_dir/checkpoints.sqlite` y un `awaiting_approval` sobrevive a un reinicio. +> - Se añadió `tests/unit/test_runtime_orchestrator.py` (5 tests; el plan no incluía tests del orchestrator). +> - Los bloques de código de Tasks 18-20 abajo son la versión original del plan; la **versión autoritativa es la committeada**. Fases siguientes que usen `build_checkpointer` deben usar el patrón `async with`. + + ### Task 17: AgentState y nodos de validación **Files:** @@ -2737,7 +2744,7 @@ git commit -m "feat(policies): añade política default v1 con guardrails comple LangGraph define el estado como `TypedDict`. El reducer `operator.add` sobre `decision_path` permite que cada nodo acumule pasos sin pisar lo previo. -- [ ] **Step 1: Test** +- [x] **Step 1: Test** ```python """Tests de los nodos del grafo (lógica pura, sin compilación de grafo).""" @@ -2818,7 +2825,7 @@ async def test_validate_input_pasa_sin_pii( assert out["status"] == "running" ``` -- [ ] **Step 2: Implementar `state.py`** +- [x] **Step 2: Implementar `state.py`** ```python """Estado tipado del grafo LangGraph.""" @@ -2844,7 +2851,7 @@ class AgentState(TypedDict, total=False): human_decision: dict | None ``` -- [ ] **Step 3: Implementar `nodes.py`** +- [x] **Step 3: Implementar `nodes.py`** ```python """Factory de nodos LangGraph parametrizados por engine, policy, llm, agent_def.""" @@ -3038,7 +3045,7 @@ def build_node_finalize() -> NodeFn: return finalize ``` -- [ ] **Step 4: Verificar y commit** +- [x] **Step 4: Verificar y commit** ```bash pytest tests/unit/test_runtime_nodes.py -v @@ -3053,35 +3060,39 @@ git commit -m "feat(runtime): añade AgentState y nodos LangGraph (validate, llm **Files:** - Create: `core/src/agentforge_core/runtime/checkpointer.py` -- [ ] **Step 1: Implementar** +- [x] **Step 1: Implementar** ```python -"""Wrapper sobre SqliteSaver de LangGraph. Centraliza el path y el ciclo de vida.""" +"""Wrapper sobre AsyncSqliteSaver de LangGraph. Centraliza el path y el ciclo de vida.""" from __future__ import annotations -import sqlite3 +from contextlib import AbstractAsyncContextManager from pathlib import Path -from langgraph.checkpoint.sqlite import SqliteSaver +from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver -def build_checkpointer(data_dir: Path) -> SqliteSaver: - """Devuelve un SqliteSaver apuntando a data_dir/checkpoints.sqlite. - - La base se crea si no existe. La conexión usa `check_same_thread=False` - porque LangGraph ejecuta nodos en hilos del executor. - """ +def checkpointer_path(data_dir: Path) -> Path: + """Ruta al fichero SQLite de checkpoints, creando data_dir si no existe.""" data_dir.mkdir(parents=True, exist_ok=True) - db_path = data_dir / "checkpoints.sqlite" - conn = sqlite3.connect(str(db_path), check_same_thread=False) - return SqliteSaver(conn) + return data_dir / "checkpoints.sqlite" + + +def build_checkpointer(data_dir: Path) -> AbstractAsyncContextManager[AsyncSqliteSaver]: + """Context manager async con un AsyncSqliteSaver sobre data_dir/checkpoints.sqlite. + + Uso: ``async with build_checkpointer(dir) as cp: ...``. La conexión aiosqlite se + abre al entrar y se cierra al salir; LangGraph crea las tablas (setup()) de forma + perezosa. El estado persiste, así que un awaiting_approval sobrevive a un reinicio. + """ + return AsyncSqliteSaver.from_conn_string(str(checkpointer_path(data_dir))) ``` -- [ ] **Step 2: Commit** +- [x] **Step 2: Commit** ```bash git add core/src/agentforge_core/runtime/checkpointer.py -git commit -m "feat(runtime): añade build_checkpointer wrapper sobre SqliteSaver" +# (committeado como) git commit -m "fix(runtime): build_checkpointer asíncrono con AsyncSqliteSaver" ``` --- @@ -3092,7 +3103,7 @@ git commit -m "feat(runtime): añade build_checkpointer wrapper sobre SqliteSave - Create: `core/src/agentforge_core/runtime/graph.py` - Create: `tests/unit/test_runtime_graph.py` -- [ ] **Step 1: Test (lógica de routing post-validate_input)** +- [x] **Step 1: Test (lógica de routing post-validate_input)** ```python """Tests del grafo compilado: happy path con MockProvider y guardrails básicos.""" @@ -3196,7 +3207,7 @@ async def test_grafo_pausa_en_hitl_si_riesgo_alto(tmp_path: Path) -> None: assert state.next # tiene siguiente paso pendiente (interrupted) ``` -- [ ] **Step 2: Implementar `graph.py`** +- [x] **Step 2: Implementar `graph.py`** ```python """Compilación del grafo LangGraph para un AgentDefinition concreto.""" @@ -3266,7 +3277,7 @@ def build_graph( return g.compile(checkpointer=checkpointer) ``` -- [ ] **Step 3: Verificar y commit** +- [x] **Step 3: Verificar y commit** ```bash pytest tests/unit/test_runtime_graph.py -v @@ -3284,7 +3295,7 @@ git commit -m "feat(runtime): añade build_graph (LangGraph StateGraph + interru El orchestrator encapsula el ciclo de vida (build graph → invoke → leer estado → serializar a `AgentExecution`). El router de FastAPI lo llamará. -- [ ] **Step 1: Implementar** +- [x] **Step 1: Implementar** ```python """Orchestrator que envuelve build_graph + ainvoke + serialización a AgentExecution.""" @@ -3432,7 +3443,7 @@ class AgentOrchestrator: ) ``` -- [ ] **Step 2: Commit** +- [x] **Step 2: Commit** ```bash git add core/src/agentforge_core/runtime/orchestrator.py core/src/agentforge_core/runtime/__init__.py