pi-background-tasks 0.7.7 → 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/PUBLISHING.md +7 -7
- package/README.md +64 -7
- package/TEST_PLAN.md +8 -1
- package/package.json +7 -2
- package/src/core/delegate/launch.ts +1 -0
- package/src/core/fusion/artifacts.ts +49 -4
- package/src/core/fusion/budget.ts +21 -12
- package/src/core/fusion/context.ts +7 -2
- package/src/core/fusion/orchestrator.ts +70 -15
- package/src/core/fusion/pi-child.ts +473 -8
- package/src/core/fusion/prompts.ts +151 -3
- package/src/core/fusion/types.ts +84 -2
- package/src/core/fusion/web-fetch.ts +904 -0
- package/src/core/fusion/workflows.ts +130 -0
- package/src/fusion-child-extension.ts +279 -2
- package/src/fusion-extension.ts +182 -27
package/src/fusion-extension.ts
CHANGED
|
@@ -22,12 +22,21 @@ import {
|
|
|
22
22
|
buildFusionCanonicalInput,
|
|
23
23
|
normalizeFusionCommandRequest,
|
|
24
24
|
} from './core/fusion/context.js';
|
|
25
|
+
import {
|
|
26
|
+
FUSION_BRAINSTORM_WORKFLOW,
|
|
27
|
+
FUSION_VALIDATE_TOOL_NAME,
|
|
28
|
+
FUSION_VALIDATE_WORKFLOW,
|
|
29
|
+
type FusionWorkflowProfile,
|
|
30
|
+
} from './core/fusion/workflows.js';
|
|
25
31
|
import type { JsonObject } from './core/common.js';
|
|
26
32
|
import { FusionOrchestrator } from './core/fusion/orchestrator.js';
|
|
27
33
|
import {
|
|
34
|
+
FUSION_CAPABILITY_VALUES,
|
|
35
|
+
FUSION_DEFAULT_CAPABILITY,
|
|
28
36
|
FUSION_RESULT_SCHEMA_VERSION,
|
|
29
37
|
FusionError,
|
|
30
38
|
cloneFusionUsage,
|
|
39
|
+
type FusionCapability,
|
|
31
40
|
type FusionModelConfigV1,
|
|
32
41
|
type FusionModelSelection,
|
|
33
42
|
type FusionProgressEvent,
|
|
@@ -73,6 +82,8 @@ interface FusionRunRequest {
|
|
|
73
82
|
source: 'command' | 'tool';
|
|
74
83
|
ctx: ExtensionContext;
|
|
75
84
|
request: string;
|
|
85
|
+
profile?: FusionWorkflowProfile | undefined;
|
|
86
|
+
capability?: FusionCapability | undefined;
|
|
76
87
|
signal?: AbortSignal | undefined;
|
|
77
88
|
toolCallId?: string | undefined;
|
|
78
89
|
onProgress?: ((event: FusionProgressEvent) => void) | undefined;
|
|
@@ -84,9 +95,15 @@ interface FusionRequestDetails {
|
|
|
84
95
|
source: 'command';
|
|
85
96
|
}
|
|
86
97
|
|
|
87
|
-
const FusionBrainstormParams = Type.Object(
|
|
98
|
+
export const FusionBrainstormParams = Type.Object(
|
|
88
99
|
{
|
|
89
100
|
prompt: Type.String({ description: 'Prompt to run through the five-model fusion workflow.' }),
|
|
101
|
+
capability: Type.Optional(
|
|
102
|
+
Type.Union([Type.Literal('reason'), Type.Literal('inspect'), Type.Literal('research')], {
|
|
103
|
+
description:
|
|
104
|
+
"Optional candidate-child capability: 'reason' uses no tools; 'inspect' enables read-only file inspection; 'research' enables read-only file inspection plus fusion_web_fetch.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
90
107
|
},
|
|
91
108
|
{ additionalProperties: false },
|
|
92
109
|
);
|
|
@@ -139,29 +156,35 @@ function toolFailureMessage(error: unknown): string {
|
|
|
139
156
|
return `Fusion failed${location}: ${errorMessage(error)}${errorArtifactSuffix(error)}`;
|
|
140
157
|
}
|
|
141
158
|
|
|
142
|
-
function progressText(event: FusionProgressEvent): string {
|
|
143
|
-
if (event.type === 'state') return
|
|
144
|
-
if (event.type === 'candidate_started')
|
|
159
|
+
function progressText(event: FusionProgressEvent, label = FUSION_BRAINSTORM_WORKFLOW.label): string {
|
|
160
|
+
if (event.type === 'state') return `${label}: ${event.state.replace(/_/g, ' ')}`;
|
|
161
|
+
if (event.type === 'candidate_started')
|
|
162
|
+
return `${label}: candidate ${String(event.slot)} starting`;
|
|
145
163
|
if (event.type === 'candidate_completed')
|
|
146
|
-
return
|
|
164
|
+
return `${label}: candidates ${String(event.completed)}/${String(event.total)} complete`;
|
|
147
165
|
if (event.type === 'evaluation_started')
|
|
148
|
-
return event.repair
|
|
166
|
+
return event.repair
|
|
167
|
+
? `${label}: repairing evaluator JSON`
|
|
168
|
+
: `${label}: evaluating candidates`;
|
|
149
169
|
if (event.type === 'evaluation_retry')
|
|
150
|
-
return
|
|
170
|
+
return `${label}: evaluator schema retry (${String(event.errors.length)} issue${event.errors.length === 1 ? '' : 's'})`;
|
|
151
171
|
if (event.type === 'budget_warning')
|
|
152
|
-
return
|
|
172
|
+
return `${label}: budget warning (${String(event.warnings.length)} stage${event.warnings.length === 1 ? '' : 's'} at or above 80%)`;
|
|
153
173
|
if (event.type === 'calibration_warning')
|
|
154
|
-
return
|
|
155
|
-
if (event.type === 'merge_started') return
|
|
156
|
-
if (event.type === 'completed') return
|
|
157
|
-
if (event.type === 'cancelled') return
|
|
158
|
-
return
|
|
174
|
+
return `${label}: calibration warning (${String(event.warning.under_forecast_tokens)} tokens under forecast)`;
|
|
175
|
+
if (event.type === 'merge_started') return `${label}: merging final answer`;
|
|
176
|
+
if (event.type === 'completed') return `${label}: completed`;
|
|
177
|
+
if (event.type === 'cancelled') return `${label}: cancelled (${event.reason})`;
|
|
178
|
+
return `${label}: failed (${event.error})`;
|
|
159
179
|
}
|
|
160
180
|
|
|
161
|
-
function makeProgressDetails(
|
|
181
|
+
function makeProgressDetails(
|
|
182
|
+
event: FusionProgressEvent,
|
|
183
|
+
label = FUSION_BRAINSTORM_WORKFLOW.label,
|
|
184
|
+
): FusionProgressDetails {
|
|
162
185
|
return {
|
|
163
186
|
schema_version: FUSION_PROGRESS_SCHEMA_VERSION,
|
|
164
|
-
status: progressText(event),
|
|
187
|
+
status: progressText(event, label),
|
|
165
188
|
event,
|
|
166
189
|
};
|
|
167
190
|
}
|
|
@@ -189,12 +212,13 @@ function renderFusionResultText(
|
|
|
189
212
|
details: FusionResultDetails,
|
|
190
213
|
options: ToolRenderResultOptions,
|
|
191
214
|
theme: Theme,
|
|
215
|
+
label = FUSION_BRAINSTORM_WORKFLOW.label,
|
|
192
216
|
) {
|
|
193
217
|
if (options.expanded) {
|
|
194
218
|
const container = new Container();
|
|
195
219
|
container.addChild(
|
|
196
220
|
new Text(
|
|
197
|
-
`${theme.fg('success',
|
|
221
|
+
`${theme.fg('success', `✓ ${label} complete`)} ${theme.fg('dim', details.run_id)}\n${theme.fg('dim', `Artifacts: ${details.artifact_dir} · ${usageSummary(details)}`)}`,
|
|
198
222
|
0,
|
|
199
223
|
0,
|
|
200
224
|
),
|
|
@@ -204,7 +228,7 @@ function renderFusionResultText(
|
|
|
204
228
|
}
|
|
205
229
|
const preview = mergedText.replace(/\s+/g, ' ').trim();
|
|
206
230
|
return new Text(
|
|
207
|
-
`${theme.fg('success',
|
|
231
|
+
`${theme.fg('success', `✓ ${label}`)} ${theme.fg('dim', details.run_id)} ${theme.fg('muted', usageSummary(details))}\n${preview}`,
|
|
208
232
|
0,
|
|
209
233
|
0,
|
|
210
234
|
);
|
|
@@ -219,6 +243,7 @@ function isFusionResultDetails(value: unknown): value is FusionResultDetails {
|
|
|
219
243
|
return (
|
|
220
244
|
value['schema_version'] === FUSION_RESULT_SCHEMA_VERSION &&
|
|
221
245
|
typeof value['run_id'] === 'string' &&
|
|
246
|
+
(value['workflow'] === 'brainstorm' || value['workflow'] === 'validate') &&
|
|
222
247
|
(value['source'] === 'command' || value['source'] === 'tool') &&
|
|
223
248
|
value['status'] === 'completed' &&
|
|
224
249
|
typeof value['artifact_dir'] === 'string' &&
|
|
@@ -278,11 +303,62 @@ function normalizeToolPrompt(value: unknown): string {
|
|
|
278
303
|
return prompt;
|
|
279
304
|
}
|
|
280
305
|
|
|
281
|
-
function
|
|
306
|
+
function normalizeFusionCapability(value: unknown): FusionCapability {
|
|
307
|
+
if (value === undefined) return FUSION_DEFAULT_CAPABILITY;
|
|
308
|
+
if (typeof value !== 'string') {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`fusion_brainstorm capability must be one of: ${FUSION_CAPABILITY_VALUES.join(', ')}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (FUSION_CAPABILITY_VALUES.includes(value as FusionCapability)) return value as FusionCapability;
|
|
314
|
+
throw new Error(
|
|
315
|
+
`fusion_brainstorm capability ${JSON.stringify(value)} is not supported; allowed values: ${FUSION_CAPABILITY_VALUES.join(', ')}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function prepareFusionArguments(args: unknown): FusionBrainstormParamsValue {
|
|
282
320
|
if (!isRecord(args)) throw new Error('fusion_brainstorm arguments must be an object');
|
|
283
321
|
const keys = Object.keys(args);
|
|
284
|
-
|
|
285
|
-
|
|
322
|
+
const unknown = keys.filter((key) => key !== 'prompt' && key !== 'capability');
|
|
323
|
+
if (unknown.length > 0 || !keys.includes('prompt')) {
|
|
324
|
+
throw new Error('fusion_brainstorm arguments must contain prompt and optional capability only');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
prompt: normalizeToolPrompt(args['prompt']),
|
|
328
|
+
capability: normalizeFusionCapability(args['capability']),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export const FusionValidateParams = Type.Object(
|
|
333
|
+
{
|
|
334
|
+
prompt: Type.String({
|
|
335
|
+
description:
|
|
336
|
+
'What was done and what must hold true about it. Reviewers read the repository themselves.',
|
|
337
|
+
}),
|
|
338
|
+
},
|
|
339
|
+
{ additionalProperties: false },
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
type FusionValidateParamsValue = Static<typeof FusionValidateParams>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* `fusion_validate` takes no capability.
|
|
346
|
+
*
|
|
347
|
+
* A caller-supplied capability is rejected rather than ignored: silently dropping
|
|
348
|
+
* `capability:'reason'` would run a review whose children never read the code,
|
|
349
|
+
* which is the one outcome this tool exists to prevent.
|
|
350
|
+
*/
|
|
351
|
+
export function prepareFusionValidateArguments(args: unknown): FusionValidateParamsValue {
|
|
352
|
+
if (!isRecord(args)) throw new Error('fusion_validate arguments must be an object');
|
|
353
|
+
const keys = Object.keys(args);
|
|
354
|
+
if (keys.includes('capability')) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
'fusion_validate does not accept capability; validation always runs candidates with read-only inspect access',
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const unknown = keys.filter((key) => key !== 'prompt');
|
|
360
|
+
if (unknown.length > 0 || !keys.includes('prompt')) {
|
|
361
|
+
throw new Error('fusion_validate arguments must contain prompt only');
|
|
286
362
|
}
|
|
287
363
|
return { prompt: normalizeToolPrompt(args['prompt']) };
|
|
288
364
|
}
|
|
@@ -328,6 +404,7 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
328
404
|
if (shuttingDown || lifecycleGeneration !== generation)
|
|
329
405
|
throw new Error('fusion extension is shutting down');
|
|
330
406
|
};
|
|
407
|
+
const profile = request.profile ?? FUSION_BRAINSTORM_WORKFLOW;
|
|
331
408
|
try {
|
|
332
409
|
assertActive();
|
|
333
410
|
const contextOptions =
|
|
@@ -335,13 +412,13 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
335
412
|
? {
|
|
336
413
|
source: request.source,
|
|
337
414
|
request: request.request,
|
|
338
|
-
toolName:
|
|
415
|
+
toolName: profile.toolName,
|
|
339
416
|
}
|
|
340
417
|
: {
|
|
341
418
|
source: request.source,
|
|
342
419
|
request: request.request,
|
|
343
420
|
toolCallId: request.toolCallId,
|
|
344
|
-
toolName:
|
|
421
|
+
toolName: profile.toolName,
|
|
345
422
|
};
|
|
346
423
|
const built = buildFusionCanonicalInput(request.ctx, contextOptions);
|
|
347
424
|
const cwd = request.ctx.cwd;
|
|
@@ -367,6 +444,8 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
367
444
|
contextLedger: built.ledger,
|
|
368
445
|
config: loaded.config,
|
|
369
446
|
models,
|
|
447
|
+
profile,
|
|
448
|
+
candidateCapability: request.capability,
|
|
370
449
|
signal: controller.signal,
|
|
371
450
|
onProgress: request.onProgress,
|
|
372
451
|
});
|
|
@@ -554,12 +633,13 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
554
633
|
pi.registerTool<typeof FusionBrainstormParams, FusionToolDetails>({
|
|
555
634
|
name: FUSION_BRAINSTORM_TOOL_NAME,
|
|
556
635
|
label: 'Fusion Brainstorm',
|
|
557
|
-
description:
|
|
636
|
+
description:
|
|
637
|
+
"Run a five-model fusion workflow for a prompt and return the merged answer. Optional capability:'inspect' lets candidate children use read-only file tools; capability:'research' also enables fusion_web_fetch.",
|
|
558
638
|
promptSnippet:
|
|
559
639
|
'Use fusion_brainstorm to get a merged answer from the five-model fusion workflow',
|
|
560
640
|
promptGuidelines: [
|
|
561
|
-
|
|
562
|
-
'
|
|
641
|
+
"fusion_brainstorm is always available; call fusion_brainstorm({prompt}) for no-tool reasoning, fusion_brainstorm({prompt, capability:'inspect'}) when candidate children need read-only file inspection, or fusion_brainstorm({prompt, capability:'research'}) when they also need to fetch a specific public URL.",
|
|
642
|
+
"Use capability:'inspect' only when the answer benefits from reading/searching/listing repository files; use capability:'research' only when public web fetching is required. Evaluator and merger remain no-tools by policy.",
|
|
563
643
|
],
|
|
564
644
|
parameters: FusionBrainstormParams,
|
|
565
645
|
prepareArguments: prepareFusionArguments,
|
|
@@ -571,6 +651,7 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
571
651
|
source: 'tool',
|
|
572
652
|
ctx,
|
|
573
653
|
request: prompt,
|
|
654
|
+
capability: params.capability ?? FUSION_DEFAULT_CAPABILITY,
|
|
574
655
|
signal,
|
|
575
656
|
toolCallId,
|
|
576
657
|
onProgress: (event) => {
|
|
@@ -610,12 +691,86 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
610
691
|
},
|
|
611
692
|
});
|
|
612
693
|
|
|
694
|
+
pi.registerTool<typeof FusionValidateParams, FusionToolDetails>({
|
|
695
|
+
name: FUSION_VALIDATE_TOOL_NAME,
|
|
696
|
+
label: 'Fusion Validate',
|
|
697
|
+
description:
|
|
698
|
+
'Run a five-model fusion validation review of completed work and return the merged review. Reviewers always have read-only repository access; there is no capability argument.',
|
|
699
|
+
promptSnippet:
|
|
700
|
+
'Use fusion_validate to get a merged multi-model review of work that was just completed',
|
|
701
|
+
promptGuidelines: [
|
|
702
|
+
'Call fusion_validate({prompt}) after work is complete to get an independent multi-model review. It always runs with read-only repository inspection and takes no capability argument.',
|
|
703
|
+
'State in the prompt what was done, where it lives, and what must hold true. Reviewers read the repository themselves, but facts that exist only inside omitted tool output are not available to them; restate those in the prompt.',
|
|
704
|
+
'fusion_validate returns a prose review whose findings are classified critical, high, or minor. It never modifies files and is not a substitute for running tests or builds.',
|
|
705
|
+
],
|
|
706
|
+
parameters: FusionValidateParams,
|
|
707
|
+
prepareArguments: prepareFusionValidateArguments,
|
|
708
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
709
|
+
const prompt = normalizeToolPrompt(params.prompt);
|
|
710
|
+
const label = FUSION_VALIDATE_WORKFLOW.label;
|
|
711
|
+
let result: FusionRunResult;
|
|
712
|
+
try {
|
|
713
|
+
result = await runFusion({
|
|
714
|
+
source: 'tool',
|
|
715
|
+
ctx,
|
|
716
|
+
request: prompt,
|
|
717
|
+
profile: FUSION_VALIDATE_WORKFLOW,
|
|
718
|
+
// Workflow policy, not caller input: the fixed capability is resolved by the
|
|
719
|
+
// workflow itself, so no caller-selected value can reach this launch.
|
|
720
|
+
signal,
|
|
721
|
+
toolCallId,
|
|
722
|
+
onProgress: (event) => {
|
|
723
|
+
onUpdate?.({
|
|
724
|
+
content: textContent(progressText(event, label)),
|
|
725
|
+
details: makeProgressDetails(event, label),
|
|
726
|
+
});
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
throw new Error(toolFailureMessage(error), { cause: error });
|
|
731
|
+
}
|
|
732
|
+
const toolResult: FusionToolResultWithUsage = {
|
|
733
|
+
content: textContent(result.mergedText),
|
|
734
|
+
details: result.details,
|
|
735
|
+
usage: cloneFusionUsage(result.details.usage),
|
|
736
|
+
};
|
|
737
|
+
return toolResult;
|
|
738
|
+
},
|
|
739
|
+
renderCall(args, theme) {
|
|
740
|
+
const preview = args.prompt.replace(/\s+/g, ' ').trim();
|
|
741
|
+
return new Text(
|
|
742
|
+
`${theme.fg('toolTitle', theme.bold('fusion_validate '))}${theme.fg('muted', preview)}`,
|
|
743
|
+
0,
|
|
744
|
+
0,
|
|
745
|
+
);
|
|
746
|
+
},
|
|
747
|
+
renderResult(result, options, theme) {
|
|
748
|
+
if (isFusionProgressDetails(result.details))
|
|
749
|
+
return renderProgressResult(result.details, theme);
|
|
750
|
+
if (!isFusionResultDetails(result.details))
|
|
751
|
+
return new Text(theme.fg('error', 'Invalid fusion tool details'), 0, 0);
|
|
752
|
+
const mergedText = result.content
|
|
753
|
+
.map((part) => (part.type === 'text' ? part.text : ''))
|
|
754
|
+
.join('\n');
|
|
755
|
+
return renderFusionResultText(
|
|
756
|
+
mergedText,
|
|
757
|
+
result.details,
|
|
758
|
+
options,
|
|
759
|
+
theme,
|
|
760
|
+
FUSION_VALIDATE_WORKFLOW.label,
|
|
761
|
+
);
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
613
765
|
pi.on('session_start', () => {
|
|
614
766
|
shuttingDown = false;
|
|
615
767
|
lifecycleGeneration += 1;
|
|
616
768
|
const active = pi.getActiveTools();
|
|
617
|
-
|
|
618
|
-
|
|
769
|
+
const missing = [FUSION_BRAINSTORM_TOOL_NAME, FUSION_VALIDATE_TOOL_NAME].filter(
|
|
770
|
+
(name) => !active.includes(name),
|
|
771
|
+
);
|
|
772
|
+
if (missing.length > 0) {
|
|
773
|
+
pi.setActiveTools([...active, ...missing]);
|
|
619
774
|
}
|
|
620
775
|
});
|
|
621
776
|
|