mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-18 07:47:23 +01:00
cache llm generated thinking headers (#304619)
* cache llm generated titles as fallback for rerendering * store llm generated headers so cli will not re-generate * address some comments, scope to non-local sessions
This commit is contained in:
@@ -116,7 +116,7 @@ export class ChatSubagentContentPart extends ChatCollapsibleContentPart implemen
|
|||||||
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
|
* Extracts subagent info (description, agentName, prompt) from a tool invocation.
|
||||||
*/
|
*/
|
||||||
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
|
private static extractSubagentInfo(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { description: string; agentName: string | undefined; prompt: string | undefined; modelName: string | undefined } {
|
||||||
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent...');
|
const defaultDescription = localize('chat.subagent.defaultDescription', 'Running subagent');
|
||||||
|
|
||||||
// Only parent subagent tools contain the full subagent info
|
// Only parent subagent tools contain the full subagent info
|
||||||
if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) {
|
if (!ChatSubagentContentPart.isParentSubagentTool(toolInvocation)) {
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ import { IChatMarkdownAnchorService } from './chatMarkdownAnchorService.js';
|
|||||||
import { ChatMessageRole, ILanguageModelsService } from '../../../common/languageModels.js';
|
import { ChatMessageRole, ILanguageModelsService } from '../../../common/languageModels.js';
|
||||||
import './media/chatThinkingContent.css';
|
import './media/chatThinkingContent.css';
|
||||||
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
|
import { IHoverService } from '../../../../../../platform/hover/browser/hover.js';
|
||||||
|
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
|
||||||
import { extractImagesFromToolInvocationOutputDetails } from '../../../common/chatImageExtraction.js';
|
import { extractImagesFromToolInvocationOutputDetails } from '../../../common/chatImageExtraction.js';
|
||||||
import { IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js';
|
import { IChatCollapsibleIODataPart } from './chatToolInputOutputContentPart.js';
|
||||||
import { ChatThinkingExternalResourceWidget } from './chatThinkingExternalResourcesWidget.js';
|
import { ChatThinkingExternalResourceWidget } from './chatThinkingExternalResourcesWidget.js';
|
||||||
|
import { LocalChatSessionUri, chatSessionResourceToId } from '../../../common/model/chatUri.js';
|
||||||
import { IEditSessionDiffStats } from '../../../common/editing/chatEditingService.js';
|
import { IEditSessionDiffStats } from '../../../common/editing/chatEditingService.js';
|
||||||
|
|
||||||
|
|
||||||
@@ -140,6 +142,10 @@ interface ILazyThinkingItem {
|
|||||||
type ILazyItem = ILazyToolItem | ILazyThinkingItem;
|
type ILazyItem = ILazyToolItem | ILazyThinkingItem;
|
||||||
const THINKING_SCROLL_MAX_HEIGHT = 200;
|
const THINKING_SCROLL_MAX_HEIGHT = 200;
|
||||||
|
|
||||||
|
const TITLE_CACHE_STORAGE_KEY = 'chat.thinkingTitleCache';
|
||||||
|
const TITLE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||||
|
const TITLE_CACHE_MAX_ENTRIES = 1000;
|
||||||
|
|
||||||
const enum WorkingMessageCategory {
|
const enum WorkingMessageCategory {
|
||||||
Thinking = 'thinking',
|
Thinking = 'thinking',
|
||||||
Terminal = 'terminal',
|
Terminal = 'terminal',
|
||||||
@@ -284,6 +290,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
|||||||
@IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService,
|
@IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService,
|
||||||
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
|
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
|
||||||
@IHoverService hoverService: IHoverService,
|
@IHoverService hoverService: IHoverService,
|
||||||
|
@IStorageService private readonly storageService: IStorageService,
|
||||||
) {
|
) {
|
||||||
const initialText = extractTextFromPart(content);
|
const initialText = extractTextFromPart(content);
|
||||||
const extractedTitle = extractTitleFromThinkingContent(initialText)
|
const extractedTitle = extractTitleFromThinkingContent(initialText)
|
||||||
@@ -897,6 +904,7 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reuse any existing generated title from tool invocations or thinking parts.
|
||||||
const existingTitle = this.toolInvocations.find(t => t.generatedTitle)?.generatedTitle
|
const existingTitle = this.toolInvocations.find(t => t.generatedTitle)?.generatedTitle
|
||||||
?? this.allThinkingParts.find(t => t.generatedTitle)?.generatedTitle;
|
?? this.allThinkingParts.find(t => t.generatedTitle)?.generatedTitle;
|
||||||
if (existingTitle) {
|
if (existingTitle) {
|
||||||
@@ -907,6 +915,27 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only check the persisted cache when re-rendering
|
||||||
|
// (all tool invocations are serialized), not during live streaming.
|
||||||
|
const allSerialized = this.toolInvocations.length > 0
|
||||||
|
&& this.toolInvocations.every(t => t.kind === 'toolInvocationSerialized');
|
||||||
|
if (allSerialized) {
|
||||||
|
// Fallback: check the persisted title cache using the last tool call (non-local sessions only)
|
||||||
|
if (!LocalChatSessionUri.isLocalSession(this.element.sessionResource)) {
|
||||||
|
const lastToolInvocation = this.toolInvocations[this.toolInvocations.length - 1];
|
||||||
|
if (lastToolInvocation) {
|
||||||
|
const cachedTitle = this.getCachedTitle(lastToolInvocation.toolCallId);
|
||||||
|
if (cachedTitle) {
|
||||||
|
this.currentTitle = cachedTitle;
|
||||||
|
this.content.generatedTitle = cachedTitle;
|
||||||
|
this.setGeneratedTitleOnAllParts(cachedTitle);
|
||||||
|
this.setFinalizedTitle(cachedTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// case where we only have one item (tool or edit) in the thinking container and no thinking parts, we want to move it back to its original position
|
// case where we only have one item (tool or edit) in the thinking container and no thinking parts, we want to move it back to its original position
|
||||||
if (this.toolInvocationCount === 1 && this.hookCount === 0 && this.currentThinkingValue.trim() === '') {
|
if (this.toolInvocationCount === 1 && this.hookCount === 0 && this.currentThinkingValue.trim() === '') {
|
||||||
// If singleItemInfo wasn't set (item was lazy/deferred), materialize it now
|
// If singleItemInfo wasn't set (item was lazy/deferred), materialize it now
|
||||||
@@ -966,6 +995,55 @@ export class ChatThinkingContentPart extends ChatCollapsibleContentPart implemen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private loadTitleCache(): Record<string, { title: string; storedAt: number }> {
|
||||||
|
return this.storageService.getObject<Record<string, { title: string; storedAt: number }>>(TITLE_CACHE_STORAGE_KEY, StorageScope.PROFILE) ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveTitleCache(cache: Record<string, { title: string; storedAt: number }>): void {
|
||||||
|
if (Object.keys(cache).length === 0) {
|
||||||
|
this.storageService.remove(TITLE_CACHE_STORAGE_KEY, StorageScope.PROFILE);
|
||||||
|
} else {
|
||||||
|
this.storageService.store(TITLE_CACHE_STORAGE_KEY, JSON.stringify(cache), StorageScope.PROFILE, StorageTarget.MACHINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTitleCacheKey(toolCallId: string): string {
|
||||||
|
return `${chatSessionResourceToId(this.element.sessionResource)}:${toolCallId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCachedTitle(toolCallId: string): string | undefined {
|
||||||
|
const entry = this.loadTitleCache()[this.getTitleCacheKey(toolCallId)];
|
||||||
|
if (!entry || (Date.now() - entry.storedAt) > TITLE_CACHE_TTL_MS) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return entry.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCachedTitle(toolCallId: string, title: string): void {
|
||||||
|
const cache = this.loadTitleCache();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Evict expired entries on write
|
||||||
|
for (const key of Object.keys(cache)) {
|
||||||
|
if ((now - cache[key].storedAt) > TITLE_CACHE_TTL_MS) {
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[this.getTitleCacheKey(toolCallId)] = { title, storedAt: now };
|
||||||
|
|
||||||
|
// Cap size by dropping oldest entries
|
||||||
|
const keys = Object.keys(cache);
|
||||||
|
if (keys.length > TITLE_CACHE_MAX_ENTRIES) {
|
||||||
|
const sorted = keys.sort((a, b) => cache[a].storedAt - cache[b].storedAt);
|
||||||
|
for (let i = 0; i < sorted.length - TITLE_CACHE_MAX_ENTRIES; i++) {
|
||||||
|
delete cache[sorted[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveTitleCache(cache);
|
||||||
|
}
|
||||||
|
|
||||||
private async generateTitleViaLLM(): Promise<void> {
|
private async generateTitleViaLLM(): Promise<void> {
|
||||||
const cts = new CancellationTokenSource();
|
const cts = new CancellationTokenSource();
|
||||||
const timeout = setTimeout(() => cts.cancel(), 5000);
|
const timeout = setTimeout(() => cts.cancel(), 5000);
|
||||||
@@ -1123,6 +1201,15 @@ ${this.hookCount > 0 ? `EXAMPLES WITH BLOCKED CONTENT (from hooks):
|
|||||||
this.setFinalizedTitle(generatedTitle);
|
this.setFinalizedTitle(generatedTitle);
|
||||||
this.content.generatedTitle = generatedTitle;
|
this.content.generatedTitle = generatedTitle;
|
||||||
this.setGeneratedTitleOnAllParts(generatedTitle);
|
this.setGeneratedTitleOnAllParts(generatedTitle);
|
||||||
|
|
||||||
|
// Persist to storage for non-local sessions only
|
||||||
|
if (!LocalChatSessionUri.isLocalSession(this.element.sessionResource)) {
|
||||||
|
const lastTool = this.toolInvocations[this.toolInvocations.length - 1];
|
||||||
|
if (lastTool) {
|
||||||
|
this.setCachedTitle(lastTool.toolCallId, generatedTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ suite('ChatSubagentContentPart', () => {
|
|||||||
prompt: 'Test prompt'
|
prompt: 'Test prompt'
|
||||||
},
|
},
|
||||||
originMessage: undefined,
|
originMessage: undefined,
|
||||||
invocationMessage: options.invocationMessage ?? 'Running subagent...',
|
invocationMessage: options.invocationMessage ?? 'Running subagent',
|
||||||
pastTenseMessage: undefined,
|
pastTenseMessage: undefined,
|
||||||
source: ToolDataSource.Internal,
|
source: ToolDataSource.Internal,
|
||||||
toolId: options.toolId ?? RunSubagentTool.Id,
|
toolId: options.toolId ?? RunSubagentTool.Id,
|
||||||
@@ -180,7 +180,7 @@ suite('ChatSubagentContentPart', () => {
|
|||||||
result: 'Test result text'
|
result: 'Test result text'
|
||||||
},
|
},
|
||||||
originMessage: undefined,
|
originMessage: undefined,
|
||||||
invocationMessage: 'Running subagent...',
|
invocationMessage: 'Running subagent',
|
||||||
pastTenseMessage: undefined,
|
pastTenseMessage: undefined,
|
||||||
resultDetails: undefined,
|
resultDetails: undefined,
|
||||||
isConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded },
|
isConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded },
|
||||||
|
|||||||
Reference in New Issue
Block a user