summaryrefslogtreecommitdiff
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/config/config.ts17
-rw-r--r--packages/server/src/tools/shell.md3
-rw-r--r--packages/server/src/tools/tool-registry.ts119
3 files changed, 135 insertions, 4 deletions
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<BaseTool<unknown, ToolResult>> = [
@@ -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>`.
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<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.
@@ -25,8 +108,40 @@ export class ToolRegistry {
}
/**
+ * 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());