Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions extensions/copilot/src/extension/intents/node/agentIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 <summary> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<vscode.LanguageModelToolInformation[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<vscode.LanguageModelToolInformation[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,19 +424,15 @@ export class SummarizedConversationHistory extends PromptElement<SummarizedAgent
override async render(state: void, sizing: PromptSizing, progress: Progress<ChatResponsePart> | 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
// captures the full pre-compaction conversation. startSession is
// idempotent — if hooks already started it, this is a no-op.
await this.ensureTranscriptSession();

const transcriptUri = this.sessionTranscriptService.getTranscriptPath(sessionId);
if (transcriptUri) {
if (this.sessionTranscriptService.getTranscriptPath(sessionId)) {
await this.sessionTranscriptService.flush(sessionId);
transcriptPath = transcriptUri.fsPath;
}
}

Expand All @@ -445,20 +441,7 @@ export class SummarizedConversationHistory extends PromptElement<SummarizedAgent
const summarizer = this.instantiationService.createInstance(ConversationHistorySummarizer, this.props, sizing, progress, token);
const summResult = await summarizer.summarizeHistory();
if (summResult) {
// Bake the transcript hint into the summary text so it is
// frozen at compaction time and never changes on subsequent renders
// (preserving Anthropic prompt cache stability).
let summary = summResult.summary;
if (transcriptPath) {
const lineCount = this.sessionTranscriptService.getLineCount(sessionId!);
summary += `\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) {
summary += `\nAt the time of this request, the transcript has ${lineCount} lines.`;
}
summary += `\nExample usage: ${ToolName.ReadFile}(filePath: "${transcriptPath}")`;
}

historyMetadata = new SummarizedConversationHistoryMetadata(summResult.toolCallRoundId, summary, {
historyMetadata = new SummarizedConversationHistoryMetadata(summResult.toolCallRoundId, summResult.summary, {
thinking: summResult.thinking,
usage: summResult.usage,
promptTokenDetails: summResult.promptTokenDetails,
Expand All @@ -468,7 +451,7 @@ export class SummarizedConversationHistory extends PromptElement<SummarizedAgent
numRoundsSinceLastSummarization: summResult.numRoundsSinceLastSummarization,
durationMs: summResult.durationMs,
});
this.addSummaryToHistory(summary, summResult.toolCallRoundId, summResult.thinking);
this.addSummaryToHistory(summResult.summary, summResult.toolCallRoundId, summResult.thinking);
}
}

Expand Down Expand Up @@ -569,6 +552,7 @@ class ConversationHistorySummarizer {
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IChatHookService private readonly chatHookService: IChatHookService,
@ISessionTranscriptService private readonly sessionTranscriptService: ISessionTranscriptService,
) { }

async summarizeHistory(): Promise<{ summary: string; toolCallRoundId: string; thinking?: ThinkingData; usage?: APIUsage; promptTokenDetails?: readonly ChatResultPromptTokenDetail[]; model?: string; summarizationMode?: string; numRounds?: number; numRoundsSinceLastSummarization?: number; durationMs?: number }> {
Expand All @@ -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,
Expand All @@ -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<SummarizationResult> {
const forceMode = this.configurationService.getConfig<string | undefined>(ConfigKey.Advanced.AgentHistorySummarizationMode);
if (this.props.forceSimpleSummary && forceMode !== SummaryMode.Full) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
});
});
Comment thread
bhavyaus marked this conversation as resolved.
Loading