Pi의 Session은 주로 ‘분기 가능한 이력을 어떻게 현재 Context로 복원할 것인가’를 중심으로 설계된다. DeepSeek Harness(DSH)는 더 엄격한 영속화 경계를 택했다. Session 자체가 append-only 방식의 typed SessionEvent 로그이며, 모델 메시지, UI, Replay, 복구 상태는 모두 같은 로그에서 파생된다.[1]
공식 문서는 이 로그를 Agent의 전체 인터랙션 기록에 대한 단일 진실의 원천으로 직접 정의한다. 모델 메시지는 별도의 권위 있는 상태로 따로 관리하지 않고, Session Log에서 deriveMessages()로 투영해 얻는다.[1]
이 설계가 먼저 해결하는 것은 여러 상태 사이의 일관성 문제다.
Message와 실행 이력을 각각 저장할 때
흔한 Agent 저장 모델은 다음과 같다:
messages table
runs table
tool_calls table
stream_events table
각 테이블은 모두 타당하지만, 한 번의 실행은 여러 상태를 동시에 변경한다.
예를 들어 모델이 응답을 시작하면:
创建 Run
追加 streaming chunks
生成 Assistant Message
写 Tool Call
写 Tool Result
更新 Run 状态
그중 한 번의 쓰기가 실패하면 ‘어떤 상태가 이미 커밋되었는지’를 처리해야 한다. 복구 로직 역시 어느 테이블을 더 신뢰할지 결정해야 한다.
DSH는 이런 사실들을 먼저 하나의 순서 있는 이벤트 스트림으로 통합한다.
turn/start
step/start
user/message
request/header
assistant/chunk
assistant/chunk
assistant/message
tool/call
tool/result
step/end
turn/end
각 이벤트에는 단조 증가하는 seq가 있다. 정상적인 로그에서는 seq가 연속이어야 하므로, 이벤트마다 Session 안에서 확정된 위치가 생긴다.[1]
이제 복구 문제는 다음과 같이 바뀐다:
读取 SessionEvent[]
↓
按 seq Replay
↓
重新构造需要的 Projection
핵심 상태의 출처는 하나뿐이다.
SessionEvent가 기록하는 범위는 Message보다 넓다
DSH의 이벤트 집합에는 여러 종류의 사실이 담겨 있다.[1]
이 이벤트 집합은 DSH의 Session 정의가 이미 Conversation Transcript를 넘어섰음을 보여준다.
예를 들어 assistant/chunk는 그 자체로 다음 LLM History를 만들지는 않지만, 원본 스트리밍 출력 기록을 보존한다. turn/start와 step/start는 실행 경계를 나타내는 데 쓰이고, request/header는 한 요청에 사용된 모델 구성, System Prompt, Tool schemas를 저장한다.
따라서 같은 Event Log를 여러 소비자가 사용할 수 있다:
이것이 Agent 환경에서 Event Sourcing의 주요 가치다. Tool, Stream, 모델 메시지, 실행 경계는 본질적으로 시간 순서로 이어지는 데이터이기 때문이다.
Surface: Event Log와 모델 History 사이의 중간 계층
모든 SessionEvent를 곧바로 모델 메시지로 바꾸면 분명한 문제가 생긴다.
모델이 볼 필요가 없는 것은 다음과 같다:
turn/start
step/end
assistant/chunk
request/context
그래서 DSH는 전체 Event Log 위에 Surface 계층을 정의한다.
현재 Core는 모델에 노출되는 Surface에 세 종류의 이벤트만 허용한다.[2]
deriveEventMessage()의 논리도 간단하다:
user/message → UserMessage
assistant/message → AssistantMessage
tool/result → ToolResultMessage
其他事件 → null
결과적으로 다음과 같은 구조가 만들어진다:
Full Session Log
↓
Surface Fold
↓
Surface Nodes
↓
deriveEventMessage()
↓
Model Messages
이 계층이 중요한 이유는 Session이 Context보다 더 풍부한 사실을 보존하면서도 Model History의 구조를 안정적으로 유지할 수 있기 때문이다.
surfaceOp가 Compaction의 append-only 성질도 유지한다
일반 메시지가 Surface에 들어올 때는 append를 사용한다:
M1 → M2 → M3 → M4
Compaction에는 특별한 문제가 생긴다. 모델은 이후 과거 이력 일부 대신 Summary를 보아야 하지만, Event Log에서 기존 이벤트를 삭제해서는 안 된다.
DSH의 Surface는 replacement operation을 지원한다.[1][2]
Surface가 현재 다음과 같다고 하자:
10 15 21 30 35
새 Summary Event는 다음과 같이 선언할 수 있다:
replace 10..30
새 Surface는 다음과 같이 된다:
42(summary) 35
하지만 Session Log에는 원래의 10, 15, 21, 30이 그대로 남아 있다.
따라서:
Log
只追加事实
Surface
允许改变模型可见顺序和范围
이는 Pi Compaction의 철학과 비슷하다. 둘 다 원본 이력을 보존하며, 달라지는 것은 이후 모델 Context를 구성하는 방식뿐이다. DSH는 이 원칙을 범용 Surface Operation으로 정식화했다.
sourceEventSeqs는 파생 관계를 저장한다
DSH의 Surface Event는 sourceEventSeqs도 기록할 수 있다.[1]
가장 직관적인 예는 Streaming이다:
assistant/chunk seq=20
assistant/chunk seq=21
assistant/chunk seq=22
↓ assemble
assistant/message seq=23
sourceEventSeqs=[20,21,22]
이렇게 하면 완성된 Message와 원본 Chunk가 동시에 존재하면서도 둘 사이의 출처 관계가 사라지지 않는다.
이 필드는 Surface Replace에서도 중요하다. Summary가 여러 이전 Surface Node를 대체할 때는 어떤 노드를 가렸는지 선언해야 한다. Surface fold는 이 provenance 관계를 검증한다.[2]
이제 Event Log는 ‘시간순으로 JSON을 저장하는 것’뿐 아니라 파생 그래프의 일부도 저장한다:
Raw Events
↓ provenance
Derived Surface Event
Debug, Replay, 감사 목적에서는 최종 Message만 저장하는 것보다 더 신뢰할 수 있다.
왜 Request Header까지 기록해야 하는가
DSH 아키텍처 문서는 매우 엄격한 요구 사항을 세운다. 모델 요청에 사용되는 입력은 로그에서 재구성할 수 있어야 한다.[3]
Message만 저장해서는 충분하지 않다. 실제 LLM Request에는 다음도 포함되기 때문이다:
provider / model
reasoning config
sampling config
System Prompt
Tool schemas
이 정보는 request/header에 저장된다.[1]
현재 EpochHeader는 대략 다음과 같다:
각 Loop 인스턴스가 시작할 때 전체 Header를 기록하고, 설정이 바뀌면 새로운 Snapshot을 계속 append한다. 복구할 때 최신 Header를 읽기만 하면 요청 환경을 재구성할 수 있다.[1]
이렇게 하면 흔히 간과되는 문제가 해결된다:
消息完全相同
+
System Prompt / Tools 不同
=
实际模型语义可能完全不同
시스템이 ‘정확한 Replay’를 약속한다면 Message만 저장해서는 부족하다.
그래서 DSH는 ‘모델에 보이는 상태를 재구성할 수 있다’는 성질을 Session의 런타임 불변식으로 삼는다.[3]
이렇게 하면 로그 크기와 schema 복잡도가 증가하지만, Resume, Fork, Replay의 의미를 더 명확하게 만든다.
Persistence의 역할은 동일한 로그를 영속화하는 것뿐이다
Event Sourcing은 다른 형태의 복잡화도 쉽게 생기게 한다. 메모리에는 한 세트의 Event가 있고, 데이터베이스에는 별도의 Persistence Event가 정의되기 때문이다.
DSH는 현재 이를 의도적으로 피한다. Persistence seam은 기존 SessionEvent를 그대로 영속화하며, 평행한 이벤트 모델은 두지 않는다.[4]
런타임에서 Event를 append하면 session/event 알림이 동기적으로 발행되고, Persistence Plugin은 이벤트를 per-session buffer에 복사한 뒤 batch로 백엔드에 기록한다. Event를 생성하는 Agent Loop은 매번 디스크 쓰기를 기다릴 필요가 없다.[4]
대략 다음 흐름이다:
Session.append(event)
↓
In-memory log committed
↓
session/event
↓
Persistence buffer
↓ batch
JSONL / SQLite
영속화 경계를 확정해야 할 때는 session/flush로 버퍼를 비운다.
여기서는 두 개념을 명확히 나눈다:
Session
定义事实和顺序
Persistence
决定这些事实如何耐久化
따라서 JSONL과 SQLite를 교체 가능한 Backend로 둘 수 있으며, 상위 Event Model은 바뀌지 않아도 된다.[4]
Crash Recovery는 중단된 Turn을 삭제하지 않는다
Agent 프로세스가 Turn 도중에 crash할 수 있다.
디스크에는 이미 다음이 있을 수 있다:
turn/start
step/start
user/message
assistant/message
tool/call
하지만 다음은 없다:
tool/result
step/end
turn/end
한 가지 복구 방법은 마지막 미완료 Turn을 잘라내는 것이다. DSH는 현재 이미 영속화된 사실을 보존하고, 콜드 복구 시 synthetic 이벤트를 append한다:
turn/end {
reason: interrupted
}
이 이벤트는 종료 이벤트가 짝지어지지 않은 Turn을 닫는 데 쓰인다.[4]
따라서 복구된 로그도 다음 사실을 명확히 보여준다:
这个 Turn 曾经发生
其中一部分工作已经完成
进程在结束前被中断
이 방식은 Turn 전체를 삭제하는 것보다 append-only 사실 모델에 더 잘 맞는다.
특히 장시간 실행되는 작업에서 중요하다. Turn 하나에는 여러 Step과 대량의 Tool Result가 포함될 수 있으며, 마지막 crash 하나로 앞에서 이미 생성된 사실이 전부 사라져서는 안 된다.
Session Header를 Event Log 바깥에 두는 이유
DSH도 모든 데이터를 Event로 만들지는 않는다.
Session의 저장 메타데이터는 별도의 SessionHeader에 보관한다. 예를 들면 다음과 같다.[4]
version
id
createdAt
cwd
parentSession
seedLength
origin
delegationDepth
agentPreset
이 필드들은 Session 자체와 저장 계보를 설명하며, 상호작용 타임라인에는 속하지 않는다.
예:
parentSession
这份 Session 从哪个 Session fork
seedLength
前多少条 Event 属于继承前缀
agentPreset
恢复这个 Session 应使用哪种 Agent 组合
여기에는 유지할 가치가 있는 경계가 있다. Event Log는 ‘세션 안에서 무슨 일이 있었는지’를 기록하고, Header는 ‘이 세션 기록이 무엇인지’를 저장한다.
Event Sourcing은 모든 Metadata를 이벤트로 변환하도록 요구하지 않는다.
DSH의 Fork는 Session Lineage에 더 가깝다
Pi는 하나의 Session 파일 안에서 트리를 구성할 수 있다.
DSH의 Session Log 자체는 선형으로 유지된다:
seq 0
seq 1
seq 2
...
Fork할 때 새 Session을 만들고 안정적인 prefix를 Seed로 삼는다. Header는 parentSession과 seedLength로 계보를 기록한다.[4]
다음처럼 나타낼 수 있다:
Session A
0 1 2 3 4 5
│
└──── fork at prefix
↓
Session B
0 1 2 3 | 4' 5' 6'
따라서 Pi와 DSH는 Branch를 처리하는 두 가지 대표적인 모델을 보여준다:
Pi
一个 Session 内保存 Entry Tree
DSH
每个 Session 保持线性 Event Log
分支形成 Session Lineage
전자는 하나의 Coding Session 안에서 이력을 자주 탐색하기에 적합하다. 후자는 Event seq의 선형성과 Replay 모델을 단순하게 유지하지만, 그 대신 Fork가 새로운 Session Identity를 만든다.
어느 쪽이 더 나은지는 제품의 실제 동작을 고려하지 않고는 판단하기 어렵다.
Event Log의 비용
DSH의 설계는 강한 재구성 능력을 얻는 대신 뚜렷한 비용도 안고 있다.
첫째, 이벤트 수가 많다. Streaming Chunk도 로그에 들어가므로, 한 번의 응답이 대량의 Event를 생성할 수 있다.
둘째, Event Schema는 영속 프로토콜이다. 한 번 공개되면 type을 변경할 때 기존 로그를 고려해야 한다. 현재 Persistence는 형식 버전과 알 수 없는 required event를 엄격히 검증해, 재구성 semantics에 영향을 미치는 이벤트를 조용히 무시하지 않도록 한다.[4]
셋째, 모든 Projection은 결정적이어야 한다. Surface, Message, UI, Transcript에 서로 다른 해석 규칙을 적용하면 일관성 문제가 다시 생긴다.
넷째, Event Log는 Compaction이나 Surface Replace 같은 메커니즘으로 모델 Context를 제어해야 하지만, 원본 로그는 여전히 오랫동안 쌓일 수 있다.
다섯째, 개발자는 다음을 구분해야 한다:
事实 Event
实时 Runtime Event
模型 Surface Event
UI-only Projection
DSH의 아키텍처 문서도 Session Event, Agent Event, capability event를 명확히 분리한다. reload를 거쳐 보존해야 하는 사실은 Session Event에 들어가며, 실행 중 이벤트를 가로채는 작업에는 agent/*를 사용한다.[3]
이 경계는 매우 중요하다. 모든 callback을 영속 Event로 기록하면 로그가 저수준 디버깅 Trace가 되고, 모든 실행 사실을 메모리 callback에만 담으면 복구할 수 없게 된다.
이런 설계를 언제 도입할 가치가 있는가
단순한 Chat Bot은 보통 turn/start, Raw Chunk, Request Header를 저장할 필요가 없다.
Event Log의 이점은 다음 요구 사항이 동시에 나타날 때 분명해진다:
시스템에 필요한 것이 다음뿐이라면:
展示聊天历史
把最近消息发送给模型
messages[] + metadata는 여전히 비용이 더 낮은 설계다.
DSH가 제공하는 가치는 ‘Event Sourcing 자체’가 아니라, Agent에서 서로 갈라지기 쉬운 여러 상태 소스를 하나로 통일하는 데 있다:
SessionEvent Log
↓
Surface
↓
Model Messages
SessionEvent Log
↓
UI / Replay / Transcript
SessionEvent Log
↓
Persistence / Recovery
전체 시스템은 하나의 사실 스트림을 중심으로 동작한다.
다음 편에서는 이 모델을 실행 가능한 Demo로 축소해 보겠다. Demo는 DSH의 모든 이벤트를 복제하지 않고 Pi의 Tree도 구현하지 않는다. 대신 핵심 네 가지 메커니즘만 유지한다. append-only Event, 연속적인 seq, Message Projection, Crash Repair. 목표는 다음 질문 하나를 검증하는 것이다. Event Log만 저장했을 때, 프로세스를 다시 시작한 뒤 Conversation과 다음 Model Context를 결정론적으로 복구할 수 있는가?
참고 자료
[1] DeepSeek Harness Session: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/session.md
[2] DSH Session Surface: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/surface.ts
[3] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.zh.md
[4] DeepSeek Harness Persistence: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/persistence.zh.md
[5] DSH Session implementation: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/index.ts