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 */