Files
gemini-cli/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts
T

166 lines
6.4 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Reusable logic for generating tool declarations that depend on runtime state
* (OS, platforms, or dynamic schema values like available skills).
*/
import { type FunctionDeclaration } from '@google/genai';
import * as os from 'node:os';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import {
SHELL_TOOL_NAME,
EXIT_PLAN_MODE_TOOL_NAME,
ACTIVATE_SKILL_TOOL_NAME,
} from './base-declarations.js';
/**
* Generates the platform-specific description for the shell tool.
*/
export function getShellToolDescription(
enableInteractiveShell: boolean,
enableEfficiency: boolean,
): string {
const efficiencyGuidelines = enableEfficiency
? `
Efficiency Guidelines:
- Quiet Flags: Always prefer silent or quiet flags (e.g., \`npm install --silent\`, \`git --no-pager\`) to reduce output volume while still capturing necessary information.
- Pagination: Always disable terminal pagination to ensure commands terminate (e.g., use \`git --no-pager\`, \`systemctl --no-pager\`, or set \`PAGER=cat\`).`
: '';
const returnedInfo = `
The following information is returned:
Output: Combined stdout/stderr. Can be \`(empty)\` or partial on error and for any unwaited background processes.
Exit Code: Only included if non-zero (command failed).
Error: Only included if a process-level error occurred (e.g., spawn failure).
Signal: Only included if process was terminated by a signal.
Background PIDs: Only included if background processes were started.
Process Group PGID: Only included if available.`;
if (os.platform() === 'win32') {
const backgroundInstructions = enableInteractiveShell
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use PowerShell background constructs.'
: 'Command can start background processes using PowerShell constructs such as `Start-Process -NoNewWindow` or `Start-Job`.';
return `This tool executes a given shell command as \`powershell.exe -NoProfile -Command <command>\`. ${backgroundInstructions}${efficiencyGuidelines}${returnedInfo}`;
} else {
const backgroundInstructions = enableInteractiveShell
? 'To run a command in the background, set the `is_background` parameter to true. Do NOT use `&` to background commands.'
: 'Command can start background processes using `&`.';
return `This tool executes a given shell command as \`bash -c <command>\`. ${backgroundInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${efficiencyGuidelines}${returnedInfo}`;
}
}
/**
* Returns the platform-specific description for the 'command' parameter.
*/
export function getCommandDescription(): string {
if (os.platform() === 'win32') {
return 'Exact command to execute as `powershell.exe -NoProfile -Command <command>`';
}
return 'Exact bash command to execute as `bash -c <command>`';
}
/**
* Returns the FunctionDeclaration for the shell tool.
*/
export function getShellDeclaration(
enableInteractiveShell: boolean,
enableEfficiency: boolean,
): FunctionDeclaration {
return {
name: SHELL_TOOL_NAME,
description: getShellToolDescription(
enableInteractiveShell,
enableEfficiency,
),
parametersJsonSchema: {
type: 'object',
properties: {
command: {
type: 'string',
description: getCommandDescription(),
},
description: {
type: 'string',
description:
'Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.',
},
dir_path: {
type: 'string',
description:
'(OPTIONAL) The path of the directory to run the command in. If not provided, the project root directory is used. Must be a directory within the workspace and must already exist.',
},
is_background: {
type: 'boolean',
description:
'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.',
},
},
required: ['command'],
},
};
}
/**
* Returns the FunctionDeclaration for exiting plan mode.
*/
export function getExitPlanModeDeclaration(
plansDir: string,
): FunctionDeclaration {
return {
name: EXIT_PLAN_MODE_TOOL_NAME,
description:
'Finalizes the planning phase and transitions to implementation by presenting the plan for user approval. This tool MUST be used to exit Plan Mode before any source code edits can be performed. Call this whenever a plan is ready or the user requests implementation.',
parametersJsonSchema: {
type: 'object',
required: ['plan_path'],
properties: {
plan_path: {
type: 'string',
description: `The file path to the finalized plan (e.g., "${plansDir}/feature-x.md"). This path MUST be within the designated plans directory: ${plansDir}/`,
},
},
},
};
}
/**
* Returns the FunctionDeclaration for activating a skill.
*/
export function getActivateSkillDeclaration(
skillNames: string[],
): FunctionDeclaration {
const availableSkillsHint =
skillNames.length > 0
? ` (Available: ${skillNames.map((n) => `'${n}'`).join(', ')})`
: '';
let schema: z.ZodTypeAny;
if (skillNames.length === 0) {
schema = z.object({
name: z.string().describe('No skills are currently available.'),
});
} else {
schema = z.object({
name: z
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
.enum(skillNames as [string, ...string[]])
.describe('The name of the skill to activate.'),
});
}
return {
name: ACTIVATE_SKILL_TOOL_NAME,
description: `Activates a specialized agent skill by name${availableSkillsHint}. Returns the skill's instructions wrapped in \`<activated_skill>\` tags. These provide specialized guidance for the current task. Use this when you identify a task that matches a skill's description. ONLY use names exactly as they appear in the \`<available_skills>\` section.`,
parametersJsonSchema: zodToJsonSchema(schema),
};
}