@context-chef/ai-sdk-middleware 0.1.2 → 1.0.1

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/dist/index.cjs CHANGED
@@ -6,7 +6,7 @@ let _context_chef_core = require("@context-chef/core");
6
6
  /**
7
7
  * Converts an AI SDK V3 prompt to context-chef IR messages.
8
8
  *
9
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
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
12
  */
@@ -26,7 +26,7 @@ function fromAISDK(prompt) {
26
26
  messages.push({
27
27
  role: "user",
28
28
  content: text,
29
- _originalContent: msg.content,
29
+ _userContent: msg.content,
30
30
  _originalText: text,
31
31
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
32
32
  });
@@ -50,7 +50,7 @@ function fromAISDK(prompt) {
50
50
  const m = {
51
51
  role: "assistant",
52
52
  content: joinedText,
53
- _originalContent: msg.content,
53
+ _assistantContent: msg.content,
54
54
  _originalText: joinedText,
55
55
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
56
56
  };
@@ -66,7 +66,7 @@ function fromAISDK(prompt) {
66
66
  role: "tool",
67
67
  content: text,
68
68
  tool_call_id: part.toolCallId,
69
- _originalContent: [part],
69
+ _toolContent: [part],
70
70
  _originalText: text,
71
71
  _toolName: part.toolName
72
72
  });
@@ -76,9 +76,15 @@ function fromAISDK(prompt) {
76
76
  return messages;
77
77
  }
78
78
  /**
79
+ * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
80
+ */
81
+ function asAISDK(msg) {
82
+ return msg;
83
+ }
84
+ /**
79
85
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
80
86
  *
81
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
87
+ * Uses per-role original content when unmodified (detected via `_originalText`).
82
88
  * Falls back to constructing from IR fields when content was modified by Janitor
83
89
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
84
90
  */
@@ -86,40 +92,37 @@ function toAISDK(messages) {
86
92
  const prompt = [];
87
93
  let i = 0;
88
94
  while (i < messages.length) {
89
- const msg = messages[i];
90
- const providerOptions = msg._providerOptions;
95
+ const msg = asAISDK(messages[i]);
91
96
  const contentModified = msg._originalText !== void 0 && msg._originalText !== msg.content;
92
97
  if (msg.role === "system") {
93
98
  prompt.push({
94
99
  role: "system",
95
100
  content: msg.content,
96
- ...providerOptions ? { providerOptions } : {}
101
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
97
102
  });
98
103
  i++;
99
104
  continue;
100
105
  }
101
106
  if (msg.role === "user") {
102
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
103
- type: "text",
104
- text: msg.content
105
- }];
106
107
  prompt.push({
107
108
  role: "user",
108
- content,
109
- ...providerOptions ? { providerOptions } : {}
109
+ content: !contentModified && msg._userContent ? msg._userContent : [{
110
+ type: "text",
111
+ text: msg.content
112
+ }],
113
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
110
114
  });
111
115
  i++;
112
116
  continue;
113
117
  }
114
118
  if (msg.role === "assistant") {
115
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
116
- type: "text",
117
- text: msg.content
118
- }];
119
119
  prompt.push({
120
120
  role: "assistant",
121
- content,
122
- ...providerOptions ? { providerOptions } : {}
121
+ content: !contentModified && msg._assistantContent ? msg._assistantContent : [{
122
+ type: "text",
123
+ text: msg.content
124
+ }],
125
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
123
126
  });
124
127
  i++;
125
128
  continue;
@@ -127,9 +130,10 @@ function toAISDK(messages) {
127
130
  if (msg.role === "tool") {
128
131
  const toolResults = [];
129
132
  while (i < messages.length && messages[i].role === "tool") {
130
- const toolMsg = messages[i];
131
- if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._originalContent) toolResults.push(...toolMsg._originalContent);
132
- else toolResults.push({
133
+ const toolMsg = asAISDK(messages[i]);
134
+ if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._toolContent) {
135
+ for (const part of toolMsg._toolContent) if (part.type === "tool-result") toolResults.push(part);
136
+ } else toolResults.push({
133
137
  type: "tool-result",
134
138
  toolCallId: toolMsg.tool_call_id ?? "",
135
139
  toolName: toolMsg._toolName ?? "unknown",
@@ -156,7 +160,7 @@ function stringifyToolOutput(output) {
156
160
  case "error-text": return output.value;
157
161
  case "json":
158
162
  case "error-json": return JSON.stringify(output.value);
159
- case "content": return output.value.map((v) => v.type === "text" ? v.text ?? "" : "").filter(Boolean).join("\n");
163
+ case "content": return output.value.map((v) => v.type === "text" ? v.text : "").filter(Boolean).join("\n");
160
164
  default: return JSON.stringify(output);
161
165
  }
162
166
  }
@@ -257,16 +261,26 @@ function createMiddleware(options) {
257
261
  tokenizer: options.tokenizer ? (msgs) => options.tokenizer?.(msgs) ?? 0 : void 0,
258
262
  preserveRatio: options.compress?.preserveRatio ?? .8,
259
263
  compressionModel: options.compress?.model ? createCompressionAdapter(options.compress.model) : void 0,
260
- onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0
264
+ onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0,
265
+ onBudgetExceeded: options.onBudgetExceeded
261
266
  });
262
267
  return {
263
268
  specificationVersion: "v3",
264
269
  transformParams: async ({ params }) => {
265
270
  let { prompt } = params;
266
271
  if (options.truncate) prompt = await truncateToolResults(prompt, options.truncate);
267
- const irMessages = fromAISDK(prompt);
268
- const compressed = await janitor.compress(irMessages);
269
- if (compressed !== irMessages) prompt = toAISDK(compressed);
272
+ let irMessages = fromAISDK(prompt);
273
+ if (options.compact) {
274
+ const preCompact = irMessages;
275
+ irMessages = janitor.compact(irMessages, options.compact);
276
+ if (options.compact.clear.includes("thinking")) {
277
+ for (let i = 0; i < irMessages.length; i++) if (preCompact[i].thinking && !irMessages[i].thinking) delete irMessages[i]._assistantContent;
278
+ }
279
+ }
280
+ irMessages = await janitor.compress(irMessages);
281
+ prompt = toAISDK(irMessages);
282
+ if (options.dynamicState) prompt = await injectDynamicState(prompt, options.dynamicState);
283
+ if (options.transformContext) prompt = await options.transformContext(prompt);
270
284
  return {
271
285
  ...params,
272
286
  prompt
@@ -301,6 +315,52 @@ function createMiddleware(options) {
301
315
  };
302
316
  }
303
317
  /**
318
+ * Injects dynamic state XML into the AI SDK prompt.
319
+ *
320
+ * - `last_user`: Appends to the last user message's content parts.
321
+ * Leverages Recency Bias for maximum LLM attention.
322
+ * - `system`: Adds as a standalone system message at the end.
323
+ */
324
+ async function injectDynamicState(prompt, config) {
325
+ const state = await config.getState();
326
+ const xml = _context_chef_core.XmlGenerator.objectToXml(state, "dynamic_state");
327
+ if ((config.placement ?? "last_user") === "system") return [...prompt, {
328
+ role: "system",
329
+ content: `CURRENT TASK STATE:\n${xml}`
330
+ }];
331
+ const result = [...prompt];
332
+ const stateBlock = `\n\n${xml}\nAbove is the current system state. Use it to guide your next action.`;
333
+ for (let i = result.length - 1; i >= 0; i--) {
334
+ const msg = result[i];
335
+ if (msg.role === "user") {
336
+ result[i] = {
337
+ ...msg,
338
+ content: [...msg.content, {
339
+ type: "text",
340
+ text: stateBlock
341
+ }]
342
+ };
343
+ return result;
344
+ }
345
+ }
346
+ result.push({
347
+ role: "user",
348
+ content: [{
349
+ type: "text",
350
+ text: stateBlock.trim()
351
+ }]
352
+ });
353
+ return result;
354
+ }
355
+ /**
356
+ * Maps an IR role to a role accepted by generateText.
357
+ * Tool messages are handled separately before this is called.
358
+ */
359
+ function toCompressRole(role) {
360
+ if (role === "system" || role === "user" || role === "assistant") return role;
361
+ return "user";
362
+ }
363
+ /**
304
364
  * Adapts an AI SDK LanguageModelV3 into the compressionModel callback
305
365
  * that Janitor expects: (messages: Message[]) => Promise<string>
306
366
  *
@@ -324,7 +384,7 @@ function createCompressionAdapter(model) {
324
384
  };
325
385
  }
326
386
  return {
327
- role: m.role,
387
+ role: toCompressRole(m.role),
328
388
  content: m.content
329
389
  };
330
390
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Offloader","Janitor"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in `_originalContent` 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): Message[] {\n const messages: Message[] = [];\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 messages.push({\n role: 'user',\n content: text,\n _originalContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\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 }\n }\n\n const joinedText = text.join('\\n');\n const m: Message = {\n role: 'assistant',\n content: joinedText,\n _originalContent: 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 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 _originalContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses `_originalContent` when content is 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 = messages[i];\n const providerOptions = msg._providerOptions as SharedV3ProviderOptions | undefined;\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 ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'user',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'assistant',\n content,\n ...(providerOptions ? { 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 = messages[i];\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._originalContent) {\n toolResults.push(...(toolMsg._originalContent as LanguageModelV3ToolResultPart[]));\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: (toolMsg._toolName as string) ?? '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: { type: string; text?: string }) => (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\n ? new Offloader({ threshold, adapter: storage, storageDir: '' })\n : null;\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 text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= 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, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: { type: 'text', value: vfsResult.content } 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, headChars);\n const tail = text.slice(text.length - tailChars);\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\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 { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\nimport { Janitor, type Message } from '@context-chef/core';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\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 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 });\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. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\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 * 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(model: LanguageModelV3): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m) => {\n if (m.role === 'tool') {\n return {\n role: 'user' as const,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n // assistant messages with tool_calls: include tool call info in content\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' as const,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: m.role as 'system' | 'user' | 'assistant',\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 { fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } 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(model: LanguageModelV3, options: ContextChefOptions): 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 type { 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 messages.push({\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\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 }\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 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\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 text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= 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, { threshold, headChars, tailChars });\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, headChars);\n const tail = text.slice(text.length - tailChars);\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\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 LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware } 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 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 onBudgetExceeded: 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. Convert to IR\n let irMessages = fromAISDK(prompt);\n\n // 3. Compact (mechanical, zero LLM cost) before compression\n if (options.compact) {\n const preCompact = irMessages;\n irMessages = janitor.compact(irMessages, options.compact);\n\n // When thinking is stripped, invalidate adapter pass-through\n // so toAISDK reconstructs from IR fields (without reasoning)\n if (options.compact.clear.includes('thinking')) {\n for (let i = 0; i < irMessages.length; i++) {\n if (preCompact[i].thinking && !irMessages[i].thinking) {\n delete irMessages[i]._assistantContent;\n }\n }\n }\n }\n\n // 4. Compress history if over token budget\n irMessages = await janitor.compress(irMessages);\n\n // 5. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 6. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 7. 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 * 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
@@ -1,5 +1,5 @@
1
- import { LanguageModelV3, LanguageModelV3Prompt } from "@ai-sdk/provider";
2
- import { Message, VFSStorageAdapter } from "@context-chef/core";
1
+ import { LanguageModelV3, LanguageModelV3Message, LanguageModelV3Prompt, SharedV3ProviderOptions } from "@ai-sdk/provider";
2
+ import { ClearTarget, Message, VFSStorageAdapter } from "@context-chef/core";
3
3
  import { LanguageModelMiddleware } from "ai";
4
4
 
5
5
  //#region src/types.d.ts
@@ -24,6 +24,32 @@ interface CompressOptions {
24
24
  /** Ratio of context window to preserve for recent messages. Default: 0.8 */
25
25
  preserveRatio?: number;
26
26
  }
27
+ /**
28
+ * Mechanical compaction options — zero LLM cost.
29
+ * Runs before LLM-based compression to reduce token usage at no cost.
30
+ */
31
+ interface CompactConfig {
32
+ /** Which content types to clear from history. */
33
+ clear: ClearTarget[];
34
+ }
35
+ /**
36
+ * Dynamic state injection config.
37
+ * State is converted to XML and injected into the prompt for maximum LLM attention.
38
+ */
39
+ interface DynamicStateConfig {
40
+ /**
41
+ * Returns the current state object. Auto-converted to XML via `objectToXml`.
42
+ * Called on every model invocation.
43
+ */
44
+ getState: () => Record<string, unknown> | Promise<Record<string, unknown>>;
45
+ /**
46
+ * Where to inject the state.
47
+ * - `'last_user'` (default): Appends to the last user message. Leverages Recency Bias
48
+ * for maximum attention, preventing "Lost in the Middle" drift in long conversations.
49
+ * - `'system'`: Adds as a standalone system message at the end.
50
+ */
51
+ placement?: 'system' | 'last_user';
52
+ }
27
53
  interface ContextChefOptions {
28
54
  /** The model's context window size in tokens. */
29
55
  contextWindow: number;
@@ -31,25 +57,71 @@ interface ContextChefOptions {
31
57
  compress?: CompressOptions;
32
58
  /** Enable tool result truncation. Omit for no truncation. */
33
59
  truncate?: TruncateOptions;
60
+ /**
61
+ * Mechanical compaction before LLM compression.
62
+ * Clears specified content types (tool-result, thinking) at zero LLM cost.
63
+ */
64
+ compact?: CompactConfig;
65
+ /**
66
+ * Dynamic state injection. State is converted to XML and placed
67
+ * for maximum LLM attention (last_user or system position).
68
+ */
69
+ dynamicState?: DynamicStateConfig;
34
70
  /** Optional tokenizer for precise per-message token counting. */
35
71
  tokenizer?: (messages: unknown[]) => number;
36
72
  /** Hook called after compression occurs. */
37
73
  onCompress?: (summary: string, truncatedCount: number) => void;
74
+ /**
75
+ * Called when token budget is exceeded, before automatic compression.
76
+ * Return modified messages to replace history, or null/undefined to
77
+ * let default compression handle it.
78
+ */
79
+ onBudgetExceeded?: (history: Message[], tokenInfo: {
80
+ currentTokens: number;
81
+ limit: number;
82
+ }) => Message[] | null | undefined | Promise<Message[] | null | undefined>;
83
+ /**
84
+ * Transform the AI SDK prompt after compression, before sending to the model.
85
+ * Use for custom prompt manipulation, RAG injection, etc.
86
+ */
87
+ transformContext?: (prompt: LanguageModelV3Prompt) => LanguageModelV3Prompt | Promise<LanguageModelV3Prompt>;
38
88
  }
39
89
  //#endregion
40
90
  //#region src/adapter.d.ts
91
+ /** Content types for each AI SDK message role */
92
+ type UserContent = Extract<LanguageModelV3Message, {
93
+ role: 'user';
94
+ }>['content'];
95
+ type AssistantContent = Extract<LanguageModelV3Message, {
96
+ role: 'assistant';
97
+ }>['content'];
98
+ type ToolContent = Extract<LanguageModelV3Message, {
99
+ role: 'tool';
100
+ }>['content'];
101
+ /**
102
+ * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.
103
+ * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.
104
+ */
105
+ interface AISDKMessage extends Message {
106
+ _userContent?: UserContent;
107
+ _assistantContent?: AssistantContent;
108
+ _toolContent?: ToolContent;
109
+ _originalText?: string;
110
+ _providerOptions?: SharedV3ProviderOptions;
111
+ _toolName?: string;
112
+ }
41
113
  /**
42
114
  * Converts an AI SDK V3 prompt to context-chef IR messages.
43
115
  *
44
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
116
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
45
117
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
46
118
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
47
119
  */
48
- declare function fromAISDK(prompt: LanguageModelV3Prompt): Message[];
120
+ declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
49
121
  /**
50
122
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
51
123
  *
52
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
124
+ * Uses per-role original content when unmodified (detected via `_originalText`).
53
125
  * Falls back to constructing from IR fields when content was modified by Janitor
54
126
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
55
127
  */
@@ -88,5 +160,5 @@ declare function createMiddleware(options: ContextChefOptions): LanguageModelMid
88
160
  */
89
161
  declare function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3;
90
162
  //#endregion
91
- export { type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
163
+ export { type AISDKMessage, type CompactConfig, type CompressOptions, type ContextChefOptions, type DynamicStateConfig, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
92
164
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { LanguageModelMiddleware } from "ai";
2
- import { Message, VFSStorageAdapter } from "@context-chef/core";
3
- import { LanguageModelV3, LanguageModelV3Prompt } from "@ai-sdk/provider";
2
+ import { ClearTarget, Message, VFSStorageAdapter } from "@context-chef/core";
3
+ import { LanguageModelV3, LanguageModelV3Message, LanguageModelV3Prompt, SharedV3ProviderOptions } from "@ai-sdk/provider";
4
4
 
5
5
  //#region src/types.d.ts
6
6
  interface TruncateOptions {
@@ -24,6 +24,32 @@ interface CompressOptions {
24
24
  /** Ratio of context window to preserve for recent messages. Default: 0.8 */
25
25
  preserveRatio?: number;
26
26
  }
27
+ /**
28
+ * Mechanical compaction options — zero LLM cost.
29
+ * Runs before LLM-based compression to reduce token usage at no cost.
30
+ */
31
+ interface CompactConfig {
32
+ /** Which content types to clear from history. */
33
+ clear: ClearTarget[];
34
+ }
35
+ /**
36
+ * Dynamic state injection config.
37
+ * State is converted to XML and injected into the prompt for maximum LLM attention.
38
+ */
39
+ interface DynamicStateConfig {
40
+ /**
41
+ * Returns the current state object. Auto-converted to XML via `objectToXml`.
42
+ * Called on every model invocation.
43
+ */
44
+ getState: () => Record<string, unknown> | Promise<Record<string, unknown>>;
45
+ /**
46
+ * Where to inject the state.
47
+ * - `'last_user'` (default): Appends to the last user message. Leverages Recency Bias
48
+ * for maximum attention, preventing "Lost in the Middle" drift in long conversations.
49
+ * - `'system'`: Adds as a standalone system message at the end.
50
+ */
51
+ placement?: 'system' | 'last_user';
52
+ }
27
53
  interface ContextChefOptions {
28
54
  /** The model's context window size in tokens. */
29
55
  contextWindow: number;
@@ -31,25 +57,71 @@ interface ContextChefOptions {
31
57
  compress?: CompressOptions;
32
58
  /** Enable tool result truncation. Omit for no truncation. */
33
59
  truncate?: TruncateOptions;
60
+ /**
61
+ * Mechanical compaction before LLM compression.
62
+ * Clears specified content types (tool-result, thinking) at zero LLM cost.
63
+ */
64
+ compact?: CompactConfig;
65
+ /**
66
+ * Dynamic state injection. State is converted to XML and placed
67
+ * for maximum LLM attention (last_user or system position).
68
+ */
69
+ dynamicState?: DynamicStateConfig;
34
70
  /** Optional tokenizer for precise per-message token counting. */
35
71
  tokenizer?: (messages: unknown[]) => number;
36
72
  /** Hook called after compression occurs. */
37
73
  onCompress?: (summary: string, truncatedCount: number) => void;
74
+ /**
75
+ * Called when token budget is exceeded, before automatic compression.
76
+ * Return modified messages to replace history, or null/undefined to
77
+ * let default compression handle it.
78
+ */
79
+ onBudgetExceeded?: (history: Message[], tokenInfo: {
80
+ currentTokens: number;
81
+ limit: number;
82
+ }) => Message[] | null | undefined | Promise<Message[] | null | undefined>;
83
+ /**
84
+ * Transform the AI SDK prompt after compression, before sending to the model.
85
+ * Use for custom prompt manipulation, RAG injection, etc.
86
+ */
87
+ transformContext?: (prompt: LanguageModelV3Prompt) => LanguageModelV3Prompt | Promise<LanguageModelV3Prompt>;
38
88
  }
39
89
  //#endregion
40
90
  //#region src/adapter.d.ts
91
+ /** Content types for each AI SDK message role */
92
+ type UserContent = Extract<LanguageModelV3Message, {
93
+ role: 'user';
94
+ }>['content'];
95
+ type AssistantContent = Extract<LanguageModelV3Message, {
96
+ role: 'assistant';
97
+ }>['content'];
98
+ type ToolContent = Extract<LanguageModelV3Message, {
99
+ role: 'tool';
100
+ }>['content'];
101
+ /**
102
+ * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.
103
+ * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.
104
+ */
105
+ interface AISDKMessage extends Message {
106
+ _userContent?: UserContent;
107
+ _assistantContent?: AssistantContent;
108
+ _toolContent?: ToolContent;
109
+ _originalText?: string;
110
+ _providerOptions?: SharedV3ProviderOptions;
111
+ _toolName?: string;
112
+ }
41
113
  /**
42
114
  * Converts an AI SDK V3 prompt to context-chef IR messages.
43
115
  *
44
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
116
+ * Original AI SDK content is stored in per-role fields for lossless round-trip.
45
117
  * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.
46
118
  * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).
47
119
  */
48
- declare function fromAISDK(prompt: LanguageModelV3Prompt): Message[];
120
+ declare function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[];
49
121
  /**
50
122
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
51
123
  *
52
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
124
+ * Uses per-role original content when unmodified (detected via `_originalText`).
53
125
  * Falls back to constructing from IR fields when content was modified by Janitor
54
126
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
55
127
  */
@@ -88,5 +160,5 @@ declare function createMiddleware(options: ContextChefOptions): LanguageModelMid
88
160
  */
89
161
  declare function withContextChef(model: LanguageModelV3, options: ContextChefOptions): LanguageModelV3;
90
162
  //#endregion
91
- export { type CompressOptions, type ContextChefOptions, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
163
+ export { type AISDKMessage, type CompactConfig, type CompressOptions, type ContextChefOptions, type DynamicStateConfig, type TruncateOptions, createMiddleware, fromAISDK, toAISDK, withContextChef };
92
164
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  import { generateText, wrapLanguageModel } from "ai";
2
- import { Janitor, Offloader } from "@context-chef/core";
2
+ import { Janitor, Offloader, XmlGenerator } from "@context-chef/core";
3
3
 
4
4
  //#region src/adapter.ts
5
5
  /**
6
6
  * Converts an AI SDK V3 prompt to context-chef IR messages.
7
7
  *
8
- * Original AI SDK content is stored in `_originalContent` for lossless round-trip.
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
11
  */
@@ -25,7 +25,7 @@ function fromAISDK(prompt) {
25
25
  messages.push({
26
26
  role: "user",
27
27
  content: text,
28
- _originalContent: msg.content,
28
+ _userContent: msg.content,
29
29
  _originalText: text,
30
30
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
31
31
  });
@@ -49,7 +49,7 @@ function fromAISDK(prompt) {
49
49
  const m = {
50
50
  role: "assistant",
51
51
  content: joinedText,
52
- _originalContent: msg.content,
52
+ _assistantContent: msg.content,
53
53
  _originalText: joinedText,
54
54
  ...msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}
55
55
  };
@@ -65,7 +65,7 @@ function fromAISDK(prompt) {
65
65
  role: "tool",
66
66
  content: text,
67
67
  tool_call_id: part.toolCallId,
68
- _originalContent: [part],
68
+ _toolContent: [part],
69
69
  _originalText: text,
70
70
  _toolName: part.toolName
71
71
  });
@@ -75,9 +75,15 @@ function fromAISDK(prompt) {
75
75
  return messages;
76
76
  }
77
77
  /**
78
+ * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.
79
+ */
80
+ function asAISDK(msg) {
81
+ return msg;
82
+ }
83
+ /**
78
84
  * Converts context-chef IR messages back to AI SDK V3 prompt format.
79
85
  *
80
- * Uses `_originalContent` when content is unmodified (detected via `_originalText`).
86
+ * Uses per-role original content when unmodified (detected via `_originalText`).
81
87
  * Falls back to constructing from IR fields when content was modified by Janitor
82
88
  * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).
83
89
  */
@@ -85,40 +91,37 @@ function toAISDK(messages) {
85
91
  const prompt = [];
86
92
  let i = 0;
87
93
  while (i < messages.length) {
88
- const msg = messages[i];
89
- const providerOptions = msg._providerOptions;
94
+ const msg = asAISDK(messages[i]);
90
95
  const contentModified = msg._originalText !== void 0 && msg._originalText !== msg.content;
91
96
  if (msg.role === "system") {
92
97
  prompt.push({
93
98
  role: "system",
94
99
  content: msg.content,
95
- ...providerOptions ? { providerOptions } : {}
100
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
96
101
  });
97
102
  i++;
98
103
  continue;
99
104
  }
100
105
  if (msg.role === "user") {
101
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
102
- type: "text",
103
- text: msg.content
104
- }];
105
106
  prompt.push({
106
107
  role: "user",
107
- content,
108
- ...providerOptions ? { providerOptions } : {}
108
+ content: !contentModified && msg._userContent ? msg._userContent : [{
109
+ type: "text",
110
+ text: msg.content
111
+ }],
112
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
109
113
  });
110
114
  i++;
111
115
  continue;
112
116
  }
113
117
  if (msg.role === "assistant") {
114
- const content = !contentModified && Array.isArray(msg._originalContent) ? msg._originalContent : [{
115
- type: "text",
116
- text: msg.content
117
- }];
118
118
  prompt.push({
119
119
  role: "assistant",
120
- content,
121
- ...providerOptions ? { providerOptions } : {}
120
+ content: !contentModified && msg._assistantContent ? msg._assistantContent : [{
121
+ type: "text",
122
+ text: msg.content
123
+ }],
124
+ ...msg._providerOptions ? { providerOptions: msg._providerOptions } : {}
122
125
  });
123
126
  i++;
124
127
  continue;
@@ -126,9 +129,10 @@ function toAISDK(messages) {
126
129
  if (msg.role === "tool") {
127
130
  const toolResults = [];
128
131
  while (i < messages.length && messages[i].role === "tool") {
129
- const toolMsg = messages[i];
130
- if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._originalContent) toolResults.push(...toolMsg._originalContent);
131
- else toolResults.push({
132
+ const toolMsg = asAISDK(messages[i]);
133
+ if (!(toolMsg._originalText !== void 0 && toolMsg._originalText !== toolMsg.content) && toolMsg._toolContent) {
134
+ for (const part of toolMsg._toolContent) if (part.type === "tool-result") toolResults.push(part);
135
+ } else toolResults.push({
132
136
  type: "tool-result",
133
137
  toolCallId: toolMsg.tool_call_id ?? "",
134
138
  toolName: toolMsg._toolName ?? "unknown",
@@ -155,7 +159,7 @@ function stringifyToolOutput(output) {
155
159
  case "error-text": return output.value;
156
160
  case "json":
157
161
  case "error-json": return JSON.stringify(output.value);
158
- case "content": return output.value.map((v) => v.type === "text" ? v.text ?? "" : "").filter(Boolean).join("\n");
162
+ case "content": return output.value.map((v) => v.type === "text" ? v.text : "").filter(Boolean).join("\n");
159
163
  default: return JSON.stringify(output);
160
164
  }
161
165
  }
@@ -256,16 +260,26 @@ function createMiddleware(options) {
256
260
  tokenizer: options.tokenizer ? (msgs) => options.tokenizer?.(msgs) ?? 0 : void 0,
257
261
  preserveRatio: options.compress?.preserveRatio ?? .8,
258
262
  compressionModel: options.compress?.model ? createCompressionAdapter(options.compress.model) : void 0,
259
- onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0
263
+ onCompress: options.onCompress ? (summary, count) => options.onCompress?.(summary.content, count) : void 0,
264
+ onBudgetExceeded: options.onBudgetExceeded
260
265
  });
261
266
  return {
262
267
  specificationVersion: "v3",
263
268
  transformParams: async ({ params }) => {
264
269
  let { prompt } = params;
265
270
  if (options.truncate) prompt = await truncateToolResults(prompt, options.truncate);
266
- const irMessages = fromAISDK(prompt);
267
- const compressed = await janitor.compress(irMessages);
268
- if (compressed !== irMessages) prompt = toAISDK(compressed);
271
+ let irMessages = fromAISDK(prompt);
272
+ if (options.compact) {
273
+ const preCompact = irMessages;
274
+ irMessages = janitor.compact(irMessages, options.compact);
275
+ if (options.compact.clear.includes("thinking")) {
276
+ for (let i = 0; i < irMessages.length; i++) if (preCompact[i].thinking && !irMessages[i].thinking) delete irMessages[i]._assistantContent;
277
+ }
278
+ }
279
+ irMessages = await janitor.compress(irMessages);
280
+ prompt = toAISDK(irMessages);
281
+ if (options.dynamicState) prompt = await injectDynamicState(prompt, options.dynamicState);
282
+ if (options.transformContext) prompt = await options.transformContext(prompt);
269
283
  return {
270
284
  ...params,
271
285
  prompt
@@ -300,6 +314,52 @@ function createMiddleware(options) {
300
314
  };
301
315
  }
302
316
  /**
317
+ * Injects dynamic state XML into the AI SDK prompt.
318
+ *
319
+ * - `last_user`: Appends to the last user message's content parts.
320
+ * Leverages Recency Bias for maximum LLM attention.
321
+ * - `system`: Adds as a standalone system message at the end.
322
+ */
323
+ async function injectDynamicState(prompt, config) {
324
+ const state = await config.getState();
325
+ const xml = XmlGenerator.objectToXml(state, "dynamic_state");
326
+ if ((config.placement ?? "last_user") === "system") return [...prompt, {
327
+ role: "system",
328
+ content: `CURRENT TASK STATE:\n${xml}`
329
+ }];
330
+ const result = [...prompt];
331
+ const stateBlock = `\n\n${xml}\nAbove is the current system state. Use it to guide your next action.`;
332
+ for (let i = result.length - 1; i >= 0; i--) {
333
+ const msg = result[i];
334
+ if (msg.role === "user") {
335
+ result[i] = {
336
+ ...msg,
337
+ content: [...msg.content, {
338
+ type: "text",
339
+ text: stateBlock
340
+ }]
341
+ };
342
+ return result;
343
+ }
344
+ }
345
+ result.push({
346
+ role: "user",
347
+ content: [{
348
+ type: "text",
349
+ text: stateBlock.trim()
350
+ }]
351
+ });
352
+ return result;
353
+ }
354
+ /**
355
+ * Maps an IR role to a role accepted by generateText.
356
+ * Tool messages are handled separately before this is called.
357
+ */
358
+ function toCompressRole(role) {
359
+ if (role === "system" || role === "user" || role === "assistant") return role;
360
+ return "user";
361
+ }
362
+ /**
303
363
  * Adapts an AI SDK LanguageModelV3 into the compressionModel callback
304
364
  * that Janitor expects: (messages: Message[]) => Promise<string>
305
365
  *
@@ -323,7 +383,7 @@ function createCompressionAdapter(model) {
323
383
  };
324
384
  }
325
385
  return {
326
- role: m.role,
386
+ role: toCompressRole(m.role),
327
387
  content: m.content
328
388
  };
329
389
  }),
@@ -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 LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport type { Message, ToolCall } from '@context-chef/core';\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in `_originalContent` 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): Message[] {\n const messages: Message[] = [];\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 messages.push({\n role: 'user',\n content: text,\n _originalContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\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 }\n }\n\n const joinedText = text.join('\\n');\n const m: Message = {\n role: 'assistant',\n content: joinedText,\n _originalContent: 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 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 _originalContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses `_originalContent` when content is 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 = messages[i];\n const providerOptions = msg._providerOptions as SharedV3ProviderOptions | undefined;\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 ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'user',\n content,\n ...(providerOptions ? { providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n const content =\n !contentModified && Array.isArray(msg._originalContent)\n ? (msg._originalContent as any)\n : [{ type: 'text' as const, text: msg.content }];\n prompt.push({\n role: 'assistant',\n content,\n ...(providerOptions ? { 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 = messages[i];\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._originalContent) {\n toolResults.push(...(toolMsg._originalContent as LanguageModelV3ToolResultPart[]));\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n toolName: (toolMsg._toolName as string) ?? '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: { type: string; text?: string }) => (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\n ? new Offloader({ threshold, adapter: storage, storageDir: '' })\n : null;\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 text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= 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, { threshold, headChars, tailChars });\n newContent.push({\n ...part,\n output: { type: 'text', value: vfsResult.content } 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, headChars);\n const tail = text.slice(text.length - tailChars);\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\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 { LanguageModelV3, LanguageModelV3StreamPart } from '@ai-sdk/provider';\nimport { generateText, type LanguageModelMiddleware } from 'ai';\nimport { Janitor, type Message } from '@context-chef/core';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions } from './types';\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 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 });\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. Compress history if over token budget\n const irMessages = fromAISDK(prompt);\n const compressed = await janitor.compress(irMessages);\n\n // Only convert back if compression actually changed something\n if (compressed !== irMessages) {\n prompt = toAISDK(compressed);\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 * 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(model: LanguageModelV3): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m) => {\n if (m.role === 'tool') {\n return {\n role: 'user' as const,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n // assistant messages with tool_calls: include tool call info in content\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' as const,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: m.role as 'system' | 'user' | 'assistant',\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 { fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type { CompressOptions, ContextChefOptions, TruncateOptions } 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(model: LanguageModelV3, options: ContextChefOptions): 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 type { 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 messages.push({\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\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 }\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 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\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 text = extractText(part.output);\n if (text.length <= threshold || headChars + tailChars >= 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, { threshold, headChars, tailChars });\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, headChars);\n const tail = text.slice(text.length - tailChars);\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\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 LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware } 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 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 onBudgetExceeded: 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. Convert to IR\n let irMessages = fromAISDK(prompt);\n\n // 3. Compact (mechanical, zero LLM cost) before compression\n if (options.compact) {\n const preCompact = irMessages;\n irMessages = janitor.compact(irMessages, options.compact);\n\n // When thinking is stripped, invalidate adapter pass-through\n // so toAISDK reconstructs from IR fields (without reasoning)\n if (options.compact.clear.includes('thinking')) {\n for (let i = 0; i < irMessages.length; i++) {\n if (preCompact[i].thinking && !irMessages[i].thinking) {\n delete irMessages[i]._assistantContent;\n }\n }\n }\n }\n\n // 4. Compress history if over token budget\n irMessages = await janitor.compress(irMessages);\n\n // 5. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 6. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 7. 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 * 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": "0.1.2",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -15,11 +15,6 @@
15
15
  "files": [
16
16
  "dist"
17
17
  ],
18
- "scripts": {
19
- "build": "tsdown",
20
- "test": "vitest run",
21
- "typecheck": "tsc -p tsconfig.build.json --noEmit"
22
- },
23
18
  "keywords": [
24
19
  "ai-sdk",
25
20
  "vercel-ai",
@@ -43,7 +38,7 @@
43
38
  "url": "https://github.com/MyPrototypeWhat/context-chef/issues"
44
39
  },
45
40
  "dependencies": {
46
- "@context-chef/core": "workspace:*"
41
+ "@context-chef/core": "2.1.3"
47
42
  },
48
43
  "peerDependencies": {
49
44
  "@ai-sdk/provider": ">=3",
@@ -56,5 +51,10 @@
56
51
  "tsdown": "^0.20.3",
57
52
  "typescript": "^5.9.3",
58
53
  "vitest": "^4.0.18"
54
+ },
55
+ "scripts": {
56
+ "build": "tsdown",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit"
59
59
  }
60
- }
60
+ }