@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/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
- const states = readFusionRunStates(entries);
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 isFusionRunEntry(
458
- value: unknown,
459
- ): value is { type: "custom"; customType: string; data: unknown } {
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 true;
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
- isFiniteNumber(value.index) &&
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
- isFiniteNumber(value.index) &&
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: panel · ${formatProgressCounts(progress)} · ${run.profileName}`;
55
+ return `fusion: ${phase} · ${formatProgressCounts(progress)} · ${run.profileName}`;
56
56
  }
57
57
  if (activeRunId) {
58
58
  return `fusion: ${phase} · ${run.profileName} · ${activeRunId}`;
@@ -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,