Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/cli/src/utils/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/agents/cli-help-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.',
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/agents/cli-help-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand Down
25 changes: 17 additions & 8 deletions packages/core/src/agents/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,20 @@ export class AgentRegistry {
* Discovers and loads agents.
*/
async initialize(): Promise<void> {
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.
*/
Expand All @@ -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<void> {
this.loadBuiltInAgents();

Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,18 @@
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<typeof import('../utils/events.js')>();
return {
...actual,
coreEvents: mockCoreEvents,
};
});

vi.mock('../utils/fetch.js', () => ({
setGlobalProxy: mockSetGlobalProxy,
Expand Down Expand Up @@ -805,7 +810,7 @@
it('should disable useWriteTodos for preview models', () => {
const params: ConfigParameters = {
...baseParams,
model: 'gemini-3-pro-preview',

Check warning on line 813 in packages/core/src/config/config.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Found sensitive keyword "gemini-3". Please make sure this change is appropriate to submit.
};
const config = new Config(params);
expect(config.getUseWriteTodos()).toBe(false);
Expand Down
48 changes: 43 additions & 5 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 ||
Expand All @@ -1783,10 +1796,6 @@ export class Config {
registry.registerTool(delegateTool);
}
}

await registry.discoverAllTools();
registry.sortTools();
return registry;
}

/**
Expand Down Expand Up @@ -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<void> {
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 };
2 changes: 2 additions & 0 deletions packages/core/src/core/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
Loading