
01 · THE QUESTION
Notebook에서는 되는데 웹 화면에서 왜 자꾸 깨질까
Notebook의 agent.run()은 함수가 끝난 뒤 완성된 객체 하나를 보면 됩니다. 사용자용 서비스는 수 초 동안 진행되는 실행을 message, tool call, approval, state update, error, cancellation 같은 여러 사건으로 보여줘야 합니다.
02 · LAYERS
Agent service의 네 레이어를 분리합니다
| 레이어 | 책임 | 대표 실패 |
|---|---|---|
| Agent runtime | Model, tool, graph, state 실행 | Framework-native object가 수시로 변함 |
| Application adapter | Runtime event를 product contract로 변환 | Message와 tool id가 유실됨 |
| Transport | HTTP, SSE, WebSocket으로 순서 있게 전달 | Reconnect, buffering, proxy timeout |
| Frontend reducer | Event를 화면 state로 축적 | Delta 중복, out-of-order, partial state |
Framework object를 API response로 그대로 노출하면 backend library update가 frontend breaking change가 됩니다. Adapter는 작고 명시적으로 두고, product가 소유하는 event contract만 외부로 보냅니다.
03 · TWO CONTRACTS
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" }
}04 · STREAMING
Streaming은 빠른 타이핑 효과가 아닙니다
| Event category | UI state |
|---|---|
| run.started / finished / error | 전체 실행 lifecycle과 retry |
| message.started / delta / completed | 어느 message에 text를 붙일지 |
| tool.started / args / result | Tool card, progress, result summary |
| state.snapshot / delta | 공유 application state 동기화 |
| approval.requested / resolved | 중단과 사용자 결정 |
| activity / progress | Plan, search, long-running task 상태 |
Text stream과 SSE는 같은 말이 아닙니다
Text delta는 application event이고, SSE는 event를 운반하는 HTTP transport입니다. SSE를 사용해도 message id, sequence, reconnect cursor, heartbeat, error semantics가 없으면 상태를 안전하게 복구할 수 없습니다.
05 · REDUCER
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 cursorText delta를 도착 즉시 DOM에 붙이는 방식은 reconnect와 retry에서 중복을 만듭니다. Canonical client state를 먼저 만들고 UI는 그 state의 결과로 렌더링해야 합니다. Full snapshot으로 재동기화하는 경로도 필요합니다.
Error도 contract입니다
사용자에게 보여 줄 message와 retry 가능 여부, 실패한 stage, 이미 발생한 side effect를 구분합니다. Transport disconnect와 run failure를 같은 오류로 다루면, 실제로는 계속 실행 중인 작업을 다시 시작해 중복 행동을 만들 수 있습니다.
06 · CONTRACT TEST
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가 합의하는지 봅니다.