The agent side of A2UI for Elixir, built on a2ui_core. An agent registers the catalogs it can write for, negotiates them with each renderer, puts the matching schemas in the model's prompt and reads the model's output back as validated A2UI messages, from a complete response or chunk by chunk while it streams.

It supports protocol versions v0.9, v0.9.1 and v1.0, and two inference formats. In direct JSON, the default, the model writes message lists between <a2ui-json> and </a2ui-json> tags. In Express (A2UIAgent.Express) it writes a compact notation between <a2ui> and </a2ui> tags, which the agent compiles into messages. An Express prompt can also be packaged as skills (A2UIAgent.SkillGenerator).

Installation

def deps do
  [
    {:a2ui_agent, "~> 0.1.0"}
  ]
end

Usage

An outline of the flow; Serving an agent walks through it with examples that run. Here example_turn is an example conversation turn for the prompt, capabilities the capabilities the renderer sent, and model_output and chunk what the model wrote.

alias A2UIAgent.{CatalogConfig, CatalogTransformer, Generator, RequestProcessor}
alias A2UIAgent.DirectJson.Parser

# Once, when the agent starts.
basic =
  CatalogConfig.new(A2UICore.Catalog.Basic.catalog("v1.0"),
    transformers: [CatalogTransformer.component_pruning(["Column", "Text", "Button"])]
  )

generator = Generator.new([basic], examples: [example_turn])

# Per request, with the capabilities the renderer sent.
{:ok, processor} = Generator.create_processor(generator, capabilities)
{:ok, snippet} = RequestProcessor.prompt(processor)

# A complete response...
{:ok, parts} = RequestProcessor.parse_response(processor, model_output)
#=> [{:text, "Here is your form."}, {:a2ui, [%{"version" => "v1.0", ...}, ...]}]

# ...or a streamed one, chunk by chunk.
parser = RequestProcessor.parser(processor)
{:ok, parser, parts} = Parser.parse_chunk(parser, chunk)

snippet goes into the model's system prompt. Each {:a2ui, messages} part holds messages that a renderer holding the negotiated catalogs accepts (A2UIAgent.PayloadValidator). Send them to the renderer over whatever transport connects the two.

Catalogs can also be loaded from files (A2UIAgent.CatalogConfig.from_path/2) or sent by the renderer itself (inlineCatalogs, with accepts_inline_catalogs: true).

For Express, pass format: A2UIAgent.Express to Generator.new/2 (every request) or Generator.create_processor/3 (one request). Express responses are parsed once they are complete.

Besides A2A (below), A2UIAgent.MCP and A2UIAgent.AGUI carry A2UI over MCP and AG-UI. Components can also be built in Elixir (A2UIAgent.Builder), and macros (A2UIAgent.Macro, A2UIAgent.MacroExpander) give the model components of your own that expand into the catalog's before they reach the renderer.

Serving over A2A

A2UIAgent.A2A.Plug serves an agent over the JSON-RPC binding of A2A: message/send answers with the finished task, message/stream with server-sent events.

defmodule MyAgent do
  @behaviour A2UIAgent.A2A.Agent

  alias A2UIAgent.A2A.Request

  # No catalog negotiated: answer in text.
  @impl true
  def run(%Request{processor: nil, text: text}), do: {:ok, MyModel.reply(text)}

  # `events` holds the renderer's A2UI messages, such as actions.
  def run(%Request{processor: processor, text: text, events: events}) do
    {:ok, prompt} = A2UIAgent.RequestProcessor.prompt(processor)
    # A string, or an enumerable of chunks as the model streams them.
    {:ok, MyModel.stream(prompt, text, events)}
  end

  @impl true
  def options do
    basic = A2UIAgent.CatalogConfig.new(A2UICore.Catalog.Basic.catalog("v1.0"))
    [generator: A2UIAgent.Generator.new([basic])]
  end

  @impl true
  def card do
    %{
      "name" => "My agent",
      "capabilities" => %{"extensions" => [A2UIAgent.A2A.agent_extension("1.0")]}
    }
  end
end

# In a Phoenix router or a Plug.Router:
forward "/a2a", A2UIAgent.A2A.Plug, agent: MyAgent

The renderer's capabilities, which it sends in the message metadata, select the catalogs. The model's output goes back as a text part for each run of text and an A2UI data part for each message. A2UIAgent.A2A.Server does the same without Plug, and A2UIAgent.A2A holds the part and extension helpers. The plug needs the optional plug dependency.

License

Apache-2.0. The prompt rules are the A2UI project's text, which the conformance suites compare exactly, under the same license.