Skip to content

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 A2uiPlugin and the render_a2ui tool, covered below.
  • Streaming sub-agent output through run_streaming_subagent(), with JSON Render and OpenUI Lang presets behind the json-render and openui features of awaken-ext-generative-ui.
  • 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-starter or openui-chat examples
[dependencies]
awaken = { git = "https://github.com/AwakenWorks/awaken" }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
  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.

  1. 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 messages array.

Message TypePurpose
surfaceUpdateDefine or update the component tree
dataModelUpdatePopulate or change data values
beginRenderingSelect the root component and start rendering
deleteSurfaceRemove a surface

For a new surface, send surfaceUpdate, then dataModelUpdate when data is needed, then beginRendering. Use deleteSurface when the workflow is complete.

  1. Define the component tree.

    Components are a flat list. Each component has an id and 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 id and component.
  • component must be an object with one key, such as { "Text": {...} }.
  • Relationships are expressed by component IDs inside component props, such as child or children.explicitList.
  1. Bind data with data model entries.

    dataModelUpdate.contents is an array of key/value entries. Supported value fields are valueString, valueNumber, valueBoolean, and valueMap.

let message = serde_json::json!({
"dataModelUpdate": {
"surfaceId": "order-form-1",
"path": "/order",
"contents": [
{ "key": "customer", "valueString": "" },
{ "key": "quantity", "valueNumber": 1.0 }
]
}
});
  1. Start rendering.
let message = serde_json::json!({
"beginRendering": {
"surfaceId": "order-form-1",
"root": "root"
}
});
  1. Delete a surface.
let message = serde_json::json!({
"deleteSurface": {
"surfaceId": "order-form-1"
}
});
  1. Send multiple messages in one tool call.

    The render_a2ui tool accepts a messages array 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" }}
]
});
  1. Customize plugin instructions.

    The plugin injects prompt instructions that teach the LLM how to use the render_a2ui tool. Defaults can be set on the plugin, and per-agent overrides can be saved in the generative-ui config section.

// With catalog ID and custom examples appended to the default instructions
let 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 console
let agent_spec = agent_spec.with_section("generative-ui", serde_json::json!({
"catalog_id": "my-catalog",
"examples": "Example: render a compact order summary."
}));
  1. Register the A2UI plugin and run the agent with a prompt that asks it to display information visually.
  2. The agent should call the render_a2ui tool with valid A2UI messages.
  3. The tool result on AgentEvent::ToolCallDone is a small confirmation: {"rendered": true, "count": N} where N is the number of A2UI messages the LLM submitted.
  4. The actual UI markup is on the tool call args, not the result — the frontend reads it from AgentEvent::ToolCallReady (which carries name = "render_a2ui" and arguments = { ..A2UI messages.. }) and renders the surface from there.
  5. On the frontend, confirm the surface appears with the expected components.
SymptomCauseFix
expected at least one A2UI message keyTool called without a direct message key or messages arraySend one of surfaceUpdate, dataModelUpdate, beginRendering, deleteSurface, or {"messages": [...]}
messages array must not be emptyEmpty messages arrayInclude at least one A2UI message
surfaceUpdate.components is requiredComponent update has no component listAdd a non-empty components array
component must contain exactly one component payloadcomponent is not shaped like { "Text": {...} }Use one v0.8 component type per component
dataModelUpdate.contents must not be emptyData model update has no entriesAdd contents entries with key and valueString/valueNumber/valueBoolean/valueMap
beginRendering.root is requiredRender start did not specify the root componentSet root to an existing component ID
LLM does not call the toolPlugin not loaded or hook filtered outAdd "generative-ui" to plugin_ids and include with_hook_filter("generative-ui") when using hook filters
  • examples/examples/generative_ui.rs — streaming sub-agent pipeline with OpenUI Lang output
  • crates/awaken-ext-generative-ui/src/a2ui/tests.rs — validation and tool execution test cases
PathPurpose
crates/awaken-ext-generative-ui/src/a2ui/mod.rsA2UI module root, constants, re-exports
crates/awaken-ext-generative-ui/src/a2ui/plugin.rsA2uiPlugin registration and prompt instructions
crates/awaken-ext-generative-ui/src/a2ui/tool.rsA2uiRenderTool — validation and execution
crates/awaken-ext-generative-ui/src/a2ui/types.rsA2uiMessage, A2uiComponent, and related structs
crates/awaken-ext-generative-ui/src/a2ui/validation.rsvalidate_a2ui_messages structural checks
crates/awaken-ext-generative-ui/src/json_render.rsJSON Render prompt and output compiler
crates/awaken-ext-generative-ui/src/openui.rsOpenUI Lang prompt preset