import { skillRegistry } from "./registry"; import { SkillExecution, SkillStatus, SkillHandler } from "./types"; import { getDbInstance } from "../db/core"; import { getSettings } from "../db/settings"; import { randomUUID } from "crypto"; import { logger } from "../../../open-sse/utils/logger.ts"; const log = logger("SKILLS_EXECUTOR"); class SkillExecutor { private static instance: SkillExecutor; private handlers: Map = new Map(); private timeout: number = 30000; private maxRetries: number = 3; private constructor() {} static getInstance(): SkillExecutor { if (!SkillExecutor.instance) { SkillExecutor.instance = new SkillExecutor(); } return SkillExecutor.instance; } registerHandler(name: string, handler: SkillHandler): void { this.handlers.set(name, handler); } setTimeout(ms: number): void { this.timeout = ms; } setMaxRetries(count: number): void { this.maxRetries = count; } async execute( skillName: string, input: Record, context: { apiKeyId: string; sessionId?: string } ): Promise { const settings = await getSettings(); if (settings.skillsEnabled === false) { throw new Error("Skills execution is disabled. Enable Skills in Settings > AI."); } const skill = skillRegistry.getSkill(skillName, context.apiKeyId); if (!skill) { throw new Error(`Skill not found: ${skillName}`); } if (!skill.enabled) { throw new Error(`Skill is disabled: ${skillName}`); } const db = getDbInstance(); const executionId = randomUUID(); const startTime = Date.now(); log.info("skills.executor.start", { skillId: skill.id, skillName, apiKeyId: context.apiKeyId }); try { db.prepare( `INSERT INTO skill_executions (id, skill_id, api_key_id, session_id, input, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)` ).run( executionId, skill.id, context.apiKeyId, context.sessionId || null, JSON.stringify(input), SkillStatus.RUNNING, new Date().toISOString() ); const handler = this.handlers.get(skill.handler); if (!handler) { throw new Error(`Handler not found: ${skill.handler}`); } let output: Record | null = null; let errorMessage: string | null = null; let status = SkillStatus.SUCCESS; try { const result = await this.executeWithTimeout( handler(input, { apiKeyId: context.apiKeyId, sessionId: context.sessionId || "" }) ); output = result; } catch (err) { errorMessage = err instanceof Error ? err.message : String(err); status = SkillStatus.ERROR; } const durationMs = Date.now() - startTime; db.prepare( `UPDATE skill_executions SET output = ?, status = ?, error_message = ?, duration_ms = ? WHERE id = ?` ).run(output ? JSON.stringify(output) : null, status, errorMessage, durationMs, executionId); log.info("skills.executor.complete", { skillId: skill.id, success: status === SkillStatus.SUCCESS, durationMs, }); return { id: executionId, skillId: skill.id, apiKeyId: context.apiKeyId, sessionId: context.sessionId || "", input, output, status, errorMessage, durationMs, createdAt: new Date(), }; } catch (err) { const durationMs = Date.now() - startTime; const errorMessage = err instanceof Error ? err.message : String(err); db.prepare( `UPDATE skill_executions SET status = ?, error_message = ?, duration_ms = ? WHERE id = ?` ).run(SkillStatus.ERROR, errorMessage, durationMs, executionId); throw err; } } private async executeWithTimeout(promise: Promise): Promise { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error("Skill execution timed out")), this.timeout) ), ]); } getExecution(executionId: string): SkillExecution | undefined { const db = getDbInstance(); const row = db.prepare("SELECT * FROM skill_executions WHERE id = ?").get(executionId) as any; if (!row) return undefined; return { id: row.id, skillId: row.skill_id, apiKeyId: row.api_key_id, sessionId: row.session_id || "", input: JSON.parse(row.input), output: row.output ? JSON.parse(row.output) : null, status: row.status as SkillStatus, errorMessage: row.error_message, durationMs: row.duration_ms, createdAt: new Date(row.created_at), }; } listExecutions(apiKeyId?: string, limit: number = 50, offset: number = 0): SkillExecution[] { const db = getDbInstance(); const rows = apiKeyId ? db .prepare( "SELECT * FROM skill_executions WHERE api_key_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?" ) .all(apiKeyId, limit, offset) : db .prepare("SELECT * FROM skill_executions ORDER BY created_at DESC LIMIT ? OFFSET ?") .all(limit, offset); return (rows as any[]).map((row) => ({ id: row.id, skillId: row.skill_id, apiKeyId: row.api_key_id, sessionId: row.session_id || "", input: JSON.parse(row.input), output: row.output ? JSON.parse(row.output) : null, status: row.status as SkillStatus, errorMessage: row.error_message, durationMs: row.duration_ms, createdAt: new Date(row.created_at), })); } countExecutions(apiKeyId?: string): number { const db = getDbInstance(); const row = apiKeyId ? (db .prepare("SELECT COUNT(*) as count FROM skill_executions WHERE api_key_id = ?") .get(apiKeyId) as any) : (db.prepare("SELECT COUNT(*) as count FROM skill_executions").get() as any); return row?.count ?? 0; } } export const skillExecutor = SkillExecutor.getInstance();