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