Настройка, расширение, параметры платформы и справочник 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 объектов. Понимание этих типов необходимо для анализа сеансов и написания расширений.

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

  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

Ключевые методы работы с сессиями программно.

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