@alexeiled/pi-fusion 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/agents/fusion-composer.md +3 -0
- package/agents/fusion-judge.md +3 -0
- package/agents/fusion-panelist.md +3 -0
- package/docs/user-guide.md +32 -11
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/caller-contract.ts +80 -0
- package/src/config.ts +58 -2
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +88 -13
- package/src/index.ts +33 -5
- package/src/lifecycle-reconcile.ts +445 -0
- package/src/orchestrator.ts +562 -129
- package/src/panel-completion.ts +105 -7
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +105 -3
- package/src/result-extract.ts +84 -7
- package/src/run-builder.ts +141 -15
- package/src/run-store.ts +377 -11
- package/src/status.ts +1 -1
- package/src/types.ts +89 -0
package/src/orchestrator.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { applyClaudeAliasShorthand } from "./claude-aliases.js";
|
|
2
|
+
import {
|
|
3
|
+
detectCallerOutputContract,
|
|
4
|
+
validateCallerOutput,
|
|
5
|
+
} from "./caller-contract.js";
|
|
2
6
|
import {
|
|
3
7
|
buildInlinePanelProfile,
|
|
4
8
|
loadFusionConfig,
|
|
@@ -7,7 +11,10 @@ import {
|
|
|
7
11
|
} from "./config.js";
|
|
8
12
|
import { FusionArgsError } from "./errors.js";
|
|
9
13
|
import { parseFusionArgs } from "./fusion-args.js";
|
|
10
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
decidePanelCompletion,
|
|
16
|
+
resolveMinimumSuccessfulPanelists,
|
|
17
|
+
} from "./panel-completion.js";
|
|
11
18
|
import {
|
|
12
19
|
renderCancelledReport,
|
|
13
20
|
renderFailureReport,
|
|
@@ -17,8 +24,20 @@ import {
|
|
|
17
24
|
extractPanelResults,
|
|
18
25
|
type ExtractPanelResultsSuccess,
|
|
19
26
|
} from "./result-extract.js";
|
|
20
|
-
import {
|
|
21
|
-
|
|
27
|
+
import {
|
|
28
|
+
reconcileIndexedLifecycleResult,
|
|
29
|
+
reconcilePanelResults,
|
|
30
|
+
} from "./lifecycle-reconcile.js";
|
|
31
|
+
import {
|
|
32
|
+
appendThinkingSuffix,
|
|
33
|
+
buildPanelSpawnParams,
|
|
34
|
+
resolveEffectiveTimeouts,
|
|
35
|
+
} from "./run-builder.js";
|
|
36
|
+
import {
|
|
37
|
+
FusionRunStore,
|
|
38
|
+
FusionRunStoreError,
|
|
39
|
+
validateFusionRunPanelSlots,
|
|
40
|
+
} from "./run-store.js";
|
|
22
41
|
import {
|
|
23
42
|
clearFusionUi,
|
|
24
43
|
extractFusionProgressCounts,
|
|
@@ -37,6 +56,7 @@ import {
|
|
|
37
56
|
resolveSynthesisMode,
|
|
38
57
|
type FailedPanelSummary,
|
|
39
58
|
type FusionProfile,
|
|
59
|
+
type FusionProfileSnapshot,
|
|
40
60
|
type FusionRun,
|
|
41
61
|
type PanelOutput,
|
|
42
62
|
type ParsedFusionArgs,
|
|
@@ -105,6 +125,8 @@ export type FusionCommandResult =
|
|
|
105
125
|
interface RunLifecycleSnapshot {
|
|
106
126
|
statusPayload?: unknown;
|
|
107
127
|
resultPayload?: unknown;
|
|
128
|
+
/** Compact event results need stable slots; status/artifacts are ordered snapshots. */
|
|
129
|
+
resultSource?: "artifact" | "event" | "status";
|
|
108
130
|
resultIsTerminal: boolean;
|
|
109
131
|
resultArtifactPending?: boolean;
|
|
110
132
|
}
|
|
@@ -138,6 +160,11 @@ export class FusionOrchestrator {
|
|
|
138
160
|
this.context = ctx;
|
|
139
161
|
|
|
140
162
|
const args = typeof input === "string" ? parseFusionArgs(input) : input;
|
|
163
|
+
const inputError = validateStartArgs(args);
|
|
164
|
+
if (inputError) {
|
|
165
|
+
this.notify(ctx, inputError, "error");
|
|
166
|
+
return { status: "failed", error: inputError };
|
|
167
|
+
}
|
|
141
168
|
const existing = this.runStore.getActiveRun();
|
|
142
169
|
if (existing) {
|
|
143
170
|
const message = `Fusion run ${existing.id} is already active.`;
|
|
@@ -188,6 +215,9 @@ export class FusionOrchestrator {
|
|
|
188
215
|
return { status: "failed", error: message };
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
const outputContract =
|
|
219
|
+
args.outputContract ?? detectCallerOutputContract(args.prompt);
|
|
220
|
+
const profileSnapshot = snapshotProfile(resolved.profile);
|
|
191
221
|
let run: FusionRun;
|
|
192
222
|
try {
|
|
193
223
|
run = this.runStore.startRun({
|
|
@@ -199,10 +229,25 @@ export class FusionOrchestrator {
|
|
|
199
229
|
...(args.operationId !== undefined
|
|
200
230
|
? { operationId: args.operationId }
|
|
201
231
|
: {}),
|
|
232
|
+
...(outputContract ? { outputContract } : {}),
|
|
233
|
+
// profileSnapshot is the sole quorum record for new runs. The
|
|
234
|
+
// run-level field remains readable only for legacy snapshots.
|
|
235
|
+
profileSnapshot,
|
|
236
|
+
...(args.timeoutOverrides
|
|
237
|
+
? { timeoutOverrides: args.timeoutOverrides }
|
|
238
|
+
: {}),
|
|
239
|
+
effectiveTimeouts: resolveEffectiveTimeouts(
|
|
240
|
+
resolved.profile,
|
|
241
|
+
args.timeoutOverrides,
|
|
242
|
+
),
|
|
202
243
|
phase: "panel",
|
|
203
244
|
});
|
|
204
245
|
} catch (error: unknown) {
|
|
205
|
-
if (!(error instanceof FusionRunStoreError))
|
|
246
|
+
if (!(error instanceof FusionRunStoreError)) {
|
|
247
|
+
const message = errorMessage(error);
|
|
248
|
+
this.notify(ctx, message, "error");
|
|
249
|
+
return { status: "failed", error: message };
|
|
250
|
+
}
|
|
206
251
|
const active = this.runStore.getActiveRun();
|
|
207
252
|
if (active) {
|
|
208
253
|
this.notify(
|
|
@@ -214,12 +259,25 @@ export class FusionOrchestrator {
|
|
|
214
259
|
}
|
|
215
260
|
return { status: "failed", error: errorMessage(error) };
|
|
216
261
|
}
|
|
217
|
-
|
|
262
|
+
// Keep runtime behavior aligned with the exact durable profile that a
|
|
263
|
+
// restart will use, rather than retaining a mutable config object.
|
|
264
|
+
this.activeProfile = profileFromSnapshot(profileSnapshot);
|
|
218
265
|
publishFusionStatus(ctx, run);
|
|
219
266
|
|
|
220
267
|
try {
|
|
268
|
+
// Persist before the side effect. Public pi-subagents RPC has no
|
|
269
|
+
// correlation-key lookup, so restore treats this intent without its ID
|
|
270
|
+
// as unsafe to replay rather than creating an orphaned duplicate.
|
|
271
|
+
this.runStore.updateRun(run.id, {
|
|
272
|
+
spawnIntent: { stage: "panel", requestedAt: Date.now() },
|
|
273
|
+
});
|
|
221
274
|
const spawnResult = await this.rpc.spawn(
|
|
222
|
-
buildPanelSpawnParams(
|
|
275
|
+
buildPanelSpawnParams(
|
|
276
|
+
resolved.profile,
|
|
277
|
+
args.prompt,
|
|
278
|
+
outputContract,
|
|
279
|
+
args.timeoutOverrides,
|
|
280
|
+
),
|
|
223
281
|
);
|
|
224
282
|
const spawnError = extractSubagentFailure(spawnResult);
|
|
225
283
|
if (spawnError) throw new FusionArgsError(spawnError);
|
|
@@ -240,10 +298,19 @@ export class FusionOrchestrator {
|
|
|
240
298
|
? { status: "cancelled", run: cancelled, report: cancelled.report }
|
|
241
299
|
: { status: "ignored" };
|
|
242
300
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
301
|
+
let updated: FusionRun;
|
|
302
|
+
try {
|
|
303
|
+
updated = this.runStore.updateRun(run.id, {
|
|
304
|
+
spawnIntent: null,
|
|
305
|
+
panelRunId,
|
|
306
|
+
...(panelAsyncDir ? { panelAsyncDir } : {}),
|
|
307
|
+
});
|
|
308
|
+
} catch (persistenceError: unknown) {
|
|
309
|
+
// The remote run is now known but its ID is not durable. It cannot be
|
|
310
|
+
// safely recovered through the public RPC, so stop it before failing.
|
|
311
|
+
await this.stopOrphanedRun(panelRunId);
|
|
312
|
+
throw persistenceError;
|
|
313
|
+
}
|
|
247
314
|
publishFusionStatus(ctx, updated);
|
|
248
315
|
this.ensureReconcileLoop();
|
|
249
316
|
this.notify(
|
|
@@ -330,7 +397,7 @@ export class FusionOrchestrator {
|
|
|
330
397
|
this.context,
|
|
331
398
|
active,
|
|
332
399
|
progress,
|
|
333
|
-
deriveFusionStatusPhase(active, statusPayload),
|
|
400
|
+
deriveFusionStatusPhase(active, statusPayload, this.activeProfile),
|
|
334
401
|
);
|
|
335
402
|
}
|
|
336
403
|
return statusPayload;
|
|
@@ -408,34 +475,72 @@ export class FusionOrchestrator {
|
|
|
408
475
|
const summary = this.runStore.restoreFromSession(ctx);
|
|
409
476
|
this.clearActiveRuntime();
|
|
410
477
|
|
|
478
|
+
const restoreError = this.runStore.getRestoreError();
|
|
479
|
+
if (restoreError) {
|
|
480
|
+
this.configWarning = restoreError;
|
|
481
|
+
this.notify(ctx, restoreError, "warning");
|
|
482
|
+
clearFusionUi(ctx);
|
|
483
|
+
return summary;
|
|
484
|
+
}
|
|
485
|
+
|
|
411
486
|
const active = this.runStore.getActiveRun();
|
|
487
|
+
if (active && hasUnresolvedSpawnIntent(active)) {
|
|
488
|
+
const message = `Fusion recovery stopped: ${active.spawnIntent!.stage} spawn may have reached pi-subagents, but its run ID was not persisted. It will not be replayed because public RPC cannot safely adopt it.`;
|
|
489
|
+
this.failActiveRun(message);
|
|
490
|
+
this.notify(ctx, message, "warning");
|
|
491
|
+
return this.runStore.getLastRunSummary();
|
|
492
|
+
}
|
|
412
493
|
if (!active) {
|
|
413
494
|
this.stopReconcileLoop();
|
|
414
495
|
clearFusionUi(ctx);
|
|
415
496
|
return summary;
|
|
416
497
|
}
|
|
417
498
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
)
|
|
430
|
-
this.activeProfile = active.inlinePanel?.length
|
|
431
|
-
? buildInlinePanelProfile(base, active.inlinePanel)
|
|
432
|
-
: base;
|
|
499
|
+
const lifecycleError = validateRestoredRunLifecycle(active);
|
|
500
|
+
if (lifecycleError) {
|
|
501
|
+
this.failActiveRun(lifecycleError);
|
|
502
|
+
this.notify(ctx, lifecycleError, "warning");
|
|
503
|
+
return this.runStore.getLastRunSummary();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (active.profileSnapshot) {
|
|
507
|
+
// The snapshot was schema-validated while deserializing the run. It is
|
|
508
|
+
// the source of truth for labels, quorum, synthesis, and judge spawning;
|
|
509
|
+
// config edits made while a run is active must not rewrite that run.
|
|
510
|
+
this.activeProfile = profileFromSnapshot(active.profileSnapshot);
|
|
433
511
|
this.configWarning = undefined;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
512
|
+
} else {
|
|
513
|
+
try {
|
|
514
|
+
const config = await this.loadConfig(ctx);
|
|
515
|
+
// Backward compatibility for sessions written before profile snapshots.
|
|
516
|
+
// An inline run's display name has no config entry, so rebuild it from
|
|
517
|
+
// its base profile plus persisted inline entries.
|
|
518
|
+
const base = this.resolveProfile(
|
|
519
|
+
config,
|
|
520
|
+
active.inlinePanel?.length
|
|
521
|
+
? active.baseProfileName
|
|
522
|
+
: active.profileName,
|
|
523
|
+
).profile;
|
|
524
|
+
this.activeProfile = active.inlinePanel?.length
|
|
525
|
+
? buildInlinePanelProfile(base, active.inlinePanel)
|
|
526
|
+
: base;
|
|
527
|
+
this.configWarning = undefined;
|
|
528
|
+
} catch (error: unknown) {
|
|
529
|
+
const message = `Could not restore legacy fusion profile "${active.profileName}": ${errorMessage(error)}`;
|
|
530
|
+
this.configWarning = message;
|
|
531
|
+
this.notify(ctx, message, "warning");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const panelSlotError = validateFusionRunPanelSlots(
|
|
535
|
+
active,
|
|
536
|
+
this.activeProfile?.panel.length ?? 0,
|
|
537
|
+
);
|
|
538
|
+
if (panelSlotError) {
|
|
539
|
+
this.failActiveRun(panelSlotError);
|
|
540
|
+
this.notify(ctx, panelSlotError, "warning");
|
|
541
|
+
return this.runStore.getLastRunSummary();
|
|
438
542
|
}
|
|
543
|
+
|
|
439
544
|
publishFusionStatus(ctx, active);
|
|
440
545
|
this.ensureReconcileLoop();
|
|
441
546
|
await this.reconcileActiveRun();
|
|
@@ -559,6 +664,7 @@ export class FusionOrchestrator {
|
|
|
559
664
|
eventPayload: payload,
|
|
560
665
|
});
|
|
561
666
|
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
667
|
+
this.persistVerifiedPanelResults(active, profile, snapshot.statusPayload);
|
|
562
668
|
const terminalPayload =
|
|
563
669
|
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
564
670
|
if (
|
|
@@ -578,6 +684,9 @@ export class FusionOrchestrator {
|
|
|
578
684
|
const extracted = extractPanelResults(lifecyclePayload, {
|
|
579
685
|
panel: profile.panel,
|
|
580
686
|
limit: profile.panel.length,
|
|
687
|
+
...(snapshot.resultSource === "event"
|
|
688
|
+
? { requireStableSlotIdentity: true }
|
|
689
|
+
: {}),
|
|
581
690
|
});
|
|
582
691
|
if (!extracted.ok) {
|
|
583
692
|
return this.failActiveRun(
|
|
@@ -585,19 +694,63 @@ export class FusionOrchestrator {
|
|
|
585
694
|
);
|
|
586
695
|
}
|
|
587
696
|
|
|
588
|
-
const observedPanels =
|
|
697
|
+
const observedPanels = reconcilePanelResults(
|
|
589
698
|
extracted,
|
|
590
699
|
snapshot.statusPayload,
|
|
591
700
|
profile,
|
|
701
|
+
lifecyclePayload,
|
|
702
|
+
{
|
|
703
|
+
allowedTrailingResults: 1,
|
|
704
|
+
...(isWorkflowDeadline(lifecyclePayload) ||
|
|
705
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
706
|
+
? { terminalizeRunning: true }
|
|
707
|
+
: {}),
|
|
708
|
+
},
|
|
592
709
|
);
|
|
710
|
+
if (!observedPanels.ok) {
|
|
711
|
+
return this.failActiveRun(
|
|
712
|
+
`${observedPanels.error.message} (${observedPanels.error.path})`,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
593
715
|
const updated = this.storePanelResults(
|
|
594
716
|
active.id,
|
|
595
717
|
observedPanels.outputs,
|
|
596
718
|
observedPanels.failures,
|
|
597
719
|
);
|
|
598
720
|
|
|
599
|
-
|
|
600
|
-
|
|
721
|
+
// A legacy workflow may contain an embedded judge result even when the
|
|
722
|
+
// current persisted quorum policy would not permit synthesis. Apply the
|
|
723
|
+
// same completion decision as modern panel-only runs before accepting it.
|
|
724
|
+
const completion = decidePanelCompletion({
|
|
725
|
+
run: updated,
|
|
726
|
+
profile,
|
|
727
|
+
panelOutputs: observedPanels.outputs,
|
|
728
|
+
panelFailures: observedPanels.failures,
|
|
729
|
+
fallbackJudge: true,
|
|
730
|
+
});
|
|
731
|
+
if (completion.kind === "fail") {
|
|
732
|
+
return this.failActiveRun(completion.error, completion.report);
|
|
733
|
+
}
|
|
734
|
+
if (completion.kind === "complete") {
|
|
735
|
+
this.runStore.updateRun(updated.id, {
|
|
736
|
+
completionQuality: completionQuality(profile, observedPanels.outputs, observedPanels.failures),
|
|
737
|
+
});
|
|
738
|
+
return this.completeActiveRun(completion.report);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const judgePayload =
|
|
742
|
+
hasJudgeResult(snapshot.resultPayload, profile.panel.length)
|
|
743
|
+
? snapshot.resultPayload
|
|
744
|
+
: snapshot.statusPayload;
|
|
745
|
+
if (hasJudgeResult(judgePayload, profile.panel.length)) {
|
|
746
|
+
const lifecycleConflict = reconcileIndexedLifecycleResult(
|
|
747
|
+
snapshot.resultPayload,
|
|
748
|
+
snapshot.statusPayload,
|
|
749
|
+
profile.panel.length,
|
|
750
|
+
"judge",
|
|
751
|
+
);
|
|
752
|
+
if (lifecycleConflict) return this.failActiveRun(lifecycleConflict);
|
|
753
|
+
const output = extractJudgeOutput(judgePayload, {
|
|
601
754
|
resultIndex: profile.panel.length,
|
|
602
755
|
});
|
|
603
756
|
if (!output.ok) return this.failActiveRun(output.error);
|
|
@@ -608,13 +761,19 @@ export class FusionOrchestrator {
|
|
|
608
761
|
snapshot.statusPayload,
|
|
609
762
|
),
|
|
610
763
|
extractRunObservation(
|
|
611
|
-
findResult(
|
|
612
|
-
snapshot.resultPayload,
|
|
764
|
+
findResult(judgePayload, profile.panel.length) ?? judgePayload,
|
|
613
765
|
),
|
|
614
766
|
);
|
|
615
767
|
const observed = this.runStore.updateRun(updated.id, {
|
|
768
|
+
completionQuality: completionQuality(profile, observedPanels.outputs, observedPanels.failures),
|
|
616
769
|
judgeObservation,
|
|
617
770
|
});
|
|
771
|
+
const callerContract =
|
|
772
|
+
observed.outputContract ?? detectCallerOutputContract(observed.prompt);
|
|
773
|
+
if (callerContract) {
|
|
774
|
+
const validation = validateCallerOutput(callerContract, output.output);
|
|
775
|
+
if (!validation.ok) return this.failActiveRun(validation.error);
|
|
776
|
+
}
|
|
618
777
|
const report = renderJudgeReport({
|
|
619
778
|
run: observed,
|
|
620
779
|
judgeOutput: output.output,
|
|
@@ -660,17 +819,23 @@ export class FusionOrchestrator {
|
|
|
660
819
|
});
|
|
661
820
|
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
662
821
|
|
|
822
|
+
const partial = this.persistVerifiedPanelResults(
|
|
823
|
+
active,
|
|
824
|
+
profile,
|
|
825
|
+
snapshot.statusPayload,
|
|
826
|
+
);
|
|
663
827
|
const panelIsTerminal =
|
|
664
828
|
snapshot.resultIsTerminal ||
|
|
665
829
|
isTerminalSubagentState(extractSubagentState(snapshot.statusPayload));
|
|
666
830
|
if (!active.panelStopReason && !panelIsTerminal) {
|
|
667
|
-
const partial = extractPanelResults(snapshot.statusPayload, {
|
|
668
|
-
panel: profile.panel,
|
|
669
|
-
completedOnly: true,
|
|
670
|
-
});
|
|
671
831
|
if (
|
|
672
|
-
partial
|
|
673
|
-
shouldStopWhenPanelAgrees(
|
|
832
|
+
partial &&
|
|
833
|
+
shouldStopWhenPanelAgrees(
|
|
834
|
+
profile,
|
|
835
|
+
partial.outputs,
|
|
836
|
+
partial.failures,
|
|
837
|
+
active.minimumSuccessfulPanelists,
|
|
838
|
+
)
|
|
674
839
|
) {
|
|
675
840
|
return this.stopPanelAfterAgreement(
|
|
676
841
|
active,
|
|
@@ -699,6 +864,11 @@ export class FusionOrchestrator {
|
|
|
699
864
|
const workflowStoppedIndices = extractWorkflowStoppedPanelIndices(
|
|
700
865
|
lifecyclePayload,
|
|
701
866
|
);
|
|
867
|
+
if (workflowStoppedIndices.some((index) => index >= profile.panel.length)) {
|
|
868
|
+
return this.failActiveRun(
|
|
869
|
+
"Terminal subagents data referenced a panel slot outside the configured panel.",
|
|
870
|
+
);
|
|
871
|
+
}
|
|
702
872
|
const stoppedPanelIndices =
|
|
703
873
|
active.panelStoppedIndices ??
|
|
704
874
|
(workflowStoppedIndices.length > 0 ? workflowStoppedIndices : undefined);
|
|
@@ -706,6 +876,9 @@ export class FusionOrchestrator {
|
|
|
706
876
|
panel: profile.panel,
|
|
707
877
|
limit: profile.panel.length,
|
|
708
878
|
...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
|
|
879
|
+
...(snapshot.resultSource === "event"
|
|
880
|
+
? { requireStableSlotIdentity: true }
|
|
881
|
+
: {}),
|
|
709
882
|
});
|
|
710
883
|
if (!extracted.ok) {
|
|
711
884
|
return this.failActiveRun(
|
|
@@ -713,15 +886,36 @@ export class FusionOrchestrator {
|
|
|
713
886
|
);
|
|
714
887
|
}
|
|
715
888
|
|
|
716
|
-
const observedPanels =
|
|
889
|
+
const observedPanels = reconcilePanelResults(
|
|
717
890
|
extracted,
|
|
718
891
|
snapshot.statusPayload,
|
|
719
892
|
profile,
|
|
893
|
+
lifecyclePayload,
|
|
894
|
+
{
|
|
895
|
+
...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
|
|
896
|
+
...(isWorkflowDeadline(lifecyclePayload) ||
|
|
897
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
898
|
+
? { terminalizeRunning: true }
|
|
899
|
+
: {}),
|
|
900
|
+
},
|
|
901
|
+
);
|
|
902
|
+
if (!observedPanels.ok) {
|
|
903
|
+
return this.failActiveRun(
|
|
904
|
+
`${observedPanels.error.message} (${observedPanels.error.path})`,
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const reconciledFailures = withWorkflowDeadlineFailures(
|
|
908
|
+
observedPanels.failures,
|
|
909
|
+
isWorkflowDeadline(lifecyclePayload) ||
|
|
910
|
+
isWorkflowDeadline(snapshot.statusPayload)
|
|
911
|
+
? extractSubagentFailure(snapshot.statusPayload) ??
|
|
912
|
+
extractSubagentFailure(lifecyclePayload)
|
|
913
|
+
: undefined,
|
|
720
914
|
);
|
|
721
915
|
const stored = this.storePanelResults(
|
|
722
916
|
active.id,
|
|
723
917
|
observedPanels.outputs,
|
|
724
|
-
|
|
918
|
+
reconciledFailures,
|
|
725
919
|
);
|
|
726
920
|
const updated =
|
|
727
921
|
workflowStoppedIndices.length > 0 && !active.panelStopReason
|
|
@@ -735,7 +929,7 @@ export class FusionOrchestrator {
|
|
|
735
929
|
updated,
|
|
736
930
|
profile,
|
|
737
931
|
observedPanels.outputs,
|
|
738
|
-
|
|
932
|
+
reconciledFailures,
|
|
739
933
|
{ fallbackJudge: false },
|
|
740
934
|
);
|
|
741
935
|
}
|
|
@@ -813,10 +1007,16 @@ export class FusionOrchestrator {
|
|
|
813
1007
|
return this.failActiveRun(decision.error, decision.report);
|
|
814
1008
|
}
|
|
815
1009
|
if (decision.kind === "complete") {
|
|
1010
|
+
this.runStore.updateRun(run.id, {
|
|
1011
|
+
completionQuality: completionQuality(profile, panelOutputs, panelFailures),
|
|
1012
|
+
});
|
|
816
1013
|
return this.completeActiveRun(decision.report);
|
|
817
1014
|
}
|
|
818
1015
|
|
|
819
1016
|
try {
|
|
1017
|
+
this.runStore.updateRun(run.id, {
|
|
1018
|
+
spawnIntent: { stage: "judge", requestedAt: Date.now() },
|
|
1019
|
+
});
|
|
820
1020
|
const spawnResult = await this.rpc.spawn(decision.params);
|
|
821
1021
|
const spawnError = extractSubagentFailure(spawnResult);
|
|
822
1022
|
if (spawnError) throw new FusionArgsError(spawnError);
|
|
@@ -829,13 +1029,23 @@ export class FusionOrchestrator {
|
|
|
829
1029
|
await this.stopOrphanedRun(judgeRunId, "judge");
|
|
830
1030
|
return { status: "ignored" };
|
|
831
1031
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1032
|
+
let nextRun: FusionRun;
|
|
1033
|
+
try {
|
|
1034
|
+
nextRun = this.runStore.updateRun(run.id, {
|
|
1035
|
+
spawnIntent: null,
|
|
1036
|
+
phase: "judge",
|
|
1037
|
+
completionQuality: completionQuality(profile, panelOutputs, panelFailures),
|
|
1038
|
+
judgeRunId,
|
|
1039
|
+
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
1040
|
+
panelOutputs: [...panelOutputs],
|
|
1041
|
+
panelFailures: [...panelFailures],
|
|
1042
|
+
});
|
|
1043
|
+
} catch (persistenceError: unknown) {
|
|
1044
|
+
// As for the panel, never leave a remotely started synthesis run
|
|
1045
|
+
// alive when recording its public ID failed.
|
|
1046
|
+
await this.stopOrphanedRun(judgeRunId, "judge");
|
|
1047
|
+
throw persistenceError;
|
|
1048
|
+
}
|
|
839
1049
|
publishFusionStatus(this.context, nextRun);
|
|
840
1050
|
this.notify(
|
|
841
1051
|
this.context,
|
|
@@ -875,8 +1085,25 @@ export class FusionOrchestrator {
|
|
|
875
1085
|
return this.failActiveRun(lifecycleError);
|
|
876
1086
|
}
|
|
877
1087
|
|
|
1088
|
+
const lifecycleConflict = reconcileIndexedLifecycleResult(
|
|
1089
|
+
snapshot.resultPayload,
|
|
1090
|
+
snapshot.statusPayload,
|
|
1091
|
+
0,
|
|
1092
|
+
"judge",
|
|
1093
|
+
);
|
|
1094
|
+
if (lifecycleConflict) return this.failActiveRun(lifecycleConflict);
|
|
1095
|
+
|
|
878
1096
|
const output = extractJudgeOutput(lifecyclePayload);
|
|
879
|
-
if (!output.ok)
|
|
1097
|
+
if (!output.ok) {
|
|
1098
|
+
const stepError = extractSubagentFailure(
|
|
1099
|
+
findStepsArray(snapshot.statusPayload)[0],
|
|
1100
|
+
);
|
|
1101
|
+
const errors = [lifecycleError, stepError, output.error].filter(
|
|
1102
|
+
(error, index, all): error is string =>
|
|
1103
|
+
Boolean(error) && all.indexOf(error) === index,
|
|
1104
|
+
);
|
|
1105
|
+
return this.failActiveRun(errors.join(" "));
|
|
1106
|
+
}
|
|
880
1107
|
|
|
881
1108
|
// The panel and chain handlers already treat a missing profile as fatal.
|
|
882
1109
|
// Without the same guard here the run "succeeds" with a degraded report:
|
|
@@ -888,6 +1115,13 @@ export class FusionOrchestrator {
|
|
|
888
1115
|
);
|
|
889
1116
|
}
|
|
890
1117
|
|
|
1118
|
+
const callerContract =
|
|
1119
|
+
active.outputContract ?? detectCallerOutputContract(active.prompt);
|
|
1120
|
+
if (callerContract) {
|
|
1121
|
+
const validation = validateCallerOutput(callerContract, output.output);
|
|
1122
|
+
if (!validation.ok) return this.failActiveRun(validation.error);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
891
1125
|
const judgeModel = configuredJudgeModel(profile);
|
|
892
1126
|
const judgeObservation = mergeRunObservations(
|
|
893
1127
|
extractRunObservation(
|
|
@@ -940,7 +1174,7 @@ export class FusionOrchestrator {
|
|
|
940
1174
|
this.context,
|
|
941
1175
|
input.run,
|
|
942
1176
|
progress,
|
|
943
|
-
deriveFusionStatusPhase(input.run, statusPayload),
|
|
1177
|
+
deriveFusionStatusPhase(input.run, statusPayload, this.activeProfile),
|
|
944
1178
|
);
|
|
945
1179
|
}
|
|
946
1180
|
|
|
@@ -950,23 +1184,20 @@ export class FusionOrchestrator {
|
|
|
950
1184
|
const eventIsTerminal =
|
|
951
1185
|
isTerminalSubagentState(extractSubagentState(input.eventPayload)) ||
|
|
952
1186
|
isTerminalFusionProgress(input.eventPayload);
|
|
953
|
-
const eventHasResults =
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
resultIsTerminal: eventIsTerminal || statusIsTerminal,
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
|
|
1187
|
+
const eventHasResults = hasExplicitResultsArray(input.eventPayload);
|
|
1188
|
+
// Completion events are compact and can truncate long inline outputs. A
|
|
1189
|
+
// result artifact is the complete terminal record, so select it before an
|
|
1190
|
+
// event and use it as the reconciliation snapshot as well. Otherwise a
|
|
1191
|
+
// partial status/event could discard verified slots or the judge end tag.
|
|
962
1192
|
const artifactResult = readSubagentResultArtifact({
|
|
963
1193
|
...(input.runId ? { runId: input.runId } : {}),
|
|
964
1194
|
...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
|
|
965
1195
|
});
|
|
966
|
-
if (
|
|
1196
|
+
if (hasExplicitResultsArray(artifactResult)) {
|
|
967
1197
|
return {
|
|
968
|
-
statusPayload,
|
|
1198
|
+
statusPayload: artifactResult,
|
|
969
1199
|
resultPayload: artifactResult,
|
|
1200
|
+
resultSource: "artifact",
|
|
970
1201
|
resultIsTerminal: true,
|
|
971
1202
|
};
|
|
972
1203
|
}
|
|
@@ -982,10 +1213,20 @@ export class FusionOrchestrator {
|
|
|
982
1213
|
};
|
|
983
1214
|
}
|
|
984
1215
|
|
|
985
|
-
if (
|
|
1216
|
+
if (eventPayloadMatches && eventHasResults) {
|
|
1217
|
+
return {
|
|
1218
|
+
statusPayload,
|
|
1219
|
+
resultPayload: input.eventPayload,
|
|
1220
|
+
resultSource: "event",
|
|
1221
|
+
resultIsTerminal: eventIsTerminal || statusIsTerminal,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (hasExplicitResultsArray(statusPayload)) {
|
|
986
1226
|
return {
|
|
987
1227
|
statusPayload,
|
|
988
1228
|
resultPayload: statusPayload,
|
|
1229
|
+
resultSource: "status",
|
|
989
1230
|
resultIsTerminal: statusIsTerminal,
|
|
990
1231
|
};
|
|
991
1232
|
}
|
|
@@ -1009,14 +1250,64 @@ export class FusionOrchestrator {
|
|
|
1009
1250
|
return { statusPayload, resultIsTerminal: false };
|
|
1010
1251
|
}
|
|
1011
1252
|
|
|
1253
|
+
/** Persists every terminal child slot observed while the workflow continues. */
|
|
1254
|
+
private persistVerifiedPanelResults(
|
|
1255
|
+
run: FusionRun,
|
|
1256
|
+
profile: FusionProfile,
|
|
1257
|
+
statusPayload: unknown,
|
|
1258
|
+
): ExtractPanelResultsSuccess | undefined {
|
|
1259
|
+
if (statusPayload === undefined) return undefined;
|
|
1260
|
+
const partial = extractPanelResults(statusPayload, {
|
|
1261
|
+
panel: profile.panel,
|
|
1262
|
+
completedOnly: true,
|
|
1263
|
+
limit: profile.panel.length,
|
|
1264
|
+
});
|
|
1265
|
+
if (!partial.ok) return undefined;
|
|
1266
|
+
if (partial.outputs.length > 0 || partial.failures.length > 0) {
|
|
1267
|
+
this.storePanelResults(run.id, partial.outputs, partial.failures);
|
|
1268
|
+
}
|
|
1269
|
+
return partial;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1012
1272
|
private storePanelResults(
|
|
1013
1273
|
runId: string,
|
|
1014
1274
|
panelOutputs: readonly PanelOutput[],
|
|
1015
1275
|
panelFailures: readonly FailedPanelSummary[],
|
|
1016
1276
|
): FusionRun {
|
|
1277
|
+
// Lifecycle status is append-only in intent but not guaranteed to repeat
|
|
1278
|
+
// older slots on every poll. Keep persisted verified slots until a newer
|
|
1279
|
+
// observation for that exact stable index supersedes them.
|
|
1280
|
+
const existing = this.runStore.getActiveRun();
|
|
1281
|
+
const slots = new Map<
|
|
1282
|
+
number,
|
|
1283
|
+
{ output?: PanelOutput; failure?: FailedPanelSummary }
|
|
1284
|
+
>();
|
|
1285
|
+
for (const output of existing?.panelOutputs ?? []) {
|
|
1286
|
+
slots.set(output.index, { output });
|
|
1287
|
+
}
|
|
1288
|
+
for (const failure of existing?.panelFailures ?? []) {
|
|
1289
|
+
slots.set(failure.index, { failure });
|
|
1290
|
+
}
|
|
1291
|
+
for (const output of panelOutputs) slots.set(output.index, { output });
|
|
1292
|
+
for (const failure of panelFailures) slots.set(failure.index, { failure });
|
|
1293
|
+
const mergedOutputs = Array.from(slots.values())
|
|
1294
|
+
.flatMap((slot) => (slot.output ? [slot.output] : []))
|
|
1295
|
+
.sort((left, right) => left.index - right.index);
|
|
1296
|
+
const mergedFailures = Array.from(slots.values())
|
|
1297
|
+
.flatMap((slot) => (slot.failure ? [slot.failure] : []))
|
|
1298
|
+
.sort((left, right) => left.index - right.index);
|
|
1299
|
+
// Status polling repeats complete snapshots. Persist only a material slot
|
|
1300
|
+
// change so a long-running restore does not append identical session data.
|
|
1301
|
+
if (
|
|
1302
|
+
existing?.id === runId &&
|
|
1303
|
+
sameSnapshot(existing.panelOutputs, mergedOutputs) &&
|
|
1304
|
+
sameSnapshot(existing.panelFailures, mergedFailures)
|
|
1305
|
+
) {
|
|
1306
|
+
return existing;
|
|
1307
|
+
}
|
|
1017
1308
|
return this.runStore.updateRun(runId, {
|
|
1018
|
-
panelOutputs:
|
|
1019
|
-
panelFailures:
|
|
1309
|
+
panelOutputs: mergedOutputs,
|
|
1310
|
+
panelFailures: mergedFailures,
|
|
1020
1311
|
});
|
|
1021
1312
|
}
|
|
1022
1313
|
|
|
@@ -1047,6 +1338,12 @@ export class FusionOrchestrator {
|
|
|
1047
1338
|
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
1048
1339
|
...(active.panelRunId ? { panelRunId: active.panelRunId } : {}),
|
|
1049
1340
|
...(active.judgeRunId ? { judgeRunId: active.judgeRunId } : {}),
|
|
1341
|
+
recovery: {
|
|
1342
|
+
retryDeferred: true,
|
|
1343
|
+
failedPanelIndices: storedPanelFailures(active)
|
|
1344
|
+
.map((failure) => failure.index)
|
|
1345
|
+
.sort((left, right) => left - right),
|
|
1346
|
+
},
|
|
1050
1347
|
report,
|
|
1051
1348
|
error,
|
|
1052
1349
|
});
|
|
@@ -1134,6 +1431,124 @@ export class FusionOrchestrator {
|
|
|
1134
1431
|
}
|
|
1135
1432
|
}
|
|
1136
1433
|
|
|
1434
|
+
function sameSnapshot<T>(
|
|
1435
|
+
left: readonly T[] | undefined,
|
|
1436
|
+
right: readonly T[],
|
|
1437
|
+
): boolean {
|
|
1438
|
+
return JSON.stringify(left ?? []) === JSON.stringify(right);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** Captures only settings that can affect post-restart reconciliation/reporting. */
|
|
1442
|
+
function snapshotProfile(profile: FusionProfile): FusionProfileSnapshot {
|
|
1443
|
+
return {
|
|
1444
|
+
panel: profile.panel.map((member) => ({ ...member })),
|
|
1445
|
+
judge: { ...profile.judge },
|
|
1446
|
+
minimumSuccessfulPanelists: resolveMinimumSuccessfulPanelists(
|
|
1447
|
+
profile.minimumSuccessfulPanelists,
|
|
1448
|
+
profile.panel.length,
|
|
1449
|
+
),
|
|
1450
|
+
...(profile.context !== undefined ? { context: profile.context } : {}),
|
|
1451
|
+
...(profile.stopWhenPanelAgrees !== undefined
|
|
1452
|
+
? { stopWhenPanelAgrees: profile.stopWhenPanelAgrees }
|
|
1453
|
+
: {}),
|
|
1454
|
+
...(profile.blindPanelLabels !== undefined
|
|
1455
|
+
? { blindPanelLabels: profile.blindPanelLabels }
|
|
1456
|
+
: {}),
|
|
1457
|
+
...(profile.judgeToolBudget !== undefined
|
|
1458
|
+
? {
|
|
1459
|
+
judgeToolBudget: {
|
|
1460
|
+
...profile.judgeToolBudget,
|
|
1461
|
+
...(Array.isArray(profile.judgeToolBudget.block)
|
|
1462
|
+
? { block: [...profile.judgeToolBudget.block] }
|
|
1463
|
+
: {}),
|
|
1464
|
+
},
|
|
1465
|
+
}
|
|
1466
|
+
: {}),
|
|
1467
|
+
...(profile.synthesis !== undefined ? { synthesis: profile.synthesis } : {}),
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function profileFromSnapshot(snapshot: FusionProfileSnapshot): FusionProfile {
|
|
1472
|
+
return {
|
|
1473
|
+
panel: snapshot.panel.map((member) => ({ ...member })),
|
|
1474
|
+
judge: { ...snapshot.judge },
|
|
1475
|
+
minimumSuccessfulPanelists: snapshot.minimumSuccessfulPanelists,
|
|
1476
|
+
...(snapshot.context !== undefined ? { context: snapshot.context } : {}),
|
|
1477
|
+
...(snapshot.stopWhenPanelAgrees !== undefined
|
|
1478
|
+
? { stopWhenPanelAgrees: snapshot.stopWhenPanelAgrees }
|
|
1479
|
+
: {}),
|
|
1480
|
+
...(snapshot.blindPanelLabels !== undefined
|
|
1481
|
+
? { blindPanelLabels: snapshot.blindPanelLabels }
|
|
1482
|
+
: {}),
|
|
1483
|
+
...(snapshot.judgeToolBudget !== undefined
|
|
1484
|
+
? {
|
|
1485
|
+
judgeToolBudget: {
|
|
1486
|
+
...snapshot.judgeToolBudget,
|
|
1487
|
+
...(Array.isArray(snapshot.judgeToolBudget.block)
|
|
1488
|
+
? { block: [...snapshot.judgeToolBudget.block] }
|
|
1489
|
+
: {}),
|
|
1490
|
+
},
|
|
1491
|
+
}
|
|
1492
|
+
: {}),
|
|
1493
|
+
...(snapshot.synthesis !== undefined ? { synthesis: snapshot.synthesis } : {}),
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function completionQuality(
|
|
1498
|
+
profile: FusionProfile,
|
|
1499
|
+
outputs: readonly PanelOutput[],
|
|
1500
|
+
failures: readonly FailedPanelSummary[],
|
|
1501
|
+
): "complete" | "partial" {
|
|
1502
|
+
return outputs.length === profile.panel.length ||
|
|
1503
|
+
(profile.stopWhenPanelAgrees === true &&
|
|
1504
|
+
failures.length > 0 &&
|
|
1505
|
+
failures.every((failure) => failure.reason === "stopped-after-agreement"))
|
|
1506
|
+
? "complete"
|
|
1507
|
+
: "partial";
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function validateStartArgs(args: ParsedFusionArgs): string | undefined {
|
|
1511
|
+
if (typeof args.prompt !== "string" || !args.prompt.trim()) {
|
|
1512
|
+
return "Fusion prompt must not be blank.";
|
|
1513
|
+
}
|
|
1514
|
+
if (args.profile !== undefined && !args.profile.trim()) {
|
|
1515
|
+
return "Fusion profile must not be blank.";
|
|
1516
|
+
}
|
|
1517
|
+
if (
|
|
1518
|
+
args.panel !== undefined &&
|
|
1519
|
+
(args.panel.length === 0 || args.panel.some((entry) => !entry.trim()))
|
|
1520
|
+
) {
|
|
1521
|
+
return "Fusion panel must contain at least one non-blank entry.";
|
|
1522
|
+
}
|
|
1523
|
+
return undefined;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function hasUnresolvedSpawnIntent(run: FusionRun): boolean {
|
|
1527
|
+
if (!run.spawnIntent) return false;
|
|
1528
|
+
return run.spawnIntent.stage === "panel"
|
|
1529
|
+
? !run.panelRunId && !run.chainRunId
|
|
1530
|
+
: !run.judgeRunId;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* A restored nonterminal phase without the remote ID cannot be reconciled.
|
|
1535
|
+
* Spawn intents are handled first so their more specific no-replay failure is
|
|
1536
|
+
* preserved; all other incomplete records are terminalized rather than left
|
|
1537
|
+
* active forever.
|
|
1538
|
+
*/
|
|
1539
|
+
function validateRestoredRunLifecycle(run: FusionRun): string | undefined {
|
|
1540
|
+
if (run.phase === "judge" && !run.judgeRunId) {
|
|
1541
|
+
return "Fusion recovery stopped: judge phase has no persisted judge run ID.";
|
|
1542
|
+
}
|
|
1543
|
+
if (run.phase === "chain" && !run.chainRunId) {
|
|
1544
|
+
return "Fusion recovery stopped: chain phase has no persisted chain run ID.";
|
|
1545
|
+
}
|
|
1546
|
+
if (run.phase === "panel" && !run.panelRunId && !run.chainRunId) {
|
|
1547
|
+
return "Fusion recovery stopped: panel phase has no persisted panel run ID.";
|
|
1548
|
+
}
|
|
1549
|
+
return undefined;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1137
1552
|
export function extractSubagentRunId(payload: unknown): string | undefined {
|
|
1138
1553
|
if (!isRecord(payload)) return undefined;
|
|
1139
1554
|
const direct = firstNonBlankString(
|
|
@@ -1238,14 +1653,17 @@ function formatFusionStatusReport(input: {
|
|
|
1238
1653
|
);
|
|
1239
1654
|
lines.push(`Profile: ${input.active.profileName}`);
|
|
1240
1655
|
lines.push(`Phase: ${input.details?.phaseLabel ?? input.active.phase}`);
|
|
1656
|
+
appendEffectiveTimeouts(lines, input.active);
|
|
1241
1657
|
if (input.active.chainRunId)
|
|
1242
1658
|
lines.push(`Chain run: ${input.active.chainRunId}`);
|
|
1243
1659
|
else if (input.active.panelRunId)
|
|
1244
1660
|
lines.push(`Panel run: ${input.active.panelRunId}`);
|
|
1245
1661
|
if (input.active.judgeRunId) {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1662
|
+
const synthesisLabel =
|
|
1663
|
+
input.details?.fallbackJudge?.label ??
|
|
1664
|
+
input.details?.judge?.label ??
|
|
1665
|
+
(input.active.chainRunId ? "Fallback judge" : "Judge");
|
|
1666
|
+
lines.push(`${synthesisLabel} run: ${input.active.judgeRunId}`);
|
|
1249
1667
|
}
|
|
1250
1668
|
lines.push(
|
|
1251
1669
|
`Progress: ${input.progress ? formatProgressCounts(input.progress) : "unknown"}`,
|
|
@@ -1257,6 +1675,7 @@ function formatFusionStatusReport(input: {
|
|
|
1257
1675
|
lines.push(`Prompt: ${firstLine(input.last.prompt)}`);
|
|
1258
1676
|
lines.push(`Profile: ${input.last.profileName}`);
|
|
1259
1677
|
lines.push(`Phase: ${input.last.phase}`);
|
|
1678
|
+
appendEffectiveTimeouts(lines, input.last);
|
|
1260
1679
|
if (input.last.chainRunId)
|
|
1261
1680
|
lines.push(`Chain run: ${input.last.chainRunId}`);
|
|
1262
1681
|
else if (input.last.panelRunId)
|
|
@@ -1278,6 +1697,20 @@ function formatFusionStatusReport(input: {
|
|
|
1278
1697
|
return lines.join("\n");
|
|
1279
1698
|
}
|
|
1280
1699
|
|
|
1700
|
+
function appendEffectiveTimeouts(
|
|
1701
|
+
lines: string[],
|
|
1702
|
+
run: Pick<FusionRun, "effectiveTimeouts">,
|
|
1703
|
+
): void {
|
|
1704
|
+
const timeouts = run.effectiveTimeouts;
|
|
1705
|
+
if (!timeouts) return;
|
|
1706
|
+
lines.push(
|
|
1707
|
+
`Deadlines: panelist ${timeouts.panelistTimeoutMs}ms, panel ${timeouts.panelTimeoutMs}ms (+${timeouts.panelGraceMs}ms grace), judge ${timeouts.judgeTimeoutMs}ms`,
|
|
1708
|
+
);
|
|
1709
|
+
if (timeouts.usesLegacyTimeout) {
|
|
1710
|
+
lines.push("Warning: legacy timeoutMs supplied one or more effective deadlines.");
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1281
1714
|
function appendStatusDetails(
|
|
1282
1715
|
lines: string[],
|
|
1283
1716
|
details: FusionStatusDetails | undefined,
|
|
@@ -1332,7 +1765,7 @@ function buildFusionStatusDetails(
|
|
|
1332
1765
|
): FusionStatusDetails {
|
|
1333
1766
|
const details: FusionStatusDetails = {
|
|
1334
1767
|
prompt: run.prompt,
|
|
1335
|
-
phaseLabel: deriveFusionStatusPhase(run, payload),
|
|
1768
|
+
phaseLabel: deriveFusionStatusPhase(run, payload, profile),
|
|
1336
1769
|
};
|
|
1337
1770
|
if (!profile) return details;
|
|
1338
1771
|
|
|
@@ -1345,7 +1778,7 @@ function buildFusionStatusDetails(
|
|
|
1345
1778
|
storedPanelFailures(run),
|
|
1346
1779
|
);
|
|
1347
1780
|
details.fallbackJudge = {
|
|
1348
|
-
label: run.chainRunId
|
|
1781
|
+
label: synthesisStatusLabel(profile, Boolean(run.chainRunId)),
|
|
1349
1782
|
...(judgeModel ? { model: judgeModel } : {}),
|
|
1350
1783
|
status: describeStandaloneRunStatus(payload),
|
|
1351
1784
|
};
|
|
@@ -1374,7 +1807,7 @@ function buildFusionStatusDetails(
|
|
|
1374
1807
|
const judgeActivity = describeStepActivity(steps[profile.panel.length]);
|
|
1375
1808
|
const judgeMetrics = describeStepMetrics(steps[profile.panel.length]);
|
|
1376
1809
|
details.judge = {
|
|
1377
|
-
label:
|
|
1810
|
+
label: synthesisStatusLabel(profile),
|
|
1378
1811
|
...(judgeModel ? { model: judgeModel } : {}),
|
|
1379
1812
|
status: describeChainJudgeStatus(steps[profile.panel.length], panelists),
|
|
1380
1813
|
...([judgeActivity, judgeMetrics].filter(Boolean).length > 0
|
|
@@ -1387,9 +1820,10 @@ function buildFusionStatusDetails(
|
|
|
1387
1820
|
function deriveFusionStatusPhase(
|
|
1388
1821
|
run: Pick<FusionRun, "phase" | "chainRunId">,
|
|
1389
1822
|
payload: unknown,
|
|
1823
|
+
profile?: Pick<FusionProfile, "panel" | "synthesis">,
|
|
1390
1824
|
): string {
|
|
1391
1825
|
if (run.phase === "judge") {
|
|
1392
|
-
return run.chainRunId
|
|
1826
|
+
return synthesisStatusLabel(profile, Boolean(run.chainRunId)).toLowerCase();
|
|
1393
1827
|
}
|
|
1394
1828
|
if (run.phase !== "chain") return run.phase;
|
|
1395
1829
|
|
|
@@ -1397,7 +1831,7 @@ function deriveFusionStatusPhase(
|
|
|
1397
1831
|
if (steps.length === 0) return "chain";
|
|
1398
1832
|
const judgeStatus = normalizeStatusLabel(steps.at(-1));
|
|
1399
1833
|
if (judgeStatus === "running" || judgeStatus === "completed") {
|
|
1400
|
-
return
|
|
1834
|
+
return synthesisStatusLabel(profile).toLowerCase();
|
|
1401
1835
|
}
|
|
1402
1836
|
const panelSteps = steps.slice(0, -1);
|
|
1403
1837
|
if (panelSteps.some((step) => normalizeStatusLabel(step) === "running")) {
|
|
@@ -1408,11 +1842,23 @@ function deriveFusionStatusPhase(
|
|
|
1408
1842
|
const status = normalizeStatusLabel(step);
|
|
1409
1843
|
return status === "completed" || status === "failed";
|
|
1410
1844
|
});
|
|
1411
|
-
return allPanelsFinished
|
|
1845
|
+
return allPanelsFinished
|
|
1846
|
+
? synthesisStatusLabel(profile).toLowerCase()
|
|
1847
|
+
: "panel";
|
|
1412
1848
|
}
|
|
1413
1849
|
return "panel";
|
|
1414
1850
|
}
|
|
1415
1851
|
|
|
1852
|
+
function synthesisStatusLabel(
|
|
1853
|
+
profile: Pick<FusionProfile, "panel" | "synthesis"> | undefined,
|
|
1854
|
+
fallback = false,
|
|
1855
|
+
): "Judge" | "Fallback judge" | "Composer" | "Fallback composer" {
|
|
1856
|
+
const base = profile && resolveSynthesisMode(profile) === "merge"
|
|
1857
|
+
? "Composer"
|
|
1858
|
+
: "Judge";
|
|
1859
|
+
return fallback ? `Fallback ${base.toLowerCase()}` as "Fallback judge" | "Fallback composer" : base;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1416
1862
|
function buildCompletedPanelStatusLines(
|
|
1417
1863
|
panel: FusionProfile["panel"],
|
|
1418
1864
|
outputs: readonly PanelOutput[],
|
|
@@ -1570,20 +2016,26 @@ function findResultsArray(payload: unknown): readonly unknown[] | undefined {
|
|
|
1570
2016
|
return undefined;
|
|
1571
2017
|
}
|
|
1572
2018
|
|
|
2019
|
+
function hasExplicitResultsArray(payload: unknown): boolean {
|
|
2020
|
+
return findResultsArray(payload) !== undefined;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1573
2023
|
function hasResultsArray(payload: unknown): boolean {
|
|
1574
2024
|
return (findResultsArray(payload)?.length ?? 0) > 0;
|
|
1575
2025
|
}
|
|
1576
2026
|
|
|
1577
2027
|
function hasJudgeResult(payload: unknown, panelCount: number): boolean {
|
|
1578
|
-
|
|
2028
|
+
const count =
|
|
2029
|
+
findResultsArray(payload)?.length ?? findStepsArray(payload).length;
|
|
2030
|
+
return count > panelCount;
|
|
1579
2031
|
}
|
|
1580
2032
|
|
|
1581
2033
|
function findResult(
|
|
1582
2034
|
payload: unknown,
|
|
1583
2035
|
resultIndex?: number,
|
|
1584
2036
|
): Record<string, unknown> | undefined {
|
|
1585
|
-
const results = findResultsArray(payload);
|
|
1586
|
-
if (
|
|
2037
|
+
const results = findResultsArray(payload) ?? findStepsArray(payload);
|
|
2038
|
+
if (results.length === 0) return undefined;
|
|
1587
2039
|
if (resultIndex !== undefined) {
|
|
1588
2040
|
const indexed = results[resultIndex];
|
|
1589
2041
|
return isRecord(indexed) ? indexed : undefined;
|
|
@@ -1678,6 +2130,31 @@ function extractWorkflowStoppedPanelIndices(payload: unknown): number[] {
|
|
|
1678
2130
|
return [];
|
|
1679
2131
|
}
|
|
1680
2132
|
|
|
2133
|
+
function withWorkflowDeadlineFailures(
|
|
2134
|
+
failures: readonly FailedPanelSummary[],
|
|
2135
|
+
deadlineError: string | undefined,
|
|
2136
|
+
): FailedPanelSummary[] {
|
|
2137
|
+
if (!deadlineError) return [...failures];
|
|
2138
|
+
return failures.map((failure) => ({
|
|
2139
|
+
...failure,
|
|
2140
|
+
summary: failure.summary.includes(deadlineError)
|
|
2141
|
+
? failure.summary
|
|
2142
|
+
: `${deadlineError}\n${failure.summary}`,
|
|
2143
|
+
reason: failure.reason ?? "timeout",
|
|
2144
|
+
}));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
function isWorkflowDeadline(payload: unknown): boolean {
|
|
2148
|
+
if (!isRecord(payload)) return false;
|
|
2149
|
+
if (payload.timedOut === true) return true;
|
|
2150
|
+
const error = firstNonBlankString(payload.error, payload.errorMessage);
|
|
2151
|
+
if (error && /(?:workflow.*(?:timed out|timeout)|(?:timed out|timeout).*workflow)/i.test(error)) {
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
if (isRecord(payload.details) && isWorkflowDeadline(payload.details)) return true;
|
|
2155
|
+
return isRecord(payload.data) ? isWorkflowDeadline(payload.data) : false;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1681
2158
|
function extractSubagentFailure(payload: unknown): string | undefined {
|
|
1682
2159
|
if (!isRecord(payload)) return undefined;
|
|
1683
2160
|
const direct = firstNonBlankString(payload.error, payload.errorMessage);
|
|
@@ -1752,63 +2229,19 @@ function extractArtifactPath(
|
|
|
1752
2229
|
return undefined;
|
|
1753
2230
|
}
|
|
1754
2231
|
|
|
1755
|
-
function mergePanelObservations(
|
|
1756
|
-
result: ExtractPanelResultsSuccess,
|
|
1757
|
-
statusPayload: unknown,
|
|
1758
|
-
profile: FusionProfile,
|
|
1759
|
-
): ExtractPanelResultsSuccess {
|
|
1760
|
-
const status = extractPanelResults(statusPayload, {
|
|
1761
|
-
panel: profile.panel,
|
|
1762
|
-
completedOnly: true,
|
|
1763
|
-
limit: profile.panel.length,
|
|
1764
|
-
});
|
|
1765
|
-
if (!status.ok) return result;
|
|
1766
|
-
|
|
1767
|
-
const observations = new Map<number, PanelOutput["observation"]>();
|
|
1768
|
-
for (const output of status.outputs) {
|
|
1769
|
-
observations.set(output.index, output.observation);
|
|
1770
|
-
}
|
|
1771
|
-
for (const failure of status.failures) {
|
|
1772
|
-
observations.set(failure.index, failure.observation);
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
return {
|
|
1776
|
-
...result,
|
|
1777
|
-
outputs: result.outputs.map((output) =>
|
|
1778
|
-
withMergedObservation(output, observations.get(output.index)),
|
|
1779
|
-
),
|
|
1780
|
-
failures: result.failures.map((failure) =>
|
|
1781
|
-
withMergedObservation(failure, observations.get(failure.index)),
|
|
1782
|
-
),
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
function withMergedObservation<T extends PanelOutput | FailedPanelSummary>(
|
|
1787
|
-
item: T,
|
|
1788
|
-
statusObservation: PanelOutput["observation"] | undefined,
|
|
1789
|
-
): T {
|
|
1790
|
-
const observation = mergeRunObservations(statusObservation, item.observation);
|
|
1791
|
-
return hasObservationData(observation) ? { ...item, observation } : item;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
function hasObservationData(observation: PanelOutput["observation"]): boolean {
|
|
1795
|
-
return Boolean(
|
|
1796
|
-
observation &&
|
|
1797
|
-
(observation.model ||
|
|
1798
|
-
observation.durationMs !== undefined ||
|
|
1799
|
-
observation.usage ||
|
|
1800
|
-
observation.attempts ||
|
|
1801
|
-
observation.providerFailures),
|
|
1802
|
-
);
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
2232
|
function shouldStopWhenPanelAgrees(
|
|
1806
2233
|
profile: FusionProfile,
|
|
1807
2234
|
outputs: readonly PanelOutput[],
|
|
1808
2235
|
failures: readonly FailedPanelSummary[],
|
|
2236
|
+
persistedPolicy?: FusionRun["minimumSuccessfulPanelists"],
|
|
1809
2237
|
): boolean {
|
|
2238
|
+
const required = resolveMinimumSuccessfulPanelists(
|
|
2239
|
+
persistedPolicy ?? profile.minimumSuccessfulPanelists,
|
|
2240
|
+
profile.panel.length,
|
|
2241
|
+
);
|
|
1810
2242
|
return (
|
|
1811
2243
|
profile.stopWhenPanelAgrees === true &&
|
|
2244
|
+
outputs.length >= required &&
|
|
1812
2245
|
hasStrongPanelAgreement(
|
|
1813
2246
|
outputs,
|
|
1814
2247
|
outputs.length + failures.length,
|