import { Type } from '@sinclair/typebox'; import type { ToolDefinition } from '@mariozechner/pi-coding-agent'; import type { SkillsService } from '../../skills/skills.service.js'; /** * Creates meta-tools that allow agents to list and invoke skills from the catalog. * * skill_list — list all enabled skills * skill_invoke — execute a skill by name with parameters */ export function createSkillTools(skillsService: SkillsService): ToolDefinition[] { const skillList: ToolDefinition = { name: 'skill_list', label: 'List Skills', description: 'List all enabled skills available in the catalog. Returns name, description, type, and config for each skill.', parameters: Type.Object({}), async execute() { const skills = await skillsService.findEnabled(); const summary = skills.map((s) => ({ name: s.name, description: s.description, version: s.version, source: s.source, config: s.config, })); return { content: [ { type: 'text' as const, text: summary.length > 0 ? JSON.stringify(summary, null, 2) : 'No enabled skills found in catalog.', }, ], details: undefined, }; }, }; const skillInvoke: ToolDefinition = { name: 'skill_invoke', label: 'Invoke Skill', description: 'Invoke a skill from the catalog by name. For prompt skills, returns the prompt addition. ' + 'For tool skills, executes the embedded logic. For workflow skills, returns the workflow steps.', parameters: Type.Object({ name: Type.String({ description: 'Skill name to invoke' }), params: Type.Optional( Type.Record(Type.String(), Type.Unknown(), { description: 'Parameters to pass to the skill (if applicable)', }), ), }), async execute(_toolCallId, rawParams) { const { name, params } = rawParams as { name: string; params?: Record; }; const skill = await skillsService.findByName(name); if (!skill) { return { content: [{ type: 'text' as const, text: `Skill not found: ${name}` }], details: undefined, }; } if (!skill.enabled) { return { content: [{ type: 'text' as const, text: `Skill is disabled: ${name}` }], details: undefined, }; } const config = (skill.config ?? {}) as Record; const skillType = (config['type'] as string | undefined) ?? 'prompt'; switch (skillType) { case 'prompt': { const promptAddition = (config['prompt'] as string | undefined) ?? skill.description ?? ''; return { content: [ { type: 'text' as const, text: promptAddition ? `[Skill: ${name}] ${promptAddition}` : `[Skill: ${name}] No prompt content defined.`, }, ], details: undefined, }; } case 'tool': { const toolLogic = config['logic'] as string | undefined; if (!toolLogic) { return { content: [ { type: 'text' as const, text: `[Skill: ${name}] Tool skill has no logic defined.`, }, ], details: undefined, }; } // Inline tool skill execution: the logic field holds a JS expression or template // For safety, treat it as a template that can reference params const result = renderTemplate(toolLogic, { params: params ?? {}, skill }); return { content: [{ type: 'text' as const, text: `[Skill: ${name}]\n${result}` }], details: undefined, }; } case 'workflow': { const steps = config['steps'] as unknown[] | undefined; if (!steps || steps.length === 0) { return { content: [ { type: 'text' as const, text: `[Skill: ${name}] Workflow has no steps defined.`, }, ], details: undefined, }; } return { content: [ { type: 'text' as const, text: `[Skill: ${name}] Workflow steps:\n${JSON.stringify(steps, null, 2)}`, }, ], details: undefined, }; } default: { // Unknown type — return full config so the agent can decide what to do return { content: [ { type: 'text' as const, text: `[Skill: ${name}] (type: ${skillType})\n${JSON.stringify(config, null, 2)}`, }, ], details: undefined, }; } } }, }; return [skillList, skillInvoke]; } /** * Minimal template renderer — replaces {{key}} with values from the context. * Used for tool skill logic templates. */ function renderTemplate(template: string, context: Record): string { return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_match, path: string) => { const parts = path.split('.'); let value: unknown = context; for (const part of parts) { if (value != null && typeof value === 'object') { value = (value as Record)[part]; } else { value = undefined; break; } } return value !== undefined && value !== null ? String(value) : ''; }); }