All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
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<string, unknown>;
|
|
};
|
|
|
|
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<string, unknown>;
|
|
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, unknown>): 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<string, unknown>)[part];
|
|
} else {
|
|
value = undefined;
|
|
break;
|
|
}
|
|
}
|
|
return value !== undefined && value !== null ? String(value) : '';
|
|
});
|
|
}
|