本文为社区中文翻译(依据 MIT 协议),阅读英文原文 →。译文可能滞后于上游更新。

会话文件格式

文件位置

~/.pi/agent/sessions/--<路径>--/<时间戳>_<会话id>.jsonl

默认 <会话id> 是 UUID;调用方可通过 SDK 或 --session-id 提供自定义 ID。<路径> 会去掉开头的路径分隔符,并把 /\: 替换为 -

删除会话

直接删除 ~/.pi/agent/sessions/ 下对应的 .jsonl 文件即可。

Pi 也支持在 /resume 中交互删除(选中会话按 Ctrl+D 后确认)。可用时 Pi 会用 trash CLI,避免永久删除。

会话版本

会话头部带版本字段:

已有会话在加载时自动迁移到当前版本(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 编码
  mimeType: string;  // 如 "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 毫秒
}

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;      // 工具专属元数据
  usage?: Usage;      // 工具内部执行的嵌套 LLM 工作
  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 持久化助手消息前会替换为完成原因,因此会话 JSONL 中不应出现 "pending""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
  timestamp: number;
}

interface CustomMessage {
  role: "custom";
  customType: string;            // 扩展标识
  content: string | (TextContent | ImageContent)[];
  display: boolean;              // 是否在 TUI 中显示
  details?: any;                 // 扩展专属元数据
  timestamp: number;
}

interface BranchSummaryMessage {
  role: "branchSummary";
  summary: string;
  fromId: string | null;         // 被摘要路径的原叶子节点
  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 位十六进制 ID;可能退化为完整 UUID
  parentId: string | null;  // 父条目 ID(根条目为 null)
  timestamp: string;    // ISO 时间戳
}

条目类型

SessionHeader

文件第一行。只有元数据,不属于树(没有 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"}

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 用压缩摘要替换更早的被摘要条目,并保留从该条目开始的范围。

可选字段:

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 是被摘要路径的原叶子节点。

可选字段:

CustomEntry

扩展状态持久化。进入 LLM 上下文。

{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}

/reload 时用 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}

字段:

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 可以创建多个根:

[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← 当前叶子
                                                            │
                                                            └─ [branch_summary] ─── [user msg] ← 另一条分支

上下文构建

buildContextEntries() 从当前叶子走到根,产出活动条目列表并处理压缩:

  1. 收集路径上的全部条目
  2. 路径上存在一个或多个 CompactionEntry 时,取最新一个:
    • 先包含该压缩条目
    • 包含从 firstKeptEntryId 到(不含)压缩条目之间的条目
    • 包含压缩条目之后的条目
  3. 所选范围内的非消息条目会保留,供交互模式渲染

buildSessionContext() 在该条目列表之上构建发给 LLM 的消息列表:

  1. 从完整路径提取当前模型与思考等级设置
  2. 把选中的条目转换为消息:
    • message -> 存储的 AgentMessage
    • compaction -> compactionSummary
    • branch_summary -> branchSummary
    • custom_message -> CustomMessage
    • custom -> 无上下文消息

压缩摘要替换 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

以编程方式操作会话的关键方法。

静态创建方法

静态列举方法

实例方法 —— 会话管理

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

实例方法 —— 树导航

实例方法 —— 上下文与信息