Skip to content

Conversation

@Robird
Copy link

@Robird Robird commented Dec 5, 2025

Implements a more granular summarization strategy that only compresses
the first half of unsummarized rounds, preserving more recent context.

  • Add FlattenedRound interface for tracking round origins
  • Refactor getProps() with feature flag github.copilot.chat.halfContextSummarization
  • Preserve original logic in getPropsLegacy() as fallback
  • New getPropsHalfContext() flattens rounds and splits at midpoint
  • Add 20 unit tests covering split logic, edge cases, and fallback

This enables fine-grained compression that can cut through Turn boundaries,
improving context retention for long conversations.

Related: microsoft/vscode-copilot-release#11103

…rization

Implements a more granular summarization strategy that only compresses
the first half of unsummarized rounds, preserving more recent context.

- Add FlattenedRound interface for tracking round origins
- Refactor getProps() with feature flag github.copilot.chat.halfContextSummarization
- Preserve original logic in getPropsLegacy() as fallback
- New getPropsHalfContext() flattens rounds and splits at midpoint
- Add 20 unit tests covering split logic, edge cases, and fallback

This enables fine-grained compression that can cut through Turn boundaries,
improving context retention for long conversations.

Related: microsoft/vscode-copilot-release#11103
Signed-off-by: robird <RobirdLiu@Gmail.com>
Copilot AI review requested due to automatic review settings December 5, 2025 09:40
@Robird
Copy link
Author

Robird commented Dec 5, 2025

@microsoft-github-policy-service agree

Copilot finished reviewing on behalf of Robird December 5, 2025 09:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a more granular conversation summarization strategy for agent prompts that compresses only the first half of unsummarized rounds instead of all rounds except the last one. This experimental feature, controlled by the github.copilot.chat.halfContextSummarization configuration flag, aims to preserve more recent context in long conversations while still managing token budgets effectively.

Key Changes

  • Half-context summarization algorithm: Flattens all rounds across turn boundaries, identifies unsummarized rounds, and splits them at the midpoint - summarizing the older half while keeping the recent half in full detail
  • Feature flag with fallback: New getPropsHalfContext() method with automatic fallback to getPropsLegacy() when half-context mode is disabled or edge cases are encountered
  • Comprehensive test coverage: 20 unit tests covering split point calculations, cross-turn boundary scenarios, already-summarized rounds handling, overflow cases, and edge cases

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/platform/configuration/common/configurationService.ts Adds HalfContextSummarization experimental configuration key
src/extension/prompts/node/agent/summarizedConversationHistory.tsx Implements half-context summarization with FlattenedRound interface, refactored getProps() to route between legacy and half-context logic, and adds getPropsHalfContext() with midpoint splitting algorithm
src/extension/prompts/node/agent/test/halfContextSummarization.spec.ts Adds comprehensive test suite with 20 tests covering split calculations, boundary cases, already-summarized rounds, overflow handling, edge cases, and configuration toggling
package.nls.json Adds localized description for half-context summarization setting
package.json Registers github.copilot.chat.halfContextSummarization as experimental boolean configuration option

Comment on lines 902 to 910
return {
props: {
...props,
workingNotebook: this.getWorkingNotebook(props),
promptContext
},
summarizedToolCallRoundId
};
}
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The getPropsHalfContext method doesn't include summarizedThinking in its return value, but the legacy method does (line 769). This creates an inconsistency where Anthropic models with thinking enabled will lose the last thinking block when half-context summarization is enabled. The method should calculate and include summarizedThinking similar to how getPropsLegacy does on lines 755-756.

Suggested change
return {
props: {
...props,
workingNotebook: this.getWorkingNotebook(props),
promptContext
},
summarizedToolCallRoundId
};
}
// Calculate summarizedThinking similar to legacy method
let summarizedThinking: ThinkingData | undefined;
if (props.promptContext.thinking && Array.isArray(props.promptContext.thinking) && props.promptContext.thinking.length > 0) {
summarizedThinking = props.promptContext.thinking[props.promptContext.thinking.length - 1];
}
return {
props: {
...props,
workingNotebook: this.getWorkingNotebook(props),
promptContext,
summarizedThinking
},
summarizedToolCallRoundId
};

Copilot uses AI. Check for mistakes.
Copy link
Author

@Robird Robird Dec 6, 2025

Choose a reason for hiding this comment

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

You're absolutely right. I missed adding summarizedThinking support when implementing the half-context path.

Ccorrection from internal review: Simply reusing findLastThinking(props) is incorrect because:

  1. findLastThinking only scans props.promptContext.toolCallRounds (current turn)
  2. It ignores thinking in historical rounds when split is in history
  3. It might return thinking from the kept span instead of the summarized span

Fixed approach: Find thinking directly from the toSummarize array (the rounds being summarized):

// Find the last thinking block from the rounds being summarized (toSummarize),
// not from the full promptContext. This ensures we capture thinking from
// the summarized span, including historical rounds.
let summarizedThinking: ThinkingData | undefined;
if (isAnthropicFamily(props.endpoint)) {
    for (let i = toSummarize.length - 1; i >= 0; i--) {
        if (toSummarize[i].round.thinking) {
            summarizedThinking = toSummarize[i].round.thinking;
            break;
        }
    }
}

This correctly handles:

  • Thinking in current turn's toolCallRounds
  • Thinking in historical rounds (when split is in history)
  • Only returns thinking from the summarized span, not the kept span

Comment on lines +667 to +710
describe('Comparison: Legacy vs Half-Context', () => {
test('half-context preserves more recent context', () => {
// Scenario: 6 rounds
// Legacy: excludes last round, summarizes up to round 5
// Half-context: keeps 3 recent rounds, summarizes 3 older rounds
const rounds = [
createRound('round 1', 1),
createRound('round 2', 2),
createRound('round 3', 3),
createRound('round 4', 4),
createRound('round 5', 5),
createRound('round 6', 6),
];

const promptContext: IBuildPromptContext = {
chatVariables: new ChatVariablesCollection([]),
history: [],
query: 'test',
toolCallRounds: rounds,
toolCallResults: createToolResult(1, 2, 3, 4, 5, 6),
tools,
};

// Test legacy
configService.setConfig(ConfigKey.Advanced.HalfContextSummarization, false);
const legacyResult = getPropsBuilder().getProps(createBaseProps(promptContext));

// Legacy: summarize up to round 5 (all but last)
expect(legacyResult.summarizedToolCallRoundId).toBe('round_5');
expect(legacyResult.props.promptContext.toolCallRounds).toHaveLength(5);

// Test half-context
configService.setConfig(ConfigKey.Advanced.HalfContextSummarization, true);
const halfContextResult = getPropsBuilder().getProps(createBaseProps(promptContext));

// Half-context: summarize only first 3
expect(halfContextResult.summarizedToolCallRoundId).toBe('round_3');
expect(halfContextResult.props.promptContext.toolCallRounds).toHaveLength(3);

// Half-context preserves more rounds in detailed form (3 vs 1)
// Legacy keeps only 1 round after the summary point
// Half-context keeps 3 rounds after the summary point
});
});
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Test coverage is missing for the summarizedThinking functionality. The legacy method includes thinking data for Anthropic models (see summarizedConversationHistory.tsx lines 755-756), but there are no tests verifying this behavior. This is particularly important because the new getPropsHalfContext method doesn't include summarizedThinking in its return value (see separate bug comment), and without tests, this regression wouldn't be caught. Consider adding tests with Anthropic endpoints that verify summarizedThinking is properly populated in the returned ISummarizedConversationHistoryInfo.

Copilot uses AI. Check for mistakes.
Copy link
Author

@Robird Robird Dec 6, 2025

Choose a reason for hiding this comment

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

Added 5 test cases that cover:

  1. Verify summarizedThinking is returned for Anthropic model endpoints
  2. Verify summarizedThinking is undefined for non-Anthropic endpoints
  3. Verify thinking is found from summarized span only (not kept span)
  4. Verify semantic difference between legacy (full scan) and half-context (summarized span only)
  5. NEW: Verify thinking from historical rounds when split point is in history

const lastRound = toSummarize.at(-1)!;
if (lastRound.round.summary === undefined) {
const turnIndex = lastRound.turnIndex === -1 ? props.promptContext.history.length : lastRound.turnIndex;
const turn = turnIndex >= 0 ? props.promptContext.history[turnIndex] : undefined;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

The condition turnIndex >= 0 on line 843 is always true since turnIndex is either a valid array index (>= 0) or props.promptContext.history.length (also >= 0). This check doesn't protect against anything and could be simplified. Consider removing the ternary and just using props.promptContext.history[turnIndex], which will return undefined for out-of-bounds access as intended.

Suggested change
const turn = turnIndex >= 0 ? props.promptContext.history[turnIndex] : undefined;
const turn = props.promptContext.history[turnIndex];

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

You're correct that turnIndex >= 0 is always true given the preceding assignment. However, I intentionally wrote it this way to make the code's intent explicit:

// When lastRound.turnIndex === -1, turnIndex becomes history.length,
// which is an out-of-bounds index. We want `turn` to be undefined in this case.
const turnIndex = lastRound.turnIndex === -1 ? props.promptContext.history.length : lastRound.turnIndex;
const turn = turnIndex >= 0 ? props.promptContext.history[turnIndex] : undefined;

While JavaScript arrays return undefined for out-of-bounds access, relying on this implicit behavior feels fragile. The explicit ternary documents the intent: "if the index is valid, get the turn; otherwise, undefined."

@benibenj benibenj assigned roblourens and unassigned benibenj Dec 5, 2025
Robird and others added 4 commits December 6, 2025 10:18
….tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
….tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Find thinking from summarized rounds (toSummarize) instead of full promptContext
- This correctly handles thinking in historical rounds when split is in history
- Reuse existing findLastThinking() method for legacy behavior only

Add 5 new test cases for Anthropic thinking support:
- Verify summarizedThinking is returned for Anthropic endpoints
- Verify summarizedThinking is undefined for non-Anthropic endpoints
- Verify thinking is found from summarized span only (not kept span)
- Verify semantic difference between legacy and half-context
- Verify thinking from historical rounds when split is in history

Addresses review feedback about missing and incorrect summarizedThinking.

Signed-off-by: robird <RobirdLiu@Gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants