@alexeiled/pi-fusion 0.1.2 → 0.2.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
@@ -3,7 +3,7 @@ import type { FusionRun } from "./types.js";
3
3
 
4
4
  type ReportRun = Pick<
5
5
  FusionRun,
6
- "id" | "prompt" | "profileName" | "panelRunId" | "judgeRunId"
6
+ "id" | "prompt" | "profileName" | "chainRunId" | "panelRunId" | "judgeRunId"
7
7
  > &
8
8
  Partial<Pick<FusionRun, "phase" | "createdAt" | "updatedAt">>;
9
9
 
@@ -11,12 +11,14 @@ export interface RenderPanelFailureReportInput {
11
11
  run: ReportRun;
12
12
  failures: readonly FailedPanelSummary[];
13
13
  error?: string;
14
+ judgeModel?: string;
14
15
  }
15
16
 
16
17
  export interface RenderSinglePanelReportInput {
17
18
  run: ReportRun;
18
19
  output: PanelOutput;
19
20
  failures: readonly FailedPanelSummary[];
21
+ judgeModel?: string;
20
22
  }
21
23
 
22
24
  export interface RenderJudgeReportInput {
@@ -24,6 +26,7 @@ export interface RenderJudgeReportInput {
24
26
  judgeOutput: string;
25
27
  panelOutputs?: readonly PanelOutput[];
26
28
  failures?: readonly FailedPanelSummary[];
29
+ judgeModel?: string;
27
30
  }
28
31
 
29
32
  export interface RenderFailureReportInput {
@@ -31,6 +34,7 @@ export interface RenderFailureReportInput {
31
34
  error: string;
32
35
  panelOutputs?: readonly PanelOutput[];
33
36
  failures?: readonly FailedPanelSummary[];
37
+ judgeModel?: string;
34
38
  }
35
39
 
36
40
  export interface RenderCancelledReportInput {
@@ -39,6 +43,7 @@ export interface RenderCancelledReportInput {
39
43
  targetRunId?: string;
40
44
  panelOutputs?: readonly PanelOutput[];
41
45
  failures?: readonly FailedPanelSummary[];
46
+ judgeModel?: string;
42
47
  }
43
48
 
44
49
  type ReportSectionTitle =
@@ -62,6 +67,7 @@ interface AgentStatusOptions {
62
67
  panelOutputs?: readonly PanelOutput[];
63
68
  failures?: readonly FailedPanelSummary[];
64
69
  judgeStatus: string;
70
+ judgeModel?: string;
65
71
  extra?: readonly string[];
66
72
  }
67
73
 
@@ -80,6 +86,7 @@ export function renderPanelFailureReport(
80
86
  panelOutputs: [],
81
87
  failures: input.failures,
82
88
  judgeStatus: "not run - no successful panelists",
89
+ ...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
83
90
  }),
84
91
  },
85
92
  {
@@ -132,6 +139,7 @@ export function renderSinglePanelReport(
132
139
  panelOutputs: [input.output],
133
140
  failures: input.failures,
134
141
  judgeStatus: "skipped - one successful panelist",
142
+ ...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
135
143
  }),
136
144
  },
137
145
  {
@@ -193,6 +201,7 @@ export function renderJudgeReport(input: RenderJudgeReportInput): string {
193
201
  panelOutputs,
194
202
  failures,
195
203
  judgeStatus: "succeeded",
204
+ ...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
196
205
  }),
197
206
  },
198
207
  {
@@ -244,6 +253,7 @@ export function renderFailureReport(input: RenderFailureReportInput): string {
244
253
  : {}),
245
254
  ...(input.failures !== undefined ? { failures: input.failures } : {}),
246
255
  judgeStatus: `failed - ${firstLine(input.error)}`,
256
+ ...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
247
257
  extra: [`- Phase: ${phase}`],
248
258
  }),
249
259
  },
@@ -291,6 +301,7 @@ export function renderCancelledReport(
291
301
  : {}),
292
302
  ...(input.failures !== undefined ? { failures: input.failures } : {}),
293
303
  judgeStatus: "cancelled or not completed",
304
+ ...(input.judgeModel ? { judgeModel: input.judgeModel } : {}),
294
305
  extra: [
295
306
  `- Phase: ${input.run.phase ?? "unknown"}`,
296
307
  `- Cancellation method: ${input.method}`,
@@ -371,15 +382,21 @@ function formatAgentStatus(options: AgentStatusOptions): string[] {
371
382
  }
372
383
 
373
384
  lines.push(`- Judge: ${options.judgeStatus}`);
385
+ if (options.judgeModel) lines.push(` Model: ${options.judgeModel}`);
374
386
  if (options.extra) lines.push(...options.extra);
375
387
  return lines;
376
388
  }
377
389
 
378
390
  function formatPanelDetails(
379
- item: Pick<PanelOutput, "agent" | "artifactPath" | "sessionPath">,
391
+ item: Pick<
392
+ PanelOutput,
393
+ "agent" | "role" | "model" | "artifactPath" | "sessionPath"
394
+ >,
380
395
  ): string[] {
381
396
  return [
382
397
  ` Agent: ${item.agent}`,
398
+ ...(item.role ? [` Role: ${item.role}`] : []),
399
+ ...(item.model ? [` Model: ${item.model}`] : []),
383
400
  ...(item.artifactPath ? [` Artifact: ${item.artifactPath}`] : []),
384
401
  ...(item.sessionPath ? [` Session: ${item.sessionPath}`] : []),
385
402
  ];
@@ -391,8 +408,9 @@ function formatRunMetadata(run: ReportRun): string[] {
391
408
  `- Profile: ${run.profileName}`,
392
409
  ...(run.phase ? [`- Phase: ${run.phase}`] : []),
393
410
  `- Prompt: ${firstLine(run.prompt)}`,
411
+ ...(run.chainRunId ? [`- Chain run: ${run.chainRunId}`] : []),
394
412
  ...(run.panelRunId ? [`- Panel run: ${run.panelRunId}`] : []),
395
- ...(run.judgeRunId ? [`- Judge run: ${run.judgeRunId}`] : []),
413
+ ...(run.judgeRunId ? [`- Fallback judge run: ${run.judgeRunId}`] : []),
396
414
  ...(typeof run.createdAt === "number"
397
415
  ? [`- Created: ${formatTimestamp(run.createdAt)}`]
398
416
  : []),
@@ -1,4 +1,8 @@
1
- import type { FailedPanelSummary, PanelOutput } from "./run-builder.js";
1
+ import {
2
+ appendThinkingSuffix,
3
+ type FailedPanelSummary,
4
+ type PanelOutput,
5
+ } from "./run-builder.js";
2
6
  import type { PanelMemberConfig } from "./types.js";
3
7
 
4
8
  export type ResultExtractErrorCode =
@@ -12,6 +16,7 @@ export interface ResultExtractError {
12
16
 
13
17
  export interface ExtractPanelResultsOptions {
14
18
  panel?: readonly PanelMemberConfig[];
19
+ limit?: number;
15
20
  }
16
21
 
17
22
  export type ExtractPanelResultsResult =
@@ -40,7 +45,11 @@ export function extractPanelResults(
40
45
 
41
46
  const outputs: PanelOutput[] = [];
42
47
  const failures: FailedPanelSummary[] = [];
43
- for (const [index, rawResult] of container.results.entries()) {
48
+ const results =
49
+ options.limit === undefined
50
+ ? container.results
51
+ : container.results.slice(0, options.limit);
52
+ for (const [index, rawResult] of results.entries()) {
44
53
  const child = normalizeChildResult(rawResult, index, options);
45
54
  if (!child.ok) return child;
46
55
  if (child.status === "success") outputs.push(child.output);
@@ -232,12 +241,17 @@ function buildPanelOutput(input: {
232
241
  artifactPath: string | undefined;
233
242
  sessionPath: string | undefined;
234
243
  }): PanelOutput {
244
+ const model = input.member
245
+ ? appendThinkingSuffix(input.member.model, input.member.thinking)
246
+ : undefined;
235
247
  return {
236
248
  index: input.index,
237
249
  agent: input.agent,
238
250
  output: input.output,
239
251
  ...(input.member?.id ? { id: input.member.id } : {}),
240
252
  ...(input.member?.label ? { label: input.member.label } : {}),
253
+ ...(input.member?.role ? { role: input.member.role } : {}),
254
+ ...(model ? { model } : {}),
241
255
  ...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
242
256
  ...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
243
257
  };
@@ -251,12 +265,17 @@ function buildFailedPanelSummary(input: {
251
265
  artifactPath: string | undefined;
252
266
  sessionPath: string | undefined;
253
267
  }): FailedPanelSummary {
268
+ const model = input.member
269
+ ? appendThinkingSuffix(input.member.model, input.member.thinking)
270
+ : undefined;
254
271
  return {
255
272
  index: input.index,
256
273
  agent: input.agent,
257
274
  summary: input.summary,
258
275
  ...(input.member?.id ? { id: input.member.id } : {}),
259
276
  ...(input.member?.label ? { label: input.member.label } : {}),
277
+ ...(input.member?.role ? { role: input.member.role } : {}),
278
+ ...(model ? { model } : {}),
260
279
  ...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
261
280
  ...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
262
281
  };
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  THINKING_LEVELS,
3
+ type FailedPanelSummary,
3
4
  type FusionProfile,
4
5
  type PanelMemberConfig,
6
+ type PanelOutput,
5
7
  type ThinkingLevel,
6
8
  } from "./types.js";
7
9
 
@@ -24,6 +26,12 @@ export interface PanelSubagentTaskParams {
24
26
  model?: string;
25
27
  }
26
28
 
29
+ export interface PanelChainTaskParams extends PanelSubagentTaskParams {
30
+ as: string;
31
+ label: string;
32
+ phase: "Panel";
33
+ }
34
+
27
35
  export interface PanelSpawnParams {
28
36
  tasks: PanelSubagentTaskParams[];
29
37
  async: true;
@@ -36,39 +44,51 @@ export interface PanelSpawnParams {
36
44
  timeoutMs?: number;
37
45
  }
38
46
 
39
- export interface JudgeSpawnParams {
47
+ export interface FusionChainParallelStepParams {
48
+ parallel: PanelChainTaskParams[];
49
+ concurrency: number;
50
+ failFast: false;
51
+ }
52
+
53
+ export interface FusionChainJudgeStepParams {
40
54
  agent: string;
41
55
  task: string;
56
+ label: "Judge";
57
+ phase: "Judge";
58
+ output: true;
59
+ outputMode: "inline";
60
+ skill: false;
61
+ acceptance: FusionAcceptanceDisabled;
62
+ model?: string;
63
+ }
64
+
65
+ export interface FusionChainSpawnParams {
66
+ chain: [FusionChainParallelStepParams, FusionChainJudgeStepParams];
67
+ task: string;
42
68
  async: true;
43
69
  clarify: false;
44
70
  context: "fresh" | "fork";
45
71
  output: true;
46
72
  outputMode: "inline";
47
- skill: false;
48
73
  acceptance: FusionAcceptanceDisabled;
49
- model?: string;
50
74
  timeoutMs?: number;
51
75
  }
52
76
 
53
- export interface PanelOutput {
54
- index: number;
77
+ export interface JudgeSpawnParams {
55
78
  agent: string;
56
- output: string;
57
- id?: string;
58
- label?: string;
59
- artifactPath?: string;
60
- sessionPath?: string;
79
+ task: string;
80
+ async: true;
81
+ clarify: false;
82
+ context: "fresh" | "fork";
83
+ output: true;
84
+ outputMode: "inline";
85
+ skill: false;
86
+ acceptance: FusionAcceptanceDisabled;
87
+ model?: string;
88
+ timeoutMs?: number;
61
89
  }
62
90
 
63
- export interface FailedPanelSummary {
64
- index: number;
65
- agent: string;
66
- summary: string;
67
- id?: string;
68
- label?: string;
69
- artifactPath?: string;
70
- sessionPath?: string;
71
- }
91
+ export type { FailedPanelSummary, PanelOutput } from "./types.js";
72
92
 
73
93
  export interface BuildJudgeSpawnParamsInput {
74
94
  profile: FusionProfile;
@@ -127,6 +147,48 @@ export function buildPanelSpawnParams(
127
147
  };
128
148
  }
129
149
 
150
+ export function buildFusionChainSpawnParams(
151
+ profile: FusionProfile,
152
+ prompt: string,
153
+ ): FusionChainSpawnParams {
154
+ const model = appendThinkingSuffix(
155
+ profile.judge.model,
156
+ profile.judge.thinking,
157
+ );
158
+ return {
159
+ chain: [
160
+ {
161
+ parallel: profile.panel.map((member, index) =>
162
+ buildPanelChainTaskParams(member, index),
163
+ ),
164
+ concurrency: profile.concurrency ?? profile.panel.length,
165
+ failFast: false,
166
+ },
167
+ {
168
+ agent: profile.judge.agent,
169
+ task: buildChainJudgeTask(profile.panel),
170
+ label: "Judge",
171
+ phase: "Judge",
172
+ output: true,
173
+ outputMode: "inline",
174
+ skill: false,
175
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
176
+ ...(model ? { model } : {}),
177
+ },
178
+ ],
179
+ task: prompt.trim(),
180
+ async: true,
181
+ clarify: false,
182
+ context: profile.context ?? "fresh",
183
+ output: true,
184
+ outputMode: "inline",
185
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
186
+ ...(profile.timeoutMs !== undefined
187
+ ? { timeoutMs: profile.timeoutMs }
188
+ : {}),
189
+ };
190
+ }
191
+
130
192
  export function buildJudgeSpawnParams(
131
193
  input: BuildJudgeSpawnParamsInput,
132
194
  ): JudgeSpawnParams {
@@ -168,6 +230,26 @@ function buildPanelTaskParams(
168
230
  };
169
231
  }
170
232
 
233
+ function buildPanelChainTaskParams(
234
+ member: PanelMemberConfig,
235
+ index: number,
236
+ ): PanelChainTaskParams {
237
+ const model = appendThinkingSuffix(member.model, member.thinking);
238
+ return {
239
+ agent: member.agent,
240
+ task: buildPanelTask(member, "{task}"),
241
+ as: chainOutputName(member, index),
242
+ label: member.label,
243
+ phase: "Panel",
244
+ output: true,
245
+ outputMode: "inline",
246
+ progress: true,
247
+ skill: false,
248
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
249
+ ...(model ? { model } : {}),
250
+ };
251
+ }
252
+
171
253
  function buildPanelTask(member: PanelMemberConfig, prompt: string): string {
172
254
  const role = member.role?.trim() || "independent analysis and critique";
173
255
  return [
@@ -215,6 +297,30 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
215
297
  ].join("\n");
216
298
  }
217
299
 
300
+ function buildChainJudgeTask(panel: readonly PanelMemberConfig[]): string {
301
+ return [
302
+ "You are the fusion judge.",
303
+ "Read-only synthesis only. Leave files, git state, and the workspace untouched. Do not ask other agents. Do not run subagents.",
304
+ "Synthesize the panel results. Preserve disagreement instead of forcing consensus.",
305
+ "",
306
+ "Original task:",
307
+ "{task}",
308
+ "",
309
+ "All listed panelists completed successfully.",
310
+ "",
311
+ "Panel outputs:",
312
+ ...panel.flatMap((member, index) => [
313
+ `## ${member.label} (${member.id})`,
314
+ `Agent: ${member.agent}`,
315
+ "",
316
+ `{outputs.${chainOutputName(member, index)}}`,
317
+ "",
318
+ ]),
319
+ "Output contract:",
320
+ ...JUDGE_OUTPUT_CONTRACT,
321
+ ].join("\n");
322
+ }
323
+
218
324
  function formatPanelStatus(
219
325
  outputs: readonly PanelOutput[],
220
326
  failures: readonly FailedPanelSummary[],
@@ -275,6 +381,12 @@ function firstLine(value: string): string {
275
381
  return value.split(/\r?\n/, 1)[0]?.trim() || "unknown failure";
276
382
  }
277
383
 
384
+ function chainOutputName(member: PanelMemberConfig, index: number): string {
385
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(member.id)
386
+ ? member.id
387
+ : `panel_${index + 1}`;
388
+ }
389
+
278
390
  function hasThinkingSuffix(model: string): boolean {
279
391
  const colonIndex = model.lastIndexOf(":");
280
392
  if (colonIndex === -1) return false;
package/src/run-store.ts CHANGED
@@ -17,6 +17,7 @@ export type FusionRunSummary = Omit<
17
17
  | "phase"
18
18
  | "createdAt"
19
19
  | "updatedAt"
20
+ | "chainRunId"
20
21
  | "panelRunId"
21
22
  | "judgeRunId"
22
23
  | "report"
@@ -29,19 +30,26 @@ export interface FusionRunStartInput {
29
30
  id?: string;
30
31
  prompt: string;
31
32
  profileName: string;
33
+ phase?: Exclude<FusionPhase, FusionTerminalPhase>;
32
34
  createdAt?: number;
33
35
  }
34
36
 
35
37
  export interface FusionRunPatch {
36
38
  phase?: Exclude<FusionPhase, FusionTerminalPhase>;
39
+ chainRunId?: string;
40
+ chainAsyncDir?: string;
37
41
  panelRunId?: string;
38
42
  judgeRunId?: string;
43
+ judgeAsyncDir?: string;
44
+ panelOutputs?: FusionRun["panelOutputs"];
45
+ panelFailures?: FusionRun["panelFailures"];
39
46
  report?: string;
40
47
  error?: string;
41
48
  updatedAt?: number;
42
49
  }
43
50
 
44
51
  export interface FusionRunTransitionPatch {
52
+ chainRunId?: string;
45
53
  panelRunId?: string;
46
54
  judgeRunId?: string;
47
55
  report?: string;
@@ -106,7 +114,7 @@ export class FusionRunStore {
106
114
  id: input.id ?? this.idFactory(),
107
115
  prompt: input.prompt,
108
116
  profileName: input.profileName,
109
- phase: "panel",
117
+ phase: input.phase ?? "chain",
110
118
  createdAt,
111
119
  updatedAt: createdAt,
112
120
  };
@@ -246,8 +254,21 @@ function applyPatch(
246
254
  const updated = cloneRun(run);
247
255
  updated.updatedAt = updatedAt;
248
256
  if (patch.phase !== undefined) updated.phase = patch.phase;
257
+ if (patch.chainRunId !== undefined) updated.chainRunId = patch.chainRunId;
258
+ if (patch.chainAsyncDir !== undefined) {
259
+ updated.chainAsyncDir = patch.chainAsyncDir;
260
+ }
249
261
  if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
250
262
  if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
263
+ if (patch.judgeAsyncDir !== undefined) {
264
+ updated.judgeAsyncDir = patch.judgeAsyncDir;
265
+ }
266
+ if (patch.panelOutputs !== undefined) {
267
+ updated.panelOutputs = clonePanelOutputs(patch.panelOutputs);
268
+ }
269
+ if (patch.panelFailures !== undefined) {
270
+ updated.panelFailures = clonePanelFailures(patch.panelFailures);
271
+ }
251
272
  if (patch.report !== undefined) updated.report = patch.report;
252
273
  if (patch.error !== undefined) updated.error = patch.error;
253
274
  return updated;
@@ -264,6 +285,7 @@ function applyTransitionPatch(
264
285
  phase,
265
286
  };
266
287
  updated.updatedAt = updatedAt;
288
+ if (patch.chainRunId !== undefined) updated.chainRunId = patch.chainRunId;
267
289
  if (patch.panelRunId !== undefined) updated.panelRunId = patch.panelRunId;
268
290
  if (patch.judgeRunId !== undefined) updated.judgeRunId = patch.judgeRunId;
269
291
  if (patch.report !== undefined) updated.report = patch.report;
@@ -281,6 +303,7 @@ function toRunSummary(
281
303
  phase: run.phase,
282
304
  createdAt: run.createdAt,
283
305
  updatedAt: run.updatedAt,
306
+ ...(run.chainRunId !== undefined ? { chainRunId: run.chainRunId } : {}),
284
307
  ...(run.panelRunId !== undefined ? { panelRunId: run.panelRunId } : {}),
285
308
  ...(run.judgeRunId !== undefined ? { judgeRunId: run.judgeRunId } : {}),
286
309
  ...(run.report !== undefined ? { report: run.report } : {}),
@@ -296,8 +319,21 @@ function cloneRun(run: FusionRun): FusionRun {
296
319
  phase: run.phase,
297
320
  createdAt: run.createdAt,
298
321
  updatedAt: run.updatedAt,
322
+ ...(run.chainRunId !== undefined ? { chainRunId: run.chainRunId } : {}),
323
+ ...(run.chainAsyncDir !== undefined
324
+ ? { chainAsyncDir: run.chainAsyncDir }
325
+ : {}),
299
326
  ...(run.panelRunId !== undefined ? { panelRunId: run.panelRunId } : {}),
300
327
  ...(run.judgeRunId !== undefined ? { judgeRunId: run.judgeRunId } : {}),
328
+ ...(run.judgeAsyncDir !== undefined
329
+ ? { judgeAsyncDir: run.judgeAsyncDir }
330
+ : {}),
331
+ ...(run.panelOutputs !== undefined
332
+ ? { panelOutputs: clonePanelOutputs(run.panelOutputs) }
333
+ : {}),
334
+ ...(run.panelFailures !== undefined
335
+ ? { panelFailures: clonePanelFailures(run.panelFailures) }
336
+ : {}),
301
337
  ...(run.report !== undefined ? { report: run.report } : {}),
302
338
  ...(run.error !== undefined ? { error: run.error } : {}),
303
339
  };
@@ -326,12 +362,39 @@ function isFusionRunState(value: unknown): value is FusionRun {
326
362
  if (!isFusionPhase(value.phase)) return false;
327
363
  if (!isFiniteNumber(value.createdAt)) return false;
328
364
  if (!isFiniteNumber(value.updatedAt)) return false;
365
+ if (value.chainRunId !== undefined && typeof value.chainRunId !== "string") {
366
+ return false;
367
+ }
368
+ if (
369
+ value.chainAsyncDir !== undefined &&
370
+ typeof value.chainAsyncDir !== "string"
371
+ ) {
372
+ return false;
373
+ }
329
374
  if (value.panelRunId !== undefined && typeof value.panelRunId !== "string") {
330
375
  return false;
331
376
  }
332
377
  if (value.judgeRunId !== undefined && typeof value.judgeRunId !== "string") {
333
378
  return false;
334
379
  }
380
+ if (
381
+ value.judgeAsyncDir !== undefined &&
382
+ typeof value.judgeAsyncDir !== "string"
383
+ ) {
384
+ return false;
385
+ }
386
+ if (
387
+ value.panelOutputs !== undefined &&
388
+ !isPanelOutputArray(value.panelOutputs)
389
+ ) {
390
+ return false;
391
+ }
392
+ if (
393
+ value.panelFailures !== undefined &&
394
+ !isPanelFailureArray(value.panelFailures)
395
+ ) {
396
+ return false;
397
+ }
335
398
  if (value.report !== undefined && typeof value.report !== "string") {
336
399
  return false;
337
400
  }
@@ -348,6 +411,7 @@ function isFusionRunSummary(value: unknown): value is FusionRunSummary {
348
411
  function isFusionPhase(value: unknown): value is FusionPhase {
349
412
  return (
350
413
  value === "panel" ||
414
+ value === "chain" ||
351
415
  value === "judge" ||
352
416
  value === "done" ||
353
417
  value === "failed" ||
@@ -370,3 +434,59 @@ function isNonEmptyString(value: unknown): value is string {
370
434
  function isFiniteNumber(value: unknown): value is number {
371
435
  return typeof value === "number" && Number.isFinite(value);
372
436
  }
437
+
438
+ function isPanelOutputArray(
439
+ value: unknown,
440
+ ): value is NonNullable<FusionRun["panelOutputs"]> {
441
+ return Array.isArray(value) && value.every(isPanelOutput);
442
+ }
443
+
444
+ function isPanelOutput(
445
+ value: unknown,
446
+ ): value is NonNullable<FusionRun["panelOutputs"]>[number] {
447
+ return (
448
+ isRecord(value) &&
449
+ isFiniteNumber(value.index) &&
450
+ isNonEmptyString(value.agent) &&
451
+ typeof value.output === "string" &&
452
+ (value.id === undefined || typeof value.id === "string") &&
453
+ (value.label === undefined || typeof value.label === "string") &&
454
+ (value.artifactPath === undefined ||
455
+ typeof value.artifactPath === "string") &&
456
+ (value.sessionPath === undefined || typeof value.sessionPath === "string")
457
+ );
458
+ }
459
+
460
+ function isPanelFailureArray(
461
+ value: unknown,
462
+ ): value is NonNullable<FusionRun["panelFailures"]> {
463
+ return Array.isArray(value) && value.every(isPanelFailure);
464
+ }
465
+
466
+ function isPanelFailure(
467
+ value: unknown,
468
+ ): value is NonNullable<FusionRun["panelFailures"]>[number] {
469
+ return (
470
+ isRecord(value) &&
471
+ isFiniteNumber(value.index) &&
472
+ isNonEmptyString(value.agent) &&
473
+ typeof value.summary === "string" &&
474
+ (value.id === undefined || typeof value.id === "string") &&
475
+ (value.label === undefined || typeof value.label === "string") &&
476
+ (value.artifactPath === undefined ||
477
+ typeof value.artifactPath === "string") &&
478
+ (value.sessionPath === undefined || typeof value.sessionPath === "string")
479
+ );
480
+ }
481
+
482
+ function clonePanelOutputs(
483
+ outputs: NonNullable<FusionRun["panelOutputs"]>,
484
+ ): NonNullable<FusionRun["panelOutputs"]> {
485
+ return outputs.map((output) => ({ ...output }));
486
+ }
487
+
488
+ function clonePanelFailures(
489
+ failures: NonNullable<FusionRun["panelFailures"]>,
490
+ ): NonNullable<FusionRun["panelFailures"]> {
491
+ return failures.map((failure) => ({ ...failure }));
492
+ }
package/src/status.ts CHANGED
@@ -23,12 +23,16 @@ export function publishFusionStatus(
23
23
  ctx: FusionUiContext | undefined,
24
24
  run: Pick<
25
25
  FusionRun,
26
- "id" | "phase" | "profileName" | "panelRunId" | "judgeRunId"
26
+ "id" | "phase" | "profileName" | "chainRunId" | "panelRunId" | "judgeRunId"
27
27
  >,
28
28
  progress?: FusionProgressCounts,
29
+ phaseLabel?: string,
29
30
  ): void {
30
31
  if (!ctx?.hasUI) return;
31
- ctx.ui.setStatus(FUSION_STATUS_KEY, formatFusionStatusText(run, progress));
32
+ ctx.ui.setStatus(
33
+ FUSION_STATUS_KEY,
34
+ formatFusionStatusText(run, progress, phaseLabel),
35
+ );
32
36
  }
33
37
 
34
38
  export function clearFusionUi(ctx: FusionUiContext | undefined): void {
@@ -37,20 +41,30 @@ export function clearFusionUi(ctx: FusionUiContext | undefined): void {
37
41
  }
38
42
 
39
43
  export function formatFusionStatusText(
40
- run: Pick<FusionRun, "phase" | "profileName" | "panelRunId" | "judgeRunId">,
44
+ run: Pick<
45
+ FusionRun,
46
+ "phase" | "profileName" | "chainRunId" | "panelRunId" | "judgeRunId"
47
+ >,
41
48
  progress?: FusionProgressCounts,
49
+ phaseLabel?: string,
42
50
  ): string {
43
- const activeRunId = run.phase === "judge" ? run.judgeRunId : run.panelRunId;
44
- const progressText = progress ? ` ${formatProgressCounts(progress)}` : "";
45
- const runText = activeRunId ? ` ${activeRunId}` : " starting";
46
- return `fusion: ${run.phase} ${run.profileName}${runText}${progressText}`;
51
+ const activeRunId =
52
+ run.phase === "judge" ? run.judgeRunId : (run.chainRunId ?? run.panelRunId);
53
+ const phase = phaseLabel ?? run.phase;
54
+ if (progress) {
55
+ return `fusion: panel · ${formatProgressCounts(progress)} · ${run.profileName}`;
56
+ }
57
+ if (activeRunId) {
58
+ return `fusion: ${phase} · ${run.profileName} · ${activeRunId}`;
59
+ }
60
+ return `fusion: ${phase} · starting · ${run.profileName}`;
47
61
  }
48
62
 
49
63
  export function extractFusionProgressCounts(
50
64
  payload: unknown,
51
65
  ): FusionProgressCounts | undefined {
52
66
  const container = findProgressContainer(payload);
53
- if (!container) return undefined;
67
+ if (!container || container.length === 0) return undefined;
54
68
 
55
69
  const counts: FusionProgressCounts = {
56
70
  total: container.length,
@@ -84,15 +98,19 @@ function findProgressContainer(
84
98
  payload: unknown,
85
99
  ): readonly unknown[] | undefined {
86
100
  if (!isRecord(payload)) return undefined;
87
- const progress = unknownArray(payload.progress);
101
+ const progress = nonEmptyArray(payload.progress);
88
102
  if (progress) return progress;
89
- const results = unknownArray(payload.results);
103
+ const results = nonEmptyArray(payload.results);
90
104
  if (results) return results;
105
+ const steps = nonEmptyArray(payload.steps);
106
+ if (steps) return steps;
91
107
  if (isRecord(payload.details)) {
92
- const detailsProgress = unknownArray(payload.details.progress);
108
+ const detailsProgress = nonEmptyArray(payload.details.progress);
93
109
  if (detailsProgress) return detailsProgress;
94
- const detailsResults = unknownArray(payload.details.results);
110
+ const detailsResults = nonEmptyArray(payload.details.results);
95
111
  if (detailsResults) return detailsResults;
112
+ const detailsSteps = nonEmptyArray(payload.details.steps);
113
+ if (detailsSteps) return detailsSteps;
96
114
  }
97
115
  if (isRecord(payload.data)) return findProgressContainer(payload.data);
98
116
  return undefined;
@@ -127,8 +145,10 @@ function firstString(...values: readonly unknown[]): string | undefined {
127
145
  return undefined;
128
146
  }
129
147
 
130
- function unknownArray(value: unknown): readonly unknown[] | undefined {
131
- return Array.isArray(value) ? (value as readonly unknown[]) : undefined;
148
+ function nonEmptyArray(value: unknown): readonly unknown[] | undefined {
149
+ return Array.isArray(value) && value.length > 0
150
+ ? (value as readonly unknown[])
151
+ : undefined;
132
152
  }
133
153
 
134
154
  function isRecord(value: unknown): value is Record<string, unknown> {