Energent AI
Panel de agente IA de nivel productivo con uso de computadora en vivo
EL DESAFÍO
La demo de computer-use del Vercel AI SDK ofrece una prueba de concepto mínima, pero no está lista para producción. El estado de VNC y el del chat están acoplados, lo que provoca que el visor se reinicialice con cada mensaje. Los límites de tipos entre paquetes del SDK son inconsistentes, generando aserciones de tipos en toda la base de código. No hay observabilidad sobre la duración de llamadas individuales a herramientas ni sobre el ciclo de vida de la sesión.
STACK TECNOLÓGICO
Enfoque de ingeniería
Aislamiento de stores por diseño
Separé el estado de la sesión VNC del estado de chat y eventos en dos stores completamente independientes en Zustand antes de escribir cualquier UI. Los límites de stores fueron una decisión arquitectónica tomada de antemano, no una refactorización tras detectar problemas de rendimiento.
Sistema de tipos rastreado, no parcheado
En lugar de suprimir errores de TypeScript con aserciones de tipos, rastreé la jerarquía completa de tipos del AI SDK a través de @ai-sdk/ui-utils para encontrar el límite de tipo correcto. Refactoricé el manejo de mensajes para usar UIMessage de forma consistente en la capa UI.
Control explícito de renders
Envolví VNCViewer en React.memo con un comparador de igualdad personalizado en lugar de depender de la comparación superficial por defecto, dando control preciso sobre cuándo se reinicializa la conexión VNC.
Prevención de closures obsoletos
Usé useRef para el seguimiento de duración por llamada a herramienta en lugar de useState, asegurando que los callbacks siempre lean los valores actuales sin provocar re-renders ni capturar closures obsoletos.
Decisiones técnicas clave
Las elecciones que dieron forma a la arquitectura y determinaron la mantenibilidad a largo plazo.
Dos stores aisladas en Zustand
El pipeline de eventos y la gestión de sesión viven en stores completamente separadas sin suscripciones cruzadas.
Los re-renders de VNC son costosos y disruptivos — cortan la conexión en vivo. Una store compartida significa que cualquier actualización del chat dispara un diff de VNC. El aislamiento garantiza que el visor solo se re-renderice cuando el estado de sesión cambia genuinamente.
React.memo con comparador personalizado
VNCViewer está envuelto en React.memo con una función de igualdad explícita en lugar de la comparación superficial por defecto.
La comparación superficial por defecto sigue disparando re-renders cuando cambia la identidad de las props de objeto aunque los valores sean idénticos. El comparador explícito da control determinista sobre la reinicialización.
UIMessage en la capa UI
Estandaricé UIMessage de @ai-sdk/ui-utils como tipo usado en todos los componentes UI que manejan mensajes.
El tipo Message vive en la capa core del SDK y no lleva campos específicos de UI. Usarlo en la capa UI fuerza aserciones de tipos. UIMessage es el tipo de límite correcto y elimina el conflicto por completo.
useRef para seguimiento de duración
Los timestamps de inicio de llamadas a herramientas se almacenan en un ref, actualizados via partes de mensaje onFinish.
Almacenar timestamps en estado dispararía re-renders con cada inicio de llamada a herramienta. Un ref mantiene valores mutables sin afectar el ciclo de render, y siempre da a los callbacks acceso al valor actual.
Desafíos y soluciones
El visor VNC se reinicializaba con cada mensaje del chat, causando cortes de conexión en vivo durante sesiones activas del agente.
Aislé el estado de sesión VNC en una store Zustand dedicada sin suscripciones al estado del chat. Añadí React.memo con comparador explícito a VNCViewer para asegurar re-renders solo ante cambios genuinos de sesión.
El conflicto de tipos entre Message y UIMessage de @ai-sdk/react y @ai-sdk/ui-utils causaba errores TypeScript en todo el pipeline de manejo de mensajes.
Rastreé la jerarquía completa de tipos a través de los paquetes del AI SDK para identificar el límite de tipo correcto. Refactoricé todo el manejo de mensajes en la capa UI para usar UIMessage de forma consistente, eliminando cada aserción de tipos.
El seguimiento de duración por llamada a herramienta via callbacks de eventos leía timestamps de inicio obsoletos debido a la captura del closure en el momento del registro.
Moví el almacenamiento de timestamps de estado a useRef, dando a todos los callbacks una referencia mutable que siempre apunta al valor actual independientemente de cuándo se creó el closure.
Resultados e impacto
El panel ejecuta sesiones de agente estables con cero cortes de conexión VNC ante actualizaciones del chat, un pipeline de mensajes completamente seguro en tipos sin aserciones, y datos precisos de duración por llamada a herramienta visibles en el timeline de eventos.
APRENDIZAJES CLAVE
Las decisiones arquitectónicas sobre límites de estado deben tomarse antes de escribir UI, no después. Refactorizar el aislamiento de stores en un sistema acoplado cuesta mucho más que diseñar el límite desde el principio.
