Formato de arquivo de sessão
As sessões são armazenadas como arquivos JSONL (JSON Linhas). Cada linha é um objeto JSON com um campo type. As entradas de sessão formam uma estrutura em árvore por meio dos campos id/parentId, permitindo ramificações no local sem criar novos arquivos.
Localização do arquivo
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonlOnde <path> é o diretório de trabalho com / substituído por -.
Excluindo Sessões
As sessões podem ser removidas excluindo seus arquivos .jsonl em ~/.pi/agent/sessions/.
Pi também suporta a exclusão interativa de sessões de /resume (selecione uma sessão e pressione Ctrl+D e confirme). Quando disponível, pi usa trash CLI para evitar exclusão permanente.
Versão da sessão
As sessões têm um campo de versão no cabeçalho:
- Versão 1: sequência de entrada linear (herdada, migrada automaticamente durante o carregamento)
- Versão 2: Estrutura em árvore com ligação
id/parentId - Versão 3: Função
hookMessagerenomeada paracustom(unificação de extensões)
As sessões existentes são migradas automaticamente para a versão atual (v3) quando carregadas.
Arquivos de origem
Fonte em GitHub (pi-mono):
packages/coding-agent/src/core/session-manager.ts- Tipos de entrada de sessão e SessionManagerpackages/coding-agent/src/core/messages.ts- Tipos de mensagens estendidas (BashExecutionMessage, CustomMessage, etc.)packages/ai/src/types.ts- Tipos de mensagens base (UserMessage, AssistantMessage, ToolResultMessage)packages/agent/src/types.ts- Tipo de união AgentMessage
Para definições de TypeScript em seu projeto, inspecione node_modules/@earendil-works/pi-coding-agent/dist/ e node_modules/@earendil-works/pi-ai/dist/.
Tipos de mensagens
As entradas de sessão contêm objetos AgentMessage. Compreender esses tipos é essencial para analisar sessões e escrever extensões.
Blocos de conteúdo
As mensagens contêm matrizes de blocos de conteúdo digitados:
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>;
}Tipos básicos de mensagens (de 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;
};
}O tipo pi-ai StopReason exportado também inclui "pending", mas esse valor é reservado para mensagens parciais em eventos de streaming. As mensagens done/error do terminal substituem-no por um motivo de conclusão antes que pi persista a mensagem do assistente, então "pending" nunca deve aparecer na sessão JSONL.
Tipos de mensagens estendidas (do 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;
}União de mensagem do agente
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;Base de Entrada
Todas as entradas (exceto SessionHeader) estendem 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
}Tipos de entrada
Cabeçalho da Sessão
Primeira linha do arquivo. Apenas metadados, não fazem parte da árvore (não id/parentId).
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}Para sessões com um pai (criadas via /fork, /clone ou 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"}SessãoMessageEntry
Uma mensagem na conversa. O campo message contém um 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
Emitido quando o usuário troca de modelo no meio da sessão.
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}ThinkingLevelChangeEntry
Emitido quando o usuário altera o nível de pensamento/raciocínio.
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}Entrada de compactação
Criado quando o contexto é compactado. Armazena um resumo de mensagens anteriores.
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}As compactações geradas por chicotes mais recentes incorporam o contexto pós-compactação retido diretamente na entrada, em vez de 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"}]}Campos opcionais:
usage: uso do LLM a partir da geração do resumo; incluído no token de sessão e nos totais de custoretainedTail: MaterializadoAgentMessage[]mantido após compactação. Isto é opcional apenas para compatibilidade retroativa com sessões mais antigas. As compactações geradas por chicotes mais recentes incluem-no para que possamos reconstruir o contexto a partir deste ponto de verificação sem percorrer entradas mais antigas antes da entrada de compactação.details: Dados específicos da implementação (por exemplo,{ readFiles: string[], modifiedFiles: string[] }para padrão ou dados personalizados para extensões)fromHook:truese gerado por uma extensão,false/undefinedse gerado por pi (nome do campo legado)firstKeptEntryId: para compatibilidade com formato de entrada antigo.
FilialSummaryEntry
Criado ao alternar ramificações via /tree com um resumo gerado por LLM da ramificação esquerda até o ancestral comum. Captura o contexto do caminho abandonado.
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}Campos opcionais:
usage: uso do LLM a partir da geração do resumo; incluído no token de sessão e nos totais de custodetails: Dados de rastreamento de arquivo ({ readFiles: string[], modifiedFiles: string[] }) para padrão ou dados personalizados para extensõesfromHook:truese gerado por uma extensão,false/undefinedse gerado por pi (nome do campo legado)
Entrada personalizada
Persistência do estado de extensão. NÃO participa do contexto LLM.
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}Use customType para identificar as entradas da sua extensão ao recarregar. O modo interativo pode renderizar entradas personalizadas via pi.registerEntryRenderer(customType, renderer), mas elas ainda não participam do contexto LLM.
Entrada de mensagem personalizada
Mensagens injetadas por extensão que participam do contexto LLM.
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}Campos:
content: String ou(TextContent | ImageContent)[](igual a UserMessage)display:true= mostrar em TUI com estilo distinto,false= ocultodetails: Metadados opcionais específicos da extensão (não enviados para LLM)
LabelEntry
Marcador/marcador definido pelo usuário em uma entrada.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Defina label como undefined para limpar um rótulo.
SessãoInfoEntry
Metadados da sessão (por exemplo, nome de exibição definido pelo usuário). Defina via /name, --name / -n ou pi.setSessionName() nas extensões.
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}O nome da sessão é exibido no seletor de sessão (/resume) em vez da primeira mensagem quando definido.
Estrutura da árvore
As entradas formam uma árvore:
- A primeira entrada tem
parentId: null - Cada entrada subsequente aponta para seu pai via
parentId - A ramificação cria novos filhos a partir de uma entrada anterior
- A "folha" é a posição atual na árvore
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branchConstrução de Contexto
buildContextEntries() caminha da folha atual até a raiz, produzindo a lista de entradas ativas enquanto respeita a compactação:
- Coleta todas as entradas no caminho
- Se um
CompactionEntryestiver no caminho:- Inclui a entrada de compactação primeiro
- Se
retainedTailestiver presente, ele atua como um ponto de verificação independente e as entradas após a compactação são incluídas - Caso contrário, as entradas de
firstKeptEntryIdpara a compactação serão incluídas - Então as entradas após a compactação são incluídas
- Preserva entradas que não são de mensagem no intervalo selecionado para que o modo interativo possa renderizá-las
buildSessionContext() baseia-se nessa lista de entradas para produzir a lista de mensagens para o LLM:
- Extrai o modelo atual e as configurações de nível de pensamento do caminho completo
- Converte entradas selecionadas em mensagens:
message-> armazenadoAgentMessagecompaction->compactionSummarymaisretainedTailquando presentebranch_summary->branchSummarycustom_message->CustomMessagecustom-> nenhuma mensagem de contexto
Isso faz com que as compactações mais recentes atuem como pontos de verificação independentes. retainedTail é opcional apenas para que sessões mais antigas que armazenam apenas firstKeptEntryId continuem a carregar corretamente.
Exemplo de análise
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;
}
}Gerenciador de Sessão API
Principais métodos para trabalhar com sessões programaticamente.
Métodos de criação estática
SessionManager.create(cwd, sessionDir?)- Nova sessãoSessionManager.open(path, sessionDir?)- Abra o arquivo de sessão existenteSessionManager.continueRecent(cwd, sessionDir?)- Continue o mais recente ou crie um novoSessionManager.inMemory(cwd?)- Sem persistência de arquivoSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- Sessão bifurcada de outro projeto
Métodos de listagem estática
SessionManager.list(cwd, sessionDir?, onProgress?)- Lista sessões para um diretórioSessionManager.listAll(onProgress?)- Lista todas as sessões em todos os projetos
Métodos de Instância - Gerenciamento de Sessão
newSession(options?)- Iniciar uma nova sessão (opções:{ parentSession?: string })setSessionFile(path)- Mudar para um arquivo de sessão diferentecreateBranchedSession(leafId)- Extraia branch para novo arquivo de sessão
Métodos de instância - Anexando (todos os IDs de entrada de retorno)
appendMessage(message)- Adicionar mensagemappendThinkingLevelChange(level)- Registrar mudança de pensamentoappendModelChange(provider, modelId)- Registrar mudança de modeloappendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)- Adicionar compactaçãoappendCustomEntry(customType, data?)- Estado da extensão (fora do contexto)appendSessionInfo(name)- Definir nome de exibição da sessãoappendCustomMessageEntry(customType, content, display, details?)- Mensagem de extensão (no contexto)appendLabelChange(targetId, label)- Definir/limpar rótulo
Métodos de Instância - Navegação em Árvore
getLeafId()- Posição atualgetLeafEntry()- Obtenha a entrada atual da folhagetEntry(id)- Obtenha entrada por IDgetBranch(fromId?)- Caminhe da entrada até a raizgetTree()- Obtenha estrutura de árvore completagetChildren(parentId)- Obtenha filhos diretosgetLabel(id)- Obtenha etiqueta para entradabranch(entryId)- Mover folha para entrada anteriorresetLeaf()- Redefinir folha para nulo (antes de qualquer entrada)branchWithSummary(entryId, summary, details?, fromHook?)- Ramificação com resumo de contexto
Métodos de Instância - Contexto e Informações
buildContextEntries()- Obtenha entradas de ramificação ativas com compactação aplicadabuildSessionContext()- Obtenha mensagens, nível de pensamento e modelo para LLMgetEntries()- Todas as entradas (excluindo cabeçalho)getHeader()- Metadados do cabeçalho da sessãogetSessionName()- Obtenha o nome de exibição da última entrada session_infogetCwd()- Diretório de trabalhogetSessionDir()- Diretório de armazenamento de sessãogetSessionId()- UUID da sessãogetSessionFile()- Caminho do arquivo da sessão (indefinido para memória)isPersisted()- Se a sessão é salva no disco