// Context size limit and trimming logic. /** * Validates and trims input to stay within character budget. * * @param {{ question: string, context?: string, projectSummary?: string, taskSummary?: string, relevantFiles?: Array<{ path: string, content: string }>, logs?: string }} input * @param {{ maxInputChars: number, maxFileChars: number, maxFiles: number, maxLogChars: number }} config * @returns {{ ok: true, input: object, warnings: string[] } | { ok: false, error: string, warnings: string[], input: null }} */ export function checkContextBudget(input, config) { if (!input || typeof input.question !== 'string' || input.question.length === 0) { throw new Error('Validation error: question is required.'); } const warnings = []; const files = input.relevantFiles ? [...input.relevantFiles] : []; let logs = input.logs ?? ''; let context = input.context ?? ''; let projectSummary = input.projectSummary ?? ''; let taskSummary = input.taskSummary ?? ''; // Step 1 — Enforce hard limits (maxFiles, maxFileChars, maxLogChars) if (files.length > config.maxFiles) { warnings.push(`Only the first ${config.maxFiles} relevant files were kept. (maxFiles=${config.maxFiles})`); files.splice(config.maxFiles); } for (const file of files) { if (file.content && file.content.length > config.maxFileChars) { warnings.push(`File ${file.path || 'unknown'} was truncated to ${config.maxFileChars} characters. (maxFileChars=${config.maxFileChars})`); file.content = file.content.slice(0, config.maxFileChars); } } if (logs.length > config.maxLogChars) { warnings.push(`Logs were truncated to ${config.maxLogChars} characters. (maxLogChars=${config.maxLogChars})`); logs = logs.slice(0, config.maxLogChars); } // Helper: count all input characters function countInputChars() { let total = input.question.length; if (context) total += context.length; if (projectSummary) total += projectSummary.length; if (taskSummary) total += taskSummary.length; if (logs) total += logs.length; for (const file of files) { total += file.content ? file.content.length : 0; } return total; } // Step 2 — If still over global limit, trim droppable sections let remaining = countInputChars(); if (remaining > config.maxInputChars && context) { const allowed = Math.max(0, config.maxInputChars - remaining + context.length); if (allowed < context.length) { context = context.slice(0, allowed); warnings.push(`Context was trimmed to ${allowed} characters. (budget exceeded)`); remaining = countInputChars(); } } if (remaining > config.maxInputChars && projectSummary) { const allowed = Math.max(0, config.maxInputChars - remaining + projectSummary.length); if (allowed < projectSummary.length) { projectSummary = projectSummary.slice(0, allowed); warnings.push(`Project summary was trimmed to ${allowed} characters. (budget exceeded)`); remaining = countInputChars(); } } if (remaining > config.maxInputChars && taskSummary) { const allowed = Math.max(0, config.maxInputChars - remaining + taskSummary.length); if (allowed < taskSummary.length) { taskSummary = taskSummary.slice(0, allowed); warnings.push(`Task summary was trimmed to ${allowed} characters. (budget exceeded)`); remaining = countInputChars(); } } // Step 3 — Final check: if question alone or question + hard-limited files/logs exceeds limit, reject remaining = countInputChars(); if (remaining > config.maxInputChars) { return { ok: false, error: 'Input is too large for a focused review. Please retry with only the specific files, diff, or error section relevant to the question.', warnings, input: null, }; } // Build trimmed input object (only include non-empty optional fields) const trimmedInput = { question: input.question }; if (context.length > 0) trimmedInput.context = context; if (projectSummary.length > 0) trimmedInput.projectSummary = projectSummary; if (taskSummary.length > 0) trimmedInput.taskSummary = taskSummary; if (files.length > 0) trimmedInput.relevantFiles = files; if (logs.length > 0) trimmedInput.logs = logs; return { ok: true, input: trimmedInput, warnings }; }