import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import { getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent"; import type { ApplyMode, ExposureMode, TreeLabelerSettings } from "./types.ts"; export const DEFAULT_PROMPT = `Curate sparse navigation labels for this Pi conversation tree. Label only genuine landmarks: branch roots, decisions, checkpoints, outcomes, and useful endpoints. Prefer concrete labels that tell a future reader what happened on that path. Do not label routine turns. Zero labels is a valid result. Avoid generic labels such as “Discussion”, “Implementation”, or “Progress”. Never propose replacing an entry marked as manually labeled.`; const DEFAULTS: TreeLabelerSettings = { enabled: true, model: "current", thinkingLevel: "off", prompt: DEFAULT_PROMPT, exposure: "balanced", applyMode: "review", automation: { enabled: false, afterTurns: 5, onBranchCreated: true, onTreeNavigation: true, onCompaction: true, }, sessionNaming: { enabled: true, fallbackToMessage: true, maxChars: 80, }, autoLimits: true, offerLimitIncreases: true, maxNodes: 400, excerptChars: 420, maxInputChars: 48_000, maxLabels: 12, maxLabelChars: 48, outputTokens: 1400, timeoutMs: 30_000, includeToolNames: true, persistPayloads: false, }; type JsonObject = Record; function object(value: unknown): JsonObject { return value && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : {}; } function text(value: unknown, fallback: string): string { return typeof value === "string" && value.trim() ? value.trim() : fallback; } function integer(value: unknown, fallback: number, min: number, max: number): number { return typeof value === "number" && Number.isInteger(value) ? Math.min(max, Math.max(min, value)) : fallback; } function bool(value: unknown, fallback: boolean): boolean { return typeof value === "boolean" ? value : fallback; } function oneOf(value: unknown, choices: readonly T[], fallback: T): T { return typeof value === "string" && choices.includes(value as T) ? value as T : fallback; } function merge(globalValue: JsonObject, projectValue: JsonObject): JsonObject { return { ...globalValue, ...projectValue }; } export function resolveSettings(globalRootValue: unknown, projectRootValue: unknown = {}): TreeLabelerSettings { const globalRoot = object(globalRootValue); const projectRoot = object(projectRootValue); const globalTreeLabeler = object(globalRoot.treeLabeler); const projectTreeLabeler = object(projectRoot.treeLabeler); const raw = merge(globalTreeLabeler, projectTreeLabeler); const automation = merge(object(globalTreeLabeler.automation), object(projectTreeLabeler.automation)); const sessionNaming = merge(object(globalTreeLabeler.sessionNaming), object(projectTreeLabeler.sessionNaming)); return { enabled: bool(raw.enabled, DEFAULTS.enabled), model: text(raw.model, DEFAULTS.model), thinkingLevel: oneOf(raw.thinkingLevel, ["off", "minimal", "low", "medium", "high", "xhigh", "max"], DEFAULTS.thinkingLevel), prompt: text(raw.prompt, DEFAULTS.prompt), exposure: oneOf(raw.exposure, ["balanced", "tree", "branch", "neighborhood"], DEFAULTS.exposure), applyMode: oneOf(raw.applyMode, ["review", "auto", "manual"], DEFAULTS.applyMode), automation: { enabled: bool(automation.enabled, DEFAULTS.automation.enabled), afterTurns: integer(automation.afterTurns, DEFAULTS.automation.afterTurns, 1, 100), onBranchCreated: bool(automation.onBranchCreated, DEFAULTS.automation.onBranchCreated), onTreeNavigation: bool(automation.onTreeNavigation, DEFAULTS.automation.onTreeNavigation), onCompaction: bool(automation.onCompaction, DEFAULTS.automation.onCompaction), }, sessionNaming: { enabled: bool(sessionNaming.enabled, DEFAULTS.sessionNaming.enabled), fallbackToMessage: bool(sessionNaming.fallbackToMessage, DEFAULTS.sessionNaming.fallbackToMessage), maxChars: integer(sessionNaming.maxChars, DEFAULTS.sessionNaming.maxChars, 20, 120), }, autoLimits: bool(raw.autoLimits, DEFAULTS.autoLimits), offerLimitIncreases: bool(raw.offerLimitIncreases, DEFAULTS.offerLimitIncreases), maxNodes: integer(raw.maxNodes, DEFAULTS.maxNodes, 20, 10000), excerptChars: integer(raw.excerptChars, DEFAULTS.excerptChars, 80, 10000), maxInputChars: integer(raw.maxInputChars, DEFAULTS.maxInputChars, 8000, 3000000), maxLabels: integer(raw.maxLabels, DEFAULTS.maxLabels, 1, 200), maxLabelChars: integer(raw.maxLabelChars, DEFAULTS.maxLabelChars, 12, 200), outputTokens: raw.outputTokens === null ? undefined : integer(raw.outputTokens, DEFAULTS.outputTokens!, 256, 64000), timeoutMs: integer(raw.timeoutSecs, DEFAULTS.timeoutMs / 1000, 5, 1800) * 1000, includeToolNames: bool(raw.includeToolNames, DEFAULTS.includeToolNames), persistPayloads: bool(raw.persistPayloads, DEFAULTS.persistPayloads), }; } export type TreeLabelerSettingSource = "project" | "global" | "default"; export function settingSource(cwd: string, projectTrusted: boolean, key: string): TreeLabelerSettingSource { const manager = SettingsManager.create(cwd, getAgentDir(), { projectTrusted }); const project = object(object(manager.getProjectSettings()).treeLabeler); const global = object(object(manager.getGlobalSettings()).treeLabeler); if (projectTrusted && Object.prototype.hasOwnProperty.call(project, key)) return "project"; if (Object.prototype.hasOwnProperty.call(global, key)) return "global"; return "default"; } export function loadSettings(cwd: string, projectTrusted: boolean): TreeLabelerSettings { const manager = SettingsManager.create(cwd, getAgentDir(), { projectTrusted }); return resolveSettings( manager.getGlobalSettings(), projectTrusted ? manager.getProjectSettings() : {}, ); }