앞선 두 글에서는 Pi Session Tree와 DeepSeek Harness Session Event Log를 각각 분석했다. 두 모델의 영속화 방식은 서로 다르지만, 모두 같은 원칙을 따랐다. 프로세스가 종료된 뒤에는 새로운 Runtime이 이전 Runtime 객체가 계속 살아 있다고 가정하지 않고, 영속 상태에서 세션을 다시 만들어야 한다.
이번 글에서는 아주 작은 TypeScript 데모로 이 원칙을 검증한다.
이 데모는 실제 LLM을 구현하지 않으며, DSH의 전체 SessionEvent 프로토콜을 복제하지도 않는다. 다음 네 가지 메커니즘만 유지한다.
append-only Event Log
continuous seq
Message Projection
Crash Repair
최종적으로 다음을 검증한다.
이 흐름이 성립하면 Session과 Runtime의 생명주기가 실제로 분리된다.
먼저 최소 이벤트 모델부터 정의한다
데모에는 몇 가지 이벤트만 유지한다.
여기서는 의도적으로 세 가지 정보를 구분했다.
첫째는 모델에 보이는 히스토리다.
user/message
assistant/message
tool/result
둘째는 실행 사실이다.
turn/start
turn/end
tool/call
셋째는 요청 환경이다.
request/header
이 분류는 DSH의 전체 모델과 비슷하지만, 데모에서는 Step, Chunk, Surface Replace, Request Context 같은 고급 기능을 제거했다.
모든 Event에는 연속된 seq가 붙는다.
seq는 단순히 표시하기 위한 필드가 아니다. Replay 순서를 정의하고, 가장 기본적인 무결성 검증도 제공한다.
Durable Append가 메모리 Projection보다 먼저
append()의 구현은 매우 짧다.
여기에는 의도적으로 유지한 쓰기 순서가 있다.
append JSONL
↓ success
push in-memory events
데모는 동기식 파일 쓰기를 사용하므로 이 경계를 비교적 쉽게 표현할 수 있다.
실제 시스템은 보통 비동기 일괄 처리를 사용하며, 이 경우 명시적인 flush / checkpoint 동작이 필요하다. DSH의 Persistence Plugin은 session/event를 백그라운드 buffer에 복사하고, 내구성을 보장하는 체크포인트가 필요할 때 session/flush를 통해 buffer를 비웁니다. [1]
데모가 동기식 쓰기를 사용하는 것은 상태 모델 검증에 집중하기 위해서일 뿐이다. 프로덕션 시스템이 Agent Loop에서 Chunk마다 동기식 디스크 I/O를 수행해야 한다는 뜻은 아니다.
Message는 Event Log의 Projection
Session 안에서 messages.json을 별도로 영속화해 관리하지 않는다.
deriveMessages()는 매번 Event Log에서 Message를 구성한다.
따라서 다음과 같다.
turn/start와 tool/call이 사라진 것은 아니다. 여전히 Session Fact에 속하지만 Model Message Projection에는 들어가지 않을 뿐이다.
이렇게 하면 두 영속 상태 사이의 checkpoint 문제를 피할 수 있다.
session.jsonl authoritative
messages derived
향후 UI에서 Tool Call을 표시해야 한다면 Model Message Projection을 수정하지 않고도 다른 Projection을 추가하면 된다.
Request Header로 Message 외의 Context 정보 보완하기
Message만 복원해서는 다음 모델 요청을 완전히 구성할 수 없다.
그래서 데모에서는 단순화한 request/header를 저장한다.
Context를 복원할 때는 가장 최근 Header를 읽는다.
즉, Model Context 자체는 여전히 영속화되는 객체가 아니다.
Model Context는 두 종류의 Durable State를 조합해 만들어진다.
request/header
+
derived messages
↓
Model Context
실제 시스템에서는 다음 요소가 추가될 수도 있다.
Memory
Compaction
Current Agent Policy
Runtime Injection
하지만 구성 원리는 변하지 않는다.
완료되지 않은 Turn 하나를 시뮬레이션해 보기
Process A가 Session을 만든다.
그다음 정상적으로 이벤트를 추가한다.
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
코드는 의도적으로 다음 이벤트를 기록하지 않는다.
turn/end
Phase 1을 여기서 멈춰 두어 프로세스가 Turn 도중에 종료되는 상황을 시뮬레이션한다.
이때 디스크에 기록된 사실만으로 다음 두 가지를 알 수 있다.
Turn 1 曾经开始
Tool Result 已经完成并落盘
동시에 다음 사실도 분명하게 드러난다.
Turn 1 没有正常结束
이 방식은 run.status = RUNNING만 남은 경우보다 복구에 유용한 정보를 제공한다. 정확히 어떤 사실까지 완료되었는지가 로그에 그대로 남아 있기 때문이다.
open()은 먼저 seq를 검증한다
Process B가 시작되면 JSONL을 읽는다.
assertContiguousSeq()는 다음을 검증한다.
만약 로그가 다음과 같아진다면
0
1
2
4
시스템은 조용히 Replay하지 않는다.
seq=3이 애초에 발생하지 않은 것인지, 이미 발생했지만 유실된 것인지 판단할 수 없기 때문이다.
이러한 엄격한 검증은 Event Log가 사실의 원천이 되기 위한 전제 중 하나이다. DSH도 Session seq가 연속일 것을 요구하며, Surface Fold와 로드 경계에서 이벤트 구조를 검증한다. [2]
Crash Repair가 interrupted를 추가한다
복구할 때 Turn Boundary를 스캔한다.
따라서 다시 열면 로그가 다음 상태로 바뀐다.
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
0 request/header
1 turn/start
2 user/message
3 assistant/message
4 tool/call
5 tool/result
6 turn/end:interrupted
기존 Event는 삭제되지 않았고, completed로 위조되지도 않았다.
새로운 복구 사실은 다음 내용만 알려준다.
当前进程接管时,发现上一 Turn 未闭合;
该 Turn 因中断结束。
이 처리 방식은 DSH의 cold Session에 대한 Crash Repair 전략과 직접 대응한다. [1]
복구된 Message는 여전히 확정된 상태다
turn/end:interrupted는 Message Projection에 포함되지 않는다.
따라서 Process B에서 다음을 실행하면
다음 결과가 그대로 나온다.
User
Check UserService.
Assistant
I will inspect the file first.
Tool Result
class UserService { ... }
이미 영속화된 Tool Result는 Turn이 중단되었더라도 사라지지 않는다.
이 방식은 Event Log의 실용적인 장점 하나를 보여 준다. 실행 무결성과 사실 보존을 서로 분리해 처리할 수 있다.
Turn이 실패하거나 중단되더라도, 그전에 성공적으로 완료된 이벤트는 그대로 남아 있다.
후속 로직은 이벤트 의미에 따라 다음과 같이 판단할 수 있다.
保留 Tool Result
忽略半完成 Assistant Chunk
重新执行未完成 Tool
要求用户确认
自动继续下一 Turn
이 전략들은 Crash 시점에 과거 기록을 수정할 필요가 없다.
새 Runtime은 계속 작업을 이어갈 수 있다
복구가 끝나면 데모는 두 번째 Turn을 이어서 추가한다.
전체 히스토리는 이제 다음과 같다.
Turn 1
start
messages
tool result
interrupted
Turn 2
start
messages
completed
Process B는 Process A의 어떤 메모리 객체도 넘겨받지 않았다.
오직 다음에 의존한다.
Session Event Log
이것이 Session과 Runtime을 분리한 이후의 가장 기본적인 복구 기능이다.
이 데모에 아직 부족한 것
현재 구현은 상태 모델을 실험하는 수준에 그칠 뿐이며, 프로덕션급 Session과는 아직 상당한 차이가 있다.
최소한 다음 기능이 부족하다.
그중 가장 어려운 문제는 보통 append()가 아니라 외부 부작용에 있다.
예를 들어 로그에 tool/call이 남아 있는데, 프로세스가 그 뒤에 결제나 메일 발송, 데이터베이스 수정을 수행하고 tool/result가 기록되기 전에 크래시했다.
복구 후 Tool을 단순히 다시 실행하면 중복 부작용이 발생할 수 있다.
따라서 Tool 계층에서 추가 설계가 필요하다.
Idempotency Key
Execution Record
External Transaction ID
Reconciliation
따라서 Event Log는 Agent가 알고 있는 사실을 정확히 기록할 수 있지만, 모든 분산 일관성 문제를 자동으로 해결해 주지는 못한다.
중요한 경계가 바로 여기다. Agent Session의 Crash Recovery와 외부 시스템의 Exactly-once Execution은 서로 다른 문제다.
데모에서 얻은 네 가지 설계 결론
이 작은 구현을 통해 네 가지를 확인할 수 있다.
첫째, Session은 Runtime 객체를 저장할 필요가 없다. 영속화된 사실만 충분하다면 새 프로세스에서 Runtime State를 다시 만들 수 있다.
둘째, Message는 Projection으로 사용할 수 있다. 따라서 실행 Event, UI Event, Model Message를 같은 데이터 구조에 억지로 넣을 필요가 없다.
셋째, Model Context도 재구성할 수 있다. Message History뿐 아니라 System Prompt, Tools, 모델 설정 같은 요청 상태에는 분명한 출처가 필요하다.
넷째, Crash Recovery는 이미 영속화된 기록을 변조하기보다 새로운 복구 사실을 표현하는 편이 좋다. 미완료 Turn은 interrupted로 표시하고, 완료된 Tool Result는 그대로 유지한다.
이 모델을 앞선 두 글과 함께 보면 세 가지 복잡도 수준을 확인할 수 있다.
简单 Chat
messages[]
Pi
JSONL Entry Tree
→ current path
→ buildSessionContext()
DSH
SessionEvent Log
→ Surface / Projection
→ Model / UI / Replay / Recovery
시스템은 제품 요구사항에 맞는 최소 복잡도 모델을 선택해야 한다.
채팅 내용만 저장하면 Message로 충분하다. 분기 가능한 장기 Coding Session이 필요하면 Pi의 Tree가 잘 맞는다. Replay, 복구, 다중 Projection, 요청 재구성이 핵심 기능이 된다면 DSH 같은 Event-sourced Session이 더 큰 가치를 발휘한다.
2번 유닛은 여기서 ‘Session은 무엇을 저장해야 하는가?’에서 출발해 두 가지 실제 프레임워크 설계를 거쳐, 실행 가능한 최소 구현으로 돌아오는 완전한 사이클을 완성했다.
다음 유닛에서는 다른 문제로 넘어간다. Tool, Memory, Sandbox, MCP, SubAgent, UI 같은 기능이 Agent 시스템에 계속 들어올 때 Runtime Core이 계속 비대해지지 않도록 하는 방법이다. Pi Extension, Cordis, DeepSeek Harness의 Everything is a Plugin은 모두 이 질문에 답하고 있다.
데모
디렉터리 구조:
demo/
├── package.json
├── README.md
└── src/
├── event-session.ts
└── demo.ts
실행:
cd demo
npm run demo
참고 자료
[1] DeepSeek Harness Persistence: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/persistence.zh.md
[2] DeepSeek Harness Session / Surface: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/session.md ; https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/surface.ts