직렬화

직렬화 (Serialization)

AG-UI에서 이벤트 스트림을 저장하고, 분기(branch)하고, 압축(compact)하기 위해 어떻게 직렬화하는지 다루는 페이지예요. 직렬화된 스트림을 이용하면 대화와 UI 상태를 복원하고, 실행 중인 에이전트에 붙고, 과거 실행에서 분기하는 것까지 가능합니다.

출처: 문서

본문

AG-UI의 직렬화는 에이전트–UI 세션을 구동하는 이벤트 스트림을 영속화하고 복원하는 표준 방법을 제공해요. 직렬화된 스트림이 있으면 여러분은:

  • 새로고침이나 재연결 뒤에 채팅 내역과 UI 상태를 복원할 수 있어요
  • 실행 중인 에이전트에 붙어서 계속 이벤트를 받을 수 있어요
  • 이전 실행에서 분기(타임 트래블)를 만들 수 있어요
  • 의미를 잃지 않으면서 저장된 내역을 압축해 크기를 줄일 수 있어요

이 페이지는 그 모델, 갱신된 이벤트 필드, 그리고 예시와 함께 실용적인 사용 패턴을 설명합니다.

핵심 개념 (Core Concepts)

  • 스트림 직렬화 – 전체 이벤트 내역을 휴대용 표현(예: JSON)으로/로부터 변환해 데이터베이스, 파일, 로그에 저장합니다.
  • 이벤트 압축 – 장황한 스트림을 의미를 보존하면서 스냅샷으로 줄여요(예: 콘텐츠 청크 병합, delta를 snapshot으로 축소).
  • 실행 계보 (Run lineage) – parentRunId로 대화의 분기를 추적해, 타임 트래블과 대안 경로를 가능하게 하는 git 같은 append-only 로그를 형성합니다.

갱신된 이벤트 필드 (Updated Event Fields)

RunStarted 이벤트는 추가 선택 필드들을 포함합니다:

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
}

이 필드들은 계보 추적을 가능하게 하고, 구현체가 이미 기록된 메시지와 무관하게 에이전트에게 전달된 것이 정확히 무엇인지 기록할 수 있게 해 줍니다.

이벤트 압축 (Event Compaction)

압축은 동일한 관찰 가능한 결과를 유지하면서 이벤트 스트림의 잡음을 줄여요. 전형적인 구현은 유틸리티를 제공합니다:

declare function compactEvents(events: BaseEvent[]): BaseEvent[]

흔한 압축 규칙은 다음과 같습니다:

  • 메시지 스트림 – TEXT_MESSAGE_* 시퀀스를 단일 메시지 스냅샷으로 결합하고, 같은 메시지에 대한 인접한 TEXT_MESSAGE_CONTENT를 이어붙입니다.
  • 도구 호출 – 도구 호출 start/content/end를 컴팩트한 레코드로 축소해요.
  • 상태 – 연속된 STATE_DELTA 이벤트를 단일 최종 STATE_SNAPSHOT으로 병합하고, 대체된 업데이트를 버립니다.
  • 실행 입력 정규화 – RunStarted.input.messages에서 스트림 앞부분에 이미 있는 메시지를 제거합니다.

분기와 타임 트래블 (Branching and Time Travel)

RunStarted 이벤트에 parentRunId를 설정하면 git 같은 계보를 만듭니다. 스트림은 불변의 append-only 로그가 되며, 각 실행은 이전 실행 중 어느 것에서든 분기할 수 있습니다.

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"

장점:

  • 같은 직렬화된 로그에 여러 분기
  • 불변 내역(append-only)
  • 어떤 지점으로든 결정적 타임 트래블

예시 (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 },
  },
]

parentRunId로 분기 (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
}

메타데이터 (Metadata)

이벤트와 메시지는 선택적인 metadata 객체를 가지며, 그것은 직렬화 대상의 일부예요 — 다른 모든 것과 함께 영속시키세요. 아니면 재생된 스트림에 토큰 사용량, 트레이스 ID, 그리고 생산자가 붙인 나머지가 빠져 버립니다.

영속화할 때 알아야 할 두 가지:

  • 압축이 그것을 접습니다. delta를 축소하면 그 메타데이터가 병합되며, 마지막 쓰기가 이깁니다. 이벤트 압축과 순서 주의점에 대한 압축 레퍼런스를 참고하세요.
  • 바이너리는 JSON보다 좁아요. protobuf 위에서 메타데이터는 google.protobuf.Struct이므로 null, 배열, 중첩 객체는 살아남지만 — 숫자는 IEEE-754 double이라 2^53을 넘는 정수는 정밀도를 잃고, 일부 이벤트 타입은 protobuf 표현이 아예 없습니다. JSON은 모든 것을 정확히 라운드트립해요. 보관 저장에는 JSON을 선호하세요.

구현 노트 (Implementation Notes)

  • 압축 및 (역)직렬화를 위한 SDK 헬퍼를 제공하세요.
  • 스트림을 append-only로 저장하고, 가능하면 증분 쓰기를 선호하세요.
  • 긴 내역을 영속화할 때는 압축을 고려하세요.
  • 빠른 검색을 위해 threadId, runId, 타임스탬프에 인덱스를 추가하세요.

같이 보기 (See Also)

더 알아보기 (Learn more)