Skip to content

A2UI v0.9 Usage in Cinatra

A2UI (Agent-to-UI) is a declarative UI payload format carried inside AG-UI STATE_SNAPSHOT events. It is distinct from AG-UI itself:

LayerRoleTransport
AG-UIStreaming lifecycle transportSSE / WebSocket
A2UIDeclarative UI surface descriptionPayload inside STATE_SNAPSHOT

A2UI v0.9 was activated in Phase 111. The spec lives at packages/agent-ui-protocol/src/a2ui-spec.ts.


Execution worker
└─ AgUiAdapter (AG-UI lifecycle calls)
└─ A2UiAdapter (A2UI surface creation/deletion)
└─ publishA2UiEvent → Redis Streams → client STATE_SNAPSHOT

A2UiAdapter is wired in parallel at every lifecycle site in:

  • packages/agents/src/agentic-execution.ts
  • packages/agents/src/langgraph-execution.ts
  • packages/agents/src/agentic-resume.ts

Phase 111 delivers the first concrete A2UI use case: collapsing sequential per-field HITL setup interrupts into a single grouped form.

  1. Add "x-renderer": "@cinatra/agent-builder:grouped-setup-form" to any one of the agent’s setup fields in agent.json.
  2. execution.ts checks agentOptsIntoGrouped = pendingFields.some(f => properties[f]?.["x-renderer"] === GROUPED_SETUP_FORM_RENDERER_ID).
  3. When pendingFields.length >= 2 and agentOptsIntoGrouped is true → grouped path. Otherwise → per-field path (Phase 105 behavior preserved).
{
"inputSchema": {
"properties": {
"offeringCompanyWebsite": {
"type": "string",
"format": "uri",
"title": "Company website",
"x-renderer": "@cinatra/agent-builder:grouped-setup-form"
}
}
}
}

Only one field needs the annotation — it is the opt-in signal, not a per-field decoration.

GroupedSetupFormRenderer (priority 50 in register-default-renderers.ts):

  • Iterates schema.properties in order: required fields first, then optional.
  • Delegates each field to fieldRendererRegistry.resolve() (same resolver as AgenticRunPanel) or falls back to SchemaFieldRenderer.
  • Wraps each sub-renderer in a react-hook-form <Controller> so sub-renderer onChange buffers into form state — it does not call the outer onChange per keystroke.
  • Strips x-renderer === grouped-setup-form from the marker field’s schema before sub-renderer resolution to prevent recursive matching.
  • A single “Save & start run” button at the bottom submits the whole form via form.handleSubmit → onChange(mergedValues).
  • Uses useEffect(() => form.reset(value ?? {}), [form, value]) to keep react-hook-form defaultValues in sync with upstream prop changes after mount.
  • Uses useWatch({ control: form.control }) (not form.watch()) to build subContext.allFieldValues without broad re-render churn.

Sub-renderers receive hideSubmit={true}. SchemaFieldRenderer and FollowUpCadenceFieldRenderer honour this by suppressing their internal Continue button. Custom renderers (gmail-sender, cta, contact-source-selector) are assumed safe in grouped mode — they expose their value via controlled value + onChange and have no standalone submit action.


createSurface with the same surfaceId emitted more than once (e.g., on resume/retry) must be treated as a no-op by consumers. The current Redis Streams TTL provides cleanup for stale surfaces.


The following agents and workflows could adopt grouped setup via the same x-renderer annotation mechanism, requiring no backend changes:

Agent / WorkflowSetup fields todayGrouped benefit
email-drafts2–3 fieldsMinor friction reduction
email-recipients2 fieldsSmall win
Future onboarding wizard4–6 fieldsHigh value — multi-step replaced by one form
Any agent with ≥ 3 required setup fields≥ 3Standard pattern to adopt

To adopt: add "x-renderer": "@cinatra/agent-builder:grouped-setup-form" to one field in the agent’s inputSchema, bump packageVersion in agent.json.


Phase 114 adds three mid-run review screens to the A2UI layer. Unlike the grouped setup form (which fires before the run starts), these surfaces fire during an already-running LangGraph execution when a build_hitl_gate node emits an interrupt.

A2UI v0.9 has no DataTable primitive. Mid-run review screens use: Column wrapping a title Text, a List with a row template (or a Card for summary views), and a Row of Approve + Reject Button components.

The three translator functions in packages/agent-ui-protocol/src/server.ts build these trees and are keyed in A2UiAdapter.MID_RUN_TRANSLATORS by xRenderer ID.

xRenderer: @cinatra-agents/email-recipients:output
Translator: translateRecipientsOutputToA2Ui

// Abbreviated component tree
Column([
Text("Review Recipients", { variant: "heading" }),
List({
items: recipients.map(r => ({
id: r.email,
cells: [Text(r.name), Text(r.email), Text(r.company ?? "")],
})),
rowTemplate: "three-column",
}),
Row([
Button("Approve", { action: { event: { name: "approve_review_task", values: { literal: { approved: true } } } } }),
Button("Reject", { action: { event: { name: "reject_review_task", values: { literal: { approved: false } } } } }),
]),
])

xRenderer: @cinatra-agents/email-drafts:output
Translator: translateDraftsOutputToA2Ui

Column([
Text("Review Email Drafts", { variant: "heading" }),
List({
items: drafts.map(d => ({
id: d.id,
cells: [Text(d.recipientEmail ?? ""), Text(d.subject), Text(d.body.slice(0, 120) + "")],
})),
}),
Row([
Button("Approve", { action: { event: { name: "approve_review_task", values: { literal: { approved: true } } } } }),
Button("Reject", { action: { event: { name: "reject_review_task", values: { literal: { approved: false } } } } }),
]),
])

Recipe 3: Summary card — Send Confirmation

Section titled “Recipe 3: Summary card — Send Confirmation”

xRenderer: @cinatra-agents/email-sender:output
Translator: translateSendOutputToA2Ui

Column([
Text("Confirm Send", { variant: "heading" }),
Card([
Text(`Recipients: ${summary.recipientCount ?? ""}`),
Text(`Drafts ready: ${summary.draftCount ?? ""}`),
]),
Text("This action sends real emails and cannot be undone.", { variant: "warning" }),
Row([
Button("Send now", { action: { event: { name: "approve_review_task", values: { literal: { approved: true } } } } }),
Button("Cancel", { action: { event: { name: "reject_review_task", values: { literal: { approved: false } } } } }),
]),
])

Both buttons use the action.event pattern:

Buttonaction.event.namecontext.values.literal
Approve"approve_review_task"{ "approved": true }
Reject"reject_review_task"{ "approved": false }

The reviewTaskId is carried separately in the interrupt envelope; the button action fires approveReviewTaskServerAction(reviewTaskId, values) on the TS side.

HITL surfaces created during a mid-run gate currently linger until Redis Streams TTL expires (see Pitfall P7 in 114-RESEARCH.md). AgentUIAdapter.onResume does not yet accept a reviewTaskId argument to delete the surface on resume. This is acceptable for v1 — no A2UI native client renders these surfaces yet; only the workspace AgenticRunPanel consumes the INTERRUPT event stream directly via fieldRendererRegistry.

A2UiAdapter.MID_RUN_TRANSLATORS maps xRenderer IDs to translator functions:

const MID_RUN_TRANSLATORS: Record<string, MidRunTranslator> = {
"@cinatra-agents/email-recipients:output": translateRecipientsOutputToA2Ui,
"@cinatra-agents/email-drafts:output": translateDraftsOutputToA2Ui,
"@cinatra-agents/email-sender:output": translateSendOutputToA2Ui,
};

onInterrupt checks this table first; grouped-setup-form interrupts fall through to the existing handler.


HITL-specific A2UI surfaces are currently cleaned up by Redis Streams TTL (Phase 999.6). Explicit deletion on resume is deferred: AgentUIAdapter.onResume does not yet accept a reviewTaskId. See Phase 111 concern D-01 for the follow-up extension.