• AbstractAgentλŠ” AG-UI ν”„λ‘œν† μ½œμ—μ„œ μ—μ΄μ „νŠΈ 역할을 μˆ˜ν–‰ν•˜λŠ” λͺ¨λ“  μ°Έμ—¬μžκ°€ κ΅¬ν˜„ν•΄μ•Ό ν•˜λŠ” 좔상 베이슀 클래슀
  • run(input) β†’ Observable<BaseEvent> 단 ν•˜λ‚˜μ˜ 좔상 λ©”μ„œλ“œλ§Œ κ΅¬ν˜„ν•˜λ©΄ AG-UI μ°Έμ—¬μžκ°€ λ˜λŠ” μ΅œμ†Œ 계약(contract)
  • κ΅¬ν˜„ μœ„μΉ˜μ— 따라 역할이 λ‹¬λΌμ§€λŠ” μ–‘λ©΄ 좔상화: λ°±μ—”λ“œμ—μ„œ κ΅¬ν˜„ν•˜λ©΄ AI μ‹€ν–‰ μ„œλ²„, ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ κ΅¬ν˜„ν•˜λ©΄ 이벀트 μˆ˜μ‹  ν΄λΌμ΄μ–ΈνŠΈ

ν•΄λ‹Ή κ°œλ…μ΄ ν•„μš”ν•œ 이유

  • μ„œλ²„Β·ν΄λΌμ΄μ–ΈνŠΈκ°€ λ™μΌν•œ μΈν„°νŽ˜μ΄μŠ€λ₯Ό μ‚¬μš©ν•΄ transport 방식(HTTP, WebSocket λ“±)κ³Ό λ¬΄κ΄€ν•˜κ²Œ μ—°κ²° κ°€λŠ₯
  • run() ν•˜λ‚˜λ§Œ κ΅¬ν˜„ν•˜λ©΄ μƒνƒœ κ΄€λ¦¬Β·μ΄λ²€νŠΈ κ²€μ¦Β·λ―Έλ“€μ›¨μ–΄Β·κ΅¬λ…μž μ•Œλ¦Όμ΄ μžλ™μœΌλ‘œ 처리됨
  • λ°±μ—”λ“œ μ—μ΄μ „νŠΈλ₯Ό ꡐ체해도 ν”„λ‘ νŠΈμ—”λ“œ μ½”λ“œ 변경이 λΆˆν•„μš”ν•œ 벀더 독립성 확보

AS-IS: 직접 κ΅¬ν˜„ μ‹œ

sequenceDiagram
    autonumber
    participant F as Frontend
    participant B as Backend Agent

    F->>B: HTTP POST (raw fetch)
    B-->>F: SSE raw bytes
    Note over F: bytes νŒŒμ‹±<br/>μƒνƒœ 관리<br/>이벀트 검증<br/>λͺ¨λ‘ 직접 κ΅¬ν˜„

TO-BE: AbstractAgent μ‚¬μš© μ‹œ

sequenceDiagram
    autonumber
    participant F as Frontend
    participant HA as "HttpAgent (AbstractAgent)"
    participant B as "Backend (AbstractAgent)"

    F->>HA: runAgent(input)
    HA->>B: HTTP POST + RunAgentInput
    B-->>HA: SSE raw bytes
    HA-->>HA: bytes β†’ Observable[BaseEvent] λ³€ν™˜
    Note over HA: transformChunks<br/>verifyEvents<br/>apply (state/messages μžλ™ κ°±μ‹ )<br/>subscribers μ•Œλ¦Ό
    HA-->>F: subscribers 콜백

κ΅¬ν˜„ μœ„μΉ˜μ— λ”°λ₯Έ μ—­ν•  차이

같은 좔상 ν΄λž˜μŠ€μ§€λ§Œ μ–΄λ””μ„œ κ΅¬ν˜„ν•˜λŠλƒμ— 따라 역할이 μ™„μ „νžˆ 달라진닀.

// λ°±μ—”λ“œ β€” LLM 호좜 ν›„ 이벀트 직접 생성
class BackendAgent extends AbstractAgent {
  run(input: RunAgentInput): Observable<BaseEvent> {
    return new Observable(subscriber => {
      const stream = await llm.stream(input.messages)
      subscriber.next({ type: EventType.RUN_STARTED, ... })
      for await (const chunk of stream) {
        subscriber.next({ type: EventType.TEXT_MESSAGE_CONTENT, delta: chunk.text })
      }
      subscriber.next({ type: EventType.RUN_FINISHED, ... })
      subscriber.complete()
    })
  }
}
 
// ν”„λ‘ νŠΈμ—”λ“œ β€” μ„œλ²„ SSEλ₯Ό μˆ˜μ‹ ν•΄ Observable둜 λ³€ν™˜ (핡심 μˆ˜λ„μ½”λ“œ)
class FrontendAgent extends AbstractAgent {
  run(input: RunAgentInput): Observable<BaseEvent> {
    return new Observable(subscriber => {
      fetch(url, { method: "POST", body: JSON.stringify(input) })
        .then(res => res.body.getReader())
        .then(async reader => {
          while (true) {
            const { done, value } = await reader.read()  // Uint8Array 청크
            if (done) break
            const event = parseSSE(value)                // JSON.parse(...)
            subscriber.next(event)                       // re-emit
          }
          subscriber.complete()
        })
    })
  }
}
// β†’ HttpAgentκ°€ FrontendAgentλ₯Ό 이미 κ΅¬ν˜„ν•΄ 제곡
const agent = new HttpAgent({ url: "https://my-backend/agent" })
κ΅¬λΆ„λ°±μ—”λ“œ κ΅¬ν˜„ν”„λ‘ νŠΈμ—”λ“œ κ΅¬ν˜„
run()의 μ—­ν• LLM 호좜 β†’ 이벀트 생성HTTP bytes β†’ Observable λ³€ν™˜
이벀트 λ°©ν–₯emit (μ°½μ‘°)re-emit (λ³€ν™˜Β·μ „λ‹¬)
κΈ°λ³Έ 제곡 κ΅¬ν˜„μ²΄μ—†μŒ (직접 κ΅¬ν˜„)HttpAgent

μ‹€μ œ AI와 ν†΅μ‹ ν•˜λ €λ©΄ μ„œλ²„λŠ” λ°˜λ“œμ‹œ μ‹€ν–‰ 쀑이어야 ν•œλ‹€

run() μ•ˆμ—μ„œ 무엇을 ν˜ΈμΆœν•˜λ“ , μ‹€μ œ AI μ²˜λ¦¬κ°€ μΌμ–΄λ‚˜λŠ” μ„œλ²„κ°€ λ–  μžˆμ–΄μ•Ό ν•œλ‹€.

Ollama 둜컬 μ„œλ²„ 연동:  run() β†’ fetch("http://localhost:11434") β†’ Ollama μ„œλ²„ (ν•„μˆ˜ μ‹€ν–‰)
μ™ΈλΆ€ LLM API 연동:      run() β†’ fetch("https://api.openai.com") β†’ OpenAI μ„œλ²„ (μ™ΈλΆ€, μƒμ‹œ μ‹€ν–‰)

단, μ„œλ²„ 없이 λ™μž‘ν•˜λŠ” κ²½μš°λ„ μžˆλ‹€ β€” ν…ŒμŠ€νŠΈμš© MockAgent:

// ν…ŒμŠ€νŠΈ λͺ©μ μœΌλ‘œλ§Œ μ‚¬μš©. μ‹€μ œ AI와 ν†΅μ‹ ν•˜μ§€ μ•ŠμŒ
class MockAgent extends AbstractAgent {
  run(input: RunAgentInput): Observable<BaseEvent> {
    return of([κ³ μ •λœ μ΄λ²€νŠΈλ“€])
  }
}
// CopilotKit의 selfManagedAgents prop이 이 νŒ¨ν„΄μ„ 지원
<CopilotKitProvider selfManagedAgents={{ mock: new MockAgent() }}>

run()이 λ°˜ν™˜ν•˜λŠ” Observable의 의미 β€” ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ run()을 κ΅¬ν˜„ν•˜λŠ” 이유

μ™œ 이 질문이 μƒκΈ°λŠ”κ°€

μ„œλ²„κ°€ 이미 이벀트λ₯Ό λ§Œλ“€μ–΄ μ „λ‹¬ν•˜λ‹ˆ ν΄λΌμ΄μ–ΈνŠΈλŠ” κ·Έλƒ₯ μ˜΅μ €λΉ™λ§Œ ν•˜λ©΄ 될 것 κ°™λ‹€λŠ” 직관이 μžˆλ‹€. κ·Έλ ‡λ‹€λ©΄ ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ run()을 κ΅¬ν˜„ν•  ν•„μš”κ°€ μžˆλŠ”κ°€?

닡은 β€œλ¬΄μ—‡μ„ μ˜΅μ €λΉ™ν•˜λŠλƒβ€ 에 μžˆλ‹€. μ„œλ²„μ˜ Observable<BaseEvent>λŠ” λ„€νŠΈμ›Œν¬λ₯Ό ν†΅κ³Όν•˜λŠ” μˆœκ°„ μ†Œλ©Έλœλ‹€. HTTPλŠ” λ°”μ΄νŠΈλ§Œ 전달할 수 있기 λ•Œλ¬Έμ΄λ‹€. ν”„λ‘ νŠΈμ—”λ“œ run()은 이벀트λ₯Ό μ°½μ‘°ν•˜λŠ” 것이 μ•„λ‹ˆλΌ μ†Œλ©Έλœ Observable을 raw bytesλ‘œλΆ€ν„° λ³΅μ›ν•˜λŠ” 역할이닀.

sequenceDiagram
    autonumber
    participant B as "BackendAgent.run()"
    participant NET as "Network (HTTP)"
    participant C as "FrontendAgent.run()"
    participant PP as "runAgent() Pipeline"

    Note over B: Observable[BaseEvent] 생성
    B->>B: subscriber.next(TEXT_MESSAGE_CONTENT, delta:μ•ˆλ…•)

    Note over B,NET: Observable은 λ„€νŠΈμ›Œν¬ 전솑 λΆˆκ°€<br/>직렬화 ν•„μˆ˜
    B->>NET: SSE data: type=TEXT_MESSAGE_CONTENT, delta=μ•ˆλ…•

    Note over NET: raw bytes (Uint8Array)<br/>Observable μ†Œλ©Έ

    NET->>C: Uint8Array 청크 μˆ˜μ‹ 
    Note over C: bytes β†’ Observable 볡원 ν•„μš”<br/>ν”„λ‘ νŠΈμ—”λ“œ run()의 μ—­ν• 
    C->>C: TextDecoder β†’ SSE ν…μŠ€νŠΈ
    C->>C: SSE νŒŒμ‹± β†’ JSON.parse()
    C->>C: subscriber.next(event) re-emit
    C->>PP: Observable[BaseEvent] 전달
    Note over PP: transformChunks β†’ verifyEvents<br/>β†’ apply β†’ subscribers

runAgent() 호좜 흐름 β€” μ‹€μ œ λ©”μ„œλ“œ μ‹€ν–‰ μˆœμ„œ

runAgent()의 호좜 μ£Όμ²΄λŠ” **Application(ν”„λ‘ νŠΈμ—”λ“œ UI)**이닀. 호좜 이후 λ‚΄λΆ€ νŒŒμ΄ν”„λΌμΈμ΄ μžλ™μœΌλ‘œ μ‹€ν–‰λœλ‹€.

sequenceDiagram
    autonumber
    box Frontend
        participant App as "Application (UI)"
        participant AA as "AbstractAgent.runAgent()"
        participant HA as "HttpAgent.run()"
        participant TR as "transformHttpEventStream()"
        participant PP as Pipeline
        participant SUB as Subscribers
    end
    box Backend
        participant B as "BackendAgent.run()"
    end

    App->>AA: agent.runAgent(parameters)
    AA->>AA: prepareRunAgentInput()
    AA->>HA: this.run(input)
    HA->>B: HTTP POST + RunAgentInput (JSON body)
    B->>B: LLM 호좜 β†’ BaseEvent emit
    B-->>HA: SSE raw bytes
    HA->>TR: transformHttpEventStream()
    TR->>TR: SSE νŒŒμ‹± β†’ JSON.parse()
    TR-->>AA: Observable[BaseEvent]
    AA->>PP: transformChunks β†’ verifyEvents β†’ apply
    PP-->>SUB: onMessagesChanged / onStateChanged
    SUB-->>App: UI μ—…λ°μ΄νŠΈ

RunAgentInput β€” λˆ„κ°€ μ–΄λ–€ 값을 λ„£λŠ”κ°€

Frontend 관점 β€” AbstractAgentκ°€ μžλ™μœΌλ‘œ 쑰립

prepareRunAgentInput()이 runAgent() 호좜 μ‹œ μžλ™μœΌλ‘œ κ΅¬μ„±ν•œλ‹€. κ°œλ°œμžκ°€ 직접 μ±„μšΈ ν•„μš”κ°€ μ—†λ‹€.

// agent.ts - prepareRunAgentInput()
{
  threadId:      this.threadId,               // AbstractAgentκ°€ 생성·관리 (uuid)
  runId:         uuidv4(),                    // λ§€ μ‹€ν–‰λ§ˆλ‹€ μƒˆλ‘œ 생성
  messages:      this.messages,               // λˆ„μ λœ λŒ€ν™” 이λ ₯ (μžλ™ 관리)
  state:         this.state,                  // ν˜„μž¬ 곡유 μƒνƒœ (μžλ™ 관리)
  tools:         parameters?.tools,           // runAgent() 호좜 μ‹œ κ°œλ°œμžκ°€ 전달
  context:       parameters?.context,         // runAgent() 호좜 μ‹œ κ°œλ°œμžκ°€ 전달
  forwardedProps: parameters?.forwardedProps, // μΆ”κ°€ props μ „λ‹¬μš©
}

κ°œλ°œμžκ°€ 직접 λ„£λŠ” 값은 tools, context, forwardedProps뿐이닀:

agent.runAgent({
  tools: [{ name: "search", description: "...", parameters: schema }],
  context: [{ description: "User timezone", value: "Asia/Seoul" }],
})
// threadId, runId, messages, state λŠ” AbstractAgentκ°€ μžλ™ 처리

Backend 관점 β€” HTTP POST body둜 μˆ˜μ‹ , run()의 νŒŒλΌλ―Έν„°λ‘œ 전달

BackendλŠ” RunAgentInput을 λ§Œλ“€μ§€ μ•Šκ³  λ°›λŠ”λ‹€. Frontendκ°€ μ‘°λ¦½ν•œ 값을 κ·ΈλŒ€λ‘œ ν™œμš©ν•œλ‹€.

class BackendAgent extends AbstractAgent {
  run(input: RunAgentInput): Observable<BaseEvent> {
    // input.messages β†’ LLM context (이전 λŒ€ν™” 이λ ₯)
    // input.tools    β†’ LLM에 λ„˜κΈΈ tool λͺ©λ‘
    // input.state    β†’ ν˜„μž¬ 곡유 μƒνƒœ μ°Έμ‘°
    // input.threadId β†’ μ„Έμ…˜ μΆ”μ μš©
    const response = await llm.chat({
      messages: input.messages,
      tools: input.tools,
    })
    ...
  }
}
ν•„λ“œμƒμ„± 주체Frontend μ—­ν• Backend μ—­ν• 
threadIdAbstractAgent (μžλ™)μ„Έμ…˜ μ‹λ³„λ‘œκ·ΈΒ·μΆ”μ μš©
runIdAbstractAgent (μžλ™)μ‹€ν–‰ μ‹λ³„λ‘œκ·ΈΒ·μΆ”μ μš©
messagesAbstractAgent (μžλ™ λˆ„μ )λŒ€ν™” 이λ ₯ 전솑LLM context둜 μ‚¬μš©
stateAbstractAgent (μžλ™ 관리)ν˜„μž¬ μƒνƒœ μ „μ†‘μƒνƒœ μ°Έμ‘°Β·λΆ„κΈ°
tools개발자 β†’ runAgent()tool λͺ©λ‘ 전달LLM에 tool 등둝
context개발자 β†’ runAgent()λΆ€κ°€ 정보 μ „λ‹¬ν”„λ‘¬ν”„νŠΈ 보강

Subscribers β€” μƒνƒœ λ³€ν™” ꡬ독

raw μ΄λ²€νŠΈκ°€ μ•„λ‹Œ 처리 ν›„ κ²°κ³Όλ₯Ό λ°›λŠ” 콜백 μ‹œμŠ€ν…œμ΄λ‹€.

agent.subscribe({
  onMessagesChanged:  ({ messages }) => { /* μƒˆ λ©”μ‹œμ§€ μˆ˜μ‹  μ‹œ */ },
  onStateChanged:     ({ state })    => { /* μƒνƒœ λ³€κ²½ μ‹œ */ },
  onRunFinishedEvent: ({ result })   => { /* μ‹€ν–‰ μ™„λ£Œ μ‹œ */ },
  onRunFailed:        ({ error })    => { /* μ—λŸ¬ λ°œμƒ μ‹œ */ },
})

raw BaseEventλ₯Ό 직접 보렀면 run(input).subscribe(event => ...) μ‚¬μš©.

μ°Έκ³  λ¬Έμ„œ