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
JavaScript
/**
* 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;