pi-background-tasks 2.1.4 → 2.4.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 -8
- package/TESTING.md +23 -14
- package/TEST_PLAN.md +13 -12
- 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/concepts/context-projection-and-budgeting.md +4 -2
- package/docs/getting-started.md +3 -0
- package/docs/manifest.json +86 -22
- package/docs/operations/configuration.md +15 -1
- package/docs/operations/releasing.md +6 -3
- package/docs/operations/troubleshooting.md +3 -1
- package/docs/read-before-edit.md +4 -1
- package/docs/reference/runtime-contracts.md +53 -53
- 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 +34 -17
- package/docs/subsystems/docs-freshness-gate.md +6 -6
- package/docs/subsystems/fusion.md +2 -2
- package/docs/tools/bg_delegate.md +33 -16
- package/docs/tools/bg_result.md +2 -2
- 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/context/token-budget.ts +16 -3
- package/src/core/delegate/artifacts.ts +18 -12
- package/src/core/delegate/budget.ts +78 -33
- package/src/core/delegate/launch.ts +48 -16
- package/src/core/delegate/result-package.ts +16 -0
- package/src/core/delegate/runner.ts +45 -2
- package/src/core/delegate/seed.ts +12 -0
- package/src/core/delegate/types.ts +22 -3
- package/src/core/fusion/config.ts +1 -1
- package/src/core/fusion/pi-child.ts +7 -124
- package/src/core/registry.ts +4 -1
- package/src/delegate-child-extension.ts +377 -71
- package/src/delegate-extension.ts +58 -6
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
DELEGATE_TOOL_NAME,
|
|
18
18
|
DelegateError,
|
|
19
19
|
type DelegateCapability,
|
|
20
|
+
type DelegateExtensionMode,
|
|
20
21
|
type DelegateLimits,
|
|
21
22
|
type DelegatePinnedRoute,
|
|
22
23
|
type DelegateRoute,
|
|
@@ -87,7 +88,7 @@ export function resolveDelegateRoute(input: DelegateRouteResolutionInput): Deleg
|
|
|
87
88
|
childCreated: false,
|
|
88
89
|
remediation: [
|
|
89
90
|
'Name a provider/model pair that appears in the current model registry.',
|
|
90
|
-
|
|
91
|
+
"Omit the route argument to use the parent session's current model.",
|
|
91
92
|
],
|
|
92
93
|
},
|
|
93
94
|
);
|
|
@@ -239,12 +240,23 @@ export function delegateToolsFor(capability: DelegateCapability): readonly strin
|
|
|
239
240
|
});
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
function assertDelegateExtensionMode(mode: DelegateExtensionMode): void {
|
|
244
|
+
if (mode === 'isolated' || mode === 'ambient') return;
|
|
245
|
+
throw new DelegateError(`bg_delegate extension mode ${String(mode)} is not supported`, {
|
|
246
|
+
code: 'invalid_arguments',
|
|
247
|
+
childCreated: false,
|
|
248
|
+
remediation: ['Use extensionMode "isolated" or "ambient".'],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
242
252
|
export interface DelegateChildArgvInput {
|
|
243
253
|
route: DelegatePinnedRoute;
|
|
244
254
|
capability: DelegateCapability;
|
|
255
|
+
extensionMode: DelegateExtensionMode;
|
|
245
256
|
childSessionId: string;
|
|
246
257
|
childSessionDir: string;
|
|
247
258
|
childExtensionPath: string;
|
|
259
|
+
attributionExtensionPath?: string | undefined;
|
|
248
260
|
systemPrompt: string;
|
|
249
261
|
}
|
|
250
262
|
|
|
@@ -252,11 +264,15 @@ export interface DelegateChildArgvInput {
|
|
|
252
264
|
* Build the child argv.
|
|
253
265
|
*
|
|
254
266
|
* The child gets its own `--session-id` and a task-owned `--session-dir`, so it
|
|
255
|
-
* is structurally incapable of opening or mutating the parent session.
|
|
256
|
-
*
|
|
257
|
-
* disabled
|
|
267
|
+
* is structurally incapable of opening or mutating the parent session. Skills,
|
|
268
|
+
* prompt templates, themes, and context files are always disabled. Extension
|
|
269
|
+
* discovery is disabled in isolated mode and deliberately enabled in ambient
|
|
270
|
+
* mode; ambient extensions execute arbitrary code and are not sandboxed by the
|
|
271
|
+
* model-visible tool allowlist. The package guard is always explicit; Anthropic
|
|
272
|
+
* routes first load the package attribution/sanitization extension.
|
|
258
273
|
*/
|
|
259
274
|
export function buildDelegateChildArgv(input: DelegateChildArgvInput): string[] {
|
|
275
|
+
assertDelegateExtensionMode(input.extensionMode);
|
|
260
276
|
const tools = delegateToolsFor(input.capability);
|
|
261
277
|
for (const forbidden of DELEGATE_FORBIDDEN_TOOLS) {
|
|
262
278
|
if (tools.includes(forbidden)) {
|
|
@@ -266,6 +282,20 @@ export function buildDelegateChildArgv(input: DelegateChildArgvInput): string[]
|
|
|
266
282
|
);
|
|
267
283
|
}
|
|
268
284
|
}
|
|
285
|
+
const extensionPaths =
|
|
286
|
+
input.route.provider === 'anthropic'
|
|
287
|
+
? [
|
|
288
|
+
input.attributionExtensionPath ??
|
|
289
|
+
(() => {
|
|
290
|
+
throw new DelegateError(
|
|
291
|
+
'Anthropic delegate launch requires the package attribution extension',
|
|
292
|
+
{ code: 'delegate_isolation_unsupported', childCreated: false },
|
|
293
|
+
);
|
|
294
|
+
})(),
|
|
295
|
+
input.childExtensionPath,
|
|
296
|
+
]
|
|
297
|
+
: [input.childExtensionPath];
|
|
298
|
+
|
|
269
299
|
return [
|
|
270
300
|
'--mode',
|
|
271
301
|
'text',
|
|
@@ -279,13 +309,12 @@ export function buildDelegateChildArgv(input: DelegateChildArgvInput): string[]
|
|
|
279
309
|
tools.join(','),
|
|
280
310
|
'--exclude-tools',
|
|
281
311
|
DELEGATE_FORBIDDEN_TOOLS.join(','),
|
|
282
|
-
'--no-extensions',
|
|
312
|
+
...(input.extensionMode === 'isolated' ? ['--no-extensions'] : []),
|
|
283
313
|
'--no-skills',
|
|
284
314
|
'--no-prompt-templates',
|
|
285
315
|
'--no-themes',
|
|
286
316
|
'--no-context-files',
|
|
287
|
-
'--extension',
|
|
288
|
-
input.childExtensionPath,
|
|
317
|
+
...extensionPaths.flatMap((path) => ['--extension', path]),
|
|
289
318
|
'--provider',
|
|
290
319
|
input.route.provider,
|
|
291
320
|
'--model',
|
|
@@ -351,7 +380,9 @@ export function buildDelegateChildSystemPrompt(seedPathHint: string): string {
|
|
|
351
380
|
'',
|
|
352
381
|
'You are inspect-only. You can read, search, and list files. You cannot run shell commands, edit or write files, reach the network, or start further delegates. Do not claim to have done so.',
|
|
353
382
|
'',
|
|
354
|
-
'If a tool result is replaced by a spill receipt, the complete
|
|
383
|
+
'If a tool result is replaced by a spill receipt, the complete encoded content is on disk and nothing was truncated. Use delegate_read_artifact with an exact offset and length when you genuinely need lossless base64 bytes, then interpret them using the receipt content_format.',
|
|
384
|
+
'',
|
|
385
|
+
'The child controls retained context by spilling tool results before they consume protected final-answer runway. A spill is not a failure. If a finalization-runway notice appears, all investigation tools are finished: stop investigating and answer immediately from the evidence already gathered.',
|
|
355
386
|
'',
|
|
356
387
|
'Finish with a single, direct, self-contained answer to the directive. Your final assistant message is the answer that will be returned to the parent.',
|
|
357
388
|
].join('\n');
|
|
@@ -362,6 +393,7 @@ export interface DelegatePreflightInput {
|
|
|
362
393
|
toolCallId: string | undefined;
|
|
363
394
|
prompt: string;
|
|
364
395
|
capability: DelegateCapability;
|
|
396
|
+
extensionMode: DelegateExtensionMode;
|
|
365
397
|
route: DelegatePinnedRoute;
|
|
366
398
|
limitOverrides: DelegateLimitOverrides;
|
|
367
399
|
hookEvidence: DelegateHookContractEvidence;
|
|
@@ -411,10 +443,9 @@ export function buildDelegateChildPrompt(seedSerialized: string, directive: stri
|
|
|
411
443
|
* one of these can refuse, and none of them has created a process, a session, or
|
|
412
444
|
* an artifact by the time it does.
|
|
413
445
|
*/
|
|
414
|
-
export function preflightDelegateLaunch(
|
|
415
|
-
input: DelegatePreflightInput,
|
|
416
|
-
): DelegatePreflightResult {
|
|
446
|
+
export function preflightDelegateLaunch(input: DelegatePreflightInput): DelegatePreflightResult {
|
|
417
447
|
assertDelegateHookContract(input.hookEvidence);
|
|
448
|
+
assertDelegateExtensionMode(input.extensionMode);
|
|
418
449
|
// Validates the capability and proves the tool set contains nothing forbidden.
|
|
419
450
|
delegateToolsFor(input.capability);
|
|
420
451
|
const limits = resolveDelegateLimits(input.route, input.limitOverrides);
|
|
@@ -427,16 +458,19 @@ export function preflightDelegateLaunch(
|
|
|
427
458
|
toolCallId: input.toolCallId,
|
|
428
459
|
directive: input.prompt,
|
|
429
460
|
capability: input.capability,
|
|
461
|
+
extensionMode: input.extensionMode,
|
|
430
462
|
route: input.route,
|
|
431
463
|
limits,
|
|
432
464
|
});
|
|
433
|
-
const childSystemPrompt = buildDelegateChildSystemPrompt(
|
|
465
|
+
const childSystemPrompt = buildDelegateChildSystemPrompt(
|
|
466
|
+
'the task seed in your first user message',
|
|
467
|
+
);
|
|
434
468
|
const childPrompt = buildDelegateChildPrompt(seed.serialized, seed.seed.directive.text);
|
|
435
469
|
const plan = planDelegateAdmission({
|
|
436
470
|
route: input.route,
|
|
437
471
|
// The seed reaches the child inside its prompt, so the admission forecast
|
|
438
472
|
// must measure the prompt that is actually sent, not the seed alone.
|
|
439
|
-
|
|
473
|
+
childPrompt,
|
|
440
474
|
childSystemPrompt,
|
|
441
475
|
limits,
|
|
442
476
|
});
|
|
@@ -454,9 +488,7 @@ export function preflightDelegateLaunch(
|
|
|
454
488
|
}
|
|
455
489
|
|
|
456
490
|
/** Task-owned child session directory. Never the parent's session directory. */
|
|
457
|
-
export async function ensureDelegateChildSessionDir(
|
|
458
|
-
artifactDirAbs: string,
|
|
459
|
-
): Promise<string> {
|
|
491
|
+
export async function ensureDelegateChildSessionDir(artifactDirAbs: string): Promise<string> {
|
|
460
492
|
const dir = join(artifactDirAbs, 'child-session');
|
|
461
493
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
462
494
|
return dir;
|
|
@@ -247,6 +247,21 @@ function parseAttestations(
|
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function parseSpillContentFormat(
|
|
251
|
+
value: unknown,
|
|
252
|
+
taskId: string,
|
|
253
|
+
): DelegateSpillReceipt['content_format'] {
|
|
254
|
+
if (value === undefined) return undefined;
|
|
255
|
+
if (
|
|
256
|
+
value !== 'single_text_utf8' &&
|
|
257
|
+
value !== 'tool_result_content_json_v1' &&
|
|
258
|
+
value !== 'opaque_bytes'
|
|
259
|
+
) {
|
|
260
|
+
fail('delegate spill receipt content_format is invalid', 'child_result_invalid', taskId);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
|
|
250
265
|
function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSpillReceipt[] {
|
|
251
266
|
if (!Array.isArray(value))
|
|
252
267
|
fail('delegate result package spilled_artifacts must be an array', 'child_result_invalid', taskId);
|
|
@@ -264,6 +279,7 @@ function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSp
|
|
|
264
279
|
source_call_index: requireInteger(entry, 'source_call_index', taskId),
|
|
265
280
|
byte_length: requireInteger(entry, 'byte_length', taskId),
|
|
266
281
|
sha256: requireSha256(entry, 'sha256', taskId),
|
|
282
|
+
content_format: parseSpillContentFormat(entry['content_format'], taskId),
|
|
267
283
|
};
|
|
268
284
|
});
|
|
269
285
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
4
4
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
5
|
+
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
5
6
|
import { join } from 'node:path';
|
|
6
7
|
import { DelegateArtifactStore, discardDelegateArtifactRoot } from './artifacts.js';
|
|
7
8
|
import {
|
|
@@ -20,6 +21,7 @@ import { verifyDelegateResultPackage, type VerifiedDelegateResult } from './resu
|
|
|
20
21
|
import {
|
|
21
22
|
DelegateError,
|
|
22
23
|
type DelegateAutoDeliverMode,
|
|
24
|
+
type DelegateExtensionMode,
|
|
23
25
|
type DelegateResultPackageV1,
|
|
24
26
|
} from './types.js';
|
|
25
27
|
import type { DelegateTaskFacts, DelegateTaskOutcome } from '../common.js';
|
|
@@ -48,7 +50,9 @@ export interface PrepareDelegateLaunchInput extends DelegatePreflightInput {
|
|
|
48
50
|
cwd: string;
|
|
49
51
|
sessionId: string | undefined;
|
|
50
52
|
autoDeliver: DelegateAutoDeliverMode;
|
|
53
|
+
extensionMode: DelegateExtensionMode;
|
|
51
54
|
childExtensionPath?: string | undefined;
|
|
55
|
+
attributionExtensionPath?: string | undefined;
|
|
52
56
|
env?: NodeJS.ProcessEnv | undefined;
|
|
53
57
|
now?: (() => Date) | undefined;
|
|
54
58
|
}
|
|
@@ -68,6 +72,22 @@ export async function prepareDelegateLaunch(
|
|
|
68
72
|
// its child guard must refuse rather than spawn an unguarded child.
|
|
69
73
|
const childExtensionPath =
|
|
70
74
|
input.childExtensionPath ?? resolveDelegateChildExtensionPath();
|
|
75
|
+
let attributionExtensionPath: string | undefined;
|
|
76
|
+
if (input.route.provider === 'anthropic') {
|
|
77
|
+
try {
|
|
78
|
+
attributionExtensionPath =
|
|
79
|
+
input.attributionExtensionPath ?? resolveAnthropicAttributionExtensionPath();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new DelegateError(
|
|
82
|
+
`Anthropic delegate attribution extension could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
83
|
+
{
|
|
84
|
+
code: 'delegate_isolation_unsupported',
|
|
85
|
+
childCreated: false,
|
|
86
|
+
remediation: ['Reinstall the package; Anthropic delegates require attribution.'],
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
|
|
72
92
|
const preflight = preflightDelegateLaunch(input);
|
|
73
93
|
|
|
@@ -78,6 +98,7 @@ export async function prepareDelegateLaunch(
|
|
|
78
98
|
sessionId: input.sessionId,
|
|
79
99
|
childSessionId: preflight.childSessionId,
|
|
80
100
|
childSessionDir: '',
|
|
101
|
+
extensionMode: input.extensionMode,
|
|
81
102
|
route: input.route,
|
|
82
103
|
limits: preflight.limits,
|
|
83
104
|
seedSha256: preflight.seed.sha256,
|
|
@@ -107,9 +128,11 @@ export async function prepareDelegateLaunch(
|
|
|
107
128
|
const argv = buildDelegateChildArgv({
|
|
108
129
|
route: input.route,
|
|
109
130
|
capability: input.capability,
|
|
131
|
+
extensionMode: input.extensionMode,
|
|
110
132
|
childSessionId: preflight.childSessionId,
|
|
111
133
|
childSessionDir: childSessionDirAbs,
|
|
112
134
|
childExtensionPath,
|
|
135
|
+
attributionExtensionPath,
|
|
113
136
|
systemPrompt: preflight.childSystemPrompt,
|
|
114
137
|
});
|
|
115
138
|
const env = delegateChildEnv(
|
|
@@ -137,7 +160,9 @@ export async function prepareDelegateLaunch(
|
|
|
137
160
|
budget: {
|
|
138
161
|
family: preflight.plan.route.family,
|
|
139
162
|
rate_source: preflight.plan.route.rate_source,
|
|
163
|
+
conservative_rate_source: preflight.plan.conservative_estimate.rateSource,
|
|
140
164
|
},
|
|
165
|
+
extensionMode: input.extensionMode,
|
|
141
166
|
autoDeliver: input.autoDeliver,
|
|
142
167
|
};
|
|
143
168
|
const stdinBytes = Buffer.from(preflight.childPrompt, 'utf8');
|
|
@@ -176,6 +201,9 @@ export interface EvaluateDelegateTerminalInput {
|
|
|
176
201
|
/** Terminal status observed by the background task registry. */
|
|
177
202
|
taskStatus: 'completed' | 'failed' | 'killed';
|
|
178
203
|
taskError: string | undefined;
|
|
204
|
+
/** Real merged child output owned by the background-task registry. */
|
|
205
|
+
taskOutputPath?: string | undefined;
|
|
206
|
+
taskOutputAbsPath?: string | undefined;
|
|
179
207
|
}
|
|
180
208
|
|
|
181
209
|
/**
|
|
@@ -227,6 +255,21 @@ async function adjudicateDelegateTerminal(
|
|
|
227
255
|
recorded?.message ??
|
|
228
256
|
input.taskError ??
|
|
229
257
|
'the delegate child exited without committing a result package';
|
|
258
|
+
const preserved = ['seed.json', 'budget-plan.json', 'child-terminal.json', 'runtime-budget.json']
|
|
259
|
+
.filter((name) => existsSync(join(input.artifactDirAbs, name)));
|
|
260
|
+
if (
|
|
261
|
+
input.taskOutputPath !== undefined &&
|
|
262
|
+
input.taskOutputAbsPath !== undefined &&
|
|
263
|
+
existsSync(input.taskOutputAbsPath)
|
|
264
|
+
) {
|
|
265
|
+
preserved.push(input.taskOutputPath);
|
|
266
|
+
}
|
|
267
|
+
const diagnosticTargets = preserved.filter(
|
|
268
|
+
(name) => name === 'child-terminal.json' || name === 'runtime-budget.json' || name === input.taskOutputPath,
|
|
269
|
+
);
|
|
270
|
+
const diagnostic = diagnosticTargets.length === 0
|
|
271
|
+
? 'No child terminal record or merged task output exists; inspect the preserved launch artifacts listed above.'
|
|
272
|
+
: `Inspect the preserved diagnostic evidence: ${diagnosticTargets.join(', ')}.`;
|
|
230
273
|
const error = new DelegateError(
|
|
231
274
|
`bg_delegate produced no committed answer: ${detail}`,
|
|
232
275
|
{
|
|
@@ -234,9 +277,9 @@ async function adjudicateDelegateTerminal(
|
|
|
234
277
|
childCreated: true,
|
|
235
278
|
taskId: input.taskId,
|
|
236
279
|
artifactDir: input.artifactDirAbs,
|
|
237
|
-
preserved
|
|
280
|
+
preserved,
|
|
238
281
|
remediation: [
|
|
239
|
-
|
|
282
|
+
diagnostic,
|
|
240
283
|
'No partial answer is returned; nothing was truncated to look like success.',
|
|
241
284
|
],
|
|
242
285
|
},
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type DelegateContextPolicyDescriptor,
|
|
23
23
|
type DelegateConversationProjection,
|
|
24
24
|
type DelegateLedgerV1,
|
|
25
|
+
type DelegateExtensionMode,
|
|
25
26
|
type DelegateLimits,
|
|
26
27
|
type DelegatePinnedRoute,
|
|
27
28
|
type DelegateSeedV1,
|
|
@@ -34,6 +35,7 @@ export interface BuildDelegateSeedOptions {
|
|
|
34
35
|
toolCallId: string | undefined;
|
|
35
36
|
directive: string;
|
|
36
37
|
capability: DelegateCapability;
|
|
38
|
+
extensionMode: DelegateExtensionMode;
|
|
37
39
|
route: DelegatePinnedRoute;
|
|
38
40
|
limits: DelegateLimits;
|
|
39
41
|
}
|
|
@@ -167,6 +169,7 @@ export function buildDelegateSeed(
|
|
|
167
169
|
launch_nonce: options.launchNonce,
|
|
168
170
|
cwd: ctx.cwd,
|
|
169
171
|
capability: options.capability,
|
|
172
|
+
extension_mode: options.extensionMode,
|
|
170
173
|
route: options.route,
|
|
171
174
|
parent_system_prompt: ctx.getSystemPrompt(),
|
|
172
175
|
parent_leaf_id: snapshot.leafId,
|
|
@@ -296,6 +299,14 @@ function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): Dele
|
|
|
296
299
|
taskId,
|
|
297
300
|
});
|
|
298
301
|
}
|
|
302
|
+
const extensionMode = requireString(record, 'extension_mode', taskId);
|
|
303
|
+
if (extensionMode !== 'isolated' && extensionMode !== 'ambient') {
|
|
304
|
+
throw new DelegateError(`delegate seed extension mode ${extensionMode} is not recognised`, {
|
|
305
|
+
code: 'seed_hash_mismatch',
|
|
306
|
+
childCreated: true,
|
|
307
|
+
taskId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
299
310
|
const routeRecord = requireRecord(record, 'route', taskId);
|
|
300
311
|
const routeOrigin = requireString(routeRecord, 'origin', taskId);
|
|
301
312
|
if (routeOrigin !== 'parent_current' && routeOrigin !== 'explicit') {
|
|
@@ -378,6 +389,7 @@ function rebuildSeed(record: Record<PropertyKey, unknown>, taskId: string): Dele
|
|
|
378
389
|
launch_nonce: requireString(record, 'launch_nonce', taskId),
|
|
379
390
|
cwd: requireString(record, 'cwd', taskId),
|
|
380
391
|
capability,
|
|
392
|
+
extension_mode: extensionMode,
|
|
381
393
|
route,
|
|
382
394
|
parent_system_prompt: requireString(record, 'parent_system_prompt', taskId),
|
|
383
395
|
parent_leaf_id: parentLeafId,
|
|
@@ -12,14 +12,14 @@ import type {
|
|
|
12
12
|
ProjectionEntry,
|
|
13
13
|
} from '../context/visible-conversation-v2.js';
|
|
14
14
|
|
|
15
|
-
export const DELEGATE_SEED_SCHEMA_VERSION = 'pi-background-tasks.delegate-seed.
|
|
15
|
+
export const DELEGATE_SEED_SCHEMA_VERSION = 'pi-background-tasks.delegate-seed.v2' as const;
|
|
16
16
|
export const DELEGATE_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.delegate-ledger.v1' as const;
|
|
17
17
|
export const DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION =
|
|
18
18
|
'pi-background-tasks.delegate-result.v1' as const;
|
|
19
19
|
export const DELEGATE_RECEIPT_SCHEMA_VERSION = 'pi-background-tasks.delegate-receipt.v1' as const;
|
|
20
20
|
export const DELEGATE_BUDGET_PLAN_SCHEMA_VERSION =
|
|
21
|
-
'pi-background-tasks.delegate-budget-plan.
|
|
22
|
-
export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.
|
|
21
|
+
'pi-background-tasks.delegate-budget-plan.v3' as const;
|
|
22
|
+
export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.v2' as const;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Delegate's own context policy id. It shares the frozen
|
|
@@ -35,6 +35,13 @@ export const DELEGATE_RESULT_TOOL_NAME = 'bg_result';
|
|
|
35
35
|
export const DELEGATE_CAPABILITIES = ['inspect'] as const;
|
|
36
36
|
export type DelegateCapability = (typeof DELEGATE_CAPABILITIES)[number];
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Controls only Pi's ambient extension discovery for delegate children.
|
|
40
|
+
* Tool and project-resource restrictions remain independently enforced.
|
|
41
|
+
*/
|
|
42
|
+
export const DELEGATE_EXTENSION_MODES = ['isolated', 'ambient'] as const;
|
|
43
|
+
export type DelegateExtensionMode = (typeof DELEGATE_EXTENSION_MODES)[number];
|
|
44
|
+
|
|
38
45
|
export const DELEGATE_AUTO_DELIVER_MODES = ['never', 'when_small', 'always'] as const;
|
|
39
46
|
export type DelegateAutoDeliverMode = (typeof DELEGATE_AUTO_DELIVER_MODES)[number];
|
|
40
47
|
|
|
@@ -57,6 +64,7 @@ export interface DelegatePinnedRoute extends DelegateRoute {
|
|
|
57
64
|
export interface DelegateBudgetRouteSource {
|
|
58
65
|
family: TokenBudgetFamily;
|
|
59
66
|
rate_source: TokenBudgetRateSource;
|
|
67
|
+
conservative_rate_source?: TokenBudgetRateSource | undefined;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export interface DelegateContextPolicyDescriptor {
|
|
@@ -110,6 +118,7 @@ export interface DelegateSeedV1 {
|
|
|
110
118
|
launch_nonce: string;
|
|
111
119
|
cwd: string;
|
|
112
120
|
capability: DelegateCapability;
|
|
121
|
+
extension_mode: DelegateExtensionMode;
|
|
113
122
|
route: DelegatePinnedRoute;
|
|
114
123
|
parent_system_prompt: string;
|
|
115
124
|
parent_leaf_id: string | null;
|
|
@@ -184,6 +193,11 @@ export interface DelegateResultPackageV1 {
|
|
|
184
193
|
spilled_artifacts: readonly DelegateSpillReceipt[];
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
export type DelegateSpillContentFormat =
|
|
197
|
+
| 'single_text_utf8'
|
|
198
|
+
| 'tool_result_content_json_v1'
|
|
199
|
+
| 'opaque_bytes';
|
|
200
|
+
|
|
187
201
|
export interface DelegateSpillReceipt {
|
|
188
202
|
schema_version: typeof DELEGATE_RECEIPT_SCHEMA_VERSION;
|
|
189
203
|
artifact: string;
|
|
@@ -193,6 +207,11 @@ export interface DelegateSpillReceipt {
|
|
|
193
207
|
source_call_index: number;
|
|
194
208
|
byte_length: number;
|
|
195
209
|
sha256: string;
|
|
210
|
+
/**
|
|
211
|
+
* Encoding of the hashed artifact bytes. Optional only for compatibility
|
|
212
|
+
* with v1 receipts written before content formats were recorded.
|
|
213
|
+
*/
|
|
214
|
+
content_format?: DelegateSpillContentFormat | undefined;
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
export const DELEGATE_ERROR_CODES = [
|
|
@@ -5,7 +5,7 @@ import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
|
5
5
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
6
6
|
import { isJsonObject, parseJsonText, type JsonObject } from '../common.js';
|
|
7
7
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
8
|
-
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '
|
|
8
|
+
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from '../anthropic-attribution.js';
|
|
9
9
|
import {
|
|
10
10
|
FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
11
11
|
FusionError,
|
|
@@ -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 [
|
package/src/core/registry.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
resolvePiLaunch,
|
|
59
59
|
type PiLaunchSpec,
|
|
60
60
|
} from './pi-launch.js';
|
|
61
|
+
import { resolveAnthropicAttributionExtensionPath } from './anthropic-attribution-path.js';
|
|
61
62
|
import {
|
|
62
63
|
runWindowsTaskkill,
|
|
63
64
|
type TaskkillOutcome,
|
|
@@ -1234,7 +1235,9 @@ export class BackgroundTaskRegistry {
|
|
|
1234
1235
|
if (this.shuttingDown)
|
|
1235
1236
|
throw new Error('Cannot start an attested Pi task while Pi is shutting down');
|
|
1236
1237
|
|
|
1237
|
-
const
|
|
1238
|
+
const attributionExtensionPath =
|
|
1239
|
+
request.provider === 'anthropic' ? resolveAnthropicAttributionExtensionPath() : undefined;
|
|
1240
|
+
const argv = buildAttestedPiArgv(request, attributionExtensionPath);
|
|
1238
1241
|
const attestedPiLaunch = resolvePiLaunch({ platform: this.platform });
|
|
1239
1242
|
assertWindowsCommandLineWithinLimit(
|
|
1240
1243
|
attestedPiLaunch,
|