mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Introspection agent demo (#15232)
This commit is contained in:
committed by
GitHub
parent
db67bb106a
commit
10ba348a3a
@@ -836,6 +836,11 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `"auto"`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.introspectionAgentSettings.enabled`** (boolean):
|
||||
- **Description:** Enable the Introspection Agent.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
#### `hooks`
|
||||
|
||||
- **`hooks.disabled`** (array):
|
||||
|
||||
@@ -679,6 +679,8 @@ export async function loadCliConfig(
|
||||
enableMessageBusIntegration,
|
||||
codebaseInvestigatorSettings:
|
||||
settings.experimental?.codebaseInvestigatorSettings,
|
||||
introspectionAgentSettings:
|
||||
settings.experimental?.introspectionAgentSettings,
|
||||
fakeResponses: argv.fakeResponses,
|
||||
recordResponses: argv.recordResponses,
|
||||
retryFetchErrors: settings.general?.retryFetchErrors ?? false,
|
||||
|
||||
@@ -1403,6 +1403,26 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
introspectionAgentSettings: {
|
||||
type: 'object',
|
||||
label: 'Introspection Agent Settings',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: {},
|
||||
description: 'Configuration for Introspection Agent.',
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
label: 'Enable Introspection Agent',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description: 'Enable the Introspection Agent.',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('DelegateToAgentTool', () => {
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
getDebugMode: () => false,
|
||||
getActiveModel: () => 'test-model',
|
||||
modelConfigService: {
|
||||
registerRuntimeModelConfig: vi.fn(),
|
||||
},
|
||||
|
||||
59
packages/core/src/agents/introspection-agent.test.ts
Normal file
59
packages/core/src/agents/introspection-agent.test.ts
Normal file
@@ -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));
|
||||
});
|
||||
});
|
||||
85
packages/core/src/agents/introspection-agent.ts
Normal file
85
packages/core/src/agents/introspection-agent.ts
Normal file
@@ -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', () => ({
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -134,6 +134,10 @@ export interface CodebaseInvestigatorSettings {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export interface IntrospectionAgentSettings {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* All information required in CLI to handle an extension. Defined in Core so
|
||||
* that the collection of loaded, active, and inactive extensions can be passed
|
||||
@@ -307,6 +311,7 @@ export interface ConfigParameters {
|
||||
enableMessageBusIntegration?: boolean;
|
||||
disableModelRouterForAuth?: AuthType[];
|
||||
codebaseInvestigatorSettings?: CodebaseInvestigatorSettings;
|
||||
introspectionAgentSettings?: IntrospectionAgentSettings;
|
||||
continueOnFailedApiCall?: boolean;
|
||||
retryFetchErrors?: boolean;
|
||||
enableShellOutputEfficiency?: boolean;
|
||||
@@ -429,6 +434,7 @@ export class Config {
|
||||
private readonly outputSettings: OutputSettings;
|
||||
private readonly enableMessageBusIntegration: boolean;
|
||||
private readonly codebaseInvestigatorSettings: CodebaseInvestigatorSettings;
|
||||
private readonly introspectionAgentSettings: IntrospectionAgentSettings;
|
||||
private readonly continueOnFailedApiCall: boolean;
|
||||
private readonly retryFetchErrors: boolean;
|
||||
private readonly enableShellOutputEfficiency: boolean;
|
||||
@@ -578,6 +584,9 @@ export class Config {
|
||||
DEFAULT_THINKING_MODE,
|
||||
model: params.codebaseInvestigatorSettings?.model,
|
||||
};
|
||||
this.introspectionAgentSettings = {
|
||||
enabled: params.introspectionAgentSettings?.enabled ?? false,
|
||||
};
|
||||
this.continueOnFailedApiCall = params.continueOnFailedApiCall ?? true;
|
||||
this.enableShellOutputEfficiency =
|
||||
params.enableShellOutputEfficiency ?? true;
|
||||
@@ -1550,6 +1559,10 @@ export class Config {
|
||||
return this.codebaseInvestigatorSettings;
|
||||
}
|
||||
|
||||
getIntrospectionAgentSettings(): IntrospectionAgentSettings {
|
||||
return this.introspectionAgentSettings;
|
||||
}
|
||||
|
||||
async createToolRegistry(): Promise<ToolRegistry> {
|
||||
const registry = new ToolRegistry(this);
|
||||
|
||||
|
||||
71
packages/core/src/tools/get-internal-docs.test.ts
Normal file
71
packages/core/src/tools/get-internal-docs.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { GetInternalDocsTool } from './get-internal-docs.js';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
describe('GetInternalDocsTool (Integration)', () => {
|
||||
let tool: GetInternalDocsTool;
|
||||
const abortSignal = new AbortController().signal;
|
||||
|
||||
beforeEach(() => {
|
||||
tool = new GetInternalDocsTool();
|
||||
});
|
||||
|
||||
it('should find the documentation root and list files', async () => {
|
||||
const invocation = tool.build({});
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
// Verify we found some files
|
||||
expect(result.returnDisplay).toMatch(/Found \d+ documentation files/);
|
||||
|
||||
// Check for a known file that should exist in the docs
|
||||
// We assume 'index.md' or 'sidebar.json' exists in docs/
|
||||
const content = result.llmContent as string;
|
||||
expect(content).toContain('index.md');
|
||||
});
|
||||
|
||||
it('should read a specific documentation file', async () => {
|
||||
// Read the actual index.md from the real file system to compare
|
||||
// We need to resolve the path relative to THIS test file to find the expected content
|
||||
// Test file is in packages/core/src/tools/
|
||||
// Docs are in docs/ (root)
|
||||
const expectedDocsPath = path.resolve(
|
||||
__dirname,
|
||||
'../../../../docs/index.md',
|
||||
);
|
||||
const expectedContent = await fs.readFile(expectedDocsPath, 'utf8');
|
||||
|
||||
const invocation = tool.build({ path: 'index.md' });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.llmContent).toBe(expectedContent);
|
||||
expect(result.returnDisplay).toContain('index.md');
|
||||
});
|
||||
|
||||
it('should prevent access to files outside the docs directory (Path Traversal)', async () => {
|
||||
// Attempt to read package.json from the root
|
||||
const invocation = tool.build({ path: '../package.json' });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.EXECUTION_FAILED);
|
||||
expect(result.error?.message).toContain('Access denied');
|
||||
});
|
||||
|
||||
it('should handle non-existent files', async () => {
|
||||
const invocation = tool.build({ path: 'this-file-does-not-exist.md' });
|
||||
const result = await invocation.execute(abortSignal);
|
||||
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.error?.type).toBe(ToolErrorType.EXECUTION_FAILED);
|
||||
});
|
||||
});
|
||||
187
packages/core/src/tools/get-internal-docs.ts
Normal file
187
packages/core/src/tools/get-internal-docs.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseDeclarativeTool,
|
||||
BaseToolInvocation,
|
||||
Kind,
|
||||
type ToolInvocation,
|
||||
type ToolResult,
|
||||
type ToolCallConfirmationDetails,
|
||||
} from './tools.js';
|
||||
import { GET_INTERNAL_DOCS_TOOL_NAME } from './tool-names.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { glob } from 'glob';
|
||||
import { ToolErrorType } from './tool-error.js';
|
||||
|
||||
/**
|
||||
* Parameters for the GetInternalDocs tool.
|
||||
*/
|
||||
export interface GetInternalDocsParams {
|
||||
/**
|
||||
* The relative path to a specific documentation file (e.g., 'cli/commands.md').
|
||||
* If omitted, the tool will return a list of all available documentation paths.
|
||||
*/
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to find the absolute path to the documentation directory.
|
||||
*/
|
||||
async function getDocsRoot(): Promise<string> {
|
||||
const currentFile = fileURLToPath(import.meta.url);
|
||||
const currentDir = path.dirname(currentFile);
|
||||
|
||||
const isDocsDir = async (dir: string) => {
|
||||
try {
|
||||
const stats = await fs.stat(dir);
|
||||
if (stats.isDirectory()) {
|
||||
const marker = path.join(dir, 'sidebar.json');
|
||||
await fs.access(marker);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Not a valid docs directory
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 1. Check for documentation in the distributed package (dist/docs)
|
||||
// Path: dist/src/tools/get-internal-docs.js -> dist/docs
|
||||
const distDocsPath = path.resolve(currentDir, '..', '..', 'docs');
|
||||
if (await isDocsDir(distDocsPath)) {
|
||||
return distDocsPath;
|
||||
}
|
||||
|
||||
// 2. Check for documentation in the repository root (development)
|
||||
// Path: packages/core/src/tools/get-internal-docs.ts -> docs/
|
||||
const repoDocsPath = path.resolve(currentDir, '..', '..', '..', '..', 'docs');
|
||||
if (await isDocsDir(repoDocsPath)) {
|
||||
return repoDocsPath;
|
||||
}
|
||||
|
||||
// 3. Check for documentation in the bundle directory (bundle/docs)
|
||||
// Path: bundle/gemini.js -> bundle/docs
|
||||
const bundleDocsPath = path.join(currentDir, 'docs');
|
||||
if (await isDocsDir(bundleDocsPath)) {
|
||||
return bundleDocsPath;
|
||||
}
|
||||
|
||||
throw new Error('Could not find Gemini CLI documentation directory.');
|
||||
}
|
||||
|
||||
class GetInternalDocsInvocation extends BaseToolInvocation<
|
||||
GetInternalDocsParams,
|
||||
ToolResult
|
||||
> {
|
||||
constructor(params: GetInternalDocsParams, messageBus?: MessageBus) {
|
||||
super(params, messageBus, GET_INTERNAL_DOCS_TOOL_NAME);
|
||||
}
|
||||
|
||||
override async shouldConfirmExecute(
|
||||
_abortSignal: AbortSignal,
|
||||
): Promise<ToolCallConfirmationDetails | false> {
|
||||
return false;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.params.path) {
|
||||
return `Reading internal documentation: ${this.params.path}`;
|
||||
}
|
||||
return 'Listing all available internal documentation.';
|
||||
}
|
||||
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
try {
|
||||
const docsRoot = await getDocsRoot();
|
||||
|
||||
if (!this.params.path) {
|
||||
// List all .md files recursively
|
||||
const files = await glob('**/*.md', { cwd: docsRoot, posix: true });
|
||||
files.sort();
|
||||
|
||||
const fileList = files.map((f) => `- ${f}`).join('\n');
|
||||
const resultContent = `Available Gemini CLI documentation files:\n\n${fileList}`;
|
||||
|
||||
return {
|
||||
llmContent: resultContent,
|
||||
returnDisplay: `Found ${files.length} documentation files.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Read a specific file
|
||||
// Security: Prevent path traversal by resolving and verifying it stays within docsRoot
|
||||
const resolvedPath = path.resolve(docsRoot, this.params.path);
|
||||
if (!resolvedPath.startsWith(docsRoot)) {
|
||||
throw new Error(
|
||||
'Access denied: Requested path is outside the documentation directory.',
|
||||
);
|
||||
}
|
||||
|
||||
const content = await fs.readFile(resolvedPath, 'utf8');
|
||||
|
||||
return {
|
||||
llmContent: content,
|
||||
returnDisplay: `Successfully read documentation: ${this.params.path}`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
llmContent: `Error accessing internal documentation: ${errorMessage}`,
|
||||
returnDisplay: `Failed to access documentation: ${errorMessage}`,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
type: ToolErrorType.EXECUTION_FAILED,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tool that provides access to Gemini CLI's internal documentation.
|
||||
* If no path is provided, it returns a list of all available documentation files.
|
||||
* If a path is provided, it returns the content of that specific file.
|
||||
*/
|
||||
export class GetInternalDocsTool extends BaseDeclarativeTool<
|
||||
GetInternalDocsParams,
|
||||
ToolResult
|
||||
> {
|
||||
static readonly Name = GET_INTERNAL_DOCS_TOOL_NAME;
|
||||
|
||||
constructor(messageBus?: MessageBus) {
|
||||
super(
|
||||
GetInternalDocsTool.Name,
|
||||
'GetInternalDocs',
|
||||
'Returns the content of Gemini CLI internal documentation files. If no path is provided, returns a list of all available documentation paths.',
|
||||
Kind.Think,
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
description:
|
||||
"The relative path to the documentation file (e.g., 'cli/commands.md'). If omitted, lists all available documentation.",
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
/* isOutputMarkdown */ true,
|
||||
/* canUpdateOutput */ false,
|
||||
messageBus,
|
||||
);
|
||||
}
|
||||
|
||||
protected createInvocation(
|
||||
params: GetInternalDocsParams,
|
||||
messageBus?: MessageBus,
|
||||
): ToolInvocation<GetInternalDocsParams, ToolResult> {
|
||||
return new GetInternalDocsInvocation(params, messageBus);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export const READ_MANY_FILES_TOOL_NAME = 'read_many_files';
|
||||
export const READ_FILE_TOOL_NAME = 'read_file';
|
||||
export const LS_TOOL_NAME = 'list_directory';
|
||||
export const MEMORY_TOOL_NAME = 'save_memory';
|
||||
export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs';
|
||||
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
|
||||
export const DELEGATE_TO_AGENT_TOOL_NAME = 'delegate_to_agent';
|
||||
|
||||
|
||||
@@ -1380,6 +1380,23 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"introspectionAgentSettings": {
|
||||
"title": "Introspection Agent Settings",
|
||||
"description": "Configuration for Introspection Agent.",
|
||||
"markdownDescription": "Configuration for Introspection Agent.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `{}`",
|
||||
"default": {},
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"title": "Enable Introspection Agent",
|
||||
"description": "Enable the Introspection Agent.",
|
||||
"markdownDescription": "Enable the Introspection Agent.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -18,20 +18,32 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { writeFileSync, existsSync, cpSync } from 'node:fs';
|
||||
import { join, basename } from 'node:path';
|
||||
|
||||
if (!process.cwd().includes('packages')) {
|
||||
console.error('must be invoked from a package directory');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const packageName = basename(process.cwd());
|
||||
|
||||
// build typescript files
|
||||
execSync('tsc --build', { stdio: 'inherit' });
|
||||
|
||||
// copy .{md,json} files
|
||||
execSync('node ../../scripts/copy_files.js', { stdio: 'inherit' });
|
||||
|
||||
// Copy documentation for the core package
|
||||
if (packageName === 'core') {
|
||||
const docsSource = join(process.cwd(), '..', '..', 'docs');
|
||||
const docsTarget = join(process.cwd(), 'dist', 'docs');
|
||||
if (existsSync(docsSource)) {
|
||||
cpSync(docsSource, docsTarget, { recursive: true, dereference: true });
|
||||
console.log('Copied documentation to dist/docs');
|
||||
}
|
||||
}
|
||||
|
||||
// touch dist/.last_build
|
||||
writeFileSync(join(process.cwd(), 'dist', '.last_build'), '');
|
||||
process.exit(0);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { copyFileSync, existsSync, mkdirSync, cpSync } from 'node:fs';
|
||||
import { dirname, join, basename } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { glob } from 'glob';
|
||||
@@ -53,4 +53,13 @@ for (const file of policyFiles) {
|
||||
}
|
||||
|
||||
console.log(`Copied ${policyFiles.length} policy files to bundle/policies/`);
|
||||
|
||||
// 3. Copy Documentation (docs/)
|
||||
const docsSrc = join(root, 'docs');
|
||||
const docsDest = join(bundleDir, 'docs');
|
||||
if (existsSync(docsSrc)) {
|
||||
cpSync(docsSrc, docsDest, { recursive: true, dereference: true });
|
||||
console.log('Copied docs to bundle/docs/');
|
||||
}
|
||||
|
||||
console.log('Assets copied to bundle/');
|
||||
|
||||
Reference in New Issue
Block a user