import type { Context, Message, TextContent, ImageContent, ToolCall, } from "@earendil-works/pi-ai"; import { CursorLiteError } from "./error.js"; /** Union of content part types that can appear in a message. */ type ContentPart = TextContent | ImageContent | ToolCall | { type: "thinking"; thinking: string; [key: string]: any }; /** * Build a deterministic XML prompt envelope for the Cursor Agent. * Encodes system prompt and text messages. Rejects images and unknown parts. */ export function buildPiContextEnvelope(context: Context): string { const parts: string[] = []; // System prompt if (context.systemPrompt) { parts.push(`\n${escapeXml(context.systemPrompt)}\n`); } // Messages (user, assistant, toolResult) for (const msg of context.messages) { const encoded = encodeMessage(msg); if (encoded) parts.push(encoded); } return parts.join("\n\n"); } function encodeMessage(msg: Message): string | null { switch (msg.role) { case "user": { const text = extractTextOnly(msg.content); if (!text) return null; return `\n${escapeXml(text)}\n`; } case "assistant": { const text = extractAssistantText(msg.content); if (!text) return null; return `\n${escapeXml(text)}\n`; } case "toolResult": { const text = extractTextOnly(msg.content); const header = msg.isError ? "tool_result_error" : "tool_result"; return `<${header} callId="${escapeXml(msg.toolCallId)}">\n${escapeXml(text)}\n`; } default: return null; } } function extractTextOnly(content: string | ContentPart[]): string { if (typeof content === "string") return content; const texts: string[] = []; for (const part of content) { if (part.type === "text") { texts.push(part.text); } else if (part.type === "image") { throw new CursorLiteError("INPUT", "prepare", "Image content is not supported in V1"); } else { throw new CursorLiteError("INPUT", "prepare", `Unsupported content part type: ${(part as any).type}`); } } return texts.join("\n"); } function extractAssistantText(content: ContentPart[]): string { const texts: string[] = []; for (const part of content) { if (part.type === "text") { texts.push(part.text); } else if (part.type === "thinking") { texts.push(`[thinking: ${part.thinking.slice(0, 200)}${part.thinking.length > 200 ? "..." : ""}]`); } else if (part.type === "toolCall") { texts.push(`[tool call: ${part.name}]`); } } return texts.join("\n"); } function escapeXml(s: string): string { return s .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }