Expand description
Metadata: extra information attached to events, messages, tool calls and resume entries.
Token usage, a trace id, a finish reason — anything an application needs to
carry alongside the conversation goes in metadata, an object that is open
by key. Before it existed producers hung undeclared properties off events
and hoped consumers passed them through; metadata is the declared, typed
replacement, and consumers are required to carry it.
§Where it lives
Four places, all optional:
- every event, declared once on
BaseEventso all 36 types have it; - every message, all seven roles;
- every
ToolCall— a tool call is not a message, and several calls can share one parent, so each carries its own; - every
ResumeEntry, for envelope data about an answer — signatures, routing keys — as opposed to the answer.
§Shape
Any JSON value is allowed under a key, null included. The object itself
is absent or an object, never null: an optional field with no value
is omitted from the JSON entirely, in every official SDK, and this crate
rejects "metadata": null at parse time rather than reading it as absent.
Unlike some older optional fields, metadata has no legacy producers to
tolerate — the crate-private serde_util::reject_null is the rule. An
empty object is valid and means the same as omitting it.
The AGUI_METADATA_KEY ("ag-ui") is reserved for the protocol’s own
use. Every other key is yours. Nothing rejects a write to it at runtime —
that would contradict open-by-key — but treat it as off limits.
§Merging into messages
A message is assembled from a sequence of events, and the interesting
values are only known at the end: a provider does not know its token usage
until it has finished generating. So a consumer merges each event’s
metadata into the message that event builds, as the sequence arrives, with
merge_metadata: last write wins, key by key, and a nested object or
array is replaced whole rather than blended. The client’s applier does
this for the text, tool-call, activity and reasoning-message families; an
event that builds no message — RUN_*, STEP_*, STATE_*, RAW,
CUSTOM, REASONING_START/END/ENCRYPTED_VALUE, MESSAGES_SNAPSHOT,
SUBAGENT_* — keeps its metadata to itself.
use ag_ui::{JsonObject, merge_metadata};
use serde_json::json;
let start: JsonObject = json!({ "source": "openai", "stage": "start" })
.as_object().unwrap().clone();
let end: JsonObject = json!({ "stage": "end", "usage": { "output": 340 } })
.as_object().unwrap().clone();
let merged = merge_metadata(Some(&start), Some(&end)).unwrap();
assert_eq!(merged["source"], "openai"); // nothing later set it
assert_eq!(merged["stage"], "end"); // last write wins
assert_eq!(merged["usage"]["output"], 340); // arrived only at the endConstants§
- AGUI_
METADATA_ KEY - The key reserved for the protocol’s own use inside a metadata object.
Functions§
- merge_
metadata - Folds
incomingintoexisting, key by key, with the last write winning. - merge_
metadata_ into - The in-place form of
merge_metadata, for a consumer folding a stream of events into one message without cloning the accumulated object on every delta.