elsabro 6.0.0 → 7.0.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/agents/elsabro-orchestrator.md +39 -0
- package/bin/install.js +8 -4
- package/commands/elsabro/add-phase.md +20 -0
- package/commands/elsabro/complete-milestone.md +20 -0
- package/commands/elsabro/design-ui.md +21 -0
- package/commands/elsabro/execute.md +132 -12
- package/commands/elsabro/new-milestone.md +23 -0
- package/commands/elsabro/party.md +80 -23
- package/commands/elsabro/quick.md +19 -2
- package/flow-engine/src/agent-cards.json +74 -0
- package/flow-engine/src/callbacks.js +268 -0
- package/flow-engine/src/cli.js +597 -0
- package/flow-engine/src/executors.js +14 -1
- package/flow-engine/src/index.js +4 -2
- package/flow-engine/src/party.js +414 -0
- package/flow-engine/src/runner.js +5 -5
- package/flow-engine/tests/callbacks.test.js +274 -0
- package/flow-engine/tests/cli.test.js +208 -0
- package/flow-engine/tests/executors-complex.test.js +61 -1
- package/flow-engine/tests/integration.test.js +667 -38
- package/flow-engine/tests/party.test.js +724 -0
- package/flows/development-flow.json +104 -120
- package/package.json +5 -3
- package/references/next-step-engine.md +19 -0
- package/references/state-sync.md +15 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PartyEngine — Multi-agent collaborative discussion engine.
|
|
5
|
+
*
|
|
6
|
+
* Selects relevant agents based on topic keywords, runs round-based
|
|
7
|
+
* discussions with callbacks, and produces synthesis summaries.
|
|
8
|
+
* Reuses CheckpointManager for session persistence.
|
|
9
|
+
*
|
|
10
|
+
* Zero external dependencies. CommonJS, Node 18+.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { CheckpointManager } = require('./checkpoint');
|
|
15
|
+
|
|
16
|
+
// ── Agent Personality Cards ──────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CARDS = require('./agent-cards.json');
|
|
19
|
+
|
|
20
|
+
// ── Relevance Map ────────────────────────────────────────────────────────────
|
|
21
|
+
// Maps keywords to agent scores. Each bucket awards points to 1-3 agents.
|
|
22
|
+
// Higher score = more relevant to the topic.
|
|
23
|
+
|
|
24
|
+
const RELEVANCE_MAP = {
|
|
25
|
+
architecture: { planner: 3, executor: 2, debugger: 1 },
|
|
26
|
+
testing: { qa: 3, executor: 2, verifier: 1 },
|
|
27
|
+
design: { 'ux-designer': 3, planner: 2, analyst: 1 },
|
|
28
|
+
ux: { 'ux-designer': 3, analyst: 2, 'tech-writer': 1 },
|
|
29
|
+
ui: { 'ux-designer': 3, executor: 2, analyst: 1 },
|
|
30
|
+
bug: { debugger: 3, qa: 2, executor: 1 },
|
|
31
|
+
debug: { debugger: 3, qa: 2, verifier: 1 },
|
|
32
|
+
performance: { debugger: 3, executor: 2, qa: 1 },
|
|
33
|
+
documentation: { 'tech-writer': 3, planner: 2, analyst: 1 },
|
|
34
|
+
docs: { 'tech-writer': 3, 'ux-designer': 1, planner: 1 },
|
|
35
|
+
planning: { planner: 3, 'scrum-master': 2, analyst: 1 },
|
|
36
|
+
sprint: { 'scrum-master': 3, planner: 2, qa: 1 },
|
|
37
|
+
scope: { analyst: 3, 'scrum-master': 2, planner: 1 },
|
|
38
|
+
requirements: { analyst: 3, planner: 2, 'ux-designer': 1 },
|
|
39
|
+
deploy: { executor: 3, qa: 2, verifier: 1 },
|
|
40
|
+
refactor: { executor: 3, debugger: 2, verifier: 1 },
|
|
41
|
+
security: { verifier: 3, debugger: 2, qa: 1 },
|
|
42
|
+
quality: { verifier: 3, qa: 2, 'tech-writer': 1 }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Default scores when no keywords match — ensures every agent is reachable.
|
|
46
|
+
const DEFAULT_SCORES = {
|
|
47
|
+
analyst: 2, planner: 2, executor: 1,
|
|
48
|
+
qa: 1, debugger: 1, 'ux-designer': 1,
|
|
49
|
+
'scrum-master': 1, 'tech-writer': 1, verifier: 1
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Slugify a topic string for use as a checkpoint flowId component.
|
|
56
|
+
* @param {string} text
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function slugify(text) {
|
|
60
|
+
return text
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
63
|
+
.replace(/^-+|-+$/g, '')
|
|
64
|
+
.slice(0, 40);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect agent names mentioned directly in topic text.
|
|
69
|
+
* Returns array of matching agent IDs.
|
|
70
|
+
* @param {string} topic
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
function detectAgentNames(topic) {
|
|
74
|
+
const lower = topic.toLowerCase();
|
|
75
|
+
const found = [];
|
|
76
|
+
for (const card of Object.values(CARDS)) {
|
|
77
|
+
if (lower.includes(card.name.toLowerCase())) {
|
|
78
|
+
found.push(card.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── PartyEngine ──────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
class PartyEngine {
|
|
87
|
+
/**
|
|
88
|
+
* @param {object} [options]
|
|
89
|
+
* @param {string} [options.checkpointDir] – checkpoint directory
|
|
90
|
+
* @param {number} [options.maxRounds] – max discussion rounds (default: 3)
|
|
91
|
+
* @param {number} [options.maxAgents] – max agents per party (default: 3)
|
|
92
|
+
*/
|
|
93
|
+
constructor(options = {}) {
|
|
94
|
+
this.maxRounds = options.maxRounds || 3;
|
|
95
|
+
this.maxAgents = options.maxAgents || 3;
|
|
96
|
+
this.cards = { ...CARDS };
|
|
97
|
+
this.checkpoint = new CheckpointManager({
|
|
98
|
+
dir: options.checkpointDir
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Select the most relevant agents for a topic.
|
|
104
|
+
*
|
|
105
|
+
* Algorithm:
|
|
106
|
+
* 1. Tokenize topic (lowercase, split on whitespace)
|
|
107
|
+
* 2. Match tokens against RELEVANCE_MAP keys
|
|
108
|
+
* 3. Accumulate scores (each keyword scores once per agent)
|
|
109
|
+
* 4. Break ties by first matching keyword position
|
|
110
|
+
* 5. Respect manual overrides (options.agents)
|
|
111
|
+
* 6. Detect agent names in topic → force inclusion
|
|
112
|
+
*
|
|
113
|
+
* @param {string} topic
|
|
114
|
+
* @param {object} [options]
|
|
115
|
+
* @param {string[]} [options.agents] – manual agent IDs (bypass scoring)
|
|
116
|
+
* @param {number} [options.maxAgents] – override instance maxAgents
|
|
117
|
+
* @returns {object[]} array of agent card objects
|
|
118
|
+
*/
|
|
119
|
+
selectAgents(topic, options = {}) {
|
|
120
|
+
const max = options.maxAgents || this.maxAgents;
|
|
121
|
+
|
|
122
|
+
// Manual override: bypass scoring entirely
|
|
123
|
+
if (options.agents && options.agents.length > 0) {
|
|
124
|
+
return options.agents
|
|
125
|
+
.map(id => this.cards[id])
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.slice(0, max);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Detect agent names mentioned in topic
|
|
131
|
+
const forced = detectAgentNames(topic || '');
|
|
132
|
+
|
|
133
|
+
// Tokenize
|
|
134
|
+
const tokens = (topic || '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
135
|
+
|
|
136
|
+
// Score accumulation
|
|
137
|
+
const scores = {};
|
|
138
|
+
const firstMatch = {}; // track first keyword position for tie-breaking
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
141
|
+
const token = tokens[i];
|
|
142
|
+
const bucket = RELEVANCE_MAP[token];
|
|
143
|
+
if (!bucket) continue;
|
|
144
|
+
|
|
145
|
+
for (const [agentId, score] of Object.entries(bucket)) {
|
|
146
|
+
// Each keyword scores once per agent (no duplicates from repeated tokens)
|
|
147
|
+
const key = `${agentId}:${token}`;
|
|
148
|
+
if (!scores[`_seen_${key}`]) {
|
|
149
|
+
scores[`_seen_${key}`] = true;
|
|
150
|
+
scores[agentId] = (scores[agentId] || 0) + score;
|
|
151
|
+
if (firstMatch[agentId] === undefined) {
|
|
152
|
+
firstMatch[agentId] = i;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If no keywords matched, use defaults
|
|
159
|
+
const hasMatches = Object.keys(scores).some(k => !k.startsWith('_seen_'));
|
|
160
|
+
if (!hasMatches) {
|
|
161
|
+
for (const [id, score] of Object.entries(DEFAULT_SCORES)) {
|
|
162
|
+
scores[id] = score;
|
|
163
|
+
firstMatch[id] = 999;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build sorted agent list
|
|
168
|
+
const agentIds = Object.keys(this.cards);
|
|
169
|
+
const ranked = agentIds
|
|
170
|
+
.filter(id => scores[id] > 0)
|
|
171
|
+
.sort((a, b) => {
|
|
172
|
+
// Primary: higher score first
|
|
173
|
+
const scoreDiff = (scores[b] || 0) - (scores[a] || 0);
|
|
174
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
175
|
+
// Tiebreak: earlier first-match position
|
|
176
|
+
return (firstMatch[a] || 999) - (firstMatch[b] || 999);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Merge forced agents (from name detection) with scored ranking
|
|
180
|
+
const result = [];
|
|
181
|
+
const added = new Set();
|
|
182
|
+
|
|
183
|
+
// Forced agents first
|
|
184
|
+
for (const id of forced) {
|
|
185
|
+
if (this.cards[id] && !added.has(id)) {
|
|
186
|
+
result.push(this.cards[id]);
|
|
187
|
+
added.add(id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Fill remaining from ranked
|
|
192
|
+
for (const id of ranked) {
|
|
193
|
+
if (added.has(id)) continue;
|
|
194
|
+
if (result.length >= max) break;
|
|
195
|
+
result.push(this.cards[id]);
|
|
196
|
+
added.add(id);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result.slice(0, max);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Run a full party discussion session.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} topic – discussion topic
|
|
206
|
+
* @param {object} [options] – { agents, maxRounds, maxAgents, projectContext }
|
|
207
|
+
* @param {object} callbacks
|
|
208
|
+
* @param {function} callbacks.onAgentTurn – (params) => string response
|
|
209
|
+
* @param {function} [callbacks.onSynthesize] – (params) => { consensus, debates, actionItems, keyInsights, suggestedNext }
|
|
210
|
+
* @param {function} [callbacks.onRoundComplete] – (params) => "continue"|"stop"|"add_round"
|
|
211
|
+
* @param {function} [callbacks.onFormatSummary] – (template, data) => string
|
|
212
|
+
* @returns {Promise<object>} { topic, agents, rounds, history, synthesis, sessionId }
|
|
213
|
+
*/
|
|
214
|
+
async run(topic, options = {}, callbacks = {}) {
|
|
215
|
+
if (!callbacks.onAgentTurn) {
|
|
216
|
+
throw new Error('PartyEngine.run() requires callbacks.onAgentTurn');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const agents = this.selectAgents(topic, options);
|
|
220
|
+
let totalRounds = options.maxRounds || this.maxRounds;
|
|
221
|
+
const sessionId = `party-${slugify(topic)}-${Date.now()}`;
|
|
222
|
+
const history = [];
|
|
223
|
+
|
|
224
|
+
for (let round = 1; round <= totalRounds; round++) {
|
|
225
|
+
const roundResponses = [];
|
|
226
|
+
|
|
227
|
+
for (const agent of agents) {
|
|
228
|
+
const instruction = round === 1
|
|
229
|
+
? 'Share your initial perspective on this topic.'
|
|
230
|
+
: 'React to previous perspectives and refine your position.';
|
|
231
|
+
|
|
232
|
+
const response = await callbacks.onAgentTurn({
|
|
233
|
+
agent,
|
|
234
|
+
topic,
|
|
235
|
+
round,
|
|
236
|
+
totalRounds,
|
|
237
|
+
history: [...history],
|
|
238
|
+
instruction,
|
|
239
|
+
projectContext: options.projectContext || null
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const entry = {
|
|
243
|
+
round,
|
|
244
|
+
agent: { id: agent.id, name: agent.name },
|
|
245
|
+
response: response || '',
|
|
246
|
+
timestamp: new Date().toISOString()
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
history.push(entry);
|
|
250
|
+
roundResponses.push(entry);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Checkpoint after each round
|
|
254
|
+
this.checkpoint.save(sessionId, {
|
|
255
|
+
topic,
|
|
256
|
+
agents: agents.map(a => a.id),
|
|
257
|
+
currentRound: round,
|
|
258
|
+
totalRounds,
|
|
259
|
+
history,
|
|
260
|
+
options
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Round complete callback
|
|
264
|
+
if (callbacks.onRoundComplete) {
|
|
265
|
+
const decision = await callbacks.onRoundComplete({
|
|
266
|
+
completedRound: round,
|
|
267
|
+
totalRounds,
|
|
268
|
+
lastResponses: roundResponses
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (decision === 'stop') break;
|
|
272
|
+
if (decision === 'add_round') totalRounds++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Synthesis
|
|
277
|
+
let synthesis = null;
|
|
278
|
+
if (callbacks.onSynthesize) {
|
|
279
|
+
synthesis = await callbacks.onSynthesize({
|
|
280
|
+
history,
|
|
281
|
+
topic,
|
|
282
|
+
agents
|
|
283
|
+
});
|
|
284
|
+
} else {
|
|
285
|
+
synthesis = { consensus: [], debates: [], actionItems: [], keyInsights: [], suggestedNext: null };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Clean old checkpoints (keep last 2)
|
|
289
|
+
this.checkpoint.clean(sessionId, 2);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
topic,
|
|
293
|
+
agents: agents.map(a => ({ id: a.id, name: a.name })),
|
|
294
|
+
rounds: history.length > 0 ? history[history.length - 1].round : 0,
|
|
295
|
+
history,
|
|
296
|
+
synthesis,
|
|
297
|
+
sessionId
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resume a party session from checkpoint.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} sessionId – checkpoint session ID
|
|
305
|
+
* @param {object} callbacks – same as run()
|
|
306
|
+
* @returns {Promise<object>} same as run()
|
|
307
|
+
*/
|
|
308
|
+
async resume(sessionId, callbacks = {}) {
|
|
309
|
+
if (!callbacks.onAgentTurn) {
|
|
310
|
+
throw new Error('PartyEngine.resume() requires callbacks.onAgentTurn');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const checkpoint = this.checkpoint.load(sessionId);
|
|
314
|
+
if (!checkpoint) {
|
|
315
|
+
throw new Error(`No checkpoint found for session: ${sessionId}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const { topic, agents: agentIds, currentRound, totalRounds, history, options } = checkpoint;
|
|
319
|
+
const agents = agentIds.map(id => this.cards[id]).filter(Boolean);
|
|
320
|
+
let rounds = totalRounds;
|
|
321
|
+
const startRound = currentRound + 1;
|
|
322
|
+
|
|
323
|
+
for (let round = startRound; round <= rounds; round++) {
|
|
324
|
+
const roundResponses = [];
|
|
325
|
+
|
|
326
|
+
for (const agent of agents) {
|
|
327
|
+
const instruction = round === 1
|
|
328
|
+
? 'Share your initial perspective on this topic.'
|
|
329
|
+
: 'React to previous perspectives and refine your position.';
|
|
330
|
+
|
|
331
|
+
const response = await callbacks.onAgentTurn({
|
|
332
|
+
agent,
|
|
333
|
+
topic,
|
|
334
|
+
round,
|
|
335
|
+
totalRounds: rounds,
|
|
336
|
+
history: [...history],
|
|
337
|
+
instruction,
|
|
338
|
+
projectContext: (options && options.projectContext) || null
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const entry = {
|
|
342
|
+
round,
|
|
343
|
+
agent: { id: agent.id, name: agent.name },
|
|
344
|
+
response: response || '',
|
|
345
|
+
timestamp: new Date().toISOString()
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
history.push(entry);
|
|
349
|
+
roundResponses.push(entry);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
this.checkpoint.save(sessionId, {
|
|
353
|
+
topic,
|
|
354
|
+
agents: agentIds,
|
|
355
|
+
currentRound: round,
|
|
356
|
+
totalRounds: rounds,
|
|
357
|
+
history,
|
|
358
|
+
options
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (callbacks.onRoundComplete) {
|
|
362
|
+
const decision = await callbacks.onRoundComplete({
|
|
363
|
+
completedRound: round,
|
|
364
|
+
totalRounds: rounds,
|
|
365
|
+
lastResponses: roundResponses
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (decision === 'stop') break;
|
|
369
|
+
if (decision === 'add_round') rounds++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let synthesis = null;
|
|
374
|
+
if (callbacks.onSynthesize) {
|
|
375
|
+
synthesis = await callbacks.onSynthesize({
|
|
376
|
+
history,
|
|
377
|
+
topic,
|
|
378
|
+
agents
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
synthesis = { consensus: [], debates: [], actionItems: [], keyInsights: [], suggestedNext: null };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.checkpoint.clean(sessionId, 2);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
topic,
|
|
388
|
+
agents: agents.map(a => ({ id: a.id, name: a.name })),
|
|
389
|
+
rounds: history.length > 0 ? history[history.length - 1].round : 0,
|
|
390
|
+
history,
|
|
391
|
+
synthesis,
|
|
392
|
+
sessionId
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get all loaded personality cards.
|
|
398
|
+
* @returns {object} map of agentId → card
|
|
399
|
+
*/
|
|
400
|
+
getCards() {
|
|
401
|
+
return { ...this.cards };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get a single personality card by agent ID.
|
|
406
|
+
* @param {string} agentId
|
|
407
|
+
* @returns {object|undefined}
|
|
408
|
+
*/
|
|
409
|
+
getCard(agentId) {
|
|
410
|
+
return this.cards[agentId];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = { PartyEngine, RELEVANCE_MAP, slugify, detectAgentNames };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* not_implemented nodes gracefully.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const { getExecutor, checkRuntimeStatus, NotImplementedError } = require('./executors');
|
|
11
|
+
const { getExecutor, checkRuntimeStatus, NotImplementedError, DeprecatedNodeError } = require('./executors');
|
|
12
12
|
|
|
13
13
|
class RunnerError extends Error {
|
|
14
14
|
constructor(message, nodeId) {
|
|
@@ -81,8 +81,8 @@ async function runFlow(engine, inputs, callbacks, checkpoint) {
|
|
|
81
81
|
checkRuntimeStatus(node);
|
|
82
82
|
result = await executor(node, context, callbacks);
|
|
83
83
|
} catch (err) {
|
|
84
|
-
// Let NotImplementedError propagate cleanly
|
|
85
|
-
if (err instanceof NotImplementedError) {
|
|
84
|
+
// Let NotImplementedError and DeprecatedNodeError propagate cleanly
|
|
85
|
+
if (err instanceof NotImplementedError || err instanceof DeprecatedNodeError) {
|
|
86
86
|
// Save a checkpoint before stopping
|
|
87
87
|
if (callbacks.onCheckpoint) {
|
|
88
88
|
await callbacks.onCheckpoint({
|
|
@@ -121,9 +121,9 @@ async function runFlow(engine, inputs, callbacks, checkpoint) {
|
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// 6. Notify: node completed
|
|
124
|
+
// 6. Notify: node completed (context passed for post-processing, e.g. dry-run fixups)
|
|
125
125
|
if (callbacks.onNodeComplete) {
|
|
126
|
-
await callbacks.onNodeComplete(node.id, result);
|
|
126
|
+
await callbacks.onNodeComplete(node.id, result, context);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// 7. Advance
|