@pixel-normal-edit/mcp 2.0.10 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/command-bus.js +6 -3
- package/core/server.js +98 -3
- package/package.json +1 -1
- package/rules/README.md +358 -285
- package/rules/index.js +9 -9
- package/rules/prerequisites.js +118 -61
- package/rules/rules.js +75 -34
- package/rules/workflow.js +262 -56
package/rules/workflow.js
CHANGED
|
@@ -1,45 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* workflow.js — Workflow Engine (
|
|
4
|
-
*
|
|
5
|
-
* Engine này quản lý trạng thái workflow của agent.
|
|
6
|
-
* Mỗi lần agent gọi 1 tool, workflow engine sẽ kiểm tra:
|
|
7
|
-
* - Agent đang ở bước nào?
|
|
8
|
-
* - Bước tiếp theo phải là gì?
|
|
9
|
-
* - Nếu sai thứ tự → từ chối và yêu cầu làm đúng bước
|
|
10
|
-
*
|
|
11
|
-
* Cách dùng:
|
|
12
|
-
* const workflow = require('./rules/workflow');
|
|
13
|
-
* workflow.start('my_project'); // Bắt đầu workflow mới
|
|
14
|
-
* workflow.getCurrentStep(); // Xem đang ở bước nào
|
|
15
|
-
* workflow.advance(); // Chuyển sang bước tiếp theo
|
|
16
|
-
* workflow.validateStep('draw'); // Kiểm tra có được phép vẽ chưa
|
|
3
|
+
* workflow.js — Workflow Engine (Strict step ordering with modes, phases, layers)
|
|
17
4
|
*/
|
|
18
5
|
|
|
19
|
-
const { WORKFLOW_STEPS } = require('./rules');
|
|
6
|
+
const { WORKFLOW_STEPS, MODES, STATUSES, DRAWING_PHASES } = require('./rules');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const STATE_FILE = path.join(__dirname, '..', 'workflow_state.json');
|
|
11
|
+
let sessions = new Map();
|
|
12
|
+
|
|
13
|
+
// --- PERSISTENCE ---
|
|
14
|
+
function saveState() {
|
|
15
|
+
const data = {};
|
|
16
|
+
for (const [key, val] of sessions.entries()) {
|
|
17
|
+
data[key] = val;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('Failed to save workflow state:', e);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadState() {
|
|
27
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(STATE_FILE, 'utf-8');
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
sessions = new Map(Object.entries(data));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error('Failed to load workflow state:', e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Load state on startup
|
|
39
|
+
loadState();
|
|
20
40
|
|
|
21
|
-
// ── In-memory workflow state ────────────────────────────────────────────────
|
|
22
|
-
// Key: sessionId, Value: { currentStepIndex, steps, completedSteps, startedAt }
|
|
23
|
-
const sessions = new Map();
|
|
24
41
|
|
|
25
42
|
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {string} sessionId -
|
|
28
|
-
* @
|
|
43
|
+
* Start a new workflow for a session
|
|
44
|
+
* @param {string} sessionId - Session ID (usually MCP_SESSION)
|
|
45
|
+
* @param {string} mode - The mode to start in (CREATE_IMAGE, EDIT_IMAGE, CREATE_ANIMATION)
|
|
46
|
+
* @returns {Object} Initial workflow state
|
|
29
47
|
*/
|
|
30
|
-
function start(sessionId) {
|
|
48
|
+
function start(sessionId, mode = MODES.CREATE_IMAGE) {
|
|
49
|
+
if (!WORKFLOW_STEPS[mode]) {
|
|
50
|
+
throw new Error(`Invalid mode: ${mode}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
const state = {
|
|
54
|
+
current_mode: mode,
|
|
32
55
|
currentStepIndex: 0,
|
|
33
|
-
steps: WORKFLOW_STEPS.map(s => ({ ...s, status:
|
|
56
|
+
steps: WORKFLOW_STEPS[mode].map(s => ({ ...s, status: STATUSES.PENDING })),
|
|
57
|
+
current_layer: 0,
|
|
58
|
+
current_frame: mode === MODES.CREATE_ANIMATION ? 0 : null,
|
|
59
|
+
current_phase: null,
|
|
60
|
+
animation_meta: { fps: null, total_frames: null, loop: true },
|
|
61
|
+
step_status: STATUSES.IN_PROGRESS,
|
|
62
|
+
user_approval_required: false,
|
|
63
|
+
user_input_request: null, // { id, type, fields, status, created_at, response }
|
|
64
|
+
step_completion_flags: {}, // track internal flags for steps
|
|
34
65
|
completedSteps: [],
|
|
66
|
+
history: [],
|
|
35
67
|
startedAt: Date.now(),
|
|
36
68
|
};
|
|
69
|
+
|
|
37
70
|
sessions.set(sessionId, state);
|
|
71
|
+
saveState();
|
|
38
72
|
return getState(sessionId);
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
/**
|
|
42
|
-
*
|
|
76
|
+
* Get current workflow state
|
|
43
77
|
* @param {string} sessionId
|
|
44
78
|
* @returns {Object|null}
|
|
45
79
|
*/
|
|
@@ -48,9 +82,9 @@ function getState(sessionId) {
|
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
/**
|
|
51
|
-
*
|
|
85
|
+
* Get current step
|
|
52
86
|
* @param {string} sessionId
|
|
53
|
-
* @returns {
|
|
87
|
+
* @returns {Object|null}
|
|
54
88
|
*/
|
|
55
89
|
function getCurrentStep(sessionId) {
|
|
56
90
|
const state = sessions.get(sessionId);
|
|
@@ -59,9 +93,9 @@ function getCurrentStep(sessionId) {
|
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
/**
|
|
62
|
-
*
|
|
96
|
+
* Check if the agent is allowed to perform this action
|
|
63
97
|
* @param {string} sessionId
|
|
64
|
-
* @param {string} stepId - ID
|
|
98
|
+
* @param {string} stepId - ID of the step to perform
|
|
65
99
|
* @returns {{ allowed: boolean, message?: string, currentStep?: Object }}
|
|
66
100
|
*/
|
|
67
101
|
function validateStep(sessionId, stepId) {
|
|
@@ -69,7 +103,15 @@ function validateStep(sessionId, stepId) {
|
|
|
69
103
|
if (!state) {
|
|
70
104
|
return {
|
|
71
105
|
allowed: false,
|
|
72
|
-
message: '⚠️
|
|
106
|
+
message: '⚠️ Workflow has not started. Call workflow_start first!',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (state.user_approval_required) {
|
|
111
|
+
return {
|
|
112
|
+
allowed: false,
|
|
113
|
+
message: '⛔ Workflow is waiting for user approval. Ask the user for confirmation.',
|
|
114
|
+
currentStep: state.steps[state.currentStepIndex]
|
|
73
115
|
};
|
|
74
116
|
}
|
|
75
117
|
|
|
@@ -77,29 +119,26 @@ function validateStep(sessionId, stepId) {
|
|
|
77
119
|
if (!current) {
|
|
78
120
|
return {
|
|
79
121
|
allowed: false,
|
|
80
|
-
message: '✅ Workflow
|
|
122
|
+
message: '✅ Workflow is complete! No remaining steps.',
|
|
81
123
|
};
|
|
82
124
|
}
|
|
83
125
|
|
|
84
|
-
// Tìm index của stepId trong workflow
|
|
85
126
|
const targetIndex = state.steps.findIndex(s => s.id === stepId);
|
|
86
127
|
if (targetIndex === -1) {
|
|
87
128
|
return {
|
|
88
129
|
allowed: false,
|
|
89
|
-
message: `❌
|
|
130
|
+
message: `❌ Step "${stepId}" not found in current mode workflow.`,
|
|
90
131
|
};
|
|
91
132
|
}
|
|
92
133
|
|
|
93
|
-
// Nếu stepId nằm trước bước hiện tại (đã hoàn thành) → cho phép (có thể cần quay lại)
|
|
94
134
|
if (targetIndex < state.currentStepIndex) {
|
|
95
135
|
return {
|
|
96
136
|
allowed: true,
|
|
97
|
-
message: `⚠️
|
|
137
|
+
message: `⚠️ Step "${stepId}" was already completed. Are you sure you want to redo it?`,
|
|
98
138
|
currentStep: current,
|
|
99
139
|
};
|
|
100
140
|
}
|
|
101
141
|
|
|
102
|
-
// Nếu stepId đúng là bước hiện tại → cho phép
|
|
103
142
|
if (targetIndex === state.currentStepIndex) {
|
|
104
143
|
return {
|
|
105
144
|
allowed: true,
|
|
@@ -107,81 +146,241 @@ function validateStep(sessionId, stepId) {
|
|
|
107
146
|
};
|
|
108
147
|
}
|
|
109
148
|
|
|
110
|
-
// Nếu stepId nằm ở tương lai → CHẶN
|
|
111
149
|
return {
|
|
112
150
|
allowed: false,
|
|
113
|
-
message: `⛔
|
|
151
|
+
message: `⛔ You cannot perform "${stepId}" yet. First complete: "${current.label}" (${current.description})`,
|
|
114
152
|
currentStep: current,
|
|
115
153
|
};
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
/**
|
|
119
|
-
*
|
|
157
|
+
* Set user approval requirement
|
|
158
|
+
* @param {string} sessionId
|
|
159
|
+
* @param {boolean} required
|
|
160
|
+
*/
|
|
161
|
+
function setApprovalRequired(sessionId, required) {
|
|
162
|
+
const state = sessions.get(sessionId);
|
|
163
|
+
if (!state) return false;
|
|
164
|
+
state.user_approval_required = required;
|
|
165
|
+
if (required) {
|
|
166
|
+
state.step_status = STATUSES.WAITING_FOR_USER;
|
|
167
|
+
} else {
|
|
168
|
+
if (state.user_input_request && state.user_input_request.status === 'WAITING_FOR_USER') {
|
|
169
|
+
return false; // Cannot unpause if waiting for explicit input request
|
|
170
|
+
}
|
|
171
|
+
state.step_status = STATUSES.IN_PROGRESS;
|
|
172
|
+
}
|
|
173
|
+
saveState();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a user input request
|
|
179
|
+
* @param {string} sessionId
|
|
180
|
+
* @param {string} type - e.g. CANVAS_SIZE, APPROVE_PLAN, SELECT_EDIT_REGION
|
|
181
|
+
* @param {Object} fields - fields required
|
|
182
|
+
*/
|
|
183
|
+
function createInputRequest(sessionId, type, fields = {}) {
|
|
184
|
+
const state = sessions.get(sessionId);
|
|
185
|
+
if (!state) return false;
|
|
186
|
+
|
|
187
|
+
state.user_input_request = {
|
|
188
|
+
id: `req_${Date.now()}_${Math.floor(Math.random()*1000)}`,
|
|
189
|
+
type,
|
|
190
|
+
fields,
|
|
191
|
+
status: 'WAITING_FOR_USER',
|
|
192
|
+
created_at: Date.now()
|
|
193
|
+
};
|
|
194
|
+
state.step_status = STATUSES.WAITING_FOR_USER;
|
|
195
|
+
state.history.push({ event: 'input_request_created', type, time: Date.now() });
|
|
196
|
+
saveState();
|
|
197
|
+
return state.user_input_request.id;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Complete a user input request
|
|
202
|
+
* @param {string} sessionId
|
|
203
|
+
* @param {string} reqId
|
|
204
|
+
* @param {string} response
|
|
205
|
+
*/
|
|
206
|
+
function completeInputRequest(sessionId, reqId, response) {
|
|
207
|
+
const state = sessions.get(sessionId);
|
|
208
|
+
if (!state || !state.user_input_request || state.user_input_request.id !== reqId) return false;
|
|
209
|
+
|
|
210
|
+
state.user_input_request.status = 'COMPLETED';
|
|
211
|
+
state.user_input_request.response = response;
|
|
212
|
+
state.step_status = STATUSES.IN_PROGRESS; // Unpause workflow
|
|
213
|
+
|
|
214
|
+
state.history.push({ event: 'input_request_completed', reqId, time: Date.now() });
|
|
215
|
+
saveState();
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Update layer/frame
|
|
221
|
+
*/
|
|
222
|
+
function setLayerAndFrame(sessionId, layer, frame = null) {
|
|
223
|
+
const state = sessions.get(sessionId);
|
|
224
|
+
if (!state) return false;
|
|
225
|
+
|
|
226
|
+
if (layer !== undefined && layer !== null) {
|
|
227
|
+
state.current_layer = layer;
|
|
228
|
+
}
|
|
229
|
+
if (frame !== undefined && frame !== null) {
|
|
230
|
+
state.current_frame = frame;
|
|
231
|
+
}
|
|
232
|
+
state.history.push({
|
|
233
|
+
event: 'layer_frame_change',
|
|
234
|
+
layer: state.current_layer,
|
|
235
|
+
frame: state.current_frame,
|
|
236
|
+
time: Date.now()
|
|
237
|
+
});
|
|
238
|
+
saveState();
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Update phase
|
|
244
|
+
*/
|
|
245
|
+
function setPhase(sessionId, phase) {
|
|
246
|
+
const state = sessions.get(sessionId);
|
|
247
|
+
if (!state) return false;
|
|
248
|
+
|
|
249
|
+
state.current_phase = phase;
|
|
250
|
+
state.history.push({
|
|
251
|
+
event: 'phase_change',
|
|
252
|
+
phase: phase,
|
|
253
|
+
time: Date.now()
|
|
254
|
+
});
|
|
255
|
+
saveState();
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Set animation meta
|
|
261
|
+
*/
|
|
262
|
+
function setAnimationMeta(sessionId, fps, total_frames, loop) {
|
|
263
|
+
const state = sessions.get(sessionId);
|
|
264
|
+
if (!state) return false;
|
|
265
|
+
|
|
266
|
+
if (fps !== undefined) state.animation_meta.fps = fps;
|
|
267
|
+
if (total_frames !== undefined) state.animation_meta.total_frames = total_frames;
|
|
268
|
+
if (loop !== undefined) state.animation_meta.loop = loop;
|
|
269
|
+
|
|
270
|
+
saveState();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Set step completion flag (internal validation)
|
|
276
|
+
*/
|
|
277
|
+
function setStepFlag(sessionId, flagName, value) {
|
|
278
|
+
const state = sessions.get(sessionId);
|
|
279
|
+
if (!state) return false;
|
|
280
|
+
state.step_completion_flags[flagName] = value;
|
|
281
|
+
saveState();
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Mark current step as completed and advance to the next step
|
|
120
287
|
* @param {string} sessionId
|
|
121
|
-
* @returns {Object}
|
|
288
|
+
* @returns {Object} Step transition result
|
|
122
289
|
*/
|
|
123
290
|
function advance(sessionId) {
|
|
124
291
|
const state = sessions.get(sessionId);
|
|
125
292
|
if (!state) {
|
|
126
|
-
return { success: false, message: '
|
|
293
|
+
return { success: false, message: 'Workflow has not started' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (state.user_approval_required) {
|
|
297
|
+
return { success: false, message: 'Cannot advance: Waiting for user approval.' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (state.step_status === STATUSES.WAITING_FOR_USER) {
|
|
301
|
+
return { success: false, message: 'Cannot advance: Waiting for user approval or input.' };
|
|
127
302
|
}
|
|
128
303
|
|
|
129
304
|
const current = state.steps[state.currentStepIndex];
|
|
130
305
|
if (!current) {
|
|
131
|
-
return { success: false, message: 'Workflow
|
|
306
|
+
return { success: false, message: 'Workflow is complete' };
|
|
132
307
|
}
|
|
133
308
|
|
|
134
|
-
//
|
|
135
|
-
current.
|
|
309
|
+
// Pre-condition validation block
|
|
310
|
+
if (current.id === 'INITIALIZE_CANVAS' && !state.step_completion_flags.canvas_initialized) {
|
|
311
|
+
return { success: false, message: 'Cannot advance: Canvas has not been initialized. Create a canvas first.' };
|
|
312
|
+
}
|
|
313
|
+
if (current.id === 'PLAN_DRAWING' && !state.step_completion_flags.plan_created) {
|
|
314
|
+
return { success: false, message: 'Cannot advance: Drawing plan has not been finalized.' };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
current.status = STATUSES.COMPLETED;
|
|
136
318
|
state.completedSteps.push(current.id);
|
|
319
|
+
state.history.push({ event: 'step_completed', step: current.id, layer: state.current_layer, frame: state.current_frame, time: Date.now() });
|
|
137
320
|
|
|
138
|
-
// Chuyển sang bước tiếp theo
|
|
139
321
|
state.currentStepIndex++;
|
|
140
322
|
|
|
141
323
|
const next = state.steps[state.currentStepIndex] || null;
|
|
142
324
|
if (next) {
|
|
143
|
-
next.status =
|
|
325
|
+
next.status = STATUSES.IN_PROGRESS;
|
|
326
|
+
state.step_status = STATUSES.IN_PROGRESS;
|
|
327
|
+
saveState();
|
|
144
328
|
return {
|
|
145
329
|
success: true,
|
|
146
330
|
completedStep: current,
|
|
147
331
|
nextStep: next,
|
|
148
|
-
message: `✅
|
|
332
|
+
message: `✅ Completed: "${current.label}". Next step: "${next.label}" — ${next.description}`,
|
|
149
333
|
};
|
|
150
334
|
}
|
|
151
335
|
|
|
336
|
+
state.step_status = STATUSES.COMPLETED;
|
|
337
|
+
saveState();
|
|
152
338
|
return {
|
|
153
339
|
success: true,
|
|
154
340
|
completedStep: current,
|
|
155
341
|
nextStep: null,
|
|
156
|
-
message: `🎉
|
|
342
|
+
message: `🎉 All steps completed! The task is ready.`,
|
|
157
343
|
};
|
|
158
344
|
}
|
|
159
345
|
|
|
160
346
|
/**
|
|
161
|
-
* Reset workflow
|
|
347
|
+
* Reset workflow to the beginning
|
|
162
348
|
* @param {string} sessionId
|
|
163
349
|
*/
|
|
164
350
|
function reset(sessionId) {
|
|
351
|
+
const state = sessions.get(sessionId);
|
|
352
|
+
const mode = state ? state.current_mode : MODES.CREATE_IMAGE;
|
|
165
353
|
sessions.delete(sessionId);
|
|
166
|
-
|
|
354
|
+
saveState();
|
|
355
|
+
return start(sessionId, mode);
|
|
167
356
|
}
|
|
168
357
|
|
|
169
358
|
/**
|
|
170
|
-
*
|
|
359
|
+
* Get text-based progress report for the agent
|
|
171
360
|
* @param {string} sessionId
|
|
172
361
|
* @returns {string}
|
|
173
362
|
*/
|
|
174
363
|
function getProgressReport(sessionId) {
|
|
175
364
|
const state = sessions.get(sessionId);
|
|
176
|
-
if (!state) return '
|
|
365
|
+
if (!state) return 'Workflow has not started.';
|
|
177
366
|
|
|
178
|
-
const lines = [
|
|
367
|
+
const lines = [
|
|
368
|
+
`📋 **Workflow Progress: [Mode: ${state.current_mode}]**`,
|
|
369
|
+
`Layer: ${state.current_layer} | Frame: ${state.current_frame !== null ? state.current_frame : 'N/A'}`,
|
|
370
|
+
`Phase: ${state.current_phase !== null ? state.current_phase : 'N/A'}`,
|
|
371
|
+
`Animation Meta: FPS=${state.animation_meta.fps || 'N/A'}, Frames=${state.animation_meta.total_frames || 'N/A'}, Loop=${state.animation_meta.loop}`,
|
|
372
|
+
`Status: ${state.step_status}`,
|
|
373
|
+
`User Approval Required: ${state.user_approval_required ? 'YES' : 'NO'}`,
|
|
374
|
+
state.user_input_request && state.user_input_request.status === 'WAITING_FOR_USER' ? `⚠️ PENDING REQUEST: ${state.user_input_request.type}` : '',
|
|
375
|
+
`\n**Steps:**`
|
|
376
|
+
];
|
|
377
|
+
|
|
179
378
|
state.steps.forEach((step, i) => {
|
|
180
|
-
const icon = step.status ===
|
|
181
|
-
step.status ===
|
|
182
|
-
const marker = i === state.currentStepIndex ? ' ←
|
|
379
|
+
const icon = step.status === STATUSES.COMPLETED ? '✅' :
|
|
380
|
+
step.status === STATUSES.IN_PROGRESS ? '🔄' : '⬜';
|
|
381
|
+
const marker = i === state.currentStepIndex ? ' ← **IN PROGRESS**' : '';
|
|
183
382
|
lines.push(`${icon} **${step.label}**${marker}`);
|
|
184
|
-
if (step.status ===
|
|
383
|
+
if (step.status === STATUSES.IN_PROGRESS || i === state.currentStepIndex) {
|
|
185
384
|
lines.push(` → ${step.description}`);
|
|
186
385
|
}
|
|
187
386
|
});
|
|
@@ -197,4 +396,11 @@ module.exports = {
|
|
|
197
396
|
advance,
|
|
198
397
|
reset,
|
|
199
398
|
getProgressReport,
|
|
399
|
+
setApprovalRequired,
|
|
400
|
+
createInputRequest,
|
|
401
|
+
completeInputRequest,
|
|
402
|
+
setLayerAndFrame,
|
|
403
|
+
setPhase,
|
|
404
|
+
setAnimationMeta,
|
|
405
|
+
setStepFlag
|
|
200
406
|
};
|