From 5a1c427d6a128bd2fea0bb4f88d092bdd4d9292c Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 13 May 2026 16:27:25 +0800 Subject: [PATCH] feat(subagents): use fastModel for Explore subagent (#4086) Adds a "fast" keyword to the subagent model selector. When set, the runtime resolves it via Config.getFastModel() under the parent authType, falling back to inheriting the parent model when fastModel is unset or invalid for the current authType. The built-in Explore subagent opts in, so a configured fastModel automatically powers codebase searches without affecting other subagents or the main session. --- packages/core/src/subagents/builtin-agents.ts | 1 + .../src/subagents/model-selection.test.ts | 25 ++++++++ .../core/src/subagents/model-selection.ts | 11 ++++ .../src/subagents/subagent-manager.test.ts | 63 +++++++++++++++++++ .../core/src/subagents/subagent-manager.ts | 31 ++++++++- packages/core/src/subagents/types.ts | 2 + 6 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/core/src/subagents/builtin-agents.ts b/packages/core/src/subagents/builtin-agents.ts index 74102c80a..93326472e 100644 --- a/packages/core/src/subagents/builtin-agents.ts +++ b/packages/core/src/subagents/builtin-agents.ts @@ -54,6 +54,7 @@ Notes: name: 'Explore', description: 'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.', + model: 'fast', systemPrompt: `You are a file search specialist agent. You excel at thoroughly navigating and exploring codebases. === CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS === diff --git a/packages/core/src/subagents/model-selection.test.ts b/packages/core/src/subagents/model-selection.test.ts index ce7c5ca96..dfee6c575 100644 --- a/packages/core/src/subagents/model-selection.test.ts +++ b/packages/core/src/subagents/model-selection.test.ts @@ -49,4 +49,29 @@ describe('parseSubagentModelSelection', () => { inherits: false, }); }); + + it('parses the fast keyword', () => { + expect(parseSubagentModelSelection('fast')).toEqual({ + inherits: false, + usesFastModel: true, + }); + }); + + it('parses the fast keyword with surrounding whitespace', () => { + expect(parseSubagentModelSelection(' fast ')).toEqual({ + inherits: false, + usesFastModel: true, + }); + }); + + it('treats model IDs that merely contain "fast" as bare IDs, not the keyword', () => { + expect(parseSubagentModelSelection('qwen3-coder-flash')).toEqual({ + modelId: 'qwen3-coder-flash', + inherits: false, + }); + expect(parseSubagentModelSelection('Fast')).toEqual({ + modelId: 'Fast', + inherits: false, + }); + }); }); diff --git a/packages/core/src/subagents/model-selection.ts b/packages/core/src/subagents/model-selection.ts index f9c909f78..8cd540a47 100644 --- a/packages/core/src/subagents/model-selection.ts +++ b/packages/core/src/subagents/model-selection.ts @@ -10,6 +10,12 @@ export interface ParsedSubagentModelSelection { authType?: AuthType; modelId?: string; inherits: boolean; + /** + * True when the selector was `fast` — the runtime resolves this to + * `Config.getFastModel()` if a valid fast model is configured, and + * falls back to inheriting the parent model otherwise. + */ + usesFastModel?: boolean; } const AUTH_TYPES = new Set(Object.values(AuthType)); @@ -19,6 +25,7 @@ const AUTH_TYPES = new Set(Object.values(AuthType)); * * Supported forms: * - omitted / inherit -> use parent conversation model + * - fast -> use Config.getFastModel() if available, else inherit parent model * - modelId -> use parent authType with the provided modelId * - authType:modelId -> use explicit authType and modelId */ @@ -30,6 +37,10 @@ export function parseSubagentModelSelection( return { inherits: true }; } + if (trimmed === 'fast') { + return { inherits: false, usesFastModel: true }; + } + const colonIndex = trimmed.indexOf(':'); if (colonIndex === -1) { return { modelId: trimmed, inherits: false }; diff --git a/packages/core/src/subagents/subagent-manager.test.ts b/packages/core/src/subagents/subagent-manager.test.ts index 4c7c53a36..ea64aeb14 100644 --- a/packages/core/src/subagents/subagent-manager.test.ts +++ b/packages/core/src/subagents/subagent-manager.test.ts @@ -1397,6 +1397,40 @@ System prompt 3`); ); expect(runtimeConfig.modelConfig.model).toBe('gpt-4'); }); + + it('should resolve "fast" to Config.getFastModel() when one is configured', async () => { + const fastConfig: SubagentConfig = { ...validConfig, model: 'fast' }; + vi.spyOn(mockConfig, 'getFastModel').mockReturnValue('fast-model-id'); + + const runtimeConfig = await manager.convertToRuntimeConfig( + fastConfig, + mockConfig, + ); + + expect(runtimeConfig.modelConfig.model).toBe('fast-model-id'); + }); + + it('should leave modelConfig empty for "fast" when getFastModel returns undefined', async () => { + // Mirrors the unset / invalid-for-authType cases — AgentCore then + // falls back to runtimeContext.getModel() (the parent model). + const fastConfig: SubagentConfig = { ...validConfig, model: 'fast' }; + vi.spyOn(mockConfig, 'getFastModel').mockReturnValue(undefined); + + const runtimeConfig = await manager.convertToRuntimeConfig( + fastConfig, + mockConfig, + ); + + expect(runtimeConfig.modelConfig).toEqual({}); + }); + + it('should leave modelConfig empty for "fast" when no runtimeContext is provided', async () => { + const fastConfig: SubagentConfig = { ...validConfig, model: 'fast' }; + + const runtimeConfig = await manager.convertToRuntimeConfig(fastConfig); + + expect(runtimeConfig.modelConfig).toEqual({}); + }); }); describe('mergeConfigurations', () => { @@ -1525,6 +1559,35 @@ System prompt 3`); expect(runtimeView!.contentGenerator).toBe(fakeGenerator); expect(runtimeView!.contentGeneratorConfig.model).toBe('custom-model'); }); + + it('should build a ContentGenerator with the resolved fastModel when model is "fast"', async () => { + const config = { ...agentConfig, model: 'fast' }; + vi.spyOn(mockConfig, 'getFastModel').mockReturnValue('fast-model-id'); + + await manager.createAgentHeadless(config, mockConfig); + + expect(mockCreateContentGenerator).toHaveBeenCalledWith( + expect.objectContaining({ + model: 'fast-model-id', + authType: AuthType.USE_OPENAI, + }), + mockConfig, + ); + }); + + it('should NOT build a new ContentGenerator for "fast" when getFastModel returns undefined', async () => { + const config = { ...agentConfig, model: 'fast' }; + vi.spyOn(mockConfig, 'getFastModel').mockReturnValue(undefined); + + await manager.createAgentHeadless(config, mockConfig); + + // Falls back to inheriting the parent — no override, no runtimeView. + expect(mockCreateContentGenerator).not.toHaveBeenCalled(); + const { runtimeView } = destructureAgentHeadlessCall( + mockAgentHeadlessCreate.mock.calls[0], + ); + expect(runtimeView).toBeUndefined(); + }); }); }); }); diff --git a/packages/core/src/subagents/subagent-manager.ts b/packages/core/src/subagents/subagent-manager.ts index 4165ace60..b58e27697 100644 --- a/packages/core/src/subagents/subagent-manager.ts +++ b/packages/core/src/subagents/subagent-manager.ts @@ -638,7 +638,10 @@ export class SubagentManager { }, ): Promise { try { - const runtimeConfig = await this.convertToRuntimeConfig(config); + const runtimeConfig = await this.convertToRuntimeConfig( + config, + runtimeContext, + ); const promptConfig: PromptConfig = { ...runtimeConfig.promptConfig, ...options?.promptConfigOverrides, @@ -741,6 +744,20 @@ export class SubagentManager { return undefined; } + let resolvedModelId = selection.modelId; + if (selection.usesFastModel) { + // getFastModel() returns the fastModel id only when it's valid for + // the current authType; otherwise undefined. Treat undefined as + // inherit so an unset or invalid fastModel silently falls back to + // the parent session model — matching every other getFastModel() + // call site (ForkedAgent, sessionTitle, etc.). + const fast = base.getFastModel(); + if (!fast) { + return undefined; + } + resolvedModelId = fast; + } + const authType = selection.authType ?? base.getContentGeneratorConfig().authType; const authOverrides = { @@ -750,7 +767,7 @@ export class SubagentManager { const view = await createRuntimeContentGeneratorView( base, base, - selection.modelId, + resolvedModelId, authOverrides, ); @@ -770,14 +787,22 @@ export class SubagentManager { */ async convertToRuntimeConfig( config: SubagentConfig, + runtimeContext?: Config, ): Promise { const promptConfig: PromptConfig = { systemPrompt: config.systemPrompt, }; const selection = parseSubagentModelSelection(config.model); + let resolvedModelId = selection.modelId; + if (selection.usesFastModel && runtimeContext) { + // Resolve `fast` to the configured fastModel. Undefined here means + // either unset or invalid for the current authType — leave modelConfig + // empty so the agent inherits the parent model (same as `inherit`). + resolvedModelId = runtimeContext.getFastModel(); + } const modelConfig: ModelConfig = { - ...(selection.modelId ? { model: selection.modelId } : {}), + ...(resolvedModelId ? { model: resolvedModelId } : {}), }; const runConfig: RunConfig = { diff --git a/packages/core/src/subagents/types.ts b/packages/core/src/subagents/types.ts index febfafe49..216390520 100644 --- a/packages/core/src/subagents/types.ts +++ b/packages/core/src/subagents/types.ts @@ -83,6 +83,8 @@ export interface SubagentConfig { /** * Optional model selector. * - Omitted or 'inherit': use the main conversation model + * - 'fast': use Config.getFastModel() under the parent authType when + * configured and valid; silently inherit the parent model otherwise * - 'model-id': use the given model with the main conversation authType * - 'authType:model-id': use the given authType and model ID */