Lesson 05 — Cognee + autonomía acotada
Si algo está rojo: - `cognee status` → [`setup.md#4-cognee-elegí-una-ruta`](../setup.md#4-cognee-elegí-una-ruta) - Directorios o embeddings mal seteados → [`setup.md#bugs-de-integración-con-cognee--ya-resueltos-en-la-rig`](../setup.md#bugs-de-integración-con-cognee--ya-resueltos-en-la-rig)
Starter prompt
Pegalo en tu coding agent (Claude Code, Cursor, Codex…) o usá
academy lesson prompt 5 --copy.
Rol: co-piloto Python, Lesson 05 (última).
Contexto: leé .skills/query_cognee/SKILL.md (lo escribió el sub-agent en Lesson 04),
src/agent.py, y https://docs.cognee.ai/reference/api-reference (SDK basics).
Tarea (partida en 3):
PARTE A — .skills/query_cognee/tool.py
1) async def ingest(paths: list[Path]) -> None:
- Lee cada path como texto
- Llama await cognee.add(text, dataset_name="acme")
- Al final: await cognee.cognify()
- Emite receipt event="cognee_indexed" con {files: N, dataset: "acme"}
2) async def query(question: str) -> dict[str, Any]:
- Llama await cognee.search(query_text=question, query_type=SearchType.INSIGHTS)
- Devuelve {"answer": str, "source": str, "confidence": float, "raw": [...]}
- Si no hay resultados: confidence=0.0 y source="none"
PARTE B — src/lifecycle.py
Implementá DeepAgentLifecycle con python-statemachine:
Estados: raw (initial), learning, answering, refining, escalated
Transiciones:
detect_gap: raw → learning | answering → learning
skill_ready: learning → answering
verify_ok: answering → answering (loop)
verify_fail: answering → refining
retry: refining → answering
give_up: refining → escalated | learning → escalated
En cada on_*_transition() escribí receipt en .receipts/lifecycle.jsonl
con event="transition" y payload={from, to, trigger}.
PARTE C — actualizá src/agent.py + src/cli.py
- Al arrancar CLI: instanciá DeepAgentLifecycle.
- Antes del primer invoke: si no hay skills → detect_gap() → learning.
- Después de que skill_author escribió el skill → skill_ready() → answering.
- Registrá ingest y query como tools reales (langchain @tool) que el agent
principal puede invocar.
- Después de invoke: si la respuesta tiene confidence < 0.5 → verify_fail() → refining
y hacé UN retry con la misma pregunta reformulada.
- Antes de llamar ingest (o cognify) por primera vez: interrupt() del harness
para pedir OK humano (o simplemente rprint una confirmación y seguir en modo taller).
Corré:
uv run python -m src.cli "¿cuánto facturó Café del Valle en Q3?"
uv run python -m src.cli "¿qué cliente creció más entre Q2 y Q3?"
Pegame:
- las 2 respuestas
- tail -n 20 .receipts/lifecycle.jsonl
Reglas: mypy --strict verde. check.sh verde. Cognee es async: usá asyncio.run o
async typer commands.
Duración: 14 min · Prereq: Lesson 04 completa. .skills/query_cognee/SKILL.md existe. Neo4j alive.
Pre-flight
academy cognee status # ✅ Neo4j alive (docker local o remoto)
academy status # signals 01-04 en ✅
test -d .cognee_system/databases && \ # ✅ dir donde cognee escribe SQLite
echo "cognee dirs OK" || \
(mkdir -p .cognee_system/databases .data && echo "creé cognee dirs")
grep -q "^EMBEDDING_PROVIDER=openai" .env && \ # ✅ embeddings vía OpenRouter
echo "embeddings OK" || \
echo "⚠️ revisá .env sección Cognee — ver setup.md"
Si algo está rojo:
- cognee status → setup.md#4-cognee-elegí-una-ruta
- Directorios o embeddings mal seteados → setup.md#bugs-de-integración-con-cognee--ya-resueltos-en-la-rig
Challenge
Cerrar el loop. El agente:
- Ingiere
data/acme/*.mden un grafo Cognee (backend: Neo4j). - Transiciona explícitamente entre estados (
python-statemachine). - Responde con evidencia (fuente + valor).
- Deja un
.receipts/lifecycle.jsonlque cuenta la historia.
La state machine que vamos a implementar
stateDiagram-v2
[*] --> raw
raw --> learning: detect_gap
learning --> answering: skill_ready
answering --> answering: verify_ok
answering --> refining: verify_fail
refining --> answering: retry
answering --> learning: detect_gap
refining --> escalated: give_up
learning --> escalated: give_up
escalated --> [*]
note right of raw
Sin skills.
Cualquier pregunta
dispara detect_gap.
end note
note left of escalated
Stop reason visible.
Requiere input humano.
end note
Cada transición emite un evento en .receipts/lifecycle.jsonl. Al final del taller, ese JSONL cuenta la historia de cómo tu agente pasó de crudo a útil.
Producto:
- .skills/query_cognee/tool.py — implementación del skill.
- src/lifecycle.py — DeepAgentLifecycle state machine.
- CLI actualizado que consulta la state machine antes de cada acción y emite eventos.
Manual path
Referencia — .skills/query_cognee/tool.py
"""Query the local Cognee graph.
⚠️ Ver `setup.md` sección "Bugs de integración con Cognee" para el contexto de
los mappings de env vars y del override post-import. Los patrones no son
opcionales — sin ellos falla con LLMAPIKeyNotSetError o 404 de OpenRouter.
"""
from __future__ import annotations
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
# ─── Env var mapping (bug 2 en setup.md) ──────────────────────────
# cognee 1.4 lee LLM_* / EMBEDDING_* SIN el prefijo COGNEE_.
# Nuestro .env usa COGNEE_LLM_* por claridad; mapeamos acá al importar.
_COGNEE_ENV_VARS = (
"LLM_API_KEY", "LLM_MODEL", "LLM_PROVIDER", "LLM_ENDPOINT",
"EMBEDDING_API_KEY", "EMBEDDING_MODEL", "EMBEDDING_PROVIDER",
"EMBEDDING_ENDPOINT", "EMBEDDING_DIMENSIONS",
)
def _map_cognee_env() -> None:
"""Copia COGNEE_<VAR> → <VAR> para que cognee 1.4 las encuentre."""
for var in _COGNEE_ENV_VARS:
val = os.environ.get(f"COGNEE_{var}") or os.environ.get(var)
if val:
os.environ[var] = val
_map_cognee_env() # antes de import cognee
import cognee # noqa: E402 ← import después del mapping intencional
from cognee.api.v1.search import SearchType # noqa: E402
_map_cognee_env() # DESPUÉS también: cognee re-carga .env con override=True
from src.agent import write_receipt # noqa: E402
async def ingest(paths: list[Path]) -> None:
for p in paths:
await cognee.add(p.read_text(encoding="utf-8"), dataset_name="acme")
await cognee.cognify(datasets=["acme"])
write_receipt("cognee_indexed", {"files": len(paths), "dataset": "acme"})
async def query(question: str) -> dict[str, Any]:
results = await cognee.search(
query_text=question,
query_type=SearchType.INSIGHTS,
datasets=["acme"],
)
if not results:
return {"answer": "no data", "source": "none", "confidence": 0.0, "raw": []}
# Cognee INSIGHTS devuelve una lista de tuplas/dicts según versión.
first = results[0]
return {
"answer": str(first),
"source": "acme dataset",
"confidence": 0.8,
"raw": results[:3],
}
Referencia — src/lifecycle.py
"""DeepAgent lifecycle — explicit states over hidden booleans."""
from __future__ import annotations
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from statemachine import State, StateMachine
from src.schemas import Receipt
class DeepAgentLifecycle(StateMachine):
raw = State(initial=True)
learning = State()
answering = State()
refining = State()
escalated = State(final=True)
detect_gap = raw.to(learning) | answering.to(learning)
skill_ready = learning.to(answering)
verify_ok = answering.to(answering)
verify_fail = answering.to(refining)
retry = refining.to(answering)
give_up = refining.to(escalated) | learning.to(escalated)
def on_transition(self, event: str, source: State, target: State) -> None:
_write(event=event, payload={"from": source.id, "to": target.id})
def _write(event: str, payload: dict[str, Any]) -> None:
receipts_dir = Path(os.environ.get("RECEIPTS_DIR", ".receipts"))
receipts_dir.mkdir(exist_ok=True)
path = receipts_dir / "lifecycle.jsonl"
receipt = Receipt(
ts=datetime.now(tz=timezone.utc),
actor="system",
event="transition",
payload={"trigger": event, **payload},
)
with path.open("a", encoding="utf-8") as f:
f.write(receipt.model_dump_json() + "\n")
Referencia — parche a src/cli.py
# nuevo comando async
import asyncio
from pathlib import Path
from src.lifecycle import DeepAgentLifecycle
from src.skill_registry import discover_skills
@app.command()
def ask(question: str) -> None:
asyncio.run(_ask_async(question))
async def _ask_async(question: str) -> None:
lc = DeepAgentLifecycle()
skills = discover_skills(Path(".skills"))
if not any(s.name == "query_cognee" for s in skills):
lc.detect_gap() # raw → learning
# ... invocar skill_author como antes ...
lc.skill_ready() # learning → answering
else:
lc.detect_gap()
lc.skill_ready()
# Ingesta idempotente (Cognee ignora duplicados con el mismo hash)
from importlib import import_module
tool = import_module("skills.query_cognee.tool") # o ruta según cómo importes
await tool.ingest(list(Path("data/acme").glob("*.md")))
result = await tool.query(question)
rprint("[bold cyan]Answer:[/bold cyan]", result["answer"])
rprint(f"[dim]source={result['source']} confidence={result['confidence']}[/dim]")
if result["confidence"] < 0.5:
lc.verify_fail()
# un retry con la pregunta reformulada
result2 = await tool.query(f"summarize what you know about: {question}")
lc.retry()
rprint("[bold cyan]Retry answer:[/bold cyan]", result2["answer"])
else:
lc.verify_ok()
Observe
uv run python -m src.cli "¿cuánto facturó Café del Valle en Q3?"
Esperado:
Answer: Café del Valle S.A.S. facturó 58.300.000 COP en Q3 (fuente: invoices-q3.md)
source=acme dataset confidence=0.8
Y tail .receipts/lifecycle.jsonl:
{"ts":"…","actor":"system","event":"transition","payload":{"trigger":"detect_gap","from":"raw","to":"learning"}}
{"ts":"…","actor":"system","event":"transition","payload":{"trigger":"skill_ready","from":"learning","to":"answering"}}
{"ts":"…","actor":"system","event":"transition","payload":{"trigger":"verify_ok","from":"answering","to":"answering"}}
Discuss · ¿por qué state machine y no if/else?
Pregunta para la sala: ¿qué pasa si sacás python-statemachine y reemplazás las transiciones por if/else en el CLI?
Respuesta corta: el agente puede quedar en loop infinito de refinamiento (nunca converge), o saltar de raw a answering sin haber pasado por learning (sin skill escrito).
Con un if/else disperso, esas transiciones inválidas están implícitas — nadie las revisó, nadie las prohibió. Con la state machine son explícitas: la transición raw → answering no existe, entonces el agente no puede tomarla. El "stop reason" es visible en .receipts/lifecycle.jsonl — auditable, no oculto en booleanos.
Segundo debate — Cognee como memoria compartida: dos agentes distintos apuntando al mismo Cognee comparten conocimiento. Un agente ingiere data/acme/ una vez, otro pregunta días después y ya tiene contexto. Es la vía natural para escalar de un agente solo a una flota coordinada.
¿Qué implicaciones tiene eso para governance? (Ejemplo: ¿qué pasa si un agente indexa mal y contamina el grafo para todos los demás? ¿Necesitás namespaces por tenant? ¿Retention policies?)
Wrap-up (a discusión abierta)
Cosas que no hicimos por scope de 90 min pero valen la pena en casa:
- Bounded gardener: que el agente proponga nuevas preguntas para el dataset y las escriba en
data/questions.jsonlcon stop en duplicados (guardrail: si genera dos veces la misma pregunta, se detiene). - Specialist review: sub-agent
data_reviewerque valida la respuesta contra las facturas antes de que el principal la devuelva. - Supervisor runtime:
python-statemachinecon actores anidados (issue supervisor → per-question actor → dispatch actor). - TUI 2-paneles con
textual— un panel para chatear con el agente y otro para ver el receipt stream en vivo. Elimina el ida-y-vuelta a la terminal.
Repo con todo esto follow-up: https://github.com/centinela-io/ctl-academy
🚀 Fastrack · saltear al checkpoint (⚠️ hace cognify + query real, ~USD 0.30, ~10 min)
Requisito previo: academy cognee status debe estar verde (Neo4j alive local o remoto).
Cinco pasos:
- Copiá
Referencia — .skills/query_cognee/tool.pya.skills/query_cognee/tool.py. - Copiá
Referencia — src/lifecycle.pyasrc/lifecycle.py. - Copiá
Referencia — parche a src/cli.pyasrc/cli.py. - Corré
academy check— verde. - Corré las 2 preguntas del listado:
bash uv run python -m src.cli ask "¿qué cliente creció más entre Q2 y Q3?" uv run python -m src.cli ask "¿top-3 clientes por facturación acumulada?"Primera invocación indexa Cognee (~8 min). Segunda es rápida. - Verificá
.receipts/lifecycle.jsonly corréacademy validate 5.
Prompt one-shot:
Rol: implementador rápido, Lesson 05 (última).
Leé workshop/deepagents-memory-skills/lessons/05-cognee-autonomy.md, aplicá
todas las "Referencia" secciones exactamente. Corré ./scripts/check.sh. Verde.
NO invoques el agente — el humano lo corre manualmente (cognify tarda + cuesta).
Trade-off honesto: te salteás la conversación de governance (¿qué pasa si dos agentes contaminan el mismo grafo?). Es el debate que separa "prototype" de "producto real". Sin haberlo tenido con la sala, el follow-up en casa pierde contexto.
Fin del taller. Compartí tu lifecycle.jsonl en el canal — el que tenga la historia más limpia se lleva créditos de OpenRouter 🎁.
Validation prompt
Bash puro. Corré academy validate 5 para ejecutarlo, o pegalo en tu shell.
# 1) El skill tiene implementación
test -f .skills/query_cognee/tool.py && echo "✅ tool.py existe"
# 2) La state machine existe
test -f src/lifecycle.py && echo "✅ lifecycle.py existe"
# 3) La rig sigue verde
./scripts/check.sh
# 4) Corriste al menos una pregunta y hay lifecycle events
test -f .receipts/lifecycle.jsonl && \
test $(wc -l < .receipts/lifecycle.jsonl) -ge 3 && \
echo "✅ lifecycle.jsonl tiene ≥3 transiciones"
# 5) La respuesta incluye un monto real del dataset
uv run python -m src.cli "¿cuánto facturó Café del Valle en Q3?" | grep -q "58" && \
echo "✅ agente respondió con monto real (58.300.000)"
Checkpoint
Marcá cada item cuando tengas evidencia. Se persiste en tu navegador (localStorage). No avanzás a la próxima lección hasta que estén todos verdes.