UNPKG

sf-agent-framework

Version:

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

399 lines (326 loc) 13.9 kB
const fs = require("node:fs").promises; const path = require("node:path"); const DependencyResolver = require("../lib/dependency-resolver"); const yamlUtils = require("../lib/yaml-utils"); class WebBuilder { constructor(options = {}) { this.rootDir = options.rootDir || process.cwd(); this.outputDirs = options.outputDirs || [path.join(this.rootDir, "dist")]; this.resolver = new DependencyResolver(this.rootDir); this.templatePath = path.join( this.rootDir, "tools", "md-assets", "web-agent-startup-instructions.md" ); } parseYaml(content) { const yaml = require("js-yaml"); return yaml.load(content); } convertToWebPath(filePath, bundleRoot = 'sf-core') { // Convert absolute paths to web bundle paths with dot prefix const relativePath = path.relative(this.rootDir, filePath); const pathParts = relativePath.split(path.sep); // For sf-core, common, etc., remove the first part let resourcePath = pathParts.slice(1).join('/'); return `.${bundleRoot}/${resourcePath}`; } generateWebInstructions(bundleType) { // Generate dynamic web instructions based on bundle type const rootExample = '.sf-core'; const examplePath = '.sf-core/folder/filename.md'; const personasExample = '.sf-core/personas/analyst.md'; const tasksExample = '.sf-core/tasks/create-story.md'; const utilsExample = '.sf-core/utils/template-format.md'; const tasksRef = '.sf-core/tasks/create-story.md'; const slashCommands = ` ## 🎯 SLASH COMMANDS (Use these to control the AI) ### Core Commands - **\*help** - Show all available commands and how to use them - **\*status** - Display current agent, phase, context usage, and active artifacts - **\*reset** - Reset to initial state, clearing all context and artifacts ### Agent Commands - **\*agent <name>** - Switch to a specific agent (e.g., \*agent sf-architect) - **\*list-agents** - Show all available agents in this bundle - **\*transform <agent>** - Transform orchestrator into any specialist agent - **\*orchestrator** - Return to orchestrator mode ### Phase Management - **\*phase <planning|development>** - Switch between planning (128k) and development (32k) phases - **\*context** - Show current context allocation and usage ### Workflow Commands - **\*workflow** - Start interactive workflow with user choices - **\*task <task-name>** - Execute a specific task - **\*handoff <to-agent>** - Create handoff to another agent with artifacts ### Artifact Management - **\*artifacts** - List all created artifacts in this session - **\*save <artifact-name>** - Mark artifact as saved (for tracking) - **\*load <path>** - Load resource from embedded bundle ### Session Management - **\*checkpoint** - Create a checkpoint of current state - **\*summary** - Generate summary of work completed - **\*next-steps** - Suggest next actions based on current state ### Discovery Commands - **\*capabilities** - Show what the current agent can do - **\*dependencies** - List all loaded dependencies - **\*templates** - Show available templates - **\*checklists** - Show available checklists`; const commonInstructions = ` ## CRITICAL: Embedded Resource Location All resources in this bundle are embedded below and prefixed with "${rootExample}/". You MUST use these exact paths when loading resources: Example references: - "${examplePath}" - "${personasExample}" - "${tasksExample}" - "${utilsExample}" NEVER construct your own paths. Always use the embedded paths shown below. ${slashCommands}`; if (bundleType === "agent") { return `# Agent Bundle - Web UI Version ${commonInstructions} ## Resource Loading You are operating in a web environment with all resources embedded in this file. To use resources: 1. When you see dependencies like "templates#prd-template.md", look for the embedded section "${rootExample}/templates/prd-template.md" 2. Read and use the content from that section 3. Use run task patterns like: "run ${tasksRef} with parameters" ## Quick Start 1. Type **\*help** to see all available commands 2. Type **\*capabilities** to see what this agent can do 3. Type **\*workflow** to start an interactive session `; } else if (bundleType === "team") { return `# Team Bundle - Web UI Version ${commonInstructions} ## Team Configuration This bundle contains a complete team configuration with all agents and their dependencies. The team manifest is embedded below and defines which agents are part of this team. ## Resource Loading All team resources are embedded with paths like "${rootExample}/[resource-type]/[filename]". Use these exact paths when referencing any resource. ## Quick Start with Team 1. Type **\*help** to see all available commands 2. Type **\*list-agents** to see all team members 3. Type **\*agent <name>** to switch to a specific agent 4. Type **\*workflow** to start team workflow `; } return ""; } formatSection(fileName, content, bundleRoot = 'sf-core') { const webPath = this.convertToWebPath(fileName, bundleRoot); return ` <$>-=-=-=-=<$> File: ${webPath} <$>-=-=-=-=<$> ${content}`; } async ensureDirectoryExists(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); } catch (error) { console.error(`Failed to create directory ${dirPath}:`, error.message); } } async cleanOutputDirs() { for (const dir of this.outputDirs) { try { await fs.rmdir(dir, { recursive: true }); console.log(`Cleaned output directory: ${dir}`); } catch (error) { // Directory might not exist, which is fine } } } async buildAgents() { const agentsDir = path.join(this.rootDir, "sf-core", "agents"); const agentFiles = await fs.readdir(agentsDir); const markdownAgents = agentFiles.filter((f) => f.endsWith(".md")); console.log(`\nBuilding ${markdownAgents.length} agent bundles...`); for (const agentFile of markdownAgents) { const agentId = agentFile.replace(".md", ""); console.log(` Building agent: ${agentId}`); // Build individual agent bundle const bundle = await this.buildAgentBundle(agentId); // Write to all output directories for (const outputDir of this.outputDirs) { // Core agents let outputPath = path.join(outputDir, "agents"); let outputFile = path.join(outputPath, `${agentId}.txt`); await this.ensureDirectoryExists(outputPath); await fs.writeFile(outputFile, bundle, "utf8"); console.log(` ✓ Written to ${outputFile}`); } } console.log(`✓ Built ${markdownAgents.length} agent bundles`); } async buildAgentBundle(agentId) { let bundleRoot = 'sf-core'; const template = this.generateWebInstructions("agent"); const sections = [template]; // Add command processor const commandProcessorPath = path.join(this.rootDir, "tools", "lib", "web-command-processor.js"); try { const commandProcessor = await fs.readFile(commandProcessorPath, "utf8"); sections.push(` ## 🎮 Command Processor (Embedded) \`\`\`javascript ${commandProcessor} \`\`\` To use commands, instantiate the processor: \`\`\`javascript const processor = new WebCommandProcessor(); const response = processor.processCommand('*help'); console.log(response); \`\`\` `); } catch (error) { console.warn(" ⚠ Command processor not found, skipping"); } // Load agent file const agentPath = path.join(this.rootDir, "sf-core", "agents", `${agentId}.md`); const agentContent = await fs.readFile(agentPath, "utf8"); sections.push(this.formatSection(agentPath, agentContent, bundleRoot)); // Parse agent YAML to get dependencies const agentYaml = agentContent.match(/```yaml\n([\s\S]*?)\n```/); if (agentYaml) { try { const config = this.parseYaml(agentYaml[1]); if (config.dependencies) { // Collect all dependencies const allDependencies = new Set(); for (const [depType, deps] of Object.entries(config.dependencies)) { if (Array.isArray(deps)) { for (const dep of deps) { allDependencies.add(`${depType}#${dep}`); } } } // Load dependencies console.log(` Loading ${allDependencies.size} dependencies...`); for (const depKey of allDependencies) { const [type, name] = depKey.split("#"); const depPath = path.join(this.rootDir, "sf-core", type, name); try { const content = await fs.readFile(depPath, "utf8"); sections.push(this.formatSection(depPath, content, bundleRoot)); console.log(` ✓ Loaded ${type}#${name}`); } catch (error) { // Check common resources try { const commonPath = path.join(this.rootDir, "common", type, name); const content = await fs.readFile(commonPath, "utf8"); sections.push(this.formatSection(commonPath, content, bundleRoot)); console.log(` ✓ Loaded ${type}#${name} from common`); } catch (commonError) { console.warn(` ⚠ Dependency ${type}#${name} not found`); } } } } } catch (error) { console.error(` Failed to parse agent YAML: ${error.message}`); } } return sections.join("\n"); } async buildTeams() { const teamsDir = path.join(this.rootDir, "sf-core", "agent-teams"); const teamFiles = await fs.readdir(teamsDir); const yamlTeams = teamFiles.filter((f) => f.endsWith(".yaml")); console.log(`\nBuilding ${yamlTeams.length} team bundles...`); for (const teamFile of yamlTeams) { const teamId = teamFile.replace(".yaml", ""); console.log(` Building team: ${teamId}`); const teamPath = path.join(teamsDir, teamFile); const bundle = await this.buildTeamBundle(teamPath); // Write to all output directories for (const outputDir of this.outputDirs) { const outputPath = path.join(outputDir, "teams"); await this.ensureDirectoryExists(outputPath); const outputFile = path.join(outputPath, `${teamId}.txt`); await fs.writeFile(outputFile, bundle, "utf8"); console.log(` ✓ Written to ${outputFile}`); } } console.log(`✓ Built ${yamlTeams.length} team bundles`); } async buildTeamBundle(teamConfigPath) { const template = this.generateWebInstructions("team"); const sections = [template]; // Add command processor const commandProcessorPath = path.join(this.rootDir, "tools", "lib", "web-command-processor.js"); try { const commandProcessor = await fs.readFile(commandProcessorPath, "utf8"); sections.push(` ## 🎮 Command Processor (Embedded) \`\`\`javascript ${commandProcessor} \`\`\` To use commands, instantiate the processor: \`\`\`javascript const processor = new WebCommandProcessor(); const response = processor.processCommand('*help'); console.log(response); \`\`\` `); } catch (error) { console.warn(" ⚠ Command processor not found, skipping"); } // Add team configuration const teamContent = await fs.readFile(teamConfigPath, "utf8"); const teamWebPath = this.convertToWebPath(teamConfigPath); sections.push(this.formatSection(teamConfigPath, teamContent)); // Parse team config const teamConfig = this.parseYaml(teamContent); const allDependencies = new Map(); // Process each agent in the team for (const agentId of teamConfig.agents || []) { try { const agentPath = path.join(this.rootDir, "sf-core", "agents", `${agentId}.md`); const agentContent = await fs.readFile(agentPath, "utf8"); sections.push(this.formatSection(agentPath, agentContent)); // Parse agent dependencies const agentYaml = agentContent.match(/```yaml\n([\s\S]*?)\n```/); if (agentYaml) { try { const agentConfig = this.parseYaml(agentYaml[1]); if (agentConfig.dependencies) { for (const [depType, deps] of Object.entries(agentConfig.dependencies)) { if (Array.isArray(deps)) { for (const dep of deps) { const key = `${depType}#${dep}`; if (!allDependencies.has(key)) { allDependencies.set(key, { type: depType, name: dep }); } } } } } } catch (error) { console.warn(` ⚠ Failed to parse YAML for agent ${agentId}`); } } } catch (error) { console.warn(` ⚠ Agent ${agentId} not found`); } } // Load all collected dependencies console.log(` Loading ${allDependencies.size} unique dependencies...`); for (const [key, dep] of allDependencies) { const depPath = path.join(this.rootDir, "sf-core", dep.type, dep.name); try { const content = await fs.readFile(depPath, "utf8"); sections.push(this.formatSection(depPath, content)); console.log(` ✓ Loaded ${key}`); } catch (error) { // Try common resources try { const commonPath = path.join(this.rootDir, "common", dep.type, dep.name); const content = await fs.readFile(commonPath, "utf8"); sections.push(this.formatSection(commonPath, content)); console.log(` ✓ Loaded ${key} from common`); } catch (commonError) { console.warn(` ⚠ Dependency ${key} not found`); } } } return sections.join("\n"); } } module.exports = WebBuilder;