diff --git a/dcp.schema.json b/dcp.schema.json index 28019dd0..080d5863 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -110,7 +110,7 @@ "description": "Tool names that should be protected from automatic pruning" }, "contextLimit": { - "description": "When session tokens exceed this limit, a compress nudge is injected (\"model\" uses the active model's context limit)", + "description": "When session tokens exceed this limit, a compress nudge is injected (\"model\" uses the active model's context limit, \"X%\" uses percentage of the model's context window)", "default": 100000, "oneOf": [ { @@ -119,6 +119,10 @@ { "type": "string", "enum": ["model"] + }, + { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?%$" } ] } diff --git a/lib/config.ts b/lib/config.ts index e2e6da61..5e2fa563 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -27,7 +27,7 @@ export interface ToolSettings { nudgeEnabled: boolean nudgeFrequency: number protectedTools: string[] - contextLimit: number | "model" + contextLimit: number | "model" | `${number}%` } export interface Tools { @@ -290,13 +290,16 @@ function validateConfigTypes(config: Record): ValidationError[] { }) } if (tools.settings.contextLimit !== undefined) { - if ( - typeof tools.settings.contextLimit !== "number" && - tools.settings.contextLimit !== "model" - ) { + const isValidNumber = typeof tools.settings.contextLimit === "number" + const isModelString = tools.settings.contextLimit === "model" + const isPercentString = + typeof tools.settings.contextLimit === "string" && + tools.settings.contextLimit.endsWith("%") + + if (!isValidNumber && !isModelString && !isPercentString) { errors.push({ key: "tools.settings.contextLimit", - expected: 'number | "model"', + expected: 'number | "model" | "${number}%"', actual: JSON.stringify(tools.settings.contextLimit), }) } diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index f1b56025..8a989f3e 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -13,6 +13,20 @@ import { getFilePathsFromParameters, isProtected } from "../protected-file-patte import { getLastUserMessage, isMessageCompacted } from "../shared-utils" import { getCurrentTokenUsage } from "../strategies/utils" +function parsePercentageString(value: string, total: number): number | undefined { + if (!value.endsWith("%")) return undefined + const percent = parseFloat(value.slice(0, -1)) + + if (isNaN(percent)) { + return undefined + } + + const roundedPercent = Math.round(percent) + const clampedPercent = Math.max(0, Math.min(100, roundedPercent)) + + return Math.round((clampedPercent / 100) * total) +} + // XML wrappers export const wrapPrunableTools = (content: string): string => { return ` @@ -54,9 +68,22 @@ Context management was just performed. Do NOT use the ${toolName} again. A fresh const resolveContextLimit = (config: PluginConfig, state: SessionState): number | undefined => { const configLimit = config.tools.settings.contextLimit - if (configLimit === "model") { - return state.modelContextLimit + + if (typeof configLimit === "string") { + if (configLimit.endsWith("%")) { + if (state.modelContextLimit === undefined) { + return undefined + } + return parsePercentageString(configLimit, state.modelContextLimit) + } + + if (configLimit === "model") { + return state.modelContextLimit + } + + return undefined } + return configLimit }