pi-background-tasks 2.1.3 → 2.3.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/BACKGROUND-TASKS-INSTRUCTIONS.md +1 -1
- package/PUBLISHING.md +2 -0
- package/README.md +16 -9
- package/TESTING.md +15 -10
- package/TEST_PLAN.md +12 -11
- package/THIRD_PARTY_NOTICES.md +30 -0
- package/docs/INDEX.md +8 -4
- package/docs/choose-a-workflow.md +5 -2
- package/docs/commands/claude-cache.md +50 -0
- package/docs/getting-started.md +3 -0
- package/docs/manifest.json +84 -19
- package/docs/operations/configuration.md +15 -1
- package/docs/operations/releasing.md +6 -3
- package/docs/read-before-edit.md +4 -1
- package/docs/reference/runtime-contracts.md +72 -71
- package/docs/subsystems/anthropic-attribution.md +63 -0
- package/docs/subsystems/attested-pi-runs.md +2 -2
- package/docs/subsystems/child-launch-durability-and-safety.md +3 -3
- package/docs/subsystems/delegation.md +12 -4
- package/docs/subsystems/docs-freshness-gate.md +6 -6
- package/docs/subsystems/fusion.md +6 -2
- package/docs/tools/bg_delegate.md +21 -9
- package/docs/tools/bg_result.md +8 -1
- package/docs/tools/bg_run.md +5 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/package.json +4 -2
- package/src/core/anthropic-attribution-path.ts +26 -0
- package/src/core/{fusion/anthropic-attribution.ts → anthropic-attribution.ts} +61 -8
- package/src/core/attested-pi-run.ts +10 -1
- package/src/core/common.ts +2 -1
- package/src/core/delegate/artifacts.ts +4 -0
- package/src/core/delegate/launch.ts +44 -14
- package/src/core/delegate/runner.ts +24 -0
- package/src/core/delegate/seed.ts +12 -0
- package/src/core/delegate/types.ts +10 -2
- package/src/core/fusion/artifacts.ts +265 -3
- package/src/core/fusion/config.ts +1 -1
- package/src/core/fusion/orchestrator.ts +47 -57
- package/src/core/fusion/pi-child.ts +7 -124
- package/src/core/fusion/result-package.ts +550 -3
- package/src/core/fusion/types.ts +87 -0
- package/src/core/registry.ts +4 -1
- package/src/delegate-child-extension.ts +9 -2
- package/src/delegate-extension.ts +119 -9
|
@@ -5,6 +5,8 @@ import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
|
|
|
5
5
|
import { assertChildOutputWithinContract } from './output-contract.js';
|
|
6
6
|
import {
|
|
7
7
|
FusionArtifactStore,
|
|
8
|
+
buildFusionFailureSummary,
|
|
9
|
+
buildFusionRunProgress as deriveFusionRunProgress,
|
|
8
10
|
type CreateFusionArtifactStoreOptions,
|
|
9
11
|
type RecordFusionFailedAttemptInput,
|
|
10
12
|
} from './artifacts.js';
|
|
@@ -40,9 +42,7 @@ import {
|
|
|
40
42
|
FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
|
|
41
43
|
FusionError,
|
|
42
44
|
addFusionUsage,
|
|
43
|
-
cloneFusionUsage,
|
|
44
45
|
createEmptyFusionUsage,
|
|
45
|
-
type FusionArtifactManifest,
|
|
46
46
|
type FusionCalibrationViolation,
|
|
47
47
|
type FusionCapability,
|
|
48
48
|
type FusionCanonicalInputV3,
|
|
@@ -176,58 +176,7 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
manifest: FusionArtifactManifest,
|
|
181
|
-
stage: FusionStage,
|
|
182
|
-
): FusionRunProgress['candidates'] {
|
|
183
|
-
const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
|
|
184
|
-
const created = attempts.filter((attempt) => attempt.child_created).length;
|
|
185
|
-
const completed = attempts.filter(
|
|
186
|
-
(attempt) => attempt.child_created && attempt.status === 'completed',
|
|
187
|
-
).length;
|
|
188
|
-
const failed = attempts.filter(
|
|
189
|
-
(attempt) => attempt.child_created && attempt.status === 'failed',
|
|
190
|
-
).length;
|
|
191
|
-
const cancelled = attempts.filter(
|
|
192
|
-
(attempt) => attempt.child_created && attempt.status === 'cancelled',
|
|
193
|
-
).length;
|
|
194
|
-
const completedByState =
|
|
195
|
-
stage === 'candidate'
|
|
196
|
-
? completed >= 3
|
|
197
|
-
: stage === 'evaluation'
|
|
198
|
-
? manifest.artifacts['evaluation.json'] !== undefined ||
|
|
199
|
-
manifest.state === 'evaluation_complete' ||
|
|
200
|
-
manifest.state === 'merging' ||
|
|
201
|
-
manifest.state === 'completed'
|
|
202
|
-
: manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
|
|
203
|
-
const progress: FusionRunProgress['candidates'] = {
|
|
204
|
-
status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
|
|
205
|
-
attempts_recorded: attempts.length,
|
|
206
|
-
children_created: created,
|
|
207
|
-
children_completed: completed,
|
|
208
|
-
children_failed: failed,
|
|
209
|
-
children_cancelled: cancelled,
|
|
210
|
-
};
|
|
211
|
-
if (stage === 'candidate') {
|
|
212
|
-
const createdSlots = new Set(
|
|
213
|
-
attempts.flatMap((attempt) =>
|
|
214
|
-
attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : [],
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
progress.not_started_slots = 3 - createdSlots.size;
|
|
218
|
-
}
|
|
219
|
-
return progress;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function buildFusionRunProgress(manifest: FusionArtifactManifest): FusionRunProgress {
|
|
223
|
-
return {
|
|
224
|
-
manifest_state: manifest.state,
|
|
225
|
-
candidates: fusionStageProgress(manifest, 'candidate'),
|
|
226
|
-
evaluation: fusionStageProgress(manifest, 'evaluation'),
|
|
227
|
-
merge: fusionStageProgress(manifest, 'merge'),
|
|
228
|
-
usage_so_far: cloneFusionUsage(manifest.usage),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
179
|
+
export { buildFusionRunProgress } from './artifacts.js';
|
|
231
180
|
|
|
232
181
|
function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates']): string {
|
|
233
182
|
const notStarted =
|
|
@@ -237,7 +186,31 @@ function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates
|
|
|
237
186
|
return `${name}=${stage.status} (${String(stage.children_created)} created, ${String(stage.children_completed)} completed, ${String(stage.children_failed)} failed, ${String(stage.children_cancelled)} cancelled${notStarted})`;
|
|
238
187
|
}
|
|
239
188
|
|
|
240
|
-
export function
|
|
189
|
+
export function summaryUnavailableNote(error: unknown): string {
|
|
190
|
+
const detail = errorText(error);
|
|
191
|
+
const detailBytes = Buffer.from(detail, 'utf8');
|
|
192
|
+
if (detailBytes.length > 1024) {
|
|
193
|
+
return 'failure-summary.json unavailable after terminal publication; write failure detail omitted because it exceeds the 1024-byte diagnostic cap.';
|
|
194
|
+
}
|
|
195
|
+
return `failure-summary.json unavailable after terminal publication: ${detail}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function withSummaryUnavailableNote(error: FusionError, summaryError: unknown): FusionError {
|
|
199
|
+
const details: FusionErrorDetails = {
|
|
200
|
+
code: error.code,
|
|
201
|
+
transient: error.transient,
|
|
202
|
+
childCreated: error.childCreated,
|
|
203
|
+
};
|
|
204
|
+
if (error.artifactDir !== undefined) details.artifactDir = error.artifactDir;
|
|
205
|
+
if (error.stage !== undefined) details.stage = error.stage;
|
|
206
|
+
if (error.slot !== undefined) details.slot = error.slot;
|
|
207
|
+
if (error.attempt !== undefined) details.attempt = error.attempt;
|
|
208
|
+
if (error.budget !== undefined) details.budget = error.budget;
|
|
209
|
+
if (error.runProgress !== undefined) details.runProgress = error.runProgress;
|
|
210
|
+
return new FusionError(`${error.message}\n${summaryUnavailableNote(summaryError)}`, details);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatFusionRunProgress(progress: FusionRunProgress): string {
|
|
241
214
|
const usage = progress.usage_so_far;
|
|
242
215
|
const optionalUsage = [
|
|
243
216
|
usage.cacheWrite1h === undefined ? undefined : `cacheWrite1h=${String(usage.cacheWrite1h)}`,
|
|
@@ -962,9 +935,26 @@ export class FusionOrchestrator {
|
|
|
962
935
|
terminalError = withRunProgress(
|
|
963
936
|
error,
|
|
964
937
|
store.artifactDir,
|
|
965
|
-
|
|
938
|
+
deriveFusionRunProgress(store.snapshot()),
|
|
966
939
|
);
|
|
967
|
-
|
|
940
|
+
const terminalState = cancelled ? 'cancelled' : 'failed';
|
|
941
|
+
await store.writeError(terminalState, terminalError.message);
|
|
942
|
+
// The terminal manifest/error are authoritative. Summary persistence is
|
|
943
|
+
// subordinate and intentionally attempted once from that fresh snapshot.
|
|
944
|
+
try {
|
|
945
|
+
const terminalManifest = store.snapshot();
|
|
946
|
+
await store.writeFailureSummary(
|
|
947
|
+
buildFusionFailureSummary({
|
|
948
|
+
manifest: terminalManifest,
|
|
949
|
+
terminalError,
|
|
950
|
+
progress: deriveFusionRunProgress(terminalManifest),
|
|
951
|
+
terminalState,
|
|
952
|
+
createdAt: terminalManifest.updated_at,
|
|
953
|
+
}),
|
|
954
|
+
);
|
|
955
|
+
} catch (summaryError) {
|
|
956
|
+
terminalError = withSummaryUnavailableNote(terminalError, summaryError);
|
|
957
|
+
}
|
|
968
958
|
} catch (artifactError) {
|
|
969
959
|
throw withTerminalArtifactFailure(error, store.artifactDir, artifactError);
|
|
970
960
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { constants, existsSync
|
|
3
|
+
import { constants, existsSync } from 'node:fs';
|
|
4
4
|
import { open } from 'node:fs/promises';
|
|
5
|
-
import { createRequire } from 'node:module';
|
|
6
5
|
import { dirname, resolve } from 'node:path';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
7
|
import {
|
|
@@ -73,6 +72,7 @@ import {
|
|
|
73
72
|
resolvePiLaunch,
|
|
74
73
|
type PiLaunchDependencies,
|
|
75
74
|
} from '../pi-launch.js';
|
|
75
|
+
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
76
76
|
|
|
77
77
|
// The response cap now applies to one final full answer, not cumulative Pi JSON events.
|
|
78
78
|
export const FUSION_CHILD_STDOUT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
@@ -274,21 +274,6 @@ export function fusionPiChildEnv(
|
|
|
274
274
|
return out;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
export function resolveFusionAnthropicAttributionExtensionPath(
|
|
278
|
-
moduleUrl = import.meta.url,
|
|
279
|
-
pathExists: (path: string) => boolean = existsSync,
|
|
280
|
-
): string {
|
|
281
|
-
const modulePath = fileURLToPath(moduleUrl);
|
|
282
|
-
const extension = modulePath.endsWith('.ts')
|
|
283
|
-
? 'anthropic-attribution.ts'
|
|
284
|
-
: 'anthropic-attribution.js';
|
|
285
|
-
const candidate = resolve(dirname(modulePath), extension);
|
|
286
|
-
if (!pathExists(candidate)) {
|
|
287
|
-
throw new Error(`Fusion Anthropic attribution extension is missing: ${candidate}`);
|
|
288
|
-
}
|
|
289
|
-
return candidate;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
277
|
export function resolveFusionChildExtensionPath(
|
|
293
278
|
moduleUrl = import.meta.url,
|
|
294
279
|
pathExists: (path: string) => boolean = existsSync,
|
|
@@ -303,109 +288,10 @@ export function resolveFusionChildExtensionPath(
|
|
|
303
288
|
}
|
|
304
289
|
|
|
305
290
|
/**
|
|
306
|
-
* Provider whose children require the
|
|
307
|
-
*
|
|
308
|
-
* Pi's own system prompt contains documentation lines that Anthropic rejects, so a
|
|
309
|
-
* Claude child launched without the sanitizer fails at the provider rather than
|
|
310
|
-
* producing an answer. The parent session loads the sanitizer through ordinary
|
|
311
|
-
* extension discovery, but Fusion children run with `--no-extensions` for
|
|
312
|
-
* isolation and therefore inherit nothing; the sanitizer must be re-supplied
|
|
313
|
-
* explicitly per child.
|
|
291
|
+
* Provider whose isolated children require the package-owned attribution and
|
|
292
|
+
* exact-match system-prompt sanitization extension.
|
|
314
293
|
*/
|
|
315
294
|
export const FUSION_SANITIZED_PROVIDER = 'anthropic';
|
|
316
|
-
export const FUSION_ANTHROPIC_SANITIZER_PACKAGE = '@ravshansbox/pi-anthropic-sps';
|
|
317
|
-
const FUSION_ANTHROPIC_SANITIZER_MANIFEST = `${FUSION_ANTHROPIC_SANITIZER_PACKAGE}/package.json`;
|
|
318
|
-
|
|
319
|
-
export interface FusionSanitizerDependencies {
|
|
320
|
-
resolvePackageJson?: ((specifier: string) => string) | undefined;
|
|
321
|
-
readManifest?: ((path: string) => string) | undefined;
|
|
322
|
-
pathExists?: ((path: string) => boolean) | undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function manifestExtensionEntry(manifestText: string, manifestPath: string): string {
|
|
326
|
-
let parsed: unknown;
|
|
327
|
-
try {
|
|
328
|
-
parsed = parseJsonText(manifestText);
|
|
329
|
-
} catch (error) {
|
|
330
|
-
throw new FusionError(
|
|
331
|
-
`Anthropic sanitizer manifest ${manifestPath} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
332
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
if (!isJsonObject(parsed)) {
|
|
336
|
-
throw new FusionError(`Anthropic sanitizer manifest ${manifestPath} must be an object`, {
|
|
337
|
-
code: 'orchestration_failed',
|
|
338
|
-
childCreated: false,
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
const pi = parsed['pi'];
|
|
342
|
-
if (!isJsonObject(pi)) {
|
|
343
|
-
throw new FusionError(
|
|
344
|
-
`Anthropic sanitizer manifest ${manifestPath} has no "pi" section declaring its extension`,
|
|
345
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const extensions = pi['extensions'];
|
|
349
|
-
if (!Array.isArray(extensions) || extensions.length === 0) {
|
|
350
|
-
throw new FusionError(
|
|
351
|
-
`Anthropic sanitizer manifest ${manifestPath} declares no pi.extensions entries`,
|
|
352
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
const [entry] = extensions;
|
|
356
|
-
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
357
|
-
throw new FusionError(
|
|
358
|
-
`Anthropic sanitizer manifest ${manifestPath} pi.extensions[0] must be a non-blank string`,
|
|
359
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
return entry;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Resolve the sanitizer extension file shipped by the sanitizer package.
|
|
367
|
-
*
|
|
368
|
-
* The package intentionally publishes no `main`/`exports`, so the entry cannot be
|
|
369
|
-
* required directly; its manifest is resolved and the declared `pi.extensions[0]`
|
|
370
|
-
* path is joined against the package root. Every failure is loud: a Claude child
|
|
371
|
-
* launched without the sanitizer would fail at the provider with a far less
|
|
372
|
-
* actionable error, so silently omitting it is never correct.
|
|
373
|
-
*/
|
|
374
|
-
export function resolveAnthropicSanitizerExtensionPath(
|
|
375
|
-
dependencies: FusionSanitizerDependencies = {},
|
|
376
|
-
): string {
|
|
377
|
-
const resolvePackageJson =
|
|
378
|
-
dependencies.resolvePackageJson ?? createRequire(import.meta.url).resolve;
|
|
379
|
-
const readManifest = dependencies.readManifest ?? ((path: string) => readFileSync(path, 'utf8'));
|
|
380
|
-
const pathExists = dependencies.pathExists ?? existsSync;
|
|
381
|
-
let manifestPath: string;
|
|
382
|
-
try {
|
|
383
|
-
manifestPath = resolvePackageJson(FUSION_ANTHROPIC_SANITIZER_MANIFEST);
|
|
384
|
-
} catch (error) {
|
|
385
|
-
throw new FusionError(
|
|
386
|
-
`Anthropic sanitizer package ${FUSION_ANTHROPIC_SANITIZER_PACKAGE} could not be resolved: ${error instanceof Error ? error.message : String(error)}. Claude children cannot be launched without it.`,
|
|
387
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
let manifestText: string;
|
|
391
|
-
try {
|
|
392
|
-
manifestText = readManifest(manifestPath);
|
|
393
|
-
} catch (error) {
|
|
394
|
-
throw new FusionError(
|
|
395
|
-
`Anthropic sanitizer manifest ${manifestPath} could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
396
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const entry = manifestExtensionEntry(manifestText, manifestPath);
|
|
400
|
-
const extensionPath = resolve(dirname(manifestPath), entry);
|
|
401
|
-
if (!pathExists(extensionPath)) {
|
|
402
|
-
throw new FusionError(
|
|
403
|
-
`Anthropic sanitizer extension is missing: ${extensionPath} (declared by ${manifestPath})`,
|
|
404
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
return extensionPath;
|
|
408
|
-
}
|
|
409
295
|
|
|
410
296
|
export function assertFusionToolPolicyDisjoint(
|
|
411
297
|
allowlist: readonly string[] = FUSION_INSPECT_TOOLS,
|
|
@@ -468,11 +354,10 @@ function fusionToolArgv(capability: FusionCapability): string[] {
|
|
|
468
354
|
export function fusionChildExtensionPaths(
|
|
469
355
|
model: ResolvedFusionModel,
|
|
470
356
|
childExtensionPath: string,
|
|
471
|
-
|
|
472
|
-
resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
|
|
357
|
+
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
473
358
|
): readonly string[] {
|
|
474
359
|
if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
|
|
475
|
-
return [resolveAttribution(),
|
|
360
|
+
return [resolveAttribution(), childExtensionPath];
|
|
476
361
|
}
|
|
477
362
|
|
|
478
363
|
export function buildFusionPiChildArgv(
|
|
@@ -480,13 +365,11 @@ export function buildFusionPiChildArgv(
|
|
|
480
365
|
systemPrompt: string,
|
|
481
366
|
childExtensionPath = resolveFusionChildExtensionPath(),
|
|
482
367
|
capability: FusionCapability = FUSION_NO_TOOLS_CAPABILITY,
|
|
483
|
-
|
|
484
|
-
resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
|
|
368
|
+
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
485
369
|
): string[] {
|
|
486
370
|
const extensionArgs = fusionChildExtensionPaths(
|
|
487
371
|
model,
|
|
488
372
|
childExtensionPath,
|
|
489
|
-
resolveSanitizer,
|
|
490
373
|
resolveAttribution,
|
|
491
374
|
).flatMap((path) => ['--extension', path]);
|
|
492
375
|
return [
|