@alexeiled/pi-fusion 0.5.2 → 0.6.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/README.md +24 -11
- package/agents/fusion-composer.md +49 -0
- package/agents/fusion-judge.md +7 -0
- package/agents/fusion-panelist-full.md +53 -0
- package/agents/fusion-panelist-web.md +41 -0
- package/agents/fusion-panelist.md +2 -0
- package/docs/user-guide.md +208 -21
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +69 -18
- package/src/commands.ts +1 -0
- package/src/config.ts +167 -5
- package/src/fusion-args.ts +36 -2
- package/src/index.ts +11 -2
- package/src/orchestrator.ts +146 -19
- package/src/panel-completion.ts +17 -6
- package/src/report.ts +233 -78
- package/src/result-extract.ts +15 -1
- package/src/run-builder.ts +298 -153
- package/src/run-store.ts +31 -0
- package/src/types.ts +89 -1
package/src/report.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
buildBlindLabelMap,
|
|
3
|
+
type FailedPanelSummary,
|
|
4
|
+
type PanelOutput,
|
|
5
|
+
} from "./run-builder.js";
|
|
2
6
|
import { summarizeProviderFailures } from "./run-observations.js";
|
|
3
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
memberLabel,
|
|
9
|
+
panelItemLabel,
|
|
10
|
+
type FusionRun,
|
|
11
|
+
type FusionSynthesisMode,
|
|
12
|
+
type PanelMemberConfig,
|
|
13
|
+
type ProviderFailure,
|
|
14
|
+
type RunObservation,
|
|
15
|
+
} from "./types.js";
|
|
4
16
|
|
|
5
17
|
type ReportRun = Pick<
|
|
6
18
|
FusionRun,
|
|
@@ -19,6 +31,9 @@ export interface RenderPanelFailureReportInput {
|
|
|
19
31
|
failures: readonly FailedPanelSummary[];
|
|
20
32
|
error?: string;
|
|
21
33
|
judgeModel?: string;
|
|
34
|
+
synthesis?: FusionSynthesisMode;
|
|
35
|
+
/** Configured members, so a merge failure can name the uncovered facets. */
|
|
36
|
+
panel?: readonly PanelMemberConfig[];
|
|
22
37
|
}
|
|
23
38
|
|
|
24
39
|
export interface RenderSinglePanelReportInput {
|
|
@@ -35,9 +50,19 @@ export interface RenderJudgeReportInput {
|
|
|
35
50
|
failures?: readonly FailedPanelSummary[];
|
|
36
51
|
judgeModel?: string;
|
|
37
52
|
judgeObservation?: RunObservation;
|
|
53
|
+
/**
|
|
54
|
+
* Set when the judge was shown neutral candidate names. The report always
|
|
55
|
+
* shows real member names, so the judge's prose is rewritten before parsing.
|
|
56
|
+
*/
|
|
57
|
+
blindPanelLabels?: boolean;
|
|
58
|
+
/** Selects which synthesis sections the report renders. Defaults to `select`. */
|
|
59
|
+
synthesis?: FusionSynthesisMode;
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
export interface RenderFailureReportInput {
|
|
63
|
+
synthesis?: FusionSynthesisMode;
|
|
64
|
+
/** Configured members, so a merge failure can name the uncovered facets. */
|
|
65
|
+
panel?: readonly PanelMemberConfig[];
|
|
41
66
|
run: ReportRun;
|
|
42
67
|
error: string;
|
|
43
68
|
panelOutputs?: readonly PanelOutput[];
|
|
@@ -46,6 +71,9 @@ export interface RenderFailureReportInput {
|
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
export interface RenderCancelledReportInput {
|
|
74
|
+
synthesis?: FusionSynthesisMode;
|
|
75
|
+
/** Configured members, so a merge failure can name the uncovered facets. */
|
|
76
|
+
panel?: readonly PanelMemberConfig[];
|
|
49
77
|
run: ReportRun;
|
|
50
78
|
method: "stop" | "interrupt" | "local";
|
|
51
79
|
targetRunId?: string;
|
|
@@ -59,8 +87,13 @@ type ReportSectionTitle =
|
|
|
59
87
|
| "Agent Status"
|
|
60
88
|
| "Consensus"
|
|
61
89
|
| "Disagreements"
|
|
90
|
+
| "Contested Claims"
|
|
62
91
|
| "Unique Insights"
|
|
63
92
|
| "Blind Spots"
|
|
93
|
+
| "Coverage Map"
|
|
94
|
+
| "Combined Answer"
|
|
95
|
+
| "Gaps"
|
|
96
|
+
| "Conflicts At Seams"
|
|
64
97
|
| "Recommendation"
|
|
65
98
|
| "Risks"
|
|
66
99
|
| "Next Step"
|
|
@@ -78,11 +111,86 @@ interface AgentStatusOptions {
|
|
|
78
111
|
judgeStatus: string;
|
|
79
112
|
judgeModel?: string;
|
|
80
113
|
extra?: readonly string[];
|
|
114
|
+
/** Names the synthesis step for the reader. Merge runs the composer. */
|
|
115
|
+
synthesis?: FusionSynthesisMode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Lists the facets that no panelist covered, for a merge-mode failure. */
|
|
119
|
+
function formatUncoveredFacets(
|
|
120
|
+
panel?: readonly PanelMemberConfig[],
|
|
121
|
+
): string | string[] {
|
|
122
|
+
if (!panel?.length) return "Every configured facet is uncovered.";
|
|
123
|
+
return panel.map((member) => {
|
|
124
|
+
const facet =
|
|
125
|
+
member.question?.trim() ?? member.role?.trim() ?? "the whole task";
|
|
126
|
+
return `- ${memberLabel(member)}: ${facet} (uncovered)`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The synthesis-shaped sections for a report that has no synthesis output:
|
|
132
|
+
* failure, cancellation, or an all-failed panel. Select and merge need
|
|
133
|
+
* different section names, so every such renderer goes through here rather than
|
|
134
|
+
* hardcoding one shape.
|
|
135
|
+
*/
|
|
136
|
+
function emptySynthesisSections(input: {
|
|
137
|
+
synthesis?: FusionSynthesisMode;
|
|
138
|
+
reason: string;
|
|
139
|
+
/** Select-mode wording, passed verbatim so each caller keeps its own text. */
|
|
140
|
+
select: {
|
|
141
|
+
consensus: string;
|
|
142
|
+
disagreements: string;
|
|
143
|
+
uniqueInsights: string;
|
|
144
|
+
blindSpots: string;
|
|
145
|
+
};
|
|
146
|
+
panel?: readonly PanelMemberConfig[];
|
|
147
|
+
}): ReportSection[] {
|
|
148
|
+
if (input.synthesis === "merge") {
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
title: "Coverage Map",
|
|
152
|
+
content: `Nothing was covered because ${input.reason}.`,
|
|
153
|
+
},
|
|
154
|
+
{ title: "Combined Answer", content: "No answer is available." },
|
|
155
|
+
{ title: "Gaps", content: formatUncoveredFacets(input.panel) },
|
|
156
|
+
{
|
|
157
|
+
title: "Conflicts At Seams",
|
|
158
|
+
content: "No answers were produced, so no seams could conflict.",
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
return [
|
|
163
|
+
{ title: "Consensus", content: input.select.consensus },
|
|
164
|
+
{ title: "Disagreements", content: input.select.disagreements },
|
|
165
|
+
{ title: "Unique Insights", content: input.select.uniqueInsights },
|
|
166
|
+
{ title: "Blind Spots", content: input.select.blindSpots },
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What actually ran in the synthesis slot, for report labels. */
|
|
171
|
+
function synthesisLabel(synthesis?: FusionSynthesisMode): string {
|
|
172
|
+
return synthesis === "merge" ? "Composer" : "Judge";
|
|
81
173
|
}
|
|
82
174
|
|
|
83
175
|
export function renderPanelFailureReport(
|
|
84
176
|
input: RenderPanelFailureReportInput,
|
|
85
177
|
): string {
|
|
178
|
+
const synthesisName = synthesisLabel(input.synthesis);
|
|
179
|
+
// Under merge the select sections are meaningless: nobody answered the same
|
|
180
|
+
// question, so there is no consensus to be absent. What the reader needs is
|
|
181
|
+
// which facets went uncovered.
|
|
182
|
+
const emptySections = emptySynthesisSections({
|
|
183
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
184
|
+
reason: "all panelists failed",
|
|
185
|
+
select: {
|
|
186
|
+
consensus: "No consensus was available because all panelists failed.",
|
|
187
|
+
disagreements: `No disagreements were synthesized because the ${synthesisName.toLowerCase()} did not run.`,
|
|
188
|
+
uniqueInsights: "No panel output was available to summarize.",
|
|
189
|
+
blindSpots:
|
|
190
|
+
"All panelists failed, so the report may be missing every intended review perspective.",
|
|
191
|
+
},
|
|
192
|
+
...(input.panel ? { panel: input.panel } : {}),
|
|
193
|
+
});
|
|
86
194
|
return renderReport([
|
|
87
195
|
{
|
|
88
196
|
title: "Summary",
|
|
@@ -96,26 +204,10 @@ export function renderPanelFailureReport(
|
|
|
96
204
|
failures: input.failures,
|
|
97
205
|
judgeStatus: "not run - no successful panelists",
|
|
98
206
|
...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
|
|
207
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
99
208
|
}),
|
|
100
209
|
},
|
|
101
|
-
|
|
102
|
-
title: "Consensus",
|
|
103
|
-
content: "No consensus was available because all panelists failed.",
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
title: "Disagreements",
|
|
107
|
-
content:
|
|
108
|
-
"No disagreements were synthesized because the judge did not run.",
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
title: "Unique Insights",
|
|
112
|
-
content: "No panel output was available to summarize.",
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
title: "Blind Spots",
|
|
116
|
-
content:
|
|
117
|
-
"All panelists failed, so the report may be missing every intended review perspective.",
|
|
118
|
-
},
|
|
210
|
+
...emptySections,
|
|
119
211
|
{ title: "Recommendation", content: "No recommendation is available." },
|
|
120
212
|
{
|
|
121
213
|
title: "Risks",
|
|
@@ -195,15 +287,97 @@ export function renderSinglePanelReport(
|
|
|
195
287
|
return renderReport(sections);
|
|
196
288
|
}
|
|
197
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Rewrites the neutral names the judge saw back to the configured member names.
|
|
292
|
+
* Longest label first so "Candidate AA" is not partly replaced by "Candidate A".
|
|
293
|
+
*/
|
|
294
|
+
function restoreBlindLabels(
|
|
295
|
+
judgeOutput: string,
|
|
296
|
+
panelOutputs: readonly PanelOutput[],
|
|
297
|
+
failures: readonly FailedPanelSummary[],
|
|
298
|
+
): string {
|
|
299
|
+
const items = [...panelOutputs, ...failures];
|
|
300
|
+
const blindLabels = buildBlindLabelMap(items);
|
|
301
|
+
const realNames = new Map(
|
|
302
|
+
items.map((item) => [item.index, panelItemLabel(item)]),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
let restored = judgeOutput;
|
|
306
|
+
const ordered = [...blindLabels.entries()].sort(
|
|
307
|
+
(left, right) => right[1].length - left[1].length,
|
|
308
|
+
);
|
|
309
|
+
for (const [index, blindLabel] of ordered) {
|
|
310
|
+
const realName = realNames.get(index);
|
|
311
|
+
if (!realName) continue;
|
|
312
|
+
restored = restored.replaceAll(blindLabel, realName);
|
|
313
|
+
}
|
|
314
|
+
return restored;
|
|
315
|
+
}
|
|
316
|
+
|
|
198
317
|
export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
199
318
|
const panelOutputs = input.panelOutputs ?? [];
|
|
200
319
|
const failures = input.failures ?? [];
|
|
201
|
-
const
|
|
202
|
-
|
|
320
|
+
const judgeOutput = input.blindPanelLabels
|
|
321
|
+
? restoreBlindLabels(input.judgeOutput, panelOutputs, failures)
|
|
322
|
+
: input.judgeOutput;
|
|
323
|
+
const sections = parseMarkdownSections(judgeOutput);
|
|
324
|
+
const unsectionedOutput = stripReportTitle(judgeOutput);
|
|
203
325
|
const recommendationFallback =
|
|
204
326
|
sections.size === 0 && unsectionedOutput
|
|
205
327
|
? unsectionedOutput
|
|
206
|
-
:
|
|
328
|
+
: `${synthesisLabel(input.synthesis)} completed without a recommendation.`;
|
|
329
|
+
|
|
330
|
+
// Section titles differ by synthesis mode; everything around them - agent
|
|
331
|
+
// status, metadata, run details - is shared.
|
|
332
|
+
const merging = input.synthesis === "merge";
|
|
333
|
+
const synthesisSections: ReportSection[] = merging
|
|
334
|
+
? [
|
|
335
|
+
{
|
|
336
|
+
title: "Coverage Map",
|
|
337
|
+
content:
|
|
338
|
+
sections.get("Coverage Map") ?? "Not specified by the composer.",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
title: "Combined Answer",
|
|
342
|
+
content:
|
|
343
|
+
sections.get("Combined Answer") ?? "Not specified by the composer.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
title: "Gaps",
|
|
347
|
+
content: sections.get("Gaps") ?? "Not specified by the composer.",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
title: "Conflicts At Seams",
|
|
351
|
+
content:
|
|
352
|
+
sections.get("Conflicts At Seams") ??
|
|
353
|
+
"Not specified by the composer.",
|
|
354
|
+
},
|
|
355
|
+
]
|
|
356
|
+
: [
|
|
357
|
+
{
|
|
358
|
+
title: "Consensus",
|
|
359
|
+
content: sections.get("Consensus") ?? "Not specified by the judge.",
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
title: "Disagreements",
|
|
363
|
+
content:
|
|
364
|
+
sections.get("Disagreements") ?? "Not specified by the judge.",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
title: "Contested Claims",
|
|
368
|
+
content:
|
|
369
|
+
sections.get("Contested Claims") ?? "Not specified by the judge.",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
title: "Unique Insights",
|
|
373
|
+
content:
|
|
374
|
+
sections.get("Unique Insights") ?? "Not specified by the judge.",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
title: "Blind Spots",
|
|
378
|
+
content: sections.get("Blind Spots") ?? "Not specified by the judge.",
|
|
379
|
+
},
|
|
380
|
+
];
|
|
207
381
|
|
|
208
382
|
const reportSections: ReportSection[] = [
|
|
209
383
|
{
|
|
@@ -217,24 +391,10 @@ export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
|
217
391
|
failures,
|
|
218
392
|
judgeStatus: "succeeded",
|
|
219
393
|
...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
|
|
394
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
220
395
|
}),
|
|
221
396
|
},
|
|
222
|
-
|
|
223
|
-
title: "Consensus",
|
|
224
|
-
content: sections.get("Consensus") ?? "Not specified by the judge.",
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
title: "Disagreements",
|
|
228
|
-
content: sections.get("Disagreements") ?? "Not specified by the judge.",
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
title: "Unique Insights",
|
|
232
|
-
content: sections.get("Unique Insights") ?? "Not specified by the judge.",
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
title: "Blind Spots",
|
|
236
|
-
content: sections.get("Blind Spots") ?? "Not specified by the judge.",
|
|
237
|
-
},
|
|
397
|
+
...synthesisSections,
|
|
238
398
|
{
|
|
239
399
|
title: "Recommendation",
|
|
240
400
|
content: sections.get("Recommendation") ?? recommendationFallback,
|
|
@@ -258,6 +418,7 @@ export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
|
258
418
|
...(input.judgeObservation
|
|
259
419
|
? { judgeObservation: input.judgeObservation }
|
|
260
420
|
: {}),
|
|
421
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
261
422
|
});
|
|
262
423
|
if (runDetails) reportSections.splice(-1, 0, runDetails);
|
|
263
424
|
return renderReport(reportSections);
|
|
@@ -282,23 +443,20 @@ export function renderFailureReport(input: RenderFailureReportInput): string {
|
|
|
282
443
|
extra: [`- Phase: ${phase}`],
|
|
283
444
|
}),
|
|
284
445
|
},
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
content:
|
|
300
|
-
"The failure may hide panel disagreements, missing evidence, or provider-specific errors.",
|
|
301
|
-
},
|
|
446
|
+
...emptySynthesisSections({
|
|
447
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
448
|
+
reason: "fusion failed",
|
|
449
|
+
select: {
|
|
450
|
+
consensus: "No consensus was available because fusion failed.",
|
|
451
|
+
disagreements:
|
|
452
|
+
"No disagreements were synthesized because fusion failed.",
|
|
453
|
+
uniqueInsights:
|
|
454
|
+
"No unique insights were synthesized because fusion failed.",
|
|
455
|
+
blindSpots:
|
|
456
|
+
"The failure may hide panel disagreements, missing evidence, or provider-specific errors.",
|
|
457
|
+
},
|
|
458
|
+
...(input.panel ? { panel: input.panel } : {}),
|
|
459
|
+
}),
|
|
302
460
|
{ title: "Recommendation", content: "No recommendation is available." },
|
|
303
461
|
{
|
|
304
462
|
title: "Risks",
|
|
@@ -334,25 +492,21 @@ export function renderCancelledReport(
|
|
|
334
492
|
],
|
|
335
493
|
}),
|
|
336
494
|
},
|
|
337
|
-
{
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
title: "Blind Spots",
|
|
353
|
-
content:
|
|
354
|
-
"Cancellation may leave in-flight panel or judge output incomplete.",
|
|
355
|
-
},
|
|
495
|
+
...emptySynthesisSections({
|
|
496
|
+
...(input.synthesis ? { synthesis: input.synthesis } : {}),
|
|
497
|
+
reason: "fusion was cancelled",
|
|
498
|
+
select: {
|
|
499
|
+
consensus:
|
|
500
|
+
"No final consensus was available because fusion was cancelled.",
|
|
501
|
+
disagreements:
|
|
502
|
+
"No final disagreements were synthesized because fusion was cancelled.",
|
|
503
|
+
uniqueInsights:
|
|
504
|
+
"No final unique insights were synthesized because fusion was cancelled.",
|
|
505
|
+
blindSpots:
|
|
506
|
+
"Cancellation may leave in-flight panel or judge output incomplete.",
|
|
507
|
+
},
|
|
508
|
+
...(input.panel ? { panel: input.panel } : {}),
|
|
509
|
+
}),
|
|
356
510
|
{ title: "Recommendation", content: "No recommendation is available." },
|
|
357
511
|
{
|
|
358
512
|
title: "Risks",
|
|
@@ -387,6 +541,7 @@ interface RunDetailsInput {
|
|
|
387
541
|
failures: readonly FailedPanelSummary[];
|
|
388
542
|
judgeModel?: string;
|
|
389
543
|
judgeObservation?: RunObservation;
|
|
544
|
+
synthesis?: FusionSynthesisMode;
|
|
390
545
|
}
|
|
391
546
|
|
|
392
547
|
function formatRunDetails(input: RunDetailsInput): ReportSection | undefined {
|
|
@@ -406,7 +561,7 @@ function formatRunDetails(input: RunDetailsInput): ReportSection | undefined {
|
|
|
406
561
|
];
|
|
407
562
|
if (input.judgeObservation) {
|
|
408
563
|
entries.push({
|
|
409
|
-
label:
|
|
564
|
+
label: synthesisLabel(input.synthesis),
|
|
410
565
|
status: "completed",
|
|
411
566
|
configuredModel: input.judgeModel,
|
|
412
567
|
observation: input.judgeObservation,
|
|
@@ -518,7 +673,7 @@ function formatAgentStatus(options: AgentStatusOptions): string[] {
|
|
|
518
673
|
lines.push("- Panel status: not available");
|
|
519
674
|
}
|
|
520
675
|
|
|
521
|
-
lines.push(`-
|
|
676
|
+
lines.push(`- ${synthesisLabel(options.synthesis)}: ${options.judgeStatus}`);
|
|
522
677
|
if (options.judgeModel) {
|
|
523
678
|
lines.push(` Configured model: ${options.judgeModel}`);
|
|
524
679
|
}
|
|
@@ -638,7 +793,7 @@ function plural(count: number, singular: string): string {
|
|
|
638
793
|
function formatPanelName(
|
|
639
794
|
item: Pick<PanelOutput, "index" | "id" | "label">,
|
|
640
795
|
): string {
|
|
641
|
-
return item
|
|
796
|
+
return panelItemLabel(item);
|
|
642
797
|
}
|
|
643
798
|
|
|
644
799
|
function comparePanelItems(
|
package/src/result-extract.ts
CHANGED
|
@@ -75,6 +75,20 @@ export function extractPanelResults(
|
|
|
75
75
|
else failures.push(child.failure);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
for (const index of options.stoppedPanelIndices ?? []) {
|
|
79
|
+
if (index < results.length || index >= (options.limit ?? Infinity)) continue;
|
|
80
|
+
const child = normalizeChildResult(
|
|
81
|
+
{
|
|
82
|
+
success: false,
|
|
83
|
+
error: "Stopped after strong panel agreement.",
|
|
84
|
+
},
|
|
85
|
+
index,
|
|
86
|
+
options,
|
|
87
|
+
);
|
|
88
|
+
if (!child.ok) return child;
|
|
89
|
+
if (child.status === "failed") failures.push(child.failure);
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
const runId = firstString(container.payload.runId, container.payload.id);
|
|
79
93
|
return {
|
|
80
94
|
ok: true,
|
|
@@ -197,7 +211,7 @@ function normalizeChildResult(
|
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
const member = options.panel?.[index];
|
|
200
|
-
const agent = firstString(
|
|
214
|
+
const agent = firstString(member?.agent, rawResult.agent);
|
|
201
215
|
if (!agent) {
|
|
202
216
|
return error(
|
|
203
217
|
"missing-result-field",
|