From 3745cd2b4c71f3a0f6908b8ea6216b6e24728868 Mon Sep 17 00:00:00 2001 From: Amogh Sunil Date: Wed, 1 Jul 2026 12:58:25 +0530 Subject: [PATCH 1/4] feat: add compression layer for token optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SmartCrusher: compress JSON tool outputs (removes nulls, deduplicates arrays, truncates strings) - CodeCompressor: strip comments and blank lines from source files (TS, JS, Py, Go, Rust, Java, C++) - CacheAligner: order system prompt static-first to maximize provider KV cache hits - doc-converter: convert PDF, DOCX, XLSX, PPTX to clean markdown via pdf-parse, mammoth, xlsx, jszip - doc-store + CCR: chunk large docs into sections, return outline first, agent fetches on demand - Disk cache for converted docs at memory/.doc-cache/ with source-newer-than-cache invalidation - Fix: check isConvertible() by extension before isBinary() — PDFs without null bytes were bypassing conversion - Add benchmark scripts: test-compression.mjs, test-live-benchmark.mjs, test-token-proof.mjs --- package-lock.json | 590 +++++++++++++++++++++++++++++ package.json | 7 + src/compression/cache-aligner.ts | 87 +++++ src/compression/code-compressor.ts | 166 ++++++++ src/compression/index.ts | 8 + src/compression/smart-crusher.ts | 140 +++++++ src/cost-tracker.ts | 13 + src/loader.ts | 75 ++-- src/sdk.ts | 4 + src/tools/cli.ts | 9 + src/tools/doc-converter.ts | 231 +++++++++++ src/tools/doc-store.ts | 152 ++++++++ src/tools/index.ts | 43 ++- src/tools/read.ts | 124 +++++- test-compression.mjs | 184 +++++++++ test-live-benchmark.mjs | 210 ++++++++++ test-token-proof.mjs | 195 ++++++++++ 17 files changed, 2200 insertions(+), 38 deletions(-) create mode 100644 src/compression/cache-aligner.ts create mode 100644 src/compression/code-compressor.ts create mode 100644 src/compression/index.ts create mode 100644 src/compression/smart-crusher.ts create mode 100644 src/tools/doc-converter.ts create mode 100644 src/tools/doc-store.ts create mode 100644 test-compression.mjs create mode 100644 test-live-benchmark.mjs create mode 100644 test-token-proof.mjs diff --git a/package-lock.json b/package-lock.json index 2316271..8af62c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,13 @@ "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@sinclair/typebox": "^0.34.41", + "headroom-ai": "^0.22.4", "js-yaml": "^4.1.0", + "jszip": "^3.10.1", + "mammoth": "^1.12.0", "node-cron": "^3.0.3", + "pdf-parse": "^2.4.5", + "xlsx": "^0.18.5", "yaml": "^2.8.2" }, "bin": { @@ -31,8 +36,10 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", + "@types/jszip": "^3.4.0", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", + "@types/pdf-parse": "^1.1.5", "typescript": "^5.7.0" }, "engines": { @@ -596,6 +603,190 @@ "zod-to-json-schema": "^3.25.0" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodable/entities": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", @@ -1748,6 +1939,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jszip": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz", + "integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jszip": "*" + } + }, "node_modules/@types/node": { "version": "22.19.20", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", @@ -1764,12 +1965,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pdf-parse": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz", + "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1791,6 +2011,15 @@ "acorn": "^8" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1880,6 +2109,12 @@ "node": "*" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -1892,6 +2127,19 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1924,6 +2172,15 @@ "node": ">=12" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1942,6 +2199,24 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1982,6 +2257,21 @@ "node": ">= 14" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -2136,6 +2426,15 @@ "node": ">=12.20.0" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/gaxios": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", @@ -2222,6 +2521,35 @@ "node": ">=14" } }, + "node_modules/headroom-ai": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/headroom-ai/-/headroom-ai-0.22.4.tgz", + "integrity": "sha512-9a0rgB/jsWe8gs/ggyUwe6E8DYwKAuBvlUml2ApwlUjb5EfJ611X6X+WG0SiXw3nO6sdyV1/+Ah5uw9P7ecnjw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@ai-sdk/provider": ">=1.0.0", + "@anthropic-ai/sdk": ">=0.30.0", + "ai": ">=6.0.0", + "openai": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@ai-sdk/provider": { + "optional": true + }, + "@anthropic-ai/sdk": { + "optional": true + }, + "ai": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -2248,6 +2576,12 @@ "node": ">= 14" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-in-the-middle": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", @@ -2263,6 +2597,12 @@ "node": ">=18" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -2281,6 +2621,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", @@ -2325,6 +2671,18 @@ "node": ">=16" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2346,6 +2704,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -2358,6 +2725,17 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -2367,6 +2745,39 @@ "node": ">=12" } }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -2459,6 +2870,12 @@ } } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -2504,6 +2921,12 @@ "node": ">= 14" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -2525,6 +2948,53 @@ "node": ">=14.0.0" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/protobufjs": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", @@ -2574,6 +3044,27 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2625,6 +3116,12 @@ ], "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2673,6 +3170,39 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2743,6 +3273,12 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", @@ -2758,6 +3294,12 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2777,6 +3319,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2815,6 +3375,27 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-naming": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", @@ -2830,6 +3411,15 @@ "node": ">=16.0.0" } }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 00aa77c..d3a2312 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,13 @@ "@opentelemetry/sdk-trace-node": "^2.7.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@sinclair/typebox": "^0.34.41", + "headroom-ai": "^0.22.4", "js-yaml": "^4.1.0", + "jszip": "^3.10.1", + "mammoth": "^1.12.0", "node-cron": "^3.0.3", + "pdf-parse": "^2.4.5", + "xlsx": "^0.18.5", "yaml": "^2.8.2" }, "peerDependencies": { @@ -76,8 +81,10 @@ }, "devDependencies": { "@types/js-yaml": "^4.0.9", + "@types/jszip": "^3.4.0", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", + "@types/pdf-parse": "^1.1.5", "typescript": "^5.7.0" } } diff --git a/src/compression/cache-aligner.ts b/src/compression/cache-aligner.ts new file mode 100644 index 0000000..8eb8dcc --- /dev/null +++ b/src/compression/cache-aligner.ts @@ -0,0 +1,87 @@ +/** + * CacheAligner — stabilizes the system prompt prefix to maximize KV cache hits. + * + * Anthropic and OpenAI both cache the prefix of the system prompt. If the prefix + * is identical across requests, the provider reuses the cached KV state and you + * pay zero input tokens for it. If anything changes at the top — even a timestamp + * or a dynamic memory line — the cache misses and you pay full price. + * + * The fix: put all STATIC content first (SOUL.md, RULES.md, knowledge, skills), + * and all DYNAMIC content last (memory, current task, recent conversation). + * + * This module assembles a system prompt in that order and reports the stable + * prefix length so callers can log or track cache effectiveness. + */ + +export interface SystemPromptParts { + /** Static — never changes between sessions (SOUL.md, RULES.md) */ + identity: string; + /** Static — knowledge files marked always_load */ + knowledge: string; + /** Static — skill definitions loaded for this session */ + skills: string; + /** Dynamic — changes every session (memory, conversation summary) */ + memory: string; + /** Dynamic — changes every turn */ + task: string; +} + +export interface AlignedPrompt { + /** The full assembled system prompt */ + prompt: string; + /** Character index where the static prefix ends and dynamic content begins */ + staticPrefixEnd: number; + /** Estimated tokens in the static prefix (eligible for KV cache) */ + staticTokens: number; + /** Estimated tokens in the dynamic suffix (never cached) */ + dynamicTokens: number; +} + +function estimateTokens(s: string): number { + return Math.ceil(s.length / 4); +} + +function section(header: string, content: string): string { + if (!content.trim()) return ""; + return `${header}\n\n${content.trim()}`; +} + +/** + * Assembles a system prompt with static parts first, dynamic parts last. + * Static parts are eligible for provider-side KV cache reuse. + */ +export function alignSystemPrompt(parts: SystemPromptParts): AlignedPrompt { + const staticSections = [ + section("# Identity", parts.identity), + section("# Knowledge", parts.knowledge), + section("# Skills", parts.skills), + ].filter(Boolean); + + const dynamicSections = [ + section("# Memory", parts.memory), + section("# Current Task", parts.task), + ].filter(Boolean); + + const staticPrefix = staticSections.join("\n\n"); + const dynamicSuffix = dynamicSections.join("\n\n"); + + const prompt = dynamicSuffix + ? `${staticPrefix}\n\n${dynamicSuffix}` + : staticPrefix; + + const staticPrefixEnd = staticPrefix.length; + const staticTokens = estimateTokens(staticPrefix); + const dynamicTokens = estimateTokens(dynamicSuffix); + + return { prompt, staticPrefixEnd, staticTokens, dynamicTokens }; +} + +/** + * Returns the cache efficiency ratio: what fraction of the prompt is static. + * 1.0 = fully cacheable, 0.0 = nothing cacheable. + */ +export function cacheEfficiency(aligned: AlignedPrompt): number { + const total = aligned.staticTokens + aligned.dynamicTokens; + if (total === 0) return 0; + return aligned.staticTokens / total; +} diff --git a/src/compression/code-compressor.ts b/src/compression/code-compressor.ts new file mode 100644 index 0000000..b8debc0 --- /dev/null +++ b/src/compression/code-compressor.ts @@ -0,0 +1,166 @@ +/** + * CodeCompressor — strips noise from source code before the LLM sees it. + * + * When an agent reads code files, most of the token cost is comments, + * docstrings, blank lines, and decorative whitespace. The LLM needs the + * structure and logic — not the annotations. + * + * Techniques applied: + * 1. Strip single-line comments (// and #) + * 2. Strip block comments (/* ... *\/ and """ ... """) + * 3. Collapse multiple blank lines into one + * 4. Trim trailing whitespace per line + * + * Does NOT strip: + * - String literals (could contain // or # that look like comments) + * - Type annotations (useful signal for the LLM) + * - Import statements + */ + +export type Language = "ts" | "js" | "py" | "go" | "rust" | "java" | "cpp" | "unknown"; + +const EXTENSION_MAP: Record = { + ".ts": "ts", + ".tsx": "ts", + ".js": "js", + ".jsx": "js", + ".py": "py", + ".go": "go", + ".rs": "rust", + ".java": "java", + ".cpp": "cpp", + ".cc": "cpp", + ".c": "cpp", + ".h": "cpp", +}; + +export function detectLanguage(filename: string): Language { + const ext = filename.slice(filename.lastIndexOf(".")).toLowerCase(); + return EXTENSION_MAP[ext] ?? "unknown"; +} + +function stripSlashComments(code: string): string { + // Remove // comments but preserve URLs (https://) and protocol strings + // Strategy: only strip if // is preceded by whitespace or start-of-line + return code + .split("\n") + .map((line) => { + // Find // that isn't inside a string + let inString: string | null = null; + for (let i = 0; i < line.length - 1; i++) { + const ch = line[i]; + if (inString) { + if (ch === inString && line[i - 1] !== "\\") inString = null; + } else { + if (ch === '"' || ch === "'" || ch === "`") { inString = ch; continue; } + if (ch === "/" && line[i + 1] === "/") { + return line.slice(0, i).trimEnd(); + } + } + } + return line; + }) + .join("\n"); +} + +function stripHashComments(code: string): string { + return code + .split("\n") + .map((line) => { + let inString: string | null = null; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (inString) { + if (ch === inString && line[i - 1] !== "\\") inString = null; + } else { + if (ch === '"' || ch === "'") { inString = ch; continue; } + if (ch === "#") return line.slice(0, i).trimEnd(); + } + } + return line; + }) + .join("\n"); +} + +function stripBlockComments(code: string): string { + // Remove /* ... */ block comments + return code.replace(/\/\*[\s\S]*?\*\//g, ""); +} + +function stripPythonDocstrings(code: string): string { + // Remove triple-quoted strings that appear as standalone statements (docstrings) + // Matches """ or ''' docstrings at the start of a block + return code + .replace(/^(\s*)"""[\s\S]*?"""/gm, "") + .replace(/^(\s*)'''[\s\S]*?'''/gm, ""); +} + +function collapseBlankLines(code: string): string { + // Replace 3+ consecutive blank lines with a single blank line + return code.replace(/\n{3,}/g, "\n\n"); +} + +function trimTrailingWhitespace(code: string): string { + return code + .split("\n") + .map((l) => l.trimEnd()) + .join("\n"); +} + +function estimateTokens(s: string): number { + return Math.ceil(s.length / 4); +} + +export interface CompressionResult { + compressed: string; + originalTokens: number; + compressedTokens: number; + reductionPct: number; + language: Language; +} + +/** + * Compress source code by removing comments and noise. + * Returns the original if the file language is unknown or compression doesn't help. + */ +export function compressCode(text: string, filename: string): CompressionResult { + const language = detectLanguage(filename); + const originalTokens = estimateTokens(text); + + if (language === "unknown") { + return { compressed: text, originalTokens, compressedTokens: originalTokens, reductionPct: 0, language }; + } + + let result = text; + + if (language === "py") { + result = stripPythonDocstrings(result); + result = stripHashComments(result); + } else if (language === "ts" || language === "js") { + result = stripBlockComments(result); + result = stripSlashComments(result); + } else if (language === "go" || language === "rust" || language === "java" || language === "cpp") { + result = stripBlockComments(result); + result = stripSlashComments(result); + } + + result = collapseBlankLines(result); + result = trimTrailingWhitespace(result); + result = result.trim(); + + const compressedTokens = estimateTokens(result); + + if (compressedTokens >= originalTokens) { + return { compressed: text, originalTokens, compressedTokens: originalTokens, reductionPct: 0, language }; + } + + const reductionPct = Math.round(((originalTokens - compressedTokens) / originalTokens) * 100); + return { compressed: result, originalTokens, compressedTokens, reductionPct, language }; +} + +/** + * Returns true for file extensions the compressor handles. + */ +export function isSourceFile(filename: string): boolean { + return detectLanguage(filename) !== "unknown"; +} diff --git a/src/compression/index.ts b/src/compression/index.ts new file mode 100644 index 0000000..ca66da2 --- /dev/null +++ b/src/compression/index.ts @@ -0,0 +1,8 @@ +export { crushJson, isJson } from "./smart-crusher.js"; +export type { CompressionResult as JsonCompressionResult } from "./smart-crusher.js"; + +export { compressCode, isSourceFile, detectLanguage } from "./code-compressor.js"; +export type { CompressionResult as CodeCompressionResult, Language } from "./code-compressor.js"; + +export { alignSystemPrompt, cacheEfficiency } from "./cache-aligner.js"; +export type { SystemPromptParts, AlignedPrompt } from "./cache-aligner.js"; diff --git a/src/compression/smart-crusher.ts b/src/compression/smart-crusher.ts new file mode 100644 index 0000000..118a8e7 --- /dev/null +++ b/src/compression/smart-crusher.ts @@ -0,0 +1,140 @@ +/** + * SmartCrusher — compresses JSON tool outputs before the LLM sees them. + * + * JSON tool results (API responses, search results, file listings) are the + * highest-value compression target in gitagent. They are structured, often + * redundant, and the LLM only needs the semantic content — not perfect fidelity. + * + * Techniques applied in order: + * 1. Remove null / undefined / empty values + * 2. Deduplicate identical objects in arrays + * 3. Truncate long string values + * 4. Shorten repetitive keys across the object + * 5. Collapse arrays longer than MAX_ARRAY_ITEMS with a count annotation + */ + +const MAX_STRING_LENGTH = 300; +const MAX_ARRAY_ITEMS = 20; + +function removeEmpty(obj: unknown): unknown { + if (Array.isArray(obj)) { + return obj + .map(removeEmpty) + .filter((v) => v !== null && v !== undefined && v !== "" && !(Array.isArray(v) && v.length === 0)); + } + if (obj !== null && typeof obj === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(obj as Record)) { + if (v === null || v === undefined || v === "") continue; + if (Array.isArray(v) && v.length === 0) continue; + if (typeof v === "object" && !Array.isArray(v) && Object.keys(v as object).length === 0) continue; + result[k] = removeEmpty(v); + } + return result; + } + return obj; +} + +function truncateStrings(obj: unknown, maxLen: number): unknown { + if (typeof obj === "string") { + if (obj.length > maxLen) return obj.slice(0, maxLen) + `…[+${obj.length - maxLen}]`; + return obj; + } + if (Array.isArray(obj)) return obj.map((v) => truncateStrings(v, maxLen)); + if (obj !== null && typeof obj === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(obj as Record)) { + result[k] = truncateStrings(v, maxLen); + } + return result; + } + return obj; +} + +function deduplicateArray(arr: unknown[]): unknown[] { + const seen = new Set(); + const result: unknown[] = []; + for (const item of arr) { + const key = JSON.stringify(item); + if (!seen.has(key)) { + seen.add(key); + result.push(item); + } + } + return result; +} + +function collapseArrays(obj: unknown, maxItems: number): unknown { + if (Array.isArray(obj)) { + const deduped = deduplicateArray(obj.map((v) => collapseArrays(v, maxItems))); + if (deduped.length > maxItems) { + const kept = deduped.slice(0, maxItems); + const dropped = deduped.length - maxItems; + return [...kept, `[…${dropped} more items omitted]`]; + } + return deduped; + } + if (obj !== null && typeof obj === "object") { + const result: Record = {}; + for (const [k, v] of Object.entries(obj as Record)) { + result[k] = collapseArrays(v, maxItems); + } + return result; + } + return obj; +} + +export interface CompressionResult { + compressed: string; + originalTokens: number; + compressedTokens: number; + reductionPct: number; +} + +function estimateTokens(s: string): number { + return Math.ceil(s.length / 4); +} + +/** + * Returns true if the string is valid JSON (object or array). + */ +export function isJson(text: string): boolean { + const t = text.trimStart(); + if (t[0] !== "{" && t[0] !== "[") return false; + try { + JSON.parse(text); + return true; + } catch { + return false; + } +} + +/** + * Compress a JSON string. Returns original if parsing fails or compression + * doesn't help (i.e. result is longer than input). + */ +export function crushJson(text: string): CompressionResult { + const originalTokens = estimateTokens(text); + + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + return { compressed: text, originalTokens, compressedTokens: originalTokens, reductionPct: 0 }; + } + + let result = removeEmpty(parsed); + result = collapseArrays(result, MAX_ARRAY_ITEMS); + result = truncateStrings(result, MAX_STRING_LENGTH); + + const compressed = JSON.stringify(result); + const compressedTokens = estimateTokens(compressed); + + // Only return compressed version if it actually saves tokens + if (compressedTokens >= originalTokens) { + return { compressed: text, originalTokens, compressedTokens: originalTokens, reductionPct: 0 }; + } + + const reductionPct = Math.round(((originalTokens - compressedTokens) / originalTokens) * 100); + return { compressed, originalTokens, compressedTokens, reductionPct }; +} diff --git a/src/cost-tracker.ts b/src/cost-tracker.ts index 3340da9..2a8ada6 100644 --- a/src/cost-tracker.ts +++ b/src/cost-tracker.ts @@ -10,6 +10,11 @@ export interface ModelUsage { requests: number; } +export interface ConversionSavings { + filesConverted: number; + tokensSaved: number; +} + export interface SessionCosts { totalCostUsd: number; totalInputTokens: number; @@ -17,6 +22,7 @@ export interface SessionCosts { totalRequests: number; startTime: number; modelUsage: Record; + conversionSavings: ConversionSavings; } /** @@ -34,6 +40,7 @@ export class CostTracker { totalRequests: 0, startTime: Date.now(), modelUsage: {}, + conversionSavings: { filesConverted: 0, tokensSaved: 0 }, }; } @@ -74,6 +81,11 @@ export class CostTracker { mu.requests++; } + addConversion(savedTokens: number): void { + this.costs.conversionSavings.filesConverted++; + this.costs.conversionSavings.tokensSaved += savedTokens; + } + get(): SessionCosts { return { ...this.costs, @@ -89,6 +101,7 @@ export class CostTracker { totalRequests: 0, startTime: Date.now(), modelUsage: {}, + conversionSavings: { filesConverted: 0, tokensSaved: 0 }, }; } } diff --git a/src/loader.ts b/src/loader.ts index b8d193e..ad18bd4 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -22,6 +22,7 @@ import type { ComplianceWarning } from "./compliance.js"; import { discoverAndLoadPlugins } from "./plugins.js"; import type { LoadedPlugin } from "./plugin-types.js"; import type { PluginConfig } from "./plugin-types.js"; +import { alignSystemPrompt, cacheEfficiency } from "./compression/index.js"; export interface AgentManifest { spec_version: string; @@ -272,25 +273,9 @@ export async function loadAgent( const duties = await readFileOr(join(agentDir, "DUTIES.md"), ""); const agentsMd = await readFileOr(join(agentDir, "AGENTS.md"), ""); - // Build system prompt - const parts: string[] = []; - - parts.push(`# ${manifest.name} v${manifest.version}\n${manifest.description}`); - - if (soul) parts.push(soul); - if (rules) parts.push(rules); - if (parentRules) parts.push(parentRules); // Append parent rules (union) - if (duties) parts.push(duties); - if (agentsMd) parts.push(agentsMd); - - parts.push( - `# Memory\n\nYou have a memory file at memory/MEMORY.md. Use the \`memory\` tool to load and save memories. Each save creates a git commit, so your memory has full history. You can also use the \`cli\` tool to run git commands for deeper memory inspection (git log, git diff, git show).\n\nYour memories define who you are. When you have none, you are newly awakened — curious and eager to understand the person you're talking to. As memories grow, so do you. Save memories proactively when you learn something meaningful about the user.`, - ); - // Discover and load knowledge const knowledge = await loadKnowledge(agentDir); const knowledgeBlock = formatKnowledgeForPrompt(knowledge); - if (knowledgeBlock) parts.push(knowledgeBlock); // Discover skills (filtered by manifest.skills if set) let skills = await discoverSkills(agentDir); @@ -304,33 +289,21 @@ export async function loadAgent( skills = [...skills, ...plugin.skills]; } const skillsBlock = formatSkillsForPrompt(skills); - if (skillsBlock) parts.push(skillsBlock); // Discover workflows const workflows = await discoverWorkflows(agentDir); const workflowsBlock = formatWorkflowsForPrompt(workflows); - if (workflowsBlock) parts.push(workflowsBlock); // Discover sub-agents (Phase 2.1) const subAgents = await discoverSubAgents(agentDir); const subAgentsBlock = formatSubAgentsForPrompt(subAgents); - if (subAgentsBlock) parts.push(subAgentsBlock); // Load examples (Phase 2.3) const examples = await loadExamples(agentDir); const examplesBlock = formatExamplesForPrompt(examples); - if (examplesBlock) parts.push(examplesBlock); - - // Append plugin prompt additions - for (const plugin of plugins) { - if (plugin.promptAddition) { - parts.push(`# Plugin: ${plugin.manifest.name}\n\n${plugin.promptAddition}`); - } - } // Load compliance context (Phase 3) const complianceBlock = await loadComplianceContext(agentDir); - if (complianceBlock) parts.push(complianceBlock); // Workspace directory — all generated files go here const cloudMode = @@ -350,10 +323,8 @@ When creating files (documents, markdown files, PDFs, images, spreadsheets, code const cloudBlock = cloudMode ? `\n\n## Cloud Mode\n\nYou are running inside a containerized cloud deployment — there is no desktop. Do NOT call \`open\`, \`xdg-open\`, \`start\`, \`osascript\`, or any GUI launcher; they will silently fail. To "show" the user an artifact:\n- Write it to \`workspace/\` (e.g. \`workspace/index.html\`, \`workspace/deck.pptx\`).\n- Mention the relative path in your reply.\n\nThe web UI auto-opens generated files in its viewer: HTML renders inline (with relative \`\`/\`