Use Generative UI
Use this when you want the agent to send declarative UI components to a frontend — for example, rendering a form, a data table, or an interactive card without the frontend knowing the layout in advance.
Awaken currently supports two integration styles:
- A2UI tool calls through
A2uiPluginand therender_a2uitool, covered below. - Streaming sub-agent output through
run_streaming_subagent(), with JSON Render and OpenUI Lang presets behind thejson-renderandopenuifeatures ofawaken-ext-generative-ui.
Prerequisites
Section titled “Prerequisites”- A working awaken agent runtime (see Build an Agent)
- For A2UI: a frontend that consumes A2UI messages from the event stream and a registered component catalog
- For JSON Render or OpenUI Lang: a frontend renderer that consumes streamed tool output, such as the
ai-sdk-starteroropenui-chatexamples
[dependencies]awaken = { git = "https://github.com/AwakenWorks/awaken" }tokio = { version = "1", features = ["full"] }serde_json = "1"- Register the A2UI plugin.
use std::sync::Arc;use awaken::engine::GenaiExecutor;use awaken::ext_generative_ui::A2uiPlugin;use awaken::registry_spec::ModelSpec;use awaken::registry_spec::AgentSpec;use awaken::{AgentRuntimeBuilder, Plugin};
let plugin = A2uiPlugin::with_catalog_id("my-catalog");let mut agent_spec = AgentSpec::new("ui-agent") .with_model_id("gpt-4o-mini") .with_system_prompt("Render structured UI when visual output helps.") .with_hook_filter("generative-ui");agent_spec.plugin_ids.push("generative-ui".into());
let runtime = AgentRuntimeBuilder::new() .with_provider("openai", Arc::new(GenaiExecutor::new())) .with_model(ModelSpec::new("gpt-4o-mini", "openai", "gpt-4o-mini")) .with_agent_spec(agent_spec) .with_plugin("generative-ui", Arc::new(plugin) as Arc<dyn Plugin>) .build() .expect("failed to build runtime");The plugin registers a tool called render_a2ui that the LLM can invoke. When the LLM calls this tool with A2UI messages, the tool validates the message structure and returns the validated payload, which flows through the event stream to the frontend.
plugin_ids loads the plugin for the agent. with_hook_filter("generative-ui")
keeps the A2UI prompt-injection hook active when the agent also loads other
plugins.
-
Understand the A2UI protocol.
Awaken’s A2UI tool uses the v0.8 server-to-client message keys directly. A tool call can pass exactly one message object or a legacy
messagesarray.
| Message Type | Purpose |
|---|---|
surfaceUpdate | Define or update the component tree |
dataModelUpdate | Populate or change data values |
beginRendering | Select the root component and start rendering |
deleteSurface | Remove a surface |
For a new surface, send surfaceUpdate, then dataModelUpdate when data is
needed, then beginRendering. Use deleteSurface when the workflow is complete.
-
Define the component tree.
Components are a flat list. Each component has an
idand a v0.8 component payload object with exactly one component type:
// The LLM sends this via the render_a2ui tool:let message = serde_json::json!({ "surfaceUpdate": { "surfaceId": "order-form-1", "components": [ { "id": "root", "component": { "Card": { "child": "title" } } }, { "id": "title", "component": { "Text": { "text": { "literalString": "New Order" } } } } ] }});Rules for component lists:
- Every component requires
idandcomponent. componentmust be an object with one key, such as{ "Text": {...} }.- Relationships are expressed by component IDs inside component props, such as
childorchildren.explicitList.
-
Bind data with data model entries.
dataModelUpdate.contentsis an array of key/value entries. Supported value fields arevalueString,valueNumber,valueBoolean, andvalueMap.
let message = serde_json::json!({ "dataModelUpdate": { "surfaceId": "order-form-1", "path": "/order", "contents": [ { "key": "customer", "valueString": "" }, { "key": "quantity", "valueNumber": 1.0 } ] }});- Start rendering.
let message = serde_json::json!({ "beginRendering": { "surfaceId": "order-form-1", "root": "root" }});- Delete a surface.
let message = serde_json::json!({ "deleteSurface": { "surfaceId": "order-form-1" }});-
Send multiple messages in one tool call.
The
render_a2uitool accepts amessagesarray for multi-message calls:
let args = serde_json::json!({ "messages": [ { "surfaceUpdate": { "surfaceId": "s1", "components": [ { "id": "root", "component": { "Text": { "text": { "literalString": "Hello" } }}} ] }}, { "beginRendering": { "surfaceId": "s1", "root": "root" }} ]});-
Customize plugin instructions.
The plugin injects prompt instructions that teach the LLM how to use the
render_a2uitool. Defaults can be set on the plugin, and per-agent overrides can be saved in thegenerative-uiconfig section.
// With catalog ID and custom examples appended to the default instructionslet plugin = A2uiPlugin::with_catalog_and_examples( "my-catalog", "Example: create a card with a title and a button...");
// With fully custom instructions (replaces the default instructions entirely)let plugin = A2uiPlugin::with_custom_instructions( "You can render UI by calling render_a2ui...".to_string());
// Per-agent config, equivalent to editing the generative-ui section in the admin consolelet agent_spec = agent_spec.with_section("generative-ui", serde_json::json!({ "catalog_id": "my-catalog", "examples": "Example: render a compact order summary."}));Verify
Section titled “Verify”- Register the A2UI plugin and run the agent with a prompt that asks it to display information visually.
- The agent should call the
render_a2uitool with valid A2UI messages. - The tool result on
AgentEvent::ToolCallDoneis a small confirmation:{"rendered": true, "count": N}whereNis the number of A2UI messages the LLM submitted. - The actual UI markup is on the tool call args, not the result — the frontend reads it from
AgentEvent::ToolCallReady(which carriesname = "render_a2ui"andarguments = { ..A2UI messages.. }) and renders the surface from there. - On the frontend, confirm the surface appears with the expected components.
Common Errors
Section titled “Common Errors”| Symptom | Cause | Fix |
|---|---|---|
expected at least one A2UI message key | Tool called without a direct message key or messages array | Send one of surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface, or {"messages": [...]} |
messages array must not be empty | Empty messages array | Include at least one A2UI message |
surfaceUpdate.components is required | Component update has no component list | Add a non-empty components array |
component must contain exactly one component payload | component is not shaped like { "Text": {...} } | Use one v0.8 component type per component |
dataModelUpdate.contents must not be empty | Data model update has no entries | Add contents entries with key and valueString/valueNumber/valueBoolean/valueMap |
beginRendering.root is required | Render start did not specify the root component | Set root to an existing component ID |
| LLM does not call the tool | Plugin not loaded or hook filtered out | Add "generative-ui" to plugin_ids and include with_hook_filter("generative-ui") when using hook filters |
Related Example
Section titled “Related Example”examples/examples/generative_ui.rs— streaming sub-agent pipeline with OpenUI Lang outputcrates/awaken-ext-generative-ui/src/a2ui/tests.rs— validation and tool execution test cases
Key Files
Section titled “Key Files”| Path | Purpose |
|---|---|
crates/awaken-ext-generative-ui/src/a2ui/mod.rs | A2UI module root, constants, re-exports |
crates/awaken-ext-generative-ui/src/a2ui/plugin.rs | A2uiPlugin registration and prompt instructions |
crates/awaken-ext-generative-ui/src/a2ui/tool.rs | A2uiRenderTool — validation and execution |
crates/awaken-ext-generative-ui/src/a2ui/types.rs | A2uiMessage, A2uiComponent, and related structs |
crates/awaken-ext-generative-ui/src/a2ui/validation.rs | validate_a2ui_messages structural checks |
crates/awaken-ext-generative-ui/src/json_render.rs | JSON Render prompt and output compiler |
crates/awaken-ext-generative-ui/src/openui.rs | OpenUI Lang prompt preset |