How to Debug A2UI: A Complete Guide to Troubleshooting Agent-to-UI Applications

Enable debug logging, launch the A2UI Inspector at localhost:5173, and step through message processing in the Gallery App to trace issues from JSON payloads to rendered components.

Debugging A2UI requires tracing data flow across three distinct layers: message processing, state management, and framework-specific rendering. This guide covers the first-party debugging tools provided by the google/A2UI repository—including the Inspector web UI, Gallery App, and Eval harness—to help you isolate bugs in the JSON payload, data model bindings, or component lifecycle.

Enable Verbose Logging to Capture State Transitions

Most core libraries emit logs through a Winston-style logger. The default level is info, but you must set it to debug to see internal state changes.

For the Inspector, edit tools/inspector/package.json or run:

npm run dev -- --log-level=debug

For Eval tests, pass the flag as documented in specification/v0_9/eval/README.md:

--log-level=debug

At the debug level, the logger prints chronological traces of:

  • MessageProcessor → model mutations
  • DataModel → path subscriptions and cascade notifications
  • Component lifecycle events (create, update, delete)

These logs appear in the console and in output.log, giving you a complete trace of the system.

To enable logging programmatically in a custom renderer (as implemented in renderers/lit/src/index.ts):

import { createLogger } from 'a2ui-core/logging';

// Set the global logger level before any renderer code runs
createLogger({ level: 'debug' });

Visualize Live State with the A2UI Inspector

The A2UI Inspector is the quickest way to view live state while a client runs. Start it from tools/inspector:

npm i && npm run dev

Then open http://localhost:5173/. The interface shows three panels:

  • Message Stream – Raw JSON payloads received from the agent
  • Component Tree – Hierarchical view of the current surface with component id, type, and resolved properties
  • Data Model – JSON view of the full DataModel, updating in real time as paths change

When a component appears blank or behaves unexpectedly, click it in the tree to reveal its resolved props after binder processing. Hover over a data path in the Data Model panel to see which components subscribe to it—the inspector highlights them automatically.

The inspector receives messages over a WebSocket at ws://localhost:5173/api. For advanced testing, you can manually send payloads:

const ws = new WebSocket('ws://localhost:5173/api');
ws.onopen = () => {
  ws.send(JSON.stringify({
    type: 'updateComponents',
    components: [{ id: 'lbl1', type: 'Text', properties: { text: 'Hello' } }]
  }));
};

The source and setup instructions live in tools/inspector/README.md.

The Gallery App serves as the reference implementation for all renderers, implementing a three-column layout documented in specification/v0_9/docs/renderer_guide.md:

Column Purpose
Samples Select predefined A2UI JSON samples
Surface + Messages Render the UI and display messages with an Advance button
Inspection Live Data Model view and Action logs

Running the Gallery lets you pause after each message to isolate the exact point where a component is added or updated. Inspect the Action log to verify that dispatched actions contain the expected surfaceId, sourceComponentId, and payload. The Gallery forwards logger configuration, so you can toggle debug logging while the UI stays responsive.

If a bug reproduces in the Gallery, the problem likely exists in the core layer rather than a framework-specific adapter.

Diagnose Data-Model Path Binding Issues

A common source of "empty" UI is an incorrect JSON Pointer in a DynamicString or DynamicNumber. The DataModel implements auto-vivification and cascade notifications (see the architecture guide in specification/v0_9/docs/renderer_guide.md).

To verify path subscriptions manually:

// Example: manually subscribe to a path while the app runs
const sub = surfaceModel.dataModel.subscribe<string>('/user/name', v => {
  console.log('User name changed to', v);
});

// Later, when you no longer need the subscription
sub.unsubscribe();

If the callback never fires, the path either does not exist or is never set. Use the Data Model panel in the Inspector to verify that the key /user/name appears after the relevant message.

For custom components, use the binder layer's subscribeDynamicValue method (as described in the DataContext section of specification/v0_9/docs/renderer_guide.md):

export const MyDynamicLabel = (ctx: ComponentContext<any>) => {
  const sub = ctx.dataContext.subscribeDynamicValue<string>(
    ctx.componentModel.properties['label'],
    value => {
      console.log('Label resolved to', value);
    }
  );

  // Clean up when the component unmounts
  return () => sub.unsubscribe();
};

Verify Component Lifecycle Hooks to Prevent Leaks

Each component implementation must respect three lifecycle rules: lazy subscription, path stability, and cleanup (see "Component Subscription Lifecycle Rules" in specification/v0_9/docs/renderer_guide.md). When debugging memory-leak symptoms:

  1. Open browser DevTools Performance tab
  2. Record a short interaction (e.g., open a dialog, then close it)
  3. Look for lingering EventSource or Subscription objects

If you spot lingering objects, ensure your component's dispose() method calls binding.dispose() and unsubscribes from all DataModel paths.

Run Automated Checks with the Eval Test Harness

The Eval framework provides a command-line test harness with configurable log levels. It runs prompts against a chosen LLM and validates generated UI against the spec while recording detailed events in output.log.

To isolate a failing scenario:

pnpm run eval -- --model=gemini-2.5-flash-lite --prompt=loginForm --log-level=debug

After the run, open results/<model>/output.log and search for ERROR or WARN. The log contains timestamps for each MessageProcessor.processMessages call, making it easy to pinpoint the exact message that caused an invalid component or missing data path.

Configuration details are documented in specification/v0_9/eval/README.md.

Summary

Debugging A2UI systematically requires the right tool for each layer:

  • Enable debug logging in tools/inspector/package.json or Eval runs to capture all model mutations in output.log
  • Run the A2UI Inspector from tools/inspector to visualize the component tree, resolved props, and real-time data paths at localhost:5173
  • Step through the Gallery App to isolate offending messages using the Advance button and Action logs
  • Verify DataModel paths using subscribe() or subscribeDynamicValue() to ensure dynamic bindings resolve correctly
  • Check component lifecycle in the browser Performance tab to confirm dispose() methods clean up subscriptions
  • Use the Eval harness with --log-level=debug for automated, reproducible regression checks against the specification

Frequently Asked Questions

How do I enable debug logging in A2UI?

Set the Winston-style logger level to debug before starting your tool. For the Inspector, run npm run dev -- --log-level=debug or edit tools/inspector/package.json. For Eval tests, pass --log-level=debug as documented in specification/v0_9/eval/README.md. This captures MessageProcessor mutations, DataModel subscriptions, and component lifecycle events in the console and output.log.

What is the A2UI Inspector and how does it help?

The A2UI Inspector is a lightweight web UI in tools/inspector that visualizes incoming messages, the component tree, and live data models. Running on localhost:5173, it shows raw JSON payloads, resolved component properties, and real-time DataModel updates. You can hover over data paths to see which components subscribe to them, making it ideal for diagnosing blank or misbehaving UI elements.

How can I check if my data bindings are working correctly?

Verify that the JSON Pointer in your DynamicString or DynamicNumber exists in the DataModel. Use the Inspector's Data Model panel to confirm the path appears after message processing. Programmatically, call surfaceModel.dataModel.subscribe('/path', callback) or ctx.dataContext.subscribeDynamicValue()—if the callback never fires, the path is unset or incorrect.

Where can I find logs from the Eval test harness?

The Eval harness writes detailed logs to results/<model>/output.log after each run. Execute the harness with --log-level=debug to capture timestamps for every MessageProcessor.processMessages call. Search this file for ERROR or WARN to identify the specific message causing spec violations or missing data paths.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →