Tech Blog로 돌아가기

AI AGENT STUDY · FRONTEND

14

Notebook agent를 사용자용 서비스로 옮길 때 필요한 contract

Notebook의 한 번짜리 실행을 message·tool·state·error event로 나누고, frontend가 잃지 않아야 할 streaming contract를 설계합니다.

Backend event stream이 adapter를 거쳐 message, tool, progress, error UI로 조립되는 구조
Agentic UI는 텍스트를 빠르게 보여주는 화면이 아니라, 실행 상태를 잃지 않는 event contract 위에서 동작합니다.원본 크기로 보기

Notebook에서는 되는데 웹 화면에서 왜 자꾸 깨질까

Notebook의 agent.run()은 함수가 끝난 뒤 완성된 객체 하나를 보면 됩니다. 사용자용 서비스는 수 초 동안 진행되는 실행을 message, tool call, approval, state update, error, cancellation 같은 여러 사건으로 보여줘야 합니다.

Agent service의 네 레이어를 분리합니다

레이어책임대표 실패
Agent runtimeModel, tool, graph, state 실행Framework-native object가 수시로 변함
Application adapterRuntime event를 product contract로 변환Message와 tool id가 유실됨
TransportHTTP, SSE, WebSocket으로 순서 있게 전달Reconnect, buffering, proxy timeout
Frontend reducerEvent를 화면 state로 축적Delta 중복, out-of-order, partial state

Framework object를 API response로 그대로 노출하면 backend library update가 frontend breaking change가 됩니다. Adapter는 작고 명시적으로 두고, product가 소유하는 event contract만 외부로 보냅니다.

Request contract와 event contract를 섞지 않습니다

MINIMUM REQUEST

{
  "threadId": "thread_42",
  "runId": "run_105",
  "messages": [{ "id": "msg_u1", "role": "user", "content": "..." }],
  "context": { "locale": "ko-KR" }
}

Request는 사용자가 무엇을 시작하려는지 설명합니다. Stream event는 실행 중 무엇이 일어났는지 설명합니다. 같은 message라는 단어를 쓰더라도 request의 content와 streaming delta, 저장된 canonical message는 서로 다른 schema가 될 수 있습니다.

EVENT ENVELOPE

{
  "eventId": "evt_031",
  "runId": "run_105",
  "sequence": 31,
  "type": "tool.result",
  "timestamp": "2026-07-14T09:00:00Z",
  "payload": { "toolCallId": "tool_7", "status": "succeeded" }
}

Streaming은 빠른 타이핑 효과가 아닙니다

Event categoryUI state
run.started / finished / error전체 실행 lifecycle과 retry
message.started / delta / completed어느 message에 text를 붙일지
tool.started / args / resultTool card, progress, result summary
state.snapshot / delta공유 application state 동기화
approval.requested / resolved중단과 사용자 결정
activity / progressPlan, search, long-running task 상태

Text stream과 SSE는 같은 말이 아닙니다

Text delta는 application event이고, SSE는 event를 운반하는 HTTP transport입니다. SSE를 사용해도 message id, sequence, reconnect cursor, heartbeat, error semantics가 없으면 상태를 안전하게 복구할 수 없습니다.

Frontend는 event를 reducer로 바꾼 결과를 그립니다

CLIENT REDUCER

event → validate envelope
      → deduplicate by eventId
      → order by sequence
      → reduce into run/messages/tools/state
      → render derived UI
      → persist resume cursor

Text delta를 도착 즉시 DOM에 붙이는 방식은 reconnect와 retry에서 중복을 만듭니다. Canonical client state를 먼저 만들고 UI는 그 state의 결과로 렌더링해야 합니다. Full snapshot으로 재동기화하는 경로도 필요합니다.

Error도 contract입니다

사용자에게 보여 줄 message와 retry 가능 여부, 실패한 stage, 이미 발생한 side effect를 구분합니다. Transport disconnect와 run failure를 같은 오류로 다루면, 실제로는 계속 실행 중인 작업을 다시 시작해 중복 행동을 만들 수 있습니다.

Model 없이 fake stream으로 먼저 검증합니다

  • Text delta가 여러 chunk와 reconnect 후에도 한 message로 합쳐지는지 확인합니다.
  • 두 tool call이 교차해 도착해도 각각의 argument와 result가 섞이지 않는지 봅니다.
  • Out-of-order와 duplicate event를 reducer가 안전하게 처리하는지 검사합니다.
  • Stop 요청이 server cancellation과 tool abort까지 전달되는지 확인합니다.
  • Retry가 새 run인지 같은 run의 resume인지 UI와 backend가 합의하는지 봅니다.