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
|
@@ -7,15 +7,21 @@ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
|
7
7
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
8
8
|
import {
|
|
9
9
|
DELEGATE_RECEIPT_SCHEMA_VERSION,
|
|
10
|
+
DELEGATE_CAPABILITIES,
|
|
10
11
|
type DelegateRouteAttestation,
|
|
11
12
|
type DelegateSeedV1,
|
|
12
13
|
type DelegateSpillReceipt,
|
|
13
14
|
type DelegateUsageReport,
|
|
14
15
|
} from './core/delegate/types.js';
|
|
15
16
|
import { verifyDelegateSeedBytes } from './core/delegate/seed.js';
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
|
|
19
|
+
DELEGATE_FINALIZATION_TRIGGER_TOKENS,
|
|
20
|
+
evaluateDelegateRuntimeBudget,
|
|
21
|
+
} from './core/delegate/budget.js';
|
|
17
22
|
import { utf8ByteClassBreakdown } from './core/context/token-budget.js';
|
|
18
23
|
import {
|
|
24
|
+
assertWellFormedUtf8,
|
|
19
25
|
buildDelegateResultPackage,
|
|
20
26
|
serializeDelegateResultPackage,
|
|
21
27
|
} from './core/delegate/result-package.js';
|
|
@@ -23,15 +29,17 @@ import {
|
|
|
23
29
|
/**
|
|
24
30
|
* Package-owned delegate child extension.
|
|
25
31
|
*
|
|
26
|
-
* This runs inside
|
|
27
|
-
*
|
|
32
|
+
* This runs inside every delegate child Pi process and is the package-owned
|
|
33
|
+
* child guard. Anthropic routes load attribution first, and ambient mode may
|
|
34
|
+
* also execute discovered extensions; this guard remains
|
|
35
|
+
* responsible for every isolation guarantee that cannot be enforced from the
|
|
28
36
|
* parent:
|
|
29
37
|
*
|
|
30
38
|
* - verifying the frozen seed bytes before the first model call;
|
|
31
|
-
* - measuring the outgoing message set before every model call and
|
|
32
|
-
*
|
|
33
|
-
* - spilling oversized tool results to hashed artifacts
|
|
34
|
-
* explicit receipts before
|
|
39
|
+
* - measuring the outgoing message set before every model call and requesting
|
|
40
|
+
* no-tool finalization when advisory runway becomes low;
|
|
41
|
+
* - spilling oversized or runway-pressuring tool results to hashed artifacts
|
|
42
|
+
* and replacing them with explicit receipts before transcript entry;
|
|
35
43
|
* - asserting every assistant message came from the pinned route;
|
|
36
44
|
* - enforcing turn and tool-call limits;
|
|
37
45
|
* - committing exactly one result package atomically.
|
|
@@ -60,8 +68,19 @@ interface GuardState {
|
|
|
60
68
|
spilled: DelegateSpillReceipt[];
|
|
61
69
|
attestations: DelegateRouteAttestation[];
|
|
62
70
|
usage: Usage | undefined;
|
|
71
|
+
usageIncomplete: boolean;
|
|
63
72
|
usageUnavailableReason: string | undefined;
|
|
64
73
|
answerBlocks: string[];
|
|
74
|
+
answerBytes: number;
|
|
75
|
+
retainedGrowthTokens: number;
|
|
76
|
+
retainedGrowthBudgetTokens: number | undefined;
|
|
77
|
+
retainedToolResultBytes: number;
|
|
78
|
+
contextPressureSpillBytes: number;
|
|
79
|
+
finalizationRequested: boolean;
|
|
80
|
+
finalizationReason: string | undefined;
|
|
81
|
+
contextMeasurements: RuntimeContextMeasurement[];
|
|
82
|
+
firstRequestObservedInputTokens: number | undefined;
|
|
83
|
+
runtimeBudgetWritten: boolean;
|
|
65
84
|
terminal: TerminalLatch | undefined;
|
|
66
85
|
committed: boolean;
|
|
67
86
|
}
|
|
@@ -79,6 +98,16 @@ interface TerminalLatch {
|
|
|
79
98
|
message: string;
|
|
80
99
|
}
|
|
81
100
|
|
|
101
|
+
interface RuntimeContextMeasurement {
|
|
102
|
+
request_ordinal: number;
|
|
103
|
+
retained_utf8_bytes: number;
|
|
104
|
+
estimated_input_tokens: number;
|
|
105
|
+
allowed_input_tokens: number;
|
|
106
|
+
signed_headroom_tokens: number;
|
|
107
|
+
dominant_byte_class: string;
|
|
108
|
+
finalization_requested: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
82
111
|
/**
|
|
83
112
|
* Stop reasons that may be committed as a complete answer.
|
|
84
113
|
*
|
|
@@ -112,6 +141,85 @@ function utf8(value: string): Buffer {
|
|
|
112
141
|
return Buffer.from(value, 'utf8');
|
|
113
142
|
}
|
|
114
143
|
|
|
144
|
+
interface DelegateToolTextPart {
|
|
145
|
+
readonly type: 'text';
|
|
146
|
+
readonly text: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
interface DelegateToolImagePart {
|
|
150
|
+
readonly type: 'image';
|
|
151
|
+
readonly data: string;
|
|
152
|
+
readonly mimeType: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type DelegateToolResultPart = DelegateToolTextPart | DelegateToolImagePart;
|
|
156
|
+
type SpillContentFormat = DelegateSpillReceipt['content_format'];
|
|
157
|
+
|
|
158
|
+
function encodeToolResultContent(
|
|
159
|
+
content: ReadonlyArray<DelegateToolResultPart>,
|
|
160
|
+
): { payload: Buffer; contentFormat: Exclude<SpillContentFormat, undefined> } {
|
|
161
|
+
if (content.length === 1) {
|
|
162
|
+
const only = content[0];
|
|
163
|
+
if (only?.type === 'text') {
|
|
164
|
+
return {
|
|
165
|
+
payload: assertWellFormedUtf8(only.text, 'delegate single-text tool result'),
|
|
166
|
+
contentFormat: 'single_text_utf8',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const normalized = content.map((part) => {
|
|
171
|
+
if (part.type === 'text' && typeof part.text === 'string') {
|
|
172
|
+
return { type: 'text' as const, text: part.text };
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
part.type === 'image' &&
|
|
176
|
+
typeof part.data === 'string' &&
|
|
177
|
+
typeof part.mimeType === 'string'
|
|
178
|
+
) {
|
|
179
|
+
return {
|
|
180
|
+
type: 'image' as const,
|
|
181
|
+
data: part.data,
|
|
182
|
+
mimeType: part.mimeType,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
throw new Error('delegate tool result contains an unsupported or malformed content block');
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
payload: utf8(
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
schema_version: 'pi-background-tasks.delegate-tool-result-content.v1',
|
|
191
|
+
content: normalized,
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
194
|
+
contentFormat: 'tool_result_content_json_v1',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function addUsage(current: Usage | undefined, delta: Usage): Usage {
|
|
199
|
+
const prior = current ?? {
|
|
200
|
+
input: 0,
|
|
201
|
+
output: 0,
|
|
202
|
+
cacheRead: 0,
|
|
203
|
+
cacheWrite: 0,
|
|
204
|
+
totalTokens: 0,
|
|
205
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
input: prior.input + delta.input,
|
|
209
|
+
output: prior.output + delta.output,
|
|
210
|
+
cacheRead: prior.cacheRead + delta.cacheRead,
|
|
211
|
+
cacheWrite: prior.cacheWrite + delta.cacheWrite,
|
|
212
|
+
totalTokens: prior.totalTokens + delta.totalTokens,
|
|
213
|
+
cost: {
|
|
214
|
+
input: prior.cost.input + delta.cost.input,
|
|
215
|
+
output: prior.cost.output + delta.cost.output,
|
|
216
|
+
cacheRead: prior.cost.cacheRead + delta.cost.cacheRead,
|
|
217
|
+
cacheWrite: prior.cost.cacheWrite + delta.cost.cacheWrite,
|
|
218
|
+
total: prior.cost.total + delta.cost.total,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
115
223
|
function finiteNonNegative(source: object, key: string): number | undefined {
|
|
116
224
|
const value: unknown = Reflect.get(source, key);
|
|
117
225
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
|
|
@@ -289,6 +397,10 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
289
397
|
launchNonce: expectedNonce,
|
|
290
398
|
});
|
|
291
399
|
|
|
400
|
+
if (!DELEGATE_CAPABILITIES.includes(seed.capability)) {
|
|
401
|
+
throw new Error(`delegate child cannot enforce capability ${seed.capability}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
292
404
|
const state: GuardState = {
|
|
293
405
|
seed,
|
|
294
406
|
artifactDirAbs,
|
|
@@ -298,8 +410,19 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
298
410
|
spilled: [],
|
|
299
411
|
attestations: [],
|
|
300
412
|
usage: undefined,
|
|
413
|
+
usageIncomplete: false,
|
|
301
414
|
usageUnavailableReason: 'the child produced no assistant message carrying usage',
|
|
302
415
|
answerBlocks: [],
|
|
416
|
+
answerBytes: 0,
|
|
417
|
+
retainedGrowthTokens: 0,
|
|
418
|
+
retainedGrowthBudgetTokens: undefined,
|
|
419
|
+
retainedToolResultBytes: 0,
|
|
420
|
+
contextPressureSpillBytes: 0,
|
|
421
|
+
finalizationRequested: false,
|
|
422
|
+
finalizationReason: undefined,
|
|
423
|
+
contextMeasurements: [],
|
|
424
|
+
firstRequestObservedInputTokens: undefined,
|
|
425
|
+
runtimeBudgetWritten: false,
|
|
303
426
|
terminal: undefined,
|
|
304
427
|
committed: false,
|
|
305
428
|
};
|
|
@@ -309,13 +432,92 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
309
432
|
}
|
|
310
433
|
|
|
311
434
|
function usageReport(): DelegateUsageReport {
|
|
312
|
-
if (state.usage !== undefined)
|
|
435
|
+
if (!state.usageIncomplete && state.usage !== undefined) {
|
|
436
|
+
return { status: 'observed', usage: state.usage };
|
|
437
|
+
}
|
|
313
438
|
return {
|
|
314
439
|
status: 'unavailable',
|
|
315
440
|
reason: state.usageUnavailableReason ?? 'usage was not reported by the provider',
|
|
316
441
|
};
|
|
317
442
|
}
|
|
318
443
|
|
|
444
|
+
function remainingGrowthTokens(): number {
|
|
445
|
+
if (state.retainedGrowthBudgetTokens === undefined) return 0;
|
|
446
|
+
return Math.max(0, state.retainedGrowthBudgetTokens - state.retainedGrowthTokens);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function requestFinalization(reason: string): void {
|
|
450
|
+
if (!state.finalizationRequested) {
|
|
451
|
+
state.finalizationRequested = true;
|
|
452
|
+
state.finalizationReason = reason;
|
|
453
|
+
}
|
|
454
|
+
pi.setActiveTools([]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function accountRetainedGrowth(bytes: number, source: string): void {
|
|
458
|
+
state.retainedGrowthTokens += bytes;
|
|
459
|
+
if (
|
|
460
|
+
state.retainedGrowthBudgetTokens !== undefined &&
|
|
461
|
+
remainingGrowthTokens() <= DELEGATE_FINALIZATION_TRIGGER_TOKENS
|
|
462
|
+
) {
|
|
463
|
+
requestFinalization(
|
|
464
|
+
`${source} left ${String(remainingGrowthTokens())} protected retained-growth tokens`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function writeRuntimeBudgetRecord(): void {
|
|
470
|
+
if (state.runtimeBudgetWritten) return;
|
|
471
|
+
const latest = state.contextMeasurements.at(-1);
|
|
472
|
+
const firstEstimate = state.contextMeasurements[0]?.estimated_input_tokens;
|
|
473
|
+
const calibrationViolation =
|
|
474
|
+
firstEstimate !== undefined &&
|
|
475
|
+
state.firstRequestObservedInputTokens !== undefined &&
|
|
476
|
+
state.firstRequestObservedInputTokens > firstEstimate
|
|
477
|
+
? {
|
|
478
|
+
forecast_input_tokens: firstEstimate,
|
|
479
|
+
observed_input_tokens: state.firstRequestObservedInputTokens,
|
|
480
|
+
tokens_under_forecast:
|
|
481
|
+
state.firstRequestObservedInputTokens - firstEstimate,
|
|
482
|
+
}
|
|
483
|
+
: null;
|
|
484
|
+
commitFileSync(
|
|
485
|
+
join(artifactDirAbs, 'runtime-budget.json'),
|
|
486
|
+
utf8(
|
|
487
|
+
`${JSON.stringify(
|
|
488
|
+
{
|
|
489
|
+
schema_version: 'pi-background-tasks.delegate-runtime-budget.v1',
|
|
490
|
+
task_id: seed.task_id,
|
|
491
|
+
launch_nonce: seed.launch_nonce,
|
|
492
|
+
policy: {
|
|
493
|
+
live_provider_context_owner: 'pi_and_provider',
|
|
494
|
+
retained_growth_estimator: 'provable_1_byte_per_token',
|
|
495
|
+
finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
|
|
496
|
+
finalization_trigger_tokens: DELEGATE_FINALIZATION_TRIGGER_TOKENS,
|
|
497
|
+
},
|
|
498
|
+
retained_growth_budget_tokens: state.retainedGrowthBudgetTokens ?? null,
|
|
499
|
+
retained_growth_tokens: state.retainedGrowthTokens,
|
|
500
|
+
retained_tool_result_bytes: state.retainedToolResultBytes,
|
|
501
|
+
spilled_tool_result_bytes: state.spilled.reduce(
|
|
502
|
+
(sum, receipt) => sum + receipt.byte_length,
|
|
503
|
+
0,
|
|
504
|
+
),
|
|
505
|
+
context_pressure_spill_bytes: state.contextPressureSpillBytes,
|
|
506
|
+
finalization_requested: state.finalizationRequested,
|
|
507
|
+
finalization_reason: state.finalizationReason ?? null,
|
|
508
|
+
first_request_observed_input_tokens: state.firstRequestObservedInputTokens ?? null,
|
|
509
|
+
calibration_violation: calibrationViolation,
|
|
510
|
+
latest_context_estimate: latest ?? null,
|
|
511
|
+
context_measurements: state.contextMeasurements,
|
|
512
|
+
},
|
|
513
|
+
null,
|
|
514
|
+
2,
|
|
515
|
+
)}\n`,
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
state.runtimeBudgetWritten = true;
|
|
519
|
+
}
|
|
520
|
+
|
|
319
521
|
/**
|
|
320
522
|
* Commit exactly one result package.
|
|
321
523
|
*
|
|
@@ -328,13 +530,6 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
328
530
|
writeTerminalRecord(state.terminal);
|
|
329
531
|
return;
|
|
330
532
|
}
|
|
331
|
-
if (state.answerBlocks.length === 0) {
|
|
332
|
-
writeTerminalRecord({
|
|
333
|
-
code: 'child_exited_without_commit',
|
|
334
|
-
message: 'the delegate child produced no assistant answer text',
|
|
335
|
-
});
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
533
|
// A hash proves the bytes are intact; it cannot prove they are complete.
|
|
339
534
|
// Only an approved terminal stop reason may be committed as success, so a
|
|
340
535
|
// response cut short by the output-token limit, a content filter, an
|
|
@@ -342,7 +537,14 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
342
537
|
if (!ACCEPTED_STOP_REASONS.has(stopReason)) {
|
|
343
538
|
writeTerminalRecord({
|
|
344
539
|
code: stopReason === 'length' ? 'child_model_output_limit' : 'child_result_invalid',
|
|
345
|
-
message: `the delegate child stopped with reason "${stopReason}", so its answer is incomplete and is not committed as a result; the
|
|
540
|
+
message: `the delegate child stopped with reason "${stopReason}", so its answer is incomplete and is not committed as a result; the complete assistant message remains in the child transcript`,
|
|
541
|
+
});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (state.answerBlocks.length === 0) {
|
|
545
|
+
writeTerminalRecord({
|
|
546
|
+
code: 'child_exited_without_commit',
|
|
547
|
+
message: 'the delegate child produced no final assistant answer text',
|
|
346
548
|
});
|
|
347
549
|
return;
|
|
348
550
|
}
|
|
@@ -353,6 +555,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
353
555
|
});
|
|
354
556
|
return;
|
|
355
557
|
}
|
|
558
|
+
writeRuntimeBudgetRecord();
|
|
356
559
|
const pkg = buildDelegateResultPackage({
|
|
357
560
|
taskId: seed.task_id,
|
|
358
561
|
launchNonce: seed.launch_nonce,
|
|
@@ -372,6 +575,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
372
575
|
}
|
|
373
576
|
|
|
374
577
|
function writeTerminalRecord(terminal: TerminalLatch): void {
|
|
578
|
+
writeRuntimeBudgetRecord();
|
|
375
579
|
commitFileSync(
|
|
376
580
|
join(artifactDirAbs, 'child-terminal.json'),
|
|
377
581
|
utf8(
|
|
@@ -397,10 +601,11 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
397
601
|
name: 'delegate_read_artifact',
|
|
398
602
|
label: 'Delegate Artifact Read',
|
|
399
603
|
description:
|
|
400
|
-
'Read an exact byte range from a spilled tool-result artifact. Returns
|
|
401
|
-
promptSnippet: 'Read an exact byte range
|
|
604
|
+
'Read an exact byte range from a spilled tool-result artifact as lossless base64. Returns the complete requested range or fails; it never decodes arbitrary bytes as UTF-8, returns fewer bytes, or clamps the request.',
|
|
605
|
+
promptSnippet: 'Read an exact artifact byte range as lossless base64',
|
|
402
606
|
promptGuidelines: [
|
|
403
607
|
'Use delegate_read_artifact when a tool result was replaced by a spill receipt and the omitted bytes are actually needed.',
|
|
608
|
+
'The response body is base64 for the exact requested bytes. Decode it according to the receipt content_format.',
|
|
404
609
|
'Request a bounded range. A request past the end of the artifact fails loudly rather than returning a short read.',
|
|
405
610
|
],
|
|
406
611
|
parameters: ArtifactReadParams,
|
|
@@ -438,8 +643,32 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
438
643
|
`delegate_read_artifact returned ${String(slice.length)} of ${String(params.length)} requested bytes`,
|
|
439
644
|
);
|
|
440
645
|
}
|
|
646
|
+
const encoded = slice.toString('base64');
|
|
647
|
+
const responseText = [
|
|
648
|
+
`[delegate artifact range] artifact=${params.artifact} offset=${String(params.offset)} length=${String(params.length)} encoding=base64`,
|
|
649
|
+
encoded,
|
|
650
|
+
].join('\n');
|
|
651
|
+
const responseBytes = utf8(responseText).length;
|
|
652
|
+
const availableInlineBytes = Math.max(
|
|
653
|
+
0,
|
|
654
|
+
Math.min(
|
|
655
|
+
seed.limits.max_tool_result_bytes,
|
|
656
|
+
remainingGrowthTokens() - DELEGATE_FINALIZATION_TRIGGER_TOKENS,
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
if (responseBytes > availableInlineBytes) {
|
|
660
|
+
if (availableInlineBytes === 0) {
|
|
661
|
+
requestFinalization('no retained-growth runway remains for an artifact range read');
|
|
662
|
+
throw new Error(
|
|
663
|
+
`delegate_read_artifact cannot retain another artifact range because protected final-answer runway is active; stop reading and answer from gathered evidence`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
throw new Error(
|
|
667
|
+
`delegate_read_artifact requested ${String(params.length)} raw bytes whose lossless base64 response is ${String(responseBytes)} bytes, but current protected runway permits at most ${String(availableInlineBytes)} inline bytes; request a smaller exact range`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
441
670
|
return Promise.resolve({
|
|
442
|
-
content: [{ type: 'text' as const, text:
|
|
671
|
+
content: [{ type: 'text' as const, text: responseText }],
|
|
443
672
|
details: {
|
|
444
673
|
artifact: params.artifact,
|
|
445
674
|
offset: params.offset,
|
|
@@ -451,12 +680,15 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
451
680
|
});
|
|
452
681
|
|
|
453
682
|
pi.on('context', (event, ctx) => {
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// the content rather than letting the original through.
|
|
683
|
+
// Measurement failures remain fail-closed. A successful estimate is
|
|
684
|
+
// advisory: Fusion BUG-185 proved that subtracting hypothetical output from
|
|
685
|
+
// a live provider payload can falsely refuse valid work. Package-owned
|
|
686
|
+
// growth is bounded earlier by the tool-result spill governor instead.
|
|
459
687
|
try {
|
|
688
|
+
if (state.terminal !== undefined) {
|
|
689
|
+
ctx.abort();
|
|
690
|
+
return { messages: suppressedMessages(event.messages) };
|
|
691
|
+
}
|
|
460
692
|
const measurement = retainedInputMeasurement(event.messages, ctx.getSystemPrompt());
|
|
461
693
|
const verdict = evaluateDelegateRuntimeBudget(
|
|
462
694
|
{
|
|
@@ -467,32 +699,74 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
467
699
|
seed.limits.allowed_input_tokens,
|
|
468
700
|
seed.route,
|
|
469
701
|
);
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
702
|
+
if (state.retainedGrowthBudgetTokens === undefined) {
|
|
703
|
+
state.retainedGrowthBudgetTokens = Math.max(
|
|
704
|
+
0,
|
|
705
|
+
verdict.allowedTokens -
|
|
706
|
+
verdict.measuredTokens -
|
|
707
|
+
DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
state.contextMeasurements.push({
|
|
711
|
+
request_ordinal: state.contextMeasurements.length + 1,
|
|
712
|
+
retained_utf8_bytes: measurement.bytes,
|
|
713
|
+
estimated_input_tokens: verdict.measuredTokens,
|
|
714
|
+
allowed_input_tokens: verdict.allowedTokens,
|
|
715
|
+
signed_headroom_tokens: verdict.allowedTokens - verdict.measuredTokens,
|
|
716
|
+
dominant_byte_class: verdict.dominantByteClass,
|
|
717
|
+
finalization_requested: state.finalizationRequested,
|
|
718
|
+
});
|
|
719
|
+
if (
|
|
720
|
+
verdict.measuredTokens >=
|
|
721
|
+
verdict.allowedTokens - DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS
|
|
722
|
+
) {
|
|
723
|
+
requestFinalization(
|
|
724
|
+
`advisory retained-input estimate reached ${String(verdict.measuredTokens)} of ${String(verdict.allowedTokens)} allowed tokens`,
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
if (!state.finalizationRequested) return undefined;
|
|
728
|
+
const finalizationMessage = {
|
|
729
|
+
role: 'user' as const,
|
|
730
|
+
content: `[delegate finalization runway] Stop investigating and do not call tools. Produce the final self-contained answer now from evidence already gathered. Reason: ${state.finalizationReason ?? 'protected final-answer runway is active'}.`,
|
|
731
|
+
timestamp: Date.now(),
|
|
732
|
+
};
|
|
733
|
+
return { messages: [...event.messages, finalizationMessage] };
|
|
480
734
|
} catch (error) {
|
|
481
735
|
latch(
|
|
482
736
|
'child_result_invalid',
|
|
483
|
-
`delegate context
|
|
737
|
+
`delegate context measurement failed and the run was stopped rather than dispatched unguarded: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
738
|
);
|
|
485
739
|
try {
|
|
486
740
|
ctx.abort();
|
|
487
741
|
} catch {
|
|
488
|
-
//
|
|
489
|
-
// latch above already prevents a success commit, and the suppressed
|
|
490
|
-
// replacement below still removes the content from this request.
|
|
742
|
+
// The terminal latch prevents success even if abort itself fails.
|
|
491
743
|
}
|
|
492
744
|
return { messages: suppressedMessages(event.messages) };
|
|
493
745
|
}
|
|
494
746
|
});
|
|
495
747
|
|
|
748
|
+
pi.on('tool_call', (_event, ctx) => {
|
|
749
|
+
if (state.finalizationRequested) {
|
|
750
|
+
return {
|
|
751
|
+
block: true,
|
|
752
|
+
reason: 'delegate protected final-answer runway is active; answer now without tools',
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
state.toolCalls += 1;
|
|
756
|
+
if (state.toolCalls > seed.limits.max_tool_calls) {
|
|
757
|
+
latch(
|
|
758
|
+
'child_tool_call_limit',
|
|
759
|
+
`delegate child exceeded its ${String(seed.limits.max_tool_calls)} tool-call limit`,
|
|
760
|
+
);
|
|
761
|
+
ctx.abort();
|
|
762
|
+
return {
|
|
763
|
+
block: true,
|
|
764
|
+
reason: `delegate child exceeded its ${String(seed.limits.max_tool_calls)} tool-call limit`,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
return undefined;
|
|
768
|
+
});
|
|
769
|
+
|
|
496
770
|
pi.on('tool_result', (event) => {
|
|
497
771
|
// Fail closed for the same reason as the context guard: a throw here would
|
|
498
772
|
// let the ORIGINAL oversized payload flow into the transcript.
|
|
@@ -518,31 +792,36 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
518
792
|
function guardToolResult(event: {
|
|
519
793
|
toolName: string;
|
|
520
794
|
toolCallId: string;
|
|
521
|
-
content: ReadonlyArray<
|
|
795
|
+
content: ReadonlyArray<DelegateToolResultPart>;
|
|
522
796
|
}): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } | undefined {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
const texts = event.content.flatMap((part) =>
|
|
531
|
-
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
532
|
-
);
|
|
533
|
-
const joined = texts.join('');
|
|
534
|
-
const payload = utf8(joined);
|
|
797
|
+
// A single text block keeps its exact UTF-8 payload for convenient range
|
|
798
|
+
// reads. Multi-block and image-bearing results use a closed JSON envelope
|
|
799
|
+
// so block boundaries, MIME types, and complete base64 image data survive
|
|
800
|
+
// a spill. Unknown blocks fail closed rather than disappearing from hashes.
|
|
801
|
+
const encodedContent = encodeToolResultContent(event.content);
|
|
802
|
+
const payload = encodedContent.payload;
|
|
535
803
|
state.totalToolOutputBytes += payload.length;
|
|
536
804
|
if (state.totalToolOutputBytes > seed.limits.max_total_tool_output_bytes) {
|
|
537
805
|
latch(
|
|
538
806
|
'aggregate_tool_output_cap',
|
|
539
807
|
`delegate child accumulated ${String(state.totalToolOutputBytes)} bytes of tool output, exceeding its ${String(seed.limits.max_total_tool_output_bytes)}-byte cap`,
|
|
540
808
|
);
|
|
809
|
+
requestFinalization('the aggregate raw tool-output cap was reached');
|
|
810
|
+
const withheld = '[delegate: tool result withheld because the aggregate raw-output cap was reached; answer from evidence already gathered]';
|
|
811
|
+
accountRetainedGrowth(utf8(withheld).length, 'aggregate-cap receipt');
|
|
812
|
+
return { content: [{ type: 'text', text: withheld }], isError: true };
|
|
813
|
+
}
|
|
814
|
+
const contextPressure =
|
|
815
|
+
state.retainedGrowthBudgetTokens === undefined || payload.length > remainingGrowthTokens();
|
|
816
|
+
if (!contextPressure && payload.length <= seed.limits.max_tool_result_bytes) {
|
|
817
|
+
state.retainedToolResultBytes += payload.length;
|
|
818
|
+
accountRetainedGrowth(payload.length, `${event.toolName} inline result`);
|
|
819
|
+
return undefined;
|
|
541
820
|
}
|
|
542
|
-
if (payload.length <= seed.limits.max_tool_result_bytes) return undefined;
|
|
543
821
|
|
|
544
|
-
//
|
|
545
|
-
//
|
|
822
|
+
// Per-result oversized or route-pressure output is spilled in full and
|
|
823
|
+
// replaced by a receipt. Conservative false positives cause an explicit
|
|
824
|
+
// spill, never task failure or silent byte loss.
|
|
546
825
|
const turnSequence = state.turns;
|
|
547
826
|
const sourceCallIndex = state.spilled.length;
|
|
548
827
|
const safeCallId = event.toolCallId.replace(/[^a-zA-Z0-9_.-]+/g, '-').slice(0, 64);
|
|
@@ -588,20 +867,22 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
588
867
|
source_call_index: sourceCallIndex,
|
|
589
868
|
byte_length: payload.length,
|
|
590
869
|
sha256: sha256(payload),
|
|
870
|
+
content_format: encodedContent.contentFormat,
|
|
591
871
|
};
|
|
592
872
|
state.spilled.push(receipt);
|
|
873
|
+
if (contextPressure) state.contextPressureSpillBytes += payload.length;
|
|
874
|
+
const spillReason = contextPressure
|
|
875
|
+
? `retaining it would consume protected final-answer runway (${String(remainingGrowthTokens())} tokens remain)`
|
|
876
|
+
: `it exceeded the ${String(seed.limits.max_tool_result_bytes)}-byte per-result transcript cap`;
|
|
877
|
+
const receiptText = [
|
|
878
|
+
`[delegate spill receipt] The ${event.toolName} result was ${String(payload.length)} bytes; ${spillReason}.`,
|
|
879
|
+
`It was written in full to ${relPath} (sha256 ${receipt.sha256}, content_format ${encodedContent.contentFormat}).`,
|
|
880
|
+
'Nothing was truncated: the complete encoded content is on disk.',
|
|
881
|
+
`Read an exact range as base64 with delegate_read_artifact({artifact:"${relPath}", offset, length}).`,
|
|
882
|
+
].join('\n');
|
|
883
|
+
accountRetainedGrowth(utf8(receiptText).length, `${event.toolName} spill receipt`);
|
|
593
884
|
return {
|
|
594
|
-
content: [
|
|
595
|
-
{
|
|
596
|
-
type: 'text' as const,
|
|
597
|
-
text: [
|
|
598
|
-
`[delegate spill receipt] The ${event.toolName} result was ${String(payload.length)} bytes, over the ${String(seed.limits.max_tool_result_bytes)}-byte transcript cap.`,
|
|
599
|
-
`It was written in full to ${relPath} (sha256 ${receipt.sha256}).`,
|
|
600
|
-
'Nothing was truncated: the complete bytes are on disk.',
|
|
601
|
-
`Read an exact range with delegate_read_artifact({artifact:"${relPath}", offset, length}).`,
|
|
602
|
-
].join('\n'),
|
|
603
|
-
},
|
|
604
|
-
],
|
|
885
|
+
content: [{ type: 'text' as const, text: receiptText }],
|
|
605
886
|
};
|
|
606
887
|
}
|
|
607
888
|
|
|
@@ -637,21 +918,46 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
637
918
|
}
|
|
638
919
|
const observedUsage = readUsage(Reflect.get(event.message, 'usage'));
|
|
639
920
|
if (observedUsage === undefined) {
|
|
640
|
-
// Never synthesize zero usage.
|
|
641
|
-
//
|
|
921
|
+
// Never synthesize zero usage. Once one turn is incomplete, later usage
|
|
922
|
+
// cannot conceal it by replacing the missing record.
|
|
923
|
+
state.usageIncomplete = true;
|
|
642
924
|
state.usageUnavailableReason =
|
|
643
|
-
'
|
|
925
|
+
'at least one provider turn did not report a complete token/cost usage record';
|
|
644
926
|
} else {
|
|
645
|
-
state.
|
|
646
|
-
|
|
927
|
+
if (state.firstRequestObservedInputTokens === undefined) {
|
|
928
|
+
state.firstRequestObservedInputTokens =
|
|
929
|
+
observedUsage.input + observedUsage.cacheRead + observedUsage.cacheWrite;
|
|
930
|
+
}
|
|
931
|
+
state.usage = addUsage(state.usage, observedUsage);
|
|
932
|
+
if (!state.usageIncomplete) state.usageUnavailableReason = undefined;
|
|
647
933
|
}
|
|
648
934
|
const content: unknown = Reflect.get(event.message, 'content');
|
|
649
935
|
if (!Array.isArray(content)) return;
|
|
936
|
+
if (attestation.stop_reason !== 'stop') {
|
|
937
|
+
const retained = messageContentMeasurement(content);
|
|
938
|
+
accountRetainedGrowth(retained.bytes, 'assistant intermediate message');
|
|
939
|
+
// Tool-use narration is retained in the transcript for reasoning but is
|
|
940
|
+
// not part of the delegate's committed answer. Only the final clean-stop
|
|
941
|
+
// assistant message owns the answer data plane.
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
state.answerBlocks = [];
|
|
945
|
+
state.answerBytes = 0;
|
|
650
946
|
for (const part of content) {
|
|
651
947
|
if (typeof part !== 'object' || part === null) continue;
|
|
652
948
|
if (Reflect.get(part, 'type') !== 'text') continue;
|
|
653
949
|
const text: unknown = Reflect.get(part, 'text');
|
|
654
|
-
if (typeof text
|
|
950
|
+
if (typeof text !== 'string' || text.length === 0) continue;
|
|
951
|
+
const bytes = utf8(text).length;
|
|
952
|
+
state.answerBytes += bytes;
|
|
953
|
+
if (state.answerBytes > seed.limits.max_answer_bytes) {
|
|
954
|
+
latch(
|
|
955
|
+
'child_capture_limit',
|
|
956
|
+
`delegate child answer text reached ${String(state.answerBytes)} bytes, exceeding its ${String(seed.limits.max_answer_bytes)}-byte capture contract; the transcript is preserved and no prefix is committed`,
|
|
957
|
+
);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
state.answerBlocks.push(text);
|
|
655
961
|
}
|
|
656
962
|
});
|
|
657
963
|
|