@ema.co/mcp-toolkit 2026.1.27 → 2026.1.28-2
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/data/index.js +3 -0
- package/dist/mcp/handlers/persona/create.js +16 -0
- package/dist/mcp/handlers/persona/list.js +9 -4
- package/dist/mcp/handlers/persona/update.js +24 -2
- package/dist/mcp/handlers/workflow/deploy.js +20 -2
- package/dist/mcp/handlers/workflow/generate.js +39 -2
- package/dist/mcp/handlers/workflow/index.js +8 -3
- package/dist/mcp/handlers/workflow/modify.js +34 -7
- package/dist/mcp/handlers/workflow/validate.js +85 -0
- package/dist/mcp/handlers/workflow/validation.js +160 -0
- package/dist/mcp/resources.js +286 -4
- package/dist/mcp/server.js +16 -3
- package/dist/mcp/tools.js +32 -11
- package/dist/sdk/client.js +36 -9
- package/dist/sdk/ema-client.js +32 -4
- package/dist/sdk/index.js +3 -1
- package/dist/sdk/knowledge.js +5 -5
- package/dist/sdk/structural-rules.js +498 -0
- package/dist/sdk/workflow-generator.js +2 -1
- package/dist/sdk/workflow-intent.js +28 -96
- package/dist/sdk/workflow-path-enumerator.js +278 -0
- package/dist/sdk/workflow-static-validator.js +291 -0
- package/dist/sdk/workflow-validation-types.js +7 -0
- package/docs/README.md +14 -0
- package/docs/go-validator-analysis.md +323 -0
- package/docs/rule-format-specification.md +346 -0
- package/docs/validation-contract.md +397 -0
- package/docs/validation-error-format.md +326 -0
- package/package.json +1 -1
- package/dist/mcp/workflow-operations.js +0 -100
- package/dist/sdk/workflow-fixer.js +0 -48
- package/docs/dashboard-operations.md +0 -281
- package/docs/ema-user-guide.md +0 -1201
- package/docs/email-patterns.md +0 -120
- package/docs/mcp-tools-guide.md +0 -575
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static Workflow Validator
|
|
3
|
+
*
|
|
4
|
+
* Implements Go validator's static validation logic:
|
|
5
|
+
* - Path enumeration
|
|
6
|
+
* - Required output validation (all paths produce all named results)
|
|
7
|
+
* - Multiple writers detection
|
|
8
|
+
* - Response/abstain validation
|
|
9
|
+
* - Category hierarchy validation
|
|
10
|
+
*
|
|
11
|
+
* Based on: workflow-engine/pkg/engine/validator.go
|
|
12
|
+
*/
|
|
13
|
+
import { enumeratePaths } from "./workflow-path-enumerator.js";
|
|
14
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
// Main Validation Function
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Perform static validation on a workflow.
|
|
19
|
+
*
|
|
20
|
+
* Only validates workflows with named results (resultMappings).
|
|
21
|
+
* If no named results, returns valid (matching Go validator behavior).
|
|
22
|
+
*/
|
|
23
|
+
export function validateWorkflowStatic(workflow, options) {
|
|
24
|
+
// Collect all errors (path-dependent and global)
|
|
25
|
+
const errors = [];
|
|
26
|
+
const warnings = [];
|
|
27
|
+
const info = [];
|
|
28
|
+
// Validate categorizers globally (not dependent on named results)
|
|
29
|
+
const categorizerErrors = validateAllCategorizers(workflow);
|
|
30
|
+
errors.push(...categorizerErrors.filter(e => e.severity === "critical"));
|
|
31
|
+
warnings.push(...categorizerErrors.filter(e => e.severity === "warning"));
|
|
32
|
+
info.push(...categorizerErrors.filter(e => e.severity === "info"));
|
|
33
|
+
// Static validation only applies to workflows with named results
|
|
34
|
+
if (!workflow.resultMappings || workflow.resultMappings.length === 0) {
|
|
35
|
+
// Count errors by type
|
|
36
|
+
const errorsByType = {
|
|
37
|
+
required_output_not_produced: 0,
|
|
38
|
+
multiple_writers: 0,
|
|
39
|
+
missing_response_or_abstain_reason: 0,
|
|
40
|
+
category_hierarchy_violation: errors.filter(e => e.type === "category_hierarchy_violation").length,
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
valid: errors.length === 0,
|
|
44
|
+
errors,
|
|
45
|
+
warnings,
|
|
46
|
+
info,
|
|
47
|
+
summary: {
|
|
48
|
+
total_paths: 0,
|
|
49
|
+
valid_paths: 0,
|
|
50
|
+
invalid_paths: 0,
|
|
51
|
+
errors_by_type: errorsByType,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Enumerate all execution paths
|
|
56
|
+
const paths = enumeratePaths(workflow, options);
|
|
57
|
+
// Extract named results from resultMappings
|
|
58
|
+
const namedResults = extractNamedResults(workflow.resultMappings);
|
|
59
|
+
// Validate each path
|
|
60
|
+
for (const path of paths) {
|
|
61
|
+
const pathErrors = validatePath(path, workflow, namedResults);
|
|
62
|
+
errors.push(...pathErrors.filter(e => e.severity === "critical"));
|
|
63
|
+
warnings.push(...pathErrors.filter(e => e.severity === "warning"));
|
|
64
|
+
info.push(...pathErrors.filter(e => e.severity === "info"));
|
|
65
|
+
}
|
|
66
|
+
// Count errors by type
|
|
67
|
+
const errorsByType = {
|
|
68
|
+
required_output_not_produced: errors.filter(e => e.type === "required_output_not_produced").length,
|
|
69
|
+
multiple_writers: errors.filter(e => e.type === "multiple_writers").length,
|
|
70
|
+
missing_response_or_abstain_reason: errors.filter(e => e.type === "missing_response_or_abstain_reason").length,
|
|
71
|
+
category_hierarchy_violation: errors.filter(e => e.type === "category_hierarchy_violation").length,
|
|
72
|
+
};
|
|
73
|
+
const validPaths = paths.filter(p => !p.validation_failed).length;
|
|
74
|
+
const invalidPaths = paths.filter(p => p.validation_failed).length;
|
|
75
|
+
return {
|
|
76
|
+
valid: errors.length === 0,
|
|
77
|
+
errors,
|
|
78
|
+
warnings,
|
|
79
|
+
info,
|
|
80
|
+
summary: {
|
|
81
|
+
total_paths: paths.length,
|
|
82
|
+
valid_paths: validPaths,
|
|
83
|
+
invalid_paths: invalidPaths,
|
|
84
|
+
errors_by_type: errorsByType,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Path Validation
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
function validatePath(path, workflow, namedResults) {
|
|
92
|
+
const errors = [];
|
|
93
|
+
// 1. Check required outputs
|
|
94
|
+
errors.push(...validateRequiredOutputs(path, workflow, namedResults));
|
|
95
|
+
// 2. Check multiple writers
|
|
96
|
+
errors.push(...validateMultipleWriters(path, workflow, namedResults));
|
|
97
|
+
// 3. Check response/abstain
|
|
98
|
+
errors.push(...validateResponseOrAbstain(path, workflow));
|
|
99
|
+
// 4. Check category hierarchy (if path has categorizer)
|
|
100
|
+
errors.push(...validateCategoryHierarchy(path, workflow));
|
|
101
|
+
return errors;
|
|
102
|
+
}
|
|
103
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
104
|
+
// Validation: Required Outputs
|
|
105
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
function validateRequiredOutputs(path, workflow, namedResults) {
|
|
107
|
+
const errors = [];
|
|
108
|
+
// Get outputs produced on this path
|
|
109
|
+
const producedOutputs = new Set(Object.keys(path.final_outputs));
|
|
110
|
+
// Check each named result
|
|
111
|
+
for (const namedResult of namedResults) {
|
|
112
|
+
if (!producedOutputs.has(namedResult)) {
|
|
113
|
+
// Extract node and output from named result format: "nodeId.outputName"
|
|
114
|
+
const [nodeId, outputName] = namedResult.split(".");
|
|
115
|
+
errors.push({
|
|
116
|
+
type: "required_output_not_produced",
|
|
117
|
+
rule_id: "required_output_all_paths",
|
|
118
|
+
severity: "critical",
|
|
119
|
+
location: {
|
|
120
|
+
path: path.completed_actions,
|
|
121
|
+
category: path.chosen_enumerable_value,
|
|
122
|
+
named_result: namedResult,
|
|
123
|
+
action_name: path.action_name,
|
|
124
|
+
output_name: path.enumerable_output,
|
|
125
|
+
output_value: path.chosen_enumerable_value,
|
|
126
|
+
},
|
|
127
|
+
what: `Path '${formatPathName(path)}' does not produce required output '${namedResult}'`,
|
|
128
|
+
why: `All execution paths from trigger must produce all named results. The path '${formatPathName(path)}' ends at '${path.action_name}' without producing '${namedResult}'.`,
|
|
129
|
+
how_to_fix: `Add a node that produces '${namedResult}' to the '${formatPathName(path)}' path. Connect the node's '${outputName}' output to WORKFLOW_OUTPUT via resultMappings.`,
|
|
130
|
+
rule_reference: "ema://validation/rules#required_output_all_paths",
|
|
131
|
+
examples: {
|
|
132
|
+
bad: `trigger → ${path.completed_actions.join(" → ")} → (no ${outputName})`,
|
|
133
|
+
good: `trigger → ${path.completed_actions.join(" → ")} → ${nodeId} → WORKFLOW_OUTPUT`,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return errors;
|
|
139
|
+
}
|
|
140
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Validation: Multiple Writers
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
function validateMultipleWriters(path, workflow, namedResults) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
// Build map: namedResult -> nodes that produce it
|
|
146
|
+
const producers = new Map();
|
|
147
|
+
for (const namedResult of namedResults) {
|
|
148
|
+
const [nodeId] = namedResult.split(".");
|
|
149
|
+
// Check if this node is in the path
|
|
150
|
+
if (path.completed_actions.includes(nodeId)) {
|
|
151
|
+
if (!producers.has(namedResult)) {
|
|
152
|
+
producers.set(namedResult, []);
|
|
153
|
+
}
|
|
154
|
+
producers.get(namedResult).push(nodeId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Check for multiple producers
|
|
158
|
+
for (const [namedResult, nodeIds] of producers.entries()) {
|
|
159
|
+
if (nodeIds.length > 1) {
|
|
160
|
+
errors.push({
|
|
161
|
+
type: "multiple_writers",
|
|
162
|
+
rule_id: "single_writer_per_output",
|
|
163
|
+
severity: "critical",
|
|
164
|
+
location: {
|
|
165
|
+
path: path.completed_actions,
|
|
166
|
+
category: path.chosen_enumerable_value,
|
|
167
|
+
named_result: namedResult,
|
|
168
|
+
action_name: path.action_name,
|
|
169
|
+
},
|
|
170
|
+
what: `Multiple nodes produce '${namedResult}' on path '${formatPathName(path)}'`,
|
|
171
|
+
why: `A single named result can only have one producer per execution path. The path '${formatPathName(path)}' has both '${nodeIds[0]}' and '${nodeIds[1]}' producing '${namedResult}'.`,
|
|
172
|
+
how_to_fix: `Ensure only one node produces '${namedResult}' on the '${formatPathName(path)}' path. Either remove one of the producers or use different named results for each node.`,
|
|
173
|
+
rule_reference: "ema://validation/rules#single_writer_per_output",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return errors;
|
|
178
|
+
}
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
// Validation: Response or Abstain
|
|
181
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
function validateResponseOrAbstain(path, workflow) {
|
|
183
|
+
const errors = [];
|
|
184
|
+
// Check if path produces any response
|
|
185
|
+
const responseNodes = ["respond_with_sources", "call_llm", "fixed_response"];
|
|
186
|
+
const hasResponse = path.completed_actions.some(actionId => {
|
|
187
|
+
const node = workflow.nodes.find(n => n.id === actionId);
|
|
188
|
+
return node && responseNodes.includes(node.actionType);
|
|
189
|
+
});
|
|
190
|
+
// Check if path has abstain reason
|
|
191
|
+
// TODO: How is abstain reason defined? Need to check Go code
|
|
192
|
+
const hasAbstain = false; // Placeholder
|
|
193
|
+
if (!hasResponse && !hasAbstain) {
|
|
194
|
+
errors.push({
|
|
195
|
+
type: "missing_response_or_abstain_reason",
|
|
196
|
+
rule_id: "response_or_abstain_required",
|
|
197
|
+
severity: "critical",
|
|
198
|
+
location: {
|
|
199
|
+
path: path.completed_actions,
|
|
200
|
+
category: path.chosen_enumerable_value,
|
|
201
|
+
action_name: path.action_name,
|
|
202
|
+
},
|
|
203
|
+
what: `Path '${formatPathName(path)}' does not produce a response and has no abstain reason`,
|
|
204
|
+
why: `Every execution path must either produce a user-visible response or explicitly abstain with a reason. The path '${formatPathName(path)}' ends at '${path.action_name}' without a response node and no abstain reason is specified.`,
|
|
205
|
+
how_to_fix: `Either add a response node (respond_with_sources, call_llm, or fixed_response) to the '${formatPathName(path)}' path, or add an abstain reason explaining why this path doesn't respond.`,
|
|
206
|
+
rule_reference: "ema://validation/rules#response_or_abstain_required",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return errors;
|
|
210
|
+
}
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
212
|
+
// Validation: Category Hierarchy
|
|
213
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
function validateCategoryHierarchy(path, workflow) {
|
|
215
|
+
const errors = [];
|
|
216
|
+
// Find categorizer nodes in path
|
|
217
|
+
const categorizers = path.completed_actions
|
|
218
|
+
.map(actionId => workflow.nodes.find(n => n.id === actionId))
|
|
219
|
+
.filter((node) => node !== undefined &&
|
|
220
|
+
(node.actionType === "chat_categorizer" || node.actionType === "text_categorizer"));
|
|
221
|
+
for (const categorizer of categorizers) {
|
|
222
|
+
// Check if categorizer has Fallback category
|
|
223
|
+
const hasFallback = categorizer.categories?.some(cat => cat.name === "Fallback" || cat.name === "Other");
|
|
224
|
+
if (!hasFallback) {
|
|
225
|
+
errors.push({
|
|
226
|
+
type: "category_hierarchy_violation",
|
|
227
|
+
rule_id: "category_hierarchy_valid",
|
|
228
|
+
severity: "critical",
|
|
229
|
+
location: {
|
|
230
|
+
node_id: categorizer.id,
|
|
231
|
+
path: path.completed_actions,
|
|
232
|
+
},
|
|
233
|
+
what: `Categorizer '${categorizer.id}' does not have a Fallback category`,
|
|
234
|
+
why: "Every categorizer must have a Fallback category to handle unrecognized intents. Without it, some user queries will have no handler.",
|
|
235
|
+
how_to_fix: `Add a Fallback category to the categorizer '${categorizer.id}' with description 'For unclear or other requests'.`,
|
|
236
|
+
rule_reference: "ema://validation/rules#category_hierarchy_valid",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// TODO: Check for category hierarchy violations
|
|
240
|
+
// Need to analyze named_result_agent_assist_validator.go for exact rules
|
|
241
|
+
}
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
244
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
245
|
+
// Helper Functions
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
247
|
+
function extractNamedResults(resultMappings) {
|
|
248
|
+
const namedResults = new Set();
|
|
249
|
+
for (const mapping of resultMappings) {
|
|
250
|
+
const namedResult = `${mapping.nodeId}.${mapping.output}`;
|
|
251
|
+
namedResults.add(namedResult);
|
|
252
|
+
}
|
|
253
|
+
return namedResults;
|
|
254
|
+
}
|
|
255
|
+
function formatPathName(path) {
|
|
256
|
+
if (path.chosen_enumerable_value) {
|
|
257
|
+
return path.chosen_enumerable_value;
|
|
258
|
+
}
|
|
259
|
+
if (path.action_name) {
|
|
260
|
+
return path.action_name;
|
|
261
|
+
}
|
|
262
|
+
return "Unknown";
|
|
263
|
+
}
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
// Global Categorizer Validation (not path-dependent)
|
|
266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
function validateAllCategorizers(workflow) {
|
|
268
|
+
const errors = [];
|
|
269
|
+
// Find all categorizer nodes in workflow
|
|
270
|
+
const categorizers = workflow.nodes.filter(node => node.actionType === "chat_categorizer" ||
|
|
271
|
+
node.actionType === "text_categorizer");
|
|
272
|
+
for (const categorizer of categorizers) {
|
|
273
|
+
// Check if categorizer has Fallback category
|
|
274
|
+
const hasFallback = categorizer.categories?.some(cat => cat.name === "Fallback" || cat.name === "Other");
|
|
275
|
+
if (!hasFallback) {
|
|
276
|
+
errors.push({
|
|
277
|
+
type: "category_hierarchy_violation",
|
|
278
|
+
rule_id: "category_hierarchy_valid",
|
|
279
|
+
severity: "critical",
|
|
280
|
+
location: {
|
|
281
|
+
node_id: categorizer.id,
|
|
282
|
+
},
|
|
283
|
+
what: `Categorizer '${categorizer.id}' does not have a Fallback category`,
|
|
284
|
+
why: "Every categorizer must have a Fallback category to handle unrecognized intents. Without it, some user queries will have no handler.",
|
|
285
|
+
how_to_fix: `Add a Fallback category to the categorizer '${categorizer.id}' with description 'For unclear or other requests'.`,
|
|
286
|
+
rule_reference: "ema://validation/rules#category_hierarchy_valid",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return errors;
|
|
291
|
+
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Deprecated
|
|
2
|
+
|
|
3
|
+
> **This directory is deprecated.** Public documentation has moved to `.context/public/guides/`.
|
|
4
|
+
|
|
5
|
+
## Moved files
|
|
6
|
+
|
|
7
|
+
- `ema-user-guide.md` → `.context/public/guides/ema-user-guide.md`
|
|
8
|
+
- `mcp-tools-guide.md` → `.context/public/guides/mcp-tools-guide.md`
|
|
9
|
+
- `dashboard-operations.md` → `.context/public/guides/dashboard-operations.md`
|
|
10
|
+
- `email-patterns.md` → `.context/public/guides/email-patterns.md`
|
|
11
|
+
|
|
12
|
+
## Future
|
|
13
|
+
|
|
14
|
+
This directory may be re-added as a symlink to internal docs once cutover is verified.
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# Go Validator Analysis
|
|
2
|
+
|
|
3
|
+
**Date**: 2026-01-27
|
|
4
|
+
**Source**: `workflow-engine/pkg/engine/validator.go`
|
|
5
|
+
**Purpose**: Detailed analysis of Go validator implementation
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Architecture Overview
|
|
10
|
+
|
|
11
|
+
### Components
|
|
12
|
+
|
|
13
|
+
1. **Validator Interface**: `Validator.Validate(ctx, workflow)`
|
|
14
|
+
2. **Local Validator**: Implements validator interface
|
|
15
|
+
3. **Path Enumeration**: `enumerateAndValidatePaths()` - Core algorithm
|
|
16
|
+
4. **Static Validation Strategy**: `StaticValidationStrategy` - Simulation strategy
|
|
17
|
+
5. **Validation Session**: `ValidationWorkflowSession` - Session state management
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Core Algorithm: Path Enumeration
|
|
22
|
+
|
|
23
|
+
### Algorithm Flow
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
1. Create initial validation session
|
|
27
|
+
└─> Dummy inputs, empty state
|
|
28
|
+
|
|
29
|
+
2. Queue initial session
|
|
30
|
+
└─> sessionsToProcess = [initialSession]
|
|
31
|
+
|
|
32
|
+
3. While queue not empty:
|
|
33
|
+
a. Dequeue session
|
|
34
|
+
b. Run validation on session
|
|
35
|
+
└─> session.RunValidation(ctx)
|
|
36
|
+
|
|
37
|
+
c. Handle result:
|
|
38
|
+
├─> OutcomeBranch:
|
|
39
|
+
│ └─> For each possible value:
|
|
40
|
+
│ ├─> Fork session
|
|
41
|
+
│ ├─> Plug chosen value
|
|
42
|
+
│ ├─> Mark action complete
|
|
43
|
+
│ └─> Queue forked session
|
|
44
|
+
│
|
|
45
|
+
└─> OutcomeCompleted:
|
|
46
|
+
├─> Track completed path
|
|
47
|
+
├─> Validate named results
|
|
48
|
+
├─> Apply custom validators
|
|
49
|
+
└─> Store in completedSessionMap
|
|
50
|
+
|
|
51
|
+
4. Cleanup incomplete sessions
|
|
52
|
+
5. Return completedSessionMap
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Key Data Structures
|
|
56
|
+
|
|
57
|
+
#### completedPathInfo
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
type completedPathInfo struct {
|
|
61
|
+
CompletedActions []string // Actions executed (topological order)
|
|
62
|
+
FinalOutputs map[string]*pb.Value // Final outputs produced
|
|
63
|
+
ActionName string // Path identifier
|
|
64
|
+
EnumerableOutput *string // Branching output name (if branched)
|
|
65
|
+
ChosenEnumerableValue *string // Chosen value (if branched)
|
|
66
|
+
NamedResultsValidationsFailed bool // Validation failure flag
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### BranchPointInfo
|
|
71
|
+
|
|
72
|
+
```go
|
|
73
|
+
type BranchPointInfo struct {
|
|
74
|
+
ActionThatBranched *ActionNode // The branching action
|
|
75
|
+
BranchingOutputName string // Output that branches
|
|
76
|
+
PossibleValues []*pb.Value // All possible values
|
|
77
|
+
ChosenValue *pb.Value // Value chosen for this fork
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Static Validation Strategy
|
|
84
|
+
|
|
85
|
+
### Purpose
|
|
86
|
+
|
|
87
|
+
Simulates workflow execution without actually running actions:
|
|
88
|
+
- Produces dummy outputs for non-branching actions
|
|
89
|
+
- Returns branching information for branching actions
|
|
90
|
+
- Never calls actual action code
|
|
91
|
+
|
|
92
|
+
### Key Methods
|
|
93
|
+
|
|
94
|
+
#### BindWorkflowInputs
|
|
95
|
+
|
|
96
|
+
- Generates type-aware dummy values for all workflow inputs
|
|
97
|
+
- Binds to input bindings
|
|
98
|
+
- Validates type compatibility
|
|
99
|
+
|
|
100
|
+
#### BindWidgetConfigs
|
|
101
|
+
|
|
102
|
+
- Generates type-aware dummy values for all widget configs
|
|
103
|
+
- Binds to widget config bindings
|
|
104
|
+
- Validates type compatibility
|
|
105
|
+
|
|
106
|
+
#### ExecuteAction
|
|
107
|
+
|
|
108
|
+
**For Branching Actions**:
|
|
109
|
+
- Returns `ActionExecutionResult{Outputs: nil, BranchPoint: &branchPoint}`
|
|
110
|
+
- BranchPoint contains: action, output name, possible values
|
|
111
|
+
|
|
112
|
+
**For Non-Branching Actions**:
|
|
113
|
+
- Generates type-aware dummy outputs for all outputs
|
|
114
|
+
- Marks action as completed
|
|
115
|
+
- Returns `ActionExecutionResult{Outputs: outputs}`
|
|
116
|
+
|
|
117
|
+
### Enumerable Output Detection
|
|
118
|
+
|
|
119
|
+
```go
|
|
120
|
+
func isOutputTypeEnumerable(outputDef *pb.ActionType_OutputDef) bool {
|
|
121
|
+
if outputDef.GetArgumentType().GetStatic() == nil {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
return outputDef.GetArgumentType().GetStatic().GetEnumType() != nil ||
|
|
125
|
+
outputDef.GetArgumentType().GetStatic().GetWellKnownType() == pb.WellKnownType_WELL_KNOWN_TYPE_BOOL
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Enumerable Types**:
|
|
130
|
+
- Enum types (categorizer categories)
|
|
131
|
+
- Boolean types (true/false)
|
|
132
|
+
|
|
133
|
+
**Non-Enumerable Types**:
|
|
134
|
+
- Strings, numbers, complex types
|
|
135
|
+
- Unknown types (dynamic)
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Path Forking Logic
|
|
140
|
+
|
|
141
|
+
### forkForBranching Function
|
|
142
|
+
|
|
143
|
+
```go
|
|
144
|
+
func forkForBranching(
|
|
145
|
+
session ValidationWorkflowSession,
|
|
146
|
+
branchInfo *BranchPointInfo,
|
|
147
|
+
branchValue *pb.Value,
|
|
148
|
+
) (ValidationWorkflowSession, error)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Steps**:
|
|
152
|
+
1. Deep copy session
|
|
153
|
+
2. Create fake outputs for branching action:
|
|
154
|
+
- For branching output: Use chosen value
|
|
155
|
+
- For other outputs: Generate type-aware dummy values
|
|
156
|
+
3. Merge fake outputs into session
|
|
157
|
+
4. Mark branching action as completed
|
|
158
|
+
5. Update last encountered branching action in session
|
|
159
|
+
6. Return forked session
|
|
160
|
+
|
|
161
|
+
### Session State Management
|
|
162
|
+
|
|
163
|
+
- Each forked session is independent
|
|
164
|
+
- State includes: completed actions, available outputs, action states
|
|
165
|
+
- Forking preserves parent state, then modifies for chosen branch
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Named Results Validation
|
|
170
|
+
|
|
171
|
+
### Validation Points
|
|
172
|
+
|
|
173
|
+
1. **Required Output Check**: All named results produced on all paths
|
|
174
|
+
2. **Multiple Writers Check**: No duplicate producers on same path
|
|
175
|
+
3. **Response/Abstain Check**: Every path has response or abstain
|
|
176
|
+
4. **Category Hierarchy Check**: Categorizer structure valid
|
|
177
|
+
|
|
178
|
+
### Where Validation Happens
|
|
179
|
+
|
|
180
|
+
Validation occurs in `local_executor.go` during path completion:
|
|
181
|
+
- `AddNamedResultError()` called when validation fails
|
|
182
|
+
- Errors stored in workflow graph
|
|
183
|
+
- Workflow marked as non-runnable if errors exist
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Error Reporting
|
|
188
|
+
|
|
189
|
+
### Error Storage
|
|
190
|
+
|
|
191
|
+
Errors stored in workflow graph:
|
|
192
|
+
```go
|
|
193
|
+
workflow.AddNamedResultError(errorType, namedResultName, pathIdentifier, message)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Error Structure
|
|
197
|
+
|
|
198
|
+
```go
|
|
199
|
+
type WorkflowError_NamedResultError struct {
|
|
200
|
+
ErrorType pb.WorkflowError_ResultErrorType
|
|
201
|
+
NamedResultName string
|
|
202
|
+
PathIdentifier *WorkflowError_ActionOutput
|
|
203
|
+
Message string
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Path Identifier Format
|
|
208
|
+
|
|
209
|
+
```go
|
|
210
|
+
type WorkflowError_ActionOutput struct {
|
|
211
|
+
ActionName string // Always present
|
|
212
|
+
OutputName string // Only for branching paths
|
|
213
|
+
OutputValue string // Only for branching paths
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Custom Validators
|
|
220
|
+
|
|
221
|
+
### Interface
|
|
222
|
+
|
|
223
|
+
```go
|
|
224
|
+
type CustomNamedResultValidator interface {
|
|
225
|
+
Validate(workflow *WorkflowGraph, pathInfo *completedPathInfo) error
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Implementation Example
|
|
230
|
+
|
|
231
|
+
`named_result_agent_assist_validator.go`:
|
|
232
|
+
- Validates Agent Assist persona-specific rules
|
|
233
|
+
- Checks category hierarchy
|
|
234
|
+
- Checks response/abstain for Agent Assist patterns
|
|
235
|
+
|
|
236
|
+
### Integration
|
|
237
|
+
|
|
238
|
+
- Retrieved via `GetCustomValidatorForPersonaType()`
|
|
239
|
+
- Called after general validations pass
|
|
240
|
+
- Can add persona-specific errors
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Performance Characteristics
|
|
245
|
+
|
|
246
|
+
### Complexity
|
|
247
|
+
|
|
248
|
+
- **Time**: O(P * A) where P = paths, A = actions per path
|
|
249
|
+
- **Space**: O(P * A) for session storage
|
|
250
|
+
|
|
251
|
+
### Optimization Opportunities
|
|
252
|
+
|
|
253
|
+
1. **Early Pruning**: Stop if path already invalid
|
|
254
|
+
2. **Path Caching**: Cache results for unchanged workflows
|
|
255
|
+
3. **Parallel Enumeration**: Enumerate paths in parallel (future)
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Test Coverage
|
|
260
|
+
|
|
261
|
+
### Test Files
|
|
262
|
+
|
|
263
|
+
- `local_executor_static_validation_test.go` - Core validation tests
|
|
264
|
+
- `local_executor_named_outputs_test.go` - Named results tests
|
|
265
|
+
- `local_enumerator_test.go` - Path enumeration tests
|
|
266
|
+
|
|
267
|
+
### Test Patterns
|
|
268
|
+
|
|
269
|
+
1. **Simple workflows**: Linear, no branching
|
|
270
|
+
2. **Categorizer workflows**: Single categorizer, multiple branches
|
|
271
|
+
3. **Nested branching**: Multiple categorizers in sequence
|
|
272
|
+
4. **Error cases**: Missing outputs, multiple writers, etc.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Key Insights
|
|
277
|
+
|
|
278
|
+
### 1. Dummy Value Generation
|
|
279
|
+
|
|
280
|
+
- All dummy values are **type-aware**
|
|
281
|
+
- Uses `util.CreateTypeAwareDummyValue()` for type compatibility
|
|
282
|
+
- Preserves type information for validation
|
|
283
|
+
|
|
284
|
+
### 2. Branching Detection
|
|
285
|
+
|
|
286
|
+
- Only enum and boolean outputs are considered enumerable
|
|
287
|
+
- Other types cannot be used for path enumeration
|
|
288
|
+
- Unknown types default to non-branching
|
|
289
|
+
|
|
290
|
+
### 3. Path Identification
|
|
291
|
+
|
|
292
|
+
- Paths are identified by **last branching action + value** OR **last action**
|
|
293
|
+
- This allows precise error reporting
|
|
294
|
+
- Path identifier used in error messages
|
|
295
|
+
|
|
296
|
+
### 4. Session Management
|
|
297
|
+
|
|
298
|
+
- Sessions are deep copied when forking
|
|
299
|
+
- Each session maintains independent state
|
|
300
|
+
- Completed sessions stored in map for analysis
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Implementation Notes for TypeScript
|
|
305
|
+
|
|
306
|
+
### Must Implement
|
|
307
|
+
|
|
308
|
+
1. **Path Enumeration**: Topological sort + DFS + forking
|
|
309
|
+
2. **Dummy Value Generation**: Type-aware dummy values
|
|
310
|
+
3. **Branching Detection**: Enum and boolean outputs
|
|
311
|
+
4. **Session Management**: Deep copying, state tracking
|
|
312
|
+
5. **Error Reporting**: Same error types and path identifiers
|
|
313
|
+
|
|
314
|
+
### Can Enhance
|
|
315
|
+
|
|
316
|
+
1. **Error Messages**: Add what/why/how format
|
|
317
|
+
2. **Performance**: Add caching, early pruning
|
|
318
|
+
3. **Additional Validations**: Add more checks (document as extensions)
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
**Status**: ✅ Analysis complete
|
|
323
|
+
**Next**: Use this analysis to implement TypeScript validator
|