Energent AI
Production-grade AI agent dashboard with live computer use
THE CHALLENGE
The Vercel AI SDK computer-use demo provides a minimal proof of concept, but it is not production-ready. VNC state and chat state are coupled, causing the viewer to re-initialize on every message. Type boundaries between SDK packages are inconsistent, leading to type assertions throughout the codebase. There is no observability into individual tool call durations or session lifecycle.
TECH STACK
Engineering Approach
Store Isolation by Design
Separated VNC session state from chat/event state into two completely independent Zustand stores before writing any UI. Store boundaries were an architectural decision made upfront, not a refactor after noticing performance issues.
Type System Traced, Not Hacked
Instead of suppressing TypeScript errors with type assertions, traced the full AI SDK type hierarchy through @ai-sdk/ui-utils to find the correct type boundary. Refactored message handling to use UIMessage consistently at the UI layer.
Explicit Render Control
Wrapped VNCViewer in React.memo with a custom equality comparator rather than relying on default shallow comparison. This gives precise control over when the VNC connection re-initializes, preventing drops during active agent sessions.
Stale Closure Prevention
Used useRef for per-tool-call duration tracking instead of useState, ensuring event callbacks always read current values without causing re-renders or capturing stale closures.
Key Technical Decisions
The choices that shaped the architecture and determined long-term maintainability.
Two Isolated Zustand Stores
Event pipeline and session management live in completely separate stores with no cross-subscription.
VNC re-renders are expensive and disruptive — they drop the live connection. A shared store means any chat update triggers a VNC diff. Isolation ensures the viewer only re-renders when session state genuinely changes.
React.memo with Custom Comparator
VNCViewer is wrapped in React.memo with an explicit equality function instead of the default shallow comparison.
Default shallow comparison still triggers re-renders on object prop identity changes even when values are identical. The explicit comparator gives deterministic control over re-initialization.
UIMessage at the UI Layer
Standardized on UIMessage from @ai-sdk/ui-utils as the type used across all UI components handling messages.
The Message type lives at the core SDK layer and does not carry UI-specific fields. Using it at the UI layer forces type assertions. UIMessage is the correct boundary type and eliminates the mismatch entirely.
useRef for Duration Tracking
Start timestamps for tool calls are stored in a ref, updated via onFinish message parts.
Storing timestamps in state would trigger re-renders on every tool call start. A ref holds mutable values without affecting the render cycle, and always gives callbacks access to the current value.
Challenges & Solutions
VNC viewer was re-initializing on every chat message, causing live connection drops during active agent sessions.
Isolated VNC session state into a dedicated Zustand store with no subscriptions to chat state. Added React.memo with an explicit comparator to VNCViewer, ensuring re-renders only trigger on genuine session changes.
Message vs UIMessage type conflict between @ai-sdk/react and @ai-sdk/ui-utils caused TypeScript errors throughout the message handling pipeline.
Traced the full type hierarchy through the AI SDK packages to identify the correct type boundary. Refactored all UI-layer message handling to use UIMessage consistently, eliminating every type assertion.
Per-tool-call duration tracking via event callbacks was reading stale start timestamps due to closure capture at registration time.
Moved timestamp storage from state to useRef, giving all callbacks a mutable reference that always points to the current value regardless of when the closure was created.
Outcomes & Impact
The dashboard runs stable agent sessions with zero VNC connection drops on chat updates, a fully type-safe message pipeline with no assertions, and accurate per-tool-call duration data visible in the event timeline.
KEY TAKEAWAYS
Architectural decisions about state boundaries have to be made before writing UI, not after. Retrofitting store isolation into a coupled system costs far more than designing the boundary upfront.
