@exaudeus/workrail 0.0.20 → 0.1.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.
- package/README.md +17 -1
- package/dist/application/services/context-optimizer.d.ts +11 -0
- package/dist/application/services/context-optimizer.js +62 -0
- package/dist/application/services/loop-execution-context.d.ts +18 -0
- package/dist/application/services/loop-execution-context.js +127 -0
- package/dist/application/services/loop-step-resolver.d.ts +11 -0
- package/dist/application/services/loop-step-resolver.js +70 -0
- package/dist/application/services/validation-engine.d.ts +4 -0
- package/dist/application/services/validation-engine.js +171 -0
- package/dist/application/services/workflow-service.d.ts +7 -0
- package/dist/application/services/workflow-service.js +184 -12
- package/dist/cli/migrate-workflow.d.ts +22 -0
- package/dist/cli/migrate-workflow.js +195 -0
- package/dist/cli.js +9 -0
- package/dist/types/workflow-types.d.ts +40 -1
- package/dist/types/workflow-types.js +4 -0
- package/dist/utils/config.d.ts +2 -2
- package/dist/utils/context-size.d.ts +12 -0
- package/dist/utils/context-size.js +80 -0
- package/package.json +2 -1
- package/spec/examples/loop-test.json +31 -0
- package/spec/workflow.schema.json +282 -176
- package/spec/workflow.schema.v0.0.1.json +393 -0
- package/workflows/coding-task-workflow-with-loops.json +434 -0
- package/workflows/examples/loops/README.md +71 -0
- package/workflows/examples/loops/simple-batch.json +54 -0
- package/workflows/examples/loops/simple-polling.json +47 -0
- package/workflows/examples/loops/simple-retry.json +45 -0
- package/workflows/examples/loops/simple-search.json +50 -0
package/README.md
CHANGED
|
@@ -16,6 +16,15 @@ It follows [Model Context Protocol (MCP)](https://modelcontextprotocol.org) conv
|
|
|
16
16
|
|
|
17
17
|
The codebase now implements the full MVP described in the original specification, refactored with Clean Architecture for long-term maintainability.
|
|
18
18
|
|
|
19
|
+
### ✨ New in v0.1.0: Loop Support
|
|
20
|
+
WorkRail now supports powerful iteration patterns with four loop types:
|
|
21
|
+
- **while**: Continue while a condition is true
|
|
22
|
+
- **until**: Continue until a condition is met
|
|
23
|
+
- **for**: Execute a fixed number of times
|
|
24
|
+
- **forEach**: Process items in an array
|
|
25
|
+
|
|
26
|
+
See the [Loop Documentation](docs/features/loops.md) for details.
|
|
27
|
+
|
|
19
28
|
---
|
|
20
29
|
|
|
21
30
|
## 🚀 Quick Start
|
|
@@ -58,11 +67,18 @@ workrail start
|
|
|
58
67
|
- `workflow_next` - Get the next step in a workflow
|
|
59
68
|
- `workflow_validate` - Advanced validation of step outputs with schema, context-aware, and composition rules
|
|
60
69
|
- `workflow_validate_json` - Direct JSON workflow validation with comprehensive error reporting and actionable suggestions
|
|
70
|
+
* **Loop Support (v0.1.0)** – Four loop types for powerful iteration patterns:
|
|
71
|
+
- `while` loops - Continue while a condition is true
|
|
72
|
+
- `until` loops - Continue until a condition is met
|
|
73
|
+
- `for` loops - Execute a fixed number of times
|
|
74
|
+
- `forEach` loops - Process items in an array
|
|
61
75
|
* **Dependency Injection** – pluggable components are wired by `src/container.ts` (Inversify-style, no runtime reflection).
|
|
62
76
|
* **Async, Secure Storage** – interchangeable back-ends: in-memory (default for tests) and file-based storage with path-traversal safeguards.
|
|
63
77
|
* **Advanced ValidationEngine** – Three-tier validation system with JSON Schema validation (AJV), Context-Aware Validation (conditional rules), and Logical Composition (and/or/not operators) for comprehensive step output quality assurance.
|
|
64
78
|
* **Typed Error Mapping** – domain errors (`WorkflowNotFoundError`, `ValidationError`, …) automatically translate to proper JSON-RPC codes.
|
|
65
|
-
* **CLI
|
|
79
|
+
* **CLI Tools** –
|
|
80
|
+
- `validate` - Test workflow files locally with comprehensive error reporting
|
|
81
|
+
- `migrate` - Automatically migrate workflows from v0.0.1 to v0.1.0
|
|
66
82
|
* **Comprehensive Test Coverage** – 81 tests passing, 7 failing (performance optimizations in progress), 88 total tests covering storage, validation, error mapping, CLI, and server logic.
|
|
67
83
|
|
|
68
84
|
---
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { EnhancedContext } from '../../types/workflow-types';
|
|
2
|
+
type ConditionContext = Record<string, any>;
|
|
3
|
+
export declare class ContextOptimizer {
|
|
4
|
+
static createEnhancedContext(base: ConditionContext, enhancements: Partial<EnhancedContext>): EnhancedContext;
|
|
5
|
+
static mergeLoopState(context: EnhancedContext, loopId: string, loopState: any): EnhancedContext;
|
|
6
|
+
static addWarnings(context: EnhancedContext, category: string, key: string, warnings: string[]): EnhancedContext;
|
|
7
|
+
static hasProperty(context: ConditionContext, property: string): boolean;
|
|
8
|
+
static getProperty(context: ConditionContext, property: string): any;
|
|
9
|
+
static getOwnPropertiesSize(context: ConditionContext): number;
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContextOptimizer = void 0;
|
|
4
|
+
class ContextOptimizer {
|
|
5
|
+
static createEnhancedContext(base, enhancements) {
|
|
6
|
+
const hasOverlap = Object.keys(enhancements).some(key => key in base);
|
|
7
|
+
if (!hasOverlap) {
|
|
8
|
+
return Object.assign({}, base, enhancements);
|
|
9
|
+
}
|
|
10
|
+
return { ...base, ...enhancements };
|
|
11
|
+
}
|
|
12
|
+
static mergeLoopState(context, loopId, loopState) {
|
|
13
|
+
if (!context._loopState) {
|
|
14
|
+
return ContextOptimizer.createEnhancedContext(context, {
|
|
15
|
+
_loopState: { [loopId]: loopState }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (context._loopState[loopId] === loopState) {
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
21
|
+
return ContextOptimizer.createEnhancedContext(context, {
|
|
22
|
+
_loopState: { ...context._loopState, [loopId]: loopState }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
static addWarnings(context, category, key, warnings) {
|
|
26
|
+
if (warnings.length === 0) {
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
const existingWarnings = context._warnings || {};
|
|
30
|
+
const categoryWarnings = existingWarnings[category] || {};
|
|
31
|
+
return ContextOptimizer.createEnhancedContext(context, {
|
|
32
|
+
_warnings: {
|
|
33
|
+
...existingWarnings,
|
|
34
|
+
[category]: {
|
|
35
|
+
...categoryWarnings,
|
|
36
|
+
[key]: warnings
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
static hasProperty(context, property) {
|
|
42
|
+
return property in context;
|
|
43
|
+
}
|
|
44
|
+
static getProperty(context, property) {
|
|
45
|
+
return context[property];
|
|
46
|
+
}
|
|
47
|
+
static getOwnPropertiesSize(context) {
|
|
48
|
+
const ownProps = Object.getOwnPropertyNames(context);
|
|
49
|
+
let size = 0;
|
|
50
|
+
for (const prop of ownProps) {
|
|
51
|
+
const value = context[prop];
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
size += value.length;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
size += JSON.stringify(value).length;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return size;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.ContextOptimizer = ContextOptimizer;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LoopConfig, LoopState, EnhancedContext } from '../../types/workflow-types';
|
|
2
|
+
import { ConditionContext } from '../../utils/condition-evaluator';
|
|
3
|
+
export declare class LoopExecutionContext {
|
|
4
|
+
private loopId;
|
|
5
|
+
private loopConfig;
|
|
6
|
+
private state;
|
|
7
|
+
private readonly maxExecutionTime;
|
|
8
|
+
constructor(loopId: string, loopConfig: LoopConfig, existingState?: LoopState[string]);
|
|
9
|
+
incrementIteration(): void;
|
|
10
|
+
getCurrentState(): LoopState[string];
|
|
11
|
+
shouldContinue(context: ConditionContext): boolean;
|
|
12
|
+
initializeForEach(context: ConditionContext): void;
|
|
13
|
+
injectVariables(context: ConditionContext): EnhancedContext;
|
|
14
|
+
private resolveCount;
|
|
15
|
+
private addWarning;
|
|
16
|
+
getLoopId(): string;
|
|
17
|
+
getLoopConfig(): LoopConfig;
|
|
18
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoopExecutionContext = void 0;
|
|
4
|
+
const condition_evaluator_1 = require("../../utils/condition-evaluator");
|
|
5
|
+
class LoopExecutionContext {
|
|
6
|
+
constructor(loopId, loopConfig, existingState) {
|
|
7
|
+
this.maxExecutionTime = 5 * 60 * 1000;
|
|
8
|
+
this.loopId = loopId;
|
|
9
|
+
this.loopConfig = loopConfig;
|
|
10
|
+
this.state = existingState || {
|
|
11
|
+
iteration: 0,
|
|
12
|
+
started: Date.now(),
|
|
13
|
+
warnings: []
|
|
14
|
+
};
|
|
15
|
+
if (loopConfig.type === 'forEach' && loopConfig.items && this.state.index === undefined) {
|
|
16
|
+
this.state.index = 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
incrementIteration() {
|
|
20
|
+
this.state.iteration++;
|
|
21
|
+
if (this.loopConfig.type === 'forEach' && typeof this.state.index === 'number') {
|
|
22
|
+
this.state.index++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
getCurrentState() {
|
|
26
|
+
return { ...this.state };
|
|
27
|
+
}
|
|
28
|
+
shouldContinue(context) {
|
|
29
|
+
if (this.state.iteration >= this.loopConfig.maxIterations) {
|
|
30
|
+
this.addWarning(`Maximum iterations (${this.loopConfig.maxIterations}) reached`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const executionTime = Date.now() - this.state.started;
|
|
34
|
+
if (executionTime > this.maxExecutionTime) {
|
|
35
|
+
this.addWarning(`Maximum execution time (${this.maxExecutionTime / 1000}s) exceeded`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
switch (this.loopConfig.type) {
|
|
39
|
+
case 'while':
|
|
40
|
+
return this.loopConfig.condition
|
|
41
|
+
? (0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
|
|
42
|
+
: false;
|
|
43
|
+
case 'until':
|
|
44
|
+
return this.loopConfig.condition
|
|
45
|
+
? !(0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
|
|
46
|
+
: false;
|
|
47
|
+
case 'for':
|
|
48
|
+
const count = this.resolveCount(context);
|
|
49
|
+
return this.state.iteration < count;
|
|
50
|
+
case 'forEach':
|
|
51
|
+
return this.state.items
|
|
52
|
+
? (this.state.index || 0) < this.state.items.length
|
|
53
|
+
: false;
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
initializeForEach(context) {
|
|
59
|
+
if (this.loopConfig.type === 'forEach' && this.loopConfig.items) {
|
|
60
|
+
const items = context[this.loopConfig.items];
|
|
61
|
+
if (Array.isArray(items)) {
|
|
62
|
+
this.state.items = items;
|
|
63
|
+
this.state.index = 0;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.addWarning(`Expected array for forEach items '${this.loopConfig.items}', got ${typeof items}`);
|
|
67
|
+
this.state.items = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
injectVariables(context) {
|
|
72
|
+
const enhancements = {
|
|
73
|
+
_loopState: {
|
|
74
|
+
...context._loopState,
|
|
75
|
+
[this.loopId]: this.getCurrentState()
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const iterationVar = this.loopConfig.iterationVar || 'currentIteration';
|
|
79
|
+
enhancements[iterationVar] = this.state.iteration + 1;
|
|
80
|
+
if (this.loopConfig.type === 'forEach' && this.state.items) {
|
|
81
|
+
const index = this.state.index || 0;
|
|
82
|
+
const itemVar = this.loopConfig.itemVar || 'currentItem';
|
|
83
|
+
enhancements[itemVar] = this.state.items[index];
|
|
84
|
+
const indexVar = this.loopConfig.indexVar || 'currentIndex';
|
|
85
|
+
enhancements[indexVar] = index;
|
|
86
|
+
}
|
|
87
|
+
if (this.state.warnings && this.state.warnings.length > 0) {
|
|
88
|
+
const existingWarnings = context._warnings || {};
|
|
89
|
+
const existingLoopWarnings = existingWarnings.loops || {};
|
|
90
|
+
enhancements._warnings = {
|
|
91
|
+
...existingWarnings,
|
|
92
|
+
loops: {
|
|
93
|
+
...existingLoopWarnings,
|
|
94
|
+
[this.loopId]: [...this.state.warnings]
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { ...context, ...enhancements };
|
|
99
|
+
}
|
|
100
|
+
resolveCount(context) {
|
|
101
|
+
if (this.loopConfig.type !== 'for' || !this.loopConfig.count) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
if (typeof this.loopConfig.count === 'number') {
|
|
105
|
+
return this.loopConfig.count;
|
|
106
|
+
}
|
|
107
|
+
const count = context[this.loopConfig.count];
|
|
108
|
+
if (typeof count === 'number') {
|
|
109
|
+
return count;
|
|
110
|
+
}
|
|
111
|
+
this.addWarning(`Invalid count value for 'for' loop: ${this.loopConfig.count}`);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
addWarning(message) {
|
|
115
|
+
if (!this.state.warnings) {
|
|
116
|
+
this.state.warnings = [];
|
|
117
|
+
}
|
|
118
|
+
this.state.warnings.push(message);
|
|
119
|
+
}
|
|
120
|
+
getLoopId() {
|
|
121
|
+
return this.loopId;
|
|
122
|
+
}
|
|
123
|
+
getLoopConfig() {
|
|
124
|
+
return { ...this.loopConfig };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.LoopExecutionContext = LoopExecutionContext;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Workflow, WorkflowStep } from '../../types/mcp-types';
|
|
2
|
+
export declare class LoopStepResolver {
|
|
3
|
+
private resolvedStepsCache;
|
|
4
|
+
resolveLoopBody(workflow: Workflow, body: string | WorkflowStep[], currentLoopId?: string): WorkflowStep | WorkflowStep[];
|
|
5
|
+
validateStepReference(workflow: Workflow, stepId: string): boolean;
|
|
6
|
+
findAllLoopReferences(workflow: Workflow): string[];
|
|
7
|
+
validateAllReferences(workflow: Workflow): void;
|
|
8
|
+
clearCache(): void;
|
|
9
|
+
getCacheSize(): number;
|
|
10
|
+
private findStepById;
|
|
11
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoopStepResolver = void 0;
|
|
4
|
+
const workflow_types_1 = require("../../types/workflow-types");
|
|
5
|
+
const error_handler_1 = require("../../core/error-handler");
|
|
6
|
+
class LoopStepResolver {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.resolvedStepsCache = new Map();
|
|
9
|
+
}
|
|
10
|
+
resolveLoopBody(workflow, body, currentLoopId) {
|
|
11
|
+
if (Array.isArray(body)) {
|
|
12
|
+
return body;
|
|
13
|
+
}
|
|
14
|
+
const cacheKey = `${workflow.id}:${body}`;
|
|
15
|
+
if (this.resolvedStepsCache.has(cacheKey)) {
|
|
16
|
+
return this.resolvedStepsCache.get(cacheKey);
|
|
17
|
+
}
|
|
18
|
+
const referencedStep = this.findStepById(workflow, body);
|
|
19
|
+
if (!referencedStep) {
|
|
20
|
+
throw new error_handler_1.StepNotFoundError(workflow.id, body);
|
|
21
|
+
}
|
|
22
|
+
if (currentLoopId && body === currentLoopId) {
|
|
23
|
+
throw new Error(`Circular reference detected: loop step '${body}' references itself`);
|
|
24
|
+
}
|
|
25
|
+
this.resolvedStepsCache.set(cacheKey, referencedStep);
|
|
26
|
+
return referencedStep;
|
|
27
|
+
}
|
|
28
|
+
validateStepReference(workflow, stepId) {
|
|
29
|
+
return this.findStepById(workflow, stepId) !== null;
|
|
30
|
+
}
|
|
31
|
+
findAllLoopReferences(workflow) {
|
|
32
|
+
const references = [];
|
|
33
|
+
for (const step of workflow.steps) {
|
|
34
|
+
if ((0, workflow_types_1.isLoopStep)(step)) {
|
|
35
|
+
const loopStep = step;
|
|
36
|
+
if (typeof loopStep.body === 'string') {
|
|
37
|
+
references.push(loopStep.body);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return references;
|
|
42
|
+
}
|
|
43
|
+
validateAllReferences(workflow) {
|
|
44
|
+
const references = this.findAllLoopReferences(workflow);
|
|
45
|
+
const stepIds = new Set(workflow.steps.map(s => s.id));
|
|
46
|
+
for (const ref of references) {
|
|
47
|
+
if (!stepIds.has(ref)) {
|
|
48
|
+
throw new error_handler_1.StepNotFoundError(workflow.id, ref);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const step of workflow.steps) {
|
|
52
|
+
if ((0, workflow_types_1.isLoopStep)(step)) {
|
|
53
|
+
const loopStep = step;
|
|
54
|
+
if (typeof loopStep.body === 'string' && loopStep.body === loopStep.id) {
|
|
55
|
+
throw new Error(`Circular reference detected: loop step '${loopStep.id}' references itself`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
clearCache() {
|
|
61
|
+
this.resolvedStepsCache.clear();
|
|
62
|
+
}
|
|
63
|
+
getCacheSize() {
|
|
64
|
+
return this.resolvedStepsCache.size;
|
|
65
|
+
}
|
|
66
|
+
findStepById(workflow, stepId) {
|
|
67
|
+
return workflow.steps.find(s => s.id === stepId) || null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.LoopStepResolver = LoopStepResolver;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Condition, ConditionContext } from '../../utils/condition-evaluator';
|
|
2
|
+
import { LoopStep, Workflow } from '../../types/workflow-types';
|
|
2
3
|
export interface ValidationRule {
|
|
3
4
|
type: 'contains' | 'regex' | 'length' | 'schema';
|
|
4
5
|
message: string;
|
|
@@ -34,4 +35,7 @@ export declare class ValidationEngine {
|
|
|
34
35
|
private isValidationComposition;
|
|
35
36
|
validate(output: string, criteria: ValidationRule[] | ValidationComposition, context?: ConditionContext): Promise<ValidationResult>;
|
|
36
37
|
private evaluateRule;
|
|
38
|
+
validateLoopStep(step: LoopStep, workflow: Workflow): ValidationResult;
|
|
39
|
+
validateWorkflow(workflow: Workflow): ValidationResult;
|
|
40
|
+
private isValidVariableName;
|
|
37
41
|
}
|
|
@@ -7,6 +7,7 @@ exports.ValidationEngine = void 0;
|
|
|
7
7
|
const error_handler_1 = require("../../core/error-handler");
|
|
8
8
|
const condition_evaluator_1 = require("../../utils/condition-evaluator");
|
|
9
9
|
const ajv_1 = __importDefault(require("ajv"));
|
|
10
|
+
const workflow_types_1 = require("../../types/workflow-types");
|
|
10
11
|
class ValidationEngine {
|
|
11
12
|
constructor() {
|
|
12
13
|
this.schemaCache = new Map();
|
|
@@ -203,5 +204,175 @@ class ValidationEngine {
|
|
|
203
204
|
}
|
|
204
205
|
throw new error_handler_1.ValidationError('Invalid validationCriteria format.');
|
|
205
206
|
}
|
|
207
|
+
validateLoopStep(step, workflow) {
|
|
208
|
+
const issues = [];
|
|
209
|
+
const suggestions = [];
|
|
210
|
+
const validTypes = ['while', 'until', 'for', 'forEach'];
|
|
211
|
+
if (!validTypes.includes(step.loop.type)) {
|
|
212
|
+
issues.push(`Invalid loop type '${step.loop.type}'. Must be one of: ${validTypes.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
if (typeof step.loop.maxIterations !== 'number' || step.loop.maxIterations <= 0) {
|
|
215
|
+
issues.push(`maxIterations must be a positive number`);
|
|
216
|
+
suggestions.push('Set maxIterations to a reasonable limit (e.g., 100) to prevent infinite loops');
|
|
217
|
+
}
|
|
218
|
+
else if (step.loop.maxIterations > 1000) {
|
|
219
|
+
issues.push(`maxIterations (${step.loop.maxIterations}) exceeds safety limit of 1000`);
|
|
220
|
+
suggestions.push('Consider reducing maxIterations or breaking the loop into smaller chunks');
|
|
221
|
+
}
|
|
222
|
+
switch (step.loop.type) {
|
|
223
|
+
case 'while':
|
|
224
|
+
case 'until':
|
|
225
|
+
if (!step.loop.condition) {
|
|
226
|
+
issues.push(`${step.loop.type} loop requires a condition`);
|
|
227
|
+
suggestions.push(`Add a condition that evaluates to false (for while) or true (for until) to exit the loop`);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case 'for':
|
|
231
|
+
if (step.loop.count === undefined) {
|
|
232
|
+
issues.push(`for loop requires a count`);
|
|
233
|
+
suggestions.push('Set count to a number or context variable name');
|
|
234
|
+
}
|
|
235
|
+
else if (typeof step.loop.count === 'string') {
|
|
236
|
+
}
|
|
237
|
+
else if (typeof step.loop.count !== 'number' || step.loop.count <= 0) {
|
|
238
|
+
issues.push(`for loop count must be a positive number or context variable name`);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
case 'forEach':
|
|
242
|
+
if (!step.loop.items) {
|
|
243
|
+
issues.push(`forEach loop requires items`);
|
|
244
|
+
suggestions.push('Set items to a context variable name containing an array');
|
|
245
|
+
}
|
|
246
|
+
else if (typeof step.loop.items !== 'string') {
|
|
247
|
+
issues.push(`forEach loop items must be a context variable name`);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (!step.body) {
|
|
252
|
+
issues.push(`Loop step must have a body`);
|
|
253
|
+
suggestions.push('Set body to a step ID or array of step IDs');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
if (typeof step.body === 'string') {
|
|
257
|
+
const bodyStep = workflow.steps.find(s => s.id === step.body);
|
|
258
|
+
if (!bodyStep) {
|
|
259
|
+
issues.push(`Loop body references non-existent step '${step.body}'`);
|
|
260
|
+
suggestions.push(`Create a step with ID '${step.body}' or update the body reference`);
|
|
261
|
+
}
|
|
262
|
+
else if ((0, workflow_types_1.isLoopStep)(bodyStep)) {
|
|
263
|
+
issues.push(`Nested loops are not currently supported. Step '${step.body}' is a loop`);
|
|
264
|
+
suggestions.push('Refactor to avoid nested loops or use sequential loops');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (Array.isArray(step.body)) {
|
|
268
|
+
if (step.body.length === 0) {
|
|
269
|
+
issues.push(`Loop body array cannot be empty`);
|
|
270
|
+
suggestions.push('Add at least one step to the loop body');
|
|
271
|
+
}
|
|
272
|
+
const stepIds = new Set();
|
|
273
|
+
for (const inlineStep of step.body) {
|
|
274
|
+
if (!inlineStep.id) {
|
|
275
|
+
issues.push(`Inline step in loop body must have an ID`);
|
|
276
|
+
suggestions.push('Add an ID to all inline steps');
|
|
277
|
+
}
|
|
278
|
+
else if (stepIds.has(inlineStep.id)) {
|
|
279
|
+
issues.push(`Duplicate step ID '${inlineStep.id}' in loop body`);
|
|
280
|
+
suggestions.push('Use unique IDs for each inline step');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
stepIds.add(inlineStep.id);
|
|
284
|
+
}
|
|
285
|
+
if (!inlineStep.title) {
|
|
286
|
+
issues.push(`Inline step '${inlineStep.id || 'unknown'}' must have a title`);
|
|
287
|
+
suggestions.push('Add a title to all inline steps');
|
|
288
|
+
}
|
|
289
|
+
if (!inlineStep.prompt) {
|
|
290
|
+
issues.push(`Inline step '${inlineStep.id || 'unknown'}' must have a prompt`);
|
|
291
|
+
suggestions.push('Add a prompt to all inline steps');
|
|
292
|
+
}
|
|
293
|
+
if ((0, workflow_types_1.isLoopStep)(inlineStep)) {
|
|
294
|
+
issues.push(`Nested loops are not currently supported. Inline step '${inlineStep.id}' is a loop`);
|
|
295
|
+
suggestions.push('Refactor to avoid nested loops');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (step.loop.iterationVar && !this.isValidVariableName(step.loop.iterationVar)) {
|
|
301
|
+
issues.push(`Invalid iteration variable name '${step.loop.iterationVar}'`);
|
|
302
|
+
suggestions.push('Use a valid JavaScript variable name (alphanumeric, _, $)');
|
|
303
|
+
}
|
|
304
|
+
if (step.loop.itemVar && !this.isValidVariableName(step.loop.itemVar)) {
|
|
305
|
+
issues.push(`Invalid item variable name '${step.loop.itemVar}'`);
|
|
306
|
+
suggestions.push('Use a valid JavaScript variable name');
|
|
307
|
+
}
|
|
308
|
+
if (step.loop.indexVar && !this.isValidVariableName(step.loop.indexVar)) {
|
|
309
|
+
issues.push(`Invalid index variable name '${step.loop.indexVar}'`);
|
|
310
|
+
suggestions.push('Use a valid JavaScript variable name');
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
valid: issues.length === 0,
|
|
314
|
+
issues,
|
|
315
|
+
suggestions
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
validateWorkflow(workflow) {
|
|
319
|
+
const issues = [];
|
|
320
|
+
const suggestions = [];
|
|
321
|
+
const stepIds = new Set();
|
|
322
|
+
for (const step of workflow.steps) {
|
|
323
|
+
if (stepIds.has(step.id)) {
|
|
324
|
+
issues.push(`Duplicate step ID '${step.id}'`);
|
|
325
|
+
suggestions.push('Ensure all step IDs are unique');
|
|
326
|
+
}
|
|
327
|
+
stepIds.add(step.id);
|
|
328
|
+
}
|
|
329
|
+
for (const step of workflow.steps) {
|
|
330
|
+
if ((0, workflow_types_1.isLoopStep)(step)) {
|
|
331
|
+
const loopResult = this.validateLoopStep(step, workflow);
|
|
332
|
+
issues.push(...loopResult.issues.map(issue => `Step '${step.id}': ${issue}`));
|
|
333
|
+
suggestions.push(...loopResult.suggestions);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
if (!step.id) {
|
|
337
|
+
issues.push('Step missing required ID');
|
|
338
|
+
}
|
|
339
|
+
if (!step.title) {
|
|
340
|
+
issues.push(`Step '${step.id}' missing required title`);
|
|
341
|
+
}
|
|
342
|
+
if (!step.prompt) {
|
|
343
|
+
issues.push(`Step '${step.id}' missing required prompt`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const loopBodySteps = new Set();
|
|
348
|
+
for (const step of workflow.steps) {
|
|
349
|
+
if ((0, workflow_types_1.isLoopStep)(step)) {
|
|
350
|
+
if (typeof step.body === 'string') {
|
|
351
|
+
loopBodySteps.add(step.body);
|
|
352
|
+
}
|
|
353
|
+
else if (Array.isArray(step.body)) {
|
|
354
|
+
step.body.forEach(id => {
|
|
355
|
+
if (typeof id === 'string') {
|
|
356
|
+
loopBodySteps.add(id);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const step of workflow.steps) {
|
|
363
|
+
if (loopBodySteps.has(step.id) && step.runCondition) {
|
|
364
|
+
issues.push(`Step '${step.id}' is a loop body but has runCondition - this may cause conflicts`);
|
|
365
|
+
suggestions.push('Remove runCondition from loop body steps as they are controlled by the loop');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
valid: issues.length === 0,
|
|
370
|
+
issues,
|
|
371
|
+
suggestions
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
isValidVariableName(name) {
|
|
375
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
376
|
+
}
|
|
206
377
|
}
|
|
207
378
|
exports.ValidationEngine = ValidationEngine;
|
|
@@ -5,6 +5,7 @@ export interface WorkflowService {
|
|
|
5
5
|
step: import('../../types/mcp-types').WorkflowStep | null;
|
|
6
6
|
guidance: import('../../types/mcp-types').WorkflowGuidance;
|
|
7
7
|
isComplete: boolean;
|
|
8
|
+
context?: ConditionContext;
|
|
8
9
|
}>;
|
|
9
10
|
validateStepOutput(workflowId: string, stepId: string, output: string): Promise<{
|
|
10
11
|
valid: boolean;
|
|
@@ -16,9 +17,11 @@ import { Workflow, WorkflowSummary, WorkflowStep, WorkflowGuidance } from '../..
|
|
|
16
17
|
import { IWorkflowStorage } from '../../types/storage';
|
|
17
18
|
import { ConditionContext } from '../../utils/condition-evaluator';
|
|
18
19
|
import { ValidationEngine } from './validation-engine';
|
|
20
|
+
import { EnhancedContext } from '../../types/workflow-types';
|
|
19
21
|
export declare class DefaultWorkflowService implements WorkflowService {
|
|
20
22
|
private readonly storage;
|
|
21
23
|
private readonly validationEngine;
|
|
24
|
+
private loopStepResolver;
|
|
22
25
|
constructor(storage?: IWorkflowStorage, validationEngine?: ValidationEngine);
|
|
23
26
|
listWorkflowSummaries(): Promise<WorkflowSummary[]>;
|
|
24
27
|
getWorkflowById(id: string): Promise<Workflow | null>;
|
|
@@ -26,11 +29,15 @@ export declare class DefaultWorkflowService implements WorkflowService {
|
|
|
26
29
|
step: WorkflowStep | null;
|
|
27
30
|
guidance: WorkflowGuidance;
|
|
28
31
|
isComplete: boolean;
|
|
32
|
+
context?: ConditionContext;
|
|
29
33
|
}>;
|
|
34
|
+
private buildStepPrompt;
|
|
35
|
+
private findLoopStepById;
|
|
30
36
|
validateStepOutput(workflowId: string, stepId: string, output: string): Promise<{
|
|
31
37
|
valid: boolean;
|
|
32
38
|
issues: string[];
|
|
33
39
|
suggestions: string[];
|
|
34
40
|
}>;
|
|
41
|
+
updateContextForStepCompletion(workflowId: string, stepId: string, context: ConditionContext): Promise<EnhancedContext>;
|
|
35
42
|
}
|
|
36
43
|
export declare const defaultWorkflowService: WorkflowService;
|