From d65074a0243a579251083519e8ea77789bd1e2e5 Mon Sep 17 00:00:00 2001 From: Aashna Garg Date: Thu, 1 Oct 2026 12:24:08 -0700 Subject: [PATCH 1/2] agentHost: lean tools + CLI-parity prompt for HydraFusion Default-exclude VS Code-only tools for hydrafusion unless a tool filter is configured, and add CLI tone/search/tool-efficiency prompt sections for hydrafusion (microsoft/vscode-internalbacklog#9756). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../agentHost/common/copilotCliConfig.ts | 27 +++++++++ .../node/copilot/copilotSessionLauncher.ts | 13 ++++- .../agentHost/node/copilot/prompts/AGENTS.md | 3 +- .../node/copilot/prompts/allPrompts.ts | 1 + .../node/copilot/prompts/hydraFusionPrompt.ts | 55 +++++++++++++++++++ .../test/node/agentHostPromptRegistry.test.ts | 28 ++++++++++ .../test/node/copilotSessionLauncher.test.ts | 27 ++++++++- 7 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 src/vs/platform/agentHost/node/copilot/prompts/hydraFusionPrompt.ts diff --git a/src/vs/platform/agentHost/common/copilotCliConfig.ts b/src/vs/platform/agentHost/common/copilotCliConfig.ts index 6a069a4ea1904e..ffe39dfbcb6048 100644 --- a/src/vs/platform/agentHost/common/copilotCliConfig.ts +++ b/src/vs/platform/agentHost/common/copilotCliConfig.ts @@ -121,6 +121,33 @@ export interface ICopilotCliModelCapabilityOverride { /** Map of model id → capability override. */ export type CopilotCliModelCapabilityOverrides = Record; +/** + * 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. A configured `availableTools` or `excludedTools` + * capability override replaces this default. + */ +export const HYDRAFUSION_DEFAULT_EXCLUDED_TOOLS: readonly string[] = [ + 'web_fetch', 'ask_user', + 'usages', 'problems', + 'createJupyterNotebook', 'editNotebook', 'runNotebookCell', 'getNotebookSummary', 'readNotebookCellOutput', + 'runTask', 'getTaskOutput', 'createAndRunTask', + '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 = '*'; diff --git a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts index e5bbd8cfeff8b7..42e8558a4f3461 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotSessionLauncher.ts @@ -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'; @@ -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] diff --git a/src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md b/src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md index 27972fc1ef9dc3..62b5b66ef51d1f 100644 --- a/src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md +++ b/src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md @@ -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`). - `allPrompts.ts` — side-effect import hub; importing it registers every contributor into the shared `agentHostPromptRegistry`. ## How the system message is built @@ -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. diff --git a/src/vs/platform/agentHost/node/copilot/prompts/allPrompts.ts b/src/vs/platform/agentHost/node/copilot/prompts/allPrompts.ts index 1562c9d487b4d4..fa852ecef6a873 100644 --- a/src/vs/platform/agentHost/node/copilot/prompts/allPrompts.ts +++ b/src/vs/platform/agentHost/node/copilot/prompts/allPrompts.ts @@ -13,4 +13,5 @@ // import './geminiPrompt.js'; import './anthropicPrompt.js'; +import './hydraFusionPrompt.js'; import './openaiPrompt.js'; diff --git a/src/vs/platform/agentHost/node/copilot/prompts/hydraFusionPrompt.ts b/src/vs/platform/agentHost/node/copilot/prompts/hydraFusionPrompt.ts new file mode 100644 index 00000000000000..26401f4940971c --- /dev/null +++ b/src/vs/platform/agentHost/node/copilot/prompts/hydraFusionPrompt.ts @@ -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> { + return { + tone: { action: 'replace', content: HYDRAFUSION_TONE }, + tool_efficiency: { action: 'replace', content: HYDRAFUSION_TOOL_EFFICIENCY }, + }; + } +} + +agentHostPromptRegistry.registerPrompt(HydraFusionPromptResolver); diff --git a/src/vs/platform/agentHost/test/node/agentHostPromptRegistry.test.ts b/src/vs/platform/agentHost/test/node/agentHostPromptRegistry.test.ts index 307128adb3ef0d..eb3ffe8bd4716c 100644 --- a/src/vs/platform/agentHost/test/node/agentHostPromptRegistry.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostPromptRegistry.test.ts @@ -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. diff --git a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts index ff086994e59570..f58f155b705e58 100644 --- a/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotSessionLauncher.test.ts @@ -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'; @@ -1886,6 +1886,31 @@ 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]], + ] + ); + 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' } }; From 71d2177055a1974ce95bff383a82264aff59bb02 Mon Sep 17 00:00:00 2001 From: Aashna Garg Date: Thu, 1 Oct 2026 12:42:48 -0700 Subject: [PATCH 2/2] agentHost: keep ask_user and web_fetch for HydraFusion The interactive Copilot CLI ships both tools, so keeping them is true parity (42 excluded). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/vs/platform/agentHost/common/copilotCliConfig.ts | 4 ++-- src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md | 2 +- .../agentHost/test/node/copilotSessionLauncher.test.ts | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/agentHost/common/copilotCliConfig.ts b/src/vs/platform/agentHost/common/copilotCliConfig.ts index ffe39dfbcb6048..a010f98d033dc6 100644 --- a/src/vs/platform/agentHost/common/copilotCliConfig.ts +++ b/src/vs/platform/agentHost/common/copilotCliConfig.ts @@ -124,11 +124,11 @@ export type CopilotCliModelCapabilityOverrides = Record { [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(); });