UNPKG

@virtron/agency

Version:

A framework for building autonomous agents that can perform tasks, manage memory, and interact with tools.

1,504 lines (1,313 loc) 64.7 kB
/** * Agency class with workflow orchestration, memory management, * event-based communication, and improved error handling */ import { EventEmitter, MemoryManager } from './Agent.js'; /** * @class Agency * @classdesc Manages agents, teams, jobs, and workflows within the system. */ export class Agency { /** * Create a new Agency * @param {Object} config - Agency configuration * @param {string} config.name - Agency name * @param {string} config.description - Agency description * @param {Object} [config.logging] - Logging configuration * @param {boolean|string} [config.logging.level='none'] - Logging level ('none', 'basic', 'detailed', 'debug') * @param {boolean} [config.logging.tracing=false] - Enable detailed event tracing * @param {string} [config.logging.format='text'] - Log format ('text' or 'json') * @param {Object} [config.logging.destination='console'] - Log destination ('console', 'file', or writable stream) * @param {string} [config.logging.filepath] - Path to log file if destination is 'file' */ constructor(config) { this.name = config.name; this.description = config.description; // Set up logging configuration this.logging = { level: (config.logging?.level || 'none'), tracing: Boolean(config.logging?.tracing), format: (config.logging?.format || 'text'), destination: (config.logging?.destination || 'console'), filepath: config.logging?.filepath }; // Core components this.agents = {}; this.team = {}; this.brief = {}; this.activeJobs = {}; // Components this.events = new EventEmitter(); this.globalMemory = new MemoryManager(config.memoryConfig); this.memoryScopes = { global: this.globalMemory }; this.jobContexts = {}; this.workflow = {}; this.errorHandlers = {}; this.workflowErrorHandlers = {}; this.jobSchemas = {}; this.memoryAccessControl = {}; // Set up event listeners for tracing if enabled if (this.logging.tracing) { this.setupTracingListeners(); } // Log agency creation if logging is enabled if (this.logging.level !== 'none') { this.log('info', `Created agency: ${this.name}`); this.log('info', `Description: ${this.description}`); this.log('debug', `Logging level: ${this.logging.level}`); this.log('debug', `Tracing: ${this.logging.tracing ? 'enabled' : 'disabled'}`); } } /** * Log a message with the specified level * @param {string} level - Log level ('info', 'warn', 'error', 'debug') * @param {string} message - Message to log * @param {Object} [data] - Additional data to include in the log */ /** * Logs a message with a specified level and optional data. * @param {string} level - The log level (e.g., 'info', 'warn', 'error'). * @param {string} message - The log message. * @param {object} [data={}] - Optional data to include in the log. */ log(level, message, data = {}) { // Skip logging if level is not sufficient if (this.logging.level === 'none' || (this.logging.level === 'basic' && level === 'debug') || (this.logging.level !== 'debug' && level === 'debug')) { return; } const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, agency: this.name, message, ...data }; // Format the log entry let formattedLog; if (this.logging.format === 'json') { formattedLog = JSON.stringify(logEntry); } else { // Text format with emoji indicators const emoji = level === 'info' ? 'ℹ️' : level === 'warn' ? '⚠️' : level === 'error' ? '❌' : level === 'debug' ? '🔍' : ''; formattedLog = `${emoji} [${timestamp}] ${level.toUpperCase()}: ${message}`; // Add data if present and not empty if (data && Object.keys(data).length > 0) { formattedLog += ` ${JSON.stringify(data)}`; } } // Output the log if (this.logging.destination === 'console') { if (level === 'error') { console.error(formattedLog); } else if (level === 'warn') { console.warn(formattedLog); } else { console.log(formattedLog); } } else if (this.logging.destination === 'file' && this.logging.filepath) { // File logging would be implemented here // This would require fs module and is left as an implementation detail } else if (typeof this.logging.destination.write === 'function') { // Writable stream this.logging.destination.write(formattedLog + '\n'); } } /** * Set up event listeners for tracing agent and workflow activities * @private * @function setupTracingListeners */ /** * Sets up listeners for tracing events. * This method is responsible for initializing the tracing listeners. */ setupTracingListeners() { // Agent events this.events.on('agent:statusChanged', (data) => { this.log('debug', `Agent ${data.agentId} status changed to ${data.newStatus}`, data); }); this.events.on('agent:runCompleted', (data) => { this.log('debug', `Agent ${data.agentId} completed run`, { agentId: data.agentId, responseLength: data.response ? JSON.stringify(data.response).length : 0 }); }); this.events.on('agent:runError', (data) => { this.log('debug', `Agent ${data.agentId} encountered error: ${data.error}`, data); }); this.events.on('agent:message', (data) => { this.log('debug', `Message from ${data.senderId} to ${data.recipientId}`, { senderId: data.senderId, recipientId: data.recipientId, preview: JSON.stringify(data.message).substring(0, 50) + (JSON.stringify(data.message).length > 50 ? '...' : '') }); }); // Job events this.events.on('job:assigned', (data) => { this.log('debug', `Job ${data.jobId} assigned to ${data.assigneeType} ${data.assigneeId}`, data); }); this.events.on('job:started', (data) => { this.log('debug', `Job ${data.jobId} started execution`, data); }); this.events.on('job:completed', (data) => { this.log('debug', `Job ${data.jobId} completed successfully`, data); }); this.events.on('job:failed', (data) => { this.log('debug', `Job ${data.jobId} failed with error: ${data.error}`, data); }); // Workflow events this.events.on('workflow:started', (data) => { this.log('debug', `Workflow ${data.workflowId} started with ${data.steps} steps`, data); }); this.events.on('workflow:step', (data) => { this.log('debug', `Workflow ${data.workflowId} executing step ${data.stepIndex + 1}: ${data.jobId}`, data); }); this.events.on('workflow:completed', (data) => { this.log('debug', `Workflow ${data.workflowId} completed successfully`, data); }); this.events.on('workflow:failed', (data) => { this.log('debug', `Workflow ${data.workflowId} failed at step ${data.step + 1}`, { workflowId: data.workflowId, step: data.step, error: data.error }); }); // Planning events this.events.on('planning:started', (data) => { this.log('debug', `Agent ${data.agentId} started planning for goal`, { agentId: data.agentId, goalPreview: data.goal.substring(0, 50) + (data.goal.length > 50 ? '...' : '') }); }); this.events.on('planning:completed', (data) => { this.log('debug', `Agent ${data.agentId} completed planning with ${data.jobIds.length} jobs`, data); }); // Memory events this.events.on('memory:shared', (data) => { this.log('debug', `Memory shared from ${data.sourceScope} to ${data.targetScope}`, { sourceScope: data.sourceScope, targetScope: data.targetScope, keys: data.keys, accessMode: data.accessMode }); }); this.events.on('memory:created', (data) => { this.log('debug', `Memory scope created: ${data.scopeId}`, data); }); this.events.on('memory:deleted', (data) => { this.log('debug', `Memory scope deleted: ${data.scopeId}`, data); }); this.events.on('memory:updated', (data) => { this.log('debug', `Memory updated in scope ${data.scopeId}`, { scopeId: data.scopeId, key: data.key, valuePreview: typeof data.value === 'object' ? `[Object with ${Object.keys(data.value).length} keys]` : String(data.value).substring(0, 30) + (String(data.value).length > 30 ? '...' : '') }); }); } /** * Add an agent to the agency * @param {string} id - Agent identifier * @param {Object} agent - Agent instance */ /** * Adds an agent to the agency. * @param {string} id - The ID of the agent. * @param {Agent} agent - The agent object. */ addAgent(id, agent) { this.agents[id] = agent; // Subscribe to agent events and relay them through the agency if (agent.on) { agent.on('statusChanged', (data) => { this.events.emit('agent:statusChanged', { agentId: id, ...data }); }); agent.on('runCompleted', (data) => { this.events.emit('agent:runCompleted', { agentId: id, ...data }); }); agent.on('runError', (data) => { this.events.emit('agent:runError', { agentId: id, ...data }); }); } return this; } /** * Add a team to the agency * @param {string} id - Team identifier * @param {Object} team - Team instance */ /** * Adds a team to the agency. * @param {string} id - The ID of the team. * @param {Team} team - The team object. */ addTeam(id, team) { this.team[id] = team; return this; } /** * Subscribe to agency events * @param {string} eventName - Event name to subscribe to * @param {Function} listener - Function to call when event is emitted * @returns {Function} - Unsubscribe function */ /** * Registers an event listener. * @param {string} eventName - The name of the event to listen for. * @param {function} listener - The function to call when the event is emitted. */ on(eventName, listener) { return this.events.on(eventName, listener); } /** * Broadcast an event to all subscribers * @param {string} eventName - Event name to broadcast * @param {*} data - Event data */ /** * Broadcasts an event to all listeners. * @param {string} eventName - The name of the event. * @param {*} data - The data to pass to the event listeners. */ broadcastEvent(eventName, data) { this.events.emit(eventName, { source: 'agency', agency: this.name, timestamp: new Date(), ...data }); } /** * Subscribe an agent to an agency event * @param {string} agentId - Agent ID to subscribe * @param {string} eventName - Event name to subscribe to * @param {Function} callback - Optional callback to transform the event data before sending to agent * @returns {Function} - Unsubscribe function */ /** * Subscribes an agent to an event. * @param {string} agentId - The ID of the agent. * @param {string} eventName - The name of the event. * @param {function} [callback=data => data] - The callback function to execute when the event is emitted. */ subscribeAgent(agentId, eventName, callback = data => data) { const agent = this.agents[agentId]; if (!agent) { throw new Error(`Agent ${agentId} not found`); } if (!agent.on) { throw new Error(`Agent ${agentId} does not support event subscription`); } return this.events.on(eventName, (data) => { const transformedData = callback(data); // If the callback returns false or null, don't forward the event if (transformedData) { agent.events.emit(eventName, transformedData); } }); } /** * Send a message from one agent to another * @param {string} senderId - Sender agent ID * @param {string} recipientId - Recipient agent ID * @param {*} message - Message content */ /** * Sends a message from one agent to another. * @param {string} senderId - The ID of the sender agent. * @param {string} recipientId - The ID of the recipient agent. * @param {string} message - The message to send. */ sendMessage(senderId, recipientId, message) { // Validate sender and recipient exist if (!this.agents[senderId]) { throw new Error(`Sender agent ${senderId} not found`); } if (!this.agents[recipientId]) { throw new Error(`Recipient agent ${recipientId} not found`); } const messageData = { senderId, recipientId, message, timestamp: new Date() }; // Emit targeted event for recipient this.events.emit(`message:${recipientId}`, messageData); // Emit general event for logging/monitoring this.events.emit('agent:message', messageData); return messageData; } /** * Subscribe an agent to receive messages * @param {string} agentId - Agent ID to receive messages * @param {Function} listener - Function to call when a message is received * @returns {Function} - Unsubscribe function */ /** * Registers a listener for messages sent to an agent. * @param {string} agentId - The ID of the agent. * @param {function} listener - The function to call when a message is received. */ onMessage(agentId, listener) { if (!this.agents[agentId]) { throw new Error(`Agent ${agentId} not found`); } return this.events.on(`message:${agentId}`, listener); } /** * Create a memory scope for sharing data * @param {string} scopeId - Unique identifier for the memory scope * @param {Object} config - Memory manager configuration * @returns {MemoryManager} - The created memory scope */ /** * Creates a memory scope. * @param {string} scopeId - The ID of the memory scope. * @param {object} [config={}] - Configuration options for the memory scope. */ createMemoryScope(scopeId, config = {}) { if (this.memoryScopes[scopeId]) { throw new Error(`Memory scope ${scopeId} already exists`); } const memoryScope = new MemoryManager(config); this.memoryScopes[scopeId] = memoryScope; return memoryScope; } /** * Get a memory scope by ID * @param {string} scopeId - Memory scope identifier * @returns {MemoryManager} - The memory scope */ /** * Retrieves a memory scope. * @param {string} scopeId - The ID of the memory scope. * @returns {object|undefined} The memory scope, or undefined if not found. */ getMemoryScope(scopeId) { const scope = this.memoryScopes[scopeId]; if (!scope) { throw new Error(`Memory scope ${scopeId} not found`); } return scope; } /** * Share memory between scopes with access control * @param {string} sourceId - Source memory scope ID * @param {string} targetId - Target memory scope ID * @param {Array<string>} keys - Keys to share (empty array shares all) * @param {string} accessMode - Access mode ('read-only' or 'read-write') */ /** * Shares memory between two agents. * @param {string} sourceId - The ID of the source agent. * @param {string} targetId - The ID of the target agent. * @param {string[]} [keys=[]] - The keys to share. If empty, all keys are shared. * @param {string} [accessMode='read-write'] - The access mode ('read-write' or 'read-only'). */ shareMemoryBetween(sourceId, targetId, keys = [], accessMode = 'read-write') { const sourceScope = this.getMemoryScope(sourceId); const targetScope = this.getMemoryScope(targetId); // If keys is empty, share all keys const keysToShare = keys.length > 0 ? keys : Object.keys(sourceScope.keyValueStore); // Set up access control this.memoryAccessControl[targetId] = this.memoryAccessControl[targetId] || {}; for (const key of keysToShare) { const value = sourceScope.recall(key); if (value !== undefined) { // Store the value in target scope targetScope.remember(key, value); // Record access control information this.memoryAccessControl[targetId][key] = { sourceScope: sourceId, accessMode, sharedAt: new Date() }; // If read-only, set up a proxy to prevent writes if (accessMode === 'read-only') { // Override the remember method for this key const originalRemember = targetScope.remember.bind(targetScope); targetScope.remember = (k, v) => { if (k === key) { console.warn(`Cannot modify read-only shared memory key "${k}" in scope "${targetId}"`); return false; } return originalRemember(k, v); }; } } } this.events.emit('memory:shared', { sourceScope: sourceId, targetScope: targetId, keys: keysToShare, accessMode }); } /** * Get memory access control information * @param {string} scopeId - Memory scope ID * @param {string} key - Memory key * @returns {Object} - Access control information */ /** * Gets information about memory access for a specific key. * @param {string} scopeId - The ID of the memory scope. * @param {string} key - The key to get access info for. * @returns {object|undefined} The memory access info, or undefined if not found. */ getMemoryAccessInfo(scopeId, key) { if (!this.memoryAccessControl[scopeId] || !this.memoryAccessControl[scopeId][key]) { return null; } return this.memoryAccessControl[scopeId][key]; } /** * Create a brief for a job * @param {string} jobId - Unique job identifier * @param {Object} briefData - Brief information * @returns {Object} - Created brief */ /** * Creates a brief for a job. * @param {string} jobId - The ID of the job. * @param {object} briefData - The data for the brief. */ createBrief(jobId, briefData) { const brief = { jobId, createdAt: new Date(), ...briefData }; this.brief[jobId] = brief; this.events.emit('brief:created', { jobId, brief }); return brief; } /** * Create a job context for sharing data between agents working on the same job * @param {string} jobId - Job identifier * @param {Object} initialContext - Initial context data * @returns {Object} - The job context */ /** * Creates a job context. * @param {string} jobId - The ID of the job. * @param {object} [initialContext={}] - The initial context data. */ createJobContext(jobId, initialContext = {}) { this.jobContexts[jobId] = { ...initialContext, createdAt: new Date(), updatedAt: new Date() }; this.events.emit('jobContext:created', { jobId, context: this.jobContexts[jobId] }); return this.jobContexts[jobId]; } /** * Update a job context * @param {string} jobId - Job identifier * @param {Object} updates - Context updates * @returns {Object} - Updated job context */ /** * Updates a job context. * @param {string} jobId - The ID of the job. * @param {object} updates - The updates to apply to the context. */ updateJobContext(jobId, updates) { if (!this.jobContexts[jobId]) { throw new Error(`Job context for ${jobId} not found`); } this.jobContexts[jobId] = { ...this.jobContexts[jobId], ...updates, updatedAt: new Date() }; this.events.emit('jobContext:updated', { jobId, context: this.jobContexts[jobId], updates: Object.keys(updates) }); return this.jobContexts[jobId]; } /** * Get a job context * @param {string} jobId - Job identifier * @returns {Object} - The job context */ /** * Retrieves a job context. * @param {string} jobId - The ID of the job. * @returns {object|undefined} The job context, or undefined if not found. */ getJobContext(jobId) { const context = this.jobContexts[jobId]; if (!context) { throw new Error(`Job context for ${jobId} not found`); } return context; } /** * Assign a job to an agent or team * @param {string} jobId - Job identifier * @param {string} assigneeId - Agent or team identifier * @param {string} assigneeType - Type of assignee ('agent' or 'team') * @returns {Object} - Job information */ /** * Assigns a job to an agent or team. * @param {string} jobId - The ID of the job. * @param {string} assigneeId - The ID of the assignee (agent or team). * @param {string} [assigneeType='agent'] - The type of assignee ('agent' or 'team'). */ assignJob(jobId, assigneeId, assigneeType = 'agent') { const brief = this.brief[jobId]; if (!brief) { throw new Error(`No brief found for job ${jobId}`); } const assignee = assigneeType === 'agent' ? this.agents[assigneeId] : this.team[assigneeId]; if (!assignee) { throw new Error(`${assigneeType} ${assigneeId} not found`); } const job = { jobId, brief, assigneeId, assigneeType, status: 'assigned', assignedAt: new Date(), results: null }; this.activeJobs[jobId] = job; // Create job context if it doesn't exist if (!this.jobContexts[jobId]) { this.createJobContext(jobId); } this.events.emit('job:assigned', { jobId, assigneeId, assigneeType }); return job; } /** * Set an error handler for a job * @param {string} jobId - Job identifier * @param {Function} handlerFn - Error handler function */ /** * Sets an error handler for a job. * @param {string} jobId - The ID of the job. * @param {function} handlerFn - The error handler function. */ setErrorHandler(jobId, handlerFn) { this.errorHandlers[jobId] = handlerFn; } /** * Set an error handler for a workflow * @param {string} workflowId - Workflow identifier * @param {Function} handlerFn - Error handler function */ /** * Sets a workflow error handler. * @param {string} workflowId - The ID of the workflow. * @param {function} handlerFn - The error handler function. */ setWorkflowErrorHandler(workflowId, handlerFn) { this.workflowErrorHandlers[workflowId] = handlerFn; } /** * Define a schema for job inputs and outputs * @param {string} jobId - Job identifier * @param {Object} inputSchema - Schema for job inputs * @param {Object} outputSchema - Schema for job outputs */ /** * Defines the schema for a job. * @param {string} jobId - The ID of the job. * @param {object} inputSchema - The input schema. * @param {object} outputSchema - The output schema. */ defineJobSchema(jobId, inputSchema, outputSchema) { this.jobSchemas[jobId] = { input: inputSchema, output: outputSchema }; } /** * Validate data against a schema * @param {Object} data - Data to validate * @param {Object} schema - Schema to validate against * @returns {Object} - Validation result with isValid and errors * @private */ /** * Validate data against a schema definition * @param {Object} data - Data to validate * @param {Object} schema - Schema definition with field specifications * @param {Object} schema.fieldName - Field specification * @param {boolean} [schema.fieldName.required] - Whether field is required * @param {string} [schema.fieldName.type] - Expected data type * @param {Array} [schema.fieldName.enum] - Allowed values for enum validation * @returns {Object} - Validation result with isValid boolean and errors array */ /** * Validates data against a schema. * @param {*} data - The data to validate. * @param {object} schema - The schema to validate against. * @returns {boolean} True if the data is valid, false otherwise. */ _validateAgainstSchema(data, schema) { if (!schema) return { isValid: true, errors: [] }; const errors = []; // Simple schema validation for (const [key, schemaValue] of Object.entries(schema)) { // Required field check if (schemaValue.required && (data[key] === undefined || data[key] === null)) { errors.push(`Required field '${key}' is missing`); continue; } // Type check if value exists if (data[key] !== undefined && schemaValue.type) { const actualType = Array.isArray(data[key]) ? 'array' : typeof data[key]; if (actualType !== schemaValue.type) { errors.push(`Field '${key}' should be of type '${schemaValue.type}', but got '${actualType}'`); } } // Enum check if (data[key] !== undefined && schemaValue.enum && !schemaValue.enum.includes(data[key])) { errors.push(`Field '${key}' should be one of [${schemaValue.enum.join(', ')}], but got '${data[key]}'`); } } return { isValid: errors.length === 0, errors }; } /** * Execute a job * @param {string} jobId - Job identifier * @param {Object} additionalInputs - Additional input data for the job * @returns {Promise<Object>} - Job results */ /** * Executes a job. * @param {string} jobId - The ID of the job. * @param {object} [additionalInputs={}] - Additional input data for the job. * @returns {Promise<*>} The result of the job execution. */ async execute(jobId, additionalInputs = {}) { const job = this.activeJobs[jobId]; if (!job) { throw new Error(`No active job found with id ${jobId}`); } const assignee = job.assigneeType === 'agent' ? this.agents[job.assigneeId] : this.team[job.assigneeId]; try { // Update job status job.status = 'in_progress'; job.startedAt = new Date(); this.events.emit('job:started', { jobId }); // Extract relevant information from the brief to use as inputs const briefInputs = this.extractInputsFromBrief(job.brief); // Get job context const jobContext = this.jobContexts[jobId] || {}; // Merge brief inputs with job context and additional inputs const mergedInputs = { ...briefInputs, ...jobContext, ...additionalInputs }; // Validate inputs against schema if defined if (this.jobSchemas[jobId] && this.jobSchemas[jobId].input) { const validation = this._validateAgainstSchema(mergedInputs, this.jobSchemas[jobId].input); if (!validation.isValid) { throw new Error(`Input validation failed: ${validation.errors.join(', ')}`); } } // Execute the job let results; if (job.assigneeType === 'agent') { // For a single agent, we need to format the input as a prompt const prompt = this.formatBriefAsPrompt(job.brief, mergedInputs); results = await assignee.run(prompt, { jobId, jobContext }); } else { // For a team, we pass the inputs and context results = await assignee.run(mergedInputs, { brief: job.brief, jobId, jobContext }); } // Validate outputs against schema if defined if (this.jobSchemas[jobId] && this.jobSchemas[jobId].output) { const validation = this._validateAgainstSchema(results, this.jobSchemas[jobId].output); if (!validation.isValid) { throw new Error(`Output validation failed: ${validation.errors.join(', ')}`); } } // Update job with results job.status = 'completed'; job.completedAt = new Date(); job.results = results; // Update job context with results this.updateJobContext(jobId, { results }); this.events.emit('job:completed', { jobId, results }); return results; } catch (error) { // Update job with error job.status = 'failed'; job.error = error.message; this.events.emit('job:failed', { jobId, error: error.message }); // Call error handler if exists if (this.errorHandlers[jobId]) { try { return await this.errorHandlers[jobId](error, job); } catch (handlerError) { console.error(`Error handler for job ${jobId} failed:`, handlerError); } } throw error; } } /** * Retry a failed job * @param {string} jobId - Job identifier * @param {number} maxRetries - Maximum number of retry attempts * @param {Object} additionalInputs - Additional inputs for the retry * @returns {Promise<Object>} - Job results */ /** * Retries a job. * @param {string} jobId - The ID of the job. * @param {number} [maxRetries=3] - The maximum number of retries. * @param {object} [additionalInputs={}] - Additional input data for the job. * @returns {Promise<*>} The result of the job execution. */ async retryJob(jobId, maxRetries = 3, additionalInputs = {}) { const job = this.activeJobs[jobId]; if (!job) { throw new Error(`No job found with id ${jobId}`); } if (job.status !== 'failed') { throw new Error(`Job ${jobId} is not in failed state`); } // Initialize retry count if not present job.retryCount = job.retryCount || 0; if (job.retryCount >= maxRetries) { throw new Error(`Maximum retry attempts (${maxRetries}) reached for job ${jobId}`); } // Increment retry count job.retryCount++; // Reset job status job.status = 'assigned'; job.error = null; this.events.emit('job:retrying', { jobId, retryCount: job.retryCount, maxRetries }); // Execute the job again return this.execute(jobId, additionalInputs); } /** * Execute a workflow of jobs (sequential or parallel) * @param {Object} workflowDefinition - Workflow definition * @param {string} workflowId - Unique workflow identifier * @param {Object} initialData - Initial data for the workflow * @returns {Promise<Object>} - Workflow results */ /** * Executes a workflow. * @param {object} workflowDefinition - The workflow definition. * @param {string} workflowId - The ID of the workflow. * @param {object} [initialData={}] - Initial data for the workflow. * @returns {Promise<*>} The result of the workflow execution. */ async executeWorkflow(workflowDefinition, workflowId, initialData = {}) { // Create a workflow record this.workflow[workflowId] = { id: workflowId, definition: workflowDefinition, status: 'in_progress', startedAt: new Date(), currentStep: 0, results: {}, error: null }; this.events.emit('workflow:started', { workflowId, steps: Array.isArray(workflowDefinition) ? workflowDefinition.length : 0 }); // Create a memory scope for this workflow const workflowMemory = this.createMemoryScope(`workflow:${workflowId}`); // Store initial data in workflow memory for (const [key, value] of Object.entries(initialData)) { workflowMemory.remember(key, value); } let currentData = initialData; try { // Execute each step for (let i = 0; i < workflowDefinition.length; i++) { const step = workflowDefinition[i]; const { jobId, assigneeId, assigneeType, inputs = {}, type = 'sequential' } = step; // Update workflow status this.workflow[workflowId].currentStep = i; this.events.emit('workflow:step', { workflowId, stepIndex: i, jobId, assigneeId, assigneeType, type }); // Handle different step types if (type === 'parallel' && Array.isArray(jobId)) { // Parallel execution of multiple jobs const parallelJobs = jobId.map((jid, index) => { const parallelAssigneeId = Array.isArray(assigneeId) ? assigneeId[index] : assigneeId; const parallelAssigneeType = Array.isArray(assigneeType) ? assigneeType[index] : assigneeType; const parallelInputs = Array.isArray(inputs) ? inputs[index] : inputs; const parallelBrief = Array.isArray(step.brief) ? step.brief[index] : step.brief; // Create brief if not exists if (!this.brief[jid]) { this.createBrief(jid, { ...parallelBrief, workflowId, workflowStep: i, isParallel: true, parallelIndex: index }); } // Assign job this.assignJob(jid, parallelAssigneeId, parallelAssigneeType); // Prepare inputs const jobInputs = { ...parallelInputs, previousStepData: currentData, workflowId, workflowStep: i, isParallel: true, parallelIndex: index }; // Execute job return this.execute(jid, jobInputs) .then(result => ({ jobId: jid, result })); }); // Wait for all parallel jobs to complete const parallelResults = await Promise.all(parallelJobs); // Store results const combinedResults = {}; for (const { jobId: jid, result } of parallelResults) { combinedResults[jid] = result; workflowMemory.remember(`step_${i}_job_${jid}_results`, result); this.workflow[workflowId].results[jid] = result; // Check if job failed const job = this.getJob(jid); if (job.status === 'failed') { throw new Error(`Parallel workflow step ${i} (job ${jid}) failed: ${job.error}`); } } // Update current data with combined results currentData = combinedResults; workflowMemory.remember(`step_${i}_results`, combinedResults); } else if (type === 'conditional') { // Conditional execution based on a condition function const conditionFn = step.condition || (() => true); const shouldExecute = conditionFn(currentData, workflowMemory); if (shouldExecute) { // Create brief if not exists if (!this.brief[jobId]) { this.createBrief(jobId, { ...step.brief, workflowId, workflowStep: i, isConditional: true }); } // Assign job this.assignJob(jobId, assigneeId, assigneeType); // Prepare inputs const stepInputs = { ...inputs, previousStepData: currentData, workflowId, workflowStep: i, isConditional: true }; // Execute job currentData = await this.execute(jobId, stepInputs); // Store results workflowMemory.remember(`step_${i}_results`, currentData); this.workflow[workflowId].results[jobId] = currentData; // Check if job failed const job = this.getJob(jobId); if (job.status === 'failed') { throw new Error(`Conditional workflow step ${i} (job ${jobId}) failed: ${job.error}`); } } else { // Skip this step workflowMemory.remember(`step_${i}_skipped`, true); this.events.emit('workflow:stepSkipped', { workflowId, stepIndex: i, reason: 'condition-not-met' }); } } else { // Default: sequential execution // Create a brief for this job if not already exists if (!this.brief[jobId]) { this.createBrief(jobId, { ...step.brief, workflowId, workflowStep: i }); } // Assign the job this.assignJob(jobId, assigneeId, assigneeType); // Execute the job with current data const stepInputs = { ...inputs, previousStepData: currentData, workflowId, workflowStep: i }; currentData = await this.execute(jobId, stepInputs); // Store results in workflow memory workflowMemory.remember(`step_${i}_results`, currentData); // Store in workflow results this.workflow[workflowId].results[jobId] = currentData; // Check if job failed const job = this.getJob(jobId); if (job.status === 'failed') { throw new Error(`Workflow step ${i} (job ${jobId}) failed: ${job.error}`); } } } // Update workflow status this.workflow[workflowId].status = 'completed'; this.workflow[workflowId].completedAt = new Date(); this.events.emit('workflow:completed', { workflowId, results: this.workflow[workflowId].results }); return { status: 'completed', workflowId, results: this.workflow[workflowId].results }; } catch (error) { // Update workflow status this.workflow[workflowId].status = 'failed'; this.workflow[workflowId].error = error.message; this.events.emit('workflow:failed', { workflowId, error: error.message, step: this.workflow[workflowId].currentStep }); // Call workflow error handler if exists if (this.workflowErrorHandlers[workflowId]) { try { return await this.workflowErrorHandlers[workflowId](error, this.workflow[workflowId]); } catch (handlerError) { console.error(`Workflow error handler for ${workflowId} failed:`, handlerError); } } return { status: 'failed', workflowId, error: error.message, step: this.workflow[workflowId].currentStep, results: this.workflow[workflowId].results }; } } // Include existing methods from Agency.js /** * Formats a brief as a prompt. * @param {object} brief - The brief data. * @param {object} [inputs={}] - Input data for the prompt. * @returns {string} The formatted prompt. */ formatBriefAsPrompt(brief, inputs = {}) { // Implementation from original Agency.js let prompt = ` # JOB BRIEF: ${brief.title} ## Overview ${brief.overview} ## Background ${brief.background || 'Not provided'} ## Objective ${brief.objective} ## Target Audience ${brief.targetAudience || 'Not specified'} `; // Add preferences if available if (brief.preferences || inputs.preferences) { prompt += `\n## Preferences\n${brief.preferences || inputs.preferences}\n`; } // Add dates if available if (brief.dates || inputs.dates) { prompt += `\n## Dates\n${brief.dates || inputs.dates}\n`; } // Add budget if available if (brief.budget || inputs.budget) { prompt += `\n## Budget\n${brief.budget || inputs.budget}\n`; } // Add transportation if available if (brief.transportation || inputs.transportation) { prompt += `\n## Transportation\n${brief.transportation || inputs.transportation}\n`; } // Add deliverables if available if (brief.deliverables) { prompt += `\n## Deliverables\n${brief.deliverables}\n`; } // Add additional information if available if (brief.additionalInfo) { prompt += `\n## Additional Information\n${brief.additionalInfo}\n`; } // Add workflow context if available if (inputs.workflowId) { prompt += `\n## Workflow Context\nThis is step ${inputs.workflowStep + 1} of workflow ${inputs.workflowId}.\n`; if (inputs.previousStepData) { prompt += `\n## Previous Step Results\n${JSON.stringify(inputs.previousStepData, null, 2)}\n`; } } return prompt; } /** * Extracts inputs from a brief. * @param {object} brief - The brief data. * @returns {object} The extracted inputs. */ extractInputsFromBrief(brief) { // Implementation from original Agency.js const inputs = {}; // Extract topic from title, overview or background if (brief.title && brief.title.toLowerCase().includes('water bottle')) { inputs.topic = 'eco-friendly water bottles'; } else if (brief.overview && brief.overview.toLowerCase().includes('water bottle')) { inputs.topic = 'eco-friendly water bottles'; } else if (brief.background && brief.background.toLowerCase().includes('water bottle')) { inputs.topic = 'eco-friendly water bottles'; } // Extract target audience if (brief.targetAudience) { inputs.targetAudience = brief.targetAudience; } // Extract content type from deliverables if (brief.deliverables && brief.deliverables.toLowerCase().includes('social media')) { inputs.contentType = 'social media posts'; } // Extract vacation-specific inputs if (brief.preferences) { inputs.preferences = brief.preferences; } if (brief.dates) { inputs.dates = brief.dates; } if (brief.budget) { inputs.budget = brief.budget; } if (brief.transportation) { inputs.transportation = brief.transportation; } return inputs; } /** * Get all jobs with a specific status * @param {string} status - Job status to filter by ('pending', 'running', 'completed', 'failed') * @returns {Array<Object>} - Array of jobs with the specified status */ /** * Gets jobs by status. * @param {string} status - The status of the jobs to retrieve. * @returns {object[]} An array of jobs with the specified status. */ getJobsByStatus(status) { return Object.values(this.activeJobs) .filter(job => job.status === status); } /** * Get a specific job by its ID * @param {string} jobId - Job identifier * @returns {Object|null} - Job object if found, null otherwise */ /** * Gets a job by ID. * @param {string} jobId - The ID of the job. * @returns {object|undefined} The job, or undefined if not found. */ getJob(jobId) { return this.activeJobs[jobId]; } /** * Clean up resources for a completed job * @param {string} jobId - Job identifier * @param {boolean} keepResults - Whether to keep job results * @returns {boolean} - Success status */ /** * Cleans up a job. * @param {string} jobId - The ID of the job. * @param {boolean} [keepResults=true] - Whether to keep the results. */ cleanupJob(jobId, keepResults = true) { const job = this.activeJobs[jobId]; if (!job) { return false; } // Only clean up completed or failed jobs if (job.status !== 'completed' && job.status !== 'failed') { return false; } // Store results if needed const results = keepResults ? job.results : null; // Clean up job context if (this.jobContexts[jobId]) { delete this.jobContexts[jobId]; } // Remove job from active jobs delete this.activeJobs[jobId]; // Keep brief for reference this.events.emit('job:cleaned', { jobId, keepResults }); // If keeping results, store them in global memory if (keepResults && results) { this.globalMemory.remember(`job_${jobId}_results`, { results, cleanedAt: new Date() }); } return true; } /** * Clean up resources for a completed workflow * @param {string} workflowId - Workflow identifier * @param {boolean} keepResults - Whether to keep workflow results * @returns {boolean} - Success status */ /** * Cleans up a workflow. * @param {string} workflowId - The ID of the workflow. * @param {boolean} [keepResults=true] - Whether to keep the results. */ cleanupWorkflow(workflowId, keepResults = true) { const workflow = this.workflow[workflowId]; if (!workflow) { return false; } // Only clean up completed or failed workflow if (workflow.status !== 'completed' && workflow.status !== 'failed') { return false; } // Store results if needed const results = keepResults ? workflow.results : null; // Clean up workflow memory scope if (this.memoryScopes[`workflow:${workflowId}`]) { delete this.memoryScopes[`workflow:${workflowId}`]; } // Clean up workflow error handler if (this.workflowErrorHandlers[workflowId]) { delete this.workflowErrorHandlers[workflowId]; } // Remove workflow delete this.workflow[workflowId]; this.events.emit('workflow:cleaned', { workflowId, keepResults }); // If keeping results, store them in global memory if (keepResults && results) { this.globalMemory.remember(`workflow_${workflowId}_results`, { results, cleanedAt: new Date() }); } return true; } /** * Allows an agent to plan jobs from a high-level goal * @param {string} agentId - ID of the agent setting the goal * @param {string} goal - High-level goal description * @returns {Promise<Array>} - Array of created job IDs */ /** * Plans jobs from a goal. * @param {string} agentId - The ID of the agent. * @param {string} goal - The goal to plan for. * @returns {Promise<object[]>} An array of planned jobs. */ async planJobsFromGoal(agentId, goal) { const agent = this.agents[agentId]; if (!agent) throw new Error(`Agent ${agentId} not found`); // Step 1: Prompt for job planning with structured JSON output format const planningPrompt = { contents: [ { role: "system", parts: [ { text: `You are a JSON-only task planner. You MUST ONLY output valid JSON with no other text. Your response will be directly parsed with JSON.parse() and any non-JSON text will cause a critical error. DO NOT include any explanations, notes, or text outside the JSON structure. DO NOT use markdown formatting. DO NOT start with "Here's the JSON:" or similar phrases. ONLY RETURN THE RAW JSON ARRAY.` } ] }, { role: "user", parts: [ { text: `I need to plan a vacation to Hawaii. Break it down into tasks and return ONLY a JSON array.` } ] }, { role: "assistant", parts: [ { text: `[{"name":"Research Hawaii destinations","description":"Identify the best islands and locations to visit based on interests","inputs":["vacation_preferences"],"outputs":["destination_options"]},{"name":"Plan accommodations","description":"Find and book suitable hotels or rentals","inputs":["destination_options","budget","dates"],"outputs":["accommodation_bookings"]},{"name":"Arrange transportation","description":"Book flights and plan local transportation","inputs":["destination_options","dates","budget"],"outputs":["transportation_bookings"]},{"name":"Create activity itinerary","description":"Plan daily activities and excursions","inputs":["destination_options","dates","interests"],"outputs":["daily_itinerary"]},{"name":"Prepare packing list","description":"Create a comprehensive packing list for Hawaii","inputs":["activities","weather_forecast","trip_duration"],"outputs":["packing_list"]}]` } ] }, { role: "user", parts: [ { text: `I need to organize a team-building event. Break it down into tasks and return ONLY a JSON array.` } ] }, { role: "assistant", parts: [ { text: `[{"name":"Define event objectives","description":"Clarify goals and desired outcomes for the team-building event","inputs":["team_size","team_dynamics","company_culture"],"outputs":["event_objectives"]},{"name":"Select venue and date","description":"Find and book an appropriate location and time","inputs":["team_size","budget","event_objectives"],"outputs":["venue_booking","event_date"]},{"name":"Plan activities","description":"Choose team-building exercises and activities","inputs":["event_objectives","team_size","venue_details"],"outputs":["activity_schedule"]},{"name":"Arrange catering","