Lesson 02 — Reliability floor
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.
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 statusmuestra 2 archivos nuevos (src/schemas.py,tests/test_schemas.py).ruff check .verde.mypy --strict src/verde — ceroAnyimplí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:
- Copiá
Referencia — src/schemas.pyde arriba asrc/schemas.py. - Copiá
Referencia — tests/test_schemas.pyatests/test_schemas.py. - Corré
academy check— debe salir verde (ruff + mypy--strict+ pytest). - 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.
Siguiente → Lesson 03 — Raw Deep Agent
Validation prompt
Bash puro. Corré academy validate 2 para ejecutarlo, o pegalo en tu shell.
./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.