Formato de archivo de sesión
Las sesiones se almacenan como archivos JSONL (JSON Líneas). Cada línea es un objeto JSON con un campo type. Las entradas de sesión forman una estructura de árbol a través de los campos id/parentId, lo que permite la bifurcación in situ sin crear nuevos archivos.
Ubicación del archivo
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonlDonde <path> es el directorio de trabajo con / reemplazado por -.
Eliminar sesiones
Las sesiones se pueden eliminar eliminando sus archivos .jsonl en ~/.pi/agent/sessions/.
Pi también admite la eliminación de sesiones de forma interactiva desde /resume (seleccione una sesión y presione Ctrl+D, luego confirme). Cuando está disponible, pi usa trash CLI para evitar la eliminación permanente.
Versión de sesión
Las sesiones tienen un campo de versión en el encabezado:
- Versión 1: Secuencia de entrada lineal (heredada, migrada automáticamente al cargar)
- Versión 2: Estructura de árbol con enlaces
id/parentId - Versión 3: Se cambió el nombre del rol
hookMessageacustom(unificación de extensiones)
Las sesiones existentes se migran automáticamente a la versión actual (v3) cuando se cargan.
Archivos fuente
Fuente en GitHub (pi-mono):
packages/coding-agent/src/core/session-manager.ts- Tipos de entrada de sesión y SessionManagerpackages/coding-agent/src/core/messages.ts- Tipos de mensajes extendidos (BashExecutionMessage, CustomMessage, etc.)packages/ai/src/types.ts- Tipos de mensajes base (UserMessage, AssistantMessage, ToolResultMessage)packages/agent/src/types.ts- Tipo de unión AgentMessage
Para las definiciones de TypeScript en su proyecto, inspeccione node_modules/@earendil-works/pi-coding-agent/dist/ y node_modules/@earendil-works/pi-ai/dist/.
Tipos de mensajes
Las entradas de sesión contienen AgentMessage objetos. Comprender estos tipos es esencial para analizar sesiones y escribir extensiones.
Bloques de contenido
Los mensajes contienen matrices de bloques de contenido escritos:
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 de mensajes básicos (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;
};
}El tipo pi-ai exportado StopReason también incluye "pending", pero ese valor está reservado para mensajes parciales en eventos de transmisión. Los mensajes del terminal done/error lo reemplazan con un motivo de finalización antes de que pi persista en el mensaje del asistente, por lo que "pending" nunca debería aparecer en la sesión JSONL.
Tipos de mensajes extendidos (de 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ón de mensajes de agente
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;Base de entrada
Todas las entradas (excepto SessionHeader) extienden 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
Encabezado de sesión
Primera línea del archivo. Solo metadatos, no parte del árbol (no id/parentId).
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}Para sesiones con uno de los padres (creadas mediante /fork, /clone o 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"}Entrada de mensaje de sesión
Un mensaje en la conversación. El campo message contiene un 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}}Entrada de cambio de modelo
Emitido cuando el usuario cambia de modelo a mitad de sesión.
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}PensamientoNivelCambioEntrada
Emitido cuando el usuario cambia el nivel de pensamiento/razonamiento.
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}Entrada de compactación
Creado cuando se compacta el contexto. Almacena un resumen de mensajes anteriores.
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}Las compactaciones más nuevas generadas por arnés incorporan el contexto posterior a la compactación retenido directamente en la entrada, en lugar 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 opcionales:
usage: uso de LLM desde la generación del resumen; incluido en el token de sesión y los costos totalesretainedTail: MaterializadoAgentMessage[]mantenido después de la compactación. Esto es opcional sólo por compatibilidad con sesiones anteriores. Las compactaciones más nuevas generadas por arnés lo incluyen para que podamos reconstruir el contexto desde este punto de control sin tener que recorrer las entradas más antiguas antes de la entrada de compactación.details: datos específicos de la implementación (por ejemplo,{ readFiles: string[], modifiedFiles: string[] }para datos predeterminados o personalizados para extensiones)fromHook:truesi es generado por una extensión,false/undefinedsi es generado por pi (nombre de campo heredado)firstKeptEntryId: por compatibilidad con el formato de entrada anterior.
RamaResumenEntrada
Creado al cambiar de rama a través de /tree con un resumen generado por LLM de la rama izquierda hasta el ancestro común. Captura el contexto del camino abandonado.
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}Campos opcionales:
usage: uso de LLM desde la generación del resumen; incluido en el token de sesión y los costos totalesdetails: datos de seguimiento de archivos ({ readFiles: string[], modifiedFiles: string[] }) para datos predeterminados o personalizados para extensionesfromHook:truesi es generado por una extensión,false/undefinedsi es generado por pi (nombre de campo heredado)
Entrada personalizada
Persistencia del estado de extensión. NO participa en el contexto LLM.
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}Utilice customType para identificar las entradas de su extensión al recargar. El modo interactivo puede representar entradas personalizadas a través de pi.registerEntryRenderer(customType, renderer), pero aún no participan en el contexto LLM.
Entrada de mensaje personalizado
Mensajes inyectados con extensión que SÍ participan en el 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: Cadena o(TextContent | ImageContent)[](igual que UserMessage)display:true= mostrar en TUI con un estilo distinto,false= ocultodetails: metadatos específicos de la extensión opcionales (no enviados a LLM)
Entrada de etiqueta
Marcador/marcador definido por el usuario en una entrada.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Establezca label en undefined para borrar una etiqueta.
Entrada de información de sesión
Metadatos de la sesión (por ejemplo, nombre para mostrar definido por el usuario). Establecer mediante /name, --name / -n o pi.setSessionName() en extensiones.
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}El nombre de la sesión se muestra en el selector de sesión (/resume) en lugar del primer mensaje cuando se configura.
Estructura de árbol
Las entradas forman un árbol:
- La primera entrada tiene
parentId: null - Cada entrada posterior apunta a su padre mediante
parentId - La ramificación crea nuevos hijos a partir de una entrada anterior.
- La "hoja" es la posición actual en el árbol.
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branchConstrucción de contexto
buildContextEntries() camina desde la hoja actual hasta la raíz, generando la lista de entradas activas respetando la compactación:
- Recoge todas las entradas en el camino.
- Si hay un
CompactionEntryen el camino:- Incluye la entrada de compactación primero.
- Si
retainedTailestá presente, actúa como un punto de control autónomo y se incluyen las entradas después de la compactación. - De lo contrario se incluyen las entradas desde
firstKeptEntryIda la compactación. - Luego se incluyen las entradas después de la compactación.
- Conserva las entradas que no son mensajes en el rango seleccionado para que el modo interactivo pueda representarlas
buildSessionContext() se basa en esa lista de entradas para producir la lista de mensajes para el LLM:
- Extrae el modelo actual y la configuración del nivel de pensamiento de la ruta completa.
- Convierte entradas seleccionadas en mensajes:
message-> almacenadoAgentMessagecompaction->compactionSummarymásretainedTailcuando esté presentebranch_summary->branchSummarycustom_message->CustomMessagecustom-> sin mensaje de contexto
Esto hace que las compactaciones más nuevas actúen como puntos de control autónomos. retainedTail es opcional solo para que las sesiones más antiguas que solo almacenan firstKeptEntryId continúen cargándose correctamente.
Ejemplo de análisis
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;
}
}Administrador de sesión API
Métodos clave para trabajar con sesiones mediante programación.
Métodos de creación estática
SessionManager.create(cwd, sessionDir?)- Nueva sesiónSessionManager.open(path, sessionDir?)- Abrir archivo de sesión existenteSessionManager.continueRecent(cwd, sessionDir?)- Continuar con el más reciente o crear uno nuevoSessionManager.inMemory(cwd?)- Sin persistencia de archivosSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- Sesión bifurcada de otro proyecto
Métodos de listado estático
SessionManager.list(cwd, sessionDir?, onProgress?)- Listar sesiones para un directorioSessionManager.listAll(onProgress?): enumera todas las sesiones de todos los proyectos
Métodos de instancia: gestión de sesiones
newSession(options?)- Iniciar una nueva sesión (opciones:{ parentSession?: string })setSessionFile(path)- Cambiar a un archivo de sesión diferentecreateBranchedSession(leafId)- Extraer rama a un nuevo archivo de sesión
Métodos de instancia: anexar (todos los ID de entrada devueltos)
appendMessage(message)- Agregar mensajeappendThinkingLevelChange(level)- Registrar cambio de pensamientoappendModelChange(provider, modelId)- Cambio de modelo de registroappendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)- Agregar compactaciónappendCustomEntry(customType, data?)- Estado de extensión (no en contexto)appendSessionInfo(name)- Establecer nombre para mostrar de la sesiónappendCustomMessageEntry(customType, content, display, details?)- Mensaje de extensión (en contexto)appendLabelChange(targetId, label)- Establecer/borrar etiqueta
Métodos de instancia: navegación en árbol
getLeafId()- Posición actualgetLeafEntry()- Obtener la entrada de la hoja actualgetEntry(id)- Obtener entrada por IDgetBranch(fromId?)- Camina desde la entrada hasta la raízgetTree()- Obtener estructura de árbol completagetChildren(parentId)- Obtener hijos directosgetLabel(id)- Obtener etiqueta para ingresarbranch(entryId)- Mover hoja a la entrada anteriorresetLeaf()- Restablecer la hoja a nula (antes de cualquier entrada)branchWithSummary(entryId, summary, details?, fromHook?)- Rama con resumen de contexto
Métodos de instancia: contexto e información
buildContextEntries()- Obtener entradas de sucursales activas con compactación aplicadabuildSessionContext()- Recibe mensajes, nivel de pensamiento y modelo para LLMgetEntries()- Todas las entradas (excluyendo el encabezado)getHeader()- Metadatos del encabezado de sesióngetSessionName(): obtiene el nombre para mostrar de la última entrada de session_infogetCwd()- Directorio de trabajogetSessionDir()- Directorio de almacenamiento de sesionesgetSessionId()- UUID de sesióngetSessionFile()- Ruta del archivo de sesión (no definida para en memoria)isPersisted(): si la sesión se guarda en el disco