{"version":3,"file":"truncation.cjs","sources":["../../../src/utils/truncation.ts"],"sourcesContent":["/**\n * Ingestion-time and pre-flight truncation utilities for tool results.\n *\n * Prevents oversized tool outputs from entering the message array and\n * consuming the entire context window.\n */\n\n/**\n * Absolute hard cap on tool result length (characters).\n * Even if the model has a 1M-token context, a single tool result\n * larger than this is almost certainly a bug (e.g., dumping a binary file).\n */\nexport const HARD_MAX_TOOL_RESULT_CHARS = 400_000;\n\n/**\n * Absolute hard cap on the aggregate size (characters) of all registered\n * tool outputs kept for `{{tool<i>turn<n>}}` substitution. Set at 5 MB\n * because the registry stores *raw, untruncated* tool output — full\n * fidelity for piping into downstream bash/jq — so the budget needs\n * enough headroom to keep a handful of large responses without\n * ballooning unbounded.\n */\nexport const HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE = 5_000_000;\n\n/**\n * Computes the dynamic max tool result size based on the model's context window.\n * Uses 30% of the context window (in estimated characters, ~4 chars/token)\n * capped at HARD_MAX_TOOL_RESULT_CHARS.\n *\n * @param contextWindowTokens - The model's max context tokens (optional).\n * @returns Maximum allowed characters for a single tool result.\n */\nexport function calculateMaxToolResultChars(\n  contextWindowTokens?: number\n): number {\n  if (contextWindowTokens == null || contextWindowTokens <= 0) {\n    return HARD_MAX_TOOL_RESULT_CHARS;\n  }\n  return Math.min(\n    Math.floor(contextWindowTokens * 0.3) * 4,\n    HARD_MAX_TOOL_RESULT_CHARS\n  );\n}\n\n/**\n * Computes the default aggregate size (characters) for the tool output\n * reference registry based on the per-output budget. Mirrors\n * `calculateMaxToolResultChars`'s shape: a multiple of the per-output\n * cap, clamped to `HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE`.\n *\n * @param maxOutputSize - Per-output maximum characters (e.g., the\n *   ToolNode's `maxToolResultChars`). When omitted or non-positive,\n *   falls back to the absolute total cap.\n * @returns Maximum total characters retained across the registry.\n */\nexport function calculateMaxTotalToolOutputSize(\n  maxOutputSize?: number\n): number {\n  if (maxOutputSize == null || maxOutputSize <= 0) {\n    return HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE;\n  }\n  return Math.min(maxOutputSize * 2, HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE);\n}\n\n/**\n * Truncates a tool-call input (the arguments/payload of a tool_use block)\n * using head+tail strategy. Returns an object with `_truncated` (the\n * truncated string) and `_originalChars` (for diagnostics).\n *\n * Accepts any type — objects are JSON-serialized before truncation.\n *\n * @param input - The tool input (string, object, etc.).\n * @param maxChars - Maximum allowed characters.\n */\nexport function truncateToolInput(\n  input: unknown,\n  maxChars: number\n): { _truncated: string; _originalChars: number } {\n  const serialized = typeof input === 'string' ? input : JSON.stringify(input);\n  if (serialized.length <= maxChars) {\n    return { _truncated: serialized, _originalChars: serialized.length };\n  }\n  const indicator = `\\n… [truncated: ${serialized.length} chars exceeded ${maxChars} limit] …\\n`;\n  const available = maxChars - indicator.length;\n\n  if (available < 100) {\n    return {\n      _truncated: serialized.slice(0, maxChars) + indicator.trimEnd(),\n      _originalChars: serialized.length,\n    };\n  }\n\n  const headSize = Math.ceil(available * 0.7);\n  const tailSize = available - headSize;\n\n  return {\n    _truncated:\n      serialized.slice(0, headSize) +\n      indicator +\n      serialized.slice(serialized.length - tailSize),\n    _originalChars: serialized.length,\n  };\n}\n\n/**\n * Truncates tool result content that exceeds `maxChars` using a head+tail\n * strategy. Keeps the beginning (structure/headers) and end (return value /\n * conclusion) of the content so the model retains both the opening context\n * and the final outcome.\n *\n * Head gets ~70% of the budget, tail gets ~30%. Falls back to head-only\n * when the budget is too small for a meaningful tail.\n *\n * @param content - The tool result string content.\n * @param maxChars - Maximum allowed characters.\n * @returns The (possibly truncated) content string.\n */\nexport function truncateToolResultContent(\n  content: string,\n  maxChars: number\n): string {\n  if (content.length <= maxChars) {\n    return content;\n  }\n\n  const indicator = `\\n\\n… [truncated: ${content.length} chars exceeded ${maxChars} limit] …\\n\\n`;\n  const available = maxChars - indicator.length;\n  if (available <= 0) {\n    return content.slice(0, maxChars);\n  }\n\n  // When budget is too small for a meaningful tail, fall back to head-only\n  if (available < 200) {\n    return content.slice(0, available) + indicator.trimEnd();\n  }\n\n  const headSize = Math.ceil(available * 0.7);\n  const tailSize = available - headSize;\n\n  // Try to break at newline boundaries for cleaner output\n  let headEnd = headSize;\n  const headNewline = content.lastIndexOf('\\n', headSize);\n  if (headNewline > headSize - 200 && headNewline > 0) {\n    headEnd = headNewline;\n  }\n\n  let tailStart = content.length - tailSize;\n  const tailNewline = content.indexOf('\\n', tailStart);\n  if (tailNewline > 0 && tailNewline < tailStart + 200) {\n    tailStart = tailNewline + 1;\n  }\n\n  return content.slice(0, headEnd) + indicator + content.slice(tailStart);\n}\n"],"names":[],"mappings":"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"}