ai-patterns
Version:
Production-ready TypeScript patterns to build solid and robust AI applications. Retry logic, circuit breakers, rate limiting, human-in-the-loop escalation, prompt versioning, response validation, context window management, and more—all with complete type
239 lines • 10.1 kB
JavaScript
;
/**
* Smart Context Window Management Pattern
*
* Automatically manages context token limits by truncating, summarizing,
* or chunking content to prevent context_length_exceeded errors.
*
* @example
* ```typescript
* import { smartContextWindow, ContextStrategy } from 'ai-patterns';
*
* const result = await smartContextWindow({
* execute: async (messages) => {
* return await generateText({
* model: openai('gpt-4-turbo'),
* messages
* });
* },
* messages: conversationHistory,
* maxTokens: 120000,
* strategy: ContextStrategy.SLIDING_WINDOW,
* keepRecentCount: 50
* });
* ```
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.smartContextWindow = smartContextWindow;
exports.createAISummarizer = createAISummarizer;
const context_window_1 = require("../types/context-window");
const common_1 = require("../types/common");
const errors_1 = require("../types/errors");
/**
* Simple token counter (approximately 4 characters per token)
* For production, use a proper tokenizer like tiktoken
*/
function defaultTokenCounter(messages) {
const totalChars = messages.reduce((sum, msg) => {
return sum + msg.content.length + (msg.name?.length || 0) + msg.role.length;
}, 0);
// Rough estimation: ~4 chars per token
return Math.ceil(totalChars / 4);
}
/**
* Sliding window strategy: keep only recent messages
*/
function slidingWindowStrategy(messages, keepCount) {
// Always keep system messages
const systemMessages = messages.filter((m) => m.role === "system");
const otherMessages = messages.filter((m) => m.role !== "system");
// Keep the most recent messages
const recentMessages = otherMessages.slice(-keepCount);
return [...systemMessages, ...recentMessages];
}
/**
* Prioritize important strategy: keep system, mentions, and important messages
*/
function prioritizeImportantStrategy(messages) {
return messages.filter((msg) => {
// Always keep system messages
if (msg.role === "system")
return true;
// Keep messages marked as important
if (msg.metadata?.important === true)
return true;
// Keep messages with mentions (@)
if (msg.content.includes("@"))
return true;
// Keep messages that look like commands (start with /)
if (msg.content.trim().startsWith("/"))
return true;
// Keep function/tool calls
if (msg.role === "function" || msg.role === "tool")
return true;
return false;
});
}
/**
* Truncate middle strategy: keep first and last messages, remove middle
*/
function truncateMiddleStrategy(messages, keepFirstCount = 10, keepLastCount = 30) {
if (messages.length <= keepFirstCount + keepLastCount) {
return messages;
}
const systemMessages = messages.filter((m) => m.role === "system");
const otherMessages = messages.filter((m) => m.role !== "system");
const firstMessages = otherMessages.slice(0, keepFirstCount);
const lastMessages = otherMessages.slice(-keepLastCount);
// Add a marker message to indicate truncation
const truncationMarker = {
role: "system",
content: `[... ${otherMessages.length - keepFirstCount - keepLastCount} messages omitted ...]`,
};
return [...systemMessages, ...firstMessages, truncationMarker, ...lastMessages];
}
/**
* Apply optimization strategy to messages
*/
async function optimizeMessages(messages, currentTokens, config) {
const { strategy = context_window_1.ContextStrategy.SLIDING_WINDOW, strategies = {}, keepRecentCount = 50, summarizeOldCount = 20, summarizer, logger = common_1.defaultLogger, } = config;
logger.debug(`Optimizing messages using strategy: ${strategy}`);
// Use custom strategy if provided
if (strategy === context_window_1.ContextStrategy.CUSTOM && strategies.custom) {
return await strategies.custom(messages, currentTokens, config.maxTokens);
}
// Use user-provided strategy override for built-in strategies
if (strategy !== context_window_1.ContextStrategy.CUSTOM && strategies[strategy]) {
const result = strategies[strategy](messages);
return result instanceof Promise ? await result : result;
}
// Built-in strategies
switch (strategy) {
case context_window_1.ContextStrategy.SLIDING_WINDOW:
return slidingWindowStrategy(messages, keepRecentCount);
case context_window_1.ContextStrategy.SUMMARIZE_OLD:
if (!summarizer) {
logger.warn("No summarizer provided, falling back to sliding-window");
return slidingWindowStrategy(messages, keepRecentCount);
}
const systemMessages = messages.filter((m) => m.role === "system");
const otherMessages = messages.filter((m) => m.role !== "system");
if (otherMessages.length <= summarizeOldCount) {
return messages;
}
const oldMessages = otherMessages.slice(0, -summarizeOldCount);
const recentMessages = otherMessages.slice(-summarizeOldCount);
try {
const summary = await summarizer(oldMessages);
const summaryMessage = {
role: "system",
content: `Previous conversation summary: ${summary}`,
};
return [...systemMessages, summaryMessage, ...recentMessages];
}
catch (error) {
logger.error("Failed to summarize messages, falling back to sliding-window", {
error: error instanceof Error ? error.message : String(error),
});
return slidingWindowStrategy(messages, keepRecentCount);
}
case context_window_1.ContextStrategy.PRIORITIZE_IMPORTANT:
return prioritizeImportantStrategy(messages);
case context_window_1.ContextStrategy.TRUNCATE_MIDDLE:
return truncateMiddleStrategy(messages);
default:
logger.warn(`Unknown strategy: ${strategy}, using sliding-window`);
return slidingWindowStrategy(messages, keepRecentCount);
}
}
/**
* Smart context window management
*/
async function smartContextWindow(config) {
const { execute, messages, maxTokens, tokenCounter = defaultTokenCounter, onTruncation, onOptimization, logger = common_1.defaultLogger, } = config;
// Validate configuration
if (!messages || messages.length === 0) {
throw new errors_1.PatternError("At least one message is required", errors_1.ErrorCode.INVALID_ARGUMENT);
}
if (maxTokens <= 0) {
throw new errors_1.PatternError("maxTokens must be greater than 0", errors_1.ErrorCode.INVALID_ARGUMENT);
}
const originalMessageCount = messages.length;
// Count tokens in original messages
const originalTokens = typeof tokenCounter === "function"
? await tokenCounter(messages)
: defaultTokenCounter(messages);
logger.debug(`Original context: ${originalMessageCount} messages, ${originalTokens} tokens`);
let optimizedMessages = messages;
let wasOptimized = false;
let strategyUsed;
// Check if optimization is needed
if (originalTokens > maxTokens) {
logger.info(`Context exceeds limit (${originalTokens} > ${maxTokens}), applying optimization`);
optimizedMessages = await optimizeMessages(messages, originalTokens, config);
wasOptimized = true;
strategyUsed = config.strategy || "sliding-window";
// Count tokens in optimized messages
const optimizedTokens = await tokenCounter(optimizedMessages);
logger.info(`Optimized context: ${optimizedMessages.length} messages, ${optimizedTokens} tokens`);
// Call optimization callback
if (onOptimization) {
await onOptimization(strategyUsed, optimizedMessages);
}
// Call truncation callback
if (onTruncation) {
await onTruncation(originalMessageCount, optimizedMessages.length, originalTokens, optimizedTokens);
}
// Check if still over limit
if (optimizedTokens > maxTokens) {
logger.warn(`Optimized context still exceeds limit (${optimizedTokens} > ${maxTokens})`);
}
}
else {
logger.debug("Context within limits, no optimization needed");
}
// Execute with optimized messages
try {
const result = await execute(optimizedMessages);
const optimizedTokens = wasOptimized
? await tokenCounter(optimizedMessages)
: originalTokens;
return {
value: result,
originalMessageCount,
optimizedMessageCount: optimizedMessages.length,
originalTokens,
optimizedTokens,
wasOptimized,
strategyUsed: wasOptimized ? strategyUsed : undefined,
timestamp: Date.now(),
};
}
catch (error) {
const execError = error instanceof Error ? error : new Error(String(error));
logger.error("Execution failed with optimized context", {
error: execError.message,
});
throw new errors_1.PatternError(`Smart context window execution failed: ${execError.message}`, errors_1.ErrorCode.EXECUTION_FAILED, execError, {
originalMessageCount,
optimizedMessageCount: optimizedMessages.length,
originalTokens,
wasOptimized,
strategyUsed,
});
}
}
/**
* Helper function to create a summarizer using an AI model
*/
function createAISummarizer(summarizeFn) {
return async (messages) => {
try {
return await summarizeFn(messages);
}
catch (error) {
throw new errors_1.PatternError(`Failed to summarize messages: ${error instanceof Error ? error.message : String(error)}`, errors_1.ErrorCode.EXECUTION_FAILED, error instanceof Error ? error : undefined);
}
};
}
//# sourceMappingURL=context-window.js.map