Настройка, расширение, параметры платформы и справочник API для Pi.

Формат файла сеанса

Сессии сохраняются в виде файлов 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;
}

АгентСообщение Союз

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() проходит от текущего листа к корню, создавая активный список записей, соблюдая сжатие:

  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) — Извлечь ветку в новый файл сеанса.

Методы экземпляра — добавление (все идентификаторы возвращаемых записей)

  • 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() — сохраняется ли сессия на диске