Skip to main content

Crear un Digital FTE: curso intensivo de 4 horas

15 conceptos, 80% de uso real: skills, sistema de registro y MCP

Este es el segundo curso en Modo 2, la ruta de fabricación. Es movimiento dos de siete: tomas el agente que ya creaste y das el primer paso real de agente a Worker de IA. El curso anterior, Crear agentes de IA, creó el agente en sí: un agente de chat en streaming con sesiones, guardrails y trazas, que se ejecuta en un sandbox para cómputo. Funcionó. También se olvidó de todo en el momento en que cerraste la terminal, y todas las herramientas que tenía estaban escritas en Python.

Un Worker de IA es más que un agente inteligente. También puedes verlo como empleado de IA o Digital FTE (equivalente de tiempo completo). Son tres nombres para lo mismo, cada uno visto desde un ángulo distinto: AI Worker describe cómo se construye, AI Employee indica dónde encaja en el equipo y Digital FTE muestra cómo aparece en el presupuesto. Este curso usa AI Worker (o simplemente Worker para abreviar).

Ese mismo agente se convierte en un Worker de IA al evolucionar de tres maneras. Es extensible: adquiere nuevas skills sin reescribirlo. Trabaja contra un sistema de registro: la fuente de verdad de la empresa, no una ventana de contexto que se vacía al reiniciar. Y está bajo tu control: tú decides dónde se ejecuta, qué datos conserva y qué reglas sigue. Un Worker de IA completo también está siempre activo, es proactivo y multicanal, pero esos son pasos posteriores. Este curso construye la base: el núcleo extensible, duradero y propio al que se suma todo lo demás.

Mantienes el mismo SDK y el mismo runtime SandboxAgent del curso anterior. Un Worker en ejecución tiene dos partes que producción despliega por separado: el arnés (el runtime del agente, el bucle del SDK) y el cómputo (el sandbox donde realmente se ejecuta el código del agente). Cuando el agente ejecuta una herramienta, entrega el código a su sandbox; ese sandbox es el cómputo. En este curso, ambos permanecen locales: UnixLocalSandboxClient ejecuta el sandbox directamente en tu equipo (cero infraestructura, una clave de API) y puedes apuntarlo a Docker, Cloudflare, E2B o Modal con un cambio de una línea (guía de intercambio de la Parte 5). Desplegar el arnés como un servicio en la nube siempre activo es su propio curso, Despliega tu arnés de agente en la nube. Aquí no despliegas ninguno de los dos. Lo que cambia este curso es lo que rodea al runtime.

Promover el agente requiere dos pasos: sacar sus capacidades de su código y sacar su estado, registros y memoria de su proceso. Luego conectas el agente con ambos.

  • Las capacidades se convierten en Skills: carpetas portátiles que el agente descubre y carga bajo demanda, en lugar de herramientas escritas en Python.
  • Las cosas que el agente solía perder al reiniciar se trasladan a Postgres. Allí viven tres cosas diferentes, y no son iguales:
    • Estado: la condición actual del trabajo. Qué conversaciones están abiertas, qué espera aprobación.
    • Sistema de registro: el rastro autorizado y auditado de lo que realmente sucedió. Cada acción que tomó el Worker se puede reproducir más tarde.
    • Memoria: lo que el Worker ha aprendido y puede reutilizar. Casos anteriores, documentos de referencia, resúmenes de conversaciones antiguas.
  • MCP (el Model Context Protocol) será el cable entre el agente y esos almacenes.

Una pieza más, y es la que la mayoría de la gente etiqueta mal. La recuperación semántica, encontrar cosas por significado en lugar de por coincidencia exacta, es un método de recuperación, no un cuarto almacén. Puedes ejecutarla sobre el estado, el sistema de registro o la memoria. La obtenemos de pgvector, una extensión que agrega búsqueda vectorial a Postgres. Dos preguntas muestran por qué los almacenes son diferentes:

  • "¿Cuál es la política de nuestra empresa para el caso de este cliente?" pregunta al sistema de registro una respuesta autorizada.
  • "¿Hemos visto una pregunta como esta antes?" pregunta a la memoria, y la recuperación semántica es la forma de encontrar la coincidencia.

Misma base de datos, diferentes almacenes, un método de búsqueda que funciona en todas ellas.

La única frase para recordar. Promover un agente a Worker implica dos movimientos: sacar sus capacidades de su código (Skills) y sacar su estado, registros y memoria de su proceso (Postgres, al que llega por MCP). Todo en este curso es uno de esos dos movimientos, o el cable entre ellos.

La promoción en una imagen: las capacidades pasan del código del agente a una biblioteca de skills descubribles; el estado, el sistema de registro y la memoria del agente salen de su proceso a Postgres con pgvector; y MCP es el cable que conecta el agente a ambos.

Dónde se ubica este curso en la tesis de Agent Factory

La tesis nombra siete invariantes que todo sistema de agentes de producción debe satisfacer. El curso anterior cubrió el motor (Invariante 4): el SDK de agentes OpenAI como arnés y un entorno de pruebas como proceso en el que se ejecuta. Este curso agrega la siguiente capa.

  • Invariante 5: cada Worker se ejecuta contra un sistema de registro. El motor es donde se ejecuta un Worker; el sistema de registro es aquello contra lo que trabaja. Lo implementamos con Postgres y pgvector. La arquitectura es la invariante; el producto de base de datos se puede reemplazar. Usamos Neon porque permite empezar gratis, escala a cero cuando está inactivo y ofrece un servidor MCP oficial, pero cualquier Postgres duradero y gobernado funciona. La guía de intercambio de la Parte 5 enumera alternativas.
  • La capa de capacidad de Skills. Las skills son el estándar abierto (originalmente Anthropic, ahora en todo el ecosistema en agentskills.io) para permitir que las capacidades de un trabajador vivan en carpetas que descubre y carga según demanda.
  • MCP como conector. El curso anterior no tenía ningún MCP. Sus herramientas eran funciones @function_tool y su estado vivía en una sesión SQLite en proceso. Ahora que las capacidades y el estado viven fuera del proceso, el agente necesita un cable estándar para llegar a ellos. Ese cable es MCP y es el nuevo patrón clave en este curso.

El curso anterior construyó el motor. Este curso desarrolla contra qué funciona el motor.

La hoja de trucos de 15 conceptos

La mayoría de las fallas de producción se remontan a una de tres cosas: una skill que el agente nunca encontró, una base de datos que no es realmente la fuente de la verdad o un cable MCP que deja caer datos. Esta tabla es el diagnóstico.

#ConceptoCapa¿Qué pregunta responde?
1Qué es una skill de agenteSkills¿Dónde vive la capacidad reutilizable? En una carpeta, con SKILL.md más scripts/referencias opcionales.
2Revelación progresivaSkills¿Por qué es barato tener skills a mano? Descubrimiento → activación → ejecución carga solo lo que se necesita cuando es necesario.
3Escribir un SKILL.mdSkills¿Qué contiene realmente un archivo de skill? Metadatos, descripción del disparador, instrucciones operativas.
4Convenciones de empaquetado de skillsSkills¿Cómo viajan las skills entre herramientas? La misma carpeta funciona en Claude Code, OpenCode y cualquier cliente compatible.
5Componer skillsSkillsCuándo encadenar skills pequeñas mediante traspaso por sistema de archivos en lugar de escribir una skill grande.
6Por qué se gestiona PostgresSistema de registro¿Qué almacén gana "sistema de registro"? Uno con persistencia, ramificación, gobernanza y los vectores primitivos que necesita un agente.
7El esquema del trabajadorSistema de registro¿Qué tablas necesita realmente un agente? Conversaciones, mensajes, documentos, incrustaciones, registro de auditoría, invocaciones de capacidades.
8Conceptos básicos de pgvectorSistema de registro¿Cómo funciona la búsqueda semántica en Postgres? Columna de incrustación, operadores de distancia, tipos de índice.
9El pipeline de embeddingsSistema de registro¿Cómo se convierte el texto en un vector consultable? Fragmentación, modelo de embedding, cuándo volver a generar embeddings.
10Traza de auditoría como disciplinaSistema de registro¿Qué significa "lee y escribe" para un Trabajador? Cada acción que realiza un trabajador deja un rastro que la empresa puede reproducir.
11Qué es y qué no es MCPMCPUn protocolo para herramientas, recursos e indicaciones: no un marco, no un servicio.
12El servidor Neon MCPMCPLa interfaz del agente con su base de datos: qué expone, cómo se autentica.
13Conexión de MCP al SDK de agentesMCPLa integración MCP del SDK: cómo registrar un servidor, qué ve el modelo, dónde reside el límite de confianza.
14Servidores MCP personalizadosMCPCuándo escribir su propio servidor o simplemente usar @function_tool. El árbol de decisiones.
15MCP bajo cargaMCPOpciones de transporte, agrupación de conexiones, cuándo hacer cola.

Una vez que tienes este mapa, el resto es principalmente mecánica. Una falla en producción suele venir de una de estas tres cosas: una skill que nunca se descubrió (descripción demasiado vaga), un sistema de registro sobre el que dos Workers no están de acuerdo (carrera de esquema) o un cable MCP que descarta eventos (transporte incorrecto para la carga de trabajo). El diagnóstico te dice cuál.

¿Para quién es este curso?

Intermedio. Deberías tener:

  • Idealmente, haber completado Construir agentes de IA. Es recomendable, no obligatorio: el zip complementario incluye un starter ejecutable que reproduce el estado final de ese curso, así que puedes comenzar sin él. Si lo hiciste, simplemente entenderás mejor por qué las piezas tienen esta forma.
  • Los hábitos de modo de plan y archivo de reglas del Curso intensivo de codificación agente.
  • Un ciclo PRIMM-AI+ en tu haber.
  • Un modelo mental funcional de Postgres (tablas, filas, SQL).

Aquí no escribes Python a mano. Tu agente de codificación (Claude Code u OpenCode) escribe cada bloque de código; tu trabajo es dirigirlo, leer lo que produce y detectar cuándo se desvía. Esa es la disciplina del Modo 2, igual que en el curso anterior. Las Partes 2 y 3 se vuelven más densas (modelos Pydantic, pools de asyncpg, un pequeño servidor MCP personalizado), así que espera más ida y vuelta con tu agente allí. Los requisitos previos completos y el glosario están en el Apéndice A.1 y A.5.

Moneda

Vigente en mayo de 2026. Verificado con openai-agents 0.17.x, el SDK mcp actual, la documentación del servidor MCP de Neon y pgvector 0.8+. Fija las versiones en tu pyproject.toml cuando empieces a construir. La arquitectura de estado y confianza que enseña este curso no cambia cuando cambian los productos; los productos son la mejor opción de este año. Cuando la documentación y esta página discrepen, gana la documentación: el tutorial de Cloudflare Sandbox y la documentación de Neon son canónicos. La guía de intercambio de la Parte 5 enumera alternativas por producto (Supabase, Pinecone, embeddings de Cohere, LangGraph, sandboxes Docker/E2B/Modal y otros).

Lo que queda del curso anterior, lo nuevo

Lo que sigues usando: Agent, Runner, @function_tool, sesiones, eventos de streaming, guardrails y SandboxAgent con capacidades Shell() y Filesystem(). El sandbox en sí (UnixLocalSandboxClient para aprendizaje, luego Docker, Cloudflare, E2B o Modal) sigue siendo tu límite de confianza para cualquier cosa que ejecuta el agente, incluidos los scripts de Skill que ejecutan comandos de shell.

Novedades: esas primitivas ahora se encuentran encima de una biblioteca de skills y un sistema de registro Neon conectado por MCP. Las herramientas auxiliares y la sesión SQLite del curso anterior desaparecieron.

La arquitectura combina un SDK abierto (OpenAI Agents SDK, con licencia MIT), un protocolo de conector abierto (MCP) e infraestructura administrada reemplazable (tu sandbox para la ejecución, Neon para el almacenamiento). Cada pieza puede cambiarse sin modificar las demás.

Este es un curso centrado en Python, como su predecesor. El formato Skills es independiente del lenguaje (un SKILL.md es un archivo Markdown, ya sea que tu agente esté en Python o TypeScript) y MCP es independiente del transporte, pero el agente que extendemos en la Parte 4 es el mismo chat-agent de Python que creaste en el curso anterior.

Elige tu herramienta, la página sigue

Continúa el patrón de doble herramienta del curso anterior. Las secciones que divergen entre Claude Code y OpenCode tienen un conmutador; elige uno y la página se sincronizará entre las visitas.

Ruta de lectura para líderes no tecnológicos (no construirás, necesitas determinar el alcance de lo que hará tu equipo)

Si un CTO o un socio de producto te envió aquí para que puedas hacer buenas preguntas en la próxima reunión de planificación, no necesitas el capítulo completo. Un recorrido de 25 minutos:

  1. La "una frase para recordar" en la leyenda anterior (capacidades + memoria).
  2. Qué es realmente una skill: Concepto 1. Dos párrafos.
  3. Por qué es importante el sistema de registro: apertura del Concepto 6 (omite los detalles del esquema).
  4. La disciplina del registro de auditoría: apertura del Concepto 10.
  5. Forma del costo: Parte 5 "Forma del costo de un trabajador". Cifras reales: entre 60 y 240 dólares al mes para un trabajador, entre 3.000 y 12.000 dólares para una fuerza laboral de 50 personas.
  6. La guía de intercambio: "Guía de intercambio" de la Parte 5. Qué opciones de proveedores son estratégicas (la arquitectura permanece) versus tácticas (elección de los ingenieros).

Después de esas seis, las tres preguntas que vale la pena abordar en una reunión de planificación: _(1) ¿Qué skills necesita nuestro primer trabajador y de qué manuales existentes proviene cada una? (2) ¿Cuál es el registro de auditoría más pequeño que podemos enviar y aun así pasar la revisión? (3) ¿Qué compromisos de proveedores podemos mantener durante 12 meses y cuáles siguen siendo intercambiables?

Cómo utiliza este capítulo su agente de codificación

Descarga digital-fte-crash-course.zip y descomprímelo. El zip ahora contiene un proyecto inicial completo y ejecutable, no un parche para colocar en algo que ya tienes. Reproduce el estado final del curso anterior: uv sync, configura OPENAI_API_KEY en .env y tendrás un agente de chat en sandbox funcionando. Tu agente de codificación recoge el resumen AGENTS.md al inicio de la sesión, y ese resumen conserva las reglas duraderas en las ocho decisiones: límites de MCP, disciplina de auditoría, invariantes del modelo de embedding y vocabulario canónico de acciones.

Los resúmenes por decisión en la Parte 4 son entonces quirúrgicos: nombran solo lo que cambia en ESTE paso (un nuevo archivo, una nueva herramienta, un nuevo cableado), mientras que AGENTS.md mantiene las reglas arquitectónicas en contexto paso a paso.

Si prefieres construir antes que leer, hojea las Partes 1 a 3 y salta a la Parte 4. El agente de chat del curso anterior evoluciona hasta convertirse en un Worker de atención al cliente mediante ocho decisiones quirúrgicas, con la misma forma que el ejemplo resuelto de ese curso.

Toda la arquitectura, en una línea. Motor = OpenAI Agents SDK + tu sandbox (UnixLocalSandboxClient para aprender, Docker para aislamiento local real, Cloudflare para producción; el código del agente es idéntico en los tres). Capacidad = Skills (Parte 1). Verdad = Neon Postgres + pgvector (Parte 2). Conector = MCP (Parte 3). Las ocho decisiones de la Parte 4 conectan las cuatro. Si solo recuerdas una frase de este documento, que sea esta.


La victoria rápida en quince minutos: triunfa una vez y luego estudia por qué funcionó

Antes de leer los 15 conceptos que explican por qué funciona esta arquitectura, crea la versión más pequeña posible que realmente funcione. Al final de esta sección tendrás:

  • una tabla de Postgres que contiene la primera escritura duradera del agente
  • una fila de auditoría que registra lo que hizo el agente, en la misma transacción
  • un límite de MCP que el agente cruzó para hacerlo
  • una carpeta de Skills, lista para activarse más tarde cuando ejecutes el mismo agente desde Claude Code
  • una respuesta funcional a la pregunta "¿el sistema de registro + límite MCP realmente hizo algo por mí?"

Este no es el ejemplo práctico de la Parte 4; ese es el Trabajador completo, ocho Decisiones, cientos de líneas. Esta es una pantalla. Si solo tienes una sesión, haz esto y luego regresa para ver los conceptos cuando quieras saber por qué cada pieza tuvo la forma que tenía.

Antes de comenzar: el proyecto inicial viene en el archivo zip. Descarga digital-fte-crash-course.zip, descomprímelo, ejecuta uv sync y configura OPENAI_API_KEY en .env. Ahora tienes un agente de chat en sandbox completamente ejecutable: el mismo estado final que creó el curso anterior, reproducido desde cero. No se requiere el curso previo. Luego, Quick Win agrega una carpeta de skills, un servidor MCP y unas 30 líneas al cli.py del proyecto. Con el starter descomprimido, son dos comandos uv (uv add, luego uv run).

Acerca del sandbox que ejecutarás. El sandboxed.py del starter ejecuta el agente como SandboxAgent sobre UnixLocalSandboxClient. Sé honesto sobre lo que eso significa: UnixLocal ejecuta las operaciones de shell y sistema de archivos del agente como un subproceso en tu equipo real, sin aislamiento. Es el camino sin infraestructura para aprender con un agente confiable que tú mismo diriges. No es un límite de confianza. Cuando quieras aislamiento real, cambiar a DockerSandboxClient (contenedores locales) o CloudflareSandboxClient (producción) es un cambio de cliente de una línea: el código del agente es idéntico. La progresión es UnixLocal para aprender → Docker para ejecuciones locales aisladas → Cloudflare para producción, y la elección es tuya.

Quick Win en sí crea un agente de chat Python independiente (cli.py) para mantener el foco en el límite de MCP, así que no necesita ningún cliente sandbox.

Una condición previa. El descubrimiento de skills (escanear .claude/skills/, leer frontmatter, presentar descripciones al modelo) es una capacidad del cliente en Claude Code, OpenCode y Codex. Un Agent(...) simple del OpenAI Agents SDK no lee .claude/skills/SKILL.md por sí solo. Quick Win crea un agente de chat Python independiente (la misma forma cli.py que el curso anterior) y enruta "Recuerda esto:..." mediante la docstring de la herramienta MCP. La carpeta Skill del Paso 1 se activa más tarde cuando ejecutas el mismo agente desde Claude Code; no se activa en la ruta Python independiente de Quick Win, y ese es el punto: el límite de MCP por sí solo basta para demostrar que la arquitectura funciona.

Paso 1. En el proyecto inicial descomprimido, crea una carpeta de Skill con un archivo:

mkdir -p .claude/skills/log-a-note
cat > .claude/skills/log-a-note/SKILL.md << 'EOF'
---
name: log-a-note
description: Saves a short text note to the durable notes table in Postgres, along with a timestamp. Use when the user says "remember this", "save a note", "log that", or otherwise asks for something to be persisted between sessions. Returns the row_id of the saved note.
---

# Log a note

When this skill activates:

1. Extract the note text from the user's message — everything after "remember:" or "save this:" or similar trigger phrases, or the whole message if no trigger phrase is used.
2. Call the `save_note` tool with the extracted text.
3. Reply with one short sentence confirming the save and citing the row_id.
EOF

Paso 2. Crea una tabla de Postgres y una pequeña tabla de auditoría en una base de datos nueva o una rama de Neon, no en una que ya tenga tablas. (Usa la consola web de Neon o CREATE DATABASE en cualquier Postgres que controles; todavía no necesitas pgvector). Los nombres notes y audit_log son lo bastante genéricos como para chocar con un esquema existente, y el CREATE TABLE de abajo asume un destino vacío.

CREATE TABLE notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
note_text TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
action TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Paso 3. Escribe un servidor MCP de 30 líneas que exponga una herramienta. El patrón completo aparece de nuevo en el Concepto 14 y la Decisión 6; esta es la versión más pequeña posible:

# notes_mcp.py
import json
import os
import asyncpg
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("notes")
_pool = None

async def pool():
global _pool
if _pool is None:
_pool = await asyncpg.create_pool(os.environ["DATABASE_URL"])
return _pool

@mcp.tool()
async def save_note(text: str) -> dict[str, str]:
"""Save a note to the durable notes table. Returns the row_id."""
p = await pool()
async with p.acquire() as c:
async with c.transaction():
row_id = await c.fetchval(
"INSERT INTO notes (note_text) VALUES ($1) RETURNING id::text", text)
await c.execute(
"INSERT INTO audit_log (action, payload) VALUES ($1, $2::jsonb)",
"note_saved",
json.dumps({"row_id": row_id, "len": len(text)}),
)
return {"row_id": row_id, "status": "saved"}

if __name__ == "__main__":
mcp.run(transport="stdio")

Usa json.dumps() para la carga útil de auditoría, no una f-string. Los valores de Quick Win resultan ser seguros (UUID e int), pero el ejemplo completo acepta cadenas escritas por el usuario, así que el mismo patrón protege ambos. La clave row_id es importante: la consulta de verificación del Paso 5 une con payload->>'row_id', por lo que esa clave exacta debe estar en la carga útil.

Paso 4. Construya el agente. Primero, instale el SDK, el controlador DB y el paquete MCP (el iniciador ya incluye el SDK, por lo que en la práctica agregará asyncpg y mcp):

uv add openai-agents asyncpg mcp

Luego, en el cli.py del motor de arranque, las importaciones y el cableado main() se ven así (el resto de cli.py permanece tal como lo envía el motor de arranque):

# cli.py — Quick Win shape
import asyncio
import os

from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled
from agents.mcp import MCPServerStdio
from openai import AsyncOpenAI

# Uncomment if your backend is not OpenAI (DeepSeek, OpenRouter, local proxy):
# set_tracing_disabled(True)


async def main():
# The OpenAI Agents SDK accepts any OpenAI-API-compatible backend.
# Set OPENAI_BASE_URL only when pointing at a non-OpenAI host.
client = AsyncOpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL"),
)
model = OpenAIChatCompletionsModel(
model=os.environ["MODEL_NAME"],
openai_client=client,
)

async with MCPServerStdio(
name="notes",
params={
"command": "python",
"args": ["notes_mcp.py"],
"env": {**os.environ}, # inherit PATH + DATABASE_URL into the subprocess
},
client_session_timeout_seconds=30, # default 5s is too short for first call to a remote Postgres
) as notes:
agent = Agent(
name="ChatAgent",
model=model,
mcp_servers=[notes],
instructions="You are a helpful assistant. Use the tools available to you.",
)
result = await Runner.run(
agent,
"Remember this: the production deploy needs a new env var before Friday.",
)
print(result.final_output)


if __name__ == "__main__":
asyncio.run(main())

Tres detalles dignos de mencionar:

  • env={**os.environ} es la solución para el error más común de Quick Win. Si pasa solo DATABASE_URL, el subproceso generado pierde PATH y python notes_mcp.py ya no se puede encontrar en algunos shells. Heredar el entorno principal inicia el subproceso limpiamente.
  • client_session_timeout_seconds=30 anula el valor predeterminado de 5 segundos del SDK. La primera llamada save_note tiene que abrir una conexión TLS a Postgres remoto, adquirir un grupo nuevo y ejecutar dos inserciones dentro de una transacción. A los 5 segundos, el cliente se da por vencido y vuelve a intentarlo, la primera llamada aún se confirma en el lado del servidor y termina con filas duplicadas.
  • set_tracing_disabled(True) está comentado en el ejemplo anterior. Silencia el intento del SDK de subir trazas a platform.openai.com. Déjalo comentado en una ejecución real de OpenAI; descoméntalo si tu backend es otro.

Paso 5. Ejecútelo.

export DATABASE_URL='postgresql://…/your_db?sslmode=require'
export OPENAI_API_KEY='sk-…' # OpenAI, DeepSeek, OpenRouter, whatever you have

# Pick any model your provider serves. Common choices:
# 'gpt-4o' or 'gpt-5.5' (OpenAI), 'deepseek-chat' (DeepSeek),
# 'meta-llama-3.1-…' (OpenRouter), etc.
export MODEL_NAME='gpt-4o'

# UNCOMMENT THIS UNLESS your OPENAI_API_KEY is from OpenAI itself.
# DeepSeek, OpenRouter, and other OpenAI-API-compatible providers need
# the base URL set, otherwise the SDK will hit api.openai.com with your
# non-OpenAI key and you'll see a 401.
# export OPENAI_BASE_URL='https://api.deepseek.com/v1'

# Smoke-test the key before the full run. This should print the start of a
# JSON model list; a 401 here means the key or OPENAI_BASE_URL is wrong, and
# you learn that in one second instead of four files deep.
curl -sS "${OPENAI_BASE_URL:-https://api.openai.com/v1}/models" \
-H "Authorization: Bearer $OPENAI_API_KEY" | head -c 200; echo

uv run python cli.py

La cadena de documentación de la herramienta MCP es la única señal de enrutamiento: el modelo hace coincidir "Recuerde esto:..." con save_note, lo llama y el servidor escribe ambas filas en una transacción. Un límite de MCP, una palabre de activación, una transacción (la siguiente figura muestra la ruta completa).

Flujo Quick Win como un único canal horizontal: el mensaje del usuario ingresa al SDK, que llama a list_tools() en notes_mcp.py y recupera save_note(text); el modelo hace coincidir el mensaje con esa herramienta desde su cadena de documentación; MCPServerStdio enruta la llamada a través de stdio a notes_mcp.py, que escribe una fila en notas y otra en audit_log en una transacción; un único SQL JOIN en la parte inferior verifica que se haya realizado la confirmación de dos filas.

La cadena de documentación de la herramienta MCP está haciendo todo el trabajo de enrutamiento. Una transacción escribe ambas filas. One JOIN verifica la confirmación.

La verificación, en una consulta SQL:

SELECT n.id, n.note_text, n.created_at, al.action, al.payload
FROM notes n
JOIN audit_log al ON al.payload->>'row_id' = n.id::text
ORDER BY n.created_at DESC LIMIT 1;

Si ve el texto de su nota en una fila y "action": "note_saved" con un row_id coincidente en la fila de auditoría unida, la arquitectura está funcionando. Un sistema de registro que contiene la verdad. Un límite de MCP que el agente cruzó para escribirlo. Una pista de auditoría que puede reproducir.

Si un SELECT COUNT(*) FROM notes; devuelve 2 en lugar de 1 después de un único mensaje, la primera llamada save_note expiró en el lado del cliente y el SDK volvió a intentarlo en el lado del servidor. La solución es client_session_timeout_seconds=30 en el Paso 4 (volver a ejecutar después de editar). La verificación ÚNETE anterior oculta esto con LIMIT 1, por lo que vale la pena verificar el recuento.

Esta es una skill, una herramienta, dos tablas y ~50 líneas de código nuevo. El ejemplo resuelto de la Parte 4 escala el mismo patrón a tres skills, diez tablas, tres herramientas MCP y un canal de integración, pero la forma es idéntica. Si esta rápida victoria funcionó, el resto del curso explicará por qué cada pieza tiene la forma que tiene y qué cambia cuando la escalas.

Si algo no funcionó, escanee la tabla "Cuando algo anda mal" cerca del final del documento; relaciona cada falla común con el concepto que la explica. Entonces vuelve aquí.


Parte 1: Skills, capacidad como carpetas portátiles

El agente del curso anterior tenía sus capacidades integradas en Python. Cada herramienta era una función decorada con @function_tool en tools.py; cada comportamiento era una cadena de instrucciones en agents.py. Eso funcionó para una demostración. No funciona para una fuerza laboral. Capacidad de extracción de skills del código y en carpetas que el agente descubre, carga y ejecuta según demanda: versión controlada, compartible entre agentes, agregable sin necesidad de volver a implementar.

Concepto 1: Qué es una skill de agente

Una skill de agente es una carpeta que contiene un archivo SKILL.md más scripts, referencias y recursos opcionales. La carpeta es la skill; el SKILL.md es su punto de entrada. Anthropic lanzó originalmente el formato y ahora es un estándar abierto publicado en agentskills.io, con Claude Code y OpenCode como los dos clientes de referencia actuales y una lista creciente de clientes compatibles. La especificación de agentskills.io es la fuente autorizada para las afirmaciones de compatibilidad.

La skill mínima viable:

hello-skill/
└── SKILL.md

Con contenidos:

---
name: hello-skill
description: A skill that responds with a friendly greeting tailored to the user's name and the time of day. Use when the user asks to be greeted, says hello, or starts a conversation.
---

# Hello skill

When the user greets you or asks to be greeted:

1. Determine the current local time of day.
2. Compose a friendly, time-appropriate greeting that uses the user's name if available.
3. Keep the greeting under 25 words.

Examples:

- Morning + name known: "Good morning, Sara — hope your week is starting well."
- Evening + no name: "Good evening — happy to be of help."

Esa es una skill completa y válida. Al inicio, el cliente lee el nombre y la descripción; el cuerpo se carga solo cuando el modelo selecciona la skill para una tarea. Sin código. Sin despliegue. Sin llamada SDK. Una carpeta con un archivo Markdown.

Esa propiedad de archivo en disco es lo que hace que las skills sean portátiles entre herramientas, compartibles entre equipos y versionables como cualquier otro artefacto de texto. Las skills no son objetos de Python que importas, endpoints de API que llamas ni primitivas de framework que instancias.

PRIMM, Predecir. Dada la carpeta hello-skill anterior, ¿qué carga el agente en contexto al inicio, antes de que llegue cualquier mensaje de usuario? Tres opciones: (a) el archivo SKILL.md completo, incluidas las instrucciones; (b) solo el frontmatter name y description, nada más; (c) nada, ya que las skills se cargan solo cuando se invocan. Confianza 1–5.

La respuesta es (b). Al inicio, el agente lee solo los metadatos de cada skill en su biblioteca de skills. El cuerpo completo (las instrucciones, los ejemplos, las referencias) se carga bajo demanda. Esta es revelación progresiva y es el siguiente concepto.

Prueba con IA

I want to write my first Agent Skill. Give me three skill ideas
suitable for a customer-support agent. For each, write the frontmatter
(name + description) only. The description must be specific enough
that the agent knows EXACTLY when to invoke it — vague descriptions
like "Helps with tickets" don't count. After I review your three,
ask me which I'd refine first.

Concepto 2: Revelación progresiva, el modelo de carga de skills en tres etapas

Cincuenta skills, todas cargadas a la vez, enterrarían al modelo en instrucciones que aún no necesita. Entonces, las skills se cargan en tres etapas. Cada etapa cuesta más que la anterior, y la siguiente etapa solo se carga cuando la anterior dice que la skill es relevante. Esa carga por etapas es revelación progresiva.

Etapa 1, descubrimiento. Al inicio, el agente carga name y description de cada skill disponible. La especificación recomienda mantener esta huella pequeña: aproximadamente 100 tokens por skill. Un Worker con cincuenta skills paga aproximadamente 5.000 tokens por el descubrimiento, en cada turno, en cada conversación. Ese es el costo de saber qué hay en la biblioteca.

Etapa 2, activación. Cuando el modelo decide que una tarea coincide con la descripción de una skill, carga el cuerpo SKILL.md completo en contexto. La especificación recomienda mantener esto en menos de 5.000 tokens por skill. La mayoría de las skills bien escritas oscilan entre 500 y 2.000 tokens. El costo de activación se paga solo en los turnos en los que realmente se utiliza la skill.

Etapa 3, ejecución. Si las instrucciones de la skill hacen referencia a otros archivos (un script bajo scripts/, una referencia bajo references/, una plantilla bajo assets/), esos se cargan solo cuando el agente los busca explícitamente. Algunas skills nunca cargan referencias; las skills técnicas profundas pueden cargar tres o cuatro.

Cronograma de revelación progresiva: al inicio, solo se cargan los nombres y descripciones de todas las skills (barato, pagado en cada turno). Al activarse, el cuerpo completo de SKILL.md se carga (medio, se paga solo en turnos coincidentes). Durante la ejecución, los archivos referenciados se cargan según demanda (variable, se paga solo cuando se alcanzan).

Un breve recorrido de cómo se ve esto en la práctica. Imagina que tu Worker de atención al cliente tiene 30 skills en .claude/skills/, cada una con un SKILL.md. El agente arranca; el runtime lee cada SKILL.md y extrae solo el frontmatter YAML (name y description) de los 30 archivos. Aproximadamente 3.000 tokens aparecen en el contexto del modelo como una tabla de "skills disponibles". El cuerpo de cada SKILL.md no está cargado; las referencias bajo references/ no están cargadas; los scripts bajo scripts/ ni siquiera se leen. El modelo ahora sabe qué herramientas tiene, pero no cómo usarlas.

El usuario envía un mensaje: "Ponme al día con el ticket TKT-1042". El modelo escanea su lista de 30 descripciones, encuentra coincidencia con summarize-ticket (que menciona "ID de ticket" y "ponme al día") y decide que esta skill aplica. Ahora el runtime carga el cuerpo completo de summarize-ticket/SKILL.md, unos 1500 tokens, en contexto. El modelo lee las instrucciones operativas: "extrae el ID del ticket, llama a lookup_customer, llama a find_similar_resolved_tickets, redacta un resumen de cinco secciones". Sigue las instrucciones.

El paso 3 de esas instrucciones hace referencia a references/style.md, la guía de formato para resúmenes. El modelo busca ese archivo; el tiempo de ejecución lo carga bajo demanda. Los cuerpos de las otras 29 skills, los otros archivos de referencia, los scripts bajo scripts/: nada de eso entró en el contexto del modelo. Solo se pagaron los metadatos de los 30, más el cuerpo y una referencia para la única skill que se disparó. La predicción a continuación pone números en esa brecha.

PRIMM, Predecir. Un Worker tiene 30 skills en su biblioteca. El frontmatter promedio de SKILL.md es de 80 tokens; el cuerpo promedio es de 1.500 tokens; cada skill tiene en promedio dos archivos de referencia con un total de 4.000 tokens de referencias. En un turno típico que activa una skill y lee una referencia, ¿cuál es el costo de contexto aproximado del sistema de skills? Tres opciones: (a) ~2400 tokens (metadatos de inicio + una activación + una referencia); (b) ~6900 tokens; (c) ~135.000 tokens (todo cargado todo el tiempo). Confianza 1–5.

La respuesta es (b), aproximadamente 6900 tokens: 30 × 80 tokens para descubrimiento (2400), más 1500 tokens para la skill activada, más ~2000 tokens para un archivo referenciado. El costo aumenta linealmente con el tamaño de la biblioteca durante el descubrimiento y se mantiene constante por turno durante la activación y ejecución. Esto es lo que hace que una biblioteca de skills sea asequible. Sin revelación progresiva, 30 skills × 1500 tokens de cuerpo × 4000 tokens de referencias = 165 000 tokens por turno solo para saber qué puede hacer el agente. Nadie ejecuta eso.

Dos consecuencias para el diseño de skills:

  • Las descripciones son el campo clave. Son lo que se activa en la Etapa 1 y determina si la Etapa 2 sucede o no. Una descripción vaga ("ayuda con los archivos PDF") se activa con demasiada frecuencia y quema tokens de activación para nada; una descripción precisa ("extrae texto y tablas de archivos PDF, completa formularios PDF, fusiona archivos PDF; úsala cuando trabajes con documentos PDF") se activa cuando debería y permanece silenciosa cuando no debería. La mayoría de las fallas de skills son fallas de descripción.

  • Los cuerpos largos de SKILL.md son caros. Mantén SKILL.md por debajo de ~500 líneas, idealmente entre 100 y 300. Mueve la profundidad a los archivos de referencia. El límite de 500 líneas no es arbitrario; es el umbral donde el costo de activación empieza a desplazar el contexto de razonamiento real.

Prueba con IA

I have a SKILL.md that's grown to 1,200 lines because I kept adding
edge cases. Walk me through how to split it. The skill is a
"customer-refund-issuance" skill: it has the core refund process,
five edge cases (international, partial, post-30-days, with-restocking-fee,
fraud-flagged), three reference policies (US, EU, UK), and a Python script
that calls the payment gateway. Help me decide what stays in SKILL.md,
what moves to references/, what moves to assets/, and what stays as scripts/.

Concepto 3: Escribir un SKILL.md, el contrato que hace una skill con el modelo

El archivo SKILL.md tiene dos partes: YAML frontmatter (el contrato) y el Markdown body (las instrucciones operativas). El tema central es la API orientada al agente; el cuerpo es el hacer.

La portada, por campo.

CampoRequeridoRestricciónPropósito
nameDe 1 a 64 caracteres, alfanuméricos en minúsculas + guiones, sin guiones iniciales, finales o consecutivos; debe coincidir con el nombre de la carpetaEl identificador de la skill.
description1–1024 caracteres, no vacíosLa superficie del gatillo. Lo que el agente lee en el momento del descubrimiento para decidir si invoca esta skill.
licenseNoNombre de la licencia o ruta al archivo de licencia incluidoBajo qué términos se envía la skill.
compatibilityNo≤500 caracteresRequisitos ambientales (producto previsto, paquetes de sistema, acceso a la red). La mayoría de las skills no necesitan esto.
metadataNoMapeo arbitrario de valores claveExtensiones específicas del cliente (autor, versión, etc.).
allowed-toolsNoLista de herramientas separadas por espaciosHerramientas preaprobadas que la skill puede utilizar. Experimental; El soporte varía.

El frente mínimo viable son dos campos:

---
name: my-skill
description: One sentence explaining what this skill does and when to use it.
---

Una cuestión frontal más orientada a la producción:

---
name: extract-meeting-notes
description: Extracts structured action items, decisions, and follow-ups from raw meeting transcripts. Use when the user provides a transcript, meeting recording text, or asks to summarize a meeting. Produces a markdown summary with explicit sections for Decisions, Action Items (with owner and deadline), and Follow-ups.
license: MIT
compatibility: Requires Python 3.12+ and access to the OpenAI API.
metadata:
author: panaversity
version: "1.2"
---

La descripción es el campo clave. Vuelve a leer los ejemplos buenos y malos de la especificación e interioriza la diferencia:

Bueno: "Extrae texto y tablas de archivos PDF, completa formularios PDF y combina varios archivos PDF. Úsalo cuando trabajes con documentos PDF o cuando el usuario mencione archivos PDF, formularios o extracción de documentos".

Deficiente: "Ayuda con archivos PDF".

La buena descripción dice qué ("extrae texto y tablas, completa formularios, combina"), cuándo ("cuando se trabaja con documentos PDF o cuando el usuario menciona archivos PDF, formularios o extracción de documentos") y muestra palabras clave específicas con las que el modelo puede comparar ("PDF", "formularios", "extracción"). El pobre no dice nada de eso. La calidad de la descripción es el mayor determinante de si tu skill se dispara cuando debería y permanece en silencio cuando no debería.

El cuerpo, por convención. No hay requisitos de formato; la especificación dice "escribe cualquier cosa que ayude a los agentes a realizar la tarea de manera efectiva". Pero en casi todas las skills bien escritas aparecen tres secciones:

  1. Instrucciones paso a paso. Lo que hace el agente, en pasos numerados, en lenguaje operativo. No es narrativo ("Esta skill está diseñada para ayudar con los reembolsos") sino imperativo ("Busca el pedido. Consulta la política. Emite el reembolso"). Las skills imperativas funcionan mejor que las narrativas.

  2. Ejemplos de entradas y salidas. Uno o dos ejemplos breves que muestren la forma esperada de la entrada y la forma esperada de la salida. Los ejemplos valen aproximadamente cinco veces más que las descripciones para orientar el comportamiento del modelo. No te los saltes.

  3. Casos extremos comunes. Dos o tres casos que no son obvios desde el camino feliz, generalmente los casos que realmente fallaron en producción. Los casos extremos se ganan su lugar al provenir de fallas reales, no imaginadas.

PRIMM, Predecir. Dos skills tienen el mismo campo name (summarize-document) pero se encuentran en carpetas diferentes: una en ~/.claude/skills/ (nivel de usuario) y otra en .claude/skills/ (nivel de proyecto). La tarea actual coincide con ambas descripciones. ¿Qué sucede? Tres opciones: (a) el agente elige una al azar; (b) la skill a nivel de proyecto gana debido a la precedencia de carpetas; (c) el agente carga ambas y deja que el modelo elija. Confianza 1–5.

La respuesta depende del cliente, pero la convención predominante en Claude Code y OpenCode es (b), el nivel de proyecto gana al nivel de usuario. El mismo patrón de precedencia que la resolución CLAUDE.md, el comportamiento del archivo de reglas del curso intensivo de codificación agente y la mayoría de la configuración de herramientas. El principio: el contexto más específico anula el contexto más general. Dos skills con el mismo nombre de la misma carpeta son un problema diferente; Ese suele ser un error, ya que los nombres de las carpetas deben coincidir con los valores name.

Prueba con IA

You have two skills in the same project:
- name: lookup-customer
description: Looks up a customer record.
- name: get-customer-profile
description: Retrieves customer profile information.

A user asks: "What's John Smith's email?"

Walk through three steps with the AI:

1. For each skill, predict whether it WILL fire on this prompt.
Confidence 1-5.
2. If both fire, the model has to pick one — predict which it picks
and why.
3. Rewrite BOTH descriptions so exactly one fires on this prompt,
and the other fires only on a clearly different prompt. State the
prompt that SHOULD fire the other skill.

Concepto 4: Paquete de skills, dónde viven las skills y cómo viajan

Las skills son artefactos del sistema de archivos. Eso es lo que las hace portátiles; también es lo que vuelve importantes sus convenciones de empaquetado. Si aciertas con la estructura de carpetas, tu skill funcionará en todos los clientes compatibles; si la haces mal, no funcionará en ninguno.

Donde cada herramienta busca skills.

HerramientaNivel de proyectoNivel de usuario (global)
Código Claude.claude/skills/<skill-name>/SKILL.md~/.claude/skills/<skill-name>/SKILL.md
Código abierto.opencode/skills/<skill-name>/SKILL.md~/.config/opencode/skills/<skill-name>/SKILL.md
OpenCode (alternativa).claude/skills/<skill-name>/SKILL.md~/.claude/skills/<skill-name>/SKILL.md

El fallback de OpenCode es la pieza clave para la portabilidad. OpenCode lee primero desde su propio .opencode/skills/, pero recurre a .claude/skills/ si no encuentra una skill allí. La consecuencia práctica: escribe tu skill una vez en .claude/skills/ y funcionará en ambas herramientas sin modificaciones. Esta es la recompensa más concreta del formato abierto Agent Skills. El mismo SKILL.md que envías a un usuario de Claude Code funciona para un usuario de OpenCode, byte por byte.

La estructura completa de carpetas, por directorio.

my-skill/
├── SKILL.md # Required: frontmatter + body. The entry point.
├── scripts/ # Optional: executable code the skill can run.
│ ├── extract.py
│ └── normalize.sh
├── references/ # Optional: deep documentation, loaded on demand.
│ ├── REFERENCE.md
│ └── policies/
│ └── us-refund-policy.md
└── assets/ # Optional: templates, schemas, lookup tables.
├── report-template.md
└── status-codes.json

Cada directorio tiene un trabajo específico:

  • scripts/ contiene código ejecutable que el agente puede ejecutar. Python, Bash, JavaScript: la compatibilidad con el lenguaje depende del cliente y del sandbox. El agente invoca scripts por ruta relativa (por ejemplo, scripts/extract.py). Los scripts deben ser autónomos, documentar sus dependencias en un encabezado de comentario y manejar los casos extremos con elegancia porque el agente no lo hará.

  • references/ contiene documentación detallada que el agente carga bajo demanda. La convención: mantén cada archivo de referencia enfocado en un tema (finance.md, legal.md, error-codes.md) para que el agente cargue solo lo que necesita la tarea actual. Un archivo de referencia de 5000 tokens al que el agente accede una vez por sesión es económico; uno de 50.000 tokens no lo es. Mantén las referencias a un solo nivel de profundidad desde SKILL.md. Evita el anidamiento al estilo references/category/subcategory/file.md; la especificación es explícita al respecto.

  • assets/ contiene recursos estáticos: plantillas de documentos, plantillas de configuración, tablas de búsqueda, esquemas, imágenes. Estos son consumidos por el agente (o por scripts que ejecuta el agente) pero no leídos como instrucciones.

La sintaxis de referencia de archivos dentro de SKILL.md. Usa rutas relativas desde la raíz de la skill:

For policy details, see [the US refund policy](references/policies/us-refund-policy.md).

To extract the structured data, run `scripts/extract.py`.

The output should follow the template in `assets/report-template.md`.

El agente lee el cuerpo de SKILL.md, ve la ruta y carga el archivo al que se hace referencia a pedido. Sin sintaxis especial, sin ejercicios de resolución de enlaces, solo rutas relativas.

Comprobación rápida. Tienes una skill en .claude/skills/refund-issuance/SKILL.md que hace referencia a references/policies/us.md. La skill se invoca mientras el directorio de trabajo del agente es /home/user/projects/customer-support. ¿Dónde busca el agente el archivo de política? Respuesta: /home/user/projects/customer-support/.claude/skills/refund-issuance/references/policies/us.md; las rutas son relativas al directorio de la skill, no al directorio de trabajo del agente. Si haces esto mal, tu skill se romperá de forma sutil durante el despliegue.

Prueba con IA

I'm porting a skill from Claude Code to OpenCode and want to verify
my folder layout is correct. The skill is at .claude/skills/extract-meeting-notes/
and contains SKILL.md, scripts/parse.py, and references/glossary.md.

For each of these claims, tell me TRUE or FALSE and explain why:

1. Without any changes, this skill will be discovered by OpenCode.
2. If I move the folder to .opencode/skills/extract-meeting-notes/,
Claude Code will stop discovering it.
3. The script reference inside SKILL.md should use the absolute path
~/.claude/skills/extract-meeting-notes/scripts/parse.py.
4. If two skills have the same name and one is in .claude/skills/
and the other in ~/.claude/skills/, the user-level one wins.

Concepto 5: Skills de composición, cuándo encadenar skills pequeñas versus escribir una grande

Las skills componen. Una capacidad de "informe semanal sobre la salud del cliente" podría ser una skill gigante que investiga, redacta, formatea y revisa de una sola vez. O podrían ser cuatro skills (research-customer-health, draft-customer-health-report, format-customer-health-report, review-customer-health-report) que se transfieren entre sí a través del sistema de archivos.

Ambos funcionan. Tienen propiedades muy diferentes.

Una gran skill. Más fácil de descubrir (una descripción, una coincidencia). Menor costo de descubrimiento (una entrada en la etapa de descubrimiento). Acoplamiento más estrecho: cuando se activa la activación, todos los pasos se ejecutan, en orden, en un contexto. Difícil de probar de forma aislada. Es difícil reutilizar una pieza en otro lugar. Cuando algo falla a mitad de camino, el modelo tiene que recuperarse con todo el trabajo anterior ahora irrelevante todavía en contexto.

Muchas skills pequeñas. Mayor costo de descubrimiento (cuatro entradas en lugar de una). Mayor coste de orquestación de activación (algo tiene que encadenarlos). Acoplamiento más flexible: cada paso se puede probar, reemplazar o reutilizar de forma independiente. Cuando uno falla, el fracaso está localizado; Los artefactos de los pasos anteriores ya están en el disco. Cada paso recibe una nueva activación, lo que significa que no hay contaminación de contexto acumulada.

Skills de composición: una skill monolítica de &#39;informe de salud del cliente&#39; (arriba) ejecuta cuatro pasos en un contexto con una activación, frente a cuatro skills pequeñas (abajo) que se transmiten a través de archivos tmp/. La versión monolítica es más sencilla de descubrir pero acumula contexto en todos los pasos; la versión encadenada paga cuatro activaciones, pero cada una comienza de nuevo, se puede reutilizar sola y deja artefactos intermedios en el disco para su depuración.

La regla de decisión que funciona en la práctica:

Escribe una skill si los pasos están estrechamente relacionados y rara vez se reutilizan de forma aislada. Escribe muchas skills si algún paso puede invocarse por sí solo desde un flujo de trabajo diferente o si el aislamiento del contexto entre pasos vale más que la simplicidad de la orquestación.

Para un trabajador que brinda atención al cliente, summarize-ticket es probablemente su propia skill (utilizada por humanos, utilizada por el flujo de escalada, utilizada por el resumen semanal, utilizada por la repetición de auditoría). escalate-to-tier-2 es probablemente su propia skill (se reutilizan los criterios de escalada y los requisitos de tono). La descomposición es valor de aislamiento versus costo de orquestación; el aislamiento generalmente gana más allá de dos o tres pasos compuestos.

El patrón de transferencia del sistema de archivos. Cuando las skills componen, la transferencia más limpia es el sistema de archivos, no la conversación. La skill A escribe su salida en tmp/research-customer-{id}.md. Se invoca la skill B, lee desde tmp/research-customer-{id}.md, escribe en tmp/draft-customer-{id}.md. La skill C lee el borrador y escribe el informe. La conversación sólo ve el informe final; Los artefactos intermedios viven en el disco donde el agente puede volver a leerlos, el ser humano puede inspeccionarlos y la pista de auditoría puede encontrarlos más tarde. Este es el mismo patrón que se utilizó en el curso anterior para el aislamiento de resultados de subagente: la misma información, aplicada en la granularidad de las skills.

customer-health-pipeline/
├── tmp/ # ephemeral handoff dir
│ ├── research-customer-{id}.md # skill A output
│ ├── draft-customer-{id}.md # skill B output
│ └── reviewed-draft-customer-{id}.md # skill C output
└── output/
└── customer-health-{id}.pdf # skill D final

Aquí es también donde las siguientes dos partes del curso empiezan a importar. Algunas transferencias de skills no pertenecen al sistema de archivos: pertenecen al sistema de registro. La instantánea de la "salud del cliente" de la Skill A no solo alimenta la Skill B; también es un registro que la empresa debe mantener, para consultarlo más tarde, para encontrar casos similares en contra, para auditar si el cliente se queja seis semanas después. Una skill que escribe en un archivo temporal es un borrador. Una skill que se escribe en el sistema de registro es una acción. Esa distinción es lo que construye la Parte 2.

Prueba con IA

Compare two design approaches for a customer-refund-issuance workflow:

Design A: One big skill called "issue-refund" that handles eligibility check,
policy lookup, amount calculation, gateway call, ticket update, and customer
notification.

Design B: Five small skills (check-refund-eligibility, lookup-refund-policy,
calculate-refund-amount, call-payment-gateway, update-support-ticket,
notify-customer) chained via filesystem handoffs in tmp/.

For each design, name (1) one situation where it's the right choice and
(2) one specific failure mode it's vulnerable to. Then tell me which
design you'd ship and why.
Skills de escritura para modelos menos disciplinados

Los conceptos 1 a 5 describen cómo deberían funcionar las skills cuando el modelo sigue firmemente las instrucciones (Claude Sonnet/Opus, clase GPT-5, Gemini 2.5 Pro). Con un modelo más pequeño o más barato (deepseek-chat, clase Haiku, Llama-70B, Mistral, la mayoría de los modelos locales), tres cosas cambian:

  1. Secuenciación de skills múltiples. Los imperativos de SKILL.md como "SIEMPRE ejecuta esto ANTES de redactar" o "llamar a X, luego Y, luego Z" son confiables en los modelos fuertes y no confiables en los más débiles. La solución es agregar un breve preámbulo de FLUJO GENERAL en el mensaje del sistema del agente que detalla el orden. Los cuerpos de HABILIDAD siguen siendo declarativos; el indicador del sistema proporciona la estructura de orquestación.
  2. Deriva de formato. Un modelo más débil agregará silenciosamente emojis, tablas de Markdown, encabezados de "Acción realizada" o parafraseará tus entradas incluso cuando el cuerpo de SKILL diga "salida en cinco párrafos, sin tablas, conserva el texto del usuario palabra por palabra". Sé más explícito con un modelo más débil: enumera lo que NO debe hacer, no solo lo que debe hacer.
  3. Ceguera del disparador. Las descripciones que se activan con "resumir ticket TKT-1042" pueden omitir "cuál es la historia del n.° 1042" en un modelo más pequeño. Escribe descripciones con frases desencadenantes más concretas (la disciplina del Concepto 3 importa más, no menos, cuando el modelo es más débil).

Regla general: presupuestar el esfuerzo del modelo fuerte en SKILL.md, presupuestar el esfuerzo del modelo débil en el indicador del sistema. La arquitectura todavía funciona en modelos más débiles, simplemente escribe más andamios a su alrededor. Los límites entre Skills, MCP y la base de datos siguen siendo los mismos; sólo cambian las indicaciones.


Parte 2: Neon Postgres + pgvector como sistema de registro

La Parte 1 le dio capacidades al agente. Ahora necesita estado contra el cual trabajar: historial del cliente, conversaciones anteriores, biblioteca de políticas, registro de auditoría. El agente del curso anterior mantenía el estado en un archivo SQLite local y los datos auxiliares en listas de Python. Eso funciona para una demostración. No funciona para un Worker que debe responder "¿qué le dijimos a este cliente hace seis semanas?" o "¿hemos visto esta pregunta antes y cómo se resolvió?".

Esta Parte reemplaza ambos con un sistema de registro: el término de tesis para el almacén duradero del que la fuerza laboral lee y en el que escribe. Usamos Neon Postgres con la extensión pgvector. La arquitectura es fija; Neon es el producto. Cualquier Postgres duradero y gobernado satisface el requisito.

Hojea el camino en una primera pasada. La Parte 2 es densa (seis tablas, SQL, pipelines de embeddings, matemáticas de índice vectorial). Si tienes Postgres oxidado, lee los Conceptos 6 y 7 para entender la forma y hojea los Conceptos 8 a 10. El Worker mínimo viable no necesita las seis tablas el primer día: messages + embeddings basta para sentir que la arquitectura funciona. Agrega documents cuando tengas una biblioteca real para convertir en embeddings; audit_log + capability_invocations la primera vez que necesites responder "¿qué hizo el agente a las 3 a. m. del martes pasado?"; conversations cuando un usuario tenga más de una. El esquema de seis tablas de abajo es donde terminas, no donde empiezas.

Concepto 6: Por qué Postgres administrado y por qué Neon específicamente

La tesis se mantiene independiente del producto en cuanto a los sistemas de registro: "las bases de datos, flujos de trabajo y plataformas operativas existentes de AI-Native Company (CRM, ERP, sistemas de emisión de tickets, almacenes de datos, libros de contabilidad) sirven como sistema de registro". Sin embargo, para un agente que creas desde cero, tienes que elegir algo. La pregunta no es "Postgres versus MongoDB versus una base de datos vectorial". Es "qué Postgres".

Por qué Postgres, no una base de datos vectorial dedicada. Tres razones que se mantendrán incluso en 2026.

  1. Una base de datos, una transacción, un límite de autenticación. Una base de datos vectorial más una base de datos relacional significa dos almacenes para mantener la coherencia, dos sistemas de autenticación para el alcance, dos canales de respaldo para mantener. La extensión pgvector coloca vectores junto a los registros de usuario, registros de tickets y registros de auditoría con los que están relacionados: una JOIN es una JOIN, no un salto de red entre dos servicios con consistencia eventual. Todos los principales proveedores administrados de Postgres (AWS RDS, Google Cloud SQL, Azure, Supabase, Neon) incluyen pgvector de forma predeterminada y se ubica constantemente entre las extensiones de Postgres más instaladas. Para la mayoría de las cargas de trabajo, es la respuesta empírica a "¿es esto suficiente?". Casi siempre lo es.

  2. Postgres ya hace las partes difíciles. Transacciones, índices, claves externas, seguridad a nivel de fila, recuperación en un momento dado, planificación de consultas. Una base de datos vectorial dedicada tiene que inventarlos desde cero y, por lo general, hace algunos peores. La opción aburrida por defecto tiene ventajas compuestas.

  3. Existen servidores MCP para Postgres en cada capa. Neon ofrece uno (para administración). Existen servidores MCP generales para Postgres (para ejecución de SQL). Puedes escribir el tuyo propio (para acceso en tiempo de ejecución con alcance). El ecosistema MCP alrededor de Postgres es el más maduro.

Por qué Neon específicamente: tres diferenciadores.

  • Serverless con escala a cero. El nivel gratuito no cuesta nada cuando está inactivo. Un Worker que maneja 50 conversaciones por día pasa la mayor parte del tiempo costando $0, no $50/mes por una instancia aprovisionada. Es fundamental para la economía de una empresa nativa de IA con varios Workers, donde muchos Workers tienen uso esporádico.

  • Branching. Una base de datos de Neon se ramifica en segundos: un clon completo copy-on-write de tus datos de producción, listo para consultar. El uso previsto es desarrollo/pruebas y seguridad de migraciones; el uso relevante para agentes es permitir que el agente experimente en una rama sin tocar producción. Una migración que sale mal en una rama se revierte eliminando la rama. La misma operación en una base de datos sin branching exige restaurar desde una copia de seguridad.

  • Integración MCP de primera clase. Neon incluye un servidor MCP oficial (remoto en https://mcp.neon.tech/mcp, con autenticación OAuth) que expone gestión de proyectos, ciclo de vida de ramas, ejecución de SQL y herramientas de migración a cualquier cliente MCP. Lo usaremos para desarrollo. Y esto es clave: Neon no recomienda explícitamente el servidor Neon MCP para uso en runtime de producción. Es una interfaz potente de lenguaje natural; los agentes de producción hablan con Postgres mediante servidores MCP de alcance más limitado que escribes tú, o mediante conexiones directas. La distinción importa; la Parte 3 la vuelve operativa.

Verificación rápida. Tres afirmaciones; marca cada una como Verdadero o Falso antes de leer el siguiente párrafo: (a) El servidor Neon MCP está diseñado para ser la conexión de base de datos en tiempo de ejecución para Workers de IA en producción. (b) Una base de datos de Neon ramificada desde producción empieza con todos los datos de producción ya presentes. (c) Una base de datos de Neon que no ha recibido una consulta en una hora todavía cuesta dinero en el nivel gratuito.

Respuestas: (a) Falso: La propia documentación de Neon dice que el servidor MCP es solo para desarrollo/prueba. (b) Verdadero: las ramas se copian en escritura, por lo que la rama comienza como un clon lógico de producción sin movimiento de datos. (c) Falso: escalar a cero significa que las bases de datos inactivas no cuestan nada en el nivel gratuito. Estas tres respuestas son la forma operativa de por qué Neon encaja como SoR.

Prueba con IA

Your customer-support Worker handles EU customers. Your legal team
says: "All customer data must stay in Frankfurt." Neon's free tier
offers US-East regions only; their paid tier offers EU regions.

For each of these three approaches, decide GO or NO-GO and give ONE
sentence of justification:

A) Use Neon's paid tier in an EU region (pay-as-you-go, typically $25–75/month for a single Worker per [Neon's pricing](https://neon.com/pricing)) and ship.
B) Use Postgres on a Frankfurt VPS you manage yourself (no pgvector
MCP server, no branching, no scale-to-zero) and ship.
C) Use Neon's free US tier; embed everything client-side before
sending; tell legal it's "encrypted at rest."

Then say which one you'd actually pick and what you'd ask legal
before committing.

Concepto 7: El esquema del trabajador, qué tablas necesita realmente un agente

El sistema de registro de un Trabajador no consiste simplemente en "almacenar conversaciones en algún lugar". Es un esquema estructurado que respalda las cuatro cosas que la tesis dice que todo Trabajador hace: leer la verdad, escribir resultados, dejar rastros, encontrar trabajos anteriores similares. Seis tablas cubren el 90% del caso. Agregará más para detalles específicos del dominio; Estas son las seis claves.

-- 1. CONVERSATIONS: the top-level unit of work
CREATE TABLE conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ended_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
-- searchable summary; populated by the agent at conversation end
summary TEXT
);
CREATE INDEX idx_conversations_user ON conversations(user_id, started_at DESC);

-- 2. MESSAGES: the turns of a conversation
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'tool', 'system')),
content TEXT NOT NULL,
-- token counts let you reason about cost without re-counting
input_tokens INT,
output_tokens INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_messages_conv ON messages(conversation_id, created_at);

-- 3. DOCUMENTS: the agent's reference library
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source TEXT NOT NULL, -- 'policy_library', 'kb_article', 'past_case', etc.
title TEXT NOT NULL,
body TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_documents_source ON documents(source);

-- 4. EMBEDDINGS: vector representations of documents AND past conversations
CREATE TABLE embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- one of these is populated; the other is NULL
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
chunk_text TEXT NOT NULL,
chunk_index INT NOT NULL,
embedding VECTOR(1536) NOT NULL,
model TEXT NOT NULL, -- 'text-embedding-3-small', etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (
(document_id IS NOT NULL)::int + (conversation_id IS NOT NULL)::int = 1
)
);
-- the key index for semantic search; see Concept 8
CREATE INDEX idx_embeddings_hnsw
ON embeddings USING hnsw (embedding vector_cosine_ops);

-- 5. AUDIT_LOG: every action the Worker takes, replayable forever
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
actor TEXT NOT NULL, -- 'worker:customer-support', 'system', etc.
action TEXT NOT NULL, -- see "canonical action vocabulary" below
target TEXT, -- table name, skill name, etc.
payload JSONB NOT NULL, -- the data of the action
result JSONB, -- what happened
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_conv ON audit_log(conversation_id, created_at);
CREATE INDEX idx_audit_action ON audit_log(action, created_at);

-- 6. CAPABILITY_INVOCATIONS: every skill or tool call, for replay and metrics
CREATE TABLE capability_invocations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
capability TEXT NOT NULL, -- 'skill:summarize-ticket', 'tool:search_docs', etc.
arguments JSONB NOT NULL,
result JSONB,
status TEXT NOT NULL CHECK (status IN ('ok', 'error', 'timeout')),
latency_ms INT,
cost_cents INT, -- approximate cost in 1/100 cents
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_cap_conv ON capability_invocations(conversation_id, created_at);

Esquema de trabajador: seis tablas. Las conversaciones y los mensajes mantienen el diálogo. Los documentos e incrustaciones contienen la biblioteca de referencia y sus vectores. El registro de auditoría y las invocaciones de capacidades mantienen el seguimiento. Las claves externas los vinculan en un sistema de registro consistente.

Algunas notas sobre las decisiones de diseño, porque todas importan:

  • Una tabla de embeddings para documentos y conversaciones. Una restricción CHECK garantiza que se establezca exactamente uno de document_id o conversation_id. Esto permite hacer una búsqueda semántica entre "políticas Y conversaciones pasadas" en una sola consulta; la pregunta "¿hemos respondido esto antes?" obtiene un índice, no dos.

  • audit_log usa BIGSERIAL, no UUID. Las filas de auditoría se escriben constantemente; la clave entera más simple mantiene las inserciones rápidas y los pedidos triviales. Las otras tablas usan UUID porque las filas abandonan la base de datos (en respuestas API, en URL) y los UUID evitan filtrar recuentos de filas.

  • capability_invocations separa las skills de las herramientas. Una invocación de skill y una invocación @function_tool son conceptualmente similares, pero operativamente diferentes (distintas rutas de código, perfiles de costos y modos de falla). Guardarlas en una tabla con un discriminador capability permite consultarlas juntas cuando preguntas "¿qué hizo el agente?", sin perder la posibilidad de separarlas al preguntar "¿cuánto costaron las skills frente a las herramientas?".

  • Las columnas metadata JSONB son vías de escape. El esquema no puede predecir todos los campos específicos del dominio que necesitarás; JSONB te permite agregar campos sin migraciones. Úsalo con moderación: los campos que aparecen en muchas consultas deben promoverse a columnas.

Este es el esquema operativo mínimo para un Worker: tres tablas para el trabajo (conversaciones, mensajes, documentos), una para embeddings, dos para el seguimiento (audit_log, capability_invocations). Agregarás más para detalles específicos del dominio: una tabla customers, una tabla tickets, una tabla policies con control de versiones. Tienen la misma forma: datos relacionales que el agente lee y escribe a través de MCP.

PRIMM, Predecir. Un Worker maneja 200 conversaciones/día, cada una con un promedio de 10 mensajes, de los cuales el 30 % activa una invocación de skill y el 50 % escribe dos filas de auditoría más allá de la fila de skill. Después de un mes (30 días), ¿cuántas filas aproximadamente hay en cada una de las seis tablas? Tres opciones: (a) volúmenes similares en las seis (entre 6.000 y 60.000 cada una); (b) audit_log eclipsa a las demás entre 10× y 50×; (c) los embeddings eclipsan todo porque cada mensaje queda convertido en embedding. Confianza 1–5.

La respuesta es (b): audit_log será el más grande por un amplio margen porque cada interacción produce múltiples filas de auditoría: mensaje enviado, skill invocada, escritura en la base de datos, escritura en la base de datos nuevamente, conversación cerrada, etc. Una estimación aproximada: conversaciones ~6.000; mensajes ~60.000; capacidades_invocaciones ~1.800; audit_log probablemente entre 150.000 y 300.000. Planifica la retención y la indexación en consecuencia; audit_log es la tabla que particionarás primero a medida que crezca.

Prueba con IA

I want to extend this schema for a customer-support Worker that
handles software bug reports specifically. What three additional
tables would you add, and what columns would they have? For each,
say what the agent will use it for (read access? write access?
both?) and what foreign keys connect it to the six core tables.

Concepto 8: conceptos básicos de pgvector, tipos, operadores de distancia, índices

La tabla embeddings permite que el Worker busque texto almacenado por significado, no solo por coincidencia exacta. Ese texto puede ser una memoria, una entrada del sistema de registro o un documento. La búsqueda la impulsa pgvector: una extensión de Postgres que agrega tipos de datos vectoriales y operadores de búsqueda por similitud. Viene incluida por defecto en los principales proveedores de Postgres administrado. Para la mayoría de las cargas de trabajo, responde a "¿debería usar una base de datos vectorial independiente?". La respuesta es no.

El tipo de vector. VECTOR(n) es una columna de punto flotante de longitud fija. n es la dimensionalidad de tus embeddings: 1536 para text-embedding-3-small de OpenAI, 3072 para text-embedding-3-large, y varía en otros modelos. La dimensión debe coincidir con el modelo que produjo el embedding. Mezclar dimensiones es el error más común de pgvector: insertas vectores de 1536 dimensiones del modelo A, consultas con vectores de 1536 dimensiones del modelo B, y los resultados no tienen sentido aunque cada consulta "funcione".

Para dimensiones superiores a 2000, el tipo HALFVEC utiliza flotantes de media precisión (16 bits en lugar de 32 bits, menos precisión por valor) y aproximadamente reduce a la mitad el almacenamiento con un costo de recuperación menor. Para nuestro caso 1536-dim, simple VECTOR(1536) está bien.

Los tres operadores de distancia. pgvector expone tres formas de medir qué tan similares son dos vectores. Elige uno para tu caso de uso y mantenlo; mezclar operadores crea confusión.

OperadorNombreQué mideCuándo utilizar
<->Distancia L2 (euclidiana)Distancia en línea recta en un espacio n-dimensionalIncrustaciones de imágenes, similitud geométrica
<#>Producto interior negativoProducto escalar (negado)Cuando tu modelo de embedding produce vectores no normalizados y te importa la magnitud
<=>Distancia del cosenoÁngulo entre vectores, independientemente de su magnitudEmbeddings de texto: el valor predeterminado para nuestro caso

text-embedding-3-small y text-embedding-3-large de OpenAI producen vectores normalizados, lo que significa que la distancia del coseno y la distancia euclidiana dan clasificaciones equivalentes. Usa la distancia del coseno (<=>) por convención para embeddings de texto; es la opción más común, la más documentada y por eso las operaciones de índice se llaman vector_cosine_ops.

Una consulta de búsqueda semántica, en su totalidad:

-- Find the 5 documents most similar to the user's question
SELECT d.id, d.title, d.body,
e.embedding <=> $1 AS distance
FROM embeddings e
JOIN documents d ON d.id = e.document_id
WHERE e.document_id IS NOT NULL -- excludes conversation embeddings
ORDER BY e.embedding <=> $1 -- smaller distance = more similar
LIMIT 5;

$1 aquí es el embedding de la pregunta del usuario, generado en el momento de la consulta. El operador <=> es lo que convierte este O(n) en O(log n) dado el índice correcto.

Dos tipos de índice: HNSW y IVFFlat. pgvector tiene dos opciones de índice prácticas. A partir de 2026, la recomendación ha convergido:

  • Empieza con HNSW a menos que tengas una razón para no hacerlo. Basado en grafos (un grafo de vecinos en memoria que la base de datos recorre en el momento de la consulta), se construye lentamente pero consulta rápido, tiene la compatibilidad de operadores más amplia y un rendimiento predecible. Es el valor predeterminado para nuevos proyectos.
  • Usa IVFFlat si el tiempo de construcción importa más que el tiempo de consulta. Basado en particiones (los vectores agrupados en depósitos se buscan por turno), se construye entre 5 y 6 veces más rápido que HNSW, pero las consultas son más lentas. Es útil cuando reconstruyes el índice con frecuencia o tienes una gran carga de inserción.
  • DiskANN existe (a través de la extensión pgvectorscale de Timescale, un complemento independiente) para índices muy grandes que no caben en la RAM. Es casi seguro que no lo necesitas.

El índice HNSW del esquema anterior:

CREATE INDEX idx_embeddings_hnsw
ON embeddings USING hnsw (embedding vector_cosine_ops);

Dos perillas de ajuste que configurarás si ajustas algo: m (el número de conexiones por nodo, predeterminado 16) y ef_construction (el esfuerzo en tiempo de construcción, predeterminado 64). Los valores predeterminados son sensatos para la mayoría de las cargas de trabajo; tócalos solo cuando hayas hecho una evaluación comparativa.

Comprobación rápida. Tres afirmaciones para marcar Verdadero o Falso. (a) Puedes tener varios índices HNSW en la misma columna embedding, uno por operador de distancia. (b) Insertar un vector en una tabla con un índice HNSW genera más costo que insertarlo en una tabla sin índice vectorial. (c) Se puede crear un índice HNSW antes de cargar cualquier dato en la tabla. Respuestas: las tres son Verdaderas. Pgvector permite indexar para múltiples operadores de distancia (poco común pero posible), el costo de mantenimiento del índice es real (razón por la cual algunas cargas de trabajo prefieren insertar por lotes y luego indexar) y HNSW (a diferencia de IVFFlat) no necesita un paso de entrenamiento.

Prueba con IA

Two scenarios. For each, pick HNSW or IVFFlat and justify with one
specific property of the index:

Scenario A: A research index of 10M scientific papers. Built once,
queried millions of times. Build time is "whatever it takes —
overnight is fine." Query latency directly affects user experience.

Scenario B: A live index of customer support tickets that's
re-indexed every 4 hours because thousands of new tickets stream in.
Query patterns are simple (top-5 nearest neighbors). The current
HNSW build takes 20 minutes — a third of the re-index cycle.

After you answer: name ONE thing that would change your answer for
each scenario. Be specific about what you'd need to see in
production metrics before switching.

Concepto 9: pipeline de embeddings, texto de entrada, vector consultable de salida

Un embedding convierte un fragmento de texto en un punto en el espacio. El texto sobre reembolsos aparece cerca de otro texto sobre reembolsos; el texto sobre errores de inicio de sesión llega a otra parte. Entonces "buscar tickets similares" se convierte en "encontrar los puntos más cercanos". Esa es toda la idea. El resto es mecánica.

La mecánica consta de cuatro pasos y cada uno tiene una decisión que importa:

  1. Divide el documento en fragmentos lo suficientemente pequeños como para contener una idea cada uno.
  2. Genera el embedding de cada fragmento llamando al modelo; recuperas su punto.
  3. Almacena el texto, su punto y algunos metadatos en la tabla embeddings.
  4. Consulta convirtiendo también la pregunta del usuario en un punto y buscando luego los puntos almacenados más cercanos.

El proceso de incrustación: el documento de política sin procesar (arriba a la izquierda) está dividido en límites semánticos en partes de ~400 tokens con superposición; cada fragmento se incrusta por lotes en un vector de 1536 dimensiones a través de text-embedding-3-small de OpenAI; los vectores se almacenan como filas VECTOR(1536) en la tabla de incrustaciones con un índice HNSW. En el momento de la consulta (abajo), la pregunta del usuario pasa por el mismo paso de inserción, luego una única consulta SQL con el operador coseno-distancia encuentra los 5 fragmentos más cercanos. En la parte inferior se indican tres trampas que se deben evitar: discrepancia de modelo, tamaño de fragmento incorrecto, mezcla de fuentes sin filtro.

Fragmentación. Los modelos de embedding tienen límites de tokens: text-embedding-3-small de OpenAI acepta hasta 8.191 tokens por fragmento, mucho más de lo que normalmente deseas. La pregunta no es "cuál es el máximo", sino "cuál es el tamaño del fragmento que preserva la unidad de significado que buscará el usuario". Tres reglas que funcionan en la práctica:

  • Parte de límites semánticos cuando puedas. Encabezados de sección, saltos de párrafo, marcadores estructurales en tu fuente. Un fragmento que termina a mitad de una frase se recupera mal.
  • Objetivo entre 200 y 500 tokens por fragmento para la mayoría del texto. Lo suficientemente largo para transmitir significado, lo suficientemente corto para ser específico. Los documentos con fragmentos más largos tienden a "coincidir débilmente con todo" en lugar de "coincidir firmemente con lo correcto".
  • Incluya entre un 10% y un 20% de superposición entre fragmentos para los fragmentos que se encuentran a través de límites naturales. La superposición cuesta almacenamiento, pero mejora la recuperación cuando la pregunta del usuario se encuentra cerca del límite de un fragmento.
  • No fragmentes lo que ya es corto. Los documentos que ya tienen menos de ~300 tokens (tickets resueltos únicos, párrafos cortos de políticas, entradas de preguntas frecuentes) no necesitan fragmentación; incrustarlos como una sola pieza. El siguiente fragmento está diseñado para documentos fuente más extensos (PDF de políticas, artículos de base de conocimientos, transcripciones de conversaciones). Para el corpus inicial de tickets resueltos cortos del ejemplo trabajado, puede omitir el fragmentador e incrustar cada ticket como un solo fragmento (la Decisión 5 almacena cada ticket como una fila documents cuyo body es summary || '\n' || resolution y lo incrusta).

Una función de fragmentación escrita para el ejemplo resuelto:

# src/chat_agent/embedding/chunker.py
from dataclasses import dataclass

@dataclass(frozen=True)
class Chunk:
text: str
index: int
source_offset: int # character offset in the original document

def chunk_text(
text: str,
target_tokens: int = 400,
overlap_tokens: int = 60,
) -> list[Chunk]:
"""Split text into overlapping chunks at paragraph boundaries.

Approximates tokens as words * 0.75 — fine for chunking; not for
actual token counting. Use tiktoken for precise counts.
"""
paragraphs: list[str] = [p.strip() for p in text.split("\n\n") if p.strip()]
target_words: int = int(target_tokens / 0.75)
overlap_words: int = int(overlap_tokens / 0.75)

chunks: list[Chunk] = []
current: list[str] = []
current_word_count: int = 0
source_offset: int = 0

for para in paragraphs:
para_words: int = len(para.split())
if current_word_count + para_words > target_words and current:
chunks.append(Chunk(
text="\n\n".join(current),
index=len(chunks),
source_offset=source_offset,
))
# carry overlap forward
overlap_chunk: list[str] = []
overlap_count: int = 0
for prev in reversed(current):
if overlap_count + len(prev.split()) > overlap_words:
break
overlap_chunk.insert(0, prev)
overlap_count += len(prev.split())
current = overlap_chunk
current_word_count = overlap_count
source_offset += sum(len(p) for p in current) + 2 * len(current)
current.append(para)
current_word_count += para_words

if current:
chunks.append(Chunk(
text="\n\n".join(current),
index=len(chunks),
source_offset=source_offset,
))
return chunks

Incrustación. Llame al modelo con fragmentos por lotes (la API de OpenAI acepta matrices de entradas en una solicitud, mucho más eficiente que una llamada por fragmento):

# src/chat_agent/embedding/embedder.py
from openai import AsyncOpenAI

EMBEDDING_MODEL: str = "text-embedding-3-small"
EMBEDDING_DIM: int = 1536 # the table column must match

async def embed_chunks(
client: AsyncOpenAI, chunks: list[str],
) -> list[list[float]]:
"""Embed a batch of chunks. Returns one vector per chunk, in order."""
response = await client.embeddings.create(
model=EMBEDDING_MODEL, input=chunks,
)
return [item.embedding for item in response.data]
Registra pgvector con asyncpg, o tus INSERT fallarán silenciosamente

Los vectores que devuelve embed_chunks son Python list[float]. De fábrica, asyncpg no sabe que la columna VECTOR(1536) de pgvector espera un vector literal serializado, por lo que los envía como una matriz real[] de Postgres y el INSERT genera errores con expected vector, got list[float] o (peor) almacena silenciosamente algo que falla al consultar. Registra el tipo una vez por conexión antes de escribir embeddings:

from pgvector.asyncpg import register_vector

# Option A: register on every connect
conn = await asyncpg.connect(NEON_DATABASE_URL)
await register_vector(conn)

# Option B (recommended for production): pool init callback so every pooled
# connection registers automatically, no chance of forgetting.
pool = await asyncpg.create_pool(
NEON_DATABASE_URL, init=register_vector,
)

Solo necesitas esto en conexiones que leen o escriben la columna embedding. La Decisión 5 usa el formulario de pool de abajo; el servidor MCP de la Decisión 6 usa el mismo patrón.

¿Qué pasa si tu proveedor de inferencia no es OpenAI? OpenAI es el único proveedor importante que también ofrece una API integrada de embeddings de primera clase. Si diriges la inferencia a través de DeepSeek, Anthropic, Gemini o un modelo local, tienes cuatro opciones. Elige una antes de crear la columna VECTOR(n): la dimensión de la columna debe coincidir con el modelo de embedding.

ModeloDimensionesCosto (entrada, por 1 millón de tokens)Donde viveCuándo utilizar
text-embedding-3-small1536$0,02OpenAI (y agregadores compatibles con OpenAI como OpenRouter)El valor predeterminado si tiene una clave OpenAI. Barato, rápido, bueno para la mayoría de las recuperaciones.
text-embedding-3-large3072$0,13 (estándar) / $0,065 (lote)OpenAICuando has medido que -small rinde por debajo de lo necesario.
embed-english-v3 / embed-multilingual-v31024$0,10CohereCuando ya estás en Cohere para inferencia o cuando la recuperación multilingüe es importante.
voyage-3 / voyage-3-lite1024$0,06 / $0,02ViajeIncorporaciones como servicio; Se integra limpiamente con pilas de forma antrópica.
all-MiniLM-L6-v2 / bge-small-en-v1.5 (local)384 / 384"gratis" (su cálculo)paquete sentence-transformers; ejecuta solo CPU sin llamada APICuando su proveedor de inferencia no tiene API integrada (DeepSeek, la mayoría de los LLM locales) o cuando la residencia de datos prohíbe enviar texto a un tercero.

El número de costo principal: incorporar 50.000 fragmentos a ~300 tokens cada uno = 15 millones de tokens × $0,02/M = $0,30 en OpenAI. Lo mismo con -large cuesta $1,95. Con un modelo local de 384 atenuaciones, $0 más 30 segundos de CPU. Las incrustaciones son la línea más barata de la factura; la elección rara vez mueve el dial del dólar, pero sí mueve la arquitectura.

La dimensión es el contrato. VECTOR(1536) solo acepta vectores de 1536 dimensiones. Si cambia de text-embedding-3-small (1536) a all-MiniLM-L6-v2 (384), vuelve a crear la columna como VECTOR(384) y vuelve a incrustar cada fila. No hay "encaja si está lo suficientemente cerca" con pgvector; Las dimensiones son absolutas.

Reincrustación. ¿Cuándo se vuelve a incrustar? Tres desencadenantes:

  1. El documento fuente cambió. Elimina y vuelve a insertar todos los embeddings cuyo document_id coincida.
  2. Cambió el modelo de embedding. La migración de tu vida: si cambias de -small a -large, todos los embeddings existentes son incompatibles con los nuevos. Vuelve a generar todos los embeddings o ejecuta dos columnas de embedding durante la transición.
  3. Cambió la estrategia de fragmentación. Si decides que 400 tokens estaban mal y 250 son correctos, vuelve a fragmentar y a generar embeddings. Versionar tus fragmentos (almacenar chunk_strategy_version en metadata JSONB) permite hacerlo de forma segura.

PRIMM, Predecir. Has generado embeddings de 100.000 fragmentos con text-embedding-3-small. Luego decides generar embeddings también de todos los mensajes (no solo de los documentos) para que el agente pueda hacer búsquedas del tipo "¿hemos hablado de esto antes?". Escribes los embeddings de mensajes en la misma tabla embeddings con la misma columna. Una consulta de búsqueda semántica (encontrar los 5 vecinos más cercanos a una pregunta de usuario, sin filtro) arroja resultados mixtos de documentos y mensajes. ¿Es esto lo que querías? ¿Cuál es la forma de consulta correcta? Confianza 1–5.

La respuesta: casi seguro que no es lo que querías. Mezclar documentos y mensajes en los resultados de recuperación sin distinguirlos produce respuestas incoherentes: el modelo ve un resultado principal que es el mensaje anterior de un cliente y lo trata como documentación autorizada. El patrón correcto es filtrar por tipo de fuente en la consulta: ya sea uniendo y filtrando por WHERE document_id IS NOT NULL para la búsqueda de documentos, o ejecutando dos búsquedas separadas y clasificándolas de manera diferente. La restricción CHECK del esquema de que se establece exactamente uno de document_id/conversation_id hace que este filtro sea barato y la intención sea explícita.

Depuración de recuperación deficiente. Cuando los resultados top-k no coinciden con las expectativas, se realizan cuatro comprobaciones en orden, cada una de las cuales descarta una causa común:

SíntomaCausa probableArreglar
Todas las 5 distancias principales son > 0,7 (distancia coseno, por lo que cualquier distancia > 0,5 está "lejos")El modelo de embedding de consultas difiere del modelo de embedding del corpusConfirma que ambos pasaron por el mismo EMBEDDING_MODEL. Mezclar text-embedding-3-small con -large, o local con OpenAI, produce rangos sin sentido en ambos grupos.
Los 5 resultados principales provienen todos de un tipo de fuente cuando esperabas variedadFalta el filtro de tipo de fuenteAgrega WHERE document_id IS NOT NULL (o el lado derecho de la restricción CHECK) a la consulta, o divídela en dos consultas clasificadas y combínalas con pesos explícitos.
Top-k cambia enormemente entre consultas casi idénticasTamaño del fragmento demasiado pequeño (cada fragmento carece de suficiente contexto)Vuelve a fragmentar con fragmentos más grandes (200, luego 400, luego 600 tokens) y vuelve a generar embeddings. Repetir la misma consulta ahora debería producir un top-k estable.
La consulta no devuelve nadaFalta el índice HNSW o la dimensión del vector no coincide\d+ embeddings en psql para confirmar que la columna es VECTOR(1536) y que el índice HNSW existe con vector_cosine_ops. Si la columna tiene una dimensionalidad incorrecta, vuelve a crearla.

Tres consultas de diagnóstico que vale la pena mantener en un scratch.sql:

-- 1. Distance band on the top-5 for a known-good query
SELECT chunk_text, embedding <=> :query_vec AS distance
FROM embeddings ORDER BY distance LIMIT 5;

-- 2. Source-type breakdown of top-20 (catch the "wrong source dominates" bug)
SELECT
CASE WHEN document_id IS NOT NULL THEN 'document' ELSE 'message' END AS src,
COUNT(*)
FROM (SELECT * FROM embeddings ORDER BY embedding <=> :query_vec LIMIT 20) t
GROUP BY src;

-- 3. Confirm the index is actually being used
EXPLAIN (FORMAT TEXT) SELECT id FROM embeddings ORDER BY embedding <=> :query_vec LIMIT 10;

La calidad de la recuperación es el asesino silencioso de la precisión del trabajador. La respuesta final puede parecer perfectamente razonable aunque se cite evidencia incorrecta; la única forma de detectar esto es evaluar la recuperación antes de la respuesta final.

Prueba con IA

I'm chunking a corpus of legal contracts (each averaging 8,000 words)
for semantic search. The user will query things like "what's the
termination clause in this contract" — phrases that map cleanly to
specific sections. Walk me through three chunking strategies:

A) Fixed 400-token chunks with 60-token overlap (the default)
B) Chunk at section headings only, with no overlap
C) A two-level approach: store both 400-token chunks AND
whole-section chunks, search both, combine results

For each, name (1) when it wins and (2) when it loses.

Concepto 10: Traza de auditoría como disciplina, qué significa "lee y escribe" para un Trabajador

Cada acción significativa que realice el agente debe dejar una fila en la base de datos. Sin esa fila, no podrás responder luego "¿qué hizo el agente y cuándo?" Ese rastro es lo que separa una acción real de una respuesta que suena plausible.

Traducido a términos operativos: cada acción significativa que realiza el agente escribe una fila en audit_log, más una fila más estructurada en capability_invocations si se trata de una llamada de skill o herramienta. Los datos sobre los que se actúa se encuentran en su tabla apropiada (un mensaje va en messages, una actualización de documento va en documents); el hecho de que ocurrió la acción vive en audit_log. Los dos están unidos por clave externa.

Qué registrar.

  • Cada llamada de modelo: tokens de entrada, tokens de salida, nombre del modelo, estimación de costos
  • Cada invocación de skill: nombre de la skill, argumentos, resumen de resultados, latencia, éxito/error/tiempo de espera
  • Cada escritura en la base de datos: qué tabla, qué cambió (la carga útil JSONB), bajo qué contexto de conversación
  • Cada llamada de herramienta externa: nombre de la herramienta, entrada, resumen de salida, latencia
  • Cada evento de barandilla: qué barandilla se disparó, cuál fue la entrada, cuál fue la acción (bloqueada/permitida/modificada)

Qué no registrar.

  • El historial de conversaciones completo en cada turno; eso ya está en messages, lo estarías almacenando dos veces
  • Datos confidenciales de la carga útil textualmente si la fila es consultable por humanos; almacenar un hash o resumen, mantener los datos completos en una tabla restringida
  • Razonamiento del modelo interno que el usuario no debería ver; Esa es una decisión de gestión del contexto, no una decisión de auditoría.

La prueba de repetición. Una buena pista de auditoría pasa una prueba específica: dado un ID de conversación y una marca de tiempo, puede reconstruir lo que hizo el agente y por qué, sin volver a ejecutar el modelo. Si su registro de auditoría no pasa esta prueba, está registrando, no auditando.

Un asistente concreto de escritura en acción, que se utiliza en todos los lugares donde se ejecuta una capacidad:

# src/chat_agent/audit.py
import time
import json
from typing import Any
from uuid import UUID

import asyncpg

async def log_capability(
pool: asyncpg.Pool,
conversation_id: UUID,
capability: str,
arguments: dict[str, Any],
result: Any,
status: str,
started_at: float,
cost_cents: int | None = None,
) -> None:
"""Write a capability_invocations row plus a denormalized audit_log row.

Both writes happen in one transaction so they're either both present
or both absent — the audit trail never gets partial.
"""
latency_ms: int = int((time.monotonic() - started_at) * 1000)
async with pool.acquire() as conn:
async with conn.transaction():
await conn.execute(
"""INSERT INTO capability_invocations
(conversation_id, capability, arguments, result, status,
latency_ms, cost_cents)
VALUES ($1, $2, $3::jsonb, $4::jsonb, $5, $6, $7)""",
conversation_id, capability,
json.dumps(arguments), json.dumps(result),
status, latency_ms, cost_cents,
)
await conn.execute(
"""INSERT INTO audit_log
(conversation_id, actor, action, target, payload, result)
VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb)""",
conversation_id, "worker:customer-support",
"capability_invoked", capability,
json.dumps({"arguments": arguments, "latency_ms": latency_ms}),
json.dumps({"status": status, "result": result}),
)

Las dos escrituras en una transacción es el detalle clave. Las pistas de auditoría a medio redactar son peores que las pistas de auditoría faltantes: sugieren que la información está completa sin entregarla. O entran ambas filas o ninguna.

Vocabulario de acciones canónicas. Cada sitio que escribe en audit_log elige un valor action de una pequeña lista acordada. La deriva aquí (el mismo evento recibe tres nombres diferentes en el código base) es lo que hace que las consultas de repetición sean frágiles seis meses después. Los seis valores que utiliza el ejemplo trabajado y qué ruta de código escribe cada uno:

actiontargetDonde escrito
message_received(nulo)cli.py, cuando el usuario envía un mensaje y una fila llega a messages
message_sent(nulo)cli.py, cuando la respuesta del agente llega a messages
skill_activatednombre de la skillcli.py, en RunItemStreamEvent para una carga de skills
capability_invokedidentificación de capacidadlog_capability ayudante arriba, en cada llamada de skill o herramienta MCP
mcp_callednombre de la herramientacli.py, después de que regrese cada herramienta MCP (nivel inferior a capability_invoked; usa una u otra de manera consistente)
refund_issued / refund_blockedidentificación del pedidoEl servidor MCP customer-data, la herramienta issue_refund, sobre el éxito y la denegación de políticas

Elige uno de capability_invoked y mcp_called y quédese con él; escribir ambos por llamada cuenta doblemente. El ejemplo resuelto utiliza capability_invoked para el asistente del lado del agente (Decisión 7) y acciones de dominio con nombre como refund_issued para escrituras de cambio de estado dentro de las herramientas MCP (Decisión 6). Las escrituras de estado de dominio siempre obtienen su propio nombre de acción, por lo que la fila de auditoría es el recibo del evento comercial, no solo de la llamada a la herramienta que lo desencadenó.

Por qué esto no es solo un registro. Tres propiedades separan los datos de auditoría de los datos de registro:

  1. Reproducible. El esquema le permite reconstruir el rastreo de razonamiento del agente desde audit_log unido con messages unido con capability_invocations. Una línea de registro en un archivo JSONL no.
  2. Consultable. "¿Qué le dijo el agente al cliente X el mes pasado y qué política citó?" es una consulta SQL. "¿Qué skill provocó la mayor cantidad de tiempos de espera en los últimos 7 días?" es una consulta SQL. Los registros requieren preparación y suerte.
  3. Confiable. Las filas de auditoría están en la misma base de datos que los datos comerciales. Se realizan copias de seguridad juntas, se pueden recuperar juntas en un momento dado y el acceso está controlado juntas. Sobreviven al proceso del agente, la región de implementación y la versión del modelo.

Esto es lo que quiere decir la tesis cuando dice: "Los trabajadores sólo se vuelven gobernables como fuerza laboral cuando un libro de contabilidad los hace legibles, como unidades de capacidad, costo, latencia y resultado". Sus tablas audit_log y capability_invocations son ese libro mayor. Trátalos como tal.

Prueba con IA

Here's a customer support scenario: a customer claims the Worker told
them they would receive a $50 refund, but the actual refund issued was
$30. The Worker handled the conversation 19 days ago.

Walk me through the audit-trail query path to resolve this:

1. Find the conversation. (Which columns of which tables?)
2. Find the message where the refund amount was promised. (How do you
distinguish "discussed" from "promised"?)
3. Find the capability invocation that issued the refund.
4. Find the database write that recorded the $30 amount.

For each step, name the table you'd query and the WHERE clauses.
Then say what's MISSING from the six-table schema that would make
this query easier.

Parte 3: MCP, cableado del agente al sistema de registro

La Parte 1 le dio al agente una biblioteca de Skills. La parte 2 le proporcionó un sistema de registro Postgres. La Parte 3 conecta los dos con el Protocolo de contexto modelo: el estándar abierto sobre cómo los agentes alcanzan el estado externo y la capacidad externa. La tesis es directa sobre el lugar de MCP: "MCP es la forma en que la fuerza laboral llega a [sus sistemas de registro]: cada almacén autorizado se vuelve direccionable a cualquier trabajador a través de un servidor MCP, según la política". Esta Parte lo hace operativo.

Concepto 11: Qué es y qué no es MCP

El Protocolo de contexto modelo (modelcontextprotocol.io) es un protocolo abierto cliente/servidor (originalmente de Anthropic, ahora regido como un estándar abierto) sobre cómo un agente de IA se conecta a herramientas, datos y mensajes externos. El marco que se repite es "USB-C para herramientas de IA": un protocolo, muchas implementaciones, intercambia cualquier lado sin romper el otro. El encuadre es preciso; Como todas las metáforas, tiene límites que vale la pena nombrar.

Qué es MCP. Un protocolo. Una especificación. Tres primitivas que el servidor puede exponer al cliente.

  1. Herramientas: funciones que el modelo puede invocar. El cliente los enumera, el modelo elige uno y el servidor lo ejecuta. Conceptualmente similar a un decorador @function_tool del curso anterior, pero la implementación reside en el proceso del servidor MCP, no en el proceso del agente. Esta es, con diferencia, la primitiva más utilizada.
  2. Recursos: datos de solo lectura que el agente puede recuperar. Archivos, resultados de consultas de bases de datos, respuestas API. Piense en ellos como el lado exclusivo de MCP. Menos común que las herramientas en la práctica, pero útil para "permitir que el agente lea este documento cuando lo solicite".
  3. Mensajes: plantillas de mensajes reutilizables que proporciona el servidor. Un equipo puede publicar mensajes estandarizados ("resumir-informe-incidente") que cualquier agente que se conecte al servidor puede invocar. Rara vez se utiliza en comparación con herramientas y recursos.

Tres transportes, con recomendaciones vigentes a 2026:

TransporteCuándo utilizarEstado
stdioSubproceso local; agente y servidor en la misma máquinaMaduro. Valor predeterminado para herramientas locales.
streamable HTTPServidor remoto; implementaciones de producciónRecomendado para nuevos trabajos remotos. Punto final único, bidireccional y reanudable.
SSEServidor remoto; implementaciones más antiguasLegado. Muchos servidores todavía lo exponen; los nuevos utilizan cada vez más por defecto HTTP transmisible.

Lo que no es MCP.

  • No es un marco. Es un protocolo. Su agente no "usa MCP" de la misma manera que usa el SDK de agentes; El cliente MCP de su agente habla MCP con un servidor MCP. El SDK de agentes incluye un cliente MCP; ese es el punto de integración.
  • No es un servicio. No existe una "nube MCP". Los servidores MCP son programas que tú ejecutas (o que los proveedores ejecutan por ti). El servidor Neon MCP está alojado en mcp.neon.tech; el servidor MCP del sistema de archivos se ejecuta como un subproceso local; un servidor MCP personalizado que escribes se ejecuta dondequiera que lo despliegues.
  • No es un límite de seguridad. MCP define transporte y protocolo; qué herramientas expone un servidor MCP y qué pueden hacer es responsabilidad del servidor. Un servidor MCP malicioso puede hacer cualquier cosa que haga su código del lado del servidor. El límite de confianza sigue siendo el bucle del agente que decide a qué herramientas llamar y el entorno de pruebas en el que se ejecutan las herramientas.
  • No reemplaza a @function_tool. Ambos todavía tienen un lugar. El árbol de decisión es el Concepto 14.

Comprobación rápida. Verdadero o falso: (a) Un cliente MCP habla exactamente con un servidor MCP a la vez. (b) La misma función de estilo @function_tool, si lo desea, podría exponerse como una herramienta MCP o mantenerse como una herramienta de función, y el modelo no notaría la diferencia. (c) Los servidores MCP y el SDK de agentes OpenAI están estrechamente vinculados, por lo que para usar MCP debe usar el SDK. Respuestas: (a) Falso: un agente puede conectarse a múltiples servidores MCP y ver la unión de sus herramientas. (b) Verdadero: para el modelo, ambas parecen herramientas invocables con esquemas. La diferencia es dónde vive la implementación. (c) Falso: MCP es independiente del modelo. Claude, Gemini y otros tienen sus propios clientes MCP. El SDK de OpenAI Agents es un cliente entre muchos.

Prueba con IA

You're building three capabilities for an internal "weekly report"
Worker. For each, decide: @function_tool, custom MCP server, or
vendor MCP server (use the existing one). Justify with ONE specific
property from Concept 11:

A) generate_summary(text: str) -> str — wraps OpenAI's completion
API to summarize the input. Used only by this Worker.

B) read_jira_issues(project: str, since: date) -> list[Issue] — your
company already runs Atlassian's official Jira MCP server in
production.

C) post_to_slack(channel: str, message: str) -> None — used by this
Worker today. The Engineering team's incident-response Worker
needs the same capability next quarter.

Then answer: which of the three is the EASIEST design decision, and
why? Which is the HARDEST, and why?

Concepto 12: El servidor Neon MCP, plano de desarrollo, no tiempo de ejecución

Los detalles de este concepto envejecerán. El patrón no. Las herramientas del servidor MCP de Neon, el flujo de autenticación y la superficie exacta de herramientas cambian cada pocos meses. Lo que sigue siendo cierto: un proveedor de bases de datos administradas expone su API de administración a través de MCP para operaciones en lenguaje natural, mientras que el tráfico de producción en runtime usa conexiones directas o servidores personalizados con alcance. Verifica la documentación de Neon antes de fijar detalles.

El servidor MCP Neon es el ejemplo más pulido de un servidor MCP proporcionado por el proveedor. Expone la API de administración de Neon (proyectos, sucursales, bases de datos, migraciones, SQL ad-hoc) como herramientas MCP que su agente puede llamar en lenguaje natural. Esto es profundamente útil durante el desarrollo. Explícitamente no es para tiempo de ejecución de producción. La propia advertencia de Neon es inequívoca: "Recomendamos MCP solo para desarrollo y pruebas, no para entornos de producción". Y la versión más segura, también de los documentos de Neon: "Nunca conecte agentes MCP a bases de datos de producción".

Por qué es importante la distinción. La herramienta run_sql del servidor Neon MCP puede ejecutar cualquier SQL que produzca el modelo. En desarrollo, eso es una ventaja: dices "muéstrame los usuarios que se registraron la semana pasada y no iniciaron sesión", el modelo genera SELECT * FROM users WHERE ..., el servidor lo ejecuta y devuelve los resultados. El mismo camino en producción es un agujero de seguridad. Un atacante que pudiera inyectar prompts en tu Worker de atención al cliente podría leer toda tu base de datos haciendo la pregunta correcta.

Para qué sirve el servidor Neon MCP. Tres funciones en las que es la herramienta adecuada:

  • Diseño de esquemas y migraciones durante el desarrollo. "Agregar una columna priority a la tabla de tickets". Las migraciones basadas en sucursales permiten que el modelo pruebe la migración en una sucursal antes de confirmarla; Patrón de diseño MCP propio de Neon. Esta es la forma correcta de usarlo.
  • Los datos exploratorios funcionan en el ciclo de creación de agentes. "¿Cuántas incorporaciones hay en la base de datos, desglosadas por fuente?" Trabajar con el modelo suele ser más rápido que escribir SQL a mano, especialmente para preguntas puntuales.
  • Recuperación de cadenas de conexión e introspección del proyecto. El servidor MCP puede entregarle una cadena de conexión para la rama correcta sin que tenga que iniciar sesión en la consola Neon.

Configuración rápida para uso en desarrollo. El servidor Neon MCP está disponible de tres maneras. Usa el servidor remoto basado en OAuth para la ruta más sencilla:

# In your project, run:
npx -y mcp-remote@latest https://mcp.neon.tech/mcp

O agrega a .claude/settings.json:

{
"mcpServers": {
"neon": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://mcp.neon.tech/mcp"]
}
}
}

Claude Code solicita OAuth en el primer uso.

Una sesión de desarrollo con el servidor Neon MCP, sin SQL escrito a mano. Los nombres de las herramientas a continuación coinciden con la documentación MCP actual de Neon; La redacción exacta y el formato de salida variarán según el lanzamiento. Lo que queda es la forma:

You: Set up the chat-agent SoR. Create a project called "chat-agent",
enable pgvector, run the schema from migrations/001_initial.sql,
and tell me the connection string for the main branch.

Agent: [calls create_project] Created project chat-agent (ID: abc123).
[calls run_sql] Ran CREATE EXTENSION vector; — succeeded.
[calls prepare_database_migration with migrations/001_initial.sql]
Created temporary branch migration-test/abc123.
Migration ran on the branch; 6 tables created, 7 indexes built.
Want me to commit to main?

You: Yes, commit.

Agent: [calls complete_database_migration] Committed. Main branch
updated. Connection string:
postgresql://...@ep-cool-name.neon.tech/chat-agent?sslmode=require

Toda esa interacción es una sesión de Claude Code u OpenCode: no hay SQL escrito a mano, la migración se prueba en una rama antes de ir a principal. Para eso sirve el servidor Neon MCP durante el desarrollo. No es lo que utilizará su agente de tiempo de ejecución para escribir una sola fila de auditoría.

PRIMM, Predecir. Estás escribiendo un Worker de atención al cliente. El Worker debe: (a) consultar el historial de pedidos del cliente; (b) consultar la política de reembolso del nivel del cliente; (c) emitir un reembolso; (d) escribir una fila de auditoría registrando qué se emitió y por qué. ¿Debería hablar con Neon a través del servidor oficial de Neon MCP o de alguna otra forma? Confianza 1–5.

La respuesta: de otra manera. Para las cuatro operaciones. El run_sql del servidor Neon MCP es una interfaz demasiado amplia para un agente de producción: otorga al agente permiso para ejecutar SQL arbitrario, que es mucha más autoridad de la que requieren las cuatro operaciones limitadas anteriores. Los patrones de producción son conexiones directas de Postgres (una connection_pool de asyncpg) que envuelven operaciones comerciales con alcance, o un servidor MCP personalizado que expone solo esas cuatro operaciones. El concepto 14 cubre este último. El ejemplo resuelto de la parte 4 utiliza ambos: un servidor MCP customer-data personalizado para operaciones comerciales (búsquedas, búsqueda de vectores, reembolsos) y un asyncpg directo solo para el subsistema de auditoría. La Decisión 7 explica el motivo: la auditoría es la metacapa y no debe compartir el límite del MCP que está auditando.

La distinción que hace la tesis con el Invariante 5 (que la fuerza laboral lee y escribe en almacenes gobernadas) depende de esta distinción. Una herramienta run_sql MCP amplia no es gobernanza; elimina la gobernanza y coloca una interfaz amigable en la parte superior.

Las herramientas de Neon MCP que tocará su trabajador. El servidor Neon MCP expone aproximadamente veinte herramientas en gestión de proyectos, bifurcación, esquema, ejecución de SQL y ajuste de consultas. La decisión 3 del ejemplo resuelto utiliza un pequeño subconjunto; el resto son útiles para el trabajo posterior (ampliar el esquema más adelante, proteger una migración hipotética en una rama). Como referencia:

HerramientaQué haceDónde se utiliza en la Parte 4
list_projectsLista proyectos de Neon en tu cuentaDecisión 3, confirmar que el proyecto no existe ya
create_projectAprovisionar un nuevo proyecto Neon (base de datos Postgres + rama predeterminada main)Decisión 3, crear el proyecto chat-agent
describe_projectObtener metadatos del proyecto (rama predeterminada, región, configuración informática)Decisión 3, verificación de cordura después del aprovisionamiento
get_connection_stringDevuelve la cadena de conexión de Postgres para una rama con nombreDecisión 3, completar NEON_DATABASE_URL para asyncpg
prepare_database_migrationIniciar una migración: abre una rama temporal y permite que el agente ejecuta DDL allíDecisión 3, probar el esquema de seis tablas antes de fusionarlo con main
run_sqlEjecutar SQL en una rama con nombre (dentro de una sesión mediada por MCP)Decisión 3, ejecutar la creación del esquema en la rama temporal
complete_database_migrationConfirmar una migración: fusiona la rama temporal nuevamente con mainDecisión 3, después de que el esquema se verifique limpio en la rama temporal
describe_table_schemaVuelca el DDL completo de una tabla con nombre (útil para que el agente conecte a tierra el trabajo futuro en modo plan)Opcional, cuando le pide al Modo Plan que exalmacén el esquema más adelante
create_branch / delete_branchAdministrar ramas aisladas de copia en escrituraOpcional, cuando desee una rama sandbox para una migración hipotética

Tiempo de ejecución de producción Los trabajadores no tocan este servidor. Llegan a Postgres a través del servidor MCP personalizado que escribe en la Decisión 6, o mediante asyncpg directo para el subsistema de auditoría (Decisión 7).

Prueba con IA

Read Neon's MCP server documentation page and answer three questions:

1. List THREE management operations the Neon MCP server exposes that
would be useful during development of a customer-support Worker.
2. List THREE operations a runtime Worker would NEED that the Neon MCP
server should NOT be used for, and why.
3. For each of the three in (2), propose what the Worker should use
instead (direct Postgres connection? custom MCP server? function_tool?).

Concepto 13: Conexión de MCP al SDK de agentes OpenAI

El SDK de Agents se entrega con un cliente MCP de primera clase. Tres clases hacen el trabajo, una por transporte:

from agents.mcp import (
MCPServerStdio, # local subprocess
MCPServerStreamableHttp, # remote, modern transport
MCPServerSse, # remote, legacy transport (avoid for new work)
)

Los tres son administradores de contexto asíncronos (async with server as ...:). Una vez conectado, se los pasa al Agente mediante el argumento mcp_servers. El agente descubre automáticamente las herramientas de cada servidor al inicio, las presenta al modelo junto con cualquier @function_tools que haya definido y enruta las llamadas al servidor correcto según la herramienta que elige el modelo.

Aumenta el tiempo de espera de inicio si tu servidor MCP importa algo pesado

MCPServerStdio con client_session_timeout_seconds=5 por defecto es suficiente para un servidor Python puro que solo importa mcp y asyncpg. No es suficiente cuando el servidor importa torch, sentence-transformers o cualquier cosa que cargue un modelo en la memoria en el momento de la importación, lo que puede tardar entre 10 y 60 segundos en el primer inicio. El síntoma es un error de inicio confuso ("el servidor MCP no respondió a tiempo"); la solución es un parámetro:

MCPServerStdio(
name="customer-data",
params={...},
client_session_timeout_seconds=60,
)

Configúrelo una vez para cualquier servidor cuyo proceso funcione realmente al inicio y olvídelo.

Arquitectura MCP: el modelo decide a qué herramienta llamar; el cliente MCP enruta la llamada a través del límite de confianza a través de HTTP transmitible (o stdio o SSE heredado); el servidor MCP expone un conjunto de herramientas de alcance limitado y es lo único que toca a Postgres. Tres propiedades que te ofrece la frontera: alcance, aislamiento y reutilización. Un ejemplo mínimo, utilizando el servidor Neon MCP durante el desarrollo:

# tools/scratch_with_neon.py — development utility, not production
import asyncio
from agents import Agent, Runner
from agents.mcp import MCPServerStreamableHttp


async def main() -> None:
async with MCPServerStreamableHttp(
name="Neon (development)",
params={"url": "https://mcp.neon.tech/mcp"},
cache_tools_list=True,
) as neon:
agent: Agent = Agent(
name="DBAssistant",
instructions=(
"You help with Neon database operations during development. "
"Always use prepare_database_migration / complete_database_migration "
"for schema changes — never run DDL directly on main."
),
mcp_servers=[neon],
model="gpt-5.5",
)
result = await Runner.run(
agent,
"List all projects, then show me the schema of the largest one.",
)
print(result.final_output)


if __name__ == "__main__":
asyncio.run(main())

Tres detalles dignos de mencionar:

  • async with no es opcional. La conexión MCP contiene un transporte abierto (un subproceso para stdio, una sesión HTTPS para HTTP transmitible). Sin el administrador de contexto, la conexión se filtra y el estado del lado del servidor queda obsoleto. Usa siempre async with.

  • cache_tools_list=True es una aceleración sustancial de la producción. De forma predeterminada, el SDK llama a list_tools() cada Runner.run, que es un viaje de ida y vuelta de la red. El almacenamiento en caché lo hace una vez por proceso. La invalidación de la caché es manual: llame a server.invalidate_tools_cache() cuando haya agregado o eliminado herramientas. Para desarrollo con un servidor cuyas herramientas cambian, déjelo False.

  • Varios servidores MCP se apilan de forma natural. Pasa mcp_servers=[neon, custom_server, hosted_server]; el modelo ve la unión de todas las herramientas. Usa MCPServerManager si necesitas gestionar el ciclo de vida de muchos; es un helper delgado alrededor del mismo patrón.

El parámetro require_approval es el cambio de producción que vale la pena conocer. De forma predeterminada, las llamadas a la herramienta MCP se ejecutan sin confirmación. Para servidores confidenciales, puede requerir aprobación humana por herramienta:

async with MCPServerStreamableHttp(
name="Neon (development, guarded)",
params={"url": "https://mcp.neon.tech/mcp"},
require_approval={
"always": {"tool_names": ["delete_project", "delete_branch", "run_sql"]},
"never": {"tool_names": ["list_projects", "describe_project"]},
},
) as neon:
...

Las operaciones destructivas reciben un control humano; los de solo lectura se ejecutan en silencio. Esta es la perilla práctica para la brecha entre desarrollo y producción del Concepto 12: incluso cuando estás usando el servidor Neon MCP para trabajo práctico, controlar sus herramientas destructivas a través de la aprobación es una verdadera mejora de seguridad.

Una mirada a cómo el modelo ve las herramientas MCP. Cuando se ejecuta el Agente, el modelo obtiene una lista de herramientas que se ve estructuralmente similar a una lista @function_tool:

[
{"type": "function", "function": {"name": "search_docs", "description": "...", "parameters": {...}}},
{"type": "function", "function": {"name": "get_billing_invoice", "description": "...", "parameters": {...}}},

// These three came from the Neon MCP server:
{"type": "function", "function": {"name": "list_projects", "description": "Lists the first 10 Neon projects ...", "parameters": {...}}},
{"type": "function", "function": {"name": "run_sql", "description": "Executes a single SQL query ...", "parameters": {...}}},
{"type": "function", "function": {"name": "prepare_database_migration", "description": "Initiates a database migration by creating a temporary branch ...", "parameters": {...}}}
]

El modelo no puede distinguir entre una herramienta MCP y @function_tool, y no debería ser necesario. La capa de enrutamiento del SDK envía cada llamada al backend correcto.

Prueba con IA

I want to connect my OpenAI Agents SDK agent to TWO MCP servers
simultaneously: (a) the Neon MCP server for database operations, and
(b) a local filesystem MCP server (npx @modelcontextprotocol/server-filesystem)
for reading project files.

Write the async setup code that connects to both servers and uses
them in a single Agent. Include:

1. The right imports.
2. Both connections as async context managers (one streamable HTTP,
one stdio).
3. An Agent that uses both, with require_approval set to require
approval for any tool that writes to the filesystem.
4. A sample Runner.run that exercises tools from both servers in
one turn.

Concepto 14: Servidores MCP personalizados, cuándo escribir los tuyos propios y cuándo no

El servidor Neon MCP es genérico: puede hacer cualquier cosa que pueda hacer la API de Neon. Ésa es su fortaleza para el desarrollo y su debilidad para el tiempo de ejecución. Un servidor MCP personalizado invierte el equilibrio: superficie estrecha, sin run_sql de uso general, solo las operaciones específicas que su trabajador realmente necesita.

El árbol de decisiones, en orden de prioridad.

Árbol de decisión para la ubicación de capacidades: comenzando desde la pregunta raíz, responda cinco filtros en orden (¿de un solo uso? ¿El proveedor tiene uno? ¿Reutilización de múltiples agentes? ¿Alcance sensible? ¿Aislamiento de procesos?). Tres hojas son verdes (usa lo que tiene: @function_tool o servidor MCP del proveedor); tres son de color ámbar (construya algo nuevo: servidor MCP personalizado). Detente en el primer SÍ.

La misma lógica en una tabla de escaneo rápido:

Quieres exponer...Usa estoPor qué
Una función con una entrada, utilizada por un agente@function_toolNo hay necesidad de sobrecarga de protocolo. La llamada a función local está bien.
Varias funciones estrechamente acopladas al código de su agente@function_toolSi comparten estado con el agente y viven en el mismo repositorio, son parte del agente.
Una capacidad que utilizarán varios agentes (o varias implementaciones)Servidor MCP personalizadoEl protocolo es lo que lo hace reutilizable.
Una capacidad que debe sobrevivir al proceso del agenteServidor MCP personalizadoConexiones de larga duración, trabajos en segundo plano, consumidores en cola.
Funcionalidad proporcionada por el proveedor (Neon, GitHub, Linear)Servidor MCP del proveedorNo reconstruyas lo que envían.
Operaciones sensibles que necesitan un alcance limitadoServidor MCP personalizadoDefina exactamente las herramientas que necesita; nada más.

El servidor MCP personalizado mínimo viable en Python, utilizando el SDK de MCP oficial:

# servers/customer_data_mcp/server.py
# Run with: python -m customer_data_mcp.server
import os
from typing import Annotated
import asyncpg
from mcp.server.fastmcp import FastMCP
from pydantic import Field

mcp: FastMCP = FastMCP("customer-data")

# Connection pool: created once, reused across tool calls.
_pool: asyncpg.Pool | None = None


async def get_pool() -> asyncpg.Pool:
global _pool
if _pool is None:
_pool = await asyncpg.create_pool(
os.environ["DATABASE_URL"],
min_size=1, max_size=10,
)
return _pool


@mcp.tool()
async def lookup_customer(
customer_id: Annotated[
str, Field(description="The customer's UUID, as provided by the user."),
],
) -> dict[str, str | int]:
"""Look up a customer by ID. Returns id, email, tier, and active_tickets count.

Use when the user provides a customer ID and you need their basic profile.
Does NOT return billing details — use lookup_billing for that.
"""
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""SELECT id::text, email, tier,
(SELECT COUNT(*) FROM tickets t
WHERE t.customer_id = c.id AND t.status='open') AS active_tickets
FROM customers c WHERE id = $1::uuid""",
customer_id,
)
if row is None:
return {"error": f"customer {customer_id} not found"}
return dict(row)


@mcp.tool()
async def find_similar_resolved_tickets(
description: Annotated[
str, Field(description="The user's question or description of the issue."),
],
limit: Annotated[int, Field(description="Max results.", ge=1, le=10)] = 5,
) -> list[dict[str, str | float]]:
"""Find resolved tickets similar to a description, via pgvector.

Use when the user describes an issue and you want to check if a similar
issue has been resolved before. Returns ticket_id, summary, resolution,
and distance (cosine distance, so lower means more similar).
"""
# Embed the description with the SAME model used at insert time
# (text-embedding-3-small, 1536 dim per Concept 9). Mixed models
# silently return nonsense rankings. embed_chunks is batch-oriented;
# pass a one-element list and unpack the result.
from openai import AsyncOpenAI

from chat_agent.embedding.embedder import embed_chunks

[query_embedding] = await embed_chunks(AsyncOpenAI(), [description])

pool = await get_pool()
# Note the JOIN path: embeddings.document_id references documents(id), not
# tickets(id). See Concept 7's schema. A resolved ticket is embedded by
# storing it as a `documents` row (source='past_case') whose metadata
# holds the ticket id, so the join goes embeddings -> documents -> tickets.
async with pool.acquire() as conn:
rows = await conn.fetch(
"""SELECT t.id::text AS ticket_id, t.summary, t.resolution,
(e.embedding <=> $1::vector) AS distance
FROM embeddings e
JOIN documents d ON d.id = e.document_id
JOIN tickets t ON t.id = (d.metadata->>'ticket_id')::uuid
WHERE d.source = 'past_case' AND t.status = 'resolved'
ORDER BY e.embedding <=> $1::vector
LIMIT $2""",
query_embedding, limit,
)
return [dict(r) for r in rows]


if __name__ == "__main__":
mcp.run(transport="stdio")

Luego en su agente:

async with MCPServerStdio(
name="customer-data",
params={
"command": "python",
"args": ["-m", "customer_data_mcp.server"],
"env": {"DATABASE_URL": os.environ["DATABASE_URL"]},
},
cache_tools_list=True,
) as customer_data:
agent: Agent = Agent(
name="CustomerSupport",
mcp_servers=[customer_data],
...,
)

Tres cosas que este servidor te ofrece y @function_tool no.

  1. Aislamiento de procesos. El servidor MCP se ejecuta en su propio proceso (subproceso para stdio, servicio separado para HTTP transmitible). Una falla en el servidor no bloquea al agente; una pérdida de memoria en el servidor no se pierde en el agente.

  2. Alcance. El servidor tiene exactamente dos herramientas. No run_sql. Nada de "ejecutar código arbitrario". El modelo no puede escapar de este alcance porque el protocolo no expone nada más. Esta es una verdadera defensa en profundidad: incluso si el modelo decide hacer algo estúpido, la superficie para hacerlo es de dos funciones.

  3. Reutilizabilidad entre agentes. Un segundo agente (un trabajador de ventas, un trabajador de informes) puede hablar con el mismo servidor MCP customer-data. Mismo alcance, mismo protocolo, mismo límite de confianza. La capacidad se convierte en una pieza de infraestructura compartida en lugar de copiar y pegar entre agentes.

La compensación es real. Los servidores MCP personalizados añaden complejidad operativa: otro proceso para desplegar, otro conjunto de registros, otro salto de red (si es remoto), otra versión para administrar. No escribas uno para una sola función usada por un solo agente. Escribe uno cuando la capacidad vaya a reutilizarse, cuando el alcance importe o cuando el aislamiento te dé seguridad.

Cuando decides construir uno, no escribes a mano el boilerplate de FastMCP. Instalas la skill mcp-builder y se la pides: el mismo patrón de instalar una skill y luego pedir, usado para los servidores MCP, donde tú describes las herramientas, el agente construye el servidor y el agente lo verifica. El código anterior muestra la forma hacia la que te diriges; la Decisión 6 recorre la construcción de principio a fin. Tu criterio se centra en el alcance (qué herramientas existen y cuáles deliberadamente no), no en el cableado del pool de conexiones.

PRIMM, Predecir. Estás diseñando el Worker de atención al cliente. Necesita: (1) búsqueda semántica de tickets resueltos anteriormente; (2) escribir una fila de auditoría de reembolso; (3) leer el clima actual (usado en una skill de saludo que dice "buenos días desde la soleada Karachi"); (4) llamar a la pasarela de pago para emitir un reembolso. Para cada uno, predice: @function_tool, servidor MCP personalizado o servidor MCP del proveedor (por ejemplo, Stripe, si existe).

Las respuestas revelan el marco:

  1. Servidor MCP personalizado (customer-data). Reutilizado entre agentes; datos sensibles; Las herramientas con alcance superan a un amplio run_sql.
  2. Servidor MCP personalizado (customer-data) o @function_tool. Cualquiera de los dos funciona; Si el trabajador es el único escritor, la herramienta de función está bien. Si varios trabajadores escriben filas de auditoría, el servidor MCP.
  3. @function_tool. Un agente, una pequeña función, sin superficie de seguridad que defender. No construyas un servidor para ello.
  4. Servidor MCP del proveedor (Stripe MCP) si existe; de ​​lo contrario, @function_tool llama a la API de Stripe. No incluya API de terceros en su propio servidor MCP a menos que necesite agregar una política encima.

El marco es claro una vez que lo rastreas: el valor de MCP aumenta con el valor del límite que crea. Un límite que no necesitas es el superior.

Prueba con IA

You're designing the architecture for a research-Worker that does these
five things:

A. Reads PDFs from a Google Drive folder (uses Google Drive's API)
B. Queries a private Snowflake warehouse the company owns
C. Calls a third-party data-enrichment API (Clearbit-like)
D. Logs every action to the company's central audit log database
E. Generates a Markdown report by string-formatting the results

For each, decide: @function_tool, custom MCP server, or vendor MCP
server (if a credible vendor MCP server exists). For each choice,
justify with ONE of the three properties of MCP from Concept 14
(isolation, scope, reusability) OR justify why the boundary isn't
worth building.

Concepto 15: MCP bajo carga: transportes, agrupación y lo que sucede a escala

Una demostración con un servidor MCP y un agente en una equipo portátil funciona bien. Las preguntas se vuelven más difíciles cuando el trabajador está en producción manejando 10 conversaciones por minuto. Este Concepto es la lista de verificación operativa.

Elección de transporte, a escala.

  • stdio es para desarrollo local e implementaciones de proceso único. El servidor MCP se ejecuta como un subproceso del agente. Reinicie el agente → el servidor se reinicia → estado nuevo. Barato. Obras. No se escala entre máquinas.

  • streamable HTTP es para servidores remotos de producción. Varios agentes pueden conectarse a un servidor MCP. El servidor puedes ejecutarse en un hardware diferente al del agente. Reconexión automática, flujos reanudables, punto final único: diseñado para la producción desde cero.

  • SSE existe, principalmente por razones heredadas. El nuevo trabajo debe usar HTTP transmitible de forma predeterminada. Los servidores existentes exclusivamente para SSE siguen siendo comunes (el SDK los admite); no reescriba un servidor sólo para cambiar de transporte.

Gestión de conexiones. Las conexiones MCP no son gratuitas. Cada una es una sesión: un protocolo de enlace de autenticación, un list_tools de ida y vuelta, una conexión persistente. Tres patrones que importan:

  1. Guarda en caché la lista de herramientas. cache_tools_list=True guarda un viaje de ida y vuelta por Runner.run. Esta es la mayor ganancia de latencia para el ciclo de agente típico. Invalide el caché cuando implemente una nueva versión del servidor.

  2. Reutilice las instancias del servidor en las ejecuciones. No crea un nuevo MCPServerStreamableHttp(...) por solicitud. Ábralo una vez al iniciar el agente, mantenga abierto el async with durante la vida útil del proceso del agente y ciérrelo al apagar. El costo de la conexión se amortiza en miles de ejecuciones de agentes.

  3. El grupo de conexiones se encuentra dentro del servidor MCP, no en el cliente. Si su servidor MCP se comunica con Postgres (como lo hace el ejemplo customer-data), el servidor debe tener un asyncpg.create_pool(min_size=1, max_size=10) y compartir el grupo entre llamadas de herramientas. El lado del cliente habla con una conexión de servidor MCP; el lado del servidor se despliega hacia un grupo de conexiones de bases de datos.

Límites de concurrencia. Tres lugares para establecerlos, en orden del más barato al más caro para equivocarse:

  • max_turns sobre el agente. Ya estoy familiarizado con el curso anterior. Longitud del bucle de las tapas independientemente de las herramientas.
  • max_retry_attempts en la conexión del servidor MCP. Los reintentos de mayúsculas aparecen cuando un servidor MCP falla. Predeterminado 0 (sin reintentos); establecido en 2-3 para la resiliencia de la producción.
  • Límites de tarifas del lado del servidor. Dentro de tu servidor MCP personalizado, cuenta las llamadas simultáneas por agente y rechaza por encima de cierto umbral. El protocolo permite que las llamadas a herramientas devuelvan errores; los clientes los ven como fallas de la herramienta y el modelo puede decidir volver a intentarlo, darse por vencido o elegir una herramienta diferente.

Seguimiento a través del límite de MCP. La configuración de seguimiento del curso anterior continúa, pero con un detalle importante: Las llamadas a la herramienta MCP aparecen en el seguimiento del agente como llamadas a herramientas ordinarias: misma forma de tramo, mismo nombre de tramo (el nombre de la herramienta MCP), mismo tiempo. Lo que no es visible desde el lado del agente es lo que sucedió dentro del proceso del servidor MCP. Si su servidor MCP personalizado realiza sus propias llamadas a la base de datos o llamadas API posteriores, éstas necesitan su propio seguimiento dentro del servidor.

Un patrón práctico: propagar el contexto de seguimiento a través del límite de MCP usando _meta. El SDK admite tool_meta_resolver que agrega metadatos a cada llamada de herramienta (ID de inquilino, ID de seguimiento, ID de solicitud); su servidor puede extraerlos y usarlos. Así es como la cadena de seguimiento de la ejecución de un único agente permanece intacta desde el agente → Cliente MCP → Servidor MCP → Postgres.

Comprobación rápida. Verdadero o falso: (a) Cambiar de MCPServerSse a MCPServerStreamableHttp requiere cambios en el servidor MCP. (b) cache_tools_list=True es seguro en producción siempre que llame a invalidate_tools_cache() después de implementar una nueva versión del servidor. (c) Un servidor MCP con cinco herramientas siempre utiliza más presupuesto de contexto de agente que un @function_tool con cinco funciones. Respuestas: (a) Depende: el servidor debe admitir el transporte HTTP transmitible; la mayoría de los servidores modernos lo hacen, los más antiguos solo pueden hablar SSE. (b) Verdadero: ese es el patrón previsto. (c) Falso: a nivel de esquema son equivalentes. Cinco definiciones de herramientas cuestan aproximadamente lo mismo en cualquier forma.

Prueba con IA

My customer-support Worker is in production. It runs 80 conversations/minute
at peak. Each conversation makes 2-4 MCP tool calls on average. I'm seeing
intermittent latency spikes — most calls return in 200ms, but a small
percentage take 5-15 seconds.

Walk me through five places I'd investigate, in order of priority:

1. The agent-side MCP client connection management.
2. The transport choice between agent and MCP server.
3. The MCP server's internal connection pool to Postgres.
4. Postgres-side query performance (slow queries blocking the pool).
5. Network or DNS issues between agent and MCP server.

For each, name the specific signal I'd look for and the rough fix.

Parte 4: El ejemplo resuelto, trabajador de atención al cliente

Una evolución realista, todos los conceptos anteriores, ambas herramientas. Tomamos el proyecto chat-agent de Crear agentes de IA y lo convertimos en un trabajador de atención al cliente agregando tres skills, un sistema de registro Neon y una capa de cableado MCP. Ocho decisiones de construcción, la misma forma que la Parte 5 de ese curso.

Parte 4 en una imagen: a partir del agente de chat del curso anterior (izquierda), ocho decisiones agrupadas en tres fases. La fase 1 (azul) es Fundación: D1 actualiza CLAUDE.md, D2 planea en modo Plan, D3 aprovisiona Neon. La fase 2 (ámbar) es Capacidad: D4 escribe la primera Skill (la que se escribe a mano de manera deliberada), D5 construye el proceso de incorporación. La fase 3 (verde) es runtime + verificación: D6 crea el servidor MCP personalizado, D7 audita el registro y D8 verifica de un extremo a otro. Consulta aquí siempre que te preguntes dónde encaja una decisión en el arco.

Antes de comenzar: la configuración que necesitas no está en los requisitos previos. Tres cosas que esta parte supone ya hechas. (1) Tienes el proyecto inicial chat-agent (se envía en el archivo zip de este curso y reproduce el estado final del curso anterior): cli.py, agents.py, tools.py, models.py, guardrails.py, más sandboxed.py. Modificamos estos archivos; no los reemplazamos. Si completaste el curso anterior, usa ese proyecto en su lugar; es la misma forma. (2) Tienes una cuenta Neon gratuita y ejecutaste npx neonctl@latest init una vez para autenticarte. (3) Tienes Claude Code u OpenCode instalado y autenticado. Si falta alguno de estos, corrígelo antes de la Decisión 1.

El breve

Desarrolla el chat-agent del curso anterior a un Trabajador de atención al cliente que:

  • Carga tres skills bajo demanda: summarize-ticket, find-similar-cases y escalate-with-context.
  • Lee y escribe en un sistema de registro Neon Postgres con las seis tablas del Concepto 7.
  • Utiliza pgvector para búsqueda semántica en una pequeña biblioteca de casos resueltos en el pasado.
  • Habla con Postgres en tiempo de ejecución a través de un servidor MCP personalizado con alcance (customer-data), no a través del servidor Neon MCP y no a través de llamadas directas asyncpg en código de agente.
  • Escribe una fila de auditoría para cada acción significativa: cada skill invocada, cada escritura en la base de datos, cada reembolso considerado.

La prueba de "verificación al final": un cliente envía un mensaje "No he recibido mi reembolso del pedido n.º 4429, han pasado dos semanas". Luego, el Trabajador ejecuta una cadena de acciones. Encuentra tres casos anteriores similares a través de una búsqueda vectorial, redacta una respuesta que cita la resolución del caso más similar y escribe una fila de auditoría que registra lo que hizo (y, en una implementación real, escala si el cliente es un usuario de nivel Pro). Para resolver el registro del cliente o del pedido a partir del propio mensaje se necesita una herramienta de búsqueda que se agrega más adelante; La Decisión 8 muestra exactamente dónde está esa brecha.

Una nota sobre las indicaciones que siguen. Cada decisión muestra una pregunta estructurada como una indicación citada en bloque. El patrón que funciona mejor en la práctica es preceder cada pregunta con un movimiento de orientación ("Lee CLAUDE.md y los archivos relevantes, dime lo que ves y haz 1 o 2 preguntas antes de comenzar"_) y luego enviar la pregunta estructurada cuando el agente ya haya cargado el contexto y aclarado ambigüedades. Las preguntas estructuradas de abajo son el destino, no el primer movimiento. Pegarlas en frío funciona; pegarlas después de la orientación funciona mejor, especialmente a medida que el proyecto crece.


Decisión 1: Actualizar el archivo de reglas con la nueva arquitectura

Qué haces (Claude Code). Abre Claude Code en tu proyecto chat-agent/ existente. Informa al agente sobre los cambios arquitectónicos que agrega este curso y pídele que actualice CLAUDE.md en consecuencia:

We're extending this project with three additions on top of the Course
#3 stack: Skills loaded from .claude/skills/, a Neon Postgres system of
record (six-table schema in migrations/001_initial.sql), and MCP wiring
(Neon MCP server for development; a custom customer-data MCP server at
servers/customer_data_mcp/ for runtime).

Update CLAUDE.md to add:

1. New stack lines (Neon Postgres + pgvector, MCP).
2. A new "Architecture (NEW)" section describing where each piece lives.
3. Critical rules preventing three failure modes: business data access
must go through the customer-data MCP server (agent decision logic
never bypasses MCP for business reads or writes); the only permitted
direct asyncpg usage is in the audit subsystem via a separate audit
pool; never use the Neon MCP server's run_sql in production paths
(it's a dev tool); every skill invocation, refund, or database write
produces an audit_log row; skills live in .claude/skills/ (OpenCode
reads as fallback) — do not duplicate in .opencode/skills/.
4. New uv-run commands for the MCP server and migrations.

Keep the file under 2,500 tokens. Show me the diff before writing.

Claude Code redacta la actualización. Lee el diff con atención. La sección "Reglas críticas" es la parte clave; cualquier punto débil allí no previene los modos de falla que debe prevenir. El resultado debería verse más o menos así:

# chat-agent

## Stack

Python 3.12+, uv, openai-agents >=0.14.0 (latest at time of writing: 0.17.2),
your SandboxAgent runtime (UnixLocalSandboxClient for this build), Postgres + pgvector,
MCP (custom server for runtime).

## Architecture (NEW)

- Capabilities: three Skills in .claude/skills/
(summarize-ticket, find-similar-cases, escalate-with-context).
See [agentskills.io](https://agentskills.io/specification) for the format.
- System of record: Neon Postgres, schema in migrations/001_initial.sql.
Six tables: conversations, messages, documents, embeddings, audit_log,
capability_invocations.
- MCP: Neon MCP server for development ONLY (schema, migrations).
Runtime traffic uses a custom MCP server at servers/customer_data_mcp/,
exposing scoped tools (lookup_customer, find_similar_resolved_tickets,
issue_refund). No run_sql in production paths.
- Embeddings: text-embedding-3-small (1536 dims), cosine distance,
HNSW index.

## Critical rules (NEW)

- Business data access must go through the `customer-data` MCP server.
Agent decision logic must never bypass MCP to query or mutate business
data. The only permitted direct asyncpg usage is inside the audit
subsystem, through a separate audit connection pool (Decision 7
explains why audit can't share the MCP boundary it's auditing).
- Never use the Neon MCP server's run_sql tool in production paths.
It's a development tool.
- Every skill invocation, every refund consideration, every database
write produces an audit_log row. No silent actions.
- Skills are in .claude/skills/ (OpenCode reads this as a fallback);
do not duplicate in .opencode/skills/.

## Commands

- `uv run python -m chat_agent.cli`: local dev (uses customer-data MCP via stdio)
- `uv run python -m chat_agent.sandboxed`: sandbox deployment
- `uv run python -m customer_data_mcp.server`: run MCP server standalone
- `uv run alembic upgrade head`: apply pending migrations

El archivo crece de ~30 líneas a ~50 líneas, todavía muy por debajo del presupuesto de 2500 tokens del curso anterior. Si el diff de Claude Code muestra que omite alguna regla crítica o alucina una restricción que no está en el informe, retrocede y vuelve a indicarle. El archivo de reglas es un lugar donde las pequeñas imprecisiones tienen enormes efectos posteriores.

Por qué. El archivo de reglas del curso anterior fijó el motor y la zona de pruebas. Esta actualización fija la arquitectura en capas superiores: skills, sistema de registro, límites de MCP. La regla "datos comerciales a través de MCP, auditoría a través de asyncpg directo con alcance" es la clave: evita que el agente cortocircuite el límite de MCP más tarde cuando alguien tiene prisa, mientras mantiene el subsistema de auditoría independiente del límite que audita. La Decisión 7 explica por qué es importante esa independencia.

Por qué cada regla ocupa su lugar. La misma forma que la tabla de indicaciones de auditoría del curso anterior. Cada regla se asigna a una falla específica que el modelo cometería de otro modo, además de un mensaje de auditoría que se puede pegar y que le permite a su agente de codificación verificar que la regla aún se sigue en el código base actual:

ReglaFallo que previeneSolicitud de auditoría (péguela en su agente de codificación)
Los datos comerciales pasan por el servidor MCP customer-dataLa lógica de decisión del agente provoca un cortocircuito en MCP y consulta Postgres directamente, sin pasar por las herramientas de ámbito y el registro de auditoría"Enumere cada asyncpg, psycopg o sitio de llamadas SQL sin formato en src/chat_agent/. Para cada uno, asigne un nombre al archivo y confirme si está en el subsistema de auditoría (permitido) o en cualquier otro lugar (infracción). Marque cada infracción".
La auditoría utiliza un grupo asyncpg separado, nunca MCPEl subsistema de auditoría depende del límite que audita, por lo que una interrupción del MCP también desactiva silenciosamente la auditoría"Busca la configuración de conexión del subsistema de auditoría. Confirma que crea su propio asyncpg.create_pool(...) y NO importa nada de la capa MCP de datos del cliente".
Neon MCP run_sql es solo para desarrolladores, nunca para producciónSe llama a una herramienta de desarrollo desechable en una ruta de producción, lo que abre la ejecución de SQL sin alcance para el agente. "Busca en el repositorio llamadas run_sql. Enumera todas las coincidencias. Confirma que cada una esté dentro de tools/, scripts/ o un directorio similar exclusivo para desarrolladores; marca cualquiera en las rutas de runtime src/chat_agent/".
Cada skill, reembolso, escritura produce una fila audit_logUna invocación de skill o una escritura destructiva ocurre silenciosamente, por lo que la próxima revisión de cumplimiento no tiene rastro"Enumere cada @function_tool, invocación de skill y escritura de base de datos en el código del agente. Para cada uno, confirme que hay una inserción audit_log correspondiente en la misma transacción o inmediatamente después".
Las skills se encuentran solo en .claude/skills/Las skills se duplican en .opencode/skills/, las dos copias se desplazan y un sutil cambio de comportamiento pasa desapercibido. "Ejecuta ls .claude/skills/ y ls .opencode/skills/ (si existe). No deben contener carpetas de skills. Si .opencode/skills/ existe, indique cualquier carpeta que también esté en .claude/skills/ como una infracción".

Ejecutar las indicaciones de auditoría después de cualquier cambio significativo en el Trabajador; la siguiente conversación que se debe tener con el equipo es una respuesta del agente codificador que enumere las infracciones. La tabla también funciona como una lista de verificación de revisión trimestral para quien posee el gobierno del Trabajador.

Qué cambia en OpenCode. Mismo flujo: informar al agente, revisar la diferencia. Cambia el nombre del archivo a AGENTS.md (si todavía no lo hiciste en el curso anterior) o déjalo como CLAUDE.md (OpenCode lo lee como fallback). Mismo contenido.


Decisión 2: Planificar el esquema y el conjunto de skills

Qué haces (Claude Code). Presiona Shift+Tab dos veces para entrar en modo Plan. El modelo puede leer tu proyecto existente, pero no puede editar nada. Indicación:

Plan the customer-support Worker evolution of this project. The
foundation (OpenAI Agents SDK, your sandbox runtime, sessions, streaming,
guardrails) stays. We're adding:

1. Three Skills: summarize-ticket, find-similar-cases, escalate-with-context.
For each, propose: the description, the operational shape (script-driven
or instruction-driven), and what reference files it needs.

2. The six-table schema from Part 2 Concept 7, plus any tables specific
to a customer-support domain (probably: customers, orders, tickets, refunds).

3. The custom MCP server (customer-data), with exactly the runtime tools
our agent will need. Propose the tool list and signatures. No run_sql.

4. The audit-logging plan — what writes an audit row, what doesn't.

Output the plan as a markdown file at plans/customer-support-worker-plan.md.
Do not write code yet.

El modelo produce un plan. Léelo con atención. Dos lugares donde el primer borrador suele ser incorrecto:

  • Las descripciones de las Skills serán vagas ("resume tickets"). Retrocede: las descripciones deben ser lo bastante específicas como para activarse correctamente (el Concepto 3 de este curso trataba exactamente de esto).
  • Los esquemas de entrada de las herramientas MCP serán más amplios de lo necesario ("consulta: cadena"). Retrocede: cada herramienta debe tener el alcance mínimo que necesita. Una herramienta lookup_customer necesita un customer_id, no un query con el que construya SQL.

Por qué. El modo Plan detecta dos fallas que horas después cuestan más: una skill con una descripción vaga nunca se activa y una herramienta MCP con un esquema de entrada amplio es solo run_sql con pasos adicionales. Ambas son más fáciles de corregir en un plan Markdown que después de crearlas.

Qué cambia en OpenCode. Presiona Tab para cambiar al agente de Plan. Mismo mensaje, mismo resultado del plan. Guarda el archivo del plan con el mismo nombre.


Quién ejecuta SQL y qué servidor MCP

Está a punto de tocar la base de datos por primera vez y verá una gran cantidad de SQL en las Decisiones 3 a 8. Nunca lo escribe ni lo ejecuta a mano. Tres componentes lo poseen y hay dos servidores MCP diferentes que realizan dos trabajos diferentes.

SQL/ruta de datosQuién lo escribeQuién lo ejecutaCuándo
Esquema + migraciones (esta Decisión)Tú lo describes; el agente lo redactaServidor Neon MCP (una herramienta de desarrollo que conectas a tu agente)Una vez, en la configuración
Consultas de verificación (las comprobaciones "Hecho cuando")Mostrado en la lecciónServidor Neon MCP run_sql, manejado por ti en inglés sencilloPara confirmar un paso trabajado
SQL empresarial en tiempo de ejecución: búsquedas, búsqueda vectorial, reembolsos (D6)mcp-builder lo generaEl servidor MCP customer-data que tú creasCada interacción con el cliente
Escrituras de auditoría (D7)El código del subsistema de auditoríaUn grupo asyncpg separado (sin MCP)Cada acción

Dos servidores MCP, nunca confundidos. El servidor Neon MCP (que autenticaste en el paso de configuración anterior) es una herramienta de desarrollo: la usas para aprovisionar y verificar la base de datos en inglés simple, y nunca la usas en runtime. El servidor MCP customer-data es el servidor con alcance que se crea en la Decisión 6; el Worker en ejecución habla con ese, y solo con ese, para obtener datos comerciales. El Concepto 12 explica por qué un run_sql de uso general en producción es un agujero de inyección de prompts.

Leer, escribir y soltar no tienen la misma autoridad. Las herramientas del trabajador en ejecución se dividen por riesgo:

  • Leer (lookup_customer, find_similar_resolved_tickets, integrado en D6): corre libremente, sin puerta. Las lecturas son baratas de permitir.
  • Escribir (issue_refund, integrado en D6): controlado por aprobación (needs_approval=True), por lo que un humano cierra la sesión antes de que se mueva el dinero. Las escrituras de auditoría son solo para agregar: se insertan, nunca se actualizan ni se eliminan.
  • Eliminación/cambio de esquema (CREATE/DROP TABLE, DDL): no se puede invocar en tiempo de ejecución. El servidor personalizado nunca expone una herramienta DDL, por lo que no hay nada que aprobar. Los cambios de esquema ocurren solo en el momento del desarrollo (esta Decisión), a través del servidor Neon MCP, en una rama temporal antes de que lleguen a main.

La regla general: las lecturas se ejecutan libremente, las escrituras están controladas y los cambios estructurales nunca llegan a la producción a través del agente.

Decisión 3: aprovisionar Neon y ejecutar la migración del esquema

Impacto en los costos (Decisión 3)

El nivel gratuito de Neon cubre un solo Worker en el volumen que asume la Parte 5 (~200 conversaciones/día). Planifica $0/mes aquí. Los límites del plan gratuito son 0,5 GB de almacenamiento y 100 horas de computación por proyecto (precios de Neon); por encima de eso, el nivel de lanzamiento es de pago por uso (aproximadamente $0,11/hora CU + $0,35/GB-mes), y un Worker del ejemplo resuelto generalmente permanece por debajo de $25/mes. Consulta la tabla de formas de costos de la Parte 5 para obtener un desglose completo.

Qué haces (Claude Code). Presiona Shift+Tab para salir del modo Plan. Asegúrate de que el servidor Neon MCP esté conectado (del Concepto 12). Entonces:

Using the Neon MCP server, provision a project called "chat-agent",
enable the vector extension, and run the schema from
migrations/001_initial.sql (which you'll generate from the plan).
Use prepare_database_migration to test on a branch before committing
to main. Then add NEON_DATABASE_URL to my .env (use the main-branch
connection string).

El agente usa las herramientas del servidor Neon MCP: create_project, run_sql (para CREATE EXTENSION vector), prepare_database_migration (para probar en una rama) y complete_database_migration (para aplicar a main). Entra el esquema del lado del Worker. Al final de esta Decisión, tienes una base de datos Neon en funcionamiento con las seis tablas, pgvector habilitado, índices creados y la cadena de conexión en .env.

Este es exactamente el caso de uso de desarrollo del Concepto 12. Gestión de esquemas mediante lenguaje natural en una rama, con aprobación humana explícita (tú dijiste "adelante") antes de tocar main. El servidor Neon MCP se ha ganado su lugar aquí.

Qué cambia en OpenCode. Mismo flujo. La integración MCP de OpenCode utiliza el servidor Neon de manera idéntica; la interfaz de usuario visible difiere (el formato de solicitud de aprobación) pero las operaciones son las mismas.

Cómo se ve migrations/001_initial.sql

La migración completa es el esquema de seis tablas del Concepto 7, más cuatro tablas específicas de dominio para el ejemplo de atención al cliente. Dos piezas que vale la pena mostrar aquí:

-- migrations/001_initial.sql — abbreviated; full file in the repo

CREATE EXTENSION IF NOT EXISTS vector;

-- [Six tables from Concept 7 omitted: conversations, messages, documents,
-- embeddings, audit_log, capability_invocations. See Concept 7.]

-- Domain-specific tables for customer-support
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
tier TEXT NOT NULL CHECK (tier IN ('free', 'pro', 'enterprise')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
public_id TEXT NOT NULL UNIQUE, -- the #4429-style display ID
placed_at TIMESTAMPTZ NOT NULL,
amount_cents INT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('placed','shipped','delivered','refunded','cancelled'))
);

CREATE TABLE tickets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
order_id UUID REFERENCES orders(id) ON DELETE SET NULL,
summary TEXT NOT NULL,
resolution TEXT,
status TEXT NOT NULL CHECK (status IN ('open','in_progress','resolved','escalated')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMPTZ
);

CREATE TABLE refunds (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
ticket_id UUID REFERENCES tickets(id) ON DELETE SET NULL,
amount_cents INT NOT NULL,
reason TEXT NOT NULL,
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Verifica que la Decisión 3 funcionó. Abre psql con tu NEON_DATABASE_URL y ejecuta:

SELECT extname FROM pg_extension WHERE extname = 'vector';   -- one row: 'vector'
SELECT COUNT(*) AS table_count FROM information_schema.tables
WHERE table_schema='public'; -- expect 10
SELECT indexname FROM pg_indexes
WHERE indexname = 'idx_embeddings_hnsw'; -- one row

Tres filas de vuelta, sin errores: la Decisión 3 está hecha. Si falta vector, el paso CREATE EXTENSION en la rama temporal no se fusionó con main; vuelve a ejecutar complete_database_migration. Si el recuento de tablas no cuadra, la migración no se aplicó correctamente.


Decisión 4: Escribe la primera skill, summarize-ticket

Lo que haces: desarrollar una skill y luego ser dueño del único campo que importa.

No crees manualmente la carpeta a partir de un archivo en blanco. Genérala con una skill y luego dirígela. Instala skill-creator del ecosistema y pídele que use el andamiaje de summarize-ticket. Produce la carpeta, el SKILL.md, el cuerpo y los ejemplos. Luego haces lo único que la skill no puede juzgar por ti: hacerte cargo del campo description. Es el mismo patrón de instalar una skill y luego pedir, usado para los servidores MCP, aplicado a Skills: tú describes, el agente scaffoldea, tú diriges.

Instala la skill (misma sintaxis que el Capítulo 57):

npx skills add https://github.com/anthropics/skills --skill skill-creator

Luego pídele a Claude Code que desarrolle la skill:

Use skill-creator to scaffold a skill named summarize-ticket in
.claude/skills/. It summarizes a customer support ticket into five
sections: Customer Context, Issue Description, Resolution Steps Taken,
Current Status, Recommended Next Action. The body should be imperative
step-by-step instructions, two examples (one short ticket, one
complex), and three edge cases (escalated ticket, ticket with no
resolution yet, irate customer). Keep the body under 200 lines.

skill-creator produce la carpeta completa. Ahora haz la parte que no delegas: reescribe el description tú mismo. La razón es el Concepto 3. El description determina si la skill llega a activarse; si está mal, la skill nunca se activa en silencio o se activa en cada mensaje (peor). La calidad de la descripción es una decisión sobre qué frases del usuario deberían activar esta skill, y ese juicio se agrava durante toda la vida de la skill. Una descripción estructurada cumple la especificación, pero es genérica; una escrita a mano captura los disparadores específicos que te importan. El cuerpo, los ejemplos y los casos extremos pueden generarse bajo revisión humana.

El description que eliges, después de algunos borradores:

---
name: summarize-ticket
description: Summarizes a customer support ticket into a structured format with sections for Customer Context, Issue Description, Resolution Steps Taken, Current Status, and Recommended Next Action. Use when the user provides a ticket ID, asks for a ticket summary, asks "what's the status of ticket X", or asks to be brought up to speed on a ticket. Produces a concise summary suitable for handoff to another agent.
---

La disciplina del Concepto 3 tiene tres partes:

  • Nombre qué ("resume un ticket de atención al cliente en un formato estructurado").
  • Nombre cuándo ("cuando el usuario proporciona un ID de ticket, solicita un resumen del ticket, pregunta 'cuál es el estado del ticket X' o solicita que se le informe sobre un ticket").
  • Superficie palabras clave específicas ("ID del ticket", "resumen del ticket", "estado del ticket", "actualizado").

Luego lee el cuerpo que escribió el creador de skills. Rechaza cualquier cosa narrativa. Las skills funcionan mejor con instrucciones imperativas ("Busca el ticket. Extrae X. Formatea como Y.") que con descripciones narrativas ("Esta skill está diseñada para ayudar con..."). Dirígelo de nuevo hacia el imperativo; la disciplina del Concepto 3 se aplica por completo.

Por qué. Esta es la primera vez que el agente obtiene una capacidad que no estaba en tools.py. La siguiente conversación que mencione un ID de ticket no pasará por el stub search_docs; activará esta skill y seguirá sus instrucciones. El modelo decide qué skill aplica basándose en la descripción; el cuerpo controla lo que sucede después.

Qué cambia en OpenCode. OpenCode descubre automáticamente la skill en .claude/skills/summarize-ticket/SKILL.md a través de su ruta alternativa (Concepto 4). No se necesita duplicación.

Cómo se ve .claude/skills/summarize-ticket/SKILL.md
---
name: summarize-ticket
description: Summarizes a customer support ticket into a structured format with sections for Customer Context, Issue Description, Resolution Steps Taken, Current Status, and Recommended Next Action. Use when the user provides a ticket ID, asks for a ticket summary, asks "what's the status of ticket X", or asks to be brought up to speed on a ticket. Produces a concise summary suitable for handoff to another agent.
---

# Summarize ticket

When this skill activates:

1. Extract the ticket ID from the user's message. Format: a UUID, or a
public ID like "TKT-1234". If both forms are present, prefer the UUID.
2. Call the `lookup_customer` tool with the ticket's customer_id (which
you'll get in step 4).
3. Call `find_similar_resolved_tickets` with the ticket's summary, to
surface 2-3 prior similar resolutions.
4. Fetch the ticket itself by querying for ticket_id = <id>. If the ticket
has order_id set, fetch the order too.
5. Compose a five-section summary:

**Customer Context** — name, tier, account age, any recent prior tickets
**Issue Description** — the ticket summary, the affected order if any
**Resolution Steps Taken** — actions logged in audit_log scoped to this ticket
**Current Status** — status field, who is assigned, time since last update
**Recommended Next Action** — based on similar resolved tickets, propose ONE
specific action: respond with template, escalate, request more info, close

6. Keep the summary under 200 words total.

## Example: short ticket

Ticket TKT-1042 from a free-tier customer who reported a missing order. Status:
open, 18 minutes old, no actions yet. Three similar resolved tickets show
this is usually a delivery-tracking issue resolved in 2 hours.

Output:
**Customer Context**: Sara K. (free tier, 7-month account, 0 prior tickets)
**Issue Description**: Order #4912 not received, 5 days post-shipping date
**Resolution Steps Taken**: None — ticket 18 minutes old
**Current Status**: Open, unassigned
**Recommended Next Action**: Reply with delivery-tracking template; auto-close
in 24h if no follow-up.

## Example: complex ticket

[A second worked example with an escalated ticket — omitted for brevity.]

## Edge cases

**Escalated ticket** — include the escalation reason and the assignee. Mark the
"Recommended Next Action" as "Continue with assignee; do not duplicate."

**No resolution yet** — explicit "no actions logged" in Resolution Steps Taken.
Don't infer.

**Irate customer (from message sentiment)** — flag in Customer Context.
Recommend tier-2 review even if the issue is small.

Las otras dos Skills, en resumen. Se aplica la misma forma. Dos bloques más de introducción, cada uno de cuarenta minutos de cuidadosa descripción escrita, y el Trabajador tiene las tres Skills que necesita.

# .claude/skills/find-similar-cases/SKILL.md (frontmatter only)
---
name: find-similar-cases
description: Searches the resolved-tickets library for tickets semantically similar to a customer's described issue, returning the top 3-5 with their resolutions, ranked by how closely each matches. Use when the user describes a problem, complaint, or symptom and you need to check whether the team has handled something similar before. Calls the find_similar_resolved_tickets MCP tool. Always run this BEFORE drafting a response, so the response can reference proven prior resolutions rather than inventing a new approach.
---

El cuerpo sigue estos pasos:

  • Extraer la descripción del problema del contexto.
  • Llamar a find_similar_resolved_tickets con limit=5.
  • Presentar los tres primeros con sus valores de distancia en una tabla Markdown.
  • Marcar explícitamente coincidencias de baja confianza (distancia superior a ~0,3, donde menor significa más similar) como "no se encontró ningún precedente previo sólido".

La instrucción "ejecuta siempre esto ANTES de redactar" está haciendo trabajo real; sin ella, el modelo a veces redacta una respuesta a partir de recuerdos y nunca mira la biblioteca.

# .claude/skills/escalate-with-context/SKILL.md (frontmatter only)
---
name: escalate-with-context
description: Packages a customer conversation for handoff to a tier-2 support agent. Produces a structured escalation note with customer profile, issue summary, what was already tried, why escalation is recommended, and the suggested specialist team. Use when (a) the customer is on the Pro or Enterprise tier AND the issue is unresolved after one round of investigation, (b) the customer's sentiment is clearly negative, (c) the issue involves billing >$500 or a refund decision, or (d) the user explicitly asks for a human.
---

El cuerpo invoca summarize-ticket primero para obtener el contexto estructurado, luego escribe una nota de escalamiento de seis secciones (contexto del cliente, problema, intentos de resolución, señales de opinión, equipo recomendado, SLA sugerido). Las cuatro condiciones de activación explícitas en la descripción son las que impiden que esta skill se dispare demasiado; un Trabajador con una lógica de escalada vaga intensifica todo, lo que frustra el propósito.

Según la Decisión 7, las tres skills estarán activas en .claude/skills/. La estructura se transfiere; sólo cambian las descripciones y los detalles operativos.


Decisión 5: construir el proceso de incrustación y generar la biblioteca de documentos

Impacto en los costos (Decisión 5)

Un corpus inicial de unas pocas docenas de tickets resueltos a ~300 tokens cada uno se incorpora por una fracción de centavo a los $0,02 de text-embedding-3-small por cada millón de tokens de entrada. La incorporación continua de nuevos tickets y mensajes normalmente se mantiene por debajo de $3/mes con el volumen del ejemplo trabajado. Las incrustaciones son la línea más barata de la factura; la palanca de costos es el presupuesto de inferencia, no el presupuesto de incorporación.

Qué haces. Dos piezas aquí: el código del pipeline de embeddings (del Concepto 9) y un pequeño corpus para convertir en embeddings. La "biblioteca" del Worker para este ejemplo es un conjunto seleccionado de tickets resueltos en el pasado: lo bastante pequeño como para ejecutarse rápido, lo bastante grande como para que la búsqueda semántica muestre su valor. Una docena de filas basta para sentir el trabajo de la arquitectura; unas pocas docenas hacen que los resultados de recuperación sean más interesantes. El curso no incluye el corpus, porque escribirlo es parte del ejercicio: dale a tu agente la forma de la columna y algunas filas de ejemplo, y haz que genere el resto.

mkdir -p data/seed
# data/seed/resolved-tickets.csv (columns: id, customer_email, summary, resolution)
# A few starter rows; ask your agent to expand this to a dozen or more,
# varied enough that semantic search has something to discriminate between.
id,customer_email,summary,resolution
1,sara@example.com,Refund not showing up two weeks after return approved,Refund was stuck in pending due to a payment-gateway batch delay; manually re-triggered and funds posted within 24h.
2,raj@example.com,Cannot log in after email change,Account email was updated but the session cache held the old address; cleared the session and the customer logged in normally.
3,mei@example.com,Duplicate charge on a single order,Gateway retry created a second authorization; voided the duplicate and confirmed only one capture settled.

La columna customer_email permite al sembrador buscar o crear una fila customers antes de insertar el ticket (la clave externa tickets.customer_id es NOT NULL, por lo que el sembrador no puede omitir este paso). Luego pregúntale al agente:

Implement src/chat_agent/embedding/{chunker,embedder,seeder}.py based on
the patterns from Concepts 9 and 10 of the course. Add a CLI command
`python -m chat_agent.embedding.seeder data/seed/resolved-tickets.csv`
that:

1. Reads the CSV.
2. For each row:
a. Find or create a customers row by email (default tier='free').
b. Insert a tickets row with status='resolved' and customer_id set.
c. Insert a documents row with source='past_case', title=the ticket
summary, body=summary+resolution combined, and metadata containing
the ticket id ({"ticket_id": "<uuid>"}). This is the row the
embedding will link to: embeddings.document_id references
documents(id), not tickets(id). See Concept 7's schema and the
Concept 14 query.
d. Chunk the documents row's body.
e. Embed each chunk with text-embedding-3-small.
f. Insert each chunk into embeddings with document_id set to the
documents row from step (c) (not conversation_id).
3. Write a single audit_log row recording "seed_run" with the row count.

Use asyncpg via a connection pool. Do NOT use the Neon MCP server for
this; direct connection only. We're populating data, not managing
the database.

El modelo produce el sembrador. Ejecútalo. Observa cómo crece el recuento de embeddings. Confirma que el índice funciona:

-- A sanity-check query you should run after seeding
SELECT COUNT(*) FROM embeddings WHERE document_id IS NOT NULL;
SELECT model, COUNT(*) FROM embeddings GROUP BY model;
EXPLAIN ANALYZE
SELECT chunk_text, embedding <=> (SELECT embedding FROM embeddings LIMIT 1) AS d
FROM embeddings ORDER BY d LIMIT 5;

El EXPLAIN ANALYZE debería mostrar "Escaneo de índice usando idx_embeddings_hnsw"; ese es el índice HNSW haciendo su trabajo. Si muestra "Seq Scan", el índice no se está utilizando (generalmente como resultado de que la tabla es demasiado pequeña; el planificador de pgvector recurre a escaneos secuenciales por debajo de un cierto número de filas, lo cual está bien para un corpus de semillas pequeño, pero vale la pena saberlo).

Por qué esto es asyncpg directo, no MCP. Los scripts semilla son infraestructura. Se ejecutan una vez, a mano, por un operador autorizado. No son una ruta de ejecución de un trabajador. El límite de MCP es para cosas que el agente hace de forma autónoma; el script semilla es para las cosas que haces. No pongas límites innecesarios entre tú y tu propia base de datos.

Qué cambia en OpenCode. Mismo enfoque. Usa el equivalente al comando de barra diagonal /build de OpenCode (o simplemente la indicación directa) para generar los mismos archivos.

Verifica que la Decisión 5 funcionó. El bloque de verificación de cordura de arriba es tu superficie de verificación. Específicamente:

  • SELECT COUNT(*) FROM documents WHERE source = 'past_case' debe ser igual al recuento de filas en tu CSV. Cada ticket resuelto se convierte en una fila documents; esta es la fila a la que se vincula el embedding.
  • SELECT COUNT(*) FROM embeddings WHERE document_id IS NOT NULL también debe ser igual al recuento de filas CSV (un embedding por ticket corto, ya que el corpus semilla está por debajo del umbral de fragmentación).
  • SELECT model, COUNT(*) FROM embeddings GROUP BY model debería mostrar una fila, nombrando el modelo de embedding que realmente usaste. Dos filas significan que mezclaste modelos a mitad de siembra; vuelve a ejecutar desde un estado limpio.
  • La línea EXPLAIN ANALYZE debería mostrar Index Scan using idx_embeddings_hnsw para corpus de más de unos cientos de filas. Con un corpus de semillas pequeño, el planificador de pgvector vuelve a Seq Scan y eso está bien.

Si los recuentos son cero, tu sembrador se tragó una excepción en silencio; revisa la fila audit_log que escribió para la ejecución de siembra.


Decisión 6: escribir el servidor MCP personalizado para acceso en tiempo de ejecución

Impacto en los costos (Decisión 6)

El servidor MCP personalizado se ejecuta en tu proceso de sandbox, así que no hay costo de alojamiento por separado. Donde aparece la factura es en inferencia: cada llamada de lookup_customer o find_similar_resolved_tickets agrega el costo de un viaje de ida y vuelta en tokens al siguiente turno del modelo. El Concepto 15 cubre la latencia y el tamaño del pool de MCP bajo carga.

Lo que debes hacer: construir el servidor con mcp-builder y luego dirigirlo con la disciplina que enseña este curso. No escribas a mano el boilerplate de FastMCP. Es el mismo patrón de instalar una skill y luego pedir que se use para servidores MCP: tú describes las tres herramientas, el agente construye el servidor, el agente lo verifica. Tu trabajo es dirigir la construcción con las reglas Digital-FTE que la generación repetitiva no inferirá por sí sola.

Instala mcp-builder (misma sintaxis que skill-creator de la Decisión 4):

npx skills add https://github.com/anthropics/skills --skill mcp-builder

Luego describe el servidor. Tres herramientas para comenzar: lookup_customer, find_similar_resolved_tickets, issue_refund. No run_sql. Cada herramienta tiene un alcance limitado. El siguiente mensaje transmite la disciplina como una dirección explícita, no como algo que el agente debe adivinar:

Use mcp-builder to build a custom MCP server at
src/customer_data_mcp/server.py on the mcp.server.fastmcp.FastMCP
framework, with three @mcp.tool() functions and NO general-purpose
run_sql tool (scope is the point; see Concept 14):

1. lookup_customer(customer_id) → returns id, email, tier, active_tickets.
2. find_similar_resolved_tickets(description, limit=5) → embed the
description with text-embedding-3-small (the SAME model used at insert
time; mixed models return nonsense rankings), THEN run the vector
query. Join embeddings to documents (embeddings.document_id →
documents.id) and on to tickets via documents.metadata->>'ticket_id',
filtered to source='past_case' and status='resolved'. Returns
ticket_id, summary, resolution, distance (cosine distance, lower = more similar).
3. issue_refund(order_id, amount_cents, reason) → inside ONE transaction:
insert a refunds row, mark the order status='refunded', and write the
audit_log row. The audit write and the action it logs must commit in
the same transaction, never as an afterthought.

Use asyncpg with a connection pool. Register pgvector on connections that
touch the embedding column. Type every function. Run via stdio as __main__.

mcp-builder produce el servidor y lo verifica. Lee la docstring de cada herramienta. Cada docstring es lo que lee el modelo para decidir cuándo llamar a esa herramienta: la misma función que una descripción de SKILL.md. Las docstrings vagas hacen que las herramientas se activen en el momento equivocado. Las cuatro disciplinas hacia las que lo dirigiste son exactamente las reglas que un andamiaje MCP genérico pasaría por alto:

  • Herramientas con alcance, no amplias run_sql.
  • Incrustar antes de consultar con un modelo coincidente.
  • registro de pgvector.
  • La auditoría se escribe dentro de la misma transacción.

Poseerlos en el mensaje es lo que hace que este sea un servidor Digital-FTE en lugar de una base de datos estándar.

Por qué este es un servidor MCP personalizado, no solo llamadas asyncpg en el código del agente. Las tres razones del Concepto 14, en el orden en que son importantes para este trabajador:

  • Alcance: el agente puede hacer exactamente tres cosas en la base de datos, nada que SQL permita.
  • Aislamiento: el servidor MCP se ejecuta en su propio proceso, con su propio grupo de conexiones que el agente no puede agotar accidentalmente.
  • Reusabilidad: cuando construimos el segundo Worker que también necesita lookup_customer, habla con el mismo servidor.

Qué cambia en OpenCode. El servidor MCP en sí es independiente de las herramientas. El registro del servidor MCP del agente pasa a opencode.json:

{
"mcp": {
"customer-data": {
"type": "local",
"command": ["python", "-m", "customer_data_mcp.server"],
"env": { "DATABASE_URL": "${env:DATABASE_URL}" }
}
}
}
Esqueleto de customer_data_mcp/server.py
# src/customer_data_mcp/server.py
import json
import os
from typing import Annotated

import asyncpg
from mcp.server.fastmcp import FastMCP
from openai import AsyncOpenAI
from pydantic import Field

mcp: FastMCP = FastMCP("customer-data")
_pool: asyncpg.Pool | None = None
_oai: AsyncOpenAI | None = None


async def get_pool() -> asyncpg.Pool:
global _pool
if _pool is None:
_pool = await asyncpg.create_pool(
os.environ["DATABASE_URL"], min_size=1, max_size=10,
)
return _pool


# The SQL in this server uses unqualified table names (embeddings, tickets, ...),
# which assumes the tables live in the `public` schema. That holds if you
# followed the course's migration as-is. If you put the schema somewhere else
# (a Neon branch with a non-default schema), schema-qualify the table names:
# Neon's pooled endpoint silently drops `server_settings` startup params and
# resets `SET search_path` on connection release, so search_path isolation does
# not survive a pooled connection.


def get_oai() -> AsyncOpenAI:
global _oai
if _oai is None:
_oai = AsyncOpenAI()
return _oai


@mcp.tool()
async def lookup_customer(
customer_id: Annotated[str, Field(description="Customer UUID.")],
) -> dict[str, str | int]:
"""Look up customer by UUID. Returns id, email, tier, active_tickets count.
Use when the user provides a customer ID and you need their profile.
"""
pool = await get_pool()
async with pool.acquire() as conn:
row = await conn.fetchrow(
"""SELECT id::text, email, tier,
(SELECT COUNT(*) FROM tickets t
WHERE t.customer_id = c.id AND t.status='open') AS active_tickets
FROM customers c WHERE id = $1::uuid""",
customer_id,
)
return dict(row) if row else {"error": "customer not found"}


@mcp.tool()
async def find_similar_resolved_tickets(
description: Annotated[str, Field(description="Customer's issue description.")],
limit: Annotated[int, Field(ge=1, le=10)] = 5,
) -> list[dict[str, str | float]]:
"""Find resolved tickets similar to a description, via pgvector.
Use when the user describes an issue and you want to check prior resolutions.
"""
oai = get_oai()
emb = await oai.embeddings.create(
model="text-embedding-3-small", input=[description],
)
query_vec = emb.data[0].embedding

pool = await get_pool()
async with pool.acquire() as conn:
# embeddings.document_id references documents(id), not tickets(id).
# Decision 5's seeder stores each resolved ticket as a documents row
# (source='past_case') with the ticket id in metadata, so the join
# path is embeddings -> documents -> tickets.
rows = await conn.fetch(
"""SELECT t.id::text AS ticket_id, t.summary, t.resolution,
(e.embedding <=> $1::vector) AS distance
FROM embeddings e
JOIN documents d ON d.id = e.document_id
JOIN tickets t ON t.id = (d.metadata->>'ticket_id')::uuid
WHERE d.source = 'past_case' AND t.status = 'resolved'
ORDER BY e.embedding <=> $1::vector
LIMIT $2""",
query_vec, limit,
)
return [dict(r) for r in rows]


@mcp.tool()
async def issue_refund(
order_id: Annotated[str, Field(description="Order UUID.")],
amount_cents: Annotated[int, Field(ge=1, description="Refund amount in cents.")],
reason: Annotated[str, Field(description="Reason recorded with the refund.")],
) -> dict[str, str]:
"""Issue a refund for an order. Writes refunds row, updates order.status,
and writes an audit_log row. Use only when refund is authorized.
"""
pool = await get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
refund_id: str = await conn.fetchval(
"""INSERT INTO refunds (order_id, amount_cents, reason)
VALUES ($1::uuid, $2, $3) RETURNING id::text""",
order_id, amount_cents, reason,
)
await conn.execute(
"UPDATE orders SET status='refunded' WHERE id = $1::uuid",
order_id,
)
await conn.execute(
"""INSERT INTO audit_log
(actor, action, target, payload, result)
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)""",
"worker:customer-support", "refund_issued", order_id,
json.dumps({"amount_cents": amount_cents, "reason": reason}),
json.dumps({"refund_id": refund_id}),
)
return {"refund_id": refund_id, "order_id": order_id, "status": "ok"}


if __name__ == "__main__":
mcp.run(transport="stdio")

Verifica que la Decisión 6 funcionó. Inicia el servidor independiente y confirma que enumere sus tres herramientas personalizadas:

# The agent spawns the stdio MCP server itself (via MCPServerStdio), so you do
# not start it separately. Run the agent and ask it to list the tools it sees.
uv run python -m chat_agent.cli "test: list your available tools"

La primera respuesta del agente debe nombrar las tres herramientas personalizadas: lookup_customer, find_similar_resolved_tickets, issue_refund. Algunos modelos también presentan sus propias metaherramientas internas (un contenedor de llamadas paralelas, por ejemplo); eso es normal y no viene de tu servidor MCP. Si ves una herramienta de base de datos genérica (run_sql o similar), el agente también está conectado al servidor Neon MCP en runtime; elimínala de su lista de runtime mcp_servers= (según el Concepto 12, es solo para desarrollo). Si faltan las tres herramientas personalizadas o aparece un error de timeout de inicio, el servidor MCP falló al arrancar; revisa stderr del subproceso para ver la excepción real. La advertencia de timeout del Concepto 13 cubre el caso de importación de ML.

Un rastreo SSL/Event loop is closed al apagar es inofensivo

Este servidor contiene un cliente AsyncOpenAI (para generar embeddings dentro de find_similar_resolved_tickets). Cuando el subproceso MCP se cierra, la limpieza de la conexión de la biblioteca HTTP puede adelantarse al desmontaje del bucle de eventos del transporte stdio e imprimir un rastreo de varias líneas Fatal error on SSL transport / RuntimeError: Event loop is closed en stderr. Es ruido de apagado: la ejecución ya se completó. Juzga el éxito por el código de salida y el registro de auditoría, no por un cierre que parezca limpio. (Si el ruido te molesta, la solución más profunda es mover la llamada de embedding al proceso del agente y pasar el vector a la herramienta como argumento; eso también reduce el alcance del servidor, que es el objetivo del Concepto 14).


Decisión 7: Registro de auditoría por cable en todas partes

Qué haces. Dos piezas de cableado: las escrituras de auditoría del propio agente (invocaciones de skills, llamadas de modelos) y las escrituras de auditoría dentro del servidor MCP (ya mostrado en issue_refund arriba). Ambos ocurren junto con acciones, nunca como una ocurrencia tardía.

El ayudante del lado del agente del Concepto 10 (log_capability) se utiliza en tres sitios en cli.py y sandboxed.py: al inicio y al final de cada invocación de Skill, después de cada llamada a la herramienta MCP y alrededor de los viajes de guardarraíl. La disciplina clave: la escritura de auditoría y la acción que registra ocurren en la misma ruta de código, idealmente en la misma transacción.

Pregúntale al agente:

Modify src/chat_agent/cli.py to log_capability around each
Runner.run_streamed call. Capture: which skills activated (visible
from RunItemStreamEvent.tool_called for skill events), which MCP
tools were called, total tokens, latency, status. Write to audit_log
via a direct asyncpg connection (NOT through the customer-data MCP
server — audit is the meta-layer; it shouldn't share the same MCP
boundary as the data it audits).

El grupo de conexiones de auditoría está separado del grupo del servidor MCP de datos del cliente. Dos razones: la auditoría debe tener éxito incluso si el grupo de datos está saturado y las escrituras de auditoría no deben competir con las escrituras comerciales para las mismas conexiones. Un subsistema de auditoría que puedes verse afectado por el sistema que está auditando no es un subsistema de auditoría.

La infraestructura de auditoría es más simple de lo que parece. El Concept 7 se envía audit_log y capability_invocations; El concepto 10 incluye el asistente que escribe ambos en una sola transacción. Lo que esta Decisión añade es llamar a ese ayudante en cada límite significativo. La mecánica es sencilla; la disciplina está en recordar hacerlo de forma constante.

Qué cambia en OpenCode. Idéntico. El código de auditoría es Python simple; No hay diferencias específicas de la herramienta.

Verifica que la Decisión 7 funcionó. Ejecuta una conversación descartable contra el agente y luego pregunta a la base de datos si se activó la auditoría:

-- Pick the most recent conversation
SELECT id FROM conversations ORDER BY started_at DESC LIMIT 1;

-- ... then for that conversation_id, every meaningful event should have a row
SELECT action, target, created_at FROM audit_log
WHERE conversation_id = '<id-from-above>'
ORDER BY created_at;

Al ejecutar desde el Python independiente cli.py, debería ver como mínimo: message_received, al menos un capability_invoked y message_sent. No hay ninguna fila skill_activated de la ruta independiente; La Decisión 8 explica por qué y muestra cómo ver uno dentro del Código Claude. Si ve solo las filas del lado de MCP (capability_invoked + acciones de dominio como refund_issued), el ayudante log_capability del lado del agente está conectado pero no se activa; verifique que lo haya invocado desde el bucle de eventos Runner.run_streamed, no solo al inicio de la ejecución. Si ve cero filas: el grupo de conexiones de auditoría no se está conectando; compare audit.py y su asyncpg.create_pool(...) con su NEON_DATABASE_URL.


Decisión 8: Verificar de un extremo a otro con el escenario de prueba

Qué haces. Ejecuta el escenario del cliente a partir del informe:

You: I haven't received my refund from order #4429 — it's been two weeks.

Rastrea lo que sucede. Al ejecutar desde el Python independiente cli.py, debería ver tres cosas en el registro de auditoría en unos segundos, y una cuarta que depende de si el agente tiene suficiente estado para buscar al cliente:

  1. action=message_received: llega el mensaje del usuario, se crea la fila de conversación.
  2. action=capability_invoked, target=mcp:find_similar_resolved_tickets: la búsqueda vectorial encuentra casos pasados ​​similares. Ésta es la decisión clave; el agente lee la resolución anterior más cercana y la utiliza para redactar.
  3. (Condicional) action=capability_invoked, target=mcp:lookup_customer: solo si el agente tiene un UUID de cliente. El primer turno normalmente no lo hace (el usuario le proporcionó un número de pedido y un correo electrónico, no un UUID), por lo que esta fila solo aparece una vez que una capa ascendente (autenticación, el orquestador, una herramienta lookup_customer_by_email que agrega más adelante) haya resuelto el cliente. Una ejecución del primer turno sin esa resolución se salta esta fila, lo cual está bien; la respuesta aún puede citar el caso pasado.
  4. action=message_sent: borrador de respuesta del agente, grabado.

Si se autorizara un reembolso: una fila action=capability_invoked, target=mcp:issue_refund, más una fila action=refund_issued escrita por la propia herramienta MCP (Decisión 6) que lleva el monto y el motivo en payload. Si un reembolso no está autorizado (la verificación de la política falla), una fila action=refund_blocked con el motivo: el caso de no acción es tan importante como el caso de acción para la repetición. Todos estos nombres de acciones coinciden con la tabla de vocabulario canónico del Concepto 10.

Lo que no verás en la ruta independiente de Python es una fila skill_activated. El descubrimiento de skills es una capacidad del cliente Claude Code/OpenCode (la leyenda "Una condición previa" cerca de Quick Win); un Agent(...) desnudo no escanea .claude/skills/. Las tres Skills de la Decisión 4 están en el disco, son válidas y listas, pero el agente independiente llega a lookup_customer y find_similar_resolved_tickets a través del límite de MCP directamente, no a través de la skill summarize-ticket. Para ver cómo se activa la capa de Skills, ejecuta el mismo agente dentro de Claude Code (siguiente llamada).

Si solo ve las filas del lado MCP

message_received y message_sent provienen del cableado de auditoría del lado del agente de la Decisión 7. Las filas capability_invoked y de reembolso provienen del propio servidor MCP (Decisión 6). Si omitiste la Decisión 7 (una porción de aprendizaje razonable), solo verás las filas del lado MCP. Sigue siendo un estado de éxito: la empresa escribe y tiene un recibo. Agrega el cableado del lado del agente cuando necesites reproducir el razonamiento, no solo los resultados.

Vea cómo se activa la capa de Skills: ejecútela dentro de Claude Code

Todo lo anterior se ejecuta a través del Python independiente cli.py, que no puede cargar .claude/skills/. Para ejercitar la capa de Skills según lo prometido, abre este mismo proyecto en Claude Code (u OpenCode) y proporciona el mensaje del escenario directamente. Ahora el descubrimiento de skills está activo: el cliente escanea .claude/skills/, el modelo coincide con la descripción summarize-ticket y el cableado log_capability del lado del agente registra una fila skill_activated junto con las filas capability_invoked. El mismo esquema de auditoría, el mismo vocabulario canónico, un valor de acción más: la fila que la ruta independiente estructuralmente no puede producir. Este es el momento en que las tres capas, Skills + sistema de registro + MCP, son todas visibles en una sola traza.

Luego ejecuta la consulta de reproducción, la que demuestra que tu registro de auditoría se puede reproducir:

SELECT al.created_at, al.action, al.target, al.payload, al.result
FROM audit_log al
WHERE conversation_id = $1
ORDER BY al.created_at;

Lee el resultado. Deberías poder reconstruir, línea por línea, lo que hizo el agente y por qué, sin volver a ejecutar el modelo. Si no puedes (si ocurrió un paso que no está en el registro, o si una fila está en el registro pero la acción que afirma que ocurrió no se refleja en las tablas comerciales), hay un error de cableado. Arréglalo ahora.

Por qué este escenario. Ejercita las piezas arquitectónicas que este curso agrega: herramientas respaldadas por MCP (lookup_customer, find_similar_resolved_tickets) se ejecutan en el sistema de registro y un registro de auditoría registra la ruta, todo reproducible en SQL. Nada de eso existía en el agente de chat del curso anterior. La capa Skills es la tercera pieza y la leyenda de arriba muestra dónde se une al trazo.

Qué cambia en OpenCode. La verificación es independiente del cliente: ejecútela dentro de Claude Code u OpenCode y obtendrá el mismo seguimiento, incluida la fila skill_activated, siempre que el código del agente, las skills y el servidor MCP sean los mismos. El Python independiente cli.py produce el mismo seguimiento menos esa fila, por el motivo que explica la Decisión 8.


¿Qué acaba de pasar?

Ocho Decisiones, y el agente de chat del curso anterior ahora tiene la base de un Trabajador. Mira lo que cambió:

  • Capacidad eliminada del código. Tres skills se encuentran en .claude/skills/, con versión controlada y compartibles entre agentes.
  • Los almacenes duraderos salieron del proceso. Un esquema real de Postgres (el núcleo de seis tablas más una capa de dominio para clientes, pedidos, tickets y reembolsos) ahora contiene el estado del Worker, su sistema de registro y la memoria que busca con pgvector.
  • El acceso a la base de datos en tiempo de ejecución está mediado. El agente no tiene asyncpg en sus importaciones; se comunica con Postgres a través de un servidor MCP con alcance que expone exactamente tres herramientas.
  • Cada acción deja un rastro. El registro de auditoría puede reproducir el rastro de razonamiento completo de cualquier conversación, semanas o meses después del hecho, en SQL.

El OpenAI Agents SDK sigue ahí. El sandbox sigue siendo tu proceso (UnixLocalSandboxClient para esta construcción; Docker o Cloudflare cuando quieres aislamiento real). El streaming, las barreras de seguridad y el tracing del curso anterior siguen ahí. Lo que cambió es la arquitectura superior: las skills contienen las capacidades, el sistema de registro contiene la verdad, MCP las conecta.

Ésa es la base de un Trabajador. Lo que todavía no es estar siempre activo, ser proactivo o formar parte de una fuerza laboral administrada. Esos son los movimientos que añaden los próximos cursos.


Parte 5: Dónde termina este curso

Forma de costo de un trabajador: ¿cuánto cuesta realmente operar?

El curso anterior tenía el hilo “cada turno vuelve a facturar al mundo”; Hasta ahora, este curso ha sido silencioso en cuanto a los dólares. Para cerrar la brecha, para el ejemplo práctico de la Parte 4 específicamente: un trabajador de atención al cliente que realiza 200 conversaciones por día, ~10 mensajes por conversación, contexto promedio de 8K tokens por turno, tres skills, tres herramientas MCP.

Cuatro líneas de costos, en orden de tamaño.

LíneaConductorCosto mensual aproximadoCuando muerde
Inferencia del modelotokens de entrada × giros × $/M$60–$200Volumen × tamaño del mensaje. Las visitas de caché a prefijos estables (CLAUDE.md, metadatos de skills, aviso del sistema) normalmente recuperan entre el 60% y el 80% del costo de entrada.
Neon Postgresalmacenamiento + computación activa$0–$25El nivel gratuito cubre a un solo trabajador que maneja este volumen. Escalar a cero significa que las horas de inactividad no cuestan nada. El nivel pago entra en vigor una vez que un proyecto cruza los límites del plan gratuito de 0,5 GB/100 horas CU (consulta precios de Neon para conocer los números actuales).
Incrustacionestrozos × $0,02/M de tokens$0,30–$3Costo único para datos iniciales más incorporación incremental de nuevos tickets/mensajes. Insignificante a menos que estés incrustando historiales de conversaciones completos continuamente.
Computación de espacio aisladominutos de contenedores$0–$15El ejemplo resuelto se ejecuta en UnixLocalSandboxClient en $0. Esta línea es para computación de producción (Docker, Cloudflare, E2B, Modal) y depende de la duración de la sesión y la simultaneidad. La cosecha ociosa ayuda; las sesiones de larga duración no lo hacen.

Total mensual para el trabajador de la Parte 4: aproximadamente entre $60 y $240. El modelo es la línea más grande, por un orden de magnitud sobre todo lo demás. El sistema de registro es esencialmente gratuito en este volumen. La disciplina de skills (revelación progresiva, Concepto 2) también es disciplina de costos: un Trabajador con cuerpos SKILL.md inflados paga por cada turno que los activa; un Trabajador con descripciones solo en el momento del descubrimiento y cuerpos ajustados al activarse paga por lo que utiliza.

Tres perillas que más mueven el dial. (1) Tasa de aciertos de caché: mantén estables CLAUDE.md, la indicación del sistema y los metadatos de skills; los errores de caché cuestan entre 5 y 10 veces más que los aciertos. (2) Nivel de modelo: el mismo Worker en un modelo más económico (DeepSeek V4 Flash, Claude Haiku) suele hacer el 80 % del trabajo al 10 % del costo; enruta las decisiones difíciles al modelo de frontera solo cuando sea necesario. (3) Crecimiento de la tabla de auditoría: audit_log es la tabla más grande por recuento de filas (PRIMM Predict del Concepto 7); particiona o archiva después de 90 días si en realidad no consultas los datos históricos de auditoría.

El número de escala honesto. Una fuerza laboral de 50 Workers en esta forma (lo que cubre el próximo curso) cuesta aproximadamente entre $3000 y $12 000 al mes en inferencia, entre $50 y $200 en Neon, dólares de un solo dígito en embeddings y entre $100 y $500 en cómputo sandbox. La capa de infraestructura sigue siendo barata; la factura del modelo es lo que escala. Por eso cada hábito de disciplina de costos del curso anterior (aciertos de caché, niveles de modelo, higiene de contexto) se agrava cuando pasas de un Worker a muchos.


Guía de intercambio: la arquitectura es invariante, los productos no

Este curso nombra proveedores específicos en cada capa (OpenAI Agents SDK, Cloudflare Sandbox, Neon, OpenAI embeddings, MCP Python SDK). Esto se debe a que un ejemplo de enseñanza necesita respuestas concretas, no "usar el tiempo de ejecución de LLM que desee". Pero la arquitectura funciona con cualquier alternativa compatible. Cinco cambios que el diseño del curso anticipa explícitamente:

  • Host de Postgres: Neon → Supabase, AWS RDS, autohospedado. Cualquier cosa con pgvector funciona. Se pierde la ramificación y la escala a cero (esos son específicos de Neon), pero el esquema de seis tablas, la canalización de incrustación, la disciplina de traza de auditoría y el patrón de servidor MCP personalizado son todos transferibles byte por byte. El único cambio es la cadena de conexión y posiblemente la configuración SSL.
  • Almacenamiento vectorial: pgvector → Pinecone, Weaviate, Qdrant. Si rechaza el argumento "una base de datos para datos relacionales y vectoriales" del Concepto 6, intercambie la tabla embeddings por un cliente de base de datos vectorial. El costo: dos almacenes para mantener la coherencia (el Concepto 6 sostiene que esto rara vez vale la pena). El beneficio: mejor recuperación a escalas muy grandes (más de 10 millones de vectores) y simplicidad operativa del servicio administrado.
  • Modelo de embedding: OpenAI → Cohere, Voyage, BGE-small (local). Cambia una constante (EMBEDDING_MODEL) y una dimensión de columna (VECTOR(n)). Ejecuta una regeneración única de embeddings para los datos existentes. El proceso del Concepto 9 no cambia.
  • Sandbox: Cloudflare Sandbox → E2B, Modal, Daytona, tu propio Docker. Cualquier cosa con límites de proceso aislados y un reinicio limpio funciona. El tiempo de ejecución SandboxAgent es independiente del backend; El ejemplo trabajado utiliza Cloudflare. Las skills scripts/ se ejecutan de la misma manera. El diagrama de límites de confianza del curso anterior todavía se aplica.
  • Runtime del agente: OpenAI Agents SDK → LangGraph, CrewAI, Pydantic AI, tu propio bucle. El límite de MCP es lo que sobrevive; cada framework moderno de agentes tiene un cliente MCP. Las skills funcionan en cualquier agente que pueda cargar archivos SKILL.md (Claude Code, OpenCode, Goose y, cada vez más, Cursor/Windsurf). La disciplina del registro de auditoría es Python independiente del framework.

Lo que no se intercambia fácilmente. El protocolo MCP en sí, la especificación del formato de skills y el hábito de la traza de auditoría. Estas son las piezas que llevas entre productos; los productos son las piezas que intercambias. Misma forma arquitectónica debajo, implementaciones reemplazables encima.


Lo que este curso no cubre (todavía)

Ahora tienes un Trabajador que satisface dos de las Siete Invariantes que establece la tesis. En concreto: se ejecuta sobre un motor (Invariante 4, del curso anterior) y se apoya en un sistema de registro (Invariante 5, de este curso). Los otros cinco invariantes son los que requieren las empresas nativas de IA de producción y lo que cubren los cursos posteriores. Aquí cada uno es una viñeta, no una sección.

  • Invariante 1: El ser humano es el principal. Especificaciones creadas, puertas de aprobación, declaraciones de presupuesto. La arquitectura para establecer intenciones y apropiarse de los resultados, tratada en la Parte 6 del libro.
  • Invariante 2: Cada ser humano necesita un delegado. Un agente personal en el borde que mantiene tu contexto, representa tu juicio e intermedia trabajo con la fuerza laboral. La tesis nombra OpenClaw como la realización actual.
  • Invariante 3: La fuerza laboral necesita un gerente. Un orquestador que asigna trabajo, hace cumplir presupuestos, audita la ejecución y expone la contratación como una capacidad invocable. La tesis nombra Paperclip.
  • Invariante 6: la fuerza laboral se puede expandir según la política. Una metacapa donde un agente autorizado genera un prompt, aprovisiona un runtime y registra un nuevo trabajador, sin despertar a un humano. Claude Managed Agents es una realización.
  • Invariante 7: La fuerza laboral funciona con un sistema nervioso. Los activadores (horarios, webhooks, llamadas API entrantes) despiertan al agente bajo el sobre de autoridad. Inngest (funciones duraderas y trabajos en segundo plano) es una realización para eventos generales de la fuerza laboral; Rutinas de código Claude es la ruta específica del agente de codificación.

Un solo trabajador que lee desde un sistema de registro es la unidad más pequeña de la arquitectura que enseña este curso. El próximo curso amplía ese Trabajador a una fuerza laboral: múltiples Trabajadores coordinados por un gerente, ampliables según demanda y activados por disparadores. La misma base de OpenAI Agents SDK, el mismo proceso de sandbox (UnixLocalSandboxClient en el ejemplo trabajado; Docker, Cloudflare, E2B o Modal funcionan de manera idéntica), el mismo formato de Skills, el mismo sistema de registro Postgres. La arquitectura es invariante.


Cómo llegar a ser realmente bueno en esto

Leer este curso intensivo no te hace bueno para formar trabajadores. Usarlo sí. El camino es el mismo que en el curso anterior: empiezas de forma manual, sientes la fricción y dejas que cada fricción te enseñe a qué concepto pertenece.

El mapeo para este curso:

  • "¿Por qué mi skill no se dispara cuando debería?" → calidad de la descripción (Concepto 3). Reescríbela. Prueba inventando cinco formas diferentes en que un usuario podría expresar el disparador.
  • "¿Por qué el agente inventa datos que la base de datos no tiene?" → el agente en realidad no está llamando al servidor MCP. Verifica la traza; verifica el registro mcp_servers=[...].
  • "¿Por qué mi registro de auditoría está incompleto?" → la escritura de auditoría no está en la misma ruta de código que la acción (Concepto 10). Muévelo al lado de la acción, en la misma transacción.
  • "¿Por qué mis resultados de pgvector son irrelevantes?" → la fragmentación es incorrecta (Concepto 9) o el modelo de embedding en el momento de la inserción no coincide con el modelo de embedding en el momento de la consulta. Vuelve a generar embeddings.
  • "¿Por qué mi servidor MCP es lento bajo carga?" → el grupo de conexiones dentro del servidor es demasiado pequeño o la lista de herramientas no está almacenada en caché en el cliente. Concepto 15.
  • "¿Por qué el servidor Neon MCP da miedo en producción?" → porque la propia documentación de Neon dice que no es para producción. Escribe un servidor MCP personalizado (Concepto 14). El primero toma 30 minutos; el segundo toma 10.

Crea la arquitectura pieza por pieza. No intentes agregar skills, sistema de registro y MCP en un solo fin de semana. Toma el agente de chat del curso anterior. Primero agrega un sistema de registro (Decisiones 3 a 5) y observa cómo cambia tu experiencia de depuración. Añade una Skill (Decisión 4) y observa cómo el modelo decide usarla. Agrega el límite de MCP al final (Decisión 6). Cada paso es su propio aprendizaje; hacer las tres cosas a la vez es un muro.

El dividendo de la portabilidad se extiende. Las skills que escribas aquí funcionan en cualquier cliente compatible con Agent Skills. Los esquemas que escribas aquí funcionan en cualquier Postgres. Los servidores MCP que escribas aquí funcionan con cualquier cliente MCP: Claude, GPT, Gemini, modelos locales. La arquitectura es invariante; los productos son de 2026. Cuando los productos cambian, tu código casi no cambia.

El cambio en lo que dedicas el tiempo

Después de la Decisión 4, tu trabajo cambia de forma. Escribir código se convierte en informar al agente; revisar la descripción (un campo del archivo de configuración que normalmente hojearías) se convierte en la tarea clave. Una descripción que dedicó 30 minutos a redactar y refinar realiza más trabajo arquitectónico que las 200 líneas de código del servidor MCP que el agente generó debajo de ella, porque la descripción es la superficie de enrutamiento que el modelo lee en cada paso.

Dos cambios prácticos. Primero, deja de preguntar "¿cómo implemento esto?" y empieza a preguntar "¿cuáles son las cinco formas diferentes en que un usuario real podría expresar el disparador?". El código viene después; si la descripción está mal, el agente nunca llega al código y la calidad del código es irrelevante. En segundo lugar, la revisión reemplaza a la autoría como skill clave. El agente redacta; tú decides si el borrador funciona en los casos disparadores para los que escribiste la descripción. La parte más difícil es resistir la tentación de reescribir cuando podrías resolverlo tú mismo en tres minutos: la misma disciplina que te impide traspasar el límite de MCP.


Referencia rápida

Los 15 conceptos en una línea cada uno

  1. Una skill de agente es una carpeta. SKILL.md más scripts/referencias/activos opcionales.
  2. Revelación progresiva. Metadatos al inicio → cuerpo completo al activar → referencias a pedido.
  3. A SKILL.md es frontmatter + body. Nombre, descripción, metadatos opcionales y luego instrucciones operativas.
  4. Las skills viajan como archivos. El mismo SKILL.md funciona en Claude Code y OpenCode sin modificaciones.
  5. Componga pequeñas skills mediante la transferencia del sistema de archivos cuando el aislamiento importa más que la simplicidad de la orquestación.
  6. Postgres + pgvector supera a una base de datos vectorial separada para casi todas las cargas de trabajo de agentes. Neon agrega ramificación, escala a cero y un servidor MCP.
  7. Seis tablas son el esquema operativo mínimo: conversaciones, mensajes, documentos, incrustaciones, auditoría_log, capacidad_invocaciones.
  8. Conceptos básicos de pgvector: VECTOR(1536) + <=> distancia coseno + índice HNSW. Usa el mismo modelo de embedding en ambos extremos.
  9. El proceso de incrustación: fragmento en límites semánticos (~400 tokens con superposición), incrustación por lotes, almacenamiento con metadatos del modelo.
  10. La auditoría no se registra. Cada acción significativa escribe una fila en la misma transacción que la acción que registra.
  11. MCP es un protocolo, no un servicio. Tres primitivas (herramientas, recursos, indicaciones), tres transportes (stdio, HTTP transmitible, SSE heredado).
  12. El servidor Neon MCP es para desarrollo. Diseño de esquemas, migraciones basadas en sucursales. No para el tiempo de ejecución de producción.
  13. El SDK de OpenAI Agents tiene un cliente MCP integrado. from agents.mcp import MCPServerStdio, MCPServerStreamableHttp. Usa async with. Almacena list_tools en caché en producción.
  14. Los servidores MCP personalizados se ganan la vida gracias al alcance, el aislamiento y la reutilización. No escriba uno para una sola función utilizada por un agente.
  15. MCP bajo carga: HTTP transmitible para control remoto, herramientas de caché, reutilización de conexiones, agrupación dentro del servidor, propagación del contexto de seguimiento a través de _meta.

Árbol de decisión: @function_tool versus servidor MCP personalizado versus servidor MCP de proveedor

Capability used by one agent, one process, one function?
→ @function_tool

Capability that multiple agents (or multiple deployments) will reuse?
→ Custom MCP server

Vendor provides one and it does what you need?
→ Vendor MCP server (don't rebuild)

Sensitive operations, needing narrow scope?
→ Custom MCP server (NOT a broad `run_sql` interface)

Long-running, background, or process-isolated work?
→ Custom MCP server (process isolation buys safety)

Referencia rápida de ubicación del archivo

QuéCamino
Skills de proyecto.claude/skills/<name>/SKILL.md (OpenCode se lee como alternativa)
Skills personales~/.claude/skills/<name>/SKILL.md (OpenCode se lee como alternativa)
Migraciones de esquemasmigrations/NNN_*.sql
Código de incrustaciónsrc/chat_agent/embedding/{chunker,embedder,seeder}.py
Servidor MCP personalizadosrc/customer_data_mcp/server.py
Registro del servidor MCP (Código Claude).claude/settings.json mcpServers
Registro del servidor MCP (OpenCode)opencode.json mcp bloque
Ayudante de auditoríasrc/chat_agent/audit.py

Cuando algo se siente mal

Skill not firing when it should
→ Description too vague. Rewrite with "Use when..." and specific keywords (Concept 3).

Skill firing when it shouldn't
→ Description too broad. Add explicit constraints in the description.

pgvector returning irrelevant results
→ Embedding model mismatch (insert vs. query). Verify the model column in
the embeddings table. Re-embed if needed.

MCP tool not appearing in agent
→ Server not registered, or list_tools cache stale. Check mcp_servers=[...]
and try cache_tools_list=False temporarily.

Audit log has gaps
→ Action and audit write are in different code paths. Move them next to
each other, ideally same transaction.

Agent timing out on Postgres operations under load
→ MCP server's connection pool too small. Check asyncpg.create_pool(max_size=...).

MCP server hangs on startup with torch / sentence-transformers / large imports
→ Default client_session_timeout_seconds=5 is too short for servers that
load ML models at import. Bump to 60. See Concept 13's callout.

CREATE TABLE fails: relation "notes" already exists
→ You're pointing at a database that already has tables. Use a fresh
database or Neon branch for the Quick Win (Step 2).

An OLD asyncpg refuses the Neon DSN with "unsupported startup parameter"
→ Only older asyncpg versions choke on channel_binding=require. The
version this course pins accepts it; if you are pinned to an old
asyncpg, either upgrade or strip that one parameter from the DSN.
TLS posture is unchanged either way.

Neon -pooler endpoint: schema-qualified SQL needed, search_path ignored
→ The pooled endpoint silently drops server_settings startup params
and resets SET search_path on connection release. If your tables
are not in the public schema, schema-qualify them (see the
Decision 6 MCP-server skeleton's get_pool comment). A reader who
built in public is unaffected.

Non-OpenAI key getting 401 against api.openai.com
→ Set OPENAI_BASE_URL to your provider's OpenAI-compatible endpoint
(e.g., https://api.deepseek.com/v1) before running the agent.

Agent fails partway with a 401 / auth / BadRequestError
→ Wrong key, wrong provider, or expired key. Run the curl key smoke-test
from Step 5 before the full run; it fails in one second instead of
four files deep.

Neon MCP server returning errors in production agent code
→ You're using it wrong. Neon's docs are explicit: development only.
Write a custom MCP server instead (Concept 14, ~30 minutes).

Apéndice: Audiencia, requisitos previos, glosario

Público, requisitos previos, un resumen del curso anterior, una introducción a Postgres, una guía de lectura de primer paso y un glosario. Úselo como referencia: retroceda cuando un término del flujo principal no le resulte familiar, o lea A.1 antes de comenzar si no está seguro de cumplir los requisitos previos.

A.1: Público y requisitos previos

Audiencia. Este es un curso intermedio a avanzado, más denso que sus predecesores. Completar Crear agentes de IA es recomendable, no obligatorio: el proyecto inicial en el archivo zip complementario reproduce el estado final de ese curso desde cero, así que puedes comenzar aquí con solo un OPENAI_API_KEY. Lo que sí necesitas es sentirte cómodo con lo que enseñó ese curso, porque ampliamos su agente de chat en lugar de volver a explicarlo. El OpenAI Agents SDK, el bucle del agente, las sesiones, el streaming, las function tools, el sandboxing: todo se asume, ya sea que lo hayas aprendido en el curso anterior o que ya lo conozcas.

Requisitos previos. Esta página asume cuatro cosas.

  1. Puedes ejecutar el proyecto inicial. El zip complementario envía un proyecto completo y ejecutable que reproduce el estado final de Crear agentes de IA: mismo diseño, mismo agents.py, mismo cli.py, mismo tools.py, más un sandboxed.py que se ejecuta en UnixLocalSandboxClient. Descomprímelo, uv sync, configura OPENAI_API_KEY y estarás en la línea de salida. Se recomienda completar el curso anterior para entender por qué las piezas tienen esta forma, pero ya no es una puerta difícil.
  2. Tienes la disciplina Curso intensivo de codificación agente. Modo de plan, archivos de reglas (CLAUDE.md / AGENTS.md), comandos de barra diagonal, disciplina de contexto. El ejemplo resuelto de este curso utiliza comandos de barra diagonal de Skills en un momento dado, por lo que el hábito del archivo de reglas se mantiene.
  3. Has realizado al menos un ciclo PRIMM-AI+ del Capítulo 42. Las indicaciones de Predicción en este curso asumen que sabes predecir, ejecutar, investigar, modificar y crear.
  4. Tienes un modelo mental de Postgres funcional. Tablas, índices, transacciones, claves foráneas. No es necesario ser un DBA. Debes saber qué hace SELECT ... WHERE, para qué sirve un índice y, aproximadamente, qué hace JOIN. Si ha escrito una aplicación CRUD en cualquier idioma, está calibrado.

A.2: Lo que te enseñó el curso anterior que este curso supone

Curso completo: Creación de agentes de IA con OpenAI Agents SDK y Cloudflare Sandbox. Las ocho cosas de ese curso se basan directamente en este:

  1. El marco de estado y confianza. Cada primitiva de agente es la respuesta del SDK a una pregunta de estado o de confianza. Este curso amplía ambos ejes: estado en un sistema de registro, confianza en una biblioteca de skills.
  2. El bucle del agente. El modelo decide → is_final? → run_tool (límite de confianza) → el historial crece → próximo turno. Este curso agrega llamadas a herramientas MCP e invocaciones de skills a ese bucle, pero la forma del bucle no cambia.
  3. @function_tool. Una función de Python escrita expuesta al modelo. El Concepto 14 de este curso lo contrasta con las herramientas expuestas a MCP; necesita saber qué es @function_tool para entender cuándo no alcanzar MCP.
  4. Sesiones. SQLiteSession del curso anterior todavía funciona. Este curso lo complementa con un traza de auditoría respaldado por Postgres, no lo reemplaza.
  5. Transmisión de eventos. Runner.run_streamed y RunItemStreamEvent. Registramos las activaciones de skills y las llamadas a herramientas MCP de estos eventos (Decisión 7).
  6. Barandillas. Barandillas de entrada y salida. Este curso no agrega nuevos conceptos de barandillas; transfieres lo que tienes.
  7. El tiempo de ejecución de la zona de pruebas. SandboxAgent con Shell() y Filesystem() (el ejemplo trabajado del curso anterior utilizó Cloudflare; Docker/E2B/Modal funciona de manera idéntica). El trabajador de este curso todavía se implementa en un entorno limitado; el sistema de registro vive fuera de él (en Neon).
  8. El patrón de doble herramienta (Claude Code + OpenCode). Las skills que escribes en .claude/skills/ funcionan en ambos. El registro del servidor MCP difiere según la configuración de la herramienta; el servidor en sí es idéntico.

Señal de parada. Si "el bucle del agente es modelo → herramienta → historial → bucle, con max_turns limitándolo" se lee como repaso, continúa. Si te parece material nuevo, detente y realiza primero el curso anterior. El ejemplo resuelto de este curso evoluciona el agente de chat de ese curso; leer sin esa base genera fricción.

A.3: Conceptos básicos de Postgres que utiliza este curso

Si ha escrito una aplicación CRUD en cualquier idioma, está calibrado. Las cosas que verás:

  • Tablas, claves primarias, claves externas. Seis tablas en nuestro esquema, cada una con una clave primaria UUID y claves externas explícitas para su padre.
  • Índices. Índices de árbol B regulares en las columnas de búsqueda; Índices HNSW en columnas vectoriales. Los índices aceleran las consultas; Cuestan en inserciones.
  • Transacciones. BEGIN ... COMMIT (o async with conn.transaction(): en asyncpg) agrupa varias escrituras para que todas sucedan o ninguna. La escritura de auditoría y la acción que registra van en una transacción, según el Concepto 10.
  • Columnas JSONB. Tipo JSON nativo de Postgres. Almacena datos clave-valor arbitrarios, consultables con operadores JSON. Utilizado en audit_log.payload y audit_log.result.
  • Extensiones. CREATE EXTENSION vector habilita pgvector. Existen otras extensiones (pg_trgm para búsqueda de texto, postgis para búsqueda espacial); solo necesitamos pgvector.

Señal de alto. Si "JOIN tickets ON tickets.customer_id = customers.id" te parece obvio, continúa. Si la sintaxis de JOIN no te resulta familiar, el ejemplo resuelto se leerá con fricción. Primero, elige un tutorial de Postgres de 90 minutos.

A.4: Cómo leer esta página en la primera pasada

Misma regla que el curso anterior de la serie:

  • Ampliar en la primera lectura: cualquier cosa etiquetada como "Lo que verá", "Transcripción de muestra", "Resultado esperado", "Verificar". Estos contienen el comportamiento ejecutable para comparar las predicciones.
  • Omitir en la primera lectura: cualquier cosa etiquetada como "Cómo se ve skill.md en su totalidad", "El SQL de migración completo" y otros listados de archivos completos en el ejemplo resuelto de la Parte 4. La narrativa sobre cada bloque te dice qué cambió; solo necesitas el contenido del archivo cuando realmente lo construyas.
  • Opcional: los bloques "Prueba con IA" al final de cada concepto. Indicaciones de extensión para Claude Code u OpenCode; omítelos sin culpa si tu herramienta no está configurada.

El objetivo del primer paso es internalizar el modelo de tres capas: las skills son la capa de capacidad, el sistema de registro de Neon es la capa de estado y MCP es el conector. La segunda parte de ese objetivo es ver cómo las tres capas se ubican sobre la pila de tiempo de ejecución de Sandbox SDK + OpenAI Agents que ya conoce. La segunda pasada con las manos sobre el teclado es donde construyes.

A.5: Glosario

Estos son los términos que con mayor probabilidad harán tropezar al lector en el primer encuentro. Cada uno se explica nuevamente en el contexto tal como aparece.

  • Skill: una carpeta con un archivo SKILL.md y scripts, referencias y recursos opcionales. La carpeta es la skill; el archivo que contiene es el punto de entrada. Un agente carga skills a través de revelación progresiva: nombre+descripción al inicio, instrucciones completas cuando se activan, archivos de referencia a pedido.

  • SKILL.md: Archivo de entrada de la skill. Frontmatter de YAML con name y description (y metadatos opcionales), luego el cuerpo Markdown con las instrucciones que sigue el agente cuando se activa la skill.

  • Revelación progresiva: El modelo de carga de skills en tres etapas. Descubrimiento: el agente lee los nombres y descripciones de todas las skills disponibles al inicio. Activación: el agente lee el SKILL.md completo de la skill correspondiente cuando una tarea la activa. Ejecución: el agente carga archivos referenciados (o ejecuta scripts empaquetados) bajo demanda durante la ejecución.

  • Sistema de registro (SoR): almacén autorizado de estado en el que el Trabajador lee y escribe. El término de tesis para "la base de datos que contiene la verdad". Para este curso: una base de datos Neon Postgres.

  • Neon: un servicio Postgres administrado con ramificación sin servidor, escalado a cero y un nivel gratuito. Su diferenciador frente a otros Postgres administrados es la bifurcación (copias de bases de datos de copia en escritura en segundos) y su servidor MCP de primera clase.

  • pgvector: una extensión de Postgres que agrega un tipo de columna vector más operadores de distancia para búsqueda por similitud. Permite que una base de datos contenga datos relacionales y búsqueda semántica basada en incrustaciones.

  • Incrustación: un vector numérico de longitud fija que representa un fragmento de texto (u otros datos) de manera que la similitud semántica se asigna a la distancia del vector. Generado por un modelo de incrustación (text-embedding-3-small es el valor predeterminado de OpenAI).

  • MCP (Protocolo de contexto modelo): un estándar abierto sobre cómo los agentes de IA se conectan a herramientas, recursos e indicaciones externos. Define una arquitectura cliente/servidor, tres primitivas (herramientas, recursos, mensajes) y tres transportes (stdio, SSE, HTTP transmisible).

  • Servidor MCP: un programa que expone capacidades (herramientas/recursos/solicitudes) a los clientes MCP. El servidor Neon MCP es un ejemplo; puedes escribir el tuyo propio en Python o TypeScript.

  • Cliente MCP: la contraparte del lado del agente que se conecta a los servidores MCP, enumera sus capacidades y las muestra en el modelo. El SDK de OpenAI Agents tiene un cliente MCP integrado.

  • Herramienta (MCP): una de las tres primitivas de MCP. Una función que el modelo puede invocar. Desde la perspectiva del modelo, una herramienta MCP se parece a @function_tool, pero la implementación reside en el servidor MCP, no en el proceso del agente.

  • Recurso (MCP): una de las tres primitivas de MCP. Una fuente de datos de solo lectura que el agente puede recuperar. Archivos, resultados de consultas de bases de datos, respuestas API. Leer pero no escribir.

  • Pregunta (MCP): una de las tres primitivas de MCP. Una plantilla de solicitud reutilizable que el servidor proporciona para que el modelo la invoque. Menos común que las herramientas y los recursos; útil para plantillas estandarizadas en todos los equipos.

  • Registro de auditoría: una tabla de base de datos que registra cada acción significativa que realiza un trabajador (cada llamada a una herramienta, cada escritura en la base de datos, cada invocación de capacidad) en una forma que la empresa puede reproducir, consultar y razonar después del hecho.

A.6: Lo que este apéndice NO reemplaza

El plan de estudios completo incluye cursos sobre la capa de administrador (Paperclip), la metacapa (contratación de Workers de IA en tiempo de ejecución) y la puerta de enlace de activación (Inngest). Ninguno de ellos se resume aquí, porque extienden más que preceden este curso. Léelos cuando los alcances; El trabajador de este curso aún no los necesita.