Doctor era un asistente personal que construí para un abogado en Hermes, y una mañana le llegó un audio: mientras caminaba a tribunales, dictó los hechos de un caso de tenencia. Un minuto después el mismo asistente le respondía por mensaje de Instagram a un desconocido que preguntaba a qué hora podía pasar por el estudio. Mismo asistente, misma conversación, misma memoria. El contexto que acababa de leer un expediente confidencial estaba a un mensaje de distancia de una persona que nunca había visto.

Como asistente personal, funcionaba bien. Incluso hablaba con la voz del propio abogado: tomé audios que había mandado por WhatsApp y se los pasé a ElevenLabs para clonarla. El problema es que el asistente completo de un estudio jurídico no es un asistente personal. Maneja expedientes privilegiados de clientes, lee documentos y páginas web que no controlamos, y habla con el mundo exterior por Instagram y mail. Simon Willison tiene un nombre para esas tres capacidades cayendo en un mismo lugar: la trifecta letal. Datos privados, input no confiable, y una forma de sacar datos afuera. Cualquier agente que tenga las tres puede ser dado vuelta por un solo mensaje envenenado (un mensaje directo armado, un PDF con trampa) para filtrar el expediente de un cliente, sin que haya un bug en tu código. Su conclusión es la incómoda: detectar no alcanza, porque en seguridad, atrapar el 95% es un aplazo.

No hay prompt que te saque de ahí. "Por favor no exfiltres datos privados" es una promesa, no un límite. Necesitábamos una solución arquitectónica, una que haga los datos privados inalcanzables y a la inyección de prompts inútil. Necesitábamos que el límite fuera estructural: un agente que simplemente no tiene ninguna herramienta ni memoria que llegue a los datos del cliente no puede filtrarlos, no importa lo que un mensaje le diga que haga.

Un equipo, no un súper-agente

El giro llegó leyendo sobre el dino-assistant de Tute Costa, un asistente personal prolijo construido sobre Mastra. El patrón supervisor hizo clic. Un supervisor delega en subagentes a través de un mapa de agentes; cada subagente tiene sus propias herramientas, modelo, instrucciones y memoria; Mastra deriva un resource id separado por agente para que dos nunca compartan memoria por accidente. El aislamiento es lo que viene por defecto, no algo que te tenés que acordar de activar. Así que dejamos de tratar de hacer un único agente cuidadoso y armamos un equipo donde ningún miembro puede traicionar a un cliente, porque ningún miembro tiene las tres patas.

Renombramos el asistente de doctor (el honorífico de un abogado) a Pretor, el magistrado romano que impartía justicia.

        Owner (el abogado)
              |
        Telegram, solo owner
              v
   +-------------------------------------------+
   |  PRETOR                                    |
   |  supervisor - voz - ruteo                  |
   |  no tiene datos; delega por referencia     |
   +-------------------------------------------+
      |             |             |           |
      v             v             v           v
 Investigador  Bibliotecario  Escribientes  Secretario
 legal + web   cada caso      redacta docs  agenda +
 investiga     + documentos    por fuero     notas de
 (sin datos)   acceso ÚNICO    (no envía)    reunión
                   ^              |
                   +-- pide datos del cliente

   PRETOR  ...A2A: texto del mensaje + id...>  COMUNICADOR
                                               mensaje de Instagram
                                               servicio aparte
                                               (sin datos)

Seis roles, cada uno deliberadamente parcial:

  • Pretor es el supervisor y el único contacto del abogado en Telegram. Rutea y habla; nunca trae datos crudos del cliente a su propio contexto.
  • Investigador hace investigación legal y web sobre fuentes oficiales argentinas (SAIJ, CSJN, Infoleg). Toca páginas no confiables y puede traerlas, pero no tiene datos del cliente, así que una página envenenada no tiene nada para robar.
  • Bibliotecario es la memoria del estudio: cada caso, cada documento, un solo almacén. Es el único agente que puede leerlos, y no tiene ningún canal hacia afuera.
  • Escribientes, uno por fuero (laboral, civil, penal, comercial, familia, contencioso), redactan los documentos de su rama. Ven los datos del cliente solo como la respuesta a una pregunta que le hicieron a Bibliotecario, y no pueden enviar nada. Sus borradores respetan el formato oficial de presentación hasta los márgenes y la numeración, renderizados a partir de un modelo guardado del tipo de documento real.
  • Secretario lleva la agenda, el mail y las notas de reuniones. Sus acciones hacia afuera, como invitar a un tercero externo, requieren aprobación.
  • Comunicador responde Instagram. Corre como un servicio aparte, con su propio webhook y sin ningún acceso a los datos del cliente. Un mensaje envenenado no encuentra nada privado.

Puesto en una tabla, el punto es que a cada fila le falta una pata:

Agente Datos privados Input no confiable Envío externo Pata que falta
Bibliotecarionosin salida
Investigadornonada privado para robar
Comunicadornonada privado para robar
Secretariosolo agendacon aprobaciónsin expedientes, envío con permiso
Pretorpor referenciavía otrossigue siendo un router

Donde las patas todavía podrían juntarse es en Pretor, porque ve todo. Tres cosas lo mantienen como router y no como blanco: delega por referencia en vez de copiar los expedientes del cliente a su contexto, Comunicador corre fuera de proceso y recibe solo el texto de un mensaje y el id del emisor, y su única conversación real es con el dueño de confianza. Para cualquier cosa hacia afuera e irreversible hay una persona en el circuito: la acción se suspende y aparece en Telegram como un único botón inline. El abogado aprueba, o no pasa.

Nunca inventar la norma

La división de la trifecta evita que se filtren los datos. Una segunda regla evita que el asistente mienta sobre la ley, y importa igual de mucho: nunca inventar la norma. Citar una fuente oficial o decir claramente que no se pudo verificar.

Hacer que eso se cumpla empieza por llegar de verdad a las fuentes, que es una pelea en sí misma. Las bases de datos oficiales de Argentina no están hechas para que las lea una máquina: algunas bloquean el acceso automatizado de una, otras ponen antes de cada consulta una barrera pensada para dejar a los bots afuera. Llegar a ellas de forma confiable requirió herramientas que van al quid de cada obstáculo en vez de taparlo, y ninguna fue la solución trivial de manual. Me guardo los detalles; lo que importa es el resultado, que una fuente protegida sigue siendo, al final, una que la investigación puede abrir y citar.

Lo que le da fundamento a la investigación es la biblioteca del estudio. Bibliotecario tiene 811 documentos que un agente puede buscar por significado en vez de por palabras exactas, y sacar los pasajes relevantes a un caso. Cómo está hecha esa búsqueda, y el quilombo de los archivos legales reales por debajo, está en la sección técnica más abajo.

La verdadera garantía, igual, es que nunca confiamos en las citas del modelo. Cuando el workflow de investigación arma un informe, no deja que el modelo escriba la lista de fuentes. Un paso de verificación vuelve a abrir cada fallo citado en el sitio oficial, y el anexo de fuentes se reconstruye solo con los fallos que efectivamente abrieron; todo lo que el modelo afirmó pero el verificador no pudo abrir se mueve a una sección aparte de "no verificado, no citar". Un modelo que alucina un fallo de la Corte Suprema no puede colarlo en la parte citable del documento, porque nunca escribe esa parte. La anti-alucinación vive en el código, no en un prompt.

Una reunión se vuelve una presentación

Lo insignia es la cosa poco glamorosa y valiosa que un estudio hace una y otra vez. Un abogado graba una reunión con un cliente, completa un cuestionario corto, y quiere de vuelta un informe escrito y una infografía, con fundamento en ley real. Lo codificamos como un workflow de Mastra con seis pasos: transcribir el audio, investigar la ley y la jurisprudencia aplicables, traer el contexto guardado del caso si el asunto ya existe, redactar los documentos, componer el informe, y renderizar un PDF. Los agentes corren en secuencia, no en paralelo, a propósito (más sobre por qué en un momento).

Lo que nos enseñó producción

Las lecciones de producción fueron más ruidosas. Una búsqueda sobre la biblioteca es barata, bastante menos de un segundo; diecisiete a la vez, no. Al principio el bot se quedó mudo cuarenta minutos en medio de un pedido porque un solo turno disparó diecisiete búsquedas concurrentes sobre la biblioteca en dos CPU compartidas, dejando sin recursos al health check, al webhook y a las actualizaciones de progreso, todo a la vez. Serializamos las búsquedas para que dejen de amontonarse, agregamos un heartbeat para que una tarea lenta igual muestre que sigue viva, y pasamos a CPU dedicadas. Por eso el workflow de la reunión corre sus agentes uno tras otro: el paralelismo es lo que casi tira abajo la máquina.

Lo que nos llevaríamos al próximo sistema

  • Hacer que el aislamiento sea estructural, no de comportamiento. Un agente que no puede llegar a los datos no puede filtrarlos; una promesa en un prompt sí.
  • Dividir cualquier herramienta que cargue dos patas de la trifecta (el mail es input no confiable y envío hacia afuera) en un servicio que no tenga datos privados.
  • Poner a la persona en el borde hacia afuera e irreversible, y en ningún otro lado. Un botón de aprobación le gana a una pared de confirmaciones.
  • Reconstruir en código, a partir de eventos reales, las partes que tienen que ser verdad (citas, entregas), no a partir de la prosa del modelo.
  • Cuando el framework está mal, arreglalo aguas arriba.

"Doctor" sigue corriendo como fallback mientras Pretor toma la posta. Respondía todo desde un solo contexto y lo hacía lo bastante bien como para que nadie notara la forma del riesgo. Pretor es más maquinaria para el mismo trabajo, y la mayor parte de esa maquinaria existe para hacer que un tipo de error sea imposible en vez de apenas improbable.


Detalles técnicos

Algunas notas de implementación para quien esté construyendo algo parecido. Todo el sistema es TypeScript sobre Mastra. El supervisor es un solo agente cuyos destinos de delegación viven en un mapa de agentes, y llamar a un subagente es simplemente una tool call. Mastra acota la memoria de conversación de cada agente a su propio resource id, así que el aislamiento del que depende todo el argumento de seguridad lo garantiza el framework, no una convención que tengamos que recordar: dos roles no pueden compartir memoria salvo que los cablees a propósito para eso.

Las delegaciones corren inline o como tareas en segundo plano, y esa distinción es donde nos topamos con un bug que valió la pena arreglar aguas arriba. En medio de construir el workflow, una delegación a un subagente empezó a hacer crashear al proveedor del modelo con un 500. La causa estaba en Mastra: cuando una delegación corre como tarea en segundo plano, el resultado de la herramienta es un string placeholder ("Background task started..."), pero el código que lo formatea leía output.text, que es undefined en un string, y serializaba contenido null en el request siguiente. Una línea:

value: typeof output === "string" ? output : (output.text ?? "")

Lo reportamos, abrimos un PR, y parcheamos nuestra instalación local hasta que se mergeó. Ese es uno de los argumentos más silenciosos para construir sobre un framework abierto: el arreglo fue a Mastra, no a un monkeypatch privado que arrastraríamos para siempre.

Comunicador no es un agente en proceso sino un servicio aparte con su propio webhook de Instagram, alcanzado a través de una frontera agente-a-agente. Todo lo que Pretor le pasa es el texto de un mensaje y un id de emisor, y todo lo que devuelve es una respuesta. Esa frontera de proceso hace trabajo de seguridad: el único agente que toca la internet abierta vive permanentemente fuera del espacio de memoria que guarda los datos del cliente, así que no hay ningún camino en memoria de un mensaje envenenado a un expediente.

La biblioteca es recuperación sobre embeddings vectoriales: cada uno de los 811 documentos se embebe y se busca por significado en vez de por palabras exactas. El diseño de la recuperación llevó tres intentos. Primero vino un índice exacto que se infló a cuarenta veces el tamaño del corpus que indexaba, reconstruyéndose con cada escritura, así que lo descartamos por un escaneo de similitud por fuerza bruta: simple, correcto y rápido de por sí, bastante menos de un segundo. Correr diecisiete de esos a la vez fue otra cosa, la caída de más arriba. Esos escaneos ya no están: los reemplazamos por un índice de vecinos más cercanos aproximados (ANN) que responde una consulta sin recorrer todo el corpus y esta vez es lo bastante compacto como para ganarse su espacio. La ingesta tenía su propia trampa: cerca de un décimo del archivo era RTF escondido detrás de una extensión .doc, así que el extractor olfatea el tipo de contenido real antes de parsear en vez de confiar en el nombre del archivo. Confiá en los bytes, no en la extensión.

Dos piezas más hacen que la regla de "nunca inventar la norma" se sostenga en código. Los Escribientes renderizan a partir de un modelo guardado del tipo de presentación real de cada fuero, así que un borrador respeta el formato oficial hasta los márgenes y la numeración en vez de reinventarse en cada documento. Y como los fallos oficiales referencian leyes en una abreviatura interna tipo LEY C 019101 ... 0076, un pequeño decodificador lo reescribe como Ley 19.101, art. 76 para que una cita se lea como tal.

Soy Fede Sapuppo, y construí Pretor, inteligencia artificial segura para tu estudio jurídico.

Más de OffTheRails