Skip to content

Commit 2631e43

Browse files
committed
agentHost: trim semantic search scope
1 parent d2e39b2 commit 2631e43

10 files changed

Lines changed: 93 additions & 440 deletions

File tree

src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { CopilotCliConfigKey, copilotCliConfigSchema, normalizeModelFamilyAlias,
1919
import { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js';
2020
import { reasoningEffortLevels, type ReasoningEffortLevel } from '../../common/reasoningEffort.js';
2121
import { AgentHostSandboxConfigKey, sandboxConfigSchema } from '../../common/sandboxConfigSchema.js';
22-
import { CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME, SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';
22+
import { SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';
2323
import type { ModelSelection, ToolDefinition } from '../../common/state/protocol/state.js';
2424
import { RUNTIME_TOOL_SEARCH_TOOL_NAME } from '../../common/toolSearchConstants.js';
2525
import type { ActiveClientToolSet } from '../activeClientState.js';
@@ -131,15 +131,13 @@ export function clientToolNamesFromSnapshot(snapshot: IActiveClientSnapshot): Re
131131
* Narrows the names that gate prompt content so the system message never
132132
* advertises a tool the filters disabled. Client tools are `custom:`-source even
133133
* when they override a built-in, so bare-name and `custom:` forms match (the
134-
* aliased tools under either of their names). Routing keeps the unfiltered
134+
* tool-search tool under either of its names). Routing keeps the unfiltered
135135
* set — the runtime is the enforcement point.
136136
*/
137137
export function filterClientToolNames(names: ReadonlySet<string>, availableTools: readonly string[] | undefined, excludedTools: readonly string[] | undefined): ReadonlySet<string> {
138138
if (!availableTools && !excludedTools) {
139139
return names;
140140
}
141-
const sdkAvailableTools = toSdkToolFilterPatterns(availableTools);
142-
const sdkExcludedTools = toSdkToolFilterPatterns(excludedTools);
143141
const matches = (patterns: readonly string[], name: string) => {
144142
const sdkName = toSdkClientToolName(name);
145143
return patterns.some(pattern =>
@@ -152,37 +150,33 @@ export function filterClientToolNames(names: ReadonlySet<string>, availableTools
152150
};
153151
const result = new Set<string>();
154152
for (const name of names) {
155-
const allowed = !sdkAvailableTools || matches(sdkAvailableTools, name);
156-
if (allowed && !(sdkExcludedTools && matches(sdkExcludedTools, name))) {
153+
const allowed = !availableTools || matches(availableTools, name);
154+
if (allowed && !(excludedTools && matches(excludedTools, name))) {
157155
result.add(name);
158156
}
159157
}
160158
return result;
161159
}
162160

163-
/** Source qualifier the SDK uses for client-contributed tools. */
164-
const CUSTOM_TOOL_PATTERN_PREFIX = 'custom:';
165-
166-
/** Maps workbench client-tool names to their SDK-registered names. */
161+
/** The SDK-registered name for a client tool; only the tool-search tool differs. */
167162
function toSdkClientToolName(name: string): string {
168-
switch (name) {
169-
case CLIENT_TOOL_SEARCH_REFERENCE_NAME:
170-
return RUNTIME_TOOL_SEARCH_TOOL_NAME;
171-
case CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME:
172-
return SEMANTIC_SEARCH_TOOL_NAME;
173-
default:
174-
return name;
175-
}
163+
return name === CLIENT_TOOL_SEARCH_REFERENCE_NAME ? RUNTIME_TOOL_SEARCH_TOOL_NAME : name;
176164
}
177165

178166
/** Maps Agent Host reference names to the names registered with the SDK. */
179167
export function toSdkToolFilterPatterns(patterns: readonly string[] | undefined): string[] | undefined {
180168
if (!patterns) {
181169
return undefined;
182170
}
183-
return [...new Set(patterns.map(pattern => pattern.startsWith(CUSTOM_TOOL_PATTERN_PREFIX)
184-
? `${CUSTOM_TOOL_PATTERN_PREFIX}${toSdkClientToolName(pattern.slice(CUSTOM_TOOL_PATTERN_PREFIX.length))}`
185-
: toSdkClientToolName(pattern)))];
171+
return [...new Set(patterns.map(pattern => {
172+
if (pattern === CLIENT_TOOL_SEARCH_REFERENCE_NAME) {
173+
return toSdkClientToolName(pattern);
174+
}
175+
if (pattern === `custom:${CLIENT_TOOL_SEARCH_REFERENCE_NAME}`) {
176+
return `custom:${toSdkClientToolName(CLIENT_TOOL_SEARCH_REFERENCE_NAME)}`;
177+
}
178+
return pattern;
179+
}))];
186180
}
187181

188182
export interface ICopilotSessionRuntime {
@@ -782,8 +776,6 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
782776
const sdkAvailableTools = toSdkToolFilterPatterns(availableTools);
783777
const configuredSdkExcludedTools = toSdkToolFilterPatterns(excludedTools);
784778
const clientToolNames = filterClientToolNames(clientToolNamesFromSnapshot(plan.snapshot), availableTools, excludedTools);
785-
// Without the client override, drop the SDK's built-in semantic search so the
786-
// setting controls the whole surface.
787779
const sdkExcludedTools = clientToolNames.has(SEMANTIC_SEARCH_TOOL_NAME)
788780
? configuredSdkExcludedTools
789781
: [...new Set([...(configuredSdkExcludedTools ?? []), `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`])];

src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,13 @@ const TOOL_INSTRUCTION_LINES: readonly ToolInstructionLine[] = [largeOutputToolI
7171
```
7272

7373
**Caveat — `hasTool` sees CLIENT tools only.** It is `context.hasClientTool`,
74-
which knows only the forwarded workbench tools, addressed by the `name` each one
75-
is published under — normally the camelCase `toolReferenceName`
76-
(e.g. `openBrowserPage`, `runTask`, `getTaskOutput`), but a tool that overrides
77-
an SDK built-in is republished under the built-in's name (`codebase` arrives as
78-
`semantic_search`; see "Semantic search override"). NOT shell / server-SDK / MCP
79-
tools (MCP is discovered dynamically and isn't in the launch snapshot). A line
80-
gated on a name that is never a client tool silently never renders. Client-tool
81-
membership is controlled by the Chat Customizations tools enablement.
74+
which knows only the forwarded workbench tools, addressed by their **camelCase
75+
`toolReferenceName`** (e.g. `openBrowserPage`, `runTask`, `getTaskOutput`) — NOT
76+
the extension's snake_case ids, and NOT shell / server-SDK / MCP tools (MCP is
77+
discovered dynamically and isn't in the launch snapshot). A
78+
line gated on a name that is never a client tool silently never renders. The
79+
default client-tool allowlist is `chat.agentHost.clientTools` (see
80+
`chat.shared.contribution.ts`). Broadening this context is a known follow-up.
8281

8382
These lines compose with a per-model `tool_instructions` override (see
8483
`composeToolInstructions`), so Lever 1 and Lever 2 stack.
@@ -119,28 +118,6 @@ This B-inject bridge is intentionally interim. A follow-up moves tool-search
119118
registration and ranking into VS Code core so Agent Host no longer depends on
120119
the Copilot extension's tool implementation or embeddings plumbing.
121120

122-
## Semantic search override
123-
124-
`chat.copilot.semanticSearch.enabled` controls the complete Copilot
125-
semantic-search surface, for local and remote Copilot CLI session types alike
126-
(`isCopilotCliSessionType`). When off, the launcher excludes the SDK's built-in
127-
`semantic_search` and the client publishes no `codebase` tool at all. When on,
128-
`AgentHostActiveClientService` republishes the Copilot extension's `codebase`
129-
tool under the name `semantic_search`; the session registers it as a
130-
non-deferred, permission-free built-in override. The launcher's
131-
`toSdkToolFilterPatterns` rewrite of `codebase``semantic_search` assumes that
132-
republishing, so the two must stay gated on the same predicate.
133-
134-
The tool is addressed by its `codebase` reference name, like every other client
135-
tool, and routing resolves that same name via `getToolByName` — so publishing
136-
and routing cannot disagree about which tool owns the slot. Any *other* tool
137-
claiming `codebase` or `semantic_search` is dropped for the session, because two
138-
client tools cannot share one SDK registration.
139-
140-
The tool-gated prompt reminders keep queries focused and require switching to
141-
`grep` or `glob` instead of retrying when the workspace index is unavailable,
142-
updating, empty, or otherwise unhelpful.
143-
144121
## Lever 2 — per-model contributor (`promptRegistry.ts` + `allPrompts.ts`)
145122

146123
Guidance scoped to a model or family. Implement `IAgentHostPrompt` and register

src/vs/platform/agentHost/node/copilot/prompts/toolInstructions.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import type { SectionOverride } from '@github/copilot-sdk';
77
import { coalesce } from '../../../../../base/common/arrays.js';
88
import { BrowserChatToolReferenceName, browserChatToolReferenceNames } from '../../../../browserView/common/browserChatToolReferenceNames.js';
9-
import { SEMANTIC_SEARCH_TOOL_NAME } from '../../../common/semanticSearchConstants.js';
109
import { CLIENT_TOOL_SEARCH_REFERENCE_NAME } from '../../../common/toolSearchConstants.js';
1110

1211
/**
@@ -27,9 +26,8 @@ import { CLIENT_TOOL_SEARCH_REFERENCE_NAME } from '../../../common/toolSearchCon
2726
*/
2827

2928
/**
30-
* A single tool-instructions entry. Returns its content (one or more
31-
* newline-separated sentences, with no leading or trailing newline) when it
32-
* applies, or `undefined` to contribute nothing.
29+
* A single tool-instructions line. Returns its content (a single sentence, no
30+
* surrounding newlines) when it applies, or `undefined` to contribute nothing.
3331
* Mirrors one `<>…</>` fragment in the extension's `toolUseInstructions` block.
3432
*
3533
* @param hasTool predicate for whether a tool name is available in the session.
@@ -64,18 +62,10 @@ const browserToolInstructions: ToolInstructionLine = hasTool => {
6462
return `Use the browser tools (${BrowserChatToolReferenceName.OpenBrowserPage}, ${companion}, etc.) when beneficial for front-end tasks, such as when visualizing or validating UI changes.`;
6563
};
6664

67-
/** Scope guidance, then what to do when the workspace index cannot answer. */
68-
export const COPILOT_AGENT_HOST_SEMANTIC_SEARCH_INSTRUCTION = [
69-
`Use \`${SEMANTIC_SEARCH_TOOL_NAME}\` only for focused natural-language codebase queries when the exact text or symbol is unknown; prefer \`grep\` or \`glob\` when you know what to look for, run it alone rather than in parallel with other searches, and keep queries narrow because broad queries can return oversized results.`,
70-
`If \`${SEMANTIC_SEARCH_TOOL_NAME}\` reports that the index is unavailable or updating, or returns no results, treat the outcome as inconclusive rather than as proof the code does not exist: do not retry or rephrase the query; switch to \`grep\` or \`glob\` instead.`,
71-
].join('\n');
72-
const semanticSearchToolInstructions: ToolInstructionLine = hasTool =>
73-
hasTool(SEMANTIC_SEARCH_TOOL_NAME) ? COPILOT_AGENT_HOST_SEMANTIC_SEARCH_INSTRUCTION : undefined;
74-
7565
/**
7666
* The registered tool-instruction lines, in render order.
7767
*/
78-
const TOOL_INSTRUCTION_LINES: readonly ToolInstructionLine[] = [largeOutputToolInstructions, browserToolInstructions, semanticSearchToolInstructions];
68+
const TOOL_INSTRUCTION_LINES: readonly ToolInstructionLine[] = [largeOutputToolInstructions, browserToolInstructions];
7969

8070
/** Tool-search guidance mirrored from the Copilot extension prompt. */
8171
const toolSearchToolInstructions: ToolInstructionLine = hasTool =>

src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import type { IByokLmBridgeConnection, IByokLmChatRequest, IByokLmChatResult, IB
1919
import { AgentHostByokModelsEnabledConfigKey, type SchemaValues } from '../../common/agentHostSchema.js';
2020
import type { IAgentHostManagedSettingsPermissions } from '../../common/agentHostManagedSettings.js';
2121
import { CopilotCliConfigKey, copilotCliConfigSchema } from '../../common/copilotCliConfig.js';
22-
import { CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME, SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';
2322
import type { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js';
2423
import { reasoningEffortLevels } from '../../common/reasoningEffort.js';
24+
import { SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';
2525
import { CustomizationType, McpServerStatus, type ModelSelection } from '../../common/state/protocol/state.js';
2626
import { CLIENT_TOOL_SEARCH_REFERENCE_NAME, RUNTIME_TOOL_SEARCH_TOOL_NAME } from '../../common/toolSearchConstants.js';
2727
import { ActiveClientToolSet } from '../../node/activeClientState.js';
@@ -925,22 +925,6 @@ suite('filterClientToolNames', () => {
925925
]
926926
);
927927
});
928-
929-
test('keeps workbench and SDK semantic-search names consistent', () => {
930-
const names = new Set([SEMANTIC_SEARCH_TOOL_NAME]);
931-
assert.deepStrictEqual(
932-
[
933-
[...filterClientToolNames(names, [CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME], undefined)],
934-
[...filterClientToolNames(names, undefined, [`custom:${CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME}`])],
935-
toSdkToolFilterPatterns([CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME, `custom:${CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME}`, 'builtin:*']),
936-
],
937-
[
938-
[SEMANTIC_SEARCH_TOOL_NAME],
939-
[],
940-
[SEMANTIC_SEARCH_TOOL_NAME, `custom:${SEMANTIC_SEARCH_TOOL_NAME}`, 'builtin:*'],
941-
]
942-
);
943-
});
944928
});
945929

946930
/**
@@ -1023,51 +1007,20 @@ suite('CopilotSessionLauncher resume config', () => {
10231007
return (launcher as unknown as { _buildSessionConfig(plan: unknown, runtime: unknown): Promise<{ model?: string; reasoningEffort?: string; contextTier?: string; availableTools?: string[]; excludedTools?: string[]; modelCapabilities?: Record<string, unknown>; toolSearch?: { enabled: boolean } }> })._buildSessionConfig(plan, runtime);
10241008
}
10251009

1026-
test('excludes the built-in semantic search unless the client override is enabled', async () => {
1027-
const store = new DisposableStore();
1028-
// Same configured filters on both arms, so the snapshot is the only variable.
1029-
const overrides = { modelCapabilityOverrides: { '*': { excludedTools: ['mcp:*'] } } };
1030-
const disabled = await buildResumeConfig(
1031-
createLauncher(store, overrides),
1032-
undefined,
1033-
{ tools: [], plugins: [], mcpServers: {} },
1034-
);
1035-
const enabled = await buildResumeConfig(
1036-
createLauncher(store, overrides),
1037-
undefined,
1038-
{ tools: [{ name: SEMANTIC_SEARCH_TOOL_NAME }], plugins: [], mcpServers: {} },
1039-
);
1040-
1041-
assert.deepStrictEqual(
1042-
[disabled.excludedTools, enabled.excludedTools],
1043-
[['mcp:*', `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], ['mcp:*']],
1044-
);
1045-
store.dispose();
1046-
});
1047-
1048-
test('does not fall back to built-in semantic search when filters remove the client override', async () => {
1010+
test('exposes only the client semantic-search override', async () => {
10491011
const store = new DisposableStore();
10501012
const snapshot = { tools: [{ name: SEMANTIC_SEARCH_TOOL_NAME }], plugins: [], mcpServers: {} };
1051-
const builtinOnly = await buildResumeConfig(
1052-
createLauncher(store, { modelCapabilityOverrides: { '*': { availableTools: [`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`] } } }),
1053-
undefined,
1054-
snapshot,
1055-
);
1056-
const customExcluded = await buildResumeConfig(
1013+
const disabled = await buildResumeConfig(createLauncher(store, {}), undefined, { tools: [], plugins: [], mcpServers: {} });
1014+
const enabled = await buildResumeConfig(createLauncher(store, {}), undefined, snapshot);
1015+
const filtered = await buildResumeConfig(
10571016
createLauncher(store, { modelCapabilityOverrides: { '*': { excludedTools: [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`] } } }),
10581017
undefined,
10591018
snapshot,
10601019
);
10611020

10621021
assert.deepStrictEqual(
1063-
[
1064-
[builtinOnly.availableTools, builtinOnly.excludedTools],
1065-
[customExcluded.availableTools, customExcluded.excludedTools],
1066-
],
1067-
[
1068-
[[`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], [`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
1069-
[undefined, [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`, `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
1070-
]
1022+
[disabled.excludedTools, enabled.excludedTools, filtered.excludedTools],
1023+
[[`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], undefined, [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`, `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
10711024
);
10721025
store.dispose();
10731026
});

src/vs/platform/agentHost/test/node/toolInstructions.test.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

66
import assert from 'assert';
77
import type { SectionOverride } from '@github/copilot-sdk';
8-
import { COPILOT_AGENT_HOST_LARGE_OUTPUT_TOOL_INSTRUCTION, COPILOT_AGENT_HOST_SEMANTIC_SEARCH_INSTRUCTION, resolveToolInstructionsOverride, toolSearchInstructionLines, universalToolInstructions } from '../../node/copilot/prompts/toolInstructions.js';
9-
import { SEMANTIC_SEARCH_TOOL_NAME } from '../../common/semanticSearchConstants.js';
8+
import { COPILOT_AGENT_HOST_LARGE_OUTPUT_TOOL_INSTRUCTION, resolveToolInstructionsOverride, toolSearchInstructionLines, universalToolInstructions } from '../../node/copilot/prompts/toolInstructions.js';
109
import { CLIENT_TOOL_SEARCH_REFERENCE_NAME } from '../../common/toolSearchConstants.js';
1110
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
1211

@@ -60,16 +59,6 @@ suite('toolInstructions', () => {
6059
]
6160
);
6261
});
63-
64-
test('adds focused-query and fallback reminders only when semantic search is available', () => {
65-
assert.deepStrictEqual([
66-
universalToolInstructions(hasTools(SEMANTIC_SEARCH_TOOL_NAME)),
67-
universalToolInstructions(hasTools()),
68-
], [
69-
`${LARGE_OUTPUT_LINE}\n${COPILOT_AGENT_HOST_SEMANTIC_SEARCH_INSTRUCTION}`,
70-
LARGE_OUTPUT_LINE,
71-
]);
72-
});
7362
});
7463

7564
// `composeToolInstructions` is module-private; its composition/spacing

0 commit comments

Comments
 (0)