Introspection agent demo (#15232)

This commit is contained in:
Tommaso Sciortino
2025-12-19 14:11:32 -08:00
committed by GitHub
parent db67bb106a
commit 10ba348a3a
17 changed files with 533 additions and 16 deletions
@@ -47,6 +47,7 @@ describe('DelegateToAgentTool', () => {
beforeEach(() => {
config = {
getDebugMode: () => false,
getActiveModel: () => 'test-model',
modelConfigService: {
registerRuntimeModelConfig: vi.fn(),
},
@@ -0,0 +1,59 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { IntrospectionAgent } from './introspection-agent.js';
import { GetInternalDocsTool } from '../tools/get-internal-docs.js';
import { GEMINI_MODEL_ALIAS_FLASH } from '../config/models.js';
import type { LocalAgentDefinition } from './types.js';
describe('IntrospectionAgent', () => {
const localAgent = IntrospectionAgent as LocalAgentDefinition;
it('should have the correct agent definition metadata', () => {
expect(localAgent.name).toBe('introspection_agent');
expect(localAgent.kind).toBe('local');
expect(localAgent.displayName).toBe('Introspection Agent');
expect(localAgent.description).toContain('Gemini CLI');
});
it('should have correctly configured inputs and outputs', () => {
expect(localAgent.inputConfig.inputs['question']).toBeDefined();
expect(localAgent.inputConfig.inputs['question'].required).toBe(true);
expect(localAgent.outputConfig?.outputName).toBe('report');
expect(localAgent.outputConfig?.description).toBeDefined();
});
it('should use the correct model and tools', () => {
expect(localAgent.modelConfig?.model).toBe(GEMINI_MODEL_ALIAS_FLASH);
const tools = localAgent.toolConfig?.tools || [];
const hasInternalDocsTool = tools.some(
(t) => t instanceof GetInternalDocsTool,
);
expect(hasInternalDocsTool).toBe(true);
});
it('should have expected prompt placeholders', () => {
const systemPrompt = localAgent.promptConfig.systemPrompt || '';
expect(systemPrompt).toContain('${cliVersion}');
expect(systemPrompt).toContain('${activeModel}');
expect(systemPrompt).toContain('${today}');
const query = localAgent.promptConfig.query || '';
expect(query).toContain('${question}');
});
it('should process output to a formatted JSON string', () => {
const mockOutput = {
answer: 'This is the answer.',
sources: ['file1.md', 'file2.md'],
};
const processed = localAgent.processOutput?.(mockOutput);
expect(processed).toBe(JSON.stringify(mockOutput, null, 2));
});
});
@@ -0,0 +1,85 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { AgentDefinition } from './types.js';
import { GetInternalDocsTool } from '../tools/get-internal-docs.js';
import { GEMINI_MODEL_ALIAS_FLASH } from '../config/models.js';
import { z } from 'zod';
const IntrospectionReportSchema = z.object({
answer: z
.string()
.describe('The detailed answer to the user question about Gemini CLI.'),
sources: z
.array(z.string())
.describe('The documentation files used to answer the question.'),
});
/**
* An agent specialized in answering questions about Gemini CLI itself,
* using its own documentation and runtime state.
*/
export const IntrospectionAgent: AgentDefinition<
typeof IntrospectionReportSchema
> = {
name: 'introspection_agent',
kind: 'local',
displayName: 'Introspection Agent',
description:
'Specialized in answering questions about yourself (Gemini CLI): features, documentation, and current runtime configuration.',
inputConfig: {
inputs: {
question: {
description: 'The specific question about Gemini CLI.',
type: 'string',
required: true,
},
},
},
outputConfig: {
outputName: 'report',
description: 'The final answer and sources as a JSON object.',
schema: IntrospectionReportSchema,
},
processOutput: (output) => JSON.stringify(output, null, 2),
modelConfig: {
model: GEMINI_MODEL_ALIAS_FLASH,
temp: 0.1,
top_p: 0.95,
thinkingBudget: -1,
},
runConfig: {
max_time_minutes: 3,
max_turns: 10,
},
toolConfig: {
tools: [new GetInternalDocsTool()],
},
promptConfig: {
query:
'Your task is to answer the following question about Gemini CLI:\n' +
'<question>\n' +
'${question}\n' +
'</question>',
systemPrompt:
"You are **Introspection Agent**, an expert on Gemini CLI. Your purpose is to provide accurate information about Gemini CLI's features, configuration, and current state.\n\n" +
'### Runtime Context\n' +
'- **CLI Version:** ${cliVersion}\n' +
'- **Active Model:** ${activeModel}\n' +
"- **Today's Date:** ${today}\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' +
'3. **Cite Sources**: Always include the specific documentation files you used in your final report.\n' +
'4. **Non-Interactive**: You operate in a loop and cannot ask the user for more info. If the question is ambiguous, answer as best as you can with the information available.\n\n' +
'You MUST call `complete_task` with a JSON report containing your `answer` and the `sources` you used.',
},
};
@@ -100,6 +100,10 @@ vi.mock('../core/nonInteractiveToolExecutor.js', () => ({
executeToolCall: mockExecuteToolCall,
}));
vi.mock('../utils/version.js', () => ({
getVersion: vi.fn().mockResolvedValue('1.2.3'),
}));
vi.mock('../utils/environmentContext.js');
vi.mock('../telemetry/loggers.js', () => ({
+15 -13
View File
@@ -44,6 +44,7 @@ import { type z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { debugLogger } from '../utils/debugLogger.js';
import { getModelConfigAlias } from './registry.js';
import { getVersion } from '../utils/version.js';
import { ApprovalMode } from '../policy/types.js';
/** A callback function to report on agent activity. */
@@ -209,7 +210,6 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
const { nextMessage, submittedOutput, taskCompleted } =
await this.processFunctionCalls(functionCalls, combinedSignal, promptId);
if (taskCompleted) {
const finalResult = submittedOutput ?? 'Task completed successfully.';
return {
@@ -373,10 +373,18 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
let chat: GeminiChat | undefined;
let tools: FunctionDeclaration[] | undefined;
try {
// Inject standard runtime context into inputs
const augmentedInputs = {
...inputs,
cliVersion: await getVersion(),
activeModel: this.runtimeContext.getActiveModel(),
today: new Date().toLocaleDateString(),
};
tools = this.prepareToolsList();
chat = await this.createChatObject(inputs, tools);
chat = await this.createChatObject(augmentedInputs, tools);
const query = this.definition.promptConfig.query
? templateString(this.definition.promptConfig.query, inputs)
? templateString(this.definition.promptConfig.query, augmentedInputs)
: 'Get Started!';
let currentMessage: Content = { role: 'user', parts: [{ text: query }] };
@@ -866,18 +874,12 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// Create a promise for the tool execution
const executionPromise = (async () => {
// Force YOLO mode for subagents to prevent hanging on confirmation
const contextProxy = new Proxy(this.runtimeContext, {
get(target, prop, receiver) {
if (prop === 'getApprovalMode') {
return () => ApprovalMode.YOLO;
}
return Reflect.get(target, prop, receiver);
},
});
const agentContext = Object.create(this.runtimeContext);
agentContext.getToolRegistry = () => this.toolRegistry;
agentContext.getApprovalMode = () => ApprovalMode.YOLO;
const { response: toolResponse } = await executeToolCall(
contextProxy,
agentContext,
requestInfo,
signal,
);
+22
View File
@@ -212,6 +212,28 @@ describe('AgentRegistry', () => {
vi.mocked(tomlLoader.loadAgentsFromDirectory),
).not.toHaveBeenCalled();
});
it('should register introspection agent if enabled', async () => {
const config = makeFakeConfig({
introspectionAgentSettings: { enabled: true },
});
const registry = new TestableAgentRegistry(config);
await registry.initialize();
expect(registry.getDefinition('introspection_agent')).toBeDefined();
});
it('should NOT register introspection agent if disabled', async () => {
const config = makeFakeConfig({
introspectionAgentSettings: { enabled: false },
});
const registry = new TestableAgentRegistry(config);
await registry.initialize();
expect(registry.getDefinition('introspection_agent')).toBeUndefined();
});
});
describe('registration logic', () => {
+7
View File
@@ -10,6 +10,7 @@ import type { Config } from '../config/config.js';
import type { AgentDefinition } from './types.js';
import { loadAgentsFromDirectory } from './toml-loader.js';
import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
import { IntrospectionAgent } from './introspection-agent.js';
import { type z } from 'zod';
import { debugLogger } from '../utils/debugLogger.js';
import {
@@ -98,6 +99,7 @@ export class AgentRegistry {
private loadBuiltInAgents(): void {
const investigatorSettings = this.config.getCodebaseInvestigatorSettings();
const introspectionSettings = this.config.getIntrospectionAgentSettings();
// Only register the agent if it's enabled in the settings.
if (investigatorSettings?.enabled) {
@@ -135,6 +137,11 @@ export class AgentRegistry {
};
this.registerAgent(agentDef);
}
// Register the introspection agent if it's explicitly enabled.
if (introspectionSettings.enabled) {
this.registerAgent(IntrospectionAgent);
}
}
private refreshAgents(): void {