Pi의 Session 파일은 표면상 매우 단순합니다. 하나의 JSONL 파일이며, 각 줄이 하나의 JSON 레코드입니다.
세션을 messages[] 형태로 직접 저장하지도, branch마다 별도의 데이터베이스 테이블을 만들지도 않습니다. 현재 버전의 Session Entry는 id / parentId로 트리를 이루며, 파일은 계속 append 순서로 늘어납니다. 현재 leaf가 현재 이력 경로를 결정합니다.[1]
따라서 동일한 Session에는 다음 두 구조가 동시에 존재합니다.
物理结构
append-only JSONL
逻辑结构
entry tree
이 두 구조를 분리한 것이 Pi Session 설계를 이해하는 열쇠입니다.
먼저 파일 자체부터 살펴봅시다
Session 파일의 첫 줄은 Header입니다.
그다음부터 Entry가 트리 구조를 시작합니다. 기본 필드는 대략 다음과 같습니다.
예를 들어 일반적인 대화 한 번은 다음과 같이 기록될 수 있습니다.
디스크에서 보면 여전히 순서대로 append된 것에 불과합니다.
A
B
C
D
하지만 parentId가 이 레코드에 논리적 관계를 부여합니다.
A
└── B
└── C
└── D
사용자가 나중에 B로 돌아와 그곳에서 계속 입력하면, 새 Entry는 계속 파일 끝에 append됩니다.
line 6 user message id=E parent=B
line 7 assistant message id=F parent=E
물리적 순서는 다음과 같이 바뀝니다.
A B C D E F
논리적 구조는 다음과 같이 바뀝니다.
A
└── B
├── C
│ └── D
│
└── E
└── F ← current leaf
파일을 다시 작성할 필요도, C와 D를 복사할 필요도 없습니다. branch를 만들려면 새 Entry가 기존 노드를 가리키기만 하면 됩니다.
이런 방식은 로컬 Coding Agent에 잘 맞는 영속 모델입니다. append가 단순하고, 이력을 직접 확인할 수 있으며, 사용자가 같은 Session에서 이전 위치로 돌아가 계속 작업할 수도 있습니다.
현재 Session은 leaf가 결정한다
트리 자체에는 여러 branch가 있지만, 모델이 한 번에 모든 branch를 현재 이력으로 취급할 수는 없습니다.
그래서 SessionManager에는 ‘현재 위치’도 필요합니다. Pi 문서는 이를 current leaf라고 부릅니다. 현재 path를 구성할 때는 leaf에서 parentId를 따라 root까지 올라간 뒤 순서를 뒤집으면 됩니다.[1]
개념적인 코드는 다음과 같습니다.
앞의 트리에서 leaf가 F라면 다음과 같습니다.
A
└── B
├── C
│ └── D
└── E
└── F
현재 path는 다음과 같습니다.
A → B → E → F
C와 D는 여전히 Session 파일에 남아 있지만, 현재 branch의 모델 이력에는 포함되지 않습니다.
이는 Session File과 Model Context가 애초부터 같은 객체가 아니라는 뜻입니다.
Session File
A B C D E F
Current Branch
A B E F
Model Context
由 A B E F 再经过 Compaction / Message conversion 构造
이후의 많은 기능은 이 세 층의 차이에서 출발합니다.
/tree의 본질은 현재 위치를 이동하는 것
Pi는 Session Tree에서 탐색할 수 있습니다. 사용자가 더 앞선 Entry를 선택하면 그 지점에서 다시 작업을 계속할 수 있습니다.
데이터 구조 관점에서 보면 이 동작에는 기존 Entry를 수정할 필요가 없습니다. SessionManager는 이후 append되는 Entry의 parent만 목표 노드로 전환하면 됩니다.
예를 들어 기존 path가 다음과 같다고 합시다.
A → B → C → D
↑
original path
B로 돌아온 뒤 계속 진행하면 다음과 같습니다.
A → B → C → D
\
E → F
이 설계는 Git의 commit graph와 어느 정도 similarities가 있습니다. 노드가 parent reference를 저장하고, current leaf는 현재 작업 위치와 비슷합니다. 하지만 Pi의 Entry 의미론, Context 구성, Branch Summary는 모두 Agent 세션에 특화된 것이므로 Git의 모델 전체를 그대로 적용할 수는 없습니다.
더 중요한 점은 트리 구조 덕분에 ‘기존 이력 보존’과 ‘현재 Context 변경’을 동시에 실현할 수 있다는 것입니다.
시스템이 일반 배열을 사용한다면 다음과 같습니다.
사용자가 이전 노드로 돌아오면 뒤따르는 이력이 바로 삭제됩니다.
Session Tree는 기존 branch를 보존합니다. 새로운 입력은 그저 다른 경로를 하나 추가할 뿐입니다.
Coding Agent에서는 이 점이 특히 유용합니다. 사용자가 방안 A를 시도해 보았다가 적합하지 않다고 판단하면 이전 상태로 돌아가 방안 B를 시도할 수 있습니다. 모델의 이전 분석과 실행 기록은 그대로 보존하면서, 현재 Context는 B branch만 따라가도록 할 수 있습니다.
Branch Summary가 branch를 벗어난 뒤 발생하는 정보 손실을 해결한다
branch를 완전히 전환하면 문제가 하나 생깁니다. 기존 branch에 이미 valuable한 정보가 생성되어 있을 수 있기 때문입니다.
예를 들어 다음과 같습니다.
A → B → C → D
D 中发现:
某个接口不能修改,因为另一个模块依赖它
사용자가 B로 돌아와 B에서 E를 만들면 현재 path는 다음과 같이 바뀝니다.
A → B → E
D는 더 이상 현재 path에 속하지 않으므로, 그 안의 valuable한 정보도 이후 Context에 자연스럽게 들어오지 않습니다.
그래서 Pi는 BranchSummaryEntry를 지원합니다. 트리 탐색 과정에서 떠난 branch의 정보를 보존해야 할 때 요약을 생성하고, 이를 새 path 위의 Entry로 추가할 수 있습니다.[1]
구조는 대략 다음과 같습니다.
A
└── B
├── C
│ └── D
│
└── BranchSummary
└── E
Branch Summary가 전달하는 의미는 다음과 같습니다.
旧分支虽然不再是当前 path,
其中一部分有价值的上下文被摘要带入新分支。
이는 D를 Context에 직접 남기는 것과는 다릅니다. 새 branch는 기존 branch의 전체 모델 트래젝터리를 상속하지 않고, 압축된 필수 정보만 이어받습니다.
여기서 Pi Session Tree의 중요한 특징을 볼 수 있습니다. 트리는 이력 선택을 결정하고, Summary는 path 사이에서 필요한 정보를 전달합니다.
Compaction 역시 Entry를 통해 Context를 바꾼다
긴 대화에서는 앞부분의 이력을 압축해야 합니다. Pi는 원본 Entry를 삭제하지 않고 CompactionEntry를 append합니다.[2]
현재 구조는 다음을 포함합니다.
현재 branch에 다음과 같은 항목이 있다고 가정해 봅시다.
A B C D E F G H
Context가 너무 길어지면 시스템은 다음과 같은 Compaction을 생성할 수 있습니다.
summary(A...E)
firstKeptEntryId = F
Session에는 여전히 다음 항목이 저장됩니다.
A B C D E F G H Compaction
하지만 buildSessionContext()가 모델 이력을 구성할 때는 다음 항목을 사용합니다.
Summary(A...E)
F
G
H
이것도 동일한 설계가 다시 드러납니다. 영속적인 이력은 온전하게 유지되고, 모델에 노출되는 뷰는 압축될 수 있습니다.
Pi의 Compaction 문서는 절단 지점이 Tool Result에 임의로 놓여서는 안 된다고 별도로 규정합니다. Tool Result는 대응하는 Tool Call과 구조적 완전성을 유지해야 하기 때문입니다.[2]
즉, Context Builder는 Token 개수만 고려할 수 없고 Provider Message의 구조적 제약도 만족해야 합니다.
예를 들어 다음 이력을 잘못 자르면 안 됩니다.
Assistant(tool_call)
--- cut ---
ToolResult
모델 프로토콜은 일반적으로 Tool Call과 Tool Result가 올바르게 paired되어야 하기 때문입니다.
따라서 Compaction은 실제로 두 가지 문제를 동시에 처리합니다.
Token budget
+
Message protocol invariants
buildSessionContext()가야말로 진정한 복구 진입점이다
Session 파일을 다시 연 뒤 런타임이 단순히 다음 코드를 실행하지는 않습니다.
Pi의 SessionManager.buildSessionContext()는 현재 leaf에서 path를 구성한 뒤, 그 path에 있는 Entry를 해석합니다.[1]
문서가 제시한 절차는 다음과 같습니다.
- current leaf부터 root까지의 path를 수집하고;
- path 위의 model / thinking level 등의 설정을 복원하고;
- Compaction이 존재하면 Summary와 firstKeptEntryId를 사용해 메시지를 재구성하고;
- Branch Summary, Custom Message 등의 Entry를 대응되는 메시지로 변환하고;
- 현재 모델 세션에 필요한 Message History를 형성합니다.
SDK가 Agent Session을 생성할 때도 먼저 sessionManager.buildSessionContext()를 호출해 기존 세션이 있는지 확인하고, 그 결과에 따라 기존 메시지와 모델 관련 상태를 복원합니다.[3]
따라서 복구 흐름은 다음과 더 가깝습니다.
여기서 SessionManager는 뚜렷한 Projection 역할을 맡습니다. 디스크의 Entry Tree가 영속적인 사실이라면, Agent Runtime에 필요한 것은 현재 branch에 대응하는 running state입니다.
하나의 Session 파일이 트리 전체를 저장할 수 있는 이유
많은 시스템은 Branch를 새로운 Session으로 직접 모델링합니다.
Session A
↓ fork
Session B
Pi는 같은 파일 안에 Tree를 유지하며, 그背后에는 분명한 제품적 이유가 있습니다.
Coding Agent 사용자는 종종 다음 기능을 필요로 합니다.
查看历史节点
回到某个节点
尝试另一个方案
再回到原分支
이 동작마다 새 Session을 만들면 방대한 수의 세션 파일이 생기고, 사용자가 여러 Session identity를 관리해야 합니다.
Tree 모델은 이러한 경로들을 같은 작업 이력의 서로 다른 갈래로 취급합니다.
Session
└── Tree
├── branch A
├── branch B
└── branch C
사용자가 실제로 어떤 branch를 독립시켜야 할 때에는 fork / branched session을 사용해 새 Session 파일을 만듭니다. Pi의 Session Header에도 이러한 Session 간 관계를 기록하는 parentSession 필드가 있습니다.[1]
따라서 여기에는 두 층의 branch가 있습니다.
同一个 Session 内
Entry Tree Branch
跨 Session
Fork / parentSession
둘을 구분하면 ‘이력 탐색’과 ‘새로운 장기 작업 단위 생성’을 같은 종류의 작업으로 혼동하지 않을 수 있습니다.
Tree 모델의 대가
Session Tree는 매우 가볍지만, 몇 가지 제약도 더합니다.
첫째, 모든 Context 구성은 먼저 현재 path를 확정해야 합니다. ‘파일의 마지막 N개 메시지가 곧 현재 이력이다’라고 가정해서는 안 됩니다.
둘째, Entry의 parent 체인은 온전해야 합니다. 손상된 parentId 하나가 현재 branch 복구에 직접 영향을 줍니다.
셋째, Tool Call, Tool Result처럼 프로토콜상 paired되어야 하는 노드는 임의로 Branch Boundary로 삼을 수 없습니다. 트리 구조가 어떤 노드로든 돌아갈 수 있게 해 주지만, 모든 노드가 합법적인 모델 이력의 종점이 될 수 있다는 뜻은 아닙니다.
넷째, branch 간 정보는 명시적으로 처리해야 합니다. Branch Summary는 valuable한 기존 branch 정보가 완전히 사라지는 것을 막기 위해 존재합니다.
다섯째, Session Entry의 의미는 점점 넓어집니다. Message뿐 아니라 Compaction, Branch Summary, 설정 변경 등의 유형이 포함됩니다. SessionManager는 이러한 Entry들이 현재 Context에 어떤 영향을 미치는지 이해해야 합니다.
이는 완전한 Event Sourcing과는 본질적으로 다릅니다. Pi의 Entry는 주로 ‘Coding Agent 세션과 모델 Context를 어떻게 복구할 것인가’를 중심으로 구성되며, 실행 중의 모든 Step, Chunk, Tool lifecycle 이벤트를 기록하려 하지 않습니다.
Pi Session은 ‘분기 가능한 영속 이력’에 더 가깝다
Pi의 Session 모델을 다음 네 층으로 압축할 수 있습니다.
Tree와 Compaction 모두 과거를 직접 다시 쓰지 않습니다.
Branch는 현재 path를 바꾸고, Compaction은 현재 Context를 해석하는 방식을 바꾸지만, 기존 Entry는 그대로 남습니다.
덕분에 Pi는 다음과 같은 몇 가지 실용적인 특성을 갖게 됩니다.
文件格式简单
历史可人工检查
分支不需要复制前缀
Compaction 不破坏原始历史
恢复逻辑集中在 SessionManager
Session의 책임 범위에도 명확한 한계가 생깁니다. Pi Session은 주로 세션의 의미를 재구성할 수 있는 Entry를 저장하며, Runtime 수준의 완전한 실행 궤적 전체가 이 트리에 들어가는 것은 아닙니다.
다음 글에서 소개할 DeepSeek Harness는 다른 경계를 선택했습니다. Turn, Step, Raw Stream Chunk, Tool Call, Tool Result, Request Header를 모두 append-only SessionEvent Log에 넣고, Surface를 통해 모델 이력을 projection합니다. 이렇게 Session은 ‘현재 세션을 어떻게 복구할 것인가’만 담당하는 데서 그치지 않고, Replay, UI, Persistence, 요청 재구성을 위한 통합된 source of truth가 됩니다.
참고 자료
[1] Pi Session File Format: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/session-format.md
[2] Pi Compaction: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/compaction.md
[3] Pi SDK Session Restore: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/src/core/sdk.ts
[4] Pi SDK Session API: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/sdk.md