@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
package/src/report.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { FailedPanelSummary, PanelOutput } from "./run-builder.js";
|
|
2
|
-
import
|
|
2
|
+
import { summarizeProviderFailures } from "./run-observations.js";
|
|
3
|
+
import type { FusionRun, ProviderFailure, RunObservation } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type ReportRun = Pick<
|
|
5
6
|
FusionRun,
|
|
6
|
-
|
|
7
|
+
| "id"
|
|
8
|
+
| "prompt"
|
|
9
|
+
| "profileName"
|
|
10
|
+
| "chainRunId"
|
|
11
|
+
| "panelRunId"
|
|
12
|
+
| "judgeRunId"
|
|
13
|
+
| "panelStopReason"
|
|
7
14
|
> &
|
|
8
15
|
Partial<Pick<FusionRun, "phase" | "createdAt" | "updatedAt">>;
|
|
9
16
|
|
|
@@ -27,6 +34,7 @@ export interface RenderJudgeReportInput {
|
|
|
27
34
|
panelOutputs?: readonly PanelOutput[];
|
|
28
35
|
failures?: readonly FailedPanelSummary[];
|
|
29
36
|
judgeModel?: string;
|
|
37
|
+
judgeObservation?: RunObservation;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export interface RenderFailureReportInput {
|
|
@@ -56,6 +64,7 @@ type ReportSectionTitle =
|
|
|
56
64
|
| "Recommendation"
|
|
57
65
|
| "Risks"
|
|
58
66
|
| "Next Step"
|
|
67
|
+
| "Run Details"
|
|
59
68
|
| "Run Metadata";
|
|
60
69
|
|
|
61
70
|
interface ReportSection {
|
|
@@ -127,7 +136,7 @@ export function renderSinglePanelReport(
|
|
|
127
136
|
input: RenderSinglePanelReportInput,
|
|
128
137
|
): string {
|
|
129
138
|
const panelName = formatPanelName(input.output);
|
|
130
|
-
|
|
139
|
+
const sections: ReportSection[] = [
|
|
131
140
|
{
|
|
132
141
|
title: "Summary",
|
|
133
142
|
content:
|
|
@@ -177,7 +186,13 @@ export function renderSinglePanelReport(
|
|
|
177
186
|
"Use this single-panel result directly, or rerun /fusion if you need judge synthesis.",
|
|
178
187
|
},
|
|
179
188
|
{ title: "Run Metadata", content: formatRunMetadata(input.run) },
|
|
180
|
-
]
|
|
189
|
+
];
|
|
190
|
+
const runDetails = formatRunDetails({
|
|
191
|
+
panelOutputs: [input.output],
|
|
192
|
+
failures: input.failures,
|
|
193
|
+
});
|
|
194
|
+
if (runDetails) sections.splice(-1, 0, runDetails);
|
|
195
|
+
return renderReport(sections);
|
|
181
196
|
}
|
|
182
197
|
|
|
183
198
|
export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
@@ -190,7 +205,7 @@ export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
|
190
205
|
? unsectionedOutput
|
|
191
206
|
: "Judge completed without a recommendation.";
|
|
192
207
|
|
|
193
|
-
|
|
208
|
+
const reportSections: ReportSection[] = [
|
|
194
209
|
{
|
|
195
210
|
title: "Summary",
|
|
196
211
|
content: sections.get("Summary") ?? judgeSummary(panelOutputs, failures),
|
|
@@ -235,7 +250,17 @@ export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
|
235
250
|
"Review the recommendation and decide whether to act on it.",
|
|
236
251
|
},
|
|
237
252
|
{ title: "Run Metadata", content: formatRunMetadata(input.run) },
|
|
238
|
-
]
|
|
253
|
+
];
|
|
254
|
+
const runDetails = formatRunDetails({
|
|
255
|
+
panelOutputs,
|
|
256
|
+
failures,
|
|
257
|
+
...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
|
|
258
|
+
...(input.judgeObservation
|
|
259
|
+
? { judgeObservation: input.judgeObservation }
|
|
260
|
+
: {}),
|
|
261
|
+
});
|
|
262
|
+
if (runDetails) reportSections.splice(-1, 0, runDetails);
|
|
263
|
+
return renderReport(reportSections);
|
|
239
264
|
}
|
|
240
265
|
|
|
241
266
|
export function renderFailureReport(input: RenderFailureReportInput): string {
|
|
@@ -357,6 +382,118 @@ function formatSectionContent(content: string | readonly string[]): string {
|
|
|
357
382
|
return text.trim() || "None.";
|
|
358
383
|
}
|
|
359
384
|
|
|
385
|
+
interface RunDetailsInput {
|
|
386
|
+
panelOutputs: readonly PanelOutput[];
|
|
387
|
+
failures: readonly FailedPanelSummary[];
|
|
388
|
+
judgeModel?: string;
|
|
389
|
+
judgeObservation?: RunObservation;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function formatRunDetails(input: RunDetailsInput): ReportSection | undefined {
|
|
393
|
+
const entries = [
|
|
394
|
+
...input.panelOutputs.map((item) => ({
|
|
395
|
+
label: formatPanelName(item),
|
|
396
|
+
status: "completed",
|
|
397
|
+
configuredModel: item.configuredModel ?? item.model,
|
|
398
|
+
observation: item.observation,
|
|
399
|
+
})),
|
|
400
|
+
...input.failures.map((item) => ({
|
|
401
|
+
label: formatPanelName(item),
|
|
402
|
+
status: item.reason === "stopped-after-agreement" ? "stopped" : "failed",
|
|
403
|
+
configuredModel: item.configuredModel ?? item.model,
|
|
404
|
+
observation: item.observation,
|
|
405
|
+
})),
|
|
406
|
+
];
|
|
407
|
+
if (input.judgeObservation) {
|
|
408
|
+
entries.push({
|
|
409
|
+
label: "Judge",
|
|
410
|
+
status: "completed",
|
|
411
|
+
configuredModel: input.judgeModel,
|
|
412
|
+
observation: input.judgeObservation,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if (!entries.some((entry) => entry.observation)) return undefined;
|
|
416
|
+
|
|
417
|
+
const observations = entries.map((entry) => entry.observation ?? {});
|
|
418
|
+
const providerFailures = summarizeProviderFailures(
|
|
419
|
+
observations.flatMap((observation) => observation.providerFailures ?? []),
|
|
420
|
+
);
|
|
421
|
+
const lines = entries.map(
|
|
422
|
+
(entry) =>
|
|
423
|
+
`- ${entry.label} (${entry.status}): ${formatObservation(entry.observation ?? {}, entry.configuredModel)}`,
|
|
424
|
+
);
|
|
425
|
+
lines.push(
|
|
426
|
+
`- Aggregate model time: ${formatTotal(observations, (observation) => observation.durationMs, formatDuration)}`,
|
|
427
|
+
`- Total input tokens: ${formatTotal(observations, (observation) => observation.usage?.inputTokens, formatTokens)}`,
|
|
428
|
+
`- Total output tokens: ${formatTotal(observations, (observation) => observation.usage?.outputTokens, formatTokens)}`,
|
|
429
|
+
`- Total estimated cost: ${formatTotal(observations, (observation) => observation.usage?.costUsd, formatCost)}`,
|
|
430
|
+
);
|
|
431
|
+
if (providerFailures.length > 0) {
|
|
432
|
+
lines.push("- Model issues:");
|
|
433
|
+
lines.push(
|
|
434
|
+
...providerFailures.map(
|
|
435
|
+
(failure) => ` - ${formatProviderFailure(failure)}`,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return { title: "Run Details", content: lines };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function formatObservation(
|
|
443
|
+
observation: RunObservation,
|
|
444
|
+
configuredModel?: string,
|
|
445
|
+
): string {
|
|
446
|
+
return [
|
|
447
|
+
observation.model ??
|
|
448
|
+
(configuredModel ? `${configuredModel} (configured)` : "model unknown"),
|
|
449
|
+
observation.durationMs !== undefined
|
|
450
|
+
? formatDuration(observation.durationMs)
|
|
451
|
+
: "time unknown",
|
|
452
|
+
formatUsage(observation),
|
|
453
|
+
].join(" · ");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatUsage(observation: RunObservation): string {
|
|
457
|
+
const input = observation.usage?.inputTokens;
|
|
458
|
+
const output = observation.usage?.outputTokens;
|
|
459
|
+
const cost = observation.usage?.costUsd;
|
|
460
|
+
return [
|
|
461
|
+
input !== undefined ? `in ${formatTokens(input)}` : "input unknown",
|
|
462
|
+
output !== undefined ? `out ${formatTokens(output)}` : "output unknown",
|
|
463
|
+
cost !== undefined ? formatCost(cost) : "cost unknown",
|
|
464
|
+
].join(", ");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function formatProviderFailure(failure: ProviderFailure): string {
|
|
468
|
+
const target = failure.model
|
|
469
|
+
? `${failure.provider}/${failure.model.split("/").slice(1).join("/")}`
|
|
470
|
+
: failure.provider;
|
|
471
|
+
return `${target}: ${failure.message}${failure.count && failure.count > 1 ? ` (x${failure.count})` : ""}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function formatTotal(
|
|
475
|
+
observations: readonly RunObservation[],
|
|
476
|
+
read: (observation: RunObservation) => number | undefined,
|
|
477
|
+
format: (value: number) => string,
|
|
478
|
+
): string {
|
|
479
|
+
const values = observations.map(read);
|
|
480
|
+
if (values.some((value) => value === undefined)) return "unknown";
|
|
481
|
+
const total = values.reduce<number>((sum, value) => sum + (value ?? 0), 0);
|
|
482
|
+
return format(total);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function formatDuration(value: number): string {
|
|
486
|
+
return value >= 100 ? `${(value / 1000).toFixed(1)}s` : `${value}ms`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function formatTokens(value: number): string {
|
|
490
|
+
return value.toLocaleString("en-US");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function formatCost(value: number): string {
|
|
494
|
+
return `$${value.toFixed(4)}`;
|
|
495
|
+
}
|
|
496
|
+
|
|
360
497
|
function formatAgentStatus(options: AgentStatusOptions): string[] {
|
|
361
498
|
const hasPanelStatus =
|
|
362
499
|
options.panelOutputs !== undefined || options.failures !== undefined;
|
|
@@ -382,7 +519,9 @@ function formatAgentStatus(options: AgentStatusOptions): string[] {
|
|
|
382
519
|
}
|
|
383
520
|
|
|
384
521
|
lines.push(`- Judge: ${options.judgeStatus}`);
|
|
385
|
-
if (options.judgeModel)
|
|
522
|
+
if (options.judgeModel) {
|
|
523
|
+
lines.push(` Configured model: ${options.judgeModel}`);
|
|
524
|
+
}
|
|
386
525
|
if (options.extra) lines.push(...options.extra);
|
|
387
526
|
return lines;
|
|
388
527
|
}
|
|
@@ -390,13 +529,28 @@ function formatAgentStatus(options: AgentStatusOptions): string[] {
|
|
|
390
529
|
function formatPanelDetails(
|
|
391
530
|
item: Pick<
|
|
392
531
|
PanelOutput,
|
|
393
|
-
|
|
532
|
+
| "agent"
|
|
533
|
+
| "role"
|
|
534
|
+
| "model"
|
|
535
|
+
| "configuredModel"
|
|
536
|
+
| "observation"
|
|
537
|
+
| "artifactPath"
|
|
538
|
+
| "sessionPath"
|
|
394
539
|
>,
|
|
395
540
|
): string[] {
|
|
396
541
|
return [
|
|
397
542
|
` Agent: ${item.agent}`,
|
|
398
543
|
...(item.role ? [` Role: ${item.role}`] : []),
|
|
399
|
-
...(item.model
|
|
544
|
+
...(item.observation?.model
|
|
545
|
+
? [` Model: ${item.observation.model}`]
|
|
546
|
+
: (item.configuredModel ?? item.model)
|
|
547
|
+
? [` Configured model: ${item.configuredModel ?? item.model}`]
|
|
548
|
+
: []),
|
|
549
|
+
...(item.configuredModel &&
|
|
550
|
+
item.observation?.model &&
|
|
551
|
+
item.configuredModel !== item.observation.model
|
|
552
|
+
? [` Configured model: ${item.configuredModel}`]
|
|
553
|
+
: []),
|
|
400
554
|
...(item.artifactPath ? [` Artifact: ${item.artifactPath}`] : []),
|
|
401
555
|
...(item.sessionPath ? [` Session: ${item.sessionPath}`] : []),
|
|
402
556
|
];
|
|
@@ -410,7 +564,14 @@ function formatRunMetadata(run: ReportRun): string[] {
|
|
|
410
564
|
`- Prompt: ${firstLine(run.prompt)}`,
|
|
411
565
|
...(run.chainRunId ? [`- Chain run: ${run.chainRunId}`] : []),
|
|
412
566
|
...(run.panelRunId ? [`- Panel run: ${run.panelRunId}`] : []),
|
|
413
|
-
...(run.
|
|
567
|
+
...(run.panelStopReason === "agreement"
|
|
568
|
+
? ["- Panel stopped after strong agreement"]
|
|
569
|
+
: []),
|
|
570
|
+
...(run.judgeRunId
|
|
571
|
+
? [
|
|
572
|
+
`- ${run.chainRunId ? "Fallback judge run" : "Judge run"}: ${run.judgeRunId}`,
|
|
573
|
+
]
|
|
574
|
+
: []),
|
|
414
575
|
...(typeof run.createdAt === "number"
|
|
415
576
|
? [`- Created: ${formatTimestamp(run.createdAt)}`]
|
|
416
577
|
: []),
|
package/src/result-extract.ts
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
type FailedPanelSummary,
|
|
4
4
|
type PanelOutput,
|
|
5
5
|
} from "./run-builder.js";
|
|
6
|
+
import {
|
|
7
|
+
extractPanelDecision,
|
|
8
|
+
extractRunObservation,
|
|
9
|
+
} from "./run-observations.js";
|
|
6
10
|
import type { PanelMemberConfig } from "./types.js";
|
|
7
11
|
|
|
8
12
|
export type ResultExtractErrorCode =
|
|
@@ -17,16 +21,19 @@ export interface ResultExtractError {
|
|
|
17
21
|
export interface ExtractPanelResultsOptions {
|
|
18
22
|
panel?: readonly PanelMemberConfig[];
|
|
19
23
|
limit?: number;
|
|
24
|
+
completedOnly?: boolean;
|
|
25
|
+
stoppedPanelIndices?: readonly number[];
|
|
20
26
|
}
|
|
21
27
|
|
|
28
|
+
export type ExtractPanelResultsSuccess = {
|
|
29
|
+
ok: true;
|
|
30
|
+
outputs: PanelOutput[];
|
|
31
|
+
failures: FailedPanelSummary[];
|
|
32
|
+
runId?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
22
35
|
export type ExtractPanelResultsResult =
|
|
23
|
-
| {
|
|
24
|
-
ok: true;
|
|
25
|
-
outputs: PanelOutput[];
|
|
26
|
-
failures: FailedPanelSummary[];
|
|
27
|
-
runId?: string;
|
|
28
|
-
}
|
|
29
|
-
| { ok: false; error: ResultExtractError };
|
|
36
|
+
ExtractPanelResultsSuccess | { ok: false; error: ResultExtractError };
|
|
30
37
|
|
|
31
38
|
interface ResultsContainer {
|
|
32
39
|
payload: Record<string, unknown>;
|
|
@@ -36,6 +43,17 @@ interface ResultsContainer {
|
|
|
36
43
|
|
|
37
44
|
type ChildStatus = "success" | "failed";
|
|
38
45
|
|
|
46
|
+
function isCompletedResult(value: unknown): boolean {
|
|
47
|
+
if (!isRecord(value)) return false;
|
|
48
|
+
const status = firstString(value.status, value.state);
|
|
49
|
+
return !(
|
|
50
|
+
status === "running" ||
|
|
51
|
+
status === "active" ||
|
|
52
|
+
status === "pending" ||
|
|
53
|
+
status === "queued"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export function extractPanelResults(
|
|
40
58
|
payload: unknown,
|
|
41
59
|
options: ExtractPanelResultsOptions = {},
|
|
@@ -50,6 +68,7 @@ export function extractPanelResults(
|
|
|
50
68
|
? container.results
|
|
51
69
|
: container.results.slice(0, options.limit);
|
|
52
70
|
for (const [index, rawResult] of results.entries()) {
|
|
71
|
+
if (options.completedOnly && !isCompletedResult(rawResult)) continue;
|
|
53
72
|
const child = normalizeChildResult(rawResult, index, options);
|
|
54
73
|
if (!child.ok) return child;
|
|
55
74
|
if (child.status === "success") outputs.push(child.output);
|
|
@@ -78,11 +97,32 @@ function findResultsContainer(
|
|
|
78
97
|
);
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
if (Array.isArray(payload.results) && payload.results.length > 0) {
|
|
101
|
+
return { ok: true, payload, results: payload.results, path: "$.results" };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
isRecord(payload.details) &&
|
|
106
|
+
Array.isArray(payload.details.results) &&
|
|
107
|
+
payload.details.results.length > 0
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
payload: { ...payload, ...payload.details },
|
|
112
|
+
results: payload.details.results,
|
|
113
|
+
path: "$.details.results",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(payload.steps)) {
|
|
118
|
+
return { ok: true, payload, results: payload.steps, path: "$.steps" };
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
if (Array.isArray(payload.results)) {
|
|
82
122
|
return { ok: true, payload, results: payload.results, path: "$.results" };
|
|
83
123
|
}
|
|
84
124
|
|
|
85
|
-
if ("results" in payload) {
|
|
125
|
+
if ("results" in payload && !Array.isArray(payload.results)) {
|
|
86
126
|
return error(
|
|
87
127
|
"unknown-result-shape",
|
|
88
128
|
"Subagents result payload results field must be an array.",
|
|
@@ -91,6 +131,25 @@ function findResultsContainer(
|
|
|
91
131
|
}
|
|
92
132
|
|
|
93
133
|
if (isRecord(payload.details)) {
|
|
134
|
+
if (
|
|
135
|
+
Array.isArray(payload.details.results) &&
|
|
136
|
+
payload.details.results.length > 0
|
|
137
|
+
) {
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
payload: { ...payload, ...payload.details },
|
|
141
|
+
results: payload.details.results,
|
|
142
|
+
path: "$.details.results",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (Array.isArray(payload.details.steps)) {
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
payload: { ...payload, ...payload.details },
|
|
149
|
+
results: payload.details.steps,
|
|
150
|
+
path: "$.details.steps",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
94
153
|
if (Array.isArray(payload.details.results)) {
|
|
95
154
|
return {
|
|
96
155
|
ok: true,
|
|
@@ -99,7 +158,10 @@ function findResultsContainer(
|
|
|
99
158
|
path: "$.details.results",
|
|
100
159
|
};
|
|
101
160
|
}
|
|
102
|
-
if (
|
|
161
|
+
if (
|
|
162
|
+
"results" in payload.details &&
|
|
163
|
+
!Array.isArray(payload.details.results)
|
|
164
|
+
) {
|
|
103
165
|
return error(
|
|
104
166
|
"unknown-result-shape",
|
|
105
167
|
"Subagents result details.results field must be an array.",
|
|
@@ -149,13 +211,19 @@ function normalizeChildResult(
|
|
|
149
211
|
const status = classifyChildStatus(rawResult);
|
|
150
212
|
|
|
151
213
|
if (status === "success") {
|
|
214
|
+
const rawOutput = firstNonBlankString(
|
|
215
|
+
rawResult.output,
|
|
216
|
+
rawResult.finalOutput,
|
|
217
|
+
rawResult.summary,
|
|
218
|
+
rawResult.text,
|
|
219
|
+
recentOutputText(rawResult.recentOutput),
|
|
220
|
+
);
|
|
221
|
+
const decision =
|
|
222
|
+
extractPanelDecision(rawResult.structuredOutput) ??
|
|
223
|
+
extractPanelDecision(rawOutput);
|
|
152
224
|
const output =
|
|
153
|
-
firstNonBlankString(
|
|
154
|
-
|
|
155
|
-
rawResult.finalOutput,
|
|
156
|
-
rawResult.summary,
|
|
157
|
-
rawResult.text,
|
|
158
|
-
) ?? artifactOutput(artifactPath);
|
|
225
|
+
firstNonBlankString(decision?.answerMarkdown, rawOutput) ??
|
|
226
|
+
artifactOutput(artifactPath);
|
|
159
227
|
if (!output) {
|
|
160
228
|
return error(
|
|
161
229
|
"missing-result-field",
|
|
@@ -171,12 +239,18 @@ function normalizeChildResult(
|
|
|
171
239
|
member,
|
|
172
240
|
agent,
|
|
173
241
|
output,
|
|
242
|
+
decision,
|
|
243
|
+
observation: extractRunObservation(rawResult),
|
|
174
244
|
artifactPath,
|
|
175
245
|
sessionPath,
|
|
176
246
|
}),
|
|
177
247
|
};
|
|
178
248
|
}
|
|
179
249
|
|
|
250
|
+
const stoppedAfterAgreement =
|
|
251
|
+
options.stoppedPanelIndices?.includes(index) === true;
|
|
252
|
+
const observation = extractRunObservation(rawResult);
|
|
253
|
+
if (stoppedAfterAgreement) delete observation.providerFailures;
|
|
180
254
|
return {
|
|
181
255
|
ok: true,
|
|
182
256
|
status,
|
|
@@ -184,7 +258,11 @@ function normalizeChildResult(
|
|
|
184
258
|
index,
|
|
185
259
|
member,
|
|
186
260
|
agent,
|
|
187
|
-
summary:
|
|
261
|
+
summary: stoppedAfterAgreement
|
|
262
|
+
? "Stopped after strong panel agreement."
|
|
263
|
+
: failureSummary(rawResult, artifactPath),
|
|
264
|
+
reason: failureReason(rawResult, stoppedAfterAgreement),
|
|
265
|
+
observation,
|
|
188
266
|
artifactPath,
|
|
189
267
|
sessionPath,
|
|
190
268
|
}),
|
|
@@ -238,13 +316,14 @@ function buildPanelOutput(input: {
|
|
|
238
316
|
member: PanelMemberConfig | undefined;
|
|
239
317
|
agent: string;
|
|
240
318
|
output: string;
|
|
319
|
+
decision: PanelOutput["decision"];
|
|
320
|
+
observation: PanelOutput["observation"];
|
|
241
321
|
artifactPath: string | undefined;
|
|
242
322
|
sessionPath: string | undefined;
|
|
243
323
|
}): PanelOutput {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return {
|
|
324
|
+
const configuredModel = configuredMemberModel(input.member);
|
|
325
|
+
const model = input.observation?.model ?? configuredModel;
|
|
326
|
+
const output: PanelOutput = {
|
|
248
327
|
index: input.index,
|
|
249
328
|
agent: input.agent,
|
|
250
329
|
output: input.output,
|
|
@@ -252,9 +331,15 @@ function buildPanelOutput(input: {
|
|
|
252
331
|
...(input.member?.label ? { label: input.member.label } : {}),
|
|
253
332
|
...(input.member?.role ? { role: input.member.role } : {}),
|
|
254
333
|
...(model ? { model } : {}),
|
|
334
|
+
...(configuredModel ? { configuredModel } : {}),
|
|
335
|
+
...(input.decision ? { decision: input.decision } : {}),
|
|
255
336
|
...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
|
|
256
337
|
...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
|
|
257
338
|
};
|
|
339
|
+
if (hasObservation(input.observation)) {
|
|
340
|
+
output.observation = input.observation;
|
|
341
|
+
}
|
|
342
|
+
return output;
|
|
258
343
|
}
|
|
259
344
|
|
|
260
345
|
function buildFailedPanelSummary(input: {
|
|
@@ -262,13 +347,14 @@ function buildFailedPanelSummary(input: {
|
|
|
262
347
|
member: PanelMemberConfig | undefined;
|
|
263
348
|
agent: string;
|
|
264
349
|
summary: string;
|
|
350
|
+
reason: FailedPanelSummary["reason"];
|
|
351
|
+
observation: FailedPanelSummary["observation"];
|
|
265
352
|
artifactPath: string | undefined;
|
|
266
353
|
sessionPath: string | undefined;
|
|
267
354
|
}): FailedPanelSummary {
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return {
|
|
355
|
+
const configuredModel = configuredMemberModel(input.member);
|
|
356
|
+
const model = input.observation?.model ?? configuredModel;
|
|
357
|
+
const failure: FailedPanelSummary = {
|
|
272
358
|
index: input.index,
|
|
273
359
|
agent: input.agent,
|
|
274
360
|
summary: input.summary,
|
|
@@ -276,9 +362,46 @@ function buildFailedPanelSummary(input: {
|
|
|
276
362
|
...(input.member?.label ? { label: input.member.label } : {}),
|
|
277
363
|
...(input.member?.role ? { role: input.member.role } : {}),
|
|
278
364
|
...(model ? { model } : {}),
|
|
365
|
+
...(configuredModel ? { configuredModel } : {}),
|
|
366
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
279
367
|
...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
|
|
280
368
|
...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
|
|
281
369
|
};
|
|
370
|
+
if (hasObservation(input.observation)) {
|
|
371
|
+
failure.observation = input.observation;
|
|
372
|
+
}
|
|
373
|
+
return failure;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function failureReason(
|
|
377
|
+
result: Record<string, unknown>,
|
|
378
|
+
stoppedAfterAgreement = false,
|
|
379
|
+
): FailedPanelSummary["reason"] {
|
|
380
|
+
if (stoppedAfterAgreement) return "stopped-after-agreement";
|
|
381
|
+
if (result.timedOut === true) return "timeout";
|
|
382
|
+
if (result.interrupted === true) return "interrupted";
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function hasObservation(
|
|
387
|
+
observation: PanelOutput["observation"] | undefined,
|
|
388
|
+
): observation is NonNullable<PanelOutput["observation"]> {
|
|
389
|
+
return Boolean(
|
|
390
|
+
observation &&
|
|
391
|
+
(observation.model ||
|
|
392
|
+
observation.durationMs !== undefined ||
|
|
393
|
+
observation.usage ||
|
|
394
|
+
observation.attempts ||
|
|
395
|
+
observation.providerFailures),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function configuredMemberModel(
|
|
400
|
+
member: PanelMemberConfig | undefined,
|
|
401
|
+
): string | undefined {
|
|
402
|
+
return member
|
|
403
|
+
? appendThinkingSuffix(member.model, member.thinking)
|
|
404
|
+
: undefined;
|
|
282
405
|
}
|
|
283
406
|
|
|
284
407
|
function extractArtifactPath(
|
|
@@ -306,6 +429,16 @@ function firstString(...values: readonly unknown[]): string | undefined {
|
|
|
306
429
|
return undefined;
|
|
307
430
|
}
|
|
308
431
|
|
|
432
|
+
function recentOutputText(value: unknown): string | undefined {
|
|
433
|
+
if (
|
|
434
|
+
!Array.isArray(value) ||
|
|
435
|
+
!value.every((item) => typeof item === "string")
|
|
436
|
+
) {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
return value.join("\n").trim() || undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
309
442
|
function firstNonBlankString(
|
|
310
443
|
...values: readonly unknown[]
|
|
311
444
|
): string | undefined {
|
package/src/run-builder.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PANEL_DECISION_CLOSE,
|
|
3
|
+
PANEL_DECISION_OPEN,
|
|
4
|
+
} from "./run-observations.js";
|
|
1
5
|
import {
|
|
2
6
|
THINKING_LEVELS,
|
|
3
7
|
type FailedPanelSummary,
|
|
@@ -133,7 +137,13 @@ export function buildPanelSpawnParams(
|
|
|
133
137
|
prompt: string,
|
|
134
138
|
): PanelSpawnParams {
|
|
135
139
|
return {
|
|
136
|
-
tasks: profile.panel.map((member) =>
|
|
140
|
+
tasks: profile.panel.map((member) =>
|
|
141
|
+
buildPanelTaskParams(
|
|
142
|
+
member,
|
|
143
|
+
prompt,
|
|
144
|
+
profile.stopWhenPanelAgrees === true,
|
|
145
|
+
),
|
|
146
|
+
),
|
|
137
147
|
async: true,
|
|
138
148
|
clarify: false,
|
|
139
149
|
concurrency: profile.concurrency ?? profile.panel.length,
|
|
@@ -216,11 +226,12 @@ export function buildJudgeSpawnParams(
|
|
|
216
226
|
function buildPanelTaskParams(
|
|
217
227
|
member: PanelMemberConfig,
|
|
218
228
|
prompt: string,
|
|
229
|
+
includeDecisionRecord: boolean,
|
|
219
230
|
): PanelSubagentTaskParams {
|
|
220
231
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
221
232
|
return {
|
|
222
233
|
agent: member.agent,
|
|
223
|
-
task: buildPanelTask(member, prompt),
|
|
234
|
+
task: buildPanelTask(member, prompt, includeDecisionRecord),
|
|
224
235
|
output: true,
|
|
225
236
|
outputMode: "inline",
|
|
226
237
|
progress: true,
|
|
@@ -237,7 +248,7 @@ function buildPanelChainTaskParams(
|
|
|
237
248
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
238
249
|
return {
|
|
239
250
|
agent: member.agent,
|
|
240
|
-
task: buildPanelTask(member, "{task}"),
|
|
251
|
+
task: buildPanelTask(member, "{task}", false),
|
|
241
252
|
as: chainOutputName(member, index),
|
|
242
253
|
label: member.label,
|
|
243
254
|
phase: "Panel",
|
|
@@ -250,7 +261,11 @@ function buildPanelChainTaskParams(
|
|
|
250
261
|
};
|
|
251
262
|
}
|
|
252
263
|
|
|
253
|
-
function buildPanelTask(
|
|
264
|
+
function buildPanelTask(
|
|
265
|
+
member: PanelMemberConfig,
|
|
266
|
+
prompt: string,
|
|
267
|
+
includeDecisionRecord: boolean,
|
|
268
|
+
): string {
|
|
254
269
|
const role = member.role?.trim() || "independent analysis and critique";
|
|
255
270
|
return [
|
|
256
271
|
`Panel member: ${member.label} (${member.id})`,
|
|
@@ -269,6 +284,19 @@ function buildPanelTask(member: PanelMemberConfig, prompt: string): string {
|
|
|
269
284
|
"",
|
|
270
285
|
"Output contract:",
|
|
271
286
|
...PANEL_OUTPUT_CONTRACT,
|
|
287
|
+
...(includeDecisionRecord
|
|
288
|
+
? [
|
|
289
|
+
"",
|
|
290
|
+
"Decision record:",
|
|
291
|
+
"- End with exactly one single-line JSON record wrapped in the tags below.",
|
|
292
|
+
"- Keep the complete human-readable answer in the Markdown sections above the record.",
|
|
293
|
+
"- recommendation: one short plain-language conclusion.",
|
|
294
|
+
"- confidence: low, medium, or high.",
|
|
295
|
+
"- needsMoreEvidence: true when the answer should not be trusted without more investigation.",
|
|
296
|
+
`- Format: ${PANEL_DECISION_OPEN}{"recommendation":"...","confidence":"high","needsMoreEvidence":false}${PANEL_DECISION_CLOSE}`,
|
|
297
|
+
"- Do not add Markdown or any other text after the record.",
|
|
298
|
+
]
|
|
299
|
+
: []),
|
|
272
300
|
].join("\n");
|
|
273
301
|
}
|
|
274
302
|
|