mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 22:14:52 -07:00
Introspection agent demo (#15232)
This commit is contained in:
committed by
GitHub
parent
db67bb106a
commit
10ba348a3a
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user