UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

989 lines (846 loc) 30.3 kB
/** * Workflow Integration Module for Shipdeck Ultimate * * Bridges the agent system with the DAG workflow engine, providing: * - Agent-to-workflow task conversion * - Parallel agent execution in workflows * - Quality gate integration * - Rollback and checkpoint support * - Error recovery and retry mechanisms * - Comprehensive workflow status tracking * * This module serves as the orchestration layer between agents and workflows, * enabling complex multi-agent deployments with full error handling and recovery. * * @author ShipDeck Team * @version 2.0.0 */ const EventEmitter = require('events'); const { DAGWorkflow } = require('../workflow/dag-workflow'); const { WorkflowExecutor } = require('../workflow/executor'); const { QualityGatesSystem } = require('../quality-gates'); const { CheckpointManager } = require('../rollback/checkpoint-manager'); const { registry: agentRegistry } = require('./agent-registry'); /** * Agent Task Converter - Transforms agent tasks into workflow nodes */ class AgentTaskConverter { constructor() { // Task type mappings for different agent categories this.agentTypeMap = { 'backend-architect': 'development', 'frontend-developer': 'development', 'ai-engineer': 'development', 'devops-automator': 'deployment', 'mobile-app-builder': 'development', 'rapid-prototyper': 'development', 'test-writer-fixer': 'testing', 'whimsy-injector': 'enhancement', 'ui-designer': 'design', 'ux-researcher': 'research', 'visual-storyteller': 'content', 'brand-guardian': 'validation', 'tiktok-strategist': 'marketing', 'growth-hacker': 'marketing', 'content-creator': 'content', 'instagram-curator': 'marketing', 'reddit-community-builder': 'marketing', 'twitter-engager': 'marketing', 'app-store-optimizer': 'marketing', 'feedback-synthesizer': 'analysis', 'sprint-prioritizer': 'planning', 'trend-researcher': 'research', 'analytics-reporter': 'analysis', 'finance-tracker': 'analysis', 'infrastructure-maintainer': 'maintenance', 'legal-compliance-checker': 'validation', 'support-responder': 'support', 'experiment-tracker': 'analysis', 'project-shipper': 'deployment', 'studio-producer': 'coordination', 'api-tester': 'testing', 'performance-benchmarker': 'testing', 'test-results-analyzer': 'analysis', 'tool-evaluator': 'evaluation', 'workflow-optimizer': 'optimization', 'task-coordinator': 'coordination', 'parallel-runner': 'execution', 'result-synthesizer': 'coordination', 'llm-judge': 'evaluation', 'variant-generator': 'generation', 'worktree-manager': 'coordination', 'conflict-detector': 'validation', 'orchestrator': 'coordination', 'joker': 'enhancement', 'studio-coach': 'support' }; } /** * Convert agent task to workflow node * @param {Object} agentTask - Agent task configuration * @param {Object} options - Conversion options * @returns {Object} Workflow node configuration */ convertToWorkflowNode(agentTask, options = {}) { if (!agentTask.agent) { throw new Error('Agent task must specify an agent type'); } const agentType = this.agentTypeMap[agentTask.agent] || 'general'; const nodeId = options.nodeId || this.generateNodeId(agentTask.agent); return { id: nodeId, name: agentTask.name || `${agentTask.agent} Task`, description: agentTask.description || `Execute ${agentTask.agent} task`, agent: agentTask.agent, type: agentType, prompt: this.buildAgentPrompt(agentTask), inputs: agentTask.inputs || {}, outputs: agentTask.outputs || {}, dependencies: agentTask.dependencies || [], condition: agentTask.condition || null, retryConfig: { maxRetries: agentTask.retries || 3, retryDelay: agentTask.retryDelay || 1000, retryMultiplier: agentTask.retryMultiplier || 2, ...agentTask.retryConfig }, timeout: agentTask.timeout || 300000, // 5 minutes default parallel: agentTask.parallel || false, qualityGates: agentTask.qualityGates !== false, // Enabled by default checkpoint: agentTask.checkpoint !== false, // Enabled by default metadata: { agentType, originalTask: agentTask, createdAt: new Date().toISOString(), ...agentTask.metadata } }; } /** * Convert multiple agent tasks to workflow nodes with dependency resolution * @param {Array<Object>} agentTasks - Array of agent tasks * @param {Object} options - Conversion options * @returns {Array<Object>} Array of workflow nodes */ convertMultipleToWorkflowNodes(agentTasks, options = {}) { const nodes = []; const nodeMap = new Map(); // First pass: Create nodes for (let i = 0; i < agentTasks.length; i++) { const task = agentTasks[i]; const nodeId = options.nodeIdPrefix ? `${options.nodeIdPrefix}-${i}` : this.generateNodeId(task.agent, i); const node = this.convertToWorkflowNode(task, { ...options, nodeId }); nodes.push(node); nodeMap.set(task.id || i, node.id); } // Second pass: Resolve dependencies for (let i = 0; i < agentTasks.length; i++) { const task = agentTasks[i]; const node = nodes[i]; if (task.dependsOn && task.dependsOn.length > 0) { node.dependencies = task.dependsOn.map(depId => { const resolvedId = nodeMap.get(depId); if (!resolvedId) { throw new Error(`Dependency '${depId}' not found for task '${task.id || i}'`); } return resolvedId; }); } } return nodes; } /** * Build agent-specific prompt * @param {Object} agentTask - Agent task configuration * @returns {string} Agent prompt */ buildAgentPrompt(agentTask) { let prompt = agentTask.prompt || ''; if (agentTask.context) { prompt += `\n\nContext: ${JSON.stringify(agentTask.context, null, 2)}`; } if (agentTask.requirements) { const requirements = Array.isArray(agentTask.requirements) ? agentTask.requirements.join('\n- ') : agentTask.requirements; prompt += `\n\nRequirements:\n- ${requirements}`; } return prompt.trim(); } /** * Generate unique node ID * @param {string} agentType - Agent type * @param {number} index - Optional index * @returns {string} Node ID */ generateNodeId(agentType, index = 0) { const timestamp = Date.now().toString(36); const suffix = index > 0 ? `-${index}` : ''; return `${agentType}-${timestamp}${suffix}`; } } /** * Agent Workflow Status Tracker */ class AgentWorkflowStatusTracker extends EventEmitter { constructor() { super(); this.workflowStatuses = new Map(); this.nodeStatuses = new Map(); this.agentExecutionHistory = new Map(); } /** * Track workflow status * @param {string} workflowId - Workflow ID * @param {Object} status - Status information */ updateWorkflowStatus(workflowId, status) { const previousStatus = this.workflowStatuses.get(workflowId); this.workflowStatuses.set(workflowId, { ...status, updatedAt: new Date().toISOString(), previousStatus: previousStatus?.status }); this.emit('workflow:status:updated', { workflowId, status, previousStatus }); } /** * Track node status * @param {string} workflowId - Workflow ID * @param {string} nodeId - Node ID * @param {Object} status - Status information */ updateNodeStatus(workflowId, nodeId, status) { const key = `${workflowId}:${nodeId}`; const previousStatus = this.nodeStatuses.get(key); const nodeStatus = { workflowId, nodeId, ...status, updatedAt: new Date().toISOString(), previousStatus: previousStatus?.status }; this.nodeStatuses.set(key, nodeStatus); // Track agent execution history if (status.agent) { this.trackAgentExecution(status.agent, nodeStatus); } this.emit('node:status:updated', { workflowId, nodeId, status: nodeStatus, previousStatus }); } /** * Track agent execution history * @param {string} agentType - Agent type * @param {Object} execution - Execution details */ trackAgentExecution(agentType, execution) { if (!this.agentExecutionHistory.has(agentType)) { this.agentExecutionHistory.set(agentType, []); } const history = this.agentExecutionHistory.get(agentType); history.push({ ...execution, executedAt: new Date().toISOString() }); // Keep only last 100 executions per agent if (history.length > 100) { history.splice(0, history.length - 100); } } /** * Get workflow status * @param {string} workflowId - Workflow ID * @returns {Object|null} Workflow status */ getWorkflowStatus(workflowId) { return this.workflowStatuses.get(workflowId) || null; } /** * Get all node statuses for a workflow * @param {string} workflowId - Workflow ID * @returns {Array<Object>} Node statuses */ getWorkflowNodeStatuses(workflowId) { const statuses = []; for (const [key, status] of this.nodeStatuses.entries()) { if (key.startsWith(`${workflowId}:`)) { statuses.push(status); } } return statuses.sort((a, b) => new Date(a.updatedAt) - new Date(b.updatedAt) ); } /** * Get agent execution statistics * @param {string} agentType - Agent type (optional) * @returns {Object} Execution statistics */ getAgentStatistics(agentType = null) { if (agentType) { const history = this.agentExecutionHistory.get(agentType) || []; return this.calculateAgentStats(agentType, history); } const allStats = {}; for (const [agent, history] of this.agentExecutionHistory.entries()) { allStats[agent] = this.calculateAgentStats(agent, history); } return allStats; } /** * Calculate agent statistics from history * @param {string} agentType - Agent type * @param {Array<Object>} history - Execution history * @returns {Object} Statistics */ calculateAgentStats(agentType, history) { if (history.length === 0) { return { agent: agentType, totalExecutions: 0, successRate: 0, averageExecutionTime: 0, lastExecuted: null }; } const successful = history.filter(h => h.status === 'completed').length; const totalTime = history.reduce((sum, h) => sum + (h.duration || 0), 0); return { agent: agentType, totalExecutions: history.length, successRate: (successful / history.length) * 100, averageExecutionTime: totalTime / history.length, lastExecuted: history[history.length - 1]?.executedAt || null, recentFailures: history.filter(h => h.status === 'failed' && Date.now() - new Date(h.executedAt).getTime() < 3600000 // Last hour ).length }; } } /** * Parallel Agent Executor - Handles concurrent agent execution */ class ParallelAgentExecutor { constructor(options = {}) { this.maxConcurrency = options.maxConcurrency || 5; this.defaultTimeout = options.defaultTimeout || 300000; // 5 minutes } /** * Execute agents in parallel with proper isolation * @param {Array<Object>} agentTasks - Agent tasks to execute * @param {Object} context - Shared execution context * @returns {Promise<Object>} Execution results */ async executeParallel(agentTasks, context = {}) { const results = new Map(); const errors = new Map(); const startTime = Date.now(); // Group tasks by parallelization compatibility const parallelGroups = this.groupTasksForParallelExecution(agentTasks); console.log(`🚀 Executing ${agentTasks.length} agent tasks in ${parallelGroups.length} parallel groups`); for (const group of parallelGroups) { await this.executeGroup(group, context, results, errors); } const duration = Date.now() - startTime; const successCount = results.size; const errorCount = errors.size; console.log(`✅ Parallel execution completed: ${successCount} succeeded, ${errorCount} failed (${duration}ms)`); return { success: errorCount === 0, results: Object.fromEntries(results), errors: Object.fromEntries(errors), statistics: { totalTasks: agentTasks.length, successCount, errorCount, duration, parallelGroups: parallelGroups.length } }; } /** * Group tasks for parallel execution based on dependencies * @param {Array<Object>} agentTasks - Agent tasks * @returns {Array<Array<Object>>} Groups of tasks that can run in parallel */ groupTasksForParallelExecution(agentTasks) { const groups = []; const processed = new Set(); const taskMap = new Map(agentTasks.map(task => [task.id || task.name, task])); while (processed.size < agentTasks.length) { const currentGroup = []; for (const task of agentTasks) { const taskId = task.id || task.name; if (processed.has(taskId)) continue; // Check if all dependencies are satisfied const canExecute = !task.dependencies || task.dependencies.every(depId => processed.has(depId)); if (canExecute) { currentGroup.push(task); processed.add(taskId); } } if (currentGroup.length === 0) { throw new Error('Circular dependency detected in agent tasks'); } groups.push(currentGroup); } return groups; } /** * Execute a group of tasks in parallel * @param {Array<Object>} group - Tasks to execute in parallel * @param {Object} context - Execution context * @param {Map} results - Results map * @param {Map} errors - Errors map */ async executeGroup(group, context, results, errors) { const groupPromises = group.map(task => this.executeAgentTask(task, context) .then(result => ({ task, result, success: true })) .catch(error => ({ task, error, success: false })) ); const groupResults = await Promise.allSettled(groupPromises); for (const result of groupResults) { const { task, success } = result.value || result.reason; const taskId = task.id || task.name; if (success) { results.set(taskId, result.value.result); } else { errors.set(taskId, result.value?.error || result.reason); } } } /** * Execute a single agent task * @param {Object} agentTask - Agent task configuration * @param {Object} context - Execution context * @returns {Promise<Object>} Execution result */ async executeAgentTask(agentTask, context = {}) { const agentName = agentTask.agent; if (!agentRegistry.has(agentName)) { throw new Error(`Agent '${agentName}' not registered`); } const agent = agentRegistry.get(agentName); const startTime = Date.now(); try { console.log(`🤖 Executing ${agentName} task...`); const result = await Promise.race([ agent.executeWithRetry(agentTask, context), this.createTimeoutPromise(agentTask.timeout || this.defaultTimeout) ]); const duration = Date.now() - startTime; console.log(`✅ ${agentName} completed in ${duration}ms`); return { ...result, agent: agentName, duration, completedAt: new Date().toISOString() }; } catch (error) { const duration = Date.now() - startTime; console.error(`❌ ${agentName} failed after ${duration}ms: ${error.message}`); throw { agent: agentName, error: error.message, duration, failedAt: new Date().toISOString() }; } } /** * Create timeout promise * @param {number} timeout - Timeout in milliseconds * @returns {Promise} Timeout promise */ createTimeoutPromise(timeout) { return new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Agent task timed out after ${timeout}ms`)); }, timeout); }); } } /** * Main Workflow Integration Class */ class WorkflowIntegration extends EventEmitter { constructor(options = {}) { super(); this.options = { enableQualityGates: options.enableQualityGates !== false, enableCheckpoints: options.enableCheckpoints !== false, enableParallelExecution: options.enableParallelExecution !== false, maxConcurrency: options.maxConcurrency || 5, defaultTimeout: options.defaultTimeout || 300000, // 5 minutes autoRetry: options.autoRetry !== false, autoRollback: options.autoRollback !== false, ...options }; // Initialize components this.taskConverter = new AgentTaskConverter(); this.statusTracker = new AgentWorkflowStatusTracker(); this.parallelExecutor = new ParallelAgentExecutor(this.options); // Initialize optional components if (this.options.enableQualityGates) { this.qualityGates = new QualityGatesSystem(this.options.qualityGatesConfig); } if (this.options.enableCheckpoints) { this.checkpointManager = new CheckpointManager(this.options.checkpointConfig); } this.workflowExecutor = new WorkflowExecutor(this.options.workflowConfig); // Active workflows and their contexts this.activeWorkflows = new Map(); this.workflowResults = new Map(); // Setup event forwarding this._setupEventHandlers(); } /** * Initialize the workflow integration system */ async initialize() { console.log('🔧 Initializing Workflow Integration System...'); // Initialize checkpoint manager if enabled if (this.checkpointManager) { await this.checkpointManager.initialize(); } console.log('✅ Workflow Integration System initialized'); return this; } /** * Create workflow from agent tasks * @param {Object} config - Workflow configuration * @returns {Object} DAG Workflow instance */ createWorkflowFromAgents(config) { if (!config.agentTasks || !Array.isArray(config.agentTasks)) { throw new Error('agentTasks array is required'); } // Convert agent tasks to workflow nodes const nodes = this.taskConverter.convertMultipleToWorkflowNodes( config.agentTasks, config.conversionOptions ); // Create base workflow configuration const workflowConfig = { name: config.name || 'Agent Workflow', description: config.description || 'Workflow generated from agent tasks', nodes, context: config.context || {}, maxConcurrency: config.maxConcurrency || this.options.maxConcurrency, timeout: config.timeout || this.options.defaultTimeout, ...config }; // Create DAG workflow const workflow = this.workflowExecutor.createWorkflow(workflowConfig); // Add quality gates if enabled if (this.options.enableQualityGates && this.qualityGates) { this.qualityGates.createWorkflowIntegration(workflow); } return workflow; } /** * Execute workflow with full integration features * @param {Object} config - Workflow configuration * @returns {Promise<Object>} Execution result */ async executeWorkflowWithIntegration(config) { const workflowId = config.id || this.generateWorkflowId(); const startTime = Date.now(); try { console.log(`🚀 Starting integrated workflow execution: ${workflowId}`); this.emit('workflow:started', { workflowId, config }); // Create checkpoint before starting if enabled let initialCheckpoint = null; if (this.checkpointManager && this.options.enableCheckpoints) { const checkpointResult = await this.checkpointManager.createCheckpoint({ type: 'workflow-start', description: `Pre-execution checkpoint for workflow ${workflowId}`, workflow: workflowId, tags: ['workflow', 'pre-execution'] }); if (checkpointResult.success) { initialCheckpoint = checkpointResult.checkpoint; console.log(`📝 Created initial checkpoint: ${initialCheckpoint.id}`); } } // Track workflow this.activeWorkflows.set(workflowId, { id: workflowId, config, startTime, initialCheckpoint, status: 'running' }); this.statusTracker.updateWorkflowStatus(workflowId, { status: 'running', startTime, progress: 0 }); // Determine execution strategy let result; if (this.options.enableParallelExecution && config.parallelExecution) { result = await this.executeParallelWorkflow(workflowId, config); } else { result = await this.executeSequentialWorkflow(workflowId, config); } // Create completion checkpoint if successful if (result.success && this.checkpointManager) { await this.checkpointManager.createCheckpoint({ type: 'workflow-complete', description: `Completion checkpoint for workflow ${workflowId}`, workflow: workflowId, tags: ['workflow', 'success', 'completion'] }); } const duration = Date.now() - startTime; // Update tracking this.statusTracker.updateWorkflowStatus(workflowId, { status: result.success ? 'completed' : 'failed', duration, progress: 100, result }); this.workflowResults.set(workflowId, result); this.activeWorkflows.delete(workflowId); console.log(`${result.success ? '✅' : '❌'} Workflow ${workflowId} ${result.success ? 'completed' : 'failed'} in ${duration}ms`); this.emit(result.success ? 'workflow:completed' : 'workflow:failed', { workflowId, result, duration }); return result; } catch (error) { console.error(`❌ Workflow execution failed: ${error.message}`); // Attempt rollback if enabled and checkpoint exists if (this.options.autoRollback && this.checkpointManager) { await this.handleWorkflowFailure(workflowId, error); } const duration = Date.now() - startTime; this.statusTracker.updateWorkflowStatus(workflowId, { status: 'failed', duration, error: error.message }); this.activeWorkflows.delete(workflowId); this.emit('workflow:failed', { workflowId, error: error.message, duration }); throw error; } } /** * Execute workflow in parallel mode * @param {string} workflowId - Workflow ID * @param {Object} config - Workflow configuration * @returns {Promise<Object>} Execution result */ async executeParallelWorkflow(workflowId, config) { console.log(`⚡ Executing workflow ${workflowId} in parallel mode`); return await this.parallelExecutor.executeParallel( config.agentTasks, config.context ); } /** * Execute workflow in sequential mode using DAG executor * @param {string} workflowId - Workflow ID * @param {Object} config - Workflow configuration * @returns {Promise<Object>} Execution result */ async executeSequentialWorkflow(workflowId, config) { console.log(`🔄 Executing workflow ${workflowId} in sequential mode`); const workflow = this.createWorkflowFromAgents(config); // Setup workflow event tracking this.setupWorkflowEventTracking(workflow, workflowId); return await this.workflowExecutor.executeWorkflow({ ...config, id: workflowId }); } /** * Handle workflow failure with rollback * @param {string} workflowId - Workflow ID * @param {Error} error - Error that caused failure */ async handleWorkflowFailure(workflowId, error) { console.log(`🚨 Handling workflow failure: ${workflowId}`); const workflowContext = this.activeWorkflows.get(workflowId); if (workflowContext?.initialCheckpoint) { console.log(`🔄 Rolling back to initial checkpoint: ${workflowContext.initialCheckpoint.id}`); try { await this.checkpointManager.rollback( workflowContext.initialCheckpoint.id, { skipRecoveryPoint: false, attemptRecovery: true } ); console.log('✅ Rollback completed successfully'); } catch (rollbackError) { console.error(`❌ Rollback failed: ${rollbackError.message}`); // Don't re-throw rollback errors, original error is more important } } } /** * Setup workflow event tracking * @param {Object} workflow - DAG workflow instance * @param {string} workflowId - Workflow ID */ setupWorkflowEventTracking(workflow, workflowId) { workflow.on('node:started', (data) => { this.statusTracker.updateNodeStatus(workflowId, data.nodeId, { status: 'running', agent: data.agent, startedAt: data.startedAt }); }); workflow.on('node:completed', (data) => { this.statusTracker.updateNodeStatus(workflowId, data.nodeId, { status: 'completed', agent: data.agent, result: data.result, duration: data.duration, completedAt: data.completedAt }); }); workflow.on('node:failed', (data) => { this.statusTracker.updateNodeStatus(workflowId, data.nodeId, { status: 'failed', agent: data.agent, error: data.error, duration: data.duration, failedAt: data.failedAt }); }); } /** * Get workflow status and progress * @param {string} workflowId - Workflow ID * @returns {Object|null} Workflow status */ getWorkflowStatus(workflowId) { const status = this.statusTracker.getWorkflowStatus(workflowId); const nodeStatuses = this.statusTracker.getWorkflowNodeStatuses(workflowId); if (!status) return null; return { ...status, nodes: nodeStatuses, isActive: this.activeWorkflows.has(workflowId) }; } /** * Get agent execution statistics * @param {string} agentType - Optional agent type filter * @returns {Object} Agent statistics */ getAgentStatistics(agentType = null) { return this.statusTracker.getAgentStatistics(agentType); } /** * Create checkpoint for current state * @param {Object} metadata - Checkpoint metadata * @returns {Promise<Object>} Checkpoint result */ async createCheckpoint(metadata = {}) { if (!this.checkpointManager) { throw new Error('Checkpoint manager not enabled'); } return await this.checkpointManager.createCheckpoint(metadata); } /** * Rollback to a specific checkpoint * @param {string} checkpointId - Checkpoint ID * @param {Object} options - Rollback options * @returns {Promise<Object>} Rollback result */ async rollbackToCheckpoint(checkpointId, options = {}) { if (!this.checkpointManager) { throw new Error('Checkpoint manager not enabled'); } return await this.checkpointManager.rollback(checkpointId, options); } /** * Apply quality gates to artifact * @param {Object} artifact - Artifact to validate * @param {Object} context - Validation context * @returns {Promise<Object>} Quality gate result */ async applyQualityGates(artifact, context = {}) { if (!this.qualityGates) { throw new Error('Quality gates not enabled'); } return await this.qualityGates.applyQualityGates(artifact, context); } /** * Generate unique workflow ID * @returns {string} Workflow ID */ generateWorkflowId() { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substr(2, 5); return `workflow-${timestamp}-${random}`; } /** * Setup event handlers for internal components * @private */ _setupEventHandlers() { // Forward status tracker events this.statusTracker.on('workflow:status:updated', (data) => { this.emit('status:workflow', data); }); this.statusTracker.on('node:status:updated', (data) => { this.emit('status:node', data); }); // Forward workflow executor events this.workflowExecutor.on('workflow:started', (data) => { this.emit('executor:workflow:started', data); }); this.workflowExecutor.on('workflow:finished', (data) => { this.emit('executor:workflow:finished', data); }); this.workflowExecutor.on('node:completed', (data) => { this.emit('executor:node:completed', data); }); this.workflowExecutor.on('node:failed', (data) => { this.emit('executor:node:failed', data); }); // Forward quality gates events if enabled if (this.qualityGates) { this.qualityGates.on('gates:success', (data) => { this.emit('quality:success', data); }); this.qualityGates.on('gates:failed', (data) => { this.emit('quality:failed', data); }); } // Forward checkpoint events if enabled if (this.checkpointManager) { this.checkpointManager.on('checkpoint:created', (data) => { this.emit('checkpoint:created', data); }); this.checkpointManager.on('rollback:success', (data) => { this.emit('rollback:success', data); }); this.checkpointManager.on('rollback:failed', (data) => { this.emit('rollback:failed', data); }); } } } // Export all classes and main integration module.exports = { WorkflowIntegration, AgentTaskConverter, AgentWorkflowStatusTracker, ParallelAgentExecutor, // Factory function for easy initialization createWorkflowIntegration: (options = {}) => new WorkflowIntegration(options), // Convenience functions convertAgentTasksToWorkflow: (agentTasks, options = {}) => { const converter = new AgentTaskConverter(); return converter.convertMultipleToWorkflowNodes(agentTasks, options); } };