Skip to content

Commit a576ad7

Browse files
logancyangclaude
andauthored
feat: implement auto-compact context with map-reduce summarization (#2106)
- Add ContextCompactor class for map-reduce style context summarization - Implement auto-compact when context exceeds configurable threshold - Track note paths for tags/folders in L3 segment metadata for L2 deduplication - Add loadAndAddChatHistory for streamlined chat history loading - Store compacted paths in envelope metadata for multi-turn deduplication - Fix: Only track L3 context paths in compactedPaths (excludes L5 user message files) Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent bb2ebf1 commit a576ad7

17 files changed

+1146
-209
lines changed

src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { err2String, getMessageRole, withSuppressedTokenWarnings } from "@/utils
1414
import { formatErrorChunk, processToolResults } from "@/utils/toolResultUtils";
1515
import { AIMessage, BaseMessage, HumanMessage } from "@langchain/core/messages";
1616
import { CopilotPlusChainRunner } from "./CopilotPlusChainRunner";
17-
import { addChatHistoryToMessages } from "./utils/chatHistoryUtils";
17+
import { loadAndAddChatHistory } from "./utils/chatHistoryUtils";
1818
import {
1919
joinPromptSections,
2020
messageRequiresTools,
@@ -281,7 +281,11 @@ ${params}
281281

282282
logInfo("[Agent] Using envelope-based context construction");
283283

284-
const context = await this.prepareAgentConversation(userMessage, chatModel);
284+
const context = await this.prepareAgentConversation(
285+
userMessage,
286+
chatModel,
287+
options.updateLoadingMessage
288+
);
285289

286290
try {
287291
const loopResult = await this.executeAgentLoop({
@@ -365,11 +369,13 @@ ${params}
365369
*
366370
* @param userMessage - The initiating user message from the UI.
367371
* @param chatModel - The active chat model instance.
372+
* @param updateLoadingMessage - Optional callback to show loading status.
368373
* @returns Aggregated context required for the autonomous agent loop.
369374
*/
370375
private async prepareAgentConversation(
371376
userMessage: ChatMessage,
372-
chatModel: any
377+
chatModel: any,
378+
updateLoadingMessage?: (message: string) => void
373379
): Promise<AgentRunContext> {
374380
const conversationMessages: ConversationMessage[] = [];
375381
const iterationHistory: string[] = [];
@@ -392,10 +398,8 @@ ${params}
392398
debug: false,
393399
});
394400

395-
// Get memory for chat history
401+
// Get memory for chat history loading
396402
const memory = this.chainManager.memoryManager.getMemory();
397-
const memoryVariables = await memory.loadMemoryVariables({});
398-
const rawHistory = memoryVariables.history || [];
399403

400404
// Build system message: L1+L2 from envelope + tool-only sections
401405
const systemMessage = baseMessages.find((m) => m.role === "system");
@@ -433,14 +437,14 @@ ${params}
433437
});
434438
}
435439

436-
// Insert L4 (chat history) between system and user
437-
addChatHistoryToMessages(rawHistory, conversationMessages);
438-
439440
// Extract L5 for original prompt and adapter enhancement
440441
const l5User = envelope.layers.find((l) => l.id === "L5_USER");
441442
const l5Text = l5User?.text || "";
442443
const originalUserPrompt = l5Text || userMessage.originalMessage || userMessage.message;
443444

445+
// Insert L4 (chat history) between system and user
446+
await loadAndAddChatHistory(memory, conversationMessages);
447+
444448
// Extract user content (L3 smart references + L5) from base messages
445449
const userMessageContent = baseMessages.find((m) => m.role === "user");
446450
if (userMessageContent) {

src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { getApiErrorMessage, getMessageRole, withSuppressedTokenWarnings } from
3030
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
3131
import { BaseChainRunner } from "./BaseChainRunner";
3232
import { ActionBlockStreamer } from "./utils/ActionBlockStreamer";
33-
import { addChatHistoryToMessages } from "./utils/chatHistoryUtils";
33+
import { loadAndAddChatHistory } from "./utils/chatHistoryUtils";
3434
import {
3535
addFallbackSources,
3636
formatSourceCatalog,
@@ -338,7 +338,12 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
338338
*/
339339
private async extractImagesFromContextBlock(
340340
l3Text: string,
341-
source: { tagName: string; identifierTag: string; displayName: string; useForResolution: boolean }
341+
source: {
342+
tagName: string;
343+
identifierTag: string;
344+
displayName: string;
345+
useForResolution: boolean;
346+
}
342347
): Promise<string[]> {
343348
// Match the context block
344349
const blockRegex = new RegExp(`<${source.tagName}>([\\s\\S]*?)<\\/${source.tagName}>`);
@@ -354,7 +359,9 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
354359
if (!content) return [];
355360

356361
// Extract identifier (path or url) for logging and optional resolution
357-
const identifierRegex = new RegExp(`<${source.identifierTag}>(.*?)<\\/${source.identifierTag}>`);
362+
const identifierRegex = new RegExp(
363+
`<${source.identifierTag}>(.*?)<\\/${source.identifierTag}>`
364+
);
358365
const identifierMatch = identifierRegex.exec(block);
359366
const identifier = identifierMatch ? identifierMatch[1] : undefined;
360367

@@ -472,8 +479,18 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
472479
// - displayName: human-readable name for logs
473480
// - useForResolution: whether to use identifier for vault path resolution
474481
const contextSources = [
475-
{ tagName: "active_note", identifierTag: "path", displayName: "active note", useForResolution: true },
476-
{ tagName: "active_web_tab", identifierTag: "url", displayName: "active web tab", useForResolution: false },
482+
{
483+
tagName: "active_note",
484+
identifierTag: "path",
485+
displayName: "active note",
486+
useForResolution: true,
487+
},
488+
{
489+
tagName: "active_web_tab",
490+
identifierTag: "url",
491+
displayName: "active web tab",
492+
useForResolution: false,
493+
},
477494
];
478495

479496
for (const source of contextSources) {
@@ -549,12 +566,11 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
549566
allToolOutputs: any[],
550567
abortController: AbortController,
551568
thinkStreamer: ThinkBlockStreamer,
552-
originalUserQuestion: string
569+
originalUserQuestion: string,
570+
updateLoadingMessage?: (message: string) => void
553571
): Promise<void> {
554-
// Get chat history
572+
// Get memory for chat history loading
555573
const memory = this.chainManager.memoryManager.getMemory();
556-
const memoryVariables = await memory.loadMemoryVariables({});
557-
const rawHistory = memoryVariables.history || [];
558574

559575
// Get chat model
560576
const chatModel = this.chainManager.chatModelManager.getChatModel();
@@ -590,9 +606,9 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
590606
}
591607

592608
// Insert L4 (chat history) between system and user
593-
addChatHistoryToMessages(rawHistory, messages);
609+
await loadAndAddChatHistory(memory, messages);
594610

595-
// Find user message (L3 smart references + L5)
611+
// Process user message (L3 smart references + L5)
596612
const userMessageContent = baseMessages.find((m) => m.role === "user");
597613
if (userMessageContent) {
598614
let finalUserContent;
@@ -827,7 +843,8 @@ OUTPUT ONLY XML - NO OTHER TEXT.`;
827843
allToolOutputs,
828844
abortController,
829845
thinkStreamer,
830-
cleanedUserMessage
846+
cleanedUserMessage,
847+
updateLoadingMessage
831848
);
832849
} catch (error: any) {
833850
// Reset loading message to default

src/LLMProviders/chainRunner/LLMChainRunner.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { LayerToMessagesConverter } from "@/context/LayerToMessagesConverter";
33
import { logInfo } from "@/logger";
44
import { getSettings } from "@/settings/model";
55
import { ChatMessage } from "@/types/message";
6-
import { extractChatHistory, findCustomModel, withSuppressedTokenWarnings } from "@/utils";
6+
import { findCustomModel, withSuppressedTokenWarnings } from "@/utils";
77
import { BaseChainRunner } from "./BaseChainRunner";
8+
import { loadAndAddChatHistory } from "./utils/chatHistoryUtils";
89
import { recordPromptPayload } from "./utils/promptPayloadRecorder";
910
import { ThinkBlockStreamer } from "./utils/ThinkBlockStreamer";
1011
import { getModelKey } from "@/aiParams";
@@ -24,19 +25,13 @@ export class LLMChainRunner extends BaseChainRunner {
2425

2526
logInfo("[LLMChainRunner] Using envelope-based context");
2627

27-
// Get chat history from memory (L4)
28-
const memory = this.chainManager.memoryManager.getMemory();
29-
const memoryVariables = await memory.loadMemoryVariables({});
30-
const chatHistory = extractChatHistory(memoryVariables);
31-
3228
// Convert envelope to messages (L1 system + L2+L3+L5 user)
3329
const baseMessages = LayerToMessagesConverter.convert(userMessage.contextEnvelope, {
3430
includeSystemMessage: true,
3531
mergeUserContent: true,
3632
debug: false,
3733
});
3834

39-
// Insert L4 (chat history) between system and user
4035
const messages: any[] = [];
4136

4237
// Add system message (L1)
@@ -46,9 +41,8 @@ export class LLMChainRunner extends BaseChainRunner {
4641
}
4742

4843
// Add chat history (L4)
49-
for (const entry of chatHistory) {
50-
messages.push({ role: entry.role, content: entry.content });
51-
}
44+
const memory = this.chainManager.memoryManager.getMemory();
45+
await loadAndAddChatHistory(memory, messages);
5246

5347
// Add user message (L2+L3+L5 merged)
5448
const userMessageContent = baseMessages.find((m) => m.role === "user");

src/LLMProviders/chainRunner/VaultQAChainRunner.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
withSuppressedTokenWarnings,
1616
} from "@/utils";
1717
import { BaseChainRunner } from "./BaseChainRunner";
18+
import { loadAndAddChatHistory } from "./utils/chatHistoryUtils";
1819
import {
1920
formatSourceCatalog,
2021
getQACitationInstructions,
@@ -175,9 +176,7 @@ export class VaultQAChainRunner extends BaseChainRunner {
175176
}
176177

177178
// Insert L4 (chat history) between system and user
178-
for (const entry of chatHistory) {
179-
messages.push({ role: entry.role, content: entry.content });
180-
}
179+
await loadAndAddChatHistory(memory, messages);
181180

182181
// Add user message with RAG prepended
183182
// User message now contains: RAG results + citations + L3 smart references + L5

0 commit comments

Comments
 (0)