• AG-UI Serialization์€ ์—์ด์ „ํŠธโ€“UI ์„ธ์…˜์˜ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ์˜์†/๋ณต์›ํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ ๋ชจ๋ธ
  • BaseEvent ์‹œํ€€์Šค๋ฅผ JSON ๋“ฑ portable ํฌ๋งท์œผ๋กœ ์ €์žฅยท์žฌ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ ์†Œ์‹ฑ(Event Sourcing) ๊ธฐ๋ฐ˜ ์˜์†ํ™” ๊ณ„์•ฝ
  • ์ƒˆ๋กœ๊ณ ์นจยท์žฌ์ ‘์†ยท๊ณต์œ  ์‹œ์—๋„ ๋Œ€ํ™”ยท์ƒํƒœยทUI๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณต์›ํ•˜๋Š” ์„ธ์…˜ ์žฌํ˜„ ๋ฉ”์ปค๋‹ˆ์ฆ˜
  • parentRunId๋กœ git-like ๊ณ„๋ณด๋ฅผ ๋งŒ๋“œ๋Š” append-only branching ๋กœ๊ทธ
  • ์˜๋ฏธ๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ์ŠคํŠธ๋ฆผ ๊ธธ์ด๋ฅผ ์ค„์ด๋Š” ์ด๋ฒคํŠธ ์••์ถ•(Compaction) ๊ทœ์•ฝ

ํ•ด๋‹น ๊ฐœ๋…์ด ํ•„์š”ํ•œ ์ด์œ 

  • ์ƒˆ๋กœ๊ณ ์นจ/์žฌ์ ‘์† ์‹œ ๋Œ€ํ™”ยทUI ์ƒํƒœ๊ฐ€ ์‚ฌ๋ผ์ง€๋Š” ๋ฌธ์ œ โ€” ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ์–ด๋””์„œ๋“  ๊ฐ™์€ ์ŠคํŠธ๋ฆผ์„ ๋ณต์› ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ
  • ๊ฐ™์€ thread ์•ˆ์—์„œ โ€œ๋‹ค๋ฅธ ๋‹ต์„ ์‹œ๋„ํ•ด๋ณด๊ณ  ๋น„๊ตโ€ํ•˜๋Š” ์›Œํฌํ”Œ๋กœ์šฐ โ€” parentRunId ๊ธฐ๋ฐ˜ ๋ถ„๊ธฐ + ์‹œ๊ฐ„ ์—ฌํ–‰ ํ•„์š”
  • streaming delta(TEXT_MESSAGE_CONTENT, STATE_DELTA, TOOL_CALL_ARGS) ๋ˆ„์ ์œผ๋กœ ์ธํ•œ ์ €์žฅ/๋„คํŠธ์›Œํฌ ๋น„์šฉ ํญ์ฆ โ€” ์˜๋ฏธ๋ฅผ ๋ณด์กดํ•œ ์••์ถ• ํ•„์š”
  • run ๋‹จ์œ„๋กœ โ€œ์—์ด์ „ํŠธ์—๊ฒŒ ์ •ํ™•ํžˆ ๋ฌด์—‡์ด ์ž…๋ ฅ๋˜์—ˆ๋Š”๊ฐ€โ€๋ฅผ ๋ช…์‹œ ๊ธฐ๋ก โ†’ ์žฌํ˜„ ๊ฐ€๋Šฅ์„ฑ(reproducibility) ๋ณด์žฅ
  • ์‹คํ–‰ ์ค‘ ์—์ด์ „ํŠธ์— **์žฌ์ ‘์†(attach)**ํ•˜์—ฌ ๋Š๊น€ ์—†์ด ์ด๋ฒคํŠธ๋ฅผ ์ด์–ด ๋ฐ›๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ํ™”๋œ ๋ฐฑํ•„(replay) ๊ฒฝ๋กœ ํ•„์š”

AS-IS โ€” ํœ˜๋ฐœ์„ฑ in-memory ์ŠคํŠธ๋ฆผ

sequenceDiagram
    autonumber
    participant UI
    participant Agent as AG-UI Agent
    participant Mem as In-Memory Stream
    UI->>Agent: runAgent(input)
    Agent-->>Mem: BaseEvent[] (ํœ˜๋ฐœ์„ฑ)
    Note over Mem: ์ƒˆ๋กœ๊ณ ์นจ / ํฌ๋ž˜์‹œ / ํƒญ ์ „ํ™˜<br/>= ์ „์ฒด ์ŠคํŠธ๋ฆผ ์†์‹ค
    UI--xMem: reload (ํžˆ์Šคํ† ๋ฆฌ ๋ณต๊ตฌ ๋ถˆ๊ฐ€)
    UI--xAgent: ์žฌ์ ‘์† ์‹œ mid-run ์ด๋ฒคํŠธ ๋ฐ›์„ ๋ฐฉ๋ฒ• ์—†์Œ

TO-BE โ€” ์ง๋ ฌํ™”๋œ append-only ์ŠคํŠธ๋ฆผ

sequenceDiagram
    autonumber
    participant UI
    participant Agent as AG-UI Agent
    participant Store as Append-Only Storage
    UI->>Agent: runAgent(input, parentRunId?)
    Agent-->>Store: BaseEvent append (JSON ์ง๋ ฌํ™”)
    UI->>Store: load(threadId)
    Store-->>UI: serialized events
    UI->>UI: compactEvents() โ†’ snapshot ๋ณต์›
    UI->>UI: ์ž„์˜ runId ์„ ํƒ โ†’ ์‹œ๊ฐ„ ์—ฌํ–‰ / ๋ถ„๊ธฐ

ํ•ต์‹ฌ 3๊ฐ€์ง€ ๋ฉ”์ปค๋‹ˆ์ฆ˜

1. Stream Serialization โ€” ์ด๋ฒคํŠธ ํžˆ์Šคํ† ๋ฆฌ ์˜์†ํ™”

  • BaseEvent ๋ฐฐ์—ด ์ „์ฒด๋ฅผ JSON ๋“ฑ portable ํฌ๋งท์œผ๋กœ ์ €์žฅ โ†’ DBยทํŒŒ์ผยท๋กœ๊ทธ ์–ด๋””๋“  ๋ณด๊ด€
  • ํ•ต์‹ฌ์€ **โ€œ์ด๋ฒคํŠธ ์ž์ฒด๊ฐ€ ์ง„์‹ค(source of truth)โ€œ**์ด๋ผ๋Š” ์ด๋ฒคํŠธ ์†Œ์‹ฑ ๋ชจ๋ธ
  • threadId, runId, timestamp ์ธ๋ฑ์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด ๋น ๋ฅธ ์กฐํšŒ ๊ฐ€๋Šฅ
  • ํด๋ผ์ด์–ธํŠธ ์žฌ๋กœ๋“œ / ๋‹ค๋ฅธ ๋””๋ฐ”์ด์Šค ์ ‘์† / ๊ณต์œ  ๋งํฌ ๋ชจ๋‘ ๋™์ผ ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ ์ฒ˜๋ฆฌ

2. Event Compaction โ€” ์˜๋ฏธ ๋ณด์กด ์••์ถ•

streaming delta๊ฐ€ ๊ทธ๋Œ€๋กœ ์Œ“์ด๋ฉด ๋น„์šฉ์ด ํญ์ฆํ•œ๋‹ค. Compaction์€ ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ๊ฒฐ๊ณผ(observable outcome)๋Š” ์œ ์ง€ํ•˜๋ฉด์„œ ์ด๋ฒคํŠธ ์ˆ˜๋ฅผ ์ค„์ธ๋‹ค.

๋Œ€์ƒBeforeAfter
๋ฉ”์‹œ์ง€TEXT_MESSAGE_START + N๊ฐœ CONTENT(delta) + END๋‹จ์ผ MESSAGES_SNAPSHOT
๋„๊ตฌ ํ˜ธ์ถœTOOL_CALL_START + ARGS(delta)* + END + RESULT์••์ถ•๋œ ๋‹จ์ผ tool record (TOOL_CALL_RESULT ํฌํ•จ ํ˜•ํƒœ)
์ƒํƒœ์—ฐ์† STATE_DELTA (JSON Patch)์ตœ์ข… STATE_SNAPSHOT
run ์ž…๋ ฅRunStarted.input.messages์— ์ค‘๋ณต๋œ ๋ฉ”์‹œ์ง€์ด์ „ ์ŠคํŠธ๋ฆผ์— ์ด๋ฏธ ์žˆ๋Š” ๋ฉ”์‹œ์ง€๋Š” ์ œ๊ฑฐ

์ž์„ธํ•œ Tool ์ด๋ฒคํŠธ ํ๋ฆ„๊ณผ Message ์ด๋ฒคํŠธ ํ๋ฆ„์€ ๊ฐ ๊ฐœ๋… ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ .

3. Run Lineage โ€” parentRunId ๊ธฐ๋ฐ˜ git-like ๊ณ„๋ณด

  • ๊ฐ RUN_STARTED๊ฐ€ ์–ด๋–ค run์—์„œ ๋ถ„๊ธฐํ–ˆ๋Š”์ง€ parentRunId๋กœ ๋ช…์‹œ
  • ํ•œ thread ์•ˆ์— ์—ฌ๋Ÿฌ ๋ถ„๊ธฐ๋ฅผ ๋™์‹œ์— ๋ณด๊ด€ ๊ฐ€๋Šฅ (ํŠธ๋ฆฌ/DAG)
  • ๋กœ๊ทธ๋Š” immutable & append-only โ€” ๊ฒฐ๊ณผ์ ์œผ๋กœ ๊ฒฐ์ •์  ์‹œ๊ฐ„ ์—ฌํ–‰๊ณผ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์„ธ์…˜ ๊ณต์œ ๊ฐ€ ๋™์‹œ์— ์„ฑ๋ฆฝ

RunStarted ํ™•์žฅ ํ•„๋“œ

AG-UI Event์˜ lifecycle ์ด๋ฒคํŠธ ์ค‘ RUN_STARTED๊ฐ€ ์ง๋ ฌํ™”/๋ถ„๊ธฐ๋ฅผ ์œ„ํ•ด ๋‹ค์Œ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€๋กœ ๊ฐ–๋Š”๋‹ค.

type RunStartedEvent = BaseEvent & {
  type: EventType.RUN_STARTED
  threadId: string
  runId: string
  /** ๊ฐ™์€ thread ์•ˆ์—์„œ ์–ด๋–ค run์„ ๋ถ€๋ชจ๋กœ ๊ฐ€์ง€์น˜๊ธฐ ํ–ˆ๋Š”์ง€ */
  parentRunId?: string
  /** ์ด๋ฒˆ run์— Agent์—๊ฒŒ ์ •ํ™•ํžˆ ์ „๋‹ฌ๋œ ์ž…๋ ฅ
   *  (์ด๋ฏธ ํžˆ์Šคํ† ๋ฆฌ์— ์žˆ๋Š” ๋ฉ”์‹œ์ง€๋Š” ์ƒ๋žต ๊ฐ€๋Šฅ โ€” Normalized Input) */
  input?: AgentInput
}

ํ•ต์‹ฌ์€ **โ€œrun ์ž…๋ ฅ์˜ ๋ช…์‹œ ๊ธฐ๋กโ€**์ด๋‹ค. ๋ฉ”์‹œ์ง€๊ฐ€ ๋ˆ„์ ๋˜๋Š” ํ™˜๊ฒฝ์—์„œ, โ€œ์ด๋ฒˆ run์— ์ •ํ™•ํžˆ ๋ฌด์—‡์ด ๋“ค์–ด๊ฐ”๋Š”๊ฐ€โ€๋ฅผ ์‚ฌํ›„์— ์•Œ๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ.

์ด๋ฒคํŠธ ์••์ถ•(Compaction) ์˜ˆ์‹œ

Before โ€” streaming raw events

[
  { 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 โ€” compacted snapshots

[
  {
    type: "MESSAGES_SNAPSHOT",
    messages: [{ id: "msg1", role: "user", content: "Hello world" }],
  },
  {
    type: "STATE_SNAPSHOT",
    state: { foo: 2 },
  },
]

์••์ถ• ํ›„์—๋„ ๊ด€์ฐฐ์ž๊ฐ€ ๋ณด๋Š” ์ตœ์ข… ๊ฒฐ๊ณผ๋Š” ๋™์ผํ•˜๋‹ค. ๋‹ค๋งŒ ์ค‘๊ฐ„ streaming์˜ ์‹œ๊ฐ์  ํšจ๊ณผ(ํƒ€์ดํ•‘ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋“ฑ)๋Š” ์‚ฌ๋ผ์ง€๋ฏ€๋กœ, โ€œlive ์žฌ์ƒโ€ ์šฉ๋„์—๋Š” raw, โ€œ์žฅ๊ธฐ ๋ณด๊ด€โ€์šฉ์—๋Š” compacted๋ฅผ ์„ ํƒ์ ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

๋ถ„๊ธฐ(Branching)์™€ ์‹œ๊ฐ„ ์—ฌํ–‰

parentRunId๋งŒ์œผ๋กœ git-like ๊ทธ๋ž˜ํ”„๊ฐ€ ์ž์—ฐ ๋ฐœ์ƒํ•œ๋‹ค.

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"

๋ถ„๊ธฐ ์‚ฌ์šฉ ์‚ฌ๋ก€

  • ๊ฐ™์€ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋Œ€์•ˆ ์‘๋‹ต ๋น„๊ต โ€” A/B ๋น„๊ต, โ€œRegenerateโ€ UX์˜ ๊ทผ๊ฐ„
  • Human-in-the-Loop ์›Œํฌํ”Œ๋กœ์šฐ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ๊ฒฐ์ •์„ ๋‚ด๋ ธ์„ ๋•Œ์˜ what-if ํƒ์ƒ‰
  • ๋””๋ฒ„๊น… ์‹œ โ€œ๋ฒ„๊ทธ ๋ฐœ์ƒ ์ง์ „ run์œผ๋กœ ๋˜๋Œ์•„๊ฐ€์„œ ๋‹ค์‹œ ์‹คํ–‰โ€ โ€” ๊ฒฐ์ •์  ์žฌํ˜„

์‹œ๊ฐ„ ์—ฌํ–‰์ด ๊ฐ€๋Šฅํ•œ ์ด์œ 

  • ๋กœ๊ทธ๊ฐ€ append-only์ด๋ฏ€๋กœ ์ž„์˜ ์‹œ์ ์˜ ์Šค๋ƒ…์ƒท์„ ๊ฒฐ์ •์ ์œผ๋กœ ์žฌ๊ตฌ์„ฑ ๊ฐ€๋Šฅ
  • run ์ž…๋ ฅ์ด RunStarted.input์— ๋ช…์‹œ๋˜์–ด ์žˆ์–ด, ๊ทธ ์‹œ์ ์˜ ์ปจํ…์ŠคํŠธ๋ฅผ ์ •ํ™•ํžˆ ๋ณต์›
// ๋ถ„๊ธฐ ์˜ˆ์‹œ
{ type: "RUN_STARTED", threadId: "t1", runId: "run1",
  input: { messages: ["Tell me about Paris"] } }
 
// run1์—์„œ ๋ถ„๊ธฐ
{ type: "RUN_STARTED", threadId: "t1", runId: "run2",
  parentRunId: "run1",
  input: { messages: ["Actually, tell me about London instead"] } }

์ •๊ทœํ™”๋œ ์ž…๋ ฅ (Normalized Input)

๊ฐ™์€ thread์—์„œ run์„ ๊ฑฐ๋“ญํ•˜๋ฉด ๋ฉ”์‹œ์ง€๊ฐ€ ๋ˆ„์ ๋œ๋‹ค. ๋‘ ๋ฒˆ์งธ run์—์„œ ์ฒซ run์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋˜ input.messages์— ๋‹ด์œผ๋ฉด ์ค‘๋ณต ์ €์žฅ์ด ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ฏธ ์ŠคํŠธ๋ฆผ์— ์กด์žฌํ•˜๋Š” ๋ฉ”์‹œ์ง€๋Š” ์ƒ๋žตํ•œ๋‹ค.

// run1: ์ฒซ ๋ฉ”์‹œ์ง€๋ฅผ ํ’€๋กœ ๊ธฐ๋ก
{ type: "RUN_STARTED", runId: "run1",
  input: { messages: [{ id: "msg1", role: "user", content: "Hello" }] } }
 
// run2: msg1์€ ์ด๋ฏธ history์— ์žˆ์œผ๋ฏ€๋กœ input์—์„œ ์ œ์™ธ
{ type: "RUN_STARTED", runId: "run2",
  input: { messages: [{ id: "msg2", role: "user", content: "How are you?" }] } }

์ด ๊ทœ์น™์€ ๋ฉ”์‹œ์ง€ ๋ชจ๋ธ์˜ โ€œ์ด๋ฒคํŠธ(ํ๋ฆ„) โ‰  ๋ฉ”์‹œ์ง€(์ƒํƒœ) ๋ถ„๋ฆฌโ€ ์ฒ ํ•™๊ณผ ๋งž๋ฌผ๋ฆฐ๋‹ค โ€” ๋ฉ”์‹œ์ง€๋Š” ๋…๋ฆฝ๋œ ์˜์† ๋‹จ์œ„์ด๋ฏ€๋กœ ๋™์ผ ๋ฉ”์‹œ์ง€๊ฐ€ ์—ฌ๋Ÿฌ run์˜ input์— ์ค‘๋ณต๋  ํ•„์š”๊ฐ€ ์—†๋‹ค.

๋ชจ๋ธ ๊ต์ฒด์™€ ์ปจํ…์ŠคํŠธ ๋ณด์กด

๋ฉ”์‹œ์ง€๊ฐ€ ๋ฒค๋” ์ค‘๋ฆฝ ์˜์† ๋‹จ์œ„๋ผ๋Š” ์‚ฌ์‹ค์˜ ์ง์ ‘์  ๊ท€๊ฒฐ: ๊ฐ™์€ thread ์•ˆ์—์„œ LLM ๋ชจ๋ธ์„ ๋ฐ”๊ฟ”๋„ ํžˆ์Šคํ† ๋ฆฌ๋Š” ๋‚ ์•„๊ฐ€์ง€ ์•Š๋Š”๋‹ค. AG-UI ๊ณต์‹ ๋ฌธ์„œ๊ฐ€ ์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ช…์‹œ์  ์„ค๊ณ„ ๋ชฉํ‘œ๋กœ ์‚ผ๋Š”๋‹ค.

โ€œAG-UI messages are designed to be vendor-neutralโ€ฆ a common interface regardless of the underlying AI service.โ€ โ€” messages.mdx

์‹œ๋‚˜๋ฆฌ์˜ค โ€” ๊ฐ™์€ thread์—์„œ ๋ชจ๋ธ A โ†’ B ์ „ํ™˜

ํšŒ์› ์ƒ๋‹ด thread์—์„œ ์ฒซ 5ํ„ด์€ OpenAI GPT-4o๋กœ ์ฒ˜๋ฆฌํ•˜๋‹ค๊ฐ€, 6ํ„ด๋ถ€ํ„ฐ ๋น„์šฉ ๋ฌธ์ œ๋กœ Anthropic Claude๋กœ ๋ผ์šฐํŒ…์„ ๋ฐ”๊พธ๋Š” ์ƒํ™ฉ. 6ํ„ด์งธ์— ์‚ฌ์šฉ์ž๊ฐ€ โ€œ์ง€๊ธˆ๊นŒ์ง€ ๋Œ€ํ™” ์š”์•ฝํ•ด์ค˜โ€๋ผ๊ณ  ๋ฌป๋Š”๋‹ค.

flowchart TD
    subgraph Persist["์˜์† ๊ณ„์ธต โ€” ๋ฒค๋” ๋ฌด๊ด€"]
        Thread["threadId t-1<br/>MESSAGES_SNAPSHOT (1~5ํ„ด)<br/>STATE_SNAPSHOT"]
    end

    subgraph Run5["Run 5 โ€” ๋ชจ๋ธ A"]
        IN5[RunAgentInput] --> ADP_A[OpenAI ์–ด๋Œ‘ํ„ฐ<br/>aguiToOpenAI]
        ADP_A --> LLM_A[GPT-4o]
    end

    subgraph Run6["Run 6 โ€” ๋ชจ๋ธ B ๊ฐ™์€ thread"]
        IN6[RunAgentInput<br/>๊ฐ™์€ messages ๋ฐฐ์—ด] --> ADP_B[Anthropic ์–ด๋Œ‘ํ„ฐ<br/>aguiToAnthropic]
        ADP_B --> LLM_B[Claude]
    end

    Thread --> IN5
    Thread --> IN6
    LLM_A -- "RUN_FINISHED ํ›„ ๋ˆ„์ " --> Thread
    LLM_B -- "์ด์ „ 5ํ„ด์„ ์ปจํ…์ŠคํŠธ๋กœ ๋ฐ›์•„ ์š”์•ฝ ๊ฐ€๋Šฅ" --> Thread

ํ•ต์‹ฌ์€ RunAgentInput.messages๊ฐ€ ๋ชจ๋ธ ๋…๋ฆฝ์ ์ธ ๋™์ผ ๋ฐฐ์—ด์ด๋ผ๋Š” ์ ์ด๋‹ค. ๋ชจ๋ธ๋ณ„ ๋ณ€ํ™˜์€ ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋งค๋ฒˆ ๋‹ค๋ฅด๊ฒŒ ์ ์šฉํ•  ๋ฟ์ด๋‹ค.

# ํ•ต์‹ฌ ์ˆ˜๋„ ์ฝ”๋“œ โ€” AbstractAgent ๋‚ด๋ถ€ ๋ผ์šฐํŒ…
def run(input: RunAgentInput):
    messages = input.messages          # ๋ฒค๋” ์ค‘๋ฆฝ AG-UI ๋ฉ”์‹œ์ง€
    if route_to_b(input):
        vendor_msgs = agui_to_anthropic(messages)
        stream = anthropic.stream(vendor_msgs)
    else:
        vendor_msgs = agui_to_openai(messages)
        stream = openai.stream(vendor_msgs)
    yield from to_agui_events(stream)  # ๊ฒฐ๊ณผ๋Š” ๋‹ค์‹œ AG-UI ์ด๋ฒคํŠธ๋กœ ํ™˜์›

๋ณด์กด๋˜๋Š” ๊ฒƒ vs ์œ ์‹ค๋˜๋Š” ๊ฒƒ

ํ•ญ๋ชฉ๋ชจ๋ธ ๊ต์ฒด ์‹œ์ด์œ 
user / assistant / tool ๋ฉ”์‹œ์ง€ ๋ณธ๋ฌธ๋ณด์กด๋ฒค๋” ์ค‘๋ฆฝ ํฌ๋งท
STATE_SNAPSHOT / STATE_DELTA๋ณด์กดJSON ๊ฐ์ฒด, ๋ชจ๋ธ ๋ฌด๊ด€
threadId / runId / parentRunId lineage๋ณด์กดAG-UI ๋ ˆ์ด์–ด๊ฐ€ ๊ด€๋ฆฌ
Reasoning ๋ฉ”์‹œ์ง€๋ถ€๋ถ„ ๋ณด์กดrole: "reasoning"์€ ํ‘œ์ค€ํ™”๋˜๋‚˜, ๋ชจ๋ธ๋ณ„ reasoning ์„œ๋ช…ยท์„ธ๋ถ€ ๊ตฌ์กฐ๋Š” ์†์‹ค ๊ฐ€๋Šฅ
Prompt cache key๋ฌดํšจํ™”์บ์‹œ๋Š” ๋ชจ๋ธ๋ณ„ โ€” ์ƒˆ ๋ชจ๋ธ์—์„œ hit ๋ถˆ๊ฐ€
๋ฒค๋” ์ „์šฉ tool ๋ฉ”ํƒ€๋ณ€ํ™˜ ํ•„์š”AG-UI Tools๋Š” JSON Schema ๊ธฐ๋ฐ˜์ด๋ผ ๋Œ€๋ถ€๋ถ„ ํ˜ธํ™˜

๋”ฐ๋ผ์„œ ๋ชจ๋ธ ์ „ํ™˜์€ โ€œ์ปจํ…์ŠคํŠธ ์œ ์‹คโ€๋ณด๋‹ค๋Š” โ€œ์บ์‹œยท๋ฏธ์„ธ ๋ฉ”ํƒ€ ๋ฆฌ์…‹โ€ ๊ด€์ ์œผ๋กœ ์ดํ•ดํ•ด์•ผ ํ•œ๋‹ค.

์žฅ๊ธฐ ๊ธฐ์–ต๊ณผ RAG ํ†ตํ•ฉ

thread ๋‚ด๋ถ€ ํžˆ์Šคํ† ๋ฆฌ๋Š” ์œ„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์œผ๋กœ ๋ณด์กด๋˜์ง€๋งŒ, ์ˆ˜๊ฐœ์›”ยท์ˆ˜๋…„์น˜ ๋Œ€ํ™”๋ฅผ ๋ชจ๋‘ ๋งค run์˜ messages์— ๋„ฃ๋Š” ๊ฑด ๋น„ํ˜„์‹ค์ ์ด๋‹ค. AG-UI๋Š” thread ์™ธ๋ถ€์˜ long-term memory๋ฅผ capability flag๋กœ ์ธ์ง€ํ•œ๋‹ค.

interface StateCapabilities {
  /** Set `true` if the agent has long-term memory beyond the current thread 
   *  (e.g., vector store, knowledge base, or cross-session recall). */
  memory?: boolean
  persistentState?: boolean
}

โ€” docs/concepts/capabilities.mdx

์ฆ‰ vector DB๋Š” AG-UI ํ”„๋กœํ† ์ฝœ์ด ์ง์ ‘ ์ œ๊ณตํ•˜์ง€๋Š” ์•Š์ง€๋งŒ, โ€œ์—์ด์ „ํŠธ๊ฐ€ ์ด๋Ÿฐ ๋Šฅ๋ ฅ์„ ๊ฐ€์งโ€์„ ํด๋ผ์ด์–ธํŠธ์— ์„ ์–ธํ•˜๋Š” ํ‘œ์ค€์ด ์žˆ๋‹ค. ์ง๋ ฌํ™”๋œ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ RAG ๋ฐฑ๋ณธ์œผ๋กœ ์žฌํ™œ์šฉํ•˜๋Š” ๊ฒƒ์€ ์„ค๊ณ„ ์˜๋„์™€ ์ •ํ™•ํžˆ ๋ถ€ํ•ฉํ•œ๋‹ค.

RAG ํŒŒ์ดํ”„๋ผ์ธ

flowchart LR
    Stream[์ง๋ ฌํ™”๋œ BaseEvent ๋กœ๊ทธ] -->|compactEvents| Snap[MESSAGES_SNAPSHOT]
    Snap -->|user/assistant ์ถ”์ถœ + ํ๋ ˆ์ด์…˜| Chunk[ํ…์ŠคํŠธ ์ฒญํฌ]
    Chunk -->|embedding model| Emb[๋ฒกํ„ฐ]
    Emb --> VDB[(Vector DB<br/>pgvector / Pinecone /<br/>Weaviate / Qdrant)]

    NewQ[์ƒˆ user ๋ฉ”์‹œ์ง€] -->|query embedding| VDB
    VDB -->|top-K ์œ ์‚ฌ ์ฒญํฌ| Inject[RunAgentInput.context]
    Inject --> Agent[AG-UI Agent]

ํ•ต์‹ฌ์€ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ messages์— ์ง์ ‘ prependํ•˜์ง€ ์•Š๊ณ  Context ์Šฌ๋กฏ์— ์ฃผ์ž…ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. AG-UI์— ์ด๋ฏธ ํ‘œ์ค€ ์Šฌ๋กฏ์ด ์žˆ๋‹ค.

# ํ•ต์‹ฌ ์ˆ˜๋„ ์ฝ”๋“œ โ€” RAG ์ปจํ…์ŠคํŠธ ์ฃผ์ž…
hits = vector_db.search(embed(latest_user_msg), top_k=5)
input = RunAgentInput(
    thread_id="t-1",
    run_id="r-42",
    messages=current_thread_messages,        # ์ •๊ทœํ™”๋œ ์งง์€ ํžˆ์Šคํ† ๋ฆฌ
    context=[
        Context(description=f"recall_{i}", value=h.text)
        for i, h in enumerate(hits)          # โ† ์žฅ๊ธฐ ๊ธฐ์–ต ์ฃผ์ž…
    ],
    state={}, tools=[], forwarded_props={},
)

์‹œ๋‚˜๋ฆฌ์˜ค โ€” 1๋…„์น˜ ๊ณ ๊ฐ ์ƒ๋‹ด RAG

๊ฐ™์€ ์‚ฌ์šฉ์ž๊ฐ€ 6๊ฐœ์›” ์ „ ํ™˜๋ถˆ ๋ฌธ์˜๋ฅผ ํ–ˆ๊ณ , ์˜ค๋Š˜ ๋‹ค์‹œ ๋น„์Šทํ•œ ์ผ€์ด์Šค๋กœ ์ ‘์†. compaction๋œ ๊ณผ๊ฑฐ ์ด๋ฒคํŠธ ๋กœ๊ทธ์—์„œ user/assistant ๋ฉ”์‹œ์ง€๋งŒ ์ถ”์ถœํ•ด ์ž„๋ฒ ๋”ฉํ•œ ์ƒํƒœ๋ผ๋ฉด:

  • ์˜ค๋Š˜ ์ƒˆ ๋ฉ”์‹œ์ง€์˜ ์ž„๋ฒ ๋”ฉ์œผ๋กœ vector DB ์งˆ์˜
  • ์œ ์‚ฌ๋„ top-3 ์ฒญํฌ๊ฐ€ Context๋กœ ์ฃผ์ž…
  • ์—์ด์ „ํŠธ๋Š” 6๊ฐœ์›” ์ „ ๊ฒฐ์ • ์‚ฌ์œ ๊นŒ์ง€ ํŒŒ์•…ํ•œ ์ฑ„๋กœ ์‘๋‹ต ์ƒ์„ฑ

ํ๋ ˆ์ด์…˜ ๊ฐ€์ด๋“œ

๋ฉ”์‹œ์ง€ role์ž„๋ฒ ๋”ฉ ๋Œ€์ƒ
user, assistant๊ถŒ์žฅ โ€” ์˜๋„/๊ฒฐ์ •์˜ ํ•ต์‹ฌ
tool์„ ํƒ์  โ€” ์งง์€ ๊ฒฐ๊ณผ๋งŒ, ๊ธด raw payload๋Š” ๋…ธ์ด์ฆˆ
reasoning์„ ํƒ์  โ€” ์˜์‚ฌ๊ฒฐ์ • ์ถ”์ ์šฉ์œผ๋กœ๋งŒ
activity๋น„๊ถŒ์žฅ โ€” UI ์ง„ํ–‰ ์ƒํ™ฉ์€ ๊ฒ€์ƒ‰ ๊ฐ€์น˜ ๋‚ฎ์Œ
system, developer๋น„๊ถŒ์žฅ โ€” ์ •์  ํ”„๋กฌํ”„ํŠธ, ๋งค๋ฒˆ ๋ณ„๋„ ์ฃผ์ž…

Storage ๋ฐฑ์—”๋“œ โ€” ํ”„๋กœํ† ์ฝœ์˜ ๊ฒฝ๊ณ„

์œ„ ๋‘ ๋ฉ”์ปค๋‹ˆ์ฆ˜(์ปจํ…์ŠคํŠธ ๋ณด์กด + RAG)์€ ๋ชจ๋‘ โ€œ์–ด๋”˜๊ฐ€์— ์ง๋ ฌํ™”๋œ ์ด๋ฒคํŠธ๊ฐ€ ์ €์žฅ๋ผ ์žˆ๋‹คโ€๋ฅผ ์ „์ œํ•œ๋‹ค. ๊ทธ โ€œ์–ด๋”˜๊ฐ€โ€๋Š” AG-UI๊ฐ€ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š”๋‹ค.

โ€œConversation history is stored only in memory. If the application restarts, all history is lost. For persistence, you must implement your own storage solution by retrieving conversations using getHistory() and saving them externally.โ€ โ€” docs/sdk/kotlin/client/stateful-agui-agent.mdx

์ฆ‰ SQLiteยทPostgresยทRedis ๊ฐ™์€ ํŠน์ • DB๊ฐ€ SDK์— ๋ฐ•ํ˜€ ์žˆ์ง€ ์•Š๋‹ค. ํ”„๋กœํ† ์ฝœ์€ ์ง๋ ฌํ™” ํ˜•์‹๊ณผ ๊ถŒ์žฅ์‚ฌํ•ญ๋งŒ ์ œ๊ณตํ•˜๊ณ , storage ์–ด๋Œ‘ํ„ฐ๋Š” ์‚ฌ์šฉ์ž/integration์ด ๋ถ™์ธ๋‹ค.

์–ด๋Œ‘ํ„ฐ ํŒจํ„ด

flowchart LR
    Bus[Event Bus] -->|"save(threadId, events)"| Adapter[StorageAdapter<br/>์ธํ„ฐํŽ˜์ด์Šค]
    Adapter --> A[(Postgres<br/>JSONB events ์ปฌ๋Ÿผ)]
    Adapter --> B[(S3<br/>append-only ํŒŒ์ผ)]
    Adapter --> C[(Redis Streams<br/>์‹ค์‹œ๊ฐ„ + ์งง์€ ๋ณด๊ด€)]
    Adapter --> D[(ClickHouse<br/>๋ถ„์„/์ง‘๊ณ„)]
    Adapter --> E[(Vector DB<br/>RAG์šฉ ์‚ฌ์ด๋“œ์นด)]
# ํ•ต์‹ฌ ์ˆ˜๋„ ์ฝ”๋“œ โ€” ์‚ฌ์šฉ์ž๊ฐ€ ๊ตฌํ˜„ํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ ๊ณ„์•ฝ
class StorageAdapter(Protocol):
    async def append(self, thread_id: str, event: BaseEvent) -> None: ...
    async def load(self, thread_id: str) -> list[BaseEvent]: ...
    async def compact(self, thread_id: str) -> None: ...

๋ฐฑ์—”๋“œ ์„ ํƒ ๊ฐ€์ด๋“œ

๋ฐฑ์—”๋“œ์ ํ•ฉํ•œ ๊ฒฝ์šฐํŠธ๋ ˆ์ด๋“œ์˜คํ”„
Postgres + JSONBํŠธ๋žœ์žญ์…˜ยท๊ด€๊ณ„ํ˜• ์กฐ์ธ ํ•„์š”, ์ค‘๊ทœ๋ชจ์ธ๋ฑ์Šค ์„ค๊ณ„ ํ•„์š”, ๋งค์šฐ ๊ธด ์ŠคํŠธ๋ฆผ์€ row ๋น„๋Œ€
S3 / ํŒŒ์ผ append์žฅ๊ธฐ ๋ณด๊ด€ยท์ €๋น„์šฉ cold storage๋ถ€๋ถ„ ์กฐํšŒ ์–ด๋ ค์›€ โ†’ compaction ํ›„ snapshot์œผ๋กœ ๋ณ„๋„ ๋ณด๊ด€
Redis Streams์‹ค์‹œ๊ฐ„ attachยท์žฌ์ ‘์†, ์งง์€ ๋ณด๊ด€์˜์†์„ฑ์€ ๋ณด์กฐ์šฉ, ์žฅ๊ธฐ ๋ณด๊ด€์—๋Š” ๋ถ€์ ํ•ฉ
ClickHouse / BigQuery๋ถ„์„ยท์ง‘๊ณ„ยท์‹œ๊ณ„์—ด ์งˆ์˜๋‹จ๊ฑด ํŠธ๋žœ์žญ์…˜ ๋ถ€์ ํ•ฉ
Vector DB (์‚ฌ์ด๋“œ์นด)RAG ์ „์šฉ โ€” ์œ„ RAG ์„น์…˜ ํŒจํ„ด์›๋ณธ ์ด๋ฒคํŠธ ์ €์žฅ์€ ๋ชป ํ•จ, ๋‹ค๋ฅธ backend์™€ ๋ณ‘ํ–‰ ํ•„์š”

์‹ค์ „์—์„œ๋Š” 2-tier ์กฐํ•ฉ์ด ํ”ํ•˜๋‹ค โ€” hot tier(Redis/Postgres)์— raw events, cold tier(S3/ClickHouse)์— compacted snapshots, ์‚ฌ์ด๋“œ์นด๋กœ vector DB.

๊ณต์‹ integration ์‚ฌ๋ก€

  • ADK middleware โ€” POST /agents/state ์—”๋“œํฌ์ธํŠธ๋กœ thread๋ณ„ messages/state ์กฐํšŒ. ๋‚ด๋ถ€ storage๋Š” ADK ์ธก ์ž์œ  ์„ ํƒ.
  • ADK + VertexAIMemoryService โ€” ์„ธ์…˜ ๋งŒ๋ฃŒ ์‹œ memory_service.add_session_to_memory() ์ž๋™ ํ˜ธ์ถœ โ†’ ์‚ฌ์‹ค์ƒ ์ž๋™ RAG ๋ฐฑํ•„
  • Kotlin getHistory(threadId) โ€” ๋ฉ”๋ชจ๋ฆฌ ์ „์šฉ. ์˜์†ํ™” ์ฑ…์ž„์€ ํ˜ธ์ถœ์ž.

์ง๋ ฌํ™” ํ๋ฆ„ โ€” End-to-End

sequenceDiagram
    autonumber
    participant UI
    participant Store as Storage (append-only)
    participant Bus as Event Bus
    participant Agent as AbstractAgent

    Agent-->>Bus: RUN_STARTED (threadId, runId, parentRunId?, input?)
    Agent-->>Bus: TEXT_MESSAGE_START / CONTENT(delta)*  / END
    Agent-->>Bus: STATE_DELTA (JSON Patch) ...
    Agent-->>Bus: TOOL_CALL_START / ARGS / END / RESULT
    Agent-->>Bus: RUN_FINISHED

    Bus->>Store: append events (JSON.stringify)

    Note over Store: ์ •์ฑ…์— ๋”ฐ๋ผ ์ฃผ๊ธฐ์ ์œผ๋กœ compactEvents() ์ ์šฉ

    UI->>Store: load(threadId)
    Store-->>UI: serialized events
    UI->>UI: compactEvents โ†’ MESSAGES_SNAPSHOT / STATE_SNAPSHOT ๋ณต์›
    UI->>UI: ์ž„์˜ runId ์„ ํƒ โ†’ ์‹œ๊ฐ„ ์—ฌํ–‰

๊ธฐ๋ณธ ์ฝ”๋“œ ์˜ˆ์‹œ

// ์ง๋ ฌํ™” (์ €์žฅ)
const events: BaseEvent[] = [...];
const serialized = JSON.stringify(events);
await storage.save(threadId, serialized);
 
// ๋ณต์› + ์••์ถ•
const restored = JSON.parse(await storage.load(threadId));
const compacted = compactEvents(restored);
 
// compacted ์ŠคํŠธ๋ฆผ์œผ๋กœ UI ๋‹ค์‹œ ๊ทธ๋ฆฌ๊ธฐ
ui.render(compacted);

๋‹ค๋ฅธ AG-UI ๊ฐœ๋…๊ณผ์˜ ๊ด€๊ณ„

  • AG-UI Event โ€” Serialization์€ ๊ฒฐ๊ตญ ์ด๋ฒคํŠธ ์‹œํ€€์Šค์˜ ์˜์†ํ™”๋‹ค. 28๊ฐœ ์ด๋ฒคํŠธ ํƒ€์ž… ์ž์ฒด๊ฐ€ ์ง๋ ฌํ™”์˜ ๋‹จ์œ„.
  • AG-UI Messages โ€” TEXT_MESSAGE_* ์ŠคํŠธ๋ฆผ์ด ์••์ถ•๋˜์–ด MESSAGES_SNAPSHOT์ด ๋œ๋‹ค. ๋ฉ”์‹œ์ง€๋Š” ์˜์† ์ƒํƒœ, ์ด๋ฒคํŠธ๋Š” ํ๋ฆ„.
  • AG-UI State Management โ€” STATE_DELTA(JSON Patch RFC 6902)๊ฐ€ ๋ˆ„์ ๋˜์–ด ์ตœ์ข… STATE_SNAPSHOT์œผ๋กœ ์••์ถ•๋œ๋‹ค.
  • AG-UI Tools โ€” TOOL_CALL_START/ARGS/END/RESULT ํ๋ฆ„์ด ๋‹จ์ผ tool record๋กœ ์••์ถ• ๊ฐ€๋Šฅ.
  • AG-UI Agents / AbstractAgent โ€” ์ง๋ ฌํ™” ๋Œ€์ƒ์€ runAgent(input) ๊ฒฐ๊ณผ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ. RunStarted.input ํ•„๋“œ๊ฐ€ ์ž…๋ ฅ์˜ ์ •ํ™•ํ•œ ๊ธฐ๋ก์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

๊ตฌํ˜„ ์‹œ ๊ณ ๋ ค์‚ฌํ•ญ

  • SDK ํ—ฌํผ ์ œ๊ณต: (de)serialize, compactEvents ๊ฐ™์€ ์œ ํ‹ธ์„ SDK ์ฐจ์›์—์„œ ์ œ๊ณตํ•ด์•ผ ์ผ๊ด€์„ฑ ํ™•๋ณด
  • append-only ์ €์žฅ: ๊ฐ€๋Šฅํ•˜๋ฉด incremental write โ€” ํ•œ run์ด ๋๋‚  ๋•Œ๋งˆ๋‹ค append, ๋„์ค‘ ์ˆ˜์ • ๊ธˆ์ง€
  • ์••์ถ• ์ •์ฑ…: ์‹ค์‹œ๊ฐ„ ๋ณด๊ธฐ = raw, ์žฅ๊ธฐ ๋ณด๊ด€ = compacted, ๋˜๋Š” [hot=raw, cold=compacted] 2-tier ์ •์ฑ…
  • ์ธ๋ฑ์‹ฑ: threadId, runId, parentRunId, timestamp์— ์ธ๋ฑ์Šค โ€” ๋ถ„๊ธฐ ๊ทธ๋ž˜ํ”„ ํƒ์ƒ‰๊ณผ ๋ถ€๋ถ„ ๋กœ๋“œ ์„ฑ๋Šฅ ํ™•๋ณด
  • ์••์ถ• ์•Œ๊ณ ๋ฆฌ์ฆ˜: ์žฅ๊ธฐ ๋ณด๊ด€ ์‹œ gzip/zstd ๋“ฑ ํŽ˜์ด๋กœ๋“œ ์••์ถ• ์ถ”๊ฐ€ ๊ณ ๋ ค
  • PII / ZDR: ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐ ์ •๋ณด๊ฐ€ ํฌํ•จ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ €์žฅ ์ „ redaction ๋˜๋Š” ํด๋ผ์ด์–ธํŠธ ๋ณด๊ด€ ์ •์ฑ… ๊ณ ๋ ค

์ฐธ๊ณ  ๋ฌธ์„œ