JINLOOPEST. 2026
浏览文档
PI DOCUMENTATION更新于

会话文件格式

会话以 JSONL(JSON Lines)文件存储。每一行都是一个 JSON 对象,包含一个 type 字段。会话条目通过 id/parentId 字段形成树状结构,从而支持原地分支,无需创建新文件。

文件位置

~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonl

其中 <path> 是工作目录, / 替换为 -.

删除会话

可以通过删除 .jsonl 下的 ~/.pi/agent/sessions/.

文件来移除会话。Pi 还支持从 /resume 交互式删除会话(选择一个会话并按 Ctrl+D,然后确认)。当可用时,pi 使用 trash CLI 来避免永久删除。

会话版本

会话在头部有一个版本字段:

  • 版本 1:线性条目序列(旧版,加载时自动迁移)
  • 版本 2:树状结构,使用 id/parentId 链接
  • 版本 3:将 hookMessage 角色重命名为 custom (扩展统一)

现有会话在加载时会自动迁移到当前版本(v3)。

源文件

GitHub 上的源代码(pi-mono):

对于项目中的 TypeScript 定义,请检查 node_modules/@earendil-works/pi-coding-agent/dist/node_modules/@earendil-works/pi-ai/dist/.

消息类型

会话条目包含 AgentMessage 对象。理解这些类型对于解析会话和编写扩展至关重要。

内容块

消息包含类型化内容块数组:

interface TextContent {
  type: "text";
  text: string;
}
 
interface ImageContent {
  type: "image";
  data: string;      // base64 encoded
  mimeType: string;  // e.g., "image/jpeg", "image/png"
}
 
interface ThinkingContent {
  type: "thinking";
  thinking: string;
}
 
interface ToolCall {
  type: "toolCall";
  id: string;
  name: string;
  arguments: Record<string, any>;
}

基础消息类型(来自 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;
  usage: Usage;
  stopReason: "stop" | "length" | "toolUse" | "error" | "aborted";
  errorMessage?: string;
  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
  isError: boolean;
  timestamp: number;
}
 
interface Usage {
  input: number;
  output: number;
  cacheRead: number;
  cacheWrite: number;
  totalTokens: number;
  cost: {
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
    total: number;
  };
}

导出的 pi-ai StopReason 类型也包含 "pending",但该值保留用于流事件中的部分消息。终端 done/error 消息在 pi 持久化助手消息之前将其替换为完成原因,因此 "pending" 不应出现在会话 JSONL 中。

扩展消息类型(来自 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;                // Entry we branched from
  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;           // 8-char hex ID
  parentId: string | null;  // Parent entry ID (null for first 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, /clonenewSession({ parentSession })):

{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project","parentSession":"/path/to/original/session.jsonl"}

创建的具有父会话的会话

对话中的一条消息。 message 对话中的一条消息。 AgentMessage.

{"type":"message","id":"a1b2c3d4","parentId":"prev1234","timestamp":"2024-12-03T14:00:01.000Z","message":{"role":"user","content":"Hello"}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"Hi!"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}}
{"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}}

字段包含一个

当用户在会话中途切换模型时发出。

{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}

思考层级变更条目

当用户更改思考/推理级别时发出。

{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}

压缩条目

在上下文压缩时创建。存储早期消息的摘要。

{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}

较新的由 harness 生成的压缩直接将保留的压缩后上下文嵌入到条目中,而不是 firstKeptEntryId:

{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","tokensBefore":50000,"retainedTail":[{"role":"user","content":"latest request"},{"role":"assistant","content":[{"type":"text","text":"latest reply"}],"provider":"anthropic","model":"claude-sonnet-4-5","usage":{...},"stopReason":"stop"}]}

可选字段:

  • usage:生成摘要的 LLM 使用情况;计入会话令牌和成本总计
  • retainedTail:物化的 AgentMessage[] 压缩后保留。此字段仅为了向后兼容旧会话而可选。较新的由 harness 生成的压缩包含它,因此我们可以从此检查点重建上下文,而无需遍历压缩条目之前的旧条目。
  • details:特定于实现的数据(例如, { readFiles: string[], modifiedFiles: string[] } 用于默认,或用于扩展的自定义数据)
  • fromHook: true 如果由扩展生成, false/undefined 如果由 pi 生成(旧字段名)
  • firstKeptEntryId:用于兼容旧条目格式。

分支摘要条目

在通过以下方式切换分支时创建 /tree 使用 LLM 生成的左侧分支到公共祖先的摘要。捕获来自放弃路径的上下文。

{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}

可选字段:

  • usage:生成摘要的 LLM 使用情况;计入会话令牌和成本总计
  • details:文件跟踪数据({ readFiles: string[], modifiedFiles: string[] })用于默认,或用于扩展的自定义数据
  • fromHook: true 如果由扩展生成, false/undefined 如果由 pi 生成(旧字段名)

自定义条目

扩展状态持久化。不参与 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 上下文。

自定义消息条目

由扩展注入的消息,参与 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)

标签条目

用户定义的条目书签/标记。

{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}

设置 labelundefined 以清除标签。

会话信息条目

会话元数据(例如,用户定义的显示名称)。通过以下方式设置 /name, --name / -n,或 pi.setSessionName() 在扩展中。

{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}

会话名称显示在会话选择器中(/resume)而不是设置时的第一条消息。

树结构

条目形成一棵树:

  • 第一个条目有 parentId: null
  • 每个后续条目通过以下方式指向其父级 parentId
  • 分支从较早的条目创建新的子级
  • “叶子”是树中的当前位置
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
                                                            │
                                                            └─ [branch_summary] ─── [user msg] ← alternate branch

上下文构建

buildContextEntries() 从当前叶子遍历到根,生成活动条目列表,同时遵守压缩:

  1. 收集路径上的所有条目
  2. 如果路径上有 CompactionEntry 位于路径:
    • 首先包含压缩条目
    • 如果存在 retainedTail ,它充当自包含的检查点,压缩之后的条目也会被包含
    • 否则,包含从 firstKeptEntryId 到压缩条目的条目
    • 然后包含压缩之后的条目
  3. 保留选定范围内的非消息条目,以便交互模式可以渲染它们

buildSessionContext() 在该条目列表的基础上生成 LLM 的消息列表:

  1. 从完整路径中提取当前模型和思考级别设置
  2. 将选定的条目转换为消息:
    • message -> 存储的 AgentMessage
    • compaction -> compactionSummary 加上 retainedTail (如果存在)
    • branch_summary -> branchSummary
    • custom_message -> CustomMessage
    • custom -> 无上下文消息

这使得较新的压缩像自包含的检查点一样工作。 retainedTail 仅是可选的,因此仅存储 firstKeptEntryId 的旧会话也能继续正确加载。

解析示例

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;
  }
}

会话管理器 API

以编程方式处理会话的关键方法。

静态创建方法

  • SessionManager.create(cwd, sessionDir?) - 新建会话
  • SessionManager.open(path, sessionDir?) - 打开现有会话文件
  • SessionManager.continueRecent(cwd, sessionDir?) - 继续最近的会话或创建新会话
  • SessionManager.inMemory(cwd?) - 无文件持久化
  • SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?) - 从另一个项目派生会话

静态列表方法

  • SessionManager.list(cwd, sessionDir?, onProgress?) - 列出目录中的会话
  • SessionManager.listAll(onProgress?) - 列出所有项目中的所有会话

实例方法 - 会话管理

  • newSession(options?) - 开始新会话(选项: { parentSession?: string })
  • setSessionFile(path) - 切换到不同的会话文件
  • createBranchedSession(leafId) - 将分支提取到新的会话文件

实例方法 - 追加(均返回条目 ID)

  • appendMessage(message) - 添加消息
  • appendThinkingLevelChange(level) - 记录思考级别更改
  • appendModelChange(provider, modelId) - 记录模型更改
  • appendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?) - 添加压缩
  • 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?) - 带上下文摘要的分支

实例方法 - 上下文与信息

  • buildContextEntries() - 获取应用压缩后的活动分支条目
  • buildSessionContext() - 获取 LLM 的消息、思考级别和模型
  • getEntries() - 所有条目(不包括头部)
  • getHeader() - 会话头部元数据
  • getSessionName() - 从最新的 session_info 条目获取显示名称
  • getCwd() - 工作目录
  • getSessionDir() - 会话存储目录
  • getSessionId() - 会话 UUID
  • getSessionFile() - 会话文件路径(内存中时为 undefined)
  • isPersisted() - 会话是否保存到磁盘

本文档内容同步自 PI 官方 GitHub 仓库。

查看源文件