/** * @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 \`. ${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 \`. ${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 `'; } return 'Exact bash command to execute as `bash -c `'; } /** * 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: 'Signals that the planning phase is complete and requests user approval to start 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 \`\` 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 \`\` section.`, parametersJsonSchema: zodToJsonSchema(schema), }; }