support for discovered tools using project settings for discovery and call commands

This commit is contained in:
olcan 2025-05-03 19:57:28 -07:00 committed by Olcan
parent 2cd976987e
commit 6b6eef5b80
5 changed files with 139 additions and 4 deletions

View File

@ -89,5 +89,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
argv.debug_mode || false,
argv.question || '',
argv.full_context || false,
settings.toolDiscoveryCommand,
settings.toolCallCommand,
);
}

View File

@ -20,6 +20,8 @@ export enum SettingScope {
export interface Settings {
theme?: string;
sandbox?: boolean | string;
toolDiscoveryCommand?: string;
toolCallCommand?: string;
// Add other settings here.
}

View File

@ -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<BaseTool<unknown, ToolResult>> = [
@ -137,5 +151,6 @@ function createToolRegistry(config: Config): ToolRegistry {
for (const tool of tools) {
registry.registerTool(tool);
}
registry.discoverTools();
return registry;
}

View File

@ -1,6 +1,7 @@
This tool executes a given shell command as `bash -c <command>`.
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)`.

View File

@ -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<string, unknown>;
export class DiscoveredTool extends BaseTool<ToolParams, ToolResult> {
constructor(
private readonly config: Config,
readonly name: string,
readonly description: string,
readonly parameterSchema: Record<string, unknown>,
) {
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<ToolResult> {
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<string, Tool> = 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<string, unknown>,
),
);
}
}
/**
* 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());