Format de fichier de session
Les sessions sont stockées sous forme de fichiers JSONL (JSON Lines). Chaque ligne est un objet JSON avec un champ type. Les entrées de session forment une arborescence via les champs id/parentId, permettant un branchement sur place sans créer de nouveaux fichiers.
Emplacement du fichier
~/.pi/agent/sessions/--<path>--/<timestamp>_<uuid>.jsonlOù <path> est le répertoire de travail avec / remplacé par -.
Suppression de sessions
Les sessions peuvent être supprimées en supprimant leurs fichiers .jsonl sous ~/.pi/agent/sessions/.
Pi prend également en charge la suppression interactive de sessions à partir de /resume (sélectionnez une session et appuyez sur Ctrl+D, puis confirmez). Lorsqu'il est disponible, pi utilise le trash CLI pour éviter une suppression permanente.
Version de la séance
Les sessions ont un champ de version dans l'en-tête:
- Version 1: séquence d'entrée linéaire (héritée, migrée automatiquement au chargement)
- Version 2: Arborescence avec liaison
id/parentId - Version 3: rôle
hookMessagerenommé encustom(unification des extensions)
Les sessions existantes sont automatiquement migrées vers la version actuelle (v3) une fois chargées.
Fichiers sources
Source sur GitHub (pi-mono):
packages/coding-agent/src/core/session-manager.ts- Types d'entrée de session et SessionManagerpackages/coding-agent/src/core/messages.ts- Types de messages étendus (BashExecutionMessage, CustomMessage, etc.)packages/ai/src/types.ts- Types de messages de base (UserMessage, AssistantMessage, ToolResultMessage)packages/agent/src/types.ts- Type d'union AgentMessage
Pour les définitions TypeScript de votre projet, inspectez node_modules/@earendil-works/pi-coding-agent/dist/ et node_modules/@earendil-works/pi-ai/dist/.
Types de messages
Les entrées de session contiennent AgentMessage objets. Comprendre ces types est essentiel pour analyser les sessions et écrire des extensions.
Blocs de contenu
Les messages contiennent des tableaux de blocs de contenu typés:
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>;
}Types de messages de base (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;
};
}Le type pi-ai StopReason exporté inclut également "pending", mais cette valeur est réservée aux messages partiels dans les événements de streaming. Les messages du terminal done/error le remplacent par une raison d'achèvement avant que pi ne persiste le message de l'assistant, donc "pending" ne devrait jamais apparaître dans la session JSONL.
Types de messages étendus (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;
}AgentMessage Union
type AgentMessage =
| UserMessage
| AssistantMessage
| ToolResultMessage
| BashExecutionMessage
| CustomMessage
| BranchSummaryMessage
| CompactionSummaryMessage;Base d'entrée
Toutes les entrées (sauf SessionHeader) étendent 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
}Types d'entrée
En-tête de session
Première ligne du fichier. Métadonnées uniquement, ne faisant pas partie de l'arborescence (pas de id/parentId).
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path/to/project"}Pour les sessions avec un parent (créées 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"}Entrée de message de session
Un message dans la conversation. Le champ message contient 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}}ModèleChangeEntry
Émis lorsque l'utilisateur change de modèle en cours de session.
{"type":"model_change","id":"d4e5f6g7","parentId":"c3d4e5f6","timestamp":"2024-12-03T14:05:00.000Z","provider":"openai","modelId":"gpt-4o"}RéflexionNiveauChangeEntrée
Émis lorsque l'utilisateur change le niveau de réflexion/raisonnement.
{"type":"thinking_level_change","id":"e5f6g7h8","parentId":"d4e5f6g7","timestamp":"2024-12-03T14:06:00.000Z","thinkingLevel":"high"}Entrée de compactage
Créé lorsque le contexte est compacté. Stocke un résumé des messages précédents.
{"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000}Les compactages plus récents générés par le faisceau intègrent le contexte post-compactage conservé directement dans l'entrée, au lieu 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"}]}Champs facultatifs:
usage: utilisation du LLM à partir de la génération du résumé; inclus dans le jeton de session et les totaux des coûtsretainedTail:AgentMessage[]matérialisé conservé après compactage. Ceci est facultatif uniquement pour des raisons de compatibilité descendante avec les anciennes sessions. Les compactages plus récents générés par le faisceau l'incluent afin que nous puissions reconstruire le contexte à partir de ce point de contrôle sans parcourir les anciennes entrées avant l'entrée de compactage.details: données spécifiques à l'implémentation (par exemple,{ readFiles: string[], modifiedFiles: string[] }pour les données par défaut ou personnalisées pour les extensions)fromHook:truesi généré par une extension,false/undefinedsi généré par pi (nom de champ hérité)firstKeptEntryId: pour la compatibilité avec l'ancien format de saisie.
BranchSummaryEntrée
Créé lors du changement de branche via /tree avec un résumé généré par LLM de la branche gauche jusqu'à l'ancêtre commun. Capture le contexte du chemin abandonné.
{"type":"branch_summary","id":"g7h8i9j0","parentId":"a1b2c3d4","timestamp":"2024-12-03T14:15:00.000Z","fromId":"f6g7h8i9","summary":"Branch explored approach A..."}Champs facultatifs:
usage: utilisation du LLM à partir de la génération du résumé; inclus dans le jeton de session et les totaux des coûtsdetails: données de suivi de fichiers ({ readFiles: string[], modifiedFiles: string[] }) pour les données par défaut ou personnalisées pour les extensionsfromHook:truesi généré par une extension,false/undefinedsi généré par pi (nom de champ hérité)
Entrée personnalisée
Persistance de l’état d’extension. Ne participe PAS au contexte LLM.
{"type":"custom","id":"h8i9j0k1","parentId":"g7h8i9j0","timestamp":"2024-12-03T14:20:00.000Z","customType":"my-extension","data":{"count":42}}Utilisez customType pour identifier les entrées de votre extension lors du rechargement. Le mode interactif peut restituer les entrées personnalisées via pi.registerEntryRenderer(customType, renderer), mais elles ne participent toujours pas au contexte LLM.
Entrée de message personnalisé
Messages injectés par extension qui participent au contexte LLM.
{"type":"custom_message","id":"i9j0k1l2","parentId":"h8i9j0k1","timestamp":"2024-12-03T14:25:00.000Z","customType":"my-extension","content":"Injected context...","display":true}Champs:
content: chaîne ou(TextContent | ImageContent)[](identique à UserMessage)display:true= afficher dans TUI avec un style distinct,false= masquédetails: métadonnées facultatives spécifiques à l'extension (non envoyées à LLM)
EntréeÉtiquette
Signet/marqueur défini par l'utilisateur sur une entrée.
{"type":"label","id":"j0k1l2m3","parentId":"i9j0k1l2","timestamp":"2024-12-03T14:30:00.000Z","targetId":"a1b2c3d4","label":"checkpoint-1"}Réglez label sur undefined pour effacer une étiquette.
EntréeInfoSession
Métadonnées de session (par exemple, nom d'affichage défini par l'utilisateur). Définissez via /name, --name / -n ou pi.setSessionName() dans les extensions.
{"type":"session_info","id":"k1l2m3n4","parentId":"j0k1l2m3","timestamp":"2024-12-03T14:35:00.000Z","name":"Refactor auth module"}Le nom de la session est affiché dans le sélecteur de session (/resume) au lieu du premier message lorsqu'il est défini.
Structure arborescente
Les entrées forment un arbre:
- La première entrée a
parentId: null - Chaque entrée suivante pointe vers son parent via
parentId - Le branchement crée de nouveaux enfants à partir d'une entrée antérieure
- La "feuille" est la position actuelle dans l'arborescence
[user msg] ─── [assistant] ─── [user msg] ─── [assistant] ─┬─ [user msg] ← current leaf
│
└─ [branch_summary] ─── [user msg] ← alternate branchCréation de contexte
buildContextEntries() marche de la feuille actuelle à la racine, produisant la liste des entrées actives tout en respectant le compactage:
- Collecte toutes les entrées sur le chemin
- Si un
CompactionEntryest sur le chemin:- Inclut d'abord l'entrée de compactage
- Si
retainedTailest présent, il agit comme un point de contrôle autonome et les entrées après le compactage sont incluses - Sinon les entrées de
firstKeptEntryIdau compactage sont incluses - Ensuite, les entrées après compactage sont incluses
- Préserve les entrées sans message dans la plage sélectionnée afin que le mode interactif puisse les restituer
buildSessionContext() s'appuie sur cette liste d'entrées pour produire la liste de messages pour le LLM:
- Extrait les paramètres actuels du modèle et du niveau de réflexion du chemin complet
- Convertit les entrées sélectionnées en messages:
message-> stockéAgentMessagecompaction->compactionSummaryplusretainedTaillorsqu'il est présentbranch_summary->branchSummarycustom_message->CustomMessagecustom-> pas de message contextuel
Cela fait que les compactages les plus récents agissent comme des points de contrôle autonomes. retainedTail est facultatif uniquement, donc les anciennes sessions qui stockent uniquement firstKeptEntryId continuent de se charger correctement.
Exemple d'analyse
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;
}
}Gestionnaire de sessions API
Méthodes clés pour travailler avec des sessions par programmation.
Méthodes de création statique
SessionManager.create(cwd, sessionDir?)- Nouvelle séanceSessionManager.open(path, sessionDir?)- Ouvrir le fichier de session existantSessionManager.continueRecent(cwd, sessionDir?)- Continuer le plus récent ou créer un nouveauSessionManager.inMemory(cwd?)- Aucune persistance du fichierSessionManager.forkFrom(sourcePath, targetCwd, sessionDir?)- Session Fork d'un autre projet
Méthodes de liste statique
SessionManager.list(cwd, sessionDir?, onProgress?)- Liste des sessions pour un répertoireSessionManager.listAll(onProgress?)- Répertorier toutes les sessions de tous les projets
Méthodes d'instance - Gestion de session
newSession(options?)- Démarrer une nouvelle session (options:{ parentSession?: string })setSessionFile(path)- Passer à un autre fichier de sessioncreateBranchedSession(leafId)- Extraire la branche vers un nouveau fichier de session
Méthodes d'instance - Ajout (tous les ID d'entrée de retour)
appendMessage(message)- Ajouter un messageappendThinkingLevelChange(level)– Enregistrer le changement de penséeappendModelChange(provider, modelId)- Enregistrer le changement de modèleappendCompaction(summary, firstKeptEntryId, tokensBefore, details?, fromHook?)- Ajouter un compactageappendCustomEntry(customType, data?)- État de l'extension (pas dans son contexte)appendSessionInfo(name)- Définir le nom d'affichage de la sessionappendCustomMessageEntry(customType, content, display, details?)- Message d'extension (en contexte)appendLabelChange(targetId, label)- Définir/effacer l'étiquette
Méthodes d'instance - Navigation dans l'arborescence
getLeafId()- Position actuellegetLeafEntry()- Obtenir l'entrée de feuille actuellegetEntry(id)- Obtenez une entrée par pièce d'identitégetBranch(fromId?)- Marcher de l'entrée à la racinegetTree()- Obtenez l'arborescence complètegetChildren(parentId)- Obtenez des enfants directsgetLabel(id)- Obtenir l'étiquette pour l'entréebranch(entryId)- Déplacer la feuille vers l'entrée précédenteresetLeaf()- Réinitialiser la feuille à null (avant toute entrée)branchWithSummary(entryId, summary, details?, fromHook?)- Branche avec résumé du contexte
Méthodes d'instance - Contexte et informations
buildContextEntries()- Obtenez les entrées de branche actives avec le compactage appliquébuildSessionContext()- Obtenez des messages, un niveau de réflexion et un modèle pour le LLMgetEntries()- Toutes les entrées (hors en-tête)getHeader()- Métadonnées d'en-tête de sessiongetSessionName()- Obtenez le nom d'affichage de la dernière entrée session_infogetCwd()- Répertoire de travailgetSessionDir()- Répertoire de stockage de sessiongetSessionId()- UUID de sessiongetSessionFile()- Chemin du fichier de session (non défini pour la mémoire)isPersisted()- Indique si la session est enregistrée sur le disque