• AG-UI ์ด๋ฒคํŠธ ํƒ€์ž… ์‹œ์Šคํ…œ์€ Zod discriminated union ํ•˜๋‚˜๋กœ 33๊ฐœ ์ด๋ฒคํŠธ์˜ schemaยทํƒ€์ž…ยท๊ฒ€์ฆ์„ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›์œผ๋กœ ๋ฌถ์€ ์„ค๊ณ„
  • z.infer๋กœ schema์—์„œ TypeScript ํƒ€์ž…์„ ์ž๋™ ๋„์ถœํ•˜๋Š” schema-first ์•„ํ‚คํ…์ฒ˜
  • BaseEvent + .passthrough()๋กœ ํ™•์žฅ์„ฑ๊ณผ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋™์‹œ์— ํ™•๋ณดํ•œ ๊ตฌ์กฐ

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

  • 33๊ฐœ ์ด๋ฒคํŠธ ร— ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ๋‹ค๋ฅธ ํ•„๋“œ๋ฅผ if/else๋กœ ๋ฐ›์œผ๋ฉด ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ๊ฐ€๋…์„ฑ ๋™์‹œ ์†์‹ค
  • ํ”„๋กœํ† ์ฝœ ์ŠคํŽ™(๋Ÿฐํƒ€์ž„)๊ณผ TypeScript ํƒ€์ž…(์ปดํŒŒ์ผํƒ€์ž„)์ด ๋ถ„๋ฆฌ๋˜๋ฉด drift ๋ฐœ์ƒ โ€” ํ•œ์ชฝ๋งŒ ์—…๋ฐ์ดํŠธ๋˜์–ด ๋ฒ„๊ทธ
  • ๋‹ค์ค‘ transport(SSE/Binary)์™€ ๋‹ค์ค‘ SDK(TS/Python)๊ฐ€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋ฏ€๋กœ ํ•˜๋‚˜์˜ schema๊ฐ€ source of truth์—ฌ์•ผ ์ผ๊ด€์„ฑ ์œ ์ง€

AS-IS โ€” ํƒ€์ž… ๋ถ„๊ธฐ๋ฅผ ์ˆ˜๋™ if/else๋กœ

function handle(event: { type: string; [k: string]: any }) {
  if (event.type === "TEXT_MESSAGE_CONTENT") {
    console.log(event.delta);     // any โ€” ํƒ€์ž… ๋ฏธ๋ณด์žฅ
  } else if (event.type === "RUN_STARTED") {
    console.log(event.threadId);  // any โ€” ํƒ€์ž… ๋ฏธ๋ณด์žฅ
  }
  // ์ƒˆ ์ด๋ฒคํŠธ ์ถ”๊ฐ€ํ•ด๋„ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ๊ฒฝ๊ณ  ์•ˆ ํ•จ
}

TO-BE โ€” discriminated union์œผ๋กœ ์ž๋™ narrowing

function handle(event: AGUIEvent) {
  switch (event.type) {
    case EventType.TEXT_MESSAGE_CONTENT:
      console.log(event.delta);     // string์œผ๋กœ ์ž๋™ ์ขํž˜
      break;
    case EventType.RUN_STARTED:
      console.log(event.threadId);  // string์œผ๋กœ ์ž๋™ ์ขํž˜
      break;
    // ๋ˆ„๋ฝ ์‹œ exhaustiveness ๊ฒฝ๊ณ 
  }
}

ํ•ต์‹ฌ ์„ค๊ณ„ 1 โ€” schema๊ฐ€ ๊ณง ํƒ€์ž…์ด๋‹ค

events.ts๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ Zod schema๋กœ ๋จผ์ € ์ •์˜ํ•˜๊ณ , TypeScript ํƒ€์ž…์€ ๊ทธ๊ฑธ z.infer๋กœ ๋„์ถœํ•œ๋‹ค.

export const TextMessageContentEventSchema = BaseEventSchema.extend({
  type: z.literal(EventType.TEXT_MESSAGE_CONTENT),
  messageId: z.string(),
  delta: z.string(),
});
 
export type TextMessageContentEvent = z.infer<typeof TextMessageContentEventSchema>;

์™œ schema-first์ธ๊ฐ€:

  • ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ(schema.parse())๊ณผ ์ปดํŒŒ์ผํƒ€์ž„ ํƒ€์ž…์„ ํ•œ ์ •์˜๋กœ ๋‘˜ ๋‹ค ์–ป์Œ
  • ํ”„๋กœํ† ์ฝœ ์ŠคํŽ™ = schema โ†’ ๋ณ€๊ฒฝ ์‹œ ๋“œ๋ฆฌํ”„ํŠธ ๋ถˆ๊ฐ€๋Šฅ
  • transport ๊ณ„์ธต์ด wire์—์„œ ๋ฐ›์€ ๊ฐ’์„ ๊ฐ™์€ schema๋กœ ๊ฒ€์ฆ โ€” ์‹ ๋ขฐ ๊ฒฝ๊ณ„ ๋ช…์‹œํ™” (AG-UI Serialization์—์„œ ๋‹ค์‹œ ๋“ฑ์žฅ)

ํ•ต์‹ฌ ์„ค๊ณ„ 2 โ€” discriminated union์œผ๋กœ 33๊ฐœ๋ฅผ ํ•˜๋‚˜๋กœ

export const EventSchemas = z.discriminatedUnion("type", [
  TextMessageStartEventSchema,
  TextMessageContentEventSchema,
  // ... 33๊ฐœ
]);
 
export type AGUIEvent = z.infer<typeof EventSchemas>;

discriminator๋Š” type ํ•„๋“œ. ๊ฐ schema๊ฐ€ type: z.literal(EventType.X)๋กœ ์ž๊ธฐ๋ฅผ ์„ ์–ธํ•˜๋ฏ€๋กœ, TypeScript๊ฐ€ event.type์„ ๋ณด๊ณ  ๋‚˜๋จธ์ง€ ํ•„๋“œ๋ฅผ ์ž๋™ ์ขํžŒ๋‹ค.

์ผ๋ฐ˜ union๊ณผ ์ฐจ์ด:

  • A | B | C๋Š” narrowing์ด ์•ฝํ•จ โ€” ์‚ฌ์šฉ์ž๊ฐ€ type guard๋ฅผ ์ง์ ‘ ์ž‘์„ฑ
  • discriminated union์€ ๊ณตํ†ต ํ•„๋“œ ํ•˜๋‚˜๋กœ ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ž๋™ ๋ถ„๊ธฐ

ํ•ต์‹ฌ ์„ค๊ณ„ 3 โ€” BaseEvent + .passthrough()

export const BaseEventSchema = z
  .object({
    type: z.nativeEnum(EventType),
    timestamp: z.number().optional(),
    rawEvent: z.any().optional(),
  })
  .passthrough();

.passthrough()๋Š” ์ •์˜๋˜์ง€ ์•Š์€ ํ•„๋“œ๋„ ํ†ต๊ณผ์‹œํ‚ค๋Š” Zod ์˜ต์…˜:

  • ์ƒˆ SDK ๋ฒ„์ „์—์„œ ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด๋„ ๊ตฌ๋ฒ„์ „ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ถ€์„œ์ง€์ง€ ์•Š์Œ
  • rawEvent๋กœ ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์ด๋ฒคํŠธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด์กด
  • forward compatibility ํ™•๋ณด โ€” ํ”„๋กœํ† ์ฝœ ์ง„ํ™”์˜ ์•ˆ์ „ํŒ

6๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ํ•œ๋ˆˆ์—

์ž์„ธํ•œ ๋ผ์ดํ”„์‚ฌ์ดํดยทํ•„๋“œ๋Š” AG-UI Event์— ์žˆ๋‹ค. ๋ณธ ๊ธ€์€ ํƒ€์ž… ์‹œ์Šคํ…œ ๊ด€์ ์— ์ง‘์ค‘.

์นดํ…Œ๊ณ ๋ฆฌ๋Œ€ํ‘œ ์ด๋ฒคํŠธํŒจํ„ด๊นŠ์€ ์„ค๋ช…
LifecycleRUN_STARTED / FINISHED / ERROR์‹คํ–‰ ๊ฒฝ๊ณ„AG-UI Event
Text MessageTEXT_MESSAGE_START / CONTENT / ENDStart-Content-EndAG-UI Messages
Tool CallTOOL_CALL_START / ARGS / END / RESULTStart-Content-End + ResultAG-UI Tools
StateSTATE_SNAPSHOT / DELTASnapshot-DeltaAG-UI State Management
ActivityACTIVITY_SNAPSHOT / DELTASnapshot-DeltaAG-UI Event
ReasoningREASONING_*Nested Start-EndAG-UI Event

๋ถ€๊ฐ€ โ€” ํƒ€์ž… ๋„๊ตฌ 3์ข…

export type AGUIEventByType = { [EventType.TEXT_MESSAGE_CONTENT]: TextMessageContentEvent; ... };
export type AGUIEventOf<T extends EventType> = AGUIEventByType[T];
export type EventPayloadOf<T extends EventType> = Omit<AGUIEventOf<T>, keyof BaseEventFields>;
  • AGUIEventByType: type โ†’ event ๋ฃฉ์—…
  • AGUIEventOf<T>: ํŠน์ • ํƒ€์ž… ํ•˜๋‚˜๋งŒ ๊บผ๋‚ด๊ธฐ (์˜ˆ: AGUIEventOf<EventType.RUN_STARTED>)
  • EventPayloadOf<T>: BaseEvent ๊ณตํ†ต ํ•„๋“œ ์ œ์™ธ โ€” factory ์ž…๋ ฅ์— ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ๋…ธ์ถœ

schema-first์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ํŒŒ์ƒ. ํ•ต์‹ฌ์€ ์•„๋‹ˆ์ง€๋งŒ ์ฝœ๋Ÿฌ์ธก ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ๋ฅผ ์ค„์—ฌ์ค€๋‹ค.

์„ค๊ณ„ ํšŒ๊ณ  โ€” ๋‹ค๋ฅธ ์„ ํƒ์ง€์™€ ๋น„๊ต

์„ ํƒ์ง€์žฅ์ ๋‹จ์ 
TypeScript union๋งŒ (Zod ์—†์ด)์˜์กด์„ฑ 0wire์—์„œ ๋ฐ›์€ ๊ฐ’ ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ ๋ถˆ๊ฐ€
JSON Schemaํ‘œ์ค€ / ๋‹ค์–ธ์–ดTS ํƒ€์ž… ๋„์ถœ์— ๋ณ„๋„ ๋„๊ตฌ ํ•„์š”
Protobuf๋งŒ๋‹ค์–ธ์–ด + ๊ฒ€์ฆ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋ฌด๊ฑฐ์›€, schema ์ง„ํ™” ์ œ์•ฝ
Zod discriminated unionTS 1๊ธ‰, ํƒ€์ž…+๊ฒ€์ฆ ํ†ตํ•ฉTS ์™ธ ์–ธ์–ด๋Š” ๋ณ„๋„ ์ •์˜ ํ•„์š”

AG-UI๋Š” TS๋Š” Zod, Python์€ Pydantic, wire๋Š” SSE+Protobuf โ€” ๊ฐ ๊ณ„์ธต์ด ๊ฐ™์€ ๋ชจ์–‘์„ ๋‹ค๋ฅธ ๋„๊ตฌ๋กœ ํ‘œํ˜„. โ€œschema๊ฐ€ ์ง„์‹ค ๊ณต๊ธ‰์›โ€์ด๋ผ๋Š” ์›์น™์ด ์ผ๊ด€๋œ๋‹ค. ์ด wire ๋ณ€ํ™˜์˜ ์‹ค์ œ ๋™์ž‘์€ AG-UI Serialization์—์„œ ๋‹ค๋ฃฌ๋‹ค.

๋ธ”๋กœ๊ทธ ๊ธ€๋กœ ์˜ฎ๊ธธ ๋•Œ ํ•ต์‹ฌ ํ๋ฆ„

  1. Hook โ€” 33๊ฐœ ์ด๋ฒคํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ๋‹ค๋ฃฐ๊นŒ?
  2. ์˜ˆ์ธก โ€” ๊ทธ๋ƒฅ union type์œผ๋กœ ์ถฉ๋ถ„ํ• ๊นŒ? โ†’ ๋Ÿฐํƒ€์ž„ ๊ฒ€์ฆ ๋ˆ„๋ฝ, ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ•„๋“œ ๋‹ค๋ฆ„
  3. ์ฝ”๋“œ ํ™•์ธ โ€” Zod discriminated union + z.infer ํŒจํ„ด
  4. ํšŒ๊ณ  โ€” ์™œ Zod์ธ๊ฐ€: ํƒ€์ž…๊ณผ ๊ฒ€์ฆ์„ ํ•œ ์ •์˜์— ๋ฌถ๊ธฐ
  5. ๋–ก๋ฐฅ โ€” ๊ทธ๋Ÿฐ๋ฐ SSE wire format์—์„œ ์–ด๋–ป๊ฒŒ ์ง๋ ฌํ™”ํ• ๊นŒ? โ†’ 4ํŽธ AG-UI Serialization

์ฐธ๊ณ  ๋ฌธ์„œ