- Implemented E2E tests for workspace switching functionality, covering scenarios such as switching workspaces, data isolation, language preference maintenance, and UI updates. - Added tests to ensure workspace data is cleared on logout and handles unsaved changes during workspace switches. - Created comprehensive backend tests for the ask_question tool, validating question creation, execution, answer handling, cancellation, and timeout scenarios. - Included edge case tests to ensure robustness against duplicate questions and invalid answers.
11 KiB
A2UI Runtime
A lightweight, framework-agnostic runtime for rendering A2UI (AI-to-UI) surfaces in React applications.
Overview
A2UI Runtime enables AI agents to generate and update interactive UI components dynamically through a structured protocol. Based on Google's A2UI specification, this runtime provides:
- Dynamic Surface Rendering: Create and update UI components in real-time
- Component Registry: Extensible system for custom component renderers
- Type Safety: Full TypeScript support with Zod validation
- State Management: Built-in state binding and action handling
- Protocol Validation: Parse and validate A2UI surface updates
Installation
Located at src/packages/a2ui-runtime/, this runtime is included as part of the CCW frontend codebase.
Core Components
1. A2UIParser
Parse and validate A2UI surface updates from JSON.
import { a2uiParser } from '@/packages/a2ui-runtime/core';
// Parse JSON string
const surfaceUpdate = a2uiParser.parse(jsonString);
// Validate object
const isValid = a2uiParser.validate(surfaceUpdate);
// Safe parse with result
const result = a2uiParser.safeParse(jsonString);
if (result.success) {
console.log(result.data);
}
2. A2UIComponentRegistry
Register and manage component renderers.
import { a2uiRegistry } from '@/packages/a2ui-runtime/core';
import { CustomRenderer } from './renderers/CustomRenderer';
// Register a component
a2uiRegistry.register('CustomComponent', CustomRenderer);
// Check if registered
if (a2uiRegistry.has('CustomComponent')) {
const renderer = a2uiRegistry.get('CustomComponent');
}
// Get all registered types
const types = a2uiRegistry.getRegisteredTypes();
3. A2UIRenderer
Render A2UI surfaces as React components.
import { A2UIRenderer } from '@/packages/a2ui-runtime/renderer';
function MyComponent() {
const [surface, setSurface] = useState<SurfaceUpdate | null>(null);
const handleAction = async (actionId: string, params: Record<string, unknown>) => {
console.log(`Action: ${actionId}`, params);
// Send action back to AI agent
};
return surface ? (
<A2UIRenderer
surface={surface}
onAction={handleAction}
className="my-surface"
/>
) : null;
}
Built-in Components
Standard Components
| Component | Props | Description |
|---|---|---|
Text |
text, usageHint |
Text with semantic hint (h1-h6, p, span, code, small) |
Button |
onClick, content, variant, disabled |
Button with variants (primary, secondary, destructive, ghost, outline) |
Dropdown |
options, selectedValue, onChange, placeholder |
Select dropdown with options |
TextField |
value, onChange, placeholder, type |
Text input (text, email, password, number, url) |
TextArea |
value, onChange, placeholder, rows |
Multi-line text input |
Checkbox |
checked, onChange, label |
Checkbox with label |
Progress |
value, max |
Progress bar |
Card |
title, description, content |
Container with title and nested content |
Custom Components
| Component | Props | Description |
|---|---|---|
CLIOutput |
output, language, streaming, maxLines |
CLI output with syntax highlighting |
DateTimeInput |
value, onChange, placeholder, includeTime |
Date/time picker input |
A2UI Protocol
Surface Update Structure
interface SurfaceUpdate {
surfaceId: string;
components: SurfaceComponent[];
initialState?: Record<string, unknown>;
}
interface SurfaceComponent {
id: string;
component: A2UIComponent;
}
Component Types
All components use a discriminated union structure:
// Text Component
{
Text: {
text: { literalString: "Hello" } | { path: "state.key" };
usageHint?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "code" | "small";
};
}
// Button Component
{
Button: {
onClick: { actionId: "submit", parameters?: {} };
content: A2UIComponent; // Nested component (usually Text)
variant?: "primary" | "secondary" | "destructive" | "ghost" | "outline";
disabled?: { literalBoolean: true } | { path: "state.disabled" };
};
}
Content Bindings
Values can be literal or bound to state:
// Literal string
{ literalString: "Hello, World!" }
// Bound to state
{ path: "user.name" }
Actions
Actions are triggered by user interactions:
{
actionId: "save-form",
parameters: {
formId: "contact-form",
validate: true
}
}
Creating Custom Components
1. Define the Renderer
import type { ComponentRenderer } from '@/packages/a2ui-runtime/core/A2UIComponentRegistry';
import type { A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes';
interface CustomComponentConfig {
title: { literalString: string } | { path: string };
items: Array<{ label: string; value: string }>;
onSelect: { actionId: string; parameters?: Record<string, unknown> };
}
export const CustomRenderer: ComponentRenderer = ({
component,
state,
onAction,
resolveBinding
}) => {
const customComp = component as { Custom: CustomComponentConfig };
const { Custom: config } = customComp;
const handleSelect = (value: string) => {
onAction(config.onSelect.actionId, {
...config.onSelect.parameters,
selectedValue: value,
});
};
return (
<div className="custom-component">
{/* Your rendering logic */}
</div>
);
};
2. Register the Component
import { a2uiRegistry } from '@/packages/a2ui-runtime/core/A2UIComponentRegistry';
import { CustomRenderer } from './CustomRenderer';
a2uiRegistry.register('Custom', CustomRenderer);
3. Add Type Definition (Optional)
For type safety, extend the A2UI types:
// In A2UITypes.ts
export const CustomComponentSchema = z.object({
Custom: z.object({
title: TextContentSchema,
items: z.array(z.object({
label: z.string(),
value: z.string(),
})),
onSelect: ActionSchema,
}),
});
// Add to ComponentSchema union
export const ComponentSchema = z.union([
// ... existing components
CustomComponentSchema,
]);
// Add to A2UIComponentType
export type A2UIComponentType =
| 'Text' | 'Button' | // ... existing types
| 'Custom';
Usage Examples
Rendering a Simple Form
const formSurface: SurfaceUpdate = {
surfaceId: 'contact-form',
components: [
{
id: 'title',
component: {
Text: {
text: { literalString: 'Contact Form' },
usageHint: 'h2',
},
},
},
{
id: 'name',
component: {
TextField: {
value: { literalString: '' },
onChange: { actionId: 'update-name', parameters: { field: 'name' } },
placeholder: 'Your Name',
type: 'text',
},
},
},
{
id: 'email',
component: {
TextField: {
value: { literalString: '' },
onChange: { actionId: 'update-email', parameters: { field: 'email' } },
placeholder: 'your@email.com',
type: 'email',
},
},
},
{
id: 'submit',
component: {
Button: {
onClick: { actionId: 'submit-form' },
content: { Text: { text: { literalString: 'Submit' } } },
variant: 'primary',
},
},
},
],
initialState: {
name: '',
email: '',
},
};
function ContactForm() {
const [surface] = useState(formSurface);
const handleAction = async (actionId: string, params: any) => {
if (actionId === 'submit-form') {
// Submit logic
} else {
// Update local state
console.log(`${actionId}:`, params);
}
};
return <A2UIRenderer surface={surface} onAction={handleAction} />;
}
Using CLIOutput Component
const cliOutputSurface: SurfaceUpdate = {
surfaceId: 'build-output',
components: [
{
id: 'output',
component: {
CLIOutput: {
output: { literalString: '$ npm run build\nBuilding...\nDone!' },
language: 'bash',
streaming: false,
},
},
},
],
};
Using DateTimeInput Component
const datetimeSurface: SurfaceUpdate = {
surfaceId: 'appointment-picker',
components: [
{
id: 'datetime',
component: {
DateTimeInput: {
value: { literalString: '2024-01-15T10:30:00Z' },
onChange: { actionId: 'update-date' },
placeholder: 'Select appointment time',
includeTime: true,
},
},
},
],
};
State Management
Binding Resolution
State is resolved through the resolveBinding function:
// In your component renderer
const value = resolveBinding({ path: 'user.name' });
State Updates
Send state updates through actions:
const handleAction = (actionId: string, params: any) => {
// Update local state
setLocalState(prev => ({ ...prev, ...params }));
// Send action to backend
sendA2UIAction(actionId, surfaceId, params);
};
API Reference
A2UIParser
| Method | Parameters | Returns | Description |
|---|---|---|---|
parse() |
json: string |
SurfaceUpdate |
Parse and validate JSON string |
parseObject() |
data: unknown |
SurfaceUpdate |
Validate object |
validate() |
value: unknown |
boolean |
Type guard check |
safeParse() |
json: string |
SafeParseResult |
Parse without throwing |
safeParseObject() |
data: unknown |
SafeParseResult |
Validate without throwing |
validateComponent() |
component: unknown |
boolean |
Check if valid component |
A2UIComponentRegistry
| Method | Parameters | Returns | Description |
|---|---|---|---|
register() |
type, renderer |
void |
Register component renderer |
unregister() |
type |
void |
Remove component renderer |
get() |
type |
ComponentRenderer | undefined |
Get renderer |
has() |
type |
boolean |
Check if registered |
getRegisteredTypes() |
- | A2UIComponentType[] |
List all types |
clear() |
- | void |
Remove all renderers |
size |
- | number |
Count of renderers |
Error Handling
A2UIParseError
Custom error class for parsing failures:
import { A2UIParseError } from '@/packages/a2ui-runtime/core/A2UIParser';
try {
a2uiParser.parse(invalidJson);
} catch (error) {
if (error instanceof A2UIParseError) {
console.error(error.message);
console.error(error.getDetails()); // Detailed Zod errors
console.error(error.originalError);
}
}
Best Practices
-
Always validate surfaces before rendering:
if (a2uiParser.validate(surfaceUpdate)) { return <A2UIRenderer surface={surfaceUpdate} onAction={handleAction} />; } -
Handle unknown components gracefully:
const renderer = a2uiRegistry.get(componentType); if (!renderer) { return <FallbackComponent />; } -
Use TypeScript types for type safety:
import type { SurfaceUpdate, A2UIComponent } from '@/packages/a2ui-runtime/core/A2UITypes'; -
Clean up resources when surfaces are removed:
useEffect(() => { return () => { // Cleanup timers, subscriptions, etc. }; }, [surfaceId]);
License
Part of the CCW project.