Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/vs/platform/agentHost/common/copilotCliConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,33 @@ export interface ICopilotCliModelCapabilityOverride {
/** Map of model id → capability override. */
export type CopilotCliModelCapabilityOverrides = Record<string, ICopilotCliModelCapabilityOverride>;

/**
* Tools excluded by default for {@link COPILOT_HYDRA_FUSION_MODEL_ID}, matching
* the Copilot CLI's lean tool set. Their definitions add ~13K tokens to every
* HydraFusion request. `ask_user` and `web_fetch` stay available because the
* interactive CLI ships them. A configured `availableTools` or `excludedTools`
* capability override replaces this default.
*/
export const HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS: readonly string[] = [
'usages', 'problems',
'createJupyterNotebook', 'editNotebook', 'runNotebookCell', 'getNotebookSummary', 'readNotebookCellOutput',
'runTask', 'getTaskOutput', 'createAndRunTask',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we turning these tools off? Feels like we're overfitting for evals here.

@bhavyaus Bhavya U (bhavyaus) Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also tool search defers all of these tools. this will break scenarios in Agents window as well.

'openBrowserPage', 'readPage', 'screenshotPage', 'navigatePage', 'clickElement', 'dragElement', 'hoverElement', 'typeInPage', 'runPlaywrightCode', 'handleDialog',
'runTests', 'testFailure',
'addComment', 'listComments', 'replyToComment', 'deleteComments', 'resolveComments', 'viewUnreviewedComments',
'list_sessions', 'get_current_session', 'create_session', 'rename_chat', 'send_message', 'get_session_context', 'delete_session',
'setAgentMergeEnabled', 'readAgentMergeCI', 'replyToAgentMergeReviewThread', 'rerunAgentMergeWorkflow',
'add_artifact_or_reference', 'remove_artifact_or_reference', 'list_artifacts_and_references',
];

/**
* Built-in `excludedTools` default for a model (after `family` aliasing), used
* only when no `availableTools` or `excludedTools` capability override applies.
*/
export function getDefaultExcludedTools(modelId: string | undefined): readonly string[] | undefined {
return modelId === COPILOT_HYDRA_FUSION_MODEL_ID ? HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS : undefined;
}

/** Wildcard entry key matching every model id; a specific model-id entry wins field-by-field. */
export const MODEL_CAPABILITY_OVERRIDE_WILDCARD = '*';

Expand Down
13 changes: 11 additions & 2 deletions src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { AgentSession } from '../../common/agent.js';
import type { IAgentProviderSendStageRecorder } from '../../common/agentHostTelemetry.js';
import { getByokLmSelectionModelId, resolveByokLmEnablement, type IByokLmModelInfo } from '../../common/agentHostByokLm.js';
import { AgentHostByokModelsEnabledConfigKey, AgentHostMcpConnectorsEnabledConfigKey, AgentHostSessionSyncEnabledConfigKey, platformRootSchema, type AgentHostMcpServers } from '../../common/agentHostSchema.js';
import { CopilotCliConfigKey, copilotCliConfigSchema, normalizeModelFamilyAlias, normalizeToolSearchDeferThreshold, resolveModelCapabilityOverrideField } from '../../common/copilotCliConfig.js';
import { CopilotCliConfigKey, copilotCliConfigSchema, getDefaultExcludedTools, normalizeModelFamilyAlias, normalizeToolSearchDeferThreshold, resolveModelCapabilityOverrideField } from '../../common/copilotCliConfig.js';
import { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js';
import { reasoningEffortLevels, type ReasoningEffortLevel } from '../../common/reasoningEffort.js';
import { getSessionSandboxConfig } from '../sessionSandbox.js';
Expand Down Expand Up @@ -1010,7 +1010,16 @@ export class CopilotSessionLauncher implements ICopilotSessionLauncher {
this._logService.warn(`[Copilot:${plan.sessionId}] Ignoring unusable 'excludedTools' capability override for '${modelId}'; expected an array of tool patterns`);
});
const availableTools = getToolFilterOverride(availableToolsOverride, 'availableTools', modelId, this._logService, plan.sessionId);
const excludedTools = getToolFilterOverride(excludedToolsOverride, 'excludedTools', modelId, this._logService, plan.sessionId);
const configuredExcludedTools = getToolFilterOverride(excludedToolsOverride, 'excludedTools', modelId, this._logService, plan.sessionId);
// A configured filter on either field replaces the built-in default, so an
// `availableTools` override can re-enable a default-excluded tool.
const defaultExcludedTools = availableTools === undefined && configuredExcludedTools === undefined
? getDefaultExcludedTools(modelFamily ?? model?.id)
: undefined;
if (defaultExcludedTools) {
this._logService.info(`[Copilot:${plan.sessionId}] Applying default 'excludedTools' for '${modelFamily ?? modelId}' (${defaultExcludedTools.length} tools)`);
}
const excludedTools = configuredExcludedTools ?? defaultExcludedTools;
const sdkAvailableTools = toSdkToolFilterPatterns(availableTools);
const configuredSdkExcludedTools = plan.isEphemeral
? [...(toSdkToolFilterPatterns(excludedTools) ?? []), ...EPHEMERAL_DISABLED_COPILOT_TOOLS]
Expand Down
3 changes: 2 additions & 1 deletion src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This directory customizes the system prompt for Copilot CLI **agent host** (ahp+
- `toolInstructions.ts` — the model-agnostic `tool_instructions` layer: gated or unconditional nudges (`TOOL_INSTRUCTION_LINES`) composed into the SDK's `tool_instructions` section, including the default-model guidance for subagents.
- `anthropicPrompt.ts` — example per-model contributor (Claude Opus 4.8).
- `openaiPrompt.ts` — OpenAI targeted post-edit inspection guidance, appended to `code_change_rules` without replacing the SDK foundation prompt.
- `hydraFusionPrompt.ts` — HydraFusion (`hydrafusion`): replaces `tone` and `tool_efficiency` with the Copilot CLI's brevity, search/delegation, and tool-efficiency guidance. Pairs with the launcher's default `excludedTools` for the model (`HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS`: 42 VS Code-only tools; `ask_user` and `web_fetch` stay, as in the interactive CLI).
- `allPrompts.ts` — side-effect import hub; importing it registers every contributor into the shared `agentHostPromptRegistry`.

## How the system message is built
Expand Down Expand Up @@ -84,7 +85,7 @@ This branch's OpenAI contributor is unconditional for GPT families, legacy `o1`/

## Related — per-model experimentation knobs (`copilotCliConfig.ts`)

`chat.agentHost.copilot.modelCapabilityOverrides` entries (keyed by model id; `'*'` matches every model, a specific entry wins field-by-field) carry the experimentation knobs the launcher applies: `family` (prompt and tool-profile alias, so a preview model resolves through another family's contributor), `reasoningEffort` (wins over the model picker's thinking level; set it on the `'*'` entry to pin every model, re-applied on session resume and mid-session model change), `availableTools`/`excludedTools` (SDK tool filters; applied on launch and resume, but not on a mid-session model change — and enforced against every SDK-registered tool, including the host's shell and server tools, not just the forwarded client tools), `modelCapabilities` (per-property overrides passed through to the SDK's `modelCapabilities` field — e.g. vision support, token limits — applied on every launch and resume), and `promptOverrideString`/`promptOverrideFile` (YAML system-prompt and tool-description overrides, applied on launch and resume).
`chat.agentHost.copilot.modelCapabilityOverrides` entries (keyed by model id; `'*'` matches every model, a specific entry wins field-by-field) carry the experimentation knobs the launcher applies: `family` (prompt and tool-profile alias, so a preview model resolves through another family's contributor), `reasoningEffort` (wins over the model picker's thinking level; set it on the `'*'` entry to pin every model, re-applied on session resume and mid-session model change), `availableTools`/`excludedTools` (SDK tool filters; applied on launch and resume, but not on a mid-session model change — and enforced against every SDK-registered tool, including the host's shell and server tools, not just the forwarded client tools; `hydrafusion` defaults `excludedTools` to `HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS` when neither filter is configured for it, and any configured filter — including `excludedTools: []` to restore every tool — replaces that default), `modelCapabilities` (per-property overrides passed through to the SDK's `modelCapabilities` field — e.g. vision support, token limits — applied on every launch and resume), and `promptOverrideString`/`promptOverrideFile` (YAML system-prompt and tool-description overrides, applied on launch and resume).

`family` is host-side only: it selects the prompt contributor and the tool-search capability gate, and the model id sent to the runtime is unchanged, so the session still runs on the selected model. That is the point — a preview model can be evaluated against a known family's prompt and tool profile while still hitting its own endpoint.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@
// import './geminiPrompt.js';

import './anthropicPrompt.js';
import './hydraFusionPrompt.js';
import './openaiPrompt.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { SectionOverride, SystemMessageSection } from '@github/copilot-sdk';
import { COPILOT_HYDRA_FUSION_MODEL_ID } from '../../../common/copilotCliConfig.js';
import type { ModelSelection } from '../../../common/state/protocol/state.js';
import { agentHostPromptRegistry, type IAgentHostPrompt } from './promptRegistry.js';

const HYDRAFUSION_TONE = [
'# Tone and style',
'* When providing output or explanation to the user, limit your response to 100 words or less.',
'* Be concise in routine responses. For complex tasks, briefly explain your approach before implementing.',
'* Prioritize brevity. Default to the shortest possible response that satisfies the request. Cut filler, recap, and process narration.',
].join('\n');

const HYDRAFUSION_TOOL_EFFICIENCY = [
'# Search and delegation',
'* Give sub-agents comprehensive context; response-brevity rules do not apply to their prompts.',
'* Search files/text only in the cwd or its descendants unless absolutely necessary. For code, prefer: available code intelligence > available LSP > glob > grep with a glob > bash.',
'* For broad unfamiliar-code exploration, prefer `search_code_subagent`; use direct search tools for narrow lookups you can resolve in a couple of calls.',
'',
'# Tool usage efficiency',
'CRITICAL: Maximize tool efficiency:',
'* For simple searches, reads, or edits requiring only 2-5 direct calls, use grep, glob, view, edit yourself; delegate only complex/long work that benefits from separate context, since sub-agents add latency.',
'* **USE PARALLEL TOOL CALLING** - when you need to perform multiple independent operations, make ALL tool calls in a SINGLE response. For example, if you need to read 3 files, make 3 view tool calls in one response, NOT 3 sequential responses.',
'* Chain related bash commands with && instead of separate calls',
'* Suppress verbose output (use --quiet, --no-pager, pipe to grep/head when appropriate)',
'* Batching does not replace investigation; take as many turns as needed to understand before acting.',
'* Default task agents to sync; use background only while doing independent work, not to poll while idle.',
].join('\n');

/**
* HydraFusion prompt: replaces the `tone` and `tool_efficiency` sections with
* the Copilot CLI's guidance so HydraFusion makes as few, as concise requests
* in the agent host as it does in the standalone CLI. Pairs with the lean
* default tool set (`HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS`).
*/
class HydraFusionPromptResolver implements IAgentHostPrompt {
static readonly familyPrefixes: readonly string[] = [];

static matchesModel(model: ModelSelection): boolean {
return model.id === COPILOT_HYDRA_FUSION_MODEL_ID;
}

resolveSectionOverrides(): Partial<Record<SystemMessageSection, SectionOverride>> {
return {
tone: { action: 'replace', content: HYDRAFUSION_TONE },
tool_efficiency: { action: 'replace', content: HYDRAFUSION_TOOL_EFFICIENCY },
};
}
}

agentHostPromptRegistry.registerPrompt(HydraFusionPromptResolver);
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,34 @@ suite('AgentHostPromptRegistry', () => {
});
});

suite('HydraFusion contributor (registered via allPrompts)', () => {
// Section text is pinned by its headings so copy edits don't churn the test.
const headings = (override: SectionOverride | undefined) => override && {
action: override.action,
headings: override.content?.split('\n').filter(line => line.startsWith('#')),
};

test('replaces tone and tool_efficiency with the CLI guidance without a setting', () => {
const result = agentHostPromptRegistry.resolveSystemMessageConfig({ id: 'hydrafusion' }, context());
assert.ok(result.mode === 'customize');
const { tone, tool_efficiency, ...sections } = result.sections ?? {};
assert.deepStrictEqual({ ...result, sections, tone: headings(tone), tool_efficiency: headings(tool_efficiency) }, {
...withUniversalAgentHostInstructions(COPILOT_AGENT_HOST_SYSTEM_MESSAGE),
tone: { action: 'replace', headings: ['# Tone and style'] },
tool_efficiency: { action: 'replace', headings: ['# Search and delegation', '# Tool usage efficiency'] },
});
});

test('does not match other models', () => {
for (const id of ['hydrafusion-preview', 'claude-sonnet-5', 'unknown']) {
assert.deepStrictEqual(
agentHostPromptRegistry.resolveSystemMessageConfig({ id }, context()),
withUniversalAgentHostInstructions(COPILOT_AGENT_HOST_SYSTEM_MESSAGE)
);
}
});
});

suite('model capability overrides (family alias)', () => {
// Mirrors the launcher's composition in `_buildSessionConfig`: the
// resolved family becomes the effective model id handed to the registry.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AgentHostByokModelsEnabledConfigKey, AgentHostMcpConnectorsEnabledConfi
import type { IAgentHostManagedSettingsPermissions } from '../../common/agentHostManagedSettings.js';
import { toClientPluginMcpDefaultCwdsMeta } from '../../common/meta/clientPluginCustomizationMeta.js';
import { readSessionSandboxState } from '../../common/meta/agentSandboxStateMeta.js';
import { CopilotCliConfigKey, copilotCliConfigSchema } from '../../common/copilotCliConfig.js';
import { COPILOT_HYDRA_FUSION_MODEL_ID, CopilotCliConfigKey, copilotCliConfigSchema, HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS } from '../../common/copilotCliConfig.js';
import type { IAgentHostOTelService } from '../../common/otel/agentHostOTelService.js';
import { reasoningEffortLevels } from '../../common/reasoningEffort.js';
import { autoModeTiers } from '../../common/autoModeTiers.js';
Expand Down Expand Up @@ -1886,6 +1886,35 @@ suite('CopilotSessionLauncher resume config', () => {
store.dispose();
});

test('hydrafusion defaults to the lean excluded tool set unless a tool filter is configured', async () => {
const store = new DisposableStore();
const hydraFusion: ModelSelection = { id: COPILOT_HYDRA_FUSION_MODEL_ID };
const semanticSearch = `builtin:${SEMANTIC_SEARCH_TOOL_NAME}`;
const defaulted = await buildResumeConfig(createLauncher(store, {}), hydraFusion);
const otherModel = await buildResumeConfig(createLauncher(store, {}), { id: 'gpt-5' });
const aliased = await buildResumeConfig(createLauncher(store, { modelCapabilityOverrides: { 'preview-model': { family: COPILOT_HYDRA_FUSION_MODEL_ID } } }), { id: 'preview-model' });
const userExcluded = await buildResumeConfig(createLauncher(store, { modelCapabilityOverrides: { [COPILOT_HYDRA_FUSION_MODEL_ID]: { excludedTools: ['mcp:*'] } } }), hydraFusion);
const userAvailable = await buildResumeConfig(createLauncher(store, { modelCapabilityOverrides: { [COPILOT_HYDRA_FUSION_MODEL_ID]: { availableTools: ['web_fetch'] } } }), hydraFusion);
const wildcard = await buildResumeConfig(createLauncher(store, { modelCapabilityOverrides: { '*': { excludedTools: [] } } }), hydraFusion);

assert.deepStrictEqual(
[defaulted, otherModel, aliased, userExcluded, userAvailable, wildcard].map(config => [config.availableTools, config.excludedTools]),
[
[undefined, [...HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS, semanticSearch]],
[undefined, [semanticSearch]],
[undefined, [...HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS, semanticSearch]],
[undefined, ['mcp:*', semanticSearch]],
[['web_fetch'], [semanticSearch]],
[undefined, [semanticSearch]],
]
);
assert.deepStrictEqual(
{ count: HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS.length, keepsCliTools: ['ask_user', 'web_fetch'].filter(tool => HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS.includes(tool)) },
{ count: 42, keepsCliTools: [] }
);
store.dispose();
});

test('forwards a configured modelCapabilities override and ignores a non-object one', async () => {
const store = new DisposableStore();
const model: ModelSelection = { id: 'gpt-5', config: { thinkingLevel: 'medium' } };
Expand Down
Loading