feat: implement granular step-file workflow architecture with multi-menu support

## Major Features Added
- **Step-file workflow architecture**: Transform monolithic workflows into granular step files for improved LLM adherence and consistency
- **Multi-menu handler system**: New `handler-multi.xml` enables grouped menu items with fuzzy matching
- **Workflow compliance checker**: Added automated compliance validation for all workflows
- **Create/edit agent workflows**: New structured workflows for agent creation and editing

## Workflow Enhancements
- **Create-workflow**: Expanded from 6 to 14 detailed steps covering tools, design, compliance
- **Granular step execution**: Each workflow step now has dedicated files for focused execution
- **New documentation**: Added CSV data standards, intent vs prescriptive spectrum, and common tools reference

## Complete Migration Status
- **4 workflows fully migrated**: `create-agent`, `edit-agent`, `create-workflow`, and `edit-workflow` now use the new granular step-file architecture
- **Legacy transformation**: `edit-workflow` includes built-in capability to transform legacy single-file workflows into the new improved granular format
- **Future cleanup**: Legacy versions will be removed in a future commit after validation

## Schema Updates
- **Multi-menu support**: Updated agent schema to support `triggers` array for grouped menu items
- **Legacy compatibility**: Maintains backward compatibility with single `trigger` field
- **Discussion enhancements**: Added conversational_knowledge recommendation for discussion agents

## File Structure Changes
- Added: `create-agent/`, `edit-agent/`, `edit-workflow/`, `workflow-compliance-check/` workflows
- Added: Documentation standards and CSV reference files
- Refactored: `create-workflow/steps/` with detailed granular step files

## Handler Improvements
- Enhanced `handler-exec.xml` with clearer execution instructions
- Improved data passing context for executed files
- Better error handling and user guidance

This architectural change significantly improves workflow execution consistency across all LLM models by breaking complex processes into manageable, focused steps. The edit-workflow transformation tool ensures smooth migration of existing workflows to the new format.
This commit is contained in:
Brian Madison
2025-11-30 15:09:23 -06:00
parent 829d051c91
commit 4539ca7436
107 changed files with 12704 additions and 683 deletions

View File

@@ -1410,11 +1410,11 @@ class WebBundler {
const menuItems = [];
if (!hasHelp) {
menuItems.push(`${indent}<item cmd="*help">Show numbered menu</item>`);
menuItems.push(`${indent}<item cmd="*menu">[M] Redisplay Menu Options</item>`);
}
if (!hasExit) {
menuItems.push(`${indent}<item cmd="*exit">Exit with confirmation</item>`);
menuItems.push(`${indent}<item cmd="*dismiss">[D] Dismiss Agent</item>`);
}
if (menuItems.length === 0) {

View File

@@ -29,24 +29,52 @@ class AgentAnalyzer {
// Track the menu item
profile.menuItems.push(item);
// Check for each possible attribute
if (item.workflow) {
profile.usedAttributes.add('workflow');
}
if (item['validate-workflow']) {
profile.usedAttributes.add('validate-workflow');
}
if (item.exec) {
profile.usedAttributes.add('exec');
}
if (item.tmpl) {
profile.usedAttributes.add('tmpl');
}
if (item.data) {
profile.usedAttributes.add('data');
}
if (item.action) {
profile.usedAttributes.add('action');
// Check for multi format items
if (item.multi && item.triggers) {
profile.usedAttributes.add('multi');
// Also check attributes in nested handlers
for (const triggerGroup of item.triggers) {
for (const [triggerName, execArray] of Object.entries(triggerGroup)) {
if (Array.isArray(execArray)) {
for (const exec of execArray) {
if (exec.route) {
// Check if route is a workflow or exec
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
profile.usedAttributes.add('workflow');
} else {
profile.usedAttributes.add('exec');
}
}
if (exec.workflow) profile.usedAttributes.add('workflow');
if (exec.action) profile.usedAttributes.add('action');
if (exec.type && ['exec', 'action', 'workflow'].includes(exec.type)) {
profile.usedAttributes.add(exec.type);
}
}
}
}
}
} else {
// Check for each possible attribute in legacy items
if (item.workflow) {
profile.usedAttributes.add('workflow');
}
if (item['validate-workflow']) {
profile.usedAttributes.add('validate-workflow');
}
if (item.exec) {
profile.usedAttributes.add('exec');
}
if (item.tmpl) {
profile.usedAttributes.add('tmpl');
}
if (item.data) {
profile.usedAttributes.add('data');
}
if (item.action) {
profile.usedAttributes.add('action');
}
}
}

View File

@@ -243,44 +243,143 @@ function buildPromptsXml(prompts) {
/**
* Build menu XML section
* Supports both legacy and multi format menu items
* Multi items display as a single menu item with nested handlers
* @param {Array} menuItems - Menu items
* @returns {string} Menu XML
*/
function buildMenuXml(menuItems) {
let xml = ' <menu>\n';
// Always inject *help first
xml += ` <item cmd="*help">Show numbered menu</item>\n`;
// Always inject menu display option first
xml += ` <item cmd="*menu">[M] Redisplay Menu Options</item>\n`;
// Add user-defined menu items
if (menuItems && menuItems.length > 0) {
for (const item of menuItems) {
let trigger = item.trigger || '';
if (!trigger.startsWith('*')) {
trigger = '*' + trigger;
// Handle multi format menu items with nested handlers
if (item.multi && item.triggers && Array.isArray(item.triggers)) {
xml += ` <item type="multi">${escapeXml(item.multi)}\n`;
xml += buildNestedHandlers(item.triggers);
xml += ` </item>\n`;
}
// Handle legacy format menu items
else if (item.trigger) {
// For legacy items, keep using cmd with *<trigger> format
let trigger = item.trigger || '';
if (!trigger.startsWith('*')) {
trigger = '*' + trigger;
}
const attrs = [`cmd="${trigger}"`];
const attrs = [`cmd="${trigger}"`];
// Add handler attributes
if (item.workflow) attrs.push(`workflow="${item.workflow}"`);
if (item.exec) attrs.push(`exec="${item.exec}"`);
if (item.tmpl) attrs.push(`tmpl="${item.tmpl}"`);
if (item.data) attrs.push(`data="${item.data}"`);
if (item.action) attrs.push(`action="${item.action}"`);
// Add handler attributes
if (item.workflow) attrs.push(`workflow="${item.workflow}"`);
if (item.exec) attrs.push(`exec="${item.exec}"`);
if (item.tmpl) attrs.push(`tmpl="${item.tmpl}"`);
if (item.data) attrs.push(`data="${item.data}"`);
if (item.action) attrs.push(`action="${item.action}"`);
xml += ` <item ${attrs.join(' ')}>${escapeXml(item.description || '')}</item>\n`;
xml += ` <item ${attrs.join(' ')}>${escapeXml(item.description || '')}</item>\n`;
}
}
}
// Always inject *exit last
xml += ` <item cmd="*exit">Exit with confirmation</item>\n`;
// Always inject dismiss last
xml += ` <item cmd="*dismiss">[D] Dismiss Agent</item>\n`;
xml += ' </menu>\n';
return xml;
}
/**
* Build nested handlers for multi format menu items
* @param {Array} triggers - Triggers array from multi format
* @returns {string} Handler XML
*/
function buildNestedHandlers(triggers) {
let xml = '';
for (const triggerGroup of triggers) {
for (const [triggerName, execArray] of Object.entries(triggerGroup)) {
// Build trigger with * prefix
let trigger = triggerName.startsWith('*') ? triggerName : '*' + triggerName;
// Extract the relevant execution data
const execData = processExecArray(execArray);
// For nested handlers in multi items, we use match attribute for fuzzy matching
const attrs = [`match="${escapeXml(execData.description || '')}"`];
// Add handler attributes based on exec data
if (execData.route) attrs.push(`exec="${execData.route}"`);
if (execData.workflow) attrs.push(`workflow="${execData.workflow}"`);
if (execData['validate-workflow']) attrs.push(`validate-workflow="${execData['validate-workflow']}"`);
if (execData.action) attrs.push(`action="${execData.action}"`);
if (execData.data) attrs.push(`data="${execData.data}"`);
if (execData.tmpl) attrs.push(`tmpl="${execData.tmpl}"`);
// Only add type if it's not 'exec' (exec is already implied by the exec attribute)
if (execData.type && execData.type !== 'exec') attrs.push(`type="${execData.type}"`);
xml += ` <handler ${attrs.join(' ')}></handler>\n`;
}
}
return xml;
}
/**
* Process the execution array from multi format triggers
* Extracts relevant data for XML attributes
* @param {Array} execArray - Array of execution objects
* @returns {Object} Processed execution data
*/
function processExecArray(execArray) {
const result = {
description: '',
route: null,
workflow: null,
data: null,
action: null,
type: null,
};
if (!Array.isArray(execArray)) {
return result;
}
for (const exec of execArray) {
if (exec.input) {
// Use input as description if no explicit description is provided
result.description = exec.input;
}
if (exec.route) {
// Determine if it's a workflow or exec based on file extension or context
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
result.workflow = exec.route;
} else {
result.route = exec.route;
}
}
if (exec.data !== null && exec.data !== undefined) {
result.data = exec.data;
}
if (exec.action) {
result.action = exec.action;
}
if (exec.type) {
result.type = exec.type;
}
}
return result;
}
/**
* Compile agent YAML to proper XML format
* @param {Object} agentYaml - Parsed and processed agent YAML

View File

@@ -342,14 +342,15 @@ class YamlXmlBuilder {
/**
* Build menu XML section (renamed from commands for clarity)
* Auto-injects *help and *exit, adds * prefix to all triggers
* Supports both legacy format and new multi format with nested handlers
* @param {Array} menuItems - Menu items from YAML
* @param {boolean} forWebBundle - Whether building for web bundle
*/
buildCommandsXml(menuItems, forWebBundle = false) {
let xml = ' <menu>\n';
// Always inject *help first
xml += ` <item cmd="*help">Show numbered menu</item>\n`;
// Always inject menu display option first
xml += ` <item cmd="*menu">[M] Redisplay Menu Options</item>\n`;
// Add user-defined menu items with * prefix
if (menuItems && menuItems.length > 0) {
@@ -362,42 +363,140 @@ class YamlXmlBuilder {
if (!forWebBundle && item['web-only'] === true) {
continue;
}
// Build command attributes - add * prefix if not present
let trigger = item.trigger || '';
if (!trigger.startsWith('*')) {
trigger = '*' + trigger;
// Handle multi format menu items with nested handlers
if (item.multi && item.triggers && Array.isArray(item.triggers)) {
xml += ` <item type="multi">${this.escapeXml(item.multi)}\n`;
xml += this.buildNestedHandlers(item.triggers);
xml += ` </item>\n`;
}
// Handle legacy format menu items
else if (item.trigger) {
// For legacy items, keep using cmd with *<trigger> format
let trigger = item.trigger || '';
if (!trigger.startsWith('*')) {
trigger = '*' + trigger;
}
const attrs = [`cmd="${trigger}"`];
const attrs = [`cmd="${trigger}"`];
// Add handler attributes
// If workflow-install exists, use its value for workflow attribute (vendoring)
// workflow-install is build-time metadata - tells installer where to copy workflows
// The final XML should only have workflow pointing to the install location
if (item['workflow-install']) {
attrs.push(`workflow="${item['workflow-install']}"`);
} else if (item.workflow) {
attrs.push(`workflow="${item.workflow}"`);
// Add handler attributes
// If workflow-install exists, use its value for workflow attribute (vendoring)
// workflow-install is build-time metadata - tells installer where to copy workflows
// The final XML should only have workflow pointing to the install location
if (item['workflow-install']) {
attrs.push(`workflow="${item['workflow-install']}"`);
} else if (item.workflow) {
attrs.push(`workflow="${item.workflow}"`);
}
if (item['validate-workflow']) attrs.push(`validate-workflow="${item['validate-workflow']}"`);
if (item.exec) attrs.push(`exec="${item.exec}"`);
if (item.tmpl) attrs.push(`tmpl="${item.tmpl}"`);
if (item.data) attrs.push(`data="${item.data}"`);
if (item.action) attrs.push(`action="${item.action}"`);
xml += ` <item ${attrs.join(' ')}>${this.escapeXml(item.description || '')}</item>\n`;
}
if (item['validate-workflow']) attrs.push(`validate-workflow="${item['validate-workflow']}"`);
if (item.exec) attrs.push(`exec="${item.exec}"`);
if (item.tmpl) attrs.push(`tmpl="${item.tmpl}"`);
if (item.data) attrs.push(`data="${item.data}"`);
if (item.action) attrs.push(`action="${item.action}"`);
xml += ` <item ${attrs.join(' ')}>${this.escapeXml(item.description || '')}</item>\n`;
}
}
// Always inject *exit last
xml += ` <item cmd="*exit">Exit with confirmation</item>\n`;
// Always inject dismiss last
xml += ` <item cmd="*dismiss">[D] Dismiss Agent</item>\n`;
xml += ' </menu>\n';
return xml;
}
/**
* Build nested handlers for multi format menu items
* @param {Array} triggers - Triggers array from multi format
* @returns {string} Handler XML
*/
buildNestedHandlers(triggers) {
let xml = '';
for (const triggerGroup of triggers) {
for (const [triggerName, execArray] of Object.entries(triggerGroup)) {
// Build trigger with * prefix
let trigger = triggerName.startsWith('*') ? triggerName : '*' + triggerName;
// Extract the relevant execution data
const execData = this.processExecArray(execArray);
// For nested handlers in multi items, we don't need cmd attribute
// The match attribute will handle fuzzy matching
const attrs = [`match="${this.escapeXml(execData.description || '')}"`];
// Add handler attributes based on exec data
if (execData.route) attrs.push(`exec="${execData.route}"`);
if (execData.workflow) attrs.push(`workflow="${execData.workflow}"`);
if (execData['validate-workflow']) attrs.push(`validate-workflow="${execData['validate-workflow']}"`);
if (execData.action) attrs.push(`action="${execData.action}"`);
if (execData.data) attrs.push(`data="${execData.data}"`);
if (execData.tmpl) attrs.push(`tmpl="${execData.tmpl}"`);
// Only add type if it's not 'exec' (exec is already implied by the exec attribute)
if (execData.type && execData.type !== 'exec') attrs.push(`type="${execData.type}"`);
xml += ` <handler ${attrs.join(' ')}></handler>\n`;
}
}
return xml;
}
/**
* Process the execution array from multi format triggers
* Extracts relevant data for XML attributes
* @param {Array} execArray - Array of execution objects
* @returns {Object} Processed execution data
*/
processExecArray(execArray) {
const result = {
description: '',
route: null,
workflow: null,
data: null,
action: null,
type: null,
};
if (!Array.isArray(execArray)) {
return result;
}
for (const exec of execArray) {
if (exec.input) {
// Use input as description if no explicit description is provided
result.description = exec.input;
}
if (exec.route) {
// Determine if it's a workflow or exec based on file extension or context
if (exec.route.endsWith('.yaml') || exec.route.endsWith('.yml')) {
result.workflow = exec.route;
} else {
result.route = exec.route;
}
}
if (exec.data !== null && exec.data !== undefined) {
result.data = exec.data;
}
if (exec.action) {
result.action = exec.action;
}
if (exec.type) {
result.type = exec.type;
}
}
return result;
}
/**
* Escape XML special characters
*/