feat(chat): add tiers frontmatter and tier parameter to runSubagent tool#306846
feat(chat): add tiers frontmatter and tier parameter to runSubagent tool#306846RockyWearsAHat wants to merge 4 commits intomicrosoft:mainfrom
Conversation
Add support for capability tiers in agent definitions, allowing agents
to declare named model tiers (e.g., fast, standard, deep) in their
frontmatter. The runSubagent tool gains a `tier` parameter that selects
a model from these tiers at call time.
Implementation:
- Add `tiers` attribute to prompt file parser (YAML map of tier names
to `{ model: string }` configurations)
- Add `tiers` to ICustomAgent interface and service implementation
- Register `tiers` in prompt validator and attribute definitions
- Add `tier` parameter to runSubagent tool schema (gated behind
SubagentToolCustomAgents config, alongside `model`)
- Extend resolveSubagentModel() with 4-level priority:
model param > tier param > agent frontmatter > parent model
- Add resolveModelHint() helper for model string resolution
- Multiplier-based cost cap applies uniformly to all resolution paths
Tests:
- Schema: model and tier properties present when custom agents enabled
- Model param: override via qualified name, direct ID, multiplier cap,
unknown model fallback, precedence over tier
- Tier resolution: correct model from tiers map, overrides agent default,
multiplier cap, unknown tier fallback, no-agent ignored, no-tiers
agent fallback
Resolves microsoft#306717
The tool's modelDescription now instructs the orchestrating agent to assess subtask complexity before choosing a model, use the cheapest model that can reliably complete each task, and prefer the latest version within each model family. Updated parameter descriptions for `model` and `tier` to include concrete cost-matching guidance instead of just listing examples. This addresses the behavior where agents default to using their own (often expensive) model for all subagent invocations regardless of subtask complexity.
There was a problem hiding this comment.
Pull request overview
Adds capability-tier model routing for custom agents by introducing tiers in agent frontmatter and supporting call-time model/tier overrides on the runSubagent tool, with updated validation and tests.
Changes:
- Parse and surface
tiersfrom agent YAML frontmatter through the prompts service layer. - Extend
runSubagenttool input schema + resolution logic to support call-timemodelandtierselection with multiplier-based cost capping. - Add comprehensive unit tests for call-time model override and tier-based resolution behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts | Adds model/tier params, implements new model resolution priority + cost cap, and updates tool schema. |
| src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts | Adds new suites covering call-time model override and tier resolution, plus helper updates. |
| src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts | Extends ICustomAgent with optional tiers metadata. |
| src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts | Plumbs parsed tiers from AST header into the returned custom agent objects. |
| src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts | Adds tiers attribute constant + frontmatter parsing getter on PromptHeader. |
| src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts | Allows tiers for agent frontmatter and adds localized description text. |
| src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptFileAttributes.ts | Registers tiers as an allowed custom agent attribute of type map. |
| const mainMultiplier = mainModelMetadata?.multiplierNumeric; | ||
| const resolvedMultiplier = resolvedModelMetadata?.multiplierNumeric; | ||
| if (mainMultiplier !== undefined && resolvedMultiplier !== undefined && resolvedMultiplier > mainMultiplier) { | ||
| const source = callTimeModelHint ? 'Call-time' : callTimeTier ? `Tier '${callTimeTier}'` : subagent ? `Subagent '${subagent.name}'` : 'Unknown'; | ||
| this.logService.warn(`[RunSubagentTool] ${source} requested model '${resolvedModelMetadata?.name}' (multiplier: ${resolvedMultiplier}) which exceeds the main agent model '${mainModelMetadata?.name}' (multiplier: ${mainMultiplier}). Falling back to the main agent model.`); |
There was a problem hiding this comment.
The multiplier-cap warning can misattribute the source of the resolved model. If callTimeModelHint/callTimeTier is provided but fails to resolve (and the agent/default model ends up being the one capped), source will still report "Call-time"/"Tier ..." even though that override was ignored. Track the actual resolution source (e.g., set a variable only when an override successfully changes modeModelId) and use that for the cap warning to avoid misleading logs.
| * Maps tier names (e.g., 'fast', 'standard', 'deep') to model qualified names. | ||
| * Allows a single agent to serve at different capability/cost levels. | ||
| */ |
There was a problem hiding this comment.
The doc comment says tier models are "qualified names", but the rest of this PR (schema examples + resolveModelHint for tiers) accepts either qualified names or direct model IDs. Please update this comment/type docs to reflect the accepted format so agent authors aren’t misled.
| * Maps tier names (e.g., 'fast', 'standard', 'deep') to model qualified names. | |
| * Allows a single agent to serve at different capability/cost levels. | |
| */ | |
| * Maps tier names (e.g., 'fast', 'standard', 'deep') to models, which may be | |
| * either qualified model names or direct model IDs. Allows a single agent to | |
| * serve at different capability/cost levels. |
| function createAgent(name: string, modelQualifiedNames?: string[], tiers?: Record<string, { model: string }>): ICustomAgent { | ||
| return { | ||
| uri: URI.parse(`file:///test/${name}.md`), | ||
| name, | ||
| description: `Agent ${name}`, | ||
| tools: ['tool1'], | ||
| model: modelQualifiedNames, | ||
| tiers, | ||
| agentInstructions: { content: 'test', toolReferences: [] }, | ||
| source: { storage: PromptsStorage.local }, | ||
| target: Target.Undefined, | ||
| visibility: { userInvocable: true, agentInvocable: true } | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
This test file now has multiple near-identical helpers (createTool, createAgent, createMetadata) redefined in different suites (and there’s already a top-level createAgent). Consider reusing the existing helpers (or factoring shared helpers to the outer scope) to reduce duplication and future maintenance overhead.
| function createAgent(name: string, modelQualifiedNames?: string[], tiers?: Record<string, { model: string }>): ICustomAgent { | |
| return { | |
| uri: URI.parse(`file:///test/${name}.md`), | |
| name, | |
| description: `Agent ${name}`, | |
| tools: ['tool1'], | |
| model: modelQualifiedNames, | |
| tiers, | |
| agentInstructions: { content: 'test', toolReferences: [] }, | |
| source: { storage: PromptsStorage.local }, | |
| target: Target.Undefined, | |
| visibility: { userInvocable: true, agentInvocable: true } | |
| }; | |
| } |
Add structural validation for the new `tiers` attribute so agent prompts must provide a map of named tiers with string `model` entries, warn when the model is unknown, and flag stray properties when VS Code targets custom agents. Refactor the runSubagent tool tests to share the metadata, agent, and tool factory helpers (with an opt-in to custom agents), which cuts redundant copies across model/tier suites. This checkpoint keeps the tier feature consistent between prompt parsing and runtime coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tier validation now checks both qualified names and direct model IDs before warning about unknown models, matching resolveModelHint’s runtime logic. This keeps prompt parsing aligned with how tiers are actually resolved at execution time, preventing false warnings when authors specify direct IDs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| const cached = this._resolvedModels.get(invocation.callId); | ||
| if (cached) { | ||
| this._resolvedModels.delete(invocation.callId); | ||
| modeModelId = cached.modeModelId; | ||
| resolvedModelName = cached.resolvedModelName; | ||
| } else { | ||
| // Fallback: resolve the model here if prepare didn't cache it | ||
| const resolved = this.resolveSubagentModel(subagent, invocation.modelId); | ||
| const resolved = this.resolveSubagentModel(subagent, invocation.modelId, args.model, args.tier); | ||
| modeModelId = resolved.modeModelId; |
There was a problem hiding this comment.
When a cached model resolution is present, modeModelId is assigned directly from cached.modeModelId. Because IToolInvocationPreparationContext.modelId is optional, the cached entry can legitimately contain undefined even when invocation.modelId is defined at invoke-time. This can unintentionally clear modeModelId and cause the subagent to run without an explicit model selection. Consider falling back to the current modeModelId (or invocation.modelId) when the cached value is undefined, similar to the no-subagent branch.
| if (modeModelId && modeModelId !== mainModelId) { | ||
| const mainModelMetadata = mainModelId ? this.languageModelsService.lookupLanguageModel(mainModelId) : undefined; | ||
| const resolvedModelMetadata = this.languageModelsService.lookupLanguageModel(modeModelId); | ||
| const mainMultiplier = mainModelMetadata?.multiplierNumeric; | ||
| const resolvedMultiplier = resolvedModelMetadata?.multiplierNumeric; | ||
| if (mainMultiplier !== undefined && resolvedMultiplier !== undefined && resolvedMultiplier > mainMultiplier) { | ||
| const source = callTimeModelHint ? 'Call-time' : callTimeTier ? `Tier '${callTimeTier}'` : subagent ? `Subagent '${subagent.name}'` : 'Unknown'; | ||
| this.logService.warn(`[RunSubagentTool] ${source} requested model '${resolvedModelMetadata?.name}' (multiplier: ${resolvedMultiplier}) which exceeds the main agent model '${mainModelMetadata?.name}' (multiplier: ${mainMultiplier}). Falling back to the main agent model.`); | ||
| modeModelId = mainModelId; |
There was a problem hiding this comment.
The multiplier-cap warning message can misattribute the source of the resolved model. source is derived from whether callTimeModelHint/callTimeTier are set, even if the call-time model hint failed to resolve (and the final modeModelId came from the tier or agent default). This can produce misleading logs. Track the last successful resolution source (e.g., set a variable only when an override actually changes modeModelId) and use that for the warning.
| this._resolvedModels.delete(invocation.callId); | ||
| modeModelId = cached.modeModelId ?? modeModelId; | ||
| resolvedModelName = cached.resolvedModelName; | ||
| } else if (args.model) { | ||
| // Call-time model override without a named subagent | ||
| const resolved = this.resolveSubagentModel(undefined, invocation.modelId, args.model); | ||
| modeModelId = resolved.modeModelId; | ||
| resolvedModelName = resolved.resolvedModelName; | ||
| } else { |
There was a problem hiding this comment.
args.model is applied as a call-time override even when ChatConfiguration.SubagentToolCustomAgents is disabled. Since model/tier are introduced as parameters gated behind that config in getToolData(), it would be safer and more consistent to also ignore these parameters at runtime (both here and in prepareToolInvocation) unless the config flag is enabled. This prevents bypass via manually constructed tool invocations and keeps behavior aligned with the tool schema.
Summary
Adds support for capability tiers in agent definitions (
tiersfrontmatter) and atierparameter on therunSubagenttool, enabling semantic model selection at call time.Resolves #306717
Related: #306836 / PR #306841 (call-time
modelparameter)What Changed
Prompt file parser (
promptFileParser.ts)tiersconstant inPromptHeaderAttributesget tiers()getter onPromptHeader— parses YAML map of{ tierName: { model: qualifiedName } }Agent interface & service (
promptsService.ts,promptsServiceImpl.ts)tiers?: Readonly<Record<string, { readonly model: string }>>toICustomAgentPrompt validation (
promptValidator.ts,promptFileAttributes.ts)tiersregistered as allowed agent attribute (type:map)RunSubagent tool (
runSubagentTool.ts)model?: stringandtier?: stringtoIRunSubagentToolInputParamsinterfaceSubagentToolCustomAgentsconfig flagresolveSubagentModel()rewritten with 4-level resolution priority:modelparameter (highest)tierparameter (looks upagent.tiers[tier].model)model:frontmatterresolveModelHint()helper: tries qualified name → direct ID → undefinedTests (
runSubagentTool.test.ts)createAgent()helper extended to accepttiersparametercall-time model parametersuite (6 tests): schema, override via qualified name, override via direct ID, multiplier cap, unknown model fallback, precedence over tiertier resolutionsuite (7 tests): correct model from tiers map, overrides agent default, multiplier cap, unknown tier fallback, no-agent ignored, no-tiers agent fallback, tier-with-tiers-definedExample Agent Frontmatter