AgentUE defines how an agent-backed application communicates renderable semantic state to a user interface.
It sits between framework-specific agent events and the presentation layer:
agent runtime -> adapter -> AgentUE event stream -> reducer -> semantic UI model -> renderer
An adapter decides which runtime signals are useful to a user and expresses them as AgentUE blocks and patches. The protocol does not expose an agent runtime's internal event vocabulary directly, and it does not prescribe how a block is rendered.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY describe normative requirements.
AgentUE is designed around five properties:
- One model for every delivery mode. Live rendering, persisted history, and replay use the same semantic model.
- Stable ordered blocks. A block has a stable identity and type while remaining extensible for domain-specific data.
- Small update vocabulary. The core protocol separates lifecycle control from
setandappendstate changes. - Deterministic replay. Applying the same ordered patch sequence produces the same model.
- Transport independence. JSON event semantics are defined here; transports such as SSE are separate bindings.
A complete model has this shape:
{
"version": "1.1",
"biz": "chat",
"meta": {
"task_id": "task-123",
"error": null
},
"blocks": [
{
"id": "answer",
"type": "text",
"content": "Hello"
}
]
}| Field | Type | Requirement | Meaning |
|---|---|---|---|
version |
string | REQUIRED | Semantic model compatibility version. |
biz |
string | REQUIRED | Domain identifier that selects domain-specific model rules. |
meta |
object | REQUIRED | Extensible model-level metadata. |
blocks |
array | REQUIRED | Ordered semantic blocks. |
JSON member names defined by AgentUE use snake_case. Domain extensions SHOULD follow the same convention.
meta is an extension point. AgentUE defines these common fields:
| Field | Type | Meaning |
|---|---|---|
error |
object or null | Structured terminal or recoverable error information. |
task_id |
string or null | Stable task identity usable by application-level resume or cancel APIs. |
trace_id |
string or null | Observability correlation identifier. |
stats |
object or null | Domain-defined run statistics. |
An error object contains code and message, and MAY contain detail and trace_id.
Applications MAY add metadata fields. A generic consumer MUST preserve fields it does not interpret.
An inline block MUST contain:
| Field | Type | Meaning |
|---|---|---|
id |
string | Stable identity, unique within the model. |
type |
string | Domain-defined semantic type such as text, tool, or artifact. |
A block MAY contain parent_id to express nesting or ownership. Blocks that belong to the same execution or visual group MAY contain group_id. These dimensions are independent: parent_id is hierarchical, while group_id associates peer blocks.
All other inline block fields except the reserved ref field are domain-defined. Renderers choose a component from biz and the resolved block's type; the protocol does not ship executable UI code.
frame is the built-in technical block type for an optional framing capability,
not a directly renderable content type. In an event it appears as
event.block.type = "frame"; it does not introduce a new event operation.
{"id":"frame-unique-id","type":"frame","block_id":"result","seq":0,"total":3,"data":"..."}id identifies the frame itself; block_id identifies the original inline block
within its owning model. seq is the zero-based fragment index, independent of
the event's sequence number; total is the group's fragment count. data is a
string containing a piece of the original complete block's serialized JSON.
Reassembly checks consistent block identity and total, unique frame IDs, and
exact coverage of indices 0..total-1, then parses the joined data and checks the
restored block ID. Frames are not nested and cannot encode unresolved references.
Generic block validation and reducers preserve the open block fields; they do not automatically reassemble frames. A host using frames resolves them before rendering or applying logical updates to the original block. The optional Go storage helpers implement framing and reassembly without database or network I/O. They do not automatically fragment SSE events, alter event ordering, or provide replay semantics. See storage framing for byte budgets and persistence boundaries.
A model MAY mix inline blocks and reference blocks in the same ordered blocks array:
[
{"id": "b1", "type": "text", "content": "Inline content"},
{"id": "b2", "ref": "opaque-reference"}
]A reference block MUST contain exactly id and ref, both non-empty strings. It MUST NOT contain type, content, parent_id, group_id, or other inline fields. A block MUST NOT combine a reference and an inline body. Block IDs MUST be unique across both forms; array position, not the reference key, determines order. Multiple blocks MAY use the same reference key; the application resolves each using its model context and block ID.
ref is an opaque key. The application selected by biz owns its interpretation, association, storage, access, and lifetime. AgentUE MUST NOT interpret it as a URL, prescribe a database layout, or load it from the reducer. When to externalize blocks, including count and byte thresholds, is application policy.
Resolution MUST produce one complete inline block with the same id. Applications MUST reject mismatched IDs and results containing another ref. Resolved type, hierarchy, grouping, and body come from that inline block; there is no overlay or field-merging rule. Renderers needing those fields MUST resolve the reference first. A failed resolution MUST NOT be treated as an empty body.
A pure reducer preserves references on start and whole-block set. Before applying a field-level set or append to a reference block, the application MUST materialize its current inline body. The reducer MUST reject field patches targeting unresolved references without modifying the snapshot. Metadata and updates to other blocks do not require resolution. Loading content is not an additional patch event and does not advance seq.
Applications MUST make resolved content consistent with the snapshot and subsequent patch sequence. An opaque reference alone does not guarantee a historical snapshot: applications that replay old snapshots must retain the referenced content for those snapshots or materialize it before replay. Async consumers must coordinate loading with incoming patches to avoid duplicate appends or lost updates.
New models SHOULD use version: "1.1"; reference blocks require 1.1. SDK readers also accept existing version: "1.0" models containing inline blocks, preserving their version and using the same reducer. SDK emitters default to 1.1 when constructing new models. A producer MUST NOT send references to a consumer that only understands inline blocks; the application can materialize them into a 1.0 model for that consumer. Version support does not supply a reference resolver.
Every event uses the following envelope:
{
"op": "append",
"seq": 2,
"mask": "block.content",
"event_type": "message.delta",
"block": {
"id": "answer",
"type": "text",
"content": "Hello"
}
}| Field | Type | Requirement | Meaning |
|---|---|---|---|
op |
string | REQUIRED | Operation defined in section 5. |
seq |
non-negative integer | REQUIRED | Position in the model's ordered event timeline. |
stream_id |
string | OPTIONAL | Opaque logical stream address for multiplexed delivery; not a transport cursor. |
ts |
integer | OPTIONAL | Unix time in milliseconds. |
mask |
string | OP-SPECIFIC | Field path affected by the operation. |
event_type |
string | OPTIONAL | Domain hint for observers or renderers; it does not change reducer semantics. |
model |
object | OP-SPECIFIC | Complete model, used by start. |
meta |
object | OP-SPECIFIC | Metadata patch payload. |
block |
object | OP-SPECIFIC | Block patch payload. |
Only op and the relevant payload slot determine state transition semantics. Consumers MUST NOT require event_type to apply a patch.
Persisted, non-heartbeat events in one model timeline MUST have increasing seq values. A ping carries the latest known seq, does not advance the timeline, and can therefore repeat a value.
A reconstructed start sent during resume represents all state through its seq. The next state-changing event MUST have a greater value. Transport cursors, such as an SSE id, are separate from seq.
stream_id provides the feel of HTTP/2 multiplexing: independent logical streams
can share one transport while their events interleave. This is application-level
addressing, not an HTTP/2 stream identifier, flow-control window, or RPC lifecycle.
- Applications MAY omit
stream_id. A single model or an externally routed delivery needs no additional identifier; SDKs MUST NOT require one. - When using this field to multiplex, producers MUST keep the same identifier on events belonging to one logical stream. Omitted or empty identifiers select the unnamed stream; they are not wildcards over named streams.
- Consumers MUST route by
stream_idbefore applying events. Each stream owns its model,seq, and block IDs; equal values across different streams do not collide. Single-model reducers do not perform routing themselves. startreplaces only the addressed model.errorandendconcern only that stream's delivery, not other streams or the shared transport.- Identifiers are opaque strings supplied by the application. They MAY remain
stable across reconnections; a resumed
startcarries that stream's snapshot and coveredseq. No global sequence or cross-stream ordering is implied.
For example, a chat application can use its Message ID as stream_id, sending a
complete model on start and only addressed patches thereafter. Actor identity,
permissions, subscription selection, retention, and business completion remain
application concerns. Multiplexing does not isolate shared transport backpressure.
AgentUE divides operations by responsibility:
| Operation | Category | Changes state | Meaning |
|---|---|---|---|
start |
control + data | yes | Establishes or replaces the complete model. |
set |
data | yes | Creates or replaces a block or a selected field. |
append |
data | yes | Appends a string or list delta to a block field. |
error |
control + data | yes | Sets meta.error and signals failure. |
ping |
control | no | Keeps a live delivery active. |
end |
control | no | Ends a delivery normally or after an error. |
Every logical stream delivery MUST begin with start and MUST end with end. If a terminal failure occurs, error MUST appear before end on that logical stream. These rules apply independently when a transport multiplexes several streams.
start MUST contain model and MUST NOT contain meta or block. A consumer replaces its entire local model with this payload.
{
"op": "start",
"seq": 1,
"model": {
"version": "1.1",
"biz": "chat",
"meta": {},
"blocks": []
}
}Without mask, set MUST carry a complete inline block or reference block. The reducer replaces the block with the same id, or appends it if no matching block exists. Replacement MAY switch between inline and reference forms or change a reference key; an existing block keeps its array position.
{
"op": "set",
"seq": 2,
"block": {
"id": "stage",
"type": "stage",
"stage": "working"
}
}With mask, the first path segment selects the payload slot and target:
meta.<path>reads the value from the corresponding path inmeta.block.<path>selects a block byblock.idand reads the value from the corresponding path inblock.
{
"op": "set",
"seq": 3,
"mask": "meta.stats",
"meta": {
"stats": {"status": "running"}
}
}{
"op": "set",
"seq": 4,
"mask": "block.tool.status",
"block": {
"id": "tool-1",
"tool": {"status": "completed"}
}
}set MAY assign JSON null; null is a value, not an instruction to ignore the patch. Field patches MUST NOT change block.id or set block.ref (including nested paths). Reference creation, replacement, and materialization use whole-block replacement; an unresolved reference cannot receive a field patch.
append MUST use a block.<field> mask and MUST carry block.id. The selected value MUST be a string or array.
- Strings are concatenated.
- Arrays are concatenated in event order.
- If the block does not exist, the reducer inserts the supplied block payload. In this case the payload MUST be a complete valid inline block.
appendMUST NOT targetblock.idorblock.ref, or carry a reference block payload. An existing reference MUST be materialized before appending.
{
"op": "append",
"seq": 5,
"mask": "block.content",
"block": {
"id": "answer",
"type": "text",
"content": "world"
}
}Deduplication, ranking, numbering, and other domain rules MUST be completed before emitting an append event. The generic reducer only concatenates values.
error is the lifecycle-aware equivalent of setting meta.error. It MUST contain mask: "meta.error" and an error object in meta.error.
{
"op": "error",
"seq": 6,
"mask": "meta.error",
"meta": {
"error": {
"code": "upstream_unavailable",
"message": "The request could not be completed."
}
}
}ping does not change the model. It SHOULD contain ts and repeats the emitter's current seq.
end does not change the model. It is the final protocol event in that logical stream's current delivery, not a command to close a shared connection. A resumed delivery begins with another start.
The reducer is deterministic and independent of storage. For models containing references, the application must also satisfy the resolution consistency rules in section 3.4:
snapshot(n) = reduce(start.model, patches[1..n])
An application MAY persist the complete model, the ordered event log, or both.
For resume:
- Reconstruct or load the model covered by the requested cursor.
- Send a new
startcontaining that complete model and the highest coveredseq. - Continue with events whose
seqis greater than the reconstructedstart.seq. - Finish with
end.
The client handles first delivery and resumed delivery identically: start always replaces its local model.
- New
bizvalues and block types MAY be introduced without changing the core version. - New metadata and block fields MAY be introduced compatibly; consumers MUST preserve unknown fields.
- Envelope fields are validated by SDKs. Producers using
stream_idmust ensure consumers support it; omitting it preserves the unaddressed event format. - A new operation changes reducer semantics and therefore requires a protocol revision.
- Existing operation semantics MUST NOT be redefined within the same protocol version.
- Incompatible changes to the complete model require a new
versionvalue.
- Treat all text and URLs as untrusted data; renderers MUST escape or sanitize them for their target platform.
- Block payloads can contain tool output or traces. Producers SHOULD redact secrets before emitting or persisting events.
- Transports and intermediaries impose message-size limits. Producers SHOULD split large string and array fields across multiple
appendevents. - Consumers SHOULD impose application-appropriate limits on event size, block count, and accumulated content.