# SDK Migration Playbook

Cómo migrar un agente monolítico a SDK (`system.md` + los 8 campos de v5Config). **MCP-first**: todo
con tools de `cortex-gateway`, sin scripts ni SQL. Compañero de `AGENTS.md` (patrones) y `CLAUDE.md`
(contrato de deploy). El split en sí es MCP-agnóstico; lo que cambia vs el flujo viejo es que
**deploy = `deploy_agent_sdk`** y **verify/pull = `get_agent_config`**.

---

## Pre-flight

### 1. Contexto fresco
`git fetch origin main` (PAT-054), revisar feedback reciente, y leer conversaciones reales con
`get_conversations` — no codear a ciegas.

### 2. Sincronizar el local con producción
El prompt del repo casi siempre está atrás del live. Traer el estado real con **`get_agent_config`**
(prompt + config) y sobrescribir los archivos locales. Loguear el sync en `changelog.md`.

### 3. Confirmar que es migración, no iteración
Si `get_agent_config` ya devuelve un `v5Config` poblado, esto es una **iteración** (usar
`cortex-iterate`), no una migración.

---

## Split — monolito → archivos del SDK

### 4. Leer un agente de referencia
Elegir un agente del mismo template que ya esté en SDK y leer su config con `get_agent_config`.
Entender estructura y densidad antes de escribir.

### 5. Escribir los archivos (`agents/<name>/sdk/`)
Fuente de verdad: el **prompt de producción** (paso 2), no la versión vieja del repo.

| Archivo | Qué va | Qué NO va |
|---|---|---|
| `system.md` | Identidad, voz, flujo (stages), calibración, principios, tools, fallback | URLs, keywords, examples, scripts de objeciones |
| `examples.md` | Conversaciones turn-by-turn (flujo + voz + edge cases) | Reglas (van en system.md) |
| `knowledge_base.md` | Metodología, diferenciadores, analogías | Estructura del programa, pricing (van en program.json) |
| `personal_story.md` | Bio, credenciales, insights del creador | |
| `objections.md` | Tabla de objeciones, corner cases, reglas de derivación | |
| `keywords.json` | Keywords con conditions y reglas | |
| `resources.json` | URLs por tipo con `topic_index` | |
| `program.json` | Producto, pricing approach, elegibilidad, buyer persona | |
| `case_studies.json` | Casos de éxito (o `[]`) | |

### Decisiones clave durante el split

Compactar para matchear los agentes de referencia. Errores comunes:

- **Flujo como lista numerada, no headers.** No `### PASO 1` con párrafos; usar `1. **Apertura** —
  saludo + pregunta`. Una línea por stage. El detalle vive en examples. *(Validado: bienestar.rl, 30+
  líneas → 10.)*
- **No migrar listas de frases** ("frases que SÍ/NO dice"). El estilo va en examples, no en reglas.
  Tono = 1-2 líneas con `**Tono:**`.
- **Rotación de validación:** si el monolito repite una frase de validación, agrupar alternativas en
  un bloque `**Rotación de validación:**` en la sección Voz (solo frases del prompt original, nunca
  inventar).
- **Sin vocabulario de plataforma** ("burbuja", "turno"): el agente no sabe qué son. Usar "mensaje" y
  "respuesta".
- **Sin reglas de porcentaje** ("30-60% terminan con pregunta"): el modelo no calcula. Reemplazar por
  regla concreta ("cada mensaje cierra con una pregunta que guía al siguiente paso").
- **Sin preflight self-check:** la checklist "antes de enviar, verificá…" es 100% redundante con Voz +
  Principios. Eliminar (las reglas duplicadas degradan compliance). *(Validado: bienestar.rl.)*
- **Identidad de bot / "¿sos un bot?":** no migrar como sección. Una línea en Identidad ("no sos un
  bot de servicio, sos el clon digital de X") + un example dedicado.

**Principios de system.md:** 5-7 ideal, 10 máx. "Porque" en los 2-3 más críticos (PAT-005).
Instrucciones positivas ("hablar como X") sobre negativas. Consolidar reglas de formato en 1, no 3.

**examples.md:** cubrir happy path, keyword entry, price objection (1ra + insistente), descalificado,
returning, bot suspicion, decline + NO_RESPONSE. **Los examples salen del prompt original, no de la
interpretación del migrador** — si el original no tiene un patrón para un caso, NO inventarlo (el
example gana sobre el principle; PAT-001). Los gaps son items de iteración post-migración. *(Validado:
elartedevivir.ar — un example "hot buyer" inventado contradijo "confirmar interés antes del link" y el
agente empezó a mandar links no solicitados.)*

**Scripts fijos (TEMPLATE PRECIOS, etc.):** si el agente tiene un script multi-mensaje fijo, va inline
en system.md **Y** mostrado en examples.md — el modelo necesita verlo en el prompt cacheado.

**NO_RESPONSE:** la plataforma lo suprime automáticamente, sin config por agente. Ver §calibración
abajo y AGENTS.md §2.

---

## Audit (antes de deploy)

Los audits corren sobre los archivos locales del SDK (grep), no sobre la DB.

### 6. Anti-patterns
Chequear `system.md` contra `knowledge/prompt-antipatterns.md` y `knowledge/AGENTS.md`: sin lógica de
compliance en el prompt (contact limits), sin URLs hardcodeadas sin fallback (PAT-002), rule count ≤10
sin contradicciones, "porque" en las críticas, persona con anchors de comportamiento.

### 7. Claims fabricados
El modelo rellena gaps con patrones de venta genéricos sin fuente. Chequear scarcity ("limitados",
"cupos"), social proof sin fuente ("muchos", "la mayoría"), comparaciones no verificadas ("más
exclusivo"), quality claims ("el mejor", "garantizado") — solo si el cliente los confirmó.
```bash
grep -iE 'limitad|cupos?|muchos (llevan|aprovechan)|más (reducido|exclusivo|personalizado)|garantizad|único en' agents/<agent>/sdk/*.md
```

### 8. Dialect / modismo (PAT-015)
El prompt source suele traer el dialect del operador (rioplatense). El `Language` de `meta.md` define
el target. El agente copia el estilo de los examples, no de las reglas — una regla "español neutro" la
pisan 15 examples con voseo.
```bash
grep -iE '\b(sos|tenés|podés|querés|decís|contás|fijate|escribime)\b' agents/<agent>/sdk/*.md *.json
```
*(AVA Peru SDK scoreó 13% en el check de rioplatense porque el split preservó el voseo en examples.)*

### 9. Em dashes
Los em dashes (—) en líneas de diálogo se reproducen verbatim en WhatsApp/IG. Cero tolerancia en
`examples.md`, `objections.md`, `knowledge_base.md`, `personal_story.md`. OK en headers/markdown.
```bash
grep -P '—' agents/<agent>/sdk/examples.md
```

### 10. Coverage check: SDK vs monolítico
Leer el monolítico sección por sección; para cada regla/comportamiento, verificar que exista en algún
archivo del SDK. Flaggear lo que se perdió en el split:

| Tipo de gap | Ejemplo | Dónde fixear |
|---|---|---|
| Routing override | "peso siempre → programa" | Flujo (inline) |
| Qualifier no-bloqueante | "antecedentes no bloquean avance" | Flujo (nota en el step) |
| Anti-repetición post-acción | "post-link: ángulo nuevo, no repetir" | Calibración (nueva fila) |
| Guard anti-alucinación | "si mencionás el link, incluí la URL" | Examples (mostrar el patrón) |

Ignorar las simplificaciones deliberadas (flows consolidados, frases movidas a examples, vocabulario
de plataforma).

---

## Validar contra un agente de test

### 11. Crear el agente de test (NO tocar producción)
`create_agent` con `set_on_conversation: false` y `isTestAgent: true` (nombre `<Agente> SDK`). Deployar
con **`deploy_agent_sdk`** y verificar con `get_agent_config` que producción quedó intacto.

### 12. Replicar config operacional + triggers
El agente de test necesita la misma config de comportamiento que producción (`response_delay`,
`enable_message_chunking`, `language`, etc.) o estás testeando en otro entorno.

**Triggers NO se heredan.** Un agente nuevo arranca sin ningún trigger. Recrearlos con la skill
**`cortex-triggers`**: `list_triggers` sobre el source para inventariar, `create_trigger` sobre el
target. Cubre DM keywords, comment triggers (+ sus responses), story triggers y CSV negative configs.
Gotchas: los "grupos" de comments en la UI son distintos `match_mode` por fila; los CSV negativos
tienen su propia config con `expiryDateTime`; el agente de test necesita su propio trigger ALL_DM (o
equivalente) o no responde.

**Inventariar lo que apunta al agente de producción** (se actualiza recién en go-live): contact
limits, follow-up workflows, notification destinations, connected accounts. Documentarlos para que el
switch sea limpio.

### 13. Validar contra conversaciones reales
La verdad de campo son las conversaciones reales, no los sintéticos. Bajar 15-20 recientes con
`get_conversations` (mix: funnel completo, atascadas, dropouts cortos), scorearlas contra la checklist,
y comparar el comportamiento del agente de test contra ese baseline. **Leer cada una manualmente** — el
scoring no catchea tool calls ni matices de tono (§1.5).

---

## Go live

### 14. Reemplazar producción
- **A (más seguro):** activar el SDK (`update_agent` → `set_on_conversation: true`), desactivar el viejo
  (`set_on_conversation: false`). Ambos quedan en DB.
- **B (más limpio):** actualizar el agente de producción in-place con el v5Config del SDK.

**Pre-cutover checklist:**
- [ ] El SDK tiene `set_on_conversation: true` (distinto de `is_active`; sin esto está activo pero nunca
  se asigna a conversaciones nuevas). *(Validado: avaperu.oficial — `is_active=true` pero
  `set_on_conversation=false` → no respondía.)*
- [ ] El SDK tiene su trigger ALL_DM (o equivalente) — los agentes de test no heredan triggers.
- [ ] El SDK está linkeado a una connected sub-account (ver CLAUDE.md post-deploy activation).

**Conversaciones activas al cutover:** al desactivar el monolítico, las conversaciones abiertas siguen
asignadas a él; a volúmenes típicos (~20/día) es manejable (prender el SDK y revisar en unas horas). Si
el volumen es alto, considerar reasignar (verificar primero que la plataforma re-evalúe el agente en el
próximo inbound).

### 15. Post-deploy
Patrón recomendado: prender el SDK y revisar manualmente. A las 2-4h, revisar 3-5 conversaciones reales
del SDK; a las 24-48h, correr `cortex-analyze` para diagnosticar regresiones tempranas.

---

## Notas de infra de plataforma

### Custom notifications = 4 entidades separadas
Un contact limit NO crea una notificación. Hacen falta 4 entidades linkeadas (tools MCP entre
paréntesis):
1. `custom_property` — el evaluador booleano (`upsert_custom_property`)
2. `contact_limit` + property conditions — corta al agente (`upsert_contact_limit`)
3. `custom_notification` + destination — qué notificar y dónde (`upsert_custom_notification`,
   `upsert_notification_destination`)
4. `workflow` (CUSTOM_NOTIFICATION) + property conditions — cuándo notificar (`upsert_workflow`)

**⚠ Un workflow de notificación SIN property conditions matchea TODAS las conversaciones y floodea
Slack.** *(juanmahuss 2026-03-30 — 3 workflows sin conditions, cientos de notificaciones falsas.)*

### A/B router (cuando querés A/B real, no full switch)
Setup `A_B_WORKFLOW`: un router (parent) + 2 variants `INDIVIDUAL`.
- **Router `set_on_conversation = false` (crítico).** Con `true`, las conversaciones se pinean al
  `agent_id` del router y algunos mensajes los genera el router directo (sin v5Config → default
  permisivo que ignora los constraints de las variants). *(vicunacoach 2026-05-21 — 12/47 outbounds
  generados por el router con frases prohibidas.)*
- **Variants `set_on_conversation = true`** (sticky routing una vez elegida la variant).
- **La sub-account debe linkearse a los 3 agentes** (router + A + B), no solo al router
  (descubrir el `connected_account_id` con `list_influencer_connected_accounts` —`list_connected_accounts`
  es agent-scoped y da vacío para un agente nuevo, no significa que no haya cuenta conectada—; linkear con
  `set_connected_account` a cada uno; verificar con `list_connected_accounts`). Meta entrega el webhook solo a
  agentes linkeados; si solo está el router, los follow-ups a conversaciones pineadas no llegan a la
  variant y se pierden en silencio.
- El router enruta solo el **primer** mensaje de una conversación nueva; las pre-existentes no se
  re-enrutan.

### Top-level config keys
`deploy_agent_sdk` usa PATCH/merge semantics: solo escribe los campos provistos y **preserva** los keys
top-level (`responseFormatting`, `isTestAgent`, etc.). Igual, verificar con `get_agent_config`
post-deploy que sobrevivieron (PAT-049: el operador puede haberlos pisado por otra vía).

### Campos estructurados: JSON, no string (PAT-073)
`keywords`, `program`, `resources`, `case_studies` van a `deploy_agent_sdk` como valor JSON
(objeto/array) parseado, **nunca como string JSON-encodeado**. Una sección doble-encodeada crasheaba
el runtime; el gateway ahora los normaliza en el write (un string que parsea a objeto/array se
auto-parsea; un string inválido o escalar se rechaza con 400) — nada se guarda verbatim. Igual, pasar
objeto/array de entrada y verificar post-deploy con `get_agent_config` que los cuatro vuelven como
objetos/arrays, no strings (`platform-internals.md` §3e).

---

## NO_RESPONSE — calibración

NO_RESPONSE es el string literal que el agente emite para dejar de responder; la plataforma lo suprime
automáticamente, sin config por agente.

**Triggers estándar:** lead declinó + insiste · descalificado insiste · off-topic · emoji solo · menor ·
recursos ya entregados y sigue sin avanzar.

**Tabla de calibración** (cómo interactúa con tipos de lead):
```
| Enganchado    | Avanzar ritmo normal                         |
| Tibio         | 1 pregunta extra; si no se mueve → NO_RESPONSE |
| Desenganchado | 1 intento más; si sigue → NO_RESPONSE         |
| Precio        | TEMPLATE PRECIOS (secuencia fija)             |
```
**Qué NO hacer:** no agregar "cerrar limpio" / "soft close" antes de NO_RESPONSE para desenganchados
(ir directo a NO_RESPONSE; la excepción es el decline explícito "no me interesa", donde el agente dice
"éxitos!" primero). No poner reglas de formato en Principios (van en Voz).

---

## Errores a evitar

1. **No sincronizar el repo primero** — repo en v5, producción en v18. Siempre traer con
   `get_agent_config`.
2. **Endurecer "ideal" a "hard"** — el prompt decía "15-25 palabras ideal", se escribió "25 hard
   limit". Respetar el phrasing de producción salvo feedback que pida el cambio.
3. **Mover un script crítico (TEMPLATE PRECIOS) a tools-only** — el modelo no lo encuentra. Inline en
   system.md + en examples.
4. **Comportamiento sin example** — "mandá el testimonio si necesita social proof" sin example → nunca
   se manda. Si un comportamiento importa, necesita example.
5. **Reglas de formato en Principios** — ASCII, emojis, puntuación van en Voz.
6. **"Cerrar limpio" sin definir** — instrucción abstracta que el modelo no puede ejecutar. Usar
   NO_RESPONSE (concreto) o un voice example.
7. **Reglas del monolito que no aplican al SDK** — "máx 2 mensajes consecutivos" tenía sentido con el
   chunking viejo; en el SDK la plataforma controla los turnos.

---

## Agentes multi-plataforma (IG + Messenger + WhatsApp)

Poca evidencia (avaperu.oficial, abril 2026). Observaciones:
- Migrar la plataforma primaria (IG) primero y estabilizar el SDK antes de crear mirrors — así los
  examples del mirror se basan en patrones reales, no en teoría.
- Cada plataforma necesita sus propios examples para entry points específicos (price openers de
  click-to-Messenger, keyword triggers de WhatsApp). Compartir system.md y objections.md; divergir en
  examples.md.
- Cuando hacés un fix de comportamiento en el SDK de IG, chequear si aplica a los mirrors — driftean en
  silencio.

---

## Referencias
- Anti-patterns: `knowledge/prompt-antipatterns.md`
- Patrones validados: `knowledge/AGENTS.md`
- Playbook de keywords: `knowledge/keyword-playbook.md`
