import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead, type AgentToolResult, } from "@earendil-works/pi-coding-agent"; import { Value } from "typebox/value"; import type { LspSessionFiles } from "./lsp-session-files.js"; import { LspToolResultDetailsSchema, type LspToolResultDetails } from "./lsp-tool-contract.js"; // oxlint-disable-next-line anti-slop/no-unknown-parameters, anti-slop/no-unknown-returns -- Recursive protocol output remains opaque until JSON.stringify; only container structure is inspected here. function deterministicLspValue(value: unknown): unknown { if (Array.isArray(value)) return value.map(deterministicLspValue); if (value instanceof Map) { const entries: [unknown, unknown][] = [...value.entries()]; return entries .sort(([left], [right]) => String(left).localeCompare(String(right))) .map(([key, entryValue]) => [key, deterministicLspValue(entryValue)]); } // oxlint-disable-next-line anti-slop/no-runtime-typeof -- Protocol output is recursively normalized at this rendering boundary. if (typeof value !== "object" || value === null) return value; return Object.fromEntries( Object.entries(value) .filter(([, entryValue]) => entryValue !== undefined) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entryValue]) => [key, deterministicLspValue(entryValue)]), ); } /** Render a protocol result as stable compact JSON while retaining readable URI strings. */ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Protocol results are serialized without promising a method-specific payload contract. export function formatLspToolValue(value: unknown): string { const text = JSON.stringify(deterministicLspValue(value)); return text === undefined ? "null" : text; } /** Truncate model-visible text against Pi limits, spilling the complete text when truncated. */ export async function truncateLspOutputText( text: string, sessionFiles: LspSessionFiles, subject: string, ): Promise<{ readonly text: string; readonly spillPath?: string }> { const truncation = truncateHead(text, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES, }); if (!truncation.truncated) return { text }; const spillPath = await sessionFiles.writeResultSpill(text); return { text: `${truncation.content}\n\n[Pi LSP: ${subject} truncated; complete Result Spill: ${spillPath}]`, spillPath, }; } /** Validate normalized details, truncate model-visible text, and spill every complete oversized result. */ export async function createLspToolOutput( text: string, details: LspToolResultDetails, sessionFiles: LspSessionFiles, ): Promise> { const normalizedDetails = Value.Parse(LspToolResultDetailsSchema, details); const truncated = await truncateLspOutputText(text, sessionFiles, "output"); if (truncated.spillPath === undefined) { return { content: [{ type: "text", text }], details: normalizedDetails }; } const detailsWithSpill = normalizedDetails.kind === "operation" ? Value.Parse(LspToolResultDetailsSchema, { ...normalizedDetails, spill_path: truncated.spillPath, }) : normalizedDetails; return { content: [{ type: "text", text: truncated.text }], details: detailsWithSpill, }; }