@alexeiled/pi-fusion 0.1.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.
@@ -0,0 +1,311 @@
1
+ import type { FailedPanelSummary, PanelOutput } from "./run-builder.js";
2
+ import type { PanelMemberConfig } from "./types.js";
3
+
4
+ export type ResultExtractErrorCode =
5
+ "missing-results" | "unknown-result-shape" | "missing-result-field";
6
+
7
+ export interface ResultExtractError {
8
+ code: ResultExtractErrorCode;
9
+ message: string;
10
+ path: string;
11
+ }
12
+
13
+ export interface ExtractPanelResultsOptions {
14
+ panel?: readonly PanelMemberConfig[];
15
+ }
16
+
17
+ export type ExtractPanelResultsResult =
18
+ | {
19
+ ok: true;
20
+ outputs: PanelOutput[];
21
+ failures: FailedPanelSummary[];
22
+ runId?: string;
23
+ }
24
+ | { ok: false; error: ResultExtractError };
25
+
26
+ interface ResultsContainer {
27
+ payload: Record<string, unknown>;
28
+ results: readonly unknown[];
29
+ path: string;
30
+ }
31
+
32
+ type ChildStatus = "success" | "failed";
33
+
34
+ export function extractPanelResults(
35
+ payload: unknown,
36
+ options: ExtractPanelResultsOptions = {},
37
+ ): ExtractPanelResultsResult {
38
+ const container = findResultsContainer(payload);
39
+ if (!container.ok) return container;
40
+
41
+ const outputs: PanelOutput[] = [];
42
+ const failures: FailedPanelSummary[] = [];
43
+ for (const [index, rawResult] of container.results.entries()) {
44
+ const child = normalizeChildResult(rawResult, index, options);
45
+ if (!child.ok) return child;
46
+ if (child.status === "success") outputs.push(child.output);
47
+ else failures.push(child.failure);
48
+ }
49
+
50
+ const runId = firstString(container.payload.runId, container.payload.id);
51
+ return {
52
+ ok: true,
53
+ outputs,
54
+ failures,
55
+ ...(runId ? { runId } : {}),
56
+ };
57
+ }
58
+
59
+ function findResultsContainer(
60
+ payload: unknown,
61
+ ):
62
+ | (ExtractPanelResultsResult & { ok: false })
63
+ | ({ ok: true } & ResultsContainer) {
64
+ if (!isRecord(payload)) {
65
+ return error(
66
+ "unknown-result-shape",
67
+ "Subagents result payload must be an object.",
68
+ "$",
69
+ );
70
+ }
71
+
72
+ if (Array.isArray(payload.results)) {
73
+ return { ok: true, payload, results: payload.results, path: "$.results" };
74
+ }
75
+
76
+ if ("results" in payload) {
77
+ return error(
78
+ "unknown-result-shape",
79
+ "Subagents result payload results field must be an array.",
80
+ "$.results",
81
+ );
82
+ }
83
+
84
+ if (isRecord(payload.details)) {
85
+ if (Array.isArray(payload.details.results)) {
86
+ return {
87
+ ok: true,
88
+ payload: { ...payload, ...payload.details },
89
+ results: payload.details.results,
90
+ path: "$.details.results",
91
+ };
92
+ }
93
+ if ("results" in payload.details) {
94
+ return error(
95
+ "unknown-result-shape",
96
+ "Subagents result details.results field must be an array.",
97
+ "$.details.results",
98
+ );
99
+ }
100
+ }
101
+
102
+ if (isRecord(payload.data)) return findResultsContainer(payload.data);
103
+
104
+ return error(
105
+ "missing-results",
106
+ "Subagents result payload did not include a results array.",
107
+ "$",
108
+ );
109
+ }
110
+
111
+ function normalizeChildResult(
112
+ rawResult: unknown,
113
+ index: number,
114
+ options: ExtractPanelResultsOptions,
115
+ ):
116
+ | { ok: true; status: "success"; output: PanelOutput }
117
+ | { ok: true; status: "failed"; failure: FailedPanelSummary }
118
+ | { ok: false; error: ResultExtractError } {
119
+ const path = `$.results[${index}]`;
120
+ if (!isRecord(rawResult)) {
121
+ return error(
122
+ "unknown-result-shape",
123
+ "Subagents child result must be an object.",
124
+ path,
125
+ );
126
+ }
127
+
128
+ const member = options.panel?.[index];
129
+ const agent = firstString(rawResult.agent, member?.agent);
130
+ if (!agent) {
131
+ return error(
132
+ "missing-result-field",
133
+ "Subagents child result did not include an agent.",
134
+ `${path}.agent`,
135
+ );
136
+ }
137
+
138
+ const artifactPath = extractArtifactPath(rawResult);
139
+ const sessionPath = firstString(rawResult.sessionPath, rawResult.sessionFile);
140
+ const status = classifyChildStatus(rawResult);
141
+
142
+ if (status === "success") {
143
+ const output =
144
+ firstNonBlankString(
145
+ rawResult.output,
146
+ rawResult.finalOutput,
147
+ rawResult.summary,
148
+ rawResult.text,
149
+ ) ?? artifactOutput(artifactPath);
150
+ if (!output) {
151
+ return error(
152
+ "missing-result-field",
153
+ "Successful subagents child result did not include output or an artifact path.",
154
+ path,
155
+ );
156
+ }
157
+ return {
158
+ ok: true,
159
+ status,
160
+ output: buildPanelOutput({
161
+ index,
162
+ member,
163
+ agent,
164
+ output,
165
+ artifactPath,
166
+ sessionPath,
167
+ }),
168
+ };
169
+ }
170
+
171
+ return {
172
+ ok: true,
173
+ status,
174
+ failure: buildFailedPanelSummary({
175
+ index,
176
+ member,
177
+ agent,
178
+ summary: failureSummary(rawResult, artifactPath),
179
+ artifactPath,
180
+ sessionPath,
181
+ }),
182
+ };
183
+ }
184
+
185
+ function classifyChildStatus(result: Record<string, unknown>): ChildStatus {
186
+ if (result.success === true) return "success";
187
+ if (result.success === false) return "failed";
188
+ if (result.timedOut === true || result.interrupted === true) return "failed";
189
+ if (firstNonBlankString(result.error)) return "failed";
190
+
191
+ const status = firstString(result.status, result.state);
192
+ if (status) {
193
+ if (status === "completed" || status === "complete") return "success";
194
+ if (status === "failed" || status === "paused" || status === "detached") {
195
+ return "failed";
196
+ }
197
+ }
198
+
199
+ if (typeof result.exitCode === "number") {
200
+ return result.exitCode === 0 ? "success" : "failed";
201
+ }
202
+
203
+ return firstNonBlankString(result.output, result.finalOutput, result.summary)
204
+ ? "success"
205
+ : "failed";
206
+ }
207
+
208
+ function failureSummary(
209
+ result: Record<string, unknown>,
210
+ artifactPath: string | undefined,
211
+ ): string {
212
+ const errorText = firstNonBlankString(result.error);
213
+ const outputText = firstNonBlankString(
214
+ result.summary,
215
+ result.output,
216
+ result.finalOutput,
217
+ result.text,
218
+ );
219
+ if (errorText && outputText && errorText !== outputText) {
220
+ return `${errorText}\n\n${outputText}`;
221
+ }
222
+ if (errorText) return errorText;
223
+ if (outputText) return outputText;
224
+ return artifactOutput(artifactPath) ?? "Panelist failed without a summary.";
225
+ }
226
+
227
+ function buildPanelOutput(input: {
228
+ index: number;
229
+ member: PanelMemberConfig | undefined;
230
+ agent: string;
231
+ output: string;
232
+ artifactPath: string | undefined;
233
+ sessionPath: string | undefined;
234
+ }): PanelOutput {
235
+ return {
236
+ index: input.index,
237
+ agent: input.agent,
238
+ output: input.output,
239
+ ...(input.member?.id ? { id: input.member.id } : {}),
240
+ ...(input.member?.label ? { label: input.member.label } : {}),
241
+ ...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
242
+ ...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
243
+ };
244
+ }
245
+
246
+ function buildFailedPanelSummary(input: {
247
+ index: number;
248
+ member: PanelMemberConfig | undefined;
249
+ agent: string;
250
+ summary: string;
251
+ artifactPath: string | undefined;
252
+ sessionPath: string | undefined;
253
+ }): FailedPanelSummary {
254
+ return {
255
+ index: input.index,
256
+ agent: input.agent,
257
+ summary: input.summary,
258
+ ...(input.member?.id ? { id: input.member.id } : {}),
259
+ ...(input.member?.label ? { label: input.member.label } : {}),
260
+ ...(input.artifactPath ? { artifactPath: input.artifactPath } : {}),
261
+ ...(input.sessionPath ? { sessionPath: input.sessionPath } : {}),
262
+ };
263
+ }
264
+
265
+ function extractArtifactPath(
266
+ result: Record<string, unknown>,
267
+ ): string | undefined {
268
+ const direct = firstString(result.artifactPath, result.savedOutputPath);
269
+ if (direct) return direct;
270
+ if (isRecord(result.artifactPaths)) {
271
+ return firstString(result.artifactPaths.outputPath);
272
+ }
273
+ if (isRecord(result.outputReference)) {
274
+ return firstString(result.outputReference.path);
275
+ }
276
+ return undefined;
277
+ }
278
+
279
+ function artifactOutput(path: string | undefined): string | undefined {
280
+ return path ? `Output artifact: ${path}` : undefined;
281
+ }
282
+
283
+ function firstString(...values: readonly unknown[]): string | undefined {
284
+ for (const value of values) {
285
+ if (typeof value === "string") return value;
286
+ }
287
+ return undefined;
288
+ }
289
+
290
+ function firstNonBlankString(
291
+ ...values: readonly unknown[]
292
+ ): string | undefined {
293
+ for (const value of values) {
294
+ if (typeof value !== "string") continue;
295
+ const trimmed = value.trim();
296
+ if (trimmed) return trimmed;
297
+ }
298
+ return undefined;
299
+ }
300
+
301
+ function error(
302
+ code: ResultExtractErrorCode,
303
+ message: string,
304
+ path: string,
305
+ ): { ok: false; error: ResultExtractError } {
306
+ return { ok: false, error: { code, message, path } };
307
+ }
308
+
309
+ function isRecord(value: unknown): value is Record<string, unknown> {
310
+ return typeof value === "object" && value !== null && !Array.isArray(value);
311
+ }
@@ -0,0 +1,273 @@
1
+ import {
2
+ THINKING_LEVELS,
3
+ type FusionProfile,
4
+ type PanelMemberConfig,
5
+ type ThinkingLevel,
6
+ } from "./types.js";
7
+
8
+ export interface PanelSubagentTaskParams {
9
+ agent: string;
10
+ task: string;
11
+ output: true;
12
+ outputMode: "inline";
13
+ progress: true;
14
+ skill: false;
15
+ acceptance: "none";
16
+ model?: string;
17
+ }
18
+
19
+ export interface PanelSpawnParams {
20
+ tasks: PanelSubagentTaskParams[];
21
+ async: true;
22
+ clarify: false;
23
+ concurrency: number;
24
+ context: "fresh" | "fork";
25
+ output: true;
26
+ outputMode: "inline";
27
+ timeoutMs?: number;
28
+ }
29
+
30
+ export interface JudgeSpawnParams {
31
+ agent: string;
32
+ task: string;
33
+ async: true;
34
+ clarify: false;
35
+ context: "fresh" | "fork";
36
+ output: true;
37
+ outputMode: "inline";
38
+ skill: false;
39
+ acceptance: "none";
40
+ model?: string;
41
+ timeoutMs?: number;
42
+ }
43
+
44
+ export interface PanelOutput {
45
+ index: number;
46
+ agent: string;
47
+ output: string;
48
+ id?: string;
49
+ label?: string;
50
+ artifactPath?: string;
51
+ sessionPath?: string;
52
+ }
53
+
54
+ export interface FailedPanelSummary {
55
+ index: number;
56
+ agent: string;
57
+ summary: string;
58
+ id?: string;
59
+ label?: string;
60
+ artifactPath?: string;
61
+ sessionPath?: string;
62
+ }
63
+
64
+ export interface BuildJudgeSpawnParamsInput {
65
+ profile: FusionProfile;
66
+ prompt: string;
67
+ panelOutputs: readonly PanelOutput[];
68
+ failedPanelists: readonly FailedPanelSummary[];
69
+ }
70
+
71
+ const PANEL_OUTPUT_CONTRACT = [
72
+ "## Summary",
73
+ "## Recommendation",
74
+ "## Evidence",
75
+ "## Risks",
76
+ "## Confidence",
77
+ "## Open Questions",
78
+ ] as const;
79
+
80
+ const JUDGE_OUTPUT_CONTRACT = [
81
+ "# Fusion Report",
82
+ "## Summary",
83
+ "## Agent Status",
84
+ "## Consensus",
85
+ "## Disagreements",
86
+ "## Unique Insights",
87
+ "## Blind Spots",
88
+ "## Recommendation",
89
+ "## Risks",
90
+ "## Next Step",
91
+ ] as const;
92
+
93
+ export function appendThinkingSuffix(
94
+ model: string | undefined,
95
+ thinking: ThinkingLevel | undefined,
96
+ ): string | undefined {
97
+ if (!model || !thinking) return model;
98
+ if (hasThinkingSuffix(model)) return model;
99
+ return `${model}:${thinking}`;
100
+ }
101
+
102
+ export function buildPanelSpawnParams(
103
+ profile: FusionProfile,
104
+ prompt: string,
105
+ ): PanelSpawnParams {
106
+ return {
107
+ tasks: profile.panel.map((member) => buildPanelTaskParams(member, prompt)),
108
+ async: true,
109
+ clarify: false,
110
+ concurrency: profile.concurrency ?? profile.panel.length,
111
+ context: profile.context ?? "fresh",
112
+ output: true,
113
+ outputMode: "inline",
114
+ ...(profile.timeoutMs !== undefined
115
+ ? { timeoutMs: profile.timeoutMs }
116
+ : {}),
117
+ };
118
+ }
119
+
120
+ export function buildJudgeSpawnParams(
121
+ input: BuildJudgeSpawnParamsInput,
122
+ ): JudgeSpawnParams {
123
+ const model = appendThinkingSuffix(
124
+ input.profile.judge.model,
125
+ input.profile.judge.thinking,
126
+ );
127
+ return {
128
+ agent: input.profile.judge.agent,
129
+ task: buildJudgeTask(input),
130
+ async: true,
131
+ clarify: false,
132
+ context: input.profile.context ?? "fresh",
133
+ output: true,
134
+ outputMode: "inline",
135
+ skill: false,
136
+ acceptance: "none",
137
+ ...(model ? { model } : {}),
138
+ ...(input.profile.timeoutMs !== undefined
139
+ ? { timeoutMs: input.profile.timeoutMs }
140
+ : {}),
141
+ };
142
+ }
143
+
144
+ function buildPanelTaskParams(
145
+ member: PanelMemberConfig,
146
+ prompt: string,
147
+ ): PanelSubagentTaskParams {
148
+ const model = appendThinkingSuffix(member.model, member.thinking);
149
+ return {
150
+ agent: member.agent,
151
+ task: buildPanelTask(member, prompt),
152
+ output: true,
153
+ outputMode: "inline",
154
+ progress: true,
155
+ skill: false,
156
+ acceptance: "none",
157
+ ...(model ? { model } : {}),
158
+ };
159
+ }
160
+
161
+ function buildPanelTask(member: PanelMemberConfig, prompt: string): string {
162
+ const role = member.role?.trim() || "independent analysis and critique";
163
+ return [
164
+ `Panel member: ${member.label} (${member.id})`,
165
+ `Role: ${role}`,
166
+ "",
167
+ "Original task:",
168
+ prompt.trim(),
169
+ "",
170
+ "Instructions:",
171
+ "- Work independently from the other panelists.",
172
+ "- Do not edit files, stage changes, commit changes, or run destructive commands.",
173
+ "- Do not ask other agents.",
174
+ "- Do not run subagents.",
175
+ "- Use read-only local inspection only when code evidence is needed.",
176
+ "- Be concise and cite evidence when you inspect files.",
177
+ "",
178
+ "Output contract:",
179
+ ...PANEL_OUTPUT_CONTRACT,
180
+ ].join("\n");
181
+ }
182
+
183
+ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
184
+ const sortedOutputs = [...input.panelOutputs].sort(comparePanelItems);
185
+ const sortedFailures = [...input.failedPanelists].sort(comparePanelItems);
186
+ return [
187
+ "You are the fusion judge.",
188
+ "Do not edit files. Do not ask other agents. Do not run subagents.",
189
+ "Synthesize the panel results. Preserve disagreement instead of forcing consensus.",
190
+ "",
191
+ "Original task:",
192
+ input.prompt.trim(),
193
+ "",
194
+ "Panel status:",
195
+ ...formatPanelStatus(sortedOutputs, sortedFailures),
196
+ "",
197
+ "Successful panel outputs:",
198
+ ...formatPanelOutputs(sortedOutputs),
199
+ "",
200
+ "Failed panelists:",
201
+ ...formatFailedPanelists(sortedFailures),
202
+ "",
203
+ "Output contract:",
204
+ ...JUDGE_OUTPUT_CONTRACT,
205
+ ].join("\n");
206
+ }
207
+
208
+ function formatPanelStatus(
209
+ outputs: readonly PanelOutput[],
210
+ failures: readonly FailedPanelSummary[],
211
+ ): string[] {
212
+ const lines = [
213
+ `- Successful panelists: ${outputs.length}`,
214
+ `- Failed panelists: ${failures.length}`,
215
+ ];
216
+ for (const output of outputs) {
217
+ lines.push(`- ${formatPanelName(output)}: succeeded`);
218
+ }
219
+ for (const failure of failures) {
220
+ lines.push(
221
+ `- ${formatPanelName(failure)}: failed - ${firstLine(failure.summary)}`,
222
+ );
223
+ }
224
+ return lines;
225
+ }
226
+
227
+ function formatPanelOutputs(outputs: readonly PanelOutput[]): string[] {
228
+ if (outputs.length === 0) return ["(none)"];
229
+ return outputs.flatMap((output) => [
230
+ `## ${formatPanelName(output)}`,
231
+ `Agent: ${output.agent}`,
232
+ ...(output.artifactPath ? [`Artifact: ${output.artifactPath}`] : []),
233
+ ...(output.sessionPath ? [`Session: ${output.sessionPath}`] : []),
234
+ "",
235
+ output.output,
236
+ "",
237
+ ]);
238
+ }
239
+
240
+ function formatFailedPanelists(
241
+ failures: readonly FailedPanelSummary[],
242
+ ): string[] {
243
+ if (failures.length === 0) return ["(none)"];
244
+ return failures.flatMap((failure) => [
245
+ `- ${formatPanelName(failure)} (${failure.agent}): ${failure.summary}`,
246
+ ...(failure.artifactPath ? [` Artifact: ${failure.artifactPath}`] : []),
247
+ ...(failure.sessionPath ? [` Session: ${failure.sessionPath}`] : []),
248
+ ]);
249
+ }
250
+
251
+ function formatPanelName(
252
+ item: Pick<PanelOutput, "index" | "id" | "label">,
253
+ ): string {
254
+ return item.label ?? item.id ?? `Panelist ${item.index + 1}`;
255
+ }
256
+
257
+ function comparePanelItems(
258
+ left: Pick<PanelOutput, "index">,
259
+ right: Pick<PanelOutput, "index">,
260
+ ): number {
261
+ return left.index - right.index;
262
+ }
263
+
264
+ function firstLine(value: string): string {
265
+ return value.split(/\r?\n/, 1)[0]?.trim() || "unknown failure";
266
+ }
267
+
268
+ function hasThinkingSuffix(model: string): boolean {
269
+ const colonIndex = model.lastIndexOf(":");
270
+ if (colonIndex === -1) return false;
271
+ const suffix = model.slice(colonIndex + 1);
272
+ return (THINKING_LEVELS as readonly string[]).includes(suffix);
273
+ }