From 10ba348a3aad946603c219827caeac1c8f07fa3e Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Fri, 19 Dec 2025 14:11:32 -0800 Subject: [PATCH] Introspection agent demo (#15232) --- docs/get-started/configuration.md | 5 + packages/cli/src/config/config.ts | 2 + packages/cli/src/config/settingsSchema.ts | 20 ++ .../src/agents/delegate-to-agent-tool.test.ts | 1 + .../src/agents/introspection-agent.test.ts | 59 ++++++ .../core/src/agents/introspection-agent.ts | 85 ++++++++ .../core/src/agents/local-executor.test.ts | 4 + packages/core/src/agents/local-executor.ts | 28 +-- packages/core/src/agents/registry.test.ts | 22 +++ packages/core/src/agents/registry.ts | 7 + packages/core/src/config/config.ts | 13 ++ .../core/src/tools/get-internal-docs.test.ts | 71 +++++++ packages/core/src/tools/get-internal-docs.ts | 187 ++++++++++++++++++ packages/core/src/tools/tool-names.ts | 1 + schemas/settings.schema.json | 17 ++ scripts/build_package.js | 16 +- scripts/copy_bundle_assets.js | 11 +- 17 files changed, 533 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/agents/introspection-agent.test.ts create mode 100644 packages/core/src/agents/introspection-agent.ts create mode 100644 packages/core/src/tools/get-internal-docs.test.ts create mode 100644 packages/core/src/tools/get-internal-docs.ts diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 223f142e97..8040c34186 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -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): diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 20bc8b670e..74f9fb3f49 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -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, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 821699b8e3..9145918a48 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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, + }, + }, + }, }, }, diff --git a/packages/core/src/agents/delegate-to-agent-tool.test.ts b/packages/core/src/agents/delegate-to-agent-tool.test.ts index 009b6a0010..0075cb03a7 100644 --- a/packages/core/src/agents/delegate-to-agent-tool.test.ts +++ b/packages/core/src/agents/delegate-to-agent-tool.test.ts @@ -47,6 +47,7 @@ describe('DelegateToAgentTool', () => { beforeEach(() => { config = { getDebugMode: () => false, + getActiveModel: () => 'test-model', modelConfigService: { registerRuntimeModelConfig: vi.fn(), }, diff --git a/packages/core/src/agents/introspection-agent.test.ts b/packages/core/src/agents/introspection-agent.test.ts new file mode 100644 index 0000000000..3e28659390 --- /dev/null +++ b/packages/core/src/agents/introspection-agent.test.ts @@ -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)); + }); +}); diff --git a/packages/core/src/agents/introspection-agent.ts b/packages/core/src/agents/introspection-agent.ts new file mode 100644 index 0000000000..413caa28a6 --- /dev/null +++ b/packages/core/src/agents/introspection-agent.ts @@ -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' + + '\n' + + '${question}\n' + + '', + 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.', + }, +}; diff --git a/packages/core/src/agents/local-executor.test.ts b/packages/core/src/agents/local-executor.test.ts index aba89e0afe..1a25d8bd7a 100644 --- a/packages/core/src/agents/local-executor.test.ts +++ b/packages/core/src/agents/local-executor.test.ts @@ -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', () => ({ diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index 6cb78c5f11..cadf384e69 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -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 { 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 { 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 { // 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, ); diff --git a/packages/core/src/agents/registry.test.ts b/packages/core/src/agents/registry.test.ts index 022e1feb2c..3719d132e9 100644 --- a/packages/core/src/agents/registry.test.ts +++ b/packages/core/src/agents/registry.test.ts @@ -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', () => { diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index d42de545eb..bdbdb216d4 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -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 { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 56bb2ac3c5..dc78a78966 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -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 { const registry = new ToolRegistry(this); diff --git a/packages/core/src/tools/get-internal-docs.test.ts b/packages/core/src/tools/get-internal-docs.test.ts new file mode 100644 index 0000000000..40a47b6477 --- /dev/null +++ b/packages/core/src/tools/get-internal-docs.test.ts @@ -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); + }); +}); diff --git a/packages/core/src/tools/get-internal-docs.ts b/packages/core/src/tools/get-internal-docs.ts new file mode 100644 index 0000000000..aec44a5272 --- /dev/null +++ b/packages/core/src/tools/get-internal-docs.ts @@ -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 { + 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 { + 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 { + 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 { + return new GetInternalDocsInvocation(params, messageBus); + } +} diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index cec12de72c..db2967405e 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -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'; diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 258c8c0864..8e88a88119 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -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 diff --git a/scripts/build_package.js b/scripts/build_package.js index 73f73861e9..c201333d2c 100644 --- a/scripts/build_package.js +++ b/scripts/build_package.js @@ -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); diff --git a/scripts/copy_bundle_assets.js b/scripts/copy_bundle_assets.js index 1dbca40296..afd2b35ea4 100644 --- a/scripts/copy_bundle_assets.js +++ b/scripts/copy_bundle_assets.js @@ -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/');