Skip to content

feat(js/middleware/context-compression): Add LLM summarization - #6269

Draft
ssbushi wants to merge 1 commit into
sb/context-compression-2-dedupfrom
sb/context-compression-3-summarize
Draft

ssbushi wants to merge 1 commit into
sb/context-compression-2-dedupfrom
sb/context-compression-3-summarize

Conversation

@ssbushi

@ssbushi ssbushi commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
  • LLM Summarization: Condenses older history into a summary message using a designated model while preserving the last N messages untouched.
  • Custom prompt support
  • Cost optimization: Skips the LLM summarization call if cheaper strategies save enough context.
  • Adaptive overshoot scaling

@github-actions github-actions Bot added docs Improvements or additions to documentation js labels Sep 2, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an LLM-based summarization strategy to the context compression middleware, allowing older conversation history to be condensed while preserving recent context. It also adds dynamic preservation window adjustments for budget overshoots and options to skip summarization if cheaper strategies yield sufficient savings. Feedback on these changes highlights three key areas for improvement: reordering the operations so summarization runs before truncation to prevent premature history loss, correcting a preservation window calculation to prevent aggressive message discarding, and refining text rendering by using replaceAll with a function callback to avoid special character replacement issues and using stringifyOutput to prevent double-escaping tool outputs.

Comment on lines +682 to +685
const prompt = summaryPromptTemplate.replace(
'{conversation}',
conversationText
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using String.prototype.replace with a string replacement parameter can lead to unexpected behavior if the conversation text contains special replacement patterns (like $&, $1, etc.), which are common in code snippets or tool outputs. Additionally, replace only replaces the first occurrence. Using replaceAll with a function argument safely avoids interpreting these patterns and replaces all occurrences of the placeholder.

Suggested change
const prompt = summaryPromptTemplate.replace(
'{conversation}',
conversationText
);
const prompt = summaryPromptTemplate.replaceAll(
'{conversation}',
() => conversationText
);

Comment on lines +824 to +859
// 5. Message truncation
const effectiveMaxMessages = maxMessages
? Math.min(
maxMessages,
Math.max(
1,
maxMessages - (basePreserveRecent - adjustedPreserveRecent)
)
)
: undefined;

if (
effectiveMaxMessages &&
messages.length > effectiveMaxMessages
) {
const msgResult = applyMessageTruncation(
messages,
effectiveMaxMessages
);
messages = msgResult.messages;
noticeInserted = msgResult.noticeInserted;
}

// 6. Summarization
if (summaryModelRef) {
if (shouldSkipSummarization) {
skippedSummary = true;
} else {
const sumResult = await applySummarization(
messages,
adjustedSummaryPreserveRecent
);
messages = sumResult.messages;
isSummarized = sumResult.summarized;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two significant issues in this block of code:

  1. Architectural Order of Operations: Currently, message truncation (Step 5) is applied before summarization (Step 6). This causes the oldest messages to be permanently discarded before the summarization model has a chance to capture them, defeating the primary purpose of summarization (which is to preserve older history in a condensed form). Reordering these steps so that summarization runs first ensures maximum history preservation, with message truncation acting as a fallback or hard cap if summarization is skipped or fails.

  2. Preservation Window Violation: There is a discrepancy between the commented logic in the tests (effectiveMaxMessages = max(2+1, 6 - (4 - 2)) = 4) and the actual implementation (Math.max(1, ...)). If effectiveMaxMessages is allowed to drop below adjustedPreserveRecent + 1 (when a truncation notice is inserted), the message truncation step can aggressively discard the very messages that were supposed to be preserved. Setting the lower bound to adjustedPreserveRecent + (insertTruncationNotice ? 1 : 0) ensures the preservation guarantees are strictly respected.

            // 5. Summarization
            if (summaryModelRef) {
              if (shouldSkipSummarization) {
                skippedSummary = true;
              } else {
                const sumResult = await applySummarization(
                  messages,
                  adjustedSummaryPreserveRecent
                );
                messages = sumResult.messages;
                isSummarized = sumResult.summarized;
              }
            }

            // 6. Message truncation (as fallback or hard cap)
            const effectiveMaxMessages = maxMessages
              ? Math.min(
                  maxMessages,
                  Math.max(
                    adjustedPreserveRecent + (insertTruncationNotice ? 1 : 0),
                    maxMessages - (basePreserveRecent - adjustedPreserveRecent)
                  )
                )
              : undefined;

            if (
              effectiveMaxMessages &&
              messages.length > effectiveMaxMessages
            ) {
              const msgResult = applyMessageTruncation(
                messages,
                effectiveMaxMessages
              );
              messages = msgResult.messages;
              noticeInserted = msgResult.noticeInserted;
            }

Comment on lines +279 to +280
if (p.toolResponse)
return `[Tool response: ${p.toolResponse.name} → ${JSON.stringify(p.toolResponse.output)}]`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using JSON.stringify on p.toolResponse.output will double-escape and wrap string outputs in quotes, making the rendered conversation text less readable for the summarization model. Use the existing stringifyOutput helper instead, which safely handles both string and non-string outputs.

Suggested change
if (p.toolResponse)
return `[Tool response: ${p.toolResponse.name}${JSON.stringify(p.toolResponse.output)}]`;
if (p.toolResponse)
return `[Tool response: ${p.toolResponse.name}${stringifyOutput(p.toolResponse.output)}]`;

@pavelgj pavelgj changed the title feat(middleware/context-compression): Add LLM summarization feat(js/middleware/context-compression): Add LLM summarization Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation js

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant