@context-chef/ai-sdk-middleware 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/index.cjs +22 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.mjs +23 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -166,6 +166,7 @@ const wrappedModel = withContextChef(model, options);
|
|
|
166
166
|
| `compress.model` | `LanguageModelV3` | Yes (if compress) | Cheap model for summarization |
|
|
167
167
|
| `compress.preserveRatio` | `number` | No | Ratio of context to preserve (default: `0.8`) |
|
|
168
168
|
| `compress.toolResultStubThreshold` | `number` | No | Replace tool-result content longer than this many chars with a one-line metadata stub (`[Tool name returned N chars; omitted before summarization]`) before sending the to-be-summarized history to the compression model. Recent (preserved) tool results untouched. Default: undefined (disabled). |
|
|
169
|
+
| `compress.usagePreference` | `'max' \| 'feedFirst' \| 'tokenizerFirst'` | No | Which token source drives the trigger when both `tokenizer` and the AI SDK's reported usage are available. Default `'max'` (most conservative — `Math.max(tokenizer, fed)`). Use `'feedFirst'` to trust the API's reported usage and ignore tokenizer over-estimation; use `'tokenizerFirst'` to ignore the fed value entirely. `'tokenizerFirst'` requires `tokenizer` — if missing, it is sanitized to `'max'` at construction time with a console warning. |
|
|
169
170
|
| `truncate` | `TruncateOptions` | No | Enable tool result truncation |
|
|
170
171
|
| `truncate.threshold` | `number` | Yes (if truncate) | Character count to trigger truncation |
|
|
171
172
|
| `truncate.headChars` | `number` | No | Characters to preserve from start (default: `0`) |
|
package/README.zh-CN.md
CHANGED
|
@@ -168,6 +168,7 @@ const wrappedModel = withContextChef(model, options);
|
|
|
168
168
|
| `compress.model` | `LanguageModelV3` | 是(如启用 compress) | 用于摘要的便宜模型 |
|
|
169
169
|
| `compress.preserveRatio` | `number` | 否 | 保留上下文的比例(默认:`0.8`) |
|
|
170
170
|
| `compress.toolResultStubThreshold` | `number` | 否 | 在把待摘要历史送给 compression model 之前,将超过该字符数的 tool-result 内容替换为一行元信息桩(`[Tool name returned N chars; omitted before summarization]`)。近期保留的 tool-result 不动。默认:undefined(关闭)。 |
|
|
171
|
+
| `compress.usagePreference` | `'max' \| 'feedFirst' \| 'tokenizerFirst'` | 否 | 当 `tokenizer` 与 AI SDK 上报的 usage 同时存在时,决定触发判断使用哪个 token 来源。默认 `'max'`(最保守 — `Math.max(tokenizer, fed)`)。`'feedFirst'` 信任 API 真值,避免 tokenizer 高估导致的提前压缩;`'tokenizerFirst'` 完全忽略上报的 usage。`'tokenizerFirst'` 需要 `tokenizer`,缺失时构造期会被消毒为 `'max'` 并打印控制台警告。 |
|
|
171
172
|
| `truncate` | `TruncateOptions` | 否 | 启用工具结果截断 |
|
|
172
173
|
| `truncate.threshold` | `number` | 是(如启用 truncate) | 触发截断的字符数 |
|
|
173
174
|
| `truncate.headChars` | `number` | 否 | 保留开头的字符数(默认:`0`) |
|
package/dist/index.cjs
CHANGED
|
@@ -9,6 +9,11 @@ let _context_chef_core = require("@context-chef/core");
|
|
|
9
9
|
* Original AI SDK content is stored in per-role fields for lossless round-trip.
|
|
10
10
|
* `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
|
|
11
11
|
* `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
|
|
12
|
+
*
|
|
13
|
+
* Boundary sanitization: the result is run through {@link ensureValidHistory}
|
|
14
|
+
* to fix orphan tool results, missing tool results, and ensure the first
|
|
15
|
+
* non-system message is a user message. This is a system boundary — IR
|
|
16
|
+
* downstream is trusted to satisfy invariants.
|
|
12
17
|
*/
|
|
13
18
|
function fromAISDK(prompt) {
|
|
14
19
|
const messages = [];
|
|
@@ -88,7 +93,7 @@ function fromAISDK(prompt) {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
|
-
return messages;
|
|
96
|
+
return (0, _context_chef_core.ensureValidHistory)(messages);
|
|
92
97
|
}
|
|
93
98
|
/**
|
|
94
99
|
* Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
|
|
@@ -151,7 +156,7 @@ function toAISDK(messages) {
|
|
|
151
156
|
} else toolResults.push({
|
|
152
157
|
type: "tool-result",
|
|
153
158
|
toolCallId: toolMsg.tool_call_id ?? "",
|
|
154
|
-
toolName: toolMsg._toolName ?? "unknown",
|
|
159
|
+
toolName: toolMsg._toolName ?? toolMsg.name ?? "unknown",
|
|
155
160
|
output: {
|
|
156
161
|
type: "text",
|
|
157
162
|
value: toolMsg.content
|
|
@@ -296,14 +301,26 @@ function extractText(output) {
|
|
|
296
301
|
*/
|
|
297
302
|
function createMiddleware(options) {
|
|
298
303
|
let usageWarned = false;
|
|
299
|
-
const
|
|
304
|
+
const sharedJanitorConfig = {
|
|
300
305
|
contextWindow: options.contextWindow,
|
|
301
|
-
tokenizer: options.tokenizer ? (msgs) => options.tokenizer?.(msgs) ?? 0 : void 0,
|
|
302
|
-
preserveRatio: options.compress?.preserveRatio ?? .8,
|
|
303
306
|
toolResultStubThreshold: options.compress?.toolResultStubThreshold,
|
|
304
307
|
compressionModel: options.compress?.model ? createCompressionAdapter(options.compress.model) : void 0,
|
|
305
308
|
onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0,
|
|
306
309
|
onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded
|
|
310
|
+
};
|
|
311
|
+
let usagePreference = options.compress?.usagePreference;
|
|
312
|
+
if (usagePreference === "tokenizerFirst" && !options.tokenizer) {
|
|
313
|
+
console.warn("[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. Falling back to 'max'.");
|
|
314
|
+
usagePreference = "max";
|
|
315
|
+
}
|
|
316
|
+
const janitor = options.tokenizer ? new _context_chef_core.Janitor({
|
|
317
|
+
...sharedJanitorConfig,
|
|
318
|
+
tokenizer: (msgs) => options.tokenizer?.(msgs) ?? 0,
|
|
319
|
+
preserveRatio: options.compress?.preserveRatio ?? .8,
|
|
320
|
+
usagePreference
|
|
321
|
+
}) : new _context_chef_core.Janitor({
|
|
322
|
+
...sharedJanitorConfig,
|
|
323
|
+
usagePreference
|
|
307
324
|
});
|
|
308
325
|
return {
|
|
309
326
|
specificationVersion: "v3",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["Offloader","Janitor","XmlGenerator"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Attachment, Message, ToolCall } from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: toolMsg._toolName ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Offloader","Janitor","XmlGenerator"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -59,6 +59,20 @@ interface CompressOptions {
|
|
|
59
59
|
* undefined (disabled). Recommended starting value: `5000`.
|
|
60
60
|
*/
|
|
61
61
|
toolResultStubThreshold?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Strategy for choosing the trigger token count when both a `tokenizer`
|
|
64
|
+
* and an externally-reported usage value are available.
|
|
65
|
+
*
|
|
66
|
+
* - `'max'` (default): use the higher of the two — most conservative.
|
|
67
|
+
* - `'feedFirst'`: prefer reported usage when present, fall back to
|
|
68
|
+
* tokenizer. Use when API-reported usage is authoritative and the
|
|
69
|
+
* tokenizer over-estimates (e.g. shared config across providers, some
|
|
70
|
+
* of which report usage and some do not).
|
|
71
|
+
* - `'tokenizerFirst'`: ignore reported usage entirely. Requires a
|
|
72
|
+
* `tokenizer` to be configured; otherwise it is sanitized to `'max'`
|
|
73
|
+
* at construction time with a console warning.
|
|
74
|
+
*/
|
|
75
|
+
usagePreference?: 'max' | 'feedFirst' | 'tokenizerFirst';
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
64
78
|
* Mechanical compaction options — zero LLM cost.
|
|
@@ -203,6 +217,11 @@ interface AISDKMessage extends Message {
|
|
|
203
217
|
* Original AI SDK content is stored in per-role fields for lossless round-trip.
|
|
204
218
|
* `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
|
|
205
219
|
* `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
|
|
220
|
+
*
|
|
221
|
+
* Boundary sanitization: the result is run through {@link ensureValidHistory}
|
|
222
|
+
* to fix orphan tool results, missing tool results, and ensure the first
|
|
223
|
+
* non-system message is a user message. This is a system boundary — IR
|
|
224
|
+
* downstream is trusted to satisfy invariants.
|
|
206
225
|
*/
|
|
207
226
|
declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
|
|
208
227
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -59,6 +59,20 @@ interface CompressOptions {
|
|
|
59
59
|
* undefined (disabled). Recommended starting value: `5000`.
|
|
60
60
|
*/
|
|
61
61
|
toolResultStubThreshold?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Strategy for choosing the trigger token count when both a `tokenizer`
|
|
64
|
+
* and an externally-reported usage value are available.
|
|
65
|
+
*
|
|
66
|
+
* - `'max'` (default): use the higher of the two — most conservative.
|
|
67
|
+
* - `'feedFirst'`: prefer reported usage when present, fall back to
|
|
68
|
+
* tokenizer. Use when API-reported usage is authoritative and the
|
|
69
|
+
* tokenizer over-estimates (e.g. shared config across providers, some
|
|
70
|
+
* of which report usage and some do not).
|
|
71
|
+
* - `'tokenizerFirst'`: ignore reported usage entirely. Requires a
|
|
72
|
+
* `tokenizer` to be configured; otherwise it is sanitized to `'max'`
|
|
73
|
+
* at construction time with a console warning.
|
|
74
|
+
*/
|
|
75
|
+
usagePreference?: 'max' | 'feedFirst' | 'tokenizerFirst';
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
64
78
|
* Mechanical compaction options — zero LLM cost.
|
|
@@ -203,6 +217,11 @@ interface AISDKMessage extends Message {
|
|
|
203
217
|
* Original AI SDK content is stored in per-role fields for lossless round-trip.
|
|
204
218
|
* `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
|
|
205
219
|
* `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
|
|
220
|
+
*
|
|
221
|
+
* Boundary sanitization: the result is run through {@link ensureValidHistory}
|
|
222
|
+
* to fix orphan tool results, missing tool results, and ensure the first
|
|
223
|
+
* non-system message is a user message. This is a system boundary — IR
|
|
224
|
+
* downstream is trusted to satisfy invariants.
|
|
206
225
|
*/
|
|
207
226
|
declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
|
|
208
227
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateText, pruneMessages, wrapLanguageModel } from "ai";
|
|
2
|
-
import { Janitor, Offloader, XmlGenerator } from "@context-chef/core";
|
|
2
|
+
import { Janitor, Offloader, XmlGenerator, ensureValidHistory } from "@context-chef/core";
|
|
3
3
|
|
|
4
4
|
//#region src/adapter.ts
|
|
5
5
|
/**
|
|
@@ -8,6 +8,11 @@ import { Janitor, Offloader, XmlGenerator } from "@context-chef/core";
|
|
|
8
8
|
* Original AI SDK content is stored in per-role fields for lossless round-trip.
|
|
9
9
|
* `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
|
|
10
10
|
* `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
|
|
11
|
+
*
|
|
12
|
+
* Boundary sanitization: the result is run through {@link ensureValidHistory}
|
|
13
|
+
* to fix orphan tool results, missing tool results, and ensure the first
|
|
14
|
+
* non-system message is a user message. This is a system boundary — IR
|
|
15
|
+
* downstream is trusted to satisfy invariants.
|
|
11
16
|
*/
|
|
12
17
|
function fromAISDK(prompt) {
|
|
13
18
|
const messages = [];
|
|
@@ -87,7 +92,7 @@ function fromAISDK(prompt) {
|
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
|
-
return messages;
|
|
95
|
+
return ensureValidHistory(messages);
|
|
91
96
|
}
|
|
92
97
|
/**
|
|
93
98
|
* Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
|
|
@@ -150,7 +155,7 @@ function toAISDK(messages) {
|
|
|
150
155
|
} else toolResults.push({
|
|
151
156
|
type: "tool-result",
|
|
152
157
|
toolCallId: toolMsg.tool_call_id ?? "",
|
|
153
|
-
toolName: toolMsg._toolName ?? "unknown",
|
|
158
|
+
toolName: toolMsg._toolName ?? toolMsg.name ?? "unknown",
|
|
154
159
|
output: {
|
|
155
160
|
type: "text",
|
|
156
161
|
value: toolMsg.content
|
|
@@ -295,14 +300,26 @@ function extractText(output) {
|
|
|
295
300
|
*/
|
|
296
301
|
function createMiddleware(options) {
|
|
297
302
|
let usageWarned = false;
|
|
298
|
-
const
|
|
303
|
+
const sharedJanitorConfig = {
|
|
299
304
|
contextWindow: options.contextWindow,
|
|
300
|
-
tokenizer: options.tokenizer ? (msgs) => options.tokenizer?.(msgs) ?? 0 : void 0,
|
|
301
|
-
preserveRatio: options.compress?.preserveRatio ?? .8,
|
|
302
305
|
toolResultStubThreshold: options.compress?.toolResultStubThreshold,
|
|
303
306
|
compressionModel: options.compress?.model ? createCompressionAdapter(options.compress.model) : void 0,
|
|
304
307
|
onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0,
|
|
305
308
|
onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded
|
|
309
|
+
};
|
|
310
|
+
let usagePreference = options.compress?.usagePreference;
|
|
311
|
+
if (usagePreference === "tokenizerFirst" && !options.tokenizer) {
|
|
312
|
+
console.warn("[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. Falling back to 'max'.");
|
|
313
|
+
usagePreference = "max";
|
|
314
|
+
}
|
|
315
|
+
const janitor = options.tokenizer ? new Janitor({
|
|
316
|
+
...sharedJanitorConfig,
|
|
317
|
+
tokenizer: (msgs) => options.tokenizer?.(msgs) ?? 0,
|
|
318
|
+
preserveRatio: options.compress?.preserveRatio ?? .8,
|
|
319
|
+
usagePreference
|
|
320
|
+
}) : new Janitor({
|
|
321
|
+
...sharedJanitorConfig,
|
|
322
|
+
usagePreference
|
|
306
323
|
});
|
|
307
324
|
return {
|
|
308
325
|
specificationVersion: "v3",
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Attachment, Message, ToolCall } from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: toolMsg._toolName ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n const janitor = new Janitor({\n contextWindow: options.contextWindow,\n tokenizer: options.tokenizer ? (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0 : undefined,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary, count) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@context-chef/ai-sdk-middleware",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/MyPrototypeWhat/context-chef/issues"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@context-chef/core": "3.
|
|
42
|
+
"@context-chef/core": "3.4.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@ai-sdk/provider": ">=3",
|