UNPKG

sf-agent-framework

Version:

AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction

758 lines (620 loc) 21.3 kB
#!/usr/bin/env node /** * Agent Handoff Protocol for SF-Agent Framework * Manages formal handoffs between agents with artifact passing and collaborative review */ const fs = require('fs-extra'); const path = require('path'); const yaml = require('js-yaml'); const chalk = require('chalk'); const crypto = require('crypto'); class AgentHandoffProtocol { constructor(options = {}) { this.options = { handoffDir: options.handoffDir || 'docs/handoffs', artifactDir: options.artifactDir || 'docs/artifacts', reviewDir: options.reviewDir || 'docs/reviews', queueFile: options.queueFile || 'docs/handoffs/handoff-queue.yaml', verbose: options.verbose || false, ...options, }; this.activeHandoffs = new Map(); this.handoffQueue = []; this.completedHandoffs = []; this.artifactRegistry = new Map(); } /** * Initialize handoff protocol system */ async initialize() { console.log(chalk.blue('🤝 Initializing Agent Handoff Protocol...')); // Ensure directories exist await fs.ensureDir(this.options.handoffDir); await fs.ensureDir(this.options.artifactDir); await fs.ensureDir(this.options.reviewDir); // Load existing handoff queue await this.loadHandoffQueue(); // Load artifact registry await this.loadArtifactRegistry(); console.log(chalk.green('✅ Handoff Protocol initialized')); } /** * Create a new handoff between agents */ async createHandoff(config) { const handoffId = this.generateHandoffId(); const handoff = { id: handoffId, timestamp: new Date().toISOString(), status: 'pending', from_agent: config.fromAgent, to_agent: config.toAgent, artifacts: config.artifacts || [], context: config.context || {}, requirements: config.requirements || [], validation_criteria: config.validationCriteria || [], notes: config.notes || '', metadata: { workflow: config.workflow, phase: config.phase, step: config.step, priority: config.priority || 'normal', }, }; // Validate artifacts exist for (const artifact of handoff.artifacts) { if (!(await this.validateArtifact(artifact))) { throw new Error(`Artifact not found or invalid: ${artifact.path}`); } } // Register handoff this.activeHandoffs.set(handoffId, handoff); this.handoffQueue.push(handoff); // Create handoff package await this.createHandoffPackage(handoff); console.log(chalk.green(`✅ Handoff created: ${handoffId}`)); console.log(chalk.blue(` From: ${handoff.from_agent} → To: ${handoff.to_agent}`)); return handoffId; } /** * Accept a handoff as receiving agent */ async acceptHandoff(handoffId, agentId) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { throw new Error(`Handoff not found: ${handoffId}`); } if (handoff.to_agent !== agentId && handoff.to_agent !== 'any') { throw new Error(`Handoff ${handoffId} is not for agent ${agentId}`); } handoff.status = 'in_progress'; handoff.accepted_at = new Date().toISOString(); handoff.accepted_by = agentId; // Load artifacts for agent const artifacts = await this.loadHandoffArtifacts(handoff); // Create working directory for agent const workingDir = await this.createWorkingDirectory(handoffId, agentId); console.log(chalk.green(`✅ Handoff accepted by ${agentId}`)); console.log(chalk.blue(` Working directory: ${workingDir}`)); console.log(chalk.blue(` Artifacts loaded: ${artifacts.length}`)); return { handoff, artifacts, workingDir, }; } /** * Complete a handoff with output artifacts */ async completeHandoff(handoffId, completion) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { throw new Error(`Handoff not found: ${handoffId}`); } if (handoff.status !== 'in_progress') { throw new Error(`Handoff ${handoffId} is not in progress`); } handoff.status = 'completed'; handoff.completed_at = new Date().toISOString(); handoff.output_artifacts = completion.artifacts || []; handoff.completion_notes = completion.notes || ''; handoff.validation_results = await this.validateCompletion(handoff, completion); // Register output artifacts for (const artifact of handoff.output_artifacts) { await this.registerArtifact(artifact); } // Move to completed this.completedHandoffs.push(handoff); this.activeHandoffs.delete(handoffId); // Update queue await this.updateHandoffQueue(); // Trigger next handoff if configured if (completion.nextAgent) { await this.createHandoff({ fromAgent: handoff.accepted_by, toAgent: completion.nextAgent, artifacts: handoff.output_artifacts, context: { ...handoff.context, previous_handoff: handoffId, }, workflow: handoff.metadata.workflow, phase: handoff.metadata.phase, step: handoff.metadata.step + 1, }); } console.log(chalk.green(`✅ Handoff completed: ${handoffId}`)); return handoff; } /** * Request collaborative review */ async requestReview(handoffId, reviewConfig) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { throw new Error(`Handoff not found: ${handoffId}`); } const reviewId = this.generateReviewId(); const review = { id: reviewId, handoff_id: handoffId, requested_at: new Date().toISOString(), requested_by: reviewConfig.requestedBy, reviewers: reviewConfig.reviewers || [], type: reviewConfig.type || 'standard', criteria: reviewConfig.criteria || [], deadline: reviewConfig.deadline, status: 'pending', comments: [], decisions: [], }; // Create review package await this.createReviewPackage(review, handoff); // Notify reviewers for (const reviewer of review.reviewers) { await this.notifyReviewer(reviewer, review); } handoff.review = review; handoff.status = 'in_review'; console.log(chalk.yellow(`📝 Review requested: ${reviewId}`)); console.log(chalk.blue(` Reviewers: ${review.reviewers.join(', ')}`)); return reviewId; } /** * Submit review feedback */ async submitReview(reviewId, feedback) { const review = await this.loadReview(reviewId); if (!review) { throw new Error(`Review not found: ${reviewId}`); } const reviewFeedback = { reviewer: feedback.reviewer, timestamp: new Date().toISOString(), decision: feedback.decision, // approve, reject, request_changes comments: feedback.comments || [], suggestions: feedback.suggestions || [], required_changes: feedback.requiredChanges || [], }; review.comments.push(...reviewFeedback.comments); review.decisions.push({ reviewer: reviewFeedback.reviewer, decision: reviewFeedback.decision, }); // Check if all reviews complete const allReviewed = review.reviewers.every((reviewer) => review.decisions.some((d) => d.reviewer === reviewer) ); if (allReviewed) { review.status = 'completed'; review.completed_at = new Date().toISOString(); // Determine overall decision const approvals = review.decisions.filter((d) => d.decision === 'approve').length; const rejections = review.decisions.filter((d) => d.decision === 'reject').length; if (rejections > 0) { review.overall_decision = 'rejected'; } else if (approvals === review.reviewers.length) { review.overall_decision = 'approved'; } else { review.overall_decision = 'changes_requested'; } } await this.saveReview(review); console.log(chalk.green(`✅ Review submitted by ${feedback.reviewer}`)); return review; } /** * Create handoff package with all artifacts */ async createHandoffPackage(handoff) { const packagePath = path.join(this.options.handoffDir, `${handoff.id}`); await fs.ensureDir(packagePath); // Create manifest const manifest = { handoff_id: handoff.id, created_at: handoff.timestamp, from_agent: handoff.from_agent, to_agent: handoff.to_agent, artifacts: handoff.artifacts, context: handoff.context, requirements: handoff.requirements, validation_criteria: handoff.validation_criteria, instructions: this.generateHandoffInstructions(handoff), }; await fs.writeFile(path.join(packagePath, 'manifest.yaml'), yaml.dump(manifest), 'utf-8'); // Copy artifacts const artifactsPath = path.join(packagePath, 'artifacts'); await fs.ensureDir(artifactsPath); for (const artifact of handoff.artifacts) { const sourcePath = artifact.path; const destPath = path.join(artifactsPath, path.basename(artifact.path)); if (await fs.pathExists(sourcePath)) { await fs.copy(sourcePath, destPath); } } // Create README const readme = this.generateHandoffReadme(handoff); await fs.writeFile(path.join(packagePath, 'README.md'), readme, 'utf-8'); } /** * Generate handoff instructions */ generateHandoffInstructions(handoff) { const instructions = []; instructions.push(`# Handoff Instructions`); instructions.push(`\nFrom: ${handoff.from_agent}`); instructions.push(`To: ${handoff.to_agent}`); instructions.push(`Date: ${handoff.timestamp}`); instructions.push(`\n## Context`); instructions.push(JSON.stringify(handoff.context, null, 2)); if (handoff.requirements.length > 0) { instructions.push(`\n## Requirements`); handoff.requirements.forEach((req) => { instructions.push(`- ${req}`); }); } if (handoff.validation_criteria.length > 0) { instructions.push(`\n## Validation Criteria`); handoff.validation_criteria.forEach((criteria) => { instructions.push(`- ${criteria}`); }); } if (handoff.notes) { instructions.push(`\n## Notes`); instructions.push(handoff.notes); } return instructions.join('\n'); } /** * Generate handoff README */ generateHandoffReadme(handoff) { return `# Agent Handoff Package ## Handoff ID: ${handoff.id} ### Overview - **From Agent**: ${handoff.from_agent} - **To Agent**: ${handoff.to_agent} - **Created**: ${handoff.timestamp} - **Status**: ${handoff.status} - **Priority**: ${handoff.metadata.priority} ### Workflow Context - **Workflow**: ${handoff.metadata.workflow} - **Phase**: ${handoff.metadata.phase} - **Step**: ${handoff.metadata.step} ### Included Artifacts ${handoff.artifacts.map((a) => `- ${a.name}: ${a.description || 'No description'}`).join('\n')} ### Requirements ${handoff.requirements.map((r) => `- ${r}`).join('\n')} ### Validation Criteria ${handoff.validation_criteria.map((v) => `- ${v}`).join('\n')} ### Instructions for Receiving Agent 1. Review all artifacts in the \`artifacts/\` directory 2. Check the manifest.yaml for detailed context 3. Complete the requirements listed above 4. Ensure all validation criteria are met 5. Use \`sf-agent handoff complete ${handoff.id}\` when finished ### Notes ${handoff.notes || 'No additional notes provided'} `; } /** * Validate artifact exists and is accessible */ async validateArtifact(artifact) { if (!artifact.path) return false; const exists = await fs.pathExists(artifact.path); if (!exists) return false; // Check artifact integrity if hash provided if (artifact.hash) { const actualHash = await this.calculateFileHash(artifact.path); return actualHash === artifact.hash; } return true; } /** * Register artifact in registry */ async registerArtifact(artifact) { const artifactId = artifact.id || this.generateArtifactId(); const registeredArtifact = { id: artifactId, name: artifact.name, path: artifact.path, type: artifact.type || 'document', created_at: artifact.created_at || new Date().toISOString(), created_by: artifact.created_by, hash: await this.calculateFileHash(artifact.path), size: (await fs.stat(artifact.path)).size, metadata: artifact.metadata || {}, }; this.artifactRegistry.set(artifactId, registeredArtifact); // Save to registry file await this.saveArtifactRegistry(); return artifactId; } /** * Load handoff artifacts */ async loadHandoffArtifacts(handoff) { const artifacts = []; for (const artifactRef of handoff.artifacts) { const artifact = this.artifactRegistry.get(artifactRef.id); if (artifact) { const content = await fs.readFile(artifact.path, 'utf-8'); artifacts.push({ ...artifact, content, }); } } return artifacts; } /** * Create working directory for agent */ async createWorkingDirectory(handoffId, agentId) { const workingDir = path.join(this.options.handoffDir, handoffId, 'working', agentId); await fs.ensureDir(workingDir); return workingDir; } /** * Validate completion against criteria */ async validateCompletion(handoff, completion) { const results = []; for (const criteria of handoff.validation_criteria) { const result = { criteria, passed: false, message: '', }; // Check if output artifacts meet criteria // This is a simplified validation - extend as needed if (criteria.includes('artifact:')) { const requiredArtifact = criteria.split(':')[1].trim(); result.passed = completion.artifacts.some((a) => a.name === requiredArtifact); result.message = result.passed ? 'Artifact found' : 'Required artifact missing'; } else { // Assume manual validation passed result.passed = true; result.message = 'Manual validation assumed passed'; } results.push(result); } return results; } /** * Create review package */ async createReviewPackage(review, handoff) { const reviewPath = path.join(this.options.reviewDir, review.id); await fs.ensureDir(reviewPath); // Copy handoff artifacts for review const handoffPath = path.join(this.options.handoffDir, handoff.id); await fs.copy(handoffPath, path.join(reviewPath, 'handoff')); // Create review manifest await fs.writeFile(path.join(reviewPath, 'review.yaml'), yaml.dump(review), 'utf-8'); } /** * Notify reviewer (placeholder - implement actual notification) */ async notifyReviewer(reviewer, review) { console.log(chalk.blue(` 📧 Notifying ${reviewer} about review ${review.id}`)); // In real implementation, this could send email, Slack message, etc. } /** * Load review */ async loadReview(reviewId) { const reviewPath = path.join(this.options.reviewDir, reviewId, 'review.yaml'); if (!(await fs.pathExists(reviewPath))) { return null; } const content = await fs.readFile(reviewPath, 'utf-8'); return yaml.load(content); } /** * Save review */ async saveReview(review) { const reviewPath = path.join(this.options.reviewDir, review.id, 'review.yaml'); await fs.writeFile(reviewPath, yaml.dump(review), 'utf-8'); } /** * Load handoff queue */ async loadHandoffQueue() { if (await fs.pathExists(this.options.queueFile)) { const content = await fs.readFile(this.options.queueFile, 'utf-8'); const data = yaml.load(content) || {}; this.handoffQueue = data.queue || []; this.completedHandoffs = data.completed || []; } } /** * Update handoff queue */ async updateHandoffQueue() { const data = { queue: this.handoffQueue, completed: this.completedHandoffs, updated_at: new Date().toISOString(), }; await fs.writeFile(this.options.queueFile, yaml.dump(data), 'utf-8'); } /** * Load artifact registry */ async loadArtifactRegistry() { const registryPath = path.join(this.options.artifactDir, 'registry.yaml'); if (await fs.pathExists(registryPath)) { const content = await fs.readFile(registryPath, 'utf-8'); const data = yaml.load(content) || {}; Object.entries(data).forEach(([id, artifact]) => { this.artifactRegistry.set(id, artifact); }); } } /** * Save artifact registry */ async saveArtifactRegistry() { const registryPath = path.join(this.options.artifactDir, 'registry.yaml'); const data = Object.fromEntries(this.artifactRegistry); await fs.writeFile(registryPath, yaml.dump(data), 'utf-8'); } /** * Calculate file hash */ async calculateFileHash(filepath) { const content = await fs.readFile(filepath); return crypto.createHash('sha256').update(content).digest('hex'); } /** * Generate unique handoff ID */ generateHandoffId() { return `HO-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Generate unique review ID */ generateReviewId() { return `RV-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Generate unique artifact ID */ generateArtifactId() { return `AR-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Get handoff status */ getHandoffStatus(handoffId) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { const completed = this.completedHandoffs.find((h) => h.id === handoffId); if (completed) { return { status: 'completed', handoff: completed }; } return { status: 'not_found' }; } return { status: handoff.status, handoff }; } /** * List pending handoffs for agent */ listPendingHandoffs(agentId) { return this.handoffQueue.filter( (h) => h.status === 'pending' && (h.to_agent === agentId || h.to_agent === 'any') ); } /** * Get handoff metrics */ getMetrics() { return { active: this.activeHandoffs.size, pending: this.handoffQueue.filter((h) => h.status === 'pending').length, in_progress: this.handoffQueue.filter((h) => h.status === 'in_progress').length, in_review: this.handoffQueue.filter((h) => h.status === 'in_review').length, completed: this.completedHandoffs.length, total_artifacts: this.artifactRegistry.size, }; } } // CLI Interface if (require.main === module) { const { program } = require('commander'); program.description('Agent Handoff Protocol Manager'); program .command('create') .description('Create a new handoff') .requiredOption('--from <agent>', 'Source agent') .requiredOption('--to <agent>', 'Target agent') .option('--artifacts <paths...>', 'Artifact paths') .option('--notes <text>', 'Handoff notes') .action(async (options) => { const protocol = new AgentHandoffProtocol(); await protocol.initialize(); const artifacts = (options.artifacts || []).map((path) => ({ name: path.split('/').pop(), path, })); const handoffId = await protocol.createHandoff({ fromAgent: options.from, toAgent: options.to, artifacts, notes: options.notes, }); console.log(`Handoff created: ${handoffId}`); }); program .command('accept <handoffId>') .description('Accept a handoff') .requiredOption('--agent <agent>', 'Accepting agent ID') .action(async (handoffId, options) => { const protocol = new AgentHandoffProtocol(); await protocol.initialize(); const result = await protocol.acceptHandoff(handoffId, options.agent); console.log(`Handoff accepted. Working directory: ${result.workingDir}`); }); program .command('complete <handoffId>') .description('Complete a handoff') .option('--artifacts <paths...>', 'Output artifact paths') .option('--notes <text>', 'Completion notes') .option('--next <agent>', 'Next agent in chain') .action(async (handoffId, options) => { const protocol = new AgentHandoffProtocol(); await protocol.initialize(); const artifacts = (options.artifacts || []).map((path) => ({ name: path.split('/').pop(), path, })); await protocol.completeHandoff(handoffId, { artifacts, notes: options.notes, nextAgent: options.next, }); console.log(`Handoff completed: ${handoffId}`); }); program .command('status [handoffId]') .description('Get handoff status') .action(async (handoffId) => { const protocol = new AgentHandoffProtocol(); await protocol.initialize(); if (handoffId) { const status = protocol.getHandoffStatus(handoffId); console.log(JSON.stringify(status, null, 2)); } else { const metrics = protocol.getMetrics(); console.log('Handoff Metrics:'); console.log(JSON.stringify(metrics, null, 2)); } }); program.parse(); } module.exports = AgentHandoffProtocol;