{"version":3,"file":"skillCatalog.cjs","sources":["../../../src/tools/skillCatalog.ts"],"sourcesContent":["// src/tools/skillCatalog.ts\nimport type { SkillCatalogEntry } from '@/types';\n\nconst HEADER = '## Available Skills';\nconst DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;\nconst DEFAULT_BUDGET_PERCENT = 0.01;\nconst DEFAULT_MAX_ENTRY_CHARS = 250;\nconst DEFAULT_MIN_DESC_LENGTH = 20;\nconst DEFAULT_CHARS_PER_TOKEN = 4;\n\nexport type SkillCatalogOptions = {\n  /** Total context window in tokens. Default: 200_000 */\n  contextWindowTokens?: number;\n  /** Fraction of context budget for catalog. Default: 0.01 (1%) */\n  budgetPercent?: number;\n  /** Max chars per entry description. Default: 250 */\n  maxEntryChars?: number;\n  /** Descriptions below this length trigger names-only fallback. Default: 20 */\n  minDescLength?: number;\n  /** Approximate chars per token for budget calculation. Default: 4 */\n  charsPerToken?: number;\n};\n\n/**\n * Formats a skill catalog for injection into agent context.\n * Uses a truncation ladder: full descriptions, proportional truncation, names-only.\n * Returns empty string for empty input.\n */\nexport function formatSkillCatalog(\n  skills: SkillCatalogEntry[],\n  opts?: SkillCatalogOptions\n): string {\n  if (skills.length === 0) return '';\n\n  const contextWindowTokens =\n    opts?.contextWindowTokens ?? DEFAULT_CONTEXT_WINDOW_TOKENS;\n  const budgetPercent = opts?.budgetPercent ?? DEFAULT_BUDGET_PERCENT;\n  const maxEntryChars = Math.max(\n    1,\n    opts?.maxEntryChars ?? DEFAULT_MAX_ENTRY_CHARS\n  );\n  const minDescLength = opts?.minDescLength ?? DEFAULT_MIN_DESC_LENGTH;\n  const charsPerToken = opts?.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;\n\n  const budgetChars = Math.floor(\n    contextWindowTokens * budgetPercent * charsPerToken\n  );\n\n  const capped = skills.map((s) => ({\n    name: s.name,\n    description:\n      s.description.length > maxEntryChars\n        ? s.description.slice(0, maxEntryChars - 1) + '\\u2026'\n        : s.description,\n  }));\n\n  const fullOutput = formatEntries(capped);\n  if (fullOutput.length <= budgetChars) return fullOutput;\n\n  const headerLen = HEADER.length + 2;\n  const newlineChars = capped.length > 1 ? capped.length - 1 : 0;\n  const availableChars = budgetChars - headerLen - newlineChars;\n  const perEntryOverhead = 4;\n  const nameCharsTotal = capped.reduce(\n    (sum, s) => sum + s.name.length + perEntryOverhead,\n    0\n  );\n  const availableForDescs = availableChars - nameCharsTotal;\n\n  if (availableForDescs <= 0) {\n    return fitNamesOnly(capped, budgetChars);\n  }\n\n  const maxDescPerEntry = Math.floor(availableForDescs / capped.length);\n\n  if (maxDescPerEntry < minDescLength) {\n    return fitNamesOnly(capped, budgetChars);\n  }\n\n  const truncated = capped.map((s) => ({\n    name: s.name,\n    description:\n      s.description.length > maxDescPerEntry\n        ? s.description.slice(0, maxDescPerEntry - 1) + '\\u2026'\n        : s.description,\n  }));\n\n  const result = formatEntries(truncated);\n  if (result.length <= budgetChars) return result;\n  return fitNamesOnly(capped, budgetChars);\n}\n\nfunction formatEntries(\n  entries: { name: string; description: string }[]\n): string {\n  const lines = entries.map((e) =>\n    e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`\n  );\n  return `${HEADER}\\n\\n${lines.join('\\n')}`;\n}\n\n/** Names-only fallback that drops trailing entries if the list still exceeds budget. */\nfunction fitNamesOnly(\n  entries: { name: string }[],\n  budgetChars: number\n): string {\n  // Format: \"HEADER\\n\\n- name1\\n- name2\\n...\"\n  // Running sum avoids O(n²) repeated string construction.\n  const prefix = HEADER.length + 2; // \"HEADER\\n\\n\"\n  const entryOverhead = 2; // \"- \"\n  let total = prefix;\n  let fitCount = 0;\n\n  for (let i = 0; i < entries.length; i++) {\n    const added = (i > 0 ? 1 : 0) + entryOverhead + entries[i].name.length;\n    if (total + added > budgetChars) break;\n    total += added;\n    fitCount = i + 1;\n  }\n\n  if (fitCount === 0) return '';\n  const namesOnly = entries\n    .slice(0, fitCount)\n    .map((s) => ({ name: s.name, description: '' }));\n  return formatEntries(namesOnly);\n}\n"],"names":[],"mappings":"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"}