@exaudeus/workrail 0.1.6 → 0.1.7
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 +10 -1
- package/dist/application/services/loop-context-optimizer.d.ts +8 -0
- package/dist/application/services/loop-context-optimizer.js +114 -0
- package/dist/application/services/loop-execution-context.d.ts +7 -2
- package/dist/application/services/loop-execution-context.js +85 -24
- package/dist/application/services/workflow-service.d.ts +3 -1
- package/dist/application/services/workflow-service.js +43 -12
- package/dist/container.d.ts +2 -0
- package/dist/container.js +4 -1
- package/dist/types/loop-context-optimizer.d.ts +7 -0
- package/dist/types/loop-context-optimizer.js +2 -0
- package/dist/types/workflow-types.d.ts +32 -0
- package/dist/types/workflow-types.js +14 -0
- package/package.json +2 -2
- package/spec/workflow.schema.json +70 -1
- package/workflows/coding-task-workflow-with-loops.json +19 -22
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **Transform chaotic AI interactions into structured, reliable workflows**
|
|
4
4
|
|
|
5
5
|
[](https://modelcontextprotocol.org)
|
|
6
|
-
[]()
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -123,6 +123,15 @@ WorkRail supports powerful iteration patterns for complex tasks:
|
|
|
123
123
|
|
|
124
124
|
Perfect for batch operations, retries, polling, and iterative refinement.
|
|
125
125
|
|
|
126
|
+
### 🚀 v0.2.0: Optimized Loop Execution
|
|
127
|
+
|
|
128
|
+
- **60-80% smaller context** after first iteration
|
|
129
|
+
- **Progressive disclosure** pattern for loop information
|
|
130
|
+
- **Native function DSL** to reduce duplication
|
|
131
|
+
- **Automatic empty loop detection** and skipping
|
|
132
|
+
|
|
133
|
+
See [Loop Optimization Guide](docs/features/loop-optimization.md) for details.
|
|
134
|
+
|
|
126
135
|
---
|
|
127
136
|
|
|
128
137
|
## 📖 Quick Example
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from '../../types/workflow-types';
|
|
2
|
+
import { ILoopContextOptimizer } from '../../types/loop-context-optimizer';
|
|
3
|
+
export declare class LoopContextOptimizer implements ILoopContextOptimizer {
|
|
4
|
+
optimizeLoopContext(context: EnhancedContext, iteration: number): OptimizedLoopContext;
|
|
5
|
+
createPhaseReference(loopStep: LoopStep): LoopPhaseReference;
|
|
6
|
+
hasLoopItems(context: EnhancedContext, loopStep: LoopStep): boolean;
|
|
7
|
+
stripLoopMetadata(context: EnhancedContext): OptimizedLoopContext;
|
|
8
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoopContextOptimizer = void 0;
|
|
4
|
+
class LoopContextOptimizer {
|
|
5
|
+
optimizeLoopContext(context, iteration) {
|
|
6
|
+
if (!context._currentLoop) {
|
|
7
|
+
throw new Error('Cannot optimize context without active loop');
|
|
8
|
+
}
|
|
9
|
+
const { loopId, loopStep } = context._currentLoop;
|
|
10
|
+
const isFirstIteration = iteration === 0;
|
|
11
|
+
const optimizedContext = {
|
|
12
|
+
...context,
|
|
13
|
+
_currentLoop: {
|
|
14
|
+
loopId,
|
|
15
|
+
loopType: loopStep.loop.type,
|
|
16
|
+
iteration,
|
|
17
|
+
isFirstIteration
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (!isFirstIteration && optimizedContext._currentLoop) {
|
|
21
|
+
optimizedContext._currentLoop.phaseReference = this.createPhaseReference(loopStep);
|
|
22
|
+
return this.stripLoopMetadata(optimizedContext);
|
|
23
|
+
}
|
|
24
|
+
return optimizedContext;
|
|
25
|
+
}
|
|
26
|
+
createPhaseReference(loopStep) {
|
|
27
|
+
const reference = {
|
|
28
|
+
loopId: loopStep.id,
|
|
29
|
+
phaseTitle: loopStep.title,
|
|
30
|
+
totalSteps: Array.isArray(loopStep.body) ? loopStep.body.length : 1
|
|
31
|
+
};
|
|
32
|
+
if (loopStep.functionDefinitions && loopStep.functionDefinitions.length > 0) {
|
|
33
|
+
reference.functionDefinitions = loopStep.functionDefinitions;
|
|
34
|
+
}
|
|
35
|
+
return reference;
|
|
36
|
+
}
|
|
37
|
+
hasLoopItems(context, loopStep) {
|
|
38
|
+
const { loop } = loopStep;
|
|
39
|
+
switch (loop.type) {
|
|
40
|
+
case 'forEach':
|
|
41
|
+
if (loop.items) {
|
|
42
|
+
const items = context[loop.items];
|
|
43
|
+
return Array.isArray(items) && items.length > 0;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
case 'for':
|
|
47
|
+
const count = typeof loop.count === 'number'
|
|
48
|
+
? loop.count
|
|
49
|
+
: (loop.count ? context[loop.count] : 0);
|
|
50
|
+
return count > 0;
|
|
51
|
+
case 'while':
|
|
52
|
+
case 'until':
|
|
53
|
+
return true;
|
|
54
|
+
default:
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
stripLoopMetadata(context) {
|
|
59
|
+
let optimizedContext;
|
|
60
|
+
if ('loopType' in (context._currentLoop || {})) {
|
|
61
|
+
optimizedContext = context;
|
|
62
|
+
}
|
|
63
|
+
else if (context._currentLoop) {
|
|
64
|
+
const { loopId, loopStep } = context._currentLoop;
|
|
65
|
+
const iteration = context._loopState?.[loopId]?.iteration || 0;
|
|
66
|
+
optimizedContext = {
|
|
67
|
+
...context,
|
|
68
|
+
_currentLoop: {
|
|
69
|
+
loopId,
|
|
70
|
+
loopType: loopStep.loop.type,
|
|
71
|
+
iteration,
|
|
72
|
+
isFirstIteration: iteration === 0
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return context;
|
|
78
|
+
}
|
|
79
|
+
const strippedContext = { ...optimizedContext };
|
|
80
|
+
if (strippedContext._currentLoop?.loopType === 'forEach') {
|
|
81
|
+
const loopState = strippedContext._loopState;
|
|
82
|
+
if (loopState) {
|
|
83
|
+
Object.keys(loopState).forEach(loopId => {
|
|
84
|
+
const state = loopState[loopId];
|
|
85
|
+
if (state.items && Array.isArray(state.items)) {
|
|
86
|
+
const currentIndex = state.index || 0;
|
|
87
|
+
if (currentIndex < state.items.length) {
|
|
88
|
+
state.items = [state.items[currentIndex]];
|
|
89
|
+
state.index = 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const keysToCheck = Object.keys(strippedContext);
|
|
96
|
+
keysToCheck.forEach(key => {
|
|
97
|
+
if (key.startsWith('_'))
|
|
98
|
+
return;
|
|
99
|
+
const value = strippedContext[key];
|
|
100
|
+
if (Array.isArray(value) && value.length > 10) {
|
|
101
|
+
const currentLoop = strippedContext._currentLoop;
|
|
102
|
+
if (currentLoop?.loopType === 'forEach') {
|
|
103
|
+
const loopState = strippedContext._loopState?.[currentLoop.loopId];
|
|
104
|
+
if (loopState?.items === value) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
delete strippedContext[key];
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return strippedContext;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.LoopContextOptimizer = LoopContextOptimizer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LoopConfig, LoopState, EnhancedContext } from '../../types/workflow-types';
|
|
1
|
+
import { LoopConfig, LoopState, EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from '../../types/workflow-types';
|
|
2
2
|
import { ConditionContext } from '../../utils/condition-evaluator';
|
|
3
3
|
export declare class LoopExecutionContext {
|
|
4
4
|
private loopId;
|
|
@@ -10,9 +10,14 @@ export declare class LoopExecutionContext {
|
|
|
10
10
|
getCurrentState(): LoopState[string];
|
|
11
11
|
shouldContinue(context: ConditionContext): boolean;
|
|
12
12
|
initializeForEach(context: ConditionContext): void;
|
|
13
|
-
injectVariables(context: ConditionContext): EnhancedContext;
|
|
14
13
|
private resolveCount;
|
|
15
14
|
private addWarning;
|
|
16
15
|
getLoopId(): string;
|
|
17
16
|
getLoopConfig(): LoopConfig;
|
|
17
|
+
getMinimalContext(context: ConditionContext): OptimizedLoopContext;
|
|
18
|
+
getPhaseReference(loopStep: LoopStep): LoopPhaseReference;
|
|
19
|
+
injectVariables(context: ConditionContext, minimal?: boolean): EnhancedContext | OptimizedLoopContext;
|
|
20
|
+
private injectVariablesFull;
|
|
21
|
+
isFirstIteration(): boolean;
|
|
22
|
+
isEmpty(context: ConditionContext): boolean;
|
|
18
23
|
}
|
|
@@ -68,7 +68,72 @@ class LoopExecutionContext {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
resolveCount(context) {
|
|
72
|
+
if (this.loopConfig.type !== 'for' || !this.loopConfig.count) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (typeof this.loopConfig.count === 'number') {
|
|
76
|
+
return this.loopConfig.count;
|
|
77
|
+
}
|
|
78
|
+
const count = context[this.loopConfig.count];
|
|
79
|
+
if (typeof count === 'number') {
|
|
80
|
+
return count;
|
|
81
|
+
}
|
|
82
|
+
this.addWarning(`Invalid count value for 'for' loop: ${this.loopConfig.count}`);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
addWarning(message) {
|
|
86
|
+
if (!this.state.warnings) {
|
|
87
|
+
this.state.warnings = [];
|
|
88
|
+
}
|
|
89
|
+
this.state.warnings.push(message);
|
|
90
|
+
}
|
|
91
|
+
getLoopId() {
|
|
92
|
+
return this.loopId;
|
|
93
|
+
}
|
|
94
|
+
getLoopConfig() {
|
|
95
|
+
return { ...this.loopConfig };
|
|
96
|
+
}
|
|
97
|
+
getMinimalContext(context) {
|
|
98
|
+
const optimizedContext = {
|
|
99
|
+
...context,
|
|
100
|
+
_loopState: {
|
|
101
|
+
...context._loopState,
|
|
102
|
+
[this.loopId]: this.getCurrentState()
|
|
103
|
+
},
|
|
104
|
+
_currentLoop: {
|
|
105
|
+
loopId: this.loopId,
|
|
106
|
+
loopType: this.loopConfig.type,
|
|
107
|
+
iteration: this.state.iteration,
|
|
108
|
+
isFirstIteration: false
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
if (this.loopConfig.type === 'forEach' && this.state.items) {
|
|
112
|
+
const index = this.state.index || 0;
|
|
113
|
+
const itemVar = this.loopConfig.itemVar || 'currentItem';
|
|
114
|
+
const indexVar = this.loopConfig.indexVar || 'currentIndex';
|
|
115
|
+
optimizedContext[itemVar] = this.state.items[index];
|
|
116
|
+
optimizedContext[indexVar] = index;
|
|
117
|
+
}
|
|
118
|
+
const iterationVar = this.loopConfig.iterationVar || 'currentIteration';
|
|
119
|
+
optimizedContext[iterationVar] = this.state.iteration + 1;
|
|
120
|
+
return optimizedContext;
|
|
121
|
+
}
|
|
122
|
+
getPhaseReference(loopStep) {
|
|
123
|
+
return {
|
|
124
|
+
loopId: this.loopId,
|
|
125
|
+
phaseTitle: loopStep.title,
|
|
126
|
+
totalSteps: Array.isArray(loopStep.body) ? loopStep.body.length : 1,
|
|
127
|
+
functionDefinitions: loopStep.functionDefinitions
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
injectVariables(context, minimal = false) {
|
|
131
|
+
if (minimal) {
|
|
132
|
+
return this.getMinimalContext(context);
|
|
133
|
+
}
|
|
134
|
+
return this.injectVariablesFull(context);
|
|
135
|
+
}
|
|
136
|
+
injectVariablesFull(context) {
|
|
72
137
|
const enhancements = {
|
|
73
138
|
_loopState: {
|
|
74
139
|
...context._loopState,
|
|
@@ -97,31 +162,27 @@ class LoopExecutionContext {
|
|
|
97
162
|
}
|
|
98
163
|
return { ...context, ...enhancements };
|
|
99
164
|
}
|
|
100
|
-
|
|
101
|
-
|
|
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;
|
|
165
|
+
isFirstIteration() {
|
|
166
|
+
return this.state.iteration === 0;
|
|
113
167
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
isEmpty(context) {
|
|
169
|
+
switch (this.loopConfig.type) {
|
|
170
|
+
case 'forEach':
|
|
171
|
+
return !this.state.items || this.state.items.length === 0;
|
|
172
|
+
case 'for':
|
|
173
|
+
const count = this.resolveCount(context);
|
|
174
|
+
return count <= 0;
|
|
175
|
+
case 'while':
|
|
176
|
+
return this.loopConfig.condition
|
|
177
|
+
? !(0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
|
|
178
|
+
: true;
|
|
179
|
+
case 'until':
|
|
180
|
+
return this.loopConfig.condition
|
|
181
|
+
? (0, condition_evaluator_1.evaluateCondition)(this.loopConfig.condition, context)
|
|
182
|
+
: true;
|
|
183
|
+
default:
|
|
184
|
+
return false;
|
|
117
185
|
}
|
|
118
|
-
this.state.warnings.push(message);
|
|
119
|
-
}
|
|
120
|
-
getLoopId() {
|
|
121
|
-
return this.loopId;
|
|
122
|
-
}
|
|
123
|
-
getLoopConfig() {
|
|
124
|
-
return { ...this.loopConfig };
|
|
125
186
|
}
|
|
126
187
|
}
|
|
127
188
|
exports.LoopExecutionContext = LoopExecutionContext;
|
|
@@ -18,11 +18,13 @@ import { IWorkflowStorage } from '../../types/storage';
|
|
|
18
18
|
import { ConditionContext } from '../../utils/condition-evaluator';
|
|
19
19
|
import { ValidationEngine } from './validation-engine';
|
|
20
20
|
import { EnhancedContext } from '../../types/workflow-types';
|
|
21
|
+
import { ILoopContextOptimizer } from '../../types/loop-context-optimizer';
|
|
21
22
|
export declare class DefaultWorkflowService implements WorkflowService {
|
|
22
23
|
private readonly storage;
|
|
23
24
|
private readonly validationEngine;
|
|
25
|
+
private readonly loopContextOptimizer?;
|
|
24
26
|
private loopStepResolver;
|
|
25
|
-
constructor(storage?: IWorkflowStorage, validationEngine?: ValidationEngine);
|
|
27
|
+
constructor(storage?: IWorkflowStorage, validationEngine?: ValidationEngine, loopContextOptimizer?: ILoopContextOptimizer | undefined);
|
|
26
28
|
listWorkflowSummaries(): Promise<WorkflowSummary[]>;
|
|
27
29
|
getWorkflowById(id: string): Promise<Workflow | null>;
|
|
28
30
|
getNextStep(workflowId: string, completedSteps: string[], context?: ConditionContext): Promise<{
|
|
@@ -11,9 +11,10 @@ const loop_step_resolver_1 = require("./loop-step-resolver");
|
|
|
11
11
|
const context_size_1 = require("../../utils/context-size");
|
|
12
12
|
const context_optimizer_1 = require("./context-optimizer");
|
|
13
13
|
class DefaultWorkflowService {
|
|
14
|
-
constructor(storage = (0, storage_1.createDefaultWorkflowStorage)(), validationEngine = new validation_engine_1.ValidationEngine()) {
|
|
14
|
+
constructor(storage = (0, storage_1.createDefaultWorkflowStorage)(), validationEngine = new validation_engine_1.ValidationEngine(), loopContextOptimizer) {
|
|
15
15
|
this.storage = storage;
|
|
16
16
|
this.validationEngine = validationEngine;
|
|
17
|
+
this.loopContextOptimizer = loopContextOptimizer;
|
|
17
18
|
this.loopStepResolver = new loop_step_resolver_1.LoopStepResolver();
|
|
18
19
|
}
|
|
19
20
|
async listWorkflowSummaries() {
|
|
@@ -56,15 +57,26 @@ class DefaultWorkflowService {
|
|
|
56
57
|
if (loopContext.shouldContinue(context)) {
|
|
57
58
|
const bodyStep = this.loopStepResolver.resolveLoopBody(workflow, loopStep.body, loopStep.id);
|
|
58
59
|
if (!Array.isArray(bodyStep)) {
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
const isFirst = loopContext.isFirstIteration();
|
|
61
|
+
if (isFirst && loopContext.isEmpty(context)) {
|
|
62
|
+
const skipContext = context_optimizer_1.ContextOptimizer.createEnhancedContext(context, completed);
|
|
63
|
+
delete skipContext._currentLoop;
|
|
64
|
+
const nextStep = await this.getNextStep(workflow.id, completed, skipContext);
|
|
65
|
+
return nextStep;
|
|
66
|
+
}
|
|
67
|
+
const useMinimal = !isFirst && !!this.loopContextOptimizer;
|
|
68
|
+
const loopEnhancedContext = loopContext.injectVariables(context, useMinimal);
|
|
69
|
+
const optimizedContext = useMinimal && this.loopContextOptimizer
|
|
70
|
+
? this.loopContextOptimizer.stripLoopMetadata(loopEnhancedContext)
|
|
71
|
+
: loopEnhancedContext;
|
|
72
|
+
const loopSizeCheck = (0, context_size_1.checkContextSize)(optimizedContext);
|
|
61
73
|
if (loopSizeCheck.isError) {
|
|
62
74
|
throw new Error(`Context size (${Math.round(loopSizeCheck.sizeBytes / 1024)}KB) exceeds maximum allowed size (256KB) during loop execution`);
|
|
63
75
|
}
|
|
64
76
|
return {
|
|
65
77
|
step: bodyStep,
|
|
66
78
|
guidance: {
|
|
67
|
-
prompt: this.buildStepPrompt(bodyStep, loopContext)
|
|
79
|
+
prompt: this.buildStepPrompt(bodyStep, loopContext, useMinimal)
|
|
68
80
|
},
|
|
69
81
|
isComplete: false,
|
|
70
82
|
context: loopSizeCheck.context
|
|
@@ -81,15 +93,26 @@ class DefaultWorkflowService {
|
|
|
81
93
|
return true;
|
|
82
94
|
});
|
|
83
95
|
if (uncompletedBodyStep) {
|
|
84
|
-
const
|
|
85
|
-
|
|
96
|
+
const isFirst = loopContext.isFirstIteration();
|
|
97
|
+
if (isFirst && loopContext.isEmpty(context)) {
|
|
98
|
+
const skipContext = context_optimizer_1.ContextOptimizer.createEnhancedContext(context, completed);
|
|
99
|
+
delete skipContext._currentLoop;
|
|
100
|
+
const nextStep = await this.getNextStep(workflow.id, completed, skipContext);
|
|
101
|
+
return nextStep;
|
|
102
|
+
}
|
|
103
|
+
const useMinimal = !isFirst && !!this.loopContextOptimizer;
|
|
104
|
+
const loopEnhancedContext = loopContext.injectVariables(context, useMinimal);
|
|
105
|
+
const optimizedContext = useMinimal && this.loopContextOptimizer
|
|
106
|
+
? this.loopContextOptimizer.stripLoopMetadata(loopEnhancedContext)
|
|
107
|
+
: loopEnhancedContext;
|
|
108
|
+
const loopSizeCheck = (0, context_size_1.checkContextSize)(optimizedContext);
|
|
86
109
|
if (loopSizeCheck.isError) {
|
|
87
110
|
throw new Error(`Context size (${Math.round(loopSizeCheck.sizeBytes / 1024)}KB) exceeds maximum allowed size (256KB) during loop execution`);
|
|
88
111
|
}
|
|
89
112
|
return {
|
|
90
113
|
step: uncompletedBodyStep,
|
|
91
114
|
guidance: {
|
|
92
|
-
prompt: this.buildStepPrompt(uncompletedBodyStep, loopContext)
|
|
115
|
+
prompt: this.buildStepPrompt(uncompletedBodyStep, loopContext, useMinimal)
|
|
93
116
|
},
|
|
94
117
|
isComplete: false,
|
|
95
118
|
context: loopSizeCheck.context
|
|
@@ -180,7 +203,7 @@ class DefaultWorkflowService {
|
|
|
180
203
|
context: enhancedContext
|
|
181
204
|
};
|
|
182
205
|
}
|
|
183
|
-
buildStepPrompt(step, loopContext) {
|
|
206
|
+
buildStepPrompt(step, loopContext, useMinimal = false) {
|
|
184
207
|
let stepGuidance = '';
|
|
185
208
|
if (step.guidance && step.guidance.length > 0) {
|
|
186
209
|
const guidanceHeader = '## Step Guidance';
|
|
@@ -193,10 +216,18 @@ class DefaultWorkflowService {
|
|
|
193
216
|
}
|
|
194
217
|
if (loopContext) {
|
|
195
218
|
const state = loopContext.getCurrentState();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
if (useMinimal && this.loopContextOptimizer) {
|
|
220
|
+
finalPrompt += `\n\n## Loop Context\n- Iteration: ${state.iteration + 1}`;
|
|
221
|
+
if (!loopContext.isFirstIteration()) {
|
|
222
|
+
finalPrompt += '\n\n_Note: Refer to the phase overview provided in the first iteration for overall context._';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
finalPrompt += `\n\n## Loop Context\n- Iteration: ${state.iteration + 1}`;
|
|
227
|
+
if (state.items) {
|
|
228
|
+
finalPrompt += `\n- Total Items: ${state.items.length}`;
|
|
229
|
+
finalPrompt += `\n- Current Index: ${state.index}`;
|
|
230
|
+
}
|
|
200
231
|
}
|
|
201
232
|
}
|
|
202
233
|
return finalPrompt;
|
package/dist/container.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { WorkflowService } from './application/services/workflow-service.js';
|
|
|
2
2
|
import { ValidationEngine } from './application/services/validation-engine.js';
|
|
3
3
|
import { IWorkflowStorage } from './types/storage.js';
|
|
4
4
|
import { WorkflowLookupServer } from './types/server.js';
|
|
5
|
+
import { ILoopContextOptimizer } from './types/loop-context-optimizer.js';
|
|
5
6
|
export interface AppContainer {
|
|
6
7
|
storage: IWorkflowStorage;
|
|
7
8
|
validationEngine: ValidationEngine;
|
|
9
|
+
loopContextOptimizer: ILoopContextOptimizer;
|
|
8
10
|
workflowService: WorkflowService;
|
|
9
11
|
server: WorkflowLookupServer;
|
|
10
12
|
}
|
package/dist/container.js
CHANGED
|
@@ -5,14 +5,17 @@ const storage_js_1 = require("./infrastructure/storage/storage.js");
|
|
|
5
5
|
const workflow_service_js_1 = require("./application/services/workflow-service.js");
|
|
6
6
|
const validation_engine_js_1 = require("./application/services/validation-engine.js");
|
|
7
7
|
const server_js_1 = require("./infrastructure/rpc/server.js");
|
|
8
|
+
const loop_context_optimizer_js_1 = require("./application/services/loop-context-optimizer.js");
|
|
8
9
|
function createAppContainer(overrides = {}) {
|
|
9
10
|
const storage = overrides.storage ?? (0, storage_js_1.createDefaultWorkflowStorage)();
|
|
10
11
|
const validationEngine = overrides.validationEngine ?? new validation_engine_js_1.ValidationEngine();
|
|
11
|
-
const
|
|
12
|
+
const loopContextOptimizer = overrides.loopContextOptimizer ?? new loop_context_optimizer_js_1.LoopContextOptimizer();
|
|
13
|
+
const workflowService = overrides.workflowService ?? new workflow_service_js_1.DefaultWorkflowService(storage, validationEngine, loopContextOptimizer);
|
|
12
14
|
const server = overrides.server ?? (0, server_js_1.createWorkflowLookupServer)(workflowService);
|
|
13
15
|
return {
|
|
14
16
|
storage,
|
|
15
17
|
validationEngine,
|
|
18
|
+
loopContextOptimizer,
|
|
16
19
|
workflowService,
|
|
17
20
|
server
|
|
18
21
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EnhancedContext, OptimizedLoopContext, LoopPhaseReference, LoopStep } from './workflow-types';
|
|
2
|
+
export interface ILoopContextOptimizer {
|
|
3
|
+
optimizeLoopContext(context: EnhancedContext, iteration: number): OptimizedLoopContext;
|
|
4
|
+
createPhaseReference(loopStep: LoopStep): LoopPhaseReference;
|
|
5
|
+
hasLoopItems(context: EnhancedContext, loopStep: LoopStep): boolean;
|
|
6
|
+
stripLoopMetadata(context: EnhancedContext): OptimizedLoopContext;
|
|
7
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface Workflow {
|
|
|
8
8
|
clarificationPrompts?: string[];
|
|
9
9
|
steps: (WorkflowStep | LoopStep)[];
|
|
10
10
|
metaGuidance?: string[];
|
|
11
|
+
functionDefinitions?: FunctionDefinition[];
|
|
11
12
|
}
|
|
12
13
|
export interface WorkflowStep {
|
|
13
14
|
id: string;
|
|
@@ -18,6 +19,8 @@ export interface WorkflowStep {
|
|
|
18
19
|
askForFiles?: boolean;
|
|
19
20
|
requireConfirmation?: boolean;
|
|
20
21
|
runCondition?: object;
|
|
22
|
+
functionDefinitions?: FunctionDefinition[];
|
|
23
|
+
functionReferences?: string[];
|
|
21
24
|
}
|
|
22
25
|
export interface LoopStep extends WorkflowStep {
|
|
23
26
|
type: 'loop';
|
|
@@ -56,7 +59,36 @@ export interface EnhancedContext extends ConditionContext {
|
|
|
56
59
|
loopStep: LoopStep;
|
|
57
60
|
};
|
|
58
61
|
}
|
|
62
|
+
export interface OptimizedLoopContext extends ConditionContext {
|
|
63
|
+
_loopState?: LoopState;
|
|
64
|
+
_warnings?: {
|
|
65
|
+
loops?: {
|
|
66
|
+
[loopId: string]: string[];
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
_contextSize?: number;
|
|
70
|
+
_currentLoop?: {
|
|
71
|
+
loopId: string;
|
|
72
|
+
loopType: 'for' | 'forEach' | 'while' | 'until';
|
|
73
|
+
iteration: number;
|
|
74
|
+
isFirstIteration: boolean;
|
|
75
|
+
phaseReference?: LoopPhaseReference;
|
|
76
|
+
};
|
|
77
|
+
_loopPhaseReference?: LoopPhaseReference;
|
|
78
|
+
}
|
|
79
|
+
export interface LoopPhaseReference {
|
|
80
|
+
loopId: string;
|
|
81
|
+
phaseTitle: string;
|
|
82
|
+
totalSteps: number;
|
|
83
|
+
functionDefinitions?: FunctionDefinition[];
|
|
84
|
+
}
|
|
85
|
+
export interface FunctionDefinition {
|
|
86
|
+
name: string;
|
|
87
|
+
definition: string;
|
|
88
|
+
scope?: 'workflow' | 'loop' | 'step';
|
|
89
|
+
}
|
|
59
90
|
export declare function isLoopStep(step: WorkflowStep | LoopStep): step is LoopStep;
|
|
91
|
+
export declare function isFirstLoopIteration(context: EnhancedContext | OptimizedLoopContext): boolean;
|
|
60
92
|
export interface WorkflowValidationResult {
|
|
61
93
|
valid: boolean;
|
|
62
94
|
errors: WorkflowValidationError[];
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isLoopStep = isLoopStep;
|
|
4
|
+
exports.isFirstLoopIteration = isFirstLoopIteration;
|
|
4
5
|
function isLoopStep(step) {
|
|
5
6
|
return 'type' in step && step.type === 'loop';
|
|
6
7
|
}
|
|
8
|
+
function isFirstLoopIteration(context) {
|
|
9
|
+
if ('isFirstIteration' in (context._currentLoop || {})) {
|
|
10
|
+
return context._currentLoop?.isFirstIteration === true;
|
|
11
|
+
}
|
|
12
|
+
const loopState = context._loopState;
|
|
13
|
+
if (loopState) {
|
|
14
|
+
const currentLoopId = context._currentLoop?.loopId;
|
|
15
|
+
if (currentLoopId && loopState[currentLoopId]) {
|
|
16
|
+
return loopState[currentLoopId].iteration === 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exaudeus/workrail",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "MCP server for structured workflow orchestration and step-by-step task guidance",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc -p tsconfig.build.json",
|
|
16
|
-
"release": "./scripts/release.sh",
|
|
16
|
+
"release": "bash -c './scripts/release.sh'",
|
|
17
17
|
"dev": "npm run build && node dist/mcp-server.js",
|
|
18
18
|
"prepare": "npm run build",
|
|
19
19
|
"watch": "tsc --watch"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://workflowlookup.io/schemas/workflow/v0.
|
|
3
|
+
"$id": "https://workflowlookup.io/schemas/workflow/v0.2.0",
|
|
4
4
|
"title": "Workflow Schema",
|
|
5
5
|
"description": "Schema for defining workflows in the Workflow Orchestration System",
|
|
6
6
|
"type": "object",
|
|
@@ -74,6 +74,32 @@
|
|
|
74
74
|
"maxLength": 256
|
|
75
75
|
},
|
|
76
76
|
"uniqueItems": true
|
|
77
|
+
},
|
|
78
|
+
"functionDefinitions": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"description": "Function definitions for reducing duplication. Agents can reference these definitions to understand repeated patterns without full expansion.",
|
|
81
|
+
"items": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"name": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
|
87
|
+
"description": "Function name following JavaScript identifier rules"
|
|
88
|
+
},
|
|
89
|
+
"definition": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "The function definition explaining what it does"
|
|
92
|
+
},
|
|
93
|
+
"scope": {
|
|
94
|
+
"type": "string",
|
|
95
|
+
"enum": ["workflow", "loop", "step"],
|
|
96
|
+
"description": "Scope where this function is available"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"required": ["name", "definition"],
|
|
100
|
+
"additionalProperties": false
|
|
101
|
+
},
|
|
102
|
+
"uniqueItems": true
|
|
77
103
|
}
|
|
78
104
|
},
|
|
79
105
|
"required": [
|
|
@@ -91,6 +117,27 @@
|
|
|
91
117
|
"minLength": 3,
|
|
92
118
|
"maxLength": 64
|
|
93
119
|
},
|
|
120
|
+
"functionDefinition": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"name": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
|
126
|
+
"description": "Function name following JavaScript identifier rules"
|
|
127
|
+
},
|
|
128
|
+
"definition": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": "The function definition explaining what it does"
|
|
131
|
+
},
|
|
132
|
+
"scope": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"enum": ["workflow", "loop", "step"],
|
|
135
|
+
"description": "Scope where this function is available"
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"required": ["name", "definition"],
|
|
139
|
+
"additionalProperties": false
|
|
140
|
+
},
|
|
94
141
|
"standardStep": {
|
|
95
142
|
"type": "object",
|
|
96
143
|
"properties": {
|
|
@@ -153,6 +200,21 @@
|
|
|
153
200
|
"$ref": "#/$defs/validationComposition"
|
|
154
201
|
}
|
|
155
202
|
]
|
|
203
|
+
},
|
|
204
|
+
"functionDefinitions": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"description": "Function definitions specific to this step. Agents can reference these for understanding step-specific patterns.",
|
|
207
|
+
"items": {
|
|
208
|
+
"$ref": "#/$defs/functionDefinition"
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"functionReferences": {
|
|
212
|
+
"type": "array",
|
|
213
|
+
"description": "References to functions defined at workflow or loop level that this step uses.",
|
|
214
|
+
"items": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*\\(\\)$"
|
|
217
|
+
}
|
|
156
218
|
}
|
|
157
219
|
},
|
|
158
220
|
"required": [
|
|
@@ -198,6 +260,13 @@
|
|
|
198
260
|
}
|
|
199
261
|
]
|
|
200
262
|
},
|
|
263
|
+
"functionDefinitions": {
|
|
264
|
+
"type": "array",
|
|
265
|
+
"description": "Function definitions specific to this loop. Available to all steps within the loop body.",
|
|
266
|
+
"items": {
|
|
267
|
+
"$ref": "#/$defs/functionDefinition"
|
|
268
|
+
}
|
|
269
|
+
},
|
|
201
270
|
"requireConfirmation": {
|
|
202
271
|
"$ref": "#/$defs/confirmationRule"
|
|
203
272
|
},
|
|
@@ -404,15 +404,14 @@
|
|
|
404
404
|
"requireConfirmation": false
|
|
405
405
|
},
|
|
406
406
|
{
|
|
407
|
-
"id": "phase-6-
|
|
408
|
-
"title": "Phase 6 Preparation:
|
|
409
|
-
"prompt": "Before starting the implementation loop,
|
|
410
|
-
"agentRole": "You are preparing the implementation phase by
|
|
407
|
+
"id": "phase-6-count-steps",
|
|
408
|
+
"title": "Phase 6 Preparation: Count Implementation Steps",
|
|
409
|
+
"prompt": "Before starting the implementation loop, count the discrete implementation steps in the approved `implementation_plan.md`.\n\n**Your task:**\n1. Read the implementation_plan.md\n2. Count each discrete, actionable step from the Implementation Strategy section\n3. Set `totalImplementationSteps` to the count (e.g., 8)\n\n**What counts as a step:**\n- Each bullet point or numbered item that represents a concrete implementation task\n- Each item that would result in a code change or file creation\n- Each item that could be a logical git commit\n\n**Example:**\nIf the plan has:\n- Create user model\n- Add authentication service\n- Implement validation\n- Write tests\n\nThen set `totalImplementationSteps` = 4\n\n**IMPORTANT:** Only count, don't extract or store the steps. You'll read them directly from the plan during implementation.",
|
|
410
|
+
"agentRole": "You are preparing the implementation phase by counting the steps to enable progress tracking.",
|
|
411
411
|
"guidance": [
|
|
412
|
-
"
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"This enables the loop to iterate over a concrete list"
|
|
412
|
+
"Count only actionable implementation steps",
|
|
413
|
+
"Don't include meta-tasks like 'review' or 'document'",
|
|
414
|
+
"This enables progress tracking without bloating context"
|
|
416
415
|
],
|
|
417
416
|
"runCondition": {"var": "taskComplexity", "not_equals": "Small"},
|
|
418
417
|
"requireConfirmation": false
|
|
@@ -423,22 +422,20 @@
|
|
|
423
422
|
"title": "Phase 6: Iterative Implementation Loop (PREP -> IMPLEMENT -> VERIFY)",
|
|
424
423
|
"runCondition": {"var": "taskComplexity", "not_equals": "Small"},
|
|
425
424
|
"loop": {
|
|
426
|
-
"type": "
|
|
427
|
-
"
|
|
428
|
-
"
|
|
429
|
-
"
|
|
430
|
-
"maxIterations": 25,
|
|
431
|
-
"iterationVar": "stepIteration"
|
|
425
|
+
"type": "for",
|
|
426
|
+
"count": "totalImplementationSteps",
|
|
427
|
+
"indexVar": "currentStepNumber",
|
|
428
|
+
"maxIterations": 25
|
|
432
429
|
},
|
|
433
430
|
"body": [
|
|
434
431
|
{
|
|
435
432
|
"id": "phase-6-prep",
|
|
436
|
-
"title": "PREP: Prepare for {{
|
|
437
|
-
"prompt": "**PREPARATION PHASE for Step {{
|
|
433
|
+
"title": "PREP: Prepare for Step {{currentStepNumber + 1}}",
|
|
434
|
+
"prompt": "**PREPARATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nBefore implementing this step, you must first PREPARE:\n\n1. **Read implementation_plan.md** and locate step #{{currentStepNumber + 1}}\n2. **Extract step details**: Note the title, description, and expected outputs\n3. **Confirm prerequisites**: Verify the previous step (if any) was completed correctly\n4. **Validate current state**: Ensure the plan for this step is still valid in the current codebase\n5. **List requirements**: Identify all required inputs, files, or dependencies\n\n**BRANCH SETUP (first iteration only):** If this is the first implementation step ({{currentStepNumber}} === 0):\n- Check git availability with 'git status'\n- If available, create feature branch: 'git checkout -b wip-[unique-task-id]'\n- Track the featureBranch variable for later use\n- If git unavailable: Skip branching, log in CONTEXT.md\n\n**CRITICAL CONTEXT OPTIMIZATION:**\nWhen calling workflow_next after this step, send ONLY:\n- currentStepNumber and totalImplementationSteps\n- Any NEW variables you created (like featureBranch)\n- DO NOT send: arrays, plans, _loopState, _currentLoop, unchanged data\n\n**Do not proceed if anything is unclear or missing.**",
|
|
438
435
|
"agentRole": "You are preparing to implement a specific step from the plan. Be meticulous in verifying all prerequisites are met before proceeding.",
|
|
439
436
|
"guidance": [
|
|
440
|
-
"This is step {{
|
|
441
|
-
"
|
|
437
|
+
"This is step {{currentStepNumber + 1}} of {{totalImplementationSteps}} total implementation steps",
|
|
438
|
+
"Read the step directly from implementation_plan.md",
|
|
442
439
|
"Focus only on preparation - implementation comes next",
|
|
443
440
|
"If this is the first step, handle git branch setup"
|
|
444
441
|
],
|
|
@@ -446,8 +443,8 @@
|
|
|
446
443
|
},
|
|
447
444
|
{
|
|
448
445
|
"id": "phase-6-implement",
|
|
449
|
-
"title": "IMPLEMENT: Execute {{
|
|
450
|
-
"prompt": "**IMPLEMENTATION PHASE for {{
|
|
446
|
+
"title": "IMPLEMENT: Execute Step {{currentStepNumber + 1}}",
|
|
447
|
+
"prompt": "**IMPLEMENTATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nNow implement the step you just prepared for:\n\n**Instructions:**\n1. Re-read step #{{currentStepNumber + 1}} from implementation_plan.md\n2. Focus only on this single step\n3. useTools() to make code changes\n4. Follow quality standards\n5. Adapt to unexpected discoveries\n6. createFile() for ALL code changes\n\n**Remember:** applyUserRules() and matchPatterns() throughout.\n\n**Progress Tracking:**\n- This is step {{currentStepNumber + 1}} of {{totalImplementationSteps}}\n- If we've done > 20 steps total, pause for user intervention\n\n**CONTEXT UPDATES:** If this is every 3rd step ({{currentStepNumber + 1}} % 3 === 0):\n- Update CONTEXT.md\n- addResumptionJson(phase-6-implement)\n- updateDecisionLog()\n- List files modified with line ranges\n\n**CONTEXT OPTIMIZATION for workflow_next:**\nSend ONLY: currentStepNumber, totalImplementationSteps, filesCreated (new), any other NEW data\nDO NOT send: arrays, plans, _loopState, unchanged fields",
|
|
451
448
|
"agentRole": "You are implementing a specific step from the approved plan. Focus on precise execution while maintaining code quality.",
|
|
452
449
|
"guidance": [
|
|
453
450
|
"Implement only what this step requires",
|
|
@@ -458,8 +455,8 @@
|
|
|
458
455
|
},
|
|
459
456
|
{
|
|
460
457
|
"id": "phase-6-verify",
|
|
461
|
-
"title": "VERIFY: Validate {{
|
|
462
|
-
"prompt": "**VERIFICATION PHASE for {{
|
|
458
|
+
"title": "VERIFY: Validate Step {{currentStepNumber + 1}}",
|
|
459
|
+
"prompt": "**VERIFICATION PHASE for Step {{currentStepNumber + 1}}/{{totalImplementationSteps}}**\n\nVerify the implementation is complete and correct:\n\n**Required:** verifyImplementation()\n\n**COMMIT Decision (if all passes):**\n- checkAutomation(commit)\n- gitCommit(type, scope: description)\n- If git unavailable: Log in CONTEXT.md\n\n**FAILURE PROTOCOL:** If verification fails after 2 attempts:\n1. Do not try a third time\n2. Fall back to alternative tools\n3. updateDecisionLog() with failure details\n4. Present summary and recommendations\n5. Set 'verificationFailed' context variable to true\n\n**CONTEXT OPTIMIZATION:**\nFor workflow_next, send ONLY what changed: currentStepNumber, verificationResult, verificationFailed (if true)\nNEVER send back the full context!",
|
|
463
460
|
"agentRole": "You are verifying the implementation meets all quality standards. Be thorough but respect failure bounds.",
|
|
464
461
|
"guidance": [
|
|
465
462
|
"All three verification steps must pass",
|