fix(core): ensure sub-agent schema and prompt refresh during runtime (#16409)

Co-authored-by: Sehoon Shon <sshon@google.com>
This commit is contained in:
Adam Weidman
2026-01-12 12:11:24 -05:00
committed by GitHub
parent 950244f6b0
commit 465ec9759d
7 changed files with 101 additions and 16 deletions

View File

@@ -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()) {

View File

@@ -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.',

View File

@@ -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' +

View File

@@ -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.
*/
@@ -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();

View File

@@ -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<typeof import('../utils/events.js')>();
return {
...actual,
coreEvents: mockCoreEvents,
};
});
vi.mock('../utils/fetch.js', () => ({
setGlobalProxy: mockSetGlobalProxy,

View File

@@ -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<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 };

View File

@@ -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),