Формат файла сеанса
Сессии сохраняются в виде файлов 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 объектов. Понимание этих типов необходимо для анализа сеансов и написания расширений.
Блоки контента
Сообщения содержат массивы типизированных блоков контента:
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;
}АгентСообщение Союз
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, /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"}Сеансмессажеэнтри
Сообщение в разговоре. Поле 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"}МышлениеУровеньИзмененияВход
Генерируется, когда пользователь меняет уровень мышления/рассуждения.
{"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}Новые сжатия, генерируемые жгутами, встраивают сохраненный контекст после сжатия непосредственно в запись вместо 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[]сохраняется после уплотнения. Это необязательно только для обратной совместимости со старыми сеансами. Новые сжатия, генерируемые жгутами, включают его, поэтому мы можем перестроить контекст из этой контрольной точки, не проходя старые записи перед записью уплотнения.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 (устаревшее имя поля)
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).
МеткаEntry
Пользовательская закладка/маркер для записи.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Установите от label до undefined, чтобы очистить метку.
Сеансинфоэнтри
Метаданные сеанса (например, отображаемое имя, определяемое пользователем). Устанавливается с помощью /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
Ключевые методы работы с сессиями программно.
Статические методы создания
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)— Извлечь ветку в новый файл сеанса.
Методы экземпляра — добавление (все идентификаторы возвращаемых записей)
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)— Получить запись по идентификаторуgetBranch(fromId?)— Пройти от входа до корняgetTree()— Получить полную древовидную структуруgetChildren(parentId)— Получить прямых детейgetLabel(id)— Получить метку для входа.branch(entryId)— Переместить лист на более раннюю запись.resetLeaf()— Сбросить лист до нуля (перед любыми записями)branchWithSummary(entryId, summary, details?, fromHook?)— Ветка с контекстной сводкой.
Методы экземпляра — контекст и информация
buildContextEntries()— получить активные записи ветвей с примененным сжатием.buildSessionContext()— Получайте сообщения, уровень мышления и модель для LLM.getEntries()— Все записи (кроме заголовка)getHeader()— метаданные заголовка сеанса.getSessionName()— Получить отображаемое имя из последней записи session_info.getCwd()— Рабочий каталог.getSessionDir()— Каталог хранения сеансов.getSessionId()— UUID сеансаgetSessionFile()— путь к файлу сеанса (не определен для хранения в памяти)isPersisted()— сохраняется ли сессия на диске