78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
/** Configuration resolution for deterministic tool-result pruning. */
|
|
|
|
import { deepFreeze } from '@deepseek-ai/dsh-llm'
|
|
import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts'
|
|
|
|
/** Fixed marker substituted for every removed middle span. */
|
|
export const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'
|
|
|
|
/** Low-friction defaults for coding-agent tool output. */
|
|
export const DEFAULTS: ResolvedConfig = deepFreeze({
|
|
thresholdChars: 8192,
|
|
headChars: 4096,
|
|
tailChars: 1024,
|
|
})
|
|
|
|
const CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
'thresholdChars',
|
|
'headChars',
|
|
'tailChars',
|
|
])
|
|
|
|
/**
|
|
* Count Unicode code points without splitting surrogate pairs.
|
|
* @param text - text to measure.
|
|
* @returns the Unicode code-point count.
|
|
*/
|
|
export function codePointLength(text: string): number {
|
|
return Array.from(text).length
|
|
}
|
|
|
|
/**
|
|
* Resolve and validate pruning budgets.
|
|
* @param config - raw plugin configuration.
|
|
* @returns a detached deeply immutable configuration.
|
|
*/
|
|
export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig {
|
|
for (const key of Object.keys(config)) {
|
|
if (!CONFIG_KEYS.has(key)) {
|
|
throw new Error(
|
|
`ToolResultPruneConfig: unknown key "${key}" `
|
|
+ '(allowed: thresholdChars, headChars, tailChars)',
|
|
)
|
|
}
|
|
}
|
|
|
|
const resolved: ResolvedConfig = {
|
|
thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars,
|
|
headChars: config.headChars ?? DEFAULTS.headChars,
|
|
tailChars: config.tailChars ?? DEFAULTS.tailChars,
|
|
}
|
|
assertPositiveInteger('thresholdChars', resolved.thresholdChars)
|
|
assertNonNegativeInteger('headChars', resolved.headChars)
|
|
assertNonNegativeInteger('tailChars', resolved.tailChars)
|
|
|
|
const emittedChars = resolved.headChars
|
|
+ codePointLength(PRUNE_MARKER)
|
|
+ resolved.tailChars
|
|
if (emittedChars > resolved.thresholdChars) {
|
|
throw new Error(
|
|
`ToolResultPruneConfig: headChars + marker + tailChars (${emittedChars}) `
|
|
+ `must be at most thresholdChars (${resolved.thresholdChars})`,
|
|
)
|
|
}
|
|
return deepFreeze(structuredClone(resolved))
|
|
}
|
|
|
|
function assertPositiveInteger(name: string, value: number): void {
|
|
if (!Number.isInteger(value) || value <= 0) {
|
|
throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a positive integer`)
|
|
}
|
|
}
|
|
|
|
function assertNonNegativeInteger(name: string, value: number): void {
|
|
if (!Number.isInteger(value) || value < 0) {
|
|
throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a non-negative integer`)
|
|
}
|
|
}
|