mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 15:03:31 +00:00
- New utils.ts with shared functions: - FileOperations type and createFileOps() - extractFileOpsFromMessage() - computeFileLists() - formatFileOperations() - serializeConversation() - SUMMARIZATION_SYSTEM_PROMPT - branch-summarization.ts now uses: - Serialization approach (conversation as text, not LLM messages) - completeSimple with system prompt - Shared utility functions
154 lines
5 KiB
TypeScript
154 lines
5 KiB
TypeScript
/**
|
|
* Shared utilities for compaction and branch summarization.
|
|
*/
|
|
|
|
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
|
import type { Message } from "@mariozechner/pi-ai";
|
|
|
|
// ============================================================================
|
|
// File Operation Tracking
|
|
// ============================================================================
|
|
|
|
export interface FileOperations {
|
|
read: Set<string>;
|
|
written: Set<string>;
|
|
edited: Set<string>;
|
|
}
|
|
|
|
export function createFileOps(): FileOperations {
|
|
return {
|
|
read: new Set(),
|
|
written: new Set(),
|
|
edited: new Set(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract file operations from tool calls in an assistant message.
|
|
*/
|
|
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
|
|
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 as Record<string, unknown> | undefined;
|
|
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: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
|
|
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: string[], modifiedFiles: string[]): string {
|
|
const sections: string[] = [];
|
|
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
|
|
// ============================================================================
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
export function serializeConversation(messages: Message[]): string {
|
|
const parts: string[] = [];
|
|
|
|
for (const msg of messages) {
|
|
if (msg.role === "user") {
|
|
const content =
|
|
typeof msg.content === "string"
|
|
? msg.content
|
|
: msg.content
|
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
.map((c) => c.text)
|
|
.join("");
|
|
if (content) parts.push(`[User]: ${content}`);
|
|
} else if (msg.role === "assistant") {
|
|
const textParts: string[] = [];
|
|
const thinkingParts: string[] = [];
|
|
const toolCalls: string[] = [];
|
|
|
|
for (const block of msg.content) {
|
|
if (block.type === "text") {
|
|
textParts.push(block.text);
|
|
} else if (block.type === "thinking") {
|
|
thinkingParts.push(block.thinking);
|
|
} else if (block.type === "toolCall") {
|
|
const args = block.arguments as Record<string, unknown>;
|
|
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 (textParts.length > 0) {
|
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
}
|
|
if (toolCalls.length > 0) {
|
|
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
}
|
|
} else if (msg.role === "toolResult") {
|
|
const content = msg.content
|
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
.map((c) => c.text)
|
|
.join("");
|
|
if (content) {
|
|
parts.push(`[Tool result]: ${content}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 coding 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.`;
|