@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.
- package/README.md +6 -2
- package/agents/fusion-panelist.md +7 -1
- package/docs/user-guide.md +56 -20
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +4 -0
- package/src/config.ts +7 -0
- package/src/index.ts +29 -13
- package/src/orchestrator.ts +379 -53
- package/src/report.ts +171 -10
- package/src/result-extract.ts +157 -24
- package/src/run-builder.ts +32 -4
- package/src/run-observations.ts +335 -0
- package/src/run-store.ts +188 -5
- package/src/types.ts +50 -0
|
@@ -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 {
|
|
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 ||
|
|
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 ||
|
|
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) => ({
|
|
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) => ({
|
|
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;
|