From 091a6de0bff42f11a64fec78ea8d5069763f1f25 Mon Sep 17 00:00:00 2001 From: bhavyaus Date: Sat, 18 Apr 2026 12:33:48 -0700 Subject: [PATCH 1/2] Bake transcript pointer into conversation summaries at creation time Append a stable transcript-file hint (path + line-count snapshot) to every conversation summary, so after compaction the model can read the uncompacted transcript on disk. The hint is appended exactly once at summary-creation time and stored on round.summary / turn metadata. Subsequent renders replay that string byte-identically, preserving Anthropic prompt cache hits even as the transcript keeps growing. Covers all three summarization paths: - Full / Simple via ConversationHistorySummarizer.summarizeHistory() - Inline background via agentIntent.ts _startBackgroundSummarization (flushes the transcript before snapshotting the line count so the baked count matches the on-disk file) Shared via new exported helper appendTranscriptHintToSummary. --- .../src/extension/intents/node/agentIntent.ts | 17 ++++- .../agent/summarizedConversationHistory.tsx | 63 ++++++++++++------- .../node/agent/test/summarization.spec.tsx | 44 ++++++++++++- 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/agentIntent.ts b/extensions/copilot/src/extension/intents/node/agentIntent.ts index 720617e5ca841..ba822c56f8ea7 100644 --- a/extensions/copilot/src/extension/intents/node/agentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/agentIntent.ts @@ -9,6 +9,7 @@ import { BudgetExceededError } from '@vscode/prompt-tsx/dist/base/materialized'; import type * as vscode from 'vscode'; import { IChatSessionService } from '../../../platform/chat/common/chatSessionService'; import { ChatFetchResponseType, ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes'; +import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { isAnthropicFamily, isGptFamily, modelCanUseApplyPatchExclusively, modelCanUseReplaceStringExclusively, modelSupportsApplyPatch, modelSupportsMultiReplaceString, modelSupportsReplaceString, modelSupportsSimplifiedApplyPatchInstructions } from '../../../platform/endpoint/common/chatModelCapabilities'; import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider'; @@ -47,7 +48,7 @@ import { IBuildPromptResult, IIntent, IIntentInvocation } from '../../prompt/nod import { AgentPrompt, AgentPromptProps } from '../../prompts/node/agent/agentPrompt'; import { BackgroundSummarizationState, BackgroundSummarizer, IBackgroundSummarizationResult, shouldKickOffBackgroundSummarization } from '../../prompts/node/agent/backgroundSummarizer'; import { AgentPromptCustomizations, PromptRegistry } from '../../prompts/node/agent/promptRegistry'; -import { extractInlineSummary, InlineSummarizationUserMessage, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder } from '../../prompts/node/agent/summarizedConversationHistory'; +import { extractInlineSummary, InlineSummarizationUserMessage, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder, appendTranscriptHintToSummary } from '../../prompts/node/agent/summarizedConversationHistory'; import { PromptRenderer, renderPromptElement } from '../../prompts/node/base/promptRenderer'; import { ICodeMapperService } from '../../prompts/node/codeMapper/codeMapperService'; import { EditCodePrompt2 } from '../../prompts/node/panel/editCodePrompt2'; @@ -388,6 +389,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I @IExperimentationService private readonly expService: IExperimentationService, @IAutomodeService private readonly automodeService: IAutomodeService, @IOTelService override readonly otelService: IOTelService, + @ISessionTranscriptService private readonly sessionTranscriptService: ISessionTranscriptService, ) { super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, otelService); } @@ -860,13 +862,22 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation implements I if (response.type !== ChatFetchResponseType.Success) { throw new Error(`Background inline summarization request failed: ${response.type}`); } - const summaryText = extractInlineSummary(response.value); - if (!summaryText) { + const rawSummaryText = extractInlineSummary(response.value); + if (!rawSummaryText) { throw new Error('Background inline summarization: no tags found in response'); } if (!toolCallRoundId) { throw new Error('Background inline summarization: no round ID to apply summary to'); } + // Flush the transcript before snapshotting the line count so + // the baked "N lines" hint matches the on-disk file at this + // moment (mirrors the full/simple path in SummarizedConversationHistory.render). + if (conversationId && this.sessionTranscriptService.getTranscriptPath(conversationId)) { + await this.sessionTranscriptService.flush(conversationId); + } + const summaryText = conversationId + ? appendTranscriptHintToSummary(rawSummaryText, conversationId, this.sessionTranscriptService) + : rawSummaryText; this.logService.debug(`[ConversationHistorySummarizer] background inline compaction completed (${summaryText.length} chars, roundId=${toolCallRoundId})`); // Send summarizedConversationHistory telemetry for parity diff --git a/extensions/copilot/src/extension/prompts/node/agent/summarizedConversationHistory.tsx b/extensions/copilot/src/extension/prompts/node/agent/summarizedConversationHistory.tsx index f99392b102dd6..c6944dcc46788 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/summarizedConversationHistory.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/summarizedConversationHistory.tsx @@ -424,8 +424,6 @@ export class SummarizedConversationHistory extends PromptElement | undefined, token: CancellationToken | undefined) { const promptContext = { ...this.props.promptContext }; let historyMetadata: SummarizedConversationHistoryMetadata | undefined; - // Resolve transcript path and flush to disk so the model can read the up-to-date file - let transcriptPath: string | undefined; const sessionId = this.props.promptContext.conversation?.sessionId; if (sessionId) { // Lazily start the transcript session now (before summarization) so it @@ -433,10 +431,8 @@ export class SummarizedConversationHistory extends PromptElement { @@ -589,7 +573,7 @@ class ConversationHistorySummarizer { const summary = await summaryPromise; const { numRounds, numRoundsSinceLastSummarization } = this.computeRoundCounts(); return { - summary: summary.result.value, + summary: this.appendTranscriptHint(summary.result.value), toolCallRoundId: propsInfo.summarizedToolCallRoundId, thinking: propsInfo.summarizedThinking, usage: summary.result.usage, @@ -602,6 +586,14 @@ class ConversationHistorySummarizer { }; } + private appendTranscriptHint(summary: string): string { + const sessionId = this.props.promptContext.conversation?.sessionId; + if (!sessionId) { + return summary; + } + return appendTranscriptHintToSummary(summary, sessionId, this.sessionTranscriptService); + } + private async getSummaryWithFallback(propsInfo: ISummarizedConversationHistoryInfo): Promise { const forceMode = this.configurationService.getConfig(ConfigKey.Advanced.AgentHistorySummarizationMode); if (this.props.forceSimpleSummary && forceMode !== SummaryMode.Full) { @@ -926,6 +918,35 @@ function replaceImageContentWithPlaceholders(messages: ChatMessage[]): void { }); } +/** + * Bake a stable transcript pointer into a freshly-produced summary text. + * + * Shared by both the full/simple summarization path + * ({@link ConversationHistorySummarizer}) and the inline background + * summarization path in `agentIntent.ts`. The hint is appended exactly once, + * at summary creation time, so the resulting string is frozen from then on + * and replayed verbatim — preserving Anthropic prompt cache hits across + * subsequent renders. + * + * Returns the input unchanged when there is no transcript on disk for the + * session. + */ +export function appendTranscriptHintToSummary(summary: string, sessionId: string, sessionTranscriptService: ISessionTranscriptService): string { + const transcriptUri = sessionTranscriptService.getTranscriptPath(sessionId); + if (!transcriptUri) { + return summary; + } + const transcriptPath = transcriptUri.fsPath; + const lineCount = sessionTranscriptService.getLineCount(sessionId); + let out = summary; + out += `\nIf you need specific details from before compaction (such as exact code snippets, error messages, tool results, or content you previously generated), use the ${ToolName.ReadFile} tool to look up the full uncompacted conversation transcript at: "${transcriptPath}"`; + if (lineCount !== undefined) { + out += `\nAt the time this summary was created, the transcript had ${lineCount} lines.`; + } + out += `\nExample usage: ${ToolName.ReadFile}(filePath: "${transcriptPath}")`; + return out; +} + /** * Strip custom client-side tool search (tool_search) tool_use and tool_result * messages from the conversation. The summarization call uses ChatLocation.Other diff --git a/extensions/copilot/src/extension/prompts/node/agent/test/summarization.spec.tsx b/extensions/copilot/src/extension/prompts/node/agent/test/summarization.spec.tsx index 38d4ad3a70ce9..8987972c923e8 100644 --- a/extensions/copilot/src/extension/prompts/node/agent/test/summarization.spec.tsx +++ b/extensions/copilot/src/extension/prompts/node/agent/test/summarization.spec.tsx @@ -30,7 +30,8 @@ import { ToolName } from '../../../../tools/common/toolNames'; import { PromptRenderer } from '../../base/promptRenderer'; import { AgentPrompt, AgentPromptProps } from '../agentPrompt'; import { PromptRegistry } from '../promptRegistry'; -import { ConversationHistorySummarizationPrompt, extractInlineSummary, stripToolSearchMessages, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder } from '../summarizedConversationHistory'; +import { ISessionTranscriptService, NullSessionTranscriptService } from '../../../../../platform/chat/common/sessionTranscriptService'; +import { appendTranscriptHintToSummary, ConversationHistorySummarizationPrompt, extractInlineSummary, stripToolSearchMessages, SummarizedConversationHistory, SummarizedConversationHistoryMetadata, SummarizedConversationHistoryPropsBuilder } from '../summarizedConversationHistory'; suite('Agent Summarization', () => { let accessor: ITestingServicesAccessor; @@ -670,3 +671,44 @@ suite('stripToolSearchMessages', () => { expect(result[2].content[0]).toEqual({ type: Raw.ChatCompletionContentPartKind.Text, text: 'second' }); }); }); + +suite('appendTranscriptHintToSummary', () => { + class FakeTranscriptService extends NullSessionTranscriptService { + constructor( + private readonly path: URI | undefined, + private readonly lineCount: number | undefined, + ) { + super(); + } + override getTranscriptPath(): URI | undefined { return this.path; } + override getLineCount(): number | undefined { return this.lineCount; } + } + + function makeService(path: URI | undefined, lineCount: number | undefined): ISessionTranscriptService { + return new FakeTranscriptService(path, lineCount); + } + + test('returns summary unchanged when no transcript path is available', () => { + const svc = makeService(undefined, undefined); + const result = appendTranscriptHintToSummary('original summary', 'session-1', svc); + expect(result).toBe('original summary'); + }); + + test('appends path-only hint when line count is missing', () => { + const transcript = URI.file('/tmp/transcript.jsonl'); + const svc = makeService(transcript, undefined); + const result = appendTranscriptHintToSummary('S', 'session-1', svc); + expect(result.startsWith('S\n')).toBe(true); + expect(result).toContain(transcript.fsPath); + expect(result).toContain(`${ToolName.ReadFile}`); + expect(result).not.toContain('the transcript had'); + }); + + test('bakes line count snapshot into hint when available', () => { + const transcript = URI.file('/tmp/transcript.jsonl'); + const svc = makeService(transcript, 42); + const result = appendTranscriptHintToSummary('S', 'session-1', svc); + expect(result).toContain('At the time this summary was created, the transcript had 42 lines.'); + expect(result).toContain(transcript.fsPath); + }); +}); From dd2762bf10c30d8c7dd1a86cacf3998cedf1e639 Mon Sep 17 00:00:00 2001 From: bhavyaus Date: Sat, 18 Apr 2026 15:35:08 -0700 Subject: [PATCH 2/2] Wire ISessionTranscriptService through AgentIntentInvocation subclasses --- .../copilot/src/extension/intents/node/askAgentIntent.ts | 4 +++- .../copilot/src/extension/intents/node/editCodeIntent2.ts | 4 +++- .../src/extension/intents/node/notebookEditorIntent.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts index 3be37f4426725..8a8cd2ecdbd72 100644 --- a/extensions/copilot/src/extension/intents/node/askAgentIntent.ts +++ b/extensions/copilot/src/extension/intents/node/askAgentIntent.ts @@ -14,6 +14,7 @@ import { IEditLogService } from '../../../platform/multiFileEdit/common/editLogS import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { IOTelService } from '../../../platform/otel/common/otelService'; +import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; @@ -128,8 +129,9 @@ export class AskAgentIntentInvocation extends AgentIntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, + @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, ) { - super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); + super(intent, location, endpoint, request, { processCodeblocks: true }, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); } public override async getAvailableTools(): Promise { diff --git a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts index afc6281042088..1c9063e066b74 100644 --- a/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts +++ b/extensions/copilot/src/extension/intents/node/editCodeIntent2.ts @@ -16,6 +16,7 @@ import { IChatEndpoint } from '../../../platform/networking/common/networking'; import { requestHasNotebookRefs } from '../../../platform/notebook/common/helpers'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { IOTelService } from '../../../platform/otel/common/otelService'; +import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; @@ -89,8 +90,9 @@ export class EditCode2IntentInvocation extends AgentIntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, + @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); } public override async getAvailableTools(): Promise { diff --git a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts index 1b0cfdd866531..a0e71c9757101 100644 --- a/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts +++ b/extensions/copilot/src/extension/intents/node/notebookEditorIntent.ts @@ -16,6 +16,7 @@ import { IAlternativeNotebookContentService } from '../../../platform/notebook/c import { getCellId } from '../../../platform/notebook/common/helpers'; import { INotebookService } from '../../../platform/notebook/common/notebookService'; import { IOTelService } from '../../../platform/otel/common/otelService'; +import { ISessionTranscriptService } from '../../../platform/chat/common/sessionTranscriptService'; import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService'; import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService'; import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService'; @@ -107,8 +108,9 @@ export class NotebookEditorIntentInvocation extends EditCode2IntentInvocation { @IExperimentationService expService: IExperimentationService, @IAutomodeService automodeService: IAutomodeService, @IOTelService otelService: IOTelService, + @ISessionTranscriptService sessionTranscriptService: ISessionTranscriptService, ) { - super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService); + super(intent, location, endpoint, request, intentOptions, instantiationService, codeMapperService, envService, promptPathRepresentationService, endpointProvider, workspaceService, toolsService, configurationService, editLogService, commandService, telemetryService, notebookService, logService, expService, automodeService, otelService, sessionTranscriptService); } protected override prompt = NotebookInlinePrompt;