{"_id":"@azghr/foreshorten","_rev":"2-e01431f593d33397ff509bcbb6b63732","name":"@azghr/foreshorten","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@azghr/foreshorten","version":"0.1.0","keywords":["chat","history","trim","context","tokens","budget","memory","conversation","llm","model","truncate","system","recency"],"license":"MIT","_id":"@azghr/foreshorten@0.1.0","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"homepage":"https://github.com/az/npmdevkit#readme","bugs":{"url":"https://github.com/az/npmdevkit/issues"},"dist":{"shasum":"3f2aedd6bb0dc2946c8d52bcde011d91a5541b97","tarball":"https://registry.npmjs.org/@azghr/foreshorten/-/foreshorten-0.1.0.tgz","fileCount":10,"integrity":"sha512-tyonFjwbtrWy+0HOg8vxMIIoawaab3lTHHkVSE7L8/PUWsa0ds9yHwM48R4V7NV5wBfOolZ0Y5ByrQIX21bEdw==","signatures":[{"sig":"MEYCIQDz3DRNvWFoGPem2OFEn24NZYFLLmCrf9swLPMLZYCJ4gIhALCGXegY89u9f55NDAIrDht08iEgWV1UOjyYiazNDSTd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":34181},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"33f6cbe0a59426a591a6876d1e0ab3c26e4e0b7f","scripts":{"demo":"tsx examples/demo.ts","lint":"eslint src test examples","test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","check":"npm run typecheck && npm run lint && npm run test && npm run build","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"repository":{"url":"git+https://github.com/az/npmdevkit.git","type":"git","directory":"foreshorten"},"_npmVersion":"11.6.2","description":"Trim a chat message array to fit a token/size budget — keep the system prompt, pin the most recent turns, never split a message.","directories":{},"sideEffects":false,"_nodeVersion":"24.12.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/foreshorten_0.1.0_1784979432086_0.6809297209002774","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@azghr/foreshorten","version":"0.1.1","description":"Trim a chat message array to fit a token/size budget — keep the system prompt, pin the most recent turns, never split a message.","license":"MIT","type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"engines":{"node":">=18"},"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","lint":"eslint src test examples","demo":"tsx examples/demo.ts","check":"npm run typecheck && npm run lint && npm run test && npm run build"},"keywords":["chat","history","trim","context","tokens","budget","memory","conversation","llm","model","truncate","system","recency"],"gitHead":"33f6cbe0a59426a591a6876d1e0ab3c26e4e0b7f","_id":"@azghr/foreshorten@0.1.1","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-79d3EXfsHjNJ+ajReWmno1DT9XqSNyE9SvTQ6TeCOu51ztzDle1D1c6UF4GLGxrdD5DYhLLRUnBkoez/kBsWRw==","shasum":"05dc722501af1dd1a57300ffd08fcfbadd8b5f04","tarball":"https://registry.npmjs.org/@azghr/foreshorten/-/foreshorten-0.1.1.tgz","fileCount":10,"unpackedSize":34129,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCoza2HxgcmNcKRfCp6xm5I9f1ANX18rTB3TLJ5CmIS2wIhAL7hHl88DSvexR/gYQC5MRqXenPANsX6XSvlRIWwXT3s"}]},"_npmUser":{"name":"azghr","email":"masgharali.eng@gmail.com"},"directories":{},"maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/foreshorten_0.1.1_1784980014559_0.6754036873395666"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-25T11:37:11.964Z","modified":"2026-07-25T11:46:54.870Z","0.1.0":"2026-07-25T11:37:12.222Z","0.1.1":"2026-07-25T11:46:54.717Z"},"license":"MIT","keywords":["chat","history","trim","context","tokens","budget","memory","conversation","llm","model","truncate","system","recency"],"description":"Trim a chat message array to fit a token/size budget — keep the system prompt, pin the most recent turns, never split a message.","maintainers":[{"name":"azghr","email":"masgharali.eng@gmail.com"}],"readme":"# @azghr/foreshorten\n\n[![npm](https://img.shields.io/npm/v/@azghr/foreshorten)](https://www.npmjs.com/package/@azghr/foreshorten)\n[![MIT License](https://img.shields.io/npm/l/@azghr/foreshorten)](LICENSE)\n\n> Trim a chat message array to fit a token/size budget — keep the system prompt, pin the most recent turns, never split a message.\n\n## The problem\n\nWhen working with LLMs, chat message arrays can grow unbounded, exceeding context window limits and increasing costs. You need to trim conversation histories while preserving system prompts and maintaining conversation context. Manual trimming is error-prone and can split messages or lose important context.\n\n## Install\n\n```bash\nnpm install @azghr/foreshorten\n# or\npnpm add @azghr/foreshorten\n# or\nyarn add @azghr/foreshorten\n```\n\n## Use\n\n```typescript\nimport { foreshorten } from \"@azghr/foreshorten\";\n\nconst messages = [\n  { role: \"system\", content: \"You are a helpful assistant.\" },\n  { role: \"user\", content: \"Hello!\" },\n  { role: \"assistant\", content: \"Hi there!\" },\n  { role: \"user\", content: \"How are you?\" },\n  { role: \"assistant\", content: \"I'm doing well, thanks!\" }\n];\n\n// Simple character count measure\nconst countChars = (m: Message) => m.content.length;\n\n// Trim to fit within 50 characters\nconst trimmed = foreshorten(messages, 50, countChars);\n```\n\n### Preserve system prompt and pin recent messages\n\n```typescript\nconst longConversation = [\n  { role: \"system\", content: \"You are a coding assistant.\" },\n  // ... many messages ...\n  { role: \"user\", content: \"What about memoization?\" },\n  { role: \"assistant\", content: \"Memoization caches computed results...\" }\n];\n\n// Keep the 2 most recent messages (after system)\nconst trimmed = foreshorten(longConversation, 50, estimateTokens, {\n  keepRecent: 2\n});\n```\n\n### Handle overflow\n\n```typescript\nimport { foreshorten, BudgetTooSmall } from \"@azghr/foreshorten\";\n\n// Option 1: truncate-oldest (default)\nconst trimmed = foreshorten(messages, 30, estimateTokens, {\n  overflow: \"truncate-oldest\"\n});\n\n// Option 2: throw if budget exceeded\ntry {\n  const trimmed = foreshorten(messages, 30, estimateTokens, {\n    overflow: \"throw\"\n  });\n} catch (error) {\n  if (error instanceof BudgetTooSmall) {\n    console.log(\"Budget too small:\", error.message);\n  }\n}\n```\n\n### Custom measure function\n\n```typescript\n// Use your own tokenizer\nconst tokenizer = /* your tiktoken or similar */;\nconst countTokens = (m: Message) => tokenizer.encode(m.content).length;\n\nconst trimmed = foreshorten(messages, 4096, countTokens);\n```\n\n## API\n\n### `foreshorten(messages, budget, measure, options?)`\n\nTrim a chat message array to fit a token/size budget.\n\n**Parameters:**\n\n- `messages` (`readonly MessageType[]`) — The chat history to trim\n- `budget` (`number`) — Maximum total cost allowed (e.g., tokens, bytes)\n- `measure` (`MeasureFn<MessageType>`) — Function to measure cost of a single message\n- `options?` (`ForeshortenOptions`) — Optional configuration\n\n**Returns:** `MessageType[]` — A new array with messages fitting the budget\n\n**Throws:** `BudgetTooSmall` — If overflow is \"throw\" and required messages exceed budget\n\n### `ForeshortenOptions`\n\nConfiguration options for foreshorten.\n\n**Properties:**\n\n- `overflow` — How to handle overflow when budget is exceeded\n  - `\"throw\"` — Throw `BudgetTooSmall` error\n  - `\"truncate-oldest\"` — Remove oldest non-required messages (default)\n- `keepRecent` — Number of most recent messages to always keep (default: `0`)\n\n### `BudgetTooSmall`\n\nError thrown when required messages exceed budget and `overflow` is `\"throw\"`.\n\n```typescript\nclass BudgetTooSmall extends Error {\n  constructor(deficit: number);\n}\n```\n\n## How it works\n\n1. **Preserve system messages** — All messages with `role: \"system\"` are kept\n2. **Pin recent messages** — The N most recent messages (after system) are pinned\n3. **Fill remaining budget** — Add older messages from newest to oldest while budget allows\n4. **Apply overflow policy** — Either throw or stop adding when budget is exhausted\n\n```\nOriginal: [system, user1, assistant1, user2, assistant2, user3, assistant3]\n           ↓                        ↓                    ↓\n        System          keepRecent: 2          Fill from newest\n           │                        │                    │\nResult:   [system, assistant2, user3, assistant3, user2]\n           └───────────────────────────────────────────────────\n           System + Recent + Older (until budget exhausted)\n```\n\n## Non-goals\n\n- **Token counting** — This package doesn't count tokens; you provide the measure function\n- **Message splitting** — Messages are never split; they're kept whole or removed entirely\n- **Conversation state management** — This package only trims arrays; it doesn't manage conversation state\n- **LLM API integration** — This is a utility package; it doesn't make API calls\n\n## Related Packages\n\n- **[@azghr/filterkit](https://www.npmjs.com/package/@azghr/filterkit)** — Framework-agnostic, type-safe filtering for TypeScript\n- **[@azghr/shorn](https://www.npmjs.com/package/@azghr/shorn)** — Truncate strings by byte budget without breaking graphemes\n- **[@azghr/singlet](https://www.npmjs.com/package/@azghr/singlet)** — Deduplicate concurrent async calls\n- **[congeal](https://www.npmjs.com/package/congeal)** — Data structure utilities\n- **[decant](https://www.npmjs.com/package/decant)** — Extract and transform utilities\n- **[expunge](https://www.npmjs.com/package/expunge)** — Remove or exclude items from collections\n- **[extricate](https://www.npmjs.com/package/extricate)** — Extract and separate utilities\n- **[forbar](https://www.npmjs.com/package/forbear)** — Read server rate-limit instructions from HTTP responses\n- **[forestall](https://www.npmjs.com/package/forestall)** — Delay execution until a condition is met\n- **[obviate](https://www.npmjs.com/package/obviate)** — Render operations unnecessary through caching\n- **[occlude](https://www.npmjs.com/package/occlude)** — Hide or mask data and functionality\n- **[placemark](https://www.npmjs.com/package/placemark)** — Geographic location and mapping utilities\n- **[quiesce](https://www.npmjs.com/package/quiesce)** — Ordered, timeboxed graceful shutdown for Node\n- **[seriatim](https://www.npmjs.com/package/seriatim)** — Sequential processing utilities\n- **[sortition](https://www.npmjs.com/package/sortition)** — Deterministic percentage rollouts and A/B bucketing\n- **[specie](https://www.npmjs.com/package/specie)** — Currency and financial calculations\n- **[stanch](https://www.npmjs.com/package/stanch)** — Stop flows or operations based on conditions\n- **[staleness](https://www.npmjs.com/package/staleness)** — Stale-while-revalidate caching for async functions\n- **[wend](https://www.npmjs.com/package/wend)** — Polling utilities with exponential backoff\n\n---\n\n*Note: This list should be kept in sync with the packages in `pnpm-workspace.yaml`. When adding a new package to the monorepo, update this list to include all sibling packages.*\n\n## License\n\nMIT\n","readmeFilename":"README.md"}