import { readFileSync } from "node:fs"; import { join } from "node:path"; import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { clampThinkingLevel, type Model } from "@earendil-works/pi-ai"; import { getAgentDir, type ExtensionContext, } from "@earendil-works/pi-coding-agent"; export const REASONING_EFFORTS = [ "off", "minimal", "low", "medium", "high", "xhigh", "max", ] as const satisfies readonly ThinkingLevel[]; export type ReasoningEffort = (typeof REASONING_EFFORTS)[number]; export interface CompactionConfig { model?: string; reasoningEffort?: ReasoningEffort; } export interface LoadedCompactionConfig { config: CompactionConfig; warning?: string; } export interface ResolvedCompactionTarget { model: Model; reasoningEffort: ThinkingLevel; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function parseCompactionConfig(text: string): LoadedCompactionConfig { if (!text.trim()) return { config: {} }; let value: unknown; try { value = JSON.parse(text); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { config: {}, warning: `Could not parse compaction.json: ${message}` }; } if (!isRecord(value)) { return { config: {}, warning: "compaction.json must contain a JSON object." }; } const config: CompactionConfig = {}; if (value.model !== undefined) { if (typeof value.model !== "string" || !parseModelSelector(value.model.trim())) { return { config: {}, warning: "compaction.json model must be a non-empty provider/model string." }; } config.model = value.model.trim(); } if (value.reasoningEffort !== undefined) { if ( typeof value.reasoningEffort !== "string" || !REASONING_EFFORTS.includes(value.reasoningEffort as ReasoningEffort) ) { return { config: {}, warning: `compaction.json reasoningEffort must be one of: ${REASONING_EFFORTS.join(", ")}.`, }; } config.reasoningEffort = value.reasoningEffort as ReasoningEffort; } return { config }; } export function compactionConfigPath(): string { return join(getAgentDir(), "compaction.json"); } export function loadCompactionConfig(path = compactionConfigPath()): LoadedCompactionConfig { try { return parseCompactionConfig(readFileSync(path, "utf8")); } catch (error) { const code = isRecord(error) ? error.code : undefined; if (code === "ENOENT") return { config: {} }; const message = error instanceof Error ? error.message : String(error); return { config: {}, warning: `Could not read compaction.json: ${message}` }; } } export function hasConfiguredOverride(config: CompactionConfig): boolean { return config.model !== undefined || config.reasoningEffort !== undefined; } export function parseModelSelector(selector: string): { provider: string; modelId: string } | undefined { const slash = selector.indexOf("/"); if (slash <= 0 || slash === selector.length - 1) return undefined; return { provider: selector.slice(0, slash), modelId: selector.slice(slash + 1) }; } export function resolveConfiguredTarget( config: CompactionConfig, ctx: Pick, ): ResolvedCompactionTarget { const currentModel = ctx.model; let model = currentModel; if (config.model) { const selector = parseModelSelector(config.model); if (!selector) throw new Error(`Invalid compaction model selector: ${config.model}`); model = ctx.modelRegistry.find(selector.provider, selector.modelId); if (!model) throw new Error(`Compaction model not found: ${config.model}`); } if (!model) throw new Error("No model is selected for compaction."); const requested = config.reasoningEffort ?? ctx.thinkingLevel ?? "off"; return { model, reasoningEffort: clampThinkingLevel(model, requested) as ThinkingLevel, }; } export function resolveCurrentTarget( ctx: Pick, ): ResolvedCompactionTarget { if (!ctx.model) throw new Error("No current model is selected for compaction."); return { model: ctx.model, reasoningEffort: clampThinkingLevel(ctx.model, ctx.thinkingLevel ?? "off") as ThinkingLevel, }; }