@alexeiled/pi-fusion 0.3.1 → 0.5.1

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,5 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { FusionPhase, FusionRun } from "./types.js";
2
+ import type {
3
+ FusionPhase,
4
+ FusionRun,
5
+ ModelAttempt,
6
+ PanelDecision,
7
+ ProviderFailure,
8
+ RunObservation,
9
+ RunUsage,
10
+ } from "./types.js";
3
11
  import { isFiniteNumber, isNonEmptyString, isRecord } from "./utils.js";
4
12
 
5
13
  export const FUSION_RUN_ENTRY_TYPE = "fusion-run";
@@ -15,6 +23,7 @@ export type FusionRunSummary = Omit<
15
23
  | "id"
16
24
  | "prompt"
17
25
  | "profileName"
26
+ | "operationId"
18
27
  | "phase"
19
28
  | "createdAt"
20
29
  | "updatedAt"
@@ -31,6 +40,7 @@ export interface FusionRunStartInput {
31
40
  id?: string;
32
41
  prompt: string;
33
42
  profileName: string;
43
+ operationId?: string;
34
44
  phase?: Exclude<FusionPhase, FusionTerminalPhase>;
35
45
  createdAt?: number;
36
46
  }
@@ -40,8 +50,12 @@ export interface FusionRunPatch {
40
50
  chainRunId?: string;
41
51
  chainAsyncDir?: string;
42
52
  panelRunId?: string;
53
+ panelAsyncDir?: string;
54
+ panelStopReason?: FusionRun["panelStopReason"];
55
+ panelStoppedIndices?: FusionRun["panelStoppedIndices"];
43
56
  judgeRunId?: string;
44
57
  judgeAsyncDir?: string;
58
+ judgeObservation?: FusionRun["judgeObservation"];
45
59
  panelOutputs?: FusionRun["panelOutputs"];
46
60
  panelFailures?: FusionRun["panelFailures"];
47
61
  report?: string;
@@ -84,6 +98,8 @@ export class FusionRunStoreError extends Error {
84
98
  export class FusionRunStore {
85
99
  private activeRun: FusionRun | undefined;
86
100
  private lastRunSummary: FusionRunSummary | undefined;
101
+ private readonly runsById = new Map<string, FusionRun>();
102
+ private readonly runIdsByOperationId = new Map<string, string>();
87
103
  private readonly now: () => number;
88
104
  private readonly idFactory: () => string;
89
105
  private readonly persistence: FusionRunStorePersistence | undefined;
@@ -104,22 +120,44 @@ export class FusionRunStore {
104
120
  : undefined;
105
121
  }
106
122
 
123
+ getRunById(id: string): FusionRun | undefined {
124
+ const run = this.runsById.get(id);
125
+ return run ? cloneRun(run) : undefined;
126
+ }
127
+
128
+ getRunByOperationId(operationId: string): FusionRun | undefined {
129
+ const runId = this.runIdsByOperationId.get(operationId);
130
+ return runId ? this.getRunById(runId) : undefined;
131
+ }
132
+
107
133
  startRun(input: FusionRunStartInput): FusionRun {
108
134
  if (this.activeRun) {
109
135
  throw new FusionRunStoreError(
110
136
  `Fusion run ${this.activeRun.id} is already active.`,
111
137
  );
112
138
  }
139
+ if (
140
+ input.operationId !== undefined &&
141
+ this.runIdsByOperationId.has(input.operationId)
142
+ ) {
143
+ throw new FusionRunStoreError(
144
+ `Fusion operation ${input.operationId} already has a run.`,
145
+ );
146
+ }
113
147
  const createdAt = input.createdAt ?? this.now();
114
148
  const run: FusionRun = {
115
149
  id: input.id ?? this.idFactory(),
116
150
  prompt: input.prompt,
117
151
  profileName: input.profileName,
152
+ ...(input.operationId !== undefined
153
+ ? { operationId: input.operationId }
154
+ : {}),
118
155
  phase: input.phase ?? "chain",
119
156
  createdAt,
120
157
  updatedAt: createdAt,
121
158
  };
122
159
  this.activeRun = run;
160
+ this.rememberRun(run);
123
161
  this.persistRun(run);
124
162
  return cloneRun(run);
125
163
  }
@@ -128,6 +166,7 @@ export class FusionRunStore {
128
166
  const active = this.requireActiveRun(id);
129
167
  const updated = applyPatch(active, patch, patch.updatedAt ?? this.now());
130
168
  this.activeRun = updated;
169
+ this.rememberRun(updated);
131
170
  this.persistRun(updated);
132
171
  return cloneRun(updated);
133
172
  }
@@ -159,6 +198,7 @@ export class FusionRunStore {
159
198
  const summary = toRunSummary(finished);
160
199
  this.activeRun = undefined;
161
200
  this.lastRunSummary = summary;
201
+ this.rememberRun(finished);
162
202
  this.persistRun(summary);
163
203
  return cloneRun(finished);
164
204
  }
@@ -166,7 +206,12 @@ export class FusionRunStore {
166
206
  restoreFromEntries(
167
207
  entries: readonly unknown[],
168
208
  ): FusionRunSummary | undefined {
169
- const latestState = readLastFusionRunState(entries);
209
+ const states = readFusionRunStates(entries);
210
+ this.runsById.clear();
211
+ this.runIdsByOperationId.clear();
212
+ for (const state of states) this.rememberRun(state);
213
+
214
+ const latestState = states.at(-1);
170
215
  const summary = readLastFusionRunSummary(entries);
171
216
  this.activeRun =
172
217
  latestState && !isTerminalPhase(latestState.phase)
@@ -196,6 +241,16 @@ export class FusionRunStore {
196
241
  this.persistence?.appendEntry(FUSION_RUN_ENTRY_TYPE, cloneRun(run));
197
242
  }
198
243
 
244
+ private rememberRun(run: FusionRun): void {
245
+ this.runsById.set(run.id, cloneRun(run));
246
+ if (
247
+ run.operationId !== undefined &&
248
+ !this.runIdsByOperationId.has(run.operationId)
249
+ ) {
250
+ this.runIdsByOperationId.set(run.operationId, run.id);
251
+ }
252
+ }
253
+
199
254
  private requireActiveRun(id: string): FusionRun {
200
255
  if (!this.activeRun) {
201
256
  throw new FusionRunStoreError("No active fusion run.");
@@ -260,10 +315,22 @@ function applyPatch(
260
315
  updated.chainAsyncDir = patch.chainAsyncDir;
261
316
  }
262
317
  if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
318
+ if (patch.panelAsyncDir !== undefined) {
319
+ updated.panelAsyncDir = patch.panelAsyncDir;
320
+ }
321
+ if (patch.panelStopReason !== undefined) {
322
+ updated.panelStopReason = patch.panelStopReason;
323
+ }
324
+ if (patch.panelStoppedIndices !== undefined) {
325
+ updated.panelStoppedIndices = [...patch.panelStoppedIndices];
326
+ }
263
327
  if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
264
328
  if (patch.judgeAsyncDir !== undefined) {
265
329
  updated.judgeAsyncDir = patch.judgeAsyncDir;
266
330
  }
331
+ if (patch.judgeObservation !== undefined) {
332
+ updated.judgeObservation = cloneObservation(patch.judgeObservation);
333
+ }
267
334
  if (patch.panelOutputs !== undefined) {
268
335
  updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
269
336
  }
@@ -301,6 +368,7 @@ function toRunSummary(
301
368
  id: run.id,
302
369
  prompt: run.prompt,
303
370
  profileName: run.profileName,
371
+ ...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
304
372
  phase: run.phase,
305
373
  createdAt: run.createdAt,
306
374
  updatedAt: run.updatedAt,
@@ -317,6 +385,7 @@ function cloneRun(run: FusionRun): FusionRun {
317
385
  id: run.id,
318
386
  prompt: run.prompt,
319
387
  profileName: run.profileName,
388
+ ...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
320
389
  phase: run.phase,
321
390
  createdAt: run.createdAt,
322
391
  updatedAt: run.updatedAt,
@@ -325,10 +394,22 @@ function cloneRun(run: FusionRun): FusionRun {
325
394
  ? { chainAsyncDir: run.chainAsyncDir }
326
395
  : {}),
327
396
  ...(run.panelRunId !== undefined ? { panelRunId: run.panelRunId } : {}),
397
+ ...(run.panelAsyncDir !== undefined
398
+ ? { panelAsyncDir: run.panelAsyncDir }
399
+ : {}),
400
+ ...(run.panelStopReason !== undefined
401
+ ? { panelStopReason: run.panelStopReason }
402
+ : {}),
403
+ ...(run.panelStoppedIndices !== undefined
404
+ ? { panelStoppedIndices: [...run.panelStoppedIndices] }
405
+ : {}),
328
406
  ...(run.judgeRunId !== undefined ? { judgeRunId: run.judgeRunId } : {}),
329
407
  ...(run.judgeAsyncDir !== undefined
330
408
  ? { judgeAsyncDir: run.judgeAsyncDir }
331
409
  : {}),
410
+ ...(run.judgeObservation !== undefined
411
+ ? { judgeObservation: cloneObservation(run.judgeObservation) }
412
+ : {}),
332
413
  ...(run.panelOutputs !== undefined
333
414
  ? { panelOutputs: clonePanelOutputs(run.panelOutputs) }
334
415
  : {}),
@@ -360,6 +441,9 @@ function isFusionRunState(value: unknown): value is FusionRun {
360
441
  if (!isNonEmptyString(value.id)) return false;
361
442
  if (typeof value.prompt !== "string") return false;
362
443
  if (!isNonEmptyString(value.profileName)) return false;
444
+ if (value.operationId !== undefined && !isNonEmptyString(value.operationId)) {
445
+ return false;
446
+ }
363
447
  if (!isFusionPhase(value.phase)) return false;
364
448
  if (!isFiniteNumber(value.createdAt)) return false;
365
449
  if (!isFiniteNumber(value.updatedAt)) return false;
@@ -375,6 +459,28 @@ function isFusionRunState(value: unknown): value is FusionRun {
375
459
  if (value.panelRunId !== undefined && typeof value.panelRunId !== "string") {
376
460
  return false;
377
461
  }
462
+ if (
463
+ value.panelAsyncDir !== undefined &&
464
+ typeof value.panelAsyncDir !== "string"
465
+ ) {
466
+ return false;
467
+ }
468
+ if (
469
+ value.panelStopReason !== undefined &&
470
+ value.panelStopReason !== "agreement"
471
+ ) {
472
+ return false;
473
+ }
474
+ if (
475
+ value.panelStoppedIndices !== undefined &&
476
+ (!Array.isArray(value.panelStoppedIndices) ||
477
+ !value.panelStoppedIndices.every(
478
+ (index: unknown) =>
479
+ typeof index === "number" && Number.isInteger(index) && index >= 0,
480
+ ))
481
+ ) {
482
+ return false;
483
+ }
378
484
  if (value.judgeRunId !== undefined && typeof value.judgeRunId !== "string") {
379
485
  return false;
380
486
  }
@@ -384,6 +490,12 @@ function isFusionRunState(value: unknown): value is FusionRun {
384
490
  ) {
385
491
  return false;
386
492
  }
493
+ if (
494
+ value.judgeObservation !== undefined &&
495
+ !isRunObservation(value.judgeObservation)
496
+ ) {
497
+ return false;
498
+ }
387
499
  if (
388
500
  value.panelOutputs !== undefined &&
389
501
  !isPanelOutputArray(value.panelOutputs)
@@ -440,9 +552,14 @@ function isPanelOutput(
440
552
  typeof value.output === "string" &&
441
553
  (value.id === undefined || typeof value.id === "string") &&
442
554
  (value.label === undefined || typeof value.label === "string") &&
555
+ (value.configuredModel === undefined ||
556
+ typeof value.configuredModel === "string") &&
443
557
  (value.artifactPath === undefined ||
444
558
  typeof value.artifactPath === "string") &&
445
- (value.sessionPath === undefined || typeof value.sessionPath === "string")
559
+ (value.sessionPath === undefined ||
560
+ typeof value.sessionPath === "string") &&
561
+ (value.decision === undefined || isPanelDecision(value.decision)) &&
562
+ (value.observation === undefined || isRunObservation(value.observation))
446
563
  );
447
564
  }
448
565
 
@@ -462,20 +579,134 @@ function isPanelFailure(
462
579
  typeof value.summary === "string" &&
463
580
  (value.id === undefined || typeof value.id === "string") &&
464
581
  (value.label === undefined || typeof value.label === "string") &&
582
+ (value.configuredModel === undefined ||
583
+ typeof value.configuredModel === "string") &&
465
584
  (value.artifactPath === undefined ||
466
585
  typeof value.artifactPath === "string") &&
467
- (value.sessionPath === undefined || typeof value.sessionPath === "string")
586
+ (value.sessionPath === undefined ||
587
+ typeof value.sessionPath === "string") &&
588
+ (value.reason === undefined || isPanelFailureReason(value.reason)) &&
589
+ (value.observation === undefined || isRunObservation(value.observation))
468
590
  );
469
591
  }
470
592
 
471
593
  function clonePanelOutputs(
472
594
  outputs: NonNullable<FusionRun["panelOutputs"]>,
473
595
  ): NonNullable<FusionRun["panelOutputs"]> {
474
- return outputs.map((output) => ({ ...output }));
596
+ return outputs.map((output) => ({
597
+ ...output,
598
+ ...(output.decision
599
+ ? { decision: clonePanelDecision(output.decision) }
600
+ : {}),
601
+ ...(output.observation
602
+ ? { observation: cloneObservation(output.observation) }
603
+ : {}),
604
+ }));
475
605
  }
476
606
 
477
607
  function clonePanelFailures(
478
608
  failures: NonNullable<FusionRun["panelFailures"]>,
479
609
  ): NonNullable<FusionRun["panelFailures"]> {
480
- return failures.map((failure) => ({ ...failure }));
610
+ return failures.map((failure) => ({
611
+ ...failure,
612
+ ...(failure.observation
613
+ ? { observation: cloneObservation(failure.observation) }
614
+ : {}),
615
+ }));
616
+ }
617
+
618
+ function cloneObservation(observation: RunObservation): RunObservation {
619
+ return {
620
+ ...(observation.model ? { model: observation.model } : {}),
621
+ ...(observation.durationMs !== undefined
622
+ ? { durationMs: observation.durationMs }
623
+ : {}),
624
+ ...(observation.usage ? { usage: { ...observation.usage } } : {}),
625
+ ...(observation.attempts
626
+ ? { attempts: observation.attempts.map((attempt) => ({ ...attempt })) }
627
+ : {}),
628
+ ...(observation.providerFailures
629
+ ? {
630
+ providerFailures: observation.providerFailures.map((failure) => ({
631
+ ...failure,
632
+ })),
633
+ }
634
+ : {}),
635
+ };
636
+ }
637
+
638
+ function clonePanelDecision(decision: PanelDecision): PanelDecision {
639
+ return { ...decision };
640
+ }
641
+
642
+ function isRunObservation(value: unknown): value is RunObservation {
643
+ if (!isRecord(value)) return false;
644
+ if (value.model !== undefined && typeof value.model !== "string")
645
+ return false;
646
+ if (value.durationMs !== undefined && !isFiniteNumber(value.durationMs)) {
647
+ return false;
648
+ }
649
+ if (value.usage !== undefined && !isRunUsage(value.usage)) return false;
650
+ if (value.attempts !== undefined && !isModelAttemptArray(value.attempts)) {
651
+ return false;
652
+ }
653
+ return (
654
+ value.providerFailures === undefined ||
655
+ (Array.isArray(value.providerFailures) &&
656
+ value.providerFailures.every(isProviderFailure))
657
+ );
658
+ }
659
+
660
+ function isRunUsage(value: unknown): value is RunUsage {
661
+ if (!isRecord(value)) return false;
662
+ return (
663
+ (value.inputTokens === undefined || isFiniteNumber(value.inputTokens)) &&
664
+ (value.outputTokens === undefined || isFiniteNumber(value.outputTokens)) &&
665
+ (value.costUsd === undefined || isFiniteNumber(value.costUsd))
666
+ );
667
+ }
668
+
669
+ function isModelAttemptArray(value: unknown): value is ModelAttempt[] {
670
+ return (
671
+ Array.isArray(value) &&
672
+ value.every((item) => {
673
+ if (!isRecord(item)) return false;
674
+ return (
675
+ typeof item.model === "string" &&
676
+ typeof item.success === "boolean" &&
677
+ (item.error === undefined || typeof item.error === "string")
678
+ );
679
+ })
680
+ );
681
+ }
682
+
683
+ function isProviderFailure(value: unknown): value is ProviderFailure {
684
+ if (!isRecord(value)) return false;
685
+ return (
686
+ typeof value.provider === "string" &&
687
+ typeof value.message === "string" &&
688
+ (value.model === undefined || typeof value.model === "string") &&
689
+ (value.count === undefined || isFiniteNumber(value.count))
690
+ );
691
+ }
692
+
693
+ function isPanelDecision(value: unknown): value is PanelDecision {
694
+ if (!isRecord(value)) return false;
695
+ return (
696
+ typeof value.recommendation === "string" &&
697
+ (value.confidence === "low" ||
698
+ value.confidence === "medium" ||
699
+ value.confidence === "high") &&
700
+ typeof value.needsMoreEvidence === "boolean" &&
701
+ typeof value.answerMarkdown === "string"
702
+ );
703
+ }
704
+
705
+ function isPanelFailureReason(value: unknown): boolean {
706
+ return (
707
+ value === "provider" ||
708
+ value === "timeout" ||
709
+ value === "interrupted" ||
710
+ value === "stopped-after-agreement"
711
+ );
481
712
  }
package/src/types.ts CHANGED
@@ -31,6 +31,43 @@ export interface FusionProfile {
31
31
  concurrency?: number;
32
32
  timeoutMs?: number;
33
33
  context?: FusionContextMode;
34
+ stopWhenPanelAgrees?: boolean;
35
+ }
36
+
37
+ export type PanelConfidence = "low" | "medium" | "high";
38
+
39
+ export interface PanelDecision {
40
+ recommendation: string;
41
+ confidence: PanelConfidence;
42
+ needsMoreEvidence: boolean;
43
+ answerMarkdown: string;
44
+ }
45
+
46
+ export interface RunUsage {
47
+ inputTokens?: number;
48
+ outputTokens?: number;
49
+ costUsd?: number;
50
+ }
51
+
52
+ export interface ModelAttempt {
53
+ model: string;
54
+ success: boolean;
55
+ error?: string;
56
+ }
57
+
58
+ export interface ProviderFailure {
59
+ provider: string;
60
+ model?: string;
61
+ message: string;
62
+ count?: number;
63
+ }
64
+
65
+ export interface RunObservation {
66
+ model?: string;
67
+ durationMs?: number;
68
+ usage?: RunUsage;
69
+ attempts?: ModelAttempt[];
70
+ providerFailures?: ProviderFailure[];
34
71
  }
35
72
 
36
73
  export interface FusionConfig {
@@ -41,6 +78,7 @@ export interface FusionConfig {
41
78
  export interface ParsedFusionArgs {
42
79
  prompt: string;
43
80
  profile?: string;
81
+ operationId?: string;
44
82
  }
45
83
 
46
84
  export interface PanelOutput {
@@ -51,10 +89,16 @@ export interface PanelOutput {
51
89
  label?: string;
52
90
  role?: string;
53
91
  model?: string;
92
+ configuredModel?: string;
93
+ decision?: PanelDecision;
94
+ observation?: RunObservation;
54
95
  artifactPath?: string;
55
96
  sessionPath?: string;
56
97
  }
57
98
 
99
+ export type PanelFailureReason =
100
+ "provider" | "timeout" | "interrupted" | "stopped-after-agreement";
101
+
58
102
  export interface FailedPanelSummary {
59
103
  index: number;
60
104
  agent: string;
@@ -63,6 +107,9 @@ export interface FailedPanelSummary {
63
107
  label?: string;
64
108
  role?: string;
65
109
  model?: string;
110
+ configuredModel?: string;
111
+ reason?: PanelFailureReason;
112
+ observation?: RunObservation;
66
113
  artifactPath?: string;
67
114
  sessionPath?: string;
68
115
  }
@@ -74,14 +121,19 @@ export interface FusionRun {
74
121
  id: string;
75
122
  prompt: string;
76
123
  profileName: string;
124
+ operationId?: string;
77
125
  phase: FusionPhase;
78
126
  createdAt: number;
79
127
  updatedAt: number;
80
128
  chainRunId?: string;
81
129
  chainAsyncDir?: string;
82
130
  panelRunId?: string;
131
+ panelAsyncDir?: string;
132
+ panelStopReason?: "agreement";
133
+ panelStoppedIndices?: number[];
83
134
  judgeRunId?: string;
84
135
  judgeAsyncDir?: string;
136
+ judgeObservation?: RunObservation;
85
137
  panelOutputs?: PanelOutput[];
86
138
  panelFailures?: FailedPanelSummary[];
87
139
  report?: string;