import { createHash } from "node:crypto"; import type { AssistantMessage, Context, Message } from "@earendil-works/pi-ai"; import { isRecord } from "./types.js"; function contentToText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .map((block) => { if (!isRecord(block)) return ""; if (block.type === "text" && typeof block.text === "string") return block.text; if (block.type === "image") return "[image omitted: Grok Build ACP text provider]"; if (block.type === "toolCall") { return `[Pi tool call: ${String(block.name ?? "unknown")} ${JSON.stringify(block.arguments ?? {})}]`; } return ""; }) .filter((part) => part.length > 0) .join("\n"); } function messageText(message: Message): string { return contentToText(message.content); } function toolName(message: Message): string { return "toolName" in message && typeof message.toolName === "string" ? message.toolName : "tool"; } export function buildFullPrompt(context: Context): string { const parts: string[] = []; if (context.systemPrompt?.trim()) parts.push(`system:\n${context.systemPrompt.trim()}`); for (const message of context.messages) { const text = messageText(message); if (message.role === "user") parts.push(`user:\n${text}`); else if (message.role === "assistant") parts.push(`assistant:\n${text}`); else if (message.role === "toolResult") parts.push(`tool result (${toolName(message)}):\n${text}`); } return parts.filter((part) => part.trim().length > 0).join("\n\n"); } export function buildIncrementalPrompt(context: Context): string { let lastAssistant = -1; for (let index = context.messages.length - 1; index >= 0; index--) { if (context.messages[index]?.role === "assistant") { lastAssistant = index; break; } } const parts: string[] = []; for (const message of context.messages.slice(lastAssistant + 1)) { const text = messageText(message); if (message.role === "user") parts.push(text); else if (message.role === "toolResult") parts.push(`tool result (${toolName(message)}):\n${text}`); } return parts.filter((part) => part.trim().length > 0).join("\n\n") || buildFullPrompt(context); } export function fingerprintMessages(messages: readonly Message[]): string { const hash = createHash("sha256"); for (const message of messages) { hash.update(message.role); hash.update("\0"); hash.update(messageText(message)); hash.update("\0"); } return hash.digest("hex"); } export function currentAssistantPrefixFingerprint(context: Context): string | undefined { let lastAssistant = -1; for (let index = context.messages.length - 1; index >= 0; index--) { if (context.messages[index]?.role === "assistant") { lastAssistant = index; break; } } return lastAssistant >= 0 ? fingerprintMessages(context.messages.slice(0, lastAssistant + 1)) : undefined; } export function expectedNextPrefixFingerprint(context: Context, output: AssistantMessage): string { return fingerprintMessages([...context.messages, output]); }