@alexeiled/pi-fusion 0.7.0 → 0.9.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 +8 -3
- package/agents/fusion-panelist-full.md +3 -1
- package/agents/fusion-panelist-web.md +3 -1
- package/agents/fusion-panelist.md +3 -1
- package/docs/user-guide.md +56 -12
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +2 -1
- package/src/commands.ts +17 -0
- package/src/config.ts +27 -0
- package/src/fusion-args.ts +29 -2
- package/src/fusion-rpc.ts +29 -0
- package/src/index.ts +50 -7
- package/src/lifecycle-reconcile.ts +234 -7
- package/src/orchestrator.ts +622 -79
- package/src/panel-completion.ts +65 -17
- package/src/panel-deadlines.ts +90 -0
- package/src/panel-quorum.ts +22 -0
- package/src/report.ts +85 -3
- package/src/result-extract.ts +67 -5
- package/src/run-builder.ts +129 -9
- package/src/run-observations.ts +3 -1
- package/src/run-store.ts +385 -11
- package/src/status.ts +1 -1
- package/src/subagents-rpc.ts +10 -0
- package/src/types.ts +89 -0
package/src/run-builder.ts
CHANGED
|
@@ -2,6 +2,9 @@ import {
|
|
|
2
2
|
callerOutputContractInstructions,
|
|
3
3
|
detectCallerOutputContract,
|
|
4
4
|
} from "./caller-contract.js";
|
|
5
|
+
import { FusionArgsError } from "./errors.js";
|
|
6
|
+
import { PANEL_FINALIZE_RESERVE_MS } from "./panel-deadlines.js";
|
|
7
|
+
import { resolveMinimumSuccessfulPanelists } from "./panel-quorum.js";
|
|
5
8
|
import {
|
|
6
9
|
PANEL_DECISION_CLOSE,
|
|
7
10
|
PANEL_DECISION_OPEN,
|
|
@@ -13,6 +16,8 @@ import {
|
|
|
13
16
|
panelItemLabel,
|
|
14
17
|
resolveSynthesisMode,
|
|
15
18
|
type CallerOutputContract,
|
|
19
|
+
type EffectiveFusionTimeouts,
|
|
20
|
+
type FusionTimeoutOverrides,
|
|
16
21
|
THINKING_LEVELS,
|
|
17
22
|
type FailedPanelSummary,
|
|
18
23
|
type FusionProfile,
|
|
@@ -31,6 +36,8 @@ export const FUSION_ACCEPTANCE_DISABLED = {
|
|
|
31
36
|
export type FusionAcceptanceDisabled = typeof FUSION_ACCEPTANCE_DISABLED;
|
|
32
37
|
|
|
33
38
|
const DEFAULT_STAGE_TIMEOUT_MS = 900_000;
|
|
39
|
+
const DEFAULT_PANELIST_TIMEOUT_MS = 840_000;
|
|
40
|
+
const DEFAULT_PANEL_GRACE_MS = 5_000;
|
|
34
41
|
const DEFAULT_TOOL_BUDGET: ToolBudget = {
|
|
35
42
|
soft: 8,
|
|
36
43
|
hard: 12,
|
|
@@ -48,6 +55,8 @@ export interface PanelSubagentTaskParams {
|
|
|
48
55
|
model?: string;
|
|
49
56
|
/** Per-task cap so a panelist finalises before the workflow timeout. */
|
|
50
57
|
toolBudget: ToolBudget;
|
|
58
|
+
/** Child deadline, always shorter than the enclosing panel workflow. */
|
|
59
|
+
timeoutMs?: number;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export interface PanelWorkflowTaskParams extends PanelSubagentTaskParams {
|
|
@@ -100,6 +109,9 @@ export interface BuildJudgeSpawnParamsInput {
|
|
|
100
109
|
*/
|
|
101
110
|
runId: string;
|
|
102
111
|
callerContract?: CallerOutputContract;
|
|
112
|
+
timeoutOverrides?: FusionTimeoutOverrides;
|
|
113
|
+
/** Persisted at panel start so restored fallback judges keep their deadline. */
|
|
114
|
+
effectiveTimeouts?: EffectiveFusionTimeouts;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
const PANEL_OUTPUT_CONTRACT = [
|
|
@@ -168,7 +180,9 @@ export function buildPanelSpawnParams(
|
|
|
168
180
|
profile: FusionProfile,
|
|
169
181
|
prompt: string,
|
|
170
182
|
callerContract?: CallerOutputContract,
|
|
183
|
+
timeoutOverrides?: FusionTimeoutOverrides,
|
|
171
184
|
): PanelSpawnParams {
|
|
185
|
+
const timeouts = resolveEffectiveTimeouts(profile, timeoutOverrides);
|
|
172
186
|
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
173
187
|
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
174
188
|
(member, index) => ({
|
|
@@ -179,22 +193,30 @@ export function buildPanelSpawnParams(
|
|
|
179
193
|
profile.stopWhenPanelAgrees === true,
|
|
180
194
|
profile.panelToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
181
195
|
callerContract,
|
|
196
|
+
timeouts.panelistTimeoutMs,
|
|
197
|
+
timeouts.panelistSoftTimeoutMs !== undefined,
|
|
182
198
|
),
|
|
183
199
|
}),
|
|
184
200
|
);
|
|
185
201
|
|
|
202
|
+
const requiredSuccessfulPanelists = resolveMinimumSuccessfulPanelists(
|
|
203
|
+
profile.minimumSuccessfulPanelists,
|
|
204
|
+
profile.panel.length,
|
|
205
|
+
);
|
|
206
|
+
|
|
186
207
|
return {
|
|
187
208
|
workflowScript: buildPanelWorkflowScript(
|
|
188
209
|
tasks,
|
|
189
210
|
concurrency,
|
|
190
211
|
profile.stopWhenPanelAgrees === true,
|
|
212
|
+
requiredSuccessfulPanelists,
|
|
191
213
|
),
|
|
192
214
|
async: true,
|
|
193
215
|
context: profile.context ?? "fresh",
|
|
194
216
|
output: true,
|
|
195
217
|
outputMode: "inline",
|
|
196
218
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
197
|
-
timeoutMs:
|
|
219
|
+
timeoutMs: timeouts.panelTimeoutMs,
|
|
198
220
|
};
|
|
199
221
|
}
|
|
200
222
|
|
|
@@ -223,10 +245,9 @@ export function buildJudgeSpawnParams(
|
|
|
223
245
|
output: true,
|
|
224
246
|
outputMode: "inline",
|
|
225
247
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
226
|
-
timeoutMs:
|
|
227
|
-
input.
|
|
228
|
-
input.profile.
|
|
229
|
-
),
|
|
248
|
+
timeoutMs:
|
|
249
|
+
input.effectiveTimeouts?.judgeTimeoutMs ??
|
|
250
|
+
resolveEffectiveTimeouts(input.profile, input.timeoutOverrides).judgeTimeoutMs,
|
|
230
251
|
};
|
|
231
252
|
}
|
|
232
253
|
|
|
@@ -234,13 +255,41 @@ function buildPanelWorkflowScript(
|
|
|
234
255
|
tasks: readonly PanelWorkflowTaskParams[],
|
|
235
256
|
concurrency: number,
|
|
236
257
|
stopWhenAgrees: boolean,
|
|
258
|
+
requiredSuccessfulPanelists: number,
|
|
237
259
|
): string {
|
|
238
260
|
const serializedTasks = JSON.stringify(tasks);
|
|
261
|
+
if (!stopWhenAgrees) {
|
|
262
|
+
return [
|
|
263
|
+
`const tasks = ${serializedTasks};`,
|
|
264
|
+
`const concurrency = ${concurrency};`,
|
|
265
|
+
"const results = new Array(tasks.length);",
|
|
266
|
+
"const pending = new Map();",
|
|
267
|
+
"let next = 0;",
|
|
268
|
+
"while (next < tasks.length || pending.size > 0) {",
|
|
269
|
+
" while (next < tasks.length && pending.size < concurrency) {",
|
|
270
|
+
" const index = next++;",
|
|
271
|
+
" const { key, ...task } = tasks[index];",
|
|
272
|
+
" const pendingRun = runs.run(key, task);",
|
|
273
|
+
" pending.set(index, Promise.all([pendingRun]).then(([result]) => ({ index, result })));",
|
|
274
|
+
" }",
|
|
275
|
+
" const { index, result } = await Promise.race(pending.values());",
|
|
276
|
+
" results[index] = result;",
|
|
277
|
+
" pending.delete(index);",
|
|
278
|
+
"}",
|
|
279
|
+
"return results;",
|
|
280
|
+
].join("\n");
|
|
281
|
+
}
|
|
282
|
+
// Agreement panels deliberately use quorum-sized rounds: starting speculative
|
|
283
|
+
// replacements would spend more calls before the current votes can agree.
|
|
284
|
+
// Start no more work than the resolved quorum requires. This preserves the
|
|
285
|
+
// two-at-a-time majority behavior while allowing a larger configured quorum
|
|
286
|
+
// to be observed before agreement can stop the remaining panelists.
|
|
239
287
|
const effectiveConcurrency = stopWhenAgrees
|
|
240
|
-
? Math.min(concurrency,
|
|
288
|
+
? Math.min(concurrency, requiredSuccessfulPanelists)
|
|
241
289
|
: concurrency;
|
|
242
290
|
const stopLogic = stopWhenAgrees
|
|
243
291
|
? [
|
|
292
|
+
`const requiredSuccessfulPanelists = ${requiredSuccessfulPanelists};`,
|
|
244
293
|
"const decisions = results",
|
|
245
294
|
" .filter((result) => result && result.ok === true)",
|
|
246
295
|
" .map((result) => {",
|
|
@@ -250,7 +299,7 @@ function buildPanelWorkflowScript(
|
|
|
250
299
|
" try { return JSON.parse(match[1]); } catch { return undefined; }",
|
|
251
300
|
" })",
|
|
252
301
|
" .filter((decision) => decision && typeof decision.recommendation === \"string\" && decision.confidence === \"high\" && decision.needsMoreEvidence === false);",
|
|
253
|
-
"if (decisions.length >=
|
|
302
|
+
"if (decisions.length >= requiredSuccessfulPanelists && results.length < tasks.length) {",
|
|
254
303
|
" const recommendation = decisions[0].recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim(),",
|
|
255
304
|
" agrees = recommendation && decisions.every((decision) => decision.recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim() === recommendation);",
|
|
256
305
|
" if (agrees) {",
|
|
@@ -278,8 +327,72 @@ function buildPanelWorkflowScript(
|
|
|
278
327
|
function resolveStageTimeout(
|
|
279
328
|
stageTimeoutMs: number | undefined,
|
|
280
329
|
legacyTimeoutMs: number | undefined,
|
|
330
|
+
defaultTimeoutMs = DEFAULT_STAGE_TIMEOUT_MS,
|
|
281
331
|
): number {
|
|
282
|
-
return stageTimeoutMs ?? legacyTimeoutMs ??
|
|
332
|
+
return stageTimeoutMs ?? legacyTimeoutMs ?? defaultTimeoutMs;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Resolves and records the deadline precedence used for one start attempt. */
|
|
336
|
+
export function resolveEffectiveTimeouts(
|
|
337
|
+
profile: FusionProfile,
|
|
338
|
+
overrides: FusionTimeoutOverrides | undefined = undefined,
|
|
339
|
+
): EffectiveFusionTimeouts {
|
|
340
|
+
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
341
|
+
const waves = Math.ceil(profile.panel.length / concurrency);
|
|
342
|
+
const panelTimeoutMs = resolveStageTimeout(
|
|
343
|
+
overrides?.panelTimeoutMs ?? profile.panelTimeoutMs,
|
|
344
|
+
profile.timeoutMs,
|
|
345
|
+
DEFAULT_STAGE_TIMEOUT_MS * waves,
|
|
346
|
+
);
|
|
347
|
+
const panelGraceMs = resolveStageTimeout(
|
|
348
|
+
overrides?.panelGraceMs ?? profile.panelGraceMs,
|
|
349
|
+
undefined,
|
|
350
|
+
DEFAULT_PANEL_GRACE_MS,
|
|
351
|
+
);
|
|
352
|
+
const requestedPanelistTimeoutMs = resolveStageTimeout(
|
|
353
|
+
overrides?.panelistTimeoutMs ?? profile.panelistTimeoutMs,
|
|
354
|
+
profile.timeoutMs,
|
|
355
|
+
DEFAULT_PANELIST_TIMEOUT_MS,
|
|
356
|
+
);
|
|
357
|
+
if (panelGraceMs >= panelTimeoutMs) {
|
|
358
|
+
throw new FusionArgsError(
|
|
359
|
+
`panelGraceMs (${panelGraceMs}ms) must be shorter than panelTimeoutMs (${panelTimeoutMs}ms).`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
// A child must conclude before the enclosing workflow. The validated grace
|
|
363
|
+
// interval guarantees this cap remains a meaningful deadline.
|
|
364
|
+
const panelistTimeoutMs = Math.min(
|
|
365
|
+
requestedPanelistTimeoutMs,
|
|
366
|
+
panelTimeoutMs - panelGraceMs,
|
|
367
|
+
);
|
|
368
|
+
const softTimeoutMs = profile.panelistSoftTimeoutMs;
|
|
369
|
+
if (softTimeoutMs !== undefined && (!Number.isInteger(softTimeoutMs) || softTimeoutMs <= 0)) {
|
|
370
|
+
throw new FusionArgsError("panelistSoftTimeoutMs must be a positive integer.");
|
|
371
|
+
}
|
|
372
|
+
if (softTimeoutMs !== undefined && panelTimeoutMs < waves * panelistTimeoutMs + panelGraceMs) {
|
|
373
|
+
throw new FusionArgsError("The panel deadline must cover every concurrency wave plus grace when soft deadlines are enabled.");
|
|
374
|
+
}
|
|
375
|
+
if (softTimeoutMs !== undefined && softTimeoutMs >= panelistTimeoutMs - PANEL_FINALIZE_RESERVE_MS) {
|
|
376
|
+
throw new FusionArgsError("panelistSoftTimeoutMs must leave more than one minute before the effective panelist hard deadline.");
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
...(softTimeoutMs !== undefined ? { panelistSoftTimeoutMs: softTimeoutMs } : {}),
|
|
380
|
+
panelistTimeoutMs,
|
|
381
|
+
panelTimeoutMs,
|
|
382
|
+
panelGraceMs,
|
|
383
|
+
judgeTimeoutMs: resolveStageTimeout(
|
|
384
|
+
overrides?.judgeTimeoutMs ?? profile.judgeTimeoutMs,
|
|
385
|
+
profile.timeoutMs,
|
|
386
|
+
),
|
|
387
|
+
usesLegacyTimeout:
|
|
388
|
+
profile.timeoutMs !== undefined &&
|
|
389
|
+
(overrides?.panelistTimeoutMs === undefined &&
|
|
390
|
+
profile.panelistTimeoutMs === undefined ||
|
|
391
|
+
overrides?.panelTimeoutMs === undefined &&
|
|
392
|
+
profile.panelTimeoutMs === undefined ||
|
|
393
|
+
overrides?.judgeTimeoutMs === undefined &&
|
|
394
|
+
profile.judgeTimeoutMs === undefined),
|
|
395
|
+
};
|
|
283
396
|
}
|
|
284
397
|
|
|
285
398
|
function buildPanelTaskParams(
|
|
@@ -288,6 +401,8 @@ function buildPanelTaskParams(
|
|
|
288
401
|
includeDecisionRecord: boolean,
|
|
289
402
|
toolBudget: ToolBudget,
|
|
290
403
|
callerContract?: CallerOutputContract,
|
|
404
|
+
timeoutMs?: number,
|
|
405
|
+
allowSupervisor = false,
|
|
291
406
|
): PanelSubagentTaskParams {
|
|
292
407
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
293
408
|
return {
|
|
@@ -297,6 +412,7 @@ function buildPanelTaskParams(
|
|
|
297
412
|
prompt,
|
|
298
413
|
includeDecisionRecord,
|
|
299
414
|
callerContract,
|
|
415
|
+
allowSupervisor,
|
|
300
416
|
),
|
|
301
417
|
output: true,
|
|
302
418
|
outputMode: "inline",
|
|
@@ -304,6 +420,7 @@ function buildPanelTaskParams(
|
|
|
304
420
|
skill: false,
|
|
305
421
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
306
422
|
toolBudget,
|
|
423
|
+
...(timeoutMs ? { timeoutMs } : {}),
|
|
307
424
|
...(model ? { model } : {}),
|
|
308
425
|
};
|
|
309
426
|
}
|
|
@@ -313,6 +430,7 @@ function buildPanelTask(
|
|
|
313
430
|
prompt: string,
|
|
314
431
|
includeDecisionRecord: boolean,
|
|
315
432
|
callerContractOverride?: CallerOutputContract,
|
|
433
|
+
allowSupervisor = false,
|
|
316
434
|
): string {
|
|
317
435
|
const role = member.role?.trim() || "independent analysis and critique";
|
|
318
436
|
const callerContract =
|
|
@@ -326,7 +444,9 @@ function buildPanelTask(
|
|
|
326
444
|
"Instructions:",
|
|
327
445
|
"- Work independently from the other panelists.",
|
|
328
446
|
"- Read-only: inspect only; leave files, git state, and the workspace untouched.",
|
|
329
|
-
|
|
447
|
+
allowSupervisor
|
|
448
|
+
? "- Do not consult other panelists. Parent supervisor coordination is allowed only for progress updates and deadline decisions."
|
|
449
|
+
: "- Do not ask other agents.",
|
|
330
450
|
"- Do not run subagents.",
|
|
331
451
|
"- Use local inspection only when code evidence is needed.",
|
|
332
452
|
"- Be concise and cite evidence when you inspect files.",
|
package/src/run-observations.ts
CHANGED
|
@@ -151,7 +151,9 @@ export function extractRunObservation(value: unknown): RunObservation {
|
|
|
151
151
|
const providerFailures = summarizeProviderFailures(
|
|
152
152
|
rawError &&
|
|
153
153
|
attemptFailures.length === 0 &&
|
|
154
|
-
(value.success === false ||
|
|
154
|
+
(value.success === false ||
|
|
155
|
+
value.state === "failed" ||
|
|
156
|
+
value.status === "failed")
|
|
155
157
|
? [
|
|
156
158
|
{
|
|
157
159
|
provider: model ? providerFromModel(model) : "unknown provider",
|