diff --git a/packages/cli/src/utils/cleanup.ts b/packages/cli/src/utils/cleanup.ts index ccb467572b5..da9ff927ef5 100644 --- a/packages/cli/src/utils/cleanup.ts +++ b/packages/cli/src/utils/cleanup.ts @@ -65,6 +65,14 @@ export async function runExitCleanup() { } cleanupFunctions.length = 0; // Clear the array + if (configForTelemetry) { + try { + await configForTelemetry.dispose(); + } catch (_) { + // Ignore errors during disposal + } + } + // IMPORTANT: Shutdown telemetry AFTER all other cleanup functions have run // This ensures SessionEnd hooks and other telemetry are properly flushed if (configForTelemetry && isTelemetrySdkInitialized()) { diff --git a/packages/core/src/agents/cli-help-agent.test.ts b/packages/core/src/agents/cli-help-agent.test.ts index b1552f3d62b..579dd58d723 100644 --- a/packages/core/src/agents/cli-help-agent.test.ts +++ b/packages/core/src/agents/cli-help-agent.test.ts @@ -14,6 +14,7 @@ import type { Config } from '../config/config.js'; describe('CliHelpAgent', () => { const fakeConfig = { getMessageBus: () => ({}), + isAgentsEnabled: () => false, } as unknown as Config; const localAgent = CliHelpAgent(fakeConfig) as LocalAgentDefinition; @@ -52,6 +53,22 @@ describe('CliHelpAgent', () => { expect(query).toContain('${question}'); }); + it('should include sub-agent information when agents are enabled', () => { + const enabledConfig = { + getMessageBus: () => ({}), + isAgentsEnabled: () => true, + getAgentRegistry: () => ({ + getDirectoryContext: () => 'Mock Agent Directory', + }), + } as unknown as Config; + const agent = CliHelpAgent(enabledConfig) as LocalAgentDefinition; + const systemPrompt = agent.promptConfig.systemPrompt || ''; + + expect(systemPrompt).toContain('### Sub-Agents (Local & Remote)'); + expect(systemPrompt).toContain('Remote Agent (A2A)'); + expect(systemPrompt).toContain('Agent2Agent functionality'); + }); + it('should process output to a formatted JSON string', () => { const mockOutput = { answer: 'This is the answer.', diff --git a/packages/core/src/agents/cli-help-agent.ts b/packages/core/src/agents/cli-help-agent.ts index 331be120e99..a1909454bf5 100644 --- a/packages/core/src/agents/cli-help-agent.ts +++ b/packages/core/src/agents/cli-help-agent.ts @@ -76,6 +76,12 @@ export const CliHelpAgent = ( '- **CLI Version:** ${cliVersion}\n' + '- **Active Model:** ${activeModel}\n' + "- **Today's Date:** ${today}\n\n" + + (config.isAgentsEnabled() + ? '### Sub-Agents (Local & Remote)\n' + + 'User defined sub-agents are defined in `.gemini/agents/` or `~/.gemini/agents/` using YAML frontmatter for metadata and Markdown for instructions (system_prompt). Always reference the types and properties outlined here directly when answering questions about sub-agents.\n' + + '- **Local Agent:** `kind = "local"`, `name`, `description`, `prompts.system_prompt`, and optional `tools`, `model`, `run`.\n' + + '- **Remote Agent (A2A):** `kind = "remote"`, `name`, `agent_card_url`. Multiple remotes can be defined using a `remote_agents` array. **Note:** When users ask about "remote agents", they are referring to this Agent2Agent functionality, which is completely distinct from MCP servers.\n\n' + : '') + '### Instructions\n' + "1. **Explore Documentation**: Use the `get_internal_docs` tool to find answers. If you don't know where to start, call `get_internal_docs()` without arguments to see the full list of available documentation files.\n" + '2. **Be Precise**: Use the provided runtime context and documentation to give exact answers.\n' + diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 8a35a70241f..163da8f2a25 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -46,18 +46,20 @@ export class AgentRegistry { * Discovers and loads agents. */ async initialize(): Promise { - coreEvents.on(CoreEvent.ModelChanged, () => { - this.refreshAgents().catch((e) => { - debugLogger.error( - '[AgentRegistry] Failed to refresh agents on model change:', - e, - ); - }); - }); + coreEvents.on(CoreEvent.ModelChanged, this.onModelChanged); await this.loadAgents(); } + private onModelChanged = () => { + this.refreshAgents().catch((e) => { + debugLogger.error( + '[AgentRegistry] Failed to refresh agents on model change:', + e, + ); + }); + }; + /** * Clears the current registry and re-scans for agents. */ @@ -68,6 +70,13 @@ export class AgentRegistry { coreEvents.emitAgentsRefreshed(); } + /** + * Disposes of resources and removes event listeners. + */ + dispose(): void { + coreEvents.off(CoreEvent.ModelChanged, this.onModelChanged); + } + private async loadAgents(): Promise { this.loadBuiltInAgents(); diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 8e1c9d9b68d..2ccbb4c546a 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -167,13 +167,18 @@ const mockCoreEvents = vi.hoisted(() => ({ emitFeedback: vi.fn(), emitModelChanged: vi.fn(), emitConsoleLog: vi.fn(), + on: vi.fn(), })); const mockSetGlobalProxy = vi.hoisted(() => vi.fn()); -vi.mock('../utils/events.js', () => ({ - coreEvents: mockCoreEvents, -})); +vi.mock('../utils/events.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + coreEvents: mockCoreEvents, + }; +}); vi.mock('../utils/fetch.js', () => ({ setGlobalProxy: mockSetGlobalProxy, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 60783cffabe..241de0d04fe 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -43,7 +43,7 @@ import { DEFAULT_OTLP_ENDPOINT, uiTelemetryService, } from '../telemetry/index.js'; -import { coreEvents } from '../utils/events.js'; +import { coreEvents, CoreEvent } from '../utils/events.js'; import { tokenLimit } from '../core/tokenLimits.js'; import { DEFAULT_GEMINI_EMBEDDING_MODEL, @@ -735,6 +735,8 @@ export class Config { this.agentRegistry = new AgentRegistry(this); await this.agentRegistry.initialize(); + coreEvents.on(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); + this.toolRegistry = await this.createToolRegistry(); discoverToolsHandle?.end(); this.mcpClientManager = new McpClientManager( @@ -1764,6 +1766,17 @@ export class Config { // Register Subagents as Tools // Register DelegateToAgentTool if agents are enabled + this.registerDelegateToAgentTool(registry); + + await registry.discoverAllTools(); + registry.sortTools(); + return registry; + } + + /** + * Registers the DelegateToAgentTool if agents or related features are enabled. + */ + private registerDelegateToAgentTool(registry: ToolRegistry): void { if ( this.isAgentsEnabled() || this.getCodebaseInvestigatorSettings().enabled || @@ -1783,10 +1796,6 @@ export class Config { registry.registerTool(delegateTool); } } - - await registry.discoverAllTools(); - registry.sortTools(); - return registry; } /** @@ -1870,6 +1879,35 @@ export class Config { }); debugLogger.debug('Experiments loaded', summaryString); } + + private onAgentsRefreshed = async () => { + if (this.toolRegistry) { + this.registerDelegateToAgentTool(this.toolRegistry); + } + // Propagate updates to the active chat session + const client = this.getGeminiClient(); + if (client?.isInitialized()) { + await client.setTools(); + await client.updateSystemInstruction(); + } else { + debugLogger.debug( + '[Config] GeminiClient not initialized; skipping live prompt/tool refresh.', + ); + } + }; + + /** + * Disposes of resources and removes event listeners. + */ + async dispose(): Promise { + coreEvents.off(CoreEvent.AgentsRefreshed, this.onAgentsRefreshed); + if (this.agentRegistry) { + this.agentRegistry.dispose(); + } + if (this.mcpClientManager) { + await this.mcpClientManager.stop(); + } + } } // Export model constants for use in CLI export { DEFAULT_GEMINI_FLASH_MODEL }; diff --git a/packages/core/src/core/prompts.test.ts b/packages/core/src/core/prompts.test.ts index 1ecdcc27760..039453ca12f 100644 --- a/packages/core/src/core/prompts.test.ts +++ b/packages/core/src/core/prompts.test.ts @@ -66,6 +66,7 @@ describe('Core System Prompt (prompts.ts)', () => { }, isInteractive: vi.fn().mockReturnValue(true), isInteractiveShellEnabled: vi.fn().mockReturnValue(true), + isAgentsEnabled: vi.fn().mockReturnValue(false), getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO), getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL), getPreviewFeatures: vi.fn().mockReturnValue(false), @@ -214,6 +215,7 @@ describe('Core System Prompt (prompts.ts)', () => { }, isInteractive: vi.fn().mockReturnValue(false), isInteractiveShellEnabled: vi.fn().mockReturnValue(false), + isAgentsEnabled: vi.fn().mockReturnValue(false), getModel: vi.fn().mockReturnValue('auto'), getActiveModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL), getPreviewFeatures: vi.fn().mockReturnValue(false),