Serialization
Serialization in AG-UI provides a standard way to persist and restore the event stream that drives an agent–UI session. With a serialized stream you can:
- Restore chat history and UI state after reloads or reconnects
- Attach to running agents and continue receiving events
- Create branches (time travel) from any prior run
- Compact stored history to reduce size without losing meaning
This page explains the model, the updated event fields, and practical usage patterns with examples.
Core Concepts
- Stream serialization – Convert the full event history to and from a portable representation (e.g., JSON) for storage in databases, files, or logs.
- Event compaction – Reduce verbose streams to snapshots while preserving semantics (e.g., merge content chunks, collapse deltas into snapshots).
- Run lineage – Track branches of conversation using a
parentRunId, forming a git‑like append‑only log that enables time travel and alternative paths.
Updated Event Fields
The RunStarted event includes additional optional fields:
type RunStartedEvent = BaseEvent & {
type: EventType.RUN_STARTED;
threadId: string;
runId: string;
/** Parent for branching/time travel within the same thread */
parentRunId?: string;
/** Exact agent input for this run (may omit messages already in history) */
input?: AgentInput;
};
These fields enable lineage tracking and let implementations record precisely what was passed to the agent, independent of previously recorded messages.
Event Compaction
Compaction reduces noise in an event stream while keeping the same observable outcome. A typical implementation provides a utility:
declare function compactEvents(events: BaseEvent[]): BaseEvent[];
Common compaction rules include:
- Message streams – Combine
TEXT_MESSAGE_*sequences into a single message snapshot; concatenate adjacentTEXT_MESSAGE_CONTENTfor the same message. - Tool calls – Collapse tool call start/content/end into a compact record.
- State – Merge consecutive
STATE_DELTAevents into a single finalSTATE_SNAPSHOTand discard superseded updates. - Run input normalization – Remove from
RunStarted.input.messagesany messages already present earlier in the stream.
Branching and Time Travel
Setting parentRunId on a RunStarted event creates a git‑like lineage. The
stream becomes an immutable append‑only log where each run can branch from any
previous run.
gitGraph
commit id: "run1"
commit id: "run2"
branch alternative
checkout alternative
commit id: "run3 (parent run2)"
commit id: "run4"
checkout main
commit id: "run5 (parent run2)"
commit id: "run6"
Benefits:
- Multiple branches in the same serialized log
- Immutable history (append‑only)
- Deterministic time travel to any point
Examples
Basic Serialization
// Serialize event stream
const events: BaseEvent[] = [...];
const serialized = JSON.stringify(events);
await storage.save(threadId, serialized);
// Restore and compact later
const restored = JSON.parse(await storage.load(threadId));
const compacted = compactEvents(restored);
Event Compaction
Before:
[
{ type: "TEXT_MESSAGE_START", messageId: "msg1", role: "user" },
{ type: "TEXT_MESSAGE_CONTENT", messageId: "msg1", delta: "Hello " },
{ type: "TEXT_MESSAGE_CONTENT", messageId: "msg1", delta: "world" },
{ type: "TEXT_MESSAGE_END", messageId: "msg1" },
{ type: "STATE_DELTA", patch: { op: "add", path: "/foo", value: 1 } },
{ type: "STATE_DELTA", patch: { op: "replace", path: "/foo", value: 2 } },
];
After:
[
{
type: "MESSAGES_SNAPSHOT",
messages: [{ id: "msg1", role: "user", content: "Hello world" }],
},
{
type: "STATE_SNAPSHOT",
state: { foo: 2 },
},
];
Branching With parentRunId
// Original run
{
type: "RUN_STARTED",
threadId: "thread1",
runId: "run1",
input: { messages: ["Tell me about Paris"] },
}
// Branch from run1
{
type: "RUN_STARTED",
threadId: "thread1",
runId: "run2",
parentRunId: "run1",
input: { messages: ["Actually, tell me about London instead"] },
}
Normalized Input
// First run includes full message
{
type: "RUN_STARTED",
runId: "run1",
input: { messages: [{ id: "msg1", role: "user", content: "Hello" }] },
}
// Second run omits already‑present message
{
type: "RUN_STARTED",
runId: "run2",
input: { messages: [{ id: "msg2", role: "user", content: "How are you?" }] },
// msg1 omitted; it already exists in history
}
Implementation Notes
- Provide SDK helpers for compaction and (de)serialization.
- Store streams append‑only; prefer incremental writes when possible.
- Consider compression when persisting long histories.
- Add indexes by
threadId,runId, and timestamps for fast retrieval.
See Also
- Concepts: Events, State Management
- SDKs: TypeScript encoder and core event types