centinela Academy
Lecciones 01 Rules before code 02 Reliability floor 03 Raw Deep Agent 04 Skill generation 05 Cognee + autonomía acotada
🧶 Lesson 02 · CTL Academy · PyCon Colombia 2026

Lesson 02 — Reliability floor

⏱ 14 min · prereq: Lesson 01 completa.

Hacer que **input malformado falle fuerte y con contexto**. Antes de darle capacidades al agente, sus estructuras de datos deben rechazar basura visiblemente.

Starter prompt

Pegalo en tu coding agent (Claude Code, Cursor, Codex…) o usá academy lesson prompt 2 --copy.

Starter prompt · Lesson 02
Rol: co-piloto Python. Estamos en la Lesson 02 del workshop CTL Academy.

Contexto: VISION.md ya está negociado. Leelo. También leé .skills/README.md y
workshop/deepagents-memory-skills/tooling.md.

Tarea (una sola pasada, chica):
1) Creá src/schemas.py con estos modelos Pydantic v2:
   - BusinessQuestion(question: str, tenant: str, deadline: datetime)
     Validaciones: question no vacío, tenant no vacío, deadline futuro.
   - SkillManifest(name: str, description: str, when_to_use: str,
                   tools: list[str], allowed_actions: list[Literal["read","write","network"]])
     Validaciones: name en snake_case, description ≥ 20 chars, tools no vacía.
   - Receipt(ts: datetime, actor: Literal["user","agent","system"],
             event: str, payload: dict[str, Any])
     Sin validaciones adicionales — es append-only.

2) Creá tests/test_schemas.py con 4 tests pytest:
   - test_business_question_rejects_empty_question
   - test_business_question_rejects_past_deadline
   - test_skill_manifest_rejects_non_snake_case_name
   - test_receipt_accepts_arbitrary_payload

3) Corré ./scripts/check.sh y pegame el output completo.

Reglas:
- mypy --strict debe pasar. Nada de Any implícito.
- Usá `from __future__ import annotations`.
- Usá `field_validator` de Pydantic v2 (no el legacy `validator`).
- No modifiques nada fuera de src/schemas.py y tests/test_schemas.py.

Duración: 14 min · Prereq: Lesson 01 completa.

Challenge

Hacer que input malformado falle fuerte y con contexto. Antes de darle capacidades al agente, sus estructuras de datos deben rechazar basura visiblemente.

Producto: src/schemas.py con 3 modelos Pydantic + tests/test_schemas.py con 4 tests que pasan.

Manual path

Referencia — src/schemas.py
"""Pydantic schemas — reliability floor for the workshop."""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Literal

from pydantic import BaseModel, Field, field_validator

Actor = Literal["user", "agent", "system"]
AllowedAction = Literal["read", "write", "network"]


class BusinessQuestion(BaseModel):
    question: str = Field(min_length=1)
    tenant: str = Field(min_length=1)
    deadline: datetime

    @field_validator("deadline")
    @classmethod
    def _deadline_must_be_future(cls, v: datetime) -> datetime:
        now = datetime.now(tz=v.tzinfo or timezone.utc)
        if v <= now:
            raise ValueError("deadline must be in the future")
        return v


class SkillManifest(BaseModel):
    name: str
    description: str = Field(min_length=20)
    when_to_use: str = Field(min_length=1)
    tools: list[str] = Field(min_length=1)
    allowed_actions: list[AllowedAction] = Field(min_length=1)

    @field_validator("name")
    @classmethod
    def _snake_case(cls, v: str) -> str:
        if not v.replace("_", "").isalnum() or not v.islower():
            raise ValueError("name must be snake_case (lowercase + underscores)")
        return v


class Receipt(BaseModel):
    ts: datetime
    actor: Actor
    event: str
    payload: dict[str, Any]
Referencia — tests/test_schemas.py
"""Reliability floor tests."""

from __future__ import annotations

from datetime import datetime, timedelta, timezone

import pytest
from pydantic import ValidationError

from src.schemas import BusinessQuestion, Receipt, SkillManifest


def _future() -> datetime:
    return datetime.now(tz=timezone.utc) + timedelta(days=1)


def test_business_question_rejects_empty_question() -> None:
    with pytest.raises(ValidationError, match="at least 1 character"):
        BusinessQuestion(question="", tenant="acme", deadline=_future())


def test_business_question_rejects_past_deadline() -> None:
    past = datetime.now(tz=timezone.utc) - timedelta(days=1)
    with pytest.raises(ValidationError, match="future"):
        BusinessQuestion(question="q?", tenant="acme", deadline=past)


def test_skill_manifest_rejects_non_snake_case_name() -> None:
    with pytest.raises(ValidationError, match="snake_case"):
        SkillManifest(
            name="QueryCognee",
            description="Answer business questions from the graph.",
            when_to_use="user asks a data question",
            tools=["cognee_search"],
            allowed_actions=["read"],
        )


def test_receipt_accepts_arbitrary_payload() -> None:
    r = Receipt(
        ts=datetime.now(tz=timezone.utc),
        actor="agent",
        event="skill_created",
        payload={"skill": "query_cognee", "path": ".skills/query_cognee/SKILL.md"},
    )
    assert r.event == "skill_created"

Observe

  • git status muestra 2 archivos nuevos (src/schemas.py, tests/test_schemas.py).
  • ruff check . verde.
  • mypy --strict src/ verde — cero Any implícitos, cero funciones sin firma.
  • pytest -q → 4 passed.

Discuss · schemas antes que agente

Por qué esta lección va segunda y no décima: en la Lesson 03 el agente empieza a escribir Receipts. Si el schema no existe o es débil, esos receipts son basura y perdés trazabilidad exactamente cuando más la necesitás.

El schema es la fuente de verdad de qué es un evento válido — no un dict que evoluciona a espaldas del programa.

Por qué Pydantic v2 y no un TypedDict o un dataclass:

Opción Type check Runtime check Trade-off
dataclass puro ❌ (question="" pasa) Ligero pero inseguro con LLM
TypedDict Solo hint, no valida
Pydantic v2 La única que rechaza basura en runtime

Cuando el productor de datos es un LLM (que va a inventar cosas raras), Pydantic es indispensable. Aceptás la ceremonia de escribir modelos a cambio de que el agente no pueda meter un payload={"cost": "muy caro"} sin que explote antes de llegar al .jsonl.

Fade

En la próxima lección no te acompaño con "escribí un test primero". Asumo que sabés qué es lo suficientemente robusto para pasar mypy --strict y que tus receipts van a validar contra Receipt.

🚀 Fastrack · saltear al checkpoint (si estás corto de tiempo)

Cuatro pasos:

  1. Copiá Referencia — src/schemas.py de arriba a src/schemas.py.
  2. Copiá Referencia — tests/test_schemas.py a tests/test_schemas.py.
  3. Corré academy check — debe salir verde (ruff + mypy --strict + pytest).
  4. Corré academy validate 2.

Prompt one-shot:

Rol: implementador rápido, Lesson 02.
Leé workshop/deepagents-memory-skills/lessons/02-reliability-floor.md,
aplicá exactamente la sección "Manual path" a src/schemas.py y
tests/test_schemas.py. Corré ./scripts/check.sh. Confirmá verde.
No agregues validators extras. No re-diseñes los modelos.

Trade-off honesto: te salteás la reflexión sobre por qué Pydantic v2 y no dataclass. Vas a tener los schemas pero no el reasoning para defenderlos frente a un code review.


SiguienteLesson 03 — Raw Deep Agent

Validation prompt

Bash puro. Corré academy validate 2 para ejecutarlo, o pegalo en tu shell.

Validation · Lesson 02
./scripts/check.sh
# Debe terminar con "✅ check verde"

# Además, verificá que los 4 tests corrieron:
uv run pytest tests/test_schemas.py -v | grep -c "PASSED"
# Esperado: 4

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.

Evidence · Lesson 02 0 / 4 done