@alexeiled/pi-fusion 0.3.0 → 0.5.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.
@@ -0,0 +1,335 @@
1
+ import type {
2
+ ModelAttempt,
3
+ PanelConfidence,
4
+ PanelDecision,
5
+ PanelOutput,
6
+ ProviderFailure,
7
+ RunObservation,
8
+ RunUsage,
9
+ } from "./types.js";
10
+ import { isFiniteNumber, isRecord } from "./utils.js";
11
+
12
+ export const PANEL_DECISION_OPEN = "<fusion-panel-decision>";
13
+ export const PANEL_DECISION_CLOSE = "</fusion-panel-decision>";
14
+
15
+ export function extractPanelDecision(
16
+ value: unknown,
17
+ ): PanelDecision | undefined {
18
+ if (typeof value === "string") return extractTaggedPanelDecision(value);
19
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
20
+ return extractTaggedPanelDecision(value.join("\n"));
21
+ }
22
+ return isRecord(value) ? panelDecisionFromRecord(value) : undefined;
23
+ }
24
+
25
+ function extractTaggedPanelDecision(value: string): PanelDecision | undefined {
26
+ const openIndex = value.lastIndexOf(PANEL_DECISION_OPEN);
27
+ if (openIndex < 0) return undefined;
28
+ const jsonStart = openIndex + PANEL_DECISION_OPEN.length;
29
+ const closeIndex = value.indexOf(PANEL_DECISION_CLOSE, jsonStart);
30
+ if (closeIndex < 0) return undefined;
31
+ const closeEnd = closeIndex + PANEL_DECISION_CLOSE.length;
32
+ if (value.slice(closeEnd).trim()) return undefined;
33
+
34
+ let parsed: unknown;
35
+ try {
36
+ parsed = JSON.parse(value.slice(jsonStart, closeIndex));
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ if (!isRecord(parsed)) return undefined;
41
+
42
+ const answerMarkdown = value.slice(0, openIndex).trim();
43
+ return panelDecisionFromRecord(parsed, answerMarkdown);
44
+ }
45
+
46
+ function panelDecisionFromRecord(
47
+ value: Record<string, unknown>,
48
+ fallbackAnswerMarkdown?: string,
49
+ ): PanelDecision | undefined {
50
+ const recommendation = firstNonBlankString(value.recommendation);
51
+ const answerMarkdown =
52
+ firstNonBlankString(value.answerMarkdown) ?? fallbackAnswerMarkdown;
53
+ const confidence = value.confidence;
54
+ if (
55
+ !recommendation ||
56
+ !answerMarkdown ||
57
+ !isPanelConfidence(confidence) ||
58
+ typeof value.needsMoreEvidence !== "boolean"
59
+ ) {
60
+ return undefined;
61
+ }
62
+ return {
63
+ recommendation,
64
+ confidence,
65
+ needsMoreEvidence: value.needsMoreEvidence,
66
+ answerMarkdown,
67
+ };
68
+ }
69
+
70
+ export function normalizeRecommendation(value: string): string {
71
+ return value
72
+ .toLocaleLowerCase()
73
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
74
+ .trim();
75
+ }
76
+
77
+ export function hasStrongPanelAgreement(
78
+ outputs: readonly PanelOutput[],
79
+ completedPanelCount: number,
80
+ totalPanelCount: number,
81
+ ): boolean {
82
+ if (outputs.length < 2 || completedPanelCount >= totalPanelCount) {
83
+ return false;
84
+ }
85
+ const decisions = outputs.map((output) => output.decision);
86
+ if (decisions.some((decision) => !decision)) return false;
87
+ if (
88
+ decisions.some(
89
+ (decision) =>
90
+ decision?.confidence !== "high" || decision.needsMoreEvidence,
91
+ )
92
+ ) {
93
+ return false;
94
+ }
95
+ const recommendation = normalizeRecommendation(
96
+ decisions[0]?.recommendation ?? "",
97
+ );
98
+ return Boolean(
99
+ recommendation &&
100
+ decisions.every(
101
+ (decision) =>
102
+ decision &&
103
+ normalizeRecommendation(decision.recommendation) === recommendation,
104
+ ),
105
+ );
106
+ }
107
+
108
+ export function mergeRunObservations(
109
+ base: RunObservation | undefined,
110
+ latest: RunObservation | undefined,
111
+ ): RunObservation {
112
+ if (!base) return latest ? cloneObservation(latest) : {};
113
+ if (!latest) return cloneObservation(base);
114
+ const failures = summarizeProviderFailures(
115
+ latest.providerFailures ?? base.providerFailures ?? [],
116
+ ).map(({ count, ...failure }) =>
117
+ count && count > 1 ? { ...failure, count } : failure,
118
+ );
119
+ return {
120
+ ...((latest.model ?? base.model)
121
+ ? { model: latest.model ?? base.model }
122
+ : {}),
123
+ ...((latest.durationMs ?? base.durationMs) !== undefined
124
+ ? { durationMs: latest.durationMs ?? base.durationMs }
125
+ : {}),
126
+ ...mergeUsage(base.usage, latest.usage),
127
+ ...((latest.attempts ?? base.attempts)
128
+ ? {
129
+ attempts: [...(latest.attempts ?? base.attempts ?? [])].map(
130
+ (attempt) => ({ ...attempt }),
131
+ ),
132
+ }
133
+ : {}),
134
+ ...(failures.length > 0 ? { providerFailures: failures } : {}),
135
+ };
136
+ }
137
+
138
+ export function extractRunObservation(value: unknown): RunObservation {
139
+ if (!isRecord(value)) return {};
140
+
141
+ const model = firstString(value.model);
142
+ const durationMs = extractDuration(value);
143
+ const usage = extractUsage(value);
144
+ const attempts = extractAttempts(value.modelAttempts);
145
+ const attemptFailures = attempts.flatMap((attempt) =>
146
+ attempt.success || !attempt.error
147
+ ? []
148
+ : [providerFailureFromAttempt(attempt)],
149
+ );
150
+ const rawError = firstNonBlankString(value.error);
151
+ const providerFailures = summarizeProviderFailures(
152
+ rawError &&
153
+ attemptFailures.length === 0 &&
154
+ (value.success === false || value.state === "failed")
155
+ ? [
156
+ {
157
+ provider: model ? providerFromModel(model) : "unknown provider",
158
+ ...(model ? { model } : {}),
159
+ message: rawError,
160
+ },
161
+ ]
162
+ : attemptFailures,
163
+ ).map(({ count, ...failure }) =>
164
+ count && count > 1 ? { ...failure, count } : failure,
165
+ );
166
+
167
+ return {
168
+ ...(model ? { model } : {}),
169
+ ...(durationMs !== undefined ? { durationMs } : {}),
170
+ ...(usage ? { usage } : {}),
171
+ ...(attempts.length > 0 ? { attempts } : {}),
172
+ ...(providerFailures.length > 0 ? { providerFailures } : {}),
173
+ };
174
+ }
175
+
176
+ export function summarizeProviderFailures(
177
+ failures: readonly ProviderFailure[],
178
+ ): ProviderFailure[] {
179
+ const grouped = new Map<string, ProviderFailure & { count: number }>();
180
+ for (const failure of failures) {
181
+ const message = failure.message.trim();
182
+ if (!message) continue;
183
+ const key = `${failure.provider}\u0000${failure.model ?? ""}\u0000${message}`;
184
+ const existing = grouped.get(key);
185
+ if (existing) {
186
+ existing.count += failure.count ?? 1;
187
+ continue;
188
+ }
189
+ grouped.set(key, {
190
+ ...failure,
191
+ message,
192
+ count: failure.count ?? 1,
193
+ });
194
+ }
195
+
196
+ return [...grouped.values()]
197
+ .sort((left, right) =>
198
+ `${left.provider}\u0000${left.model ?? ""}\u0000${left.message}`.localeCompare(
199
+ `${right.provider}\u0000${right.model ?? ""}\u0000${right.message}`,
200
+ ),
201
+ )
202
+ .map(({ count, ...failure }) => ({ ...failure, count }));
203
+ }
204
+
205
+ function mergeUsage(
206
+ base: RunUsage | undefined,
207
+ latest: RunUsage | undefined,
208
+ ): { usage?: RunUsage } {
209
+ if (!base && !latest) return {};
210
+ const inputTokens = latest?.inputTokens ?? base?.inputTokens;
211
+ const outputTokens = latest?.outputTokens ?? base?.outputTokens;
212
+ const costUsd = latest?.costUsd ?? base?.costUsd;
213
+ const usage: RunUsage = {
214
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
215
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
216
+ ...(costUsd !== undefined ? { costUsd } : {}),
217
+ };
218
+ return { usage };
219
+ }
220
+
221
+ function cloneObservation(observation: RunObservation): RunObservation {
222
+ return {
223
+ ...observation,
224
+ ...(observation.usage ? { usage: { ...observation.usage } } : {}),
225
+ ...(observation.attempts
226
+ ? { attempts: observation.attempts.map((attempt) => ({ ...attempt })) }
227
+ : {}),
228
+ ...(observation.providerFailures
229
+ ? {
230
+ providerFailures: observation.providerFailures.map((failure) => ({
231
+ ...failure,
232
+ })),
233
+ }
234
+ : {}),
235
+ };
236
+ }
237
+
238
+ function extractDuration(value: Record<string, unknown>): number | undefined {
239
+ if (isFiniteNumber(value.durationMs) && value.durationMs >= 0) {
240
+ return value.durationMs;
241
+ }
242
+ if (
243
+ isFiniteNumber(value.startedAt) &&
244
+ isFiniteNumber(value.endedAt) &&
245
+ value.endedAt >= value.startedAt
246
+ ) {
247
+ return value.endedAt - value.startedAt;
248
+ }
249
+ return undefined;
250
+ }
251
+
252
+ function extractUsage(value: Record<string, unknown>): RunUsage | undefined {
253
+ const totalCost = isRecord(value.totalCost) ? value.totalCost : undefined;
254
+ const rawUsage = isRecord(value.usage) ? value.usage : undefined;
255
+ const inputTokens = firstFinite(
256
+ totalCost?.inputTokens,
257
+ rawUsage?.inputTokens,
258
+ rawUsage?.input,
259
+ );
260
+ const outputTokens = firstFinite(
261
+ totalCost?.outputTokens,
262
+ rawUsage?.outputTokens,
263
+ rawUsage?.output,
264
+ );
265
+ const costUsd = firstFinite(
266
+ totalCost?.costUsd,
267
+ rawUsage?.costUsd,
268
+ isRecord(rawUsage?.cost) ? rawUsage.cost.total : undefined,
269
+ );
270
+
271
+ if (
272
+ inputTokens === undefined &&
273
+ outputTokens === undefined &&
274
+ costUsd === undefined
275
+ ) {
276
+ return undefined;
277
+ }
278
+ return {
279
+ ...(inputTokens !== undefined ? { inputTokens } : {}),
280
+ ...(outputTokens !== undefined ? { outputTokens } : {}),
281
+ ...(costUsd !== undefined ? { costUsd } : {}),
282
+ };
283
+ }
284
+
285
+ function extractAttempts(value: unknown): ModelAttempt[] {
286
+ if (!Array.isArray(value)) return [];
287
+ const attempts: ModelAttempt[] = [];
288
+ for (const item of value) {
289
+ if (!isRecord(item)) continue;
290
+ const model = firstString(item.model);
291
+ if (!model || typeof item.success !== "boolean") continue;
292
+ const error = firstString(item.error);
293
+ attempts.push({
294
+ model,
295
+ success: item.success,
296
+ ...(error ? { error } : {}),
297
+ });
298
+ }
299
+ return attempts;
300
+ }
301
+
302
+ function providerFailureFromAttempt(attempt: ModelAttempt): ProviderFailure {
303
+ return {
304
+ provider: providerFromModel(attempt.model),
305
+ model: attempt.model,
306
+ message: attempt.error ?? "model attempt failed",
307
+ };
308
+ }
309
+
310
+ function providerFromModel(model: string): string {
311
+ return model.split("/", 1)[0] || "unknown provider";
312
+ }
313
+
314
+ function firstFinite(...values: readonly unknown[]): number | undefined {
315
+ return values.find(isFiniteNumber);
316
+ }
317
+
318
+ function isPanelConfidence(value: unknown): value is PanelConfidence {
319
+ return value === "low" || value === "medium" || value === "high";
320
+ }
321
+
322
+ function firstNonBlankString(
323
+ ...values: readonly unknown[]
324
+ ): string | undefined {
325
+ for (const value of values) {
326
+ if (typeof value !== "string") continue;
327
+ const trimmed = value.trim();
328
+ if (trimmed) return trimmed;
329
+ }
330
+ return undefined;
331
+ }
332
+
333
+ function firstString(...values: readonly unknown[]): string | undefined {
334
+ return values.find((value): value is string => typeof value === "string");
335
+ }
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";
@@ -40,8 +48,12 @@ export interface FusionRunPatch {
40
48
  chainRunId?: string;
41
49
  chainAsyncDir?: string;
42
50
  panelRunId?: string;
51
+ panelAsyncDir?: string;
52
+ panelStopReason?: FusionRun["panelStopReason"];
53
+ panelStoppedIndices?: FusionRun["panelStoppedIndices"];
43
54
  judgeRunId?: string;
44
55
  judgeAsyncDir?: string;
56
+ judgeObservation?: FusionRun["judgeObservation"];
45
57
  panelOutputs?: FusionRun["panelOutputs"];
46
58
  panelFailures?: FusionRun["panelFailures"];
47
59
  report?: string;
@@ -260,10 +272,22 @@ function applyPatch(
260
272
  updated.chainAsyncDir = patch.chainAsyncDir;
261
273
  }
262
274
  if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
275
+ if (patch.panelAsyncDir !== undefined) {
276
+ updated.panelAsyncDir = patch.panelAsyncDir;
277
+ }
278
+ if (patch.panelStopReason !== undefined) {
279
+ updated.panelStopReason = patch.panelStopReason;
280
+ }
281
+ if (patch.panelStoppedIndices !== undefined) {
282
+ updated.panelStoppedIndices = [...patch.panelStoppedIndices];
283
+ }
263
284
  if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
264
285
  if (patch.judgeAsyncDir !== undefined) {
265
286
  updated.judgeAsyncDir = patch.judgeAsyncDir;
266
287
  }
288
+ if (patch.judgeObservation !== undefined) {
289
+ updated.judgeObservation = cloneObservation(patch.judgeObservation);
290
+ }
267
291
  if (patch.panelOutputs !== undefined) {
268
292
  updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
269
293
  }
@@ -325,10 +349,22 @@ function cloneRun(run: FusionRun): FusionRun {
325
349
  ? { chainAsyncDir: run.chainAsyncDir }
326
350
  : {}),
327
351
  ...(run.panelRunId !== undefined ? { panelRunId: run.panelRunId } : {}),
352
+ ...(run.panelAsyncDir !== undefined
353
+ ? { panelAsyncDir: run.panelAsyncDir }
354
+ : {}),
355
+ ...(run.panelStopReason !== undefined
356
+ ? { panelStopReason: run.panelStopReason }
357
+ : {}),
358
+ ...(run.panelStoppedIndices !== undefined
359
+ ? { panelStoppedIndices: [...run.panelStoppedIndices] }
360
+ : {}),
328
361
  ...(run.judgeRunId !== undefined ? { judgeRunId: run.judgeRunId } : {}),
329
362
  ...(run.judgeAsyncDir !== undefined
330
363
  ? { judgeAsyncDir: run.judgeAsyncDir }
331
364
  : {}),
365
+ ...(run.judgeObservation !== undefined
366
+ ? { judgeObservation: cloneObservation(run.judgeObservation) }
367
+ : {}),
332
368
  ...(run.panelOutputs !== undefined
333
369
  ? { panelOutputs: clonePanelOutputs(run.panelOutputs) }
334
370
  : {}),
@@ -375,6 +411,28 @@ function isFusionRunState(value: unknown): value is FusionRun {
375
411
  if (value.panelRunId !== undefined && typeof value.panelRunId !== "string") {
376
412
  return false;
377
413
  }
414
+ if (
415
+ value.panelAsyncDir !== undefined &&
416
+ typeof value.panelAsyncDir !== "string"
417
+ ) {
418
+ return false;
419
+ }
420
+ if (
421
+ value.panelStopReason !== undefined &&
422
+ value.panelStopReason !== "agreement"
423
+ ) {
424
+ return false;
425
+ }
426
+ if (
427
+ value.panelStoppedIndices !== undefined &&
428
+ (!Array.isArray(value.panelStoppedIndices) ||
429
+ !value.panelStoppedIndices.every(
430
+ (index: unknown) =>
431
+ typeof index === "number" && Number.isInteger(index) && index >= 0,
432
+ ))
433
+ ) {
434
+ return false;
435
+ }
378
436
  if (value.judgeRunId !== undefined && typeof value.judgeRunId !== "string") {
379
437
  return false;
380
438
  }
@@ -384,6 +442,12 @@ function isFusionRunState(value: unknown): value is FusionRun {
384
442
  ) {
385
443
  return false;
386
444
  }
445
+ if (
446
+ value.judgeObservation !== undefined &&
447
+ !isRunObservation(value.judgeObservation)
448
+ ) {
449
+ return false;
450
+ }
387
451
  if (
388
452
  value.panelOutputs !== undefined &&
389
453
  !isPanelOutputArray(value.panelOutputs)
@@ -440,9 +504,14 @@ function isPanelOutput(
440
504
  typeof value.output === "string" &&
441
505
  (value.id === undefined || typeof value.id === "string") &&
442
506
  (value.label === undefined || typeof value.label === "string") &&
507
+ (value.configuredModel === undefined ||
508
+ typeof value.configuredModel === "string") &&
443
509
  (value.artifactPath === undefined ||
444
510
  typeof value.artifactPath === "string") &&
445
- (value.sessionPath === undefined || typeof value.sessionPath === "string")
511
+ (value.sessionPath === undefined ||
512
+ typeof value.sessionPath === "string") &&
513
+ (value.decision === undefined || isPanelDecision(value.decision)) &&
514
+ (value.observation === undefined || isRunObservation(value.observation))
446
515
  );
447
516
  }
448
517
 
@@ -462,20 +531,134 @@ function isPanelFailure(
462
531
  typeof value.summary === "string" &&
463
532
  (value.id === undefined || typeof value.id === "string") &&
464
533
  (value.label === undefined || typeof value.label === "string") &&
534
+ (value.configuredModel === undefined ||
535
+ typeof value.configuredModel === "string") &&
465
536
  (value.artifactPath === undefined ||
466
537
  typeof value.artifactPath === "string") &&
467
- (value.sessionPath === undefined || typeof value.sessionPath === "string")
538
+ (value.sessionPath === undefined ||
539
+ typeof value.sessionPath === "string") &&
540
+ (value.reason === undefined || isPanelFailureReason(value.reason)) &&
541
+ (value.observation === undefined || isRunObservation(value.observation))
468
542
  );
469
543
  }
470
544
 
471
545
  function clonePanelOutputs(
472
546
  outputs: NonNullable<FusionRun["panelOutputs"]>,
473
547
  ): NonNullable<FusionRun["panelOutputs"]> {
474
- return outputs.map((output) => ({ ...output }));
548
+ return outputs.map((output) => ({
549
+ ...output,
550
+ ...(output.decision
551
+ ? { decision: clonePanelDecision(output.decision) }
552
+ : {}),
553
+ ...(output.observation
554
+ ? { observation: cloneObservation(output.observation) }
555
+ : {}),
556
+ }));
475
557
  }
476
558
 
477
559
  function clonePanelFailures(
478
560
  failures: NonNullable<FusionRun["panelFailures"]>,
479
561
  ): NonNullable<FusionRun["panelFailures"]> {
480
- return failures.map((failure) => ({ ...failure }));
562
+ return failures.map((failure) => ({
563
+ ...failure,
564
+ ...(failure.observation
565
+ ? { observation: cloneObservation(failure.observation) }
566
+ : {}),
567
+ }));
568
+ }
569
+
570
+ function cloneObservation(observation: RunObservation): RunObservation {
571
+ return {
572
+ ...(observation.model ? { model: observation.model } : {}),
573
+ ...(observation.durationMs !== undefined
574
+ ? { durationMs: observation.durationMs }
575
+ : {}),
576
+ ...(observation.usage ? { usage: { ...observation.usage } } : {}),
577
+ ...(observation.attempts
578
+ ? { attempts: observation.attempts.map((attempt) => ({ ...attempt })) }
579
+ : {}),
580
+ ...(observation.providerFailures
581
+ ? {
582
+ providerFailures: observation.providerFailures.map((failure) => ({
583
+ ...failure,
584
+ })),
585
+ }
586
+ : {}),
587
+ };
588
+ }
589
+
590
+ function clonePanelDecision(decision: PanelDecision): PanelDecision {
591
+ return { ...decision };
592
+ }
593
+
594
+ function isRunObservation(value: unknown): value is RunObservation {
595
+ if (!isRecord(value)) return false;
596
+ if (value.model !== undefined && typeof value.model !== "string")
597
+ return false;
598
+ if (value.durationMs !== undefined && !isFiniteNumber(value.durationMs)) {
599
+ return false;
600
+ }
601
+ if (value.usage !== undefined && !isRunUsage(value.usage)) return false;
602
+ if (value.attempts !== undefined && !isModelAttemptArray(value.attempts)) {
603
+ return false;
604
+ }
605
+ return (
606
+ value.providerFailures === undefined ||
607
+ (Array.isArray(value.providerFailures) &&
608
+ value.providerFailures.every(isProviderFailure))
609
+ );
610
+ }
611
+
612
+ function isRunUsage(value: unknown): value is RunUsage {
613
+ if (!isRecord(value)) return false;
614
+ return (
615
+ (value.inputTokens === undefined || isFiniteNumber(value.inputTokens)) &&
616
+ (value.outputTokens === undefined || isFiniteNumber(value.outputTokens)) &&
617
+ (value.costUsd === undefined || isFiniteNumber(value.costUsd))
618
+ );
619
+ }
620
+
621
+ function isModelAttemptArray(value: unknown): value is ModelAttempt[] {
622
+ return (
623
+ Array.isArray(value) &&
624
+ value.every((item) => {
625
+ if (!isRecord(item)) return false;
626
+ return (
627
+ typeof item.model === "string" &&
628
+ typeof item.success === "boolean" &&
629
+ (item.error === undefined || typeof item.error === "string")
630
+ );
631
+ })
632
+ );
633
+ }
634
+
635
+ function isProviderFailure(value: unknown): value is ProviderFailure {
636
+ if (!isRecord(value)) return false;
637
+ return (
638
+ typeof value.provider === "string" &&
639
+ typeof value.message === "string" &&
640
+ (value.model === undefined || typeof value.model === "string") &&
641
+ (value.count === undefined || isFiniteNumber(value.count))
642
+ );
643
+ }
644
+
645
+ function isPanelDecision(value: unknown): value is PanelDecision {
646
+ if (!isRecord(value)) return false;
647
+ return (
648
+ typeof value.recommendation === "string" &&
649
+ (value.confidence === "low" ||
650
+ value.confidence === "medium" ||
651
+ value.confidence === "high") &&
652
+ typeof value.needsMoreEvidence === "boolean" &&
653
+ typeof value.answerMarkdown === "string"
654
+ );
655
+ }
656
+
657
+ function isPanelFailureReason(value: unknown): boolean {
658
+ return (
659
+ value === "provider" ||
660
+ value === "timeout" ||
661
+ value === "interrupted" ||
662
+ value === "stopped-after-agreement"
663
+ );
481
664
  }
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 {
@@ -51,10 +88,16 @@ export interface PanelOutput {
51
88
  label?: string;
52
89
  role?: string;
53
90
  model?: string;
91
+ configuredModel?: string;
92
+ decision?: PanelDecision;
93
+ observation?: RunObservation;
54
94
  artifactPath?: string;
55
95
  sessionPath?: string;
56
96
  }
57
97
 
98
+ export type PanelFailureReason =
99
+ "provider" | "timeout" | "interrupted" | "stopped-after-agreement";
100
+
58
101
  export interface FailedPanelSummary {
59
102
  index: number;
60
103
  agent: string;
@@ -63,6 +106,9 @@ export interface FailedPanelSummary {
63
106
  label?: string;
64
107
  role?: string;
65
108
  model?: string;
109
+ configuredModel?: string;
110
+ reason?: PanelFailureReason;
111
+ observation?: RunObservation;
66
112
  artifactPath?: string;
67
113
  sessionPath?: string;
68
114
  }
@@ -80,8 +126,12 @@ export interface FusionRun {
80
126
  chainRunId?: string;
81
127
  chainAsyncDir?: string;
82
128
  panelRunId?: string;
129
+ panelAsyncDir?: string;
130
+ panelStopReason?: "agreement";
131
+ panelStoppedIndices?: number[];
83
132
  judgeRunId?: string;
84
133
  judgeAsyncDir?: string;
134
+ judgeObservation?: RunObservation;
85
135
  panelOutputs?: PanelOutput[];
86
136
  panelFailures?: FailedPanelSummary[];
87
137
  report?: string;