Saltar al contenido principal

0010 — Los subflujos se aplanan al compilar

Fecha: 2026-09-23 · Estado: aceptada

Contexto​

Hasta ahora nada era reutilizable entre bots. bot_flow_versions está clavada a un bot_id con FK y CASCADE, y las plantillas del editor son un catálogo estático del bundle del frontend, con ids de nodo fijos y semántica de reemplazo total del lienzo. Un tenant que quiere el mismo trozo de conversación —"verificar identidad", "agendar", "cobrar"— en cinco bots lo dibuja cinco veces y lo corrige cinco veces.

Decisiones de producto (2026-09-22): la biblioteca es del tenant; el bot fija una versión concreta y editar el subflujo no le afecta hasta que actualice; se comparten todas las variables de la conversación; y se editan en una pantalla propia con el mismo lienzo.

Decisión​

El subflujo se aplana en tiempo de compilación: antes de construir el grafo, cada nodo "Subflujo" se sustituye por los pasos de la versión que tiene fijada, con los ids prefijados sf__<idDelNodo>__<idInterno>. No se usan los subgrafos de LangGraph (addNode(nombre, grafoCompilado)).

Por qué no subgrafos​

En este motor todo se indexa por el id plano del nodo. Un subgrafo real convertiría cada subflujo en UN solo nodo para seis mecanismos a la vez:

MecanismoQué pasaría con un subgrafo
Router de START (resume.nodeId, enterAt)No se puede reanudar ni volver a un paso interno: el pathMap solo conoce el contenedor
Medición por nodo (bot_node_runs, bot_nodes_daily)Una fila por subflujo entero: se pierde qué paso falla, cuesta o tarda
Continuación de bot_approvalsSu node_id no casaría con ninguna arista del grafo
Guarda de interrupts huérfanos (turn-input.ts)tasks[].name pasa a ser el nombre del contenedor
Send de "Repetir por cada elemento"Solo funciona dentro del mismo grafo
defer de "Juntar respuestas" y el graph.stream de BotsServiceCambian de ámbito y de nombre

Aplanando, los seis siguen funcionando sin tocarlos. Solo cambian los ids.

El prefijo​

sf__<idDelNodoQueLoInserta>__<idInterno>, y anidado compone solo (sf__c1__sf__c2__x).

  • Lleva el id del contenedor, que es estable en el flujo del bot: así el mismo subflujo se puede insertar dos veces sin chocar, y siempre se puede volver del id expandido al nodo que el usuario ve en el lienzo (containerOf).
  • El separador no puede ser : ni |: LangGraph los reserva para el espacio de nombres de sus propios subgrafos y rechaza el nodo al compilar. Los ids que genera el editor son <tipo>-<fecha>-<n>, así que __ no aparece en ninguno; además, el editor rechaza un id escrito a mano que empiece por sf__.

Consecuencias, y dónde se resuelven​

  • La revisión se calcula sobre el flujo expandido. computeFlowRevision no cambió: cambia lo que se le pasa. Efecto buscado: re-fijar una versión de contenido idéntico da la misma revisión y no manda a Inicio a las conversaciones vivas. Y un flujo sin subflujos devuelve el mismo objeto, así que su revisión no se mueve ni una coma.
  • El flujo expandido vive en BotGraphInstance (bots.registry.ts), junto a su revisión y a los problemas de la expansión. Quien casa un id con el grafo mira ahí: la medición al registrar la revisión (si no, los pasos internos se quedan sin etiqueta), las aprobaciones al buscar la rama de continuación, y el inspector al etiquetar y al calcular resumeStale.
  • Un subflujo que no se puede resolver falla el turno. El nodo sobrevive a la expansión y el runtime lo compila como un nodo que lanza. Saltarse en silencio un paso que puede ser "verificar identidad" es peor que fallar.

Versión fijada e inmutabilidad​

bot_subflow_versions es inmutable, con trigger. Es lo que hace que "versión fijada" signifique algo: si el contenido de una versión pudiera cambiar, los bots que la usan cambiarían de comportamiento sin enterarse. De paso hace el grafo de referencias acíclico por construcción: solo se puede fijar una versión que ya existía.

Publicar una versión no recarga ningún bot. Actualizar es un acto explícito por bot, y no es gratis: cambia la revisión de su flujo, así que sus conversaciones en curso vuelven a empezar por Inicio, como con cualquier edición.

Accesos​

  • bot_subflow_versions: cerrada a anon y authenticated, como bot_flow_versions. flow_definition es código ejecutable y puede traer secretos dentro (las cabeceras de un nodo "Llamar API" en línea). Solo la lee el backend con service-role.
  • bot_subflows (la metadata): GRANT SELECT con política de pertenencia y bot.view. La necesitan el selector del nodo y el aviso de "hay versión nueva", y no contiene nada sensible.
  • Sin permisos nuevos. Editar un subflujo es un subconjunto estricto de lo que bot.update ya concede —quien puede editar el flujo de un bot ya puede dibujar esos mismos nodos dentro del bot— y publicar no afecta a nadie. Un permiso nuevo obligaría a un backfill por rol en todos los tenants, con riesgo de dejar fuera a quien hoy edita flujos sin ser admin.

Variables​

Se comparten todas las variables de la conversación: no hay entradas ni salidas declaradas. Es lo más simple de entender y lo que pidió el usuario, pero tiene un riesgo que conviene decir en voz alta: un subflujo que escribe result pisa el result del flujo que lo usa. Convención recomendada: prefijar las variables del subflujo con su código (identidad_cedula).

Las salidas sí son explícitas: cada nodo Fin con output_name es un handle del nodo que lo inserta. Si no se nombra ninguna, el subflujo tiene una sola salida, "Fin".

Fuera de alcance (a propósito)​

  • Anidar subflujos. Las guardas de profundidad y de ciclo están implementadas, pero el editor no deja hacerlo todavía.
  • Actualizar la versión en lote. Escribir treinta flujos, crear treinta versiones y reiniciar treinta conjuntos de conversaciones vivas merece su propia fase, con vista previa y confirmación.
  • Un subflujo como cuerpo de "Repetir por cada elemento". La regla "el cuerpo converge en Juntar respuestas" cruza la frontera y el editor del bot no puede comprobarla.
  • Agrupar métricas por subflujo en el informe y ofrecérselos al asistente. El prefijo deja ambas cosas preparadas.