@alexeiled/pi-fusion 0.7.0 → 0.9.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 +8 -3
- package/agents/fusion-panelist-full.md +3 -1
- package/agents/fusion-panelist-web.md +3 -1
- package/agents/fusion-panelist.md +3 -1
- package/docs/user-guide.md +56 -12
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/commands.ts +17 -0
- package/src/config.ts +27 -0
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +29 -0
- package/src/index.ts +50 -7
- package/src/lifecycle-reconcile.ts +234 -7
- package/src/orchestrator.ts +622 -79
- package/src/panel-completion.ts +65 -17
- package/src/panel-deadlines.ts +90 -0
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +85 -3
- package/src/result-extract.ts +67 -5
- package/src/run-builder.ts +129 -9
- package/src/run-observations.ts +3 -1
- package/src/run-store.ts +385 -11
- package/src/status.ts +1 -1
- package/src/subagents-rpc.ts +10 -0
- package/src/types.ts +89 -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
|
}
|
|
@@ -57,11 +66,16 @@ export interface FusionRunPatch {
|
|
|
57
66
|
panelAsyncDir?: string;
|
|
58
67
|
panelStopReason?: FusionRun["panelStopReason"];
|
|
59
68
|
panelStoppedIndices?: FusionRun["panelStoppedIndices"];
|
|
69
|
+
panelDeadlines?: FusionRun["panelDeadlines"];
|
|
60
70
|
judgeRunId?: string;
|
|
61
71
|
judgeAsyncDir?: string;
|
|
62
72
|
judgeObservation?: FusionRun["judgeObservation"];
|
|
73
|
+
completionQuality?: FusionRun["completionQuality"];
|
|
63
74
|
panelOutputs?: FusionRun["panelOutputs"];
|
|
64
75
|
panelFailures?: FusionRun["panelFailures"];
|
|
76
|
+
recovery?: FusionRun["recovery"];
|
|
77
|
+
/** `null` clears an intent once the RPC returned its durable remote ID. */
|
|
78
|
+
spawnIntent?: FusionRun["spawnIntent"] | null;
|
|
65
79
|
report?: string;
|
|
66
80
|
error?: string;
|
|
67
81
|
updatedAt?: number;
|
|
@@ -71,6 +85,8 @@ export interface FusionRunTransitionPatch {
|
|
|
71
85
|
chainRunId?: string;
|
|
72
86
|
panelRunId?: string;
|
|
73
87
|
judgeRunId?: string;
|
|
88
|
+
recovery?: FusionRun["recovery"];
|
|
89
|
+
spawnIntent?: FusionRun["spawnIntent"];
|
|
74
90
|
report?: string;
|
|
75
91
|
error?: string;
|
|
76
92
|
updatedAt?: number;
|
|
@@ -107,6 +123,7 @@ export class FusionRunStore {
|
|
|
107
123
|
private readonly now: () => number;
|
|
108
124
|
private readonly idFactory: () => string;
|
|
109
125
|
private readonly persistence: FusionRunStorePersistence | undefined;
|
|
126
|
+
private restoreError: string | undefined;
|
|
110
127
|
|
|
111
128
|
constructor(options: FusionRunStoreOptions = {}) {
|
|
112
129
|
this.now = options.now ?? Date.now;
|
|
@@ -134,6 +151,11 @@ export class FusionRunStore {
|
|
|
134
151
|
return runId ? this.getRunById(runId) : undefined;
|
|
135
152
|
}
|
|
136
153
|
|
|
154
|
+
/** A corrupt newest snapshot must never revive an older active run. */
|
|
155
|
+
getRestoreError(): string | undefined {
|
|
156
|
+
return this.restoreError;
|
|
157
|
+
}
|
|
158
|
+
|
|
137
159
|
startRun(input: FusionRunStartInput): FusionRun {
|
|
138
160
|
if (this.activeRun) {
|
|
139
161
|
throw new FusionRunStoreError(
|
|
@@ -167,13 +189,25 @@ export class FusionRunStore {
|
|
|
167
189
|
...(input.outputContract !== undefined
|
|
168
190
|
? { outputContract: input.outputContract }
|
|
169
191
|
: {}),
|
|
192
|
+
...(input.minimumSuccessfulPanelists !== undefined
|
|
193
|
+
? { minimumSuccessfulPanelists: input.minimumSuccessfulPanelists }
|
|
194
|
+
: {}),
|
|
195
|
+
...(input.profileSnapshot !== undefined
|
|
196
|
+
? { profileSnapshot: cloneProfileSnapshot(input.profileSnapshot) }
|
|
197
|
+
: {}),
|
|
198
|
+
...(input.timeoutOverrides !== undefined
|
|
199
|
+
? { timeoutOverrides: { ...input.timeoutOverrides } }
|
|
200
|
+
: {}),
|
|
201
|
+
...(input.effectiveTimeouts !== undefined
|
|
202
|
+
? { effectiveTimeouts: { ...input.effectiveTimeouts } }
|
|
203
|
+
: {}),
|
|
170
204
|
phase: input.phase ?? "chain",
|
|
171
205
|
createdAt,
|
|
172
206
|
updatedAt: createdAt,
|
|
173
207
|
};
|
|
208
|
+
this.persistRun(run);
|
|
174
209
|
this.activeRun = run;
|
|
175
210
|
this.rememberRun(run);
|
|
176
|
-
this.persistRun(run);
|
|
177
211
|
return cloneRun(run);
|
|
178
212
|
}
|
|
179
213
|
|
|
@@ -221,11 +255,26 @@ export class FusionRunStore {
|
|
|
221
255
|
restoreFromEntries(
|
|
222
256
|
entries: readonly unknown[],
|
|
223
257
|
): FusionRunSummary | undefined {
|
|
224
|
-
|
|
258
|
+
this.restoreError = undefined;
|
|
225
259
|
this.runsById.clear();
|
|
226
260
|
this.runIdsByOperationId.clear();
|
|
227
|
-
for (const state of states) this.rememberRun(state);
|
|
228
261
|
|
|
262
|
+
const latestPersisted = lastFusionRunEnvelope(entries);
|
|
263
|
+
if (
|
|
264
|
+
latestPersisted &&
|
|
265
|
+
(!isFusionRunEntry(latestPersisted) || !isFusionRunState(latestPersisted.data))
|
|
266
|
+
) {
|
|
267
|
+
// History readers may skip malformed entries, but restore must not adopt
|
|
268
|
+
// an older active state after a newer snapshot was corrupted.
|
|
269
|
+
this.activeRun = undefined;
|
|
270
|
+
this.lastRunSummary = undefined;
|
|
271
|
+
this.restoreError =
|
|
272
|
+
"Latest persisted fusion run snapshot is invalid; refusing stale active-run recovery.";
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const states = readFusionRunStates(entries);
|
|
277
|
+
for (const state of states) this.rememberRun(state);
|
|
229
278
|
const latestState = states.at(-1);
|
|
230
279
|
const summary = readLastFusionRunSummary(entries);
|
|
231
280
|
this.activeRun =
|
|
@@ -339,6 +388,7 @@ function applyPatch(
|
|
|
339
388
|
if (patch.panelStoppedIndices !== undefined) {
|
|
340
389
|
updated.panelStoppedIndices = [...patch.panelStoppedIndices];
|
|
341
390
|
}
|
|
391
|
+
if (patch.panelDeadlines !== undefined) updated.panelDeadlines = patch.panelDeadlines.map((item) => ({ ...item }));
|
|
342
392
|
if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
|
|
343
393
|
if (patch.judgeAsyncDir !== undefined) {
|
|
344
394
|
updated.judgeAsyncDir = patch.judgeAsyncDir;
|
|
@@ -346,12 +396,20 @@ function applyPatch(
|
|
|
346
396
|
if (patch.judgeObservation !== undefined) {
|
|
347
397
|
updated.judgeObservation = cloneObservation(patch.judgeObservation);
|
|
348
398
|
}
|
|
399
|
+
if (patch.completionQuality !== undefined) {
|
|
400
|
+
updated.completionQuality = patch.completionQuality;
|
|
401
|
+
}
|
|
349
402
|
if (patch.panelOutputs !== undefined) {
|
|
350
403
|
updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
|
|
351
404
|
}
|
|
352
405
|
if (patch.panelFailures !== undefined) {
|
|
353
406
|
updated.panelFailures = clonePanelFailures(patch.panelFailures);
|
|
354
407
|
}
|
|
408
|
+
if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
|
|
409
|
+
if (patch.spawnIntent === null) delete updated.spawnIntent;
|
|
410
|
+
else if (patch.spawnIntent !== undefined) {
|
|
411
|
+
updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
|
|
412
|
+
}
|
|
355
413
|
if (patch.report !== undefined) updated.report = patch.report;
|
|
356
414
|
if (patch.error !== undefined) updated.error = patch.error;
|
|
357
415
|
return updated;
|
|
@@ -371,6 +429,10 @@ function applyTransitionPatch(
|
|
|
371
429
|
if (patch.chainRunId !== undefined) updated.chainRunId = patch.chainRunId;
|
|
372
430
|
if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
|
|
373
431
|
if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
|
|
432
|
+
if (patch.recovery !== undefined) updated.recovery = cloneRecovery(patch.recovery);
|
|
433
|
+
if (patch.spawnIntent !== undefined) {
|
|
434
|
+
updated.spawnIntent = cloneSpawnIntent(patch.spawnIntent);
|
|
435
|
+
}
|
|
374
436
|
if (patch.report !== undefined) updated.report = patch.report;
|
|
375
437
|
if (patch.error !== undefined) updated.error = patch.error;
|
|
376
438
|
return updated;
|
|
@@ -387,6 +449,13 @@ function toRunSummary(
|
|
|
387
449
|
...(run.outputContract !== undefined
|
|
388
450
|
? { outputContract: run.outputContract }
|
|
389
451
|
: {}),
|
|
452
|
+
...(run.completionQuality !== undefined
|
|
453
|
+
? { completionQuality: run.completionQuality }
|
|
454
|
+
: {}),
|
|
455
|
+
...(run.effectiveTimeouts !== undefined
|
|
456
|
+
? { effectiveTimeouts: { ...run.effectiveTimeouts } }
|
|
457
|
+
: {}),
|
|
458
|
+
...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
|
|
390
459
|
phase: run.phase,
|
|
391
460
|
createdAt: run.createdAt,
|
|
392
461
|
updatedAt: run.updatedAt,
|
|
@@ -415,6 +484,21 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
415
484
|
...(run.outputContract !== undefined
|
|
416
485
|
? { outputContract: run.outputContract }
|
|
417
486
|
: {}),
|
|
487
|
+
...(run.minimumSuccessfulPanelists !== undefined
|
|
488
|
+
? { minimumSuccessfulPanelists: run.minimumSuccessfulPanelists }
|
|
489
|
+
: {}),
|
|
490
|
+
...(run.profileSnapshot !== undefined
|
|
491
|
+
? { profileSnapshot: cloneProfileSnapshot(run.profileSnapshot) }
|
|
492
|
+
: {}),
|
|
493
|
+
...(run.timeoutOverrides !== undefined
|
|
494
|
+
? { timeoutOverrides: { ...run.timeoutOverrides } }
|
|
495
|
+
: {}),
|
|
496
|
+
...(run.effectiveTimeouts !== undefined
|
|
497
|
+
? { effectiveTimeouts: { ...run.effectiveTimeouts } }
|
|
498
|
+
: {}),
|
|
499
|
+
...(run.completionQuality !== undefined
|
|
500
|
+
? { completionQuality: run.completionQuality }
|
|
501
|
+
: {}),
|
|
418
502
|
phase: run.phase,
|
|
419
503
|
createdAt: run.createdAt,
|
|
420
504
|
updatedAt: run.updatedAt,
|
|
@@ -432,6 +516,9 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
432
516
|
...(run.panelStoppedIndices !== undefined
|
|
433
517
|
? { panelStoppedIndices: [...run.panelStoppedIndices] }
|
|
434
518
|
: {}),
|
|
519
|
+
...(run.panelDeadlines !== undefined
|
|
520
|
+
? { panelDeadlines: run.panelDeadlines.map((item) => ({ ...item })) }
|
|
521
|
+
: {}),
|
|
435
522
|
...(run.judgeRunId !== undefined ? { judgeRunId: run.judgeRunId } : {}),
|
|
436
523
|
...(run.judgeAsyncDir !== undefined
|
|
437
524
|
? { judgeAsyncDir: run.judgeAsyncDir }
|
|
@@ -445,6 +532,10 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
445
532
|
...(run.panelFailures !== undefined
|
|
446
533
|
? { panelFailures: clonePanelFailures(run.panelFailures) }
|
|
447
534
|
: {}),
|
|
535
|
+
...(run.recovery !== undefined ? { recovery: cloneRecovery(run.recovery) } : {}),
|
|
536
|
+
...(run.spawnIntent !== undefined
|
|
537
|
+
? { spawnIntent: cloneSpawnIntent(run.spawnIntent) }
|
|
538
|
+
: {}),
|
|
448
539
|
...(run.report !== undefined ? { report: run.report } : {}),
|
|
449
540
|
...(run.error !== undefined ? { error: run.error } : {}),
|
|
450
541
|
};
|
|
@@ -454,17 +545,30 @@ function cloneRunSummary(summary: FusionRunSummary): FusionRunSummary {
|
|
|
454
545
|
return toRunSummary(summary);
|
|
455
546
|
}
|
|
456
547
|
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
548
|
+
function lastFusionRunEnvelope(entries: readonly unknown[]): unknown {
|
|
549
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
550
|
+
const entry = entries[index];
|
|
551
|
+
// Locate the newest entry by its envelope before inspecting data. A
|
|
552
|
+
// malformed data field (or its absence) must not revive an older run.
|
|
553
|
+
if (isFusionRunEnvelope(entry)) return entry;
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function isFusionRunEnvelope(value: unknown): value is Record<string, unknown> {
|
|
460
559
|
return (
|
|
461
560
|
isRecord(value) &&
|
|
462
561
|
value.type === "custom" &&
|
|
463
|
-
value.customType === FUSION_RUN_ENTRY_TYPE
|
|
464
|
-
"data" in value
|
|
562
|
+
value.customType === FUSION_RUN_ENTRY_TYPE
|
|
465
563
|
);
|
|
466
564
|
}
|
|
467
565
|
|
|
566
|
+
function isFusionRunEntry(
|
|
567
|
+
value: unknown,
|
|
568
|
+
): value is { type: "custom"; customType: string; data: unknown } {
|
|
569
|
+
return isFusionRunEnvelope(value) && "data" in value;
|
|
570
|
+
}
|
|
571
|
+
|
|
468
572
|
function isFusionRunState(value: unknown): value is FusionRun {
|
|
469
573
|
if (!isRecord(value)) return false;
|
|
470
574
|
if (!isNonEmptyString(value.id)) return false;
|
|
@@ -479,6 +583,47 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
479
583
|
) {
|
|
480
584
|
return false;
|
|
481
585
|
}
|
|
586
|
+
if (
|
|
587
|
+
value.minimumSuccessfulPanelists !== undefined &&
|
|
588
|
+
value.minimumSuccessfulPanelists !== "majority" &&
|
|
589
|
+
value.minimumSuccessfulPanelists !== "all" &&
|
|
590
|
+
(!isFiniteNumber(value.minimumSuccessfulPanelists) ||
|
|
591
|
+
!Number.isInteger(value.minimumSuccessfulPanelists) ||
|
|
592
|
+
value.minimumSuccessfulPanelists < 1)
|
|
593
|
+
) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
if (
|
|
597
|
+
value.profileSnapshot !== undefined &&
|
|
598
|
+
!isProfileSnapshot(value.profileSnapshot)
|
|
599
|
+
) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
// Snapshots are the canonical quorum record for new runs. Older records may
|
|
603
|
+
// also carry the run-level policy, but it must resolve to the same quorum.
|
|
604
|
+
if (
|
|
605
|
+
value.profileSnapshot !== undefined &&
|
|
606
|
+
value.minimumSuccessfulPanelists !== undefined &&
|
|
607
|
+
resolvePersistedQuorum(
|
|
608
|
+
value.minimumSuccessfulPanelists,
|
|
609
|
+
value.profileSnapshot.panel.length,
|
|
610
|
+
) !== value.profileSnapshot.minimumSuccessfulPanelists
|
|
611
|
+
) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
if (value.timeoutOverrides !== undefined && !isTimeoutOverrides(value.timeoutOverrides)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
if (value.effectiveTimeouts !== undefined && !isEffectiveTimeouts(value.effectiveTimeouts)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
if (
|
|
621
|
+
value.completionQuality !== undefined &&
|
|
622
|
+
value.completionQuality !== "complete" &&
|
|
623
|
+
value.completionQuality !== "partial"
|
|
624
|
+
) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
482
627
|
if (
|
|
483
628
|
value.inlinePanel !== undefined &&
|
|
484
629
|
(!Array.isArray(value.inlinePanel) ||
|
|
@@ -529,6 +674,7 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
529
674
|
) {
|
|
530
675
|
return false;
|
|
531
676
|
}
|
|
677
|
+
if (value.panelDeadlines !== undefined && !isPanelDeadlines(value.panelDeadlines)) return false;
|
|
532
678
|
if (value.judgeRunId !== undefined && typeof value.judgeRunId !== "string") {
|
|
533
679
|
return false;
|
|
534
680
|
}
|
|
@@ -556,19 +702,69 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
556
702
|
) {
|
|
557
703
|
return false;
|
|
558
704
|
}
|
|
705
|
+
if (value.recovery !== undefined && !isRecoveryState(value.recovery)) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
if (value.spawnIntent !== undefined && !isSpawnIntent(value.spawnIntent)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
559
711
|
if (value.report !== undefined && typeof value.report !== "string") {
|
|
560
712
|
return false;
|
|
561
713
|
}
|
|
562
714
|
if (value.error !== undefined && typeof value.error !== "string") {
|
|
563
715
|
return false;
|
|
564
716
|
}
|
|
565
|
-
return
|
|
717
|
+
return (
|
|
718
|
+
value.profileSnapshot === undefined ||
|
|
719
|
+
validateFusionRunPanelSlots(value, value.profileSnapshot.panel.length) ===
|
|
720
|
+
undefined
|
|
721
|
+
);
|
|
566
722
|
}
|
|
567
723
|
|
|
568
724
|
function isFusionRunSummary(value: unknown): value is FusionRunSummary {
|
|
569
725
|
return isFusionRunState(value) && isTerminalPhase(value.phase);
|
|
570
726
|
}
|
|
571
727
|
|
|
728
|
+
function isPanelDeadlines(value: unknown): boolean {
|
|
729
|
+
if (!Array.isArray(value)) return false;
|
|
730
|
+
const slots = new Set<number>();
|
|
731
|
+
const children = new Set<string>();
|
|
732
|
+
return value.every((item: unknown) => {
|
|
733
|
+
if (!isRecord(item) || !isPanelSlotIndex(item.index) || !isNonEmptyString(item.childRunId) ||
|
|
734
|
+
!isFiniteNumber(item.requestedAt) || !isFiniteNumber(item.finalizeAt) || !isFiniteNumber(item.hardDeadlineAt) ||
|
|
735
|
+
item.finalizeAt >= item.hardDeadlineAt ||
|
|
736
|
+
!["pending", "continued", "finishing"].includes(String(item.status)) ||
|
|
737
|
+
(item.deliveryError !== undefined && typeof item.deliveryError !== "string") ||
|
|
738
|
+
slots.has(item.index) || children.has(item.childRunId)) return false;
|
|
739
|
+
slots.add(item.index);
|
|
740
|
+
children.add(item.childRunId);
|
|
741
|
+
return true;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function isTimeoutOverrides(value: unknown): boolean {
|
|
746
|
+
if (!isRecord(value)) return false;
|
|
747
|
+
return [
|
|
748
|
+
value.panelistSoftTimeoutMs,
|
|
749
|
+
value.panelistTimeoutMs,
|
|
750
|
+
value.panelTimeoutMs,
|
|
751
|
+
value.panelGraceMs,
|
|
752
|
+
value.judgeTimeoutMs,
|
|
753
|
+
].every((timeout) => timeout === undefined || (isFiniteNumber(timeout) && Number.isInteger(timeout) && timeout > 0));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function isEffectiveTimeouts(value: unknown): boolean {
|
|
757
|
+
return (
|
|
758
|
+
isTimeoutOverrides(value) &&
|
|
759
|
+
isRecord(value) &&
|
|
760
|
+
isFiniteNumber(value.panelistTimeoutMs) &&
|
|
761
|
+
isFiniteNumber(value.panelTimeoutMs) &&
|
|
762
|
+
isFiniteNumber(value.panelGraceMs) &&
|
|
763
|
+
isFiniteNumber(value.judgeTimeoutMs) &&
|
|
764
|
+
typeof value.usesLegacyTimeout === "boolean"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
572
768
|
function isFusionPhase(value: unknown): value is FusionPhase {
|
|
573
769
|
return (
|
|
574
770
|
value === "panel" ||
|
|
@@ -584,6 +780,46 @@ function isTerminalPhase(value: unknown): value is FusionTerminalPhase {
|
|
|
584
780
|
return value === "done" || value === "failed" || value === "cancelled";
|
|
585
781
|
}
|
|
586
782
|
|
|
783
|
+
/**
|
|
784
|
+
* Checks persisted slot references before they are restored or merged. Legacy
|
|
785
|
+
* runs omit a profile snapshot, so their caller supplies the safely resolved
|
|
786
|
+
* profile length during restore.
|
|
787
|
+
*/
|
|
788
|
+
export function validateFusionRunPanelSlots(
|
|
789
|
+
run: Pick<
|
|
790
|
+
FusionRun,
|
|
791
|
+
"panelOutputs" | "panelFailures" | "panelStoppedIndices" | "panelDeadlines" | "recovery"
|
|
792
|
+
>,
|
|
793
|
+
panelLength: number,
|
|
794
|
+
): string | undefined {
|
|
795
|
+
const slotGroups: ReadonlyArray<readonly number[] | undefined> = [
|
|
796
|
+
run.panelOutputs?.map((output) => output.index),
|
|
797
|
+
run.panelFailures?.map((failure) => failure.index),
|
|
798
|
+
run.panelStoppedIndices,
|
|
799
|
+
run.panelDeadlines?.map((item) => item.index),
|
|
800
|
+
run.recovery?.failedPanelIndices,
|
|
801
|
+
];
|
|
802
|
+
for (const slots of slotGroups) {
|
|
803
|
+
for (const slot of slots ?? []) {
|
|
804
|
+
if (!isPanelSlotIndex(slot) || slot >= panelLength) {
|
|
805
|
+
return `Persisted panel slot ${String(slot)} is outside the configured panel.`;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function resolvePersistedQuorum(
|
|
813
|
+
policy: NonNullable<FusionRun["minimumSuccessfulPanelists"]>,
|
|
814
|
+
panelLength: number,
|
|
815
|
+
): number {
|
|
816
|
+
if (policy === "all") return panelLength;
|
|
817
|
+
if (typeof policy === "number") {
|
|
818
|
+
return panelLength > 1 ? Math.max(2, Math.min(policy, panelLength)) : 1;
|
|
819
|
+
}
|
|
820
|
+
return Math.ceil(panelLength / 2);
|
|
821
|
+
}
|
|
822
|
+
|
|
587
823
|
function isPanelOutputArray(
|
|
588
824
|
value: unknown,
|
|
589
825
|
): value is NonNullable<FusionRun["panelOutputs"]> {
|
|
@@ -595,7 +831,7 @@ function isPanelOutput(
|
|
|
595
831
|
): value is NonNullable<FusionRun["panelOutputs"]>[number] {
|
|
596
832
|
return (
|
|
597
833
|
isRecord(value) &&
|
|
598
|
-
|
|
834
|
+
isPanelSlotIndex(value.index) &&
|
|
599
835
|
isNonEmptyString(value.agent) &&
|
|
600
836
|
typeof value.output === "string" &&
|
|
601
837
|
(value.id === undefined || typeof value.id === "string") &&
|
|
@@ -622,7 +858,7 @@ function isPanelFailure(
|
|
|
622
858
|
): value is NonNullable<FusionRun["panelFailures"]>[number] {
|
|
623
859
|
return (
|
|
624
860
|
isRecord(value) &&
|
|
625
|
-
|
|
861
|
+
isPanelSlotIndex(value.index) &&
|
|
626
862
|
isNonEmptyString(value.agent) &&
|
|
627
863
|
typeof value.summary === "string" &&
|
|
628
864
|
(value.id === undefined || typeof value.id === "string") &&
|
|
@@ -638,6 +874,144 @@ function isPanelFailure(
|
|
|
638
874
|
);
|
|
639
875
|
}
|
|
640
876
|
|
|
877
|
+
function isProfileSnapshot(value: unknown): value is FusionProfileSnapshot {
|
|
878
|
+
if (!isRecord(value) || !Array.isArray(value.panel) || value.panel.length === 0) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
if (!value.panel.every(isSnapshotPanelMember) || !isSnapshotJudge(value.judge)) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (
|
|
885
|
+
!isFiniteNumber(value.minimumSuccessfulPanelists) ||
|
|
886
|
+
!Number.isInteger(value.minimumSuccessfulPanelists) ||
|
|
887
|
+
value.minimumSuccessfulPanelists < 1 ||
|
|
888
|
+
value.minimumSuccessfulPanelists > value.panel.length
|
|
889
|
+
) {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
if (value.context !== undefined && value.context !== "fresh" && value.context !== "fork") {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
if (value.stopWhenPanelAgrees !== undefined && typeof value.stopWhenPanelAgrees !== "boolean") {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
if (value.blindPanelLabels !== undefined && typeof value.blindPanelLabels !== "boolean") {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
if (value.synthesis !== undefined && value.synthesis !== "select" && value.synthesis !== "merge") {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
return value.judgeToolBudget === undefined || isSnapshotToolBudget(value.judgeToolBudget);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function isSnapshotPanelMember(value: unknown): boolean {
|
|
908
|
+
if (!isRecord(value) || !isNonEmptyString(value.id) || !isSnapshotAgent(value.agent)) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
return (
|
|
912
|
+
(value.label === undefined || isNonEmptyString(value.label)) &&
|
|
913
|
+
(value.model === undefined || isNonEmptyString(value.model)) &&
|
|
914
|
+
(value.thinking === undefined || isThinkingLevel(value.thinking)) &&
|
|
915
|
+
(value.role === undefined || typeof value.role === "string") &&
|
|
916
|
+
(value.question === undefined || isNonEmptyString(value.question))
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function isSnapshotJudge(value: unknown): boolean {
|
|
921
|
+
return (
|
|
922
|
+
isRecord(value) &&
|
|
923
|
+
isSnapshotAgent(value.agent) &&
|
|
924
|
+
(value.model === undefined || isNonEmptyString(value.model)) &&
|
|
925
|
+
(value.thinking === undefined || isThinkingLevel(value.thinking))
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function isSnapshotAgent(value: unknown): boolean {
|
|
930
|
+
return (
|
|
931
|
+
isNonEmptyString(value) &&
|
|
932
|
+
/^[^\s.]+(?:\.[^\s.]+)*$/.test(value.trim())
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function isThinkingLevel(value: unknown): boolean {
|
|
937
|
+
return value === "off" || value === "minimal" || value === "low" ||
|
|
938
|
+
value === "medium" || value === "high" || value === "xhigh";
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function isSnapshotToolBudget(value: unknown): boolean {
|
|
942
|
+
if (!isRecord(value) || (value.soft === undefined && value.hard === undefined)) {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
if (value.soft !== undefined && (!isFiniteNumber(value.soft) || !Number.isInteger(value.soft) || value.soft < 1)) {
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
if (value.hard !== undefined && (!isFiniteNumber(value.hard) || !Number.isInteger(value.hard) || value.hard < 1)) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (typeof value.soft === "number" && typeof value.hard === "number" && value.soft > value.hard) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
return value.block === undefined || value.block === "*" ||
|
|
955
|
+
(Array.isArray(value.block) && value.block.length > 0 && value.block.every(isNonEmptyString));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function cloneProfileSnapshot(snapshot: FusionProfileSnapshot): FusionProfileSnapshot {
|
|
959
|
+
return {
|
|
960
|
+
panel: snapshot.panel.map((member) => ({ ...member })),
|
|
961
|
+
judge: { ...snapshot.judge },
|
|
962
|
+
minimumSuccessfulPanelists: snapshot.minimumSuccessfulPanelists,
|
|
963
|
+
...(snapshot.context !== undefined ? { context: snapshot.context } : {}),
|
|
964
|
+
...(snapshot.stopWhenPanelAgrees !== undefined
|
|
965
|
+
? { stopWhenPanelAgrees: snapshot.stopWhenPanelAgrees }
|
|
966
|
+
: {}),
|
|
967
|
+
...(snapshot.blindPanelLabels !== undefined
|
|
968
|
+
? { blindPanelLabels: snapshot.blindPanelLabels }
|
|
969
|
+
: {}),
|
|
970
|
+
...(snapshot.judgeToolBudget !== undefined
|
|
971
|
+
? {
|
|
972
|
+
judgeToolBudget: {
|
|
973
|
+
...snapshot.judgeToolBudget,
|
|
974
|
+
...(Array.isArray(snapshot.judgeToolBudget.block)
|
|
975
|
+
? { block: [...snapshot.judgeToolBudget.block] }
|
|
976
|
+
: {}),
|
|
977
|
+
},
|
|
978
|
+
}
|
|
979
|
+
: {}),
|
|
980
|
+
...(snapshot.synthesis !== undefined ? { synthesis: snapshot.synthesis } : {}),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function isRecoveryState(value: unknown): value is FusionRecoveryState {
|
|
985
|
+
return (
|
|
986
|
+
isRecord(value) &&
|
|
987
|
+
value.retryDeferred === true &&
|
|
988
|
+
Array.isArray(value.failedPanelIndices) &&
|
|
989
|
+
value.failedPanelIndices.every(isPanelSlotIndex)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function isPanelSlotIndex(value: unknown): value is number {
|
|
994
|
+
return isFiniteNumber(value) && Number.isInteger(value) && value >= 0;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function cloneRecovery(recovery: FusionRecoveryState): FusionRecoveryState {
|
|
998
|
+
return { ...recovery, failedPanelIndices: [...recovery.failedPanelIndices] };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function isSpawnIntent(value: unknown): value is FusionRun["spawnIntent"] {
|
|
1002
|
+
return (
|
|
1003
|
+
isRecord(value) &&
|
|
1004
|
+
(value.stage === "panel" || value.stage === "judge") &&
|
|
1005
|
+
isFiniteNumber(value.requestedAt)
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function cloneSpawnIntent(
|
|
1010
|
+
intent: NonNullable<FusionRun["spawnIntent"]>,
|
|
1011
|
+
): NonNullable<FusionRun["spawnIntent"]> {
|
|
1012
|
+
return { ...intent };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
641
1015
|
function clonePanelOutputs(
|
|
642
1016
|
outputs: NonNullable<FusionRun["panelOutputs"]>,
|
|
643
1017
|
): 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/subagents-rpc.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const SUBAGENTS_RPC_METHODS = [
|
|
|
11
11
|
"status",
|
|
12
12
|
"stop",
|
|
13
13
|
"interrupt",
|
|
14
|
+
"steer",
|
|
14
15
|
] as const;
|
|
15
16
|
|
|
16
17
|
export type SubagentsRpcMethod = (typeof SUBAGENTS_RPC_METHODS)[number];
|
|
@@ -42,6 +43,11 @@ export interface SubagentsTargetParams {
|
|
|
42
43
|
index?: number;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface SubagentsSteerParams extends SubagentsTargetParams {
|
|
47
|
+
message: string;
|
|
48
|
+
mode: "auto";
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export type SubagentsSpawnParams = object;
|
|
46
52
|
|
|
47
53
|
export interface SubagentsRpcRequestEnvelope {
|
|
@@ -145,6 +151,10 @@ export class SubagentsRpcClient {
|
|
|
145
151
|
this.source = options.source ?? { extension: "pi-fusion" };
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
steer(params: SubagentsSteerParams): Promise<unknown> {
|
|
155
|
+
return this.request("steer", params);
|
|
156
|
+
}
|
|
157
|
+
|
|
148
158
|
request<T = unknown>(
|
|
149
159
|
method: SubagentsRpcMethod,
|
|
150
160
|
params?: unknown,
|