import type { ToolPlugin, ToolResult, ToolContext } from "./types.js"; import { SKILL_TOOL_NAME } from "../constants/tools.js"; import { GENERAL_PURPOSE_SUBAGENT_TYPE } from "../constants/subagents.js"; import type { SkillMetadata } from "../types/skills.js"; import { logger } from "../utils/globalLogger.js"; import { countToolBlocks, formatToolTokenSummary, } from "../utils/messageOperations.js"; /** * Skill tool plugin for invoking Wave skills */ const SKILL_TOOL_DESCRIPTION = "Execute a skill within the main conversation. When users ask you to perform tasks, check if any of the available skills match. Skills provide specialized capabilities and domain knowledge."; export const skillTool: ToolPlugin = { name: SKILL_TOOL_NAME, config: { type: "function" as const, function: { name: SKILL_TOOL_NAME, description: SKILL_TOOL_DESCRIPTION, parameters: { type: "object", properties: { skill_name: { type: "string", description: "Name of the skill to invoke", }, args: { type: "string", description: "Optional arguments to pass to the skill", }, }, required: ["skill_name"], }, }, }, prompt: (args?: { availableSkills?: SkillMetadata[]; isSubagent?: boolean; }) => { let availableSkills = args?.availableSkills?.filter( (skill) => !skill.disableModelInvocation, ); if (args?.isSubagent) { availableSkills = availableSkills?.filter( (skill) => skill.context !== "fork", ); } if (!availableSkills || availableSkills.length === 0) { return `${SKILL_TOOL_DESCRIPTION} No skills are currently available.`; } const skillList = availableSkills .map( (skill) => `• **${skill.name}** (${skill.type}): ${skill.description}`, ) .join("\n"); return `${SKILL_TOOL_DESCRIPTION} Do not invoke the same skill repeatedly if it has already been called with the same arguments.\n\nAvailable skills:\n${skillList}`; }, execute: async ( args: Record, context: ToolContext, ): Promise => { try { const skillManager = context.skillManager; if (!skillManager) { return { success: false, content: "", error: "Skill manager not available in tool context", }; } // Validate arguments const skillName = args.skill_name as string; const skillArgs = args.args as string | undefined; if (!skillName || typeof skillName !== "string") { return { success: false, content: "", error: "skill_name parameter is required and must be a string", }; } const skillMetadata = skillManager.getSkillMetadata(skillName); if (skillMetadata?.disableModelInvocation) { return { success: false, content: "", error: `Skill "${skillName}" is not available for model invocation.`, }; } // Handle fork context if (skillMetadata?.context === "fork") { const subagentManager = context.subagentManager; if (!subagentManager) { return { success: false, content: "", error: "Subagent manager not available in tool context", }; } const agentType = skillMetadata.agent || GENERAL_PURPOSE_SUBAGENT_TYPE; const configuration = await subagentManager.findSubagent(agentType); if (!configuration) { return { success: false, content: "", error: `No subagent found matching "${agentType}" for skill "${skillName}"`, }; } // Execute the skill to get the content const skillResult = await skillManager.executeSkill({ skill_name: skillName, args: skillArgs, }); logger.debug(`Skill ${skillName} executed, allowedTools:`, { allowedTools: skillResult.allowedTools, }); // Create subagent instance const instance = await subagentManager.createInstance( configuration, { description: `Skill: ${skillName}`, prompt: skillResult.content, subagent_type: agentType, allowedTools: skillResult.allowedTools, model: skillMetadata.model, }, false, // run_in_background () => { // Update shortResult const messages = instance.messageManager.getMessages(); const tokens = instance.messageManager.getLatestTotalTokens(); const usedTools = instance.usedTools; const toolCount = countToolBlocks(messages); const summary = formatToolTokenSummary(toolCount, tokens); const getDisplayParam = (t: { name: string; parameters: string; compactParams?: string; stage?: string; }) => { if ( (t.stage === "end" || t.stage === "running") && t.compactParams ) { return t.compactParams; } const flat = t.parameters.replace(/\n/g, "\\n"); return flat.length > 30 ? `…${flat.slice(-30)}` : flat; }; let shortResult = ""; if (toolCount > 2) { shortResult += "... "; } shortResult += summary; if (usedTools.length > 0) { shortResult += "\n" + usedTools .map((t) => `${t.name} ${getDisplayParam(t)}`) .join("\n"); } context.onShortResultUpdate?.(shortResult); }, ); try { const result = await subagentManager.executeAgent( instance, skillResult.content, context.abortSignal, false, ); // Cleanup subagent instance after agent completion subagentManager.cleanupInstance(instance.subagentId); const messages = instance.messageManager.getMessages(); const tokens = instance.messageManager.getLatestTotalTokens(); const toolCount = countToolBlocks(messages); const summary = formatToolTokenSummary(toolCount, tokens); return { success: true, content: result, shortResult: `Invoked skill: ${skillName}${summary ? ` ${summary}` : ""}`, }; } catch (error) { return { success: false, content: "", error: `Skill fork failed: ${error instanceof Error ? error.message : String(error)}`, }; } } // Standard execution const result = await skillManager.executeSkill({ skill_name: skillName, args: skillArgs, }); // Add temporary rules if allowedTools are present if (result.allowedTools && result.allowedTools.length > 0) { context.permissionManager?.addTemporaryRules(result.allowedTools); } return { success: true, content: result.content, shortResult: `Invoked skill: ${skillName}`, }; } catch (error) { return { success: false, content: "", error: error instanceof Error ? error.message : String(error), }; } }, formatCompactParams: (params: Record) => { const skillName = params.skill_name as string; return skillName || "unknown-skill"; }, };