@pellux/goodvibes-sdk 0.33.17 → 0.33.19
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/dist/contracts/artifacts/operator-contract.json +1 -1
- package/dist/events/workflows.d.ts +1 -1
- package/dist/events/workflows.d.ts.map +1 -1
- package/dist/platform/agents/completion-report.d.ts +3 -3
- package/dist/platform/agents/completion-report.d.ts.map +1 -1
- package/dist/platform/agents/completion-report.js +59 -11
- package/dist/platform/agents/index.d.ts +1 -0
- package/dist/platform/agents/index.d.ts.map +1 -1
- package/dist/platform/agents/index.js +1 -0
- package/dist/platform/agents/wrfc-controller.d.ts +14 -6
- package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-controller.js +316 -100
- package/dist/platform/agents/wrfc-external-adapter.d.ts +45 -0
- package/dist/platform/agents/wrfc-external-adapter.d.ts.map +1 -0
- package/dist/platform/agents/wrfc-external-adapter.js +18 -0
- package/dist/platform/agents/wrfc-prompt-addenda.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-prompt-addenda.js +27 -0
- package/dist/platform/agents/wrfc-reporting.d.ts +7 -3
- package/dist/platform/agents/wrfc-reporting.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-reporting.js +38 -14
- package/dist/platform/agents/wrfc-types.d.ts +35 -5
- package/dist/platform/agents/wrfc-types.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-workmap.d.ts +7 -1
- package/dist/platform/agents/wrfc-workmap.d.ts.map +1 -1
- package/dist/platform/tools/agent/index.d.ts.map +1 -1
- package/dist/platform/tools/agent/index.js +6 -2
- package/dist/platform/tools/agent/manager.d.ts +3 -0
- package/dist/platform/tools/agent/manager.d.ts.map +1 -1
- package/dist/platform/tools/agent/manager.js +43 -6
- package/dist/platform/tools/agent/schema.d.ts +2 -0
- package/dist/platform/tools/agent/schema.d.ts.map +1 -1
- package/dist/platform/version.js +1 -1
- package/package.json +9 -9
|
@@ -8,7 +8,7 @@ import { AgentWorktree } from './worktree.js';
|
|
|
8
8
|
import { completePlanItemsForAgent } from './wrfc-plan-sync.js';
|
|
9
9
|
import { logger } from '../utils/logger.js';
|
|
10
10
|
import { summarizeError } from '../utils/error-display.js';
|
|
11
|
-
import { emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
|
|
11
|
+
import { emitAgentCompleted, emitAgentFailed, emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
|
|
12
12
|
import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, } from './wrfc-config.js';
|
|
13
13
|
import { buildEngineerConstraintAddendum, } from './wrfc-prompt-addenda.js';
|
|
14
14
|
import { completeWrfcOrchestrationNode, createWrfcWorkflowContext, emitWrfcAutoCommitted, emitWrfcCascadeAbort, emitWrfcChainCreated, emitWrfcChainPassed, emitWrfcConstraintsEnumerated, emitWrfcGraphCreated, emitWrfcStateChanged, failWrfcOrchestrationNode, startWrfcOrchestrationNode, } from './wrfc-runtime-events.js';
|
|
@@ -20,7 +20,7 @@ const VALID_TRANSITIONS = {
|
|
|
20
20
|
reviewing: ['fixing', 'awaiting_gates', 'failed'],
|
|
21
21
|
fixing: ['reviewing', 'failed'],
|
|
22
22
|
awaiting_gates: ['gating', 'failed'],
|
|
23
|
-
gating: ['passed', 'failed', 'committing'],
|
|
23
|
+
gating: ['passed', 'failed', 'committing', 'fixing'],
|
|
24
24
|
committing: ['passed', 'failed'],
|
|
25
25
|
};
|
|
26
26
|
const MAX_ACTIVE_CHAINS = 6;
|
|
@@ -30,9 +30,6 @@ export class WrfcController {
|
|
|
30
30
|
chainQueue = [];
|
|
31
31
|
unsubscribers = [];
|
|
32
32
|
activeChainCount = 0;
|
|
33
|
-
pendingParentChainIds = new Map();
|
|
34
|
-
/** Constraints to inherit when a gate-retry child chain is created via the pending path. */
|
|
35
|
-
pendingParentConstraints = new Map();
|
|
36
33
|
sessionId;
|
|
37
34
|
workmap;
|
|
38
35
|
projectRoot;
|
|
@@ -42,6 +39,7 @@ export class WrfcController {
|
|
|
42
39
|
agentManager;
|
|
43
40
|
configManager;
|
|
44
41
|
createWorktree;
|
|
42
|
+
selectChildRoute;
|
|
45
43
|
constructor(runtimeBus, messageBus, deps) {
|
|
46
44
|
this.runtimeBus = runtimeBus;
|
|
47
45
|
this.messageBus = messageBus;
|
|
@@ -49,22 +47,23 @@ export class WrfcController {
|
|
|
49
47
|
this.configManager = deps.configManager;
|
|
50
48
|
this.projectRoot = deps.projectRoot;
|
|
51
49
|
this.createWorktree = deps.createWorktree ?? (() => new AgentWorktree(this.projectRoot));
|
|
50
|
+
this.selectChildRoute = deps.selectChildRoute ?? null;
|
|
52
51
|
this.sessionId = crypto.randomUUID().slice(0, 8);
|
|
53
52
|
this.workmap = new WrfcWorkmap(this.projectRoot, this.sessionId, { surfaceRoot: deps.surfaceRoot });
|
|
54
53
|
this.setupListeners();
|
|
55
54
|
}
|
|
56
|
-
createChain(
|
|
55
|
+
createChain(ownerRecord) {
|
|
57
56
|
logger.info('WrfcController.createChain: called', {
|
|
58
|
-
agentId:
|
|
59
|
-
task:
|
|
57
|
+
agentId: ownerRecord.id,
|
|
58
|
+
task: ownerRecord.task.slice(0, 60),
|
|
60
59
|
activeChainCount: this.activeChainCount,
|
|
61
60
|
});
|
|
62
|
-
const chain = this.createBaseChain(
|
|
61
|
+
const chain = this.createBaseChain(ownerRecord);
|
|
63
62
|
if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
|
|
64
|
-
this.chainQueue.push({ record:
|
|
63
|
+
this.chainQueue.push({ record: ownerRecord, queuedAt: Date.now() });
|
|
65
64
|
logger.debug('WrfcController.createChain: at cap, queued', {
|
|
66
65
|
chainId: chain.id,
|
|
67
|
-
agentId:
|
|
66
|
+
agentId: ownerRecord.id,
|
|
68
67
|
activeCount: this.activeChainCount,
|
|
69
68
|
queueLength: this.chainQueue.length,
|
|
70
69
|
});
|
|
@@ -72,7 +71,7 @@ export class WrfcController {
|
|
|
72
71
|
return chain;
|
|
73
72
|
}
|
|
74
73
|
this.startEngineeringChain(chain, true);
|
|
75
|
-
logger.debug('WrfcController.createChain', { chainId: chain.id, agentId:
|
|
74
|
+
logger.debug('WrfcController.createChain', { chainId: chain.id, agentId: ownerRecord.id });
|
|
76
75
|
return chain;
|
|
77
76
|
}
|
|
78
77
|
getSessionId() { return this.sessionId; }
|
|
@@ -89,6 +88,39 @@ export class WrfcController {
|
|
|
89
88
|
}
|
|
90
89
|
getChain(chainId) { return this.chains.get(chainId) ?? null; }
|
|
91
90
|
listChains() { return Array.from(this.chains.values()); }
|
|
91
|
+
resumeChain(chainId) {
|
|
92
|
+
const chain = this.chains.get(chainId);
|
|
93
|
+
if (!chain || chain.state === 'passed' || chain.state === 'failed')
|
|
94
|
+
return false;
|
|
95
|
+
if (this.hasRunningChild(chain)) {
|
|
96
|
+
this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain already has an active child agent');
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (chain.state !== 'pending') {
|
|
100
|
+
this.appendOwnerDecision(chain, 'resume_skipped', `WRFC chain state ${chain.state} cannot be resumed without an active phase result`);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
|
|
104
|
+
if (!this.chainQueue.some((queued) => queued.record.id === chain.ownerAgentId)) {
|
|
105
|
+
const owner = this.agentManager.getStatus(chain.ownerAgentId);
|
|
106
|
+
if (owner)
|
|
107
|
+
this.chainQueue.push({ record: owner, queuedAt: Date.now() });
|
|
108
|
+
}
|
|
109
|
+
this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain is queued because active chain capacity is full');
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
this.appendOwnerDecision(chain, 'resume_started', 'WRFC owner resumed pending chain');
|
|
113
|
+
this.startEngineeringChain(chain, false);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
resumeAllActiveChains() {
|
|
117
|
+
let resumed = 0;
|
|
118
|
+
for (const chain of this.chains.values()) {
|
|
119
|
+
if (this.resumeChain(chain.id))
|
|
120
|
+
resumed += 1;
|
|
121
|
+
}
|
|
122
|
+
return resumed;
|
|
123
|
+
}
|
|
92
124
|
dispose() {
|
|
93
125
|
for (const unsub of this.unsubscribers)
|
|
94
126
|
unsub();
|
|
@@ -121,12 +153,21 @@ export class WrfcController {
|
|
|
121
153
|
const unsubError = this.runtimeBus.on('AGENT_FAILED', ({ payload }) => {
|
|
122
154
|
this.onAgentFailed(payload.agentId, payload.error);
|
|
123
155
|
});
|
|
124
|
-
this.
|
|
156
|
+
const unsubCancelled = this.runtimeBus.on('AGENT_CANCELLED', ({ payload }) => {
|
|
157
|
+
this.onAgentCancelled(payload.agentId, payload.reason);
|
|
158
|
+
});
|
|
159
|
+
this.unsubscribers.push(unsubComplete, unsubError, unsubCancelled);
|
|
125
160
|
}
|
|
126
161
|
async onAgentComplete(agentId) {
|
|
127
162
|
const chain = this.findChainByAgentId(agentId);
|
|
128
163
|
if (!chain)
|
|
129
164
|
return;
|
|
165
|
+
if (agentId === chain.ownerAgentId) {
|
|
166
|
+
if (chain.ownerTerminalEmitted)
|
|
167
|
+
return;
|
|
168
|
+
this.failChain(chain, 'WRFC owner agent completed before the chain reached a terminal state');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
130
171
|
const record = this.agentManager.getStatus(agentId);
|
|
131
172
|
const rawOutput = record?.fullOutput ?? '';
|
|
132
173
|
logger.debug('WrfcController.onAgentComplete', {
|
|
@@ -165,13 +206,25 @@ export class WrfcController {
|
|
|
165
206
|
const chain = this.findChainByAgentId(agentId);
|
|
166
207
|
if (!chain)
|
|
167
208
|
return;
|
|
209
|
+
if (agentId === chain.ownerAgentId && (chain.state === 'passed' || chain.state === 'failed'))
|
|
210
|
+
return;
|
|
168
211
|
this.failChain(chain, errorMessage ?? `Agent ${agentId} failed`);
|
|
169
212
|
}
|
|
213
|
+
onAgentCancelled(agentId, reason) {
|
|
214
|
+
const chain = this.findChainByAgentId(agentId);
|
|
215
|
+
if (!chain || chain.state === 'passed' || chain.state === 'failed')
|
|
216
|
+
return;
|
|
217
|
+
if (agentId === chain.ownerAgentId) {
|
|
218
|
+
this.cancelChain(chain, reason ?? 'WRFC owner agent cancelled');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.failChain(chain, reason ?? `Agent ${agentId} cancelled`);
|
|
222
|
+
}
|
|
170
223
|
startReview(chain, report) {
|
|
171
224
|
this.transition(chain, 'reviewing');
|
|
172
225
|
// Prepend any synthetic issues from the controller (e.g. fixer constraint-continuity
|
|
173
226
|
// violations) to the review task body, then clear them so they fire only once.
|
|
174
|
-
let reviewTask = buildReviewTask(chain.id, report, getWrfcScoreThreshold(this.configManager), chain.constraints);
|
|
227
|
+
let reviewTask = buildReviewTask(chain.id, chain.task, report, getWrfcScoreThreshold(this.configManager), chain.constraints);
|
|
175
228
|
if (chain.syntheticIssues && chain.syntheticIssues.length > 0) {
|
|
176
229
|
const syntheticBlock = [
|
|
177
230
|
`## Synthetic issues from controller`,
|
|
@@ -181,8 +234,9 @@ export class WrfcController {
|
|
|
181
234
|
reviewTask = syntheticBlock + '\n\n---\n\n' + reviewTask;
|
|
182
235
|
chain.syntheticIssues = [];
|
|
183
236
|
}
|
|
184
|
-
const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', reviewTask, true);
|
|
237
|
+
const reviewerRecord = this.spawnWrfcAgent(chain, 'reviewer', 'reviewer', reviewTask, true);
|
|
185
238
|
chain.reviewerAgentId = reviewerRecord.id;
|
|
239
|
+
reviewerRecord.wrfcRole = 'reviewer';
|
|
186
240
|
chain.allAgentIds.push(reviewerRecord.id);
|
|
187
241
|
reviewerRecord.wrfcId = chain.id;
|
|
188
242
|
this.messageBus.registerAgent({
|
|
@@ -195,6 +249,11 @@ export class WrfcController {
|
|
|
195
249
|
chainId: chain.id,
|
|
196
250
|
reviewerAgentId: reviewerRecord.id,
|
|
197
251
|
});
|
|
252
|
+
this.appendOwnerDecision(chain, 'spawn_reviewer', this.withRouteReason('Review full current result against the original WRFC ask', reviewerRecord), {
|
|
253
|
+
agentId: reviewerRecord.id,
|
|
254
|
+
role: 'reviewer',
|
|
255
|
+
record: reviewerRecord,
|
|
256
|
+
});
|
|
198
257
|
}
|
|
199
258
|
async processReview(chain, review) {
|
|
200
259
|
const threshold = getWrfcScoreThreshold(this.configManager);
|
|
@@ -249,6 +308,11 @@ export class WrfcController {
|
|
|
249
308
|
});
|
|
250
309
|
chain.reviewScores.push(review.score);
|
|
251
310
|
if (passed) {
|
|
311
|
+
this.appendOwnerDecision(chain, 'review_passed', `Review score ${review.score}/10 met threshold ${threshold}/10`, {
|
|
312
|
+
agentId: chain.reviewerAgentId,
|
|
313
|
+
role: 'reviewer',
|
|
314
|
+
reviewScore: review.score,
|
|
315
|
+
});
|
|
252
316
|
this.transition(chain, 'awaiting_gates');
|
|
253
317
|
await this.checkAndRunGatesForAll();
|
|
254
318
|
return;
|
|
@@ -269,6 +333,11 @@ export class WrfcController {
|
|
|
269
333
|
this.failChain(chain, failureReason);
|
|
270
334
|
return;
|
|
271
335
|
}
|
|
336
|
+
this.appendOwnerDecision(chain, 'review_failed', `Review score ${review.score}/10 did not pass full-scope WRFC review`, {
|
|
337
|
+
agentId: chain.reviewerAgentId,
|
|
338
|
+
role: 'reviewer',
|
|
339
|
+
reviewScore: review.score,
|
|
340
|
+
});
|
|
272
341
|
this.startFix(chain, review);
|
|
273
342
|
}
|
|
274
343
|
startFix(chain, review) {
|
|
@@ -282,8 +351,9 @@ export class WrfcController {
|
|
|
282
351
|
maxAttempts,
|
|
283
352
|
...(targetConstraintIds.length > 0 ? { targetConstraintIds } : {}),
|
|
284
353
|
});
|
|
285
|
-
const fixerRecord = this.spawnWrfcAgent(chain, 'engineer', buildFixTask(chain.id, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts, chain.constraints, review.constraintFindings ?? []), true);
|
|
354
|
+
const fixerRecord = this.spawnWrfcAgent(chain, 'fixer', 'engineer', buildFixTask(chain.id, chain.task, review, getWrfcScoreThreshold(this.configManager), chain.fixAttempts, chain.constraints, review.constraintFindings ?? []), true);
|
|
286
355
|
chain.fixerAgentId = fixerRecord.id;
|
|
356
|
+
fixerRecord.wrfcRole = 'fixer';
|
|
287
357
|
chain.allAgentIds.push(fixerRecord.id);
|
|
288
358
|
fixerRecord.wrfcId = chain.id;
|
|
289
359
|
this.messageBus.registerAgent({
|
|
@@ -304,6 +374,11 @@ export class WrfcController {
|
|
|
304
374
|
fixerAgentId: fixerRecord.id,
|
|
305
375
|
attempt: chain.fixAttempts,
|
|
306
376
|
});
|
|
377
|
+
this.appendOwnerDecision(chain, 'spawn_fixer', this.withRouteReason('Fix review findings while preserving the full original WRFC ask', fixerRecord), {
|
|
378
|
+
agentId: fixerRecord.id,
|
|
379
|
+
role: 'fixer',
|
|
380
|
+
record: fixerRecord,
|
|
381
|
+
});
|
|
307
382
|
}
|
|
308
383
|
async runGates(chain) {
|
|
309
384
|
this.transition(chain, 'gating');
|
|
@@ -382,6 +457,7 @@ export class WrfcController {
|
|
|
382
457
|
if (allPassed) {
|
|
383
458
|
this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_passed' });
|
|
384
459
|
chain.gatesPassed = true;
|
|
460
|
+
this.appendOwnerDecision(chain, 'gate_passed', 'All configured WRFC quality gates passed');
|
|
385
461
|
if (autoCommit) {
|
|
386
462
|
await this.autoCommit(chain);
|
|
387
463
|
}
|
|
@@ -391,50 +467,54 @@ export class WrfcController {
|
|
|
391
467
|
return;
|
|
392
468
|
}
|
|
393
469
|
const failedGates = results.filter((result) => !result.passed);
|
|
394
|
-
const fingerprint = failedGates.map((result) => `${result.gate}:${result.output.slice(0, 200)}`).join('|');
|
|
395
470
|
const maxGateRetries = getWrfcMaxFixAttempts(this.configManager);
|
|
396
|
-
chain.
|
|
397
|
-
|
|
398
|
-
if (chain.gateRetryDepth >= maxGateRetries) {
|
|
471
|
+
this.appendOwnerDecision(chain, 'gate_failed', `${failedGates.length} quality gate(s) failed and require same-chain fixing`);
|
|
472
|
+
if (chain.fixAttempts >= maxGateRetries) {
|
|
399
473
|
logger.error('WrfcController.processGateResults: gate retry limit reached, manual intervention required', {
|
|
400
474
|
chainId: chain.id,
|
|
401
|
-
|
|
475
|
+
fixAttempts: chain.fixAttempts,
|
|
402
476
|
maxGateRetries,
|
|
403
477
|
});
|
|
404
|
-
emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.
|
|
478
|
+
emitWrfcCascadeAbort(this.runtimeBus, this.sessionId, chain.id, `Gate failures exceeded max retries (${chain.fixAttempts}/${maxGateRetries}). Manual intervention required.`);
|
|
479
|
+
this.failChain(chain, `Gate failures exceeded max retries (${chain.fixAttempts}/${maxGateRetries})`);
|
|
405
480
|
return;
|
|
406
481
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
logger.debug('WrfcController.processGateResults: gate failure — spawned
|
|
436
|
-
|
|
437
|
-
|
|
482
|
+
chain.fixAttempts += 1;
|
|
483
|
+
this.transition(chain, 'fixing');
|
|
484
|
+
emitWorkflowFixAttempted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
|
|
485
|
+
chainId: chain.id,
|
|
486
|
+
attempt: chain.fixAttempts,
|
|
487
|
+
maxAttempts: maxGateRetries,
|
|
488
|
+
...(chain.constraints.length > 0 ? { targetConstraintIds: chain.constraints.map((constraint) => constraint.id) } : {}),
|
|
489
|
+
});
|
|
490
|
+
const gateFixTask = buildGateFailureTask(chain.id, chain.task, failedGates, chain.constraints);
|
|
491
|
+
const fixerRecord = this.spawnWrfcAgent(chain, 'fixer', 'engineer', gateFixTask, true);
|
|
492
|
+
fixerRecord.wrfcRole = 'fixer';
|
|
493
|
+
chain.fixerAgentId = fixerRecord.id;
|
|
494
|
+
chain.allAgentIds.push(fixerRecord.id);
|
|
495
|
+
fixerRecord.wrfcId = chain.id;
|
|
496
|
+
this.messageBus.registerAgent({
|
|
497
|
+
agentId: fixerRecord.id,
|
|
498
|
+
role: 'fixer',
|
|
499
|
+
wrfcId: chain.id,
|
|
500
|
+
});
|
|
501
|
+
chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `fix:${chain.fixAttempts}:gates`, 'fixer', `Gate fix attempt ${chain.fixAttempts}`, fixerRecord.id);
|
|
502
|
+
this.workmap.append({
|
|
503
|
+
ts: new Date().toISOString(),
|
|
504
|
+
wrfcId: chain.id,
|
|
505
|
+
event: 'fix_started',
|
|
506
|
+
agentId: fixerRecord.id,
|
|
507
|
+
attempt: chain.fixAttempts,
|
|
508
|
+
gate: failedGates.map((gate) => gate.gate).join(', '),
|
|
509
|
+
});
|
|
510
|
+
logger.debug('WrfcController.processGateResults: gate failure — spawned same-chain fixer', {
|
|
511
|
+
chainId: chain.id,
|
|
512
|
+
fixerAgentId: fixerRecord.id,
|
|
513
|
+
});
|
|
514
|
+
this.appendOwnerDecision(chain, 'spawn_gate_fixer', this.withRouteReason('Fix failed quality gates in the same WRFC owner chain', fixerRecord), {
|
|
515
|
+
agentId: fixerRecord.id,
|
|
516
|
+
role: 'fixer',
|
|
517
|
+
record: fixerRecord,
|
|
438
518
|
});
|
|
439
519
|
}
|
|
440
520
|
scheduleChainCleanup(chain) {
|
|
@@ -513,7 +593,7 @@ export class WrfcController {
|
|
|
513
593
|
}
|
|
514
594
|
failChain(chain, reason) {
|
|
515
595
|
if (chain.state === 'pending') {
|
|
516
|
-
this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.
|
|
596
|
+
this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.ownerAgentId);
|
|
517
597
|
}
|
|
518
598
|
const wasActive = chain.state !== 'passed' && chain.state !== 'failed' && chain.state !== 'pending';
|
|
519
599
|
this.failCurrentNode(chain, reason);
|
|
@@ -528,12 +608,69 @@ export class WrfcController {
|
|
|
528
608
|
}
|
|
529
609
|
chain.error = reason;
|
|
530
610
|
chain.completedAt = Date.now();
|
|
611
|
+
this.cancelRunningChildren(chain);
|
|
612
|
+
this.appendOwnerDecision(chain, 'chain_failed', reason, {
|
|
613
|
+
agentId: chain.ownerAgentId,
|
|
614
|
+
});
|
|
615
|
+
this.completeOwnerAgent(chain, 'failed', reason);
|
|
531
616
|
this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
|
|
532
617
|
emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
|
|
533
618
|
logger.error('WrfcController.failChain', { chainId: chain.id, reason });
|
|
534
619
|
this.scheduleChainCleanup(chain);
|
|
535
620
|
this.safeDequeueNext();
|
|
536
621
|
}
|
|
622
|
+
cancelRunningChildren(chain) {
|
|
623
|
+
for (const agentId of chain.allAgentIds) {
|
|
624
|
+
if (agentId === chain.ownerAgentId)
|
|
625
|
+
continue;
|
|
626
|
+
const record = this.agentManager.getStatus(agentId);
|
|
627
|
+
if (record?.status === 'pending' || record?.status === 'running') {
|
|
628
|
+
this.agentManager.cancel(agentId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
hasRunningChild(chain) {
|
|
633
|
+
return chain.allAgentIds.some((agentId) => {
|
|
634
|
+
if (agentId === chain.ownerAgentId)
|
|
635
|
+
return false;
|
|
636
|
+
const record = this.agentManager.getStatus(agentId);
|
|
637
|
+
return record?.status === 'pending' || record?.status === 'running';
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
cancelChain(chain, reason) {
|
|
641
|
+
if (chain.state === 'pending') {
|
|
642
|
+
this.chainQueue = this.chainQueue.filter((queued) => queued.record.id !== chain.ownerAgentId);
|
|
643
|
+
}
|
|
644
|
+
const wasActive = chain.state !== 'pending';
|
|
645
|
+
this.failCurrentNode(chain, reason);
|
|
646
|
+
try {
|
|
647
|
+
this.transition(chain, 'failed');
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
chain.state = 'failed';
|
|
651
|
+
}
|
|
652
|
+
if (wasActive) {
|
|
653
|
+
this.activeChainCount = Math.max(0, this.activeChainCount - 1);
|
|
654
|
+
}
|
|
655
|
+
chain.error = reason;
|
|
656
|
+
chain.completedAt = Date.now();
|
|
657
|
+
chain.ownerTerminalEmitted = true;
|
|
658
|
+
this.appendOwnerDecision(chain, 'chain_cancelled', reason, {
|
|
659
|
+
agentId: chain.ownerAgentId,
|
|
660
|
+
});
|
|
661
|
+
const owner = this.agentManager.getStatus(chain.ownerAgentId);
|
|
662
|
+
if (owner && (owner.status === 'pending' || owner.status === 'running')) {
|
|
663
|
+
owner.status = 'cancelled';
|
|
664
|
+
owner.completedAt = Date.now();
|
|
665
|
+
owner.progress = reason;
|
|
666
|
+
}
|
|
667
|
+
this.cancelRunningChildren(chain);
|
|
668
|
+
this.workmap.append({ ts: new Date().toISOString(), wrfcId: chain.id, event: 'chain_failed', reason });
|
|
669
|
+
emitWorkflowChainFailed(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), { chainId: chain.id, reason });
|
|
670
|
+
logger.warn('WrfcController.cancelChain', { chainId: chain.id, reason });
|
|
671
|
+
this.scheduleChainCleanup(chain);
|
|
672
|
+
this.safeDequeueNext();
|
|
673
|
+
}
|
|
537
674
|
async dequeueNext() {
|
|
538
675
|
if (this.chainQueue.length === 0 || this.activeChainCount >= MAX_ACTIVE_CHAINS)
|
|
539
676
|
return;
|
|
@@ -565,6 +702,38 @@ export class WrfcController {
|
|
|
565
702
|
return null;
|
|
566
703
|
}
|
|
567
704
|
generateWrfcId() { return `wrfc-${crypto.randomUUID().slice(0, 8)}`; }
|
|
705
|
+
generateDecisionId() { return `wrfc-decision-${crypto.randomUUID().slice(0, 8)}`; }
|
|
706
|
+
appendOwnerDecision(chain, action, reason, details = {}) {
|
|
707
|
+
const record = details.record ?? (details.agentId ? this.agentManager.getStatus(details.agentId) ?? undefined : undefined);
|
|
708
|
+
const decision = {
|
|
709
|
+
id: this.generateDecisionId(),
|
|
710
|
+
ts: new Date().toISOString(),
|
|
711
|
+
action,
|
|
712
|
+
state: chain.state,
|
|
713
|
+
reason,
|
|
714
|
+
...(details.agentId ? { agentId: details.agentId } : {}),
|
|
715
|
+
...(details.role ? { role: details.role } : {}),
|
|
716
|
+
...(record?.model ? { model: record.model } : {}),
|
|
717
|
+
...(record?.provider ? { provider: record.provider } : {}),
|
|
718
|
+
...(record?.reasoningEffort ? { reasoningEffort: record.reasoningEffort } : {}),
|
|
719
|
+
...(typeof details.reviewScore === 'number' ? { reviewScore: details.reviewScore } : {}),
|
|
720
|
+
};
|
|
721
|
+
chain.ownerDecisions.push(decision);
|
|
722
|
+
this.workmap.append({
|
|
723
|
+
ts: decision.ts,
|
|
724
|
+
wrfcId: chain.id,
|
|
725
|
+
event: 'owner_decision',
|
|
726
|
+
action,
|
|
727
|
+
state: chain.state,
|
|
728
|
+
reason,
|
|
729
|
+
...(decision.agentId ? { agentId: decision.agentId } : {}),
|
|
730
|
+
...(decision.role ? { role: decision.role } : {}),
|
|
731
|
+
...(decision.model ? { model: decision.model } : {}),
|
|
732
|
+
...(decision.provider ? { provider: decision.provider } : {}),
|
|
733
|
+
...(decision.reasoningEffort ? { reasoningEffort: decision.reasoningEffort } : {}),
|
|
734
|
+
...(typeof decision.reviewScore === 'number' ? { score: decision.reviewScore } : {}),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
568
737
|
completeCurrentNode(chain, summary) {
|
|
569
738
|
if (!chain.currentNodeId)
|
|
570
739
|
return;
|
|
@@ -577,63 +746,59 @@ export class WrfcController {
|
|
|
577
746
|
failWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, chain.currentNodeId, error);
|
|
578
747
|
chain.currentNodeId = undefined;
|
|
579
748
|
}
|
|
580
|
-
createBaseChain(
|
|
581
|
-
// Inject the engineer constraint addendum before the runner reads the system prompt.
|
|
582
|
-
// createBaseChain is called synchronously inside manager.spawn() before
|
|
583
|
-
// executor.runAgent(record), so the field is visible to the runner.
|
|
584
|
-
engineerRecord.systemPromptAddendum = '\n\n---\n\n' + buildEngineerConstraintAddendum();
|
|
749
|
+
createBaseChain(ownerRecord) {
|
|
585
750
|
const chain = {
|
|
586
751
|
id: this.generateWrfcId(),
|
|
587
752
|
state: 'pending',
|
|
588
|
-
task:
|
|
589
|
-
|
|
590
|
-
allAgentIds: [
|
|
753
|
+
task: ownerRecord.task,
|
|
754
|
+
ownerAgentId: ownerRecord.id,
|
|
755
|
+
allAgentIds: [ownerRecord.id],
|
|
591
756
|
fixAttempts: 0,
|
|
592
757
|
reviewCycles: 0,
|
|
593
|
-
gateRetryDepth: 0,
|
|
594
758
|
reviewScores: [],
|
|
759
|
+
ownerDecisions: [],
|
|
760
|
+
ownerTerminalEmitted: false,
|
|
595
761
|
constraints: [],
|
|
596
762
|
constraintsEnumerated: false,
|
|
597
763
|
createdAt: Date.now(),
|
|
598
764
|
};
|
|
599
765
|
this.chains.set(chain.id, chain);
|
|
600
|
-
emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${
|
|
601
|
-
|
|
766
|
+
emitWrfcGraphCreated(this.runtimeBus, this.sessionId, chain.id, `WRFC: ${ownerRecord.task}`);
|
|
767
|
+
ownerRecord.wrfcId = chain.id;
|
|
768
|
+
ownerRecord.wrfcRole = 'owner';
|
|
769
|
+
ownerRecord.progress = 'WRFC owner supervising child agents';
|
|
602
770
|
this.messageBus.registerAgent({
|
|
603
|
-
agentId:
|
|
604
|
-
template:
|
|
771
|
+
agentId: ownerRecord.id,
|
|
772
|
+
template: ownerRecord.template,
|
|
605
773
|
wrfcId: chain.id,
|
|
606
774
|
});
|
|
607
|
-
this.
|
|
775
|
+
this.appendOwnerDecision(chain, 'chain_created', 'WRFC owner created for original ask', {
|
|
776
|
+
agentId: ownerRecord.id,
|
|
777
|
+
});
|
|
608
778
|
return chain;
|
|
609
779
|
}
|
|
610
|
-
attachPendingParentChain(chain, agentId) {
|
|
611
|
-
const pendingParentId = this.pendingParentChainIds.get(agentId);
|
|
612
|
-
if (!pendingParentId)
|
|
613
|
-
return;
|
|
614
|
-
chain.parentChainId = pendingParentId;
|
|
615
|
-
const parent = this.chains.get(pendingParentId);
|
|
616
|
-
if (parent) {
|
|
617
|
-
chain.gateRetryDepth = parent.gateRetryDepth + (parent.gateFailureFingerprint ? 1 : 0);
|
|
618
|
-
}
|
|
619
|
-
this.pendingParentChainIds.delete(agentId);
|
|
620
|
-
// Inherit constraints from parent when they were queued via the pending path.
|
|
621
|
-
// The inherited list is authoritative; child output is checked for continuity
|
|
622
|
-
// on completion instead of being allowed to change scope.
|
|
623
|
-
const inherited = this.pendingParentConstraints.get(agentId);
|
|
624
|
-
if (inherited && inherited.length > 0) {
|
|
625
|
-
chain.constraints = inherited;
|
|
626
|
-
chain.constraintsEnumerated = true;
|
|
627
|
-
}
|
|
628
|
-
this.pendingParentConstraints.delete(agentId);
|
|
629
|
-
}
|
|
630
780
|
startEngineeringChain(chain, emitCreated) {
|
|
631
781
|
this.activeChainCount += 1;
|
|
632
782
|
this.transition(chain, 'engineering');
|
|
633
|
-
|
|
783
|
+
const engineerRecord = this.spawnWrfcAgent(chain, 'engineer', 'engineer', chain.task, true);
|
|
784
|
+
engineerRecord.wrfcRole = 'engineer';
|
|
785
|
+
chain.engineerAgentId = engineerRecord.id;
|
|
786
|
+
chain.allAgentIds.push(engineerRecord.id);
|
|
787
|
+
engineerRecord.wrfcId = chain.id;
|
|
788
|
+
this.messageBus.registerAgent({
|
|
789
|
+
agentId: engineerRecord.id,
|
|
790
|
+
role: 'engineer',
|
|
791
|
+
wrfcId: chain.id,
|
|
792
|
+
});
|
|
793
|
+
chain.currentNodeId = startWrfcOrchestrationNode(this.runtimeBus, this.sessionId, chain.id, `engineer:${chain.fixAttempts}`, 'engineer', 'Engineer implementation', engineerRecord.id);
|
|
634
794
|
if (emitCreated) {
|
|
635
795
|
emitWrfcChainCreated(this.runtimeBus, this.sessionId, chain.id, chain.task);
|
|
636
796
|
}
|
|
797
|
+
this.appendOwnerDecision(chain, 'spawn_engineer', this.withRouteReason('Start WRFC implementation child for the original ask', engineerRecord), {
|
|
798
|
+
agentId: engineerRecord.id,
|
|
799
|
+
role: 'engineer',
|
|
800
|
+
record: engineerRecord,
|
|
801
|
+
});
|
|
637
802
|
}
|
|
638
803
|
handleEngineerCompletion(chain, agentId, report) {
|
|
639
804
|
this.completeCurrentNode(chain, report.summary);
|
|
@@ -683,31 +848,82 @@ export class WrfcController {
|
|
|
683
848
|
}
|
|
684
849
|
this.startReview(chain, report);
|
|
685
850
|
}
|
|
686
|
-
spawnWrfcAgent(chain, template, task, dangerouslyDisableWrfc) {
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
851
|
+
spawnWrfcAgent(chain, role, template, task, dangerouslyDisableWrfc) {
|
|
852
|
+
const owner = this.agentManager.getStatus(chain.ownerAgentId);
|
|
853
|
+
const selectedRoute = this.selectChildRoute?.({ chain, role, task, ownerAgent: owner }) ?? null;
|
|
854
|
+
const model = selectedRoute?.model ?? owner?.model;
|
|
855
|
+
const provider = selectedRoute?.provider ?? owner?.provider;
|
|
856
|
+
const fallbackModels = selectedRoute?.fallbackModels ?? owner?.fallbackModels;
|
|
857
|
+
const routing = selectedRoute?.routing ?? owner?.routing;
|
|
858
|
+
const reasoningEffort = selectedRoute?.reasoningEffort ?? owner?.reasoningEffort;
|
|
859
|
+
const record = this.agentManager.spawn({
|
|
692
860
|
mode: 'spawn',
|
|
693
861
|
task,
|
|
694
862
|
template,
|
|
695
|
-
|
|
696
|
-
...(
|
|
697
|
-
...(
|
|
698
|
-
...(
|
|
699
|
-
...(
|
|
863
|
+
parentAgentId: chain.ownerAgentId,
|
|
864
|
+
...(model ? { model } : {}),
|
|
865
|
+
...(provider ? { provider } : {}),
|
|
866
|
+
...(fallbackModels?.length ? { fallbackModels: [...fallbackModels] } : {}),
|
|
867
|
+
...(routing ? { routing } : {}),
|
|
868
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
869
|
+
...(template === 'engineer' ? { systemPromptAddendum: '\n\n---\n\n' + buildEngineerConstraintAddendum() } : {}),
|
|
700
870
|
...(dangerouslyDisableWrfc ? { dangerously_disable_wrfc: true } : {}),
|
|
701
871
|
});
|
|
872
|
+
record.wrfcId = chain.id;
|
|
873
|
+
if (selectedRoute?.reason) {
|
|
874
|
+
record.wrfcRouteReason = selectedRoute.reason;
|
|
875
|
+
}
|
|
876
|
+
return record;
|
|
877
|
+
}
|
|
878
|
+
withRouteReason(baseReason, record) {
|
|
879
|
+
return record.wrfcRouteReason ? `${baseReason}; route: ${record.wrfcRouteReason}` : baseReason;
|
|
702
880
|
}
|
|
703
881
|
completeChainAsPassed(chain) {
|
|
704
882
|
this.activeChainCount = Math.max(0, this.activeChainCount - 1);
|
|
705
883
|
this.transition(chain, 'passed');
|
|
706
884
|
chain.completedAt = Date.now();
|
|
885
|
+
this.appendOwnerDecision(chain, 'chain_passed', 'WRFC full-scope review and quality gates passed', {
|
|
886
|
+
agentId: chain.ownerAgentId,
|
|
887
|
+
});
|
|
888
|
+
this.completeOwnerAgent(chain, 'completed', `WRFC chain ${chain.id} passed`);
|
|
707
889
|
emitWrfcChainPassed(this.runtimeBus, this.sessionId, chain.id);
|
|
708
890
|
this.scheduleChainCleanup(chain);
|
|
709
891
|
this.safeDequeueNext();
|
|
710
892
|
}
|
|
893
|
+
completeOwnerAgent(chain, status, message) {
|
|
894
|
+
if (chain.ownerTerminalEmitted)
|
|
895
|
+
return;
|
|
896
|
+
const owner = this.agentManager.getStatus(chain.ownerAgentId);
|
|
897
|
+
if (!owner)
|
|
898
|
+
return;
|
|
899
|
+
owner.status = status;
|
|
900
|
+
owner.completedAt = Date.now();
|
|
901
|
+
owner.progress = message;
|
|
902
|
+
owner.fullOutput = message;
|
|
903
|
+
chain.ownerTerminalEmitted = true;
|
|
904
|
+
const context = {
|
|
905
|
+
sessionId: this.sessionId,
|
|
906
|
+
traceId: `${this.sessionId}:wrfc-owner:${chain.id}:${status}`,
|
|
907
|
+
source: 'wrfc-controller',
|
|
908
|
+
agentId: owner.id,
|
|
909
|
+
};
|
|
910
|
+
if (status === 'completed') {
|
|
911
|
+
emitAgentCompleted(this.runtimeBus, context, {
|
|
912
|
+
agentId: owner.id,
|
|
913
|
+
durationMs: Math.max(0, owner.completedAt - owner.startedAt),
|
|
914
|
+
output: message,
|
|
915
|
+
toolCallsMade: owner.toolCallCount,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
owner.error = message;
|
|
920
|
+
emitAgentFailed(this.runtimeBus, context, {
|
|
921
|
+
agentId: owner.id,
|
|
922
|
+
durationMs: Math.max(0, owner.completedAt - owner.startedAt),
|
|
923
|
+
error: message,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
711
927
|
safeDequeueNext() {
|
|
712
928
|
this.dequeueNext().catch((error) => {
|
|
713
929
|
logger.error('WrfcController.dequeueNext unhandled error', { error: summarizeError(error) });
|