agents-relay 1.0.177 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/adapters.js +26 -2
- package/dist/blockers.js +60 -65
- package/dist/cli-support.js +33 -1
- package/dist/cli.js +72 -86
- package/dist/dashboard-actions.js +13 -35
- package/dist/dashboard-html.js +1 -1
- package/dist/dashboard.js +39 -0
- package/dist/execution-recovery.js +37 -0
- package/dist/graph-runtime.js +1 -0
- package/dist/job-actions.js +53 -0
- package/dist/manual-plan.js +15 -17
- package/dist/planner-runtime.js +135 -0
- package/dist/planner.js +76 -60
- package/dist/pool.js +54 -10
- package/dist/projected-store.js +314 -0
- package/dist/reconciler-review.js +185 -110
- package/dist/reconciler-runtime.js +67 -7
- package/dist/reconciler.js +134 -282
- package/dist/relay-store.js +380 -0
- package/dist/relayd-main.js +6 -0
- package/dist/relayd.js +122 -29
- package/dist/runtime-status.js +2 -1
- package/dist/scheduled-jobs.js +40 -31
- package/dist/scheduler.js +19 -18
- package/dist/store.js +1 -1
- package/dist/task-actions.js +139 -0
- package/dist/troubleshooter.js +14 -8
- package/dist/types.js +3 -3
- package/dist/unblock.js +31 -21
- package/dist/workflow/authoritative-unblock-v2.js +20 -0
- package/dist/workflow/blocker-resolution-v2.js +33 -0
- package/dist/workflow/contracts.js +21 -0
- package/dist/workflow/definitions-v2.js +75 -0
- package/dist/workflow/dispatcher.js +56 -0
- package/dist/workflow/entity-creation.js +32 -0
- package/dist/workflow/graph-v2.js +391 -0
- package/dist/workflow/job-reconcile-v2.js +19 -0
- package/dist/workflow/lifecycle.js +82 -4
- package/dist/workflow/planner-orchestration-v2.js +58 -0
- package/dist/workflow/planner-trigger-v2.js +24 -0
- package/dist/workflow/review-v2.js +42 -0
- package/dist/workflow/runtime.js +15 -1
- package/dist/workflow/schedule-cycle-v2.js +25 -0
- package/dist/workflow/scheduled-run-v2.js +30 -0
- package/dist/workflow/task-action-policy.js +19 -0
- package/dist/workflow/task-reconcile.js +6 -6
- package/dist/workflow/task-related-action-v2.js +42 -0
- package/dist/workflow/troubleshooter-v2.js +29 -0
- package/dist/workspace.js +6 -1
- package/package.json +3 -3
- package/skills/agents-relay/agents/planner.agent.md +5 -5
- package/skills/agents-relay/contracts/task-job-contract.md +2 -51
- package/workflow/authoritative-unblock-v2.mmd +15 -0
- package/workflow/blocker-resolution-v2.mmd +73 -0
- package/workflow/job-lifecycle-v2.mmd +70 -0
- package/workflow/job-lifecycle.mmd +2 -2
- package/workflow/planner-orchestration-v2.mmd +65 -0
- package/workflow/planner-trigger-v2.mmd +32 -0
- package/workflow/review-lifecycle-v2.mmd +124 -0
- package/workflow/schedule-cycle-v2.mmd +19 -0
- package/workflow/scheduled-run-v2.mmd +55 -0
- package/workflow/task-lifecycle-v2.mmd +201 -0
- package/workflow/task-lifecycle.mmd +3 -0
- package/workflow/task-related-action-v2.mmd +28 -0
- package/workflow/troubleshooter-v2.mmd +43 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/** Strict, executable Mermaid flowchart profile for the incremental v2 graph runtime. */
|
|
2
|
+
const IDENTIFIER = '[A-Za-z_][A-Za-z0-9_.-]*';
|
|
3
|
+
const NODE = new RegExp(`^(${IDENTIFIER})\\["(state|guard|action|emit):\\s*([^|\\]"]+?)(?:\\s*\\|[^\\]"]*)?"\\]$`);
|
|
4
|
+
const EDGE = new RegExp(`^(${IDENTIFIER})\\s+-->\\s*(?:\\|\\s*([^|]+?)\\s*\\|\\s*)?(${IDENTIFIER})$`);
|
|
5
|
+
const HEADER = /^flowchart (LR|RL|TD|TB)$/;
|
|
6
|
+
const NAME = new RegExp(`^${IDENTIFIER}$`);
|
|
7
|
+
function fail(line, message) { throw new Error(`line ${line}: ${message}`); }
|
|
8
|
+
function lookup(value, id) {
|
|
9
|
+
return value instanceof Map ? value.get(id) : value?.[id];
|
|
10
|
+
}
|
|
11
|
+
function entries(value) {
|
|
12
|
+
return value instanceof Map ? value.entries() : Object.entries(value ?? {});
|
|
13
|
+
}
|
|
14
|
+
function parseLabel(raw, line) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return { kind: 'unlabelled', semanticId: null };
|
|
17
|
+
const label = raw.trim();
|
|
18
|
+
if (label === 'true' || label === 'false')
|
|
19
|
+
return { kind: label, semanticId: null };
|
|
20
|
+
const match = label.match(/^(on|request|to|emit):\s*(.*)$/);
|
|
21
|
+
if (!match || !NAME.test(match[2]))
|
|
22
|
+
fail(line, `unsupported edge label '${label}'`);
|
|
23
|
+
return { kind: match[1], semanticId: match[2] };
|
|
24
|
+
}
|
|
25
|
+
export function parseGraphV2(source) {
|
|
26
|
+
const lines = source.split(/\r?\n/);
|
|
27
|
+
let direction = null;
|
|
28
|
+
let initial = null;
|
|
29
|
+
const nodes = new Map();
|
|
30
|
+
const edges = [];
|
|
31
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
32
|
+
const lineNumber = index + 1;
|
|
33
|
+
const line = lines[index].trim();
|
|
34
|
+
if (!line)
|
|
35
|
+
continue;
|
|
36
|
+
if (line.startsWith('%%')) {
|
|
37
|
+
const directive = line.match(/^%%\s*relay:\s*initial\s+(.+)$/);
|
|
38
|
+
if (directive) {
|
|
39
|
+
if (initial)
|
|
40
|
+
fail(lineNumber, 'duplicate initial declaration');
|
|
41
|
+
initial = directive[1].trim();
|
|
42
|
+
if (!NAME.test(initial))
|
|
43
|
+
fail(lineNumber, `invalid initial state '${initial}'`);
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!direction) {
|
|
48
|
+
const header = line.match(HEADER);
|
|
49
|
+
if (!header)
|
|
50
|
+
fail(lineNumber, 'expected flowchart LR, RL, TD, or TB');
|
|
51
|
+
direction = header[1];
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const node = line.match(NODE);
|
|
55
|
+
if (node) {
|
|
56
|
+
if (nodes.has(node[1]))
|
|
57
|
+
fail(lineNumber, `duplicate node '${node[1]}'`);
|
|
58
|
+
const semanticId = node[3].trim();
|
|
59
|
+
if (!NAME.test(semanticId))
|
|
60
|
+
fail(lineNumber, `invalid ${node[2]} identifier '${semanticId}'`);
|
|
61
|
+
if (node[2] === 'state' && [...nodes.values()].some(candidate => candidate.kind === 'state' && candidate.semanticId === semanticId))
|
|
62
|
+
fail(lineNumber, `duplicate semantic declaration 'state: ${semanticId}'`);
|
|
63
|
+
nodes.set(node[1], { id: node[1], kind: node[2], semanticId, line: lineNumber });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const edge = line.match(EDGE);
|
|
67
|
+
if (edge) {
|
|
68
|
+
const label = parseLabel(edge[2], lineNumber);
|
|
69
|
+
edges.push({ from: edge[1], to: edge[3], ...label, line: lineNumber });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const unsupportedShape = line.match(/[({<]/)?.[0];
|
|
73
|
+
if (unsupportedShape) {
|
|
74
|
+
const shape = unsupportedShape === '{' ? '{...}' : `${unsupportedShape}...`;
|
|
75
|
+
fail(lineNumber, `unsupported node shape '${shape}'; expected '[\"...\"]'`);
|
|
76
|
+
}
|
|
77
|
+
fail(lineNumber, `unsupported syntax '${line}'`);
|
|
78
|
+
}
|
|
79
|
+
if (!direction)
|
|
80
|
+
throw new Error('line 1: executable graph is empty; expected flowchart header');
|
|
81
|
+
if (!initial)
|
|
82
|
+
throw new Error('graph requires exactly one %% relay: initial STATE declaration');
|
|
83
|
+
if (!nodes.has(initial) || nodes.get(initial)?.kind !== 'state')
|
|
84
|
+
throw new Error(`initial state '${initial}' is not a declared state node`);
|
|
85
|
+
if (![...nodes.values()].some(node => node.kind === 'state'))
|
|
86
|
+
throw new Error('graph requires at least one state node');
|
|
87
|
+
if (!edges.length)
|
|
88
|
+
throw new Error('graph requires at least one executable edge');
|
|
89
|
+
for (const edge of edges) {
|
|
90
|
+
if (!nodes.has(edge.from))
|
|
91
|
+
fail(edge.line, `edge source '${edge.from}' is not declared`);
|
|
92
|
+
if (!nodes.has(edge.to))
|
|
93
|
+
fail(edge.line, `edge target '${edge.to}' is not declared`);
|
|
94
|
+
}
|
|
95
|
+
validateTopology(nodes, edges, initial);
|
|
96
|
+
return { source, direction, nodes, edges, initial };
|
|
97
|
+
}
|
|
98
|
+
function validateTopology(nodes, edges, initial) {
|
|
99
|
+
const outgoing = new Map();
|
|
100
|
+
for (const edge of edges)
|
|
101
|
+
outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge]);
|
|
102
|
+
for (const node of nodes.values()) {
|
|
103
|
+
const exits = outgoing.get(node.id) ?? [];
|
|
104
|
+
if (node.kind === 'state') {
|
|
105
|
+
for (const edge of exits) {
|
|
106
|
+
if (edge.kind !== 'on' && edge.kind !== 'request')
|
|
107
|
+
fail(edge.line, `state '${node.semanticId}' may only have on:/request: exits`);
|
|
108
|
+
const target = nodes.get(edge.to);
|
|
109
|
+
if (target.kind !== 'state' && target.kind !== 'guard' && target.kind !== 'action')
|
|
110
|
+
fail(edge.line, `${edge.kind} edge must lead to a state, guard, or action`);
|
|
111
|
+
}
|
|
112
|
+
const groups = new Map();
|
|
113
|
+
for (const edge of exits) {
|
|
114
|
+
const key = `${edge.kind}\\0${edge.semanticId}`;
|
|
115
|
+
groups.set(key, [...(groups.get(key) ?? []), edge]);
|
|
116
|
+
}
|
|
117
|
+
for (const group of groups.values()) {
|
|
118
|
+
if (group.length < 2)
|
|
119
|
+
continue;
|
|
120
|
+
if (group.some(edge => nodes.get(edge.to).kind !== 'guard') || new Set(group.map(edge => edge.to)).size !== group.length) {
|
|
121
|
+
const edge = group[1];
|
|
122
|
+
fail(edge.line, `ambiguous input path for ${edge.kind} '${edge.semanticId}' from state '${node.semanticId}'`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else if (node.kind === 'guard') {
|
|
127
|
+
if (exits.length < 1 || exits.length > 2 || exits.some(edge => edge.kind !== 'true' && edge.kind !== 'false') || new Set(exits.map(edge => edge.kind)).size !== exits.length)
|
|
128
|
+
fail(node.line, `guard '${node.semanticId}' requires one or two unique true/false exits`);
|
|
129
|
+
for (const edge of exits)
|
|
130
|
+
if (!['state', 'guard', 'action'].includes(nodes.get(edge.to).kind))
|
|
131
|
+
fail(edge.line, `guard edge '${edge.kind}' must lead to a state, guard, or action`);
|
|
132
|
+
}
|
|
133
|
+
else if (node.kind === 'action') {
|
|
134
|
+
const continuations = exits.filter(edge => edge.kind === 'to' || edge.kind === 'unlabelled');
|
|
135
|
+
if (continuations.length > 1)
|
|
136
|
+
fail(node.line, `action '${node.semanticId}' has multiple continuations`);
|
|
137
|
+
for (const edge of continuations) {
|
|
138
|
+
const target = nodes.get(edge.to);
|
|
139
|
+
if (edge.kind === 'to') {
|
|
140
|
+
if (target.kind !== 'state')
|
|
141
|
+
fail(edge.line, 'to: action continuation must target a state');
|
|
142
|
+
if (edge.semanticId !== target.semanticId)
|
|
143
|
+
fail(edge.line, `to edge names state '${edge.semanticId}' but targets state '${target.semanticId}'`);
|
|
144
|
+
}
|
|
145
|
+
else if (target.kind !== 'state' && target.kind !== 'action')
|
|
146
|
+
fail(edge.line, 'unlabelled action continuation must lead to an action or state');
|
|
147
|
+
}
|
|
148
|
+
for (const edge of exits.filter(edge => edge.kind === 'emit')) {
|
|
149
|
+
const target = nodes.get(edge.to);
|
|
150
|
+
if (target.kind !== 'emit')
|
|
151
|
+
fail(edge.line, 'emit edge must lead to an emit node');
|
|
152
|
+
if (edge.semanticId !== target.semanticId)
|
|
153
|
+
fail(edge.line, `emit edge names event '${edge.semanticId}' but targets emit '${target.semanticId}'`);
|
|
154
|
+
}
|
|
155
|
+
if (!continuations.length)
|
|
156
|
+
fail(node.line, `action '${node.semanticId}' must have one continuation`);
|
|
157
|
+
}
|
|
158
|
+
else if (exits.length)
|
|
159
|
+
fail(node.line, `emit node '${node.semanticId}' must not have outgoing edges`);
|
|
160
|
+
}
|
|
161
|
+
const reachable = new Set([initial]);
|
|
162
|
+
let changed = true;
|
|
163
|
+
while (changed) {
|
|
164
|
+
changed = false;
|
|
165
|
+
for (const edge of edges)
|
|
166
|
+
if (reachable.has(edge.from) && !reachable.has(edge.to)) {
|
|
167
|
+
reachable.add(edge.to);
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const node of nodes.values())
|
|
172
|
+
if (node.kind === 'state' && !reachable.has(node.id))
|
|
173
|
+
fail(node.line, `state '${node.semanticId}' is unreachable from initial state '${initial}'`);
|
|
174
|
+
}
|
|
175
|
+
export class GraphRegistry {
|
|
176
|
+
states;
|
|
177
|
+
guards;
|
|
178
|
+
actions;
|
|
179
|
+
events;
|
|
180
|
+
constructor(registries = {}) {
|
|
181
|
+
this.states = registries.states ?? new Set();
|
|
182
|
+
this.guards = new Map(entries(registries.guards));
|
|
183
|
+
this.actions = new Map(entries(registries.actions));
|
|
184
|
+
this.events = new Map(entries(registries.events));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export class GraphRuntime {
|
|
188
|
+
graph;
|
|
189
|
+
registry;
|
|
190
|
+
constructor(graphOrSource, registries = {}) {
|
|
191
|
+
this.graph = typeof graphOrSource === 'string' ? parseGraphV2(graphOrSource) : graphOrSource;
|
|
192
|
+
this.registry = new GraphRegistry(registries);
|
|
193
|
+
this.validateRegistries();
|
|
194
|
+
}
|
|
195
|
+
validateRegistries() {
|
|
196
|
+
for (const node of this.graph.nodes.values()) {
|
|
197
|
+
if (node.kind === 'state' && !this.registry.states.has(node.semanticId))
|
|
198
|
+
throw new Error(`unknown state '${node.semanticId}'`);
|
|
199
|
+
if (node.kind === 'guard' && !this.registry.guards.has(node.semanticId))
|
|
200
|
+
throw new Error(`unknown guard '${node.semanticId}'`);
|
|
201
|
+
if (node.kind === 'action' && !this.registry.actions.has(node.semanticId))
|
|
202
|
+
throw new Error(`unknown action '${node.semanticId}'`);
|
|
203
|
+
if (node.kind === 'emit' && !this.registry.events.has(node.semanticId))
|
|
204
|
+
throw new Error(`unknown emitted event '${node.semanticId}'`);
|
|
205
|
+
}
|
|
206
|
+
for (const edge of this.graph.edges) {
|
|
207
|
+
if (edge.kind === 'on' || edge.kind === 'emit') {
|
|
208
|
+
if (!this.registry.events.has(edge.semanticId))
|
|
209
|
+
throw new Error(`unknown event '${edge.semanticId}'`);
|
|
210
|
+
}
|
|
211
|
+
if (edge.kind === 'request' && !this.registry.actions.has(edge.semanticId))
|
|
212
|
+
throw new Error(`unknown action '${edge.semanticId}'`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
validateInput(input) {
|
|
216
|
+
const definition = input.transport === 'event' ? this.registry.events.get(input.id) : this.registry.actions.get(input.id);
|
|
217
|
+
if (!definition)
|
|
218
|
+
throw new Error(`unknown ${input.transport} '${input.id}'`);
|
|
219
|
+
if (definition.validate && !definition.validate(input.payload))
|
|
220
|
+
throw new Error(`invalid payload for ${input.transport} '${input.id}'`);
|
|
221
|
+
}
|
|
222
|
+
pathFromSync(current, facts) {
|
|
223
|
+
const actions = [];
|
|
224
|
+
const visited = new Set();
|
|
225
|
+
while (true) {
|
|
226
|
+
if (visited.has(current))
|
|
227
|
+
throw new Error(`cyclic graph path at '${current}'`);
|
|
228
|
+
visited.add(current);
|
|
229
|
+
const currentNode = this.graph.nodes.get(current);
|
|
230
|
+
if (currentNode.kind === 'state')
|
|
231
|
+
return { target: currentNode.semanticId, actions };
|
|
232
|
+
if (currentNode.kind === 'guard') {
|
|
233
|
+
const guard = this.registry.guards.get(currentNode.semanticId);
|
|
234
|
+
const result = guard(facts);
|
|
235
|
+
if (result instanceof Promise)
|
|
236
|
+
throw new Error(`guard '${currentNode.semanticId}' is asynchronous; use evaluate()`);
|
|
237
|
+
const branch = (result ? 'true' : 'false');
|
|
238
|
+
const branchEdge = this.graph.edges.find(edge => edge.from === currentNode.id && edge.kind === branch);
|
|
239
|
+
if (!branchEdge)
|
|
240
|
+
return null;
|
|
241
|
+
current = branchEdge.to;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (currentNode.kind === 'action') {
|
|
245
|
+
const emittedEvents = this.graph.edges.filter(edge => edge.from === currentNode.id && edge.kind === 'emit').map(edge => edge.semanticId);
|
|
246
|
+
actions.push({ node: currentNode, emittedEvents });
|
|
247
|
+
current = this.graph.edges.find(edge => edge.from === currentNode.id && (edge.kind === 'to' || edge.kind === 'unlabelled')).to;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`invalid executable path through emit node '${currentNode.semanticId}'`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
evaluateSync(facts) {
|
|
254
|
+
this.validateInput(facts.input);
|
|
255
|
+
const stateNode = [...this.graph.nodes.values()].find(node => node.kind === 'state' && node.semanticId === facts.currentState);
|
|
256
|
+
if (!stateNode)
|
|
257
|
+
throw new Error(`unknown state '${facts.currentState}'`);
|
|
258
|
+
const edges = this.graph.edges.filter(edge => edge.from === stateNode.id && edge.kind === (facts.input.transport === 'event' ? 'on' : 'request') && edge.semanticId === facts.input.id);
|
|
259
|
+
const results = edges.map(edge => this.pathFromSync(edge.to, facts)).filter((result) => result !== null);
|
|
260
|
+
if (results.length === 0)
|
|
261
|
+
return { accepted: false, source: facts.currentState, reason: 'no_transition' };
|
|
262
|
+
if (results.length > 1)
|
|
263
|
+
return { accepted: false, source: facts.currentState, reason: 'ambiguous' };
|
|
264
|
+
const result = results[0];
|
|
265
|
+
return { accepted: true, source: facts.currentState, target: result.target, actions: result.actions.map(step => ({ id: step.node.semanticId, input: facts.input, emittedEvents: step.emittedEvents })) };
|
|
266
|
+
}
|
|
267
|
+
async evaluate(facts) {
|
|
268
|
+
this.validateInput(facts.input);
|
|
269
|
+
const stateNode = [...this.graph.nodes.values()].find(node => node.kind === 'state' && node.semanticId === facts.currentState);
|
|
270
|
+
if (!stateNode)
|
|
271
|
+
throw new Error(`unknown state '${facts.currentState}'`);
|
|
272
|
+
const edges = this.graph.edges.filter(candidate => candidate.from === stateNode.id && candidate.kind === (facts.input.transport === 'event' ? 'on' : 'request') && candidate.semanticId === facts.input.id);
|
|
273
|
+
const evaluatePath = async (start) => {
|
|
274
|
+
let current = start;
|
|
275
|
+
const actions = [];
|
|
276
|
+
while (true) {
|
|
277
|
+
const node = this.graph.nodes.get(current);
|
|
278
|
+
if (node.kind === 'state')
|
|
279
|
+
return { target: node.semanticId, actions };
|
|
280
|
+
if (node.kind === 'guard') {
|
|
281
|
+
const result = await this.registry.guards.get(node.semanticId)(facts);
|
|
282
|
+
const branch = this.graph.edges.find(candidate => candidate.from === node.id && candidate.kind === (result ? 'true' : 'false'));
|
|
283
|
+
if (!branch)
|
|
284
|
+
return null;
|
|
285
|
+
current = branch.to;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (node.kind === 'action') {
|
|
289
|
+
const emittedEvents = this.graph.edges.filter(candidate => candidate.from === node.id && candidate.kind === 'emit').map(candidate => candidate.semanticId);
|
|
290
|
+
actions.push({ node, emittedEvents });
|
|
291
|
+
current = this.graph.edges.find(candidate => candidate.from === node.id && (candidate.kind === 'to' || candidate.kind === 'unlabelled')).to;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`invalid executable path through emit node '${node.semanticId}'`);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const results = (await Promise.all(edges.map(edge => evaluatePath(edge.to)))).filter((result) => result !== null);
|
|
298
|
+
if (results.length === 0)
|
|
299
|
+
return { accepted: false, source: facts.currentState, reason: 'no_transition' };
|
|
300
|
+
if (results.length > 1)
|
|
301
|
+
return { accepted: false, source: facts.currentState, reason: 'ambiguous' };
|
|
302
|
+
const result = results[0];
|
|
303
|
+
return { accepted: true, source: facts.currentState, target: result.target, actions: result.actions.map(step => ({ id: step.node.semanticId, input: facts.input, emittedEvents: step.emittedEvents })) };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
export class GraphDispatcher {
|
|
307
|
+
runtime;
|
|
308
|
+
ports;
|
|
309
|
+
constructor(runtime, ports) {
|
|
310
|
+
this.runtime = runtime;
|
|
311
|
+
this.ports = ports;
|
|
312
|
+
}
|
|
313
|
+
dispatchSync(facts) {
|
|
314
|
+
const decision = this.runtime.evaluateSync(facts);
|
|
315
|
+
if (!decision.accepted)
|
|
316
|
+
return { decision, receipts: [] };
|
|
317
|
+
const persisted = this.ports.persistTransition({ source: decision.source, target: decision.target });
|
|
318
|
+
if (persisted instanceof Promise)
|
|
319
|
+
throw new Error('GraphDispatcher.dispatchSync requires synchronous persistTransition');
|
|
320
|
+
const receipts = [];
|
|
321
|
+
for (const step of decision.actions) {
|
|
322
|
+
const action = this.runtime.registry.actions.get(step.id);
|
|
323
|
+
const context = { facts, actionId: step.id, input: step.input };
|
|
324
|
+
const pending = this.ports.executeAction ? this.ports.executeAction(step.id, context) : action.execute ? action.execute(context) : {};
|
|
325
|
+
if (pending instanceof Promise)
|
|
326
|
+
throw new Error('GraphDispatcher.dispatchSync requires synchronous executeAction');
|
|
327
|
+
const receipt = pending;
|
|
328
|
+
receipts.push(receipt);
|
|
329
|
+
if (receipt.ok === false) {
|
|
330
|
+
const definition = this.runtime.registry.events.get(receipt.failureEvent.id);
|
|
331
|
+
if (!definition)
|
|
332
|
+
throw new Error(`unknown failure event '${receipt.failureEvent.id}'`);
|
|
333
|
+
if (definition.validate && !definition.validate(receipt.failureEvent.payload))
|
|
334
|
+
throw new Error(`invalid payload for failure event '${receipt.failureEvent.id}'`);
|
|
335
|
+
const published = this.ports.publishEvent(receipt.failureEvent);
|
|
336
|
+
if (published instanceof Promise)
|
|
337
|
+
throw new Error('GraphDispatcher.dispatchSync requires synchronous publishEvent');
|
|
338
|
+
return { decision, receipts };
|
|
339
|
+
}
|
|
340
|
+
const payloads = receipt.payloads ?? {};
|
|
341
|
+
for (const eventId of step.emittedEvents) {
|
|
342
|
+
const definition = this.runtime.registry.events.get(eventId);
|
|
343
|
+
if (!(eventId in payloads))
|
|
344
|
+
throw new Error(`action '${step.id}' did not supply payload for emitted event '${eventId}'`);
|
|
345
|
+
if (definition.validate && !definition.validate(payloads[eventId]))
|
|
346
|
+
throw new Error(`invalid payload for emitted event '${eventId}'`);
|
|
347
|
+
}
|
|
348
|
+
for (const eventId of step.emittedEvents) {
|
|
349
|
+
const published = this.ports.publishEvent({ id: eventId, payload: payloads[eventId] });
|
|
350
|
+
if (published instanceof Promise)
|
|
351
|
+
throw new Error('GraphDispatcher.dispatchSync requires synchronous publishEvent');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { decision, receipts };
|
|
355
|
+
}
|
|
356
|
+
async dispatch(facts) {
|
|
357
|
+
const decision = await this.runtime.evaluate(facts);
|
|
358
|
+
if (!decision.accepted)
|
|
359
|
+
return { decision, receipts: [] };
|
|
360
|
+
await this.ports.persistTransition({ source: decision.source, target: decision.target });
|
|
361
|
+
const receipts = [];
|
|
362
|
+
for (const step of decision.actions) {
|
|
363
|
+
const action = this.runtime.registry.actions.get(step.id);
|
|
364
|
+
const context = { facts, actionId: step.id, input: step.input };
|
|
365
|
+
const receipt = this.ports.executeAction ? await this.ports.executeAction(step.id, context) : action.execute ? await action.execute(context) : {};
|
|
366
|
+
receipts.push(receipt);
|
|
367
|
+
if (receipt.ok === false) {
|
|
368
|
+
const definition = this.runtime.registry.events.get(receipt.failureEvent.id);
|
|
369
|
+
if (!definition)
|
|
370
|
+
throw new Error(`unknown failure event '${receipt.failureEvent.id}'`);
|
|
371
|
+
if (definition.validate && !definition.validate(receipt.failureEvent.payload))
|
|
372
|
+
throw new Error(`invalid payload for failure event '${receipt.failureEvent.id}'`);
|
|
373
|
+
await this.ports.publishEvent(receipt.failureEvent);
|
|
374
|
+
return { decision, receipts };
|
|
375
|
+
}
|
|
376
|
+
const payloads = receipt.payloads ?? {};
|
|
377
|
+
for (const eventId of step.emittedEvents) {
|
|
378
|
+
const definition = this.runtime.registry.events.get(eventId);
|
|
379
|
+
if (!(eventId in payloads))
|
|
380
|
+
throw new Error(`action '${step.id}' did not supply payload for emitted event '${eventId}'`);
|
|
381
|
+
if (definition.validate && !definition.validate(payloads[eventId]))
|
|
382
|
+
throw new Error(`invalid payload for emitted event '${eventId}'`);
|
|
383
|
+
}
|
|
384
|
+
for (const eventId of step.emittedEvents)
|
|
385
|
+
await this.ports.publishEvent({ id: eventId, payload: payloads[eventId] });
|
|
386
|
+
}
|
|
387
|
+
return { decision, receipts };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
export const parseExecutableGraph = parseGraphV2;
|
|
391
|
+
export const loadGraphV2 = (source, registries = {}) => new GraphRuntime(source, registries);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { decideJobLifecycle, dispatchJobLifecycle } from './lifecycle.js';
|
|
2
|
+
import { implementationComplete } from '../runtime-status.js';
|
|
3
|
+
export function decideJobReconcile(job, facts) {
|
|
4
|
+
return decideJobLifecycle(job, 'reconcile', {
|
|
5
|
+
prOpen: facts.prState === 'OPEN',
|
|
6
|
+
reviewDisabled: job.reviewEnabled === false,
|
|
7
|
+
implementationComplete: implementationComplete(job),
|
|
8
|
+
autoMerge: job.autoMerge !== false,
|
|
9
|
+
headAvailable: Boolean(facts.headSha),
|
|
10
|
+
mergeCapable: facts.mergeCapable,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function dispatchJobReconcile(job, facts, executeEffect, persist) {
|
|
14
|
+
return dispatchJobLifecycle(job, 'reconcile', {
|
|
15
|
+
prOpen: facts.prState === 'OPEN', reviewDisabled: job.reviewEnabled === false,
|
|
16
|
+
implementationComplete: implementationComplete(job), autoMerge: job.autoMerge !== false,
|
|
17
|
+
headAvailable: Boolean(facts.headSha), mergeCapable: facts.mergeCapable,
|
|
18
|
+
}, executeEffect, persist);
|
|
19
|
+
}
|
|
@@ -1,8 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { classifyWorkflowActions, emptyWorkflowDecision } from './contracts.js';
|
|
2
|
+
import { jobLifecycleV2, taskLifecycleV2 } from './definitions-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
import { applyJobGraphTarget, applyTaskGraphTarget, applyTaskMutation } from './runtime.js';
|
|
5
|
+
function v2WorkflowDecision(runtime, from, event, facts = {}) {
|
|
6
|
+
const actions = [];
|
|
7
|
+
const decision = dispatchWorkflowGraphSync(runtime, {
|
|
8
|
+
initialState: from,
|
|
9
|
+
input: { transport: 'event', id: event, payload: {} },
|
|
10
|
+
durableFacts: facts,
|
|
11
|
+
executeAction: actionId => { actions.push(actionId); return {}; },
|
|
12
|
+
});
|
|
13
|
+
if (!decision.accepted)
|
|
14
|
+
return emptyWorkflowDecision(decision.reason === 'ambiguous' ? 'ambiguous' : 'no_transition');
|
|
15
|
+
const classified = classifyWorkflowActions(actions);
|
|
16
|
+
return {
|
|
17
|
+
accepted: true,
|
|
18
|
+
target: decision.target,
|
|
19
|
+
guard: null,
|
|
20
|
+
actions,
|
|
21
|
+
...classified,
|
|
22
|
+
reason: 'accepted',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
3
25
|
export function decideTaskLifecycle(task, event, facts = {}) {
|
|
4
|
-
return
|
|
26
|
+
return v2WorkflowDecision(taskLifecycleV2, task.state, event, facts);
|
|
5
27
|
}
|
|
6
28
|
export function decideJobLifecycle(job, event, facts = {}) {
|
|
7
|
-
return
|
|
29
|
+
return v2WorkflowDecision(jobLifecycleV2, job.state, event, facts);
|
|
30
|
+
}
|
|
31
|
+
export function dispatchTaskLifecycleSync(task, event, facts = {}) {
|
|
32
|
+
const actions = [];
|
|
33
|
+
const result = dispatchWorkflowGraphSync(taskLifecycleV2, {
|
|
34
|
+
initialState: task.state,
|
|
35
|
+
input: { transport: 'event', id: event, payload: {} },
|
|
36
|
+
durableFacts: facts,
|
|
37
|
+
persistTransition: (_source, target) => applyTaskGraphTarget(task, target),
|
|
38
|
+
executeAction: actionId => { actions.push(actionId); if (actionId.startsWith('mutation.'))
|
|
39
|
+
applyTaskMutation(task, actionId); return {}; },
|
|
40
|
+
});
|
|
41
|
+
if (!result.accepted)
|
|
42
|
+
return emptyWorkflowDecision(result.reason === 'ambiguous' ? 'ambiguous' : 'no_transition');
|
|
43
|
+
return { accepted: true, target: result.target, guard: null, actions, ...classifyWorkflowActions(actions), reason: 'accepted' };
|
|
44
|
+
}
|
|
45
|
+
export async function dispatchTaskLifecycle(task, event, facts = {}, executeEffect, persist) {
|
|
46
|
+
const actions = [];
|
|
47
|
+
const result = await dispatchWorkflowGraph(taskLifecycleV2, {
|
|
48
|
+
initialState: task.state, input: { transport: 'event', id: event, payload: {} }, durableFacts: facts,
|
|
49
|
+
persistTransition: async (_source, target) => { applyTaskGraphTarget(task, target); await persist?.(); },
|
|
50
|
+
executeAction: async (actionId) => {
|
|
51
|
+
actions.push(actionId);
|
|
52
|
+
if (actionId.startsWith('mutation.'))
|
|
53
|
+
applyTaskMutation(task, actionId);
|
|
54
|
+
else
|
|
55
|
+
await executeEffect?.(actionId);
|
|
56
|
+
return {};
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (!result.accepted)
|
|
60
|
+
return emptyWorkflowDecision(result.reason === 'ambiguous' ? 'ambiguous' : 'no_transition');
|
|
61
|
+
return { accepted: true, target: result.target, guard: null, actions, ...classifyWorkflowActions(actions), reason: 'accepted' };
|
|
62
|
+
}
|
|
63
|
+
export function dispatchJobLifecycleSync(job, event, facts = {}) {
|
|
64
|
+
const actions = [];
|
|
65
|
+
const result = dispatchWorkflowGraphSync(jobLifecycleV2, {
|
|
66
|
+
initialState: job.state,
|
|
67
|
+
input: { transport: 'event', id: event, payload: {} },
|
|
68
|
+
durableFacts: facts,
|
|
69
|
+
persistTransition: (_source, target) => applyJobGraphTarget(job, target),
|
|
70
|
+
executeAction: actionId => { actions.push(actionId); return {}; },
|
|
71
|
+
});
|
|
72
|
+
if (!result.accepted)
|
|
73
|
+
return emptyWorkflowDecision(result.reason === 'ambiguous' ? 'ambiguous' : 'no_transition');
|
|
74
|
+
return { accepted: true, target: result.target, guard: null, actions, ...classifyWorkflowActions(actions), reason: 'accepted' };
|
|
75
|
+
}
|
|
76
|
+
export async function dispatchJobLifecycle(job, event, facts = {}, executeEffect, persist) {
|
|
77
|
+
const actions = [];
|
|
78
|
+
const result = await dispatchWorkflowGraph(jobLifecycleV2, {
|
|
79
|
+
initialState: job.state, input: { transport: 'event', id: event, payload: {} }, durableFacts: facts,
|
|
80
|
+
persistTransition: async (_source, target) => { applyJobGraphTarget(job, target); await persist?.(); },
|
|
81
|
+
executeAction: async (actionId) => { actions.push(actionId); await executeEffect?.(actionId); return {}; },
|
|
82
|
+
});
|
|
83
|
+
if (!result.accepted)
|
|
84
|
+
return emptyWorkflowDecision(result.reason === 'ambiguous' ? 'ambiguous' : 'no_transition');
|
|
85
|
+
return { accepted: true, target: result.target, guard: null, actions, ...classifyWorkflowActions(actions), reason: 'accepted' };
|
|
8
86
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
import { isEffectivelyBlocked } from '../unblock.js';
|
|
5
|
+
const source = readFileSync(new URL('../../workflow/planner-orchestration-v2.mmd', import.meta.url), 'utf8');
|
|
6
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
7
|
+
export const plannerOrchestrationV2 = new GraphRuntime(source, {
|
|
8
|
+
states: new Set([
|
|
9
|
+
'idle', 'check_legacy_pending', 'check_recover', 'check_seed', 'check_planner',
|
|
10
|
+
'check_blocked', 'check_work', 'check_materialized', 'check_objective', 'wait', 'recovered', 'created',
|
|
11
|
+
]),
|
|
12
|
+
guards: {
|
|
13
|
+
autonomousJob: boolGuard('autonomousJob'),
|
|
14
|
+
legacyPlanningPending: boolGuard('legacyPlanningPending'),
|
|
15
|
+
recoverablePendingPlan: boolGuard('recoverablePendingPlan'),
|
|
16
|
+
hasSeedWork: boolGuard('hasSeedWork'),
|
|
17
|
+
noActivePlanner: boolGuard('noActivePlanner'),
|
|
18
|
+
noBlockedWork: boolGuard('noBlockedWork'),
|
|
19
|
+
noActiveImplementation: boolGuard('noActiveImplementation'),
|
|
20
|
+
continuationMaterialized: boolGuard('continuationMaterialized'),
|
|
21
|
+
objectiveUnsatisfied: boolGuard('objectiveUnsatisfied'),
|
|
22
|
+
},
|
|
23
|
+
actions: {
|
|
24
|
+
'planner.recoverPending': { id: 'planner.recoverPending' },
|
|
25
|
+
'planner.createRound': { id: 'planner.createRound' },
|
|
26
|
+
},
|
|
27
|
+
events: { 'planner.reconcile': { id: 'planner.reconcile' } },
|
|
28
|
+
});
|
|
29
|
+
export async function dispatchPlannerOrchestration(facts, executeAction) {
|
|
30
|
+
return dispatchWorkflowGraph(plannerOrchestrationV2, { initialState: 'idle', input: { transport: 'event', id: 'planner.reconcile', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 9, executeAction });
|
|
31
|
+
}
|
|
32
|
+
export function decidePlannerOrchestration(facts) {
|
|
33
|
+
return dispatchWorkflowGraphSync(plannerOrchestrationV2, { initialState: 'idle', input: { transport: 'event', id: 'planner.reconcile', payload: {} }, durableFacts: facts, isTransientState: state => state.startsWith('check_'), maxSteps: 9 });
|
|
34
|
+
}
|
|
35
|
+
export function plannerOrchestrationFacts(job, recoverablePendingPlan) {
|
|
36
|
+
const slot = job.schedule?.currentRunSlot ? Date.parse(job.schedule.currentRunSlot) : Number.NaN;
|
|
37
|
+
const scopedTasks = Number.isFinite(slot) ? job.tasks.filter(task => Date.parse(task.createdAt) >= slot) : job.tasks;
|
|
38
|
+
const scopedPlans = (job.plans ?? []).filter(plan => !Number.isFinite(slot) || Date.parse(plan.createdAt) >= slot)
|
|
39
|
+
.sort((a, b) => a.round - b.round || a.createdAt.localeCompare(b.createdAt));
|
|
40
|
+
const latestPlan = scopedPlans.at(-1);
|
|
41
|
+
const legacyLatest = scopedPlans.length === 0
|
|
42
|
+
? scopedTasks.filter(task => task.kind === 'planner' && task.state === 'SUCCEEDED' && task.plannerResult)
|
|
43
|
+
.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)).at(-1)
|
|
44
|
+
: undefined;
|
|
45
|
+
const latestId = latestPlan?.id ?? legacyLatest?.id;
|
|
46
|
+
const latestObjective = latestPlan?.objectiveStatus ?? legacyLatest?.plannerResult?.objective_status;
|
|
47
|
+
return {
|
|
48
|
+
autonomousJob: job.executionMode === 'autonomous',
|
|
49
|
+
legacyPlanningPending: job.initialPlanning?.status === 'pending',
|
|
50
|
+
recoverablePendingPlan,
|
|
51
|
+
hasSeedWork: !((job.plans?.length ?? 0) === 0 && job.tasks.filter(task => task.kind !== 'planner').length === 0),
|
|
52
|
+
noActivePlanner: !scopedPlans.some(plan => plan.status === 'PLANNING'),
|
|
53
|
+
noBlockedWork: !scopedTasks.some(task => task.kind !== 'planner' && isEffectivelyBlocked(task)),
|
|
54
|
+
noActiveImplementation: !scopedTasks.some(task => task.kind !== 'planner' && ['DRAFT', 'QUEUED', 'RUNNING'].includes(task.state)),
|
|
55
|
+
continuationMaterialized: !(latestObjective === 'in_progress' && latestId && !job.tasks.some(task => task.kind !== 'planner' && task.planningDecisionId === latestId)),
|
|
56
|
+
objectiveUnsatisfied: latestObjective !== 'satisfied',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/planner-trigger-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const plannerTriggerV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['idle', 'check_unblock', 'check_schedule', 'check_initial', 'manual', 'unblock', 'schedule', 'initial', 'continuation']),
|
|
8
|
+
guards: {
|
|
9
|
+
manualPlan: boolGuard('manualPlan'),
|
|
10
|
+
hasUnblock: boolGuard('hasUnblock'),
|
|
11
|
+
scheduledRun: boolGuard('scheduledRun'),
|
|
12
|
+
initialPlan: boolGuard('initialPlan'),
|
|
13
|
+
},
|
|
14
|
+
actions: {},
|
|
15
|
+
events: { 'plan.requested': { id: 'plan.requested' } },
|
|
16
|
+
});
|
|
17
|
+
export function dispatchPlannerTrigger(facts) {
|
|
18
|
+
return dispatchWorkflowGraphSync(plannerTriggerV2, {
|
|
19
|
+
initialState: 'idle', input: { transport: 'event', id: 'plan.requested', payload: {} }, durableFacts: facts,
|
|
20
|
+
isTransientState: state => state.startsWith('check_'), maxSteps: 4,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Compatibility/testing alias. Production callers use dispatchPlannerTrigger. */
|
|
24
|
+
export const decidePlannerTrigger = dispatchPlannerTrigger;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { GraphRuntime } from './graph-v2.js';
|
|
3
|
+
import { dispatchWorkflowGraph, dispatchWorkflowGraphSync } from './dispatcher.js';
|
|
4
|
+
const source = readFileSync(new URL('../../workflow/review-lifecycle-v2.mmd', import.meta.url), 'utf8');
|
|
5
|
+
const boolGuard = (name) => (facts) => facts.durableFacts[name] === true;
|
|
6
|
+
export const reviewLifecycleV2 = new GraphRuntime(source, {
|
|
7
|
+
states: new Set(['NONE', 'PENDING', 'STARTED', 'APPROVED', 'CHANGES_REQUIRED', 'BLOCKED', 'STALE']),
|
|
8
|
+
guards: {
|
|
9
|
+
unscheduled: boolGuard('unscheduled'),
|
|
10
|
+
reviewEnabled: boolGuard('reviewEnabled'),
|
|
11
|
+
prOpen: boolGuard('prOpen'),
|
|
12
|
+
jobActive: boolGuard('jobActive'),
|
|
13
|
+
implementationComplete: boolGuard('implementationComplete'),
|
|
14
|
+
headAvailable: boolGuard('headAvailable'),
|
|
15
|
+
completionGateSatisfied: boolGuard('completionGateSatisfied'),
|
|
16
|
+
noActiveReviewTask: boolGuard('noActiveReviewTask'),
|
|
17
|
+
currentReviewCancelled: boolGuard('currentReviewCancelled'),
|
|
18
|
+
noApprovedCurrentReview: boolGuard('noApprovedCurrentReview'),
|
|
19
|
+
autoMerge: boolGuard('autoMerge'),
|
|
20
|
+
},
|
|
21
|
+
actions: Object.fromEntries(['review.create', 'review.cancelStale', 'review.createFix', 'review.requestMerge'].map(id => [id, { id }])),
|
|
22
|
+
events: Object.fromEntries(['reconcile', 'review.started', 'review.approved', 'review.changes_required', 'review.blocked', 'head.changed'].map(id => [id, { id }])),
|
|
23
|
+
});
|
|
24
|
+
export function reviewGraphState(outcome) {
|
|
25
|
+
if (!outcome)
|
|
26
|
+
return 'NONE';
|
|
27
|
+
return outcome.toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
export function reviewOutcome(state) {
|
|
30
|
+
if (state === 'NONE')
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!['PENDING', 'STARTED', 'APPROVED', 'CHANGES_REQUIRED', 'BLOCKED', 'STALE'].includes(state)) {
|
|
33
|
+
throw new Error('Unknown review graph state ' + state);
|
|
34
|
+
}
|
|
35
|
+
return state.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
export async function dispatchReviewLifecycle(outcome, event, facts = {}, executeAction) {
|
|
38
|
+
return dispatchWorkflowGraph(reviewLifecycleV2, { initialState: reviewGraphState(outcome), input: { transport: 'event', id: event, payload: {} }, durableFacts: facts, executeAction });
|
|
39
|
+
}
|
|
40
|
+
export function decideReviewLifecycle(outcome, event, facts = {}) {
|
|
41
|
+
return dispatchWorkflowGraphSync(reviewLifecycleV2, { initialState: reviewGraphState(outcome), input: { transport: 'event', id: event, payload: {} }, durableFacts: facts });
|
|
42
|
+
}
|
package/dist/workflow/runtime.js
CHANGED
|
@@ -17,12 +17,26 @@ export function graphWorkflowDecision(engine, from, event, facts = {}) {
|
|
|
17
17
|
reason: 'accepted',
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
-
function applyTaskMutation(task, mutation) {
|
|
20
|
+
export function applyTaskMutation(task, mutation) {
|
|
21
21
|
if (mutation === 'mutation.clearError')
|
|
22
22
|
task.error = null;
|
|
23
23
|
if (mutation === 'mutation.clearFailureReason')
|
|
24
24
|
task.failureReason = undefined;
|
|
25
|
+
if (mutation === 'mutation.clearLease') {
|
|
26
|
+
task.leaseOwner = null;
|
|
27
|
+
task.leaseExpiresAt = null;
|
|
28
|
+
}
|
|
29
|
+
if (mutation === 'mutation.clearExecution')
|
|
30
|
+
task.executionId = null;
|
|
31
|
+
if (mutation === 'mutation.markLeaseExpired')
|
|
32
|
+
task.error = 'Lease expired after maximum attempts';
|
|
33
|
+
if (mutation === 'mutation.markStartupOrphanedExhausted')
|
|
34
|
+
task.error = 'Orphaned after relayd restart after maximum attempts';
|
|
35
|
+
if (mutation === 'mutation.markPlannerUnavailable')
|
|
36
|
+
task.error = 'No objective planner configured';
|
|
25
37
|
}
|
|
38
|
+
export function applyTaskGraphTarget(task, target, timestamp = new Date().toISOString()) { task.state = target; task.updatedAt = timestamp; }
|
|
39
|
+
export function applyJobGraphTarget(job, target, timestamp = new Date().toISOString()) { job.state = target; job.updatedAt = timestamp; }
|
|
26
40
|
export function applyTaskWorkflowDecision(task, decision, timestamp = new Date().toISOString()) {
|
|
27
41
|
if (!decision.accepted || !decision.target)
|
|
28
42
|
throw new Error(`Cannot apply rejected workflow decision to task ${task.id}`);
|