pi-background-tasks 0.9.0 → 1.0.3
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/BACKGROUND-TASKS-INSTRUCTIONS.md +63 -0
- package/PUBLISHING.md +43 -29
- package/README.md +233 -441
- package/TESTING.md +15 -9
- package/TEST_PLAN.md +43 -17
- package/docs/INDEX.md +157 -0
- package/docs/api/eventbus-v1.md +166 -0
- package/docs/assets/architecture.svg +78 -0
- package/docs/assets/footer-dock.svg +47 -0
- package/docs/assets/logo.svg +49 -0
- package/docs/attestations.json +189 -0
- package/docs/choose-a-workflow.md +98 -0
- package/docs/commands/bg-clear.md +70 -0
- package/docs/commands/bg-update.md +82 -0
- package/docs/commands/bg.md +90 -0
- package/docs/commands/fusion-models.md +70 -0
- package/docs/commands/fusion.md +69 -0
- package/docs/commands/jobs.md +74 -0
- package/docs/commands/kill.md +82 -0
- package/docs/commands/logs.md +90 -0
- package/docs/commands/task-manager.md +109 -0
- package/docs/concepts/completion-delivery.md +66 -0
- package/docs/concepts/context-projection-and-budgeting.md +79 -0
- package/docs/getting-started.md +122 -0
- package/docs/manifest.json +1825 -0
- package/docs/operations/configuration.md +110 -0
- package/docs/operations/releasing.md +67 -0
- package/docs/operations/testing.md +101 -0
- package/docs/operations/troubleshooting.md +38 -0
- package/docs/read-before-edit.md +94 -0
- package/docs/reference/runtime-contracts.md +213 -0
- package/docs/reference/shortcuts-and-dock.md +70 -0
- package/docs/subsystems/attested-pi-runs.md +141 -0
- package/docs/subsystems/background-task-runtime.md +85 -0
- package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
- package/docs/subsystems/delegation.md +190 -0
- package/docs/subsystems/docs-freshness-gate.md +26 -0
- package/docs/subsystems/fusion.md +121 -0
- package/docs/subsystems/host-ui-and-telemetry.md +83 -0
- package/docs/tools/bg_delegate.md +193 -0
- package/docs/tools/bg_kill.md +114 -0
- package/docs/tools/bg_logs.md +133 -0
- package/docs/tools/bg_result.md +120 -0
- package/docs/tools/bg_run.md +168 -0
- package/docs/tools/bg_run_pi_attested.md +170 -0
- package/docs/tools/bg_status.md +111 -0
- package/docs/tools/fusion_investigate.md +116 -0
- package/docs/tools/fusion_reason.md +75 -0
- package/docs/tools/fusion_research.md +162 -0
- package/docs/tools/fusion_validate.md +206 -0
- package/logo.png +0 -0
- package/package.json +25 -7
- package/src/core/delegate/budget.ts +1 -1
- package/src/core/delegate/launch.ts +5 -0
- package/src/core/fusion/artifacts.ts +34 -4
- package/src/core/fusion/budget.ts +112 -20
- package/src/core/fusion/child-protocol.ts +82 -0
- package/src/core/fusion/clean-context.ts +91 -0
- package/src/core/fusion/config.ts +124 -35
- package/src/core/fusion/context.ts +29 -7
- package/src/core/fusion/evaluation.ts +392 -15
- package/src/core/fusion/orchestrator.ts +217 -23
- package/src/core/fusion/pi-child.ts +183 -23
- package/src/core/fusion/prompts.ts +39 -26
- package/src/core/fusion/source-policy.ts +257 -0
- package/src/core/fusion/types.ts +156 -11
- package/src/core/fusion/web-fetch.ts +104 -15
- package/src/core/fusion/workflows.ts +119 -65
- package/src/extension.ts +3 -3
- package/src/fusion-child-extension.ts +159 -120
- package/src/fusion-extension.ts +585 -240
- package/src/testing/normalize.ts +0 -22
|
@@ -3,9 +3,10 @@ import { lookup as nodeLookup } from 'node:dns/promises';
|
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import * as https from 'node:https';
|
|
5
5
|
import { isIP } from 'node:net';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
6
7
|
import { TextDecoder } from 'node:util';
|
|
7
8
|
|
|
8
|
-
import TurndownService from 'turndown';
|
|
9
|
+
import type TurndownService from 'turndown';
|
|
9
10
|
|
|
10
11
|
export const FUSION_WEB_FETCH_TIMEOUT_MS = 60_000;
|
|
11
12
|
export const FUSION_WEB_FETCH_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
@@ -117,6 +118,10 @@ interface TableReplacement {
|
|
|
117
118
|
markdown: string;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
type TurndownServiceConstructor = typeof TurndownService;
|
|
122
|
+
|
|
123
|
+
let turndownServiceLoad: Promise<TurndownServiceConstructor> | undefined;
|
|
124
|
+
|
|
120
125
|
const USER_AGENT = 'pi-background-tasks fusion_web_fetch/1.0';
|
|
121
126
|
const ACCEPT_HEADER = 'text/markdown, text/html;q=0.9, application/xhtml+xml;q=0.8, text/plain;q=0.7';
|
|
122
127
|
const METADATA_HOSTNAMES = new Set([
|
|
@@ -150,6 +155,7 @@ const IPV4_DENY_RANGES: readonly [number, number][] = [
|
|
|
150
155
|
const IPV6_DENY_RANGES: readonly [bigint, number][] = [
|
|
151
156
|
[ipv6ToBigIntLiteral('::'), 128],
|
|
152
157
|
[ipv6ToBigIntLiteral('::1'), 128],
|
|
158
|
+
[ipv6ToBigIntLiteral('64:ff9b::'), 96],
|
|
153
159
|
[ipv6ToBigIntLiteral('64:ff9b:1::'), 48],
|
|
154
160
|
[ipv6ToBigIntLiteral('100::'), 64],
|
|
155
161
|
[ipv6ToBigIntLiteral('2001:2::'), 48],
|
|
@@ -187,18 +193,19 @@ export async function fusionWebFetch(
|
|
|
187
193
|
const effective = mergeOptions(options);
|
|
188
194
|
const requestedFormat = req.extract ?? 'markdown';
|
|
189
195
|
const start = effective.now();
|
|
196
|
+
const deadlineMs = start + effective.timeoutMs;
|
|
190
197
|
const requestedUrl = normalizeRequestUrl(req.url).url;
|
|
191
198
|
let currentUrl = requestedUrl;
|
|
192
199
|
|
|
193
200
|
for (let redirectCount = 0; ; redirectCount += 1) {
|
|
194
|
-
const remainingMs =
|
|
201
|
+
const remainingMs = deadlineMs - effective.now();
|
|
195
202
|
if (remainingMs <= 0) {
|
|
196
203
|
throw new FusionWebFetchError('request_timeout', 'fusion_web_fetch request timeout elapsed', {
|
|
197
204
|
url: currentUrl.toString(),
|
|
198
205
|
});
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
const result = await fetchOne(currentUrl, effective,
|
|
208
|
+
const result = await fetchOne(currentUrl, effective, deadlineMs, redirectCount > 0);
|
|
202
209
|
if (result.kind === 'redirect') {
|
|
203
210
|
if (redirectCount >= effective.maxRedirects) {
|
|
204
211
|
throw new FusionWebFetchError(
|
|
@@ -211,7 +218,7 @@ export async function fusionWebFetch(
|
|
|
211
218
|
continue;
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
const extracted = extractContent(result.body, result.contentType, requestedFormat);
|
|
221
|
+
const extracted = await extractContent(result.body, result.contentType, requestedFormat);
|
|
215
222
|
const capped = capUtf8Bytes(extracted.content, effective.maxOutputBytes);
|
|
216
223
|
return {
|
|
217
224
|
url: requestedUrl.toString(),
|
|
@@ -234,7 +241,7 @@ function mergeOptions(options: FusionWebFetchOptions): EffectiveOptions {
|
|
|
234
241
|
request: options.request ?? defaultTransportRequest,
|
|
235
242
|
agent: options.agent ?? false,
|
|
236
243
|
createConnection: options.createConnection,
|
|
237
|
-
now: options.now ??
|
|
244
|
+
now: options.now ?? (() => performance.now()),
|
|
238
245
|
timeoutMs: options.timeoutMs ?? FUSION_WEB_FETCH_TIMEOUT_MS,
|
|
239
246
|
maxResponseBytes: options.maxResponseBytes ?? FUSION_WEB_FETCH_MAX_RESPONSE_BYTES,
|
|
240
247
|
maxOutputBytes: options.maxOutputBytes ?? FUSION_WEB_FETCH_MAX_OUTPUT_BYTES,
|
|
@@ -305,11 +312,23 @@ function normalizeRedirectUrl(baseUrl: URL, location: string): URL {
|
|
|
305
312
|
async function fetchOne(
|
|
306
313
|
url: URL,
|
|
307
314
|
options: EffectiveOptions,
|
|
308
|
-
|
|
315
|
+
deadlineMs: number,
|
|
309
316
|
redirectHop: boolean,
|
|
310
317
|
): Promise<FetchOneResult> {
|
|
311
318
|
const normalized = normalizeRequestUrl(url.toString());
|
|
312
|
-
const vetted = await vetHost(
|
|
319
|
+
const vetted = await vetHost(
|
|
320
|
+
normalized.hostname,
|
|
321
|
+
options,
|
|
322
|
+
redirectHop,
|
|
323
|
+
normalized.url.toString(),
|
|
324
|
+
deadlineMs,
|
|
325
|
+
);
|
|
326
|
+
const remainingMs = deadlineMs - options.now();
|
|
327
|
+
if (remainingMs <= 0) {
|
|
328
|
+
throw new FusionWebFetchError('request_timeout', 'fusion_web_fetch request timeout elapsed', {
|
|
329
|
+
url: normalized.url.toString(),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
313
332
|
return await executeRequest(normalized.url, vetted.selectedAddress, options, remainingMs);
|
|
314
333
|
}
|
|
315
334
|
|
|
@@ -318,6 +337,7 @@ async function vetHost(
|
|
|
318
337
|
options: EffectiveOptions,
|
|
319
338
|
redirectHop: boolean,
|
|
320
339
|
url: string,
|
|
340
|
+
deadlineMs: number,
|
|
321
341
|
): Promise<VettedHost> {
|
|
322
342
|
if (METADATA_HOSTNAMES.has(hostname) || hostname === 'localhost' || hostname.endsWith('.localhost')) {
|
|
323
343
|
throwAddressError(redirectHop, `fusion_web_fetch blocked host: ${hostname}`, url);
|
|
@@ -326,7 +346,9 @@ async function vetHost(
|
|
|
326
346
|
const literalFamily = isIP(hostname);
|
|
327
347
|
const literalAddress: FusionDnsAddress = { address: hostname, family: literalFamily === 4 ? 4 : 6 };
|
|
328
348
|
const resolvedAddresses =
|
|
329
|
-
literalFamily === 0
|
|
349
|
+
literalFamily === 0
|
|
350
|
+
? await resolveWithLookup(hostname, options.lookup, url, deadlineMs - options.now())
|
|
351
|
+
: [literalAddress];
|
|
330
352
|
|
|
331
353
|
if (resolvedAddresses.length === 0) {
|
|
332
354
|
throw new FusionWebFetchError('dns_failure', `DNS lookup returned no addresses for ${hostname}`, { url });
|
|
@@ -358,15 +380,38 @@ async function resolveWithLookup(
|
|
|
358
380
|
hostname: string,
|
|
359
381
|
lookup: FusionDnsLookup,
|
|
360
382
|
url: string,
|
|
383
|
+
remainingMs: number,
|
|
361
384
|
): Promise<readonly FusionDnsAddress[]> {
|
|
385
|
+
if (remainingMs <= 0) {
|
|
386
|
+
throw new FusionWebFetchError('request_timeout', 'fusion_web_fetch request timeout elapsed', { url });
|
|
387
|
+
}
|
|
388
|
+
let timer: NodeJS.Timeout | undefined;
|
|
362
389
|
try {
|
|
363
|
-
return await
|
|
390
|
+
return await Promise.race([
|
|
391
|
+
lookup(hostname),
|
|
392
|
+
new Promise<never>((_resolve, reject) => {
|
|
393
|
+
timer = setTimeout(
|
|
394
|
+
() =>
|
|
395
|
+
reject(
|
|
396
|
+
new FusionWebFetchError(
|
|
397
|
+
'request_timeout',
|
|
398
|
+
'fusion_web_fetch request timeout elapsed during DNS lookup',
|
|
399
|
+
{ url },
|
|
400
|
+
),
|
|
401
|
+
),
|
|
402
|
+
Math.max(1, remainingMs),
|
|
403
|
+
);
|
|
404
|
+
}),
|
|
405
|
+
]);
|
|
364
406
|
} catch (error) {
|
|
407
|
+
if (error instanceof FusionWebFetchError) throw error;
|
|
365
408
|
throw new FusionWebFetchError(
|
|
366
409
|
'dns_failure',
|
|
367
410
|
`DNS lookup failed for ${hostname}: ${error instanceof Error ? error.message : 'unknown error'}`,
|
|
368
411
|
error instanceof Error ? { url, cause: error } : { url },
|
|
369
412
|
);
|
|
413
|
+
} finally {
|
|
414
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
370
415
|
}
|
|
371
416
|
}
|
|
372
417
|
|
|
@@ -434,7 +479,9 @@ function executeRequest(
|
|
|
434
479
|
const status = response.statusCode ?? 0;
|
|
435
480
|
const location = firstHeader(response, 'location');
|
|
436
481
|
if (isRedirectStatus(status)) {
|
|
437
|
-
response
|
|
482
|
+
// Redirect payloads are never consumed. Destroy the response/socket before
|
|
483
|
+
// advancing so an endless or oversized redirect body cannot outlive this hop.
|
|
484
|
+
response.destroy();
|
|
438
485
|
if (location === undefined || location.trim().length === 0) {
|
|
439
486
|
settleReject(
|
|
440
487
|
new FusionWebFetchError('http_error', `fusion_web_fetch redirect status ${String(status)} lacks Location`, {
|
|
@@ -638,18 +685,18 @@ function readContentLength(value: string | undefined, url: string, status: numbe
|
|
|
638
685
|
return Number(trimmed);
|
|
639
686
|
}
|
|
640
687
|
|
|
641
|
-
function extractContent(
|
|
688
|
+
async function extractContent(
|
|
642
689
|
body: Buffer,
|
|
643
690
|
contentType: string,
|
|
644
691
|
requestedFormat: 'text' | 'markdown',
|
|
645
|
-
): ExtractionResult {
|
|
692
|
+
): Promise<ExtractionResult> {
|
|
646
693
|
try {
|
|
647
694
|
const mediaType = mediaTypeFromContentType(contentType);
|
|
648
695
|
const decoded = decodeBody(body, contentType);
|
|
649
696
|
if (mediaType === 'text/plain') return { content: decoded, format: 'text' };
|
|
650
697
|
if (mediaType === 'text/markdown') return { content: decoded, format: 'markdown' };
|
|
651
698
|
if (requestedFormat === 'text') return { content: htmlToText(decoded), format: 'text' };
|
|
652
|
-
return { content: htmlToMarkdown(decoded), format: 'markdown' };
|
|
699
|
+
return { content: await htmlToMarkdown(decoded), format: 'markdown' };
|
|
653
700
|
} catch (error) {
|
|
654
701
|
if (error instanceof FusionWebFetchError) throw error;
|
|
655
702
|
throw new FusionWebFetchError(
|
|
@@ -695,10 +742,11 @@ function stripAsciiQuotes(value: string): string {
|
|
|
695
742
|
return value;
|
|
696
743
|
}
|
|
697
744
|
|
|
698
|
-
function htmlToMarkdown(html: string): string {
|
|
745
|
+
async function htmlToMarkdown(html: string): Promise<string> {
|
|
746
|
+
const TurndownServiceClass = await loadTurndownService();
|
|
699
747
|
const stripped = stripUnsafeHtmlBlocks(html);
|
|
700
748
|
const tables = replaceTablesWithTokens(stripped);
|
|
701
|
-
const turndown = new
|
|
749
|
+
const turndown = new TurndownServiceClass({ bulletListMarker: '-', codeBlockStyle: 'fenced', headingStyle: 'atx' });
|
|
702
750
|
let markdown = turndown.turndown(tables.html).trim();
|
|
703
751
|
for (const table of tables.replacements) {
|
|
704
752
|
markdown = markdown.replace(table.token, table.markdown);
|
|
@@ -706,6 +754,47 @@ function htmlToMarkdown(html: string): string {
|
|
|
706
754
|
return markdown.trim();
|
|
707
755
|
}
|
|
708
756
|
|
|
757
|
+
async function loadTurndownService(): Promise<TurndownServiceConstructor> {
|
|
758
|
+
if (turndownServiceLoad === undefined) {
|
|
759
|
+
turndownServiceLoad = import('turndown').then((module) => module.default);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
return await turndownServiceLoad;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
turndownServiceLoad = undefined;
|
|
765
|
+
throw normalizeTurndownLoadError(error);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function normalizeTurndownLoadError(error: unknown): Error {
|
|
770
|
+
if (isMissingTurndownDependency(error)) {
|
|
771
|
+
const details = error instanceof Error ? { cause: error } : {};
|
|
772
|
+
return new FusionWebFetchError(
|
|
773
|
+
'extraction_failed',
|
|
774
|
+
'fusion_web_fetch markdown extraction dependency "turndown" is missing from pi-background-tasks; repair the package install with `pi update --extensions` or `npm install --omit=dev --prefix <pi-background-tasks>`.',
|
|
775
|
+
details,
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
if (error instanceof Error) return error;
|
|
779
|
+
return new Error(`fusion_web_fetch markdown extraction dependency failed to load: ${String(error)}`);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function isMissingTurndownDependency(error: unknown): boolean {
|
|
783
|
+
const code = errorCode(error);
|
|
784
|
+
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') return false;
|
|
785
|
+
return errorMessage(error).includes('turndown');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function errorCode(error: unknown): string | undefined {
|
|
789
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
790
|
+
const code = Reflect.get(error, 'code');
|
|
791
|
+
return typeof code === 'string' ? code : undefined;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function errorMessage(error: unknown): string {
|
|
795
|
+
return error instanceof Error ? error.message : String(error);
|
|
796
|
+
}
|
|
797
|
+
|
|
709
798
|
function htmlToText(html: string): string {
|
|
710
799
|
const withoutBlocks = stripUnsafeHtmlBlocks(html);
|
|
711
800
|
const withBreaks = withoutBlocks
|
|
@@ -9,84 +9,145 @@ import {
|
|
|
9
9
|
fusionValidateCandidateSystemPrompt,
|
|
10
10
|
} from './prompts.js';
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
FUSION_INSPECT_TOOLS,
|
|
13
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
14
|
+
FUSION_RESEARCH_TOOLS,
|
|
13
15
|
FUSION_VALIDATE_CAPABILITY,
|
|
14
16
|
FusionError,
|
|
15
17
|
type FusionCapability,
|
|
18
|
+
type FusionContextKind,
|
|
19
|
+
type FusionPublicWorkflowName,
|
|
16
20
|
type FusionWorkflowId,
|
|
17
21
|
} from './types.js';
|
|
18
22
|
|
|
19
|
-
export const
|
|
20
|
-
export const
|
|
23
|
+
export const FUSION_REASON_TOOL_NAME = 'fusion_reason' as const;
|
|
24
|
+
export const FUSION_INVESTIGATE_TOOL_NAME = 'fusion_investigate' as const;
|
|
25
|
+
export const FUSION_RESEARCH_TOOL_NAME = 'fusion_research' as const;
|
|
26
|
+
export const FUSION_VALIDATE_TOOL_NAME = 'fusion_validate' as const;
|
|
21
27
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* `caller_selected` lets the tool schema offer a capability argument and defaults
|
|
26
|
-
* to the least-privileged profile. `fixed` pins one capability for every run and
|
|
27
|
-
* makes each other value a loud orchestration failure rather than a silent
|
|
28
|
-
* downgrade.
|
|
29
|
-
*/
|
|
30
|
-
export type FusionCapabilityPolicy = 'caller_selected' | 'fixed';
|
|
28
|
+
/** @deprecated v4 recursion-denylist compatibility only; do not register new public APIs with this name. */
|
|
29
|
+
export const FUSION_BRAINSTORM_TOOL_NAME = 'fusion_brainstorm' as const;
|
|
31
30
|
|
|
32
|
-
/**
|
|
33
|
-
* Stage framing for one Fusion workflow.
|
|
34
|
-
*
|
|
35
|
-
* Everything a workflow can vary lives here: the four system prompts, the
|
|
36
|
-
* capability policy, and presentation strings. Everything else - the conversation
|
|
37
|
-
* projection, canonical input schema, budget policy, evaluation schema, artifact
|
|
38
|
-
* store, and state machine - is shared and must never be branched per workflow.
|
|
39
|
-
*/
|
|
40
31
|
export interface FusionWorkflowProfile {
|
|
41
32
|
readonly id: FusionWorkflowId;
|
|
42
|
-
readonly
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
|
|
47
|
-
readonly
|
|
48
|
-
readonly
|
|
33
|
+
readonly publicName: FusionPublicWorkflowName;
|
|
34
|
+
readonly toolName: FusionPublicWorkflowName;
|
|
35
|
+
/** Human-readable run-id prefix, e.g. `reason-<hex>`. */
|
|
36
|
+
readonly runIdPrefix: `${FusionWorkflowId}-`;
|
|
37
|
+
readonly contextKind: FusionContextKind;
|
|
38
|
+
readonly candidateCapability: FusionCapability;
|
|
39
|
+
readonly candidateTools: readonly string[];
|
|
40
|
+
readonly evaluatorCapability: typeof FUSION_NO_TOOLS_CAPABILITY;
|
|
41
|
+
readonly evaluatorTools: readonly [];
|
|
42
|
+
readonly mergeCapability: typeof FUSION_NO_TOOLS_CAPABILITY;
|
|
43
|
+
readonly mergeTools: readonly [];
|
|
49
44
|
readonly candidateSystemPrompt: (capability: FusionCapability) => string;
|
|
50
45
|
readonly evaluatorSystemPrompt: string;
|
|
51
46
|
readonly evaluationRepairSystemPrompt: string;
|
|
52
47
|
readonly mergerSystemPrompt: string;
|
|
53
|
-
/** Human-readable noun used in progress lines and rendered results. */
|
|
54
48
|
readonly label: string;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
function freezeProfile(profile: FusionWorkflowProfile): FusionWorkflowProfile {
|
|
52
|
+
const empty = Object.freeze([]) as readonly [];
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
...profile,
|
|
55
|
+
candidateTools: Object.freeze([...profile.candidateTools]),
|
|
56
|
+
evaluatorTools: empty,
|
|
57
|
+
mergeTools: empty,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const FUSION_REASON_WORKFLOW = freezeProfile({
|
|
62
|
+
id: 'reason',
|
|
63
|
+
publicName: FUSION_REASON_TOOL_NAME,
|
|
64
|
+
toolName: FUSION_REASON_TOOL_NAME,
|
|
65
|
+
runIdPrefix: 'reason-',
|
|
66
|
+
contextKind: 'session_projection',
|
|
67
|
+
candidateCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
68
|
+
candidateTools: [],
|
|
69
|
+
evaluatorCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
70
|
+
evaluatorTools: [],
|
|
71
|
+
mergeCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
72
|
+
mergeTools: [],
|
|
73
|
+
candidateSystemPrompt: fusionCandidateSystemPrompt,
|
|
74
|
+
evaluatorSystemPrompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
75
|
+
evaluationRepairSystemPrompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
76
|
+
mergerSystemPrompt: FUSION_MERGER_SYSTEM_PROMPT,
|
|
77
|
+
label: 'fusion reason',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const FUSION_INVESTIGATE_WORKFLOW = freezeProfile({
|
|
81
|
+
id: 'investigate',
|
|
82
|
+
publicName: FUSION_INVESTIGATE_TOOL_NAME,
|
|
83
|
+
toolName: FUSION_INVESTIGATE_TOOL_NAME,
|
|
84
|
+
runIdPrefix: 'investigate-',
|
|
85
|
+
contextKind: 'clean_task',
|
|
86
|
+
candidateCapability: 'inspect',
|
|
87
|
+
candidateTools: FUSION_INSPECT_TOOLS,
|
|
88
|
+
evaluatorCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
89
|
+
evaluatorTools: [],
|
|
90
|
+
mergeCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
91
|
+
mergeTools: [],
|
|
64
92
|
candidateSystemPrompt: fusionCandidateSystemPrompt,
|
|
65
93
|
evaluatorSystemPrompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
66
94
|
evaluationRepairSystemPrompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
67
95
|
mergerSystemPrompt: FUSION_MERGER_SYSTEM_PROMPT,
|
|
68
|
-
label: 'fusion',
|
|
96
|
+
label: 'fusion investigate',
|
|
69
97
|
});
|
|
70
98
|
|
|
71
|
-
export const
|
|
99
|
+
export const FUSION_RESEARCH_WORKFLOW = freezeProfile({
|
|
100
|
+
id: 'research',
|
|
101
|
+
publicName: FUSION_RESEARCH_TOOL_NAME,
|
|
102
|
+
toolName: FUSION_RESEARCH_TOOL_NAME,
|
|
103
|
+
runIdPrefix: 'research-',
|
|
104
|
+
contextKind: 'clean_task',
|
|
105
|
+
candidateCapability: 'research',
|
|
106
|
+
candidateTools: FUSION_RESEARCH_TOOLS,
|
|
107
|
+
evaluatorCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
108
|
+
evaluatorTools: [],
|
|
109
|
+
mergeCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
110
|
+
mergeTools: [],
|
|
111
|
+
candidateSystemPrompt: fusionCandidateSystemPrompt,
|
|
112
|
+
evaluatorSystemPrompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
113
|
+
evaluationRepairSystemPrompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
114
|
+
mergerSystemPrompt: FUSION_MERGER_SYSTEM_PROMPT,
|
|
115
|
+
label: 'fusion research',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const FUSION_VALIDATE_WORKFLOW = freezeProfile({
|
|
72
119
|
id: 'validate',
|
|
120
|
+
publicName: FUSION_VALIDATE_TOOL_NAME,
|
|
73
121
|
toolName: FUSION_VALIDATE_TOOL_NAME,
|
|
74
|
-
runIdPrefix: '
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
runIdPrefix: 'validate-',
|
|
123
|
+
contextKind: 'clean_task',
|
|
124
|
+
candidateCapability: FUSION_VALIDATE_CAPABILITY,
|
|
125
|
+
candidateTools: FUSION_INSPECT_TOOLS,
|
|
126
|
+
evaluatorCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
127
|
+
evaluatorTools: [],
|
|
128
|
+
mergeCapability: FUSION_NO_TOOLS_CAPABILITY,
|
|
129
|
+
mergeTools: [],
|
|
78
130
|
candidateSystemPrompt: fusionValidateCandidateSystemPrompt,
|
|
79
131
|
evaluatorSystemPrompt: FUSION_VALIDATE_EVALUATOR_SYSTEM_PROMPT,
|
|
80
132
|
evaluationRepairSystemPrompt: FUSION_VALIDATE_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
81
133
|
mergerSystemPrompt: FUSION_VALIDATE_MERGER_SYSTEM_PROMPT,
|
|
82
|
-
label: 'validate',
|
|
134
|
+
label: 'fusion validate',
|
|
83
135
|
});
|
|
84
136
|
|
|
85
137
|
const PROFILES_BY_ID: Readonly<Record<FusionWorkflowId, FusionWorkflowProfile>> = Object.freeze({
|
|
86
|
-
|
|
138
|
+
reason: FUSION_REASON_WORKFLOW,
|
|
139
|
+
investigate: FUSION_INVESTIGATE_WORKFLOW,
|
|
140
|
+
research: FUSION_RESEARCH_WORKFLOW,
|
|
87
141
|
validate: FUSION_VALIDATE_WORKFLOW,
|
|
88
142
|
});
|
|
89
143
|
|
|
144
|
+
export const FUSION_WORKFLOW_PROFILES = Object.freeze([
|
|
145
|
+
FUSION_REASON_WORKFLOW,
|
|
146
|
+
FUSION_INVESTIGATE_WORKFLOW,
|
|
147
|
+
FUSION_RESEARCH_WORKFLOW,
|
|
148
|
+
FUSION_VALIDATE_WORKFLOW,
|
|
149
|
+
] as const);
|
|
150
|
+
|
|
90
151
|
export function fusionWorkflowProfile(id: FusionWorkflowId): FusionWorkflowProfile {
|
|
91
152
|
const profile = PROFILES_BY_ID[id];
|
|
92
153
|
if (profile === undefined) {
|
|
@@ -98,33 +159,26 @@ export function fusionWorkflowProfile(id: FusionWorkflowId): FusionWorkflowProfi
|
|
|
98
159
|
return profile;
|
|
99
160
|
}
|
|
100
161
|
|
|
101
|
-
|
|
102
|
-
* Resolve the candidate capability for one run under its workflow's policy.
|
|
103
|
-
*
|
|
104
|
-
* A `fixed` workflow rejects each other capability instead of quietly substituting
|
|
105
|
-
* its own: silently accepting `reason` for a validation run would produce a review
|
|
106
|
-
* that never read the code, which is exactly the failure this workflow exists to
|
|
107
|
-
* prevent.
|
|
108
|
-
*/
|
|
109
|
-
export function resolveWorkflowCapability(
|
|
162
|
+
export function assertWorkflowCapability(
|
|
110
163
|
profile: FusionWorkflowProfile,
|
|
111
164
|
requested: FusionCapability | undefined,
|
|
112
165
|
): FusionCapability {
|
|
113
|
-
if (profile.
|
|
114
|
-
return requested ?? profile.defaultCapability;
|
|
115
|
-
}
|
|
116
|
-
const fixed = profile.fixedCapability;
|
|
117
|
-
if (fixed === undefined) {
|
|
166
|
+
if (requested !== undefined && requested !== profile.candidateCapability) {
|
|
118
167
|
throw new FusionError(
|
|
119
|
-
`fusion workflow ${profile.id}
|
|
168
|
+
`fusion workflow ${profile.id} always runs candidates with the ${profile.candidateCapability} capability; received ${String(requested)}`,
|
|
120
169
|
{ code: 'orchestration_failed', childCreated: false },
|
|
121
170
|
);
|
|
122
171
|
}
|
|
123
|
-
|
|
124
|
-
throw new FusionError(
|
|
125
|
-
`fusion workflow ${profile.id} always runs candidates with the ${fixed} capability; received ${String(requested)}`,
|
|
126
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
return fixed;
|
|
172
|
+
return profile.candidateCapability;
|
|
130
173
|
}
|
|
174
|
+
|
|
175
|
+
/** @deprecated v4 artifact/testing alias. The retired public tool is never registered. */
|
|
176
|
+
export const FUSION_BRAINSTORM_WORKFLOW = FUSION_REASON_WORKFLOW;
|
|
177
|
+
|
|
178
|
+
export const FUSION_REASON = FUSION_REASON_WORKFLOW;
|
|
179
|
+
export const FUSION_INVESTIGATE = FUSION_INVESTIGATE_WORKFLOW;
|
|
180
|
+
export const FUSION_RESEARCH = FUSION_RESEARCH_WORKFLOW;
|
|
181
|
+
export const FUSION_VALIDATE = FUSION_VALIDATE_WORKFLOW;
|
|
182
|
+
|
|
183
|
+
/** @deprecated v5 workflows do not default; retained for old imports. */
|
|
184
|
+
export const resolveWorkflowCapability = assertWorkflowCapability;
|
package/src/extension.ts
CHANGED
|
@@ -559,7 +559,6 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
559
559
|
const current = PACKAGE_VERSION ?? 'unknown';
|
|
560
560
|
const latest = latestKnownVersion;
|
|
561
561
|
const pinnedNpm = latest ? `${PACKAGE_NAME}@${latest}` : `${PACKAGE_NAME}@<version>`;
|
|
562
|
-
const pinnedGit = latest ? `${GIT_INSTALL_TARGET}@v${latest}` : `${GIT_INSTALL_TARGET}@<tag>`;
|
|
563
562
|
const lines = [
|
|
564
563
|
latest
|
|
565
564
|
? `pi-background-tasks ${current} is installed; ${latest} is the latest published version.`
|
|
@@ -567,8 +566,9 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
567
566
|
'Update from npm:',
|
|
568
567
|
` pi install npm:${PACKAGE_NAME}@latest`,
|
|
569
568
|
` pi install npm:${pinnedNpm}`,
|
|
570
|
-
'
|
|
571
|
-
` pi install ${
|
|
569
|
+
'Git releases are independent of npm versions; use main only when you want current repository state:',
|
|
570
|
+
` pi install ${GIT_INSTALL_TARGET}@main`,
|
|
571
|
+
`For a pinned git release, first verify the tag exists, then use ${GIT_INSTALL_TARGET}@<existing-tag>.`,
|
|
572
572
|
'This command only prints update instructions; it does not install or self-update.',
|
|
573
573
|
];
|
|
574
574
|
ctx.ui.notify(lines.join('\n'), 'info');
|