showcase/shell-docs/src/content/ag-ui/concepts/serialization.mdx
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:
This page explains the model, the updated event fields, and practical usage patterns with examples.
parentRunId, forming
a git‑like append‑only log that enables time travel and alternative paths.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.
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:
TEXT_MESSAGE_* sequences into a single message
snapshot; concatenate adjacent TEXT_MESSAGE_CONTENT for the same message.STATE_DELTA events into a single final
STATE_SNAPSHOT and discard superseded updates.RunStarted.input.messages any
messages already present earlier in the stream.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:
// 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);
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 },
},
];
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"] },
}
// 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
}
threadId, runId, and timestamps for fast retrieval.