@s_s/harmonia 1.2.0 → 1.4.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/README.md +140 -392
- package/build/cli/setup.d.ts +4 -2
- package/build/cli/setup.js +44 -18
- package/build/cli/setup.js.map +1 -1
- package/build/core/action-registry.d.ts +36 -0
- package/build/core/action-registry.js +53 -0
- package/build/core/action-registry.js.map +1 -0
- package/build/core/artifacts.d.ts +66 -0
- package/build/core/artifacts.js +178 -0
- package/build/core/artifacts.js.map +1 -0
- package/build/core/dispatch.d.ts +18 -11
- package/build/core/dispatch.js +43 -33
- package/build/core/dispatch.js.map +1 -1
- package/build/core/issues.d.ts +37 -0
- package/build/core/issues.js +100 -0
- package/build/core/issues.js.map +1 -0
- package/build/core/overrides.d.ts +19 -26
- package/build/core/overrides.js +32 -98
- package/build/core/overrides.js.map +1 -1
- package/build/core/plugin.d.ts +86 -0
- package/build/core/plugin.js +332 -0
- package/build/core/plugin.js.map +1 -0
- package/build/core/registry.d.ts +36 -3
- package/build/core/registry.js +63 -5
- package/build/core/registry.js.map +1 -1
- package/build/core/reviews.d.ts +13 -13
- package/build/core/reviews.js +31 -32
- package/build/core/reviews.js.map +1 -1
- package/build/core/schema.d.ts +43 -15
- package/build/core/schema.js +124 -20
- package/build/core/schema.js.map +1 -1
- package/build/core/state.d.ts +29 -22
- package/build/core/state.js +49 -81
- package/build/core/state.js.map +1 -1
- package/build/core/steps.d.ts +15 -15
- package/build/core/steps.js +32 -33
- package/build/core/steps.js.map +1 -1
- package/build/core/tree-utils.d.ts +52 -0
- package/build/core/tree-utils.js +226 -0
- package/build/core/tree-utils.js.map +1 -0
- package/build/core/types.d.ts +417 -117
- package/build/core/types.js +15 -1
- package/build/core/types.js.map +1 -1
- package/build/core/workflow-engine.d.ts +68 -0
- package/build/core/workflow-engine.js +821 -0
- package/build/core/workflow-engine.js.map +1 -0
- package/build/core/workflow-validator.d.ts +22 -0
- package/build/core/workflow-validator.js +489 -0
- package/build/core/workflow-validator.js.map +1 -0
- package/build/index.js +28 -25
- package/build/index.js.map +1 -1
- package/build/setup/inject.d.ts +4 -4
- package/build/setup/inject.js +6 -6
- package/build/setup/inject.js.map +1 -1
- package/build/setup/templates.d.ts +9 -7
- package/build/setup/templates.js +68 -103
- package/build/setup/templates.js.map +1 -1
- package/build/tools/artifact-approve.d.ts +8 -0
- package/build/tools/artifact-approve.js +94 -0
- package/build/tools/artifact-approve.js.map +1 -0
- package/build/tools/artifact-schema.d.ts +12 -0
- package/build/tools/artifact-schema.js +148 -0
- package/build/tools/artifact-schema.js.map +1 -0
- package/build/tools/artifact-tools.d.ts +18 -0
- package/build/tools/artifact-tools.js +465 -0
- package/build/tools/artifact-tools.js.map +1 -0
- package/build/tools/{report-dispatch.d.ts → dispatch-report.d.ts} +7 -3
- package/build/tools/dispatch-report.js +261 -0
- package/build/tools/dispatch-report.js.map +1 -0
- package/build/tools/engine-helpers.d.ts +41 -0
- package/build/tools/engine-helpers.js +182 -0
- package/build/tools/engine-helpers.js.map +1 -0
- package/build/tools/get-project-status.d.ts +6 -4
- package/build/tools/get-project-status.js +308 -246
- package/build/tools/get-project-status.js.map +1 -1
- package/build/tools/get-role-prompt.d.ts +1 -1
- package/build/tools/get-role-prompt.js +7 -41
- package/build/tools/get-role-prompt.js.map +1 -1
- package/build/tools/issue-tools.d.ts +10 -0
- package/build/tools/issue-tools.js +169 -0
- package/build/tools/issue-tools.js.map +1 -0
- package/build/tools/iteration-start.d.ts +7 -4
- package/build/tools/iteration-start.js +51 -20
- package/build/tools/iteration-start.js.map +1 -1
- package/build/tools/loop-done.d.ts +11 -0
- package/build/tools/loop-done.js +109 -0
- package/build/tools/loop-done.js.map +1 -0
- package/build/tools/patch-start.d.ts +16 -0
- package/build/tools/patch-start.js +122 -0
- package/build/tools/patch-start.js.map +1 -0
- package/build/tools/project-init.d.ts +5 -5
- package/build/tools/project-init.js +47 -18
- package/build/tools/project-init.js.map +1 -1
- package/build/tools/role-dispatch.d.ts +55 -0
- package/build/tools/role-dispatch.js +508 -0
- package/build/tools/role-dispatch.js.map +1 -0
- package/build/tools/utils.d.ts +40 -0
- package/build/tools/utils.js +97 -0
- package/build/tools/utils.js.map +1 -0
- package/package.json +1 -1
- package/{build/hooks/claude-code.js → workflows/dev/hooks/claude.js} +34 -23
- package/{build → workflows/dev}/hooks/content.js +27 -18
- package/workflows/dev/hooks/index.js +52 -0
- package/{build → workflows/dev}/hooks/openclaw.js +31 -20
- package/{build → workflows/dev}/hooks/opencode.js +31 -20
- package/workflows/dev/roles/architect.md +68 -28
- package/workflows/dev/roles/coordinator.md +103 -0
- package/workflows/dev/roles/developer.md +5 -5
- package/workflows/dev/roles/tester.md +19 -19
- package/workflows/dev/schemas/api-contract.json +42 -0
- package/workflows/dev/schemas/api-design.json +30 -13
- package/workflows/dev/schemas/data-model.json +20 -7
- package/workflows/dev/schemas/prd.completeness-check.json +6 -5
- package/workflows/dev/schemas/prd.draft.json +13 -5
- package/workflows/dev/schemas/prd.final.json +34 -11
- package/workflows/dev/schemas/prd.json +29 -11
- package/workflows/dev/schemas/prd.requirements.json +6 -5
- package/workflows/dev/schemas/prototype.json +6 -2
- package/workflows/dev/schemas/task-breakdown.coarse.json +4 -3
- package/workflows/dev/schemas/task-breakdown.dependencies.json +5 -4
- package/workflows/dev/schemas/task-breakdown.detailed.json +8 -3
- package/workflows/dev/schemas/task-breakdown.final.json +8 -3
- package/workflows/dev/schemas/task-breakdown.json +8 -3
- package/workflows/dev/schemas/tech-design.analysis.json +6 -5
- package/workflows/dev/schemas/tech-design.draft.json +14 -5
- package/workflows/dev/schemas/tech-design.final.json +39 -13
- package/workflows/dev/schemas/tech-design.json +34 -13
- package/workflows/dev/schemas/tech-design.research.json +21 -0
- package/workflows/dev/schemas/test-plan.json +17 -7
- package/workflows/dev/schemas/test-report.json +26 -9
- package/workflows/dev/schemas/user-stories.json +7 -3
- package/workflows/dev/tools/index.js +23 -0
- package/workflows/dev/workflow.json +234 -101
- package/build/core/docs.d.ts +0 -32
- package/build/core/docs.js +0 -91
- package/build/core/docs.js.map +0 -1
- package/build/core/workflow.d.ts +0 -33
- package/build/core/workflow.js +0 -140
- package/build/core/workflow.js.map +0 -1
- package/build/hooks/claude-code.d.ts +0 -20
- package/build/hooks/claude-code.js.map +0 -1
- package/build/hooks/content.d.ts +0 -43
- package/build/hooks/content.js.map +0 -1
- package/build/hooks/install.d.ts +0 -40
- package/build/hooks/install.js +0 -63
- package/build/hooks/install.js.map +0 -1
- package/build/hooks/openclaw.d.ts +0 -24
- package/build/hooks/openclaw.js.map +0 -1
- package/build/hooks/opencode.d.ts +0 -29
- package/build/hooks/opencode.js.map +0 -1
- package/build/tools/approve-doc.d.ts +0 -6
- package/build/tools/approve-doc.js +0 -108
- package/build/tools/approve-doc.js.map +0 -1
- package/build/tools/dispatch-role.d.ts +0 -16
- package/build/tools/dispatch-role.js +0 -277
- package/build/tools/dispatch-role.js.map +0 -1
- package/build/tools/doc-tools.d.ts +0 -16
- package/build/tools/doc-tools.js +0 -389
- package/build/tools/doc-tools.js.map +0 -1
- package/build/tools/override-tools.d.ts +0 -6
- package/build/tools/override-tools.js +0 -129
- package/build/tools/override-tools.js.map +0 -1
- package/build/tools/report-dispatch.js +0 -194
- package/build/tools/report-dispatch.js.map +0 -1
- package/build/tools/set-scale.d.ts +0 -6
- package/build/tools/set-scale.js +0 -107
- package/build/tools/set-scale.js.map +0 -1
- package/build/tools/setup-project.d.ts +0 -8
- package/build/tools/setup-project.js +0 -116
- package/build/tools/setup-project.js.map +0 -1
- package/build/tools/update-phase.d.ts +0 -12
- package/build/tools/update-phase.js +0 -159
- package/build/tools/update-phase.js.map +0 -1
- package/workflows/dev/roles/pm.md +0 -99
- package/workflows/dev/schemas/deploy.json +0 -20
- package/workflows/dev/schemas/fsd.json +0 -25
- package/workflows/dev/schemas/project-plan.json +0 -20
- package/workflows/dev/schemas/retrospective.json +0 -20
- package/workflows/dev/schemas/risk-assessment.json +0 -15
- package/workflows/dev/schemas/tech-design.api-contract.json +0 -20
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow engine — core state machine for node-based workflow execution.
|
|
3
|
+
*
|
|
4
|
+
* This module handles:
|
|
5
|
+
* - Node state initialization from a WorkflowDefinition
|
|
6
|
+
* - State transitions based on WorkflowEvents
|
|
7
|
+
* - NextAction computation (telling the coordinator what to do)
|
|
8
|
+
* - Gate evaluation
|
|
9
|
+
* - Goto execution (state reset + retry tracking)
|
|
10
|
+
* - Failure handling (onFailed, bubbling, onExhausted)
|
|
11
|
+
*
|
|
12
|
+
* Key insight: Core is passive (MCP server). Every interaction is
|
|
13
|
+
* coordinator-driven. Engine computes state changes and returns nextAction,
|
|
14
|
+
* but never proactively pushes the workflow forward.
|
|
15
|
+
*/
|
|
16
|
+
import { findNodeInTree, collectAllNodeIds, findParent, collectSubsequentNodeIds, } from './tree-utils.js';
|
|
17
|
+
// ─── Node State Initialization ───
|
|
18
|
+
/**
|
|
19
|
+
* Initialize node states for all nodes in a workflow definition.
|
|
20
|
+
* All nodes start as 'pending', except the first actionable node
|
|
21
|
+
* which will be activated when the workflow begins.
|
|
22
|
+
*/
|
|
23
|
+
export function initNodeStates(definition) {
|
|
24
|
+
const states = {};
|
|
25
|
+
collectNodeStates(definition.root, states);
|
|
26
|
+
if (definition.floatingNodes) {
|
|
27
|
+
for (const fn of definition.floatingNodes) {
|
|
28
|
+
states[fn.id] = createNodeState(fn.id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return states;
|
|
32
|
+
}
|
|
33
|
+
function collectNodeStates(node, states) {
|
|
34
|
+
states[node.id] = createNodeState(node.id);
|
|
35
|
+
switch (node.type) {
|
|
36
|
+
case 'sequence':
|
|
37
|
+
case 'parallel':
|
|
38
|
+
for (const child of node.children) {
|
|
39
|
+
collectNodeStates(child, states);
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case 'gate':
|
|
43
|
+
collectNodeStates(node.pass, states);
|
|
44
|
+
if ('type' in node.fail) {
|
|
45
|
+
collectNodeStates(node.fail, states);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'loop':
|
|
49
|
+
collectNodeStates(node.body, states);
|
|
50
|
+
break;
|
|
51
|
+
case 'task':
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function createNodeState(id) {
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
status: 'pending',
|
|
59
|
+
retryCount: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// ─── Start Workflow ───
|
|
63
|
+
/**
|
|
64
|
+
* Start the workflow — activate the root node and return the first nextAction.
|
|
65
|
+
*/
|
|
66
|
+
export function startWorkflow(definition, state, context) {
|
|
67
|
+
const now = new Date().toISOString();
|
|
68
|
+
const newState = { ...state, nodes: { ...state.nodes }, updatedAt: now };
|
|
69
|
+
return activateNode(definition.root, definition, newState, context);
|
|
70
|
+
}
|
|
71
|
+
// ─── Compute Next Action ───
|
|
72
|
+
/**
|
|
73
|
+
* Process a workflow event and compute the next action.
|
|
74
|
+
* This is the main entry point for all state transitions.
|
|
75
|
+
*/
|
|
76
|
+
export function computeNextAction(definition, state, event, context) {
|
|
77
|
+
const now = new Date().toISOString();
|
|
78
|
+
const newState = { ...state, nodes: { ...state.nodes }, updatedAt: now };
|
|
79
|
+
switch (event.type) {
|
|
80
|
+
case 'node_completed':
|
|
81
|
+
return completeNode(newState, definition, event.nodeId, context, event.result);
|
|
82
|
+
case 'node_failed':
|
|
83
|
+
return failNode(newState, definition, event.nodeId, event.error, context);
|
|
84
|
+
case 'artifact_written':
|
|
85
|
+
case 'artifact_approved':
|
|
86
|
+
// These events might trigger gate re-evaluation
|
|
87
|
+
return reevaluateGates(newState, definition, context);
|
|
88
|
+
case 'dispatch_requested':
|
|
89
|
+
return handleDispatchRequest(newState, definition, event.nodeId, context);
|
|
90
|
+
case 'loop_done': {
|
|
91
|
+
// Mark the loop as done — it will terminate after current iteration completes
|
|
92
|
+
const loopState = newState.nodes[event.nodeId];
|
|
93
|
+
if (!loopState || loopState.status !== 'active') {
|
|
94
|
+
return {
|
|
95
|
+
state: newState,
|
|
96
|
+
nextAction: {
|
|
97
|
+
type: 'wait',
|
|
98
|
+
instructions: `Loop node "${event.nodeId}" is not active (current: ${loopState?.status ?? 'unknown'})`,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
loopState.done = true;
|
|
103
|
+
newState.nodes[event.nodeId] = loopState;
|
|
104
|
+
return {
|
|
105
|
+
state: newState,
|
|
106
|
+
nextAction: {
|
|
107
|
+
type: 'wait',
|
|
108
|
+
nodeId: event.nodeId,
|
|
109
|
+
instructions: `Loop "${event.nodeId}" marked for termination. Current iteration (${loopState.currentIteration}) will complete, then the loop will end.`,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case 'query_status':
|
|
114
|
+
return { state: newState, nextAction: computeStatusAction(newState, definition, context) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ─── Node Activation ───
|
|
118
|
+
/**
|
|
119
|
+
* Activate a node — set it to 'active' and determine what action to take.
|
|
120
|
+
*/
|
|
121
|
+
function activateNode(node, definition, state, context, gateResults) {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
state.nodes[node.id] = {
|
|
124
|
+
...state.nodes[node.id],
|
|
125
|
+
status: 'active',
|
|
126
|
+
startedAt: now,
|
|
127
|
+
};
|
|
128
|
+
switch (node.type) {
|
|
129
|
+
case 'task':
|
|
130
|
+
return activateTask(node, state, context, gateResults);
|
|
131
|
+
case 'sequence':
|
|
132
|
+
return activateSequence(node, definition, state, context);
|
|
133
|
+
case 'parallel':
|
|
134
|
+
return activateParallel(node, definition, state, context);
|
|
135
|
+
case 'gate':
|
|
136
|
+
return activateGate(node, definition, state, context);
|
|
137
|
+
case 'loop':
|
|
138
|
+
return activateLoop(node, definition, state, context);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function activateTask(node, state, context, gateResults) {
|
|
142
|
+
state.activeNodeId = node.id;
|
|
143
|
+
const rolePrompt = context.getRolePrompt ? context.getRolePrompt(node.role, node.id, gateResults) : undefined;
|
|
144
|
+
return {
|
|
145
|
+
state,
|
|
146
|
+
nextAction: {
|
|
147
|
+
type: 'dispatch',
|
|
148
|
+
nodeId: node.id,
|
|
149
|
+
role: node.role,
|
|
150
|
+
instructions: `Dispatch role "${node.role}" for task "${node.id}"`,
|
|
151
|
+
rolePrompt,
|
|
152
|
+
gateResults,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function activateSequence(node, definition, state, context) {
|
|
157
|
+
if (node.children.length === 0) {
|
|
158
|
+
// Empty sequence — immediately complete
|
|
159
|
+
return markCompleted(node, definition, state, context);
|
|
160
|
+
}
|
|
161
|
+
// Activate the first child
|
|
162
|
+
return activateNode(node.children[0], definition, state, context);
|
|
163
|
+
}
|
|
164
|
+
function activateParallel(node, definition, state, context) {
|
|
165
|
+
if (node.children.length === 0) {
|
|
166
|
+
return markCompleted(node, definition, state, context);
|
|
167
|
+
}
|
|
168
|
+
// Activate all children simultaneously
|
|
169
|
+
for (const child of node.children) {
|
|
170
|
+
const result = activateNode(child, definition, state, context);
|
|
171
|
+
state = result.state;
|
|
172
|
+
}
|
|
173
|
+
// Build parallel dispatch list with each child's nodeId and role
|
|
174
|
+
const dispatchActions = node.children
|
|
175
|
+
.filter((c) => c.type === 'task')
|
|
176
|
+
.map((c) => ({
|
|
177
|
+
nodeId: c.id,
|
|
178
|
+
role: c.role,
|
|
179
|
+
}));
|
|
180
|
+
return {
|
|
181
|
+
state,
|
|
182
|
+
nextAction: {
|
|
183
|
+
type: 'dispatch',
|
|
184
|
+
nodeId: node.id,
|
|
185
|
+
instructions: `Parallel execution: dispatch ${dispatchActions.length} tasks simultaneously: ${dispatchActions.map((d) => `${d.role}(${d.nodeId})`).join(', ')}`,
|
|
186
|
+
role: dispatchActions[0]?.role,
|
|
187
|
+
parallelDispatch: dispatchActions,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function activateGate(node, definition, state, context) {
|
|
192
|
+
const evaluation = evaluateGate(node, context.gate);
|
|
193
|
+
if (evaluation.passed) {
|
|
194
|
+
// Gate passed — activate pass branch
|
|
195
|
+
state.nodes[node.id] = {
|
|
196
|
+
...state.nodes[node.id],
|
|
197
|
+
status: 'completed',
|
|
198
|
+
completedAt: new Date().toISOString(),
|
|
199
|
+
};
|
|
200
|
+
return activateNode(node.pass, definition, state, context);
|
|
201
|
+
}
|
|
202
|
+
// Gate failed
|
|
203
|
+
if ('goto' in node.fail && !('type' in node.fail)) {
|
|
204
|
+
// fail is a GotoTarget
|
|
205
|
+
const gotoTarget = node.fail;
|
|
206
|
+
return handleGoto(gotoTarget, node.id, definition, state, context, evaluation);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// fail is an inline WorkflowNode
|
|
210
|
+
state.nodes[node.id] = {
|
|
211
|
+
...state.nodes[node.id],
|
|
212
|
+
status: 'completed',
|
|
213
|
+
completedAt: new Date().toISOString(),
|
|
214
|
+
};
|
|
215
|
+
return activateNode(node.fail, definition, state, context, evaluation);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function activateLoop(node, definition, state, context) {
|
|
219
|
+
// Initialize loop state with iteration tracking
|
|
220
|
+
const loopState = {
|
|
221
|
+
...state.nodes[node.id],
|
|
222
|
+
status: 'active',
|
|
223
|
+
startedAt: new Date().toISOString(),
|
|
224
|
+
currentIteration: 0,
|
|
225
|
+
done: false,
|
|
226
|
+
};
|
|
227
|
+
state.nodes[node.id] = loopState;
|
|
228
|
+
// Activate the body node
|
|
229
|
+
return activateNode(node.body, definition, state, context);
|
|
230
|
+
}
|
|
231
|
+
// ─── Node Completion ───
|
|
232
|
+
/**
|
|
233
|
+
* Handle node completion: mark as completed, advance to next node.
|
|
234
|
+
*/
|
|
235
|
+
export function completeNode(state, definition, nodeId, context, result) {
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
state.nodes[nodeId] = {
|
|
238
|
+
...state.nodes[nodeId],
|
|
239
|
+
status: 'completed',
|
|
240
|
+
completedAt: now,
|
|
241
|
+
};
|
|
242
|
+
// Find the parent node to determine what happens next
|
|
243
|
+
const parentInfo = findParent(definition.root, nodeId);
|
|
244
|
+
if (!parentInfo) {
|
|
245
|
+
// This is the root node completing — workflow is done
|
|
246
|
+
return {
|
|
247
|
+
state,
|
|
248
|
+
nextAction: {
|
|
249
|
+
type: 'completed',
|
|
250
|
+
instructions: 'Workflow completed successfully',
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const { parent } = parentInfo;
|
|
255
|
+
switch (parent.type) {
|
|
256
|
+
case 'sequence':
|
|
257
|
+
return handleSequenceChildComplete(parent, nodeId, definition, state, context);
|
|
258
|
+
case 'parallel':
|
|
259
|
+
return handleParallelChildComplete(parent, definition, state, context);
|
|
260
|
+
case 'gate':
|
|
261
|
+
// Pass/fail branch task completed — gate's work is done
|
|
262
|
+
// Mark gate as completed and check parent
|
|
263
|
+
return markCompleted(parent, definition, state, context);
|
|
264
|
+
case 'loop':
|
|
265
|
+
return handleLoopBodyComplete(parent, definition, state, context);
|
|
266
|
+
default:
|
|
267
|
+
return {
|
|
268
|
+
state,
|
|
269
|
+
nextAction: {
|
|
270
|
+
type: 'wait',
|
|
271
|
+
instructions: `Node "${nodeId}" completed, but could not determine next step`,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function handleSequenceChildComplete(sequence, completedChildId, definition, state, context) {
|
|
277
|
+
const childIndex = sequence.children.findIndex((c) => c.id === completedChildId);
|
|
278
|
+
if (childIndex < 0) {
|
|
279
|
+
return {
|
|
280
|
+
state,
|
|
281
|
+
nextAction: {
|
|
282
|
+
type: 'wait',
|
|
283
|
+
instructions: `Child "${completedChildId}" not found in sequence "${sequence.id}"`,
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
// Check if there's a next child
|
|
288
|
+
const nextIndex = childIndex + 1;
|
|
289
|
+
if (nextIndex < sequence.children.length) {
|
|
290
|
+
// Activate next child
|
|
291
|
+
return activateNode(sequence.children[nextIndex], definition, state, context);
|
|
292
|
+
}
|
|
293
|
+
// All children completed — mark sequence as completed
|
|
294
|
+
return markCompleted(sequence, definition, state, context);
|
|
295
|
+
}
|
|
296
|
+
function handleParallelChildComplete(parallel, definition, state, context) {
|
|
297
|
+
// Check if all children are done
|
|
298
|
+
const allDone = parallel.children.every((c) => {
|
|
299
|
+
const childState = state.nodes[c.id];
|
|
300
|
+
return childState?.status === 'completed' || childState?.status === 'failed';
|
|
301
|
+
});
|
|
302
|
+
if (!allDone) {
|
|
303
|
+
return {
|
|
304
|
+
state,
|
|
305
|
+
nextAction: {
|
|
306
|
+
type: 'wait',
|
|
307
|
+
nodeId: parallel.id,
|
|
308
|
+
instructions: `Parallel node "${parallel.id}": waiting for remaining tasks to complete`,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Check for failures
|
|
313
|
+
const failures = parallel.children.filter((c) => state.nodes[c.id]?.status === 'failed');
|
|
314
|
+
if (failures.length > 0) {
|
|
315
|
+
// Parallel has failures — treat as failed
|
|
316
|
+
return failNode(state, definition, parallel.id, `${failures.length} child node(s) failed: ${failures.map((f) => f.id).join(', ')}`, context);
|
|
317
|
+
}
|
|
318
|
+
// All succeeded
|
|
319
|
+
return markCompleted(parallel, definition, state, context);
|
|
320
|
+
}
|
|
321
|
+
function handleLoopBodyComplete(loop, definition, state, context) {
|
|
322
|
+
const loopState = state.nodes[loop.id];
|
|
323
|
+
// Check if loop_done has been called — terminate loop
|
|
324
|
+
if (loopState.done) {
|
|
325
|
+
return markCompleted(loop, definition, state, context);
|
|
326
|
+
}
|
|
327
|
+
// Check if maxIterations reached — safety cap, mark as failed
|
|
328
|
+
const nextIteration = loopState.currentIteration + 1;
|
|
329
|
+
if (nextIteration >= loop.maxIterations) {
|
|
330
|
+
return failNode(state, definition, loop.id, `Loop "${loop.id}" reached maxIterations (${loop.maxIterations}) without loop_done being called`, context);
|
|
331
|
+
}
|
|
332
|
+
// Continue: increment iteration, reset body, re-activate
|
|
333
|
+
loopState.currentIteration = nextIteration;
|
|
334
|
+
state.nodes[loop.id] = loopState;
|
|
335
|
+
// Reset all body nodes to pending
|
|
336
|
+
resetLoopBody(state, loop);
|
|
337
|
+
// Re-activate body
|
|
338
|
+
return activateNode(loop.body, definition, state, context);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Reset all nodes within a loop's body subtree to pending state.
|
|
342
|
+
* Used between loop iterations.
|
|
343
|
+
*/
|
|
344
|
+
function resetLoopBody(state, loopNode) {
|
|
345
|
+
const bodyIds = new Set();
|
|
346
|
+
collectAllNodeIds(loopNode.body, bodyIds);
|
|
347
|
+
bodyIds.forEach((id) => {
|
|
348
|
+
const existing = state.nodes[id];
|
|
349
|
+
if (existing) {
|
|
350
|
+
state.nodes[id] = {
|
|
351
|
+
id,
|
|
352
|
+
status: 'pending',
|
|
353
|
+
retryCount: 0,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Mark a node as completed and propagate up.
|
|
360
|
+
*/
|
|
361
|
+
function markCompleted(node, definition, state, context) {
|
|
362
|
+
const now = new Date().toISOString();
|
|
363
|
+
state.nodes[node.id] = {
|
|
364
|
+
...state.nodes[node.id],
|
|
365
|
+
status: 'completed',
|
|
366
|
+
completedAt: now,
|
|
367
|
+
};
|
|
368
|
+
// Propagate up — find parent and handle completion there
|
|
369
|
+
const parentInfo = findParent(definition.root, node.id);
|
|
370
|
+
if (!parentInfo) {
|
|
371
|
+
// Root completed
|
|
372
|
+
return {
|
|
373
|
+
state,
|
|
374
|
+
nextAction: {
|
|
375
|
+
type: 'completed',
|
|
376
|
+
instructions: 'Workflow completed successfully',
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return completeNode(state, definition, node.id, context);
|
|
381
|
+
}
|
|
382
|
+
// ─── Node Failure Handling ───
|
|
383
|
+
/**
|
|
384
|
+
* Handle node failure: check onFailed handler, process goto or bubble up.
|
|
385
|
+
*
|
|
386
|
+
* Failure flow:
|
|
387
|
+
* 1. If node has onFailed with goto → try goto (with retry tracking)
|
|
388
|
+
* 2. If goto maxRetries exhausted → jump to onExhausted floating node
|
|
389
|
+
* 3. If no onFailed → bubble failure up to parent
|
|
390
|
+
*/
|
|
391
|
+
function failNode(state, definition, nodeId, error, context) {
|
|
392
|
+
const now = new Date().toISOString();
|
|
393
|
+
state.nodes[nodeId] = {
|
|
394
|
+
...state.nodes[nodeId],
|
|
395
|
+
status: 'failed',
|
|
396
|
+
completedAt: now,
|
|
397
|
+
error,
|
|
398
|
+
};
|
|
399
|
+
// Find the node to check for onFailed
|
|
400
|
+
const node = findNode(definition.root, nodeId, definition.floatingNodes);
|
|
401
|
+
if (!node) {
|
|
402
|
+
return {
|
|
403
|
+
state,
|
|
404
|
+
nextAction: {
|
|
405
|
+
type: 'wait',
|
|
406
|
+
instructions: `Node "${nodeId}" failed but could not be found in definition: ${error}`,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// Check if the node has onFailed handler
|
|
411
|
+
const failureHandler = getFailureHandler(node);
|
|
412
|
+
if (failureHandler) {
|
|
413
|
+
return handleFailureHandler(failureHandler, nodeId, definition, state, context, error);
|
|
414
|
+
}
|
|
415
|
+
// No onFailed — bubble failure up to parent
|
|
416
|
+
return bubbleFailure(nodeId, error, definition, state, context);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get the failure handler from a node (only task and parallel have onFailed).
|
|
420
|
+
*/
|
|
421
|
+
function getFailureHandler(node) {
|
|
422
|
+
if (node.type === 'task' || node.type === 'parallel' || node.type === 'loop') {
|
|
423
|
+
return node.onFailed;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Process a FailureHandler (goto with retry tracking).
|
|
429
|
+
*/
|
|
430
|
+
function handleFailureHandler(handler, failedNodeId, definition, state, context, error) {
|
|
431
|
+
// Check retry count for the goto target
|
|
432
|
+
const targetState = state.nodes[handler.goto];
|
|
433
|
+
const currentRetries = targetState?.retryCount ?? 0;
|
|
434
|
+
if (handler.maxRetries !== undefined && currentRetries >= handler.maxRetries) {
|
|
435
|
+
// Retries exhausted
|
|
436
|
+
if (handler.onExhausted) {
|
|
437
|
+
return activateFloatingNode(handler.onExhausted, definition, state, context, error);
|
|
438
|
+
}
|
|
439
|
+
// No onExhausted — bubble up
|
|
440
|
+
return bubbleFailure(failedNodeId, error, definition, state, context);
|
|
441
|
+
}
|
|
442
|
+
// Execute goto
|
|
443
|
+
return handleGoto({ goto: handler.goto, maxRetries: handler.maxRetries, onExhausted: handler.onExhausted }, failedNodeId, definition, state, context);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Bubble a failure up to the parent node.
|
|
447
|
+
*/
|
|
448
|
+
function bubbleFailure(failedNodeId, error, definition, state, context) {
|
|
449
|
+
const parentInfo = findParent(definition.root, failedNodeId);
|
|
450
|
+
if (!parentInfo) {
|
|
451
|
+
// Root node failed — workflow failed
|
|
452
|
+
return {
|
|
453
|
+
state,
|
|
454
|
+
nextAction: {
|
|
455
|
+
type: 'failed',
|
|
456
|
+
instructions: `Workflow failed: ${error}`,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const { parent } = parentInfo;
|
|
461
|
+
if (parent.type === 'parallel') {
|
|
462
|
+
if (parent.failStrategy === 'fail-fast') {
|
|
463
|
+
// Cancel remaining active children
|
|
464
|
+
for (const child of parent.children) {
|
|
465
|
+
const childState = state.nodes[child.id];
|
|
466
|
+
if (childState && childState.status === 'active') {
|
|
467
|
+
state.nodes[child.id] = {
|
|
468
|
+
...childState,
|
|
469
|
+
status: 'cancelled',
|
|
470
|
+
completedAt: new Date().toISOString(),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Fail the parallel node itself
|
|
475
|
+
return failNode(state, definition, parent.id, error, context);
|
|
476
|
+
}
|
|
477
|
+
// wait-all: check if all children are done
|
|
478
|
+
const allDone = parent.children.every((c) => {
|
|
479
|
+
const cs = state.nodes[c.id];
|
|
480
|
+
return cs?.status === 'completed' || cs?.status === 'failed' || cs?.status === 'cancelled';
|
|
481
|
+
});
|
|
482
|
+
if (allDone) {
|
|
483
|
+
return failNode(state, definition, parent.id, error, context);
|
|
484
|
+
}
|
|
485
|
+
// Still waiting for others
|
|
486
|
+
return {
|
|
487
|
+
state,
|
|
488
|
+
nextAction: {
|
|
489
|
+
type: 'wait',
|
|
490
|
+
nodeId: parent.id,
|
|
491
|
+
instructions: `Parallel node "${parent.id}": child "${failedNodeId}" failed, waiting for remaining tasks (wait-all strategy)`,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// For sequence parents, failure in a child means the sequence fails
|
|
496
|
+
return failNode(state, definition, parent.id, error, context);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Activate a floating node (used by onExhausted).
|
|
500
|
+
*/
|
|
501
|
+
function activateFloatingNode(floatingNodeId, definition, state, context, error) {
|
|
502
|
+
const floatingNode = definition.floatingNodes?.find((fn) => fn.id === floatingNodeId);
|
|
503
|
+
if (!floatingNode) {
|
|
504
|
+
return {
|
|
505
|
+
state,
|
|
506
|
+
nextAction: {
|
|
507
|
+
type: 'wait',
|
|
508
|
+
instructions: `Floating node "${floatingNodeId}" not found. Retries exhausted. Original error: ${error}`,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return activateNode(floatingNode, definition, state, context);
|
|
513
|
+
}
|
|
514
|
+
// ─── Goto Handling ───
|
|
515
|
+
/**
|
|
516
|
+
* Execute a goto: reset the target node and all subsequent nodes to pending,
|
|
517
|
+
* increment retry count on the target, then re-activate the target.
|
|
518
|
+
*/
|
|
519
|
+
function handleGoto(gotoTarget, sourceNodeId, definition, state, context, gateResults) {
|
|
520
|
+
const targetId = gotoTarget.goto;
|
|
521
|
+
// Check retry limit before proceeding
|
|
522
|
+
const targetState = state.nodes[targetId];
|
|
523
|
+
const currentRetries = targetState?.retryCount ?? 0;
|
|
524
|
+
if (gotoTarget.maxRetries !== undefined && currentRetries >= gotoTarget.maxRetries) {
|
|
525
|
+
// Retries exhausted
|
|
526
|
+
if (gotoTarget.onExhausted) {
|
|
527
|
+
return activateFloatingNode(gotoTarget.onExhausted, definition, state, context, `Goto target "${targetId}" exhausted retries (${currentRetries}/${gotoTarget.maxRetries})`);
|
|
528
|
+
}
|
|
529
|
+
// No onExhausted — report as failed
|
|
530
|
+
return {
|
|
531
|
+
state,
|
|
532
|
+
nextAction: {
|
|
533
|
+
type: 'failed',
|
|
534
|
+
instructions: `Goto target "${targetId}" exhausted retries (${currentRetries}/${gotoTarget.maxRetries}). No onExhausted handler configured.`,
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
// Reset target and subsequent nodes
|
|
539
|
+
state = executeGoto(state, definition, targetId);
|
|
540
|
+
// Find the target node and activate it
|
|
541
|
+
const targetNode = findNode(definition.root, targetId, definition.floatingNodes);
|
|
542
|
+
if (!targetNode) {
|
|
543
|
+
return {
|
|
544
|
+
state,
|
|
545
|
+
nextAction: {
|
|
546
|
+
type: 'wait',
|
|
547
|
+
instructions: `Goto target node "${targetId}" not found in definition`,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
return activateNode(targetNode, definition, state, context, gateResults);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Execute goto state reset: reset target node and all subsequent nodes to pending.
|
|
555
|
+
* Increment retry count on the target node.
|
|
556
|
+
*/
|
|
557
|
+
function executeGoto(state, definition, targetId) {
|
|
558
|
+
// Collect all node IDs that need to be reset:
|
|
559
|
+
// the target node itself + all nodes that come after it in execution order
|
|
560
|
+
const resetIds = collectSubsequentNodeIds(definition.root, targetId);
|
|
561
|
+
resetIds.add(targetId);
|
|
562
|
+
const now = new Date().toISOString();
|
|
563
|
+
resetIds.forEach((id) => {
|
|
564
|
+
const existing = state.nodes[id];
|
|
565
|
+
if (existing) {
|
|
566
|
+
const isTarget = id === targetId;
|
|
567
|
+
state.nodes[id] = {
|
|
568
|
+
id,
|
|
569
|
+
status: 'pending',
|
|
570
|
+
retryCount: isTarget ? existing.retryCount + 1 : 0,
|
|
571
|
+
// Clear timing fields
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
state.updatedAt = now;
|
|
576
|
+
return state;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
// ─── Gate Evaluation ───
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Evaluate all conditions for a gate node.
|
|
583
|
+
* All conditions must pass for the gate to pass (AND logic).
|
|
584
|
+
*/
|
|
585
|
+
export function evaluateGate(gate, gateCtx) {
|
|
586
|
+
const results = gate.conditions.map((condition) => evaluateCondition(condition, gateCtx));
|
|
587
|
+
return {
|
|
588
|
+
passed: results.every((r) => r.met),
|
|
589
|
+
conditions: results,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Evaluate a single gate condition.
|
|
594
|
+
*/
|
|
595
|
+
function evaluateCondition(condition, ctx) {
|
|
596
|
+
switch (condition.type) {
|
|
597
|
+
case 'artifact_exists': {
|
|
598
|
+
const exists = ctx.artifactExists(condition.artifact);
|
|
599
|
+
return { condition, met: exists };
|
|
600
|
+
}
|
|
601
|
+
case 'artifact_approved': {
|
|
602
|
+
const approved = ctx.artifactApproved(condition.artifact);
|
|
603
|
+
return { condition, met: approved };
|
|
604
|
+
}
|
|
605
|
+
case 'artifact_field': {
|
|
606
|
+
const value = ctx.artifactField(condition.artifact, condition.field);
|
|
607
|
+
const met = evaluateFieldCondition(value, condition.operator, condition.value);
|
|
608
|
+
return { condition, met, actualValue: value };
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Evaluate a field comparison operation.
|
|
614
|
+
*/
|
|
615
|
+
function evaluateFieldCondition(actualValue, operator, expectedValue) {
|
|
616
|
+
switch (operator) {
|
|
617
|
+
case 'eq':
|
|
618
|
+
return actualValue === expectedValue;
|
|
619
|
+
case 'neq':
|
|
620
|
+
return actualValue !== expectedValue;
|
|
621
|
+
case 'gt':
|
|
622
|
+
return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue > expectedValue;
|
|
623
|
+
case 'lt':
|
|
624
|
+
return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue < expectedValue;
|
|
625
|
+
case 'gte':
|
|
626
|
+
return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue >= expectedValue;
|
|
627
|
+
case 'lte':
|
|
628
|
+
return typeof actualValue === 'number' && typeof expectedValue === 'number' && actualValue <= expectedValue;
|
|
629
|
+
case 'contains':
|
|
630
|
+
if (typeof actualValue === 'string' && typeof expectedValue === 'string') {
|
|
631
|
+
return actualValue.includes(expectedValue);
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(actualValue)) {
|
|
634
|
+
return actualValue.includes(expectedValue);
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
case 'in':
|
|
638
|
+
if (Array.isArray(expectedValue)) {
|
|
639
|
+
return expectedValue.includes(actualValue);
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
642
|
+
default:
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// ─── Gate Re-evaluation ───
|
|
647
|
+
/**
|
|
648
|
+
* Re-evaluate all active gate nodes after an artifact event.
|
|
649
|
+
* If a gate that was previously waiting (status='active') now passes,
|
|
650
|
+
* activate its pass branch.
|
|
651
|
+
*/
|
|
652
|
+
function reevaluateGates(state, definition, context) {
|
|
653
|
+
// Find all active gate nodes
|
|
654
|
+
const activeGates = findActiveGates(definition.root, state);
|
|
655
|
+
for (const gate of activeGates) {
|
|
656
|
+
const evaluation = evaluateGate(gate, context.gate);
|
|
657
|
+
if (evaluation.passed) {
|
|
658
|
+
// Gate now passes — mark it completed and activate pass branch
|
|
659
|
+
state.nodes[gate.id] = {
|
|
660
|
+
...state.nodes[gate.id],
|
|
661
|
+
status: 'completed',
|
|
662
|
+
completedAt: new Date().toISOString(),
|
|
663
|
+
};
|
|
664
|
+
return activateNode(gate.pass, definition, state, context);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// No gate changed — return wait
|
|
668
|
+
return {
|
|
669
|
+
state,
|
|
670
|
+
nextAction: {
|
|
671
|
+
type: 'wait',
|
|
672
|
+
instructions: 'Artifact event processed. No gate conditions newly satisfied.',
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Find all gate nodes that are currently in 'active' state.
|
|
678
|
+
*/
|
|
679
|
+
function findActiveGates(node, state) {
|
|
680
|
+
const gates = [];
|
|
681
|
+
if (node.type === 'gate' && state.nodes[node.id]?.status === 'active') {
|
|
682
|
+
gates.push(node);
|
|
683
|
+
}
|
|
684
|
+
switch (node.type) {
|
|
685
|
+
case 'sequence':
|
|
686
|
+
case 'parallel':
|
|
687
|
+
for (const child of node.children) {
|
|
688
|
+
gates.push(...findActiveGates(child, state));
|
|
689
|
+
}
|
|
690
|
+
break;
|
|
691
|
+
case 'gate':
|
|
692
|
+
gates.push(...findActiveGates(node.pass, state));
|
|
693
|
+
if ('type' in node.fail) {
|
|
694
|
+
gates.push(...findActiveGates(node.fail, state));
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case 'loop':
|
|
698
|
+
if (state.nodes[node.id]?.status === 'active') {
|
|
699
|
+
gates.push(...findActiveGates(node.body, state));
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
return gates;
|
|
704
|
+
}
|
|
705
|
+
// ─── Dispatch Request Handling ───
|
|
706
|
+
/**
|
|
707
|
+
* Handle an explicit dispatch request for a specific node.
|
|
708
|
+
* Used when the coordinator explicitly requests to dispatch a node
|
|
709
|
+
* (e.g., for parallel tasks that need individual dispatching).
|
|
710
|
+
*/
|
|
711
|
+
function handleDispatchRequest(state, definition, nodeId, context) {
|
|
712
|
+
const node = findNode(definition.root, nodeId, definition.floatingNodes);
|
|
713
|
+
if (!node) {
|
|
714
|
+
return {
|
|
715
|
+
state,
|
|
716
|
+
nextAction: {
|
|
717
|
+
type: 'wait',
|
|
718
|
+
instructions: `Node "${nodeId}" not found in workflow definition`,
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
const nodeState = state.nodes[nodeId];
|
|
723
|
+
if (!nodeState || nodeState.status !== 'active') {
|
|
724
|
+
return {
|
|
725
|
+
state,
|
|
726
|
+
nextAction: {
|
|
727
|
+
type: 'wait',
|
|
728
|
+
instructions: `Node "${nodeId}" is not in 'active' state (current: ${nodeState?.status ?? 'unknown'})`,
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (node.type !== 'task') {
|
|
733
|
+
return {
|
|
734
|
+
state,
|
|
735
|
+
nextAction: {
|
|
736
|
+
type: 'wait',
|
|
737
|
+
instructions: `Node "${nodeId}" is a ${node.type} node, not a task. Only task nodes can be dispatched.`,
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
// Return dispatch action for this task
|
|
742
|
+
return activateTask(node, state, context);
|
|
743
|
+
}
|
|
744
|
+
// ─── Status Query ───
|
|
745
|
+
/**
|
|
746
|
+
* Compute the current status nextAction without modifying state.
|
|
747
|
+
* Used for query_status events — tells the coordinator what to do next
|
|
748
|
+
* based on the current workflow state.
|
|
749
|
+
*/
|
|
750
|
+
function computeStatusAction(state, definition, context) {
|
|
751
|
+
// Collect all node statuses for a summary
|
|
752
|
+
const activeNodes = [];
|
|
753
|
+
const pendingNodes = [];
|
|
754
|
+
const failedNodes = [];
|
|
755
|
+
const completedNodes = [];
|
|
756
|
+
for (const [id, nodeState] of Object.entries(state.nodes)) {
|
|
757
|
+
switch (nodeState.status) {
|
|
758
|
+
case 'active':
|
|
759
|
+
activeNodes.push(id);
|
|
760
|
+
break;
|
|
761
|
+
case 'pending':
|
|
762
|
+
pendingNodes.push(id);
|
|
763
|
+
break;
|
|
764
|
+
case 'failed':
|
|
765
|
+
failedNodes.push(id);
|
|
766
|
+
break;
|
|
767
|
+
case 'completed':
|
|
768
|
+
completedNodes.push(id);
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Check if workflow is completed (root node completed)
|
|
773
|
+
const rootState = state.nodes[definition.root.id];
|
|
774
|
+
if (rootState?.status === 'completed') {
|
|
775
|
+
return {
|
|
776
|
+
type: 'completed',
|
|
777
|
+
instructions: 'Workflow completed successfully',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (rootState?.status === 'failed') {
|
|
781
|
+
return {
|
|
782
|
+
type: 'failed',
|
|
783
|
+
instructions: `Workflow failed. Failed nodes: ${failedNodes.join(', ')}`,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
// If there's an active task node, suggest dispatching it
|
|
787
|
+
if (state.activeNodeId) {
|
|
788
|
+
const activeNode = findNode(definition.root, state.activeNodeId, definition.floatingNodes);
|
|
789
|
+
if (activeNode?.type === 'task') {
|
|
790
|
+
return {
|
|
791
|
+
type: 'dispatch',
|
|
792
|
+
nodeId: state.activeNodeId,
|
|
793
|
+
role: activeNode.role,
|
|
794
|
+
instructions: `Current active task: "${state.activeNodeId}" (role: ${activeNode.role}). ${activeNodes.length} active, ${pendingNodes.length} pending, ${completedNodes.length} completed, ${failedNodes.length} failed.`,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
type: 'wait',
|
|
800
|
+
instructions: `${activeNodes.length} active, ${pendingNodes.length} pending, ${completedNodes.length} completed, ${failedNodes.length} failed. Active nodes: ${activeNodes.join(', ') || 'none'}`,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
// ─── Helper Functions ───
|
|
804
|
+
/**
|
|
805
|
+
* Find a node by ID anywhere in the workflow tree (including floating nodes).
|
|
806
|
+
* This stays in engine because it accesses floatingNodes — engine-specific logic.
|
|
807
|
+
*/
|
|
808
|
+
function findNode(root, targetId, floatingNodes) {
|
|
809
|
+
// Check the tree
|
|
810
|
+
const found = findNodeInTree(root, targetId);
|
|
811
|
+
if (found)
|
|
812
|
+
return found;
|
|
813
|
+
// Check floating nodes
|
|
814
|
+
if (floatingNodes) {
|
|
815
|
+
const floating = floatingNodes.find((fn) => fn.id === targetId);
|
|
816
|
+
if (floating)
|
|
817
|
+
return floating;
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
//# sourceMappingURL=workflow-engine.js.map
|