✦ Native Generative UI for AI Agents

Native UI forms
for AI agents

Native Generative UI for AI agents.
Human-in-the-loop input collection via native OS forms — one JSON in, one native window, one JSON out. No server, no browser, no chat loop.

Download View on GitHub

cargo install a2native  ·  npm install @a2native/js

🤖
Agent
📄
JSON spec
stdin or arg
🪟
Native window
(egui)
📤
JSON result
via stdout

See It In Action

Light and dark themes — with the built-in multilingual AI-generated UI security warning.

a2native light mode — New Project Setup form
Light mode — dropdown, checkboxes, text field, slider
a2native dark mode — Configure Deployment form
Dark mode — radio group, number input, dropdown

The Problem with Chat Loops

Agents asking humans questions one at a time is painfully inefficient.

❌ Chat loop (status quo)

Agent asks a question → user types → agent asks next question → user types…

10 fields = 10 round-trips. Breaks context windows. Loses state on reconnect.

✅ a2native

Agent generates one JSON form spec → user fills everything at once → agent gets all answers in one structured response.

10 fields = 1 round-trip. Works offline. No server required.

Where a2native Fits in the Agentic Protocol Stack

Four complementary protocols — each for a different layer of agent interaction.

LayerProtocolPurpose
Agent ↔ Tools & DataMCPGives agents access to tools, files, APIs
Agent ↔ AgentA2AAgent-to-agent coordination
Agent ↔ Web UIAG-UIStreaming, real-time agent↔browser integration
Agent ↔ Generative UIA2UI (Google)Declarative JSON UI for web / Flutter rendering
Agent ↔ Native Desktopa2nativeSync form collection via native OS windows
Three things that sound alike but aren't:   AG-UI is a streaming web protocol (SSE/WebSocket); Google A2UI is a declarative JSON UI spec for web/Flutter; a2native is a synchronous native-desktop renderer (stdin/stdout, single binary). All three solve agent↔human interaction in different environments — they are complementary. a2native's input format is conceptually inspired by Google A2UI's flat component list but is not an implementation of the Google A2UI spec.

Features

Everything an agent needs to gather structured human input.

🎛️

17 Component Types

Text fields, dropdowns, sliders, file pickers, radio groups, checkboxes, markdown, images, cards and more.

🔄

Session Mode

UUID-keyed long-lifecycle windows. The same window updates across multiple agent turns — no flicker, no re-spawn. Learn more →

🌐

SSE & WebSocket

External agents (Python, Node…) can drive a2native over HTTP/SSE or WebSocket — no CLI wrapper needed. Learn more →

🎨

Theming

Dark mode and custom accent colours via theme in the JSON spec.

⏱️

Timeout

Auto-close windows after N seconds with timeout. Result status becomes "timeout".

🪶

Zero Runtime Deps

Single static binary. No Node, no Python, no browser engine. Ships as one executable file.

🔒

Safety Banner

Every window permanently shows an amber AI-generated warning. Cannot be suppressed by the form spec.

🧑‍💻

Human-in-the-Loop

Pause agent execution, hand control to the user for structured input, then resume — without breaking the agent pipeline.

Input Format — Three Modes

a2native auto-detects the format. Detection priority: AG-UI → Google A2UI → a2native.

Mode 1 — a2native (simplest, recommended for new integrations)

a2native's own format: one JSON object in, one JSON object out. No streaming, no protocol overhead.  JSON Schema ↗  ·  a2n schema

// Input — a2native
{
  "title":     "Deploy to production",
  "timeout":   60,
  "theme":     { "dark_mode": true, "accent_color": "#6C63FF" },
  "components": [
    { "id": "env", "type": "dropdown", "label": "Environment",
      "options": [
        { "value": "prod",  "label": "Production" },
        { "value": "stag", "label": "Staging" }
      ]},
    { "id": "ok", "type": "button", "label": "Deploy", "action": "submit" }
  ]
}

// Output — a2native
{ "status": "submitted", "values": { "env": "prod" } }
ComponentOutput typeNotes
text-fieldstringSingle-line text
textareastringMulti-line text, 4 rows default
passwordstringMasked input — warn users not to submit secrets
number-inputnumberDrag value with optional min/max/step
dropdownstringComboBox, value from options list
checkboxbooleanSingle toggle
togglebooleanOn/off switch
checkbox-groupstring[]Multiple selection
radio-groupstringExclusive selection
slidernumberRange slider, default 0–100
ratingnumberStar rating 1–max (default 5), 0 if unrated
date-pickerstringYYYY-MM-DD text field
time-pickerstringHH:MM text field
file-uploadstringNative file dialog; paths joined by ;
buttonsubmit / cancel / custom actions
cardBordered vertical group, nests any components
rowHorizontal side-by-side columns (equal width)
text / markdown / code / image / dividerDisplay only; code renders monospace read-only block

Mode 2 — Google A2UI (native surface rendering)

Natively accepts Google A2UI v0.8+ surfaceUpdate / beginRendering JSONL. Emits A2UI userAction output. Any agent SDK built for Google A2UI can drive a2native with no adaptation. A2UI Row maps to horizontal layout; Column/List to vertical card groups.

// Input — Google A2UI JSONL
{"surfaceUpdate":{"surfaceId":"form1","components":[
  {"id":"env","component":{"MultipleChoice":{
    "options":[{"label":{"literalString":"Production"},"value":"prod"}],
    "maxAllowedSelections":1,"variant":"dropdown"}}},
  {"id":"lbl","component":{"Text":{"text":{"literalString":"Deploy"}}}},
  {"id":"btn","component":{"Button":{"child":"lbl","action":{"name":"submit"}}}}
]}}

// Output — A2UI userAction
{"userAction":{"name":"submit","surfaceId":"form1","sourceComponentId":"btn",
  "timestamp":"2026-01-01T12:00:00Z","context":{"env":"prod"}}}

Mode 3 — AG-UI (frontend tool handler)

Acts as an AG-UI frontend tool: receives TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END events, renders the form, emits a TOOL_CALL_RESULT. Tool call args can be in a2native or Google A2UI format — both are auto-detected.

// Input — AG-UI event stream
{"type":"RUN_STARTED","threadId":"t1","runId":"r1"}
{"type":"TOOL_CALL_START","toolCallId":"tc1","toolCallName":"show_form"}
{"type":"TOOL_CALL_ARGS","toolCallId":"tc1","delta":"{"title":"Deploy","}
{"type":"TOOL_CALL_ARGS","toolCallId":"tc1","delta":""components":[{"id":"ok","type":"button","label":"Go","action":"submit"}]}"}
{"type":"TOOL_CALL_END","toolCallId":"tc1"}

// Output — AG-UI TOOL_CALL_RESULT
{"type":"TOOL_CALL_RESULT","messageId":"tc1-result","toolCallId":"tc1",
 "content":"{"status":"submitted","values":{}}","role":"tool"}

⚠ Security Considerations

a2native is called by AI agents automatically. Understand the risks.

The banner above appears at the top of every a2native window. It cannot be hidden by the form spec.

Prompt Injection

A compromised agent could mimic a trusted UI (fake password dialog, fake bank login).

Credential Harvesting

Malicious agents can ask for passwords or API keys and exfiltrate them.

Social Engineering

Markdown components allow arbitrary text. Attackers can craft convincing deceptive messages.

Session Hijacking

Guessable session UUIDs can be exploited by other processes on the same machine.

Mitigations: Only run a2native from agents you trust. Use random UUIDs (v4) for sessions. Set short timeout values. Never enter passwords or private keys into a2native forms.

Usage with AI Agents

Tell your agent — no SDK or setup required.

Just tell your agent

The simplest approach — include this in your system prompt or AGENTS.md:

Use `a2native` (`a2n`) to collect structured
input from the user instead of asking
questions in chat.

Quick reference:
  a2n '{"components":[...]}' # one-shot
  a2n --session UUID '...'   # multi-turn
  a2n help                   # full schema

Always include at least one submit button:
  {"id":"ok","type":"button",
   "label":"Submit","action":"submit"}

LLM tool definition

Define show_form as a tool in your framework:

{
  "name": "show_form",
  "description": "Show a native UI form to the user and return their input. Use this instead of asking questions in chat.",
  "parameters": {
    "type": "object",
    "properties": {
      "title":      { "type": "string" },
      "components": { "type": "array" }
    },
    "required": ["components"]
  }
}

Python example

import subprocess, json

form = {
  "title": "Configure deployment",
  "components": [
    {"id": "env", "type": "dropdown", "label": "Environment",
     "options": [{"value":"prod","label":"Production"},{"value":"stg","label":"Staging"}],
     "required": True},
    {"id": "tag",  "type": "text-field", "label": "Image tag",
     "placeholder": "v1.2.3", "required": True},
    {"id": "go",   "type": "button",     "label": "Deploy", "action": "submit"},
  ]
}

result = subprocess.run(["a2n", json.dumps(form)],
                        capture_output=True, text=True)
data = json.loads(result.stdout)

if data["status"] == "submitted":
    deploy(data["values"]["env"], data["values"]["tag"])

JavaScript SDK

# npm install @a2native/js
import { showForm } from "@a2native/js";

const result = await showForm({
  title: "Pick a model",
  components: [
    { id: "model", type: "radio-group", label: "Model",
      options: [{ value: "gpt-4o", label: "GPT-4o" },
                { value: "claude", label: "Claude" }],
      required: true },
    { id: "ok", type: "button", label: "Select", action: "submit" },
  ],
});
// result.status === "submitted", result.values.model === "gpt-4o"

Session Mode — Multi-Turn Interactions

Inspired by agent-browser's client-daemon pattern: the window lives, the agent turns are short. Pass the same UUID on every turn and the form updates in place — no flicker, no re-spawn.

# Turn 1 — opens the window (daemon spawns in background)
echo '{"title":"Wizard 1/3","components":[...]}' | a2n --session abc123
# → {"status":"submitted","values":{...}}

# Turn 2 — same window, new form (no new window spawned)
echo '{"title":"Wizard 2/3","components":[...]}' | a2n --session abc123

# Turn 3 — final step
echo '{"title":"Wizard 3/3","components":[...]}' | a2n --session abc123

# Close the window when done
a2n --close abc123
Turn 1
daemon spawns Window opens. First form shown.
User
Fills form, clicks Submit → JSON → stdout
Window
Shows "⏳ Waiting for the agent to send the next step…"
Turn 2
IPC update Window form replaced. No flicker.
--close
Daemon exits cleanly. Port file removed.
How it works: On the first call with a new UUID the CLI spawns a background daemon that binds a random TCP port and writes it to {TMPDIR}/a2n-session-{uuid}.port. Subsequent calls with the same UUID connect via IPC and push the new form spec without opening a new process. The egui window lives on the daemon's main thread throughout.

For HTTP/SSE and WebSocket daemon usage, see the Advanced Usage → page.

Get Started

Install via Cargo, or download a pre-built binary.

# Install from crates.io
cargo install a2native

# Or build from source
git clone https://github.com/a2native/a2native.git
cd a2native && cargo build --release

# Show help + full schema reference
a2n help

# One-shot: JSON as inline argument
a2n '{"components":[{"id":"n","type":"text-field","label":"Name","required":true}]}'

# One-shot: JSON from stdin
echo '{"title":"Pick one","components":[...]}' | a2n

For multi-turn sessions and HTTP/SSE or WebSocket integration, see the Advanced Usage → page.