support for discovered tools using project settings for discovery and call commands
This commit is contained in:
parent
2cd976987e
commit
6b6eef5b80
|
@ -89,5 +89,7 @@ export async function loadCliConfig(settings: Settings): Promise<Config> {
|
||||||
argv.debug_mode || false,
|
argv.debug_mode || false,
|
||||||
argv.question || '',
|
argv.question || '',
|
||||||
argv.full_context || false,
|
argv.full_context || false,
|
||||||
|
settings.toolDiscoveryCommand,
|
||||||
|
settings.toolCallCommand,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ export enum SettingScope {
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
theme?: string;
|
theme?: string;
|
||||||
sandbox?: boolean | string;
|
sandbox?: boolean | string;
|
||||||
|
toolDiscoveryCommand?: string;
|
||||||
|
toolCallCommand?: string;
|
||||||
// Add other settings here.
|
// Add other settings here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,8 @@ export class Config {
|
||||||
private readonly debugMode: boolean,
|
private readonly debugMode: boolean,
|
||||||
private readonly question: string | undefined, // Keep undefined possibility
|
private readonly question: string | undefined, // Keep undefined possibility
|
||||||
private readonly fullContext: boolean = false, // Default value here
|
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
|
// toolRegistry still needs initialization based on the instance
|
||||||
this.toolRegistry = createToolRegistry(this);
|
this.toolRegistry = createToolRegistry(this);
|
||||||
|
@ -67,6 +69,14 @@ export class Config {
|
||||||
getFullContext(): boolean {
|
getFullContext(): boolean {
|
||||||
return this.fullContext;
|
return this.fullContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getToolDiscoveryCommand(): string | undefined {
|
||||||
|
return this.toolDiscoveryCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToolCallCommand(): string | undefined {
|
||||||
|
return this.toolCallCommand;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findEnvFile(startDir: string): string | null {
|
function findEnvFile(startDir: string): string | null {
|
||||||
|
@ -100,6 +110,8 @@ export function createServerConfig(
|
||||||
debugMode: boolean,
|
debugMode: boolean,
|
||||||
question: string,
|
question: string,
|
||||||
fullContext?: boolean,
|
fullContext?: boolean,
|
||||||
|
toolDiscoveryCommand?: string,
|
||||||
|
toolCallCommand?: string,
|
||||||
): Config {
|
): Config {
|
||||||
return new Config(
|
return new Config(
|
||||||
apiKey,
|
apiKey,
|
||||||
|
@ -109,11 +121,13 @@ export function createServerConfig(
|
||||||
debugMode,
|
debugMode,
|
||||||
question,
|
question,
|
||||||
fullContext,
|
fullContext,
|
||||||
|
toolDiscoveryCommand,
|
||||||
|
toolCallCommand,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToolRegistry(config: Config): ToolRegistry {
|
function createToolRegistry(config: Config): ToolRegistry {
|
||||||
const registry = new ToolRegistry();
|
const registry = new ToolRegistry(config);
|
||||||
const targetDir = config.getTargetDir();
|
const targetDir = config.getTargetDir();
|
||||||
|
|
||||||
const tools: Array<BaseTool<unknown, ToolResult>> = [
|
const tools: Array<BaseTool<unknown, ToolResult>> = [
|
||||||
|
@ -137,5 +151,6 @@ function createToolRegistry(config: Config): ToolRegistry {
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
registry.registerTool(tool);
|
registry.registerTool(tool);
|
||||||
}
|
}
|
||||||
|
registry.discoverTools();
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
This tool executes a given shell command as `bash -c <command>`.
|
This tool executes a given shell command as `bash -c <command>`.
|
||||||
Command can be any valid single-line Bash command.
|
Command can be any valid single-line Bash command.
|
||||||
Command can start background processes using `&`.
|
Command can start background processes using `&`.
|
||||||
|
Command is executed as a subprocess.
|
||||||
|
|
||||||
The following information is returned:
|
The following information is returned:
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ Command: Executed command.
|
||||||
Directory: Directory (relative to project root) where command was executed, or `(root)`.
|
Directory: Directory (relative to project root) where command was executed, or `(root)`.
|
||||||
Stdout: Output on stdout stream. Can be `(empty)` or partial on error.
|
Stdout: Output on stdout stream. Can be `(empty)` or partial on error.
|
||||||
Stderr: Output on stderr 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.
|
Exit Code: Exit code or `(none)` if terminated by signal.
|
||||||
Signal: Signal number or `(none)` if no signal was received.
|
Signal: Signal number or `(none)` if no signal was received.
|
||||||
Background PIDs: List of background processes started or `(none)`.
|
Background PIDs: List of background processes started or `(none)`.
|
||||||
|
|
|
@ -5,11 +5,94 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FunctionDeclaration } from '@google/genai';
|
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 {
|
export class ToolRegistry {
|
||||||
private tools: Map<string, Tool> = new Map();
|
private tools: Map<string, Tool> = new Map();
|
||||||
|
|
||||||
|
constructor(private readonly config: Config) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a tool definition.
|
* Registers a tool definition.
|
||||||
* @param tool - The tool object containing schema and execution logic.
|
* @param tool - The tool object containing schema and execution logic.
|
||||||
|
@ -24,9 +107,41 @@ export class ToolRegistry {
|
||||||
this.tools.set(tool.name, tool);
|
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).
|
* Retrieves the list of tool schemas (FunctionDeclaration array).
|
||||||
* Extracts the declarations from the ToolListUnion structure.
|
* Extracts the declarations from the ToolListUnion structure.
|
||||||
|
* Includes discovered (vs registered) tools if configured.
|
||||||
* @returns An array of FunctionDeclarations.
|
* @returns An array of FunctionDeclarations.
|
||||||
*/
|
*/
|
||||||
getFunctionDeclarations(): FunctionDeclaration[] {
|
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[] {
|
getAllTools(): Tool[] {
|
||||||
return Array.from(this.tools.values());
|
return Array.from(this.tools.values());
|
||||||
|
|
Loading…
Reference in New Issue