From 6b6eef5b80f2cb01ba7b5b94074f8f56937ad04d Mon Sep 17 00:00:00 2001 From: olcan Date: Sat, 3 May 2025 19:57:28 -0700 Subject: [PATCH] support for discovered tools using project settings for discovery and call commands --- packages/cli/src/config/config.ts | 2 + packages/cli/src/config/settings.ts | 2 + packages/server/src/config/config.ts | 17 ++- packages/server/src/tools/shell.md | 3 +- packages/server/src/tools/tool-registry.ts | 119 ++++++++++++++++++++- 5 files changed, 139 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 20ca7806..abb93c64 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -89,5 +89,7 @@ export async function loadCliConfig(settings: Settings): Promise { argv.debug_mode || false, argv.question || '', argv.full_context || false, + settings.toolDiscoveryCommand, + settings.toolCallCommand, ); } diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index d3fbd200..a929abc8 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -20,6 +20,8 @@ export enum SettingScope { export interface Settings { theme?: string; sandbox?: boolean | string; + toolDiscoveryCommand?: string; + toolCallCommand?: string; // Add other settings here. } diff --git a/packages/server/src/config/config.ts b/packages/server/src/config/config.ts index 82b31902..de70144d 100644 --- a/packages/server/src/config/config.ts +++ b/packages/server/src/config/config.ts @@ -32,6 +32,8 @@ export class Config { private readonly debugMode: boolean, private readonly question: string | undefined, // Keep undefined possibility private readonly fullContext: boolean = false, // Default value here + private readonly toolDiscoveryCommand: string | undefined, + private readonly toolCallCommand: string | undefined, ) { // toolRegistry still needs initialization based on the instance this.toolRegistry = createToolRegistry(this); @@ -67,6 +69,14 @@ export class Config { getFullContext(): boolean { return this.fullContext; } + + getToolDiscoveryCommand(): string | undefined { + return this.toolDiscoveryCommand; + } + + getToolCallCommand(): string | undefined { + return this.toolCallCommand; + } } function findEnvFile(startDir: string): string | null { @@ -100,6 +110,8 @@ export function createServerConfig( debugMode: boolean, question: string, fullContext?: boolean, + toolDiscoveryCommand?: string, + toolCallCommand?: string, ): Config { return new Config( apiKey, @@ -109,11 +121,13 @@ export function createServerConfig( debugMode, question, fullContext, + toolDiscoveryCommand, + toolCallCommand, ); } function createToolRegistry(config: Config): ToolRegistry { - const registry = new ToolRegistry(); + const registry = new ToolRegistry(config); const targetDir = config.getTargetDir(); const tools: Array> = [ @@ -137,5 +151,6 @@ function createToolRegistry(config: Config): ToolRegistry { for (const tool of tools) { registry.registerTool(tool); } + registry.discoverTools(); return registry; } diff --git a/packages/server/src/tools/shell.md b/packages/server/src/tools/shell.md index a8a42381..66543662 100644 --- a/packages/server/src/tools/shell.md +++ b/packages/server/src/tools/shell.md @@ -1,6 +1,7 @@ This tool executes a given shell command as `bash -c `. Command can be any valid single-line Bash command. Command can start background processes using `&`. +Command is executed as a subprocess. The following information is returned: @@ -8,7 +9,7 @@ Command: Executed command. Directory: Directory (relative to project root) where command was executed, or `(root)`. Stdout: Output on stdout stream. Can be `(empty)` or partial on error. Stderr: Output on stderr stream. Can be `(empty)` or partial on error. -Error: Error or `(none)` if no error occurred. +Error: Error or `(none)` if no error was reported for the subprocess. Exit Code: Exit code or `(none)` if terminated by signal. Signal: Signal number or `(none)` if no signal was received. Background PIDs: List of background processes started or `(none)`. diff --git a/packages/server/src/tools/tool-registry.ts b/packages/server/src/tools/tool-registry.ts index 9ae41802..4affeca1 100644 --- a/packages/server/src/tools/tool-registry.ts +++ b/packages/server/src/tools/tool-registry.ts @@ -5,11 +5,94 @@ */ import { FunctionDeclaration } from '@google/genai'; -import { Tool } from './tools.js'; +import { Tool, ToolResult, BaseTool } from './tools.js'; +import { Config } from '../config/config.js'; +import { spawn, execSync } from 'node:child_process'; + +type ToolParams = Record; + +export class DiscoveredTool extends BaseTool { + constructor( + private readonly config: Config, + readonly name: string, + readonly description: string, + readonly parameterSchema: Record, + ) { + const discoveryCmd = config.getToolDiscoveryCommand()!; + const callCommand = config.getToolCallCommand()!; + description += ` +This tool was discovered from the project by executing the command \`${discoveryCmd}\` on project root. +When called, this tool will execute the command \`${callCommand} ${name}\` on project root. +Tool discovery and call commands can be configured in project settings. + +When called, the tool call command is executed as a subprocess. +On success, tool output is returned as a json string. +Otherwise, the following information is returned: + +Stdout: Output on stdout stream. Can be \`(empty)\` or partial. +Stderr: Output on stderr stream. Can be \`(empty)\` or partial. +Error: Error or \`(none)\` if no error was reported for the subprocess. +Exit Code: Exit code or \`(none)\` if terminated by signal. +Signal: Signal number or \`(none)\` if no signal was received. +`; + super(name, name, description, parameterSchema); + } + + async execute(params: ToolParams): Promise { + const callCommand = this.config.getToolCallCommand()!; + const child = spawn(callCommand, [this.name]); + child.stdin.write(JSON.stringify(params)); + child.stdin.end(); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + let error: Error | null = null; + child.on('error', (err: Error) => { + error = err; + }); + let code: number | null = null; + let signal: NodeJS.Signals | null = null; + child.on( + 'close', + (_code: number | null, _signal: NodeJS.Signals | null) => { + code = _code; + signal = _signal; + }, + ); + await new Promise((resolve) => child.on('close', resolve)); + + // if there is any error, non-zero exit code, signal, or stderr, return error details instead of stdout + if (error || code !== 0 || signal || stderr) { + const llmContent = [ + `Stdout: ${stdout || '(empty)'}`, + `Stderr: ${stderr || '(empty)'}`, + `Error: ${error ?? '(none)'}`, + `Exit Code: ${code ?? '(none)'}`, + `Signal: ${signal ?? '(none)'}`, + ].join('\n'); + return { + llmContent, + returnDisplay: llmContent, + }; + } + + return { + llmContent: stdout, + returnDisplay: stdout, + }; + } +} export class ToolRegistry { private tools: Map = new Map(); + constructor(private readonly config: Config) {} + /** * Registers a tool definition. * @param tool - The tool object containing schema and execution logic. @@ -24,9 +107,41 @@ export class ToolRegistry { this.tools.set(tool.name, tool); } + /** + * Discovers tools from project, if a discovery command is configured. + * Can be called multiple times to update discovered tools. + */ + discoverTools(): void { + const discoveryCmd = this.config.getToolDiscoveryCommand(); + if (!discoveryCmd) return; + // remove any previously discovered tools + for (const tool of this.tools.values()) { + if (tool instanceof DiscoveredTool) { + this.tools.delete(tool.name); + } + } + // execute discovery command and extract function declarations + const functions: FunctionDeclaration[] = []; + for (const tool of JSON.parse(execSync(discoveryCmd).toString().trim())) { + functions.push(...tool['function_declarations']); + } + // register each function as a tool + for (const func of functions) { + this.registerTool( + new DiscoveredTool( + this.config, + func.name!, + func.description!, + func.parameters! as Record, + ), + ); + } + } + /** * Retrieves the list of tool schemas (FunctionDeclaration array). * Extracts the declarations from the ToolListUnion structure. + * Includes discovered (vs registered) tools if configured. * @returns An array of FunctionDeclarations. */ getFunctionDeclarations(): FunctionDeclaration[] { @@ -38,7 +153,7 @@ export class ToolRegistry { } /** - * Returns an array of all registered tool instances. + * Returns an array of all registered and discovered tool instances. */ getAllTools(): Tool[] { return Array.from(this.tools.values());