@alexeiled/pi-fusion 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/agents/fusion-composer.md +3 -0
- package/agents/fusion-judge.md +3 -0
- package/agents/fusion-panelist.md +3 -0
- package/docs/user-guide.md +32 -11
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/caller-contract.ts +80 -0
- package/src/config.ts +58 -2
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +88 -13
- package/src/index.ts +33 -5
- package/src/lifecycle-reconcile.ts +445 -0
- package/src/orchestrator.ts +562 -129
- package/src/panel-completion.ts +105 -7
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +105 -3
- package/src/result-extract.ts +84 -7
- package/src/run-builder.ts +141 -15
- package/src/run-store.ts +377 -11
- package/src/status.ts +1 -1
- package/src/types.ts +89 -0
package/src/run-builder.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callerOutputContractInstructions,
|
|
3
|
+
detectCallerOutputContract,
|
|
4
|
+
} from "./caller-contract.js";
|
|
5
|
+
import { FusionArgsError } from "./errors.js";
|
|
6
|
+
import { resolveMinimumSuccessfulPanelists } from "./panel-quorum.js";
|
|
1
7
|
import {
|
|
2
8
|
PANEL_DECISION_CLOSE,
|
|
3
9
|
PANEL_DECISION_OPEN,
|
|
@@ -8,6 +14,9 @@ import {
|
|
|
8
14
|
memberLabel,
|
|
9
15
|
panelItemLabel,
|
|
10
16
|
resolveSynthesisMode,
|
|
17
|
+
type CallerOutputContract,
|
|
18
|
+
type EffectiveFusionTimeouts,
|
|
19
|
+
type FusionTimeoutOverrides,
|
|
11
20
|
THINKING_LEVELS,
|
|
12
21
|
type FailedPanelSummary,
|
|
13
22
|
type FusionProfile,
|
|
@@ -25,6 +34,15 @@ export const FUSION_ACCEPTANCE_DISABLED = {
|
|
|
25
34
|
|
|
26
35
|
export type FusionAcceptanceDisabled = typeof FUSION_ACCEPTANCE_DISABLED;
|
|
27
36
|
|
|
37
|
+
const DEFAULT_STAGE_TIMEOUT_MS = 900_000;
|
|
38
|
+
const DEFAULT_PANELIST_TIMEOUT_MS = 840_000;
|
|
39
|
+
const DEFAULT_PANEL_GRACE_MS = 5_000;
|
|
40
|
+
const DEFAULT_TOOL_BUDGET: ToolBudget = {
|
|
41
|
+
soft: 8,
|
|
42
|
+
hard: 12,
|
|
43
|
+
block: "*",
|
|
44
|
+
};
|
|
45
|
+
|
|
28
46
|
export interface PanelSubagentTaskParams {
|
|
29
47
|
agent: string;
|
|
30
48
|
task: string;
|
|
@@ -34,6 +52,10 @@ export interface PanelSubagentTaskParams {
|
|
|
34
52
|
skill: false;
|
|
35
53
|
acceptance: FusionAcceptanceDisabled;
|
|
36
54
|
model?: string;
|
|
55
|
+
/** Per-task cap so a panelist finalises before the workflow timeout. */
|
|
56
|
+
toolBudget: ToolBudget;
|
|
57
|
+
/** Child deadline, always shorter than the enclosing panel workflow. */
|
|
58
|
+
timeoutMs?: number;
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
export interface PanelWorkflowTaskParams extends PanelSubagentTaskParams {
|
|
@@ -85,6 +107,10 @@ export interface BuildJudgeSpawnParamsInput {
|
|
|
85
107
|
* and its position bias.
|
|
86
108
|
*/
|
|
87
109
|
runId: string;
|
|
110
|
+
callerContract?: CallerOutputContract;
|
|
111
|
+
timeoutOverrides?: FusionTimeoutOverrides;
|
|
112
|
+
/** Persisted at panel start so restored fallback judges keep their deadline. */
|
|
113
|
+
effectiveTimeouts?: EffectiveFusionTimeouts;
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
const PANEL_OUTPUT_CONTRACT = [
|
|
@@ -152,7 +178,10 @@ export function appendThinkingSuffix(
|
|
|
152
178
|
export function buildPanelSpawnParams(
|
|
153
179
|
profile: FusionProfile,
|
|
154
180
|
prompt: string,
|
|
181
|
+
callerContract?: CallerOutputContract,
|
|
182
|
+
timeoutOverrides?: FusionTimeoutOverrides,
|
|
155
183
|
): PanelSpawnParams {
|
|
184
|
+
const timeouts = resolveEffectiveTimeouts(profile, timeoutOverrides);
|
|
156
185
|
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
157
186
|
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
158
187
|
(member, index) => ({
|
|
@@ -161,24 +190,31 @@ export function buildPanelSpawnParams(
|
|
|
161
190
|
member,
|
|
162
191
|
prompt,
|
|
163
192
|
profile.stopWhenPanelAgrees === true,
|
|
193
|
+
profile.panelToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
194
|
+
callerContract,
|
|
195
|
+
timeouts.panelistTimeoutMs,
|
|
164
196
|
),
|
|
165
197
|
}),
|
|
166
198
|
);
|
|
167
199
|
|
|
200
|
+
const requiredSuccessfulPanelists = resolveMinimumSuccessfulPanelists(
|
|
201
|
+
profile.minimumSuccessfulPanelists,
|
|
202
|
+
profile.panel.length,
|
|
203
|
+
);
|
|
204
|
+
|
|
168
205
|
return {
|
|
169
206
|
workflowScript: buildPanelWorkflowScript(
|
|
170
207
|
tasks,
|
|
171
208
|
concurrency,
|
|
172
209
|
profile.stopWhenPanelAgrees === true,
|
|
210
|
+
requiredSuccessfulPanelists,
|
|
173
211
|
),
|
|
174
212
|
async: true,
|
|
175
213
|
context: profile.context ?? "fresh",
|
|
176
214
|
output: true,
|
|
177
215
|
outputMode: "inline",
|
|
178
216
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
179
|
-
|
|
180
|
-
? { timeoutMs: profile.timeoutMs }
|
|
181
|
-
: {}),
|
|
217
|
+
timeoutMs: timeouts.panelTimeoutMs,
|
|
182
218
|
};
|
|
183
219
|
}
|
|
184
220
|
|
|
@@ -197,9 +233,7 @@ export function buildJudgeSpawnParams(
|
|
|
197
233
|
skill: false,
|
|
198
234
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
199
235
|
...(model ? { model } : {}),
|
|
200
|
-
|
|
201
|
-
? { toolBudget: input.profile.judgeToolBudget }
|
|
202
|
-
: {}),
|
|
236
|
+
toolBudget: input.profile.judgeToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
203
237
|
};
|
|
204
238
|
|
|
205
239
|
return {
|
|
@@ -209,9 +243,9 @@ export function buildJudgeSpawnParams(
|
|
|
209
243
|
output: true,
|
|
210
244
|
outputMode: "inline",
|
|
211
245
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
246
|
+
timeoutMs:
|
|
247
|
+
input.effectiveTimeouts?.judgeTimeoutMs ??
|
|
248
|
+
resolveEffectiveTimeouts(input.profile, input.timeoutOverrides).judgeTimeoutMs,
|
|
215
249
|
};
|
|
216
250
|
}
|
|
217
251
|
|
|
@@ -219,13 +253,18 @@ function buildPanelWorkflowScript(
|
|
|
219
253
|
tasks: readonly PanelWorkflowTaskParams[],
|
|
220
254
|
concurrency: number,
|
|
221
255
|
stopWhenAgrees: boolean,
|
|
256
|
+
requiredSuccessfulPanelists: number,
|
|
222
257
|
): string {
|
|
223
258
|
const serializedTasks = JSON.stringify(tasks);
|
|
259
|
+
// Start no more work than the resolved quorum requires. This preserves the
|
|
260
|
+
// two-at-a-time majority behavior while allowing a larger configured quorum
|
|
261
|
+
// to be observed before agreement can stop the remaining panelists.
|
|
224
262
|
const effectiveConcurrency = stopWhenAgrees
|
|
225
|
-
? Math.min(concurrency,
|
|
263
|
+
? Math.min(concurrency, requiredSuccessfulPanelists)
|
|
226
264
|
: concurrency;
|
|
227
265
|
const stopLogic = stopWhenAgrees
|
|
228
266
|
? [
|
|
267
|
+
`const requiredSuccessfulPanelists = ${requiredSuccessfulPanelists};`,
|
|
229
268
|
"const decisions = results",
|
|
230
269
|
" .filter((result) => result && result.ok === true)",
|
|
231
270
|
" .map((result) => {",
|
|
@@ -235,7 +274,7 @@ function buildPanelWorkflowScript(
|
|
|
235
274
|
" try { return JSON.parse(match[1]); } catch { return undefined; }",
|
|
236
275
|
" })",
|
|
237
276
|
" .filter((decision) => decision && typeof decision.recommendation === \"string\" && decision.confidence === \"high\" && decision.needsMoreEvidence === false);",
|
|
238
|
-
"if (decisions.length >=
|
|
277
|
+
"if (decisions.length >= requiredSuccessfulPanelists && results.length < tasks.length) {",
|
|
239
278
|
" const recommendation = decisions[0].recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim(),",
|
|
240
279
|
" agrees = recommendation && decisions.every((decision) => decision.recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim() === recommendation);",
|
|
241
280
|
" if (agrees) {",
|
|
@@ -260,20 +299,87 @@ function buildPanelWorkflowScript(
|
|
|
260
299
|
.join("\n");
|
|
261
300
|
}
|
|
262
301
|
|
|
302
|
+
function resolveStageTimeout(
|
|
303
|
+
stageTimeoutMs: number | undefined,
|
|
304
|
+
legacyTimeoutMs: number | undefined,
|
|
305
|
+
defaultTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS,
|
|
306
|
+
): number {
|
|
307
|
+
return stageTimeoutMs ?? legacyTimeoutMs ?? defaultTimeoutMs;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Resolves and records the deadline precedence used for one start attempt. */
|
|
311
|
+
export function resolveEffectiveTimeouts(
|
|
312
|
+
profile: FusionProfile,
|
|
313
|
+
overrides: FusionTimeoutOverrides | undefined = undefined,
|
|
314
|
+
): EffectiveFusionTimeouts {
|
|
315
|
+
const panelTimeoutMs = resolveStageTimeout(
|
|
316
|
+
overrides?.panelTimeoutMs ?? profile.panelTimeoutMs,
|
|
317
|
+
profile.timeoutMs,
|
|
318
|
+
);
|
|
319
|
+
const panelGraceMs = resolveStageTimeout(
|
|
320
|
+
overrides?.panelGraceMs ?? profile.panelGraceMs,
|
|
321
|
+
undefined,
|
|
322
|
+
DEFAULT_PANEL_GRACE_MS,
|
|
323
|
+
);
|
|
324
|
+
const requestedPanelistTimeoutMs = resolveStageTimeout(
|
|
325
|
+
overrides?.panelistTimeoutMs ?? profile.panelistTimeoutMs,
|
|
326
|
+
profile.timeoutMs,
|
|
327
|
+
DEFAULT_PANELIST_TIMEOUT_MS,
|
|
328
|
+
);
|
|
329
|
+
if (panelGraceMs >= panelTimeoutMs) {
|
|
330
|
+
throw new FusionArgsError(
|
|
331
|
+
`panelGraceMs (${panelGraceMs}ms) must be shorter than panelTimeoutMs (${panelTimeoutMs}ms).`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
// A child must conclude before the enclosing workflow. The validated grace
|
|
335
|
+
// interval guarantees this cap remains a meaningful deadline.
|
|
336
|
+
const panelistTimeoutMs = Math.min(
|
|
337
|
+
requestedPanelistTimeoutMs,
|
|
338
|
+
panelTimeoutMs - panelGraceMs,
|
|
339
|
+
);
|
|
340
|
+
return {
|
|
341
|
+
panelistTimeoutMs,
|
|
342
|
+
panelTimeoutMs,
|
|
343
|
+
panelGraceMs,
|
|
344
|
+
judgeTimeoutMs: resolveStageTimeout(
|
|
345
|
+
overrides?.judgeTimeoutMs ?? profile.judgeTimeoutMs,
|
|
346
|
+
profile.timeoutMs,
|
|
347
|
+
),
|
|
348
|
+
usesLegacyTimeout:
|
|
349
|
+
profile.timeoutMs !== undefined &&
|
|
350
|
+
(overrides?.panelistTimeoutMs === undefined &&
|
|
351
|
+
profile.panelistTimeoutMs === undefined ||
|
|
352
|
+
overrides?.panelTimeoutMs === undefined &&
|
|
353
|
+
profile.panelTimeoutMs === undefined ||
|
|
354
|
+
overrides?.judgeTimeoutMs === undefined &&
|
|
355
|
+
profile.judgeTimeoutMs === undefined),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
263
359
|
function buildPanelTaskParams(
|
|
264
360
|
member: PanelMemberConfig,
|
|
265
361
|
prompt: string,
|
|
266
362
|
includeDecisionRecord: boolean,
|
|
363
|
+
toolBudget: ToolBudget,
|
|
364
|
+
callerContract?: CallerOutputContract,
|
|
365
|
+
timeoutMs?: number,
|
|
267
366
|
): PanelSubagentTaskParams {
|
|
268
367
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
269
368
|
return {
|
|
270
369
|
agent: member.agent,
|
|
271
|
-
task: buildPanelTask(
|
|
370
|
+
task: buildPanelTask(
|
|
371
|
+
member,
|
|
372
|
+
prompt,
|
|
373
|
+
includeDecisionRecord,
|
|
374
|
+
callerContract,
|
|
375
|
+
),
|
|
272
376
|
output: true,
|
|
273
377
|
outputMode: "inline",
|
|
274
378
|
progress: true,
|
|
275
379
|
skill: false,
|
|
276
380
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
381
|
+
toolBudget,
|
|
382
|
+
...(timeoutMs ? { timeoutMs } : {}),
|
|
277
383
|
...(model ? { model } : {}),
|
|
278
384
|
};
|
|
279
385
|
}
|
|
@@ -282,8 +388,11 @@ function buildPanelTask(
|
|
|
282
388
|
member: PanelMemberConfig,
|
|
283
389
|
prompt: string,
|
|
284
390
|
includeDecisionRecord: boolean,
|
|
391
|
+
callerContractOverride?: CallerOutputContract,
|
|
285
392
|
): string {
|
|
286
393
|
const role = member.role?.trim() || "independent analysis and critique";
|
|
394
|
+
const callerContract =
|
|
395
|
+
callerContractForPrompt(prompt, callerContractOverride);
|
|
287
396
|
return [
|
|
288
397
|
`Panel member: ${memberLabel(member)} (${member.id})`,
|
|
289
398
|
`Role: ${role}`,
|
|
@@ -299,8 +408,10 @@ function buildPanelTask(
|
|
|
299
408
|
"- Be concise and cite evidence when you inspect files.",
|
|
300
409
|
"",
|
|
301
410
|
"Output contract:",
|
|
302
|
-
...
|
|
303
|
-
|
|
411
|
+
...(callerContract
|
|
412
|
+
? callerOutputContractInstructions(callerContract)
|
|
413
|
+
: PANEL_OUTPUT_CONTRACT),
|
|
414
|
+
...(includeDecisionRecord && !callerContract
|
|
304
415
|
? [
|
|
305
416
|
"",
|
|
306
417
|
"Decision record:",
|
|
@@ -356,6 +467,13 @@ function formatMemberTask(member: PanelMemberConfig, prompt: string): string[] {
|
|
|
356
467
|
];
|
|
357
468
|
}
|
|
358
469
|
|
|
470
|
+
function callerContractForPrompt(
|
|
471
|
+
prompt: string,
|
|
472
|
+
explicit?: CallerOutputContract,
|
|
473
|
+
): CallerOutputContract | undefined {
|
|
474
|
+
return explicit ?? detectCallerOutputContract(prompt);
|
|
475
|
+
}
|
|
476
|
+
|
|
359
477
|
function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
360
478
|
const sortedOutputs = [...input.panelOutputs].sort(comparePanelItems);
|
|
361
479
|
const sortedFailures = [...input.failedPanelists].sort(comparePanelItems);
|
|
@@ -366,6 +484,10 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
366
484
|
? buildBlindLabelMap([...sortedOutputs, ...sortedFailures])
|
|
367
485
|
: undefined;
|
|
368
486
|
const merging = resolveSynthesisMode(input.profile) === "merge";
|
|
487
|
+
const callerContract = callerContractForPrompt(
|
|
488
|
+
input.prompt,
|
|
489
|
+
input.callerContract,
|
|
490
|
+
);
|
|
369
491
|
return [
|
|
370
492
|
...(merging
|
|
371
493
|
? COMPOSER_INSTRUCTIONS
|
|
@@ -399,7 +521,11 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
399
521
|
// right" wording contradicts the composer's "do not rank panelists".
|
|
400
522
|
...(merging ? [] : [...CONTESTED_CLAIMS_INSTRUCTIONS, ""]),
|
|
401
523
|
"Output contract:",
|
|
402
|
-
...(
|
|
524
|
+
...(callerContract
|
|
525
|
+
? callerOutputContractInstructions(callerContract)
|
|
526
|
+
: merging
|
|
527
|
+
? COMPOSER_OUTPUT_CONTRACT
|
|
528
|
+
: JUDGE_OUTPUT_CONTRACT),
|
|
403
529
|
].join("\n");
|
|
404
530
|
}
|
|
405
531
|
|