sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
250 lines (213 loc) • 7.69 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const { extractYamlFromAgent } = require('./yaml-utils');
class DependencyResolver {
constructor(rootDir) {
this.rootDir = rootDir;
this.sfCore = path.join(rootDir, 'sf-core');
this.common = path.join(rootDir, 'common');
this.cache = new Map();
}
async resolveAgentDependencies(agentId) {
let agentPath;
// Check if this is an expansion pack agent (format: pack/agent)
if (agentId.includes('/')) {
const [pack, agent] = agentId.split('/');
agentPath = path.join(this.rootDir, 'expansion-packs', pack, 'agents', `${agent}.md`);
} else {
// Core agent
agentPath = path.join(this.sfCore, 'agents', `${agentId}.md`);
}
const agentContent = await fs.readFile(agentPath, 'utf8');
// Extract YAML from markdown content with command cleaning
const yamlContent = extractYamlFromAgent(agentContent, true);
if (!yamlContent) {
throw new Error(`No YAML configuration found in agent ${agentId}`);
}
const agentConfig = yaml.load(yamlContent);
const dependencies = {
agent: {
id: agentId,
path: agentPath,
content: agentContent,
config: agentConfig,
},
resources: [],
};
// Personas are now embedded in agent configs, no need to resolve separately
// Resolve other dependencies
const depTypes = ['tasks', 'templates', 'checklists', 'data', 'utils'];
for (const depType of depTypes) {
const deps = agentConfig.dependencies?.[depType] || [];
for (const depId of deps) {
const resource = await this.loadResource(depType, depId);
if (resource) dependencies.resources.push(resource);
}
}
return dependencies;
}
async resolveTeamDependencies(teamId) {
const teamPath = path.join(this.sfCore, 'agent-teams', `${teamId}.yaml`);
const teamContent = await fs.readFile(teamPath, 'utf8');
const teamConfig = yaml.load(teamContent);
const dependencies = {
team: {
id: teamId,
path: teamPath,
content: teamContent,
config: teamConfig,
},
agents: [],
resources: new Map(), // Use Map to deduplicate resources
};
// Always add sf-orchestrator agent first if it's a team
const sfOrchestratorAgent = await this.resolveAgentDependencies('sf-orchestrator');
dependencies.agents.push(sfOrchestratorAgent.agent);
sfOrchestratorAgent.resources.forEach((res) => {
dependencies.resources.set(res.path, res);
});
// Resolve all agents in the team
let agentsToResolve = teamConfig.agents || [];
// Handle wildcard "*" - include all agents except sf-agent-master
if (agentsToResolve.includes('*')) {
const allAgents = await this.listAgents();
// Remove wildcard and add all agents except those already in the list and sf-agent-master
agentsToResolve = agentsToResolve.filter((a) => a !== '*');
for (const agent of allAgents) {
if (!agentsToResolve.includes(agent) && agent !== 'sf-agent-master') {
agentsToResolve.push(agent);
}
}
}
for (const agentId of agentsToResolve) {
if (agentId === 'sf-orchestrator' || agentId === 'sf-agent-master') continue; // Already added or excluded
const agentDeps = await this.resolveAgentDependencies(agentId);
dependencies.agents.push(agentDeps.agent);
// Add resources with deduplication
agentDeps.resources.forEach((res) => {
dependencies.resources.set(res.path, res);
});
}
// Resolve workflows
for (const workflowId of teamConfig.workflows || []) {
const resource = await this.loadResource('workflows', workflowId);
if (resource) dependencies.resources.set(resource.path, resource);
}
// Convert Map back to array
dependencies.resources = Array.from(dependencies.resources.values());
return dependencies;
}
async loadResource(type, id) {
const cacheKey = `${type}#${id}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
let content = null;
let filePath = null;
// Add .yaml extension for workflows if not already present
if (type === 'workflows' && !id.endsWith('.yaml') && !id.endsWith('.yml')) {
id = id + '.yaml';
}
// First try sf-core
try {
filePath = path.join(this.sfCore, type, id);
content = await fs.readFile(filePath, 'utf8');
} catch (e) {
// If not found in sf-core, try common folder
try {
filePath = path.join(this.common, type, id);
content = await fs.readFile(filePath, 'utf8');
} catch (e2) {
// If not found in common, try expansion packs
const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
try {
const packs = await fs.readdir(expansionPacksDir);
for (const pack of packs) {
try {
filePath = path.join(expansionPacksDir, pack, type, id);
content = await fs.readFile(filePath, 'utf8');
break; // Found it, stop looking
} catch (e3) {
// Continue to next pack
}
}
} catch (e4) {
// No expansion packs directory or other error
}
}
}
if (!content) {
console.warn(`Resource not found: ${type}/${id}`);
return null;
}
const resource = {
type,
id,
path: filePath,
content,
};
this.cache.set(cacheKey, resource);
return resource;
} catch (error) {
console.error(`Error loading resource ${type}/${id}:`, error.message);
return null;
}
}
async listAgents() {
try {
// Get core agents
const coreAgents = await this.listCoreAgents();
// Get expansion pack agents
const expansionAgents = await this.listExpansionPackAgents();
// Combine and return all agents
return [...coreAgents, ...expansionAgents];
} catch (error) {
return [];
}
}
async listCoreAgents() {
try {
const files = await fs.readdir(path.join(this.sfCore, 'agents'));
return files.filter((f) => f.endsWith('.md')).map((f) => f.replace('.md', ''));
} catch (error) {
return [];
}
}
async listExpansionPackAgents() {
const allAgents = [];
try {
const expansionPacksDir = path.join(this.rootDir, 'expansion-packs');
const packs = await fs.readdir(expansionPacksDir);
for (const pack of packs) {
const packPath = path.join(expansionPacksDir, pack);
const stat = await fs.stat(packPath);
if (stat.isDirectory()) {
const agentsDir = path.join(packPath, 'agents');
try {
const agentFiles = await fs.readdir(agentsDir);
const agents = agentFiles
.filter((f) => f.endsWith('.md'))
.map((f) => `${pack}/${f.replace('.md', '')}`);
allAgents.push(...agents);
} catch (error) {
// Skip if agents directory doesn't exist
}
}
}
} catch (error) {
// Return empty if expansion-packs directory doesn't exist
}
return allAgents;
}
async listTeams() {
try {
const files = await fs.readdir(path.join(this.sfCore, 'agent-teams'));
return files.filter((f) => f.endsWith('.yaml')).map((f) => f.replace('.yaml', ''));
} catch (error) {
return [];
}
}
}
module.exports = DependencyResolver;