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%
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