SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
3.0 KB · 102 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/tools/truncate.ts4 * Description: Middle-out truncation with explicit omission markers and spill reporting (TOOL_PROTOCOL §1.3).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import type { TruncationInfo } from "./types.js";1112export interface TruncateLimits {13  maxLines: number;14  maxBytes: number;15  headLines: number;16  tailLines: number;17}1819/** Byte length without importing node:buffer explicitly (UTF-8). */20export function utf8Length(text: string): number {21  return Buffer.byteLength(text, "utf8");22}2324export function formatCount(n: number): string {25  return n.toLocaleString("en-US");26}2728function formatKb(bytes: number): string {29  return `${Math.max(1, Math.round(bytes / 1024))} KB`;30}3132/** The standard omission marker line (TOOL_PROTOCOL §1.3). */33export function omissionMarker(omittedLines: number, omittedBytes: number, spillPath: string): string {34  return `[... ${formatCount(omittedLines)} lines omitted (${formatKb(omittedBytes)}). Full output: ${spillPath} — read or grep that file for the rest.]`;35}3637/** Does this text exceed the given limits? */38export function needsTruncation(text: string, limits: TruncateLimits): boolean {39  if (utf8Length(text) > limits.maxBytes) return true;40  return countLines(text) > limits.maxLines;41}4243function countLines(text: string): number {44  if (text.length === 0) return 0;45  let count = 1;46  for (let i = 0; i < text.length; i++) {47    if (text[i] === "\n") count += 1;48  }49  return count;50}5152export interface TruncatedText {53  text: string;54  info: TruncationInfo;55}5657/**58 * Middle-out truncation: keep head + tail lines, insert one explicit marker59 * pointing at the spilled full output. Call only when needsTruncation() is true.60 */61export function truncateMiddleOut(62  text: string,63  limits: TruncateLimits,64  spillPath: string,65): TruncatedText {66  const lines = text.split("\n");67  const originalLines = lines.length;68  const originalBytes = utf8Length(text);6970  let head = lines.slice(0, limits.headLines);71  let tail = lines.slice(Math.max(limits.headLines, originalLines - limits.tailLines));7273  // Byte backstop for pathological single-line output: hard-slice by chars.74  const budget = limits.maxBytes;75  let headText = head.join("\n");76  let tailText = tail.join("\n");77  if (utf8Length(headText) + utf8Length(tailText) > budget) {78    const half = Math.floor(budget / 2);79    headText = headText.slice(0, half);80    tailText = tailText.slice(-half);81    head = headText.split("\n");82    tail = tailText.split("\n");83  }8485  const omittedLines = Math.max(0, originalLines - head.length - tail.length);86  const omittedBytes = Math.max(0, originalBytes - utf8Length(headText) - utf8Length(tailText));87  const marker = omissionMarker(omittedLines, omittedBytes, spillPath);88  const parts = tailText.length > 0 ? [headText, marker, tailText] : [headText, marker];8990  return {91    text: parts.join("\n"),92    info: {93      originalBytes,94      originalLines,95      shownHeadLines: head.length,96      shownTailLines: tail.length,97      omittedLines,98      spillPath,99    },100  };101}102