Agentsworld Blog

Agent Render Protocol

Agent Render Protocol (ARP) defines typed components and interactions for agent-generated interfaces. It does not define a visual system.

A host maps each component to its own view representation and applies its existing design system. Ocean, our agent workspace, uses Leptos component classes and Ocean CSS. A Tailwind application can use Tailwind. An organization can use its existing brand kit. The tool exposed to the agent does not change.

Basecoat and shadcn-html are close to that model. ARP adds an agent-facing component registry and interaction protocol. The agent selects a component and supplies validated props rather than writing JSX, framework code, or HTML:

ARP standardizes the tool schema and event behavior; it does not standardize a DOM shape. The JSON call below is part of the protocol:

{
  "tool": "component_render",
  "args": {
    "id": "deploy-confirmation",
    "kind": "confirm",
    "props": {
      "title": "Deploy to production?",
      "body": "3 services will be updated.",
      "confirm_label": "Deploy",
      "cancel_label": "Cancel",
      "variant": "info"
    }
  }
}

The host validates the call and maps it into its UI. The following HTML is the reference web mapping, not a required ARP output:

<section
  data-component="confirm"
  data-variant="info"
  data-id="deploy-confirmation"
>
  <h3>Deploy to production?</h3>
  <p>3 services will be updated.</p>
  <footer>
    <button data-action="cancel">Cancel</button>
    <button data-action="confirm">Deploy</button>
  </footer>
</section>

The reference mapping uses stable data-* hooks, so a plain stylesheet can target it:

[data-component="confirm"] {
  display: grid;
  gap: 0.75rem;
  padding: 1rem;
  border: 1px solid;
  border-radius: 0.875rem;
}

[data-component="confirm"] [data-action="confirm"] {
  background: LinkText;
  color: Canvas;
}

The tool schema is independent of the renderer and stylesheet.

Rendering architecture

The current production web renderer is written in Rust, compiled to WebAssembly, and rendered with Leptos. It does not depend on React.

A React client, native Swift client, terminal UI, or server renderer can consume the same event by implementing the component mapping for its environment. The agent-facing tool remains unchanged.

Ocean’s Leptos renderer does not use the reference data-* mapping. It emits Ocean component classes and uses the existing Ocean token and component stylesheets. A Tailwind renderer could map the same props to precompiled utilities or component classes. ARP does not require either approach.

Render events use the same JSON/SSE stream as text and tool activity:

The protocol defines three tools:

The component registry currently includes 18 kinds:

kanban, form, table, progress, markdown, dashboard, chart, interactive_plot, timeline, stat, file_tree, diff, code, callout, gallery, confirm, map, and video.

Interactive components return typed events such as form_submit, card_moved, parameters_changed, and confirm_response. An interaction resolves the pending tool call and does not start a separate agent turn.

Reference emitters

The repository includes executable implementations of the reference data-attribute mapping in Rust, TypeScript, Swift, Python, and Ruby. Each reads the same fixture, validates it, escapes strings, and emits the same HTML.

Rustrender.rs
let call: Value =
    serde_json::from_str(&source)?;
assert_eq!(
    call["tool"],
    "component_render"
);
println!("{}", render(&call));
TypeScriptrender.ts
const call = JSON.parse(
  readFileSync(path, "utf8"),
) as ToolCall;
if (call.args.kind !== "confirm")
  throw new Error("expected confirm");
process.stdout.write(render(call));
Swiftrender.swift
let data = try Data(
  contentsOf: URL(fileURLWithPath: path)
)
let call = try JSONDecoder()
  .decode(ToolCall.self, from: data)
print(render(call))
Pythonrender.py
call = json.loads(
    path.read_text()
)
if call["tool"] != "component_render":
    raise ValueError("unexpected tool")
print(render(call))
Rubyrender.rb
call = JSON.parse(
  File.read(path)
)
raise "unexpected tool" unless
  call.fetch("tool") ==
    "component_render"
puts render(call)

The outputs for this fixture and reference mapping are byte-identical:

sha256 60960f9bd6de5a356cfddee0c7e8a511372173023fb42e0c8f2d707931007c20

Download the exact tool call, reference stylesheet, or any emitter above.

The byte comparison checks the reference mapping, not a protocol requirement that every host emit identical HTML. These emitters are not five production clients. The production web renderer remains Rust/WASM + Leptos.

Typed components instead of generated HTML

Allowing the agent to emit arbitrary HTML would bypass schema validation, accessibility defaults, predictable interaction events, and the host-controlled theming surface. The typed registry keeps those responsibilities separate:

  1. The agent chooses a component.
  2. The tool validates its props.
  3. The host escapes content and renders its own view representation.
  4. The host’s design system styles that representation.
  5. Interactions return typed events.

Adding a component requires a schema and a renderer for each supported host. Changing the visual system remains a host concern. A host renderer can be replaced without changing the tool exposed to the agent.