Glassys se pone delante de varios agentes de código — Claude Code, Codex, Cursor, OpenCode y cualquiera que hable ACP — y cada uno tiene un adaptador cuyo único trabajo es convertir el flujo de eventos de ese agente en las pocas cosas que la pantalla de chat sabe dibujar: texto, una tarjeta de herramienta con su ruta o su comando, un diff, una shell con su salida y su código de salida, una ejecución terminada.

Esos mapeadores tenían tests. Los tests pasaban. Y varios adaptadores seguían mostrando casi nada de lo que su agente estaba haciendo de verdad.

El motivo da vergüenza visto desde aquí. Había escrito los fixtures a mano, a partir de cómo suponía que eran los eventos, y nada los comparaba nunca con los SDK. Un mapeador y su test pueden estar perfectamente de acuerdo sobre una forma que no llega jamás en ejecución, y a partir de ahí el test no es un test: es una segunda copia de mi suposición, con una marca verde al lado.

Grabar en vez de imaginar

Así que dejé de escribir fixtures. Ahora cualquier adaptador puede volcar a un fichero los eventos crudos que recibe — una variable de entorno, GLASSYS_RECORD_FIXTURES, apuntando a un directorio —, de modo que un fixture es la grabación de una sesión real y no una conjetura. Cada mapeador sigue los tipos que están instalados de verdad, y los fixtures también.

Encima de eso hay un script de humo que lleva un adaptador contra un agente real en un espacio de trabajo desechable y comprueba que llega la cadena entera: texto, una tarjeta con su ruta o su comando, el diff de la edición, una shell que falla, una ejecución que termina. No está en CI, porque necesita credenciales y gasta tokens, pero es un comando cada vez que toco un adaptador.

Las primeras grabaciones reales ya pagaron la tarde. Una ejecución de Claude con Claude Code sin sesión pintaba el error de inicio dos veces — una desde el mensaje sintético del asistente y otra desde el resultado — y añadía una fila de consumo con todo a cero. Las dos cosas se fueron, y el caso del consumo a cero está arreglado para todos los adaptadores. Un CLI de OpenCode instalado pero que se caía se reportaba como "instala el CLI de OpenCode"; ahora la comprobación lanza el comando de versión y te dice que el CLI está instalado pero no arranca, con su código de salida o su señal.

El fallo de ACP que ningún fixture podía cazar

ACP es la vía por la que Glassys admite agentes para los que no he escrito un adaptador, Gemini entre ellos. Mi initialize salía con trama Content-Length. ACP va delimitado por saltos de línea, y en ACP el agente nunca habla primero: así que no volvía nada y todo agente que cumple el estándar se quedaba ahí plantado hasta que expiraba el arranque. Nada de mi propia suite hablaba el formato real, de modo que el fallo se quedó esperando a que alguien apuntara un agente de verdad contra él.

Con la trama arreglada, el resto del protocolo recibió el mismo trato. Las respuestas de permiso eligen los identificadores de opción del propio agente por tipo en vez de buscar nombres, la autenticación solo corre cuando la sesión la pide, los terminales respetan el directorio de trabajo, el entorno y el límite de salida y se matan como grupo de procesos, un terminal matado sigue siendo legible en vez de afirmar que salió limpio, las lecturas aceptan rangos de líneas, los métodos desconocidos reciben su error de método inexistente y cancelar espera la señal de la ejecución en lugar de dos bucles de sondeo.

Un agente falso, para que CI hable el protocolo

La otra mitad del arreglo es un agente ACP falso: con la forma del estándar y ningún modelo detrás. Su turno de demostración mueve escrituras a través del host, un terminal con permiso de por medio que falla y una lectura de fichero; si le pides una respuesta larga, emite hasta que lo cancelas. Eso corre en CI, así que el camino del protocolo está cubierto por algo que no cuesta tokens ni pide credenciales: diffs, salida de shell y códigos de salida, denegaciones con la ejecución automática apagada, cancelaciones, el catálogo de modelos.

ACP también lista los modelos que su agente anunció al abrir la sesión, guardados de la última o leídos una vez con una sesión de sondeo corta que no se reintenta en cinco minutos. Manda bloques de imagen solo a los agentes que dijeron aceptarlas, y al resto les pasa las rutas en el texto. Y por fin tiene su propia etiqueta de permisos: con la ejecución automática apagada, Glassys rechaza escrituras, terminales y cualquier petición de permiso que no sea de lectura. Conviene decirlo claro, porque antes les mostraba a los usuarios de ACP una etiqueta prestada de Cursor que no explicaba nada.

Lo que los demás adaptadores estaban escondiendo

Leyendo los tipos de evento reales me encontré un fallo de distinta naturaleza en cada uno.

  • Cursor pone los resultados de herramienta un nivel más abajo de donde yo los tenía, así que los diffs, la salida y el código de salida de una shell fallida simplemente no se mostraban nunca. Ahora la salida de la shell entra en su tarjeta mientras ocurre, y las llamadas MCP salen nombradas por proveedor y herramienta.
  • OpenCode informa de los fallos como un resultado de error en vez de lanzar una excepción, así que una sesión que no existe o un prompt rechazado parecían un éxito. Las peticiones de permiso se quedaban colgadas diez minutos y ahora se rechazan. También estaba abriendo las sesiones en el directorio de trabajo del propio gateway y no en el del espacio de trabajo, que es una cosa rara de descubrir sobre tu propia herramienta.
  • Claude: un turno cancelado se lee ahora hasta su propio resultado. Volver antes dejaba ese resultado en la cola, así que todas las respuestas siguientes llegaban con un turno de retraso. Las denegaciones se marcan, las ediciones sacan su diff del parche estructurado de Claude Code, el consumo cuenta los tokens de prompt en caché, un cambio de modelo ya no reinicia el proceso y una conversación que ha desaparecido se reemplaza y se reenvía el prompt.
  • Codex: los cambios de ficheros listan sus rutas, las llamadas MCP muestran el servidor, la herramienta, el error y la salida, una salida distinta de cero marca la tarjeta como fallida y la comprobación resuelve el binario que trae el propio SDK en vez de exigir uno en el PATH.

La lección que me quedo

Un test vale exactamente lo que valen sus entradas. Para cualquier cosa cuya forma no es mía — el SDK de otro, un protocolo, un webhook —, grabar una sesión real y comitearla vale más que diez fixtures escritos a partir de la documentación. El grabador me llevó una tarde y encontró algo mal en todos y cada uno de los adaptadores.

Está todo a la vista: Glassys en GitHub. Si no lo conoces, esto es lo que es, y esta fue la semana que pasé cerrándolo.