Manual de arquitectura

Perla
v3.0

Mapa de obra del producto: vocabulario común, perfiles, routing, instalaciones, sistema constructivo, pliegos y roadmap del agente. Siete capítulos más dos anexos técnicos.

3 de mayo de 2026 7 capítulos · 2 anexos Vertical: salud y bienestar
00 Mapa de obra
00

Mapa de obra

Mapa de obra · v3 · 2026-05-03

Este es el primer documento que cualquier integrante del equipo debería leer antes de entrar al manual. Es el mapa que explica cómo está pensada la arquitectura de Perla, por qué está dividida así, y dónde encontrar cada cosa.


La metáfora: pensar el producto como una obra

Construir Perla es como construir un edificio. No alcanza con tener una buena idea visual ni con conocer los materiales: hay que poder explicar cómo se traduce esa idea en planos, instalaciones, sistema constructivo, especificaciones técnicas y cronograma de obra. Cada capa tiene su lenguaje y su responsable, pero todas hablan del mismo proyecto.

Por eso este manual está dividido en siete capítulos, cada uno equivalente a una capa de un proyecto de arquitectura. Un capítulo no se entiende del todo sin los anteriores, pero cada uno tiene autonomía suficiente para que el responsable de ese frente pueda trabajarlo sin tener que leer todo el resto cada vez.


Por qué construimos así (la apuesta)

Perla no es un asistente de turnos para un negocio individual. Es la apuesta opuesta a la del mercado actual: mientras la competencia (Lidia, AgentlyX, TurnoFácil, entre otras) vende automatizaciones personalizadas a cada cliente por entre $40.000 y $100.000 ARS por mes, Perla propone una inteligencia central compartida que cualquier negocio puede contratar por una fracción de ese precio. La economía estimada es de tipo comodity (alrededor de USD 10-20 por mes por negocio) con margen 4-6× sobre el costo operativo (estimado en USD 50/mes para 1000 turnos agendados).

Esa apuesta de modelo determina toda la arquitectura que viene después. Un sistema multi-tenant real, un agente único compartido entre todos los negocios, un contrato de herramientas estable, una base de datos aislada por tenant — cada decisión técnica del manual existe para sostener esa apuesta. Si cambiara el modelo (por ejemplo: ofrecer instancias dedicadas tipo premium), la arquitectura tendría que cambiar con él. Por eso este párrafo debería leerse antes de los capítulos: explica el por qué antes que el cómo.


Vertical objetivo

Perla está enfocada en salud y bienestar: kinesiología, estética, odontología, terapias, peluquería, barbería. Decisión cerrada el 30/04/2026 después de validar con beta testers iniciales. La arquitectura técnica soporta cualquier vertical de turnos — el lock es de mensaje, no de capacidad técnica. El vocabulario, los presets de personalidad del agente, las plantillas de notificaciones y el copy del onboarding están afinados para ese vertical.


Roles del equipo

Decisión cerrada el 28/04/2026:

  • Tomi — CTO + lead técnico backend. Dueño del repo tomillo/perla.
  • Nahue — frontend / dashboard. Dueño de src/routes/app/* y src/components/.
  • Guido — Supabase + ayudante backend. Acompaña a Tomi en migrations y RLS.
  • Seba — agente conversacional + coherencia interna del producto + diseño + identidad visual. Dueño del módulo src/backend/agent/ cuando arranque y de los siete capítulos del manual.
  • Fede — marca / clientes / GTM / vínculo con beta testers. No toca el repo.

El manual describe el producto, no al equipo. Estos roles entran acá porque determinan quién toca qué capítulo (Tomi y Guido editan el Cap 5 y el Anexo 04a; Nahue edita el Anexo 06a; Seba edita los siete + es dueño del Cap 7). El cronograma operativo (cuándo hace cada uno qué cosa) vive en PM/SISTEMA.md + PM.md.


Estructura del manual

Archivo Rol en el manual Qué resuelve Estado
01_ARQUITECTURA.md Introducción Vocabulario común, principios de diseño no negociables, vista panorámica del sistema, cómo leer el manual v3 — 2026-05-03
02_PROGRAMA.md Capítulo 2 — Programa Define los cuatro perfiles que usan Perla y qué puede hacer cada uno v3 — 2026-05-03
03_CIRCULACIONES.md Capítulo 3 — Plantas y circulaciones Define cómo el sistema identifica al tenant, al rol y a la conversación en curso, con dedup y lock por sesión v3 — 2026-05-03
04_INSTALACIONES.md Capítulo 4 — Instalaciones Define las entidades del producto en siete familias (negocio, personas, interacciones, conversación, agente, billing, permisos) v3 — 2026-05-03
04a_INVENTARIO_SCHEMA.md Anexo A del Cap 4 Inventario técnico verificable: 57 tablas + 50 enums del repo mapeados contra entidades del manual + gaps del schema v3 — 2026-05-03
05_CONSTRUCCION.md Capítulo 5 — Sistema constructivo Define el stack técnico, los tres ambientes Supabase, el sistema de gobierno IA del repo, la observabilidad y el principio de cierre UI↔agente v3 — 2026-05-03
06_PLIEGOS.md Capítulo 6 — Pliegos de obra Define los seis contratos formales: tools, system prompt, adapters, errores, acciones destructivas, confirmación conversacional vs interna v3 — 2026-05-03
06a_MATRIZ_ACCION_TOOL.md Anexo A del Cap 6 Matriz de 113 acciones del dashboard ↔ tools del agente con permisos requeridos · principio de cierre · decisiones de producto abiertas y cerradas v3 — 2026-05-03
07_ROADMAP_AGENTE.md Capítulo 7 — Roadmap operativo del agente Define el orden temporal de los 15 PRs que llevan el agente de n8n al repo TS · gates entre PRs · piezas del módulo src/backend/agent/ v1 — 2026-05-03 (nuevo)

El presente archivo (00_EL-PLAN.md) es el mapa que estás leyendo. No es un capítulo: es la entrada al manual.

Los anexos técnicos (04a, 06a) son ciudadanos de primer nivel — viven al lado de los capítulos, no al final del manual. Existen porque hay contratos que requieren precisión tabular que la prosa conceptual no puede sostener sin volverse ilegible. Cada anexo se referencia explícitamente desde su capítulo.

La asignación de personas a cada capítulo, los roles, la cadencia y el cronograma del equipo no viven en el manual — viven en PM/SISTEMA.md y PM.md en la raíz del proyecto. El manual describe el producto; el PM describe al equipo que lo construye.


Orden de dependencia

Los capítulos no son una lista plana: cada uno apoya en los anteriores. El orden lógico de construcción es:

Programa  →  Instalaciones  →  Circulaciones  →  Pliegos  →  Construcción  →  Roadmap
(quiénes)    (qué existe)      (cómo entran)     (cómo hablan)  (con qué)        (en qué orden)

Se pueden trabajar en paralelo si hay distintos interlocutores en distintos frentes. Cada capítulo tiene un perfil de oficio asociado:

  • Programa se cierra con quien tenga estudiados los flujos conversacionales reales del usuario (Seba con input de Fede).
  • Instalaciones se cierra con quien lleve la base de datos (Tomi y Guido, validado por Seba contra el manual).
  • Construcción se cierra con quien sea responsable del repo de producción (Tomi).
  • Circulaciones, Pliegos y Roadmap se cierran con Seba a partir del feedback que vino de los otros tres frentes.
  • Anexos 04a y 06a se mantienen vivos como ficha técnica — actualizables ante cualquier cambio del schema o del dashboard.

Lo que el manual NO incluye

Perla tiene varias capas más allá de su arquitectura técnica. Algunas ya están cerradas y solo se referencian; otras están en construcción y viven en otros documentos. Ninguna entra al manual porque mezclaría lenguajes y responsabilidades.

Tema Dónde vive Por qué afuera del manual
Marca y voz _Deploy_PERLA_BRAND_GUIDE/ (en el Drive del proyecto) Ya cerrada. Es del producto, no de la construcción.
Modelo de negocio y pricing pendiente (ver Frente E8 en PM.md) Pricing, costos, GTM. Es del producto.
Casos de uso conversacionales 01_INFRA/SPEC_v0.md Es input para el Capítulo 2, no parte del manual.
Cronograma y oficios del equipo PM/SISTEMA.md + PM.md Es proceso, no arquitectura. Antes era Cap 07; se sacó el 2026-04-20 y el nuevo Cap 07 v3 trata otra cosa (roadmap del agente).
Roadmap operativo del producto PM.md (Frentes + sub-tareas) Cambia mes a mes. El roadmap del agente sí entra al manual porque es decisión arquitectónica con N pasos verificables.
Compliance legal 02_MIDDLE/_logics/legal/ Privacy policy, TyC, Ley 25.326, Régimen Transparencia Fiscal. No es arquitectura.
Credenciales y secretos operativos 01_INFRA/CREDENCIALES_PERLA.html (local, no commitable) Operativo, sensible.
Backlog de tareas y bugs del agente n8n 01_INFRA/PLAN_AGENTE_v1.md y eventualmente issues del repo Operativo. El roadmap arquitectónico vive en Cap 7.

Reglas del manual

  • Cada capítulo es un solo archivo canónico. Si cambia, se versiona con sufijo _v2, _v3. Las versiones anteriores se guardan en _versions/ dentro de esta misma carpeta.
  • Cuando un capítulo se considera cerrado, pasa a “ley del proyecto” y solo cambia con discusión explícita del equipo.
  • Si un capítulo necesita citar a otro, se cita explícito (“según Capítulo 3, sección 2”) y no se duplica información.
  • Las siete capas del manual son fijas (Cap 01-07). Si surge contenido nuevo, se ubica como sub-sección dentro del capítulo que corresponda. No se inventa un capítulo extra salvo decisión arquitectónica explícita (como pasó con Cap 07 NUEVO en v3).
  • Toda mención al sistema usa la palabra agente, nunca bot. La distinción es deliberada y se explica en la introducción.
  • Vocabulario “paciente” en el manual + “customer” en el código + “paciente o cliente” en el agente conversacional según business_instructions. Cada capa habla su idioma — ver Cap 1.

Estado actual del producto (línea de horizonte)

Al cierre de v3 del manual:

  • Bot Perla v3.0.1 vivo en n8n.ticketsport.com.ar con 8 tools cableadas, respondiendo a mensajes reales por WhatsApp Cloud API para los dos beta testers iniciales (NUDO, SURI). Workflow Perla_mvp_v2.2.json.
  • Repo tomillo/perla con la plataforma técnica completa: 57 tablas en 7 familias, dashboard con 11 rutas mock, CI/CD activo (preview por PR + staging + producción manual), 3 ambientes Supabase parametrizados, adapters de canal (Telegram + WhatsApp) implementados, Mercado Pago integrado, sistema de permisos granular (19 codes + 4 sets system).
  • PR #1 del Roadmap del agente abierto al cierre del manual (perla/agent-skeleton, PR #32 en GitHub). Skeleton del módulo src/backend/agent/ con endpoint stub. Cuando este PR mergee, arranca PR #2 (runtime mock contra DB).
  • Dominio cerrado: holaperla.com (Tomi) + @holaperla.ia (Fede), 02/05/2026.
  • Vertical lock cerrado: salud y bienestar, 30/04/2026.
  • Vocabulario cerrado: customer en código + paciente en agente y manual, 03/05/2026.
  • Sistema de permisos cerrado: 19 codes atómicos + 4 permission sets system, 23/04/2026 (cierre del gap SG-6 del Anexo 04a v1).
  • Pliego 5 implementado en DB: pending_operations + items + token + TTL con política aprobada en docs/runtime/perla-sensitive-operations-policy.md.

Cuando el PR #13 del Cap 7 mergee, el agente n8n se apaga y el bot del repo es funcional para owner por WhatsApp.


v1 — 2026-04-22 · primera versión del mapa. v2 — 2026-04-24 · incorporación de los anexos técnicos 04a (inventario del schema) y 06a (matriz acción↔tool) tras el sync post-commits del dashboard. v3 — 2026-05-03 · sumado Cap 07 (Roadmap operativo del agente) como séptimo capítulo. Sumadas secciones “Vertical objetivo” y “Roles del equipo”. Línea de horizonte actualizada con estado real del repo y el bot al 03/05. Vocabulario customer/paciente cerrado. Próxima edición cuando un capítulo cambie de estado o cuando aparezca un anexo nuevo.

01 Arquitectura
01

Arquitectura

Prefacio e introducción · v3 · 2026-05-03


Sobre este manual

Este manual describe cómo está construido Perla, no qué es ni a quién aporta valor. Es el mapa que necesitamos los seis para hablar el mismo idioma cuando discutimos producto, código, base de datos o estrategia.

Está organizado en siete capítulos (seis originales más el roadmap operativo del agente), cada uno en un archivo independiente dentro de esta carpeta. Cada capítulo responde a una pregunta distinta sobre el sistema. Se pueden leer en orden o saltar al que corresponde según en qué frente esté trabajando cada uno.


Una nota sobre vocabulario

En este manual — y en todos los documentos del proyecto — hablamos de agente, no de bot.

La distinción es deliberada. Un bot es una automatización rígida que sigue un árbol de decisiones predefinido. Un agente es una inteligencia que entiende intención, decide qué herramientas usar y conduce una conversación con criterio. Perla es lo segundo. Esa diferencia es la apuesta central del producto y se sostiene en cada decisión técnica que viene a continuación.

Las palabras que importan

Las siguientes palabras aparecen en todo el manual y conviene fijarlas acá:

  • Tenant — palabra técnica que describe la arquitectura multi-tenant. En el manual usamos negocio como sinónimo conceptual de tenant cuando hablamos de producto, y business cuando hablamos del schema (la tabla se llama businesses).

  • Paciente — la persona que recibe el servicio, sea en una clínica, una peluquería, un consultorio o un centro de estética. La palabra está alineada con el vertical del producto: salud y bienestar (decisión cerrada el 30/04/2026). En el código del repo, esta entidad se llama customer por convención técnica neutral. Cada capa habla su idioma — el manual usa “paciente”, el agente usa “paciente” o “cliente” según lo que el negocio configure (campo assistant_configs.business_instructions), y el schema usa customer. El Anexo 04a v3 hace de puente. La razón de mantener la divergencia es que cambiar el código tiene costo alto (decenas de migraciones, services, RLS policies) y resuelve un problema cosmético que la voz del agente puede manejar más arriba.

  • Owner — el dueño o la dueña del negocio. Es el perfil principal de gestión.

  • Operador / miembro del equipo — alguien con acceso al dashboard pero sin ser owner. La granularidad de qué exactamente puede hacer la define el sistema de permisos del Cap 4 Familia 7 (no es un rol fijo).

  • Profesional — la persona que presta el servicio (kinesiólogo, esteticista, peluquera). Entidad: staff_member en el schema. No es un quinto perfil humano del Cap 02 — es una membership con un permission_set específico (professional, 5 permisos casi solo lectura).

  • Cliente — palabra ambigua. Puede significar (1) el negocio que paga Perla — pero acá usamos negocio o tenant; o (2) el paciente — pero acá usamos paciente. Evitar “cliente” en el manual salvo en citas a la voz del agente cuando el business configura “cliente” en su personalidad.

Vocabulario completo

El glosario detallado del schema vive en el Anexo 04a_INVENTARIO_SCHEMA_v3.md con la correspondencia tabla por tabla. Algunos términos extra que el lector encontrará y conviene reconocer:

  • Service variant — la unidad agendable real. Un servicio (service) es conceptual; lo que se reserva es una variante con duración, precio y buffer.
  • Booking hold — una reserva temporal de slot mientras el cliente confirma.
  • Schedule block — la ocupación real de la agenda, sea por turno, hold, bloqueo manual o cierre. Centraliza anti-overlap.
  • Channel connection — la configuración de un canal (WhatsApp, Telegram) para un negocio, con referencias a secretos.
  • Conversation thread — un hilo de conversación entre un cliente y un negocio en un canal específico.
  • Agent run — una ejecución del agente: input, output, tokens consumidos, tools llamadas, modelo usado.
  • Tool invocation — una llamada a tool dentro de un agent run.
  • Pending operation — una acción del agente que requiere confirmación técnica (preview + token + TTL) antes de ejecutarse.
  • Permission set — una plantilla reusable de permisos atómicos (los 4 sets system son owner, admin, receptionist, professional).

Vertical objetivo

Perla está enfocada en salud y bienestar: kinesiología, estética, odontología, terapias, peluquería, barbería. Decisión cerrada el 30/04/2026. La arquitectura técnica soporta cualquier vertical de turnos — el lock es de mensaje, no de capacidad técnica. El vocabulario, los presets de personalidad del agente, las plantillas de notificaciones, y el copy del onboarding están afinados para ese vertical.


Principios de diseño

Cinco decisiones que están detrás de cada parte del manual. Si en algún momento se discute una sub-decisión técnica, primero hay que verificar si choca con alguno de estos principios. Si choca, se discute el principio explícitamente. Si no, se respeta.

  1. Agente, no bot. El sistema usa function calling sobre herramientas, no un árbol de decisiones predefinido. El loop de invocación de herramientas es ilimitado, no tope de dos rondas.

  2. Un núcleo, N tenants. Multi-tenant real con aislamiento a nivel de base de datos. El identificador de sesión combina business + cliente + canal. Nunca se levanta una instancia separada por negocio. Dos líneas de defensa: RLS + trigger enforce_tenant_reference.

  3. El contrato son las herramientas. El agente no sabe consultas SQL ni protocolos de canal. Solo sabe invocar herramientas con un contrato definido. Cambiar de canal (Telegram a WhatsApp), de modelo de IA o de base de datos no debería tocar al agente. Cada tool reusa un service backend de dominio que también consume el dashboard real (principio de cierre del Cap 5).

  4. El prototipo valida, la producción robusta produce. El prototipado y la validación rápida de hipótesis se hacen en herramientas de bajo nivel de fricción (n8n hoy, hasta que se cierren los PRs del Cap 7). La producción robusta vive en código (TypeScript con Cloudflare Workers). Nunca al revés.

  5. Doble entrada, mismo agente. El sistema atiende a pacientes y a owners con el mismo agente, distinguidos en el routing (Cap 3) y en los permisos efectivos cargados antes de invocar al modelo. Las herramientas con permisos elevados (cancelar masivamente, modificar política, gestionar permisos) confirman siempre antes de ejecutar — con pending_operations del Cap 6 Pliego 5 cuando aplica.


Vista panorámica

Para tener una imagen mental rápida del sistema completo:

  Canal (WhatsApp / Telegram)
          │
   ┌──────▼──────┐
   │   ENTRADA   │  recibe el mensaje, valida firma, deduplica
   │  + ROUTING  │  resuelve tenant + rol + permisos + hilo
   │             │  agrupa mensajes apilados (inbound_message_batches)
   └──────┬──────┘
          │
   ┌──────▼──────┐
   │   AGENTE    │  cerebro · entiende intención · decide qué hacer
   │             │  loop ilimitado de invocaciones a herramientas
   │             │  registra agent_run con tokens, modelo, intent
   └──────┬──────┘
          │  function calling
   ┌──────▼──────┐
   │ HERRAMIENTAS│  contrato cerrado · interfaz entre agente y datos
   │             │  consultar disponibilidad · agendar · cancelar · reprogramar
   │             │  cada tool valida permiso del catálogo (Familia 7)
   │             │  cada tool reusa service backend del dominio
   └──────┬──────┘
          │
   ┌──────▼──────┐
   │    DATOS    │  base aislada por business (multi-tenant)
   │             │  transacciones seguras · agenda como tabla, no como calendario
   │             │  schedule_blocks centraliza ocupación real
   │             │  audit_events registra cada cambio importante
   └─────────────┘
          ▲
          │
   ┌──────┴──────┐
   │  DASHBOARD  │  segunda superficie de acceso
   │             │  consume los mismos services backend (principio de cierre)
   │             │  permisos efectivos por permission_set
   └─────────────┘

Cada uno de los seis bloques de este diagrama tiene su lugar en el manual:

  • Entrada + Routing vive en el Capítulo 3 (Plantas y circulaciones)
  • Agente y Herramientas se especifican en el Capítulo 6 (Pliegos de obra) + roadmap en Cap 7
  • Datos se describen en el Capítulo 4 (Instalaciones) y su Anexo 04a
  • La infraestructura completa que sostiene los seis bloques está en el Capítulo 5 (Sistema constructivo)
  • Quiénes consumen todo este sistema se describe en el Capítulo 2 (Programa)
  • Dashboard ↔ Agente es el principio de cierre del Cap 5, materializado en la matriz Acción ↔ Tool del Anexo 06a

Cómo leer este manual

Para entender el manual alcanza con leer las descripciones que cada capítulo abre. Para implementar lo que cada uno define hace falta abrir los anexos técnicos (04a, 06a) y eventualmente el código del repo.

Cada capítulo está escrito en tres partes:

  1. Una pregunta-gancho que abre el capítulo y resume qué problema se está resolviendo.
  2. Una descripción del concepto, con suficiente detalle para que un miembro del equipo no técnico pueda seguirla.
  3. Tres aclaraciones operativas: qué incluye el capítulo, qué deliberadamente no incluye, y por qué importa para el resto del sistema.

Cuando un capítulo se considera cerrado, pasa a ser “ley del proyecto” y solo cambia con discusión explícita del equipo. Las versiones anteriores quedan archivadas en _versions/ dentro de esta misma carpeta.


Lo que el manual NO describe

Las decisiones de marca (cómo se ve, cómo habla), las decisiones de negocio (cuánto sale, a quién aporta valor) y los casos de uso conversacionales específicos viven en otros documentos. Acá hablamos solo de cómo está hecho. El mapa completo de qué está adentro y qué está afuera vive en 00_EL-PLAN.md.

El roadmap del equipo (oficios, cadencia, fechas, asignaciones) vive en PM/SISTEMA.md + PM.md. El roadmap operativo del agente — qué orden de PRs, qué entidad activa cada uno — sí entra al manual como Capítulo 7, porque es decisión arquitectónica.


v1 — 2026-04-16 · primera versión. v2 — sin cambios. v3 — 2026-05-03 · vocabulario expandido con la decisión Customer/Paciente cerrada (capa por capa habla su idioma). Vertical lock formalizado (salud + bienestar). Vista panorámica actualizada con dashboard como segunda superficie y los detalles que el routing y las herramientas implementan en cada bloque. Cap 07 (Roadmap del agente) sumado a la estructura de siete capítulos. Próxima edición cuando un principio de diseño cambie o cuando el vocabulario canónico se amplíe.

02 Programa
02

Programa

¿Quiénes usan Perla y qué hacen adentro?


Antes de pensar en código, base de datos o flujos, necesitamos un mapa claro de las personas que interactúan con el sistema. Perla no es un producto con un solo tipo de usuario: tiene cuatro perfiles distintos, cada uno con sus propios objetivos, su propia manera de entrar al sistema y su propia conversación con él.

Este capítulo define esos cuatro perfiles, qué puede hacer cada uno, qué no puede hacer, y cómo es la experiencia desde su lado. Es el punto de partida de todo el resto: las funciones que el agente tenga que cumplir, las pantallas que necesitemos mostrar y las reglas del sistema se derivan directamente de acá.

Una nota de vocabulario antes de avanzar: decimos paciente como término genérico para la persona que recibe el servicio, sea en una clínica, una peluquería, un consultorio o un centro de estética. La elección está alineada con el vertical del producto — Perla apunta a salud y bienestar (decisión cerrada el 30/04). En el código del repo, esta entidad se llama customer por convención técnica neutral. El agente conversacional usa “paciente” o “cliente” según lo que el negocio configure en assistant_configs.business_instructions. Cada capa habla su idioma.

Una segunda nota, sobre el alcance del producto. Perla está enfocada en el vertical salud y bienestar: kinesiología, estética, odontología, terapias, peluquería, barbería. La decisión cerró el 30/04 después de validar con beta testers. La arquitectura técnica soporta cualquier vertical de turnos — el lock es de mensaje, no de capacidad técnica. Cuando un dueño de gimnasio o de cancha de pádel pregunte si Perla sirve para él, la respuesta honesta es “hoy estamos enfocados en salud y bienestar; tu vertical entra cuando crezcamos”.


Los cuatro perfiles humanos

Perla convive con cuatro perfiles distintos. Tres son personas reales (paciente, owner, equipo de El Nudo) y uno es una superficie técnica (dashboard). Cada perfil tiene su propia lista de cosas que puede y no puede hacer, su propia manera de entrar al sistema, y sus propios casos límite que el sistema tiene que resolver con criterio.


1. El paciente

Es la persona que llega buscando un turno o información sobre un servicio. No sabe — ni le importa — que detrás hay tecnología compartida con otros negocios. Su relación con Perla es a través de un solo canal: WhatsApp principalmente, eventualmente Telegram para pruebas o casos puntuales.

Qué puede hacer

  • Consultar qué servicios ofrece el negocio y detalles sobre los mismos
  • Pedir una recomendación describiendo el problema que necesita resolver (“tengo dolor lumbar hace una semana, ¿qué profesional me conviene?”) y recibir una sugerencia de servicio
  • Consultar la disponibilidad de horarios y profesionales
  • Agendar un turno nuevo
  • Consultar qué turnos tiene agendados
  • Reprogramar o cancelar un turno propio (dentro de la política del negocio)
  • Hacer una consulta que excede el catálogo y obtener una respuesta escalada al owner

Qué NO puede hacer

  • Ver agenda, datos o turnos de otros pacientes
  • Acceder a información financiera o reportes del negocio
  • Modificar servicios, precios o profesionales del negocio
  • Agendar turnos a nombre de otra persona
  • Ver datos sensibles (alergias, historia clínica, consentimientos) de otros pacientes

Cómo entra al sistema

  • Por un link o un código QR que el negocio publica en sus redes, su tarjeta o su local. El link contiene un código corto del negocio (vive como businesses.public_code) que el routing del Cap 3 detecta.
  • Por un mensaje espontáneo al número de Perla mencionando con qué negocio quiere comunicarse.
  • Por una conversación previa con algún tenant que el sistema retoma automáticamente (si no pasaron muchas horas).

Casos especiales

  • Escritura en frío. El paciente llega sin link y sin historial previo. El sistema le pregunta con qué negocio quiere comunicarse y confirma con la dirección física antes de avanzar.
  • Pregunta fuera de scope. El paciente pide algo que el catálogo no contempla. El agente reconoce que no puede responder solo, le avisa al paciente que lo consulta, escala la pregunta al owner por WhatsApp (creando un handoff_request con kind = 'escalated_question'), espera su respuesta, y vuelve al paciente con la respuesta.
  • Mismo paciente, varios negocios. Un paciente puede ser cliente de varios tenants simultáneamente. Cada conversación es independiente: no comparten memoria ni estado. El modelo de datos lo refleja con un customer por business y customer_channel_identities separadas.

2. El owner

Es el dueño o dueña del negocio. Necesita gestionar agenda, equipo, servicios, precios y comunicación con sus pacientes. Perla debe permitirle hacer todo eso desde WhatsApp, con la misma naturalidad con la que le hablaría a una secretaria humana — incluida la posibilidad de operar por audio en lugar de texto cuando esté funcional la transcripción.

Qué puede hacer

  • Todo lo que puede hacer un paciente (puede agendarse a sí mismo en su propio gabinete)
  • Consultar la agenda del día, la semana o cualquier rango definido
  • Cancelar turnos en batch (“cancelame todo lo de mañana a la mañana”) — con confirmación técnica vía pending_operations (Cap 6 Pliego 5)
  • Bloquear horarios de un profesional ante una ausencia o un imprevisto
  • Modificar o reprogramar el turno de cualquier paciente
  • Agregar, modificar o desactivar servicios y sus precios
  • Agregar o desactivar profesionales y sus disponibilidades horarias
  • Asignar permission sets a las membresías de su equipo (definir quién puede hacer qué)
  • Responder consultas escaladas que el agente le derivó
  • Consultar reportes simples del negocio (“¿cuántos turnos tuve esta semana?”)
  • Configurar la personalidad del agente (nombre, tono, instrucciones específicas)
  • Ver y confirmar pending operations creadas por el agente

Qué NO puede hacer

  • Ejecutar acciones destructivas masivas sin confirmación técnica (token + TTL)
  • Acceder a datos de otros tenants
  • Modificar configuración crítica del sistema (lógica del agente común a todos los tenants)
  • Operar como otro owner

Cómo entra al sistema

  • Por mensaje directo al número de Perla, sin necesidad de link ni código.
  • El sistema lo reconoce por su número de teléfono cruzándolo contra customer_channel_identities ligadas a users con memberships activas en su tenant.

Casos especiales

  • Owner como paciente de su propio negocio. La dueña de un salón quiere agendarse un masaje en su propio gabinete. El sistema distingue por contexto: si entró por un link de cliente, modo paciente; si escribió directo al número, modo owner. Si la situación es ambigua, pregunta antes de decidir.
  • Múltiples owners por tenant. Un negocio puede tener más de una persona con permisos de gestión. Cada una entra con su propio número y opera con los permisos efectivos de su membership (calculados desde sus permission sets + overrides directos — ver Cap 4 Familia 7).
  • Confirmación obligatoria de acciones masivas. El Pliego 5 del Cap 6 distingue entre confirmación conversacional (“sí, dale”) y confirmación técnica (preview con token TTL). El owner que pide cancelar 10 turnos pasa por preview obligatorio antes de que se ejecute.

3. El dashboard

No es una persona, es una superficie. Es la ventana web que el owner (o un miembro del equipo autorizado) puede usar cuando quiere ver, filtrar o configurar cosas con más detalle del que cabe en una conversación. Es opcional: todo lo que hace el dashboard también lo puede hacer el agente por WhatsApp. La idea no es sustituir al agente sino complementarlo para situaciones donde una pantalla ayuda más que un mensaje.

Para qué sirve

  • Visualizar la agenda con más detalle (vistas por día, semana, profesional)
  • Hacer cambios masivos cómodos (importar listas de servicios, configurar disponibilidades complejas)
  • Ver métricas y reportes con gráficos
  • Configurar reglas de negocio que requieren formularios (templates de mensajes, integraciones, parámetros avanzados)
  • Configurar la personalidad del agente con presets visuales
  • Asignar permisos al equipo
  • Ver el estado del agente, los runs ejecutados, y operaciones pendientes (sección 07 del Cap 6a)

Para qué NO sirve

  • No es la fuente de verdad. Las acciones se ejecutan sobre los mismos services backend que usa el agente. El principio de cierre (Cap 5) garantiza que lo que se cambia en uno se ve en el otro al instante.
  • No es obligatorio. Un owner puede operar Perla sin abrir nunca el dashboard.
  • No es para pacientes. El paciente nunca interactúa con el dashboard.

Quién entra

  • El owner del tenant.
  • Miembros del equipo autorizados, con permisos definidos por la combinación de permission sets de su membership (Cap 4 Familia 7). Los cuatro permission sets system pre-cargados (owner / admin / receptionist / professional) cubren los casos más comunes.

4. El equipo de El Nudo

Es la organización detrás de Perla. No usa el sistema desde adentro como si fuera un usuario más: lo opera, lo monitorea y lo mejora desde el backend. Su relación con Perla es la de quien construye y mantiene la infraestructura.

Qué puede hacer

  • Acceder a logs y métricas de uso del sistema completo
  • Intervenir manualmente cuando algo se rompe (resetear sesiones, corregir datos, escalar a un humano)
  • Configurar parámetros globales que no pertenecen a un tenant en particular
  • Cruzar datos anonimizados entre tenants para mejorar el agente y descubrir patrones de uso
  • Onboardear y desactivar tenants
  • Modificar el catálogo system de permisos y permission sets globales

Qué NO hace

  • No interactúa con el sistema vía WhatsApp como si fuera un owner o un paciente
  • No interviene en conversaciones individuales sin un protocolo claro
  • No comparte datos identificables de un tenant con otro

Cómo accede

  • Vía herramientas internas del backend (Supabase Studio, panel de operaciones, alertas automáticas)
  • Acceso privilegiado a toda la infraestructura del sistema
  • No tiene memberships en tenants — vive afuera del modelo multi-tenant

Sobre el “operador con permisos limitados”

Las versiones anteriores del capítulo mencionaban un quinto perfil informal: “el operador con permisos limitados”, alguien con menos privilegios que el owner pero con acceso al dashboard. v3 formaliza la decisión: no se suma como quinto perfil humano.

La razón es que el profesional que presta el servicio (la kinesióloga, el barbero, la esteticista) no gestiona el negocio — presta el servicio. La gestión la hace siempre alguien con memberships.role igual a owner o admin. El profesional sin facultades de gestión accede al dashboard con una membership de member con permission_set professional (5 permisos: ver dashboard, leer customers, leer appointments, leer y responder mensajes).

Lo mismo aplica al recepcionista o secretaria: es una membership con role = 'admin' o 'member' y permission_set receptionist (11 permisos cubriendo agenda, clientes, mensajes y handoffs).

El Cap 4 Familia 7 detalla el sistema de permisos. El Cap 6a Anexo (Matriz Acción ↔ Tool) lista qué permiso requiere cada acción.


Sobre el agente como entidad

A diferencia de las versiones anteriores del capítulo, v3 reconoce que el agente es una entidad persistida con identidad propia, no solo “el sistema actuando”. Cada business tiene su assistant_config con nombre, personalidad, instrucciones y toggles. Cada ejecución del agente queda registrada en agent_runs con tokens consumidos, intent detectado, modelo usado. Cada llamada a tool dentro del run queda en tool_invocations.

Esto tiene una implicancia para el Capítulo 2: cuando un cliente o un owner habla con “Perla”, está hablando con la assistant_config del business — el agente puede llamarse “Perla” en todos los tenants o tener nombres distintos por business. La identidad del agente es configurable, no fija.

Cuando un cliente pregunta “¿con quién estoy hablando?”, el agente responde con el nombre que el business configuró. Cuando el owner cambia el nombre, los próximos mensajes salen con el nombre nuevo sin redeploy.


Lo que este capítulo NO incluye

  • El detalle técnico de cómo se identifica a cada perfil (Cap 3 — Circulaciones).
  • El listado exhaustivo de cada mensaje posible que puede mandar un paciente (vive en 01_INFRA/SPEC_v0.md).
  • Las funciones específicas que el agente debe ejecutar (Cap 6 — Pliegos).
  • El catálogo completo de permisos atómicos y sets system (Cap 4 Familia 7 + Anexo 04a v3).
  • El orden temporal de implementación de cada capacidad (Cap 7 — Roadmap del agente).

Por qué este capítulo importa

Sin un mapa claro de roles, terminamos construyendo un agente que intenta servir a todos al mismo tiempo y termina sirviendo mal a todos. Cada decisión técnica — desde cómo guardamos las conversaciones hasta qué información pedimos primero — depende de saber a quién le estamos hablando. Este capítulo es el que nos permite decir con confianza “el agente debe responder de esta manera porque el que escribió es un paciente nuevo, no un owner verificado”.

A la fecha de v3, los cuatro perfiles humanos están sostenidos por entidades reales en el repo: paciente vive en customers + customer_channel_identities + customer_profiles, owner y operadores viven en users + memberships + permission sets, dashboard se materializa en /app/* del frontend Worker, equipo de El Nudo vive fuera del modelo multi-tenant con acceso super-admin. La taxonomía es consistente entre el manual y el código.


v1 — 2026-04-16 · cuatro perfiles definidos. v2 — sin cambios mayores. v3 — 2026-05-03 · vertical lock formalizado (salud + bienestar). Vocabulario customer/paciente cerrado. Decisión sobre operador limitado (no es 5to perfil, es membership con permission_set específico). Sumada sección “Sobre el agente como entidad”. Cada acción del owner referenciada al permiso requerido del Cap 6a. Próxima edición cuando aparezca un perfil nuevo o cuando cambie el alcance vertical.

03 Plantas y circulaciones
03

Plantas y circulaciones

¿Cómo entra cada persona al sistema y cómo sabemos quién es?


Cuando una persona escribe un mensaje a Perla, antes de que el agente pueda contestar nada útil, el sistema necesita resolver tres preguntas: a qué negocio le está hablando, con qué rol entra esa persona, y si ya hay una conversación previa que tenemos que retomar. A este conjunto de decisiones lo llamamos routing: el camino que sigue un mensaje desde que llega hasta que el agente lo responde.

Es importante entender desde el principio que el routing no lo decide el agente. Lo decide el código, antes de que el agente vea el mensaje. Esta separación es deliberada y es uno de los principios de diseño de Perla: las decisiones que pueden resolverse con lógica determinística (consultar una tabla, comparar un código, verificar un permiso) no se delegan al modelo de lenguaje. El agente recibe el mensaje ya con todo el contexto resuelto y se concentra en lo que sí requiere inteligencia: entender la intención de la persona y responderle bien.

A diferencia de las versiones anteriores del capítulo, v3 reconoce que el routing tiene una responsabilidad cero que pasa antes de las tres preguntas: validar que el mensaje viene del proveedor que dice venir y deduplicarlo si ya lo procesamos. Esa responsabilidad la cumple el adapter de canal del Cap 6 Pliego 3 y se materializa en las entidades channel_connections y channel_events del Cap 4.


El paso cero: autenticidad y dedup

Antes de que el routing pueda preguntar nada, el adapter de canal verifica que el mensaje sea genuino. WhatsApp firma cada webhook con HMAC-SHA256 sobre el body crudo (header X-Hub-Signature-256); Telegram firma con un secret token configurable (header X-Telegram-Bot-Api-Secret-Token). Si la firma no valida, el mensaje se descarta antes de tocar el sistema. Esa validación está implementada en src/backend/channels/<canal>/service.ts y usa los webhook_secret_ref que cada channel_connection guarda como referencia (no el secret crudo).

Después del check de firma, el sistema consulta channel_connections para encontrar la fila activa que matchea con el negocio destino y el canal correcto. Si la conexión está inactiva, no existe, o tiene inbound_enabled = false, el sistema responde 200 al proveedor (para que no reintente eternamente) pero no procesa el mensaje.

A continuación viene el dedup: el adapter inserta una fila en channel_events con provider_event_id único por business + provider. Si ese evento ya existía (porque el proveedor reintentó), el INSERT es un no-op silencioso (vía ON CONFLICT DO NOTHING con unique parcial). Esto es crítico: tanto Meta como Telegram envían eventos at-least-once con reintentos exponenciales hasta siete días si no reciben 200 OK rápido. Sin dedup explícito, el mismo mensaje del cliente aparecería N veces en el sistema.

Solo después de pasar autenticidad y dedup, el mensaje entra al routing propiamente dicho y empiezan las tres preguntas.


Las tres preguntas del routing

El routing resuelve, en orden, tres preguntas independientes pero conectadas:

  1. ¿A qué negocio le está hablando la persona? — el tenant
  2. ¿Con qué rol entra? — paciente, owner, o miembro del equipo con permisos específicos
  3. ¿Hay una conversación en curso que retomar? — el hilo

Las tres tienen que tener respuesta antes de que el mensaje llegue al agente. Si alguna falla, el sistema no avanza al siguiente paso. Se resuelven en este orden porque cada una depende de la anterior: no se puede saber el rol de una persona sin saber a qué tenant le habla, ni se puede retomar una conversación sin saber a qué tenant pertenece.

A continuación, cada pregunta por separado, con sus caminos felices y sus casos límite.


1. ¿A qué negocio le está hablando la persona?

Perla puede tener un solo número de WhatsApp para todos los tenants (configuración MVP) o un número por tenant (configuración premium futura). En cualquiera de los dos casos, el sistema necesita resolver el business_id antes de hacer nada útil. Hay tres caminos posibles, en orden de prioridad:

Camino 1 — Llegó por un link o un QR. El negocio publica un link en sus redes, su tarjeta o su local. Ese link contiene el public_code del business (cierre del gap SG-1: campo en businesses con check format ^[A-Z0-9][A-Z0-9_-]{0,23}$ y unique parcial sobre lower(public_code)). Cuando el sistema recibe el mensaje, detecta el código en el texto y resuelve el tenant de forma inmediata. Es el camino preferido porque elimina toda ambigüedad.

Camino 2 — Tiene historial previo. La persona ya conversó antes con algún tenant. El sistema tiene registrado ese hilo en conversation_threads (con la combinación business_id + customer_id + channel + external_thread_id) y, por defecto, asume que la nueva conversación es continuación de la anterior. Pero asumir es peligroso: una persona puede haber conversado primero con una peluquería, después con una odontóloga, y volver a la peluquería al día siguiente. Antes de avanzar, el sistema confirma: “¿Seguimos con Suri Ecotienda?”

Camino 3 — Escribió en frío. La persona llegó sin link y sin historial. El sistema le pregunta directamente con qué negocio quiere comunicarse. A partir de ahí, la respuesta puede tomar tres formas:

  • Match único — el nombre que la persona escribió coincide con un solo tenant registrado. El sistema confirma con la dirección física (“¿Suri Ecotienda, Av. San Martín 887?”) y avanza.
  • Match ambiguo — el nombre coincide con varios tenants (dos peluquerías llamadas “Style”, una en cada barrio). El sistema lista los candidatos con sus direcciones y le pide a la persona que elija.
  • Sin match — el sistema no encontró ningún tenant que coincida con lo escrito. Le pide a la persona el link directo del negocio (public_code) o algún dato adicional para desambiguar.

La dirección física del local (campo en locations) es un verificador clave en todo el camino 3. Sin ella, dos negocios con nombres parecidos pueden generar confusiones irrecuperables.


2. ¿Con qué rol entra esta persona?

Una vez resuelto el tenant, el sistema verifica si el número de teléfono que escribió pertenece a algún users que tenga memberships activa en ese business. Hay tres situaciones posibles:

El número no aparece como customer_channel_identities ligada a users. Es un paciente. Se procesa como tal y el mensaje avanza al agente con permisos de cliente: solo puede leer y modificar sus propios datos.

El número aparece como users con membership en el tenant. Acá el rol no se puede decidir solo mirando el teléfono: depende del contexto de la conversación.

  • Si la persona vino por un link de cliente, entra en modo paciente. La dueña que se quiere agendar un masaje en su propio salón probablemente pasó por el link público que compartió con sus clientas; el sistema asume que está actuando como clienta.
  • Si escribió directo al número de Perla sin pasar por un link, entra en modo owner / miembro del equipo según su memberships.role y los permission_sets asignados. Está operando su negocio, no consumiéndolo.
  • Si hay ambigüedad (por ejemplo: tiene historial en ambos modos), el sistema pregunta antes de decidir: “¿Querés gestionar tu negocio o sacar un turno como clienta?”

El miembro del equipo tiene permisos específicos. A diferencia de las versiones anteriores del capítulo, v3 maneja permisos granulares vía la Familia 7 del Cap 4. El sistema carga los permission codes efectivos de la membership antes de pasarle el control al agente: la unión de los permisos heredados de los permission_sets asignados, menos los deny directos, más los allow directos. El agente recibe ese conjunto de codes como parte del contexto de sesión y respeta esos límites en cada tool invocation. Si el modelo intenta llamar a una tool que requiere un permiso que la membership no tiene, la tool falla con permission_denied antes de ejecutar.


3. ¿Hay una conversación en curso que retomar?

Las conversaciones por WhatsApp no son atómicas. Una persona puede mandar un mensaje, esperar una hora, y volver con otro. El sistema mantiene un hilo de conversación asociado a la combinación business_id + customer_id + channel + external_thread_id (vía la tabla conversation_threads con su unique parcial). Un mismo paciente que conversa con dos tenants distintos tiene dos hilos paralelos totalmente independientes, con memoria separada.

Lo que se preserva en el hilo:

  • La memoria de los mensajes anteriores (vive en messages, ordenados por conversation_thread_id + created_at)
  • El estado intermedio del flujo en curso (vive en conversation_state con current_intent, current_step, flow_status, slots seleccionados)
  • Las preferencias declaradas durante la conversación (vive en customers.tags + customers.preferred_channel + customers.preferred_staff_member_id)
  • El assistant_config_id que se está usando (por si el business cambió la personalidad mid-conversación)

Lo que NO se preserva indefinidamente:

  • Después de cierto tiempo de inactividad, el hilo se considera cerrado (status = 'archived'). Si la persona vuelve, el sistema ofrece retomar lo último o empezar de cero. El umbral exacto de tiempo es una decisión técnica todavía abierta — probablemente entre 24 y 48 horas, parametrizable por business_settings.
  • Si el sistema detecta que la conversación quedó en un estado roto (un error técnico, datos inconsistentes, una acción incompleta), puede resetear conversation_state y pedir empezar de nuevo, avisando al usuario con transparencia.

Garantías que el routing le ofrece al agente

Cuando el agente recibe un mensaje, el routing ya hizo todo el trabajo invisible para que pueda concentrarse en su tarea. Esas garantías son explícitas y forman parte del contrato interno del sistema:

Mensaje autenticado y deduplicado. La firma del proveedor está validada, el channel_event está registrado de forma idempotente, el mensaje está persistido en messages con su external_message_id único por thread.

Lock por sesión vía inbound_message_batches. Nuevo en v3, materializa lo que las versiones anteriores describían como concepto. Mientras un mensaje se está procesando para una sesión determinada (combinación business + customer + canal), si llega otro mensaje de la misma persona, el sistema lo agrupa en el mismo batch en lugar de disparar un segundo agente en paralelo. La tabla inbound_message_batches tiene una session_key que identifica la sesión y un status que pasa por openprocessingprocessed. Cuando una batch está en processing, los mensajes nuevos de esa sesión se agregan al batch existente. Cuando la batch cierra, los mensajes acumulados se concatenan en combined_text y se pasan como un solo input al agente. Esto evita dos respuestas incoherentes al mismo tiempo y conflictos sobre el estado de la conversación.

Mensajes apilados se agrupan antes del LLM. La gente escribe rápido y a menudo manda varios mensajes seguidos antes de que el agente alcance a contestar al primero (“hola… quiero turno… para mañana”). El batch los agrupa como una sola entrada antes de pasárselos al agente, así el agente ve la intención completa y no responde tres veces al mismo pedido. La ventana de agrupación es de unos 30-60 segundos — el batch cierra cuando pasan ese tiempo sin nuevos mensajes, o cuando el cliente envía un mensaje claramente conclusivo, o cuando un timeout de seguridad lo fuerza.

Audio y texto se procesan iguales. Si el mensaje viene como audio, antes de pasarlo al agente se transcribe a texto vía la entidad audio_transcripts (con audio_transcript_segments cuando hay múltiples speakers). El agente nunca recibe audio crudo: solo recibe el texto resultante. Esa traducción es parte del routing, no del agente.

Contexto validado. El sistema garantiza que cuando el agente recibe el mensaje, los datos de tenant, rol, permisos efectivos, assistant_config y conversation_state están cargados, son válidos y no se contradicen entre sí. Si algo no cierra (un tenant inválido, un permiso ambiguo, una sesión corrupta), el routing rechaza la operación con un mensaje claro al usuario en lugar de pasarle el problema al agente.

Degradación elegante ante fallas. Si el sistema no puede resolver alguna de las tres preguntas (la base de datos no responde, un servicio externo se cae), el usuario recibe un mensaje honesto y comprensible (“disculpá, tuve un problema técnico, ¿podés volver a intentar en un minuto?”) en lugar de un error técnico opaco o un silencio incómodo. Cualquier fallo queda registrado en audit_events con actor_type = 'system' para que el equipo lo pueda diagnosticar.


El flujo completo en una imagen

Mensaje entrante (WhatsApp Cloud API o Telegram Bot API)
   ↓
Adapter de canal verifica firma (HMAC) → si falla, descarta
   ↓
Adapter consulta channel_connections con (business_id, channel, status='active')
   ↓
Adapter inserta channel_events (dedup por provider_event_id)
   ↓ si era duplicado, retorna OK silencioso
Adapter persiste el mensaje en messages
   ↓
Adapter persiste el cliente en customers + customer_channel_identities (si es nuevo)
   ↓
Routing pregunta 1: ¿qué tenant?
   ↓ link/historial/frío → resuelve business_id
Routing pregunta 2: ¿qué rol?
   ↓ verifica memberships + carga permission codes efectivos
Routing pregunta 3: ¿hilo abierto?
   ↓ resuelve conversation_thread + carga conversation_state
Sistema agrupa con mensajes recientes en inbound_message_batches
   ↓ cuando el batch cierra:
Disparo del runtime del agente (PR #12/#13 del Cap 7)
   ↓
Runtime lee assistant_config + history + permisos efectivos
   ↓ compone system prompt + invoca LLM
LLM decide tools, runtime las ejecuta (validando permisos), persiste tool_invocations
   ↓ repite hasta respuesta final
Runtime persiste el mensaje outbound en messages (status='queued')
   ↓
Adapter manda la respuesta vía proveedor (sendXxxTextMessage)
   ↓ status pasa a 'sent', después 'delivered', después 'read' según webhooks de status
Audit_events registra el run completo

Lo que este capítulo NO incluye

  • El detalle técnico de cómo se almacenan los hilos o los locks (Cap 4 + Cap 04a v3).
  • Las herramientas que el agente usa una vez que el mensaje le llega (Cap 6).
  • Los flujos conversacionales que ocurren después del routing (vive en 01_INFRA/SPEC_v0.md).
  • La implementación de Whisper / STT que transcribe los audios (post-PR #15 del Cap 7).
  • El orden temporal de implementación de las piezas de routing (varias ya implementadas en src/backend/channels/, ver Cap 7).

Por qué este capítulo importa

Si el routing falla, el agente no tiene chance de hacer las cosas bien. Le contesta a la persona equivocada con datos del tenant equivocado, o le da permisos de owner a un paciente, o pierde el hilo de la conversación a mitad de un agendamiento, o procesa el mismo mensaje cinco veces porque el proveedor reintentó. Este capítulo define las garantías mínimas que el sistema debe ofrecerle al agente para que pueda concentrarse en lo que sabe hacer.

A la fecha de v3, todas las piezas del routing están implementadas en el repo: los adapters de canal con HMAC y dedup viven en src/backend/channels/, las entidades channel_connections y channel_events y inbound_message_batches y conversation_threads y conversation_state están en el schema con sus reglas, el sistema de permisos resuelve el rol con granularidad. Lo único que falta para cerrar el loop completo es que el adapter, después de persistir el mensaje y armar el batch, dispare al runtime del agente. Eso es exactamente lo que hacen los PRs #12 y #13 del Cap 7.


v1 — 2026-04-16 · tres preguntas del routing más garantías al agente. v2 — sin cambios. v3 — 2026-05-03 · sumadas las entidades reales que materializan cada concepto: channel_connections y channel_events para el paso cero (autenticidad + dedup), inbound_message_batches para el lock por sesión + agrupación de mensajes apilados, audio_transcripts para la traducción audio→texto, conversation_threads y conversation_state para los hilos. Sumado el sistema de permisos granulares al routing del rol. SG-1 cerrado (businesses.public_code). Próxima edición cuando se cierre el umbral de inactividad del hilo o cuando aparezca un canal nuevo.

04 Instalaciones
04

Instalaciones

¿Qué cosas existen en Perla y cómo se relacionan entre sí?


Si Perla fuera un edificio, este capítulo describe las instalaciones: el sistema circulatorio invisible que hace que todo funcione. Negocios, sucursales, profesionales, servicios, turnos, hilos de conversación, configuración del agente, suscripciones, permisos. Las cosas que existen en el producto, qué información guardamos sobre cada una, y cómo se conectan entre sí.

Este es el lenguaje común del proyecto. Cuando alguien dice “agregamos el campo de seña al appointment”, todos tienen que entender qué entidad es esa, qué información guarda, y qué consecuencias tiene tocarla. Sin un mapa explícito y compartido, cada quien construye sobre supuestos distintos y el sistema termina contradiciéndose a sí mismo.

Una nota de vocabulario. En este capítulo decimos entidad para referirnos a cada “cosa” que el sistema reconoce y guarda. Cada entidad se traduce a una o varias tablas en la base de datos. La sintaxis exacta, los tipos de datos y las migraciones viven en el código del repo tomillo/perla y se mapean al lenguaje conceptual de este capítulo en el Anexo 04a_INVENTARIO_SCHEMA_v3.md.

Una segunda nota, sobre cliente vs paciente. El manual usa paciente como término genérico para la persona que recibe el servicio (decisión de vertical lock 30/04: salud + bienestar). El código del repo usa customer como abstracción técnica neutral. El agente conversacional usa “paciente” o “cliente” según lo que el negocio configure en assistant_configs.business_instructions. Cada capa habla su idioma — el manual no se altera por la convención del schema y el schema no cambia para acompañar la voz del manual. El Anexo 04a v3 hace de puente.

Una tercera nota, sobre el alcance de las familias. En las versiones anteriores del capítulo agrupábamos las entidades en cuatro familias. v3 reconoce siete — sumamos Agente y operaciones, Billing y pagos, y Permisos. La lista creció porque el repo creció: cuarenta y siete tablas más al cierre del capítulo respecto al inventario del 24/04.


Las entidades del producto

Perla agrupa sus entidades en siete familias según la función que cumplen. Las primeras cuatro describen lo que existe en el mundo real (negocios, personas, interacciones, conversación). Las tres siguientes describen el sistema que sostiene al agente y al negocio del lado de Perla (operaciones del agente, pagos, permisos del equipo).


Familia 1 — El negocio y sus partes

Todo lo que pertenece a un tenant. Cada elemento de esta familia siempre está vinculado a un tenant específico y nunca cruza la frontera hacia otro. Se organizan en una cadena anidada: un negocio tiene sucursales, cada sucursal tiene profesionales y reglas de horario, y los servicios se ofrecen en una o varias sucursales.

  • Negocio (business). La unidad comercial completa. Tiene un nombre legal, un nombre visible, un slug técnico, un código público corto (ver más abajo) y una configuración asociada. Cierra el gap del routing por código que la versión 1 marcaba como pendiente: el campo public_code con check format y unique parcial garantiza que un cliente que llega con “Hola, vengo por SURI” o por un QR encuentra el tenant correcto sin ambigüedad.

  • Configuración del negocio (business settings). Zona horaria, idioma, moneda operativa, formato de fecha y hora, sucursal por defecto, toggles de booking público y self-service del cliente final. Es 1:1 con el negocio — no convive con varias.

  • Sucursal (location). Un local físico donde se prestan los servicios. Un negocio puede tener una sola sucursal o varias. Cada sucursal tiene su propia dirección, su propia zona horaria (puede diferir del negocio), y define los horarios de atención generales del local.

  • Profesional (staff member). Una persona que trabaja en una sucursal prestando servicios. Tiene un nombre, un rol interno, opcionalmente un vínculo a un usuario del dashboard, un color para la agenda, un toggle de “agendable”, y un estado (activo / inactivo / con licencia).

  • Especialidad y relación profesional ↔ especialidad. Las especialidades son categorías globales del tenant (“Estética”, “Masajes terapéuticos”) y los profesionales se asignan a una o varias con un nivel de proficiencia.

  • Categoría de servicio y servicio. Sólo conceptos del catálogo. Un servicio tiene nombre, descripción, instrucciones de preparación y de aftercare, y no se agenda directamente.

  • Variante de servicio (service variant). La forma concreta en que un servicio se agenda. Un mismo servicio puede tener varias variantes: “masaje 30 minutos · $X”, “masaje 60 minutos · $Y”, “masaje 90 minutos · $Z”. Cada variante tiene su propia duración, precio, buffer antes y después, capacidad y política. Separar servicio de variante permite que el catálogo sea descriptivo y la reserva sea precisa.

  • Relación profesional ↔ variante. No todos los profesionales hacen todas las variantes. Esta relación dice qué profesional puede ofrecer qué variante, en qué sucursal.

  • Recurso físico (resource). Un bien reservable que no es una persona — una silla de peluquería, una camilla de masaje, un consultorio. Tiene tipo, modo de booking (exclusivo o compartido) y estado.

  • Relación variante ↔ recurso. Qué variante necesita qué recurso, con cantidad y tipo de requerimiento (requerido / opcional / alternativo).

  • Regla de disponibilidad (availability rule). Define cuándo un staff, recurso o sucursal está disponible: “Belén trabaja en Centro los jueves de 13:00 a 18:00”. Cada regla apunta a exactamente UNO de los tres tipos de target (staff / resource / location), garantizado por un check brutal en la base.

  • Excepción de disponibilidad (availability exception). Excepción puntual a la disponibilidad normal: feriado, ausencia, cierre.

  • Política (policy set) y override de política. Reglas configurables por tenant en ocho dimensiones (booking, cancellation, reschedule, lateness, no_show, deposit, notification, general), con overrides por entidad específica (hasta granularidad de cliente individual).


Familia 2 — Las personas que entran al sistema

Las personas que interactúan con Perla llegan por dos puertas muy distintas: la conversacional (canales — los pacientes/clientes) y la del dashboard (navegador web — owners, miembros del equipo). El sistema las modela como entidades separadas porque llegan por canales distintos y se autentican de maneras distintas, aunque físicamente puedan ser la misma persona.

  • Usuario de plataforma (user). La identidad que accede al dashboard: un owner, un operador autorizado, un miembro del equipo interno de El Nudo. Se autentica con email + password vía Supabase Auth. Tiene un displayName y opcionalmente avatar y phone.

  • Membresía (membership). Asocia a un usuario con un negocio, especificando un rol base: owner, admin, member. Un mismo usuario puede tener membresías en varios negocios con distintos roles. La membresía es lo que permite que un dueño maneje dos peluquerías desde el mismo dashboard.

  • Identidad por canal del cliente (customer channel identity). Es la puerta externa por la cual entra un cliente: su número de teléfono en WhatsApp, su usuario en Telegram. Cada canal produce una identidad distinta. Si la misma persona escribe un día por WhatsApp y otro por Telegram, el sistema ve dos identidades independientes salvo que algo las conecte explícitamente.

  • Cliente (customer). La identidad abstracta del paciente desde la perspectiva del negocio. Tiene nombre completo, teléfono e164, email, canal preferido, preferencia de profesional, preferencia de variante, toggles de marketing y recordatorios, y un estado (activo / inactivo / archivado). Una persona física puede ser cliente en varios negocios distintos — cada uno con su propio registro.

  • Perfil del paciente en el negocio (customer profile). Datos sensibles del cliente que existen en el contexto de un negocio específico: fecha de nacimiento, género, alergias, tipo de piel, tipo de pelo, fórmula de color habitual, consentimientos firmados, contacto de emergencia. La separación entre cliente y perfil permite que los datos sensibles vivan separados de los datos operativos, con políticas de RLS más estrictas.

Sobre la identidad cruzada (cliente y operador a la vez). Una misma persona física puede ser owner de un negocio y paciente de otro. En el modelo de datos esto no se modela como un único registro unificado: conviven su identidad por canal (cuando habla por WhatsApp) y su usuario de plataforma (cuando entra al dashboard). El routing del Capítulo 3 las vincula en tiempo de ejecución, no en el modelo de datos.

Sobre el operador con permisos limitados. El Capítulo 02 define cuatro perfiles humanos (paciente, owner, dashboard, equipo El Nudo) y menciona “operador con permisos limitados” como caso del owner extendido. El sistema no lo modela como un quinto perfil — lo modela como una membership con role igual a admin o member y uno o más permission_sets asignados de la Familia 7. La granularidad efectiva la da el sistema de permisos, no una taxonomía rígida de roles.


Familia 3 — Las interacciones operativas

Lo que pasa entre las personas y los negocios. Esta familia es la que más cambia con el uso: cada día entran, se modifican y se cierran cientos de registros.

  • Turno (appointment). Un compromiso entre un cliente, un profesional (o un recurso físico), una sucursal, una fecha y una hora, dentro de un negocio. Tiene un estado (pending / confirmed / in_progress / completed / cancelled / no_show), un origen (manual / assistant / web / phone / import) y un código de confirmación único por negocio. Es el corazón operativo del producto.

  • Ítems del turno (appointment items). Un turno no es siempre un único servicio. Una visita al salón puede ser “cejas + tinte + hidratación”. Cada ítem es una variante de servicio con su propio profesional, duración y precio. El turno agrupa esos ítems en una sola ventana de tiempo. Un turno tiene al menos un ítem y puede tener varios.

  • Reserva temporal (booking hold). Cuando una persona empieza a agendar pero todavía no confirmó, el sistema toma el horario por unos minutos para que nadie más lo agarre. Si la confirmación nunca llega (silencio, error, abandono), la reserva expira y el horario vuelve a estar disponible. Es lo que evita que dos personas terminen agendando el mismo slot en simultáneo.

  • Bloque de agenda (schedule block) — entidad nueva en v3. Centraliza la ocupación real de la agenda en una sola tabla. Cada bloque ocupa exactamente un target (un staff, un resource, o una location — no más), determinado por un check estructural de la base. Hay cuatro tipos de bloque: derivado de un appointment, derivado de un hold, manual (creado por el owner para indicar ausencia o feriado), o de cierre del local. Esta entidad reemplaza la verificación de overlaps caso por caso — el motor de disponibilidad consulta schedule_blocks y obtiene la respuesta atómica de qué slots están libres. Es la pieza que hace que la agenda sea consistente cuando dos profesionales comparten un recurso o cuando un mismo recurso bloquea varios servicios.

  • Adjunto (attachment). Un archivo que entra o sale del sistema vinculado a un mensaje, un turno, un handoff, un cliente. Tiene un tipo (imagen / audio / video / documento / transcripción / otro), vive en un bucket de Supabase Storage, y tiene un check de path que obliga a respetar el prefix del business — los archivos de un negocio no pueden vivir en el bucket de otro.

  • Transcripción de audio (audio transcript) y segmentos de transcripción (audio transcript segments). Cuando llega un audio, el adapter de canal lo persiste como attachment, lo marca para transcripción, y un proceso separado lo procesa con Whisper o equivalente. La transcripción guarda el texto completo, idioma detectado, confidence, y opcionalmente segmentos individuales con speaker label cuando hay múltiples voces. Para audios del owner que pueden ser largos (instrucciones por voz), los segmentos permiten al agente leer porciones específicas.

  • Regla de notificación (notification rule). Define cuándo y cómo se manda una notificación: “recordar 24 horas antes del turno por WhatsApp”. Tiene un evento que dispara (turno creado, confirmado, cancelado, recordatorio, handoff solicitado, mensaje recibido, custom), un canal (WhatsApp / email / SMS / push / webhook / interno), un template con sus variables, un offset en minutos.

  • Notificación programada (scheduled notification) — entidad nueva en v3. La cola de notificaciones futuras esperando ser despachadas. Cuando se confirma un turno, el sistema crea las filas correspondientes en esta tabla con scheduled_for calculado desde el offset de la regla. Captura snapshot de variables del template al momento de crear la notificación, no al despacharla — eso permite que si el cliente cambia el nombre, el recordatorio salga con el nombre que estaba al momento de agendar. Tiene un idempotency_key único que evita duplicados por reintento.

  • Log de notificaciones (notification log). Registro histórico de cada intento de envío. Guarda qué se mandó, a quién, por qué canal, con qué resultado, qué error si falló. Permite reintentar fallidos y dejar trazabilidad para soporte.

  • Consulta escalada — resuelta por composición sobre handoff_requests con kind = 'escalated_question'. Cierra el gap SG-4 que la versión 2 del capítulo dejaba abierto.


Familia 4 — La conversación

El sistema necesita recordar lo que ya se dijo para que el agente no pregunte cinco veces lo mismo, para retomar conversaciones interrumpidas, y para tener una base estable sobre la cual confirmar acciones críticas.

  • Conexión de canal (channel connection). La configuración del canal de mensajería para un negocio. Tiene un proveedor (meta_cloud para WhatsApp, telegram para Telegram), un identificador externo (phone number ID de Meta, bot username de Telegram), referencias a secretos guardados en wrangler (credential_ref, webhook_secret_ref), una URL de webhook propia, toggles de inbound / outbound / read receipts / mensajes de voz / mensajes multimedia, ventana de mensajería (24h en WhatsApp), y un rate limit por minuto. Es la entidad que cierra el gap SG-3 de la versión 1 — los secretos por tenant viven con referencias controladas, no con tokens crudos.

  • Evento de canal (channel event). Cada webhook que llega del proveedor genera una fila acá: el ID externo del evento, el tipo, la dirección, su raw payload reference. Una unique parcial sobre (business_id, provider, provider_event_id) resuelve el dedup ante reintentos del proveedor (Meta y Telegram envían eventos at-least-once).

  • Hilo de conversación (conversation thread). Un registro abierto entre una identidad de cliente y un negocio a través de un canal específico. Tiene un estado (open / pending_handoff / in_handoff / resolved / archived), un sujeto opcional, las marcas de tiempo del último mensaje del cliente y del último del agente, una referencia al assistant_config que está usando y al channel_connection por donde llega. Un cliente que habla con dos negocios tiene dos hilos distintos, uno por cada uno, con memoria separada.

  • Estado de conversación (conversation state). Separado del hilo, guarda el intent actual del agente y los slots ya recolectados: “el paciente está en intent de agendar turno, ya eligió servicio y variante, falta profesional y horario”. El estado tiene también proposed_start_at y proposed_end_at cuando hay un horario en juego, selected_appointment_id cuando se está modificando un turno, y FK a pending_operations cuando hay una acción esperando confirmación. Es el guardaespaldas contra alucinaciones del agente: antes de invocar una herramienta destructiva, el sistema verifica contra este estado que todos los datos requeridos están completos.

  • Mensaje (message). El histórico crudo de todos los mensajes intercambiados dentro de un hilo. Tiene un tipo de remitente (customer / assistant / staff / system), una dirección (inbound / outbound / internal), un estado de delivery (queued / received / sent / delivered / read / failed), un tipo de contenido (text / audio / image / document / video / system_event / other), opcionalmente intent detectado, idioma detectado, confidence. Las marcas de tiempo de envío, entrega y lectura tienen check de orden.

  • Solicitud de handoff (handoff request). Solicitud de intervención humana cuando el agente no debe o no puede continuar de forma segura. Tiene dos tipos: human_takeover (el agente cede la conversación al humano) y escalated_question (el agente sigue conduciendo, pero deriva una pregunta puntual al owner y vuelve con la respuesta). Tiene estado, prioridad, profesional asignado, SLA, notas de resolución. Resuelve dos flujos del Cap 6: el handoff completo del Pliego 4 Caso B y la consulta escalada que la v1 marcaba como propuesta sin entidad.


Familia 5 — Agente y operaciones (familia nueva en v3)

La capa del agente que las versiones anteriores del capítulo mencionaban sin entidad concreta. v3 la consolida como familia propia porque sus tablas tienen reglas y ciclo de vida distintos a la conversación misma.

  • Configuración del asistente (assistant config). La personalidad del agente para un business. Tiene nombre, system prompt, mensaje de saludo, tono de respuesta, estilo, instrucciones del negocio, instrucciones de seguridad, proveedor de modelo (openai / anthropic / custom), nombre del modelo, toggles globales (auto-reply, handoff, activo), toggles por tool (booking, reschedule, cancellation, payment_link), límites de runtime (máximo de tool calls por run, máximo de segundos por run). Opcionalmente vinculada a una sucursal específica si la personalidad varía por sede. La capa de configuración por business que el Cap 6 Pliego 2 asume vive acá.

  • Batch de mensajes entrantes (inbound message batch). Cuando un cliente manda tres mensajes seguidos (“hola”… “quiero turno”… “para mañana”), el sistema los agrupa antes de invocar al agente. Reduce costo de tokens y evita respuestas pisadas. Tiene una session_key que combina business + cliente + canal, un texto combinado, un contador de mensajes, referencias al primer y último mensaje de la batch, y un estado (open / processing / processed / failed). Materializa el patrón de “lock por sesión” descripto en el Cap 3 como garantía del routing al agente.

  • Run del agente (agent run). Cada ejecución del agente. Captura el modelo usado, el texto de entrada (combinado de la batch), el texto de respuesta final, el intent detectado, los contadores de tokens (prompt, completion, total), errores si ocurrieron, marcas de tiempo de inicio y fin. Permite responder “¿cuánto está costando el agente este mes?” sumando tokens. Permite también auditoría — cada turno creado por el agente queda vinculado al run que lo generó vía tool_invocations.

  • Invocación de tool (tool invocation). Cada llamada a una tool dentro de un run. Captura el nombre de la tool, su nivel de riesgo, el estado (pending / succeeded / failed / requires_confirmation), un resumen de input, un resumen de output, el tipo y el ID de la entidad afectada, y opcionalmente el ID de la pending_operation que requiere para ejecutar (en caso de status requires_confirmation). Una constraint de la base garantiza que requires_confirmation ⇔ existe pending_operation_id.

  • Operación pendiente (pending operation). Cuando el agente quiere ejecutar una acción L3 o L4 (Cap 6 Pliego 5), crea una pending operation con: tipo de operación, nivel de riesgo, hash del input, título y resumen del preview, contador de entidades afectadas, hash del token de confirmación, fecha de expiración, estado. Una unique parcial impide que se cree el mismo preview dos veces en paralelo. La existencia de esta entidad cierra el frente abierto que el Cap 6 v2 marcaba como “el ajuste más inmediato cuando se cierren las tools L3/L4”.

  • Item de operación pendiente (pending operation item). Una fila por cada entidad afectada en una pending operation. Captura el tipo de entidad, el ID, la acción a aplicar, valores antes y después, resumen para mostrar al usuario, y estado individual (pending / executed / skipped / failed). Permite mostrar al owner “voy a cancelar estos 4 turnos” con detalle, y registrar resultado por turno (a veces uno falla y los otros tres pasan).

  • Evento de auditoría (audit event) y cambios de auditoría (audit event changes). Registro auditable de cada acción importante del sistema, usuario, agente o integración. Captura tipo de actor (user / customer / assistant / system), tipo de evento, tipo de entidad y ID, request ID, IP, user agent, summary. Los cambios granulares (campo a campo) viven en una tabla relacionada que captura valores antes y después con tipo. Permite responder “¿quién cambió el precio del corte el mes pasado?” sin recurrir a logs de aplicación.


Familia 6 — Billing y pagos (familia nueva en v3)

El billing tiene dos sentidos en Perla: el pago del negocio a Perla por su suscripción al plan, y el pago del cliente final al negocio (señas de turno, cobros). Las dos coexisten en la misma familia porque comparten infraestructura de proveedores externos (Mercado Pago, Stripe).

  • Plan de billing (billing plan). Catálogo global de planes que Perla ofrece. Tiene un código kebab-case con check format, un nombre, una descripción, un toggle de activo y un orden de despliegue. Sin business_id — es global.

  • Precio de plan por mercado (billing plan price). Precio comercial por plan, mercado (AR, LATAM, GLOBAL), moneda, intervalo de facturación. Tiene versionado — al cambiar un precio se inserta una fila nueva con version incrementado, y la versión anterior queda como histórico. Una unique parcial sobre el activo garantiza un solo precio activo por combinación.

  • Precio externo en proveedor (billing provider price). El mapeo entre el precio comercial de Perla y el price ID o product ID en el proveedor externo. Captura el provider (Stripe, Mercado Pago), el environment (testing / production), los IDs externos del producto y precio. Permite que el mismo plan se ofrezca en distintos mercados con distintos proveedores sin tocar la lógica de negocio interna.

  • Perfil de billing del negocio (business billing profile). 1:1 con el negocio. Captura el mercado del negocio, el país de billing, la moneda, el proveedor preferido, email de billing, tax ID, nombre fiscal. Existe separado de business_settings porque la información fiscal es sensible y tiene reglas distintas.

  • Suscripción (subscription). La suscripción vigente de un negocio a un plan. Tiene un proveedor, IDs externos, plan code, precio plan ID, precio provider ID, estado (trialing / active / past_due / cancelled / inactive), intervalo, contador de seats, períodos actuales, fin del trial, toggle de cancelación al final del período. Captura también IDs externos de cliente y price para poder reconciliar con los webhooks del proveedor.

  • Payment Intent (payment intent). Un intento de pago. Es polimórfico: vincula opcionalmente a una suscripción (cobro de plan), un appointment (seña), un customer (refund), o ninguno (ajuste). Tiene un proveedor (default mercado_pago), un kind (subscription / deposit / full / refund / adjustment), un estado (requires_action / requires_capture / processing / succeeded / cancelled / failed), un método de captura (manual / automático), monto, moneda, fecha de expiración, IDs externos del cliente, charge, refund, checkout URL. Captura webhooks del proveedor para reconciliar status.


Familia 7 — Permisos (familia nueva en v3)

El sistema de permisos granulares que cierra el gap SG-6 de la versión 1 del capítulo. La idea: el rol grueso (memberships.role con valores owner / admin / member) define la capacidad de gestión, y los permission_sets refinan qué exactamente puede hacer esa membership.

  • Permiso atómico (permission). Catálogo de permisos del sistema. Cada uno tiene un código en formato module.action (appointments.create, messages.reply, permissions.manage), un nombre legible, una descripción. Hay diecinueve permisos pre-cargados como is_system = true cubriendo dashboard, businesses, locations, staff, services, customers, appointments, messages, handoffs, billing, settings y permissions. La tabla es global, sin business_id — los permisos son los mismos para todos los tenants.

  • Permission set (permission set). Plantilla reusable de permisos. Puede ser global (business_id = NULL, pre-creada por el sistema) o por business (business_id con FK). Hay cuatro sets system pre-cargados: owner (los 19 permisos), admin (17 permisos sin permissions.manage ni billing.manage), receptionist (11 permisos cubriendo agenda, clientes, mensajes y handoffs), professional (5 permisos casi solo lectura más responder mensajes). Un business puede crear sets propios con cualquier combinación.

  • Relación permission set ↔ permission. Many-to-many que define qué permisos contiene cada set. Cuando se crea un set custom por business, se elige el subconjunto de permisos atómicos que aplica.

  • Asignación de permission set a membership (membership permission set). Liga una membership con uno o más sets. Una sola membership puede tener varios sets simultáneamente — los permisos efectivos son la unión. Un trigger guarda que el set asignado pertenezca al business de la membership (no se puede asignar un set custom de otro business).

  • Override directo de permiso por membership (membership permission). Excepción puntual a los sets asignados. Tiene un effect (allow / deny). Un deny directo gana sobre cualquier allow que viniera de un set. Permite, por ejemplo, asignar el set admin a alguien y bloquearle puntualmente appointments.cancel por una razón específica.

Cómo se calcula el permiso efectivo de una membership (lógica que vive en src/backend/permissions/service.ts):

  1. Tomar todos los permisos heredados de los sets asignados.
  2. Aplicar los overrides directos: cada allow se suma a los heredados, cada deny se resta.
  3. Un deny directo gana sobre cualquier allow (sea heredado o directo).
  4. El resultado es un conjunto de codes que esa membership puede ejecutar.

Cada tool del agente y cada acción del dashboard verifica un code específico antes de ejecutar (ver Cap 6a v3 columna Permiso requerido).


Las reglas que protegen los datos

Las entidades por sí solas no garantizan que el sistema se comporte bien. Hacen falta reglas que el sistema aplica de manera invisible y constante para que nada se corrompa, se mezcle entre negocios, o se pierda. Estas reglas son tan importantes como las entidades mismas.

Aislamiento por tenant, con dos líneas de defensa. Una clínica nunca puede ver los turnos de una peluquería, ni accidentalmente, ni a propósito. El sistema implementa esto en dos capas que se refuerzan mutuamente:

  • La primera línea es Row Level Security (RLS): toda consulta a la base de datos pasa por un filtro automático que limita los resultados al business al que pertenece el usuario que ejecuta la consulta. Cubre al dashboard, al agente y a cualquier endpoint que exponga datos. Las tablas de las siete familias tienen políticas RLS específicas.
  • La segunda línea, más profunda, es un trigger de integridad cruzada (enforce_tenant_reference): si una tabla tiene una referencia hacia otra, el trigger verifica que ambos registros pertenezcan al mismo business antes de permitir la operación. Aunque el código tuviera un bug que intente cruzar datos entre tenants, la base de datos lo rechaza sola.

Integridad referencial. No puede existir un turno sin un negocio asociado. No puede existir un mensaje sin un thread. No puede existir un schedule_block que apunte a un staff de otro tenant. Si alguien intenta crear o modificar datos rompiendo estas relaciones, el sistema lo rechaza antes de que el daño ocurra.

Soft delete por defecto. Nada se borra de verdad. Cuando una persona cancela un turno, el turno no desaparece: se marca como cancelado y queda en la base. Cuando se desactiva un servicio o un profesional, se marca como inactivo pero los datos históricos siguen intactos. El borrado físico se reserva para casos puntuales (por ejemplo, cumplir con un pedido formal de eliminación de datos personales según la política legal del Cap 5).

Identificadores opacos. Cada registro tiene un identificador único de tipo UUID. El agente no puede inventar identificadores razonables, no se filtran datos por la URL o el log, y se evitan colisiones entre tenants. Toda herramienta del agente que reciba un identificador como entrada debe recibir un UUID real, nunca un nombre o una descripción.

Marcas de tiempo en todo. Cada registro guarda cuándo fue creado y cuándo fue modificado por última vez. Estas marcas son invisibles para el usuario final pero invaluables para diagnóstico, auditoría y métricas.

Auditoría granular. A diferencia de la versión 2 del capítulo, v3 reconoce que la auditoría es una entidad de primera clase: cualquier cambio importante queda registrado en audit_events + audit_event_changes. Permite reconstruir la historia del producto sin recurrir a logs de aplicación.

Hashes en lugar de tokens crudos. En pending_operations, el confirmation_token_hash no guarda el token plano — guarda su hash. El sistema solo puede validar que un token presentado coincide con el hash, nunca exhibir el token original. Es la misma lógica que se aplica a passwords en users.


Frentes abiertos al cierre de v3

De los cinco frentes que la versión 2 del capítulo dejaba abiertos, la mayoría cerró:

Gap v1/v2 Estado v3
Código corto del tenant (routing_code) ✅ Cerrado · businesses.public_code con check format y unique parcial.
Credenciales de canal por tenant ✅ Cerrado · channel_connections con credential_ref y webhook_secret_ref.
Prevención de colisiones en reservas temporales ⚠️ Mitigado · schedule_blocks centraliza la ocupación con check estructural; un constraint DB-level de no-overlap entre bloques activos del mismo target queda como pregunta abierta para Tomi (GAP-OVERLAP-01 en el Anexo 04a v3).
Consulta escalada como entidad ✅ Cerrado · handoff_requests.kind = 'escalated_question'.
Log de consultas fuera de scope ⚠️ Abierto · sin tabla dedicada todavía; se puede derivar de audit_events. Decisión post-MVP.
Operador con permisos limitados ✅ Cerrado · sistema de permisos granulares completo (Familia 7).

Frentes nuevos que aparecen al cierre de v3 (anotados en el Anexo 04a v3):

  • GAP-FILTER-01: el permission_set professional ve todos los customers y appointments del business. Para verticales donde el profesional comparte espacio con otros (kinesiólogos, esteticistas), hace falta extender las RLS para filtrar por staff_id además de business_id cuando el rol es professional. No bloquea MVP.
  • GAP-MSG-01: distinción entre messages.reply y handoffs.manage en la matriz Acción↔Tool. Decisión a cerrar antes del PR del runtime del agente.
  • GAP-PAY-01: permisos específicos de cobros y refunds del cliente final. Sumar cuando Mercado Pago entre en producción.
  • GAP-PROVIDER-01: enum assistant_provider no incluye gemini. Fix trivial cuando se sume Gemini al runtime.

Lo que este capítulo NO incluye

  • La sintaxis SQL del schema, las migraciones, los tipos de datos exactos y los índices (vive en el repo del Cap 5).
  • Las decisiones de motor de base de datos y de plataforma de hosting (eso es del Cap 5).
  • La lógica de cómo el agente consulta estos datos (eso es del Cap 6 Pliego 1).
  • La correspondencia exacta tabla por tabla con sus columnas y constraints (vive en el Anexo 04a v3).
  • El roadmap operativo de implementación (Cap 7).
  • Los reportes y métricas que se construyen sobre estos datos (vive en el dashboard, no en el manual).

Por qué este capítulo importa

Las entidades son el lenguaje común del proyecto. Si una persona del equipo entiende que un turno siempre tiene un servicio asociado y otra entiende que el servicio es opcional, el sistema termina con datos inconsistentes que no se pueden recuperar después. Si una entiende que un profesional pertenece a una sucursal y otra cree que puede moverse libremente entre negocios, las reglas de aislamiento se rompen sin que nadie se dé cuenta.

Este capítulo es el contrato de las cosas que existen. Tiene que estar cerrado y compartido antes de que se discutan herramientas, infraestructura o procesos. Todo lo que viene después se apoya en este lenguaje.

A la fecha de v3, las siete familias tienen materialización completa en el repo. La mayoría de los gaps de las versiones anteriores cerraron. Lo que sigue es activar la familia 5 (el agente) con código TS, según el orden del Cap 7 — todo el resto del manual ya está sostenido por entidades reales en una base real.


v1 — 2026-04-16 · primer mapa de cuatro familias. v2 — 2026-04-24 · alineación con el riel real (Cap 5 v2 + Anexo 04a v1). v3 — 2026-05-03 · regeneración mayor con tres familias nuevas (5 Agente y operaciones, 6 Billing, 7 Permisos), entidades nuevas (schedule_blocks, audio_transcript_segments, scheduled_notifications, customer_notes), 5 gaps cerrados, 1 mitigado, 1 abierto, 4 gaps nuevos identificados. 57 tablas en total (vs 35 en v1). Próxima edición cuando aparezca una entidad o gap nuevo en el repo.

04a Anexo A · Inventario del schema
04a

Anexo A · Inventario del schema

La correspondencia verificable entre entidades conceptuales del manual y tablas del repo tomillo/perla.


Para qué sirve este anexo

Este documento traduce las siete familias de entidades del Capítulo 4 al schema real que vive en tomillo/perla:src/lib/db/schema.ts. Es la ficha técnica que cualquier IA o desarrollador nuevo usa para responder “cuando el manual dice reserva temporal, ¿qué tabla del repo toco?”.

El manual describe el producto en lenguaje arquitectónico. El schema mantiene su naming inglés (convención del repo: customer, staff_member, appointment, service_variant). Este anexo es el puente — el manual conserva su voz de producto, el código conserva su convención, y este documento traduce uno al otro sin tocar ninguno.

Fuente: HEAD 13ac2bc del repo al 2026-05-03 para la estructura base. Schema con 57 pgTable + 50 pgEnum = 107 objetos. Crecimiento de 35→57 tablas y 29→50 enums entre el inventario v1 y el v3, principalmente por las migrations 23-24/04 que sumaron permisos, schedule blocks, audit, agent runs, billing plan catalog, conversations/channels/handoff y notifications queue.

v4 (2026-05-16) suma las observaciones operativas del dataset piloto cargado el 14/05 + los refinements del runtime del agente que entraron post-merge del módulo. No hay cambios estructurales nuevos — son confirmaciones de cómo se usan tablas existentes y convenciones runtime que el módulo del agente cimentó.


Las siete familias de entidades

A diferencia de la v1 (cuatro familias), el inventario actual reconoce siete agrupaciones conceptuales:

  1. El negocio y sus partes — el catálogo, agenda, profesionales, recursos, políticas. Lo que existe del lado del prestador del servicio.
  2. Las personas que entran al sistema — usuarios del dashboard, clientes finales, identidades por canal.
  3. Las interacciones operativas — turnos, holds, ocupación real de agenda, archivos, transcripciones, notificaciones.
  4. La conversación — hilos, estado, mensajes, conexiones de canal, eventos, handoffs.
  5. Agente y operaciones (familia nueva) — configuración del agente, runs, invocaciones de tools, pending operations, batching de mensajes, auditoría.
  6. Billing y pagos (familia nueva) — catálogo de planes, precios por mercado, suscripciones, payment intents.
  7. Permisos (familia nueva) — permisos atómicos, sets reusables, asignaciones por membership.

Familia 1 — El negocio y sus partes (16 tablas)

Todo lo que pertenece a un tenant. Cada elemento siempre vinculado a un business_id específico, nunca cruza la frontera hacia otro.

Entidad del Cap 4 Tabla Enums asociados Observaciones
Negocio businesses Entidad raíz del tenant. public_code con check format ^[A-Z0-9][A-Z0-9_-]{0,23}$ y unique index sobre lower(public_code) cierra el SG-1 (routing por código corto).
Configuración del negocio business_settings 1:1 con businesses. Default timezone, locale, currency, formato de fecha/hora, location por defecto, toggles de booking público y self-service del cliente.
Sucursal locations Dirección, timezone propio (puede diferir del negocio), is_primary.
Profesional staff_members staff_member_status (active · inactive · on_leave) Vincula opcionalmente a users.id cuando el profesional accede al dashboard. is_bookable, color_hex para agenda, internal_notes.
Especialidad specialties Categoría global por tenant (“Estética”, “Masajes terapéuticos”).
Profesional ↔ Especialidad staff_specialties Many-to-many. proficiency_level 1-5 con check.
Categoría de servicio service_categories “Cortes”, “Tratamientos”.
Servicio services Concepto abstracto. requires_staff, requires_customer_profile, preparation_instructions, aftercare_instructions.
Variante de servicio service_variants La unidad agendable. duration_minutes, price_amount con currency, buffer_before/after_minutes, capacity, online_booking_enabled. Check de tiempos coherentes.
Profesional ↔ Variante staff_service_variants Many-to-many con priority y is_default.
Recurso físico resources resource_type (room · chair · equipment · vehicle · other) · resource_booking_mode (exclusive · shared) · resource_status (active · inactive · maintenance) El modo shared permite ocupación concurrente.
Variante ↔ Recurso service_variant_resources service_variant_resource_requirement (required · optional · alternative) Cardinalidad n-a-n con quantity.
Regla de disponibilidad availability_rules availability_target_type (staff_member · resource · location) Reglas recurrentes por día de semana. Check brutal: target_type debe coincidir exactamente con UNA de las 3 FKs (xor pattern).
Excepción de disponibilidad availability_exceptions mismo availability_target_type Ausencias puntuales, feriados, cierres. Patrón xor target idéntico.
Política policy_sets policy_type (booking · cancellation · reschedule · lateness · no_show · deposit · notification · general) Reglas como jsonb. is_default por tipo.
Override de política policy_overrides policy_target_type (business · location · service · service_variant · staff_member · customer) Override por entidad específica. Unique sobre (policy_set_id, target_type, target_id, rule_key).

Familia 2 — Las personas que entran al sistema (4 tablas)

Llegan por dos puertas: la conversacional (canales) y la del dashboard (web).

Entidad del Cap 4 Tabla Enums asociados Observaciones
Usuario de plataforma users Autenticación Supabase. FK a auth.users se mantiene en SQL migration porque Supabase posee esa tabla. Email, displayName, avatarUrl, phone.
Identidad por canal del cliente customer_channel_identities channel (whatsapp · telegram · sms · email · phone · instagram · webchat · facebook · other) SG-7 cerrado — el enum incluye telegram. Unique sobre (business_id, channel, external_identity).
Cliente customers customer_status (active · inactive · archived) En el manual paciente (vertical salud + bienestar), en código customer (abstracción técnica neutral). Tiene whatsapp_phone, e164_phone, preferred_channel, preferred_staff_member_id, preferred_service_variant_id, accepts_marketing, accepts_reminders. Tres unique indexes (email, e164_phone, whatsapp_phone) por business.
Perfil del paciente en el negocio customer_profiles 1:1 con customers. Datos sensibles del Cap 4 Familia 3: birth_date, gender, allergies, skin_type, hair_type, usual_color_formula, consent_signed_at, consent_expires_at, emergency_contact_*.

Sobre la identidad cruzada (cliente y operador a la vez): sin tabla unificada, idéntico a v1. La misma persona aparece como customer_channel_identities (cuando habla por WhatsApp) y como users + memberships (cuando entra al dashboard). El routing del Cap 3 las vincula en tiempo de ejecución.


Familia 3 — Las interacciones operativas (10 tablas)

Lo que pasa entre las personas y los negocios. Esta familia creció considerablemente en v3 con schedule_blocks y audio_transcript_segments.

Entidad del Cap 4 Tabla Enums asociados Observaciones
Turno appointments appointment_status (pending · confirmed · in_progress · completed · cancelled · no_show) · appointment_source (manual · assistant · web · phone · import) · channel opcional en requested_channel El enum source permite responder “¿cuántos turnos agendó el agente vs cuántos a mano desde dashboard?” sin logs. confirmation_code único por business.
Ítem del turno appointment_items appointment_item_status (pending · confirmed · completed · cancelled · no_show) Cada servicio dentro de un turno multi-item. service_variant_id con restrict on delete (no se borra el variant si tiene items).
Reserva temporal booking_holds booking_hold_status (active · converted · expired · released) hold_token único globalmente, expires_at. SG-2 (UNIQUE parcial sobre (staff_member_id, starts_at) WHERE status='active') NO existe directamente. Está mitigado por schedule_blocks que centraliza la ocupación real (ver fila siguiente). Pendiente confirmar con Tomi si hay algún constraint de no-overlap a nivel servicio.
Bloque de agenda schedule_blocks schedule_block_type (appointment · hold · manual_block · closed) · schedule_block_status (active · released · cancelled · expired) Tabla nueva de v3. Centraliza la ocupación real de la agenda sobre un único target (staff/resource/location). Check num_nonnulls(staff, resource, location) = 1 garantiza target único. Check source valida coherencia entre block_type y FKs (appointment_item_id para appointments, booking_hold_id para holds). Es la entidad que materializa el “Schedule Block” del CONTEXT.md y permite anti-overlap consistente para todos los tipos de bloqueo. Pieza arquitectónica importante que el manual debe documentar en Cap 4.
Adjunto attachments attachment_kind (image · audio · video · document · transcript · other) Archivos vinculables a thread/message/handoff/customer/appointment. Storage bucket fijo perla-attachments. Check de path obliga prefix business_id/.
Transcripción de audio audio_transcripts transcript_status (pending · processing · completed · failed) 1:1 con attachments. Duración, confidence, speaker count.
Segmento de transcripción audio_transcript_segments Tabla nueva de v3. Permite trabajar con transcripts segmento-a-segmento (con start_ms, end_ms, speaker_label). Útil cuando un audio tiene múltiples speakers (owner + paciente).
Regla de notificación notification_rules notification_event (appointment_created · appointment_confirmed · appointment_cancelled · appointment_reminder · handoff_requested · message_received · custom) · notification_channel (whatsapp · email · sms · push · webhook · internal) · notification_recipient_type (customer · staff · owner · custom) 7 eventos predefinidos + custom. 6 canales. Templates con subject_template, body_template, template_key.
Notificación programada scheduled_notifications mismo notification_event y notification_channel · notification_status (pending · sent · failed · cancelled) Tabla nueva de v3 (separada del notification_logs). Cola de notificaciones futuras con scheduled_for, next_attempt_at, attempt_count, idempotency_key único. Snapshot de variables del template (customer_name, business_name, service_name, staff_name, action_url) capturadas al crear la notificación, no al enviarla.
Log de notificaciones notification_logs mismo notification_event · notification_status Registro histórico de cada intento de envío. attempted_at, sent_at, provider_response_code, failure_reason.

Familia 4 — La conversación (6 tablas)

El sistema necesita recordar lo que ya se dijo. La Familia 4 de v1 tenía 3 tablas; v3 reconoce 6 acá y mueve assistant_configs a Familia 5 (es configuración del agente, no de la conversación).

Entidad del Cap 4 Tabla Enums asociados Observaciones
Conexión de canal channel_connections channel · channel_connection_status (pending · active · inactive · failed) SG-3 cerrado. Cada tenant tiene su channel connection con provider, external_account_id (phone_number_id de Meta, bot username de Telegram según nomenclatura oficial de la Bot API), credential_ref y webhook_secret_ref (referencias a secretos, no tokens crudos), inbound_enabled/outbound_enabled/read_receipts_enabled/voice_messages_enabled/media_messages_enabled, message_window_hours (24h de WhatsApp), rate_limit_per_minute. Unique parcial sobre (business_id, channel).
Evento de canal channel_events mismo channel · message_direction · channel_event_status (received · processed · ignored · failed) Cada webhook de proveedor genera un evento. Unique sobre (business_id, provider, provider_event_id) resuelve dedup. raw_payload_ref apunta a almacenamiento del payload crudo.
Hilo de conversación conversation_threads mismo channel · conversation_thread_status (open · pending_handoff · in_handoff · resolved · archived) FK opcional a assistant_config_id (qué config se está usando), channel_connection_id (por dónde llega), assigned_staff_member_id (handoff). last_message_at, last_customer_message_at, last_assistant_message_at para sorting. Unique parcial sobre (business_id, channel, external_thread_id).
Estado de conversación conversation_state conversation_flow_status (idle · collecting_info · waiting_confirmation · waiting_payment · handoff · completed) 1:1 con thread. Slots ya recolectados: selected_location_id, selected_service_variant_id, selected_staff_member_id, proposed_start_at/end_at, selected_appointment_id. FK opcional a pending_operation_id para acciones esperando confirmación. last_customer_message_id, last_assistant_message_id, summary, needs_human.
Mensaje messages message_sender (customer · assistant · staff · system) · message_direction (inbound · outbound · internal) · message_status (queued · received · sent · delivered · read · failed) · message_type (text · audio · image · document · video · system_event · other) El histórico crudo. external_message_id único por thread (resuelve dedup de proveedor). detected_intent, detected_language_code, confidence (numeric 5,4 con check 0-1). Timestamps sent_at/delivered_at/read_at/failed_at con check de orden.
Solicitud de handoff handoff_requests handoff_kind (human_takeover · escalated_question) · handoff_status (requested · accepted · answered · rejected · completed · cancelled) · message_sender para requested_by Dos tipos de handoff. Para human_takeover el agente cede la conversación al humano. Para escalated_question el agente sigue conduciendo, pero deriva una pregunta puntual al owner y vuelve con la respuesta (resuelve SG-4 — consulta escalada). priority, assigned_staff_member_id, sla_due_at, resolution_notes.

⚠️ Cambio respecto a v1: el enum handoff_status cambió completamente. Antes era requested · acknowledged · resolved · dismissed; ahora es requested · accepted · answered · rejected · completed · cancelled. Refleja la separación entre handoff de takeover (con accepted/completed) y handoff de pregunta (con answered).


Familia 5 — Agente y operaciones (familia nueva, 8 tablas)

La capa del agente que el manual v2 mencionaba sin entidad concreta. v3 la consolida como familia propia.

Entidad del Cap 4 Tabla Enums asociados Observaciones
Configuración del asistente assistant_configs assistant_provider (openai · anthropic · custom) · channel opcional La “personalidad por tenant”. system_prompt, greeting_message, response_tone (varchar 64), response_style, business_instructions, safety_instructions, name, provider, model. Toggles: auto_reply_enabled, handoff_enabled, is_active, booking_tool_enabled, reschedule_tool_enabled, cancellation_tool_enabled, payment_link_tool_enabled. Guardrails: max_tool_calls_per_run (default 8) y max_run_seconds (default 60). FK opcional a location_id para fineado por sede. Drift menor: el enum assistant_provider no incluye gemini — pendiente sumar (decisión de producto + migration trivial).
Batch de mensajes entrantes inbound_message_batches mismo channel · inbound_message_batch_status (open · processing · processed · failed) Materializa el “lock por sesión + agrupar mensajes seguidos” del Cap 3. session_key por business + customer + canal, combined_text, message_count, first_message_id, last_message_id, opened_at, closed_at, processed_at. Cuando el cliente manda 3 mensajes seguidos, el sistema los agrupa antes de invocar al LLM — reduce costo y evita respuestas pisadas.
Run del agente agent_runs agent_run_status (running · completed · failed · cancelled) Cada ejecución del agente. FK a conversation_thread_id, inbound_message_batch_id, assistant_config_id. Captura model_provider, model_name, input_text, output_text, detected_intent, contadores de tokens (prompt_tokens, completion_tokens, total_tokens con check ≥ 0), error_code/error_message, started_at/completed_at.
Invocación de tool tool_invocations risk_level (low · medium · high · critical) · tool_invocation_status (pending · succeeded · failed · requires_confirmation) Cada llamada a tool dentro de un run. tool_name, input_summary, output_summary, target_entity_type, target_entity_id, FK opcional a pending_operation_id. Constraint clave: status = 'requires_confirmation'pending_operation_id IS NOT NULL.
Operación pendiente pending_operations mismo risk_level · pending_operation_status (pending · confirmed · executed · expired · cancelled · failed) Materializa el Pliego 5. operation_type, input_hash, preview_title, preview_summary, affected_count, confirmation_token_hash, expires_at. Unique parcial sobre (business_id, input_hash) WHERE status IN ('pending', 'confirmed') evita preview duplicado de la misma operación. Check de orden temporal: confirmed_at ≥ created_at, executed_at ≥ confirmed_at.
Item de operación pendiente pending_operation_items pending_operation_item_status (pending · executed · skipped · failed) Una fila por entidad afectada. entity_type, entity_id, action, current_value_text, proposed_value_text, item_summary. Permite mostrar al usuario “Voy a cancelar estos 4 turnos: …” y registrar resultado individual.
Evento de auditoría audit_events audit_actor_type (user · customer · assistant · system) Registro auditable de acciones importantes. event_type, entity_type, entity_id, request_id, ip_address (tipo inet), user_agent, reason, summary. Check de actor: si actor_type es user debe tener actor_user_id; si es customer debe tener actor_customer_id; si es assistant o system ambos null.
Cambio de auditoría audit_event_changes audit_value_type (text · number · boolean · date · uuid · enum) Detalle de cambios por field. field_name, old_value_text, new_value_text, old_value_id, new_value_id, value_type.

⚡ = tabla nueva en v3, no existía en el inventario v1.


Familia 6 — Billing y pagos (familia nueva, 6 tablas)

El billing de Perla hacia el negocio (suscripción al plan) y de Mercado Pago hacia el cliente final (señas, refunds). En v1 se mencionaban solo subscriptions y payment_intents “fuera de las 4 familias”; v3 las formaliza como familia con catálogo completo.

Entidad del Cap 4 Tabla Enums asociados Observaciones
Plan de billing billing_plans Catálogo de planes de Perla. code (kebab-case con check format), name, description, is_active, display_order. Tabla global, sin business_id.
Precio de plan por mercado billing_plan_prices billing_interval (month · year) Precio comercial por plan/mercado/moneda/intervalo. market_code (en mayúsculas con check), country_code, currency_code, amount, trial_days, version. Unique sobre versión + unique parcial sobre la versión activa.
Precio externo en proveedor billing_provider_prices Mapeo entre el precio de Perla y el price/product ID del proveedor (Stripe, Mercado Pago). provider, environment (testing/production), external_product_id, external_price_id, external_plan_id.
Perfil de billing del negocio business_billing_profiles 1:1 con businesses. market_code (default GLOBAL), billing_country_code, billing_currency_code (default USD), preferred_provider (default stripe), billing_email, tax_id, tax_name.
Suscripción subscriptions subscription_status (trialing · active · past_due · cancelled · inactive) · billing_interval Vincula business con un billing_plan_price_id y opcionalmente con billing_provider_price_id. provider, external_subscription_id, current_period_start/end, trial_ends_at, cancel_at_period_end, seat_count, last_payment_error_*.
Payment Intent payment_intents payment_intent_kind (subscription · deposit · full · refund · adjustment) · payment_intent_status (requires_action · requires_capture · processing · succeeded · cancelled · failed) · payment_capture_method (manual · automatic) Polimórfico: vincula a subscription_id (cobro de plan), appointment_id (seña/cobro de turno) o ninguno (refund/adjustment). provider, external_payment_intent_id, external_charge_id, external_checkout_url, external_refund_id, provider_webhook_event_id. Default provider mercado_pago.

Familia 7 — Permisos (familia nueva, 5 tablas)

El gap SG-6 cerró con un sistema de permisos granulares. Ver sección “Cómo se modelan los permisos en práctica” más abajo.

Tabla Enums Observaciones
permissions Catálogo atómico de 19 permisos system-seeded (ver lista abajo). code único, module, action, name, description, is_system.
permission_sets Plantillas reusables. Pueden ser system (business_id IS NULL) o por business. Unique parcial: globales sobre code, por business sobre (business_id, code).
permission_set_permissions Many-to-many entre sets y permisos atómicos. PK compuesta.
membership_permission_sets Qué sets tiene cada membership. assigned_by_user_id para auditoría. Trigger guard valida que el set pertenezca al business de la membership (función permission_set_matches_membership).
membership_permissions permission_effect (allow · deny) Overrides directos por membership. deny directo gana sobre cualquier allow (lógica resuelta en src/backend/permissions/service.ts).

Los 19 permisos atómicos

Pre-seedados como is_system = true:

Module Codes
dashboard dashboard.access
businesses businesses.update
locations locations.manage
staff staff.manage
services services.manage
customers customers.read, customers.create, customers.update
appointments appointments.read, appointments.create, appointments.update, appointments.cancel
messages messages.read, messages.reply
handoffs handoffs.manage
billing billing.read, billing.manage
settings settings.manage
permissions permissions.manage

Los 4 permission sets system

Pre-seedados con business_id = NULL:

Set code Permisos Mapea a
owner 19/19 (todos) Dueño/dueña del negocio.
admin 17/19 (sin permissions.manage ni billing.manage) “Mano derecha” del dueño.
receptionist 11/19 (agenda + clientes + mensajes + handoffs + dashboard) Recepción / secretaria.
professional 5/19 (dashboard.access, customers.read, appointments.read, messages.read, messages.reply) Profesional que presta el servicio.

Importante para el manual: los 4 perfiles humanos del Cap 2 se mantienen (paciente · owner · dashboard · equipo El Nudo). El “operador con permisos limitados” del Cap 2 NO es un 5to perfil — es una membership con un permission set específico (admin / receptionist / professional o un set custom por business). El profesional NO gestiona el negocio, presta el servicio.


Inventario de enums (50)

Familia Enums Cantidad
Membresías y permisos membership_role, permission_effect 2
Canal channel (incluye telegram ✅) 1
Cliente / profesional customer_status, customer_note_visibility, customer_note_type, staff_member_status 4
Recursos resource_type, resource_booking_mode, resource_status, service_variant_resource_requirement, availability_target_type 5
Turnos / agenda appointment_status, appointment_source, appointment_item_status, booking_hold_status, schedule_block_type, schedule_block_status 6
Políticas policy_type, policy_target_type 2
Asistente / agente assistant_provider ⚠️ (sin gemini), agent_run_status, tool_invocation_status, risk_level, pending_operation_status, pending_operation_item_status, inbound_message_batch_status 7
Conversación conversation_thread_status, conversation_flow_status, message_sender, message_direction, message_status, message_type, handoff_kind, handoff_status 8
Canales / eventos channel_connection_status, channel_event_status 2
Adjuntos attachment_kind, transcript_status 2
Notificaciones notification_event, notification_channel, notification_recipient_type, notification_status 4
Auditoría audit_actor_type, audit_value_type 2
Billing subscription_status, billing_interval, payment_intent_kind, payment_intent_status, payment_capture_method 5
Total 50

Entidades conceptuales sin tabla propia (resueltas por composición)

El manual describe algunas entidades conceptuales que en el schema se resuelven por composición. No es error — es el patrón correcto para un modelo relacional limpio.

Entidad del Cap 4 Cómo se resuelve en el schema
Recordatorio programado No existe tabla reminders. Los recordatorios viven en scheduled_notifications (cola futura) + notification_logs (histórico). Las reglas de cuándo y cómo recordar viven en notification_rules.
Identidad cruzada (cliente y operador a la vez) Sin tabla unificada. La misma persona aparece como customer_channel_identities (cuando habla por WhatsApp) y como users + memberships (cuando entra al dashboard). El routing del Cap 3 las vincula en tiempo de ejecución.
Operador con permisos limitados Es una membership con role IN ('admin', 'member') y uno o más permission_sets asignados (admin / receptionist / professional o un set custom por business).
Consulta escalada Es un handoff_request con kind = 'escalated_question'. La pregunta del paciente vive en question, la respuesta del owner en answer. SG-4 cerrado.

Estado de los gaps del v1 (snapshot 2026-05-03)

ID Gap original Estado
SG-1 Código corto del tenant (routing_code) Cerrado. businesses.public_code con check format y unique parcial.
SG-2 UNIQUE parcial en booking_holds para anti-overlap ⚠️ Mitigado, no cerrado. No hay UNIQUE explícito sobre (staff_member_id, starts_at) WHERE status='active'. La protección reside en schedule_blocks que centraliza ocupación con check de target único, pero el constraint de no-overlap entre bloques activos del mismo staff/resource depende del service layer (no del schema). Pregunta abierta para Tomi: ¿agregamos exclusion constraint con tstzrange en schedule_blocks para garantizarlo a nivel DB?
SG-3 Credenciales de canal por tenant Cerrado. channel_connections con credential_ref, webhook_secret_ref, external_account_id.
SG-4 Consulta escalada como entidad Cerrado. handoff_requests con kind = 'escalated_question'.
SG-5 Log de consultas fuera de scope ⚠️ Abierto. Sin tabla dedicada. Se puede derivar de audit_events filtrando por event_type específico, pero sin eventness explícito. Decisión post-MVP.
SG-6 Operador con permisos limitados Cerrado con sistema de permisos completo (Familia 7).
SG-7 channel enum sin telegram Cerrado. telegram ya está en el enum.
SG-8 Dónde vive assistant_configs arquitectónicamente Cerrado. En Familia 5 (Agente y operaciones). El manual v3 lo formaliza así.

Gaps nuevos (anotados, no bloquean MVP)

Detectados durante el AUDIT del 03/05.

ID Gap Tipo Prioridad
GAP-FILTER-01 Profesional ve solo sus pacientes/turnos Cambio de RLS — filtrar por staff_id además de business_id cuando rol es professional Media · activar cuando un beta tester serio lo pida (caso Mati)
GAP-MSG-01 Distinguir messages.reply (responder mensaje del staff) vs handoffs.manage (tomar conversación del agente) en la matriz acción↔tool Decisión de diseño + actualización 06a Media · al redactar 06a v3
GAP-PAY-01 Permisos de cobros / refunds del cliente final Sumar permission codes tipo payments.refund o payments.cancel Baja · cuando MercadoPago entre en producción
GAP-OVERLAP-01 Constraint DB-level de no-overlap entre schedule_blocks activos del mismo target Exclusion constraint con tstzrange o trigger de validación Media · pregunta abierta a Tomi
GAP-PROVIDER-01 Enum assistant_provider no incluye gemini (manual lo prevé) Agregar valor al enum + migration Baja · trivial, se hace cuando se sume Gemini al runtime

Cómo se mantiene el schema (proceso operativo)

El repo tiene infraestructura de migrations consolidada:

Tres ambientes Supabase, cada uno con su scripts en scripts/: - Local (development): DATABASE_URL apuntando a Postgres local. npm run db:studio abre Drizzle Studio. - Testing (staging): scripts db:testing:info, db:testing:migration-list, db:testing:dry-run, db:testing:push vía scripts/perla-supabase-testing.sh. - Production: scripts db:prod:info, db:prod:migration-list, db:prod:dry-run, db:prod:push vía scripts/perla-supabase-env.sh production. Push manual con autorización explícita (regla de la política de seguridad).

Workflow de migrations: 1. Cambiar src/lib/db/schema.ts. 2. pnpm run db:generate → genera SQL en supabase/migrations/. 3. Revisar el SQL generado. 4. pnpm run db:testing:dry-run → valida en testing sin aplicar. 5. pnpm run db:testing:push → aplica en testing. 6. Test E2E contra testing. 7. Cuando se aprueba, pnpm run db:prod:push con autorización del responsable técnico.

Las migrations actuales (HEAD del 03/05): - 0000_platform_foundation.sql — base inicial - 0001_product_domain_schema.sql — primera versión del dominio - 0002_security_hardening.sql — RLS hardening - 0003_perla_foundation_naming.sql — rename “norapp” → “perla” - 20260423210544_perla_permissions.sql — Familia 7 completa - 20260423214509_perla_catalog_no_jsonb.sql — refactor catálogo sin jsonb - 20260423215823_perla_schedule_blocks.sql — nueva tabla schedule_blocks - 20260423220825_perla_attachments_transcripts_storage.sql — attachments + transcripts - 20260423221559_perla_conversations_channels_handoff.sql — conversación + canales + handoff - 20260423222415_perla_notifications_queue_logs.sqlscheduled_notifications + notification_logs - 20260423222806_perla_billing_mercado_pago.sql — payment_intents para Mercado Pago - 20260423223339_perla_audit_agent_operations.sql — Familia 5 completa - 20260423224400_perla_rls_permission_hardening.sql — RLS sobre Familia 5 + 7 - 20260424215126_perla_billing_plan_catalog.sql — billing_plans + prices


Correspondencia matriz acción↔tool ↔ schema

Esta tabla se mantendrá actualizada con cada actualización del 06a (matriz Acción↔Tool). Cada entidad del schema sostiene un subconjunto de acciones de la matriz.

Tabla(s) Acciones de la matriz que la(s) tocan
businesses + business_settings DA-70, DA-71, DA-201
locations DA-72, DA-73, DA-74, DA-75
staff_members + specialties + staff_specialties DA-80 a DA-85
services + service_categories + service_variants + staff_service_variants DA-90 a DA-96
resources + service_variant_resources DA-140 a DA-142
availability_rules + availability_exceptions DA-100 a DA-105
appointments + appointment_items DA-01, DA-03 a DA-09, DA-20 a DA-27, DA-45
booking_holds + schedule_blocks DA-28, DA-09 (protección anti-overlap)
policy_sets + policy_overrides DA-130, DA-131, DA-132
assistant_configs DA-110 a DA-116
conversation_threads + conversation_state + messages DA-50 a DA-57, DA-59, DA-60
handoff_requests DA-58
customers + customer_channel_identities + customer_profiles + customer_notes DA-40 a DA-47, DA-60
notification_rules + scheduled_notifications + notification_logs DA-120 a DA-122
attachments + audio_transcripts + audio_transcript_segments (sin UI dedicada todavía — vinculados a messages/handoffs)
billing_plans + billing_plan_prices + billing_provider_prices + business_billing_profiles + subscriptions + payment_intents DA-150, DA-151 (+ acciones nuevas a sumar en 06a v3)
permissions + permission_sets + permission_set_permissions + membership_permission_sets + membership_permissions (acciones nuevas a sumar en 06a v3 — gestión de permisos del equipo desde dashboard)
agent_runs + tool_invocations + pending_operations + pending_operation_items + inbound_message_batches + audit_events + audit_event_changes (acciones nuevas a sumar en 06a v3 — observabilidad del agente, confirmación de pending operations)

Convenciones confirmadas con el dataset piloto (carga 14/05)

El 2026-05-14 Tomi cargó el dataset piloto descrito en DATASET_PILOTO_PERLA.md (Drive, 01_INFRA/) sobre perla-testing. El dataset estaba escrito en lenguaje schema-agnostic; al cargarlo se confirmaron mappings y convenciones que vale documentar acá porque cualquier IA o developer que cargue otro tenant en el futuro las va a necesitar.

Schema-agnostic ↔ tabla real

Concepto del dataset (lenguaje natural) Tabla / columna real del repo
“Horarios del staff” / staff_schedules (naming del dataset) availability_rules
“Quién hace qué” / service_staff / matriz staff↔servicio staff_service_variants (a nivel service_variant, no service)
“Staff no bookable” staff_members.is_bookable = false
rules_text / metadata operativa del business policy_sets con policy_type apropiado
Conexión WhatsApp del tenant channel_connections con inboundEnabled=true + status='active'
Configuración del agente para el tenant assistant_configs (una fila por business, opcionalmente por canal)

Bloques disjuntos de horarios en availability_rules

La tabla acepta múltiples filas con la misma combinación (staff_id, day_of_week) para modelar bloques disjuntos del mismo día. Ejemplo del piloto: Tomi atiende los lunes de 9 a 11 y de 17 a 19 — eso se modela como dos rows distintos para (staff_id=Tomi, day_of_week=monday) con start_time y end_time propios. El total de 35 reglas del piloto cuadra con esta convención. La tabla no requiere unique constraint sobre (staff_id, day_of_week) justamente por este patrón.

Slot duration unificado a 15 minutos como convención

Cada service_variant define su propia duration_minutes propia. La convención del producto al 2026-05-16 es que las duraciones sean múltiplos de 15 (15 / 30 / 45 / 60 / 180 son los usados en el piloto). Permite que el cálculo de disponibilidad opere sobre una grilla uniforme sin fragmentación. No es un constraint del schema — es convención operativa que el onboarding por business respeta y el agente asume al ofrecer slots.

Staff no-bookable como mecanismo de easter egg / scope-out

staff_members.is_bookable = false se usa para staff que existen en el business (para reportes, audit, métricas) pero no se ofrecen al agente como opciones de booking. En el piloto, Nico está cargado como staff con is_bookable = false — si un cliente le pide turno con Nico, el agente responde el easter egg documentado en policy_sets del business, no abre booking_hold.


Refinamientos del runtime del agente sobre el schema (post merge 14/05)

El módulo del agente mergeado el 14/05 cristalizó dos convenciones runtime sobre el schema existente que vale documentar acá porque afectan cómo el código del agente consume las tablas — son contratos de uso, no estructuras nuevas.

Default effectiveAudienceRole = 'customer'

Cuando el input del agent_run no trae audienceRole explícito, el runtime asume customer como default. Antes el default era undefined que mantenía comportamiento legacy (sin filter de tools por audiencia). El cambio es de seguridad por default: cuando no hay audiencia explícita, se asume el rol con menos privilegios. Cualquier integración nueva (canal, endpoint manual, futuro Telegram con telegram_user_id modelado para staff) que quiera trabajar como owner o staff debe pasar audienceRole explícito.

audienceRole en el AgentToolExecutionContext

Cada tool recibe el audienceRole del remitente en su context de ejecución. Esto habilita filter interno por audiencia dentro de la tool sobre las tablas que consulta. Ejemplos:

  • list_appointments con audienceRole='customer' filtra appointments.customer_id = currentCustomerId; con audienceRole='owner' devuelve todos los del business.
  • list_conversations con audienceRole='customer' rechaza el call (la tool tiene allowedAudienceRoles=['owner', 'staff']); con audienceRole='owner' devuelve conversation_threads del business.
  • cancel_appointment puede aceptar customer cancelando su propio turno, pero exige confirmación adicional (vía pending_operations) cuando el owner cancela turnos ajenos en batch.

El filter de audiencia es complementario al RLS, no sustituto. RLS garantiza aislamiento entre tenants a nivel SQL; el filter de audiencia opera dentro de un mismo tenant para distinguir customer vs owner vs staff. Las acciones high-risk pasan también por pending_operations con token + TTL — el filter es un guardrail, no el único.

Patrón canónico: ver Cap 5 · “P-10 Audience Role como propiedad runtime del agente”.


Reglas de mantenimiento del anexo

  • Cada vez que Tomi (o quien sea) agregue o renombre una tabla en src/lib/db/schema.ts, este anexo se actualiza antes de que el Cap 04 se modifique. El Cap 04 en prosa tolera niveles de abstracción; este anexo no.
  • Naming canónico: este anexo usa los nombres del schema (staff_members, booking_holds, snake_case, inglés). El Cap 04 en prosa usa los nombres conceptuales (Profesional, Reserva temporal, español). No se unifican — cada voz vive en su documento.
  • Vocabulario “Customer/Paciente”: el anexo usa customer (naming técnico). El Cap 04 en prosa puede usar Paciente para vertical salud + bienestar (decisión 03/05). No es contradicción — son capas distintas.
  • Convenciones operativas vs schema: las convenciones del dataset piloto (slot 15min, bloques disjuntos en availability_rules, easter egg de staff no-bookable) son convenciones de uso, no constraints del schema. El anexo las documenta para que el onboarding por business las respete, pero no las cementa como reglas SQL.
  • Versionado: v4 reemplaza a v3 sumando observaciones operativas del dataset piloto + refinamientos runtime post-merge. Versiones anteriores en _versions/.

Lo que este anexo NO incluye

  • Las migrations SQL completas (viven en supabase/migrations/ del repo).
  • Los índices, foreign keys y check constraints de cada tabla en detalle (se documentan acá solo cuando aportan al contrato; el detalle vive en código).
  • Los defaults, generated columns y funciones triggers — viven en código.
  • RLS policies y functions del schema private — viven en Cap 5 o en código.
  • Los datos puntuales del business piloto (address, contacto, web, redes). Eso vive en MVP_MANUAL.html como ejemplo del onboarding del piloto, no acá. Este anexo es schema + convenciones, no contenido del piloto.

v1 · 2026-04-24 · primer volcado desde HEAD 559d6bd. v3 · 2026-05-03 · regeneración entera contra HEAD 13ac2bc. 7 familias (vs 4 en v1), 57 tablas (vs 35), 50 enums (vs 29). 5 gaps cerrados (SG-1, SG-3, SG-4, SG-6, SG-7), SG-8 resuelto, SG-2 mitigado, SG-5 abierto. 5 gaps nuevos identificados (FILTER-01, MSG-01, PAY-01, OVERLAP-01, PROVIDER-01). Próxima edición cuando un cambio relevante de schema lo amerite.

05 Sistema constructivo
05

Sistema constructivo

¿Con qué está construido Perla y cómo se sostiene en producción?


Este capítulo describe el sistema constructivo: las tecnologías concretas con las que está hecho Perla, cómo se articulan entre sí, y qué garantiza que el edificio no se caiga cuando entran los primeros inquilinos. Es el equivalente al Legajo Ejecutivo de una obra — define con qué materiales y métodos se construye lo que los capítulos anteriores describieron en abstracto.

La decisión central que atraviesa todo este capítulo es deliberada: Perla no se construye desde cero. El riel productivo es el repo tomillo/perla (antes tomillo/aun) que Tomi viene armando hace meses — una plataforma multi-tenant funcional con base de datos endurecida, dashboard maquetado sobre las cinco secciones principales del producto, infraestructura de CI/CD completa con preview por Pull Request, staging automático y producción manual, tres ambientes Supabase parametrizados con scripts dedicados, y desde el merge del 14/05 también el módulo del agente conversacional con runtime, adapter al LLM, ocho tools con lógica real, persistencia de runs y tool invocations, y audiencia cableada como propiedad runtime que filtra capabilities por rol (customer / owner / staff).

La capa del agente fue el último aporte estructural al repo. Lo que queda como evolución está cubierto por el roadmap del Capítulo 7 (Fase A · piloto + Fase B · deploy) y se materializa como capabilities específicas sobre el runtime ya existente, no como reescritura del módulo.


Los dos caminos

Conviven dos entornos con propósitos distintos. No compiten — se alimentan.

PROTOTIPO                          PRODUCCIÓN
n8n + WhatsApp + Supabase          Cloudflare Workers + WhatsApp + Supabase
validar lógica                     escalar y no caerse
horas                              días
frágil a propósito                 production-grade

n8n es el Figma de la lógica. Se arma un flujo nuevo, se prueba con mensajes reales contra un agente de test, se documenta lo que funciona, y recién ahí se le pide a quien corresponda implementarlo en el código de producción. n8n nunca toca clientes reales fuera de los dos beta testers iniciales (NUDO, SURI). Es el tablero de diseño donde se valida que una idea conversacional es implementable antes de invertir días en codificarla.

El código de Cloudflare es la construcción. Acá va todo lo que tiene que funcionar para negocios reales: escala automática, aislamiento entre tenants, webhooks de WhatsApp, cálculo de disponibilidad, pagos. Lo que llega acá no se experimenta — se ejecuta.

La regla de oro: n8n valida, código produce. Seba diseña y prueba la lógica conversacional. La producción se implementa en patrón correcto. Nunca al revés.

Cuando el agente del repo pueda recibir un mensaje, decidir qué hacer, ejecutar una tool y responder por WhatsApp, n8n se apaga. Ese momento está descripto en el Capítulo 7 como cierre del PR #13 del roadmap del agente.


El riel productivo

El stack de producción está decidido y en funcionamiento. Cada pieza resuelve un problema específico y fue elegida por razones concretas — no por preferencia estética ni por moda.

La arquitectura en dos Workers

El producto se despliega en dos Workers de Cloudflare independientes: el Worker de API (API + agente + webhooks + crons) y el Worker de frontend (dashboard TanStack Start). Viven en configuraciones separadas — wrangler.jsonc y wrangler.frontend.jsonc — y se despliegan por separado. Esto permite iterar la UI sin redeployar la API y mantener cada worker más liviano.

┌─────────────────────────────────────────────────────────┐
│  CLIENTES                                                │
│  WhatsApp Business API · Telegram (canal de pruebas)    │
└─────────────────────┬───────────────────────────────────┘
                      │ webhook HTTPS
┌─────────────────────▼───────────────────────────────────┐
│  WORKER API  (wrangler.jsonc)                           │
│                                                          │
│   ┌──────────┐   ┌──────────┐   ┌─────────────────┐    │
│   │ Adapters │ → │   Core   │ → │   Aplicación    │    │
│   │ Telegram │   │ Routing  │   │ Motor de turnos │    │
│   │ WhatsApp │   │ Identity │   │ Agente + tools  │    │
│   │ MercPago │   │ Permisos │   │ Notificaciones  │    │
│   └──────────┘   └────┬─────┘   └────────┬────────┘    │
│                       │                   │             │
│   ┌───────────────────▼───────────────────▼──────────┐ │
│   │  Capa de infraestructura                          │ │
│   │  Drizzle → Hyperdrive → Supabase Postgres         │ │
│   │  Supabase Auth (dashboard)                        │ │
│   │  AI SDK (próximamente — PR #3 del Cap 7)          │ │
│   └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
                      ▲
                      │ fetch cross-origin (API calls)
                      │
┌─────────────────────┴───────────────────────────────────┐
│  WORKER FRONTEND  (wrangler.frontend.jsonc)             │
│  TanStack Start + React                                  │
│  Sirve el dashboard en /app/*                            │
│  Auth: Supabase Auth con cookies HTTP-only               │
└─────────────────────────────────────────────────────────┘

Consecuencia práctica: cuando el dashboard consume datos del agente o de la base, hace un fetch cross-worker con Authorization: Bearer <token>. Eso requiere CORS configurado explícitamente en el Worker API (ya está, con whitelist de orígenes para localhost, staging.holaperla.com y holaperla.com).

Las piezas, de afuera hacia adentro

Cloudflare Workers. El runtime donde vive el código. Se enciende cuando llega un mensaje, se apaga cuando termina de procesarlo. Pricing por request, no por hora — alineado con el modelo de uso del producto, que tiene picos a la mañana y valles a la noche. Limitaciones conocidas: 128 MB de memoria y 30 segundos de CPU por request. Alcanza y sobra para un agente conversacional.

TanStack Start + React. El framework del dashboard web. Corre nativo sobre Workers y resuelve server-side rendering, ruteo por archivos y composición SSR en la misma aplicación. Elegido sobre Next.js porque no requiere Vercel — el deploy completo (dashboard + API + agente) vive en Cloudflare.

Hono. El router HTTP adentro del Worker API. Liviano, tipado, sin dependencias. Es donde se montan los webhooks de mensajería, los endpoints de la API pública (/api/v1/*) y los handlers de cron. Funciona como pasillo de distribución: recibe cada request y la manda al módulo correcto.

Drizzle ORM. La interface tipada con la base de datos. Cada query es TypeScript compilado a SQL, sin runtime pesado. Elegido sobre Prisma porque Prisma no corre en Workers (requiere proceso separado). Las migraciones se generan desde el schema TypeScript y se aplican a Supabase. El schema TypeScript es la fuente de verdad de la estructura de datos.

Hyperdrive. El pool de conexiones de Cloudflare hacia Postgres. Sin Hyperdrive, cada request del Worker abriría una conexión nueva a Supabase y el Postgres se ahogaría con cincuenta tenants. Con Hyperdrive, las conexiones se cachean en el edge de Cloudflare y se reutilizan. Configurado en wrangler.jsonc para los ambientes staging y production.

Supabase. La base de datos managed + autenticación de dueños del dashboard + Row Level Security incluido. Postgres real, no una versión lite. Para el plan gratuito hay que hacer backups manuales; para el ambiente production con clientes reales, el upgrade al plan pago es obligatorio.

AI SDK de Vercel (próximamente). La interface al modelo. Abstrae Gemini, Claude y OpenAI detrás de la misma API. Cambiar de modelo por rol o por tenant es cambiar una línea, no reescribir la integración. Esta pieza no está en el repo todavía — entra con el PR #3 del roadmap del Cap 7.

Qué cambia respecto a un servidor tradicional

Un servidor tradicional siempre está encendido: pagás por hora, se mantiene con SSL, firewall, actualizaciones, backups, y si se cae, se cae para todos. Workers funciona al revés: se enciende cuando hace falta, escala solo, no tiene mantenimiento, y una falla en una instancia no afecta a las demás. Para el modelo del producto (bajo costo por tenant, alta variabilidad de tráfico), Workers es la elección correcta.


Tres ambientes Supabase

A diferencia de la versión 2 del capítulo, hoy el repo tiene tres ambientes Supabase parametrizados con scripts dedicados:

  • Local (development). DATABASE_URL apuntando a Postgres local. pnpm run db:studio abre Drizzle Studio. pnpm run db:generate genera migraciones desde src/lib/db/schema.ts.
  • Testing (staging). Scripts db:testing:info, db:testing:migration-list, db:testing:dry-run, db:testing:push vía scripts/perla-supabase-testing.sh. Es el ambiente donde se valida cada migration antes de tocar producción.
  • Production. Scripts db:prod:info, db:prod:migration-list, db:prod:dry-run, db:prod:push vía scripts/perla-supabase-env.sh production. Push manual con autorización explícita del responsable técnico (regla de la política de seguridad del repo).

Workflow de migrations:

  1. Cambiar src/lib/db/schema.ts.
  2. pnpm run db:generate genera el SQL de la migration.
  3. Revisar el SQL generado.
  4. pnpm run db:testing:dry-run valida en testing sin aplicar.
  5. pnpm run db:testing:push aplica en testing.
  6. Test E2E contra testing.
  7. Cuando se aprueba, pnpm run db:prod:push con autorización del responsable técnico.

Las catorce migrations actuales (HEAD del 03/05) están listadas en el Anexo 04a_INVENTARIO_SCHEMA_v3.md con su rol específico. Las cuatro primeras son de plataforma (platform_foundation, product_domain_schema, security_hardening, perla_foundation_naming); las diez siguientes son por dominio (permissions, catalog_no_jsonb, schedule_blocks, attachments_transcripts_storage, conversations_channels_handoff, notifications_queue_logs, billing_mercado_pago, audit_agent_operations, rls_permission_hardening, billing_plan_catalog).


La base de datos como plano estructural

Si el Worker es el sistema nervioso, la base de datos es la columna vertebral. Tomi diseñó un schema de 57 tablas y 50 enums organizados en siete familias que el Capítulo 4 v3 describe en lenguaje conceptual y el Anexo 04a_INVENTARIO_SCHEMA_v3.md mapea a los nombres reales del repo.

NEGOCIO (Familia 1)
  └─ locations (sucursales)
       └─ staff_members
            └─ availability_rules / availability_exceptions
       └─ services → service_variants
            └─ staff_service_variants (quién hace qué)
            └─ service_variant_resources (con qué recursos)
       └─ resources (salas, sillas, equipamiento)
       └─ policy_sets / policy_overrides

PERSONAS (Familia 2)
  └─ users (dashboard) ↔ memberships
  └─ customers (cliente final, ver Cap 1 sobre vocabulario "Paciente")
       └─ customer_channel_identities
       └─ customer_profiles (datos sensibles del Cap 4)

INTERACCIONES (Familia 3)
  └─ booking_holds (reserva transitoria)
  └─ appointments (turno confirmado)
       └─ appointment_items (servicios dentro del turno)
       └─ schedule_blocks (ocupación real, anti-overlap)
  └─ attachments / audio_transcripts / audio_transcript_segments
  └─ notification_rules → scheduled_notifications → notification_logs

CONVERSACIÓN (Familia 4)
  └─ channel_connections (canales configurados por tenant)
  └─ channel_events (eventos crudos de los proveedores)
  └─ conversation_threads
       └─ conversation_state (intent + slots)
       └─ messages
       └─ handoff_requests

AGENTE (Familia 5)
  └─ assistant_configs (personalidad por tenant)
  └─ inbound_message_batches (anti-spam, agrupación)
  └─ agent_runs (cada ejecución del agente)
  └─ tool_invocations
  └─ pending_operations + pending_operation_items
  └─ audit_events + audit_event_changes

BILLING (Familia 6)
  └─ billing_plans → billing_plan_prices → billing_provider_prices
  └─ business_billing_profiles
  └─ subscriptions
  └─ payment_intents

PERMISOS (Familia 7)
  └─ permissions → permission_sets → permission_set_permissions
  └─ membership_permission_sets
  └─ membership_permissions (overrides directos)

Tres decisiones estructurales importantes:

Multi-tenant real, no por convención. Cada tabla de dominio tiene business_id con FK directa a businesses. No hay “el negocio por defecto”. Los datos de NUDO nunca se mezclan con los de SURI, garantizado por código y por base de datos a la vez.

Dos líneas de defensa para el aislamiento. La primera es Row Level Security (RLS), que evita que un usuario del dashboard vea datos de un negocio que no le pertenece. La segunda — más profunda — es el trigger enforce_tenant_reference instalado en todas las relaciones cruzadas: si una tabla tiene una FK hacia otra, el trigger verifica que ambos registros pertenecen al mismo business_id. Aunque el código tuviera un bug que intente cruzar datos entre tenants, la base de datos lo rechaza sola.

Sofisticación preparada para escalar. El schema distingue entre servicio (nombre, categoría) y variante (duración, precio, buffer). Separa staff de recursos físicos. Tiene políticas parametrizables por tenant con overrides por entidad. Centraliza la ocupación real de la agenda en schedule_blocks con un constraint num_nonnulls(staff, resource, location) = 1 que garantiza que cada bloque ocupa exactamente un target. Para el MVP algunas columnas y tablas quedan vacías, pero la estructura está pensada para soportar peluquerías con diez profesionales y consultorios con dos salas sin reescribir nada.

Gaps del schema consolidados en el Anexo 04a v3. De los ocho gaps que tenía la v1 (24/04), cinco están cerrados, dos mitigados, uno abierto. Cinco gaps nuevos identificados al cierre de esta versión del capítulo, todos no bloqueantes para MVP.


El principio de cierre — UI y agente como dos caras de la misma arquitectura

Hay una ley arquitectónica que atraviesa todo el producto y que vive cristalizada en el Anexo 06a_MATRIZ_ACCION_TOOL_v3.md:

Cada acción del dashboard mapea a una tool del agente. UI y agente son dos caras de la misma arquitectura. create_appointment · L2 es la misma operación se invoque desde la agenda del dashboard o desde un mensaje de WhatsApp.

Esto tiene consecuencias de diseño importantes:

Una sola fuente de lógica, dos canales de acceso. La lógica de negocio no vive en el dashboard ni en el agente — vive en la capa de services backend (src/backend/<dominio>/). El dashboard llama services; el agente llama services; la base responde a services. Agregar un botón nuevo al dashboard sin service detrás es inconsistente — rompe el principio de cierre.

El owner puede operar el producto completamente por WhatsApp. Sin abrir el dashboard. Es la promesa del Capítulo 02 perfil owner. El dashboard es comodidad, no obligación.

Consistencia de permisos garantizada a nivel service, no UI. Si una operación requiere un permission code específico, la invocación desde dashboard o desde WhatsApp pasa por la misma validación. Ningún cliente puede ejecutar update_service aunque descubra el endpoint, porque services.manage no está en su set de permisos.

Auditoría unificada. Cada acción queda loggeada en audit_events con su canal de origen (appointment_source incluye los valores manual · assistant · web · phone · import). “¿Quién canceló ese turno?” se responde leyendo logs, sin ambigüedad.

La matriz exhaustiva — 113 acciones catalogadas con tres columnas de estado (dashboard, tool n8n legacy, tool TS target) y permiso requerido — vive en el Anexo 06a_MATRIZ_ACCION_TOOL_v3.md. Es el contrato vivo del producto. Cada feature nueva (sea UI o conversacional) agrega fila a la matriz antes de tocar código.


Los canales de entrada

El agente recibe mensajes por dos canales distintos. Cada uno tiene reglas propias.

Telegram es el canal de prueba. Crear un bot de Telegram (nomenclatura oficial de la Bot API) es gratis, no requiere verificación, el API es simple. Sirve para validar flujos durante desarrollo, para pruebas internas del equipo, y para stress testing. No tiene sentido para clientes reales en Argentina — nadie usa Telegram para gestionar turnos. El adapter está implementado en src/backend/channels/telegram/ con HMAC verify, dedup por update_id de Telegram, persistence completa en channel_events / customers / customer_channel_identities / conversation_threads / messages, y helper outbound sendTelegramTextMessage para Bot API.

WhatsApp Business API es el canal de producción. Requiere número de teléfono dedicado, cuenta Meta Business verificada, y aprobación de Meta. El adapter está implementado en src/backend/channels/whatsapp/ con HMAC verify (X-Hub-Signature-256), dedup por wamid, persistence completa, y helper outbound sendWhatsAppTextMessage para Graph API.

WhatsApp tiene dos restricciones que definen el diseño del agente:

  • Ventana de veinticuatro horas. Si el cliente no escribió en las últimas veinticuatro horas, no se le puede mandar texto libre. Sólo se pueden enviar templates — mensajes pre-aprobados por Meta. Esto afecta cualquier recordatorio o reprogramación proactiva. Hay que definir y aprobar los cinco o seis templates necesarios antes del lanzamiento. Esto sigue pendiente al cierre de v3 del capítulo.
  • Costo por conversación. Meta cobra entre dos y cinco centavos de dólar por conversación según el tipo. Las conversaciones iniciadas por el cliente dentro de la ventana de 24 horas son gratuitas hasta las primeras mil al mes.

El sistema soporta que cada negocio tenga su propio número de WhatsApp (plan premium futuro) o que todos los negocios atiendan desde un número central (MVP). La arquitectura es la misma: cambia sólo la fila de channel_connections por tenant.

Ambos canales comparten el mismo flujo interno: llega un mensaje, se valida la firma del proveedor, se normaliza, se identifica al cliente vía customer_channel_identities, se resuelve el thread vía conversation_threads, se persiste el mensaje en messages + el evento en channel_events. Después del INSERT del mensaje inbound, el adapter dispara el runtime del agente via executionCtx.waitUntil() para fire-and-forget controlado — el patrón está descrito en detalle más abajo como P-09 Webhook → Agent dispatch con waitUntil.


El cerebro del agente

El modelo de lenguaje en este producto no es un chatbot libre. Es un router de intenciones y generador de respuestas naturales. Su trabajo es entender qué quiere el cliente, llamar la herramienta correcta (consultar disponibilidad, crear turno, cancelar), y formatear la respuesta en lenguaje natural. Nunca inventa datos. Nunca improvisa políticas. Si algo queda fuera de su scope, deriva al humano.

La arquitectura del agente tiene tres piezas:

El modelo. Distinto según el rol. Para el cliente (volumen alto, interacciones simples) se usa un modelo barato y rápido — gpt-4o-mini en MVP, eventualmente Gemini Flash o Claude Haiku. Para el dueño (volumen bajo, instrucciones complejas) se usa un modelo más capaz — Claude Sonnet o GPT-4o. El costo estimado en el MVP es de aproximadamente dos dólares por mes por negocio, escalando linealmente con el volumen. Configurable por tenant en assistant_configs.provider + assistant_configs.model.

Las herramientas. Funciones tipadas que el modelo puede invocar. Cada una pide los parámetros mínimos necesarios, valida contra el schema, ejecuta la operación reusando un service backend de dominio, y devuelve un resultado estructurado. Son las únicas acciones que el agente puede tomar. Lo que no tiene herramienta para hacer, no lo hace. El catálogo formal de herramientas vive en el Capítulo 06 Pliego 1, y la matriz que las correlaciona con acciones del dashboard en el Anexo 06a.

La máquina de estados. Cada conversación tiene un registro de estado (conversation_state) con el intent actual y los slots ya recolectados. Esta máquina de estados es el guardaespaldas contra alucinaciones: antes de crear un turno, el sistema verifica que todos los slots requeridos estén completos. Si el modelo intenta llamar a una herramienta sin datos suficientes, la herramienta lo rechaza con un error claro.

El diseño tiene una implicancia importante: el modelo es reemplazable. Si mañana aparece un LLM mejor, se cambia el proveedor en la configuración del tenant (assistant_configs.provider) y listo. Si uno se cae, hay fallback al otro. El sistema no depende de un modelo específico.


Patrones del runtime del agente

El módulo del agente mergeado el 14/05 cristalizó dos patrones arquitectónicos que son cimientos del sistema y reusables para futuros agentes o canales. Cada patrón resuelve un problema concreto que apareció durante la construcción del runtime y tiene una implementación canónica en tomillo/perla.

P-09 · Webhook → Agent dispatch con waitUntil

Qué problema resuelve. Cuando un webhook entrante (WhatsApp, Telegram, Instagram, futuros canales) tiene que disparar un agente conversacional, NO se puede disparar dentro de la transacción que persiste el mensaje, ni síncrono antes de devolver el ACK al proveedor.

  • La transacción que persiste messages todavía no committeó cuando se está procesando; si el agente abre una conexión separada para leer ese message, no lo va a ver por aislamiento transaccional.
  • Meta y Telegram cortan el webhook a los cinco o diez segundos si no reciben 200 OK rápido. Si el dispatch corre síncrono, el ACK se demora y el proveedor reintenta o invalida la conexión.

La solución. Cloudflare Workers expone executionCtx.waitUntil(promise) para mantener vivo el Worker después del response hasta que la promesa termine. Es la primitive correcta para fire-and-forget controlado.

Cómo se materializa en el código:

  • El service que persiste el mensaje (processInboundWhatsAppMessage, processInboundTelegramMessage) devuelve discriminated union con el messageId y el identifier del remitente.
  • El webhook handler agregador acumula los procesados en un array y los expone como inboundDispatchTargets en el resultado.
  • El route Hono itera sobre los targets DESPUÉS de obtener el result del service y ANTES de devolver el c.json, encolando cada uno via helper scheduleAgentDispatchForInboundTarget.
  • El helper de schedule defiende la ausencia de executionCtx en entornos de tests (Node standalone) — si c.executionCtx?.waitUntil no existe, se ignora silenciosamente.
  • El dispatch helper backend-side (src/backend/agent/dispatch.ts) abre su propia conexión a DB porque corre post-response, compone deps, resuelve audiencia, corre el run, cierra DB en finally.

Boundary arquitectónico. El dispatch helper vive en src/backend/agent/dispatch.ts, NO en src/api/routes/. Respeta la decisión del Cap 1: src/api/ consume src/backend/, nunca al revés. Las rutas Hono importan el helper del módulo del agente, no implementan dispatch ahí.

Anti-patterns identificados:

  • Disparar dentro de la transacción del INSERT del message → run falla porque no ve la fila todavía.
  • Disparar síncrono fuera de transacción pero antes del c.json → demora el ACK al proveedor, hay reintentos o invalidación.
  • Olvidar el .catch en la promise pasada a waitUntil → el error tira al runtime sin log estructurado.
  • Importar el dispatch desde la layer API hacia el backend → invierte el boundary correcto.

Reusabilidad. El patrón aplica a cualquier integración futura del estilo “webhook entrante → procesamiento async tras ACK”. Se replica en Instagram cuando se agregue el canal, en webhooks de Mercado Pago si se necesita post-processing async, en cualquier proveedor con SLA estricto de ACK.

P-10 · Audience Role como propiedad runtime del agente

Qué problema resuelve. Cuando un agente conversacional tiene que responder distinto según quién le escribe (cliente que reserva un turno vs dueño que gestiona la agenda), no basta con un solo system prompt y un solo set de tools. Hay que cablear el audience role como propiedad load-bearing del runtime.

  • El cliente NO debe ver tools de gestión interna (list_conversations, take_conversation). Si la tool aparece en la definition que se le pasa al LLM, el modelo la puede invocar — y la ejecución mostraría data cross-customer.
  • El owner necesita un prompt distinto: lenguaje de gestión, no de “te ayudo con tu turno”.
  • El LLM puede alucinar nombres de tools fuera del set ofrecido. El runtime tiene que defender el lookup, no confiar en que el LLM solo invoque lo declarado.
  • Sin audiencia explícita, el system prompt termina diciendo “el cliente” en contextos donde el remitente es el dueño, y la voz queda fuera de lugar.

Cómo se materializa en el código:

  • Tipo cerrado en el módulo del agente: agentAudienceRoleSchema = z.enum(['customer', 'owner', 'staff']).
  • Field opcional en cada tool: allowedAudienceRoles?: AgentAudienceRole[]. Sin declaración → tool pública (compat backward). Con declaración → filter activo.
  • Constantes nombradas para reusar: ALL_AUDIENCE_ROLES, OWNER_AND_STAFF_AUDIENCE_ROLES. Nunca string literals sueltos en cada tool.
  • Registry filtra: la función list(audienceRole?) del registry de tools devuelve solo aquellas cuyo allowedAudienceRoles incluye al rol del run actual. Tools sin declaración explícita quedan incluidas siempre.
  • System prompt branchea por rol manteniendo voz común y guardrails compartidos. Customer: “estás hablando con un cliente”. Owner: “estás hablando con el dueño, tiene acceso a la agenda completa”. Staff: subset de owner con escalation explícita.
  • Runtime propaga + defiende. El service del run extrae el audienceRole del input, lo pasa al prompt builder Y filtra el registry. Guarda allowedToolNames: Set<string> para validar cada tool call que devuelva el LLM. Si el LLM invoca una tool fuera del set permitido, no se ejecuta — el runtime devuelve { error: 'tool_not_allowed_for_audience' } al conversation, no persiste la tool_invocation, y el LLM ajusta en el siguiente turn.
  • Resolución del rol desde el identifier del remitente: función resolveAudienceRole({ businessId, identifier }, lookup) con precedencia owner > staff > customer. En WhatsApp el identifier es el teléfono normalizado; en Telegram es el telegram_user_id (con la limitación documentada: hoy solo resuelve customer porque la columna users.telegram_user_id no está modelada para staff/owner).

Refinamiento post-merge del 14/05. Tomi agregó dos ajustes que entraron en commits posteriores al PR original:

  • Default effectiveAudienceRole = 'customer' cuando el input del run no trae audienceRole. Antes el default era undefined que mantenía comportamiento legacy (sin filter). El cambio es de seguridad por default: cuando no hay audiencia explícita, se asume el rol con menos privilegios.
  • audienceRole propagado al AgentToolExecutionContext. Cada tool ahora recibe el rol del remitente en su context de ejecución. Esto habilita filter interno por audiencia dentro de la tool (por ejemplo, list_appointments con audienceRole='customer' devuelve solo los appointments del propio customer; con audienceRole='owner' devuelve todos los del business).

Decisión derivada. Las acciones high-risk (cancelación masiva, mutaciones del owner sobre customers sin pedido del customer) no se autorizan solo con el filter de audiencia — pasan también por el flujo de pending_operations con confirmación + token + TTL. El filter de audiencia es un guardrail, no el único. Ver Cap 6 Pliego 5 y Hito 3 del Cap 7.

Anti-patterns identificados:

  • Filtrar por rol solo en el prompt (“no uses list_conversations si sos customer”) → el LLM lo ignora si la tool está disponible en su tool list.
  • Hacer el rol obligatorio en el input del run → rompe callers existentes; mejor opcional con default que sea seguro (customer).
  • Hardcodear arrays ['customer', 'owner', 'staff'] en cada tool → usar las constantes nombradas.
  • Resolver el rol con string match exacto del phone → usar normalizePhone que strip non-digits, los formatos varían entre proveedores.

Reusabilidad. El patrón aplica a cualquier agente conversacional multi-rol (no necesariamente multi-tenant). Si en el futuro hay un agente que atiende admin / vendedor / cliente o paciente / médico / secretaria, el shape es el mismo: tipo cerrado, filter en registry, defensa runtime post-LLM, system prompt branched.

Origen de los patrones: build del runtime del agente en tomillo/perla, mergeado el 14/05. Para implementación referencia ver el código del repo. Para destilación operativa ver memorias feedback_webhook_agent_dispatch_pattern y feedback_audience_role_pattern del knowledge base de Seba.


El dashboard al cierre de v3

El dashboard web es, junto al agente, la segunda superficie de contacto del producto. Al 2026-05-03, después de los redesigns mergeados en PRs #28 y #30 de Tomi, el dashboard tiene once rutas principales maquetadas funcionalmente sobre datos simulados:

Ruta Sección Estado
/app/ Home — panorama del día, métricas, próximo turno, vacantes Maqueta funcional
/app/schedule Agenda — vistas day/week/month, filtro por profesional Maqueta funcional
/app/customers Clientes — búsqueda, perfil con tabs (turnos/conversaciones/pagos) Maqueta funcional (sección nueva — D-01 cerrada)
/app/messages Mensajes — bandeja, chat, intervención humana, audio + attachments Maqueta funcional
/app/account Datos del usuario Maqueta funcional
/app/profile Perfil del miembro Maqueta funcional
/app/settings/* 10 sub-páginas (general · locations · team · services · availability · resources · policies · notifications · assistant · billing) Maqueta funcional

Estado de integración con el backend: los tres endpoints platform existen y son consumidos (bootstrap, current-tenant, current-membership). El resto del dashboard vive sobre mocks. La integración con la API real se habilita cuando los services backend de dominio existan (uno por familia: src/backend/customers/, src/backend/services/, src/backend/staff/, src/backend/appointments/, etc.) — esos services son los mismos que las tools del agente del Cap 7 van a consumir.

Sobre la identidad visual del dashboard

Tomi definió, en el archivo design.md del repo, una dirección visual: preset shadcn Luma · base color Mist · accent Teal · tipografías IBM Plex Sans (headings) + Instrument Sans (body) · filosofía “calma, control, premium sobrio, operativo, no SaaS genérico”.

Esto cubre pragmáticamente la necesidad de tener una dirección visual coherente mientras Tomi trabaja solo. Pero no es la identidad visual definitiva del producto. El Brand Guide de Perla (perla-brand-guide.pages.dev) define un sistema distinto: DM Serif Display + DM Sans + paleta perla propia (teal #2D7A7A, arena #A88760, violeta #5B4B7A) + gradient iris + motion tokens específicos.

El choque es real, no semántico. Hay dos documentos vigentes con tipografías diferentes y paletas diferentes. La decisión sobre cuál queda como canonical para el dashboard de producción es D-05 del Anexo 06a, abierta. Mientras tanto, el capítulo registra el estado actual sin cementarlo.

Convenciones del repo y sistema de gobierno IA

El repo tiene un sistema de gobierno multi-capa pensado para que humanos y agentes IA (Claude Code, Codex) operen sobre el código respetando las mismas reglas:

  • AGENTS.md raíz lista las reglas globales no negociables: verificar antes de afirmar, nunca pushear directo a main, nunca mergear PRs, naming técnico inglés, commits en español Conventional Commits, contenido GitHub en español, i18n obligatorio para textos visibles, prioridad shadcn/ui antes de UI custom, secrets nunca en código.
  • docs/ia/politica-*.md son las siete políticas detalladas por dominio: politica-git-github.md, politica-frontend.md, politica-backend-api.md, politica-deploy-cloudflare.md, politica-seguridad.md, politica-naming.md, herramientas-locales.md. Cada política tiene reglas operativas específicas y mecanismos de verificación.
  • AGENTS.md anidados por subárbol en src/api/, src/backend/, src/transport/, src/components/, src/routes/, src/lib/db/, .github/workflows/. Cada uno apunta a las políticas raíz relevantes y lista las reglas locales que aplican al subárbol.
  • CLAUDE.md raíz importa AGENTS.md con @AGENTS.md y suma recordatorios críticos para Claude Code que se repiten a propósito porque no deben quedar ocultos por imports.
  • .claude/rules/*.md son wrappers que apuntan a los AGENTS.md anidados con @../../<path> cuando una regla específica por path importa para Claude Code.

Este sistema es un activo del proyecto. Cuando se incorpore alguien al equipo (sea humano o agente IA), las reglas con las que va a trabajar viven en el repo, son explícitas, y se actualizan en el mismo PR que el código que las afecta. La regla de oro: las reglas locales no deben duplicar políticas completas — la fuente de verdad vive en docs/ia/* y los archivos locales solo refuerzan reglas del subárbol.


Procesos en segundo plano

No todo es pregunta-respuesta. Hay cosas que pasan solas.

Cada 5 minutos   → expirar holds vencidos (libera schedule_blocks)
Cada 30 minutos  → revisar turnos próximos y mandar recordatorios programados
Cada hora        → cerrar inbound_message_batches abandonados
8:00 AM L-V      → enviar resumen del día al dueño
Cada 15 minutos  → reintentar notificaciones fallidas en notification_logs
Diario 3:00 AM   → limpiar sesiones inactivas, expirar pending_operations

Todos estos procesos corren como Cron Triggers de Cloudflare. El Worker se enciende a la hora indicada, ejecuta la tarea, se apaga. Sin servidor corriendo en background.

Estado actual: ningún cron está configurado en wrangler.jsonc. Es parte del orden de convergencia post-MVP del agente, no bloquea los PRs del Cap 7.

El detalle importante: las notificaciones proactivas (recordatorios, reprogramaciones) caen fuera de la ventana de 24 horas de WhatsApp la mayoría del tiempo. Por eso se mandan como templates pre-aprobados. Los templates se diseñan una vez, se aprueban con Meta, y después el sistema los rellena con los datos del turno (la tabla scheduled_notifications tiene columnas dedicadas para los snapshots de variables: template_customer_name, template_business_name, template_service_name, template_staff_name, template_appointment_start_at).

Los más críticos para el MVP:

  • Recordatorio de turno (24h antes, 2h antes)
  • Reprogramación proactiva (cuando el dueño cancela)
  • Solicitud de seña (cuando se active el flujo de Mercado Pago)
  • Confirmación inicial tras agendar
  • Mensaje de bienvenida tras onboarding

Seguridad y observabilidad

La seguridad está en tres capas:

En la base de datos. RLS activado en las 57 tablas, con políticas que sólo permiten a miembros de un negocio ver sus propios datos. Trigger enforce_tenant_reference como segunda línea sobre todas las FKs cruzadas. Secretos nunca en el código — van como variables de entorno cifradas en Cloudflare, vía wrangler secret put. Los channel_connections guardan referencias a secretos (credential_ref, webhook_secret_ref), no tokens crudos.

En el código de aplicación. Cada query pasa por funciones que exigen un business_id explícito. No hay query cruda al Postgres. Los endpoints del dashboard exigen token válido de Supabase Auth. Los webhooks de mensajería verifican firma del proveedor (Meta firma con HMAC-SHA256, Telegram con X-Telegram-Bot-Api-Secret-Token). El sistema de permisos granular de la Familia 7 valida cada acción contra el permission_code requerido (ver Cap 6a).

En el producto. El agente siempre confirma antes de ejecutar acciones destructivas (cancelar, reprogramar, cobrar). Nunca cancela a ciegas, nunca agenda sin confirmación explícita. La máquina de estados (conversation_state.flow_status) impide saltos de paso. Las acciones de nivel L3/L4 exigen el patrón preview/execute con pending_operations + token con TTL — descripto en el Pliego 5 del Capítulo 6 e implementado en DB al cierre de v3 del capítulo.

La observabilidad se apoya en tres herramientas y una convención:

  • Cloudflare Analytics captura automáticamente requests, errores y latencia en ambos Workers. Gratis, viene con Workers. Ya está activado en wrangler.jsonc con 10% de sampling.
  • Supabase Dashboard muestra el estado de la base, las queries lentas, las conexiones activas.
  • audit_events + audit_event_changes del schema (Familia 5) registran cada acción importante con actor, entidad afectada, request_id, IP, user_agent, summary. Permiten responder “¿quién hizo qué cuándo?” sin recurrir a logs de aplicación.
  • Logs estructurados en JSON que el código escribe en cada request: identificador de negocio, de sesión, intent detectado, herramientas invocadas, tiempo de respuesta. Cuando algo se rompe, el log cuenta la historia completa. Estado actual: parcialmente implementado en los adapters de canales; pendiente formalizar en el módulo agente cuando se implementen los PRs del Cap 7.

Hay también un canal de alertas previsto: si el agente no responde, si el error rate sube, si la base de datos pierde conexión — un servicio auxiliar manda un mensaje por Telegram al equipo. Estado actual: no implementado todavía. Aparece en Fase B del Cap 7 (logs estructurados + alertas).


Cómo se deploya

A diferencia de la versión 2 del capítulo, hoy el repo tiene CI/CD completo activo:

commit a perla/<feature>
    ↓
Pull Request contra main
    ↓
GitHub Actions (preview-web.yml) deploya Worker preview perla-web-pr-{N}.workers.dev
    ↓
Bot comenta la URL del preview en el PR
    ↓
Review + merge a main
    ↓
GitHub Actions (deploy-staging-web.yml) deploya a staging.holaperla.com
    ↓
Validación manual mínima (smoke test del cambio)
    ↓
Workflow manual "Deploy production web" promociona a holaperla.com

Cuatro workflows configurados en .github/workflows/:

  • preview-web.yml — Deploya Worker preview por cada PR. Se ejecuta en pull_request con tipos opened, synchronize, reopened, closed. Usa pnpm --frozen-lockfile, valida secrets VITE_SUPABASE_URL + VITE_SUPABASE_PUBLISHABLE_KEY, builda con pnpm run build, deploya con npx wrangler deploy --config wrangler.frontend.jsonc --name perla-web-pr-{N}. Comenta la URL workers.dev en el PR.
  • deploy-staging-web.yml — Deploy automático a staging.holaperla.com en cada push a main.
  • deploy-production-web.yml — Deploy manual a holaperla.com y www.holaperla.com. Política explícita: producción nunca sale automáticamente, requiere aprobación.
  • AGENTS.md local — reglas para tocar workflows desde IA: no exponer secrets, mantener flujo de preview consistente, respetar permisos mínimos.

Lo que falta para CI/CD seria de producción: tests automatizados con Vitest en el Worker API (hoy hay solo 1 archivo de test: platform-request-scope.test.ts + el del PR #1 del Cap 7), un step de pnpm typecheck antes de cada deploy. La política de Backend/API del repo exige TDD para “bugs, lógica de negocio, validaciones, permisos, billing, agenda, contratos API y servicios backend” — el cumplimiento está pendiente de los próximos PRs.


Estado actual del sistema constructivo (snapshot 2026-05-03)

Lo que está en pie:

  • Infraestructura Cloudflare (2 Workers separados + Hyperdrive configurado para staging y production) + Supabase con tres ambientes funcionando.
  • Schema de base de datos: 57 tablas + 50 enums organizados en 7 familias (Cap 4 v3 + Anexo 04a v3), con RLS y triggers de aislamiento en todas las relaciones cruzadas.
  • Dashboard con once rutas principales maquetadas funcionalmente (Home · Agenda · Customers · Messages · Account · Profile · Settings con 10 sub-páginas) sobre datos mock.
  • CI/CD activo con preview por PR + staging automático + producción manual.
  • Sistema de permisos granular completo con 19 permission codes + 4 permission sets system pre-cargados (Familia 7).
  • Política de operaciones sensibles aprobada y materializada en DB (Familia 5: pending_operations + items + tool_invocations.requires_confirmation + audit_events).
  • Adapters de canal Telegram y WhatsApp completos (Pliego 3 cerrado) con HMAC, dedup, persistence, helpers de outbound.
  • Mercado Pago integrado para pagos del cliente final del negocio (señas).
  • Sistema de gobierno IA del repo (AGENTS.md raíz + 7 políticas + AGENTS.md anidados + CLAUDE.md + .claude/rules/).
  • 3 endpoints platform (/api/v1/platform/*) listos: bootstrap, current-tenant, current-membership.
  • Dominio cerrado: holaperla.com (Tomi compró 02/05).

Lo que está vivo al cierre de v4 (cumplido entre v3 y v4):

  1. Módulo agente mergeado en tomillo/perla:main el 14/05. Runtime, adapter LLM, system prompt compuesto desde defaults (lectura de assistant_configs pendiente — ver “Lo que falta” abajo), loop de invocación de tools.
  2. Tools del Pliego 1 (lectura + escritura): las ocho del MVP owner con lógica real, no stubs — get_services, get_staff, list_appointments, create_appointment, cancel_appointment, reschedule_appointment, list_conversations, take_conversation.
  3. Disparo desde webhooks WhatsApp y Telegram al runtime del agente vía executionCtx.waitUntil (P-09).
  4. Audience role como propiedad runtime (P-10): customer / owner / staff cableado con filter de tools + defensa post-LLM + system prompt branched.
  5. Dataset piloto cargado en perla-testing el 14/05 — fixture coherente para validación end-to-end.

Lo que falta (cubierto por el roadmap del Cap 7):

  1. Inyección de assistant_configs al system prompt. El schema está completo; falta que buildSystemPrompt() del módulo del agente lea la fila del business y arme el prompt con name, greeting_message, response_tone, business_instructions, safety_instructions. Hito 5 de Fase A.
  2. Tool create_payment_link + cableado hold → confirmado por webhook MP. El módulo Mercado Pago del backend existe; falta exponerlo como capability del agente. Hito 2 de Fase A.
  3. Nueve tools del mapeo 1:1 dashboard ↔ WhatsApp: alta/baja/edición de customers, services, staff_members + modificación de horarios del local + edición de FAQs/políticas. Hito 4 de Fase A.
  4. request_handoff + bandeja en dashboard: capability central del agente (escalación a humano), no fallback. Hito 4 de Fase A.
  5. Campo source en appointments (perla / manual / importado / gcal) — pendiente con Tomi según Cap 13 del MVP_MANUAL.
  6. Orquestación de pending_operations end-to-end (preview + token + execute). Hito 3 de Fase A.
  7. Conexión dashboard ↔ API: reemplazar mocks de las once rutas por fetch real cuando los services backend de cada module existan. Trabajo de Nahue + Tomi en paralelo a Fase A.
  8. Batching de mensajes entrantes con inbound_message_batches. Fase B.
  9. Cron handlers: expirar holds + recordatorios proactivos + resumen diario + reintentos. Configuración de triggers.crons en wrangler.jsonc. Fase B.
  10. STT (speech to text): handler que use Whisper o equivalente para audios entrantes. Schema soporta (audio_transcripts + audio_transcript_segments). Fase B.
  11. Templates de WhatsApp: diseñar y aprobar con Meta los cinco a seis templates críticos para recordatorios y notificaciones fuera de la ventana de 24 horas. Fase B (trámite Meta tarda — conviene iniciar en paralelo a Fase A).
  12. Tests + CI completo: Vitest, fixtures, typecheck obligatorio en CI.
  13. Logs estructurados + alertas: JSON con contexto, canal Telegram para alertas. Fase B.

Decisiones cerradas al cierre de v3:

ID Decisión Cerrada
D-01 Clientes y Mensajes como secciones separadas 2026-04-24, confirmada en repo 2026-05-03
(sin ID) Vocabulario customer en código + paciente en agente y manual (vertical lock salud + bienestar) 2026-05-03
(sin ID) Permisos granulares con sistema de 4 perfiles humanos + permission sets 2026-05-03 (cierra el gap SG-6 del Anexo 04a)

Decisiones pendientes (detalle en Anexo 06a v3): D-02 replanteada con sistema de permisos · D-03 colores de staff configurables · D-04 personalidad del agente (3 presets de Tomi vs 4 voces del Brand Guide) · D-05 identidad visual del dashboard · D-06 batch cancel/reschedule UI.


Orden sugerido de convergencia (al cierre de v4)

Los pasos del orden de convergencia están reordenados según el avance real del repo al 16/05. Lo que ya está hecho se marca con ✅; lo que está en curso con 🟡; lo que falta con ❌:

  1. ✅ Sistema de permisos granular + public_code en businesses (cubre los gaps SG-6 y SG-1 del Anexo 04a v1).
  2. ✅ Adapter Telegram completo.
  3. ✅ Adapter WhatsApp completo.
  4. ✅ Schema de base de datos siete familias completas con RLS y triggers.
  5. ✅ Dashboard maquetado con once rutas (D-01 cerrada con /app/customers separado).
  6. ✅ CI/CD completo (preview + staging + production).
  7. ✅ Tres ambientes Supabase parametrizados.
  8. ✅ Mercado Pago backend para señas (preference + webhook + HMAC).
  9. ✅ Sistema de gobierno IA del repo (AGENTS.md + políticas + .claude/rules).
  10. ✅ Módulo agente completo: runtime, adapter LLM (gpt-4o-mini por default vía LLM_* env vars), system prompt compuesto desde defaults, loop de invocación de tools, persistencia de agent_runs y tool_invocations (merge del 14/05).
  11. ✅ Las ocho tools del Pliego 1 con lógica real (no stubs) — get_services, get_staff, list/create/cancel/reschedule_appointment, list_conversations, take_conversation.
  12. ✅ Disparo del runtime desde webhooks WhatsApp + Telegram vía executionCtx.waitUntil (patrón P-09 — ver sección “Patrones del runtime del agente”).
  13. ✅ Audience role como propiedad runtime con filter de tools + defensa post-LLM + system prompt branched (patrón P-10).
  14. ✅ Dataset piloto cargado en perla-testing (1 business, 6 staff, 5 servicios, 35 reglas de disponibilidad).
  15. 🟡 Inyección de assistant_configs al system prompt (schema listo, falta plumbing).
  16. 🟡 Mercado Pago como capability del agente (create_payment_link + cableado hold → confirmado por webhook).
  17. ❌ Nueve tools del mapeo 1:1 dashboard ↔ WhatsApp (CRUD customers + services + staff + horarios + FAQs/políticas).
  18. ❌ Tool request_handoff + bandeja del dashboard.
  19. ❌ Campo source en appointments (pendiente con Tomi).
  20. ❌ Orquestación end-to-end de pending_operations (preview + token + execute).
  21. ❌ Conexión real del dashboard a los services backend (paralelo a Fase A).
  22. ❌ Apagado del agente n8n legacy (cierre operativo de Fase A).
  23. ❌ Onboarding por business real con teléfonos en users.phone para resolver staff/owner.
  24. ❌ Producción en holaperla.com con monitoreo y rollback path documentado.
  25. ❌ Batching de mensajes entrantes con inbound_message_batches (Fase B).
  26. ❌ Cron handlers para recordatorios proactivos + templates Meta aprobados (Fase B).
  27. ❌ STT con Whisper para audios entrantes (Fase B — existe en prototipo n8n, falta portar).
  28. ❌ Tests + typecheck obligatorio en CI, logs estructurados con alertas (Fase B).

Qué incluye este capítulo

El stack completo de producción, la separación entre prototipo y producción, los tres ambientes Supabase, la estructura de la base de datos en 7 familias, los canales de entrada implementados, la arquitectura del agente (con su roadmap operativo en Cap 7), el estado del dashboard al 03/05, la identidad visual pendiente, los procesos en segundo plano previstos, el modelo de seguridad y observabilidad, el sistema de gobierno IA del repo, el CI/CD activo, el orden actualizado de convergencia, y las decisiones cerradas y pendientes.

Lo que este capítulo NO incluye

  • La descripción funcional del producto (capítulos Programa, Circulaciones e Instalaciones).
  • El modelo de datos tabla por tabla (Cap 4 v3 + Anexo 04a v3).
  • El diseño de cada herramienta del agente con firma completa (Cap 6 Pliegos).
  • La matriz exhaustiva acción↔tool (Anexo 06a v3).
  • El roadmap operativo del agente con los quince PRs ordenados (Cap 7).
  • La asignación de responsabilidades en el equipo y el cronograma (viven en PM.md + PM/SISTEMA.md, fuera del manual desde abril 2026).
  • El detalle de implementación: queries específicas, configuración de Wrangler, ejemplos de código. Eso vive en 01_INFRA/ y en el repositorio mismo.

Por qué este capítulo importa

Las decisiones de stack determinan qué es posible y qué no, cuánto cuesta operar el sistema, y qué tan rápido podemos iterar. Tener este capítulo escrito evita que las decisiones técnicas se tomen sobre la marcha en un chat de WhatsApp, y permite que cualquier nuevo integrante entienda en una sentada cómo está armada la cocina. También es el capítulo que permite sostener una conversación profesional con clientes empresariales cuando pregunten “¿dónde vive la data?” o “¿qué pasa si se cae?” — no hay que improvisar la respuesta, está escrita.

Y, desde la versión 2, es el capítulo que cementa el principio de cierre como ley del producto: el dashboard y el agente no son dos aplicaciones que comparten base de datos — son dos canales de acceso al mismo conjunto de capacidades, sostenidas por la misma capa de services backend. El que no respete ese principio introduce fragilidad al sistema y hace imposible sostener la promesa operativa del Capítulo 02.

La v3 del capítulo agrega una observación más: el repo ya está listo para que el agente entre. Las siete familias de tablas están en su lugar, el sistema de permisos cierra el gap más sensible, los adapters de canal reciben mensajes y los persisten en las tablas correctas, los workflows de CI/CD permiten validar cada cambio sin tocar producción, las políticas de IA del repo aseguran que cualquier agente o desarrollador que entre respete las mismas reglas. El edificio está terminado a nivel infraestructura. Lo que falta es la última pieza — el cerebro del agente — y el orden para construirla en quince movimientos coordinados, descritos en el Cap 7.


v1 — 2026-04-16 · primera versión. v2 — 2026-04-24 · actualización post-commits Tomi 14-23/04 + Anexos 04a y 06a. v3 — 2026-05-03 · regeneración mayor del estado al 03/05. Sumadas dos secciones (3 ambientes Supabase · sistema de gobierno IA del repo). CI/CD activo confirmado. Orden de convergencia actualizado con avance real (9 hitos cerrados de 18). D-01 confirmada cerrada. Vocabulario customer/paciente cerrado. Sistema de permisos cerrado. Próxima edición cuando los PRs del Cap 7 cierren y el snapshot de “lo que falta” se reduzca.

06 Pliegos de obra
06

Pliegos de obra

¿Cómo hablan las piezas entre sí?


Este capítulo describe los pliegos del proyecto: los contratos formales que definen cómo se comunica cada parte del sistema con las demás. Qué herramientas tiene disponibles el agente, qué inputs recibe cada una, qué outputs devuelve, qué errores puede levantar. Cómo está escrito el system prompt que le da personalidad y límites. Cómo se traduce un mensaje de WhatsApp al lenguaje interno del sistema. Qué pasa cuando algo falla. Cuándo una operación destructiva exige confirmación técnica además de la conversacional.

En obra, los pliegos son los documentos que definen con precisión cómo se ejecuta cada parte de la construcción para que distintos oficios puedan colaborar sin malentendidos. El plomero no tiene que leer los planos de estructura para saber qué caño necesita; lee el pliego sanitario. En Perla cumplen la misma función: son los contratos que permiten que el agente, las herramientas, los adapters de canal y el manejo de errores se construyan y evolucionen por separado sin que se rompa la coherencia del conjunto.

Una nota importante sobre alcance. Los pliegos son contratos, no implementación. Este capítulo define la forma de la conversación entre componentes, no cómo está escrito el código de cada uno. La implementación vive en el repositorio de producción. La voz de marca — los textos exactos que lee el cliente — vive en el Brand Guide. Acá solo definimos la arquitectura de los acuerdos.


Los seis pliegos

Perla tiene seis pliegos formales, y cada uno resuelve una pregunta distinta:

# Pliego Pregunta que responde Estado al cierre v3
1 Contrato de tools ¿Qué puede hacer el agente? Especificado · 8 tools del MVP en n8n · 0 implementadas en TS · roadmap en Cap 7
2 System prompt ¿Quién es el agente y qué límites tiene? Especificado · enriquecido con la estructura completa de assistant_configs
3 Adapters de canal ¿Cómo se traduce un mensaje al lenguaje interno? Implementado · Telegram + WhatsApp en src/backend/channels/
4 Política de errores y fuera de scope ¿Qué pasa cuando algo falla o excede al agente? Especificado · sin cambios mayores
5 Protocolo de acciones destructivas ¿Cómo se confirma algo que no se puede deshacer? Implementado en DB · pending_operations + items + token + TTL
6 Confirmación conversacional vs interna (nuevo en v3) ¿Cuándo basta el “sí” del cliente y cuándo hace falta token técnico? Especificado · materializa la política aprobada en docs/runtime/perla-sensitive-operations-policy.md

Los seis se tienen que leer juntos porque se refuerzan entre sí: un contrato de tools sin un system prompt que lo cite queda huérfano; un system prompt sin política de errores lleva al agente a improvisar cuando las tools fallan; un protocolo de acciones destructivas sin adapters que lo respeten se rompe en el primer mensaje confuso; el Pliego 6 explica cuándo cada uno aplica.


Pliego 1 — El contrato de tools

Las tools son la única superficie por la cual el agente actúa sobre el sistema. Si no hay tool para algo, el agente no lo puede hacer — por diseño. Esta restricción es deliberada: todo lo que el agente invoca está tipado, validado y auditable. Lo que no tiene tool es lo que el agente no tiene permitido.

Principios del contrato

El agente no sabe SQL, no sabe canales, no sabe adapters. Solo sabe invocar funciones tipadas con parámetros definidos. La implementación interna de cada tool puede cambiar — consultar otra tabla, usar otra API, cambiar de base de datos — sin que el agente se entere. El contrato es la interfaz.

Cada tool pide los inputs mínimos necesarios. Si falta un dato requerido, la tool rechaza la invocación con un error claro antes de ejecutar. Esto evita que el agente llame a create_appointment con fecha pero sin profesional y el sistema improvise.

Los identificadores son opacos. Toda tool que reciba un identificador como entrada recibe un UUID real (cadena larga sin significado humano), nunca un nombre o una descripción. El agente solo puede obtener UUIDs como salida de otra tool — no los inventa.

Cada tool valida multi-tenancy. Antes de cualquier operación, la tool verifica que los identificadores recibidos pertenezcan al tenant del contexto actual. Un agente en modo cliente de Suri que reciba accidentalmente un UUID de Pepito obtiene un error, no datos cruzados. La validación se sostiene en dos líneas de defensa: la lógica del service backend + el trigger enforce_tenant_reference de la base.

Cada tool reusa un service backend de dominio. La regla del Cap 7 aplicada al Pliego 1: la tool del agente es un thin wrapper sobre src/backend/<dominio>/service.ts. La lógica de negocio nunca vive dentro de src/backend/agent/tools/. Esto garantiza que el dashboard real (cuando reemplace los mocks) y el agente operen sobre la misma capa de servicios. Es el principio de cierre del Cap 5 materializado en el Pliego 1.

Cada tool requiere un permiso del catálogo. Los 19 permission codes de la Familia 7 (Anexo 04a v3) determinan qué membership puede invocar qué tool. La columna Permiso requerido de la matriz Acción↔Tool (Anexo 06a v3) es el contrato de autorización. Si un OL con permission_set professional intenta cancel_appointment, la tool falla con permission_denied antes de ejecutar.


Sección A — Tools del paciente

Las herramientas disponibles cuando el agente atiende a un paciente son de lectura y escritura limitada, siempre sobre el propio registro del paciente:

  • get_services — devuelve la lista de servicios activos del tenant, con sus variantes (duración, precio, buffer), categorías y descripciones cortas. Permiso: customers.read (implícito al ser paciente del tenant).
  • get_staff_availability — recibe un servicio elegido y devuelve los profesionales que lo ofrecen junto con su disponibilidad en rangos (no slots individuales). Permiso: implícito.
  • create_appointment (L2) — crea el turno del paciente. Permiso: appointments.create. Implementa el patrón de hold para evitar dobles reservas (ver Cap 4 Familia 3 + Cap 4 entidad schedule_blocks).
  • get_active_appointments — devuelve los turnos futuros del paciente en el tenant actual. Permiso: implícito.
  • cancel_appointment (L2) — cancela un turno del paciente. Permiso: appointments.cancel (con check adicional de que el turno pertenece al paciente solicitante).
  • reschedule_appointment (L2) — cambia un turno a otro horario; operación atómica. Permiso: appointments.update.

Sección B — Tools del MVP owner (8 tools)

Las tools implementadas en el agente n8n actual (Perla_mvp_v2.2.json) y que el roadmap del Cap 7 va a portar al agente TS. La sección original de v2 listaba 6; v3 suma get_services y get_staff que el workflow agregó como helpers de resolución de UUIDs antes de crear turnos.

Las 8 tools están firmadas con detalle (parámetros, validaciones, errores posibles) en la versión 2 de este capítulo (_versions/06_PLIEGOS_v2_20260428.md). v3 mantiene la firma sin cambios — el contrato técnico sigue siendo el mismo. Resumen tabular:

Tool Nivel Permiso requerido Notas
list_appointments L1 appointments.read Filtra por rango de fechas, opcionalmente por staff.
create_appointment L2 appointments.create Implementa hold + check anti-overlap contra schedule_blocks.
cancel_appointment L2 appointments.cancel Marca cancelado, libera schedule_block, audit_event.
reschedule_appointment L2 appointments.update Operación atómica (rollback si falla).
list_conversations L1 messages.read Filtra por status (open / pending_handoff / in_handoff / resolved / archived).
take_conversation L2 handoffs.manage Crea handoff_request con kind = 'human_takeover'. Reservado a roles con permiso explícito.
get_services (nuevo respecto a v1) L1 (implícito al tener dashboard.access) Ayuda al agente a obtener UUIDs de variantes antes de create_appointment.
get_staff (nuevo respecto a v1) L1 (implícito) Ayuda al agente a obtener UUIDs de staff antes de create_appointment.

Nota sobre la decisión D-02. En la versión 2 del capítulo, las tools list_conversations y take_conversation quedaron reservadas al owner porque el manual no tenía sistema de permisos granulares. Con la Familia 7 cerrada (Anexo 04a v3), la decisión se replantea: las tools requieren messages.read y handoffs.manage respectivamente, no rol owner. Cualquier permission_set que incluya esos codes habilita la tool. Hoy el set system receptionist incluye messages.read + handoffs.manage; professional incluye solo messages.read. La granularidad cierra D-02 desde el lado técnico — la decisión de qué set asignar a quién queda con el owner del business.


Sección C — La separación por niveles de riesgo

Las tools se clasifican en cuatro niveles según el impacto de la operación:

Nivel Tipo Ejemplo MVP Tratamiento
L1 Lectura list_appointments, list_conversations Ejecución directa, sin confirmación.
L2 Escritura simple create_appointment, cancel_appointment, reschedule_appointment, take_conversation Preview conversacional: mostrar datos → pedir “¿Confirmás?” → ejecutar.
L3 Escritura múltiple bulk_cancel_preview + execute (Pliego 5) Preview obligatorio + token de confirmación con TTL.
L4 Destructivo masivo Cancelación de toda una semana Preview + delay + token con TTL + logging detallado.

Confirmación L2 vs L3+L4 es exactamente lo que el Pliego 6 (nuevo en v3) formaliza: para L2 alcanza con confirmación verbal del cliente en chat; para L3 y L4 hace falta confirmación técnica con token. La distinción está definida en docs/runtime/perla-sensitive-operations-policy.md del repo y aprobada para MVP.


Sección D — Tools fuera del MVP owner (pendientes)

Las siguientes tools están definidas en el manual pero no implementadas en el workflow actual ni en TS. Se documentan acá para que la implementación futura sea coherente con el contrato general:

  • block_staff_slot — bloqueo de disponibilidad de profesional. Crea un schedule_block con block_type = 'manual_block'.
  • bulk_cancel_preview / bulk_cancel_execute — cancelación en batch con token (Pliego 5).
  • bulk_reschedule_preview / bulk_reschedule_execute — reprogramación en batch.
  • add_service, update_service, deactivate_service — gestión de catálogo. Permiso services.manage.
  • add_staff_member, update_staff_availability, deactivate_staff_member — gestión de equipo. Permiso staff.manage.
  • update_assistant_config — modificar la configuración del agente desde el dashboard (todos los campos enumerados en el Pliego 2 abajo). Permiso settings.manage.
  • get_report — reportes agregados. Permiso dashboard.access.
  • reply_to_escalated_inquiry — respuesta a consultas derivadas. Resuelve el flujo del handoff_request con kind = 'escalated_question'.

Cuando el owner solicite alguna de estas acciones por WhatsApp, el agente responde honestamente que esa función todavía se gestiona desde el dashboard, y propone alternativa dentro de las 8 tools disponibles si la hay.

La matriz exhaustiva con permisos requeridos y estado de implementación vive en el Anexo 06a_MATRIZ_ACCION_TOOL_v3.md.


Pliego 2 — El system prompt

El system prompt es el documento que define quién es el agente y cómo se comporta. No es literatura — es la especificación operativa del modelo. Un system prompt mal escrito hace que el mejor modelo invente datos, repita pasos, o ejecute acciones sin confirmación. Un system prompt bien escrito hace que un modelo barato se comporte como un profesional.

En Perla hay dos system prompts distintos: uno para el agente cuando atiende a un cliente (paciente) y otro para cuando atiende al owner. Comparten estructura — misma anatomía, contratos equivalentes — y se diferencian en el alcance de las tools disponibles y el tono.

Lo importante de v3: el system prompt no es texto único. Se compone en runtime leyendo la fila de assistant_configs del business + el contexto de sesión resuelto por el routing. La tabla assistant_configs (Anexo 04a v3 Familia 5) tiene dieciséis campos relevantes:

  • name — nombre del asistente para ese business (“Perla”, “Sara”, “Leo”).
  • system_prompt — el prompt completo si el business lo customizó.
  • greeting_message — saludo customizable, separado del prompt.
  • response_tone — varchar 64, default 'warm'. Las 3 personalidades del settings/assistant del dashboard (professional / friendly / balanced) son atajos de UI que setean este campo.
  • response_style — texto libre con detalles de estilo.
  • business_instructions — políticas, horarios extra, contexto del negocio que el dueño quiere que el agente conozca.
  • safety_instructions — reglas anti-alucinación, derivar a humano cuando.
  • provider (openai · anthropic · custom) y model — qué LLM usa este business.
  • auto_reply_enabled — toggle global del agente.
  • handoff_enabled — el agente puede transferir al humano.
  • is_active — toggle global de actividad.
  • booking_tool_enabled, reschedule_tool_enabled, cancellation_tool_enabled, payment_link_tool_enabled — toggles per tool. El dueño puede apagar la tool de pagos si no usa Mercado Pago, sin tocar nada del agente.
  • max_tool_calls_per_run (default 8) — guardrail de runtime para evitar loops.
  • max_run_seconds (default 60) — timeout del run.

La capa de configuración por business que la versión 2 del capítulo asumía se materializa con esta tabla. No hace falta refactor — los PRs del Cap 7 leen esta tabla en prompt.ts y componen el prompt en runtime.

Anatomía del system prompt

El system prompt del agente tiene siete secciones. Cada una resuelve un problema específico que, si se omite, el agente termina improvisando.

1. Identidad. “Sos {assistant_configs.name}, el asistente virtual de {business.display_name}. Respondés en el idioma y tono definidos por el negocio.” Resuelve quién habla — y explicita que la personalidad no es fija, se configura por tenant. Un mismo modelo es “Sara de Suri” con una clienta y “Leo de Pepito” con otra, sin cambiar de modelo ni de código.

2. Contexto de sesión. business_id, nombre del usuario (paciente o owner), chat ID, fecha actual, zona horaria del business, reglas operativas. Se inyectan por request antes de invocar al modelo. El agente recibe el contexto ya resuelto — no lo averigua. Esto es la traducción directa de la promesa del Capítulo 3 al formato del prompt.

3. Flujo de reserva. Es la receta paso a paso para el caso central del producto. Está escrita como una lista numerada con guardias condicionales:

1. Si el usuario NO sabe qué servicio quiere → llamá get_services.
2. Si el usuario YA eligió un servicio → NO vuelvas a llamar get_services.
   Llamá get_staff_availability.
3. Mostrá disponibilidad como RANGOS, NUNCA slots individuales.
4. Cuando confirme día y hora → llamá create_appointment con UUIDs reales.
5. Confirmá con resumen breve.

La redacción condicional (“Si… → …”) no es casualidad. Un flujo escrito como “Primero hacé X, después Y, después Z” lleva al agente a ejecutar los pasos secuencialmente aunque ya no correspondan. La redacción condicional lo obliga a evaluar el estado antes de cada paso.

4. Guardia anti-bucle. Dos reglas que se aprendieron en testing del prototipo n8n, cuando el agente entraba en loops preguntando cosas que ya había resuelto:

IMPORTANTE: NUNCA repitas un paso que ya se completó. Si ya mostraste
servicios, avanzá. Si ya mostraste horarios, avanzá.

IMPORTANTE: Si ya mostraste los servicios y el usuario eligió uno,
NUNCA vuelvas a listar servicios. Avanzá al siguiente paso.

5. Reglas obligatorias (anti-alucinación). Cinco reglas que evitan que el agente invente datos:

  • Nunca inventes datos — si no tenés info, consultá la tool correspondiente.
  • Nunca inventes UUIDs — son strings largas sin significado humano; usá solo los que devuelven las tools.
  • Nunca listes horarios individuales, solo rangos.
  • Si una tool de escritura devuelve error, no digas que la operación se completó. Decí que hubo un problema y pedí reintentar.
  • Si el usuario pide algo que no es un servicio del negocio, no lo improvises — decí que no ofrecés ese servicio y mostrá los disponibles.

Estas reglas se complementan con las safety_instructions que el business carga en su assistant_configs para casos específicos del vertical (por ejemplo, en kinesiología: “Nunca des consejo médico. Derivá siempre al profesional.”).

6. Mapeo de UUIDs para tools de escritura. El prompt le dice al agente cómo construir los parámetros de create_appointment:

- service_variant_id  → campo "id" de la respuesta de get_services.variants
- staff_member_id     → campo "id" de la respuesta de get_staff
- starts_at           → ISO 8601 con offset de la timezone del tenant
- ends_at             → starts_at + duration_minutes del service_variant

7. Formato de respuestas. Máximo 3-4 líneas por bloque. Negritas para nombres de servicios, profesionales y horarios confirmados. Un salto de línea entre bloques. Sin emojis salvo que el tenant lo pida en response_style.

Lo que el system prompt NO contiene

Deliberadamente, el prompt no contiene lógica de autorización, ni cálculos de disponibilidad, ni manejo de pagos, ni criterios de multi-tenancy. Todo eso vive en las tools (que validan permisos), en el routing (que resuelve contexto), y en el código de los services backend (que ejecuta la lógica). El prompt solo orquesta la conversación y decide qué tool llamar.


Pliego 3 — Los adapters de canal

Un adapter es la pieza que traduce un canal de entrada (WhatsApp, Telegram, cualquier futuro) al lenguaje interno del sistema. El core no sabe si está hablando por WhatsApp o por Telegram — recibe mensajes ya normalizados.

Estado al cierre v3: implementado. Los adapters viven en src/backend/channels/telegram/ y src/backend/channels/whatsapp/, con sus contratos Zod, services puros, y endpoints HTTP montados en /api/v1/channels/telegram/... y /api/v1/channels/whatsapp/....

El contrato del adapter

Cada adapter cumple cuatro responsabilidades mínimas:

1. Verificación de autenticidad. Verifica la firma del proveedor para confirmar que el mensaje es genuino. WhatsApp firma con HMAC-SHA256 sobre el body crudo (header X-Hub-Signature-256); Telegram firma con un secret token configurable (header X-Telegram-Bot-Api-Secret-Token). Si la firma no valida, el mensaje se descarta antes de tocar el sistema. Implementado en verifyWhatsAppWebhookSignature() y en el handler del webhook de Telegram.

2. Normalización del mensaje entrante. Extrae del payload nativo del proveedor un objeto uniforme con: identificador externo del remitente, texto del mensaje (si es audio, la transcripción), tipo de contenido, timestamp. El agente nunca ve un payload de Meta; ve un objeto normalizado persistido en messages con external_message_id, message_type, content, sent_at, etc.

3. Transcripción de audio. Si el mensaje es un audio, el adapter lo transcribe a texto antes de pasarlo. El sistema interno trabaja con texto — no con audio crudo. Estado actual: la entidad audio_transcripts existe en el schema (Familia 3) y attachments soporta mensajes de audio; el handler que dispara la transcripción con Whisper o equivalente está pendiente (paso post-PR #15 del Cap 7).

4. Envío saliente. Cuando el agente produce una respuesta, el adapter la traduce al formato del canal: texto para Telegram (sendTelegramTextMessage), texto o template para WhatsApp según la ventana de 24 horas (sendWhatsAppTextMessage). Gestiona los reintentos si la API del proveedor falla transitoriamente. Persiste el resultado en messages con su status (sent / delivered / read / failed) y captura los timestamps en columnas dedicadas.

El caso especial de WhatsApp

WhatsApp no es simétrico a Telegram. Las restricciones de Meta (ventana de 24 horas, templates pre-aprobados, costos por conversación) viven dentro del adapter, no se propagan al agente. El agente pide “mandá este mensaje” y el adapter decide: si hay ventana activa, texto libre; si no, el template apropiado con los campos rellenos.

Templates pendientes para el MVP: confirmación de turno, recordatorios 24h y 2h antes, cancelación proactiva, solicitud de seña, mensaje de bienvenida tras onboarding.

Persistence garantizada al sistema

Cada mensaje que sale del adapter hacia el core tiene cinco certezas:

  • El remitente está autenticado (firma válida).
  • El contenido está normalizado (texto, sin audio crudo en MVP — transcrito cuando STT esté implementado).
  • El canal está identificado (vía channel_connections resuelta por business_id + channel_connection_id en la URL del webhook).
  • El evento está deduplicado (vía channel_events.provider_event_id con uniqueIndex).
  • El mensaje está persistido (en messages con external_message_id único por thread).

Esa persistence garantizada es lo que permite que los PRs #12 y #13 del Cap 7 acoplen el runtime del agente al adapter sin temer race conditions o duplicados.


Pliego 4 — La política de errores y fuera de scope

No todo sale bien. Este pliego define qué pasa cuando algo falla, y qué pasa cuando el cliente pide algo que el agente no puede resolver.

Errores técnicos

El cliente nunca ve un error técnico. Nunca se muestra un mensaje como “Error 500 de Supabase” ni un stack trace. El cliente ve un mensaje amable: “Disculpá, tuve un problema técnico. ¿Podés repetirme lo que necesitás?”.

Los errores son estructurados para el agente. Cuando una tool falla, devuelve un error tipado con código, categoría (transient / permanent) y un mensaje descriptivo para el log. El agente lee el código y el tipo, no el mensaje crudo.

Si una sesión falla dos veces seguidas, se ofrece escalamiento humano. El agente deja de intentar, le dice al cliente “Estoy teniendo problemas. Te paso con el equipo de {business.display_name}” y crea un handoff_request con kind = 'human_takeover'.

Todo error queda logueado en audit_events con actor_type = 'system' o 'assistant', event_type específico, y un summary legible. Eso permite reconstruir la historia de un fallo desde el dashboard.

Fuera de scope: la política de derivación

Derivar, nunca improvisar.

Caso A — Pregunta fuera de catálogo. El cliente pide algo que el catálogo no contempla y el agente puede responder con transparencia sin inventar. Ejemplo: “¿hacen tinte vegano?” y el catálogo no lo lista. El agente responde “Hoy no lo ofrecemos. ¿Querés que te avise cuando se agregue?”.

Caso B — Consulta escalada al owner. El cliente pide algo que el agente no puede responder pero el owner sí. Se crea un handoff_request con kind = 'escalated_question', se le dice al cliente “Lo consulto con {owner_display_name} y te aviso”, y cuando el owner responde por su WhatsApp, el agente vuelve al cliente con la respuesta. Este flujo cierra el gap SG-4 del Anexo 04a v3 — la consulta escalada tiene entidad persistente en handoff_requests con campos question, answer, priority, sla_due_at.


Pliego 5 — Protocolo de acciones destructivas

Las acciones que modifican o eliminan datos masivamente o fuera de política exigen un protocolo distinto al de las de lectura. No alcanza con que el agente confirme verbalmente con el usuario — hace falta un mecanismo técnico que impida que el agente ejecute por su cuenta una operación irreversible sin que el sistema se entere.

Estado al cierre v3: implementado en DB. La política está aprobada en docs/runtime/perla-sensitive-operations-policy.md y materializada en las tablas pending_operations + pending_operation_items + tool_invocations.requires_confirmation (ver Anexo 04a v3 Familia 5). La implementación del flujo conversacional en TS es el PR #15 del Cap 7.

El patrón preview / execute

Toda operación de nivel L3 o L4 (escritura múltiple o destructiva masiva) se descompone en dos invocaciones:

  • Una invocación de preview. Recibe los parámetros de la operación, consulta qué registros serían afectados, crea una fila en pending_operations con operation_type, risk_level, preview_title, preview_summary, affected_count, un confirmation_token_hash con expires_at, y crea un pending_operation_items por cada entidad afectada con entity_type, entity_id, action, current_value_text, proposed_value_text, item_summary. El agente recibe el preview legible y se lo muestra al cliente.
  • Una invocación de execute. Recibe el token de confirmación y ejecuta. La validación verifica que el token existe en pending_operations, que expires_at > now(), que status es 'pending' o 'confirmed', y que el input_hash coincide. Solo entonces ejecuta y marca el pending_operations.status como 'executed' con executed_at.

Un execute sin token previo es rechazado. Un execute con token expirado es rechazado. Un execute con token de una operación distinta es rechazado. El unique parcial sobre (business_id, input_hash) WHERE status IN ('pending', 'confirmed') impide que se cree un duplicado de la misma operación en simultáneo.

El flujo completo, desde la conversación

Owner: "Cancelame todo lo de mañana a la mañana"
   ↓
agente invoca bulk_cancel_preview(rango: mañana 00:00 a 12:00)
   ↓
preview crea pending_operation:
  - 4 turnos afectados (4 pending_operation_items)
  - confirmation_token_hash + expires_at en 15 minutos
   ↓
agente presenta al owner (con datos del preview):
  "Tenés 4 turnos mañana antes de las 13:00:
    · 09:00 Camila — Perfilado de cejas (Belén)
    · 09:30 Lucía — Limpieza facial (Belén)
    · 10:30 Marta — Masaje (Jazz)
    · 12:00 Sol — Perfilado de cejas (Belén)
   ¿Cancelo los 4 y les ofrezco reprogramar?"
   ↓
owner: "Sí, dale"
   ↓
agente invoca bulk_cancel_execute(token)
   ↓
execute valida → ejecuta → audit_events para cada item
   ↓
agente confirma: "Listo. Cancelé los 4 y les avisé para reprogramar."

Reglas que no dependen del prompt

El protocolo de acciones destructivas no vive en el system prompt — vive en el código de las tools y en los constraints de la base. Si un prompt hostil intentara convencer al agente de invocar bulk_cancel_execute con un token inventado, la tool lo rechaza por el check del confirmation_token_hash. La única forma de pasar es tener un token real generado por un preview previo que coincida con el input_hash y esté dentro del TTL.

Cuándo aplica el protocolo

La política aprobada distingue tres tipos de acciones:

Sí requiere pending_operations: - Cancelar muchos turnos al mismo tiempo (cancel_many_appointments). - Reprogramar muchos turnos al mismo tiempo (reschedule_many_appointments). - Cancelar un turno fuera de política (con seña no reembolsable). - Reprogramar un turno sin pedido del cliente afectado. - Marcar como no-show con penalización. - Bloquear agenda de forma masiva. - Cambiar disponibilidad base de staff o local.

No requiere pending_operations (basta confirmación conversacional del cliente, ver Pliego 6): - Cliente confirma su propio turno. - Cliente cancela su propio turno dentro de política. - Cliente reprograma su propio turno a un horario disponible. - Crear hold temporal antes de confirmar. - Responder mensajes 1:1 dentro del flujo normal. - Actualizar datos livianos del cliente (nombre, email, preferencia de canal).

Fuera del agente MVP (queda para dashboard o post-MVP): - Billing de Perla (cambio de plan, cancelación de suscripción). - Pagos de cliente final (refunds, captura, créditos). - Mensajes masivos / campañas. - Datos sensibles del paciente (alergias, consentimiento, historia clínica formal). - Configuración administrativa del negocio (permisos, horarios base, políticas).


Pliego 6 — Confirmación conversacional vs interna (nuevo en v3)

Hay dos tipos de “confirmación” en el sistema. Confundirlos lleva a UX rota: el cliente recibe pedidos de confirmación que no entiende, o el sistema ejecuta acciones que el agente cree haber confirmado pero nadie autorizó técnicamente. Este pliego separa los dos casos.

Confirmación conversacional (del cliente afectado)

Cuando el cliente dice “sí” en chat, eso confirma una acción suya dentro de los flujos normales del producto. Ejemplo: “¿Querés sacar turno el viernes a las 15?”“sí” → se crea el turno.

Cuándo aplica: - L1 o L2 dentro del flujo conversacional normal. - La acción afecta solo al cliente (su turno, su preferencia, su mensaje). - El cliente tiene la información completa para decidir. - La acción es reversible o de bajo impacto.

Mecanismo técnico: - El agente persiste un messages con la pregunta. - El cliente responde con un messages con la confirmación. - El agente invoca la tool L2 directamente, sin token. - La tool valida normalmente (multi-tenancy, permisos, slot disponible). - Se ejecuta y se persiste un audit_event.

Confirmación interna del negocio (con pending_operations)

Cuando una acción afecta a múltiples clientes, va contra política, o no fue pedida por el cliente afectado, el sistema exige un mecanismo técnico — token + TTL — además de cualquier confirmación conversacional.

Cuándo aplica: - L3 o L4. - La acción afecta a más de un cliente o a la operación del negocio. - La operación es difícil o imposible de revertir. - La acción la pide el owner sobre datos de clientes ajenos.

Mecanismo técnico: - Se crea un pending_operations con preview + confirmation_token_hash + expires_at. - El agente muestra el preview al owner. - El owner confirma verbalmente en chat. - El agente invoca <operation>_execute con el token. - La tool valida el token y ejecuta atómicamente. - Cada item afectado se registra en pending_operation_items con su resultado individual. - audit_events registra la operación con event_type correspondiente.

Por qué los dos coexisten

Si todo requiriera confirmación interna, el cliente tendría que pasar por flujos de tokens TTL para sacar un turno simple. Pésima UX. Si nada requiriera confirmación interna, el agente podría cancelar 50 turnos por un malentendido y nadie se entera hasta que los clientes llaman quejándose.

La regla de bolsillo: si la acción la pide el cliente afectado dentro de su propio scope, basta confirmación conversacional. Si la acción la pide el owner sobre clientes ajenos, sobre el catálogo, o sobre la operación del negocio, va con pending_operations.

Ejemplos contrastados

Escenario Tipo de confirmación
Cliente: “Quiero turno el viernes a las 15” → agente confirma → cliente dice “sí” → se crea Conversacional
Cliente: “Cancelame el turno del viernes” → agente confirma → cliente dice “sí” → se cancela Conversacional
Owner: “Cancelame todos los turnos de mañana” → agente arma preview de 4 turnos → owner dice “sí” → execute con token Interna (pending_operations)
Owner: “Cambialé el horario al turno de Camila a las 16” (Camila no pidió) → agente arma preview → owner confirma → execute con token Interna
Owner: “Bloqueá la agenda de Belén el jueves por la tarde” → agente arma preview de schedule_block → owner confirma → execute con token Interna
Cliente preguntó por servicios → agente respondió Sin confirmación (lectura, L1)

Frentes abiertos que afectan este capítulo

  1. Implementación TS de las 8 tools del Pliego 1 sección B — definidas, no implementadas. Roadmap completo en Cap 7.

  2. Implementación de tools L3/L4 (bulk_cancel_preview, bulk_reschedule_preview, etc.) — entran en PR #15 del Cap 7. La infraestructura DB ya existe (Pliego 5 implementado).

  3. Templates de WhatsApp aprobados por Meta — pendiente. Los 5 críticos se diseñan + aprueban antes del lanzamiento abierto.

  4. STT de audios entrantes — schema soporta (audio_transcripts + audio_transcript_segments), código no. Post-PR #15.

  5. Decisión D-04 — voz del agente. Tomi puso 3 presets (professional / friendly / balanced) en settings/assistant. El Brand Guide define 4 voces (Nácar / Arena / Marfil / Espuma). v3 propone que coexistan: los presets son atajos UI para owners no técnicos, las voces se materializan vía business_instructions para casos avanzados. Decisión final pendiente.


Lo que este capítulo NO incluye

  • Los textos exactos de los mensajes que ve el cliente (voz de marca — vive en el Brand Guide).
  • La implementación interna de cada tool — queries, joins, validaciones concretas (vive en el repositorio de producción, módulos src/backend/<dominio>/).
  • Los flujos conversacionales completos con variantes por tenant (vive en 01_INFRA/SPEC_v0.md y en los prompts 02_MIDDLE/_prompts/).
  • Los esquemas JSON exactos de request y response de cada tool (son derivados técnicos del contrato — viven como tipos en el código).
  • El orden temporal de implementación de cada pliego (vive en Cap 7).

Por qué este capítulo importa

Sin contratos formales, el agente improvisa. Sin contratos formales, dos desarrolladores pueden implementar la misma tool de dos maneras incompatibles. Sin contratos formales, no hay manera de testear el sistema porque no hay un comportamiento esperado contra el cual comparar. Y, quizás lo más importante, sin contratos formales no hay manera de que un modelo reemplazable (Gemini Flash hoy, Claude Haiku mañana, GPT-5 Nano pasado) se comporte igual — el contrato es lo que hace que el agente sea una pieza reemplazable en vez de una caja negra específica.

Este capítulo es el que convierte a Perla de un experimento en un producto. Los cinco capítulos anteriores describen el edificio; este describe las reglas que hacen que las piezas del edificio se hablen sin malentendidos. Los seis pliegos juntos son lo que permite que tres personas distintas, en tres momentos distintos, escriban tres pedazos del agente que terminan funcionando como uno.


v1 — 2026-04-16 — documento original. v2 — 2026-04-28 — Pliego 1 actualizado con las 6 tools del MVP owner. Niveles L formalizados. v3 — 2026-05-03 — Pliego 1 sección B sumando las 2 tools del workflow actual (get_services, get_staff). Pliego 2 enriquecido con la estructura completa de assistant_configs (16 campos). Pliego 3 marcado como implementado con src/backend/channels/. Pliego 5 marcado como implementado en DB con la política aprobada. Pliego 6 NUEVO formalizando la distinción entre confirmación conversacional e interna. D-02 replanteada con sistema de permisos. Próxima edición cuando se implemente la primera tool TS o cuando aparezca un pliego nuevo.

06a Anexo A · Matriz Acción ↔ Tool
06a

Anexo A · Matriz Acción ↔ Tool

El contrato de cierre entre dashboard y agente.


Para qué sirve esta matriz

Cada acción que se puede ejecutar en el dashboard existe también como tool del agente, y viceversa. UI y agente son dos caras de la misma arquitectura — una no es más fuente de verdad que la otra.

El objetivo de la matriz es que cualquier inteligencia (humana o artificial) que entre al proyecto pueda responder, para cada capacidad del producto, las cinco preguntas que el SDD exige:

  1. ¿Dónde vive esta acción en la UI? (pantalla + componente)
  2. ¿Cuál es la tool del agente que la ejecuta hoy? (n8n legacy — Perla_mvp_v2.2.json)
  3. ¿Cuál será la tool del agente target? (TS en src/backend/agent/tools/ según PLAN_AGENTE)
  4. ¿Qué permiso del catálogo se requiere? (de los 19 codes de la Familia 7)
  5. ¿Está implementada a ambos lados? (estado dash + estado tool n8n + estado tool TS)

Sin esta matriz, un dev puede construir update_service en el agente con una firma distinta a la que espera el dashboard, o Nahue puede diseñar un botón “Archivar cliente” que no tiene tool detrás. Con esta matriz, el contrato es explícito y auditable.


Convenciones

Identificador único

Cada acción lleva un ID estable de la forma DA-XX (Dashboard Action). El ID no cambia aunque la acción se renombre, se mueva de pantalla, o cambie la implementación. Es la ancla que une especificación, código y tickets.

Perfil autorizado

Quién puede invocar la acción. Los perfiles humanos del Cap 02 son cuatro (paciente · owner · dashboard · equipo El Nudo); el “operador con permisos limitados” se materializa con permission sets. Codificación corta:

  • P = Paciente (no entra al dashboard, opera por canal)
  • O = Owner (membership.role=’owner’ + permission_set=’owner’)
  • OL = Operador limitado (membership.role=’admin’ o ‘member’ + permission_set ‘admin’ / ‘receptionist’ / ‘professional’ o un set custom por business)

Cuando una acción la pueden hacer varios perfiles se listan separados por coma.

Nivel de riesgo

Clasificación del Pliego 1:

  • L1 — Lectura. Ejecución directa.
  • L2 — Escritura simple. Confirmación textual del usuario en chat (preview conversacional).
  • L3 — Escritura múltiple. Patrón preview + execute con pending_operations y token.
  • L4 — Destructivo masivo. Preview + delay + token con TTL + logging detallado.
  • UI — Acción cosmética que no toca DB (filtros, navegación). No requiere tool del agente.

Permiso requerido

De los 19 codes del catálogo permissions (Familia 7 del Cap 4). Los 4 permission sets system pre-cargados (owner · admin · receptionist · professional) determinan qué OL puede ejecutar qué acción. Codificación: module.action (ej: appointments.create, messages.reply). Algunas acciones de UI pura no tienen permiso asociado (marcadas con —).

Estado de implementación

  • Implementado y funcional end-to-end
  • 🟡 Parcial — existe la superficie (UI o tool) pero le falta alguna pieza (handler real, validación, integración con backend)
  • No existe

Se evalúan tres estados por fila:

  • Estado dash — ¿existe el punto de entrada en tomillo/perla:src/routes/app/?
  • Estado tool n8n — ¿existe la tool en el workflow Perla_mvp_v2.2.json?
  • Estado tool TS — ¿existe la tool en src/backend/agent/tools/ (target del PLAN_AGENTE)?

Tenant context

Toda acción que no sea UI se ejecuta bajo el contexto de un tenant resuelto previamente por el routing (Cap 03). Si el perfil incluye paciente, la acción opera solo sobre registros del propio paciente; si es owner/OL, opera sobre el tenant completo con las restricciones del perfil. El backend cumple multi-tenancy con RLS + trigger enforce_tenant_reference en cada FK cruzada (Cap 4 · Cap 04a Familia 5/7).

Mapping a las tablas del schema

Cada fila de la matriz toca una o más tablas. La correspondencia tabla ↔ DA-XX vive en el Cap 04a sección “Correspondencia matriz acción↔tool ↔ schema”.


Estado del dashboard al 2026-05-03

Tomi mergeó PRs #28 (redesign-app-home) y #30 (redesign-app-shell) que reorganizaron el shell visual. La estructura actual de rutas:

/app/                     → Home con appointments del día + calendar
/app/schedule             → Agenda con vistas day/week/month
/app/customers            → Clientes con búsqueda + perfil con tabs (turnos/conversaciones/pagos) ← SECCIÓN NUEVA
/app/messages             → Bandeja + chat + intervención humana + audio + attachments
/app/account              → Datos del usuario logueado
/app/profile              → Perfil del miembro
/app/settings/            → 10 sub-páginas
  general                 → Datos básicos del negocio
  locations               → Sucursales
  team                    → Profesionales
  services                → Catálogo
  availability            → Disponibilidad por staff
  resources               → Recursos físicos
  policies                → Políticas (cancelación, no-show, deposit)
  notifications           → Reglas y templates
  assistant               → Configuración del agente (5 channels, 3 personalidades)
  billing                 → Plan + facturación

Decisión cerrada D-01 (Clientes y Mensajes separados) confirmada por el repo: /app/customers existe como ruta propia con CustomersPage componente.

Estado actual: todo el dashboard vive sobre datos mock hardcodeados (MOCK_CONVERSATIONS, CUSTOMERS, STAFF, initialAppointments, INITIAL_CHANNELS, etc.). Cero integración con backend de dominio. La integración con la API real se habilita cuando los endpoints HTTP de cada módulo existan — el principio de cierre dice que son las mismas operaciones.


Las tools del agente n8n actual (legacy)

Workflow Perla_mvp_v2.2.json — 8 tools cableadas:

# Tool n8n Niveles Cubre
1 list_appointments L1 DA-03, DA-20, DA-21, DA-22
2 create_appointment L2 DA-09, DA-27, DA-45
3 cancel_appointment L2 DA-05, DA-26
4 reschedule_appointment L2 DA-07, DA-25
5 list_conversations L1 DA-50
6 take_conversation L2 DA-58
7 get_services L1 (resolución de UUIDs antes de create_appointment)
8 get_staff L1 (resolución de UUIDs antes de create_appointment)

Estas 8 tools son el contrato MVP del owner por WhatsApp. Cuando el PR #13 del PLAN_AGENTE cierre (disparo desde webhook con runtime TS), n8n se apaga y todo se mueve a src/backend/agent/tools/.


Sección 01 · Home (/app/)

Pregunta de la sección: “¿Cómo está mi negocio hoy?”

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-01 Ver panorama del día (cards métricas, próximos turnos, vacantes calculadas) O, OL get_home_digest dashboard.access + appointments.read L1
DA-02 Saludo dinámico por hora del día O, OL — (UI render) UI
DA-03 Ver próximo turno del día O, OL list_appointments (orden + limit=1) list_appointments appointments.read L1
DA-04 Confirmar turno (pending → confirmed) O, OL confirm_appointment appointments.update L2
DA-05 Cancelar turno del día O, OL cancel_appointment cancel_appointment appointments.cancel L2
DA-06 Marcar turno como “no se presentó” O, OL mark_appointment_no_show appointments.update L2
DA-07 Reprogramar turno O, OL reschedule_appointment reschedule_appointment appointments.update L2
DA-08 Ver detalle de turno (sheet) O, OL get_appointment appointments.read L1
DA-09 Crear turno desde slot libre (vacancy) O, OL create_appointment create_appointment appointments.create L2
DA-10 Filtrar vista por profesional O, OL — (filtro local) UI
DA-11 Descartar alerta resuelta O, OL dismiss_alert dashboard.access UI
DA-12 Ver comparativo semanal (variación %) O, OL get_weekly_comparison appointments.read L1
DA-13 Ver estado del asistente como widget O, OL get_assistant_status dashboard.access L1
DA-14 Abrir conversación del cliente desde su turno O, OL open_thread (UI + L1) messages.read UI + L1

Notas Home: el mock actual usa professional: 'owner' | 'assistant' | 'team' como placeholder en lugar de staff_member_id real. Cuando se conecte al backend, el filtro DA-10 va a operar sobre staff_members.id con el colorHex de la tabla.


Sección 02 · Agenda (/app/schedule)

Pregunta de la sección: “¿Qué tengo hoy, mañana, esta semana?”

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-20 Ver agenda por día O, OL list_appointments (rango día) list_appointments appointments.read L1
DA-21 Ver agenda por semana O, OL list_appointments (rango semana) list_appointments appointments.read L1
DA-22 Ver agenda por mes O, OL list_appointments (rango mes) list_appointments appointments.read L1
DA-23 Ver disponibilidad libre (huecos visibles) O, OL get_staff_availability (variant owner — slots, no rangos) appointments.read L1
DA-24 Ver indicador ocupación (N de M slots) O, OL derivado de DA-22 + DA-23 (cálculo en cliente o get_occupancy_rate) appointments.read L1
DA-25 Reprogramar turno desde la agenda O, OL reschedule_appointment reschedule_appointment appointments.update L2
DA-26 Cancelar turno (con o sin notificación) O, OL cancel_appointment cancel_appointment (param notify: bool) appointments.cancel L2 🟡 (sin flag notify)
DA-27 Crear turno vía botón global O, OL create_appointment create_appointment appointments.create L2
DA-28 Slot locking visual (anti-doble-reserva) O, OL lock_slot_temporarily (booking_holds + schedule_blocks) appointments.create L2 🟡
DA-29 Bloquear rango a un profesional (ausencia, feriado) O, OL block_staff_slot (manual_block en schedule_blocks) appointments.update L2

Notas Agenda: schedule.tsx ya usa el vocabulary del schema (staffMemberId, serviceVariantId, customerId). Es la sección más alineada al backend. Cuando se conecten endpoints, va a ser la primera demoable end-to-end.


Sección 03 · Clientes (/app/customers) — SECCIÓN NUEVA RESPECTO A v1

Pregunta de la sección: “¿Quiénes son mis clientes y qué historia tienen?”

Decisión D-01 confirmada cerrada en repo: /app/customers existe como ruta propia. Tab Historial en messages eliminado. La estructura tiene búsqueda + perfil con 3 tabs (turnos/conversaciones/pagos).

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-40 Buscar cliente por nombre o teléfono O, OL search_customers customers.read L1
DA-41 Ver perfil del cliente (panel principal con stats) O, OL get_customer_profile customers.read L1
DA-42 Ver estado del cliente (active 30d / risk / inactive) O, OL get_customer_status (función de última visita) customers.read L1
DA-43 Crear cliente (NewCustomerDialog) O, OL create_customer customers.create L2
DA-44 Editar nota interna del cliente O, OL update_customer_note (tabla customer_notes con visibility/note_type) customers.update L2 🟡 (display de note, sin edit UI)
DA-45 Agendar turno desde perfil del cliente O, OL create_appointment create_appointment (con cliente precargado) appointments.create L2
DA-46 Ver tab “Turnos” del cliente O, OL list_customer_appointments appointments.read + customers.read L1
DA-47 Ver tab “Conversaciones” del cliente O, OL list_customer_threads messages.read + customers.read L1
DA-48 Ver tab “Pagos” del cliente O, OL list_customer_payments (sobre payment_intents con customer_id) customers.read + billing.read L1
DA-49 Acceso directo al hilo de canal del cliente O, OL open_thread messages.read UI + L1
DA-50 Editar datos del cliente (nombre, phone, email, channel preferido) O, OL update_customer customers.update L2

Notas Customers: el mock actual incluye noShows (count), totalSpent (string), lastAppointment/nextAppointment (string formato display), note (string), history/conversations/payments arrays. El status del cliente (active / risk / inactive) no es campo del schema — se deriva de last_interaction_at con regla de negocio (a definir cuándo el agente lo marca como risk/inactive).


Sección 04 · Mensajes (/app/messages)

Pregunta de la sección: “¿Qué está hablando el agente con mis clientes?”

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-50 (reusa) Bandeja ordenada por última actividad O, OL list_conversations list_conversations messages.read L1
DA-51 Ver hilo completo del cliente (chat tipo WhatsApp) O, OL get_thread_messages messages.read L1
DA-52 Estado del agente por conversación (assistant / needsHuman / human) O, OL get_thread_agent_status (mapeo desde conversation_threads.status + handoff_requests.status) messages.read L1
DA-53 Búsqueda en bandeja (por nombre, teléfono, texto) O, OL search_threads messages.read L1 🟡 (por nombre/teléfono, no texto)
DA-54 Ver attachment del mensaje (imagen, audio con transcript) O, OL get_message_attachments messages.read L1
DA-55 Ver delivery status del mensaje (sent / delivered / failed) O, OL (campos messages.status/failed_at/provider_error_code) messages.read L1
DA-56 Reintentar mensaje fallido O, OL retry_message_send messages.reply L2
DA-57 Marcar mensaje como leído (notification) O, OL (messages.read_at) messages.read UI 🟡 (badge unread, sin trigger explícito)
DA-58 Tomar conversación (handoff del agente al humano) O, OL take_conversation take_conversation handoffs.manage L2
DA-59 Responder conversación tomada O, OL send_human_message messages.reply L2 🟡 (UI input ámbar visible, sin handler)
DA-60 Reactivar el agente (release handoff) O, OL release_conversation handoffs.manage L2 🟡 (botón “Cancelar”, semántica ambigua)
DA-61 Marcar conversación como resuelta O, OL mark_thread_resolved handoffs.manage L2
DA-62 Bloquear cliente (anti-spam) O, OL block_customer_thread (conversation_threads.status='blocked') messages.reply + customers.update L2
DA-63 Ver historial de tool invocations del thread (debug) O list_thread_tool_invocations dashboard.access (solo owner) L1
DA-64 Confirmar pending operation desde el chat O confirm_pending_operation (depende del operation_type) L2
DA-65 Cancelar pending operation desde el chat O cancel_pending_operation (depende del operation_type) L2

Notas Messages: - DA-52 mapping clave: el dashboard reduce a 3 estados (assistant / needsHuman / human) los 6 valores del enum handoff_status del repo (requested · accepted · answered · rejected · completed · cancelled) + el enum conversation_thread_status (open · pending_handoff · in_handoff · resolved · archived). La regla de mapeo se define cuando se conecten endpoints. - DA-59 + DA-60: GAP-MSG-01 anotado en 04a v3. ¿messages.reply cubre tanto responder como tomar? ¿O take_conversation requiere handoffs.manage y messages.reply solo cubre escribir mensajes? Decisión a cerrar antes del PR del runtime. - DA-64/DA-65: las pending operations confirmadas/canceladas desde chat (no desde dashboard) requieren handler específico. Pliego 5 materializado en repo, falta el flujo conversacional.


Sección 05 · Configuración (/app/settings/*)

Pregunta de la sección: “¿Cómo está configurado mi negocio?”

5.1 General (/app/settings/general)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-70 Editar datos del negocio (legal_name, display_name) O update_business businesses.update L2
DA-71 Editar business_settings (timezone, locale, currency, formato fecha/hora) O update_business_settings settings.manage L2

5.2 Locations (/app/settings/locations)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-72 Listar sucursales O, OL list_locations locations.manage L1
DA-73 Crear sucursal O create_location locations.manage L2
DA-74 Editar sucursal (timezone propio, dirección) O update_location locations.manage L2
DA-75 Marcar sucursal como primaria O set_primary_location locations.manage L2
DA-76 Desactivar sucursal O deactivate_location locations.manage L2

5.3 Team (/app/settings/team)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-80 Listar profesionales O, OL get_staff list_staff staff.manage L1
DA-81 Crear profesional O create_staff staff.manage L2
DA-82 Editar profesional (nombre, color, isBookable, role_title) O update_staff staff.manage L2
DA-83 Asignar especialidades O update_staff_specialties staff.manage L2
DA-84 Cambiar status (active / inactive / on_leave) O update_staff_status staff.manage L2
DA-85 Desactivar profesional O deactivate_staff staff.manage L2
DA-86 Asignar permission_set a una membership O update_membership_permission_sets permissions.manage L2
DA-87 Asignar override directo (allow/deny) O update_membership_permission_override permissions.manage L2

5.4 Services (/app/settings/services)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-90 Listar servicios y variantes O, OL get_services list_services_with_variants services.manage L1
DA-91 Crear categoría de servicio O create_service_category services.manage L2
DA-92 Crear servicio O create_service services.manage L2
DA-93 Crear variante (duración, precio, buffer) O create_service_variant services.manage L2
DA-94 Editar servicio o variante O update_service / update_service_variant services.manage L2
DA-95 Asignar profesionales a una variante O update_staff_service_variants services.manage + staff.manage L2
DA-96 Asignar recursos a una variante O update_service_variant_resources services.manage L2
DA-97 Desactivar servicio O deactivate_service services.manage L2

5.5 Availability (/app/settings/availability)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-100 Ver reglas de disponibilidad de un staff/resource/location O, OL list_availability_rules staff.manage L1
DA-101 Crear regla recurrente (day_of_week, start_time, end_time) O create_availability_rule staff.manage L2
DA-102 Editar regla O update_availability_rule staff.manage L2
DA-103 Crear excepción (feriado, ausencia) O create_availability_exception staff.manage L2
DA-104 Importar feriados (modal calendario especial) O import_holidays staff.manage L3
DA-105 Eliminar regla o excepción O delete_availability_rule / delete_availability_exception staff.manage L2

5.6 Resources (/app/settings/resources)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-140 Listar recursos (salas, sillas, equipamiento) O, OL list_resources services.manage L1
DA-141 Crear recurso O create_resource services.manage L2
DA-142 Editar recurso (type, booking_mode, capacity) O update_resource services.manage L2
DA-143 Cambiar status (active / inactive / maintenance) O update_resource_status services.manage L2

5.7 Policies (/app/settings/policies)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-130 Listar políticas (booking, cancellation, deposit, etc.) O, OL list_policy_sets settings.manage L1
DA-131 Crear o editar policy_set (rules como jsonb) O update_policy_set settings.manage L2
DA-132 Crear policy_override por entidad específica O create_policy_override settings.manage L2

5.8 Notifications (/app/settings/notifications)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-120 Listar notification_rules O, OL list_notification_rules settings.manage L1
DA-121 Crear o editar regla (event, channel, template, offset_minutes) O update_notification_rule settings.manage L2
DA-122 Activar / desactivar regla O toggle_notification_rule settings.manage L2
DA-123 Ver log de notificaciones enviadas O, OL list_notification_logs messages.read L1
DA-124 Reintentar notificación fallida O retry_notification messages.reply L2

5.9 Assistant (/app/settings/assistant)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-110 Editar nombre del asistente O update_assistant_config (campo name) settings.manage L2
DA-111 Editar greeting message (con chip dinámico del nombre) O update_assistant_config (campo greeting_message) settings.manage L2
DA-112 Activar / desactivar asistente (switch global) O update_assistant_config (campo is_active) settings.manage L2
DA-113 Toggle auto-reply O update_assistant_config (campo auto_reply_enabled) settings.manage L2
DA-114 Toggle handoff enabled O update_assistant_config (campo handoff_enabled) settings.manage L2
DA-115 Elegir personalidad (professional / friendly / balanced) O update_assistant_config (presets que setean response_tone + response_style + business_instructions) settings.manage L2
DA-116 Conectar canal nuevo (telegram/instagram/webchat/email) O connect_channel (crea channel_connections + secret refs) settings.manage L3 🟡 (dialog UI, sin OAuth real)
DA-117 Desconectar canal O disconnect_channel settings.manage L3
DA-118 Toggle per-tool (booking/reschedule/cancellation/payment_link) O update_assistant_config (toggles per tool) settings.manage L2
DA-119 Editar safety_instructions / business_instructions (texto libre) O update_assistant_config (campos avanzados) settings.manage L2

Notas Assistant: las 3 personalidades del UI (professional / friendly / balanced) son presets que tienen que setear response_tone (varchar 64) + response_style (text). Hoy hardcodeadas. Decisión D-04 abierta: ¿estas 3 son las definitivas, o las 4 voces del Brand Guide (Nácar / Arena / Marfil / Espuma) las reemplazan? Mi propuesta: las 3 actuales son atajos de UI, los 4 voces son configurables vía business_instructions para casos avanzados. No se cancelan entre sí.

5.10 Billing (/app/settings/billing)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-150 Ver plan actual + período + próximo pago O get_current_subscription billing.read L1
DA-151 Ver historial de payment_intents O list_payment_intents billing.read L1
DA-152 Cambiar plan (upgrade / downgrade) O change_subscription_plan billing.manage L3 🟡
DA-153 Cancelar suscripción (al final del período) O cancel_subscription billing.manage L3 🟡
DA-154 Reactivar suscripción O reactivate_subscription billing.manage L3
DA-155 Editar billing_profile (tax_id, billing_email) O update_business_billing_profile billing.manage L2 🟡
DA-156 Generar payment_intent para seña de turno (cliente final) O, OL create_deposit_payment_intent appointments.update L3
DA-157 Refund seña al cliente final O refund_payment_intent (payments.refund — pendiente, ver GAP-PAY-01) L4

Sección 06 · Plataforma común (todas las pantallas)

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-200 Bootstrap del usuario (registro + onboarding del primer business) (sin business todavía) bootstrap_initial_tenant (sin permiso, lo crea desde cero) L2 ✅ (existe POST /api/v1/platform/bootstrap)
DA-201 Resolver tenant actual (current-tenant) O, OL get_current_tenant dashboard.access L1 ✅ (existe GET /api/v1/platform/current-tenant)
DA-202 Resolver membership actual O, OL get_current_membership dashboard.access L1 ✅ (existe GET /api/v1/platform/current-membership)
DA-203 Switch business (multi-tenancy en dashboard) O, OL (cambio de query param) dashboard.access UI + L1 ✅ (business-switcher.tsx)
DA-204 Switch location O, OL (cambio de query param) dashboard.access UI + L1 ✅ (location-switcher.tsx)
DA-205 Editar profile del usuario (cualquier user) update_user_profile (no permiso multi-tenant — es del propio user) L2
DA-206 Editar account (password, security) (cualquier user) (Supabase Auth nativo) L2
DA-207 Cerrar sesión (cualquier user) (Supabase Auth nativo) UI

Sección 07 NUEVA · Agente, runs y operaciones

Acciones del dashboard que aún no existen pero el manual y el repo van a soportar para observabilidad del agente. Crítico para operar producción seria.

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-300 Listar agent_runs del business (con filtros por thread, status) O list_agent_runs dashboard.access (solo owner) L1
DA-301 Ver detalle de un agent_run (input/output, tokens, modelo, errores) O get_agent_run dashboard.access L1
DA-302 Ver tool_invocations de un run O list_tool_invocations dashboard.access L1
DA-303 Listar pending_operations activas del business O list_pending_operations (depende del operation_type) L1
DA-304 Confirmar pending_operation desde dashboard (con token) O confirm_pending_operation (depende del operation_type) L2
DA-305 Cancelar pending_operation O cancel_pending_operation (depende del operation_type) L2
DA-306 Ver pending_operation_items (entidades afectadas) O list_pending_operation_items (depende del operation_type) L1
DA-307 Ver audit_events del business (timeline) O list_audit_events dashboard.access L1
DA-308 Ver audit_event_changes (diff de un cambio específico) O get_audit_event_changes dashboard.access L1
DA-309 Métricas del agente (runs por día, tokens, intents detectados, errores) O get_agent_metrics dashboard.access L1
DA-310 Ver inbound_message_batches abiertos (debug agrupación) O list_inbound_message_batches dashboard.access L1

Sección 08 NUEVA · Permisos del equipo

ID Acción Perfil Tool n8n Tool TS target Permiso Nivel Estado dash Estado tool n8n Estado tool TS
DA-400 Listar permission_sets del business (system + custom) O list_permission_sets permissions.manage L1
DA-401 Crear permission_set custom por business O create_permission_set permissions.manage L2
DA-402 Editar permisos atómicos de un permission_set custom O update_permission_set_permissions permissions.manage L2
DA-403 Asignar permission_set a una membership O assign_permission_set_to_membership permissions.manage L2
DA-404 Aplicar override directo (allow/deny) a una membership O update_membership_permission_override permissions.manage L2
DA-405 Ver permisos efectivos de una membership (resumen para audit) O get_effective_permissions permissions.manage L1

Resumen estadístico (snapshot 2026-05-03)

Total acciones catalogadas: 113 (vs 107 en v1).

Estado dash: - ✅ Implementadas en UI mock: 71 - 🟡 Parciales: 8 - ❌ No existen: 34

Estado tool n8n (legacy Perla_mvp_v2.2.json): - ✅ Implementadas: 8 tools que cubren ~12 acciones (la mayoría reusadas). - ❌ El resto: 101 acciones.

Estado tool TS (target del PLAN_AGENTE): - ✅ Implementadas: 3 (bootstrap_initial_tenant, get_current_tenant, get_current_membership — los endpoints platform existentes en /api/v1/platform/*). - ❌ Resto: 110.

Lectura operativa: el dashboard adelantó (71 acciones con UI mock) frente al backend (3 endpoints reales). Es por diseño — Tomi maquetó UI primero para validar producto, los endpoints se conectan en el orden del PLAN_AGENTE_v1 (slice 4 app shell ya está; siguen settings, staff/services, schedule read-only, schedule write, channel connection telegram, customers, messages).


Decisiones del producto (estado al 03/05)

ID Decisión Estado Notas
D-01 Clientes y Mensajes como secciones separadas Cerrada (24/04, confirmada en repo 03/05) /app/customers existe como ruta propia. Tab Historial de messages eliminado.
D-02 Permisos de staff vs owner para tools del agente ⚠️ Abierta — replanteada por sistema de permisos. En v1 era “¿staff puede invocar list_conversations y take_conversation?”. Hoy con permission_sets se reformula: el set professional NO incluye handoffs.manage ni messages.reply; el set receptionist SÍ. El Pliego 1 actual restringe list_conversations y take_conversation a owner — esto se relaja con el sistema de permisos. Acción: mapear cada tool del Pliego 1 a su permission code requerido. Hecho en este 06a v3.
D-03 Colores de staff (arbitrarios vs configurables) ⚠️ Abierta. Schema soporta staff_members.color_hex. Schedule.tsx ya lee colorHex del staff. Falta UI en settings/team para que el owner configure.
D-04 Personalidad del bot (presets vs voces) ⚠️ Abierta — punto de partida del lado de Tomi. Settings/assistant tiene 3 presets (professional / friendly / balanced) que setean response_tone + response_style. El Brand Guide define 4 voces (Nácar / Arena / Marfil / Espuma). Mi propuesta para v3 del manual: los 3 presets de UI son atajos para owners no técnicos, las 4 voces se materializan via business_instructions (texto libre). NO se cancelan.
D-05 Identidad visual del dashboard ⚠️ Abierta. Tomi tiene design.md con preset shadcn Luma (Mist + Teal) + IBM Plex Sans + Instrument Sans. Brand Guide Perla pide DM Serif Display + DM Sans + paleta perla (teal #2D7A7A, arena #A88760, violeta #5B4B7A) + gradient iris + motion tokens. Decisión de Seba como diseñador del producto + Nahue.
D-06 Batch cancel/reschedule UI ⚠️ Abierta. Tools bulk_cancel_preview / bulk_cancel_execute definidas en Pliego 1 sección D. UI no existe. Va a salir cuando se implementen las tools L3 con pending_operations (PR #15 del PLAN_AGENTE).

Gaps específicos de UI (matriz)

Items de la matriz con Estado dash = ❌ que el brief original prometía pero nunca fueron implementados. Útil para próximas iteraciones de Nahue:

Sección DA-XX Gap
Home DA-11 Alertas activas + sección de dismiss
Home DA-12 Comparativo semanal con variación %
Home DA-13 Estado del asistente como widget en home
Home DA-14 Atajo “abrir conversación” desde un turno
Schedule DA-29 Bloquear rango a profesional desde la agenda
Customers DA-49 Atajo a hilo del canal del cliente
Customers DA-50 Editar datos básicos del cliente desde dashboard
Messages DA-56 Reintentar mensaje fallido
Messages DA-61 Mark thread resolved
Messages DA-62 Bloquear cliente
Messages DA-63-65 Debug del agente + confirm pending desde chat
Notifications DA-123 Log de notificaciones enviadas
Notifications DA-124 Reintentar notification fallida
Assistant DA-117 Desconectar canal
Assistant DA-118 Toggle per-tool granular
Assistant DA-119 Editar safety/business instructions (avanzado)
Billing DA-154 Reactivar suscripción cancelada
Billing DA-156 Generar payment_intent de seña
Billing DA-157 Refund seña
Agente (sec 07) DA-300 a DA-310 Toda la observabilidad del agente
Permisos (sec 08) DA-400 a DA-405 Gestión de permisos del equipo desde UI

Reglas de mantenimiento de la matriz

  • Cada vez que aparece una acción nueva en el dashboard (ya sea en UI mock o conectada a backend), se agrega fila a la matriz antes de mergear el PR. La fila tiene que tener tool n8n (si aplica), tool TS target, permiso requerido, nivel, los 3 estados.
  • Cada vez que se agrega o renombra una tool en el agente (sea n8n o TS), la matriz se actualiza en el mismo PR.
  • Estados se actualizan reactivamente — cuando una tool TS pasa de ❌ a ✅, la fila correspondiente se marca.
  • DA-XX nunca se reusan. Si una acción se elimina, la fila se marca con [deprecated] y queda como histórico. Numeración crece monotónicamente.
  • Versionado: v3 reemplaza a v1. Versión anterior en _versions/.

Lo que esta matriz NO incluye

  • La firma exacta de cada tool (vive en Pliego 1 + Cap 04a).
  • Las queries SQL exactas que cada tool ejecuta (viven en src/backend/<dominio>/service.ts).
  • La UX detallada de cada componente (vive en el repo + Brand Guide).
  • El roadmap operativo (en qué orden se implementa cada cosa) — vive en 01_INFRA/PLAN_AGENTE_v1.md y eventualmente en Cap 07 NUEVO.

v1 · 2026-04-24 · primera matriz con 107 acciones. v3 · 2026-05-03 · regeneración entera contra dashboard actual (post PRs #28 + #30) y schema actualizado. 113 acciones (vs 107). Sumadas dos secciones nuevas (07 Agente y operaciones · 08 Permisos del equipo). Sumada columna “Tool n8n legacy” + columna “Permiso requerido”. D-01 confirmada cerrada. D-02 replanteada con sistema de permisos. D-04 con punto de partida de Tomi (3 presets) por validar. Resto de decisiones abiertas. Próxima edición cuando Nahue/Tomi mergeen un PR que cambie acciones del dashboard, o cuando una tool TS pase de ❌ a ✅.

07 Roadmap operativo del agente
07

Roadmap operativo del agente

¿Qué destraba cada hito hasta llegar al MVP vendible?


Los seis capítulos anteriores describen un edificio. Este describe la obra en curso: cómo el agente conversacional pasa de motor mergeado a producto vendible, hito por hito, con criterios de cierre verificables y referencias claras a cada decisión arquitectónica de los caps previos.

Este capítulo no describe arquitectura nueva — todo lo que aparece acá ya está modelado en los Caps 4, 5 y 6. Lo que aporta es el orden temporal de la implementación, los criterios para saber si cada hito está listo, y los acuerdos que el equipo necesita cerrar antes de moverse al siguiente. Es el cronograma de obra del agente, escrito como mapa, no como agenda.

Una nota de alcance. En la versión 2 del manual de arquitectura, el cronograma del equipo (oficios, cadencia, fechas) se sacó del Cap 7 y se mudó a PM/SISTEMA.md y PM.md. Esa decisión sigue vigente. Lo que entra acá es arquitectura operativa: el orden de los hitos del agente, qué entidad del repo activa cada uno, y qué decisión técnica destraba cada uno. Lo que NO entra es quién hace qué, en qué semana, con qué calendario. Eso vive en otro lado y no es ley del manual.

Una nota sobre la fuente operativa. El detalle granular del MVP — capability por capability, estado de cableado, lista exhaustiva de acciones del cliente y del dueño, capítulos sobre seguridad y pagos — vive en el MVP_MANUAL (00_EL PLAN/MVP/MVP_MANUAL.html en el Drive de Seba, fuente actualizada por el equipo en 2026-05-16). Este capítulo lo referencia como fuente de detalle pero ordena los hitos en términos arquitectónicos: qué destraba cada uno, qué entidad del schema lo materializa, qué decisión técnica lo condiciona.

Migración del MVP_MANUAL al repo como doc vivo: pendiente, ver issue del repo. Mientras tanto el link a Drive es la referencia.


El estado del agente al cierre del Cap 7 v1 (mayo 2026)

Cuando se escribió la primera versión de este capítulo (2026-05-03), convivían dos agentes: un prototipo en n8n cableado contra WhatsApp Cloud API en una instancia free de n8n.ticketsport.com.ar, y un target en el repo tomillo/perla con la plataforma técnica completa pero el módulo src/backend/agent/ vacío. El roadmap entonces era una secuencia de quince PRs para mudar el agente desde el prototipo al target.

Ese roadmap cumplió. Al cierre del 2026-05-14, los bloques originales A, B y C se mergearon en tomillo/perla:main consolidando el motor del agente: el módulo src/backend/agent/ tiene runtime, adapter al LLM, ocho tools con lógica real (no stubs), persistencia de agent_runs y tool_invocations, y la audiencia (customer | owner | staff) cableada como propiedad runtime que filtra el set de tools disponibles según el rol del remitente y defiende el lookup después de cada llamada al modelo. El dataset piloto se cargó en perla-testing ese mismo día — el agente tiene un negocio coherente sobre el que operar para validación end-to-end.

El agente n8n legacy todavía vive — el wire-up de los webhooks de WhatsApp al runtime del agente TS fue parte del merge del 14/05 (patrón P-09 webhook dispatch con waitUntil, Cap 5), y el agente TS responde mensajes reales. El agente n8n queda como prototipo histórico hasta que la validación end-to-end de Fase A cierre y se apague formalmente.


El movimiento, en una frase

El agente TS pasa de motor mergeado a producto vendible cuando puede atender a un negocio piloto de principio a fin: el cliente conversa por WhatsApp, sacó turno con seña, el dueño gestiona desde voz, el agente escala cuando hace falta, todo trazado y multi-tenant. Cada hito de este capítulo es un milímetro hacia ese instante.

Cada hito tiene su criterio de cierre concreto y su entidad del schema que activa. Algunos hitos son cosméticos para el agente pero estructurales para el negocio (apagar n8n, derogar la policy de pagos POST-MVP del repo). Otros son cambios profundos del runtime (orquestación de pending_operations, recordatorios proactivos con cron). Ninguno entrega valor solo — el valor está en la cadena cerrada de la Fase A primero, y de la Fase B después.


Fase A · piloto

La Fase A es el despliegue interno con tres a diez negocios piloto reales, con soporte alto del equipo Perla durante el primer mes. WhatsApp como canal principal, dashboard web y mobile para el dueño. Promesa central: mapeo 1:1 entre dashboard y WhatsApp — todo lo que el dueño controla por panel también lo controla por voz desde WhatsApp.

La Fase A termina cuando los pilotos operan estable, los flujos críticos están endurecidos, y el equipo Perla puede dejar de monitorear cada conversación en consola.

Los hitos arquitectónicos de Fase A, en orden:

Hito 1 · Validación end-to-end y apagado del agente n8n

Qué destraba: cierre del motor del agente. Es la señal de que la migración técnica que arrancó en marzo está consolidada. No es un ingrediente nuevo — es la prueba de que el motor mergeado funciona en condiciones reales.

Qué activa: el dataset piloto cargado en perla-testing (1 business perla, 6 staff, 5 servicios, 35 reglas de disponibilidad) sirve de fixture. Tomi mandó WhatsApp desde un teléfono customer real y desde un teléfono owner registrado en users.phone. El agente respondió en ambos canales, las filas correspondientes aparecieron en agent_runs y tool_invocations, las tools del owner devolvieron data filtrada por audiencia.

Criterio de cierre: el agente TS atiende correctamente customer y owner por WhatsApp en staging, las ejecuciones quedan trazadas, y el workflow Perla_mvp_v2.2.json se apaga en n8n.ticketsport.com.ar.

Hito 2 · Pagos por seña en producción del agente

Qué destraba: condición comercial de venta. Negocios reales como Suri operan solo con seña — sin esto el producto no funciona como propuesta vendible.

Qué activa: la tool create_payment_link que envuelve el módulo Mercado Pago del backend (preference + webhook + verificación HMAC). El flujo completo: cliente confirma slot → agente abre booking_hold + crea payment_intent con estado requires_action → manda el link init_point al cliente → webhook MP marca payment_intent.status = succeeded → libera hold + crea appointment con status = confirmed y código de confirmación.

Decisión derivada. La política perla-sensitive-operations-policy.md del repo tomillo/perla dice “señas POST-MVP, solo dashboard”. El MVP_MANUAL del 2026-05-15 deroga esa decisión — pagos pasan a ser capability del agente en Fase A. Esta tensión entre canon del repo y MVP_MANUAL es una ventana abierta a resolver con Tomi: el manual de arquitectura registra la decisión vigente, la policy del repo necesita actualización en paralelo. Ver ledger decisions/2026-05-16_signas-fase-a-tension-policy-repo.md.

Criterio de cierre: un cliente real paga la seña por Mercado Pago vía link generado por el agente, el webhook confirma el pago, el turno queda confirmed.

Hito 3 · Orquestación de pending_operations

Qué destraba: defensa contra LLM destructivo en acciones high-risk. Sin esto, una mala interpretación del owner (“cancelá todos los turnos del jueves”) borra la agenda sin reversa.

Qué activa: el patrón pending_operations materializa el Pliego 5 del Cap 6 — el agente arma un preview de la operación, persiste filas en pending_operations + pending_operation_items con confirmation_token_hash y TTL de quince minutos, responde al owner con la lista de los turnos afectados pidiendo confirmación verbal, y solo ejecuta la mutación cuando el siguiente mensaje del owner contiene el token (un “sí, dale” que el runtime matchea contra el hash).

El schema soporta este patrón hoy. Lo que falta es cablear la orquestación: detectar qué tool requiere preview, generar el resumen, validar el token, ejecutar la operación encolada, limpiar la fila al confirmar o expirar.

Criterio de cierre: las acciones masivas (cancelación batch, baja de servicios, modificaciones de catálogo cross-tenant) pasan por preview obligatorio antes de ejecutar.

Hito 4 · Mapeo 1:1 dashboard ↔ WhatsApp

Qué destraba: la promesa de marca central del producto. El dueño no abre la computadora para manejar el negocio — gestiona todo desde WhatsApp con la misma capacidad que tiene desde el panel.

Qué activa: las nueve tools del mapeo que faltan al cierre del 2026-05-16 — alta/baja/edición de clientes, servicios y profesionales — además de modificación de horarios del local y edición de FAQs/políticas. Cada tool reusa o crea su service en src/backend/<dominio>/ siguiendo el principio de cierre del Cap 5 (la lógica de dominio nunca vive dentro de src/backend/agent/tools/).

La capability request_handoff también entra acá: tool L2 que crea un handoff_request con motivo registrado, marca el thread como in_handoff, y el agente deja de responder hasta que el owner toma la conversación desde la bandeja del dashboard.

Criterio de cierre: la matriz Acción ↔ Tool del Cap 6a tiene todas las filas con ✅ en la columna “tool TS”. El owner gestiona clientes/servicios/profesionales/horarios completamente desde WhatsApp, sin abrir el panel.

Hito 5 · Inyección de assistant_configs al system prompt

Qué destraba: voz por business. Hoy cada nuevo piloto se siente “Perla genérica” porque la función buildSystemPrompt() del runtime no lee assistant_configs. Cuando se cablee la inyección, cada tenant va a tener su propia voz dentro del mismo agente.

Qué activa: plumbing trivial pero pendiente. El schema de assistant_configs está completo en el repo. Falta que prompt.ts del módulo del agente lea la fila del business y arme el prompt con name, greeting_message, response_tone, business_instructions, safety_instructions antes de pasarlo al LLM.

Criterio de cierre: dos businesses distintos con configuraciones distintas reciben respuestas con voz distinguible del agente para la misma consulta del cliente.

Hito 6 · Onboarding por business real

Qué destraba: cada negocio piloto requiere su propia configuración cargada antes de salir a producción. El dataset piloto del 14/05 sirve de fixture para validación pero no es un onboarding de cliente real.

Qué activa: el flujo de carga de un business desde cero — servicios + variantes + staff con horarios y bloques disjuntos + availability_rules + assistant_configs con voz y políticas customizadas + policy_sets con reglas internas + channel_connections con phone_number_id real de Meta + teléfonos del owner y staff en users.phone para resolver roles del agente.

Cómo se hace. El equipo Perla hace el onboarding asistido en la primera ola — entrevista pre-lanzamiento con cada beta, recolección de datos, carga manual contra perla-testing primero y después contra producción. El patrón se valida con los primeros tres a diez negocios. A medida que se consolida, se automatiza un onboarding self-service vía dashboard o wizard.

Criterio de cierre: dos negocios reales (distintos del piloto interno perla) están operando con su propia configuración cargada, sin compartir entidades con otros tenants.

Hito 7 · Producción en holaperla.com con monitoreo

Qué destraba: uso real con clientes que no son del equipo. Es el cierre operativo de Fase A.

Qué activa: el deploy productivo del Worker apunta al dominio holaperla.com. Los logs estructurados de agent_runs, tool_invocations, pending_operations y handoff_requests están instrumentados con un nivel de detalle que permita debuggear sin acceso al chat. El equipo Perla tiene rollback path documentado.

El campo source en appointments (perla / manual / importado / gcal) es ingrediente de este hito — sin él no se distingue qué turnos vinieron por el agente vs los cargados manualmente. Pendiente con Tomi según el Cap 13 del MVP_MANUAL.

Criterio de cierre: el dominio holaperla.com responde con el agente productivo, el monitoreo permite detectar y corregir incidentes en tiempo real, y el equipo Perla puede dejar de mirar cada conversación de los pilotos.


Fase B · deploy

Fase B es lo que falta para que Perla salga al público después del piloto interno. Capabilities que mueven la aguja comercial pero requieren más desarrollo y dependencias externas. Se construyen contra el feedback real de los primeros tres a diez negocios — lo que pidan y tenga sentido cablear, entra; lo que no, queda flagueado en proyectos paralelos del MVP_MANUAL.

Los hitos de Fase B no tienen un orden tan rígido como Fase A — la priorización emerge de los pilotos. Lista canónica:

Hito B-1 · Recordatorios proactivos con templates Meta

Qué destraba: promesa de venta central de Perla. “Te recuerdo los turnos” es exactamente lo que un kine o estética necesita y lo que mueve la decisión de pago del negocio. Sin esto, Perla vende como un agente de turnos genérico, no como secretaria.

Qué activa: cron handlers que disparan mensajes “tu turno es mañana a las diez de la mañana, ¿confirmás?” automático 24 hs y 2 hs antes de cada turno confirmado. Requiere templates WhatsApp aprobados por Meta para mensajes fuera de la ventana de 24 horas — trámite burocrático con tiempo de Meta que no se puede acelerar. Conviene arrancar el trámite Meta en paralelo a Fase A.

Criterio de cierre: clientes reciben recordatorios automáticos en hora, con tasa de no-show medible.

Hito B-2 · Transcripción de audios con Whisper

Qué destraba: los clientes argentinos mandan audios. Sin transcripción se pierde una porción importante de las conversaciones, especialmente del cliente menos alfabetizado digitalmente.

Qué activa: el agente recibe el audio del webhook de WhatsApp, lo transcribe con Whisper, persiste el transcript en audio_transcripts, y responde al cliente como si hubiera escrito. El prototipo existió en n8n; falta portar al runtime TS.

Criterio de cierre: un cliente manda un audio pidiendo turno, el agente lo transcribe, lo entiende, y responde con la disponibilidad correcta.

Hito B-3 · Turnos compuestos y recurrentes + paquetes

Qué destraba: patrones de negocio que aparecen en clínicas, estética y gimnasios — cliente con dos profesionales encadenados (limpieza facial + masaje), reservas que se repiten todos los miércoles, paquetes de sesiones con precio combinado.

Qué activa: las entidades del schema soportan turnos compuestos hoy (appointments + appointment_items); falta cablear el flujo conversacional del agente para que entienda “reservame los miércoles de este mes” y abra booking_hold por cada turno de la serie con seña por cada uno. Paquetes y bundles requieren entidad nueva o reuso de service_variants con metadata.

Criterio de cierre: un cliente reserva un paquete de cuatro sesiones, el dueño ve las cuatro en su agenda, las cancelaciones de la serie se manejan con criterio explícito.

Hito B-4 · No-show, confirmation activa y ventana de notificación del owner

Qué destraba: rituales operativos que diferencian un producto entregado de un producto crudo — el dueño marca “vino / no vino” después del turno, el cliente confirma activamente que va a venir respondiendo al recordatorio, el dueño configura horarios donde Perla no le pinguea.

Qué activa: UX a definir entre tres opciones (recordatorio de fin de día con lista, marcado turno a turno cuando pasa la hora, subsección en el dashboard), capability de confirmation activa con flagueo del cliente que no confirma, y campo de ventana de notificación del owner en assistant_configs o entidad relacionada.

Criterio de cierre: la tasa de no-shows del piloto es medible y el dueño tiene controles operativos para reducirla.


Fuera del MVP

Decisiones explícitas de scope-out. Lo que el equipo eligió no construir ni en Fase A ni en Fase B, con su razón.

  • Telegram como canal promocionado. El wire-up técnico existe en el repo y el agente atiende mensajes de Telegram como customer por inercia del merge del 14/05. Pero ningún piloto se vende sobre Telegram, y cualquier inversión adicional (templates específicos, audios por Telegram, schema change para que telegram_user_id resuelva staff y owner como el phone lo hace en WhatsApp) requiere justificación explícita. Hipótesis razonable: Telegram puede quedar obsoleto sin haberse usado en producción.
  • Multi-modelo por rol. Optimización de costo (Haiku para customer, Sonnet para owner) que se evalúa cuando haya volumen real para medir. Hoy LLM_* env vars soportan un único modelo activo.
  • Bring-your-own-number. Todos los pilotos usan el número Perla compartido (+54 9 341 3 675 979). La integración con número propio del negocio queda como proyecto paralelo del Cap 14 del MVP_MANUAL.
  • Tools de gestión del catálogo desde WA. Subir precios, crear servicios o modificar disponibilidad desde WhatsApp existen como hito de Fase A (mapeo 1:1) — lo que sale del MVP es la sofisticación: importadores masivos, gestión de promociones temporales, configuración de paquetes complejos. Owner usa dashboard para eso.
  • Lista de espera, tracking de abandono, reviews automatizados, reportes. Capabilities valiosas que viven como proyectos paralelos del Cap 14 del MVP_MANUAL. Cuando llegue su turno no se descubren desde cero.

Cómo este capítulo se relaciona con los otros

  • Cap 1 (Arquitectura) define los cinco principios que cada hito del roadmap respeta. El más sensible para este Cap 7 es el principio 4: “el prototipo valida, la producción robusta produce. Nunca al revés”. El agente n8n validó conceptos; el agente TS los produce. El roadmap mueve cosas del prototipo a la producción, no inventa producción.
  • Cap 2 (Programa) define los cuatro perfiles que el agente atiende. La Fase A cubre customer + owner por WhatsApp con tools cableadas para los dos; los perfiles staff y operador del equipo Perla aparecen como capabilities de Fase B (handoff, dashboard del equipo).
  • Cap 3 (Circulaciones) define el routing. El patrón P-09 (webhook dispatch con waitUntil, descrito en Cap 5) es Cap 3 materializado en código del agente. La orquestación de inbound_message_batches que entra en Fase B implementa otra capa de routing.
  • Cap 4 (Instalaciones) y su Anexo 04a definen las entidades. Cada hito activa una o varias entidades de las Familias 4 (Conversación) y 5 (Agente y operaciones).
  • Cap 5 (Construcción) define el stack y los patrones del runtime. Los patrones P-09 y P-10 (webhook dispatch y audience role) destilados al cap 5 son los cimientos sobre los que cada hito del Cap 7 opera.
  • Cap 6 (Pliegos) define los contratos. Cada hito cumple uno o más pliegos: Hito 2 cierra Pliego 4 (pagos), Hito 3 cierra Pliego 5 (pending_operations), Hito 4 cierra el resto de Pliego 1.
  • Cap 6a (Matriz Acción ↔ Tool) es el contrato que cada hito actualiza. Cuando una tool TS pasa de ❌ a ✅, la matriz se actualiza en el mismo PR que activa la tool.

Lo que este capítulo NO incluye

  • Quién hace cada hito, en qué semana, con qué cadencia. Eso vive en PM/SISTEMA.md + PM.md desde abril 2026.
  • El detalle granular del MVP — capabilities por capability, lista exhaustiva de acciones del cliente y del dueño, estado de cableado al día. Vive en MVP_MANUAL.html (Drive, migración al repo pendiente).
  • Las firmas exactas de cada tool. Viven en Cap 6 (Pliego 1) y se materializan en src/backend/agent/tools/<tool>/contracts.ts cuando aparece el código.
  • Los detalles de implementación. Viven en código.

Por qué este capítulo importa

Sin orden, los hitos se transforman en features que compiten por atención. Sin criterios de cierre, cada PR se mergea cuando alguien decide que está listo. Sin acuerdos previos, cada hito descubre en revisión que tenía un supuesto que nadie cerró.

Este capítulo formaliza el orden temporal y los gates entre cada hito para que el avance sea acumulativo. La Fase A no termina hasta que su último hito cierra; la Fase B no arranca hasta que la Fase A está estable. Cada hito tiene una definición de “listo” verificable contra el código, no contra la opinión.

El capítulo importa especialmente porque el agente es la pieza que más diferencia a Perla del resto. El dashboard es importante pero replicable; las tools de gestión son importantes pero conocidas; lo único que un competidor no puede copiar en un fin de semana es un agente que entiende intención, ejecuta tools con criterio, gestiona pagos sin alucinar, escala cuando hace falta, y respeta multi-tenancy en cada query. Construirlo despacio, hito por hito, y no en un sprint épico que rompe cuando el primer cliente prueba algo no previsto.

Cuando Fase A cierre, el Cap 7 entrará a su v3 con foco en Fase B y con los aprendizajes operativos de los primeros pilotos. Cuando Fase B cierre, el agente entra a régimen de producto vendible y el roadmap pasa a ser priorización por demanda del mercado real.


v2 · 2026-05-16. El roadmap n8n → repo de la v1 cumplió: el motor del agente está mergeado en tomillo/perla:main desde el 14/05, las ocho tools del Pliego 1 con lógica real, audiencia cableada como propiedad runtime, dataset piloto cargado. Esta versión reescribe el cap contra la nueva naturaleza del trabajo: Fase A · piloto + Fase B · deploy. Fuente operativa de detalle: MVP_MANUAL.html en Drive (migración al repo pendiente).