@context-chef/ai-sdk-middleware 1.3.2 → 1.3.4

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
@@ -407,7 +407,7 @@ function compactPrompt(prompt, config) {
407
407
  async function resolveSkillMessages(skill) {
408
408
  if (!skill) return [];
409
409
  const resolved = typeof skill === "function" ? await skill() : skill;
410
- if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];
410
+ if (!resolved?.instructions?.trim()) return [];
411
411
  return [{
412
412
  role: "system",
413
413
  content: resolved.instructions
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["Offloader","Janitor","XmlGenerator"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["Offloader","Janitor","XmlGenerator"],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved?.instructions?.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -406,7 +406,7 @@ function compactPrompt(prompt, config) {
406
406
  async function resolveSkillMessages(skill) {
407
407
  if (!skill) return [];
408
408
  const resolved = typeof skill === "function" ? await skill() : skill;
409
- if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];
409
+ if (!resolved?.instructions?.trim()) return [];
410
410
  return [{
411
411
  role: "system",
412
412
  content: resolved.instructions
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved || !resolved.instructions || !resolved.instructions.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter.ts","../src/truncator.ts","../src/middleware.ts","../src/index.ts"],"sourcesContent":["import type {\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n SharedV3ProviderOptions,\n} from '@ai-sdk/provider';\nimport {\n type Attachment,\n ensureValidHistory,\n type Message,\n type ToolCall,\n} from '@context-chef/core';\n\n/** Content types for each AI SDK message role */\ntype UserContent = Extract<LanguageModelV3Message, { role: 'user' }>['content'];\ntype AssistantContent = Extract<LanguageModelV3Message, { role: 'assistant' }>['content'];\ntype ToolContent = Extract<LanguageModelV3Message, { role: 'tool' }>['content'];\n\n/**\n * Extended IR message with typed pass-through fields for lossless AI SDK round-trip.\n * Per-role content fields avoid union types, so no `as` casts are needed in `toAISDK`.\n */\nexport interface AISDKMessage extends Message {\n _userContent?: UserContent;\n _assistantContent?: AssistantContent;\n _toolContent?: ToolContent;\n _originalText?: string;\n _providerOptions?: SharedV3ProviderOptions;\n _toolName?: string;\n}\n\n/**\n * Converts an AI SDK V3 prompt to context-chef IR messages.\n *\n * Original AI SDK content is stored in per-role fields for lossless round-trip.\n * `_originalText` caches the extracted text so `toAISDK` can detect Janitor modifications.\n * `_providerOptions` preserves message-level provider options (e.g. Anthropic cache control).\n *\n * Boundary sanitization: the result is run through {@link ensureValidHistory}\n * to fix orphan tool results, missing tool results, and ensure the first\n * non-system message is a user message. This is a system boundary — IR\n * downstream is trusted to satisfy invariants.\n */\nexport function fromAISDK(prompt: LanguageModelV3Prompt): AISDKMessage[] {\n const messages: AISDKMessage[] = [];\n\n for (const msg of prompt) {\n if (msg.role === 'system') {\n messages.push({\n role: 'system',\n content: msg.content,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n });\n continue;\n }\n\n if (msg.role === 'user') {\n const text = msg.content\n .filter((p) => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n\n const attachments: Attachment[] = [];\n for (const part of msg.content) {\n if (part.type === 'file') {\n // `attachment.data` here is just a presence/metadata signal for Janitor\n // (used by `m.attachments?.length` checks and the `[image]`/`[document]`\n // placeholder helper, neither of which read `data`). The real binary\n // payload — including `Uint8Array` / `URL` shapes — round-trips losslessly\n // through `_userContent`, which `toAISDK` hands back to the AI SDK\n // provider verbatim. We only record `data` when it's already a string,\n // so we never invent a fake encoding for non-string inputs.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const m: AISDKMessage = {\n role: 'user',\n content: text,\n _userContent: msg.content,\n _originalText: text,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'assistant') {\n const text: string[] = [];\n const toolCalls: ToolCall[] = [];\n const attachments: Attachment[] = [];\n let thinking: { thinking: string } | undefined;\n\n for (const part of msg.content) {\n if (part.type === 'text') text.push(part.text);\n else if (part.type === 'tool-call') {\n toolCalls.push({\n id: part.toolCallId,\n type: 'function',\n function: {\n name: part.toolName,\n arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input),\n },\n });\n } else if (part.type === 'reasoning') {\n thinking = { thinking: part.text };\n } else if (part.type === 'file') {\n // See user-side comment above: data is a presence signal only;\n // _assistantContent carries the actual payload through round-trip.\n attachments.push({\n mediaType: part.mediaType,\n data: typeof part.data === 'string' ? part.data : '',\n ...(part.filename ? { filename: part.filename } : {}),\n });\n }\n }\n\n const joinedText = text.join('\\n');\n const m: AISDKMessage = {\n role: 'assistant',\n content: joinedText,\n _assistantContent: msg.content,\n _originalText: joinedText,\n ...(msg.providerOptions ? { _providerOptions: msg.providerOptions } : {}),\n };\n if (toolCalls.length > 0) m.tool_calls = toolCalls;\n if (thinking) m.thinking = thinking;\n if (attachments.length) m.attachments = attachments;\n messages.push(m);\n continue;\n }\n\n if (msg.role === 'tool') {\n for (const part of msg.content) {\n if (part.type === 'tool-result') {\n const text = stringifyToolOutput(part.output);\n messages.push({\n role: 'tool',\n content: text,\n tool_call_id: part.toolCallId,\n _toolContent: [part],\n _originalText: text,\n _toolName: part.toolName,\n });\n }\n }\n }\n }\n\n // Sanitize at boundary: enforce IR invariants before handing to caller.\n // Cast is safe — ensureValidHistory only inserts plain user/tool messages without\n // _userContent/_toolContent fields; toAISDK falls back to constructing from IR fields\n // for any message lacking those (see toAISDK below).\n return ensureValidHistory(messages) as AISDKMessage[];\n}\n\n/**\n * Narrows a generic Message to AISDKMessage for typed access to pass-through fields.\n */\nfunction asAISDK(msg: Message): AISDKMessage {\n return msg;\n}\n\n/**\n * Converts context-chef IR messages back to AI SDK V3 prompt format.\n *\n * Uses per-role original content when unmodified (detected via `_originalText`).\n * Falls back to constructing from IR fields when content was modified by Janitor\n * (e.g. compact() cleared tool results) or for new messages (e.g. compression summaries).\n */\nexport function toAISDK(messages: Message[]): LanguageModelV3Prompt {\n const prompt: LanguageModelV3Prompt = [];\n\n let i = 0;\n while (i < messages.length) {\n const msg = asAISDK(messages[i]);\n const contentModified = msg._originalText !== undefined && msg._originalText !== msg.content;\n\n if (msg.role === 'system') {\n prompt.push({\n role: 'system',\n content: msg.content,\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'user') {\n prompt.push({\n role: 'user',\n content:\n !contentModified && msg._userContent\n ? msg._userContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'assistant') {\n prompt.push({\n role: 'assistant',\n content:\n !contentModified && msg._assistantContent\n ? msg._assistantContent\n : [{ type: 'text', text: msg.content }],\n ...(msg._providerOptions ? { providerOptions: msg._providerOptions } : {}),\n });\n i++;\n continue;\n }\n\n if (msg.role === 'tool') {\n const toolResults: LanguageModelV3ToolResultPart[] = [];\n while (i < messages.length && messages[i].role === 'tool') {\n const toolMsg = asAISDK(messages[i]);\n const toolModified =\n toolMsg._originalText !== undefined && toolMsg._originalText !== toolMsg.content;\n\n if (!toolModified && toolMsg._toolContent) {\n for (const part of toolMsg._toolContent) {\n if (part.type === 'tool-result') {\n toolResults.push(part);\n }\n }\n } else {\n toolResults.push({\n type: 'tool-result',\n toolCallId: toolMsg.tool_call_id ?? '',\n // Prefer the round-trip pass-through field; fall back to IR `name`\n // (set by `ensureValidHistory` for sanitized placeholders), then\n // to a literal as last resort. Skipping `name` here would emit\n // `'unknown'` for sanitized placeholders, which strict providers\n // (Gemini, AI SDK validators) reject.\n toolName: toolMsg._toolName ?? toolMsg.name ?? 'unknown',\n output: { type: 'text', value: toolMsg.content },\n });\n }\n i++;\n }\n prompt.push({ role: 'tool', content: toolResults });\n continue;\n }\n\n i++;\n }\n\n return prompt;\n}\n\nfunction stringifyToolOutput(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v) => (v.type === 'text' ? v.text : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return JSON.stringify(output);\n }\n}\n","import type {\n LanguageModelV3Prompt,\n LanguageModelV3ToolResultOutput,\n LanguageModelV3ToolResultPart,\n} from '@ai-sdk/provider';\nimport { Offloader } from '@context-chef/core';\nimport type { TruncateOptions } from './types';\n\n/**\n * Truncates tool-result content within an AI SDK prompt when it exceeds the configured threshold.\n * When a storage adapter is provided, original content is persisted and a URI is included in the output.\n */\nexport async function truncateToolResults(\n prompt: LanguageModelV3Prompt,\n options: TruncateOptions,\n): Promise<LanguageModelV3Prompt> {\n const { threshold, headChars = 0, tailChars = 1000, storage } = options;\n\n const offloader = storage ? new Offloader({ threshold, adapter: storage, storageDir: '' }) : null;\n const policy = buildPolicyMap(options.perTool);\n\n const result: LanguageModelV3Prompt = [];\n\n for (const msg of prompt) {\n if (msg.role !== 'tool') {\n result.push(msg);\n continue;\n }\n\n const newContent: typeof msg.content = [];\n\n for (const part of msg.content) {\n if (part.type !== 'tool-result') {\n newContent.push(part);\n continue;\n }\n\n const toolPolicy = policy.get(part.toolName);\n if (toolPolicy?.preserve) {\n // Preserve = full bypass: no truncation, no storage write.\n newContent.push(part);\n continue;\n }\n\n const effThreshold = toolPolicy?.threshold ?? threshold;\n const effHeadChars = toolPolicy?.headChars ?? headChars;\n const effTailChars = toolPolicy?.tailChars ?? tailChars;\n\n const text = extractText(part.output);\n if (text.length <= effThreshold || effHeadChars + effTailChars >= text.length) {\n newContent.push(part);\n continue;\n }\n\n // With storage: use Offloader to persist original and get a URI-annotated truncation\n if (offloader) {\n try {\n const vfsResult = await offloader.offloadAsync(text, {\n threshold: effThreshold,\n headChars: effHeadChars,\n tailChars: effTailChars,\n });\n newContent.push({\n ...part,\n output: {\n type: 'text',\n value: vfsResult.content,\n } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n continue;\n } catch (error) {\n console.warn(\n `[context-chef] Storage adapter write failed for tool result (${part.toolCallId}). ` +\n `Falling back to simple truncation. Error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Fall through to simple truncation below\n }\n }\n\n // Without storage: simple truncation, original is discarded\n const head = text.slice(0, effHeadChars);\n const tail = text.slice(text.length - effTailChars);\n const totalLines = text.split('\\n').length;\n\n const truncated = [\n head,\n `\\n--- truncated (${totalLines} lines, ${text.length} chars total) ---\\n`,\n tail,\n ]\n .filter(Boolean)\n .join('')\n .trim();\n\n newContent.push({\n ...part,\n output: { type: 'text', value: truncated } satisfies LanguageModelV3ToolResultOutput,\n } satisfies LanguageModelV3ToolResultPart);\n }\n\n result.push({ ...msg, content: newContent });\n }\n\n return result;\n}\n\ntype ToolPolicy =\n | { preserve: true }\n | {\n preserve?: false;\n threshold?: number;\n headChars?: number;\n tailChars?: number;\n };\n\n/**\n * Normalises `perTool` into a name → policy lookup.\n * Bare strings become `{ preserve: true }`; objects keep their partial overrides.\n * Last entry wins on duplicate names.\n */\nfunction buildPolicyMap(perTool: TruncateOptions['perTool']): Map<string, ToolPolicy> {\n const map = new Map<string, ToolPolicy>();\n if (!perTool) return map;\n for (const entry of perTool) {\n if (typeof entry === 'string') {\n map.set(entry, { preserve: true });\n } else {\n map.set(entry.name, {\n threshold: entry.threshold,\n headChars: entry.headChars,\n tailChars: entry.tailChars,\n });\n }\n }\n return map;\n}\n\nfunction extractText(output: LanguageModelV3ToolResultOutput): string {\n switch (output.type) {\n case 'text':\n case 'error-text':\n return output.value;\n case 'json':\n case 'error-json':\n return JSON.stringify(output.value);\n case 'content':\n return output.value\n .map((v: { type: string; text?: string }) => (v.type === 'text' ? (v.text ?? '') : ''))\n .filter(Boolean)\n .join('\\n');\n default:\n return '';\n }\n}\n","import type {\n LanguageModelV3,\n LanguageModelV3Message,\n LanguageModelV3Prompt,\n LanguageModelV3StreamPart,\n} from '@ai-sdk/provider';\nimport { Janitor, type Message, XmlGenerator } from '@context-chef/core';\nimport { generateText, type LanguageModelMiddleware, type ModelMessage, pruneMessages } from 'ai';\n\nimport { fromAISDK, toAISDK } from './adapter';\nimport { truncateToolResults } from './truncator';\nimport type { ContextChefOptions, DynamicStateConfig } from './types';\n\ntype CompressRole = 'system' | 'user' | 'assistant';\n\n/**\n * Creates a LanguageModelMiddleware that transparently applies\n * context-chef compression and truncation to AI SDK model calls.\n *\n * The middleware holds a stateful Janitor instance that tracks\n * token usage across calls for compression decisions.\n */\nexport function createMiddleware(options: ContextChefOptions): LanguageModelMiddleware {\n let usageWarned = false;\n\n // The Janitor config is a discriminated union on `tokenizer`. Build the\n // two branches separately so the literal type matches one of the union\n // members exactly — a single literal carrying `tokenizer: Fn | undefined`\n // would not narrow to either branch.\n const sharedJanitorConfig = {\n contextWindow: options.contextWindow,\n toolResultStubThreshold: options.compress?.toolResultStubThreshold,\n compressionModel: options.compress?.model\n ? createCompressionAdapter(options.compress.model)\n : undefined,\n onCompress: options.onCompress\n ? (summary: Message, count: number) => options.onCompress?.(summary.content, count)\n : undefined,\n onBeforeCompress: options.onBeforeCompress ?? options.onBudgetExceeded,\n };\n\n let usagePreference = options.compress?.usagePreference;\n if (usagePreference === 'tokenizerFirst' && !options.tokenizer) {\n console.warn(\n \"[context-chef] compress.usagePreference: 'tokenizerFirst' requires a tokenizer. \" +\n \"Falling back to 'max'.\",\n );\n usagePreference = 'max';\n }\n\n const janitor = options.tokenizer\n ? new Janitor({\n ...sharedJanitorConfig,\n tokenizer: (msgs: Message[]) => options.tokenizer?.(msgs) ?? 0,\n preserveRatio: options.compress?.preserveRatio ?? 0.8,\n usagePreference,\n })\n : new Janitor({\n ...sharedJanitorConfig,\n // 'tokenizerFirst' has been sanitized above; the cast narrows the\n // remaining values to the no-tokenizer branch.\n usagePreference: usagePreference as 'max' | 'feedFirst' | undefined,\n });\n\n return {\n specificationVersion: 'v3',\n\n transformParams: async ({ params }) => {\n let { prompt } = params;\n\n // 1. Truncate large tool results\n if (options.truncate) {\n prompt = await truncateToolResults(prompt, options.truncate);\n }\n\n // 2. Compact (mechanical, zero LLM cost) via pruneMessages\n if (options.compact) {\n prompt = compactPrompt(prompt, options.compact);\n }\n\n // 3. Convert to IR and separate system messages from conversation.\n // System messages are standing instructions — they must not be\n // compressed away. Only conversation history goes through compact/compress.\n const allIR = fromAISDK(prompt);\n const systemMessages = allIR.filter((m) => m.role === 'system');\n let conversation = allIR.filter((m) => m.role !== 'system');\n\n // 4. Compress conversation history if over token budget\n conversation = await janitor.compress(conversation);\n\n // 5. Reassemble sandwich: user system + skill instructions + conversation.\n // The skill slot mirrors @context-chef/core compile() ordering\n // (SKILL_SPEC §6.3): a dedicated system message AFTER user system\n // and BEFORE the conversation history. Empty instructions are\n // skipped to avoid emitting an empty system message.\n const skillMessages = await resolveSkillMessages(options.skill);\n const irMessages = [...systemMessages, ...skillMessages, ...conversation];\n\n // 6. Convert back to AI SDK format\n prompt = toAISDK(irMessages);\n\n // 7. Dynamic state injection\n if (options.dynamicState) {\n prompt = await injectDynamicState(prompt, options.dynamicState);\n }\n\n // 8. Custom transform hook\n if (options.transformContext) {\n prompt = await options.transformContext(prompt);\n }\n\n return { ...params, prompt };\n },\n\n wrapGenerate: async ({ doGenerate }) => {\n const result = await doGenerate();\n\n if (result.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(result.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Model response did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n\n return result;\n },\n\n wrapStream: async ({ doStream }) => {\n const { stream, ...rest } = await doStream();\n\n const transform = new TransformStream<LanguageModelV3StreamPart, LanguageModelV3StreamPart>({\n transform(chunk, controller) {\n if (chunk.type === 'finish') {\n if (chunk.usage?.inputTokens?.total != null) {\n janitor.feedTokenUsage(chunk.usage.inputTokens.total);\n } else if (!usageWarned && !options.tokenizer) {\n usageWarned = true;\n console.warn(\n '[context-chef] Stream finish did not include usage.inputTokens.total. ' +\n 'Token-based compression may not trigger accurately. ' +\n 'Consider providing a tokenizer for precise token counting.',\n );\n }\n }\n controller.enqueue(chunk);\n },\n });\n\n return { ...rest, stream: stream.pipeThrough(transform) };\n },\n };\n}\n\n/**\n * Prunes a LanguageModelV3Prompt via AI SDK's pruneMessages.\n *\n * LanguageModelV3Message (from @ai-sdk/provider) and ModelMessage\n * (from @ai-sdk/provider-utils) share identical runtime structure but\n * differ at the TypeScript level (e.g. ImagePart, FilePart.data).\n * Since pruneMessages only filters — never transforms — every content\n * part in the output is an original V3 part, making the casts safe.\n */\nfunction compactPrompt(\n prompt: LanguageModelV3Prompt,\n config: Omit<Parameters<typeof pruneMessages>[0], 'messages'>,\n): LanguageModelV3Prompt {\n const messages = prompt.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as ModelMessage,\n );\n const pruned = pruneMessages({ messages, ...config });\n return pruned.map(\n (msg) =>\n ({\n role: msg.role,\n content: msg.content,\n providerOptions: msg.providerOptions,\n }) as LanguageModelV3Message,\n );\n}\n\n/**\n * Resolves the `skill` option into IR system messages to insert between\n * user system messages and conversation. Returns `[]` when no skill is\n * active, the resolver returns null/undefined, or instructions are empty.\n *\n * The function form is invoked on every transformParams call so the\n * caller can swap skills dynamically without recreating the middleware.\n */\nasync function resolveSkillMessages(skill: ContextChefOptions['skill']): Promise<Message[]> {\n if (!skill) return [];\n const resolved = typeof skill === 'function' ? await skill() : skill;\n // Treat whitespace-only instructions as empty — they would otherwise pollute\n // the prompt and create a needless cache breakpoint between system and history.\n if (!resolved?.instructions?.trim()) return [];\n return [{ role: 'system', content: resolved.instructions }];\n}\n\n/**\n * Injects dynamic state XML into the AI SDK prompt.\n *\n * - `last_user`: Appends to the last user message's content parts.\n * Leverages Recency Bias for maximum LLM attention.\n * - `system`: Adds as a standalone system message at the end.\n */\nasync function injectDynamicState(\n prompt: LanguageModelV3Prompt,\n config: DynamicStateConfig,\n): Promise<LanguageModelV3Prompt> {\n const state = await config.getState();\n const xml = XmlGenerator.objectToXml(state, 'dynamic_state');\n const placement = config.placement ?? 'last_user';\n\n if (placement === 'system') {\n return [...prompt, { role: 'system', content: `CURRENT TASK STATE:\\n${xml}` }];\n }\n\n // last_user: inject into the last user message\n const result = [...prompt];\n const stateBlock = `\\n\\n${xml}\\nAbove is the current system state. Use it to guide your next action.`;\n\n for (let i = result.length - 1; i >= 0; i--) {\n const msg = result[i];\n if (msg.role === 'user') {\n result[i] = {\n ...msg,\n content: [...msg.content, { type: 'text', text: stateBlock }],\n };\n return result;\n }\n }\n\n // No user message found — append as new user message\n result.push({\n role: 'user',\n content: [{ type: 'text', text: stateBlock.trim() }],\n });\n return result;\n}\n\n/**\n * Maps an IR role to a role accepted by generateText.\n * Tool messages are handled separately before this is called.\n */\nfunction toCompressRole(role: string): CompressRole {\n if (role === 'system' || role === 'user' || role === 'assistant') return role;\n return 'user';\n}\n\n/**\n * Adapts an AI SDK LanguageModelV3 into the compressionModel callback\n * that Janitor expects: (messages: Message[]) => Promise<string>\n *\n * Tool messages are converted to user messages describing the tool interaction,\n * since generateText only accepts system/user/assistant roles.\n */\nfunction createCompressionAdapter(\n model: LanguageModelV3,\n): (messages: Message[]) => Promise<string> {\n return async (messages: Message[]): Promise<string> => {\n const formatted = messages.map((m): { role: CompressRole; content: string } => {\n if (m.role === 'tool') {\n return {\n role: 'user' satisfies CompressRole,\n content: `[Tool result${m.tool_call_id ? ` (${m.tool_call_id})` : ''}: ${m.content}]`,\n };\n }\n if (m.role === 'assistant' && m.tool_calls?.length) {\n const toolCallsDesc = m.tool_calls\n .map((tc) => `[Called tool: ${tc.function.name}(${tc.function.arguments})]`)\n .join('\\n');\n return {\n role: 'assistant' satisfies CompressRole,\n content: m.content ? `${m.content}\\n${toolCallsDesc}` : toolCallsDesc,\n };\n }\n return {\n role: toCompressRole(m.role),\n content: m.content,\n };\n });\n\n const { text } = await generateText({\n model,\n messages: formatted,\n maxOutputTokens: 2048,\n });\n\n return text || '[Compression produced no output]';\n };\n}\n","import type { LanguageModelV3 } from '@ai-sdk/provider';\nimport { wrapLanguageModel } from 'ai';\n\nimport { createMiddleware } from './middleware';\nimport type { ContextChefOptions } from './types';\n\nexport { type AISDKMessage, fromAISDK, toAISDK } from './adapter';\nexport { createMiddleware } from './middleware';\nexport type {\n CompactConfig,\n CompressOptions,\n ContextChefOptions,\n DynamicStateConfig,\n TruncateOptions,\n} from './types';\n\n/**\n * Wraps an AI SDK language model with context-chef middleware for\n * transparent history compression, tool result truncation, and token budget management.\n *\n * @example\n * ```typescript\n * import { withContextChef } from '@context-chef/ai-sdk-middleware';\n * import { openai } from '@ai-sdk/openai';\n * import { generateText } from 'ai';\n *\n * const model = withContextChef(openai('gpt-4o'), {\n * contextWindow: 128_000,\n * compress: { model: openai('gpt-4o-mini') },\n * truncate: { threshold: 5000, headChars: 500, tailChars: 1000 },\n * });\n *\n * // Use exactly like normal — zero other code changes\n * const result = await generateText({ model, messages, tools });\n * ```\n */\nexport function withContextChef(\n model: LanguageModelV3,\n options: ContextChefOptions,\n): LanguageModelV3 {\n const middleware = createMiddleware(options);\n return wrapLanguageModel({ model, middleware });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-chef/ai-sdk-middleware",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -39,7 +39,7 @@
39
39
  "url": "https://github.com/MyPrototypeWhat/context-chef/issues"
40
40
  },
41
41
  "dependencies": {
42
- "@context-chef/core": "3.4.1"
42
+ "@context-chef/core": "3.4.3"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@ai-sdk/provider": ">=3",