@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/src/report.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import type { FailedPanelSummary, PanelOutput } from "./run-builder.js";
2
- import type { FusionRun } from "./types.js";
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
- "id" | "prompt" | "profileName" | "chainRunId" | "panelRunId" | "judgeRunId"
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
- return renderReport([
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
- return renderReport([
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) lines.push(` Model: ${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
- "agent" | "role" | "model" | "artifactPath" | "sessionPath"
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 ? [` Model: ${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.judgeRunId ? [`- Fallback judge run: ${run.judgeRunId}`] : []),
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
  : []),
@@ -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 ("results" in payload.details) {
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
- rawResult.output,
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: failureSummary(rawResult, artifactPath),
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 model = input.member
245
- ? appendThinkingSuffix(input.member.model, input.member.thinking)
246
- : undefined;
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 model = input.member
269
- ? appendThinkingSuffix(input.member.model, input.member.thinking)
270
- : undefined;
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 {
@@ -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) => buildPanelTaskParams(member, prompt)),
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(member: PanelMemberConfig, prompt: string): string {
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