Skip to content

Commit 2f343ff

Browse files
unsupportedpastelsaeschliCopilot
authored
Support reasoning effort in custom agent files (#329263)
* Add reasoning effort support for custom agents * Register reasoning effort in agent language service * Fix custom agent reasoning effort typing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * update --------- Co-authored-by: Martin Aeschlimann <martinae@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b99d111 commit 2f343ff

8 files changed

Lines changed: 123 additions & 7 deletions

File tree

extensions/copilot/assets/prompts/skills/agent-customization/references/agents.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ description: "<required>" # For agent picker and subagent discovery
1717
name: "Agent Name" # Optional, defaults to filename
1818
tools: [search, web] # Optional: aliases, MCP (<server>/*), extension tools
1919
model: "Claude Sonnet 4" # Optional, uses picker default; supports array for fallback
20+
reasoning-effort: "high" # Optional: low, medium, high, xhigh, or max
2021
argument-hint: "Task..." # Optional, input guidance
2122
agents: [agent1, agent2] # Optional, restrict allowed subagents by name (omit = all, [] = none)
2223
user-invocable: true # Optional, show in agent picker (default: true)
@@ -45,6 +46,16 @@ hooks: # Optional, inline hooks for this agent's lifecycle
4546
model: ['Claude Sonnet 4.5 (copilot)', 'GPT-5 (copilot)'] # First available model is used
4647
```
4748
49+
### Reasoning Effort
50+
51+
Use `reasoning-effort` to set the reasoning level for the custom agent's model:
52+
53+
```yaml
54+
reasoning-effort: high # low, medium, high, xhigh, or max
55+
```
56+
57+
The selected model must support the configured level. If omitted, the runtime resolves the effort from the model configuration and, when applicable, the parent agent.
58+
4859
## Tools
4960

5061
Sources: built-in aliases, specific tools, MCP servers (`<server>/*`), extension tools.

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ function toStringEnv(env: Record<string, string | number | null>): Record<string
114114
// Custom agents
115115
// ---------------------------------------------------------------------------
116116

117+
const customAgentReasoningEfforts = ['low', 'medium', 'high', 'xhigh', 'max'] as const satisfies readonly NonNullable<CustomAgentConfig['reasoningEffort']>[];
118+
type CustomAgentReasoningEffort = (typeof customAgentReasoningEfforts)[number];
119+
120+
function isCustomAgentReasoningEffort(value: string | undefined): value is CustomAgentReasoningEffort {
121+
return customAgentReasoningEfforts.some(reasoningEffort => reasoningEffort === value);
122+
}
123+
117124
/**
118125
* Converts parsed plugin agents into the SDK's `customAgents` config.
119126
*
@@ -122,6 +129,7 @@ function toStringEnv(env: Record<string, string | number | null>): Record<string
122129
* - `description` is forwarded verbatim.
123130
* - `tools` is forwarded as the SDK's allow-list; an empty / missing array
124131
* becomes `null` so the SDK grants the agent access to all tools.
132+
* - `reasoning-effort` is forwarded when it is a supported runtime value.
125133
* - `prompt` is the markdown body that follows the frontmatter (or the
126134
* full file content when there is no frontmatter).
127135
*/
@@ -146,6 +154,7 @@ export async function toSdkCustomAgents(agents: readonly INamedPluginResource[],
146154
const description = md.getStringValue('description');
147155
const tools = md.getStringArrayValue('tools');
148156
const skills = md.getStringArrayValue('skills');
157+
const reasoningEffort = md.getStringValue('reasoning-effort');
149158
let infer = md.getBooleanValue('infer');
150159
const disableModelInvocation = md.getBooleanValue('disable-model-invocation');
151160
if (infer === undefined && disableModelInvocation === true) {
@@ -161,6 +170,7 @@ export async function toSdkCustomAgents(agents: readonly INamedPluginResource[],
161170
name,
162171
...(description ? { description } : {}),
163172
...(model ? { model } : {}),
173+
...(isCustomAgentReasoningEffort(reasoningEffort) ? { reasoningEffort } : {}),
164174
tools: tools && tools.length > 0 ? tools : null,
165175
...(skills !== undefined ? { skills } : {}),
166176
...(infer !== undefined ? { infer } : {}),

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,59 @@ suite('copilotPluginConverters', () => {
250250
}]);
251251
});
252252

253+
test('parses supported reasoning-effort values from frontmatter', async () => {
254+
const reasoningEfforts = ['low', 'medium', 'high', 'xhigh', 'max'] as const;
255+
const agents: INamedPluginResource[] = [];
256+
for (const reasoningEffort of reasoningEfforts) {
257+
const agentUri = URI.from({ scheme: Schemas.inMemory, path: `/agents/${reasoningEffort}.md` });
258+
await fileService.writeFile(agentUri, VSBuffer.fromString([
259+
'---',
260+
`name: ${reasoningEffort}`,
261+
`reasoning-effort: ${reasoningEffort}`,
262+
'---',
263+
'Body.',
264+
].join('\n')));
265+
agents.push({ uri: agentUri, name: reasoningEffort });
266+
}
267+
268+
const result = await toSdkCustomAgents(agents, fileService);
269+
270+
assert.deepStrictEqual(result, reasoningEfforts.map(reasoningEffort => ({
271+
name: reasoningEffort,
272+
reasoningEffort,
273+
tools: null,
274+
prompt: 'Body.',
275+
})));
276+
});
277+
278+
test('omits missing or unsupported reasoning-effort values', async () => {
279+
const missingUri = URI.from({ scheme: Schemas.inMemory, path: '/agents/missing-effort.md' });
280+
const unsupportedUri = URI.from({ scheme: Schemas.inMemory, path: '/agents/unsupported-effort.md' });
281+
await fileService.writeFile(missingUri, VSBuffer.fromString([
282+
'---',
283+
'name: missing-effort',
284+
'---',
285+
'Body.',
286+
].join('\n')));
287+
await fileService.writeFile(unsupportedUri, VSBuffer.fromString([
288+
'---',
289+
'name: unsupported-effort',
290+
'reasoning-effort: extreme',
291+
'---',
292+
'Body.',
293+
].join('\n')));
294+
295+
const result = await toSdkCustomAgents([
296+
{ uri: missingUri, name: 'missing-effort' },
297+
{ uri: unsupportedUri, name: 'unsupported-effort' },
298+
], fileService);
299+
300+
assert.deepStrictEqual(result, [
301+
{ name: 'missing-effort', tools: null, prompt: 'Body.' },
302+
{ name: 'unsupported-effort', tools: null, prompt: 'Body.' },
303+
]);
304+
});
305+
253306
test('parses skills and infer from frontmatter', async () => {
254307
const agentUri = URI.from({ scheme: Schemas.inMemory, path: '/agents/skilled.md' });
255308
await fileService.writeFile(agentUri, VSBuffer.fromString([

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ export const customAgentAttributes: Record<string, IAttributeDefinition> = {
120120
type: 'scalar | sequence',
121121
description: localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'),
122122
},
123+
[PromptHeaderAttributes.reasoningEffort]: {
124+
type: 'scalar',
125+
description: localize('promptHeader.agent.reasoningEffort', 'Specify the reasoning effort used by this custom agent.'),
126+
enums: [
127+
{ name: 'low' },
128+
{ name: 'medium' },
129+
{ name: 'high' },
130+
{ name: 'xhigh' },
131+
{ name: 'max' },
132+
],
133+
},
123134
[PromptHeaderAttributes.tools]: {
124135
type: 'scalar | sequence',
125136
description: localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'),

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,11 +1058,11 @@ function isTrueOrFalse(value: IValue): boolean {
10581058
const allAttributeNames: Record<PromptsType, string[]> = {
10591059
[PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint],
10601060
[PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent],
1061-
[PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.hooks, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github],
1061+
[PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.reasoningEffort, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.hooks, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github],
10621062
[PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.disableModelInvocation, PromptHeaderAttributes.context],
10631063
[PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter
10641064
};
1065-
const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, GithubPromptHeaderAttributes.github, PromptHeaderAttributes.infer];
1065+
const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, PromptHeaderAttributes.model, PromptHeaderAttributes.reasoningEffort, GithubPromptHeaderAttributes.mcpServers, GithubPromptHeaderAttributes.github, PromptHeaderAttributes.infer];
10661066
const recommendedAttributeNames: Record<PromptsType, string[]> = {
10671067
[PromptsType.prompt]: allAttributeNames[PromptsType.prompt].filter(name => !isNonRecommendedAttribute(name)),
10681068
[PromptsType.instructions]: allAttributeNames[PromptsType.instructions].filter(name => !isNonRecommendedAttribute(name)),

src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export namespace PromptHeaderAttributes {
6868
export const agent = 'agent';
6969
export const mode = 'mode';
7070
export const model = 'model';
71+
export const reasoningEffort = 'reasoning-effort';
7172
export const applyTo = 'applyTo';
7273
export const paths = 'paths';
7374
export const tools = 'tools';

src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ suite('PromptHeaderAutocompletion', () => {
145145
{ label: 'hooks', result: 'hooks:\n ${1|SessionStart,SessionEnd,UserPromptSubmit,PreToolUse,PostToolUse,PreCompact,SubagentStart,SubagentStop,Stop,ErrorOccurred|}:\n - type: command\n command: "$2"' },
146146
{ label: 'model', result: 'model: ${0:MAE 4 (olama)}' },
147147
{ label: 'name', result: 'name: $0' },
148+
{ label: 'reasoning-effort', result: 'reasoning-effort: ${0:low}' },
148149
{ label: 'target', result: 'target: ${0:vscode}' },
149150
{ label: 'tools', result: 'tools: ${0:[]}' },
150151
{ label: 'user-invocable', result: 'user-invocable: ${0:true}' },
@@ -167,6 +168,24 @@ suite('PromptHeaderAutocompletion', () => {
167168
].sort(sortByLabel));
168169
});
169170

171+
test('complete reasoning effort attribute value', async () => {
172+
const content = [
173+
'---',
174+
'description: "Test"',
175+
'reasoning-effort: |',
176+
'---',
177+
].join('\n');
178+
179+
const actual = await getCompletions(content, PromptsType.agent);
180+
assert.deepStrictEqual(actual.sort(sortByLabel), [
181+
{ label: 'high', result: 'reasoning-effort: high' },
182+
{ label: 'low', result: 'reasoning-effort: low' },
183+
{ label: 'max', result: 'reasoning-effort: max' },
184+
{ label: 'medium', result: 'reasoning-effort: medium' },
185+
{ label: 'xhigh', result: 'reasoning-effort: xhigh' },
186+
].sort(sortByLabel));
187+
});
188+
170189
test('complete model attribute value with partial input', async () => {
171190
const content = [
172191
'---',

src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,17 @@ suite('PromptValidator', () => {
546546
);
547547
});
548548

549+
test('reasoning effort is supported in agent file', async () => {
550+
const content = [
551+
'---',
552+
'description: "Test"',
553+
'reasoning-effort: low',
554+
'---',
555+
].join('\n');
556+
const markers = await validate(content, PromptsType.agent);
557+
assert.deepStrictEqual(markers, []);
558+
});
559+
549560
test('unknown attribute in agent file', async () => {
550561
const content = [
551562
'---',
@@ -557,7 +568,7 @@ suite('PromptValidator', () => {
557568
assert.deepStrictEqual(
558569
markers.map(m => ({ severity: m.severity, message: m.message, tags: m.tags })),
559570
[
560-
{ severity: MarkerSeverity.Hint, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, hooks, model, name, target, tools, user-invocable.`, tags: [MarkerTag.Unnecessary] },
571+
{ severity: MarkerSeverity.Hint, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, hooks, model, name, reasoning-effort, target, tools, user-invocable.`, tags: [MarkerTag.Unnecessary] },
561572
]
562573
);
563574
});
@@ -705,13 +716,14 @@ suite('PromptValidator', () => {
705716
assert.deepStrictEqual(markers, [], 'Expected no validation issues for github-copilot target');
706717
});
707718

708-
test('github-copilot agent warns about model and handoffs attributes', async () => {
719+
test('github-copilot agent warns about handoffs attribute', async () => {
709720
const content = [
710721
'---',
711722
'name: "GitHubAgent"',
712723
'description: "GitHub Copilot agent"',
713724
'target: github-copilot',
714725
'model: MAE 4.1',
726+
'reasoning-effort: high',
715727
`tools: ['shell', 'edit']`,
716728
`handoffs:`,
717729
' - label: Test',
@@ -723,9 +735,8 @@ suite('PromptValidator', () => {
723735
const markers = await validate(content, PromptsType.agent);
724736
const messages = markers.map(m => m.message);
725737
assert.deepStrictEqual(messages, [
726-
'Attribute \'model\' is not supported in custom GitHub Copilot agent files. Supported: description, github, infer, mcp-servers, name, target, tools.',
727-
'Attribute \'handoffs\' is not supported in custom GitHub Copilot agent files. Supported: description, github, infer, mcp-servers, name, target, tools.',
728-
], 'Model and handoffs are not validated for github-copilot target');
738+
'Attribute \'handoffs\' is not supported in custom GitHub Copilot agent files. Supported: description, github, infer, mcp-servers, model, name, reasoning-effort, target, tools.',
739+
], 'Only handoffs is unsupported for github-copilot target, model and reasoning-effort are supported');
729740
});
730741

731742
test('github-copilot agent does not validate variable references', async () => {

0 commit comments

Comments
 (0)