UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

336 lines (296 loc) 10.5 kB
import path from 'path'; import fs from 'fs'; import yaml from 'js-yaml'; import { loadTaskFromFile } from './frontmatter.js'; import { logger } from '../../utils/logger.js'; import { readFile, fileExists } from '../../utils/fs.js'; /** * Component types supported by the registry */ export let ComponentType = /*#__PURE__*/function (ComponentType) { ComponentType["TASK"] = "task"; ComponentType["SET"] = "set"; ComponentType["PHASE"] = "phase"; ComponentType["WORKFLOW"] = "workflow"; return ComponentType; }({}); /** * File context type for resolving file paths */ /** * Registry for managing and loading components */ export class ComponentRegistry { tasks = new Map(); sets = new Map(); phases = new Map(); workflows = new Map(); workflowPath = null; constructor(basePath) { this.basePath = basePath; } /** * Set the current workflow path for workflow-specific templates */ setWorkflowPath(workflowPath) { this.workflowPath = workflowPath; } /** * Clear the current workflow path */ clearWorkflowPath() { this.workflowPath = null; } /** * Get the default file extension for a component type */ getDefaultExtension(componentType) { switch (componentType) { case ComponentType.TASK: return '.task.md'; case ComponentType.SET: return '.set.yaml'; case ComponentType.PHASE: return '.phase.yaml'; case ComponentType.WORKFLOW: return '.yaml'; default: throw new Error(`Unknown component type: ${componentType}`); } } /** * Get all possible extensions for a component type */ getExtensions(type) { switch (type) { case ComponentType.WORKFLOW: return ['.yaml', '.yml']; case ComponentType.SET: return ['.set.yaml', '.set.yml']; case ComponentType.TASK: return ['.md']; default: return ['.yaml', '.yml']; } } /** * Resolve a component path following the specificity cascade: * 1. Look in the current workflow directory */ async resolveComponentPath(componentType, componentName, extension) { if (extension) { const filename = `${componentName}${extension}`; const componentDir = `${componentType}s`; // Look in the workflow directory if specified if (this.workflowPath) { const templatePath = path.isAbsolute(this.workflowPath) ? path.join(this.workflowPath, componentDir, filename) : path.join(this.basePath, this.workflowPath, componentDir, filename); // Use async file check if (await fileExists(templatePath)) { return templatePath; } } // TODO: Add fallback to global components directory? } else { // Try all possible extensions for (const ext of this.getExtensions(componentType)) { // Use await for the recursive async call const resolvedPath = await this.resolveComponentPath(componentType, componentName, ext); if (resolvedPath) { return resolvedPath; } } } return null; } /** * Load a task from the registry, loading from disk if not found. * Assumes task files are .md files containing only the prompt content. */ async loadTask(taskName) { // Check cache first const cachedTask = this.tasks.get(taskName); if (cachedTask) { return cachedTask; } // Validate task name if (!taskName || typeof taskName !== 'string' || !taskName.trim()) { throw new Error('Task name must be a non-empty string'); } // Find task file (expects .md) const taskPath = await this.resolveComponentPath(ComponentType.TASK, taskName); if (!taskPath) { // If not found, check if it's a built-in task (future enhancement?) throw new Error(`Task file not found: ${taskName}. Expected location relative to workflow path based on registry configuration.`); } // Load task content directly from .md file let content; try { content = await readFile(taskPath); } catch (error) { logger.error(`Error reading task file: ${taskPath}`, { error: error.message }); throw new Error(`Failed to read task file: ${taskPath}`); } // Validate content if (!content || typeof content !== 'string' || !content.trim()) { logger.error(`Invalid or empty task content in file: ${taskPath}`); throw new Error(`Invalid or empty task content in file: ${taskPath}`); } // Create default task metadata since .md files don't contain it const task = { name: taskName, description: `Task loaded from markdown file: ${path.basename(taskPath)}`, processType: 'prompt-execution', // Assuming standard prompt execution role: 'service', // Assuming standard service role requiredInput: [], // Default to no required inputs requiredOutput: [], // Default to no required outputs // Add any other essential default fields? }; // No separate taskMetadata to validate here, we just created it. // Basic validation of created object if (!task.name) { throw new Error('Internal Error: Default task metadata missing name.'); } if (!task.processType) { throw new Error('Internal Error: Default task metadata missing processType.'); } if (!task.role) { throw new Error('Internal Error: Default task metadata missing role.'); } if (!Array.isArray(task.requiredInput)) { throw new Error('Internal Error: Default task metadata requiredInput not an array.'); } if (!Array.isArray(task.requiredOutput)) { throw new Error('Internal Error: Default task metadata requiredOutput not an array.'); } // Cache task data const taskData = { task, content }; this.tasks.set(taskName, taskData); logger.debug(`Loaded and cached task from markdown: ${taskName}`, { path: taskPath }); return taskData; } /** * Load a set from a file and register it */ async loadSet(setName) { // Check if already loaded // if (this.sets.has(setName)) { // <-- Temporarily disable cache check // return this.sets.get(setName); // } // Resolve path (now async) const setPath = await this.resolveComponentPath(ComponentType.SET, setName); if (!setPath) { throw new Error(`Set template not found: ${setName}`); } // Load and parse set const content = await readFile(setPath); const set = yaml.load(content); // Resolve file paths in context if present if (set.context) { set.context = this.resolveFilePaths(set.context); } // Resolve context in task references if (set.tasks) { for (const task of set.tasks) { if (task.context) { task.context = this.resolveFilePaths(task.context); } } } // Resolve context in nested set references if (set.set) { for (const nestedSet of set.set) { if ('context' in nestedSet) { nestedSet.context = this.resolveFilePaths(nestedSet.context); } } } this.sets.set(setName, set); return set; } /** * Load a phase from a file and register it */ async loadPhase(phaseName) { // Check if already loaded if (this.phases.has(phaseName)) { return this.phases.get(phaseName); } // Resolve path (now async) const phasePath = await this.resolveComponentPath(ComponentType.PHASE, phaseName); if (!phasePath) { throw new Error(`Phase template not found: ${phaseName}`); } // Load and parse phase const content = await readFile(phasePath); logger.debug('Loading phase YAML', { content }); const phase = yaml.load(content); logger.debug('Loaded phase raw', { phase }); // Ensure phase.set is an array if (!Array.isArray(phase.set)) { phase.set = phase.set ? [phase.set] : []; } logger.debug('Loaded phase after array check', { phase, set: phase.set, isArray: Array.isArray(phase.set) }); // Resolve file paths in context if present if (phase.context) { phase.context = this.resolveFilePaths(phase.context); } // Resolve context in set references for (const setRef of phase.set) { if ('context' in setRef) { setRef.context = this.resolveFilePaths(setRef.context); } } this.phases.set(phaseName, phase); return phase; } /** * Load a workflow from a file and register it */ async loadWorkflow(workflowPath) { // The workflowPath received is now the exact file path. const workflowFilePath = path.resolve(workflowPath); // Ensure it's absolute const workflowDir = path.dirname(workflowFilePath); const workflowName = path.basename(workflowDir); // Use directory name for workflow name/cache key // Check cache first using the directory-based name if (this.workflows.has(workflowName)) { logger.debug('Workflow already loaded from cache', { workflowName }); return this.workflows.get(workflowName); } // Set internal workflow path to the DIRECTORY for resolving sets/tasks this.setWorkflowPath(workflowDir); // Load the exact workflow file path logger.debug('Loading workflow file', { workflowFilePath }); if (!(await fileExists(workflowFilePath))) { // Clear the workflow path context if file doesn't exist to avoid issues this.clearWorkflowPath(); throw new Error(`Workflow file not found: ${workflowFilePath}`); } // Load and parse workflow const content = await readFile(workflowFilePath); const workflow = yaml.load(content); // Add name if not present workflow.name = workflow.name || workflowName; // Resolve file paths in context if present if (workflow.context) { workflow.context = this.resolveFilePaths(workflow.context, path.dirname(workflowFilePath)); } // Removed loop iterating over workflow.phases as phases are deprecated /* // Resolve context in phase references for (const phaseRef of workflow.phases) { if ('context' in phaseRef) { phaseRef.context = this.resolveFilePaths(phaseRef.context, path.dirname(workflowFilePath)); } } */ logger.debug('Loaded workflow', { name: workflow.name, // phases: workflow.phases.length, // Removed reference to phases context: workflow.context ? Object.keys(workflow.context) : [] }); this.workflows.set(workflowName, workflow); return workflow; } }