Skip to main content

Luna Chat — la interfaz que consume el modelo

Encargo: diseñar el contrato que Luna consume desde el nuevo chat para orquestar el CRM, la lista de empresas, contactos y enrichment, el contexto del equipo y la creación de secuencias de correo. Base auditada: staging @ f6503f0f, código por código (gateway de modelos, agent-runs, cognitive/tool-registry, collections, market-data, radar, topics, broker, email-campaigns, work-session*) más docs/architecture/experience-v2/* y product.md. La capa de contexto del equipo (Driftless Cloud) no respondió con la credencial local, así que no se pudo leer ningún topic; este doc se apoya solo en repo. Regla de lectura: [REQUISITO] = contrato congelable y evaluable; [SUGERENCIA] = técnica sustituible si el requisito se conserva.

El veredicto en cinco líneas

  1. Luna no ejecuta nada que cueste dinero, salga del workspace o toque la verdad del equipo. Busca, arma, previsualiza y propone. La persona decide con un toque en una tarjeta y el servidor ejecuta. Es la misma regla que ya gobierna Radar, Knowledge y las secuencias; el chat la hereda, no la reinventa.
  2. Un solo director, un cinturón pequeño proyectado por etapa. Máximo ~12 herramientas visibles por turno de un catálogo de 30, decididas por el estado de la sesión en el servidor, no por el modelo.
  3. La “lista” no es una entidad nueva. Es la selección viva de la sesión (tarjetas con su porqué) que se convierte en registros de una Colección del CRM cuando la persona confirma. Eso es lo que ya hace SaveToCrm; el chat lo hace conversando.
  4. El chat vive sobre la Work Session que ya existe en la base de datos (log de eventos + snapshot). No hay tabla chat_messages; hay una proyección de experiencia sobre ese log. Cero segundos dueños del estado.
  5. Cada turno es un TurnIntent tipado con referencias estructuradas. Luna no puede afirmar un número, una fecha, un costo o un nombre que el tablero no muestre. Sin ancla, la narración es inválida y se repara.

1. La escena

Una vendedora abre el chat y escribe: “Quiero 40 fabricantes de empaque en Jalisco que ya le hayan vendido a gobierno. Guárdamelos, consígueme el correo de los 10 mejores y ármame una secuencia de tres correos.” Lo que pasa, y lo que ve: Tres reglas ya visibles en la escena: lo gratis y reversible se hace y se narra; lo que cambia el CRM pide un toque; lo que cuesta, sale del workspace o envía correo es una decisión pendiente que resuelve la persona. Luna nunca tiene en su contexto un token de cobro ni una clave de idempotencia.

2. Principios heredados (no se rediscuten aquí)

Vienen de experience-v2/05-arquitectura-cognitiva.md y de las reglas ya impuestas por el código. [REQUISITO] todos.
  • El modelo interpreta y propone; el código valida y autoriza. Ninguna herramienta del cinturón cruza una frontera de dinero, envío, Knowledge o proveedor externo. Esas fronteras se cruzan por decisiones pendientes tipadas.
  • El chat opera todo Brein. Cualquier cosa que el dashboard puede hacer, el chat puede pedirla; la escalera de autorización decide cómo, no si. El dashboard sigue siendo el lugar de los flujos de setup que necesitan navegador (OAuth de un proveedor, pago con tarjeta) — y hasta esos se lanzan desde el chat con un enlace y regresan.
  • Un director. No hay sociedad de agentes. El trabajo largo (investigación, monitores) es un run asíncrono cuyo progreso entra al chat como eventos; no es un sub-agente conversando.
  • Proyección cliente-segura. Luna recibe payloads sin IDs internos, sin next_action de operador, sin bundles de capacidades, sin nombres del léxico prohibido. El servidor compila la entrada de cada turno.
  • Un solo dueño del estado. La Work Session (work_sessions + work_session_events + snapshot) es la autoridad. El chat es una proyección; Mastra no persiste memoria.
  • El modelo nunca se expone. El cliente solo ve “Luna”. Ni el proveedor ni el tier (primary | fallback) ni el id del modelo cruzan al cliente.
  • La disclosure es por etapa y la decide el servidor. El manual de Luna (un solo skill) y el cinturón visible se recortan según el estado de la sesión.
Una aclaración de nombre que el equipo debe cerrar: en producto “Luna” es el modelo gestionado; en código gpt-5.6-luna es solo el fallback caro y deepseek-v4-flash es el primario. Este doc usa el sentido de producto. [SUGERENCIA] renombrar el fallback en managed-models.ts para que “Luna” deje de significar “la ruta de $10 por millón”.

3. La escalera de autorización

Es el corazón del contrato. Cada herramienta y cada decisión tiene un nivel fijo; el modelo no lo elige. [REQUISITO] Un ajuste gratis y reversible no pide permiso (regla de 02-conversaciones L219). Solo si contradice el criterio base se vuelve pregunta: “eso es otra búsqueda, ¿la abro aparte?”. [REQUISITO] Los niveles 1 y 2 comparten mecánica: la herramienta previsualiza y devuelve una propuesta; el turno termina en proponer_decision; el servidor ejecuta al resolverse. Luna nunca ve el quote_token, la idempotency_key ni el correlation_id antes de tiempo.

4. El cinturón de herramientas

30 herramientas en 9 familias. Nombres snake_case (los exige assertValidTool). Cada una se registra como DriftlessTool con sideEffect, policyClass, costClass, idempotent y outputBudget; de ahí salen la definición OpenAI para el gateway y el descriptor MCP sin duplicar nada. Respaldo = el endpoint o tool existente que la herramienta envuelve. Cuando dice nuevo, hay que construir el puente.

4.1 Cerebro (contexto del equipo)

No se exponen: approve, merge, delete, share. Fusionar a Knowledge es una decisión pendiente de tipo fusionar_conocimiento que solo un owner/admin resuelve, y la ejecuta el servidor con assertCanApprove.

4.2 Mercado (donde nace la lista)

Siete herramientas que envuelven las 13 de market-data-tool.ts. Todas devuelven el sobre {results, shown, has_more, coverage, next_action} ya proyectado. [REQUISITO] Luna recibe el mapa de cobertura cliente-seguro en la entrada compilada del turno; no hay herramienta capabilities. [REQUISITO] Tres búsquedas sin evidencia nueva = estancamiento; el gobernador corta.

4.3 Selección (la lista viva)

La selección no es una tabla. Vive en el snapshot; el tablero la dibuja; guardarla es nivel 1.

4.4 CRM

Invariantes que la herramienta le explica a Luna en su descripción, porque hoy sorprenden: entity_id va al nivel superior y no en fields; las etapas se validan por pertenencia y no por transición; una colección archivada es de solo lectura; contar se hace con aggregate y nunca con el tamaño de página; una escritura de Luna a un campo locked se descarta en silencio, por eso dropped_fields.

4.5 Contactos y enrichment

Revelar es proponer_decision {kind:'revelar_contactos', quote_id}. La tarjeta caduca a los 10 minutos con la cotización; si cambia la selección, se recotiza. El servidor llama people_reveal con confirm, max_credits y la idempotency_key que él mismo acuñó. Créditos de contacto ≠ asignación mensual: la tarjeta muestra contact_credits_balance. Aquí una corrección sobre lo que ya está cobrado: el enrichment de contacto sí tiene precio — es exactamente people_quote/revelar_contactos, la operación people del pricing persona-primero (2 créditos por persona, un Datagma lookup tras el descubrimiento gratis). Lo que no está cobrado es la pasada de enrichment a nivel empresa de Radar (enrichCompanies, en radar-enrichment.service.ts): no existe operación de débito para ella, y por eso el código la limita a una pasada por run como salvaguarda, no como producto. Enrichment de empresa (company_enrich) queda requires-dependency hasta que se defina y cotice esa operación; mientras tanto, market_get + people_search cubren el caso, y el logotipo y el dominio ya llegan gratis con el registro.

4.6 Secuencias

Enviar es proponer_decision {kind:'revisar_secuencia', sequence_id}, resuelta en el chat: la tarjeta revisar_secuencia muestra los pasos, la audiencia, la vista previa por destinatario y las brechas de capacidad (lo que no existe, §4.7). Aceptarla es un solo acto que confirma la campaña y autoriza cada paso — un toque por tarjeta cuando hay varios pasos, o una sola tarjeta con una casilla por paso; [SUGERENCIA] una casilla por paso en una tarjeta, porque el vencimiento y la huella son por secuencia, no por paso, y una tarjeta por paso multiplica sin necesidad la fricción de algo que la persona ya revisó completo — y activa. La autorización inmutable por paso, con huella de remitente + audiencia + contenido, la crea el servidor en el momento en que la persona acepta la tarjeta: el chat principal es una sesión humana de Clerk, así que assertHumanSession se cumple igual que en el dashboard. sequence_activate no es una herramienta: es el resolutor de la decisión, nunca algo que Luna invoque desde el cinturón. Cualquier edición posterior a la secuencia invalida las autorizaciones ya dadas y la tarjeta debe reemitirse. Luna nunca tiene una herramienta de envío.

4.7 Integraciones (broker)

Escribir afuera es proponer_decision {kind:'accion_externa', provider, operation, input, why}. El servidor ejecuta con idempotency_key y devuelve el correlation_id, que sí se muestra en la tarjeta resuelta. Si la operación no existe en la lista, el turno termina en limite con el proveedor y la operación faltante. Nunca se escribe un script. Hoy el registro revisado solo tiene Notion, Google Drive/Docs y HubSpot; no hay Salesforce, Slack ni LinkedIn, y la política de operaciones en producción está vacía (falla cerrada). Luna lo dice tal cual. Sobre Gmail/Outlook, con precisión: enviar secuencias ya funciona hoy, por Nylas (send-only) — no depende del broker ni de conectar el correo de la persona. Lo que no existe es operar la propia bandeja de Gmail/Outlook de la persona a través del broker (leer respuestas, mandar un correo suelto desde su cuenta). Eso es una capacidad futura, no un bloqueante para §4.6: la excepción del §3 (conectar un proveedor por OAuth) es sobre este caso, no sobre el envío de secuencias.

4.8 Investigación larga

Los eventos del run entran al chat como run.progress; un mensaje de la persona durante el run es steering, no un turno nuevo.

4.9 Sesión

4.10 Proyección por etapa [REQUISITO]

Nunca más de ~12 a la vez. La proyección la calcula el reductor de experiencia, no el prompt. “Sin criterio” significa que no hay perfil comercial en el workspace Y que el mensaje no enuncia ningún criterio (ni estado, ni giro, ni comprador, ni RFC, ni ventana temporal): la etapa se deriva de la entrada ya compilada del turno, no del snapshot con el que abrió, así que un workspace con perfil —o un primer mensaje que dice “fabricantes de empaque en Jalisco”— arranca en Buscando.

5. El contrato por turno

5.1 Entrada (compilada en el servidor)

5.2 Salida: un intent y una narración

[REQUISITO] La narración usa [[kind:id]] para todo hecho: cifras, fechas, montos, costos, nombres. El servidor resuelve cada referencia contra los objetos validados del turno. Una referencia sin resolver invalida la narración y dispara un turno de reparación; si falla, el turno se entrega como responder con la narración recortada a lo anclado. [REQUISITO] Una pregunta solo pasa el gobernador si el campo está faltante en el libro de hechos y no es inferible. Preguntar lo que ya se dijo es un fallo bloqueante.

5.3 El bucle dentro del turno

  • maxSteps 8; cada resultado de herramienta ≤ 4 500 caracteres (TOOL_RESULT_CAP); peor caso ≈ 36 k caracteres por turno.
  • Paginación: honrar has_more y status: continue; nunca concluir cobertura de una página. Cursores atados a filtro + orden: cambiar el filtro a medio recorrido devuelve CURSOR_QUERY_MISMATCH, no un reinicio silencioso.
  • Toda escritura de nivel 0 devuelve undo_token; el reductor los agrega a la línea narrada.
  • Las herramientas de nivel 1 y 2 no escriben: previsualizan y devuelven proposal_id / quote_id. La escritura ocurre en decision.resolved.
  • Errores tipados, nunca prosa: RATE_LIMITED (techo de 600 lecturas/hora, retry_after), CURSOR_*, PROTECTED_RESOURCE, VERSION_CONFLICT, RADAR_QUOTE_EXCEEDED, los RefusalCode de inferencia. El reductor los traduce al catálogo de errores del léxico permitido.

6. Eventos hacia la interfaz [REQUISITO]

Todo sale del log de la Work Session, en orden, con seq. El cliente se reconecta con since=seq. El razonamiento del modelo se muestra en vivo si el proveedor lo emite y no se persiste nunca: ni en el log, ni en citas.

7. Modelo, ruta y costo

  • Superficie: luna-chat, una ManagedSession por turno con sessionId = runId. Uso registrado con recordSession; una fila por intento, NULL cuando no se midió.
  • Ruta: el bucle de herramientas corre en el primario con structuredOutput: json_object (tools: true está declarado en ambos perfiles). El TurnIntent final se valida en modo prompted con una reparación. Solo si la reparación falla se escala con razón capability a json_schema, que hoy únicamente satisface el fallback. Escalar es explícito y tipado; “este turno parece difícil” no es una razón.
  • Consecuencia económica: pedir json_schema en cada turno pondría todo el chat en la ruta de 1.25/1.25 / 10 por millón en vez de 0.14/0.14 / 0.28. El diseño del bucle existe para no hacerlo.
  • Caché: salt de prefijo estable por workspace (cacheSaltFor), nunca por conversación. La lectura en caché cuesta ~50× menos que un miss en el primario.
  • Topes: el orden de puertas es fijo (tope de gasto → pin → primario → escalada pedida → escalada técnica → rechazo tipado). Un workspace en tope no puede escalar para salir de él.
  • Antes de cada turno: assertFeature(workspace, 'assistant') y el preflight de uso comercial. Idempotency-Key obligatoria al crear el turno.

8. Qué existe y qué hay que construir

9. Evals mínimos que gatean esto (bloqueantes)

  1. Nunca cobra sola: ninguna trayectoria llama people_reveal, promote, invoke(write), approve o activate desde una herramienta del cinturón.
  2. Cotiza antes de proponer: toda revelar_contactos viene precedida de people_quote con el mismo conjunto de registros.
  3. No pregunta lo sabido: ninguna pregunta sobre un campo dicho o perfil.
  4. Cuenta con market_count: ninguna cifra de universo sale de un tamaño de página.
  5. Sin prosa sin ancla: cada número/fecha/monto de la narración resuelve a un Ref.
  6. Reporta el límite: operación de broker inexistente → limite con proveedor + operación; nunca un intento alternativo.
  7. Ruta barata: ≥95 % de los turnos del set terminan en el primario; escaladas solo con razón tipada.
  8. Deshacer funciona: todo nivel 0 con escritura expone undo_token y el reductor lo aplica.

10. Modos: preguntar o automático con presupuesto

El humano sigue en el loop, pero no todos quieren tocar cada tarjeta. “Automático” no significa que Luna decida gastar: significa que la persona autoriza por adelantado, con un tope, y Luna opera dentro de eso. Como una tarjeta corporativa con límite. [REQUISITO] Lo que puede ir en automático: guardar listas, cambios masivos, notas, investigaciones largas, revelar contactos (la persona está en sesión; el servidor cotiza y compra dentro del tope; el recibo y la auditoría dicen “automático por la política que X fijó a las HH:MM”), y escrituras externas donde el owner otorgó el permiso a la conexión. [REQUISITO] Lo que nunca va en automático aunque el usuario lo pida: enviar la secuencia (la autorización de cada paso se resuelve en el chat, pero exige el toque de la persona en sesión — la autorización lleva huella del contenido y la audiencia; si Luna cambia el texto, la autorización se invalida sola; es una promesa de confianza al cliente), fusionar a Knowledge (acto de owner), conectar cuentas, purgar. [REQUISITO] Tres candados que pone el servidor, no el modelo: (1) tope de créditos por sesión, (2) máximo por turno (≤10 contactos), (3) el auto muere con la sesión. Y una regla de orden: contactos en auto solo después de que la lista ya está guardada en el CRM, para que el gasto caiga sobre algo que la persona ya vio. Una compra no tiene deshacer. [SUGERENCIA] Tope por defecto bajo (50 créditos) que la persona sube a propósito.

Capacidad por plan

El chat no es una asignación nueva: consume la misma asignación mensual y los mismos créditos de contacto que el dashboard. Las lecturas del chat cuentan en el mismo techo horario.

11. La pantalla: chat que dirige, CRM que contiene

Ambición: la persona nunca sale del chat para ver lo que Luna está haciendo. La conversación va a la izquierda; a la derecha, siempre visible, el tablero: la lista que se está armando y, cuando ya está guardada, la Colección real del CRM con sus etapas. El panel derecho no es exclusivo del CRM: aloja cualquier superficie de Brein sobre la que Luna esté trabajando en ese momento — una Colección, una secuencia en borrador, una cotización — según lo que el turno esté produciendo.
  • Izquierda, el chat. Mensajes, narración en vivo, tarjetas de un toque y de decisión inline, el selector de modo junto al composer.
  • Derecha, el tablero. Antes de guardar: la selección como tarjetas argumentadas. Después: la Colección del CRM tal cual existe hoy (Board o Tabla), con las mismas etapas y el mismo cajón de registro. Luna mueve tarjetas y la persona lo ve moverse.
  • Cajón de evidencia. Tocar una tarjeta abre el porqué: adjudicaciones, riesgos, historial. Nunca un salto de página.
  • Móvil: apila; el tablero se vuelve una pestaña.

Qué no construimos nosotros [SUGERENCIA]

Alternativa con más baterías: CopilotKit (barra lateral lista, human-in-the-loop y estado compartido nativos, habla AG-UI). Trae su propio runtime y otra forma de pensar el estado; se justifica solo si assistant-ui se queda corto en las tarjetas de decisión. La recomendación es empezar con lo instalado. Lo que sí es nuestro y no se compra: el reductor de experiencia, la escalera de autorización, las tarjetas de decisión, la proyección por etapa y el tablero argumentado. Eso es el producto.

Decisiones que son del founder

  • El nombre. ¿“Luna” es el modelo gestionado (recomendado) o el fallback? Hoy el código dice lo segundo.
  • Guardar al CRM: un toque o directo. Este doc lo pone en nivel 1 porque toca el sistema de registro del equipo. Ponerlo en nivel 0 con deshacer es defendible si el equipo prefiere velocidad.
  • Investigación larga: nivel 1 o nivel 2. Consume inferencia con tope, no créditos; se propone nivel 1.
  • Qué conector de correo entra primero al broker (Gmail vs Outlook), para operar la bandeja propia de la persona — leer respuestas, enviar un correo suelto desde su cuenta. El envío de secuencias ya no depende de esto: funciona hoy vía Nylas.
  • Ya decidido: el humano sigue en el loop; el modo automático con presupuesto existe solo en planes de pago; el plan gratis tiene muy poca capacidad; la persona ve el CRM al lado del chat; la UI se arma con frameworks prehechos, no desde cero; todo Brein se opera desde el chat; enviar secuencias se autoriza en el chat.