142 lines
5.4 KiB
JavaScript
142 lines
5.4 KiB
JavaScript
/**
|
|
* Shared utilities for compaction and branch summarization.
|
|
*/
|
|
import { contentText } from "@earendil-works/pi-ai";
|
|
export function createFileOps() {
|
|
return {
|
|
read: new Set(),
|
|
written: new Set(),
|
|
edited: new Set(),
|
|
};
|
|
}
|
|
/**
|
|
* Extract file operations from tool calls in an assistant message.
|
|
*/
|
|
export function extractFileOpsFromMessage(message, fileOps) {
|
|
if (message.role !== "assistant")
|
|
return;
|
|
if (!("content" in message) || !Array.isArray(message.content))
|
|
return;
|
|
for (const block of message.content) {
|
|
if (typeof block !== "object" || block === null)
|
|
continue;
|
|
if (!("type" in block) || block.type !== "toolCall")
|
|
continue;
|
|
if (!("arguments" in block) || !("name" in block))
|
|
continue;
|
|
const args = block.arguments;
|
|
if (!args)
|
|
continue;
|
|
const path = typeof args.path === "string" ? args.path : undefined;
|
|
if (!path)
|
|
continue;
|
|
switch (block.name) {
|
|
case "read":
|
|
fileOps.read.add(path);
|
|
break;
|
|
case "write":
|
|
fileOps.written.add(path);
|
|
break;
|
|
case "edit":
|
|
fileOps.edited.add(path);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Compute final file lists from file operations.
|
|
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
*/
|
|
export function computeFileLists(fileOps) {
|
|
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
|
|
const modifiedFiles = [...modified].sort();
|
|
return { readFiles: readOnly, modifiedFiles };
|
|
}
|
|
/**
|
|
* Format file operations as XML tags for summary.
|
|
*/
|
|
export function formatFileOperations(readFiles, modifiedFiles) {
|
|
const sections = [];
|
|
if (readFiles.length > 0) {
|
|
sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
|
}
|
|
if (modifiedFiles.length > 0) {
|
|
sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
|
|
}
|
|
if (sections.length === 0)
|
|
return "";
|
|
return `\n\n${sections.join("\n\n")}`;
|
|
}
|
|
// ============================================================================
|
|
// Message Serialization
|
|
// ============================================================================
|
|
/** Maximum characters for a tool result in serialized summaries. */
|
|
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
/**
|
|
* Truncate text to a maximum character length for summarization.
|
|
* Keeps the beginning and appends a truncation marker.
|
|
*/
|
|
function truncateForSummary(text, maxChars) {
|
|
if (text.length <= maxChars)
|
|
return text;
|
|
const truncatedChars = text.length - maxChars;
|
|
return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
|
|
}
|
|
/**
|
|
* Serialize LLM messages to text for summarization.
|
|
* This prevents the model from treating it as a conversation to continue.
|
|
* Call convertToLlm() first to handle custom message types.
|
|
*
|
|
* Tool results are truncated to keep the summarization request within
|
|
* reasonable token budgets. Full content is not needed for summarization.
|
|
*/
|
|
export function serializeConversation(messages) {
|
|
const parts = [];
|
|
for (const msg of messages) {
|
|
if (msg.role === "user") {
|
|
const content = contentText(msg.content, "");
|
|
if (content)
|
|
parts.push(`[User]: ${content}`);
|
|
}
|
|
else if (msg.role === "assistant") {
|
|
const thinkingParts = [];
|
|
const toolCalls = [];
|
|
for (const block of msg.content) {
|
|
if (block.type === "thinking") {
|
|
thinkingParts.push(block.thinking);
|
|
}
|
|
else if (block.type === "toolCall") {
|
|
const args = block.arguments;
|
|
const argsStr = Object.entries(args)
|
|
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
.join(", ");
|
|
toolCalls.push(`${block.name}(${argsStr})`);
|
|
}
|
|
}
|
|
if (thinkingParts.length > 0) {
|
|
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
|
|
}
|
|
if (msg.content.some((block) => block.type === "text")) {
|
|
parts.push(`[Assistant]: ${contentText(msg.content)}`);
|
|
}
|
|
if (toolCalls.length > 0) {
|
|
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
}
|
|
}
|
|
else if (msg.role === "toolResult") {
|
|
const content = contentText(msg.content, "");
|
|
if (content) {
|
|
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
}
|
|
}
|
|
}
|
|
return parts.join("\n\n");
|
|
}
|
|
// ============================================================================
|
|
// Summarization System Prompt
|
|
// ============================================================================
|
|
export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.
|
|
|
|
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
|
|
//# sourceMappingURL=utils.js.map
|