config.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. /** Configuration resolution for deterministic tool-result pruning. */
  2. import { deepFreeze } from '@deepseek-ai/dsh-llm'
  3. import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts'
  4. /** Fixed marker substituted for every removed middle span. */
  5. export const PRUNE_MARKER = '\n\n[... tool result middle pruned ...]\n\n'
  6. /** Low-friction defaults for coding-agent tool output. */
  7. export const DEFAULTS: ResolvedConfig = deepFreeze({
  8. thresholdChars: 8192,
  9. headChars: 4096,
  10. tailChars: 1024,
  11. })
  12. const CONFIG_KEYS: ReadonlySet<string> = new Set([
  13. 'thresholdChars',
  14. 'headChars',
  15. 'tailChars',
  16. ])
  17. /**
  18. * Count Unicode code points without splitting surrogate pairs.
  19. * @param text - text to measure.
  20. * @returns the Unicode code-point count.
  21. */
  22. export function codePointLength(text: string): number {
  23. return Array.from(text).length
  24. }
  25. /**
  26. * Resolve and validate pruning budgets.
  27. * @param config - raw plugin configuration.
  28. * @returns a detached deeply immutable configuration.
  29. */
  30. export function resolveConfig(config: ToolResultPruneConfig = {}): ResolvedConfig {
  31. for (const key of Object.keys(config)) {
  32. if (!CONFIG_KEYS.has(key)) {
  33. throw new Error(
  34. `ToolResultPruneConfig: unknown key "${key}" `
  35. + '(allowed: thresholdChars, headChars, tailChars)',
  36. )
  37. }
  38. }
  39. const resolved: ResolvedConfig = {
  40. thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars,
  41. headChars: config.headChars ?? DEFAULTS.headChars,
  42. tailChars: config.tailChars ?? DEFAULTS.tailChars,
  43. }
  44. assertPositiveInteger('thresholdChars', resolved.thresholdChars)
  45. assertNonNegativeInteger('headChars', resolved.headChars)
  46. assertNonNegativeInteger('tailChars', resolved.tailChars)
  47. const emittedChars = resolved.headChars
  48. + codePointLength(PRUNE_MARKER)
  49. + resolved.tailChars
  50. if (emittedChars > resolved.thresholdChars) {
  51. throw new Error(
  52. `ToolResultPruneConfig: headChars + marker + tailChars (${emittedChars}) `
  53. + `must be at most thresholdChars (${resolved.thresholdChars})`,
  54. )
  55. }
  56. return deepFreeze(structuredClone(resolved))
  57. }
  58. function assertPositiveInteger(name: string, value: number): void {
  59. if (!Number.isInteger(value) || value <= 0) {
  60. throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a positive integer`)
  61. }
  62. }
  63. function assertNonNegativeInteger(name: string, value: number): void {
  64. if (!Number.isInteger(value) || value < 0) {
  65. throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a non-negative integer`)
  66. }
  67. }