Así evité que mis agentes de IA se pisaran (y lo que de verdad lo arregló)

Cuando pones a trabajar a un solo agente de código, lo que te preocupa es que haga bien su tarea, pero cuando pones a varios a la vez sobre el mismo repositorio aparece un problema mucho más traicionero: cada uno puede hacer bien su parte y el conjunto salir roto. Un agente cambia la firma de una función en un fichero mientras otro, en un fichero distinto, la sigue llamando con la firma antigua, y ninguno se entera, porque desde su punto de vista todo funciona y todos sus tests están en verde.

Quería saber con datos cuánto pasa esto, qué lo evita y cuánto cuesta evitarlo, así que monté un laboratorio pequeño y reproducible y construí Médula, un kernel de concurrencia para agentes de código inspirado en los sistemas operativos. Aquí cuento lo que medí, también lo que falló, porque es de donde más se aprende.

En resumen, para quien tenga prisa: con una rama por agente, git dejó pasar el choque de verdad en las cinco ejecuciones y siempre quedaron los mismos seis tests en rojo; lo que lo arregló no fue mi kernel, sino dejar de aislar a los agentes, porque las diez ejecuciones con un directorio compartido acabaron con los 37 tests en verde; y frente a unos simples locks por fichero, Médula cuesta lo mismo, 1,65 $ por ejecución, pero detecta los seis choques reales de seis y no bloquea nada que no choque.

El laboratorio

La demo es una API pequeña de reservas de salas con seis tareas y 37 tests de aceptación, y las tareas están escritas para chocar de formas conocidas. Una añade un segundo factor al login, de modo que login() pasa a exigir un código, mientras otra crea una exportación que llama al login antiguo; otra renombra un campo de las reservas mientras otra construye un filtro sobre el nombre viejo. Esas dos parejas chocan por significado y no por fichero, y hay una tercera que toca el mismo fichero sin chocar, para comprobar que nadie bloquea por exceso de celo.

Los agentes son siempre los mismos, Claude Code con Sonnet 5 y esfuerzo alto, todos los modelos pasan por OpenRouter y lo único que cambia de un modo a otro es cómo se coordinan:

  • A, secuencial: un agente detrás de otro en el mismo directorio.
  • B, ramas: una rama por tarea y merge al final, con un agente que resuelve los conflictos de texto.
  • C, locks por fichero: directorio compartido, y quien quiere tocar un fichero ocupado espera.
  • D, Médula con Jev como decisor rápido.
  • E, Médula con Haiku como decisor rápido.
  • F, Médula con Sonnet decidiéndolo todo.

Los tests de aceptación nunca se copian al espacio de trabajo de los agentes y se pasan siempre sobre el resultado final, con las seis tareas aplicadas, que es lo que de verdad importa cuando el trabajo se integra.

Ramas: cada agente en verde y el repo en rojo

El modo B es lo que haría casi cualquiera, darle a cada agente su rama y juntarlo todo al final, y es el que peor salió. En las cinco ejecuciones, git marcó dos conflictos de texto en un fichero de tests, un agente los resolvió y eso dio la sensación de que la integración estaba hecha, mientras el choque de verdad pasaba sin que nada lo señalara: la exportación seguía llamando al login sin el segundo factor. Siempre quedaron los mismos seis tests en rojo, y eso que cada agente había terminado su tarea con todos sus tests en verde.

No es un fallo de git, que hace exactamente lo que promete, juntar líneas de texto. El problema es que el conflicto no estaba en las líneas, sino en el contrato entre dos piezas de código que vivían en ficheros distintos, y eso no lo ve ninguna herramienta que no entienda lo que hace el código. Un lock por fichero tampoco lo ve a tiempo, por la misma razón.

El modo secuencial dejó un momento muy humano. Trabajando solo, el agente de la exportación se negó a implementarla porque abría una vía para exportar todas las reservas saltándose el segundo factor, propuso la solución correcta y preguntó, pero en modo no interactivo nadie le contesta, así que ese modo terminó con 30 de 37.

El giro: lo que lo arregla es dejar de aislarlos

Médula funciona como el kernel de un sistema operativo: los agentes son los procesos, el código es el recurso compartido y el kernel pone semáforos, interrupciones y un planificador. Con hooks que se ejecutan antes y después de cada herramienta, intercepta cada escritura, edición o comando y pregunta si choca con el trabajo en curso de los demás antes de dejar que ocurra; y cuando un agente escribe algo, averigua a quién le afecta y solo avisa a esos.

Yo esperaba que eso fuera lo que salvara el repositorio, y no lo fue. Lo que evitó el desastre fue dejar de aislar a los agentes en ramas: las diez ejecuciones con un directorio compartido, ya fuera con locks, con Médula o con cualquiera de sus decisores, acabaron con los 37 tests en verde, porque los agentes ven el código de los demás y se adaptan.

ModoEjecucionesTests en verdeTiempo medioCoste de agentesCoste de decisión
A · Secuencial230 de 375,7 min0,86 $—
B · Ramas y merge al final531 de 375,5 min1,20 $0,40 $ (resolver conflictos)
C · Locks por fichero337 de 377,5 min (de 5,2 a 11,2)1,65 $0 $
D · Médula + Jev337 de 376,9 min (de 6,5 a 7,2)1,36 $0,29 $
E · Médula + Haiku337 de 3713,7 min1,74 $0,55 $
F · Médula + Sonnet1 (contaminada)37 de 376,3 min1,40 $0,59 $

Entonces, ¿qué aporta Médula frente a unos simples locks? Al mismo coste, 1,65 $ por ejecución, detecta los seis choques reales de seis, mientras que los locks detectaron cinco y de rebote; no hace ningún bloqueo innecesario, frente a los cinco de los locks; y su tiempo es estable, de 6,5 a 7,2 minutos, frente a los 5,2 a 11,2 de los locks. Los locks, además, a veces hacen esperar al agente equivocado: en una ejecución, el agente del login estuvo 453 segundos esperando al de la exportación, justo al revés de lo que tocaba, porque era la exportación la que dependía del login nuevo. Un lock decide por orden de llegada; Médula intenta decidir por significado.

Con Médula, el agente de la exportación que antes se quedaba bloqueado preguntando recibe los avisos de que otro agente está cambiando el login, y se adapta.

El decisor y lo que cuesta de verdad

Para saber si una acción choca con lo que hacen los demás, Médula pregunta a Jev, un modelo de decisión que TypeSafe AI lanzó el 15 de septiembre. Jev no genera texto: recibe un estado y devuelve decisiones tipadas con su probabilidad, en unos 0,3 segundos y por unos 0,00006 $ cada una. TypeSafe le puso ese nombre por William Stanley Jevons y su paradoja, la de que cuando algo se abarata se usa muchísimo más, y esa es justo la apuesta del proyecto: con decisiones casi gratis se pueden tomar decisiones que antes nadie tomaba, como comprobar cada escritura de cada agente contra el plan de todos los demás.

Cuando Jev no lo tiene claro, la decisión sube a un camino lento con Sonnet y, si hace falta, con Opus. En la sesión que muestro en el vídeo hubo 115 decisiones: 33 resueltas por una regla, 60 por Jev y 22 en el camino lento.

Aquí está el matiz que más me interesa contar, porque contradice la intuición. Por decisión, Jev es rapidísimo: su mediana fue de 279 ms, frente a 1.338 ms de Haiku y 2.201 ms de Sonnet, medidas el 29 de septiembre a través de OpenRouter. Pero en el sistema real, la capa de decisión con Jev cuesta 0,29 $ por ejecución frente a 0,59 $ con Sonnet decidiéndolo todo, es decir, la mitad y no una fracción ínfima, porque sus dudas suben a Sonnet y se llevan casi todo el coste. Con las parejas de laboratorio solo dudaba el 13 % de las veces; con los estados reales, con varios agentes y tareas largas, iba al camino lento el 61 % de las peticiones de escritura que pasaban por él.

Frente a Haiku como decisor rápido, Jev sale claramente mejor: la mitad de tiempo total, 6,9 minutos frente a 13,7, y casi la mitad de coste de decisión, 0,29 $ frente a 0,55 $. En acierto, sobre 100 parejas etiquetadas, Jev sacó un 92,5 %, Haiku un 91,5 % y Sonnet un 98,5 %, y cuando Jev estaba muy seguro, cosa que pasó en el 31 % de las decisiones, acertó el 100 %.

Dónde falla, sin rebajas

En el choque clave, el del login y la exportación, Jev puntuó más alto un choque falso que el real, y lo salvó el camino lento. Una vez, Sonnet «resolvió» un conflicto haciendo opcional el segundo factor, lo que hacía desaparecer el choque y, de paso, el requisito; desde entonces el camino lento recibe los criterios de aceptación de las tareas implicadas y no puede proponer nada que los incumpla. Es la idea de fondo del Spec-Driven Development: la especificación es lo que impide que un sistema arregle un problema rompiendo otro.

Hubo más tropiezos. Un agente se programó un despertador y abandonó su tarea, por un fallo de mi banco de pruebas que ya está arreglado; otro se pasó diez minutos esperando delante de un fichero roto, convencido de que alguien lo estaba editando. Y lo que menos esperaba: cuando los agentes tuvieron una herramienta para mandarse mensajes, la usaron sin que nadie se lo pidiera, y en una ejecución el que renombraba el campo avisó al que filtraba por él. Desactivé esa herramienta para que los modos fueran comparables, pero puede que el verdadero rival de Médula sea dejar que los agentes hablen entre ellos.

También aprendí algo sobre evaluar: la primera vez etiqueté a voleo las parejas de prueba y Jev salía con un 68 %; con etiquetas hechas con cuidado, esas mismas respuestas pasaron al 87 %. El modelo era el mismo; lo que había cambiado era mi examen.

Y las salvedades, que importan tanto como las cifras. Hay pocas ejecuciones por modo, de una a cinco. Los umbrales se ajustaron con las mismas parejas con las que se miden. Las etiquetas de calibración las escribió Claude, que es de la misma familia que Haiku y Sonnet, y eso probablemente les favorece. La ejecución F y una de las de E están contaminadas por mensajes entre agentes. Son mediciones de días concretos, con Jev en acceso anticipado. Por todo eso, lo más sólido es lo que se repite sin excepción, que las cinco ejecuciones con ramas fallaron igual y las diez con directorio compartido acabaron en verde, y el resto lo tomo como indicios.

Qué me llevo, y cómo comprobarlo tú

Si trabajas con varios agentes a la vez, la lección práctica es fácil de decir y fácil de olvidar: aislarlos en ramas parece prudente, pero es justo lo que esconde los problemas hasta el final. Si los tienes en ramas o worktrees, pasa los tests de integración sobre el resultado fusionado y no solo los de cada agente, porque fueron esos tests, y no git, los que pillaron el choque en todas las ejecuciones. Y si los pones a trabajar sobre el mismo directorio, unos simples locks ya te dejan en verde; coordinar por significado aporta detectar los choques antes de que se escriban y no bloquear lo que no choca, pero no sale gratis.

Todo el experimento está publicado en crudo y con licencia MIT en github.com/JoaquinRuiz/medula: las sesiones de los agentes, los diffs, las evaluaciones y un fichero SQLite por ejecución con cada decisión que tomó el kernel, su probabilidad, su latencia y su coste. Los tests del kernel y la comprobación del diseño de los choques se pueden ejecutar sin conexión, sin clave de API y sin gastar nada.

Si quieres echar una mano, lo que más valor tiene ahora mismo es etiquetar a ciegas las 100 parejas de calibración: son unos 20 o 30 minutos, no hace falta escribir código y ataca justo la salvedad que más me preocupa, la de las etiquetas escritas por un modelo. También hay problemas abiertos para traer un decisor nuevo, que puede ser cualquier modelo que devuelva una probabilidad, o para añadir escenarios distintos.

En el vídeo, «Así construí un kernel para que mis agentes de IA no se pisen», lo construyo y lo mido paso a paso, con el htop de agentes en marcha y las dos sesiones del agente de la exportación, la que se niega y la que se adapta, una al lado de la otra. Y si te interesa por qué la especificación es lo que sostiene todo esto, lo cuento con calma en mi libro «Del vibe coding al Spec-Driven Development».

¡Que la IA te acompañe!

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x