Формат файла сеанса
Сессии сохраняются в виде файлов 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):
packages/coding-agent/src/core/session-manager.ts— типы записей сеанса и SessionManager.packages/coding-agent/src/core/messages.ts— Расширенные типы сообщений (BashExecutionMessage, CustomMessage и т. д.)packages/ai/src/types.ts— Базовые типы сообщений (UserMessage, AssistantMessage, ToolResultMessage)packages/agent/src/types.ts— тип объединения AgentMessage
Для определений TypeScript в вашем проекте проверьте node_modules/@earendil-works/pi-coding-agent/dist/ и node_modules/@earendil-works/pi-ai/dist/.
Типы сообщений
Записи сеанса содержат AgentMessage объектов. Понимание этих типов необходимо для анализа сеансов и написания расширений.
Content Blocks
Сообщения содержат массивы типизированных блоков контента:
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>;
}Base Message Types (from 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.
Extended Message Types (from 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 Union
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;Базовая структура Entry
Все записи (кроме 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, /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"}}
{"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}}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}Новые сжатия, созданные Pi, встраивают сохраненный после сжатия контекст непосредственно в запись вместо 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[], сохраненный после сжатия. Это поле необязательно только для совместимости со старыми сессиями. Новые сжатия, созданные Pi, включают его, чтобы можно было восстановить контекст из этой контрольной точки без обхода старых записей перед записью сжатия.details: данные, специфичные для реализации (например,{ readFiles: string[], modifiedFiles: string[] }по умолчанию или пользовательские данные для расширений).fromHook:true, если сгенерировано расширением,false/undefined, если сгенерировано pi (устаревшее имя поля)firstKeptEntryId: для совместимости со старым форматом ввода.
BranchSummaryEntry
Создается при переключении ветвей через /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 (устаревшее имя поля)
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) вместо первого сообщения, если оно установлено.
Древовидная структура
Записи образуют дерево:
- Первая запись имеет
parentId: null - Каждая последующая запись указывает на своего родителя через
parentId. - Ветвление создает новых дочерних элементов из более ранней записи.
- «Лист» — текущая позиция в дереве.
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branchПостроение контекста
buildContextEntries() проходит от текущего листа к корню, создавая активный список записей, соблюдая сжатие:
- Собирает все записи на пути
- Если на пути находится
CompactionEntry:- Сначала включает запись уплотнения
- Если присутствует
retainedTail, он действует как автономная контрольная точка, и записи после сжатия включаются. - В противном случае включаются записи от
firstKeptEntryIdдо уплотнения. - Затем включаются записи после уплотнения.
- Сохраняет записи, не относящиеся к сообщениям, в выбранном диапазоне, чтобы интерактивный режим мог их отображать.
buildSessionContext() основывается на этом списке записей для создания списка сообщений для LLM:
- Извлекает настройки текущей модели и уровня мышления из полного пути.
- Преобразует выбранные записи в сообщения:
message-> сохраненоAgentMessagecompaction->compactionSummaryплюсretainedTail, если присутствуетbranch_summary->branchSummarycustom_message->CustomMessagecustom-> нет контекстного сообщения
Это заставляет новые уплотнения действовать как автономные контрольные точки. 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
Ключевые методы работы с сессиями программно.
Static Creation Methods
SessionManager.create(cwd, sessionDir?)— Новая сессияSessionManager.open(path, sessionDir?)— Открыть существующий файл сеанса.SessionManager.continueRecent(cwd, sessionDir?)— Продолжить последнюю версию или создать новую.SessionManager.inMemory(cwd?)— Файл не сохраняется.SessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)— Форк сессии из другого проекта
Static Listing Methods
SessionManager.list(cwd, sessionDir?, onProgress?)— список сеансов для каталога.SessionManager.listAll(onProgress?)— список всех сеансов во всех проектах.
Instance Methods - Session Management
newSession(options?)— Начать новую сессию (варианты:{ parentSession?: string })setSessionFile(path)— переключиться на другой файл сеанса.createBranchedSession(leafId)— Извлечь ветку в новый файл сеанса.
Instance Methods - Appending (all return entry 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)— Установить/очистить метку
Instance Methods - Tree Navigation
getLeafId()- Текущая позицияgetLeafEntry()— Получить текущую листовую запись.getEntry(id)— Получить запись по идентификаторуgetBranch(fromId?)— Пройти от входа до корняgetTree()— Получить полную древовидную структуруgetChildren(parentId)— Получить прямых детейgetLabel(id)— Получить метку для входа.branch(entryId)— Переместить лист на более раннюю запись.resetLeaf()— Сбросить лист до нуля (перед любыми записями)branchWithSummary(entryId, summary, details?, fromHook?)— Ветка с контекстной сводкой.
Instance Methods - Context & Info
buildContextEntries()— получить активные записи ветвей с примененным сжатием.buildSessionContext()— Получайте сообщения, уровень мышления и модель для LLM.getEntries()— Все записи (кроме заголовка)getHeader()— метаданные заголовка сеанса.getSessionName()— Получить отображаемое имя из последней записи session_info.getCwd()— Рабочий каталог.getSessionDir()— Каталог хранения сеансов.getSessionId()— UUID сеансаgetSessionFile()— путь к файлу сеанса (не определен для хранения в памяти)isPersisted()— сохраняется ли сессия на диске