UNPKG

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
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;