sf-agent-framework
Version:
AI Agent Orchestration Framework for Salesforce Development - Two-phase architecture with 70% context reduction
663 lines (571 loc) ⢠18.9 kB
JavaScript
#!/usr/bin/env node
/**
* Interactive Workflow Builder for SF-Agent Framework
* Enables dynamic workflow execution with user choice points, branching logic, and validation gates
*/
const fs = require('fs-extra');
const path = require('path');
const yaml = require('js-yaml');
const chalk = require('chalk');
const inquirer = require('inquirer');
class InteractiveWorkflowBuilder {
constructor(options = {}) {
this.options = {
workflowDir: options.workflowDir || 'sf-core/workflows',
outputDir: options.outputDir || 'docs/workflow-sessions',
templateDir: options.templateDir || 'sf-core/templates',
verbose: options.verbose || false,
...options,
};
this.currentWorkflow = null;
this.workflowState = {
phase: 'initialization',
completedSteps: [],
artifacts: {},
decisions: {},
validationResults: {},
context: {},
};
this.choiceHistory = [];
this.branchPath = [];
}
/**
* Start an interactive workflow session
*/
async startWorkflow(workflowId) {
console.log(chalk.blue(`\nš Starting Interactive Workflow: ${workflowId}`));
try {
// Load workflow definition
const workflowPath = path.join(this.options.workflowDir, `${workflowId}.yaml`);
const workflowContent = await fs.readFile(workflowPath, 'utf-8');
this.currentWorkflow = yaml.load(workflowContent);
// Validate workflow structure
this.validateWorkflow(this.currentWorkflow);
// Initialize workflow session
await this.initializeSession();
// Execute workflow phases
await this.executeWorkflow();
// Save session results
await this.saveSession();
console.log(chalk.green('\nā
Workflow completed successfully!'));
return this.workflowState;
} catch (error) {
console.error(chalk.red(`\nā Workflow error: ${error.message}`));
throw error;
}
}
/**
* Validate workflow has required interactive elements
*/
validateWorkflow(workflow) {
if (!workflow.workflow) {
throw new Error('Invalid workflow: missing workflow definition');
}
const required = ['id', 'name', 'phases'];
for (const field of required) {
if (!workflow.workflow[field]) {
throw new Error(`Workflow missing required field: ${field}`);
}
}
}
/**
* Initialize workflow session
*/
async initializeSession() {
console.log(chalk.cyan('\nš Initializing workflow session...'));
// Gather initial context
const { projectType } = await inquirer.prompt([
{
type: 'list',
name: 'projectType',
message: 'What type of Salesforce project are you working on?',
choices: [
{ name: 'New Org Implementation (Greenfield)', value: 'greenfield' },
{ name: 'Existing Org Enhancement (Brownfield)', value: 'brownfield' },
{ name: 'Data Migration Project', value: 'migration' },
{ name: 'Integration Development', value: 'integration' },
{ name: 'Package Development', value: 'package' },
{ name: 'Other/Custom', value: 'custom' },
],
},
]);
this.workflowState.context.projectType = projectType;
// Get project complexity
const { complexity } = await inquirer.prompt([
{
type: 'list',
name: 'complexity',
message: 'What is the project complexity?',
choices: [
{ name: 'Simple (< 10 user stories)', value: 'simple' },
{ name: 'Medium (10-50 user stories)', value: 'medium' },
{ name: 'Complex (50-200 user stories)', value: 'complex' },
{ name: 'Enterprise (200+ user stories)', value: 'enterprise' },
],
},
]);
this.workflowState.context.complexity = complexity;
// Ask about specific needs
const { needs } = await inquirer.prompt([
{
type: 'checkbox',
name: 'needs',
message: 'Select all that apply to your project:',
choices: [
{ name: 'Requirements Gathering', value: 'requirements' },
{ name: 'Architecture Design', value: 'architecture' },
{ name: 'Data Model Design', value: 'datamodel' },
{ name: 'Integration Design', value: 'integration' },
{ name: 'Security Design', value: 'security' },
{ name: 'UI/UX Design', value: 'uiux' },
{ name: 'Development Planning', value: 'development' },
{ name: 'Testing Strategy', value: 'testing' },
{ name: 'Deployment Planning', value: 'deployment' },
],
},
]);
this.workflowState.context.needs = needs;
}
/**
* Execute workflow with interactive decision points
*/
async executeWorkflow() {
const phases = this.currentWorkflow.workflow.phases;
for (const phase of phases) {
console.log(chalk.blue(`\nš Phase: ${phase.name}`));
// Check if phase has prerequisites
if (phase.prerequisites) {
const canProceed = await this.checkPrerequisites(phase.prerequisites);
if (!canProceed) {
const { skipOrResolve } = await inquirer.prompt([
{
type: 'list',
name: 'skipOrResolve',
message: `Prerequisites not met for ${phase.name}. What would you like to do?`,
choices: [
{ name: 'Resolve prerequisites first', value: 'resolve' },
{ name: 'Skip this phase', value: 'skip' },
{ name: 'Continue anyway (not recommended)', value: 'continue' },
],
},
]);
if (skipOrResolve === 'skip') {
console.log(chalk.yellow(`āļø Skipping phase: ${phase.name}`));
continue;
} else if (skipOrResolve === 'resolve') {
await this.resolvePrerequisites(phase.prerequisites);
}
}
}
// Execute phase based on type
if (phase.interactive) {
await this.executeInteractivePhase(phase);
} else if (phase.conditional) {
await this.executeConditionalPhase(phase);
} else if (phase.parallel) {
await this.executeParallelPhase(phase);
} else {
await this.executeSequentialPhase(phase);
}
// Phase validation gate
if (phase.validation_gate) {
await this.executeValidationGate(phase);
}
this.workflowState.phase = phase.name;
}
}
/**
* Execute interactive phase with user choices
*/
async executeInteractivePhase(phase) {
console.log(chalk.cyan(`\nšÆ Interactive Phase: ${phase.name}`));
for (const step of phase.steps) {
if (step.user_choice) {
const choice = await this.presentUserChoice(step);
this.choiceHistory.push({ step: step.id, choice });
// Execute branch based on choice
const branch = step.branches[choice];
if (branch) {
await this.executeBranch(branch);
}
} else if (step.optional) {
const { execute } = await inquirer.prompt([
{
type: 'confirm',
name: 'execute',
message: `Would you like to execute: ${step.description}?`,
default: step.recommended || false,
},
]);
if (execute) {
await this.executeStep(step);
}
} else {
await this.executeStep(step);
}
}
}
/**
* Present user choice and get selection
*/
async presentUserChoice(step) {
const choices = step.choices.map((choice) => ({
name: `${choice.label}${choice.recommended ? ' (Recommended)' : ''}`,
value: choice.value,
short: choice.value,
}));
const { selection } = await inquirer.prompt([
{
type: 'list',
name: 'selection',
message: step.question || `Choose an option for ${step.name}:`,
choices,
},
]);
this.workflowState.decisions[step.id] = selection;
return selection;
}
/**
* Execute conditional phase based on context
*/
async executeConditionalPhase(phase) {
console.log(chalk.cyan(`\nš Conditional Phase: ${phase.name}`));
for (const condition of phase.conditions) {
if (await this.evaluateCondition(condition)) {
console.log(chalk.green(`ā Condition met: ${condition.description}`));
await this.executeBranch(condition.then_branch);
return;
}
}
// Execute default branch if no conditions met
if (phase.default_branch) {
console.log(chalk.yellow('ā Executing default branch'));
await this.executeBranch(phase.default_branch);
}
}
/**
* Execute parallel phase with track selection
*/
async executeParallelPhase(phase) {
console.log(chalk.cyan(`\nā” Parallel Phase: ${phase.name}`));
const { tracks } = await inquirer.prompt([
{
type: 'checkbox',
name: 'tracks',
message: 'Select which tracks to execute in parallel:',
choices: phase.tracks.map((track) => ({
name: track.name,
value: track.id,
checked: track.default !== false,
})),
},
]);
// Execute selected tracks
for (const trackId of tracks) {
const track = phase.tracks.find((t) => t.id === trackId);
console.log(chalk.blue(`\nš Track: ${track.name}`));
for (const step of track.steps) {
await this.executeStep(step);
}
}
}
/**
* Execute sequential phase
*/
async executeSequentialPhase(phase) {
console.log(chalk.cyan(`\nā”ļø Sequential Phase: ${phase.name}`));
for (const step of phase.steps) {
await this.executeStep(step);
}
}
/**
* Execute individual step
*/
async executeStep(step) {
console.log(chalk.gray(` ⢠Executing: ${step.name || step.action}`));
// Simulate step execution
if (step.agent) {
console.log(chalk.gray(` Agent: ${step.agent}`));
}
if (step.task) {
console.log(chalk.gray(` Task: ${step.task}`));
}
if (step.creates) {
const artifacts = Array.isArray(step.creates) ? step.creates : [step.creates];
for (const artifact of artifacts) {
this.workflowState.artifacts[artifact] = {
created_by: step.agent || 'system',
created_at: new Date().toISOString(),
step: step.name,
};
console.log(chalk.green(` ā Created: ${artifact}`));
}
}
this.workflowState.completedSteps.push({
name: step.name,
agent: step.agent,
timestamp: new Date().toISOString(),
});
}
/**
* Execute branch
*/
async executeBranch(branch) {
this.branchPath.push(branch.id || branch.name);
if (branch.steps) {
for (const step of branch.steps) {
await this.executeStep(step);
}
}
if (branch.nested_choice) {
const choice = await this.presentUserChoice(branch.nested_choice);
const nestedBranch = branch.nested_choice.branches[choice];
if (nestedBranch) {
await this.executeBranch(nestedBranch);
}
}
}
/**
* Check prerequisites
*/
async checkPrerequisites(prerequisites) {
for (const prereq of prerequisites) {
if (prereq.artifact) {
if (!this.workflowState.artifacts[prereq.artifact]) {
console.log(chalk.yellow(` ā ļø Missing artifact: ${prereq.artifact}`));
return false;
}
}
if (prereq.decision) {
if (!this.workflowState.decisions[prereq.decision]) {
console.log(chalk.yellow(` ā ļø Missing decision: ${prereq.decision}`));
return false;
}
}
}
return true;
}
/**
* Resolve prerequisites interactively
*/
async resolvePrerequisites(prerequisites) {
console.log(chalk.cyan('\nš§ Resolving prerequisites...'));
for (const prereq of prerequisites) {
if (prereq.artifact && !this.workflowState.artifacts[prereq.artifact]) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: `Missing artifact: ${prereq.artifact}. How would you like to proceed?`,
choices: [
{ name: 'Create it now', value: 'create' },
{ name: 'Mark as existing', value: 'mark' },
{ name: 'Skip', value: 'skip' },
],
},
]);
if (action === 'create' || action === 'mark') {
this.workflowState.artifacts[prereq.artifact] = {
resolved: true,
resolution: action,
timestamp: new Date().toISOString(),
};
}
}
}
}
/**
* Evaluate condition
*/
async evaluateCondition(condition) {
if (condition.context_check) {
const contextValue = this.getContextValue(condition.context_check.path);
return this.compareValues(
contextValue,
condition.context_check.value,
condition.context_check.operator
);
}
if (condition.artifact_exists) {
return !!this.workflowState.artifacts[condition.artifact_exists];
}
if (condition.decision_equals) {
const decision = this.workflowState.decisions[condition.decision_equals.decision];
return decision === condition.decision_equals.value;
}
if (condition.user_confirm) {
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: condition.user_confirm,
default: false,
},
]);
return confirmed;
}
return false;
}
/**
* Execute validation gate
*/
async executeValidationGate(phase) {
console.log(chalk.cyan(`\nš¦ Validation Gate: ${phase.validation_gate.name}`));
const validations = phase.validation_gate.checks || [];
const results = [];
for (const check of validations) {
const result = await this.performValidation(check);
results.push(result);
if (result.passed) {
console.log(chalk.green(` ā ${check.name}: PASSED`));
} else {
console.log(chalk.red(` ā ${check.name}: FAILED`));
}
}
const allPassed = results.every((r) => r.passed);
if (!allPassed && phase.validation_gate.required) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Validation gate failed. How would you like to proceed?',
choices: [
{ name: 'Fix issues and retry', value: 'retry' },
{ name: 'Continue with warnings', value: 'continue' },
{ name: 'Abort workflow', value: 'abort' },
],
},
]);
if (action === 'abort') {
throw new Error('Workflow aborted at validation gate');
} else if (action === 'retry') {
await this.executeValidationGate(phase);
}
}
this.workflowState.validationResults[phase.name] = results;
}
/**
* Perform individual validation
*/
async performValidation(check) {
// Simulate validation logic
if (check.type === 'artifact_exists') {
return {
name: check.name,
passed: !!this.workflowState.artifacts[check.artifact],
};
}
if (check.type === 'manual_review') {
const { passed } = await inquirer.prompt([
{
type: 'confirm',
name: 'passed',
message: `Manual validation: ${check.question}`,
default: false,
},
]);
return { name: check.name, passed };
}
if (check.type === 'automated') {
// Simulate automated check
return {
name: check.name,
passed: Math.random() > 0.3, // 70% pass rate for demo
};
}
return { name: check.name, passed: true };
}
/**
* Get context value by path
*/
getContextValue(path) {
const parts = path.split('.');
let value = this.workflowState.context;
for (const part of parts) {
value = value[part];
if (value === undefined) break;
}
return value;
}
/**
* Compare values with operator
*/
compareValues(value1, value2, operator = '==') {
switch (operator) {
case '==':
case 'equals':
return value1 === value2;
case '!=':
case 'not_equals':
return value1 !== value2;
case 'includes':
return Array.isArray(value1) ? value1.includes(value2) : false;
case 'in':
return Array.isArray(value2) ? value2.includes(value1) : false;
default:
return false;
}
}
/**
* Save workflow session
*/
async saveSession() {
const sessionId = `session-${Date.now()}`;
const sessionPath = path.join(this.options.outputDir, `${sessionId}.yaml`);
await fs.ensureDir(this.options.outputDir);
const session = {
id: sessionId,
workflow: this.currentWorkflow.workflow.id,
timestamp: new Date().toISOString(),
state: this.workflowState,
choices: this.choiceHistory,
branch_path: this.branchPath,
};
await fs.writeFile(sessionPath, yaml.dump(session), 'utf-8');
console.log(chalk.green(`\nš¾ Session saved: ${sessionPath}`));
}
}
// CLI Interface
if (require.main === module) {
const program = require('commander');
program
.version('1.0.0')
.description('Interactive Workflow Builder for Salesforce projects')
.argument('[workflow]', 'Workflow ID to execute')
.option('-d, --dir <directory>', 'Workflow directory', 'sf-core/workflows')
.option('-o, --output <directory>', 'Output directory', 'docs/workflow-sessions')
.option('-v, --verbose', 'Verbose output')
.action(async (workflow, options) => {
try {
const builder = new InteractiveWorkflowBuilder({
workflowDir: options.dir,
outputDir: options.output,
verbose: options.verbose,
});
if (!workflow) {
// List available workflows
const files = await fs.readdir(options.dir);
const workflows = files.filter((f) => f.endsWith('.yaml'));
console.log(chalk.blue('\nš Available workflows:'));
workflows.forEach((w) => {
console.log(` ⢠${w.replace('.yaml', '')}`);
});
const { selected } = await inquirer.prompt([
{
type: 'list',
name: 'selected',
message: 'Select a workflow to execute:',
choices: workflows.map((w) => w.replace('.yaml', '')),
},
]);
workflow = selected;
}
await builder.startWorkflow(workflow);
} catch (error) {
console.error(chalk.red(`\nā Error: ${error.message}`));
process.exit(1);
}
});
program.parse();
}
module.exports = InteractiveWorkflowBuilder;