feat: add context budget utility
This commit is contained in:
@@ -1 +1,105 @@
|
||||
// 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user