Skip to content

Commit 886e657

Browse files
committed
agentHost: trim semantic search scope
1 parent 682e67e commit 886e657

10 files changed

Lines changed: 94 additions & 439 deletions

File tree

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

Lines changed: 15 additions & 21 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';
@@ -133,15 +133,13 @@ export function clientToolNamesFromSnapshot(snapshot: IActiveClientSnapshot): Re
133133
* Narrows the names that gate prompt content so the system message never
134134
* advertises a tool the filters disabled. Client tools are `custom:`-source even
135135
* when they override a built-in, so bare-name and `custom:` forms match (the
136-
* aliased tools under either of their names). Routing keeps the unfiltered
136+
* tool-search tool under either of its names). Routing keeps the unfiltered
137137
* set — the runtime is the enforcement point.
138138
*/
139139
export function filterClientToolNames(names: ReadonlySet<string>, availableTools: readonly string[] | undefined, excludedTools: readonly string[] | undefined): ReadonlySet<string> {
140140
if (!availableTools && !excludedTools) {
141141
return names;
142142
}
143-
const sdkAvailableTools = toSdkToolFilterPatterns(availableTools);
144-
const sdkExcludedTools = toSdkToolFilterPatterns(excludedTools);
145143
const matches = (patterns: readonly string[], name: string) => {
146144
const sdkName = toSdkClientToolName(name);
147145
return patterns.some(pattern =>
@@ -154,37 +152,33 @@ export function filterClientToolNames(names: ReadonlySet<string>, availableTools
154152
};
155153
const result = new Set<string>();
156154
for (const name of names) {
157-
const allowed = !sdkAvailableTools || matches(sdkAvailableTools, name);
158-
if (allowed && !(sdkExcludedTools && matches(sdkExcludedTools, name))) {
155+
const allowed = !availableTools || matches(availableTools, name);
156+
if (allowed && !(excludedTools && matches(excludedTools, name))) {
159157
result.add(name);
160158
}
161159
}
162160
return result;
163161
}
164162

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

180168
/** Maps Agent Host reference names to the names registered with the SDK. */
181169
export function toSdkToolFilterPatterns(patterns: readonly string[] | undefined): string[] | undefined {
182170
if (!patterns) {
183171
return undefined;
184172
}
185-
return [...new Set(patterns.map(pattern => pattern.startsWith(CUSTOM_TOOL_PATTERN_PREFIX)
186-
? `${CUSTOM_TOOL_PATTERN_PREFIX}${toSdkClientToolName(pattern.slice(CUSTOM_TOOL_PATTERN_PREFIX.length))}`
187-
: toSdkClientToolName(pattern)))];
173+
return [...new Set(patterns.map(pattern => {
174+
if (pattern === CLIENT_TOOL_SEARCH_REFERENCE_NAME) {
175+
return toSdkClientToolName(pattern);
176+
}
177+
if (pattern === `custom:${CLIENT_TOOL_SEARCH_REFERENCE_NAME}`) {
178+
return `custom:${toSdkClientToolName(CLIENT_TOOL_SEARCH_REFERENCE_NAME)}`;
179+
}
180+
return pattern;
181+
}))];
188182
}
189183

190184
export interface ICopilotSessionRuntime {

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: 8 additions & 55 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';
@@ -519,7 +519,7 @@ suite('CopilotSessionLauncher shared session config', () => {
519519
resumeManagedSettings: { permissions: managedSettingsPermissions },
520520
ephemeralMcpServers: {},
521521
ephemeralDisabledMcpServers: ['azure', 'disabled-workspace-server', 'github', 'native-plugin-server', 'synced-server'],
522-
ephemeralExcludedTools: ['task'],
522+
ephemeralExcludedTools: ['task', `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`],
523523
});
524524
} finally {
525525
sessions.dispose();
@@ -932,22 +932,6 @@ suite('filterClientToolNames', () => {
932932
]
933933
);
934934
});
935-
936-
test('keeps workbench and SDK semantic-search names consistent', () => {
937-
const names = new Set([SEMANTIC_SEARCH_TOOL_NAME]);
938-
assert.deepStrictEqual(
939-
[
940-
[...filterClientToolNames(names, [CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME], undefined)],
941-
[...filterClientToolNames(names, undefined, [`custom:${CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME}`])],
942-
toSdkToolFilterPatterns([CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME, `custom:${CLIENT_SEMANTIC_SEARCH_REFERENCE_NAME}`, 'builtin:*']),
943-
],
944-
[
945-
[SEMANTIC_SEARCH_TOOL_NAME],
946-
[],
947-
[SEMANTIC_SEARCH_TOOL_NAME, `custom:${SEMANTIC_SEARCH_TOOL_NAME}`, 'builtin:*'],
948-
]
949-
);
950-
});
951935
});
952936

953937
/**
@@ -1030,51 +1014,20 @@ suite('CopilotSessionLauncher resume config', () => {
10301014
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);
10311015
}
10321016

1033-
test('excludes the built-in semantic search unless the client override is enabled', async () => {
1034-
const store = new DisposableStore();
1035-
// Same configured filters on both arms, so the snapshot is the only variable.
1036-
const overrides = { modelCapabilityOverrides: { '*': { excludedTools: ['mcp:*'] } } };
1037-
const disabled = await buildResumeConfig(
1038-
createLauncher(store, overrides),
1039-
undefined,
1040-
{ tools: [], plugins: [], mcpServers: {} },
1041-
);
1042-
const enabled = await buildResumeConfig(
1043-
createLauncher(store, overrides),
1044-
undefined,
1045-
{ tools: [{ name: SEMANTIC_SEARCH_TOOL_NAME }], plugins: [], mcpServers: {} },
1046-
);
1047-
1048-
assert.deepStrictEqual(
1049-
[disabled.excludedTools, enabled.excludedTools],
1050-
[['mcp:*', `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], ['mcp:*']],
1051-
);
1052-
store.dispose();
1053-
});
1054-
1055-
test('does not fall back to built-in semantic search when filters remove the client override', async () => {
1017+
test('exposes only the client semantic-search override', async () => {
10561018
const store = new DisposableStore();
10571019
const snapshot = { tools: [{ name: SEMANTIC_SEARCH_TOOL_NAME }], plugins: [], mcpServers: {} };
1058-
const builtinOnly = await buildResumeConfig(
1059-
createLauncher(store, { modelCapabilityOverrides: { '*': { availableTools: [`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`] } } }),
1060-
undefined,
1061-
snapshot,
1062-
);
1063-
const customExcluded = await buildResumeConfig(
1020+
const disabled = await buildResumeConfig(createLauncher(store, {}), undefined, { tools: [], plugins: [], mcpServers: {} });
1021+
const enabled = await buildResumeConfig(createLauncher(store, {}), undefined, snapshot);
1022+
const filtered = await buildResumeConfig(
10641023
createLauncher(store, { modelCapabilityOverrides: { '*': { excludedTools: [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`] } } }),
10651024
undefined,
10661025
snapshot,
10671026
);
10681027

10691028
assert.deepStrictEqual(
1070-
[
1071-
[builtinOnly.availableTools, builtinOnly.excludedTools],
1072-
[customExcluded.availableTools, customExcluded.excludedTools],
1073-
],
1074-
[
1075-
[[`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], [`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
1076-
[undefined, [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`, `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
1077-
]
1029+
[disabled.excludedTools, enabled.excludedTools, filtered.excludedTools],
1030+
[[`builtin:${SEMANTIC_SEARCH_TOOL_NAME}`], undefined, [`custom:${SEMANTIC_SEARCH_TOOL_NAME}`, `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`]],
10781031
);
10791032
store.dispose();
10801033
});

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)