@ema.co/mcp-toolkit 2026.1.26 → 2026.1.27-1
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.
Potentially problematic release.
This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.
- package/dist/mcp/handlers/action/index.js +17 -20
- package/dist/mcp/handlers/data/index.js +72 -6
- package/dist/mcp/handlers/deprecation.js +50 -0
- package/dist/mcp/handlers/env/index.js +3 -3
- package/dist/mcp/handlers/knowledge/index.js +44 -237
- package/dist/mcp/handlers/persona/create.js +47 -18
- package/dist/mcp/handlers/persona/index.js +9 -10
- package/dist/mcp/handlers/persona/update.js +4 -2
- package/dist/mcp/handlers/reference/index.js +15 -2
- package/dist/mcp/handlers/sync/index.js +3 -18
- package/dist/mcp/handlers/workflow/analyze.js +53 -105
- package/dist/mcp/handlers/workflow/deploy.js +129 -0
- package/dist/mcp/handlers/workflow/generate.js +8 -28
- package/dist/mcp/handlers/workflow/index.js +258 -85
- package/dist/mcp/handlers/workflow/modify.js +9 -29
- package/dist/mcp/handlers/workflow/optimize.js +22 -108
- package/dist/mcp/handlers/workflow/utils.js +0 -102
- package/dist/mcp/handlers-consolidated.js +15 -38
- package/dist/mcp/prompts.js +82 -44
- package/dist/mcp/resources.js +335 -3
- package/dist/mcp/server.js +242 -457
- package/dist/mcp/tools.js +44 -61
- package/dist/sdk/action-schema-parser.js +11 -5
- package/dist/sdk/client.js +46 -17
- package/dist/sdk/ema-client.js +11 -0
- package/dist/sdk/generated/deprecated-actions.js +171 -0
- package/dist/sdk/guidance.js +58 -35
- package/dist/sdk/index.js +8 -7
- package/dist/sdk/knowledge.js +216 -1932
- package/dist/sdk/quality-gates.js +60 -336
- package/dist/sdk/validation-rules.js +33 -0
- package/dist/sdk/workflow-fixer.js +29 -360
- package/dist/sdk/workflow-intent.js +43 -3
- package/dist/sdk/workflow-transformer.js +0 -342
- package/docs/dashboard-operations.md +35 -0
- package/docs/ema-user-guide.md +66 -0
- package/docs/mcp-tools-guide.md +74 -45
- package/package.json +2 -2
- package/dist/mcp/handlers/persona/analyze.js +0 -275
- package/dist/mcp/handlers/persona/compare.js +0 -32
- package/dist/mcp/handlers/workflow/compile.js +0 -39
- package/docs/DEBUG-ANALYSIS-unused-category-type-mismatch.md +0 -481
- package/docs/TODO-fix-analyzer-and-modify.md +0 -182
- package/resources/action-schema.json +0 -5678
|
@@ -5,18 +5,20 @@
|
|
|
5
5
|
* instead of a giant switch statement. Each mode is in its own file
|
|
6
6
|
* for better testability and maintainability.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* - get: Fetch single persona
|
|
8
|
+
* Persona modes:
|
|
9
|
+
* - get: Fetch single persona (LLM then analyzes the data)
|
|
10
10
|
* - list: List all personas
|
|
11
11
|
* - templates: List available templates
|
|
12
|
-
* - compare: Compare two personas
|
|
13
12
|
* - sanitize: Sanitize persona data
|
|
14
13
|
* - update: Update persona config
|
|
15
14
|
* - delete: Delete persona (with confirmation)
|
|
16
15
|
* - create/clone: Create new persona from template or clone existing
|
|
17
|
-
* - analyze: Analyze persona for issues
|
|
18
16
|
* - intent: Direct Intent Architect invocation for qualification
|
|
19
17
|
*
|
|
18
|
+
* REMOVED (LLM does these):
|
|
19
|
+
* - analyze: LLM analyzes data from 'get'
|
|
20
|
+
* - compare: LLM compares data from two 'get' calls
|
|
21
|
+
*
|
|
20
22
|
* Version management modes extracted to version.ts:
|
|
21
23
|
* - snapshot/version_create, history/version_list, version_get
|
|
22
24
|
* - version_compare, restore/version_restore, version_policy
|
|
@@ -25,29 +27,27 @@
|
|
|
25
27
|
import { handleGet } from "./get.js";
|
|
26
28
|
import { handleList } from "./list.js";
|
|
27
29
|
import { handleTemplates } from "./templates.js";
|
|
28
|
-
import { handleCompare } from "./compare.js";
|
|
29
30
|
import { handleSanitize } from "./sanitize.js";
|
|
30
31
|
import { handleUpdate } from "./update.js";
|
|
31
32
|
import { handleDelete } from "./delete.js";
|
|
32
33
|
import { handleCreate } from "./create.js";
|
|
33
|
-
import { handleAnalyze } from "./analyze.js";
|
|
34
34
|
import { handleIntent } from "./intent.js";
|
|
35
35
|
/**
|
|
36
36
|
* Dispatch table for persona modes
|
|
37
37
|
*
|
|
38
38
|
* Note: create/clone handlers require getTemplateId callback as extra param
|
|
39
|
+
*
|
|
40
|
+
* REMOVED analyze/compare - LLM does analysis/comparison
|
|
39
41
|
*/
|
|
40
42
|
export const PERSONA_MODE_HANDLERS = {
|
|
41
43
|
get: handleGet,
|
|
42
44
|
list: handleList,
|
|
43
45
|
templates: handleTemplates,
|
|
44
|
-
compare: handleCompare,
|
|
45
46
|
sanitize: handleSanitize,
|
|
46
47
|
update: handleUpdate,
|
|
47
48
|
delete: handleDelete,
|
|
48
49
|
create: handleCreate,
|
|
49
50
|
clone: handleCreate, // Clone uses same handler as create
|
|
50
|
-
analyze: handleAnalyze,
|
|
51
51
|
intent: handleIntent,
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
@@ -66,12 +66,11 @@ export function getPersonaModeHandler(mode) {
|
|
|
66
66
|
export { handleGet } from "./get.js";
|
|
67
67
|
export { handleList } from "./list.js";
|
|
68
68
|
export { handleTemplates } from "./templates.js";
|
|
69
|
-
export { handleCompare } from "./compare.js";
|
|
70
69
|
export { handleSanitize } from "./sanitize.js";
|
|
71
70
|
export { handleUpdate } from "./update.js";
|
|
72
71
|
export { handleDelete } from "./delete.js";
|
|
73
72
|
export { handleCreate } from "./create.js";
|
|
74
|
-
export { handleAnalyze } from "./analyze.js";
|
|
75
73
|
export { handleIntent } from "./intent.js";
|
|
74
|
+
// REMOVED: handleCompare, handleAnalyze - LLM does analysis/comparison
|
|
76
75
|
// Version management
|
|
77
76
|
export { handleVersion, isVersionMode } from "./version.js";
|
|
@@ -174,7 +174,8 @@ function applySpecToWorkflow(spec, existingWorkflow) {
|
|
|
174
174
|
* Handle persona(mode="update") - update persona config or workflow
|
|
175
175
|
*
|
|
176
176
|
* @param args.id - Persona ID or name (required)
|
|
177
|
-
* @param args.
|
|
177
|
+
* @param args.config - Config changes with widgets (preferred, smart merged)
|
|
178
|
+
* @param args.proto_config - Alias for config (internal, for backwards compatibility)
|
|
178
179
|
* @param args.workflow - Optional raw workflow_def to set
|
|
179
180
|
* @param args.workflow_spec - Optional WorkflowSpec (agent-built) - will be compiled
|
|
180
181
|
* @param args.preview - If true, return changes without deploying (default: false)
|
|
@@ -201,8 +202,9 @@ export async function handleUpdate(args, client) {
|
|
|
201
202
|
const projectSettings = existingProtoConfig.projectSettings;
|
|
202
203
|
const isVoice = projectSettings?.projectType === 5;
|
|
203
204
|
// Build merged proto_config with smart widget-level merging
|
|
205
|
+
// Accept both 'config' (tool schema) and 'proto_config' (internal) for compatibility
|
|
204
206
|
let mergedProtoConfig = { ...existingProtoConfig };
|
|
205
|
-
const newProtoConfig = args.proto_config;
|
|
207
|
+
const newProtoConfig = (args.config ?? args.proto_config);
|
|
206
208
|
const updatedWidgets = [];
|
|
207
209
|
if (newProtoConfig) {
|
|
208
210
|
// If proto_config has widgets, do smart widget-level merge
|
|
@@ -34,10 +34,22 @@ export async function handleReference(args, context) {
|
|
|
34
34
|
if (type === "actions") {
|
|
35
35
|
const client = context?.client;
|
|
36
36
|
const id = args.id;
|
|
37
|
-
// Categories list
|
|
37
|
+
// Categories list - API-first with catalog fallback
|
|
38
38
|
if (args.categories) {
|
|
39
|
+
if (client) {
|
|
40
|
+
try {
|
|
41
|
+
const actions = await client.listActions();
|
|
42
|
+
const apiCategories = [...new Set(actions.map(a => a.category).filter(Boolean))];
|
|
43
|
+
if (apiCategories.length > 0) {
|
|
44
|
+
return { categories: apiCategories, count: apiCategories.length, source: "api" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fallback to catalog
|
|
49
|
+
}
|
|
50
|
+
}
|
|
39
51
|
const categories = [...new Set(Object.values(AGENT_CATALOG).map(a => a.category))];
|
|
40
|
-
return { categories, count: categories.length };
|
|
52
|
+
return { categories, count: categories.length, source: "catalog" };
|
|
41
53
|
}
|
|
42
54
|
// Suggest for use case
|
|
43
55
|
if (args.suggest) {
|
|
@@ -114,6 +126,7 @@ export async function handleReference(args, context) {
|
|
|
114
126
|
category: a.category,
|
|
115
127
|
enabled: a.enabled,
|
|
116
128
|
})),
|
|
129
|
+
source: "api",
|
|
117
130
|
};
|
|
118
131
|
}
|
|
119
132
|
catch {
|
|
@@ -4,28 +4,13 @@
|
|
|
4
4
|
* Handles cross-environment synchronization of personas.
|
|
5
5
|
*/
|
|
6
6
|
import { resolvePersona } from "../utils.js";
|
|
7
|
-
|
|
8
|
-
const DEPRECATED_PARAMS = {
|
|
9
|
-
identifier: { newName: "id", message: "'identifier' is deprecated, use 'id' instead (will be removed in v2.0.0)" },
|
|
10
|
-
};
|
|
11
|
-
function checkDeprecatedParams(args) {
|
|
12
|
-
const warnings = [];
|
|
13
|
-
for (const [oldName, info] of Object.entries(DEPRECATED_PARAMS)) {
|
|
14
|
-
if (args[oldName] !== undefined) {
|
|
15
|
-
warnings.push(info.message);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return warnings;
|
|
19
|
-
}
|
|
7
|
+
import { handleDeprecatedParams } from "../deprecation.js";
|
|
20
8
|
/**
|
|
21
9
|
* Handle sync tool requests - sync personas across environments
|
|
22
10
|
*/
|
|
23
11
|
export async function handleSync(args, createClient, getSyncOptions) {
|
|
24
|
-
// Check for deprecated params
|
|
25
|
-
|
|
26
|
-
for (const warning of deprecationWarnings) {
|
|
27
|
-
console.warn(`[sync] Deprecation: ${warning}`);
|
|
28
|
-
}
|
|
12
|
+
// Check for deprecated params
|
|
13
|
+
handleDeprecatedParams(args, "sync");
|
|
29
14
|
const mode = args.mode || "run";
|
|
30
15
|
const id = args.id;
|
|
31
16
|
const identifier = args.identifier; // deprecated alias for 'id'
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workflow Analyze Handler
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Returns workflow DATA for LLM to analyze using rules from ema://rules/*.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
6
|
+
* IMPORTANT: This handler does NOT pre-compute issues or fixes.
|
|
7
|
+
* The LLM applies ANTI_PATTERNS, INPUT_SOURCE_RULES, and OPTIMIZATION_RULES
|
|
8
|
+
* to reason about the workflow.
|
|
9
|
+
*
|
|
10
|
+
* Returns:
|
|
11
|
+
* - workflow_def: The raw workflow structure
|
|
12
|
+
* - connections: Type information for each edge
|
|
13
|
+
* - metrics: Basic counts (nodes, edges, etc.)
|
|
14
|
+
* - rules_hint: Pointer to ema://rules/* for analysis guidance
|
|
12
15
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { autoFixWorkflow } from "../../../sdk/workflow-fixer.js";
|
|
15
|
-
import { analyzeExecutionFlow, generateASCIIFlow } from "../../../sdk/workflow-execution-analyzer.js";
|
|
16
|
+
import { validateWorkflowConnections, parseWorkflowDef } from "../../../sdk/knowledge.js";
|
|
16
17
|
/**
|
|
17
|
-
* Calculate workflow metrics
|
|
18
|
+
* Calculate workflow metrics (pure data, no analysis)
|
|
18
19
|
*/
|
|
19
20
|
function calculateMetrics(workflow) {
|
|
20
21
|
const actions = workflow.actions;
|
|
@@ -36,15 +37,24 @@ function calculateMetrics(workflow) {
|
|
|
36
37
|
// Get trigger type
|
|
37
38
|
const trigger = workflow.trigger;
|
|
38
39
|
const triggerType = trigger?.trigger_type;
|
|
40
|
+
// Check for categorizer
|
|
41
|
+
const hasCategorizer = actions.some(a => {
|
|
42
|
+
const name = a.name?.toLowerCase() ?? "";
|
|
43
|
+
const action = a.action?.name?.name;
|
|
44
|
+
return name.includes("categorizer") || action?.includes("categorizer");
|
|
45
|
+
});
|
|
39
46
|
return {
|
|
40
47
|
node_count: actions.length,
|
|
41
48
|
connection_count: connectionCount,
|
|
42
49
|
has_hitl: hasHitl,
|
|
50
|
+
has_categorizer: hasCategorizer,
|
|
43
51
|
trigger_type: triggerType,
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
/**
|
|
47
55
|
* Handle workflow analyze mode
|
|
56
|
+
*
|
|
57
|
+
* Returns DATA for LLM to analyze - does NOT pre-compute issues.
|
|
48
58
|
*/
|
|
49
59
|
export async function handleWorkflowAnalyze(args, client) {
|
|
50
60
|
const personaId = args.persona_id;
|
|
@@ -62,110 +72,48 @@ export async function handleWorkflowAnalyze(args, client) {
|
|
|
62
72
|
if (!workflow) {
|
|
63
73
|
return { error: "No workflow to analyze. Provide workflow_def or persona_id." };
|
|
64
74
|
}
|
|
65
|
-
//
|
|
66
|
-
const
|
|
75
|
+
// Parse workflow to simpler structure
|
|
76
|
+
const nodes = parseWorkflowDef(workflow);
|
|
77
|
+
// Get connection type information
|
|
78
|
+
const connections = validateWorkflowConnections(workflow);
|
|
79
|
+
// Calculate metrics
|
|
80
|
+
const metrics = calculateMetrics(workflow);
|
|
67
81
|
const result = {
|
|
68
82
|
mode: "analyze",
|
|
69
83
|
persona_id: personaId,
|
|
70
84
|
persona_name: persona?.name,
|
|
71
|
-
|
|
85
|
+
// RAW DATA for LLM to analyze
|
|
86
|
+
workflow_def: workflow,
|
|
87
|
+
node_count: nodes.length,
|
|
88
|
+
nodes: nodes.map(n => ({
|
|
89
|
+
id: n.id,
|
|
90
|
+
action: n.action_name,
|
|
91
|
+
display_name: n.display_name,
|
|
92
|
+
inputs: n.incoming_edges?.map(e => `${e.source_node_id}.${e.source_output}`),
|
|
93
|
+
})),
|
|
94
|
+
// Type information for connections
|
|
95
|
+
connections: connections.map(c => ({
|
|
96
|
+
edge: c.edge_id,
|
|
97
|
+
source_type: c.source_type,
|
|
98
|
+
target_type: c.target_type,
|
|
99
|
+
compatible: c.compatible,
|
|
100
|
+
note: c.note,
|
|
101
|
+
})),
|
|
102
|
+
// Basic metrics
|
|
103
|
+
metrics,
|
|
104
|
+
// Tell LLM where to find analysis rules
|
|
105
|
+
_next_steps: [
|
|
106
|
+
"1. Fetch ema://rules/anti-patterns",
|
|
107
|
+
"2. Fetch ema://rules/structural-invariants",
|
|
108
|
+
"3. Check each node against the rules",
|
|
109
|
+
"4. Report issues YOU find (MCP does not pre-compute)",
|
|
110
|
+
],
|
|
72
111
|
};
|
|
73
|
-
// Issues and fixes
|
|
74
|
-
if (include.includes("issues") || include.includes("fixes")) {
|
|
75
|
-
const issues = detectWorkflowIssues(workflow);
|
|
76
|
-
if (include.includes("issues")) {
|
|
77
|
-
result.issues = issues;
|
|
78
|
-
}
|
|
79
|
-
if (include.includes("fixes")) {
|
|
80
|
-
result.fixes = suggestWorkflowFixes(issues);
|
|
81
|
-
}
|
|
82
|
-
result.issue_summary = {
|
|
83
|
-
total: issues.length,
|
|
84
|
-
critical: issues.filter((i) => i.severity === "critical").length,
|
|
85
|
-
warning: issues.filter((i) => i.severity === "warning").length,
|
|
86
|
-
info: issues.filter((i) => i.severity === "info").length,
|
|
87
|
-
};
|
|
88
|
-
result.validation_passed = issues.filter((i) => i.severity === "critical").length === 0;
|
|
89
|
-
}
|
|
90
|
-
// Connection validation
|
|
91
|
-
if (include.includes("connections")) {
|
|
92
|
-
result.connections = validateWorkflowConnections(workflow);
|
|
93
|
-
}
|
|
94
|
-
// Metrics
|
|
95
|
-
if (include.includes("metrics")) {
|
|
96
|
-
result.metrics = calculateMetrics(workflow);
|
|
97
|
-
}
|
|
98
|
-
// Execution flow analysis
|
|
99
|
-
if (include.includes("execution_flow")) {
|
|
100
|
-
const execAnalysis = analyzeExecutionFlow(workflow);
|
|
101
|
-
result.execution_flow = {
|
|
102
|
-
summary: execAnalysis.summary,
|
|
103
|
-
loops: execAnalysis.loops,
|
|
104
|
-
multiple_responder_issues: execAnalysis.multipleResponderIssues,
|
|
105
|
-
redundant_classifiers: execAnalysis.redundantClassifiers,
|
|
106
|
-
data_flow_issues: execAnalysis.dataFlowIssues,
|
|
107
|
-
dead_code_paths: execAnalysis.deadCodePaths,
|
|
108
|
-
};
|
|
109
|
-
// Include ASCII visualization if requested
|
|
110
|
-
if (args.visualize) {
|
|
111
|
-
result.execution_flow_ascii = generateASCIIFlow(execAnalysis);
|
|
112
|
-
}
|
|
113
|
-
// Add specific warnings for triple response risk
|
|
114
|
-
if (execAnalysis.summary.mayRepeatResponses) {
|
|
115
|
-
result.triple_response_warning = "⚠️ This workflow may cause duplicate/triple responses due to ungated parallel responders";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
112
|
if (persona) {
|
|
119
113
|
result.persona = {
|
|
120
114
|
id: persona.id,
|
|
121
115
|
name: persona.name
|
|
122
116
|
};
|
|
123
117
|
}
|
|
124
|
-
// Calculate optimization guidance
|
|
125
|
-
const issues = result.issues;
|
|
126
|
-
const criticalCount = issues?.filter(i => i.severity === "critical").length ?? 0;
|
|
127
|
-
const warningCount = issues?.filter(i => i.severity === "warning").length ?? 0;
|
|
128
|
-
// AUTO-FIX: If fix=true, apply fixes and deploy
|
|
129
|
-
if (args.fix && personaId && persona && workflow) {
|
|
130
|
-
const fixResult = autoFixWorkflow(workflow);
|
|
131
|
-
result.fixes_applied = fixResult.fixesApplied;
|
|
132
|
-
result.fixes_warnings = fixResult.warnings;
|
|
133
|
-
if (fixResult.fixesApplied.length > 0 && fixResult.success) {
|
|
134
|
-
// Deploy the fixed workflow
|
|
135
|
-
try {
|
|
136
|
-
const fixedWorkflow = fixResult.workflowDef;
|
|
137
|
-
await client.updateAiEmployee({
|
|
138
|
-
persona_id: personaId,
|
|
139
|
-
workflow: fixedWorkflow,
|
|
140
|
-
proto_config: persona.proto_config,
|
|
141
|
-
});
|
|
142
|
-
result.fix_status = "deployed";
|
|
143
|
-
result.fixes_deployed = fixResult.fixesApplied.length;
|
|
144
|
-
result.status = `✅ Applied ${fixResult.fixesApplied.length} fixes and deployed`;
|
|
145
|
-
}
|
|
146
|
-
catch (deployErr) {
|
|
147
|
-
result.fix_status = "failed";
|
|
148
|
-
result.fix_error = deployErr instanceof Error ? deployErr.message : String(deployErr);
|
|
149
|
-
result.status = `⚠️ Fixes applied but deploy failed: ${result.fix_error}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else if (fixResult.fixesApplied.length === 0) {
|
|
153
|
-
result.fix_status = "no_fixes_needed";
|
|
154
|
-
result.status = "✅ No auto-fixable issues found";
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
result.fix_status = "failed";
|
|
158
|
-
result.status = "⚠️ Fix generation failed";
|
|
159
|
-
}
|
|
160
|
-
return result;
|
|
161
|
-
}
|
|
162
|
-
// Provide optimization suggestion
|
|
163
|
-
if (criticalCount > 0 || warningCount > 0) {
|
|
164
|
-
result.optimization_suggestion = `This workflow has ${criticalCount} critical and ${warningCount} warning issues. ` +
|
|
165
|
-
`Use workflow(persona_id="${personaId ?? 'ID'}", optimize=true) to auto-fix.`;
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
result.status = "✅ Workflow is healthy - no issues detected";
|
|
169
|
-
}
|
|
170
118
|
return result;
|
|
171
119
|
}
|
|
@@ -2,8 +2,31 @@
|
|
|
2
2
|
* Workflow Deploy Handler
|
|
3
3
|
*
|
|
4
4
|
* Deploys a workflow_def to an existing persona.
|
|
5
|
+
* Includes deprecation warnings (does not block, just warns).
|
|
6
|
+
*
|
|
7
|
+
* BLOCKING VALIDATION:
|
|
8
|
+
* - If workflow has knowledge search nodes, data sources MUST be attached and indexed.
|
|
5
9
|
*/
|
|
6
10
|
import { sanitizeWorkflowForDeploy } from "./utils.js";
|
|
11
|
+
import { DEPRECATED_ACTIONS_FALLBACK, checkWorkflowDeprecations } from "./index.js";
|
|
12
|
+
/**
|
|
13
|
+
* Detect knowledge search nodes in a workflow.
|
|
14
|
+
* Returns true if workflow contains search/v2 or search_datastore nodes (NOT web_search).
|
|
15
|
+
*/
|
|
16
|
+
function hasKnowledgeSearchNodes(workflowDef) {
|
|
17
|
+
const actions = (workflowDef.actions ?? []);
|
|
18
|
+
return actions.some(a => {
|
|
19
|
+
const actionType = a.action || a.actionType || "";
|
|
20
|
+
const name = a.name || "";
|
|
21
|
+
// Match: search, search/v2, search_datastore
|
|
22
|
+
// Exclude: web_search, live_web_search
|
|
23
|
+
const isSearchNode = actionType.includes("search") ||
|
|
24
|
+
name.toLowerCase().includes("search");
|
|
25
|
+
const isWebSearch = actionType.includes("web_search") ||
|
|
26
|
+
name.toLowerCase().includes("web_search");
|
|
27
|
+
return isSearchNode && !isWebSearch;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
7
30
|
/**
|
|
8
31
|
* Handle workflow deploy mode
|
|
9
32
|
*/
|
|
@@ -21,8 +44,103 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
21
44
|
if (!persona) {
|
|
22
45
|
return { error: `Persona not found: ${personaId}` };
|
|
23
46
|
}
|
|
47
|
+
// ── BLOCKING VALIDATION: Bidirectional search/data source validation ──
|
|
48
|
+
const hasSearchNodes = hasKnowledgeSearchNodes(workflowDef);
|
|
49
|
+
let dataSourceStats = null;
|
|
50
|
+
// Try to get data source stats (used for both validation directions)
|
|
51
|
+
try {
|
|
52
|
+
dataSourceStats = await client.getDataSourceAggregates(personaId);
|
|
53
|
+
}
|
|
54
|
+
catch (statsError) {
|
|
55
|
+
// If we can't check stats, log warning but don't block
|
|
56
|
+
console.error(`[deploy] Could not verify data sources: ${statsError}`);
|
|
57
|
+
}
|
|
58
|
+
// VALIDATION 1: If workflow has search nodes, data sources MUST exist and be indexed
|
|
59
|
+
if (hasSearchNodes && dataSourceStats) {
|
|
60
|
+
if (dataSourceStats.total === 0) {
|
|
61
|
+
return {
|
|
62
|
+
error: "DEPLOYMENT BLOCKED: Workflow contains knowledge search nodes but persona has no data sources attached.",
|
|
63
|
+
mode: "deploy",
|
|
64
|
+
persona_id: personaId,
|
|
65
|
+
persona_name: persona.name,
|
|
66
|
+
validation_failed: "search_nodes_require_data_sources",
|
|
67
|
+
current_state: {
|
|
68
|
+
search_nodes_detected: true,
|
|
69
|
+
data_sources_total: 0,
|
|
70
|
+
data_sources_indexed: 0,
|
|
71
|
+
},
|
|
72
|
+
_fix: [
|
|
73
|
+
"1. Upload documents: persona(id='...',data={method:'upload',path:'/path/to/doc.pdf'})",
|
|
74
|
+
"2. Check status: persona(id='...',data={method:'stats'}) → wait for 'success' > 0",
|
|
75
|
+
"3. Then retry: workflow(mode='deploy', persona_id='...', workflow_def={...})",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (dataSourceStats.success === 0) {
|
|
80
|
+
return {
|
|
81
|
+
error: "DEPLOYMENT BLOCKED: Workflow contains knowledge search nodes but no data sources have been successfully indexed.",
|
|
82
|
+
mode: "deploy",
|
|
83
|
+
persona_id: personaId,
|
|
84
|
+
persona_name: persona.name,
|
|
85
|
+
validation_failed: "search_nodes_require_indexed_data",
|
|
86
|
+
current_state: {
|
|
87
|
+
search_nodes_detected: true,
|
|
88
|
+
data_sources_total: dataSourceStats.total,
|
|
89
|
+
data_sources_pending: dataSourceStats.pending,
|
|
90
|
+
data_sources_success: dataSourceStats.success,
|
|
91
|
+
data_sources_failed: dataSourceStats.failed,
|
|
92
|
+
},
|
|
93
|
+
_fix: dataSourceStats.pending > 0
|
|
94
|
+
? [
|
|
95
|
+
"Data sources are still indexing. Wait for indexing to complete:",
|
|
96
|
+
"1. Check status: persona(id='...',data={method:'stats'})",
|
|
97
|
+
"2. Retry when 'success' > 0",
|
|
98
|
+
]
|
|
99
|
+
: [
|
|
100
|
+
"All data sources failed indexing. Check file formats and re-upload:",
|
|
101
|
+
"1. List files: persona(id='...',data={method:'list'})",
|
|
102
|
+
"2. Delete failed: persona(id='...',data={method:'delete',file_id:'...'})",
|
|
103
|
+
"3. Re-upload: persona(id='...',data={method:'upload',path:'...'})",
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// VALIDATION 2: If persona has indexed data sources but workflow has NO search nodes → BLOCK
|
|
109
|
+
// This catches the case where user uploads documents but deploys a workflow that won't use them
|
|
110
|
+
if (!hasSearchNodes && dataSourceStats && dataSourceStats.success > 0) {
|
|
111
|
+
return {
|
|
112
|
+
error: "DEPLOYMENT BLOCKED: Persona has indexed knowledge files but workflow has no search nodes. The uploaded documents will NEVER be used.",
|
|
113
|
+
mode: "deploy",
|
|
114
|
+
persona_id: personaId,
|
|
115
|
+
persona_name: persona.name,
|
|
116
|
+
validation_failed: "data_sources_require_search_nodes",
|
|
117
|
+
current_state: {
|
|
118
|
+
search_nodes_detected: false,
|
|
119
|
+
data_sources_total: dataSourceStats.total,
|
|
120
|
+
data_sources_indexed: dataSourceStats.success,
|
|
121
|
+
},
|
|
122
|
+
_fix: [
|
|
123
|
+
"Your workflow needs a search node to query the uploaded documents.",
|
|
124
|
+
"1. Get workflow schema: workflow(mode='get', persona_id='...')",
|
|
125
|
+
"2. Add search node: Update workflow_def to include a search/v2 node that queries the knowledge base",
|
|
126
|
+
"3. Wire it up: Connect chat_trigger → summarizer → search → respond_for_external_actions",
|
|
127
|
+
"Example pattern: trigger → conversation_summarizer → search/v2 → respond_for_external_actions → WORKFLOW_OUTPUT",
|
|
128
|
+
],
|
|
129
|
+
_example_workflow_pattern: {
|
|
130
|
+
nodes: [
|
|
131
|
+
"chat_trigger (trigger)",
|
|
132
|
+
"conversation_summarizer (extract query from conversation)",
|
|
133
|
+
"search/v2 (query knowledge base with datastore_configs)",
|
|
134
|
+
"respond_for_external_actions (generate response with sources)",
|
|
135
|
+
],
|
|
136
|
+
flow: "trigger → summarizer → search → respond → output",
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
24
140
|
// Sanitize workflow before deployment
|
|
25
141
|
const sanitizedWorkflow = sanitizeWorkflowForDeploy(workflowDef);
|
|
142
|
+
// Check for deprecated actions (warn, don't block)
|
|
143
|
+
const deprecationWarnings = checkWorkflowDeprecations(workflowDef, DEPRECATED_ACTIONS_FALLBACK);
|
|
26
144
|
// Determine proto_config to use: provided > existing
|
|
27
145
|
const existingProtoConfig = persona.proto_config;
|
|
28
146
|
const providedProtoConfig = args.proto_config;
|
|
@@ -31,6 +149,11 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
31
149
|
const projectSettings = protoConfigToUse.projectSettings;
|
|
32
150
|
const isVoice = projectSettings?.projectType === 5;
|
|
33
151
|
const warnings = [];
|
|
152
|
+
// Add deprecation warning message if deprecated actions found
|
|
153
|
+
if (deprecationWarnings.length > 0) {
|
|
154
|
+
warnings.push(`Workflow uses ${deprecationWarnings.length} deprecated action(s). ` +
|
|
155
|
+
"Consider updating to prevent future breakage when deprecated actions are removed.");
|
|
156
|
+
}
|
|
34
157
|
if (isVoice && !providedProtoConfig) {
|
|
35
158
|
// Check if existing proto_config has conversationSettings populated
|
|
36
159
|
const widgets = (protoConfigToUse.widgets ?? []);
|
|
@@ -58,6 +181,12 @@ export async function handleWorkflowDeploy(args, client) {
|
|
|
58
181
|
if (warnings.length > 0) {
|
|
59
182
|
result.warnings = warnings;
|
|
60
183
|
}
|
|
184
|
+
// Include detailed deprecation info if any deprecated actions found
|
|
185
|
+
if (deprecationWarnings.length > 0) {
|
|
186
|
+
result.deprecation_warnings = deprecationWarnings;
|
|
187
|
+
result._warning = "Workflow deployed but contains deprecated actions. Update before platform removes them.";
|
|
188
|
+
result._next_step = "Use workflow(mode='get') to see current schema, then deploy updated workflow_def with non-deprecated actions.";
|
|
189
|
+
}
|
|
61
190
|
return result;
|
|
62
191
|
}
|
|
63
192
|
catch (err) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { parseInput, intentToSpec, generateWorkflow } from "../../../sdk/workflow-intent.js";
|
|
12
12
|
import { compileWorkflow } from "../../../sdk/workflow-generator.js";
|
|
13
|
-
|
|
13
|
+
// Detection removed - LLM analyzes with rules from ema://rules/*
|
|
14
14
|
import { runIntentArchitect } from "../../../sdk/intent-architect.js";
|
|
15
15
|
import { ensureActionRegistry } from "../../../sdk/action-registry.js";
|
|
16
16
|
import { ensureSchemaRegistry, validateWorkflowSpec, generateActionCatalogForLLM } from "../../../sdk/workflow-validator.js";
|
|
@@ -142,30 +142,13 @@ async function deployToNewPersona(args, client, compiled, actionRegistry, getTem
|
|
|
142
142
|
widgets: Array.from(widgetMap.values()),
|
|
143
143
|
};
|
|
144
144
|
// Step 4: Deploy workflow
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const wfName = workflowForDeploy.workflowName;
|
|
149
|
-
if (wfName?.name && existingWfName?.name) {
|
|
150
|
-
wfName.name.namespaces = existingWfName.name.namespaces;
|
|
151
|
-
wfName.name.name = existingWfName.name.name;
|
|
152
|
-
}
|
|
153
|
-
// Fix results format
|
|
154
|
-
const compiledResults = workflowForDeploy.results;
|
|
155
|
-
if (compiledResults) {
|
|
156
|
-
const newResults = {};
|
|
157
|
-
for (const [, value] of Object.entries(compiledResults)) {
|
|
158
|
-
if (value.actionName && value.outputName) {
|
|
159
|
-
const key = `${value.actionName}.${value.outputName}`;
|
|
160
|
-
newResults[key] = value;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
workflowForDeploy.results = newResults;
|
|
164
|
-
}
|
|
145
|
+
// NOTE: The SDK's updateAiEmployee() handles workflowName namespace automatically.
|
|
146
|
+
// It will copy from existing workflow if present, or generate a valid namespace if not.
|
|
147
|
+
// It also fixes the results format. No need to manually manipulate these here.
|
|
165
148
|
try {
|
|
166
149
|
await client.updateAiEmployee({
|
|
167
150
|
persona_id: newPersonaId,
|
|
168
|
-
workflow:
|
|
151
|
+
workflow: compiled.workflow_def,
|
|
169
152
|
proto_config: mergedProtoConfig,
|
|
170
153
|
}, { verbose: true });
|
|
171
154
|
return { success: true, personaId: newPersonaId, personaName };
|
|
@@ -182,7 +165,7 @@ async function deployToNewPersona(args, client, compiled, actionRegistry, getTem
|
|
|
182
165
|
personaId: newPersonaId,
|
|
183
166
|
personaName,
|
|
184
167
|
workflowDeployError: errMsg,
|
|
185
|
-
workflowAttempted:
|
|
168
|
+
workflowAttempted: compiled.workflow_def,
|
|
186
169
|
};
|
|
187
170
|
}
|
|
188
171
|
}
|
|
@@ -288,14 +271,14 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
|
|
|
288
271
|
}
|
|
289
272
|
}
|
|
290
273
|
const compiled = compileWorkflow(spec, { registry: actionRegistry });
|
|
291
|
-
// Validate generated workflow
|
|
292
|
-
const issues = detectWorkflowIssues(compiled.workflow_def);
|
|
293
274
|
const result = {
|
|
294
275
|
mode: "generate",
|
|
295
276
|
status: preview ? "preview" : "deployed",
|
|
296
277
|
workflow_def: compiled.workflow_def,
|
|
297
278
|
proto_config: compiled.proto_config,
|
|
298
279
|
validation: parseResult.validation,
|
|
280
|
+
// LLM should analyze with ema://rules/anti-patterns
|
|
281
|
+
_analysis_hint: "Use ema://rules/anti-patterns to check for issues",
|
|
299
282
|
};
|
|
300
283
|
if (specValidation) {
|
|
301
284
|
result.api_validation = {
|
|
@@ -304,9 +287,6 @@ export async function handleWorkflowGenerate(args, client, getTemplateId) {
|
|
|
304
287
|
action_coverage: specValidation.action_coverage,
|
|
305
288
|
};
|
|
306
289
|
}
|
|
307
|
-
if (issues.length > 0) {
|
|
308
|
-
result.issues = issues;
|
|
309
|
-
}
|
|
310
290
|
// Deploy if not preview
|
|
311
291
|
if (!preview && personaId) {
|
|
312
292
|
const persona = await client.getPersonaById(personaId);
|