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