@ema.co/mcp-toolkit 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import { createVersionPolicyEngine } from "../sdk/version-policy.js";
9
9
  import { AGENT_CATALOG, WORKFLOW_PATTERNS, QUALIFYING_QUESTIONS, PLATFORM_CONCEPTS, WORKFLOW_EXECUTION_MODEL, COMMON_MISTAKES, DEBUG_CHECKLIST, GUIDANCE_TOPICS, VOICE_PERSONA_TEMPLATE, getAgentByName, getWidgetsForPersonaType, checkTypeCompatibility, getQualifyingQuestionsByCategory, getRequiredQualifyingQuestions, getConceptByTerm, suggestAgentsForUseCase, validateWorkflowPrompt, detectWorkflowIssues, validateWorkflowConnections, suggestWorkflowFixes, } from "../sdk/knowledge.js";
10
10
  import { compileWorkflow } from "../sdk/workflow-generator.js";
11
11
  import { parseInput, intentToSpec } from "../sdk/workflow-intent.js";
12
+ import { analyzeExecutionFlow, generateASCIIFlow } from "../sdk/workflow-execution-analyzer.js";
12
13
  // ─────────────────────────────────────────────────────────────────────────────
13
14
  // Widget Validation Helpers
14
15
  // ─────────────────────────────────────────────────────────────────────────────
@@ -541,46 +542,205 @@ export async function handlePersona(args, client, getTemplateId, createClientFor
541
542
  }
542
543
  }
543
544
  // ═══════════════════════════════════════════════════════════════════════════
544
- // WORKFLOW Handler
545
+ // WORKFLOW Handler - Unified greenfield/brownfield operations
545
546
  // ═══════════════════════════════════════════════════════════════════════════
546
547
  export async function handleWorkflow(args, client) {
547
- const mode = args.mode;
548
548
  const personaId = args.persona_id;
549
549
  const workflowDef = args.workflow_def;
550
550
  const input = args.input;
551
+ const optimize = args.optimize;
552
+ const compareTo = args.compare_to;
551
553
  // Default to preview=true for safety (no accidental deploys)
552
554
  const preview = args.preview !== false;
553
- // Determine effective mode
554
- // Key routing logic:
555
- // - persona_id + input → extend (brownfield modification)
556
- // - input only generate (greenfield)
557
- // - persona_id only → analyze (inspect existing)
558
- // - workflow_def only → analyze
559
- let effectiveMode = mode;
560
- if (!effectiveMode) {
561
- if (args.include)
562
- effectiveMode = "analyze";
563
- else if (workflowDef && !personaId)
564
- effectiveMode = "analyze";
565
- else if (personaId && input)
566
- effectiveMode = "extend"; // BROWNFIELD: existing persona + new requirements
567
- else if (personaId && !input)
568
- effectiveMode = "analyze";
569
- else if (input)
570
- effectiveMode = "generate"; // GREENFIELD: new requirements only
571
- else
572
- effectiveMode = "analyze";
555
+ // Smart mode detection - user doesn't need to specify
556
+ // Priority: extraction_schema > compare > optimize > brownfield > greenfield > analyze
557
+ let effectiveMode;
558
+ if (args.mode === "extraction_schema" || args.metadata_file || args.metadata_json) {
559
+ effectiveMode = "extraction_schema";
573
560
  }
574
- // Legacy compatibility: mode="deploy" is now preview=false on extend/optimize
575
- if (effectiveMode === "deploy") {
576
- if (input && personaId) {
577
- effectiveMode = "extend";
578
- }
579
- else if (personaId) {
580
- effectiveMode = "optimize";
581
- }
561
+ else if (args.mode === "compile") {
562
+ // Explicit compile mode (for advanced users)
563
+ effectiveMode = "compile";
564
+ }
565
+ else if (compareTo) {
566
+ // Compare two workflows
567
+ effectiveMode = "compare";
568
+ }
569
+ else if (optimize && personaId) {
570
+ // Auto-fix issues
571
+ effectiveMode = "optimize";
572
+ }
573
+ else if (personaId && input) {
574
+ // BROWNFIELD: Modify existing workflow
575
+ effectiveMode = "modify"; // New unified mode for all brownfield changes
582
576
  }
577
+ else if (input && !personaId) {
578
+ // GREENFIELD: Create new workflow
579
+ effectiveMode = "generate";
580
+ }
581
+ else if (personaId || workflowDef || args.include) {
582
+ // Just analyzing
583
+ effectiveMode = "analyze";
584
+ }
585
+ else {
586
+ effectiveMode = "analyze";
587
+ }
588
+ // Legacy mode support (backwards compatibility)
589
+ const legacyMode = args.mode;
590
+ if (legacyMode === "extend")
591
+ effectiveMode = "modify";
592
+ if (legacyMode === "deploy" && personaId && input)
593
+ effectiveMode = "modify";
583
594
  switch (effectiveMode) {
595
+ case "extraction_schema": {
596
+ // Generate extraction schema from JSON metadata (fully dynamic, no hardcoding)
597
+ const metadataFile = args.metadata_file;
598
+ const metadataJson = args.metadata_json;
599
+ if (!metadataFile && !metadataJson) {
600
+ return { error: "metadata_file or metadata_json required for extraction_schema mode" };
601
+ }
602
+ let metadata;
603
+ if (metadataJson) {
604
+ metadata = metadataJson;
605
+ }
606
+ else {
607
+ // Read from file
608
+ const fs = await import("fs/promises");
609
+ const content = await fs.readFile(metadataFile, "utf8");
610
+ metadata = JSON.parse(content);
611
+ }
612
+ // Dynamically generate extraction schema (no hardcoding)
613
+ const extractionFields = [];
614
+ const jsonMapperFields = {};
615
+ // Parse entities section dynamically
616
+ const entities = metadata.entities;
617
+ if (entities) {
618
+ // Clients
619
+ if (entities.clients) {
620
+ const clientIds = Object.keys(entities.clients);
621
+ const firstClient = Object.values(entities.clients)[0];
622
+ extractionFields.push({
623
+ name: "client_id",
624
+ type: "string",
625
+ description: "Client identifier",
626
+ examples: clientIds.slice(0, 5),
627
+ required: false,
628
+ });
629
+ jsonMapperFields["client_id"] = "client_id";
630
+ extractionFields.push({
631
+ name: "client_name",
632
+ type: "string",
633
+ description: "Client full name",
634
+ examples: firstClient ? [String(firstClient.name || "")].filter(Boolean) : [],
635
+ required: false,
636
+ });
637
+ jsonMapperFields["client_name"] = "client_name";
638
+ // Dynamically extract all other client fields
639
+ if (firstClient) {
640
+ Object.keys(firstClient).forEach(key => {
641
+ if (key !== "name" && key !== "file") {
642
+ const value = firstClient[key];
643
+ const fieldType = typeof value === "boolean" ? "boolean" :
644
+ typeof value === "number" ? "number" : "string";
645
+ extractionFields.push({
646
+ name: `client_${key}`,
647
+ type: fieldType,
648
+ description: `Client ${key} (from metadata)`,
649
+ required: false,
650
+ });
651
+ jsonMapperFields[`client_${key}`] = `client_${key}`;
652
+ }
653
+ });
654
+ }
655
+ }
656
+ // Advisors
657
+ if (entities.advisors) {
658
+ const advisorIds = Object.keys(entities.advisors);
659
+ extractionFields.push({
660
+ name: "advisor_id",
661
+ type: "string",
662
+ description: "Advisor identifier",
663
+ examples: advisorIds.slice(0, 5),
664
+ required: false,
665
+ });
666
+ jsonMapperFields["advisor_id"] = "advisor_id";
667
+ const advisorEmails = Object.values(entities.advisors)
668
+ .map((a) => a.email)
669
+ .filter(Boolean)
670
+ .slice(0, 5);
671
+ if (advisorEmails.length > 0) {
672
+ extractionFields.push({
673
+ name: "advisor_email",
674
+ type: "string",
675
+ description: "Advisor email address",
676
+ examples: advisorEmails,
677
+ required: false,
678
+ });
679
+ jsonMapperFields["advisor_email"] = "advisor_email";
680
+ }
681
+ }
682
+ // Stocks/Tickers
683
+ if (entities.stocks) {
684
+ const tickers = Object.keys(entities.stocks);
685
+ extractionFields.push({
686
+ name: "ticker_symbols",
687
+ type: "array",
688
+ description: "Stock ticker symbols",
689
+ examples: tickers.slice(0, 10),
690
+ required: false,
691
+ items: { type: "string" },
692
+ });
693
+ jsonMapperFields["ticker_symbols"] = "ticker_symbols";
694
+ }
695
+ }
696
+ // Parse intents section
697
+ const intents = metadata.intents;
698
+ if (intents) {
699
+ const intentNames = Object.keys(intents);
700
+ extractionFields.push({
701
+ name: "intent",
702
+ type: "string",
703
+ description: "Detected intent from conversation",
704
+ examples: intentNames,
705
+ required: false,
706
+ });
707
+ jsonMapperFields["intent"] = "intent";
708
+ }
709
+ // Format extraction schema
710
+ const extractionSchema = {};
711
+ extractionFields.forEach(field => {
712
+ if (field.type === "array") {
713
+ extractionSchema[field.name] = {
714
+ type: "array",
715
+ items: field.items || { type: "string" },
716
+ description: field.description,
717
+ examples: field.examples,
718
+ };
719
+ }
720
+ else {
721
+ extractionSchema[field.name] = {
722
+ type: field.type,
723
+ description: field.description,
724
+ examples: field.examples,
725
+ required: field.required || false,
726
+ };
727
+ }
728
+ });
729
+ // Generate fixed response template
730
+ const fixedResponseVars = Object.keys(jsonMapperFields);
731
+ const fixedResponseTemplate = fixedResponseVars.length > 0
732
+ ? `Metadata Context:\n${fixedResponseVars.map(v => `- ${v}: {{${v}}}`).join("\n")}`
733
+ : "No metadata variables available";
734
+ return {
735
+ mode: "extraction_schema",
736
+ metadata_source: metadataFile || "inline_json",
737
+ extraction_columns: extractionSchema,
738
+ json_mapper_fields: jsonMapperFields,
739
+ fixed_response_template: fixedResponseTemplate,
740
+ fields_generated: extractionFields.length,
741
+ note: "This schema is dynamically generated from your metadata - no hardcoding. Use extraction_columns to configure entity_extraction_with_documents node.",
742
+ };
743
+ }
584
744
  case "generate": {
585
745
  if (!input) {
586
746
  return { error: "input required for generate mode" };
@@ -635,13 +795,15 @@ export async function handleWorkflow(args, client) {
635
795
  }
636
796
  return result;
637
797
  }
798
+ case "modify":
638
799
  case "extend": {
639
- // BROWNFIELD: Extend existing workflow with new capabilities
800
+ // BROWNFIELD: Intelligently modify existing workflow
801
+ // Handles: add, remove, replace, consolidate, rewire - all in one
640
802
  if (!personaId) {
641
- return { error: "persona_id required for extend mode" };
803
+ return { error: "persona_id required for workflow modifications" };
642
804
  }
643
805
  if (!input) {
644
- return { error: "input required for extend mode. Describe what to add/modify." };
806
+ return { error: "input required. Describe what you want to change." };
645
807
  }
646
808
  const persona = await client.getPersonaById(personaId);
647
809
  if (!persona) {
@@ -650,35 +812,47 @@ export async function handleWorkflow(args, client) {
650
812
  const existingWorkflow = persona.workflow_def;
651
813
  if (!existingWorkflow) {
652
814
  return {
653
- error: `Persona "${persona.name}" has no workflow to extend`,
654
- hint: "Use mode='generate' to create a new workflow for this persona",
815
+ error: `Persona "${persona.name}" has no workflow to modify`,
816
+ hint: "Provide just input= to create a new workflow (greenfield)",
655
817
  };
656
818
  }
657
- // Parse the natural language input to detect change types
658
- const detectedChanges = parseExtensionInput(input);
659
- // Merge changes into existing workflow
660
- const mergeResult = mergeWorkflowChanges(existingWorkflow, detectedChanges);
661
- // Validate the merged workflow
662
- const issues = detectWorkflowIssues(mergeResult.workflow);
819
+ // Intelligently analyze what the user wants to do
820
+ const changeAnalysis = analyzeModificationRequest(input, existingWorkflow);
821
+ // Apply the changes to the workflow
822
+ const modifyResult = applyWorkflowModifications(existingWorkflow, changeAnalysis);
823
+ // Validate the result
824
+ const issues = detectWorkflowIssues(modifyResult.workflow);
663
825
  const fixes = issues.length > 0 ? suggestWorkflowFixes(issues) : [];
664
826
  const result = {
665
- mode: "extend",
666
- status: preview ? "preview" : "deploying", // Will be updated after actual deploy
827
+ mode: "modify",
828
+ status: preview ? "preview" : "deploying",
667
829
  persona_id: personaId,
668
830
  persona_name: persona.name,
669
- detected_changes: detectedChanges,
670
- changes_applied: mergeResult.changesApplied,
671
- merged_workflow: mergeResult.workflow,
672
- new_nodes_identified: mergeResult.newNodes,
831
+ // What we understood from the request
832
+ analysis: {
833
+ understood_intent: changeAnalysis.intent,
834
+ operations: changeAnalysis.operations.map(op => ({
835
+ type: op.type,
836
+ description: op.description,
837
+ })),
838
+ },
839
+ // What we did
840
+ changes_applied: modifyResult.changesApplied,
841
+ nodes_added: modifyResult.nodesAdded,
842
+ nodes_removed: modifyResult.nodesRemoved,
843
+ nodes_modified: modifyResult.nodesModified,
844
+ connections_changed: modifyResult.connectionsChanged,
845
+ // The result
846
+ modified_workflow: modifyResult.workflow,
673
847
  };
674
848
  if (issues.length > 0) {
675
- result.issues = issues;
676
- result.suggested_fixes = fixes;
849
+ result.remaining_issues = issues.length;
850
+ result.issues = issues.slice(0, 5); // Show first 5
851
+ result.suggested_fixes = fixes.slice(0, 5);
677
852
  }
678
- // If preview=false, deploy the merged workflow
853
+ // Deploy if preview=false
679
854
  if (!preview) {
680
- // Sanitize workflow before deployment (same as deploy_workflow)
681
- const sanitizedWorkflow = sanitizeWorkflowForDeploy(mergeResult.workflow);
855
+ const sanitizedWorkflow = sanitizeWorkflowForDeploy(modifyResult.workflow);
682
856
  try {
683
857
  await client.updateAiEmployee({
684
858
  persona_id: personaId,
@@ -687,42 +861,21 @@ export async function handleWorkflow(args, client) {
687
861
  });
688
862
  result.status = "deployed";
689
863
  result.deployed = true;
690
- result.merged_workflow = sanitizedWorkflow;
864
+ result.modified_workflow = sanitizedWorkflow;
691
865
  }
692
866
  catch (err) {
693
867
  const errorMessage = err instanceof Error ? err.message : String(err);
694
- // Check if this is a validation error or server error
695
- const isServerError = errorMessage.includes("Internal Server Error") || errorMessage.includes("500");
696
868
  result.deployed = false;
697
869
  result.deploy_error = errorMessage;
698
- if (isServerError) {
699
- // Server error - likely needs Autobuilder
700
- result.status = "preview_only";
701
- result.hint = "Direct API deployment failed (common for complex changes). The merged workflow is correct - deploy it using deploy_workflow which can use Autobuilder as fallback.";
702
- result.deploy_command = {
703
- tool: "deploy_workflow",
704
- args: {
705
- persona_id: personaId,
706
- workflow_def: "Use merged_workflow from this response",
707
- validate_first: true,
708
- auto_fix: true,
709
- },
710
- };
711
- }
712
- else {
713
- // Other error - provide the message
714
- result.hint = `Deployment failed: ${errorMessage}. Review the merged_workflow for issues.`;
715
- }
870
+ result.status = "preview_only";
871
+ result.hint = `Deployment failed: ${errorMessage}. The modified_workflow is ready - you may need to deploy via UI for complex structural changes.`;
716
872
  }
717
873
  }
718
874
  else {
719
875
  result.next_steps = [
720
- "Review detected_changes and merged_workflow",
721
- issues.length > 0 ? "Address issues before deploying" : null,
722
- mergeResult.newNodes.length > 0
723
- ? `Use action(suggest='...') to get agent recommendations for: ${mergeResult.newNodes.map(n => n.name).join(', ')}`
724
- : null,
725
- `Deploy with: workflow(mode="extend", persona_id="${personaId}", input="...", preview=false)`,
876
+ "Review the changes_applied and modified_workflow",
877
+ issues.length > 0 ? `Address ${issues.length} remaining issues` : null,
878
+ `Deploy with: workflow(persona_id="${personaId}", input="...", preview=false)`,
726
879
  ].filter(Boolean);
727
880
  }
728
881
  return result;
@@ -740,9 +893,14 @@ export async function handleWorkflow(args, client) {
740
893
  if (!workflow) {
741
894
  return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
742
895
  }
743
- // Determine what to include
896
+ // Determine what to include (now supports execution_flow!)
744
897
  const include = args.include || ["issues", "connections", "fixes", "metrics"];
745
- const result = {};
898
+ const result = {
899
+ mode: "analyze",
900
+ persona_id: personaId,
901
+ persona_name: persona?.name,
902
+ environment: "demo",
903
+ };
746
904
  if (include.includes("issues") || include.includes("fixes")) {
747
905
  const issues = detectWorkflowIssues(workflow);
748
906
  if (include.includes("issues")) {
@@ -751,13 +909,13 @@ export async function handleWorkflow(args, client) {
751
909
  if (include.includes("fixes")) {
752
910
  result.fixes = suggestWorkflowFixes(issues);
753
911
  }
754
- // Always include summary even if only fixes requested
755
912
  result.issue_summary = {
756
913
  total: issues.length,
757
914
  critical: issues.filter((i) => i.severity === "critical").length,
758
- warnings: issues.filter((i) => i.severity === "warning").length,
915
+ warning: issues.filter((i) => i.severity === "warning").length,
759
916
  info: issues.filter((i) => i.severity === "info").length,
760
917
  };
918
+ result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
761
919
  }
762
920
  if (include.includes("connections")) {
763
921
  result.connections = validateWorkflowConnections(workflow);
@@ -765,6 +923,26 @@ export async function handleWorkflow(args, client) {
765
923
  if (include.includes("metrics")) {
766
924
  result.metrics = calculateMetrics(workflow);
767
925
  }
926
+ // NEW: Execution flow analysis - detects loops, multiple responders, redundant classifiers
927
+ if (include.includes("execution_flow")) {
928
+ const execAnalysis = analyzeExecutionFlow(workflow);
929
+ result.execution_flow = {
930
+ summary: execAnalysis.summary,
931
+ loops: execAnalysis.loops,
932
+ multiple_responder_issues: execAnalysis.multipleResponderIssues,
933
+ redundant_classifiers: execAnalysis.redundantClassifiers,
934
+ data_flow_issues: execAnalysis.dataFlowIssues,
935
+ dead_code_paths: execAnalysis.deadCodePaths,
936
+ };
937
+ // Include ASCII visualization if requested
938
+ if (args.visualize) {
939
+ result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
940
+ }
941
+ // Add specific warnings for triple response risk
942
+ if (execAnalysis.summary.mayRepeatResponses) {
943
+ result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
944
+ }
945
+ }
768
946
  if (persona) {
769
947
  result.persona = { id: persona.id, name: persona.name };
770
948
  }
@@ -774,8 +952,7 @@ export async function handleWorkflow(args, client) {
774
952
  const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
775
953
  if (criticalCount > 0 || warningCount > 0) {
776
954
  result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
777
- `Use workflow(mode="optimize", persona_id="${personaId ?? 'ID'}") to auto-fix, ` +
778
- `or workflow(mode="deploy", persona_id="${personaId ?? 'ID'}", auto_fix=true) to fix and deploy.`;
955
+ `Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
779
956
  }
780
957
  else {
781
958
  result.status = "✅ Workflow is healthy - no issues detected";
@@ -1134,10 +1311,13 @@ export async function handleKnowledge(args, client, readFile) {
1134
1311
  if (!persona) {
1135
1312
  return { error: `Persona not found: ${personaId}` };
1136
1313
  }
1314
+ // IMPORTANT: The Ema API requires workflow to be sent along with proto_config
1315
+ // for proto_config changes to persist. This matches what the UI does.
1137
1316
  await client.updateAiEmployee({
1138
1317
  persona_id: personaId,
1139
1318
  embedding_enabled: args.enabled,
1140
1319
  proto_config: persona.proto_config,
1320
+ workflow: persona.workflow_def,
1141
1321
  });
1142
1322
  return {
1143
1323
  success: true,
@@ -1372,6 +1552,388 @@ export async function handleSync(args, createClient, getSyncOptions) {
1372
1552
  return { error: `Unknown mode: ${mode}` };
1373
1553
  }
1374
1554
  }
1555
+ /**
1556
+ * Analyze a natural language modification request and determine what operations are needed.
1557
+ * This is the "smart" part - understanding user intent.
1558
+ */
1559
+ function analyzeModificationRequest(input, existingWorkflow) {
1560
+ const lowerInput = input.toLowerCase();
1561
+ const operations = [];
1562
+ let intent = "modify workflow";
1563
+ const actions = (existingWorkflow.actions || []);
1564
+ // ─────────────────────────────────────────────────────────────────────────
1565
+ // CONSOLIDATION: "consolidate X agents into one", "merge", "combine"
1566
+ // ─────────────────────────────────────────────────────────────────────────
1567
+ if (lowerInput.includes("consolidat") || lowerInput.includes("merge") ||
1568
+ lowerInput.includes("combine") || lowerInput.includes("into one") ||
1569
+ lowerInput.includes("single agent") || lowerInput.includes("unified")) {
1570
+ intent = "consolidate multiple nodes into one";
1571
+ // Find nodes to consolidate - look for patterns like "8 custom agents", "the agents"
1572
+ const customAgents = actions.filter(a => {
1573
+ const actionType = getActionType(a);
1574
+ return actionType === "custom_agent" || actionType.includes("custom");
1575
+ });
1576
+ if (customAgents.length > 1) {
1577
+ operations.push({
1578
+ type: "consolidate",
1579
+ description: `Consolidate ${customAgents.length} custom_agent nodes into a single unified agent`,
1580
+ target_nodes: customAgents.map(a => String(a.name || "")),
1581
+ });
1582
+ }
1583
+ }
1584
+ // ─────────────────────────────────────────────────────────────────────────
1585
+ // REMOVAL: "remove", "delete", "clean up orphan"
1586
+ // ─────────────────────────────────────────────────────────────────────────
1587
+ if (lowerInput.includes("remove") || lowerInput.includes("delete") ||
1588
+ lowerInput.includes("clean up") || lowerInput.includes("orphan")) {
1589
+ intent = "remove nodes";
1590
+ // Check if removing orphans specifically
1591
+ if (lowerInput.includes("orphan")) {
1592
+ // Find orphan nodes by checking reachability
1593
+ const orphanNodes = findOrphanNodes(existingWorkflow);
1594
+ if (orphanNodes.length > 0) {
1595
+ operations.push({
1596
+ type: "remove",
1597
+ description: `Remove ${orphanNodes.length} orphan nodes`,
1598
+ target_nodes: orphanNodes,
1599
+ });
1600
+ }
1601
+ }
1602
+ // Check for specific node names mentioned
1603
+ const nodeNameMatch = input.match(/remove\s+(?:the\s+)?(\w+)/i);
1604
+ if (nodeNameMatch) {
1605
+ const nodeName = nodeNameMatch[1];
1606
+ const matchingNodes = actions.filter(a => String(a.name || "").toLowerCase().includes(nodeName.toLowerCase()));
1607
+ if (matchingNodes.length > 0) {
1608
+ operations.push({
1609
+ type: "remove",
1610
+ description: `Remove node(s) matching "${nodeName}"`,
1611
+ target_nodes: matchingNodes.map(a => String(a.name || "")),
1612
+ });
1613
+ }
1614
+ }
1615
+ }
1616
+ // ─────────────────────────────────────────────────────────────────────────
1617
+ // REPLACEMENT: "replace X with Y", "swap", "use X instead of Y"
1618
+ // ─────────────────────────────────────────────────────────────────────────
1619
+ if (lowerInput.includes("replace") || lowerInput.includes("swap") ||
1620
+ lowerInput.includes("instead of") || lowerInput.includes("use entity_extraction")) {
1621
+ intent = "replace nodes";
1622
+ // Parse "replace X with Y" pattern
1623
+ const replaceMatch = input.match(/replace\s+(?:the\s+)?(.+?)\s+with\s+(.+)/i);
1624
+ if (replaceMatch) {
1625
+ operations.push({
1626
+ type: "replace",
1627
+ description: `Replace ${replaceMatch[1]} with ${replaceMatch[2]}`,
1628
+ source: replaceMatch[1],
1629
+ target: replaceMatch[2],
1630
+ });
1631
+ }
1632
+ // Special case: "use entity_extraction" suggests replacing LLM-based extraction
1633
+ if (lowerInput.includes("entity_extraction")) {
1634
+ const llmNodes = actions.filter(a => {
1635
+ const actionType = getActionType(a);
1636
+ const displayName = a.displaySettings?.displayName || "";
1637
+ return (actionType === "call_llm" &&
1638
+ (displayName.toLowerCase().includes("email") ||
1639
+ displayName.toLowerCase().includes("extract")));
1640
+ });
1641
+ if (llmNodes.length > 0) {
1642
+ operations.push({
1643
+ type: "replace",
1644
+ description: `Replace ${llmNodes.length} LLM extraction nodes with entity_extraction`,
1645
+ target_nodes: llmNodes.map(a => String(a.name || "")),
1646
+ });
1647
+ }
1648
+ }
1649
+ }
1650
+ // ─────────────────────────────────────────────────────────────────────────
1651
+ // HITL: "add hitl", "add approval", "confirm before"
1652
+ // ─────────────────────────────────────────────────────────────────────────
1653
+ if (lowerInput.includes("hitl") || lowerInput.includes("approval") ||
1654
+ lowerInput.includes("confirm") || lowerInput.includes("human")) {
1655
+ intent = "add human-in-the-loop";
1656
+ // Find the action to add HITL before
1657
+ const beforeMatch = input.match(/before\s+(\w+)/i);
1658
+ const targetAction = beforeMatch ? beforeMatch[1] : "email";
1659
+ // Find email/send nodes
1660
+ const emailNodes = actions.filter(a => {
1661
+ const actionType = getActionType(a);
1662
+ const nodeName = String(a.name || "").toLowerCase();
1663
+ return actionType.includes("email") || nodeName.includes("email") ||
1664
+ nodeName.includes("send") || nodeName.includes(targetAction.toLowerCase());
1665
+ });
1666
+ operations.push({
1667
+ type: "add",
1668
+ description: `Add HITL approval before ${emailNodes.length > 0 ? emailNodes[0].name : targetAction}`,
1669
+ new_node: {
1670
+ type: "general_hitl",
1671
+ name: `hitl_approval_${Date.now()}`,
1672
+ before: emailNodes.length > 0 ? String(emailNodes[0].name) : targetAction,
1673
+ },
1674
+ });
1675
+ }
1676
+ // ─────────────────────────────────────────────────────────────────────────
1677
+ // WIRING: "connect", "wire", "link X to Y"
1678
+ // ─────────────────────────────────────────────────────────────────────────
1679
+ if (lowerInput.includes("connect") || lowerInput.includes("wire") ||
1680
+ lowerInput.includes("link")) {
1681
+ intent = "rewire connections";
1682
+ // Parse "connect X to Y" pattern
1683
+ const connectMatch = input.match(/(?:connect|wire|link)\s+(\w+)\s+to\s+(\w+)/i);
1684
+ if (connectMatch) {
1685
+ operations.push({
1686
+ type: "rewire",
1687
+ description: `Connect ${connectMatch[1]} to ${connectMatch[2]}`,
1688
+ source: connectMatch[1],
1689
+ target: connectMatch[2],
1690
+ });
1691
+ }
1692
+ }
1693
+ // ─────────────────────────────────────────────────────────────────────────
1694
+ // ADD: "add", "include", "new"
1695
+ // ─────────────────────────────────────────────────────────────────────────
1696
+ if (operations.length === 0 && (lowerInput.includes("add") || lowerInput.includes("new") || lowerInput.includes("include"))) {
1697
+ intent = "add new capabilities";
1698
+ // Use existing parseExtensionInput for additive changes
1699
+ const addChanges = parseExtensionInput(input);
1700
+ for (const change of addChanges) {
1701
+ operations.push({
1702
+ type: "add",
1703
+ description: change.description,
1704
+ config: change.details,
1705
+ });
1706
+ }
1707
+ }
1708
+ // Determine confidence based on how specific the operations are
1709
+ const confidence = operations.length > 0 && operations.every(op => op.target_nodes?.length || op.new_node) ? "high" :
1710
+ operations.length > 0 ? "medium" : "low";
1711
+ return { intent, operations, confidence };
1712
+ }
1713
+ /**
1714
+ * Find orphan nodes (not reachable from trigger).
1715
+ */
1716
+ function findOrphanNodes(workflow) {
1717
+ const actions = (workflow.actions || []);
1718
+ const orphans = [];
1719
+ // Build a set of all referenced nodes (nodes that receive input from other nodes)
1720
+ const referencedNodes = new Set();
1721
+ referencedNodes.add("trigger"); // Trigger is always reachable
1722
+ // Find all nodes that are referenced in actionOutput bindings
1723
+ for (const action of actions) {
1724
+ const inputs = (action.inputs || {});
1725
+ for (const inputValue of Object.values(inputs)) {
1726
+ const binding = inputValue;
1727
+ if (binding?.actionOutput) {
1728
+ const actionOutput = binding.actionOutput;
1729
+ referencedNodes.add(String(actionOutput.actionName || ""));
1730
+ }
1731
+ // Check multiBinding
1732
+ if (binding?.multiBinding) {
1733
+ const multi = binding.multiBinding;
1734
+ const elements = (multi.elements || []);
1735
+ for (const elem of elements) {
1736
+ if (elem.namedBinding) {
1737
+ const named = elem.namedBinding;
1738
+ const value = named.value;
1739
+ if (value?.actionOutput) {
1740
+ const ao = value.actionOutput;
1741
+ referencedNodes.add(String(ao.actionName || ""));
1742
+ }
1743
+ }
1744
+ }
1745
+ }
1746
+ }
1747
+ }
1748
+ // Find nodes that don't receive input from trigger chain
1749
+ // This is a simplified check - proper reachability would need graph traversal
1750
+ for (const action of actions) {
1751
+ const nodeName = String(action.name || "");
1752
+ const hasInputFromTrigger = checkNodeHasPath(nodeName, actions, "trigger", new Set());
1753
+ if (!hasInputFromTrigger && nodeName !== "trigger") {
1754
+ orphans.push(nodeName);
1755
+ }
1756
+ }
1757
+ return orphans;
1758
+ }
1759
+ /**
1760
+ * Check if a node has a path from source (simple BFS).
1761
+ */
1762
+ function checkNodeHasPath(nodeName, actions, source, visited) {
1763
+ if (visited.has(nodeName))
1764
+ return false;
1765
+ visited.add(nodeName);
1766
+ const node = actions.find(a => a.name === nodeName);
1767
+ if (!node)
1768
+ return false;
1769
+ const inputs = (node.inputs || {});
1770
+ for (const inputValue of Object.values(inputs)) {
1771
+ const binding = inputValue;
1772
+ if (binding?.actionOutput) {
1773
+ const actionOutput = binding.actionOutput;
1774
+ const sourceNode = String(actionOutput.actionName || "");
1775
+ if (sourceNode === source)
1776
+ return true;
1777
+ if (checkNodeHasPath(sourceNode, actions, source, visited))
1778
+ return true;
1779
+ }
1780
+ }
1781
+ return false;
1782
+ }
1783
+ /**
1784
+ * Apply modifications to the workflow based on analyzed operations.
1785
+ */
1786
+ function applyWorkflowModifications(existingWorkflow, analysis) {
1787
+ const workflow = JSON.parse(JSON.stringify(existingWorkflow));
1788
+ const actions = (workflow.actions || []);
1789
+ const enumTypes = (workflow.enumTypes || []);
1790
+ const results = (workflow.results || {});
1791
+ const changesApplied = [];
1792
+ const nodesAdded = [];
1793
+ const nodesRemoved = [];
1794
+ const nodesModified = [];
1795
+ const connectionsChanged = [];
1796
+ for (const op of analysis.operations) {
1797
+ switch (op.type) {
1798
+ case "consolidate": {
1799
+ // Consolidate multiple nodes into one unified agent
1800
+ const targetNodes = op.target_nodes || [];
1801
+ if (targetNodes.length < 2) {
1802
+ changesApplied.push("Consolidation skipped: need 2+ nodes to consolidate");
1803
+ break;
1804
+ }
1805
+ // Get the first node as template
1806
+ const templateIdx = actions.findIndex(a => a.name === targetNodes[0]);
1807
+ if (templateIdx < 0)
1808
+ break;
1809
+ const template = actions[templateIdx];
1810
+ // Create unified agent node
1811
+ const unifiedNode = {
1812
+ name: "unified_analysis_agent",
1813
+ action: template.action,
1814
+ inputs: {
1815
+ ...template.inputs,
1816
+ // Update instructions to be dynamic
1817
+ role_instructions: {
1818
+ inline: {
1819
+ wellKnown: {
1820
+ stringValue: "You are a versatile analysis agent. Read your agent definition from the knowledge base based on the current intent type and adopt that role.",
1821
+ },
1822
+ },
1823
+ autoDetectedBinding: false,
1824
+ },
1825
+ task_instructions: {
1826
+ inline: {
1827
+ wellKnown: {
1828
+ stringValue: "Execute the task based on the intent type. The knowledge base contains definitions for each intent with INPUT DATA, ANALYSIS FRAMEWORK, and OUTPUT SCHEMA. Follow those precisely.",
1829
+ },
1830
+ },
1831
+ autoDetectedBinding: false,
1832
+ },
1833
+ },
1834
+ displaySettings: {
1835
+ displayName: "Unified Analysis Agent",
1836
+ description: `Consolidated from ${targetNodes.length} separate agents`,
1837
+ coordinates: template.displaySettings?.coordinates || { x: 2000, y: 1500 },
1838
+ },
1839
+ typeArguments: template.typeArguments || {},
1840
+ tools: template.tools || [],
1841
+ disableHumanInteraction: false,
1842
+ // No runIf - runs for all intents, uses internal routing
1843
+ };
1844
+ // Remove all target nodes
1845
+ for (const nodeName of targetNodes) {
1846
+ const idx = actions.findIndex(a => a.name === nodeName);
1847
+ if (idx >= 0) {
1848
+ actions.splice(idx, 1);
1849
+ nodesRemoved.push(nodeName);
1850
+ // Also remove from results mapping
1851
+ const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
1852
+ if (resultKey) {
1853
+ delete results[resultKey];
1854
+ }
1855
+ }
1856
+ }
1857
+ // Add unified node
1858
+ actions.push(unifiedNode);
1859
+ nodesAdded.push("unified_analysis_agent");
1860
+ // Add to results mapping
1861
+ results["unified_analysis_agent.response_with_sources"] = {
1862
+ actionName: "unified_analysis_agent",
1863
+ outputName: "response_with_sources",
1864
+ };
1865
+ changesApplied.push(`Consolidated ${targetNodes.length} nodes into unified_analysis_agent`);
1866
+ break;
1867
+ }
1868
+ case "remove": {
1869
+ const targetNodes = op.target_nodes || [];
1870
+ for (const nodeName of targetNodes) {
1871
+ const idx = actions.findIndex(a => a.name === nodeName);
1872
+ if (idx >= 0) {
1873
+ actions.splice(idx, 1);
1874
+ nodesRemoved.push(nodeName);
1875
+ // Also remove from results
1876
+ const resultKey = Object.keys(results).find(k => k.startsWith(nodeName + "."));
1877
+ if (resultKey) {
1878
+ delete results[resultKey];
1879
+ }
1880
+ changesApplied.push(`Removed node: ${nodeName}`);
1881
+ }
1882
+ }
1883
+ break;
1884
+ }
1885
+ case "replace": {
1886
+ // For now, just note it - full replacement is complex
1887
+ changesApplied.push(`Replace operation noted: ${op.description}. Manual verification needed.`);
1888
+ break;
1889
+ }
1890
+ case "rewire": {
1891
+ const source = op.source;
1892
+ const target = op.target;
1893
+ if (source && target) {
1894
+ // Find target node and update its inputs
1895
+ const targetNode = actions.find(a => String(a.name || "").toLowerCase().includes(target.toLowerCase()));
1896
+ if (targetNode) {
1897
+ nodesModified.push(String(targetNode.name));
1898
+ connectionsChanged.push(`${source} → ${targetNode.name}`);
1899
+ changesApplied.push(`Rewiring: ${source} → ${targetNode.name}`);
1900
+ }
1901
+ }
1902
+ break;
1903
+ }
1904
+ case "add": {
1905
+ if (op.new_node) {
1906
+ const newNode = op.new_node;
1907
+ const nodeName = String(newNode.name || `new_node_${Date.now()}`);
1908
+ nodesAdded.push(nodeName);
1909
+ changesApplied.push(`Added node: ${nodeName}`);
1910
+ }
1911
+ else if (op.config) {
1912
+ // Use legacy merge for additive changes
1913
+ const legacyChanges = parseExtensionInput(op.description);
1914
+ const legacyResult = mergeWorkflowChanges(workflow, legacyChanges);
1915
+ changesApplied.push(...legacyResult.changesApplied);
1916
+ }
1917
+ break;
1918
+ }
1919
+ case "modify_config": {
1920
+ changesApplied.push(`Config modification noted: ${op.description}`);
1921
+ break;
1922
+ }
1923
+ }
1924
+ }
1925
+ workflow.actions = actions;
1926
+ workflow.enumTypes = enumTypes;
1927
+ workflow.results = results;
1928
+ return {
1929
+ workflow,
1930
+ changesApplied,
1931
+ nodesAdded,
1932
+ nodesRemoved,
1933
+ nodesModified,
1934
+ connectionsChanged,
1935
+ };
1936
+ }
1375
1937
  /**
1376
1938
  * Parse natural language input to detect what changes are being requested.
1377
1939
  * Handles multiple changes in one input (e.g., "add X, add Y, add Z").