19 会话文件格式会话 JSONL 文件的存储结构:条目类型、消息类型、树形结构与解析示例。
会话以 JSONL(JSON Lines)文件的形式存储。每一行都是一个带有 type 字段的 JSON 对象。会话条目通过 id/parentId 字段构成树形结构,从而无需创建新文件即可就地分叉。
文件位置
~/.pi/agent/sessions/--<path>--/<timestamp>_<session-id>.jsonl默认情况下,<session-id> 是一个 UUID。调用者可以通过 SDK 或 --session-id 提供自定义 ID。对于 <path>,Pi 会去掉开头的路径分隔符,并将 /、\\ 和 : 替换为 -。
删除会话 删除 ~/.pi/agent/sessions/ 下对应的 .jsonl 文件即可移除会话。
Pi 还支持在 /resume 中交互式删除会话(选中会话后按 Ctrl+D,然后确认)。在可用时,pi 会使用 trash CLI 以避免永久删除。
会话版本 会话在头部包含一个版本字段:
版本 1 :线性条目序列(旧版,加载时自动迁移)版本 2 :通过 id/parentId 连接的树形结构版本 3 :将 hookMessage 角色重命名为 custom(扩展统一)已有会话在加载时会自动迁移到当前版本(v3)。
源码文件 GitHub 上的源码(pi ):
如需项目中的 TypeScript 定义,请查看 node_modules/@earendil-works/pi-coding-agent/dist/ 和 node_modules/@earendil-works/pi-ai/dist/。
消息类型 会话条目中包含 AgentMessage 对象。理解这些类型对于解析会话和编写扩展至关重要。
内容块 消息包含类型化内容块的数组:
interface TextContent {
type : "text" ;
text : string ;
textSignature? : string ;
}
interface ImageContent {
type : "image" ;
data : string ; // base64 encoded
mimeType : string ; // e.g., "image/jpeg", "image/png"
}
interface ThinkingContent {
type : "thinking" ;
thinking : string ;
thinkingSignature? : string ;
redacted? : boolean ;
}
interface ToolCall {
type : "toolCall" ;
id : string ;
name : string ;
arguments : Record < string , any >;
thoughtSignature? : string ;
namespace ?: string ;
} 基础消息类型(来自 pi-ai)
interface UserMessage {
role : "user" ;
content : string | ( TextContent | ImageContent )[];
timestamp : number ; // Unix ms
}
interface AssistantMessage {
role : "assistant" ;
content : ( TextContent | ThinkingContent | ToolCall )[];
api : string ;
provider : string ;
model : string ;
responseModel? : string ;
responseId? : string ;
providerThinkingLevel? : string ;
diagnostics? : AssistantMessageDiagnostic [];
usage : Usage ;
stopReason : "pending" | "stop" | "length" | "toolUse" | "error" | "aborted" | "deferred" ;
deferred? : DeferredHandle ;
errorMessage? : string ;
rawStopReason? : string ;
endTurn? : boolean ;
timestamp : number ;
}
interface ToolResultMessage {
role : "toolResult" ;
toolCallId : string ;
toolName : string ;
content : ( TextContent | ImageContent )[];
details? : any ; // Tool-specific metadata
usage? : Usage ; // Nested LLM work performed by the tool
addedToolNames? : string [];
isError : boolean ;
timestamp : number ;
}
interface Usage {
input : number ;
output : number ;
cacheRead : number ;
cacheWrite : number ;
cacheWrite1h? : number ;
reasoning? : number ;
totalTokens : number ;
cost : {
input : number ;
output : number ;
cacheRead : number ;
cacheWrite : number ;
total : number ;
};
} "pending" 保留给流式事件中的部分消息。在 Pi 持久化助手消息之前,终止事件会将其替换为完成原因,因此 "pending" 不应出现在会话 JSONL 中。"deferred" 是一种终止原因,表示提供商响应将在稍后完成;其 deferred 句柄包含取回该响应所需的提供商数据。
扩展消息类型(来自 pi-coding-agent)
interface BashExecutionMessage {
role : "bashExecution" ;
command : string ;
output : string ;
exitCode : number | undefined ;
cancelled : boolean ;
truncated : boolean ;
fullOutputPath? : string ;
excludeFromContext? : boolean ; // true for !! prefix commands
timestamp : number ;
}
interface CustomMessage {
role : "custom" ;
customType : string ; // Extension identifier
content : string | ( TextContent | ImageContent )[];
display : boolean ; // Show in TUI
details? : any ; // Extension-specific metadata
timestamp : number ;
}
interface BranchSummaryMessage {
role : "branchSummary" ;
summary : string ;
fromId : string | null ; // Previous leaf whose abandoned path was summarized
timestamp : number ;
}
interface CompactionSummaryMessage {
role : "compactionSummary" ;
summary : string ;
tokensBefore : number ;
timestamp : number ;
} AgentMessage 联合类型
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage ; 条目基础 所有条目(SessionHeader 除外)都扩展自 SessionEntryBase:
interface SessionEntryBase {
type : string ;
id : string ; // Usually an 8-char hex ID; may fall back to a full UUID
parentId : string | null ; // Parent entry ID (null for a root entry)
timestamp : string ; // ISO timestamp
} 条目类型 文件的第一行。仅包含元数据,不属于树的一部分(没有 id/parentId)。
{ "type" : "session" , "version" : 3 , "id" : "uuid" , "timestamp" : "2024-12-03T14:00:00.000Z" , "cwd" : "/path/to/project" } 对于有父会话的会话(通过 /fork、/clone 或 newSession({ parentSession }) 创建):
{ "type" : "session" , "version" : 3 , "id" : "uuid" , "timestamp" : "2024-12-03T14:00:00.000Z" , "cwd" : "/path/to/project" , "parentSession" : "/path/to/original/session.jsonl" } SessionMessageEntry 对话中的一条消息。message 字段包含一个 AgentMessage。
{ "type" : "message" , "id" : "a1b2c3d4" , "parentId" : "prev1234" , "timestamp" : "2024-12-03T14:00:01.000Z" , "message" :{ "role" : "user" , "content" : "Hello" , "timestamp" : 1733234401000 }}
{ "type" : "message" , "id" : "b2c3d4e5" , "parentId" : "a1b2c3d4" , "timestamp" : "2024-12-03T14:00:02.000Z" , "message" :{ "role" : "assistant" , "content" :[{ "type" : "text" , "text" : "Hi!" }], "api" : "anthropic-messages" , "provider" : "anthropic" , "model" : "claude-sonnet-4-5" , "usage" :{ ... }, "stopReason" : "stop" , "timestamp" : 1733234402000 }}
{ "type" : "message" , "id" : "c3d4e5f6" , "parentId" : "b2c3d4e5" , "timestamp" : "2024-12-03T14:00:03.000Z" , "message" :{ "role" : "toolResult" , "toolCallId" : "call_123" , "toolName" : "bash" , "content" :[{ "type" : "text" , "text" : "output" }], "isError" : false , "timestamp" : 1733234403000 }} ModelChangeEntry 在用户于会话中途切换模型时发出。
{ "type" : "model_change" , "id" : "d4e5f6g7" , "parentId" : "c3d4e5f6" , "timestamp" : "2024-12-03T14:05:00.000Z" , "provider" : "openai" , "modelId" : "gpt-4o" } ThinkingLevelChangeEntry 在用户更改思考/推理级别时发出。
{ "type" : "thinking_level_change" , "id" : "e5f6g7h8" , "parentId" : "d4e5f6g7" , "timestamp" : "2024-12-03T14:06:00.000Z" , "thinkingLevel" : "high" } CompactionEntry 在上下文被压缩时创建。存储较早消息的摘要。
{ "type" : "compaction" , "id" : "f6g7h8i9" , "parentId" : "e5f6g7h8" , "timestamp" : "2024-12-03T14:10:00.000Z" , "summary" : "User discussed X, Y, Z..." , "firstKeptEntryId" : "c3d4e5f6" , "tokensBefore" : 50000 } firstKeptEntryId 是必需的。它标识压缩条目之前被保留的第一条条目。重建上下文时,Pi 会用压缩摘要替换较早的被摘要条目,并保留从该条目开始的范围。
可选字段:
usage:生成摘要的 LLM 用量;计入会话的 token 与费用总计details:实现专属数据(默认为 { readFiles: string[], modifiedFiles: string[] },扩展可使用自定义数据)fromHook:由扩展生成时为 true,由 pi 生成时为 false/undefined(旧版字段名)BranchSummaryEntry 在通过 /tree 切换分支时创建,附带由 LLM 生成的、从被离开分支到共同祖先的摘要。捕获被放弃路径中的上下文。
{ "type" : "branch_summary" , "id" : "g7h8i9j0" , "parentId" : "a1b2c3d4" , "timestamp" : "2024-12-03T14:15:00.000Z" , "fromId" : "f6g7h8i9" , "summary" : "Branch explored approach A..." } parentId 是新分支延续自的条目。fromId 是被放弃路径被摘要的前一个叶子。
可选字段:
usage:生成摘要的 LLM 用量;计入会话的 token 与费用总计details:文件跟踪数据(默认为 { readFiles: string[], modifiedFiles: string[] }),扩展可使用自定义数据fromHook:由扩展生成时为 true,由 pi 生成时为 false/undefined(旧版字段名)CustomEntry 扩展状态持久化。不参与 LLM 上下文。
{ "type" : "custom" , "id" : "h8i9j0k1" , "parentId" : "g7h8i9j0" , "timestamp" : "2024-12-03T14:20:00.000Z" , "customType" : "my-extension" , "data" :{ "count" : 42 }} 使用 customType 在重新加载时识别你扩展的条目。交互模式可以通过 pi.registerEntryRenderer(customType, renderer) 渲染自定义条目,但它们仍然不参与 LLM 上下文。
CustomMessageEntry 扩展注入的、会参与 LLM 上下文的消息。
{ "type" : "custom_message" , "id" : "i9j0k1l2" , "parentId" : "h8i9j0k1" , "timestamp" : "2024-12-03T14:25:00.000Z" , "customType" : "my-extension" , "content" : "Injected context..." , "display" : true } 字段:
content:字符串或 (TextContent | ImageContent)[](与 UserMessage 相同)display:true = 以独特样式显示在 TUI 中,false = 隐藏details:可选的扩展专属元数据(不发送给 LLM)LabelEntry 条目上的用户定义书签/标记。
{ "type" : "label" , "id" : "j0k1l2m3" , "parentId" : "i9j0k1l2" , "timestamp" : "2024-12-03T14:30:00.000Z" , "targetId" : "a1b2c3d4" , "label" : "checkpoint-1" } 将 label 设为 undefined 即可清除标签。
SessionInfoEntry 会话元数据(例如用户定义的显示名称)。通过 /name、--name / -n 或扩展中的 pi.setSessionName() 设置。
{ "type" : "session_info" , "id" : "k1l2m3n4" , "parentId" : "j0k1l2m3" , "timestamp" : "2024-12-03T14:35:00.000Z" , "name" : "Refactor auth module" } 设置后,会话名称会在会话选择器(/resume)中显示,取代第一条消息。
树形结构 条目通常构成一棵树,但导航 API 可以创建多个根:
根条目的 parentId 为 null;第一条条目最初就是根 每个非根条目通过 parentId 指向其父条目 分叉会从更早的条目创建新的子条目 “叶子"是树中的当前位置 调用 resetLeaf() 或 branchWithSummary(null, ...) 可以让后续条目成为另一个根
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branch上下文构建 buildContextEntries() 从当前叶子走到根,生成活动条目列表,同时处理压缩:
收集路径上的所有条目 如果路径上有一个或多个 CompactionEntry,使用最新的那一个:首先包含该压缩条目 包含从 firstKeptEntryId 到(但不包含)压缩条目之间的条目 包含压缩条目之后的条目 在所选范围内保留非消息条目,以便交互模式渲染它们 buildSessionContext() 在该条目列表的基础上生成给 LLM 的消息列表:
从完整路径中提取当前模型和思考级别设置 将选中的条目转换为消息:message -> 存储的 AgentMessagecompaction -> compactionSummarybranch_summary -> branchSummarycustom_message -> CustomMessagecustom -> 不生成上下文消息 压缩摘要会替换 firstKeptEntryId 之前的条目。保留的条目以及压缩之后的所有条目仍对 LLM 可用。
解析示例
import { readFileSync } from "fs" ;
const lines = readFileSync ( "session.jsonl" , "utf8" ). trim (). split ( "\n" );
for ( const line of lines ) {
const entry = JSON . parse ( line );
switch ( entry . type ) {
case "session" :
console . log ( `Session v ${ entry . version ?? 1 } : ${ entry . id } ` );
break ;
case "message" :
console . log ( `[ ${ entry . id } ] ${ entry . message . role } : ${ JSON . stringify ( entry . message . content ) } ` );
break ;
case "compaction" :
console . log ( `[ ${ entry . id } ] Compaction: ${ entry . tokensBefore } tokens summarized` );
break ;
case "branch_summary" :
console . log ( `[ ${ entry . id } ] Branch from ${ entry . fromId } ` );
break ;
case "custom" :
console . log ( `[ ${ entry . id } ] Custom ( ${ entry . customType } ): ${ JSON . stringify ( entry . data ) } ` );
break ;
case "custom_message" :
console . log ( `[ ${ entry . id } ] Extension message ( ${ entry . customType } ): ${ entry . content } ` );
break ;
case "label" :
console . log ( `[ ${ entry . id } ] Label " ${ entry . label } " on ${ entry . targetId } ` );
break ;
case "model_change" :
console . log ( `[ ${ entry . id } ] Model: ${ entry . provider } / ${ entry . modelId } ` );
break ;
case "thinking_level_change" :
console . log ( `[ ${ entry . id } ] Thinking: ${ entry . thinkingLevel } ` );
break ;
}
} SessionManager API 以编程方式使用会话的关键方法。
静态创建方法 SessionManager.create(cwd, sessionDir?, options?) - 新建会话;options 可设置 id 和 parentSessionSessionManager.open(path, sessionDir?, cwdOverride?) - 打开已有的会话文件SessionManager.continueRecent(cwd, sessionDir?) - 继续最近的会话或新建SessionManager.inMemory(cwd?, options?, entries?) - 不持久化到文件,可选择从条目初始化SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?, options?) - 从另一个项目分叉会话静态列表方法 SessionManager.list(cwd, sessionDir?, onProgress?) - 列出某个目录的会话SessionManager.listAll(onProgress?) - 列出所有项目的所有会话SessionManager.listAll(sessionDir?, onProgress?) - 列出自定义会话根目录下的会话实例方法 - 会话管理 newSession(options?) - 开始新会话(options:{ id?: string, parentSession?: string })setSessionFile(path) - 切换到另一个会话文件createBranchedSession(leafId) - 将分支提取到新的会话文件实例方法 - 追加(均返回条目 ID) appendMessage(message) - 添加消息appendThinkingLevelChange(level) - 记录思考级别变化appendModelChange(provider, modelId) - 记录模型变化appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?, usage?) - 添加压缩appendCustomEntry(customType, data?) - 扩展状态(不进入上下文)appendSessionInfo(name) - 设置会话显示名称appendCustomMessageEntry(customType, content, display, details?) - 扩展消息(进入上下文)appendLabelChange(targetId, label) - 设置/清除标签实例方法 - 树形导航 getLeafId() - 当前位置getLeafEntry() - 获取当前叶子条目getEntry(id) - 按 ID 获取条目getBranch(fromId?) - 从条目走到根getTree() - 获取完整树形结构getChildren(parentId) - 获取直接子条目getLabel(id) - 获取条目标签branch(entryId) - 将叶子移到更早的条目resetLeaf() - 将叶子重置为 null(所有条目之前)branchWithSummary(entryId, summary, details?, fromHook?, usage?) - 带上下文摘要的分叉;entryId 可为 null 以从根分叉实例方法 - 上下文与信息 buildContextEntries() - 获取应用了压缩的活动分支条目buildSessionContext() - 获取给 LLM 的消息、thinkingLevel 和模型getEntries() - 所有条目(不含头部)getHeader() - 会话头部元数据getSessionName() - 从最新的 session_info 条目获取显示名称getCwd() - 工作目录getSessionDir() - 会话存储目录getSessionId() - 会话 UUIDgetSessionFile() - 会话文件路径(内存会话为 undefined)isPersisted() - 会话是否已保存到磁盘