Imported from microsoft/vscode (
src/vs/platform/agentHost/node/copilot/prompts/AGENTS.md). Install upstream withnpx skills add microsoft/vscode --skill prompts. Copyright stays with the author.
Agent host system-prompt customization
This directory customizes the system prompt for Copilot CLI agent host (ahp+cli) sessions. Read this before changing how the system message is built or adding per-model / per-tool guidance. It mirrors the Copilot extension's extensions/copilot/.../prompts/node/agent/ (agentPrompts), but the agent host runs in its own process and cannot use prompt-tsx, so contributors return plain data the SDK accepts directly.
Files
promptRegistry.ts—AgentHostPromptRegistry: resolves the finalSystemMessageConfigfor a session's model. Defines theIAgentHostPromptcontributor interface and theIAgentHostPromptContextread-time context.systemMessage.ts— the default message (COPILOT_AGENT_HOST_SYSTEM_MESSAGE), shared identity text, thefullSystemPrompt/sectionOverridesbuilders, anddescribeSystemMessageConfig(the one-line log summary).toolInstructions.ts— the model-agnostictool_instructionslayer: gated or unconditional nudges (TOOL_INSTRUCTION_LINES) composed into the SDK'stool_instructionssection, 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 tocode_change_ruleswithout replacing the SDK foundation prompt.allPrompts.ts— side-effect import hub; importing it registers every contributor into the sharedagentHostPromptRegistry.
How the system message is built
Unless the matching chat.agentHost.copilot.modelCapabilityOverrides entry provides a YAML promptOverrideString or promptOverrideFile containing systemPrompt, resolveSystemMessageConfig(model, context) layers, in order:
- Base —
_resolveModelConfigpicks the per-model (or default) config. Falls back toCOPILOT_AGENT_HOST_SYSTEM_MESSAGEwhen there's no model, no matching contributor, or the contributor opts out for thiscontext. A contributor'scustomizeconfig gets the default sections composed underneath it (withDefaultSections), so a contributor only overrides the sections it names — the defaultidentitysurvives unless explicitly overridden. _withUniversalSections— layers the model-agnostic tool instructions on top, composing with — never clobbering — any per-model override for that section. For areplacebase the lines are appended after the replacement content instead.- Workspaceless scratch + file-link contract — appended as trailing
contentfor every mode, includingreplace, so a full replacement owns the prompt body but not the host's response-format plumbing.
The per-model prompt override supports the same fields and precedence as the Copilot Chat debug prompt override: inline YAML takes precedence over a YAML file, systemPrompt bypasses this registry and is sent directly as the Copilot SDK's systemMessage in replace mode, and toolDescriptions replaces descriptions on tools registered by Agent Host. This internal debugging setting assumes those values use the documented string shape.
Launch-time freeze. The SDK accepts a system message only at session create/resume; there is no mid-session update. The prompt is resolved once per (re)launch and any tool-gated content reflects the tool set at that moment. A change to the session's tools/plugins is part of the launcher's restart snapshot, so it re-launches and recomputes; an in-flight turn keeps the prompt it launched with.
There are two ways to customize, and a model can use both at once.
Lever 1 — universal, all models (toolInstructions.ts)
Guidance that should apply to every model. A line can be unconditional for host-wide behavior such as reading offloaded tool output, gated on a client tool as the browser line is, or gated on a host setting as the subagent model-guidance line is.
- Write a
ToolInstructionLine— a function(context) => string | undefinedthat returns one sentence (no surrounding newlines), orundefinedwhen its gate does not apply. TheIToolInstructionContextexposeshasTool(name)andgetSetting(key)(aCopilotCliConfigKey). - Add it to
TOOL_INSTRUCTION_LINES.
const exampleToolInstructions: ToolInstructionLine = ({ hasTool }) =>
hasTool('someClientToolReferenceName')
? 'One sentence of guidance, shown only when that tool is present.'
: undefined;
const TOOL_INSTRUCTION_LINES: readonly ToolInstructionLine[] = [largeOutputToolInstructions, browserToolInstructions, exampleToolInstructions];
Caveat — hasTool sees CLIENT tools only. It is context.hasClientTool, which knows only the forwarded workbench tools, addressed by their camelCase toolReferenceName (e.g. openBrowserPage, runTask, getTaskOutput) — NOT the extension's snake_case ids, and NOT shell / server-SDK / MCP tools (MCP is discovered dynamically and isn't in the launch snapshot). A line gated on a name that is never a client tool silently never renders. The default client-tool allowlist is chat.agentHost.clientTools (see chat.shared.contribution.ts). Broadening this context is a known follow-up.
These lines compose with a per-model tool_instructions override (see composeToolInstructions), so Lever 1 and Lever 2 stack.
Tool search (deferred tool loading)
When chat.agentHost.copilot.toolSearch.enabled is on AND the session's model supports it (agentHostModelSupportsToolSearch), the launcher sets toolSearch: { enabled: true, deferThreshold: 1 } and the session defers MCP + non-core client tools behind the runtime's tool_search_tool:
- The override (
copilotAgentSession._createClientSdkTools): the client's forwardedtoolSearchtool is registered astool_search_toolwithoverridesBuiltInTool: trueanddefer: 'never', so the runtime routes the model's search to the client's semantic search. The SDK supplies the runtime's live deferred-tool metadata to the override handler; Agent Host carries that corpus as transient tool-call metadata and injects it only into the localtoolSearchinvocation, so embeddings rank the runtime/MCP tools rather than the extension's registry. The corpus is never added to model-facing tool input. Every other client tool getsdefer: 'never'if it is inNON_DEFERRED_CLIENT_TOOL_NAMES(runTests,rename,usages), elsedefer: 'auto'. Built-in runtime tools are never deferred. The renderer (agentHostSessionHandler._setupClientToolCall) mapstool_search_toolback totoolSearchto execute the real VS Code tool. - The prompt (this folder):
toolSearchInstructionLines(toolSearchActive)adds atool_instructionsline (toolSearchToolInstructions) telling the model to load deferred tools viatool_search_toolfirst — gated oncontext.toolSearchActivebecause thetoolSearchtool is always forwarded, so presence alone can't gate it. The runtime already emits its own deferred-tools reminder (build_deferred_tools_user_message) with the accurate deferred set, so this layer intentionally does NOT re-list the deferred tools.
The two identity/count levers are independent: deferThreshold is a total tool-count gate (1 ⇒ always active), while each tool's defer flag decides whether that tool is deferred.
This B-inject bridge is intentionally interim. A follow-up moves tool-search registration and ranking into VS Code core so Agent Host no longer depends on the Copilot extension's tool implementation or embeddings plumbing.
Lever 2 — per-model contributor (promptRegistry.ts + allPrompts.ts)
Guidance scoped to a model or family. Implement IAgentHostPrompt and register it. Use anthropicPrompt.ts as the template.
A contributor provides EITHER:
resolveSectionOverrides→{ mode: 'customize' }— overrides named sections, keeps the SDK foundation prompt and its guardrails. Prefer this. The default sections are composed underneath, so there's no need to re-state the identity.resolveFullSystemPrompt→{ mode: 'replace' }— owns the entire prompt body and drops all SDK guardrails (including safety). Only for callers that truly own the whole prompt. The registry still appends the universal layers (tool instructions, workspaceless guidance, file-link contract) after the replacement content.
class MyModelPrompt implements IAgentHostPrompt {
static readonly familyPrefixes = ['my-model']; // or implement static matchesModel(model)
resolveSectionOverrides(model: ModelSelection, context: IAgentHostPromptContext) {
// Gate on host settings; return undefined to fall back to the default message.
return context.getSetting(CopilotCliConfigKey.SomeFlag) === true
? { tool_instructions: { action: 'append', content: '\nFor this model, batch independent tool calls.' } }
: undefined;
}
}
agentHostPromptRegistry.registerPrompt(MyModelPrompt); // then add `import './myModelPrompt.js'` to allPrompts.ts
Matching: a contributor matches a model by static matchesModel(model) (takes precedence) or by familyPrefixes (model-id startsWith). The registry resolves exactly one contributor per model (first match wins) — base + version layering is a known follow-up.
This branch's OpenAI contributor is unconditional for GPT families, legacy o1/o3/o4 families, and the openai family alias; it has no setting. GPT and openai matching follows Copilot Chat's isOpenAIModel family conventions, case-insensitively. Agent Host has a model ID rather than endpoint-provider metadata; a custom model ID can use the existing family override to route through a known OpenAI family. The experiment is isolated to the branch and tracked in microsoft/vscode-internalbacklog#9579. It discourages automatic post-edit rereads and full-diff reviews, but preserves targeted reads for failed checks, ambiguous tool output, or correctness uncertainty, required validation, and explicitly requested broader reviews. It does not change delegation or non-OpenAI models. Guidance is resolved on session create/resume; existing in-flight sessions keep their launch-time prompt.
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).
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.
The runtime keeps its own per-model config (system-prompt parts, capabilities, reasoning-effort profile) keyed off the model id it receives, so an aliased session gets the real model's runtime config with the family's host overrides layered on top. Aliasing the runtime's half too would need COPILOT_MODEL_FAMILY, which is process-scoped and would leak across every session in the window.
Security note. The setting is application-scoped (not workspace-configurable) and forwarded to the agent host.
promptOverrideStringandpromptOverrideFiledeliberately carry prompt content and a host-local file path for debugging and evaluation; do not add other prompt or filesystem inputs to this setting without an explicit security review. Code-managed prompt experiments should still use a contributor (Lever 2) gated on its own opt-in setting.
Reference
- Modes (
SystemMessageConfig.mode):append(foundation + text, default),customize(override named sections),replace(own the whole prompt, no guardrails). - Sections (
SystemMessageSection):preamble,identity,tone,tool_efficiency,environment_context,code_change_rules,guidelines,safety,tool_instructions,custom_instructions,runtime_instructions,last_instructions. - Override actions (
SectionOverride.action):replace,append,prepend,remove, or a(content: string) => stringtransform.
Gotchas
- Empty overrides = no override.
resolveSectionOverridesreturning{}(orundefined) falls back to the default message — equivalent to composing nothing over the defaults, kept explicit to avoid pointless object churn. - Don't mutate the shared default.
COPILOT_AGENT_HOST_SYSTEM_MESSAGEis a shared constant; layering spreads into a fresh object, preserving any other customize-mode fields (e.g.content). Keep it that way. - Spacing is relative to the foundation.
composeToolInstructionspads by action (appendleads with\n,prependtrails with\n,replaceowns the section). When writing a section'scontentby hand, a leading\nkeeps appended text off the foundation's last line. - Observability. The launcher logs
describeSystemMessageConfig(...)atinfo(mode + overridden sections) and the full config attrace. Keep new config shapes summarizable there. - Tests.
../../../test/node/agentHostPromptRegistry.test.tscovers the registry/wiring;../../../test/node/toolInstructions.test.tscovers the composition/gating. Add cases there, not new harnesses.
