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

Lesson 03 — Raw Deep Agent

⏱ 14 min · prereq: Lesson 02 completa.

Instanciar un DeepAgent **crudo** — cero custom tools, cero dominio — y hacer que **falle explícitamente** en la primera pregunta. Vamos a leer las cuatro primitivas de DeepAgents mientras las vemos operar.

Starter prompt

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

Starter prompt · Lesson 03
Rol: co-piloto Python, Lesson 03 del workshop.

Contexto: leé src/schemas.py (existe desde Lesson 02), .env.example (para OpenRouter),
y https://github.com/langchain-ai/deepagents (README).

Tarea:
1) Creá src/agent.py con:
   - build_llm() -> BaseChatModel  → configura langchain_openai.ChatOpenAI con
     base_url="https://openrouter.ai/api/v1", api_key=env("OPENROUTER_API_KEY"),
     model=env("OPENROUTER_MODEL", "anthropic/claude-haiku-4.5").
   - build_agent(llm=None) → llama create_deep_agent(tools=[], instructions=..., model=llm)
     con instructions cortas: "sos un analista de negocio para ACME Colombia. Si
     no podés responder con evidencia, decilo explícito y reportá no_capability
     con needed_skill en JSON. NO corras shell commands (no tenés esa capacidad
     — es el coding agent quien opera el terminal)."
   - write_receipt(event, payload) → append a .receipts/session.jsonl como Receipt validado.

2) Creá src/cli.py con typer:
   - subcomando `ask` que toma una question: str
   - construye el agent
   - invoca agent.invoke({"messages": [{"role":"user","content": question}]})
   - imprime la respuesta con rich.print
   - si el agente no pudo responder, llama write_receipt("no_capability", {...})

3) Corré:
   uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?"

   Pegame el output y el contenido de .receipts/session.jsonl.

Reglas:
- mypy --strict verde.
- ./scripts/check.sh verde.
- No agregues tools custom todavía. El agente DEBE fallar en la pregunta de negocio.
- Escribí receipts usando el modelo Receipt de src/schemas.py.

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

Challenge

Instanciar un DeepAgent crudo — cero custom tools, cero dominio — y hacer que falle explícitamente en la primera pregunta. Vamos a leer las cuatro primitivas de DeepAgents mientras las vemos operar.

Producto: src/agent.py + src/cli.py. Al correr uv run python -m src.cli ask "..." el agente responde algo, y .receipts/session.jsonl tiene al menos un evento no_capability.

Manual path

Referencia — src/agent.py
"""Raw DeepAgent — no custom tools, no domain knowledge."""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from deepagents import create_deep_agent
from dotenv import load_dotenv
from langchain_core.language_models import BaseChatModel
from langchain_openai import ChatOpenAI

from src.schemas import Receipt

load_dotenv()

INSTRUCTIONS = """You are a business analyst assistant for ACME Colombia S.A.S.
You do NOT yet have any tools to query business data. If you cannot answer with
evidence, say so explicitly and do not fabricate numbers.

When you cannot answer, respond with a JSON block:
{"status": "no_capability", "reason": "<why>", "needed_skill": "<what would help>"}

## Scope

You are a domain agent (business analyst). You do NOT run shell commands, git
operations, or filesystem writes outside your own virtual FS. If the user asks
for any of those, respond with no_capability — that is the coding agent's job
(Claude Code, Pi, etc.), not yours. Ver `AGENTS.md` for the coding-agent policy.
"""


def build_llm() -> BaseChatModel:
    api_key = os.environ["OPENROUTER_API_KEY"]
    model = os.environ.get("OPENROUTER_MODEL", "anthropic/claude-haiku-4.5")
    return ChatOpenAI(
        base_url="https://openrouter.ai/api/v1",
        api_key=api_key,  # type: ignore[arg-type]
        model=model,
        temperature=0.0,
    )


def build_agent(llm: BaseChatModel | None = None) -> Any:
    return create_deep_agent(
        tools=[],
        instructions=INSTRUCTIONS,
        model=llm or build_llm(),
    )


def write_receipt(event: str, payload: dict[str, Any]) -> Path:
    receipts_dir = Path(os.environ.get("RECEIPTS_DIR", ".receipts"))
    receipts_dir.mkdir(exist_ok=True)
    path = receipts_dir / "session.jsonl"
    receipt = Receipt(
        ts=datetime.now(tz=timezone.utc),
        actor="agent",
        event=event,
        payload=payload,
    )
    with path.open("a", encoding="utf-8") as f:
        f.write(receipt.model_dump_json() + "\n")
    return path
Referencia — src/cli.py
"""CLI entrypoint — one question at a time."""

from __future__ import annotations

import json

import typer
from rich import print as rprint

from src.agent import build_agent, write_receipt

app = typer.Typer(add_completion=False, no_args_is_help=True)


@app.command()
def ask(question: str) -> None:
    """Ask the raw DeepAgent a business question."""
    agent = build_agent()
    result = agent.invoke({"messages": [{"role": "user", "content": question}]})
    reply = result["messages"][-1].content
    rprint("[bold cyan]Agent:[/bold cyan]", reply)

    if "no_capability" in reply:
        try:
            parsed = json.loads(reply[reply.index("{") : reply.rindex("}") + 1])
        except (ValueError, json.JSONDecodeError):
            parsed = {"raw_reply": reply}
        path = write_receipt("no_capability", {"question": question, **parsed})
        rprint(f"[dim]receipt → {path}[/dim]")


if __name__ == "__main__":
    app()

Observe

Corriendo:

uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?"

Esperás algo como:

Agent: {"status": "no_capability", "reason": "no tools to query invoices data",
        "needed_skill": "query_cognee"}
receipt → .receipts/session.jsonl

Y .receipts/session.jsonl con:

{"ts":"2026-...","actor":"agent","event":"no_capability","payload":{"question":"...", "reason":"...", "needed_skill":"query_cognee"}}

Discuss · ¿por qué el agente falló bien?

Pregunta para la sala: acabás de darle a un LLM cero herramientas y una pregunta que no puede responder. ¿Por qué respondió no_capability en vez de inventar un número?

Dos hipótesis:

  • Hipótesis A · fue el system prompt. "Say so explicitly and do not fabricate numbers" es literalmente la instrucción. El LLM obedeció.
  • Hipótesis B · fue el harness. DeepAgents impone estructura (planning tool, virtual FS, sub-agents, human-in-the-loop) que hace más difícil "improvisar" que "reportar honestamente".

La respuesta es "ambas" — pero la B es la que hace escalable el patrón. Un system prompt bueno se olvida a los 10k tokens; el harness sigue ahí siempre.

Aunque tu tools=[] esté vacío, DeepAgents ya usó estas 4 primitivas:

Primitiva Dónde apareció (o va a aparecer)
Planning tool (write_todos) El LLM puede armar un TODO interno visible en result["messages"].
Virtual filesystem (write_file, read_file, ls) Aislado en el estado del agente. Lo explotamos en Lesson 04.
Sub-agents Definimos skill_author en Lesson 04.
Human-in-the-loop (interrupt) Lo usamos en Lesson 05 antes de cognee.cognify().

Reflexión: si sacaras estas primitivas y dejaras solo el LLM crudo con chat.completions.create, ¿cuánto de la disciplina que viste hoy sobreviviría?

Fade

En la próxima lección ya no te acompaño con "escribí una función Python". Asumo que sabés instanciar sub-agentes y leer/escribir el virtual FS.

🚀 Fastrack · saltear al checkpoint (⚠️ hace 1 llamada real a OpenRouter, ~USD 0.02)

Cinco pasos:

  1. Copiá Referencia — src/agent.py a src/agent.py.
  2. Copiá Referencia — src/cli.py a src/cli.py.
  3. Corré academy check — verde.
  4. Corré la primera invocación real: bash uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?" Esperado: el agente responde no_capability (no tiene tools). Se genera .receipts/session.jsonl.
  5. Corré academy validate 3.

Prompt one-shot (para tu coding agent):

Rol: implementador rápido, Lesson 03.
Leé workshop/deepagents-memory-skills/lessons/03-raw-deep-agent.md, aplicá
exactamente "Manual path — src/agent.py" y "Manual path — src/cli.py".
Corré ./scripts/check.sh. Confirmá verde. NO invoques el agente todavía —
el humano lo va a correr manualmente.

Trade-off honesto: te salteás el "aha" de ver al agente decir explícitamente "no puedo". Ese momento es la justificación de todo Lesson 04 — sin haberlo vivido, la lección siguiente pierde peso.


SiguienteLesson 04 — Skill generation

Validation prompt

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

Validation · Lesson 03
# 1) Existe agent + cli
test -f src/agent.py && test -f src/cli.py && echo "✅ agent + cli"

# 2) La rig sigue verde
./scripts/check.sh

# 3) El agente falla como esperamos
uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?"

# 4) Hay al menos un receipt no_capability
grep -q "no_capability" .receipts/session.jsonl && echo "✅ receipt no_capability presente"

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 03 0 / 4 done