UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

281 lines (280 loc) 10.1 kB
/** * Agent Workspace Management * * Provides persistent, organized workspace for agent outputs with reliable completion signals. * Part of Task 2.1: Persistent Agent Output Workspace (Integration) * * Directory Structure: * artifacts/agent-workspaces/{task_id}/{agent_id}/ * ├── logs/ (stdout.log, stderr.log) * ├── reports/ (analysis.json, etc.) * ├── metrics/ (performance.json, etc.) * ├── deliverables/ (output files) * └── COMPLETION_SIGNAL.json * * Usage: * const workspace = await createWorkspace('task-123', 'agent-456'); * await writeOutput(workspace, 'logs', 'stdout.log', 'Log content'); * await signalCompletion(workspace, { success: true, confidence: 0.85 }); */ import * as path from 'path'; import { atomicWrite, ensureDirectory } from './file-operations.js'; import { createLogger } from './logging.js'; import { createError, ErrorCode } from './errors.js'; import { writeCompletionSignal } from './completion-signal-handler.js'; const logger = createLogger('agent-workspace'); /** * Workspace configuration */ const WORKSPACE_ROOT = path.resolve(process.cwd(), 'artifacts', 'agent-workspaces'); /** * Create a workspace for an agent * * Pattern: Atomic directory creation with race-condition safety * * @param taskId - Unique task identifier * @param agentId - Unique agent identifier * @param options - Workspace creation options * @returns Workspace path information * @throws INVALID_INPUT if taskId or agentId is invalid * @throws DIRECTORY_CREATE_FAILED if directory creation fails */ export async function createWorkspace(taskId, agentId, options = {}) { // Validate inputs if (!taskId || typeof taskId !== 'string') { throw createError(ErrorCode.INVALID_INPUT, 'Task ID must be a non-empty string', { taskId }); } if (!agentId || typeof agentId !== 'string') { throw createError(ErrorCode.INVALID_INPUT, 'Agent ID must be a non-empty string', { agentId }); } // Sanitize IDs to prevent path traversal const sanitizedTaskId = sanitizeId(taskId); const sanitizedAgentId = sanitizeId(agentId); const workspaceRoot = options.workspaceRoot || WORKSPACE_ROOT; const workspacePath = path.join(workspaceRoot, sanitizedTaskId, sanitizedAgentId); // Define subdirectories const subdirectories = { logs: path.join(workspacePath, 'logs'), reports: path.join(workspacePath, 'reports'), metrics: path.join(workspacePath, 'metrics'), deliverables: path.join(workspacePath, 'deliverables') }; // Create directories (atomic, race-condition safe) if (!options.skipCreate) { try { await ensureDirectory(workspacePath); await Promise.all(Object.values(subdirectories).map((dir)=>ensureDirectory(dir))); logger.info('Workspace created', { taskId, agentId, workspacePath }); } catch (error) { throw createError(ErrorCode.DIRECTORY_CREATE_FAILED, 'Failed to create workspace directories', { taskId, agentId, workspacePath, error: error.message }); } } return { path: workspacePath, taskId: sanitizedTaskId, agentId: sanitizedAgentId, subdirectories }; } /** * Get the path to an existing workspace * * @param taskId - Unique task identifier * @param agentId - Unique agent identifier * @param options - Workspace options * @returns Workspace path information */ export function getWorkspacePath(taskId, agentId, options = {}) { const sanitizedTaskId = sanitizeId(taskId); const sanitizedAgentId = sanitizeId(agentId); const workspaceRoot = options.workspaceRoot || WORKSPACE_ROOT; const workspacePath = path.join(workspaceRoot, sanitizedTaskId, sanitizedAgentId); const subdirectories = { logs: path.join(workspacePath, 'logs'), reports: path.join(workspacePath, 'reports'), metrics: path.join(workspacePath, 'metrics'), deliverables: path.join(workspacePath, 'deliverables') }; return { path: workspacePath, taskId: sanitizedTaskId, agentId: sanitizedAgentId, subdirectories }; } /** * Write output to workspace * * Pattern: Atomic write with automatic directory creation * * @param workspace - Workspace path information * @param type - Output type (logs, reports, metrics, deliverables) * @param filename - Name of the output file * @param content - Content to write (string or object) * @returns Promise that resolves when write is complete * @throws INVALID_INPUT if type is invalid * @throws FILE_WRITE_FAILED if write fails */ export async function writeOutput(workspace, type, filename, content) { // Validate type const validTypes = [ 'logs', 'reports', 'metrics', 'deliverables' ]; if (!validTypes.includes(type)) { throw createError(ErrorCode.INVALID_INPUT, `Invalid output type. Must be one of: ${validTypes.join(', ')}`, { type }); } // Sanitize filename const sanitizedFilename = sanitizeFilename(filename); const outputPath = path.join(workspace.subdirectories[type], sanitizedFilename); // Ensure directory exists await ensureDirectory(path.dirname(outputPath)); // Convert content to string if necessary const contentStr = typeof content === 'string' ? content : JSON.stringify(content, null, 2); // Atomic write try { await atomicWrite(outputPath, contentStr); logger.debug('Output written', { taskId: workspace.taskId, agentId: workspace.agentId, type, filename: sanitizedFilename, sizeBytes: contentStr.length }); } catch (error) { throw createError(ErrorCode.FILE_WRITE_FAILED, 'Failed to write output to workspace', { taskId: workspace.taskId, agentId: workspace.agentId, type, filename: sanitizedFilename, error: error.message }); } } /** * Read output from workspace * * @param workspace - Workspace path information * @param type - Output type (logs, reports, metrics, deliverables) * @param filename - Name of the output file * @returns File content (parsed as JSON if applicable) * @throws INVALID_INPUT if type is invalid * @throws FILE_NOT_FOUND if file does not exist */ export async function readOutput(workspace, type, filename) { // Validate type const validTypes = [ 'logs', 'reports', 'metrics', 'deliverables' ]; if (!validTypes.includes(type)) { throw createError(ErrorCode.INVALID_INPUT, `Invalid output type. Must be one of: ${validTypes.join(', ')}`, { type }); } const sanitizedFilename = sanitizeFilename(filename); const outputPath = path.join(workspace.subdirectories[type], sanitizedFilename); try { const fs = await import('fs/promises'); const content = await fs.readFile(outputPath, 'utf8'); // Try to parse as JSON if (filename.endsWith('.json')) { try { return JSON.parse(content); } catch { // Return as string if JSON parsing fails return content; } } return content; } catch (error) { const err = error; if (err.code === 'ENOENT') { throw createError(ErrorCode.FILE_NOT_FOUND, 'Output file not found', { taskId: workspace.taskId, agentId: workspace.agentId, type, filename: sanitizedFilename }); } throw error; } } /** * Signal agent completion * * Writes a completion signal file that coordinators can poll for. * * @param workspace - Workspace path information * @param metadata - Completion metadata * @returns Promise that resolves when signal is written */ export async function signalCompletion(workspace, metadata) { await writeCompletionSignal(workspace, { ...metadata, taskId: workspace.taskId, agentId: workspace.agentId, timestamp: new Date() }); logger.info('Completion signal written', { taskId: workspace.taskId, agentId: workspace.agentId, success: metadata.success, confidence: metadata.confidence }); } /** * List all workspaces for a task * * @param taskId - Task identifier * @param options - Workspace options * @returns Array of agent IDs with workspaces */ export async function listWorkspaces(taskId, options = {}) { const sanitizedTaskId = sanitizeId(taskId); const workspaceRoot = options.workspaceRoot || WORKSPACE_ROOT; const taskPath = path.join(workspaceRoot, sanitizedTaskId); try { const fs = await import('fs/promises'); const entries = await fs.readdir(taskPath, { withFileTypes: true }); return entries.filter((entry)=>entry.isDirectory()).map((entry)=>entry.name); } catch (error) { const err = error; if (err.code === 'ENOENT') { return []; } throw error; } } /** * Sanitize an ID to prevent path traversal * * @param id - Input ID * @returns Sanitized ID */ function sanitizeId(id) { // Remove path separators and special characters return id.replace(/[^a-zA-Z0-9_-]/g, '_'); } /** * Sanitize a filename to prevent path traversal * * @param filename - Input filename * @returns Sanitized filename */ function sanitizeFilename(filename) { // Remove path separators but keep dots for extensions return path.basename(filename).replace(/[^a-zA-Z0-9._-]/g, '_'); } //# sourceMappingURL=agent-workspace.js.map