Pi 的配置、擴充、平台設定和 API 參考。

工作階段檔案格式

工作階段儲存為 JSONL(JSON 行)檔案。每行都是一個帶有 type 欄位的 JSON 物件。工作階段條目透過 id/parentId 欄位形成樹結構,無需建立新檔案即可實作就地分支。

檔案位置

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

其中 <path> 是工作目錄,並將 / 替換為 -

刪除工作階段

可以透過刪除 ~/.pi/agent/sessions/ 下的 .jsonl 檔案來刪除工作階段。

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",但該值只為串流事件中的部分訊息保留。在 pi 持久化助理訊息之前,終端機的 done/error 訊息會將其替換為完成原因,因此 "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
}

條目類型

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

工作階段訊息條目

對話中的一條訊息。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"}

ThinkingLevelChangeEntry

當使用者改變 thinking/reasoning level 時發出。

{"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}

較新的 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 usage;包含在工作階段 token 和成本總計中
  • retainedTail:壓縮後保留的實體化 AgentMessage[]。該欄位僅為相容舊工作階段而選用。較新的 harness 產生的壓縮會包含它,因此可以從該檢查點重建上下文,而無需遍歷壓縮條目之前的舊條目。
  • details:特定於實作的資料(例如預設實作使用 { readFiles: string[], modifiedFiles: string[] },擴充也可以使用自訂資料)
  • fromHook:如果由擴充產生則為 true;如果由 pi 產生則為 false/undefined(舊欄位名)
  • 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 usage;包含在工作階段 token 和成本總計中
  • details:預設檔案追蹤資料 ({ readFiles: string[], modifiedFiles: string[] }),或擴充的自訂資料
  • fromHook:如果由擴充產生則為 true;如果由 pi 產生則為 false/undefined(舊欄位名)

自訂條目

擴充狀態持久化。不參與 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"}

label 設定為 undefined 以清除標籤。

工作階段資訊條目

工作階段元資料(例如,使用者定義的顯示名稱)。透過擴充中的 /name--name / -npi.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. 從完整路徑中提取目前模型和 thinking level 設定
  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;
  }
}

SessionManager 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) - 記錄 thinking 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() - 將葉子重置為空(在任何條目之前)
  • branchWithSummary(entryId, summary, details?, fromHook?) - 帶有上下文摘要的分支

實例方法 - 上下文和資訊

  • buildContextEntries() - 取得應用壓縮的活動分支條目
  • buildSessionContext() - 取得傳給 LLM 的訊息、thinkingLevel 和模型
  • getEntries() - 所有條目(不包括標題)
  • getHeader() - 工作階段標頭資訊元資料
  • getSessionName() - 從最新的 session_info 條目取得顯示名稱
  • getCwd() - 工作目錄
  • getSessionDir() - 工作階段儲存目錄
  • getSessionId() - 工作階段 UUID
  • getSessionFile() - 工作階段檔案路徑(記憶體中未定義)
  • isPersisted() - 工作階段是否儲存到磁碟