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

Lesson 04 — Skill generation

⏱ 14 min · prereq: Lesson 03 completa.

Enseñarle al agente a **escribirse sus propios skills** siguiendo la spec de [`agentskills.io`](https://agentskills.io/home) (resumida en `.skills/README.md`).

Starter prompt

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

Starter prompt · Lesson 04
Rol: co-piloto Python, Lesson 04.

Contexto: releé .skills/README.md (spec). Releé src/agent.py y src/schemas.py.

Tarea:
1) Creá src/skill_registry.py con:
   - discover_skills(skills_dir: Path) -> list[SkillManifest]
       Recorre .skills/*/SKILL.md. Parsea frontmatter YAML.
       Valida con SkillManifest. Skills inválidos se skipean con warning (rich).
   - as_tool_descriptions(manifests) -> str
       Devuelve un bullet list en markdown para inyectar en el system prompt
       del agente principal (para que "sepa" qué skills tiene).

2) Actualizá src/agent.py:
   - Import FilesystemBackend de deepagents.backends (o donde esté en tu versión).
   - Definí SKILL_AUTHOR_INSTRUCTIONS con CONTEXTO REAL DEL DATASET (crítico):
       * `data/acme/*.md` son markdown con tablas de facturas — NO CSV, NO SQL.
       * Los tools reales disponibles por Lesson 05 son helpers de Cognee
         (`cognee_search`, etc.), no `execute_sql`/`read_csv`/`pandas_query`.
       * Prohibí explícitamente inventar env vars (`ACME_BILLING_PATH`, etc.) o paths
         fuera de `data/acme/`.
   - Pasá subagents con el dict pero SIN el key "tools" (los sub-agents heredan
     `write_file` vía FilesystemMiddleware; pasar strings falla porque la API
     tipa tools como Sequence[BaseTool | Callable | dict], no strings):
       subagents=[{"name":"skill_author", "description":"...", "prompt":SKILL_AUTHOR_INSTRUCTIONS}]
   - Agregá backend=FilesystemBackend(root_dir=".", virtual_mode=True) al
     create_deep_agent. Sin esto los SKILL.md quedan in-memory (default es
     StateBackend) y no aparecen en `git status`. virtual_mode=True sandboxea al
     root_dir; sin él el agente puede pedir listing de `/home` y romper en macOS.
   - Cambiá INSTRUCTIONS del agente principal: si detecta gap, DEBE invocar al
     sub-agent skill_author antes de responder "no_capability".
   - Antes de invocar el agent, inyectá la lista de skills disponibles al system prompt
     usando as_tool_descriptions(discover_skills(...)).

3) En CLI: después de cada invoke, imprimir "skills disponibles ahora: N"
   y listar cualquier .skills/*/SKILL.md nuevo.

4) Corré:
   uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?"
   # después inspeccioná .skills/ — debe haber un skill nuevo

Reglas:
- El agente NO debe implementar el skill (todavía sin tool.py). Solo el SKILL.md.
- El SKILL.md generado DEBE validar contra SkillManifest.
- mypy --strict verde. check.sh verde.
- Escribí receipt event="skill_created" con {name, path} cuando escribe uno.
- Verificá que el SKILL.md referencia `data/acme/*.md`, NO tools/paths inventados.

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

Challenge

Enseñarle al agente a escribirse sus propios skills siguiendo la spec de agentskills.io (resumida en .skills/README.md).

Producto: - src/skill_registry.py — descubre .skills/*/SKILL.md, valida contra SkillManifest, expone cada skill como tool al agente principal. - Un sub-agent skill_author configurado en src/agent.py que, al detectar un gap, escribe .skills/<name>/SKILL.md real. - Al re-ejecutar la pregunta de Lesson 03, ves un archivo .skills/query_cognee/SKILL.md que no escribiste vos.

Manual path

Referencia — src/skill_registry.py
"""Skill registry — discover, validate, expose."""

from __future__ import annotations

import re
from pathlib import Path

import yaml
from pydantic import ValidationError
from rich import print as rprint

from src.schemas import SkillManifest

_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)


def _parse_frontmatter(md: str) -> dict[str, object] | None:
    m = _FRONTMATTER_RE.match(md)
    if not m:
        return None
    parsed = yaml.safe_load(m.group(1))
    return parsed if isinstance(parsed, dict) else None


def discover_skills(skills_dir: Path) -> list[SkillManifest]:
    manifests: list[SkillManifest] = []
    for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
        raw = skill_md.read_text(encoding="utf-8")
        fm = _parse_frontmatter(raw)
        if fm is None:
            rprint(f"[yellow]⚠ skipping {skill_md}: no frontmatter[/yellow]")
            continue
        try:
            manifests.append(SkillManifest.model_validate(fm))
        except ValidationError as e:
            rprint(f"[yellow]⚠ skipping {skill_md}: {e.error_count()} errors[/yellow]")
    return manifests


def as_tool_descriptions(manifests: list[SkillManifest]) -> str:
    if not manifests:
        return "(no skills available yet)"
    return "\n".join(
        f"- `{m.name}` — {m.description} (use when: {m.when_to_use})"
        for m in manifests
    )
Referencia — actualización de src/agent.py
# … imports previos …
from pathlib import Path

from deepagents.backends import FilesystemBackend  # tu ruta puede variar según versión
from src.skill_registry import as_tool_descriptions, discover_skills

SKILLS_DIR = Path(".skills")

SKILL_AUTHOR_INSTRUCTIONS = """You are a skill author for the CTL Academy workshop.

Given a description of a capability gap, write a new SKILL.md file that follows
the spec in .skills/README.md exactly. Use the `write_file` tool (inherited from
FilesystemMiddleware) to place it at `.skills/<snake_case_name>/SKILL.md`.

## Repo context — do NOT invent paths or data shapes

- Data source: `data/acme/*.md` are plain markdown files with tables of invoices,
  customers, and quarterly totals in COP. NOT CSV. NOT SQL. NOT a database.
- Memory backend by end of Lesson 05: `cognee` async Python library backed by
  Neo4j. Query API: `await cognee.search(query_text=..., query_type=SearchType.INSIGHTS)`.
- Realistic tools to declare in the SKILL.md: ["cognee_search"], optionally
  ["cognee_search", "cognee_add"]. Do NOT invent `execute_sql`, `read_csv`,
  `pandas_query`, `duckdb_query`.
- Do NOT invent env vars like `ACME_BILLING_PATH`. The dataset is fixed at
  `data/acme/*.md` — reference it explicitly in the Preconditions section.

## Rules for the SKILL.md you write

- Frontmatter MUST include: name, description, when_to_use, tools, allowed_actions.
- name must match snake_case regex ^[a-z][a-z0-9]*(_[a-z0-9]+)*$.
- description must be ≥ 20 characters.
- tools is a list of function name strings — do NOT implement them here.
- allowed_actions ⊂ {read, write, network}. Prefer ["read"] unless the skill ingests.
- Body sections: Preconditions, Procedure (numbered), Failure modes.
- Preconditions MUST cite `data/acme/*.md` and (for L05+) an initialized Cognee graph.
- Do NOT create tool.py. Only the SKILL.md.
- Return the path you wrote.
"""


def build_agent(llm: BaseChatModel | None = None) -> Any:
    skills = discover_skills(SKILLS_DIR)
    instructions = INSTRUCTIONS + "\n\n## Available skills\n" + as_tool_descriptions(skills)
    return create_deep_agent(
        tools=[],
        instructions=instructions,
        model=llm or build_llm(),
        subagents=[
            {
                "name": "skill_author",
                "description": "Writes new SKILL.md files following .skills/README.md spec",
                "prompt": SKILL_AUTHOR_INSTRUCTIONS,
                # NO pasar `tools` acá: el schema de DeepAgents no acepta strings.
                # El sub-agent hereda `write_file` desde FilesystemMiddleware.
            }
        ],
        # Backend REQUERIDO: sin esto el SKILL.md queda en StateBackend in-memory
        # y no aparece en `git status`. virtual_mode=True sandboxea al root_dir
        # (sin él el agente puede pedir /home y explota en macOS).
        backend=FilesystemBackend(root_dir=".", virtual_mode=True),
    )
Y en `INSTRUCTIONS` del agente principal, agregá:
If you cannot answer because a required skill is missing, do the following BEFORE
responding "no_capability":
1. Delegate to the `skill_author` sub-agent with a clear gap description that
   INCLUDES the data source shape ("markdown files in data/acme/") and the
   memory backend ("Cognee graph, available from Lesson 05").
2. After it writes the skill, note "skill_created: <name>" in your response.
3. Do NOT claim the skill is "ready to use" — it is not. Only the manifest
   exists; the implementation comes in Lesson 05.
4. Then respond "no_capability" — the human will re-run and this time the
   skill exists in the registry.

Observe

Primera corrida:

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

Output esperado (partes clave):

Agent: I delegated to skill_author to create `<algún-nombre>`.
skill_created: <algún-nombre> → .skills/<algún-nombre>/SKILL.md
{"status":"no_capability", "reason":"skill just created, needs implementation"}
skills disponibles ahora: 1

El nombre puede ser query_cognee, compare_periods, ranking_customers, etc. El sub-agent decide cómo describir el gap.

Y git status muestra:

Untracked: .skills/<algún-nombre>/SKILL.md

⚠️ Antes de festejar, abrí el archivo y leelo. Si el SKILL.md dice cosas como execute_sql, read_csv, pandas_query, o inventa env vars como ACME_BILLING_PATH, el sub-agent alucinó. Volvé a SKILL_AUTHOR_INSTRUCTIONS y verificá que la sección "Repo context" esté completa — es la que ancla al sub-agent al dataset real. Discutimos por qué esto pasa más abajo.

Segunda corrida (opcional): el registry ya carga el skill; el agente lo lista pero aún no puede ejecutarlo (falta tool.py que llega en Lesson 05).

Discuss · ¿tool o skill?

Pregunta para la sala: ¿cuál es la diferencia real entre un tool y un skill? ¿Por qué separarlos?

Prompt para el debate:

  • Un tool es una función Python que el LLM llama con argumentos (query_cognee(question="...")). Lo escribís vos, en código.
  • Un skill es un SKILL.md con procedimiento en markdown que el LLM lee y ejecuta paso a paso, y puede referenciar tools. Lo escribe el LLM (o vos, si preferís).

Tres razones para separarlos:

  1. Portabilidad. Un SKILL.md sirve para Claude Code, Cursor, Codex — cualquier agente que consuma la spec. Una función Python está atada al runtime.
  2. Inspeccionable. git diff .skills/ muestra exactamente qué "sabe" tu agente. Un TODO invisible en el prompt no.
  3. Versionable. Podés commitear la evolución del SKILL.md como cualquier otro artefacto. Skills mejorados = PR reviews de razonamiento del agente.

El wow moment de esta lección: git status te muestra un archivo que no escribiste. El LLM leyó una spec markdown y produjo un artefacto markdown válido contra Pydantic. La misma dinámica de un dev humano leyendo un README y escribiendo código.

La trampa: el skill "existe" pero no funciona todavía — es solo el manifest. En Lesson 05 le damos la implementación (Cognee) y el ciclo de vida.

Discuss · el registry valida forma, no verdad

Pregunta para la sala: acabás de ver al skill_author escribir un SKILL.md que pasa SkillManifest.model_validate(). ¿Eso significa que el skill es útil?

Casi seguro que no, salvo que hayas enriquecido SKILL_AUTHOR_INSTRUCTIONS con contexto del dataset. Sin ese contexto, el sub-agent tiende a inventar:

  • Un tool execute_sql cuando tu data es markdown.
  • Un path ./data/acme_billing.csv que no existe.
  • Un env var ACME_BILLING_PATH que no está en .env.

El manifest es sintácticamente válido (los 5 campos están, name es snake_case, la lista de tools no está vacía) pero semánticamente basura. Peor: el agente principal va a leer ese SKILL.md, ver tool: execute_sql, y anunciar "listo para usar" — exactamente el patrón alucinatorio que discutimos en Lesson 03.

Tres enfoques para cerrar el loop verdad ⇄ forma

Enfoque Cuándo aplica Costo Trade-off
Prompt enrichment (lo que hacemos acá) Le inyectás al skill_author el shape real del dataset + tools disponibles. 1 vez en el system prompt. Prevención. Barato pero frágil si el prompt se olvida en el futuro.
Post-write review Sub-agent skill_reviewer chequea contra el repo real después de que skill_author escribe. 1 LLM call extra por skill. Defensa en profundidad. Cuesta USD pero atrapa hallucinations que el prompt no previno.
Runtime failure (llega en Lesson 05) Cuando el skill se intenta invocar y explota, la state machine transiciona a refining y el loop aprende. Gratis (falla de todos modos). Tarde-pero-honesto. Falla ruidosa después de que el agente ya dijo "listo".

Decisión del taller: prevención + runtime failure. El review dedicado (skill_reviewer) queda como stretch goal — es el patrón natural para escalar a producción.

La lección más profunda

Un LLM no verifica su propio trabajo semánticamente. El mismo LLM que escribió el SKILL.md fue el que dijo "listo para usar". Necesitás otro proceso — otro LLM con role distinto, un test unitario, un review humano, o una falla en runtime — para cerrar el loop.

Es el mismo principio que en cualquier producto: quien escribe no revisa. Acá el "quien revisa" puede ser código (tests contra el schema real), otro agente (review), o el runtime (falla ruidosa). No hay atajo.

Fade

En la próxima lección no te acompaño con "cómo integrar Cognee". Asumo que sabés escribir un tool.py que respete el contrato del SKILL.md.

🚀 Fastrack · saltear al checkpoint (⚠️ hace 1-2 llamadas reales a OpenRouter, ~USD 0.05)

Cuatro pasos:

  1. Copiá Referencia — src/skill_registry.py a src/skill_registry.py.
  2. Copiá Referencia — actualización de src/agent.py a src/agent.py (reemplazá el archivo previo).
  3. Corré academy check — verde.
  4. Corré la misma pregunta que en Lesson 03: bash uv run python -m src.cli ask "¿cuánto facturó Café del Valle en Q3?" Esperado: el agente detecta gap → invoca skill_author → escribe .skills/query_cognee/SKILL.md → responde no_capability con skill_created en el receipt.
  5. Verificá .skills/query_cognee/SKILL.md existe y corré academy validate 4.

Prompt one-shot:

Rol: implementador rápido, Lesson 04.
Leé workshop/deepagents-memory-skills/lessons/04-skill-generation.md, aplicá
"Manual path — src/skill_registry.py" y "Manual path — actualización de
src/agent.py" exactamente. Corré ./scripts/check.sh. Verde.
NO invoques el agente — el humano lo corre manualmente para ver al sub-agent
escribir el SKILL.md.

Trade-off honesto: te salteás el momento de ver git status con un archivo que no escribiste. Ese momento es la razón por la que la spec agentskills.io existe. Sin haberlo vivido, la Lesson 05 pierde impacto.


SiguienteLesson 05 — Cognee + autonomía

Validation prompt

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

Validation · Lesson 04
# 1) Existe al menos un SKILL.md que no escribiste vos
ls .skills/*/SKILL.md 2>/dev/null | wc -l   # esperado: ≥ 1

# 2) Ese SKILL.md valida contra SkillManifest
uv run python -c "
from pathlib import Path
from src.skill_registry import discover_skills
m = discover_skills(Path('.skills'))
assert len(m) >= 1, 'no skills discovered'
for skill in m:
    print(f'✅ {skill.name}: {skill.description[:50]}…')
"

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

# 4) Hay receipt de skill_created
grep -q "skill_created" .receipts/session.jsonl && echo "✅ receipt skill_created"

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 04 0 / 7 done