Skip to content

Add cache-stable mode for vscode_renameSymbol and vscode_listCodeUsages (experimental)#312568

Merged
bhavyaus merged 3 commits intomicrosoft:mainfrom
kevin-m-kent:kevin-m-kent/symbol-tools-cache-stable
Apr 26, 2026
Merged

Add cache-stable mode for vscode_renameSymbol and vscode_listCodeUsages (experimental)#312568
bhavyaus merged 3 commits intomicrosoft:mainfrom
kevin-m-kent:kevin-m-kent/symbol-tools-cache-stable

Conversation

@kevin-m-kent
Copy link
Copy Markdown
Contributor

Why

vscode_renameSymbol and vscode_listCodeUsages currently emit a tool description that embeds the live set of registered language IDs (e.g. Currently supported for: python, r, rmd.) and return undefined from getToolData() when the corresponding provider has no registered languages.

Both behaviors poison the provider-side prompt cache during an agent turn:

  1. Description churn. Every time a language extension activates (because the model touched a file in a previously-unloaded language), the sorted language list grows and the tool description bytes change. Anthropic and OpenAI prompt caches hash the full prefix, so any byte change inside the tools[] array invalidates everything from that tool onward.
  2. Presence churn. When a provider's registeredLanguageIds goes empty, the whole tool disappears from the array, shifting every subsequent tool's index by one — a much larger byte delta than a description change. The two tools are wired to independent events (renameProvider.onDidChange vs referenceProvider.onDidChange), so they can flap in and out of the array out of step.

Both effects fire mid-turn, on perfectly normal use (e.g. the model reading one file in a new language), and produce 0% cache-hit requests in the middle of otherwise high-hit turns.

What this PR does

Adds an experimental setting:

  • Key: chat.experimental.symbolTools.cacheStable
  • Default: false
  • Tags: experimental, experiment: { mode: 'startup' } (eligible for ExP A/B treatment)

When enabled, both RenameTool and UsagesTool:

  • Always register, regardless of whether any provider is currently registered.
  • Use a static modelDescription that omits the per-language list. A single sentence is appended explaining that the tool returns an error if the file's language has no provider — which is already the runtime behavior in invoke().
  • Skip the debounced subscription to *Provider.onDidChange, so onDidUpdateToolData is never re-fired and the tool registration doesn't churn.

prepareToolInvocation and invoke are untouched; the tools still error out cleanly when a file's language has no provider registered.

When the setting is false (default) the existing behavior is preserved exactly.

Why this is safe

  • Default false — opt-in only.
  • No change to invoke(), prepareToolInvocation(), or the input schemas.
  • The model already needs to handle the "no provider" error path (it can hit it any time a user opens a file before the language extension finishes activating). Removing the language-list hint just leans more on that existing path.
  • The static description still communicates capabilities — only the dynamically-changing list of supported languages is removed. The model can attempt the call; if a provider isn't there, it gets a clear error and adapts.

Expected impact

Independent of this PR, request bodies captured locally show 0% cache-hit rounds occurring at the exact moments when the language list inside these two tools changes (or the tools flap in/out of the array). With the flag enabled, those events should no longer cause cache-prefix invalidation, leading to materially higher cache-hit percentages in long agent turns that touch multiple languages.

We will validate the impact via an A/B experiment by comparing prompt cache hit rates and uncached input tokens between treatment and control populations.

Trying it out

Set in user settings:

{
  "chat.experimental.symbolTools.cacheStable": true
}

Restart the window so the constructor's startup-time branch picks up the new value. (The setting is experiment: { mode: 'startup' } — runtime toggling is not supported.)

Files changed

  • src/vs/workbench/contrib/chat/common/constants.ts — new ChatConfiguration.SymbolToolsCacheStable enum entry.
  • src/vs/workbench/contrib/chat/browser/chat.contribution.ts — registers the configuration with experimental tag and experiment: { mode: 'startup' }.
  • src/vs/workbench/contrib/chat/browser/tools/renameTool.ts — gated branch in getToolData() returning a static IToolData; IConfigurationService injected.
  • src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts — same pattern.

These two tools today emit a description that embeds the live list of
registered language IDs and return undefined when no providers are
registered. As language extensions activate during an agent turn (and
sometimes deactivate), the tools' description bytes change and the tools
themselves can disappear from / reappear in the tool array. Both effects
shift bytes inside the tools array prefix and break the provider-side
prompt cache mid-turn.

Add an experimental setting,
`chat.experimental.symbolTools.cacheStable`, that when enabled:

- Always registers both tools, regardless of whether any provider is
  currently registered.
- Uses a static modelDescription that does not include the language list.
- Skips re-firing onDidUpdateToolData on provider changes (no
  re-registration churn).

Tool behavior at invoke time is unchanged: the tools already return an
error when the file's language has no provider, and the static
description tells the model to expect that.

Default is false so no behavior change without explicit opt-in or
experiment assignment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 25, 2026 23:56
Copy link
Copy Markdown
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

Adds an experimental “cache-stable” mode to keep vscode_renameSymbol and vscode_listCodeUsages tool registration/description byte-stable across a turn (to avoid provider-side prompt-cache invalidations when language extensions activate).

Changes:

  • Add chat.experimental.symbolTools.cacheStable configuration key (experimental, startup-mode experiment).
  • Gate RenameTool / UsagesTool getToolData() to optionally return static tool data and avoid provider-change-driven tool data updates.
  • Inject IConfigurationService into both tools to read the setting.
Show a summary per file
File Description
src/vs/workbench/contrib/chat/common/constants.ts Adds ChatConfiguration.SymbolToolsCacheStable enum entry and documentation.
src/vs/workbench/contrib/chat/browser/chat.contribution.ts Registers the new experimental startup-mode configuration setting.
src/vs/workbench/contrib/chat/browser/tools/renameTool.ts Adds cache-stable tool-data branch + config injection; skips provider-change debounced updates when enabled.
src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts Same pattern as RenameTool for usages/reference providers.

Copilot's findings

Comments suppressed due to low confidence (2)

src/vs/workbench/contrib/chat/browser/tools/renameTool.ts:100

  • The new cache-stable branch in getToolData() (static description + always-registered behavior) is currently untested. There are already unit tests covering getToolData() for the non-cache-stable path; please add coverage for chat.experimental.symbolTools.cacheStable = true (e.g. tool data is returned even with no providers, and the model description does not include a per-language list).
	getToolData(): IToolData | undefined {
		if (this._isCacheStable()) {
			return this._getStaticToolData();
		}

src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts:97

  • The new cache-stable branch in getToolData() (static description + always-registered behavior) is currently untested. There are already unit tests covering getToolData() for the non-cache-stable path; please add coverage for chat.experimental.symbolTools.cacheStable = true (e.g. tool data is returned even with no providers, and the model description does not include a per-language list).
	getToolData(): IToolData | undefined {
		if (this._isCacheStable()) {
			return this._getStaticToolData();
		}

  • Files reviewed: 4/4 changed files
  • Comments generated: 2

Comment thread src/vs/workbench/contrib/chat/browser/tools/renameTool.ts
Comment thread src/vs/workbench/contrib/chat/browser/tools/usagesTool.ts
Kevin Kent and others added 2 commits April 25, 2026 20:00
…ndency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses Copilot PR review: covers RenameTool/UsagesTool getToolData()
behavior when chat.experimental.symbolTools.cacheStable is enabled:
- tool data is returned even with no providers registered
- modelDescription does not include 'Currently supported for' list
- modelDescription is byte-stable across provider registrations

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bhavyaus bhavyaus enabled auto-merge (squash) April 26, 2026 02:02
@bhavyaus bhavyaus merged commit 9c40178 into microsoft:main Apr 26, 2026
26 checks passed
@vs-code-engineering vs-code-engineering Bot added this to the 1.118.0 milestone Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants