@alexeiled/pi-fusion 0.1.1 → 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,10 +1,20 @@
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
 
10
+ export const FUSION_ACCEPTANCE_DISABLED = {
11
+ level: "none",
12
+ reason:
13
+ "pi-fusion panelists and judge are read-only advisory tasks; pi-fusion owns final synthesis and acceptance.",
14
+ } as const;
15
+
16
+ export type FusionAcceptanceDisabled = typeof FUSION_ACCEPTANCE_DISABLED;
17
+
8
18
  export interface PanelSubagentTaskParams {
9
19
  agent: string;
10
20
  task: string;
@@ -12,10 +22,16 @@ export interface PanelSubagentTaskParams {
12
22
  outputMode: "inline";
13
23
  progress: true;
14
24
  skill: false;
15
- acceptance: "none";
25
+ acceptance: FusionAcceptanceDisabled;
16
26
  model?: string;
17
27
  }
18
28
 
29
+ export interface PanelChainTaskParams extends PanelSubagentTaskParams {
30
+ as: string;
31
+ label: string;
32
+ phase: "Panel";
33
+ }
34
+
19
35
  export interface PanelSpawnParams {
20
36
  tasks: PanelSubagentTaskParams[];
21
37
  async: true;
@@ -24,43 +40,55 @@ export interface PanelSpawnParams {
24
40
  context: "fresh" | "fork";
25
41
  output: true;
26
42
  outputMode: "inline";
27
- acceptance: "none";
43
+ acceptance: FusionAcceptanceDisabled;
28
44
  timeoutMs?: number;
29
45
  }
30
46
 
31
- export interface JudgeSpawnParams {
47
+ export interface FusionChainParallelStepParams {
48
+ parallel: PanelChainTaskParams[];
49
+ concurrency: number;
50
+ failFast: false;
51
+ }
52
+
53
+ export interface FusionChainJudgeStepParams {
32
54
  agent: string;
33
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;
34
68
  async: true;
35
69
  clarify: false;
36
70
  context: "fresh" | "fork";
37
71
  output: true;
38
72
  outputMode: "inline";
39
- skill: false;
40
- acceptance: "none";
41
- model?: string;
73
+ acceptance: FusionAcceptanceDisabled;
42
74
  timeoutMs?: number;
43
75
  }
44
76
 
45
- export interface PanelOutput {
46
- index: number;
77
+ export interface JudgeSpawnParams {
47
78
  agent: string;
48
- output: string;
49
- id?: string;
50
- label?: string;
51
- artifactPath?: string;
52
- 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;
53
89
  }
54
90
 
55
- export interface FailedPanelSummary {
56
- index: number;
57
- agent: string;
58
- summary: string;
59
- id?: string;
60
- label?: string;
61
- artifactPath?: string;
62
- sessionPath?: string;
63
- }
91
+ export type { FailedPanelSummary, PanelOutput } from "./types.js";
64
92
 
65
93
  export interface BuildJudgeSpawnParamsInput {
66
94
  profile: FusionProfile;
@@ -112,7 +140,49 @@ export function buildPanelSpawnParams(
112
140
  context: profile.context ?? "fresh",
113
141
  output: true,
114
142
  outputMode: "inline",
115
- acceptance: "none",
143
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
144
+ ...(profile.timeoutMs !== undefined
145
+ ? { timeoutMs: profile.timeoutMs }
146
+ : {}),
147
+ };
148
+ }
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,
116
186
  ...(profile.timeoutMs !== undefined
117
187
  ? { timeoutMs: profile.timeoutMs }
118
188
  : {}),
@@ -135,7 +205,7 @@ export function buildJudgeSpawnParams(
135
205
  output: true,
136
206
  outputMode: "inline",
137
207
  skill: false,
138
- acceptance: "none",
208
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
139
209
  ...(model ? { model } : {}),
140
210
  ...(input.profile.timeoutMs !== undefined
141
211
  ? { timeoutMs: input.profile.timeoutMs }
@@ -155,7 +225,27 @@ function buildPanelTaskParams(
155
225
  outputMode: "inline",
156
226
  progress: true,
157
227
  skill: false,
158
- acceptance: "none",
228
+ acceptance: FUSION_ACCEPTANCE_DISABLED,
229
+ ...(model ? { model } : {}),
230
+ };
231
+ }
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,
159
249
  ...(model ? { model } : {}),
160
250
  };
161
251
  }
@@ -171,10 +261,10 @@ function buildPanelTask(member: PanelMemberConfig, prompt: string): string {
171
261
  "",
172
262
  "Instructions:",
173
263
  "- Work independently from the other panelists.",
174
- "- Do not edit files, stage changes, commit changes, or run destructive commands.",
264
+ "- Read-only: inspect only; leave files, git state, and the workspace untouched.",
175
265
  "- Do not ask other agents.",
176
266
  "- Do not run subagents.",
177
- "- Use read-only local inspection only when code evidence is needed.",
267
+ "- Use local inspection only when code evidence is needed.",
178
268
  "- Be concise and cite evidence when you inspect files.",
179
269
  "",
180
270
  "Output contract:",
@@ -187,7 +277,7 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
187
277
  const sortedFailures = [...input.failedPanelists].sort(comparePanelItems);
188
278
  return [
189
279
  "You are the fusion judge.",
190
- "Do not edit files. Do not ask other agents. Do not run subagents.",
280
+ "Read-only synthesis only. Leave files, git state, and the workspace untouched. Do not ask other agents. Do not run subagents.",
191
281
  "Synthesize the panel results. Preserve disagreement instead of forcing consensus.",
192
282
  "",
193
283
  "Original task:",
@@ -207,6 +297,30 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
207
297
  ].join("\n");
208
298
  }
209
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
+
210
324
  function formatPanelStatus(
211
325
  outputs: readonly PanelOutput[],
212
326
  failures: readonly FailedPanelSummary[],
@@ -267,6 +381,12 @@ function firstLine(value: string): string {
267
381
  return value.split(/\r?\n/, 1)[0]?.trim() || "unknown failure";
268
382
  }
269
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
+
270
390
  function hasThinkingSuffix(model: string): boolean {
271
391
  const colonIndex = model.lastIndexOf(":");
272
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
+ }