import type { Context, Message, Tool } from "@earendil-works/pi-ai"; // Pi <= 0.85 hands providers a Context carrying `systemPrompt` and `tools` // directly. Pi 0.86+ folds both into a leading `system` message (later system // messages may append prompt text, patch named sections, or add/remove tools) // and passes only `{ messages }`. Providers that still read `context.tools` // silently send no tools, so the model "calls" tools in prose and nothing runs. // // This replays whichever shape we were given into the flat form the request // builders use. It mirrors pi-ai's getCurrentSystemPrompt/getCurrentTools/ // collapseSystemMessages without importing them, so the extension still loads // on Pi versions where those exports do not exist. export interface RequestContext { /** Base prompt plus any sections, joined the way pi-ai renders them. */ systemPrompt: string; /** Tools available after applying every transcript delta in order. */ tools: Tool[]; /** The transcript without system messages. */ messages: Message[]; } interface TranscriptSystemMessage { role: "system"; content: string | { type: string; text?: string }[]; sections?: Record; toolsAdded?: Tool[]; toolsRemoved?: { name: string }[]; } function isSystemMessage(message: { role: string }): message is TranscriptSystemMessage { return message.role === "system"; } function contentText(content: TranscriptSystemMessage["content"]): string { if (typeof content === "string") return content; return content .filter((part) => part.type === "text") .map((part) => part.text ?? "") .join(""); } export function resolveRequestContext(context: Context): RequestContext { const promptParts: string[] = []; const sections = new Map(); const tools = new Map(); const messages: Message[] = []; if (context.systemPrompt && context.systemPrompt.length > 0) promptParts.push(context.systemPrompt); for (const tool of context.tools ?? []) tools.set(tool.name, tool); for (const message of context.messages as readonly { role: string }[]) { if (!isSystemMessage(message)) { messages.push(message as Message); continue; } const text = contentText(message.content); if (text.length > 0) promptParts.push(text); for (const [name, value] of Object.entries(message.sections ?? {})) { if (value === null) sections.delete(name); else sections.set(name, value); } for (const tool of message.toolsRemoved ?? []) tools.delete(tool.name); for (const tool of message.toolsAdded ?? []) tools.set(tool.name, tool); } const systemPrompt = [...promptParts, ...sections.values()].filter((part) => part.length > 0).join("\n\n"); return { systemPrompt, tools: [...tools.values()], messages }; } // The Responses converter is loaded at runtime from whichever pi-ai copy // loadResponsesShared finds, which in a dev checkout can differ from the Pi // that built the context. A <= 0.85 converter reads `systemPrompt`/`tools` // and ignores system-role messages; a 0.86+ converter reads the leading // system message and ignores the top-level fields. Build a context that // carries both so either converter produces a complete request. export function toConverterContext(context: Context): Context { const request = resolveRequestContext(context); const first = context.messages[0] as { role: string } | undefined; const needsLeadingSystem = first?.role !== "system" && (request.systemPrompt.length > 0 || request.tools.length > 0); const leading = { role: "system", content: request.systemPrompt, ...(request.tools.length > 0 ? { toolsAdded: request.tools } : {}), timestamp: 0, } as unknown as Message; return { systemPrompt: request.systemPrompt, tools: request.tools, messages: needsLeadingSystem ? [leading, ...context.messages] : context.messages, }; }