@alexeiled/pi-fusion 0.7.0 → 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 +4 -3
- package/docs/user-guide.md +15 -12
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/config.ts +26 -0
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +29 -0
- package/src/index.ts +33 -5
- package/src/lifecycle-reconcile.ts +203 -6
- package/src/orchestrator.ts +487 -71
- package/src/panel-completion.ts +65 -17
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +85 -3
- package/src/result-extract.ts +66 -4
- package/src/run-builder.ts +84 -8
- package/src/run-store.ts +360 -11
- package/src/status.ts +1 -1
- package/src/types.ts +75 -0
package/src/run-store.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
3
|
FusionPhase,
|
|
4
|
+
FusionProfileSnapshot,
|
|
5
|
+
FusionRecoveryState,
|
|
4
6
|
FusionRun,
|
|
5
7
|
ModelAttempt,
|
|
6
8
|
PanelDecision,
|
|
@@ -25,6 +27,9 @@ export type FusionRunSummary = Omit<
|
|
|
25
27
|
| "profileName"
|
|
26
28
|
| "operationId"
|
|
27
29
|
| "outputContract"
|
|
30
|
+
| "completionQuality"
|
|
31
|
+
| "effectiveTimeouts"
|
|
32
|
+
| "recovery"
|
|
28
33
|
| "phase"
|
|
29
34
|
| "createdAt"
|
|
30
35
|
| "updatedAt"
|
|
@@ -45,6 +50,10 @@ export interface FusionRunStartInput {
|
|
|
45
50
|
baseProfileName?: string;
|
|
46
51
|
operationId?: string;
|
|
47
52
|
outputContract?: FusionRun["outputContract"];
|
|
53
|
+
minimumSuccessfulPanelists?: FusionRun["minimumSuccessfulPanelists"];
|
|
54
|
+
profileSnapshot?: FusionRun["profileSnapshot"];
|
|
55
|
+
timeoutOverrides?: FusionRun["timeoutOverrides"];
|
|
56
|
+
effectiveTimeouts?: FusionRun["effectiveTimeouts"];
|
|
48
57
|
phase?: Exclude<FusionPhase, FusionTerminalPhase>;
|
|
49
58
|
createdAt?: number;
|
|
50
59
|
}
|
|
@@ -60,8 +69,12 @@ export interface FusionRunPatch {
|
|
|
60
69
|
judgeRunId?: string;
|
|
61
70
|
judgeAsyncDir?: string;
|
|
62
71
|
judgeObservation?: FusionRun["judgeObservation"];
|
|
72
|
+
completionQuality?: FusionRun["completionQuality"];
|
|
63
73
|
panelOutputs?: FusionRun["panelOutputs"];
|
|
64
74
|
panelFailures?: FusionRun["panelFailures"];
|
|
75
|
+
recovery?: FusionRun["recovery"];
|
|
76
|
+
/** `null` clears an intent once the RPC returned its durable remote ID. */
|
|
77
|
+
spawnIntent?: FusionRun["spawnIntent"] | null;
|
|
65
78
|
report?: string;
|
|
66
79
|
error?: string;
|
|
67
80
|
updatedAt?: number;
|
|
@@ -71,6 +84,8 @@ export interface FusionRunTransitionPatch {
|
|
|
71
84
|
chainRunId?: string;
|
|
72
85
|
panelRunId?: string;
|
|
73
86
|
judgeRunId?: string;
|
|
87
|
+
recovery?: FusionRun["recovery"];
|
|
88
|
+
spawnIntent?: FusionRun["spawnIntent"];
|
|
74
89
|
report?: string;
|
|
75
90
|
error?: string;
|
|
76
91
|
updatedAt?: number;
|
|
@@ -107,6 +122,7 @@ export class FusionRunStore {
|
|
|
107
122
|
private readonly now: () => number;
|
|
108
123
|
private readonly idFactory: () => string;
|
|
109
124
|
private readonly persistence: FusionRunStorePersistence | undefined;
|
|
125
|
+
private restoreError: string | undefined;
|
|
110
126
|
|
|
111
127
|
constructor(options: FusionRunStoreOptions = {}) {
|
|
112
128
|
this.now = options.now ?? Date.now;
|
|
@@ -134,6 +150,11 @@ export class FusionRunStore {
|
|
|
134
150
|
return runId ? this.getRunById(runId) : undefined;
|
|
135
151
|
}
|
|
136
152
|
|
|
153
|
+
/** A corrupt newest snapshot must never revive an older active run. */
|
|
154
|
+
getRestoreError(): string | undefined {
|
|
155
|
+
return this.restoreError;
|
|
156
|
+
}
|
|
157
|
+
|
|
137
158
|
startRun(input: FusionRunStartInput): FusionRun {
|
|
138
159
|
if (this.activeRun) {
|
|
139
160
|
throw new FusionRunStoreError(
|
|
@@ -167,13 +188,25 @@ export class FusionRunStore {
|
|
|
167
188
|
...(input.outputContract !== undefined
|
|
168
189
|
? { outputContract: input.outputContract }
|
|
169
190
|
: {}),
|
|
191
|
+
...(input.minimumSuccessfulPanelists !== undefined
|
|
192
|
+
? { minimumSuccessfulPanelists: input.minimumSuccessfulPanelists }
|
|
193
|
+
: {}),
|
|
194
|
+
...(input.profileSnapshot !== undefined
|
|
195
|
+
? { profileSnapshot: cloneProfileSnapshot(input.profileSnapshot) }
|
|
196
|
+
: {}),
|
|
197
|
+
...(input.timeoutOverrides !== undefined
|
|
198
|
+
? { timeoutOverrides: { ...input.timeoutOverrides } }
|
|
199
|
+
: {}),
|
|
200
|
+
...(input.effectiveTimeouts !== undefined
|
|
201
|
+
? { effectiveTimeouts: { ...input.effectiveTimeouts } }
|
|
202
|
+
: {}),
|
|
170
203
|
phase: input.phase ?? "chain",
|
|
171
204
|
createdAt,
|
|
172
205
|
updatedAt: createdAt,
|
|
173
206
|
};
|
|
207
|
+
this.persistRun(run);
|
|
174
208
|
this.activeRun = run;
|
|
175
209
|
this.rememberRun(run);
|
|
176
|
-
this.persistRun(run);
|
|
177
210
|
return cloneRun(run);
|
|
178
211
|
}
|
|
179
212
|
|
|
@@ -221,11 +254,26 @@ export class FusionRunStore {
|
|
|
221
254
|
restoreFromEntries(
|
|
222
255
|
entries: readonly unknown[],
|
|
223
256
|
): FusionRunSummary | undefined {
|
|
224
|
-
|
|
257
|
+
this.restoreError = undefined;
|
|
225
258
|
this.runsById.clear();
|
|
226
259
|
this.runIdsByOperationId.clear();
|
|
227
|
-
for (const state of states) this.rememberRun(state);
|
|
228
260
|
|
|
261
|
+
const latestPersisted = lastFusionRunEnvelope(entries);
|
|
262
|
+
if (
|
|
263
|
+
latestPersisted &&
|
|
264
|
+
(!isFusionRunEntry(latestPersisted) || !isFusionRunState(latestPersisted.data))
|
|
265
|
+
) {
|
|
266
|
+
// History readers may skip malformed entries, but restore must not adopt
|
|
267
|
+
// an older active state after a newer snapshot was corrupted.
|
|
268
|
+
this.activeRun = undefined;
|
|
269
|
+
this.lastRunSummary = undefined;
|
|
270
|
+
this.restoreError =
|
|
271
|
+
"Latest persisted fusion run snapshot is invalid; refusing stale active-run recovery.";
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const states = readFusionRunStates(entries);
|
|
276
|
+
for (const state of states) this.rememberRun(state);
|
|
229
277
|
const latestState = states.at(-1);
|
|
230
278
|
const summary = readLastFusionRunSummary(entries);
|
|
231
279
|
this.activeRun =
|
|
@@ -346,12 +394,20 @@ function applyPatch(
|
|
|
346
394
|
if (patch.judgeObservation !== undefined) {
|
|
347
395
|
updated.judgeObservation = cloneObservation(patch.judgeObservation);
|
|
348
396
|
}
|
|
397
|
+
if (patch.completionQuality !== undefined) {
|
|
398
|
+
updated.completionQuality = patch.completionQuality;
|
|
399
|
+
}
|
|
349
400
|
if (patch.panelOutputs !== undefined) {
|
|
350
401
|
updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
|
|
351
402
|
}
|
|
352
403
|
if (patch.panelFailures !== undefined) {
|
|
353
404
|
updated.panelFailures = clonePanelFailures(patch.panelFailures);
|
|
354
405
|
}
|
|
406
|
+
if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
|
|
407
|
+
if (patch.spawnIntent === null) delete updated.spawnIntent;
|
|
408
|
+
else if (patch.spawnIntent !== undefined) {
|
|
409
|
+
updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
|
|
410
|
+
}
|
|
355
411
|
if (patch.report !== undefined) updated.report = patch.report;
|
|
356
412
|
if (patch.error !== undefined) updated.error = patch.error;
|
|
357
413
|
return updated;
|
|
@@ -371,6 +427,10 @@ function applyTransitionPatch(
|
|
|
371
427
|
if (patch.chainRunId !== undefined) updated.chainRunId = patch.chainRunId;
|
|
372
428
|
if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
|
|
373
429
|
if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
|
|
430
|
+
if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
|
|
431
|
+
if (patch.spawnIntent !== undefined) {
|
|
432
|
+
updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
|
|
433
|
+
}
|
|
374
434
|
if (patch.report !== undefined) updated.report = patch.report;
|
|
375
435
|
if (patch.error !== undefined) updated.error = patch.error;
|
|
376
436
|
return updated;
|
|
@@ -387,6 +447,13 @@ function toRunSummary(
|
|
|
387
447
|
...(run.outputContract !== undefined
|
|
388
448
|
? { outputContract: run.outputContract }
|
|
389
449
|
: {}),
|
|
450
|
+
...(run.completionQuality !== undefined
|
|
451
|
+
? { completionQuality: run.completionQuality }
|
|
452
|
+
: {}),
|
|
453
|
+
...(run.effectiveTimeouts !== undefined
|
|
454
|
+
? { effectiveTimeouts: { ...run.effectiveTimeouts } }
|
|
455
|
+
: {}),
|
|
456
|
+
...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
|
|
390
457
|
phase: run.phase,
|
|
391
458
|
createdAt: run.createdAt,
|
|
392
459
|
updatedAt: run.updatedAt,
|
|
@@ -415,6 +482,21 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
415
482
|
...(run.outputContract !== undefined
|
|
416
483
|
? { outputContract: run.outputContract }
|
|
417
484
|
: {}),
|
|
485
|
+
...(run.minimumSuccessfulPanelists !== undefined
|
|
486
|
+
? { minimumSuccessfulPanelists: run.minimumSuccessfulPanelists }
|
|
487
|
+
: {}),
|
|
488
|
+
...(run.profileSnapshot !== undefined
|
|
489
|
+
? { profileSnapshot: cloneProfileSnapshot(run.profileSnapshot) }
|
|
490
|
+
: {}),
|
|
491
|
+
...(run.timeoutOverrides !== undefined
|
|
492
|
+
? { timeoutOverrides: { ...run.timeoutOverrides } }
|
|
493
|
+
: {}),
|
|
494
|
+
...(run.effectiveTimeouts !== undefined
|
|
495
|
+
? { effectiveTimeouts: { ...run.effectiveTimeouts } }
|
|
496
|
+
: {}),
|
|
497
|
+
...(run.completionQuality !== undefined
|
|
498
|
+
? { completionQuality: run.completionQuality }
|
|
499
|
+
: {}),
|
|
418
500
|
phase: run.phase,
|
|
419
501
|
createdAt: run.createdAt,
|
|
420
502
|
updatedAt: run.updatedAt,
|
|
@@ -445,6 +527,10 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
445
527
|
...(run.panelFailures !== undefined
|
|
446
528
|
? { panelFailures: clonePanelFailures(run.panelFailures) }
|
|
447
529
|
: {}),
|
|
530
|
+
...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
|
|
531
|
+
...(run.spawnIntent !== undefined
|
|
532
|
+
? { spawnIntent: cloneSpawnIntent(run.spawnIntent) }
|
|
533
|
+
: {}),
|
|
448
534
|
...(run.report !== undefined ? { report: run.report } : {}),
|
|
449
535
|
...(run.error !== undefined ? { error: run.error } : {}),
|
|
450
536
|
};
|
|
@@ -454,17 +540,30 @@ function cloneRunSummary(summary: FusionRunSummary): FusionRunSummary {
|
|
|
454
540
|
return toRunSummary(summary);
|
|
455
541
|
}
|
|
456
542
|
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
543
|
+
function lastFusionRunEnvelope(entries: readonly unknown[]): unknown {
|
|
544
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
545
|
+
const entry = entries[index];
|
|
546
|
+
// Locate the newest entry by its envelope before inspecting data. A
|
|
547
|
+
// malformed data field (or its absence) must not revive an older run.
|
|
548
|
+
if (isFusionRunEnvelope(entry)) return entry;
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isFusionRunEnvelope(value: unknown): value is Record<string, unknown> {
|
|
460
554
|
return (
|
|
461
555
|
isRecord(value) &&
|
|
462
556
|
value.type === "custom" &&
|
|
463
|
-
value.customType === FUSION_RUN_ENTRY_TYPE
|
|
464
|
-
"data" in value
|
|
557
|
+
value.customType === FUSION_RUN_ENTRY_TYPE
|
|
465
558
|
);
|
|
466
559
|
}
|
|
467
560
|
|
|
561
|
+
function isFusionRunEntry(
|
|
562
|
+
value: unknown,
|
|
563
|
+
): value is { type: "custom"; customType: string; data: unknown } {
|
|
564
|
+
return isFusionRunEnvelope(value) && "data" in value;
|
|
565
|
+
}
|
|
566
|
+
|
|
468
567
|
function isFusionRunState(value: unknown): value is FusionRun {
|
|
469
568
|
if (!isRecord(value)) return false;
|
|
470
569
|
if (!isNonEmptyString(value.id)) return false;
|
|
@@ -479,6 +578,47 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
479
578
|
) {
|
|
480
579
|
return false;
|
|
481
580
|
}
|
|
581
|
+
if (
|
|
582
|
+
value.minimumSuccessfulPanelists !== undefined &&
|
|
583
|
+
value.minimumSuccessfulPanelists !== "majority" &&
|
|
584
|
+
value.minimumSuccessfulPanelists !== "all" &&
|
|
585
|
+
(!isFiniteNumber(value.minimumSuccessfulPanelists) ||
|
|
586
|
+
!Number.isInteger(value.minimumSuccessfulPanelists) ||
|
|
587
|
+
value.minimumSuccessfulPanelists < 1)
|
|
588
|
+
) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
if (
|
|
592
|
+
value.profileSnapshot !== undefined &&
|
|
593
|
+
!isProfileSnapshot(value.profileSnapshot)
|
|
594
|
+
) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
// Snapshots are the canonical quorum record for new runs. Older records may
|
|
598
|
+
// also carry the run-level policy, but it must resolve to the same quorum.
|
|
599
|
+
if (
|
|
600
|
+
value.profileSnapshot !== undefined &&
|
|
601
|
+
value.minimumSuccessfulPanelists !== undefined &&
|
|
602
|
+
resolvePersistedQuorum(
|
|
603
|
+
value.minimumSuccessfulPanelists,
|
|
604
|
+
value.profileSnapshot.panel.length,
|
|
605
|
+
) !== value.profileSnapshot.minimumSuccessfulPanelists
|
|
606
|
+
) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
if (value.timeoutOverrides !== undefined && !isTimeoutOverrides(value.timeoutOverrides)) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (value.effectiveTimeouts !== undefined && !isEffectiveTimeouts(value.effectiveTimeouts)) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
if (
|
|
616
|
+
value.completionQuality !== undefined &&
|
|
617
|
+
value.completionQuality !== "complete" &&
|
|
618
|
+
value.completionQuality !== "partial"
|
|
619
|
+
) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
482
622
|
if (
|
|
483
623
|
value.inlinePanel !== undefined &&
|
|
484
624
|
(!Array.isArray(value.inlinePanel) ||
|
|
@@ -556,19 +696,51 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
556
696
|
) {
|
|
557
697
|
return false;
|
|
558
698
|
}
|
|
699
|
+
if (value.recovery !== undefined && !isRecoveryState(value.recovery)) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
if (value.spawnIntent !== undefined && !isSpawnIntent(value.spawnIntent)) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
559
705
|
if (value.report !== undefined && typeof value.report !== "string") {
|
|
560
706
|
return false;
|
|
561
707
|
}
|
|
562
708
|
if (value.error !== undefined && typeof value.error !== "string") {
|
|
563
709
|
return false;
|
|
564
710
|
}
|
|
565
|
-
return
|
|
711
|
+
return (
|
|
712
|
+
value.profileSnapshot === undefined ||
|
|
713
|
+
validateFusionRunPanelSlots(value, value.profileSnapshot.panel.length) ===
|
|
714
|
+
undefined
|
|
715
|
+
);
|
|
566
716
|
}
|
|
567
717
|
|
|
568
718
|
function isFusionRunSummary(value: unknown): value is FusionRunSummary {
|
|
569
719
|
return isFusionRunState(value) && isTerminalPhase(value.phase);
|
|
570
720
|
}
|
|
571
721
|
|
|
722
|
+
function isTimeoutOverrides(value: unknown): boolean {
|
|
723
|
+
if (!isRecord(value)) return false;
|
|
724
|
+
return [
|
|
725
|
+
value.panelistTimeoutMs,
|
|
726
|
+
value.panelTimeoutMs,
|
|
727
|
+
value.panelGraceMs,
|
|
728
|
+
value.judgeTimeoutMs,
|
|
729
|
+
].every((timeout) => timeout === undefined || (isFiniteNumber(timeout) && Number.isInteger(timeout) && timeout > 0));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function isEffectiveTimeouts(value: unknown): boolean {
|
|
733
|
+
return (
|
|
734
|
+
isTimeoutOverrides(value) &&
|
|
735
|
+
isRecord(value) &&
|
|
736
|
+
isFiniteNumber(value.panelistTimeoutMs) &&
|
|
737
|
+
isFiniteNumber(value.panelTimeoutMs) &&
|
|
738
|
+
isFiniteNumber(value.panelGraceMs) &&
|
|
739
|
+
isFiniteNumber(value.judgeTimeoutMs) &&
|
|
740
|
+
typeof value.usesLegacyTimeout === "boolean"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
572
744
|
function isFusionPhase(value: unknown): value is FusionPhase {
|
|
573
745
|
return (
|
|
574
746
|
value === "panel" ||
|
|
@@ -584,6 +756,45 @@ function isTerminalPhase(value: unknown): value is FusionTerminalPhase {
|
|
|
584
756
|
return value === "done" || value === "failed" || value === "cancelled";
|
|
585
757
|
}
|
|
586
758
|
|
|
759
|
+
/**
|
|
760
|
+
* Checks persisted slot references before they are restored or merged. Legacy
|
|
761
|
+
* runs omit a profile snapshot, so their caller supplies the safely resolved
|
|
762
|
+
* profile length during restore.
|
|
763
|
+
*/
|
|
764
|
+
export function validateFusionRunPanelSlots(
|
|
765
|
+
run: Pick<
|
|
766
|
+
FusionRun,
|
|
767
|
+
"panelOutputs" | "panelFailures" | "panelStoppedIndices" | "recovery"
|
|
768
|
+
>,
|
|
769
|
+
panelLength: number,
|
|
770
|
+
): string | undefined {
|
|
771
|
+
const slotGroups: ReadonlyArray<readonly number[] | undefined> = [
|
|
772
|
+
run.panelOutputs?.map((output) => output.index),
|
|
773
|
+
run.panelFailures?.map((failure) => failure.index),
|
|
774
|
+
run.panelStoppedIndices,
|
|
775
|
+
run.recovery?.failedPanelIndices,
|
|
776
|
+
];
|
|
777
|
+
for (const slots of slotGroups) {
|
|
778
|
+
for (const slot of slots ?? []) {
|
|
779
|
+
if (!isPanelSlotIndex(slot) || slot >= panelLength) {
|
|
780
|
+
return `Persisted panel slot ${String(slot)} is outside the configured panel.`;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function resolvePersistedQuorum(
|
|
788
|
+
policy: NonNullable<FusionRun["minimumSuccessfulPanelists"]>,
|
|
789
|
+
panelLength: number,
|
|
790
|
+
): number {
|
|
791
|
+
if (policy === "all") return panelLength;
|
|
792
|
+
if (typeof policy === "number") {
|
|
793
|
+
return panelLength > 1 ? Math.max(2, Math.min(policy, panelLength)) : 1;
|
|
794
|
+
}
|
|
795
|
+
return Math.ceil(panelLength / 2);
|
|
796
|
+
}
|
|
797
|
+
|
|
587
798
|
function isPanelOutputArray(
|
|
588
799
|
value: unknown,
|
|
589
800
|
): value is NonNullable<FusionRun["panelOutputs"]> {
|
|
@@ -595,7 +806,7 @@ function isPanelOutput(
|
|
|
595
806
|
): value is NonNullable<FusionRun["panelOutputs"]>[number] {
|
|
596
807
|
return (
|
|
597
808
|
isRecord(value) &&
|
|
598
|
-
|
|
809
|
+
isPanelSlotIndex(value.index) &&
|
|
599
810
|
isNonEmptyString(value.agent) &&
|
|
600
811
|
typeof value.output === "string" &&
|
|
601
812
|
(value.id === undefined || typeof value.id === "string") &&
|
|
@@ -622,7 +833,7 @@ function isPanelFailure(
|
|
|
622
833
|
): value is NonNullable<FusionRun["panelFailures"]>[number] {
|
|
623
834
|
return (
|
|
624
835
|
isRecord(value) &&
|
|
625
|
-
|
|
836
|
+
isPanelSlotIndex(value.index) &&
|
|
626
837
|
isNonEmptyString(value.agent) &&
|
|
627
838
|
typeof value.summary === "string" &&
|
|
628
839
|
(value.id === undefined || typeof value.id === "string") &&
|
|
@@ -638,6 +849,144 @@ function isPanelFailure(
|
|
|
638
849
|
);
|
|
639
850
|
}
|
|
640
851
|
|
|
852
|
+
function isProfileSnapshot(value: unknown): value is FusionProfileSnapshot {
|
|
853
|
+
if (!isRecord(value) || !Array.isArray(value.panel) || value.panel.length === 0) {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
if (!value.panel.every(isSnapshotPanelMember) || !isSnapshotJudge(value.judge)) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
if (
|
|
860
|
+
!isFiniteNumber(value.minimumSuccessfulPanelists) ||
|
|
861
|
+
!Number.isInteger(value.minimumSuccessfulPanelists) ||
|
|
862
|
+
value.minimumSuccessfulPanelists < 1 ||
|
|
863
|
+
value.minimumSuccessfulPanelists > value.panel.length
|
|
864
|
+
) {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
if (value.context !== undefined && value.context !== "fresh" && value.context !== "fork") {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
if (value.stopWhenPanelAgrees !== undefined && typeof value.stopWhenPanelAgrees !== "boolean") {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
if (value.blindPanelLabels !== undefined && typeof value.blindPanelLabels !== "boolean") {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
if (value.synthesis !== undefined && value.synthesis !== "select" && value.synthesis !== "merge") {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
return value.judgeToolBudget === undefined || isSnapshotToolBudget(value.judgeToolBudget);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function isSnapshotPanelMember(value: unknown): boolean {
|
|
883
|
+
if (!isRecord(value) || !isNonEmptyString(value.id) || !isSnapshotAgent(value.agent)) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
return (
|
|
887
|
+
(value.label === undefined || isNonEmptyString(value.label)) &&
|
|
888
|
+
(value.model === undefined || isNonEmptyString(value.model)) &&
|
|
889
|
+
(value.thinking === undefined || isThinkingLevel(value.thinking)) &&
|
|
890
|
+
(value.role === undefined || typeof value.role === "string") &&
|
|
891
|
+
(value.question === undefined || isNonEmptyString(value.question))
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isSnapshotJudge(value: unknown): boolean {
|
|
896
|
+
return (
|
|
897
|
+
isRecord(value) &&
|
|
898
|
+
isSnapshotAgent(value.agent) &&
|
|
899
|
+
(value.model === undefined || isNonEmptyString(value.model)) &&
|
|
900
|
+
(value.thinking === undefined || isThinkingLevel(value.thinking))
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function isSnapshotAgent(value: unknown): boolean {
|
|
905
|
+
return (
|
|
906
|
+
isNonEmptyString(value) &&
|
|
907
|
+
/^[^\s.]+(?:\.[^\s.]+)*$/.test(value.trim())
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function isThinkingLevel(value: unknown): boolean {
|
|
912
|
+
return value === "off" || value === "minimal" || value === "low" ||
|
|
913
|
+
value === "medium" || value === "high" || value === "xhigh";
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function isSnapshotToolBudget(value: unknown): boolean {
|
|
917
|
+
if (!isRecord(value) || (value.soft === undefined && value.hard === undefined)) {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
if (value.soft !== undefined && (!isFiniteNumber(value.soft) || !Number.isInteger(value.soft) || value.soft < 1)) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
if (value.hard !== undefined && (!isFiniteNumber(value.hard) || !Number.isInteger(value.hard) || value.hard < 1)) {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
if (typeof value.soft === "number" && typeof value.hard === "number" && value.soft > value.hard) {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
return value.block === undefined || value.block === "*" ||
|
|
930
|
+
(Array.isArray(value.block) && value.block.length > 0 && value.block.every(isNonEmptyString));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function cloneProfileSnapshot(snapshot: FusionProfileSnapshot): FusionProfileSnapshot {
|
|
934
|
+
return {
|
|
935
|
+
panel: snapshot.panel.map((member) => ({ ...member })),
|
|
936
|
+
judge: { ...snapshot.judge },
|
|
937
|
+
minimumSuccessfulPanelists: snapshot.minimumSuccessfulPanelists,
|
|
938
|
+
...(snapshot.context !== undefined ? { context: snapshot.context } : {}),
|
|
939
|
+
...(snapshot.stopWhenPanelAgrees !== undefined
|
|
940
|
+
? { stopWhenPanelAgrees: snapshot.stopWhenPanelAgrees }
|
|
941
|
+
: {}),
|
|
942
|
+
...(snapshot.blindPanelLabels !== undefined
|
|
943
|
+
? { blindPanelLabels: snapshot.blindPanelLabels }
|
|
944
|
+
: {}),
|
|
945
|
+
...(snapshot.judgeToolBudget !== undefined
|
|
946
|
+
? {
|
|
947
|
+
judgeToolBudget: {
|
|
948
|
+
...snapshot.judgeToolBudget,
|
|
949
|
+
...(Array.isArray(snapshot.judgeToolBudget.block)
|
|
950
|
+
? { block: [...snapshot.judgeToolBudget.block] }
|
|
951
|
+
: {}),
|
|
952
|
+
},
|
|
953
|
+
}
|
|
954
|
+
: {}),
|
|
955
|
+
...(snapshot.synthesis !== undefined ? { synthesis: snapshot.synthesis } : {}),
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function isRecoveryState(value: unknown): value is FusionRecoveryState {
|
|
960
|
+
return (
|
|
961
|
+
isRecord(value) &&
|
|
962
|
+
value.retryDeferred === true &&
|
|
963
|
+
Array.isArray(value.failedPanelIndices) &&
|
|
964
|
+
value.failedPanelIndices.every(isPanelSlotIndex)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function isPanelSlotIndex(value: unknown): value is number {
|
|
969
|
+
return isFiniteNumber(value) && Number.isInteger(value) && value >= 0;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function cloneRecovery(recovery: FusionRecoveryState): FusionRecoveryState {
|
|
973
|
+
return { ...recovery, failedPanelIndices: [...recovery.failedPanelIndices] };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function isSpawnIntent(value: unknown): value is FusionRun["spawnIntent"] {
|
|
977
|
+
return (
|
|
978
|
+
isRecord(value) &&
|
|
979
|
+
(value.stage === "panel" || value.stage === "judge") &&
|
|
980
|
+
isFiniteNumber(value.requestedAt)
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function cloneSpawnIntent(
|
|
985
|
+
intent: NonNullable<FusionRun["spawnIntent"]>,
|
|
986
|
+
): NonNullable<FusionRun["spawnIntent"]> {
|
|
987
|
+
return { ...intent };
|
|
988
|
+
}
|
|
989
|
+
|
|
641
990
|
function clonePanelOutputs(
|
|
642
991
|
outputs: NonNullable<FusionRun["panelOutputs"]>,
|
|
643
992
|
): NonNullable<FusionRun["panelOutputs"]> {
|
package/src/status.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function formatFusionStatusText(
|
|
|
52
52
|
run.phase === "judge" ? run.judgeRunId : (run.chainRunId ?? run.panelRunId);
|
|
53
53
|
const phase = phaseLabel ?? run.phase;
|
|
54
54
|
if (progress) {
|
|
55
|
-
return `fusion:
|
|
55
|
+
return `fusion: ${phase} · ${formatProgressCounts(progress)} · ${run.profileName}`;
|
|
56
56
|
}
|
|
57
57
|
if (activeRunId) {
|
|
58
58
|
return `fusion: ${phase} · ${run.profileName} · ${activeRunId}`;
|
package/src/types.ts
CHANGED
|
@@ -45,16 +45,41 @@ export interface JudgeConfig {
|
|
|
45
45
|
thinking?: ThinkingLevel;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export type MinimumSuccessfulPanelists = "majority" | "all" | number;
|
|
49
|
+
|
|
50
|
+
/** Per-run deadline overrides accepted by CLI, tool, and RPC starts. */
|
|
51
|
+
export interface FusionTimeoutOverrides {
|
|
52
|
+
panelistTimeoutMs?: number;
|
|
53
|
+
panelTimeoutMs?: number;
|
|
54
|
+
panelGraceMs?: number;
|
|
55
|
+
judgeTimeoutMs?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface EffectiveFusionTimeouts {
|
|
59
|
+
panelistTimeoutMs: number;
|
|
60
|
+
panelTimeoutMs: number;
|
|
61
|
+
panelGraceMs: number;
|
|
62
|
+
judgeTimeoutMs: number;
|
|
63
|
+
/** True when the legacy shared timeout supplied one or more deadline values. */
|
|
64
|
+
usesLegacyTimeout: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
export interface FusionProfile {
|
|
49
68
|
panel: PanelMemberConfig[];
|
|
50
69
|
judge: JudgeConfig;
|
|
51
70
|
concurrency?: number;
|
|
52
71
|
/** Legacy shared wall-clock timeout used when a stage timeout is absent. */
|
|
53
72
|
timeoutMs?: number;
|
|
73
|
+
/** Per-child deadline. It is capped below the enclosing panel deadline. */
|
|
74
|
+
panelistTimeoutMs?: number;
|
|
54
75
|
/** Wall-clock timeout for the complete panel workflow. */
|
|
55
76
|
panelTimeoutMs?: number;
|
|
77
|
+
/** Time reserved between child and enclosing panel deadlines. */
|
|
78
|
+
panelGraceMs?: number;
|
|
56
79
|
/** Wall-clock timeout for the synthesis workflow. */
|
|
57
80
|
judgeTimeoutMs?: number;
|
|
81
|
+
/** Successful panel outputs required for synthesis. Defaults to a majority. */
|
|
82
|
+
minimumSuccessfulPanelists?: MinimumSuccessfulPanelists;
|
|
58
83
|
context?: FusionContextMode;
|
|
59
84
|
stopWhenPanelAgrees?: boolean;
|
|
60
85
|
/**
|
|
@@ -172,6 +197,7 @@ export interface ParsedFusionArgs {
|
|
|
172
197
|
outputContract?: CallerOutputContract;
|
|
173
198
|
/** Inline panel entries from `--panel`: `<model>` or `<agent>:<model>`. */
|
|
174
199
|
panel?: string[];
|
|
200
|
+
timeoutOverrides?: FusionTimeoutOverrides;
|
|
175
201
|
}
|
|
176
202
|
|
|
177
203
|
export interface PanelOutput {
|
|
@@ -210,6 +236,43 @@ export interface FailedPanelSummary {
|
|
|
210
236
|
export type FusionPhase =
|
|
211
237
|
"panel" | "chain" | "judge" | "done" | "failed" | "cancelled";
|
|
212
238
|
|
|
239
|
+
export type CompletionQuality = "complete" | "partial";
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The start-time settings required after a process restart. This intentionally
|
|
243
|
+
* excludes panel execution-only values (concurrency, panel tool budget, and
|
|
244
|
+
* raw timeout fields): the panel is already running, while resolved deadlines
|
|
245
|
+
* are persisted separately in `effectiveTimeouts`.
|
|
246
|
+
*/
|
|
247
|
+
export interface FusionProfileSnapshot {
|
|
248
|
+
panel: PanelMemberConfig[];
|
|
249
|
+
judge: JudgeConfig;
|
|
250
|
+
minimumSuccessfulPanelists: number;
|
|
251
|
+
context?: FusionContextMode;
|
|
252
|
+
stopWhenPanelAgrees?: boolean;
|
|
253
|
+
blindPanelLabels?: boolean;
|
|
254
|
+
judgeToolBudget?: ToolBudget;
|
|
255
|
+
synthesis?: FusionSynthesisMode;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Persisted recovery metadata for a terminal run. Failed-only retry is not
|
|
259
|
+
* yet exposed, so this records exactly which slots a future explicit retry may
|
|
260
|
+
* safely target without replaying verified completed work. */
|
|
261
|
+
export interface FusionRecoveryState {
|
|
262
|
+
retryDeferred: true;
|
|
263
|
+
failedPanelIndices: number[];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Durable record written before a public RPC spawn. The public API cannot
|
|
268
|
+
* query by this correlation token, so a restored intent without its returned
|
|
269
|
+
* run ID is deliberately failed instead of risking a duplicate remote run.
|
|
270
|
+
*/
|
|
271
|
+
export interface FusionSpawnIntent {
|
|
272
|
+
stage: "panel" | "judge";
|
|
273
|
+
requestedAt: number;
|
|
274
|
+
}
|
|
275
|
+
|
|
213
276
|
export interface FusionRun {
|
|
214
277
|
id: string;
|
|
215
278
|
prompt: string;
|
|
@@ -224,6 +287,16 @@ export interface FusionRun {
|
|
|
224
287
|
baseProfileName?: string;
|
|
225
288
|
operationId?: string;
|
|
226
289
|
outputContract?: CallerOutputContract;
|
|
290
|
+
/** Persisted start policy so recovery is not changed by later config edits. */
|
|
291
|
+
minimumSuccessfulPanelists?: MinimumSuccessfulPanelists;
|
|
292
|
+
/**
|
|
293
|
+
* Additive start-time profile snapshot. Old sessions omit it and retain the
|
|
294
|
+
* legacy config lookup fallback during restore.
|
|
295
|
+
*/
|
|
296
|
+
profileSnapshot?: FusionProfileSnapshot;
|
|
297
|
+
timeoutOverrides?: FusionTimeoutOverrides;
|
|
298
|
+
effectiveTimeouts?: EffectiveFusionTimeouts;
|
|
299
|
+
completionQuality?: CompletionQuality;
|
|
227
300
|
phase: FusionPhase;
|
|
228
301
|
createdAt: number;
|
|
229
302
|
updatedAt: number;
|
|
@@ -238,6 +311,8 @@ export interface FusionRun {
|
|
|
238
311
|
judgeObservation?: RunObservation;
|
|
239
312
|
panelOutputs?: PanelOutput[];
|
|
240
313
|
panelFailures?: FailedPanelSummary[];
|
|
314
|
+
recovery?: FusionRecoveryState;
|
|
315
|
+
spawnIntent?: FusionSpawnIntent;
|
|
241
316
|
report?: string;
|
|
242
317
|
error?: string;
|
|
243
318
|
}
|