UNPKG

sf-agent-framework

Version:

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

613 lines (513 loc) 16.2 kB
/** * Multi-Agent Collaboration Protocol * Manages agent handoffs, artifact passing, and collaborative reviews */ const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); const crypto = require('crypto'); class AgentCollaborationProtocol { constructor(rootDir = process.cwd()) { this.rootDir = rootDir; this.handoffsDir = path.join(rootDir, 'docs', 'handoffs'); this.artifactsDir = path.join(rootDir, 'docs', 'artifacts'); this.activeHandoffs = new Map(); this.collaborationLog = []; this.agentRegistry = new Map(); } /** * Initialize the collaboration protocol */ async initialize() { // Ensure directories exist await fs.mkdir(this.handoffsDir, { recursive: true }); await fs.mkdir(this.artifactsDir, { recursive: true }); // Load agent registry await this.loadAgentRegistry(); } /** * Load available agents and their capabilities */ async loadAgentRegistry() { const agentsDir = path.join(this.rootDir, 'sf-core', 'agents'); try { const files = await fs.readdir(agentsDir); const agentFiles = files.filter((f) => f.endsWith('.md')); for (const file of agentFiles) { const agentId = file.replace('.md', ''); const content = await fs.readFile(path.join(agentsDir, file), 'utf8'); // Extract agent metadata from YAML front matter const yamlMatch = content.match(/```yaml\n([\s\S]*?)\n```/); if (yamlMatch) { try { const metadata = yaml.load(yamlMatch[1]); this.agentRegistry.set(agentId, { id: agentId, name: metadata.name || agentId, capabilities: metadata.capabilities || [], inputs: metadata.inputs || [], outputs: metadata.outputs || [], dependencies: metadata.dependencies || {}, }); } catch (e) { console.warn(`Failed to parse metadata for ${agentId}`); } } } } catch (error) { console.error('Failed to load agent registry:', error); } } /** * Create a handoff between agents */ async createHandoff(config) { const handoffId = this.generateHandoffId(); const handoff = { id: handoffId, from: config.from, to: config.to, status: 'pending', created_at: new Date().toISOString(), artifacts: config.artifacts || [], context: config.context || {}, requirements: config.requirements || [], acceptance_criteria: config.acceptance_criteria || [], metadata: { priority: config.priority || 'normal', deadline: config.deadline, tags: config.tags || [], }, }; // Validate agents exist if (!this.agentRegistry.has(config.from)) { throw new Error(`Source agent ${config.from} not found`); } if (!this.agentRegistry.has(config.to)) { throw new Error(`Target agent ${config.to} not found`); } // Check compatibility const compatibility = await this.checkAgentCompatibility(config.from, config.to); if (!compatibility.compatible) { console.warn(`Compatibility warning: ${compatibility.message}`); } // Store handoff this.activeHandoffs.set(handoffId, handoff); // Create handoff document await this.createHandoffDocument(handoff); // Log collaboration event this.logCollaboration({ type: 'handoff_created', handoff_id: handoffId, from: config.from, to: config.to, timestamp: handoff.created_at, }); return handoff; } /** * Accept a handoff */ async acceptHandoff(handoffId, agentId) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { throw new Error(`Handoff ${handoffId} not found`); } if (handoff.to !== agentId) { throw new Error(`Handoff ${handoffId} is not assigned to ${agentId}`); } if (handoff.status !== 'pending') { throw new Error(`Handoff ${handoffId} is not pending (status: ${handoff.status})`); } // Update status handoff.status = 'in_progress'; handoff.accepted_at = new Date().toISOString(); handoff.accepted_by = agentId; // Load artifacts for the accepting agent const artifacts = await this.loadHandoffArtifacts(handoff); // Update handoff document await this.updateHandoffDocument(handoff); // Log event this.logCollaboration({ type: 'handoff_accepted', handoff_id: handoffId, agent: agentId, timestamp: handoff.accepted_at, }); return { handoff, artifacts, }; } /** * Complete a handoff with deliverables */ async completeHandoff(handoffId, agentId, deliverables) { const handoff = this.activeHandoffs.get(handoffId); if (!handoff) { throw new Error(`Handoff ${handoffId} not found`); } if (handoff.accepted_by !== agentId) { throw new Error(`Handoff ${handoffId} was not accepted by ${agentId}`); } if (handoff.status !== 'in_progress') { throw new Error(`Handoff ${handoffId} is not in progress`); } // Validate deliverables against acceptance criteria const validation = await this.validateDeliverables(deliverables, handoff.acceptance_criteria); if (!validation.passed) { throw new Error( `Deliverables do not meet acceptance criteria: ${validation.issues.join(', ')}` ); } // Update handoff handoff.status = 'completed'; handoff.completed_at = new Date().toISOString(); handoff.deliverables = deliverables; handoff.validation_results = validation; // Store deliverables as artifacts for (const deliverable of deliverables) { await this.storeArtifact(deliverable); } // Update document await this.updateHandoffDocument(handoff); // Log event this.logCollaboration({ type: 'handoff_completed', handoff_id: handoffId, agent: agentId, deliverables_count: deliverables.length, timestamp: handoff.completed_at, }); return handoff; } /** * Request collaborative review */ async requestReview(config) { const reviewId = this.generateReviewId(); const review = { id: reviewId, type: 'collaborative_review', requester: config.requester, reviewers: config.reviewers || [], artifacts: config.artifacts || [], status: 'pending', created_at: new Date().toISOString(), review_criteria: config.criteria || [], responses: [], consensus: null, }; // Notify reviewers for (const reviewer of review.reviewers) { await this.createHandoff({ from: config.requester, to: reviewer, artifacts: config.artifacts, requirements: [`Review ${config.subject || 'artifacts'}`], acceptance_criteria: config.criteria, metadata: { type: 'review_request', review_id: reviewId, }, }); } // Store review const reviewPath = path.join(this.handoffsDir, `review-${reviewId}.json`); await fs.writeFile(reviewPath, JSON.stringify(review, null, 2)); // Log event this.logCollaboration({ type: 'review_requested', review_id: reviewId, requester: config.requester, reviewers: config.reviewers, timestamp: review.created_at, }); return review; } /** * Submit review feedback */ async submitReviewFeedback(reviewId, reviewerId, feedback) { const reviewPath = path.join(this.handoffsDir, `review-${reviewId}.json`); const reviewContent = await fs.readFile(reviewPath, 'utf8'); const review = JSON.parse(reviewContent); if (!review.reviewers.includes(reviewerId)) { throw new Error(`${reviewerId} is not a reviewer for review ${reviewId}`); } // Add feedback review.responses.push({ reviewer: reviewerId, feedback: feedback.feedback, approval: feedback.approval, suggestions: feedback.suggestions || [], timestamp: new Date().toISOString(), }); // Check if all reviewers have responded if (review.responses.length === review.reviewers.length) { review.status = 'completed'; review.consensus = this.calculateConsensus(review.responses); } // Update review document await fs.writeFile(reviewPath, JSON.stringify(review, null, 2)); // Log event this.logCollaboration({ type: 'review_feedback_submitted', review_id: reviewId, reviewer: reviewerId, approval: feedback.approval, timestamp: new Date().toISOString(), }); return review; } /** * Create a collaboration sequence */ async createCollaborationSequence(config) { const sequenceId = this.generateSequenceId(); const sequence = { id: sequenceId, name: config.name, description: config.description, steps: [], status: 'pending', created_at: new Date().toISOString(), }; // Build collaboration chain for (let i = 0; i < config.agents.length - 1; i++) { const fromAgent = config.agents[i]; const toAgent = config.agents[i + 1]; const step = { order: i + 1, from: fromAgent.agent, to: toAgent.agent, creates: fromAgent.creates || [], requires: toAgent.requires || [], transforms: fromAgent.transforms || [], handoff_id: null, status: 'pending', }; sequence.steps.push(step); } // Store sequence const sequencePath = path.join(this.handoffsDir, `sequence-${sequenceId}.json`); await fs.writeFile(sequencePath, JSON.stringify(sequence, null, 2)); // Start first handoff if (sequence.steps.length > 0) { const firstStep = sequence.steps[0]; const handoff = await this.createHandoff({ from: firstStep.from, to: firstStep.to, artifacts: firstStep.creates, requirements: firstStep.requires, metadata: { sequence_id: sequenceId, step_order: 1, }, }); firstStep.handoff_id = handoff.id; firstStep.status = 'in_progress'; // Update sequence await fs.writeFile(sequencePath, JSON.stringify(sequence, null, 2)); } return sequence; } /** * Check compatibility between agents */ async checkAgentCompatibility(fromAgent, toAgent) { const from = this.agentRegistry.get(fromAgent); const to = this.agentRegistry.get(toAgent); if (!from || !to) { return { compatible: false, message: 'One or both agents not found', }; } // Check if outputs of 'from' match inputs of 'to' const outputsMatch = from.outputs.some((output) => to.inputs.includes(output)); if (!outputsMatch) { return { compatible: true, // Still allow, but warn message: `Warning: No direct output-input match between ${fromAgent} and ${toAgent}`, }; } return { compatible: true, message: 'Agents are compatible', }; } /** * Create handoff document */ async createHandoffDocument(handoff) { const doc = `# Handoff: ${handoff.from}${handoff.to} **ID:** ${handoff.id} **Status:** ${handoff.status} **Created:** ${handoff.created_at} **Priority:** ${handoff.metadata.priority} ## Context ${JSON.stringify(handoff.context, null, 2)} ## Requirements ${handoff.requirements.map((r) => `- ${r}`).join('\n')} ## Acceptance Criteria ${handoff.acceptance_criteria.map((c) => `- ${c}`).join('\n')} ## Artifacts ${handoff.artifacts.map((a) => `- ${a.name || a}`).join('\n')} ## Instructions for ${handoff.to} 1. Review the provided context and artifacts 2. Ensure all requirements are addressed 3. Validate deliverables against acceptance criteria 4. Complete the handoff with your deliverables --- *This handoff was created by the Agent Collaboration Protocol* `; const docPath = path.join(this.handoffsDir, `${handoff.id}.md`); await fs.writeFile(docPath, doc); } /** * Update handoff document */ async updateHandoffDocument(handoff) { await this.createHandoffDocument(handoff); // Also update JSON record const jsonPath = path.join(this.handoffsDir, `${handoff.id}.json`); await fs.writeFile(jsonPath, JSON.stringify(handoff, null, 2)); } /** * Load handoff artifacts */ async loadHandoffArtifacts(handoff) { const artifacts = []; for (const artifactRef of handoff.artifacts) { const artifactPath = path.join( this.artifactsDir, typeof artifactRef === 'string' ? artifactRef : artifactRef.path ); try { const content = await fs.readFile(artifactPath, 'utf8'); artifacts.push({ name: artifactRef.name || artifactRef, content, }); } catch (error) { console.warn(`Failed to load artifact: ${artifactPath}`); } } return artifacts; } /** * Store artifact */ async storeArtifact(artifact) { const filename = artifact.name || `artifact-${Date.now()}.md`; const artifactPath = path.join(this.artifactsDir, filename); await fs.writeFile(artifactPath, artifact.content || artifact); return artifactPath; } /** * Validate deliverables against criteria */ async validateDeliverables(deliverables, criteria) { const issues = []; for (const criterion of criteria) { // Simple validation - can be extended const met = deliverables.some( (d) => d.name?.includes(criterion) || d.content?.includes(criterion) || d.toString().includes(criterion) ); if (!met) { issues.push(`Criterion not met: ${criterion}`); } } return { passed: issues.length === 0, issues, }; } /** * Calculate consensus from review responses */ calculateConsensus(responses) { const approvals = responses.filter((r) => r.approval).length; const total = responses.length; const approvalRate = approvals / total; if (approvalRate >= 1) { return 'unanimous_approval'; } else if (approvalRate >= 0.75) { return 'strong_approval'; } else if (approvalRate >= 0.5) { return 'majority_approval'; } else { return 'needs_revision'; } } /** * Log collaboration event */ logCollaboration(event) { this.collaborationLog.push(event); // Also write to file for persistence const logPath = path.join(this.handoffsDir, 'collaboration.log'); fs.appendFile(logPath, JSON.stringify(event) + '\n').catch(console.error); } /** * Get handoff status */ getHandoffStatus(handoffId) { return this.activeHandoffs.get(handoffId); } /** * List active handoffs */ listActiveHandoffs(agentId) { const handoffs = []; for (const [id, handoff] of this.activeHandoffs) { if (!agentId || handoff.from === agentId || handoff.to === agentId) { handoffs.push(handoff); } } return handoffs; } /** * Get collaboration metrics */ getCollaborationMetrics() { const metrics = { total_handoffs: this.activeHandoffs.size, pending_handoffs: 0, in_progress_handoffs: 0, completed_handoffs: 0, collaboration_events: this.collaborationLog.length, active_agents: new Set(), }; for (const handoff of this.activeHandoffs.values()) { metrics[`${handoff.status}_handoffs`]++; metrics.active_agents.add(handoff.from); metrics.active_agents.add(handoff.to); } metrics.active_agents = metrics.active_agents.size; return metrics; } /** * Generate unique handoff ID */ generateHandoffId() { return `handoff-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`; } /** * Generate unique review ID */ generateReviewId() { return `review-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`; } /** * Generate unique sequence ID */ generateSequenceId() { return `sequence-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`; } } module.exports = AgentCollaborationProtocol;