/** * Hybrid input serializer for pi-omni-compact. * * Converts compaction and branch summarization event data * into a structured text format combining conversation and metadata. */ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import { type FileOperations, type SessionEntry, convertToLlm, serializeConversation, } from "@mariozechner/pi-coding-agent"; import type { SessionAnalysis } from "./session-analysis.js"; /** Subset of CompactionPreparation needed for serialization */ interface CompactionInput { messagesToSummarize: AgentMessage[]; turnPrefixMessages: AgentMessage[]; isSplitTurn: boolean; tokensBefore: number; previousSummary?: string; fileOps: FileOperations; customInstructions?: string; sessionAnalysis?: SessionAnalysis; } /** * Extract an AgentMessage from a session entry, mirroring the pattern * from compaction.ts's getMessageFromEntry(). */ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined { if (entry.type === "message") { return entry.message; } if (entry.type === "custom_message") { // Build a minimal custom-role message for serialization return { role: "custom" as const, customType: entry.customType, content: typeof entry.content === "string" ? entry.content : entry.content, display: entry.display, details: entry.details, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } if (entry.type === "branch_summary") { return { role: "branchSummary" as const, summary: entry.summary, fromId: entry.fromId, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } if (entry.type === "compaction") { return { role: "compactionSummary" as const, summary: entry.summary, tokensBefore: entry.tokensBefore, timestamp: new Date(entry.timestamp).getTime(), } as AgentMessage; } return undefined; } /** * Format file operations into the metadata section. */ function formatFileOps(fileOps: FileOperations): string { const parts: string[] = []; if (fileOps.read.size > 0) { parts.push(` read: ${[...fileOps.read].join(", ")}`); } if (fileOps.written.size > 0) { parts.push(` written: ${[...fileOps.written].join(", ")}`); } if (fileOps.edited.size > 0) { parts.push(` edited: ${[...fileOps.edited].join(", ")}`); } if (parts.length === 0) { return " (none)"; } return parts.join("\n"); } /** * Serialize compaction preparation data into hybrid input format. */ export function serializeCompactionInput(preparation: CompactionInput): string { const { messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, customInstructions, sessionAnalysis, } = preparation; const sections: string[] = []; // Session structure analysis (before conversation for context) if (sessionAnalysis) { sections.push(formatSessionAnalysis(sessionAnalysis)); } // Main conversation const conversationText = serializeConversation( convertToLlm(messagesToSummarize) ); sections.push(`\n${conversationText}\n`); // Turn prefix (if split turn) if (isSplitTurn && turnPrefixMessages.length > 0) { const prefixText = serializeConversation(convertToLlm(turnPrefixMessages)); sections.push(`\n${prefixText}\n`); } // Metadata const metadataLines = [ "", `${tokensBefore}`, `${isSplitTurn}`, "", formatFileOps(fileOps), "", "", ]; sections.push(metadataLines.join("\n")); // Previous summary (for incremental compaction) if (previousSummary) { sections.push( `\n${previousSummary}\n` ); } // User compaction note (from /compact ) if (customInstructions?.trim()) { sections.push( `\n${customInstructions.trim()}\n` ); } return sections.join("\n\n"); } /** * Serialize branch entries into hybrid input format for branch summarization. */ export function serializeBranchInput( entriesToSummarize: SessionEntry[], sessionAnalysis?: SessionAnalysis ): string { const messages: AgentMessage[] = []; for (const entry of entriesToSummarize) { const msg = getMessageFromEntry(entry); if (msg) { messages.push(msg); } } const sections: string[] = []; if (sessionAnalysis) { sections.push(formatSessionAnalysis(sessionAnalysis)); } const conversationText = serializeConversation(convertToLlm(messages)); sections.push(`\n${conversationText}\n`); return sections.join("\n\n"); } /** * Format a SessionAnalysis into the section. */ function formatSessionAnalysis(analysis: SessionAnalysis): string { const { stats, boundaries, friction, delight, filesTouched } = analysis; const lines: string[] = [ // Stats `Messages: ${stats.messageCount} (user: ${stats.userMessageCount}, assistant: ${stats.assistantMessageCount}, tool: ${stats.toolResultCount})`, ]; if (stats.modelsUsed.length > 0) { lines.push(`Models: ${stats.modelsUsed.join(", ")}`); } lines.push( `Compactions: ${stats.compactionCount} | Branch points: ${stats.branchPointCount}` ); // Boundaries if (boundaries.length > 0) { lines.push(""); lines.push("Boundaries:"); for (const b of boundaries) { lines.push(`- [${b.timestamp}] ${b.detail}`); } } // Friction const frictionLines: string[] = []; if (friction.rephrasingCascades > 0) { frictionLines.push( `- Rephrasing cascades: ${friction.rephrasingCascades} (${friction.rephrasingCascades}x 3+ consecutive user messages)` ); } if (friction.toolLoops > 0) { frictionLines.push( `- Tool loops: ${friction.toolLoops} (same error repeated 3+ times)` ); } if (friction.contextChurn > 0) { frictionLines.push( `- Context churn: ${friction.contextChurn} (10+ file reads without writes)` ); } if (friction.silentTermination) { frictionLines.push( "- Silent termination: session ended with unresolved error" ); } if (frictionLines.length > 0) { lines.push(""); lines.push("Friction:"); lines.push(...frictionLines); } // Delight const delightLines: string[] = []; if (delight.resilientRecovery) { delightLines.push( "- Resilient recovery: yes (fixed errors without user help)" ); } if (delight.oneShotSuccess) { delightLines.push( "- One-shot success: yes (task completed without corrections)" ); } if (delight.explicitPraise) { delightLines.push("- Explicit praise: yes"); } if (delightLines.length > 0) { lines.push(""); lines.push("Delight:"); lines.push(...delightLines); } // Files touched if (filesTouched.read.length > 0 || filesTouched.written.length > 0) { lines.push(""); lines.push("Files touched:"); if (filesTouched.read.length > 0) { lines.push(` read: ${filesTouched.read.join(", ")}`); } if (filesTouched.written.length > 0) { lines.push(` written: ${filesTouched.written.join(", ")}`); } } return `\n${lines.join("\n")}\n`; }