-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(prompts/agent): add experimental half-context conversation summarization #2427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
fcea11a
c9135c2
8215c23
ef4aab9
97b50fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -684,17 +684,51 @@ export interface ISummarizedConversationHistoryInfo { | |||||||||||||||||||||||||||||||||||||||||||||||||
| readonly summarizedThinking?: ThinkingData; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Represents a flattened round with its origin information. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| interface FlattenedRound { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly round: IToolCallRound; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly turnIndex: number; // -1 for current turn's toolCallRounds | ||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly roundIndexInTurn: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Half-context summarization is now controlled by ConfigKey.Advanced.HalfContextSummarization | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Exported for test | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export class SummarizedConversationHistoryPropsBuilder { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @IPromptPathRepresentationService private readonly _promptPathRepresentationService: IPromptPathRepresentationService, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @IWorkspaceService private readonly _workspaceService: IWorkspaceService, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @IConfigurationService private readonly _configurationService: IConfigurationService, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @IExperimentationService private readonly _experimentationService: IExperimentationService, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| getProps( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| props: SummarizedAgentHistoryProps | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): ISummarizedConversationHistoryInfo { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const enableHalfContext = this._configurationService.getExperimentBasedConfig( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ConfigKey.Advanced.HalfContextSummarization, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this._experimentationService | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (enableHalfContext) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const halfContextProps = this.getPropsHalfContext(props); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (halfContextProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return halfContextProps; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.getPropsLegacy(props); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Original full-context summarization logic. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Summarizes from the last round of the previous turn or excludes only the last round. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private getPropsLegacy( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| props: SummarizedAgentHistoryProps | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): ISummarizedConversationHistoryInfo { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let toolCallRounds = props.promptContext.toolCallRounds; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let isContinuation = props.promptContext.isContinuation; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -736,7 +770,7 @@ export class SummarizedConversationHistoryPropsBuilder { | |||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| private findLastThinking(props: SummarizedAgentHistoryProps): ThinkingData | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private findLastThinking(props: SummarizedAgentHistoryProps): ThinkingData | undefined { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (props.promptContext.toolCallRounds) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = props.promptContext.toolCallRounds.length - 1; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const round = props.promptContext.toolCallRounds[i]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -748,6 +782,133 @@ export class SummarizedConversationHistoryPropsBuilder { | |||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Half-context summarization logic. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Flattens all rounds across history and current turn, then summarizes only the first half | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * of unsummarized rounds. This enables fine-grained compression that can cut through Turn boundaries. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| private getPropsHalfContext( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| props: SummarizedAgentHistoryProps | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): ISummarizedConversationHistoryInfo | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 1: Flatten all rounds with origin tracking | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const flattenedRounds: FlattenedRound[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let turnIndex = 0; turnIndex < props.promptContext.history.length; turnIndex++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const turn = props.promptContext.history[turnIndex]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const rounds = turn.rounds; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flattenedRounds.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| round: rounds[roundIndex], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| turnIndex, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| roundIndexInTurn: roundIndex | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add current turn's toolCallRounds (turnIndex = -1) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentRounds = props.promptContext.toolCallRounds ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let roundIndex = 0; roundIndex < currentRounds.length; roundIndex++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| flattenedRounds.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| round: currentRounds[roundIndex], | ||||||||||||||||||||||||||||||||||||||||||||||||||
| turnIndex: -1, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| roundIndexInTurn: roundIndex | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip rounds that already have summaries – we only want to summarize new material. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let lastSummarizedIndex = -1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = flattenedRounds.length - 1; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (flattenedRounds[i].round.summary) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| lastSummarizedIndex = i; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidateRounds = flattenedRounds.slice(lastSummarizedIndex + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (candidateRounds.length <= 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 2: Calculate split point - keep half, summarize half | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const keepCount = Math.ceil(candidateRounds.length / 2); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const summarizeCount = candidateRounds.length - keepCount; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (summarizeCount <= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const toSummarize = candidateRounds.slice(0, summarizeCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the split lands on a turn that exceeded tool-call limit, the last round of that turn | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // is typically the one that got interrupted and whose result lands in the next turn. Avoid | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // compressing that interrupted round so the summary doesn't miss the corresponding result. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const turn = turnIndex >= 0 ? props.promptContext.history[turnIndex] : undefined; | |
| const turn = props.promptContext.history[turnIndex]; |
There was a problem hiding this comment.
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."
Robird marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
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.
| 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 | |
| }; |
There was a problem hiding this comment.
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:
findLastThinkingonly scansprops.promptContext.toolCallRounds(current turn)- It ignores thinking in historical rounds when split is in history
- 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
Uh oh!
There was an error while loading. Please reload this page.