Skip to content

Agent Protocol Interop — A2A, AG-UI, and A2UI

Table of Contents

  1. Introduction - The Agent Protocol Stack
  2. Protocol Comparison
  3. A2A (Agent-to-Agent) Protocol
  4. AG-UI (Agent-User Interaction) Protocol
  5. A2UI (Agent to UI) Protocol
  6. Complete Examples
  7. Cookbook
  8. Troubleshooting

1. Introduction - The Agent Protocol Stack

The Problem

Modern AI systems need to communicate in multiple directions: agents talk to other agents, agents talk to users through chat interfaces, and agents render rich interactive UIs. Each of these communication patterns has different requirements for transport, data format, and interaction model. Without standardized protocols, every integration becomes a custom project.

Helix's Answer

Helix implements four complementary agent protocols as native integration components, each serving a distinct role in the agent ecosystem:

                    The Helix Agent Protocol Stack
    ┌──────────────────────────────────────────────────────────┐
    │                                                          │
    │   ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐  │
    │   │   MCP    │  │   A2A    │  │  AG-UI   │  │  A2UI  │  │
    │   │  Server  │  │  Server  │  │  Server  │  │ Server │  │
    │   │          │  │          │  │          │  │        │  │
    │   │ Tools for│  │ Agent-to-│  │ Chat SSE │  │ Rich   │  │
    │   │ LLM apps │  │ Agent    │  │ for chat │  │ UI     │  │
    │   │          │  │ comms    │  │ frontends│  │ render │  │
    │   └────┬─────┘  └────┬─────┘  └────┬─────┘  └───┬────┘  │
    │        │             │             │             │        │
    │   ┌────┴─────────────┴─────────────┴─────────────┴────┐  │
    │   │              Helix Engine + Routes                 │  │
    │   │          (Processors, Interceptors, Orchestration Patterns)          │  │
    │   └────┬─────────────┬─────────────────────────────────┘  │
    │        │             │                                    │
    │   ┌────┴─────┐  ┌────┴─────┐                              │
    │   │   MCP    │  │   A2A    │                              │
    │   │  Client  │  │  Client  │                              │
    │   │          │  │          │                              │
    │   │ Call ext │  │ Call ext │                              │
    │   │ MCP tools│  │ A2A agts │                              │
    │   └──────────┘  └──────────┘                              │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

    External connections:

    Claude Desktop ──► MCP Server        (tool discovery + invocation)
    Other AI Agent ──► A2A Server        (JSON-RPC task execution)
    Chat Frontend  ──► AG-UI Server      (SSE streaming chat)
    Rich UI Client ──► A2UI Server       (declarative UI rendering)
    Helix Workflow ──► MCP Client  ──►   External MCP Server
    Helix Workflow ──► A2A Client  ──►   External A2A Agent

Why Four Protocols?

Each protocol addresses a different integration surface:

  • MCP (Model Context Protocol): Lets LLM applications like Claude Desktop discover and call your workflows as tools. Covered in the MCP Guide.
  • A2A (Agent-to-Agent): Lets AI agents communicate with each other using Google's open protocol. Supports task lifecycle and streaming.
  • AG-UI (Agent-User Interaction): Provides a standardized SSE-based protocol for chat frontends like CopilotKit to stream agent responses.
  • A2UI (Agent to UI): Enables agents to generate rich declarative UIs with components, data binding, and event handling.

This guide covers A2A, AG-UI, and A2UI in depth. For MCP, see the dedicated MCP Guide.


2. Protocol Comparison

Feature MCP A2A AG-UI A2UI
Purpose Tool exposure for LLMs Agent-to-agent communication Chat streaming for frontends Rich UI rendering
Transport HTTP / SSE / stdio JSON-RPC over HTTP SSE over HTTP POST SSE (GET) + HTTP POST
Default Port 3000 9000 4100 4200
SDK go-sdk v1.4.0 a2a-go v0.3.7 ag-ui community/go a2ui-go v0.3.0
Scheme (Server) mcp-server:// a2a-server:// agui:// a2ui://
Scheme (Client) mcp:// a2a:// N/A N/A
Streaming Via transport Artifact events TEXT_MESSAGE_CONTENT updateComponents
Discovery Tool listing Agent Card N/A N/A
Direction Bidirectional Bidirectional Server only Server only
Primary Consumer Claude, Cursor, IDEs Other AI agents CopilotKit, chat UIs Custom UI renderers

When to Use What

"I want Claude Desktop to call my workflows"           → MCP
"I want my agent to talk to another agent"           → A2A
"I want a chat UI that streams responses"            → AG-UI
"I want my agent to render forms, cards, dashboards" → A2UI
"I want all of the above"                            → All four (see Section 6.1)

3. A2A (Agent-to-Agent) Protocol

3.1 What is A2A

The Agent-to-Agent (A2A) protocol is an open standard initiated by Google that enables AI agents to discover, communicate with, and delegate tasks to each other. It defines:

  • Agent Cards: Machine-readable descriptions of an agent's capabilities, published at /.well-known/agent-card.json
  • Task Lifecycle: Agents send messages, receive status updates (submitted -> working -> completed/failed/canceled), and stream artifact chunks
  • Transport: JSON-RPC 2.0 over HTTP, with support for streaming via server-sent events

Helix uses the official Go SDK:

github.com/a2aproject/a2a-go v0.3.7

This provides a2alib (protocol types), a2asrv (server framework), and a2aclient (client SDK).

3.2 A2A Server - Exposing Agents

The A2A Server component starts a JSON-RPC server that exposes your Helix agents and workflows as A2A-compliant agents. External agents can discover your capabilities via the Agent Card and send tasks.

Quick Start - YAML Config

The simplest way to expose agents via A2A:

helix.yaml

helix:
  a2a:
    servers:
      - enabled: true
        addr: "0.0.0.0:9000"
        name: "support-agent"
        description: "Customer support AI agent"
        version: "1.0.0"

workflows:
  - id: handle-support
    description: "Handle customer support queries with AI"
    trigger:
      uri: direct://handle-support
    steps:
      - target: anthropic://claude-sonnet?maxTokens=1024
      - log:
          message: "Support response: ${body}"

Run it:

helix run helix.yaml

The agent is now discoverable at http://localhost:9000/.well-known/agent-card.json and accepts tasks at http://localhost:9000/invoke.

Quick Start - Workflow-Based

Define the A2A server as a workflow consumer:

workflows:
  - id: a2a-bridge
    trigger:
      uri: a2a-server://0.0.0.0:9000?name=my-agent&description=My+AI+Agent&version=1.0.0
    steps:
      - target: anthropic://claude-sonnet?maxTokens=1024

This starts the A2A server as part of the workflow lifecycle. The workflow's processor pipeline handles all incoming A2A tasks.

Quick Start - Go DSL

package main

import (
    "context"
    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/exchange"
    "github.com/arvenza/helix/pkg/component/a2a"
)

func main() {
    h := engine.NewContext("a2a-demo")

    // Register the A2A server and client components.
    h.AddComponent("a2a-server", a2a.NewServer(h))
    h.AddComponent("a2a", a2a.NewClient())

    // Define a workflow that will become an A2A skill.
    h.AddWorkflow(&engine.Workflow{
        ID:          "answer-question",
        Description: "Answer general knowledge questions",
        FromURI:     "direct://answer-question",
        Processors: []engine.Processor{
            engine.ProcessorFunc(func(ctx context.Context, ex *exchange.Exchange) error {
                question := ex.In().BodyAsString()
                ex.In().SetBody("The answer to '" + question + "' is 42.")
                return nil
            }),
        },
    })

    // Start the A2A server route.
    h.AddWorkflow(&engine.Workflow{
        ID:      "a2a-server",
        FromURI: "a2a-server://0.0.0.0:9000?name=qa-agent&description=Q%26A+Agent&version=1.0.0",
    })

    ctx := context.Background()
    h.Start(ctx)
    defer h.Stop(ctx)

    select {} // Block forever
}

How Agent Discovery Works

When the A2A server starts, it automatically discovers capabilities from two sources:

  1. Agent Component: If the agent component is registered, all agents returned by AgentIDs() are registered as A2A skills. Each agent's type (react, supervisor, plan-execute, etc.) is included as a tag.

  2. Invocable Routes: All routes with direct:// or seda:// from-URIs are registered as skills. The workflow's Description field becomes the skill description.

If no agents or invocable workflows are found, a default skill is created from the server's name and description.

Discovery priority:

1. Agent component agents  →  skill per agent
2. direct:// routes        →  skill per workflow
3. seda:// routes          →  skill per workflow
4. (fallback)              →  single default skill

Agent Card

The Agent Card is served at /.well-known/agent-card.json:

{
  "name": "support-agent",
  "description": "Customer support AI agent",
  "url": "http://0.0.0.0:9000/invoke",
  "version": "1.0.0",
  "preferredTransport": "jsonrpc",
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "capabilities": {
    "streaming": true
  },
  "skills": [
    {
      "id": "handle-support",
      "name": "handle-support",
      "description": "Handle customer support queries with AI",
      "tags": ["route"]
    }
  ]
}

External agents and agent discovery systems use this card to understand what your agent can do.

Streaming Support

The A2A server supports two streaming body types from the processor pipeline:

Body Type Behavior
*llm.StreamReader LLM chunks streamed as A2A artifact update events
<-chan string String channel streamed as A2A artifact update events
Any other type Sent as a single final message

Streaming event flow:

1. StatusUpdate(working)           ← Agent starts processing
2. ArtifactEvent(first chunk)      ← First artifact created
3. ArtifactUpdateEvent(chunk N)    ← Subsequent chunks appended
4. ArtifactUpdateEvent(lastChunk)  ← Final chunk marker
5. Message(full text)              ← Complete response message

Task invocation flow:

  1. External agent sends a message via JSON-RPC
  2. Server creates an Exchange with InOut pattern
  3. User message text is set as the exchange body
  4. Headers are set: HelixA2ATaskID, HelixA2AContextID, HelixStream=true
  5. Exchange runs through the processor pipeline (including interceptors)
  6. Result is streamed back as A2A events or returned as a final message
  7. On failure, a TaskStateFailed status update is sent with the error message

3.3 A2A Client - Calling External Agents

The A2A Client component lets your Helix workflows send messages to external A2A agents. This enables agent delegation, multi-agent orchestration, and cross-system AI collaboration.

URI Format

a2a://server-address:port
Part Description
server-address:port The A2A server's address (http:// prefix is added automatically)

The client connects to http://server-address:port/invoke using JSON-RPC transport.

YAML Example

workflows:
  - id: delegate-research
    trigger:
      uri: direct://research
    steps:
      # Set the question as the body
      - transform:
          starlang: |
            payload.get("question", "What is the meaning of life?")

      # Call the external A2A research agent
      - target: a2a://research-agent.internal:9000

      # Log the response
      - log:
          message: "Research result: ${body}"

Go DSL Example

h.AddWorkflow(&engine.Workflow{
    ID:      "call-agent",
    FromURI: "direct://delegate",
    ToURIs:  []string{"a2a://expert-agent:9000"},
})

Streaming Mode

Set the HelixStream header to "true" to receive a streaming response:

workflows:
  - id: stream-from-agent
    trigger:
      uri: direct://stream
    steps:
      - set-header:
          name: HelixStream
          constant: "true"
      - target: a2a://streaming-agent:9000
      # Body is now a <-chan string that yields chunks

When streaming is enabled: - The client calls SendStreamingMessage instead of SendMessage - A chan string is set as the exchange body - Chunks arrive from artifact update events and status messages - The channel is closed when the stream ends or an error occurs

Non-Streaming Response

Without the HelixStream header, the client uses SendMessage and waits for the complete response:

  • If the result is a Message, the text parts are extracted and set as the body
  • If the result is a Task, artifacts are extracted first, then status message, then last history message
  • The HelixA2ATaskID and HelixA2ATaskState headers are set from the task result

Lazy Connection

The A2A client uses sync.Once for lazy initialization. The connection to the external agent is established on the first message, not at route startup. This means:

  • Routes start instantly even if the target agent is not yet available
  • Connection errors surface at first use, not at boot
  • This matches the lazy-init pattern used by other Helix components (MCP, gRPC, Kafka)

Calling A2A Agents as Agent Tools (type: a2a)

Beyond using A2A in workflows with target: a2a://..., agents can call external A2A agents as tools during their reasoning loop. This is the recommended way to delegate work to remote agents from within an AI agent.

agents:
  - id: coordinator
    type: supervisor
    trigger: http-server://0.0.0.0:8080/coordinate
    model:
      provider: openai
      model: gpt-4o
    system_prompt: |
      You coordinate research and analysis tasks.
      Use the research_agent for gathering information.
      Use the analyst_agent for data analysis.
    tools:
      - name: research_agent
        type: a2a
        description: "Delegate research tasks to the research agent"
        a2a_server:
          url: "http://research-agent.internal:9000"
        auth:
          type: bearer
          token: "${env:A2A_RESEARCH_TOKEN}"
      - name: analyst_agent
        type: a2a
        description: "Send data analysis requests to the analyst agent"
        a2a_server:
          url: "http://analyst-agent.internal:9000"
        auth:
          type: oauth2
          token_url: "https://auth.example.com/token"
          client_id: "${env:OAUTH_CLIENT_ID}"
          client_secret: "${env:OAUTH_CLIENT_SECRET}"
          scopes: ["agent:invoke"]

How it works:

  1. The supervisor agent decides which tool to call based on its reasoning
  2. The agent sends the tool arguments as a message to the remote A2A agent
  3. The remote agent processes the request and returns a response
  4. The response becomes the tool result, and the supervisor continues reasoning

Auth types supported: bearer, api-key, oauth2, mtls. See the Security Guide for details.

When to use type: a2a vs sub_agents:

Scenario Mechanism
Agents in the same flow file sub_agents: [agent-id]
Agents on remote servers (A2A protocol) type: a2a tool with a2a_server.url

3.4 Configuration Reference

helix.yaml - A2A Server Config

helix:
  a2a:
    servers:
      - enabled: true                    # Enable this A2A server instance
        addr: "0.0.0.0:9000"           # Listen address (default port: 9000)
        name: "my-agent"               # Agent name in the Agent Card
        description: "My AI agent"     # Agent description in the Agent Card
        version: "1.0.0"              # Agent version in the Agent Card

URI Parameters - A2A Server

a2a-server://addr?name=my-agent&description=My+Agent&version=1.0.0
Parameter Default Description
name helix-a2a Agent name reported in the Agent Card
description Helix AI Agent Agent description in the Agent Card
version 1.0.0 Agent version in the Agent Card

URI Parameters - A2A Client

a2a://host:port

No additional parameters. The client always uses JSON-RPC transport to http://host:port/invoke.

3.5 Exchange Headers

Headers set during A2A interactions:

Header Direction Description
HelixA2ATaskID In/Out A2A task ID
HelixA2AContextID In A2A context ID (server-side)
HelixA2ATaskState Out Task state after non-streaming call (client-side)
HelixStream In Set to "true" to enable streaming mode
Content-Type Out Set to text/event-stream in streaming mode

4. AG-UI (Agent-User Interaction) Protocol

4.1 What is AG-UI

The AG-UI (Agent-User Interaction) protocol is a standard for streaming agent responses to chat-style frontend applications. It defines a set of SSE events that allow frontends to render agent messages progressively, similar to how ChatGPT streams responses.

AG-UI is the protocol used by CopilotKit and other agent UI frameworks. By implementing AG-UI, your Helix agents become compatible with any CopilotKit-powered frontend out of the box.

Helix uses the official community Go SDK:

github.com/ag-ui-protocol/ag-ui/sdks/community/go

This provides event types (events package), message types (types package), and SSE encoding (sse package).

4.2 AG-UI Server

The AG-UI server accepts HTTP POST requests containing RunAgentInput JSON and streams back AG-UI events as Server-Sent Events.

URI Format

agui://host:port/path?name=my-agent
Part Description
host:port Listen address (default port: 4100)
/path HTTP path for the endpoint (default: /)
name Agent name (default: helix-agui)

HTTP POST Endpoint

The server listens for POST requests at the configured path. The request body must be a RunAgentInput JSON object:

{
  "threadId": "thread-123",
  "runId": "run-456",
  "messages": [
    {"role": "user", "content": "What is the weather in Paris?"}
  ],
  "tools": [],
  "state": {}
}

The server extracts the last user message from the messages array and passes it to the processor pipeline.

SSE Event Flow

The server streams the following events in order:

Client                              AG-UI Server
  │                                      │
  │  POST /agent  {RunAgentInput}        │
  │─────────────────────────────────────►│
  │                                      │
  │  SSE: RUN_STARTED                    │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: TEXT_MESSAGE_START             │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: TEXT_MESSAGE_CONTENT (chunk 1) │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: TEXT_MESSAGE_CONTENT (chunk 2) │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: TEXT_MESSAGE_CONTENT (chunk N) │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: TEXT_MESSAGE_END               │
  │◄─────────────────────────────────────│
  │                                      │
  │  SSE: RUN_FINISHED                   │
  │◄─────────────────────────────────────│

Each event is a JSON object with a type field:

{"type": "RUN_STARTED", "threadId": "thread-123", "runId": "run-456"}
{"type": "TEXT_MESSAGE_START", "messageId": "msg-789", "role": "assistant"}
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-789", "delta": "The weather"}
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-789", "delta": " in Paris is"}
{"type": "TEXT_MESSAGE_CONTENT", "messageId": "msg-789", "delta": " sunny today."}
{"type": "TEXT_MESSAGE_END", "messageId": "msg-789"}
{"type": "RUN_FINISHED", "threadId": "thread-123", "runId": "run-456"}

On error, a RUN_ERROR event is sent instead of the message events:

{"type": "RUN_ERROR", "message": "processing failed: connection timeout"}

Streaming Support

The AG-UI server supports three response modes:

Body Type Behavior
*llm.StreamReader LLM chunks streamed as individual TEXT_MESSAGE_CONTENT events
<-chan string or chan string Channel values streamed as individual TEXT_MESSAGE_CONTENT events
Any other type Body converted to string, sent as single TEXT_MESSAGE_CONTENT event

The HelixStream header is automatically set to "true" on the exchange, so agent and LLM components use streaming mode.

CORS Headers

The server sets the following CORS headers on every response:

Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type

This allows any frontend application to connect to the AG-UI server.

YAML Config

helix.yaml

helix:
  agui:
    servers:
      - enabled: true
        addr: "0.0.0.0:4100"
        path: "/agent"
        name: "my-chatbot"

workflows:
  - id: chat
    description: "AI chat endpoint"
    trigger:
      uri: direct://chat
    steps:
      - target: anthropic://claude-sonnet?maxTokens=2048

Workflow-Based

workflows:
  - id: agui-server
    trigger:
      uri: agui://0.0.0.0:4100/agent?name=my-chatbot
    steps:
      - target: anthropic://claude-sonnet?maxTokens=2048

Go DSL

package main

import (
    "context"
    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/exchange"
    "github.com/arvenza/helix/pkg/component/agui"
)

func main() {
    h := engine.NewContext("agui-demo")

    h.AddComponent("agui", agui.NewServer(h))

    h.AddWorkflow(&engine.Workflow{
        ID:      "chat-server",
        FromURI: "agui://0.0.0.0:4100/agent?name=my-chatbot",
        Processors: []engine.Processor{
            engine.ProcessorFunc(func(ctx context.Context, ex *exchange.Exchange) error {
                question := ex.In().BodyAsString()
                ex.In().SetBody("You asked: " + question + ". Here is my answer...")
                return nil
            }),
        },
    })

    ctx := context.Background()
    h.Start(ctx)
    defer h.Stop(ctx)

    select {}
}

4.3 Configuration Reference

helix.yaml - AG-UI Server Config

helix:
  agui:
    servers:
      - enabled: true                    # Enable this AG-UI server instance
        addr: "0.0.0.0:4100"           # Listen address (default port: 4100)
        path: "/agent"                 # HTTP path for the endpoint
        name: "my-chatbot"            # Agent name (for logging/identification)

URI Parameters

agui://host:port/path?name=my-agent
Parameter Default Description
name helix-agui Agent name for logging

4.4 Exchange Headers

Headers set during AG-UI interactions:

Header Direction Description
HelixStream In Always set to "true" for streaming LLM responses
HelixAGUIThreadID In AG-UI thread ID from the request
HelixAGUIRunID In AG-UI run ID from the request
HelixAGUIState In JSON-encoded state from RunAgentInput.State
HelixAGUITools In JSON-encoded tools from RunAgentInput.Tools

4.5 Working with CopilotKit

CopilotKit is a popular React framework for building AI-powered UIs. It natively speaks the AG-UI protocol, making Helix a natural backend.

Frontend Setup (React)

import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";

function App() {
  return (
    <CopilotKit runtimeUrl="http://localhost:4100/agent">
      <CopilotChat />
    </CopilotKit>
  );
}

Backend (helix.yaml)

workflows:
  - id: copilot-backend
    trigger:
      uri: agui://0.0.0.0:4100/agent?name=copilot-agent
    steps:
      - target: anthropic://claude-sonnet?maxTokens=4096&systemPrompt=You+are+a+helpful+assistant

That is all that is required. CopilotKit sends RunAgentInput via POST, Helix processes it through the workflow pipeline, and streams back AG-UI events that CopilotKit renders as a chat interface.

Passing Frontend State

CopilotKit can send application state with each request. Access it from the exchange:

workflows:
  - id: stateful-agent
    trigger:
      uri: agui://0.0.0.0:4100/agent?name=stateful
    steps:
      - process:
          starlang: |
            state_json = headers.get("HelixAGUIState", "{}")
            state = read(state_json, "application/json")
            current_page = state.get("currentPage", "home")
            "User is on page: " ++ current_page ++ ". Question: " ++ payload
      - target: anthropic://claude-sonnet?maxTokens=2048

5. A2UI (Agent to UI) Protocol

5.1 What is A2UI

The A2UI (Agent to UI) protocol enables agents to generate rich, declarative UIs that client applications render with native widgets. Unlike AG-UI, which streams text messages for chat interfaces, A2UI streams structured component trees - columns, rows, cards, text fields, buttons, and more.

Think of A2UI as the difference between a chatbot (AG-UI) and a dynamic dashboard (A2UI). With A2UI, an agent can render a form, display a data table, show a dashboard with live updates, or build any UI layout - all declaratively, without writing frontend code.

Helix uses:

github.com/burka/a2ui-go v0.3.0

This provides message types, component helpers (Column, Row, Card, Text, Button, etc.), and Surface builder utilities.

5.2 A2UI Server

The A2UI server exposes two HTTP endpoints at the configured path:

  • GET - Initiates an SSE stream of A2UI JSONL messages (the agent's UI output)
  • POST - Receives userAction events from the UI client (button clicks, form submissions, etc.)

URI Format

a2ui://host:port/path?name=my-agent
Part Description
host:port Listen address (default port: 4200)
/path HTTP path for the endpoint (default: /)
name Agent name (default: helix-a2ui)

GET Endpoint - SSE Stream

The GET endpoint accepts query parameters and streams A2UI messages as SSE:

GET /surface?message=Show+me+today's+sales&surface=main
Query Parameter Default Description
message (required) The user's prompt/request text
surface main The surface ID for the UI session

The response is text/event-stream with JSONL A2UI messages:

data: {"beginRendering":{"surfaceId":"main","root":"root"}}

data: {"updateComponents":{"surfaceId":"main","components":[...]}}

POST Endpoint - User Events

The POST endpoint accepts client-to-server events (button clicks, form input, etc.):

{
  "event": {
    "surfaceId": "main",
    "componentId": "submit-btn",
    "type": "action",
    "data": {"value": "clicked"}
  }
}

The server creates an exchange with the event data and runs it through the processor pipeline. The response is returned as JSON.

A2UI Message Types

The protocol defines four message types:

Message Type Purpose
beginRendering Starts a new surface rendering session. Specifies the surface ID and root component ID.
updateComponents Sends a list of components to render or update. Components reference each other by ID to form a tree.
dataModelUpdate Updates data bindings in the component tree without re-sending components.
deleteSurface Removes a surface from the UI.

Typical message flow:

Server                              Client
  │                                    │
  │  beginRendering(surfaceId, root)   │
  │───────────────────────────────────►│  Client creates surface
  │                                    │
  │  updateComponents([Column, Card,   │
  │    Text, Button, ...])             │
  │───────────────────────────────────►│  Client renders components
  │                                    │
  │  updateComponents([Text(updated)]) │
  │───────────────────────────────────►│  Client updates text
  │                                    │
  │           POST {event: click}      │
  │◄───────────────────────────────────│  User clicks button
  │                                    │
  │  updateComponents([Text(result)])  │
  │───────────────────────────────────►│  Agent responds with new UI

Three Response Modes

The A2UI server handles three types of processor output:

1. Plain text (auto-wrapped)

If the processor sets a string body, it is automatically wrapped in a simple A2UI surface:

steps:
  - transform:
      starlang: |
        "Hello, " ++ payload ++ "!"

This generates:

{"beginRendering": {"surfaceId": "main", "root": "root"}}
{"updateComponents": {"surfaceId": "main", "components": [
  {"type": "column", "id": "root", "children": ["response-card"]},
  {"type": "card", "id": "response-card", "children": ["response-text"]},
  {"type": "text", "id": "response-text", "content": "Hello, World!"}
]}}

2. Surface builder

Return an *a2ui.Surface for full control over the component tree:

surface := a2ui.NewSurface("main")
surface.AddAll(
    a2ui.Column("root", "greeting"),
    a2ui.TextWithHint("greeting", "Hello from A2UI!", a2ui.UsageHintH1),
)
ex.In().SetBody(surface)

3. Direct Messages

Return []a2ui.Message for complete control over the message stream:

msgs := []a2ui.Message{
    {BeginRendering: &a2ui.BeginRendering{SurfaceID: "main", Root: "root"}},
    {UpdateComponents: &a2ui.UpdateComponents{
        SurfaceID:  "main",
        Components: []any{a2ui.TextStatic("root", "Direct control")},
    }},
}
ex.In().SetBody(msgs)

Streaming: Progressive Surface Updates

When the processor pipeline produces a streaming body (*llm.StreamReader or <-chan string), the A2UI server sends progressive surface updates:

  1. First, a beginRendering message is sent
  2. As each chunk arrives, an updateComponents message is sent with the accumulated text
  3. The component tree is rebuilt with the full text on each update, so clients always see the latest state
Chunk 1: "Hello"
  → updateComponents: Column > Card > Text("Hello")

Chunk 2: " World"
  → updateComponents: Column > Card > Text("Hello World")

Chunk 3: "!"
  → updateComponents: Column > Card > Text("Hello World!")

YAML Config

helix.yaml

helix:
  a2ui:
    servers:
      - enabled: true
        addr: "0.0.0.0:4200"
        path: "/surface"
        name: "my-ui-agent"

workflows:
  - id: dashboard
    description: "AI-powered dashboard"
    trigger:
      uri: direct://dashboard
    steps:
      - process:
          starlang: |
            "Dashboard data for: " ++ payload

Workflow-Based

workflows:
  - id: a2ui-server
    trigger:
      uri: a2ui://0.0.0.0:4200/surface?name=my-ui-agent
    steps:
      - process:
          starlang: |
            "Response to: " ++ payload

Go DSL

package main

import (
    "context"
    a2uilib "github.com/burka/a2ui-go"
    "github.com/arvenza/helix/pkg/engine"
    "github.com/arvenza/helix/pkg/exchange"
    "github.com/arvenza/helix/pkg/component/a2ui"
)

func main() {
    h := engine.NewContext("a2ui-demo")

    h.AddComponent("a2ui", a2ui.NewServer(h))

    h.AddWorkflow(&engine.Workflow{
        ID:      "dashboard",
        FromURI: "a2ui://0.0.0.0:4200/surface?name=my-dashboard",
        Processors: []engine.Processor{
            engine.ProcessorFunc(func(ctx context.Context, ex *exchange.Exchange) error {
                surface := a2uilib.NewSurface("main")
                surface.AddAll(
                    a2uilib.Column("root", "header", "content"),
                    a2uilib.TextWithHint("header", "Sales Dashboard", a2uilib.UsageHintH1),
                    a2uilib.Card("content", "metric-row"),
                    a2uilib.Row("metric-row", "revenue", "orders", "customers"),
                    a2uilib.TextStatic("revenue", "Revenue: $42,000"),
                    a2uilib.TextStatic("orders", "Orders: 156"),
                    a2uilib.TextStatic("customers", "Customers: 89"),
                )
                ex.In().SetBody(surface)
                return nil
            }),
        },
    })

    ctx := context.Background()
    h.Start(ctx)
    defer h.Stop(ctx)

    select {}
}

5.3 Configuration Reference

helix.yaml - A2UI Server Config

helix:
  a2ui:
    servers:
      - enabled: true                    # Enable this A2UI server instance
        addr: "0.0.0.0:4200"           # Listen address (default port: 4200)
        path: "/surface"               # HTTP path for the endpoint
        name: "my-ui-agent"           # Agent name (for logging/identification)

URI Parameters

a2ui://host:port/path?name=my-agent
Parameter Default Description
name helix-a2ui Agent name for logging

5.4 Exchange Headers

Headers set during A2UI interactions:

Header Direction Description
HelixStream In Set to "true" for streaming mode (GET requests)
HelixA2UISurfaceID In Surface ID from query param or event
HelixA2UIEventType In Event type (POST requests only)
HelixA2UIComponentID In Component that triggered the event (POST requests only)

5.5 Building Rich UIs

The a2ui-go SDK provides component helper functions for building UI trees. Each component is identified by a unique ID and references its children by their IDs.

Component Helpers

Helper Description
Column(id, children...) Vertical layout container
Row(id, children...) Horizontal layout container
Card(id, children...) Card container with visual border/shadow
TextStatic(id, text) Static text display
TextWithHint(id, text, hint) Text with usage hint (H1, H2, body, etc.)
Button(id, label) Clickable button
TextField(id, placeholder) Text input field
Icon(id, iconType) Icon display

Component Tree Structure

Components form a tree via parent-child ID references:

Column("root", "header", "body")        ← root has two children
  TextWithHint("header", "Title", H1)   ← header is a text node
  Card("body", "form-row")              ← body is a card with one child
    Row("form-row", "input", "submit")  ← form-row has two children
      TextField("input", "Enter name")  ← input field
      Button("submit", "Submit")        ← submit button

Data Binding

Components can bind to data model paths. When the agent sends a dataModelUpdate message, bound components automatically update:

// Component with data binding
text := a2ui.TextWithBinding("metric", "revenue.total")

// Later, update the data model
dataUpdate := a2ui.Message{
    DataModelUpdate: &a2ui.DataModelUpdate{
        SurfaceID: "main",
        Path:      "revenue.total",
        Value:     "$42,000",
    },
}

Event Handling

When a user interacts with a component (clicks a button, submits a form), the client sends a POST request to the A2UI server:

{
  "event": {
    "surfaceId": "main",
    "componentId": "submit-btn",
    "type": "action",
    "data": {"formData": {"name": "Alice", "email": "alice@example.com"}}
  }
}

The exchange body is set to the Event object, and the processor can inspect it:

workflows:
  - id: handle-form
    trigger:
      uri: a2ui://0.0.0.0:4200/surface?name=form-agent
    steps:
      - choice:
          when:
            - starlang: |
                headers.get("HelixA2UIEventType", "") == "action"
              steps:
                - process:
                    starlang: |
                      event = payload
                      form_data = event.get("data", {}).get("formData", {})
                      name = form_data.get("name", "Unknown")
                      {"status": "ok", "greeting": "Hello, " ++ name}
          otherwise:
            steps:
              # Initial render (GET request)
              - process:
                  starlang: |
                    "Welcome! Please fill out the form."

6. Complete Examples

6.1 Agent Exposed via All Four Protocols Simultaneously

A single agent exposed via MCP, A2A, AG-UI, and A2UI:

# helix.yaml — Multi-protocol agent
helix:
  mcp:
    servers:
      - enabled: true
        transport: http
        addr: "0.0.0.0:3000"
        name: "qa-tools"
        tools:
          ask-question:
            description: "Ask a knowledge question"
            input_schema: |
              {"type":"object","properties":{"question":{"type":"string"}},"required":["question"]}

  a2a:
    servers:
      - enabled: true
        addr: "0.0.0.0:9000"
        name: "qa-agent"
        description: "Knowledge Q&A Agent"
        version: "1.0.0"

workflows:
  # The core route — shared by all protocols
  - id: ask-question
    description: "Answer a knowledge question using AI"
    trigger:
      uri: direct://ask-question
    steps:
      - transform:
          starlang: |
            question = payload
            if type(payload) == "dict":
                question = payload.get("question", str(payload))
            question
      - target: anthropic://claude-sonnet?maxTokens=2048&systemPrompt=You+are+a+helpful+knowledge+assistant

  # AG-UI chat endpoint
  - id: chat-ui
    trigger:
      uri: agui://0.0.0.0:4100/agent?name=qa-chatbot
    steps:
      - target: direct://ask-question

  # A2UI rich endpoint
  - id: rich-ui
    trigger:
      uri: a2ui://0.0.0.0:4200/surface?name=qa-dashboard
    steps:
      - target: direct://ask-question

Now the same agent is reachable four ways:

Claude Desktop  → http://localhost:3000/mcp        (MCP)
External agents → http://localhost:9000/invoke      (A2A)
Chat frontend   → POST http://localhost:4100/agent  (AG-UI)
Rich UI client  → GET http://localhost:4200/surface (A2UI)

6.2 A2A Agent Calling Another A2A Agent

Multi-agent delegation where a coordinator delegates to specialist agents:

# coordinator.yaml
helix:
  a2a:
    servers:
      - enabled: true
        addr: "0.0.0.0:9000"
        name: "coordinator"
        description: "Coordinator agent that delegates to specialists"

workflows:
  - id: handle-request
    trigger:
      uri: direct://handle-request
    steps:
      # Classify the request
      - transform:
          starlang: |
            question = payload
            if "code" in question.lower() or "program" in question.lower():
                "coding"
            elif "math" in question.lower() or "calculate" in question.lower():
                "math"
            else:
                "general"

      # Route to the appropriate specialist
      - choice:
          when:
            - starlang: |
                payload == "coding"
              steps:
                - transform:
                    starlang: |
                      properties.get("original_question", "")
                - target: a2a://coding-agent:9001

            - starlang: |
                payload == "math"
              steps:
                - transform:
                    starlang: |
                      properties.get("original_question", "")
                - target: a2a://math-agent:9002

          otherwise:
            steps:
              - transform:
                  starlang: |
                    properties.get("original_question", "")
              - target: anthropic://claude-sonnet?maxTokens=2048

      - log:
          message: "Agent response: ${body}"

6.3 AG-UI Chatbot with Streaming

A full-featured chatbot that streams responses with context awareness:

# chatbot.yaml
workflows:
  - id: chatbot
    trigger:
      uri: agui://0.0.0.0:4100/chat?name=smart-chatbot
    steps:
      # Extract state from the AG-UI request
      - process:
          starlang: |
            state_json = headers.get("HelixAGUIState", "{}")
            state = read(state_json, "application/json")
            context = state.get("context", "")

            # Build a prompt with context
            prompt = payload
            if context:
                prompt = "Context: " ++ context ++ "\n\nUser: " ++ payload
            prompt

      # Call the LLM with streaming
      - target: anthropic://claude-sonnet?maxTokens=4096&systemPrompt=You+are+a+helpful+assistant

      # The LLM body is a StreamReader, which AG-UI automatically
      # streams as TEXT_MESSAGE_CONTENT events

Frontend (React with CopilotKit):

import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";

function App() {
  const [context, setContext] = useState("User is viewing the billing page");

  return (
    <CopilotKit
      runtimeUrl="http://localhost:4100/chat"
      properties={{ context }}
    >
      <CopilotChat
        labels={{ title: "Smart Assistant", initial: "How can I help you?" }}
      />
    </CopilotKit>
  );
}

6.4 A2UI Form-Based Agent Interface

An agent that renders a form and processes submissions:

# form-agent.yaml
workflows:
  - id: form-ui
    trigger:
      uri: a2ui://0.0.0.0:4200/surface?name=ticket-form
    steps:
      - choice:
          when:
            # Handle form submission (POST event)
            - starlang: |
                headers.get("HelixA2UIEventType", "") == "action"
              steps:
                - process:
                    starlang: |
                      event = payload
                      form_data = event.get("data", {})
                      title = form_data.get("title", "")
                      description = form_data.get("description", "")
                      priority = form_data.get("priority", "medium")

                      # Process the ticket
                      {
                        "status": "created",
                        "ticket_id": "TKT-" ++ str(int(time.time())),
                        "title": title,
                        "priority": priority
                      }

          otherwise:
            # Initial render: show the form (GET request)
            steps:
              - process:
                  starlang: |
                    # Return plain text — A2UI auto-wraps it
                    "Create a new support ticket. Fill out the form below."

6.5 A2UI + A2A: Agent with Rich UI that Delegates to Other Agents

An A2UI dashboard agent that calls specialist A2A agents for different panels:

# dashboard.yaml
workflows:
  - id: dashboard
    trigger:
      uri: a2ui://0.0.0.0:4200/dashboard?name=ops-dashboard
    steps:
      # Fetch data from specialist agents in parallel
      - multicast:
          strategy: parallel
          steps:
            - target: a2a://metrics-agent:9001
            - target: a2a://alerts-agent:9002

      # Aggregate results
      - aggregate:
          strategy: collect
          completion:
            size: 2
            timeout: 10s

      # Build dashboard surface from agent responses
      - process:
          starlang: |
            results = payload
            metrics = results[0] if sizeOf(results) > 0 else "No metrics"
            alerts = results[1] if sizeOf(results) > 1 else "No alerts"

            # Return as plain text; A2UI auto-wraps
            "Metrics: " ++ str(metrics) ++ "\nAlerts: " ++ str(alerts)

7. Cookbook

Recipe 1: Expose Existing Routes via A2A

If you have existing Helix workflows, adding A2A exposure is one line:

# Existing workflows
workflows:
  - id: process-order
    description: "Process a customer order"
    trigger:
      uri: direct://process-order
    steps:
      - target: postgres://db:5432/orders?operation=insert&table=orders
      - transform:
          starlang: write({"status": "created", "orderId": payload["id"]}, "application/json")

  - id: lookup-customer
    description: "Look up customer by ID"
    trigger:
      uri: direct://lookup-customer
    steps:
      - target: postgres://db:5432/crm?operation=query
      - transform:
          starlang: write(payload, "application/json")

# Add A2A — one block, all direct:// workflows become skills automatically
helix:
  a2a:
    servers:
      - enabled: true
        addr: "0.0.0.0:9000"
        name: "order-system"
        description: "Order processing and customer management"

The A2A Agent Card will automatically list process-order and lookup-customer as skills.

Recipe 2: CopilotKit Frontend with AG-UI Backend

Backend (helix.yaml):

workflows:
  - id: copilot
    trigger:
      uri: agui://0.0.0.0:4100/api/copilot?name=copilot-agent
    steps:
      # Use the tools from the frontend if provided
      - process:
          starlang: |
            tools_json = headers.get("HelixAGUITools", "[]")
            tools = read(tools_json, "application/json")
            if sizeOf(tools) > 0:
                # Include tool descriptions in the system prompt
                tool_desc = "\n".join([t.get("name", "") ++ ": " ++ t.get("description", "") for t in tools])
                properties["tool_context"] = tool_desc
      - target: anthropic://claude-sonnet?maxTokens=4096

Frontend (React):

import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat } from "@copilotkit/react-ui";
import { useCopilotAction } from "@copilotkit/react-core";

function App() {
  // Register client-side tools that the agent can call
  useCopilotAction({
    name: "showNotification",
    description: "Show a notification to the user",
    parameters: [
      { name: "message", type: "string", description: "Notification text" },
    ],
    handler: ({ message }) => {
      alert(message);
    },
  });

  return (
    <CopilotKit runtimeUrl="http://localhost:4100/api/copilot">
      <CopilotChat />
    </CopilotKit>
  );
}

Recipe 3: A2UI Dashboard with Live Data

A dashboard that refreshes metrics from a database:

workflows:
  - id: live-dashboard
    trigger:
      uri: a2ui://0.0.0.0:4200/metrics?name=metrics-dashboard
    steps:
      - process:
          starlang: |
            query = payload
            if not query:
                query = "overview"

      # Fetch metrics from the database
      - target: "postgres://db:5432/analytics?operation=query&query=SELECT+COUNT(*)+as+total_orders,SUM(amount)+as+revenue,AVG(amount)+as+avg_order+FROM+orders+WHERE+created_at>now()-interval+'24+hours'"

      - transform:
          starlang: |
            row = payload[0] if sizeOf(payload) > 0 else {}
            total = str(row.get("total_orders", 0))
            revenue = "$" ++ str(row.get("revenue", 0))
            avg = "$" ++ str(row.get("avg_order", 0))

            # Return formatted text — A2UI auto-wraps in a card
            "24h Metrics\nTotal Orders: " ++ total ++ "\nRevenue: " ++ revenue ++ "\nAvg Order: " ++ avg

Client request:

GET http://localhost:4200/metrics?message=refresh&surface=main

Recipe 4: Multi-Protocol Agent Gateway

A gateway that routes incoming requests from any protocol to the appropriate backend:

# gateway.yaml
helix:
  mcp:
    servers:
      - enabled: true
        transport: http
        addr: "0.0.0.0:3000"
        name: "gateway-tools"

  a2a:
    servers:
      - enabled: true
        addr: "0.0.0.0:9000"
        name: "gateway-agent"
        description: "Multi-service gateway agent"

workflows:
  # Core service workflows
  - id: search
    description: "Search across all data sources"
    trigger:
      uri: direct://search
    steps:
      - multicast:
          strategy: parallel
          steps:
            - target: elasticsearch://search-cluster:9200?index=documents
            - target: postgres://db:5432/app?operation=query
      - aggregate:
          strategy: collect
          completion:
            size: 2
            timeout: 5s
      - transform:
          starlang: write({"results": payload}, "application/json")

  - id: summarize
    description: "Summarize text using AI"
    trigger:
      uri: direct://summarize
    steps:
      - target: anthropic://claude-sonnet?maxTokens=1024&systemPrompt=Summarize+the+following+text+concisely

  # AG-UI chat gateway
  - id: chat-gateway
    trigger:
      uri: agui://0.0.0.0:4100/chat?name=gateway-chat
    steps:
      - target: anthropic://claude-sonnet?maxTokens=4096

  # A2UI dashboard gateway
  - id: dashboard-gateway
    trigger:
      uri: a2ui://0.0.0.0:4200/dashboard?name=gateway-ui
    steps:
      - target: direct://search
      - transform:
          starlang: |
            results = read(payload, "application/json").get("results", [])
            "Found " ++ str(sizeOf(results)) ++ " results"

This exposes the agent at four ports:

Port Protocol Use Case
3000 MCP Claude Desktop calls search and summarize tools
9000 A2A Other agents delegate search and summarize tasks
4100 AG-UI Chat frontend streams responses
4200 A2UI Dashboard frontend renders search results

8. Troubleshooting

A2A server starts but Agent Card has no skills

  • Ensure workflows with direct:// or seda:// from-URIs are defined before the A2A server starts
  • If using the agent component, verify agents are registered before the A2A server route
  • Check the Agent Card directly: curl http://localhost:9000/.well-known/agent-card.json
  • A default skill is always added if no agents or workflows are found

A2A client returns empty response

  • Verify the target A2A server is running and accessible
  • Check that the exchange body is not empty before calling the A2A client (empty body returns an error)
  • For streaming mode, ensure you are reading from the <-chan string body before it closes
  • Check Helix logs for a2a: client init failed or a2a: send failed errors

A2A streaming not working

  • Confirm the HelixStream header is set to "true" on the exchange
  • The target A2A server must have streaming: true in its capabilities
  • Verify the client is using SendStreamingMessage (check logs for artifact events)
  • If using the Helix A2A server, the processor pipeline must return a streaming body (*llm.StreamReader or <-chan string)

AG-UI returns 400 Bad Request

  • The request must be a POST with valid RunAgentInput JSON
  • At least one message with role: "user" must be present in the messages array
  • GET requests return 405 (Method Not Allowed) - AG-UI only accepts POST

AG-UI events not rendering in CopilotKit

  • Verify the runtimeUrl in CopilotKit matches the AG-UI server address and path
  • Check that CORS headers are present (Access-Control-Allow-Origin: *)
  • Inspect the SSE stream in browser DevTools (Network tab, filter by EventStream)
  • Ensure the event types match the AG-UI spec: RUN_STARTED, TEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT, TEXT_MESSAGE_END, RUN_FINISHED

A2UI surface not rendering

  • The GET request requires a message query parameter: GET /surface?message=hello
  • Missing the message parameter returns 400
  • Check SSE events for beginRendering followed by updateComponents
  • Verify component IDs are consistent - child IDs must match parent references

A2UI POST events not reaching the processor

  • POST body must be a valid ClientMessage JSON with a non-null event field
  • Missing event returns 400
  • The HelixA2UIEventType header is set from event.type - use it in choice steps

Common URI patterns

# A2A Server
a2a-server://0.0.0.0:9000                                    # Default settings
a2a-server://0.0.0.0:9000?name=my-agent                      # Custom name
a2a-server://0.0.0.0:9000?name=a&description=b&version=2.0   # All params

# A2A Client
a2a://agent-host:9000                                         # Call agent

# AG-UI Server
agui://0.0.0.0:4100/agent                                     # Default
agui://0.0.0.0:4100/api/chat?name=my-chatbot                  # Custom path+name

# A2UI Server
a2ui://0.0.0.0:4200/surface                                   # Default
a2ui://0.0.0.0:4200/dashboard?name=my-ui                      # Custom path+name

Port defaults

If no port is specified in the address:

Component Default Port
A2A Server 9000
AG-UI Server 4100
A2UI Server 4200

Testing

Run the test suites for each protocol component:

# A2A tests (16 tests including streaming round-trip)
go test ./pkg/component/a2a/ -v

# AG-UI tests (12 tests including SSE streaming)
go test ./pkg/component/agui/ -v

# A2UI tests (15 tests including surface rendering and events)
go test ./pkg/component/a2ui/ -v

# All protocol tests with race detector
go test ./pkg/component/a2a/ ./pkg/component/agui/ ./pkg/component/a2ui/ ./pkg/component/mcp/ -race -v

WebSocket Protocol

Helix supports bidirectional WebSocket communication via the ws-server:// component:

workflows:
  - id: ws-chat
    trigger: { uri: ws-server://0.0.0.0:8200/ws }
    steps:
      - agent:
          id: ws-agent
          type: react
          model: { provider: openai, model: gpt-4o-mini }
          system_prompt: You are a WebSocket chat assistant.

Features: - RFC 6455 native Go implementation (no external library) - Request-response: client sends message, pipeline processes, response sent back - Text and binary frame support - Multiple concurrent clients per server - Multi-turn conversations (agent retains context per connection) - Connection lifecycle: clean connect/close/reconnect


Multi-Agent Multi-Protocol Architecture

Each agent can expose multiple protocol endpoints simultaneously:

agents:
  - id: code-assistant
    triggers:
      - agui://0.0.0.0:4100/agent
      - a2a-server://0.0.0.0:9100
      - http-server://0.0.0.0:8100/api/chat

  - id: math-agent
    triggers:
      - agui://0.0.0.0:4101/agent
      - a2a-server://0.0.0.0:9101
      - direct://math-agent

mcps:
  - id: public-tools
    addr: 0.0.0.0:3100
    expose: [code-assistant, math-agent, summarizer]
    api_keys: [mcp-key]
    blocked_tools: [admin-tool]

workflows:
  - id: ws-chat
    trigger: { uri: ws-server://0.0.0.0:8200/ws }
    steps:
      - agent: { id: ws-agent, type: react, ... }

This creates 14+ protocol endpoints across AG-UI, A2A, MCP, WebSocket, and HTTP.


SynaptX Gateway Integration

When deployed behind SynaptX, all protocols gain enterprise security:

Protocol SynaptX Workflow Security
A2A /a2a/ Rate limit, audit
AG-UI /agui/ Rate limit, session
MCP /mcp/ allowed_tools, blocked_tools
AI Proxy /ai/* Virtual keys, prompt guard, PII

SynaptX adds virtual keys (per-agent budget/RPM), prompt injection guard (16 patterns), PII detection (SSN, credit cards), governance policies (ABAC/RBAC), and audit trail.