@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
100 lines (99 loc) • 4.59 kB
JavaScript
/**
* Stage 3: Structured LLM Summarization
*
* Uses the structured 10-section prompt to summarize older messages
* while preserving recent ones.
*/
import { randomUUID } from "crypto";
import { generateSummary } from "../../utils/conversationMemory.js";
import { estimateTokens } from "../../utils/tokenEstimation.js";
import { logger } from "../../utils/logger.js";
/**
* Find the split index using token counting — walk backward from the end,
* accumulating token counts until we've reserved `targetRecentTokens` worth
* of recent content. Everything before the split index gets summarized.
*/
function findSplitIndexByTokens(messages, targetRecentTokens, provider) {
let recentTokens = 0;
let splitIndex = messages.length;
for (let i = messages.length - 1; i >= 0; i--) {
const content = typeof messages[i].content === "string"
? messages[i].content
: JSON.stringify(messages[i].content);
const msgTokens = estimateTokens(content, provider);
if (recentTokens + msgTokens > targetRecentTokens) {
splitIndex = i + 1;
break;
}
recentTokens += msgTokens;
}
// Ensure at least one message is summarized
return Math.max(1, splitIndex);
}
export async function summarizeMessages(messages, config) {
const keepRecentRatio = config?.keepRecentRatio ?? 0.3;
if (messages.length <= 4) {
return { summarized: false, messages };
}
// Determine split point: prefer token-based when a target budget is available,
// fall back to message-count-based split for backward compatibility.
let splitIndex;
if (config?.targetTokens && config.targetTokens > 0) {
// Keep `keepRecentRatio` fraction of the target budget as recent context
const targetRecentTokens = Math.floor(config.targetTokens * keepRecentRatio);
// NOTE: config.provider is the summarization provider, not the generation
// provider. Ideally we'd use the generation/budget provider for accurate
// token estimation, but SummarizeConfig doesn't carry a separate
// budgetProvider field. This is a known design limitation.
splitIndex = findSplitIndexByTokens(messages, targetRecentTokens, config.provider);
}
else {
// Legacy: message-count-based split
const keepCount = Math.max(4, Math.ceil(messages.length * keepRecentRatio));
splitIndex = messages.length - keepCount;
}
// Clamp so at least the last message is always preserved (never summarize everything)
splitIndex = Math.min(splitIndex, messages.length - 1);
if (splitIndex <= 0) {
return { summarized: false, messages };
}
const messagesToSummarize = messages.slice(0, splitIndex);
const recentMessages = messages.slice(splitIndex);
// Find previous summary if exists
const previousSummary = messagesToSummarize.find((m) => m.metadata?.isSummary)?.content;
// Build effective memory config: use provided memoryConfig, or construct from provider/model
const effectiveMemoryConfig = config?.memoryConfig ? { ...config.memoryConfig } : {};
// Fill in summarization provider/model from compactor config if not already set
if (!effectiveMemoryConfig.summarizationProvider && config?.provider) {
effectiveMemoryConfig.summarizationProvider = config.provider;
}
if (!effectiveMemoryConfig.summarizationModel && config?.model) {
effectiveMemoryConfig.summarizationModel = config.model;
}
// Only skip if there's genuinely no provider available
if (!effectiveMemoryConfig.summarizationProvider &&
!effectiveMemoryConfig.summarizationModel) {
logger.debug("[ContextCompactor] Stage 3 skipped: no summarization provider or model available");
return { summarized: false, messages };
}
const summaryText = await generateSummary(messagesToSummarize, effectiveMemoryConfig, "[ContextCompactor]", previousSummary);
if (!summaryText) {
return { summarized: false, messages };
}
const summaryMessage = {
id: `summary-${randomUUID()}`,
role: "user",
content: `[Previous conversation summary]:\n\n${summaryText}`,
timestamp: new Date().toISOString(),
metadata: {
isSummary: true,
summarizesFrom: messagesToSummarize[0]?.id,
summarizesTo: messagesToSummarize[messagesToSummarize.length - 1]?.id,
},
};
return {
summarized: true,
messages: [summaryMessage, ...recentMessages],
summaryText,
};
}