pi-background-tasks 2.3.0 → 2.4.2
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/README.md +2 -1
- package/TESTING.md +10 -6
- package/TEST_PLAN.md +8 -8
- package/docs/INDEX.md +1 -1
- package/docs/concepts/context-projection-and-budgeting.md +4 -2
- package/docs/manifest.json +6 -7
- package/docs/operations/configuration.md +8 -8
- package/docs/operations/troubleshooting.md +5 -3
- package/docs/reference/runtime-contracts.md +47 -47
- package/docs/subsystems/delegation.md +22 -13
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +6 -6
- package/docs/tools/bg_delegate.md +12 -7
- package/docs/tools/bg_result.md +2 -2
- package/docs/tools/fusion_research.md +2 -2
- package/package.json +1 -1
- package/src/core/context/token-budget.ts +16 -3
- package/src/core/delegate/artifacts.ts +14 -12
- package/src/core/delegate/budget.ts +78 -33
- package/src/core/delegate/launch.ts +4 -2
- package/src/core/delegate/result-package.ts +16 -0
- package/src/core/delegate/runner.ts +21 -2
- package/src/core/delegate/types.ts +12 -1
- package/src/core/fusion/child-protocol.ts +3 -3
- package/src/core/fusion/pi-child.ts +12 -5
- package/src/core/fusion/web-fetch.ts +72 -5
- package/src/core/update-check.ts +13 -1
- package/src/delegate-child-extension.ts +368 -69
- package/src/delegate-extension.ts +5 -2
|
@@ -14,9 +14,14 @@ import {
|
|
|
14
14
|
type DelegateUsageReport,
|
|
15
15
|
} from './core/delegate/types.js';
|
|
16
16
|
import { verifyDelegateSeedBytes } from './core/delegate/seed.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
|
|
19
|
+
DELEGATE_FINALIZATION_TRIGGER_TOKENS,
|
|
20
|
+
evaluateDelegateRuntimeBudget,
|
|
21
|
+
} from './core/delegate/budget.js';
|
|
18
22
|
import { utf8ByteClassBreakdown } from './core/context/token-budget.js';
|
|
19
23
|
import {
|
|
24
|
+
assertWellFormedUtf8,
|
|
20
25
|
buildDelegateResultPackage,
|
|
21
26
|
serializeDelegateResultPackage,
|
|
22
27
|
} from './core/delegate/result-package.js';
|
|
@@ -31,10 +36,10 @@ import {
|
|
|
31
36
|
* parent:
|
|
32
37
|
*
|
|
33
38
|
* - verifying the frozen seed bytes before the first model call;
|
|
34
|
-
* - measuring the outgoing message set before every model call and
|
|
35
|
-
*
|
|
36
|
-
* - spilling oversized tool results to hashed artifacts
|
|
37
|
-
* 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;
|
|
38
43
|
* - asserting every assistant message came from the pinned route;
|
|
39
44
|
* - enforcing turn and tool-call limits;
|
|
40
45
|
* - committing exactly one result package atomically.
|
|
@@ -63,8 +68,19 @@ interface GuardState {
|
|
|
63
68
|
spilled: DelegateSpillReceipt[];
|
|
64
69
|
attestations: DelegateRouteAttestation[];
|
|
65
70
|
usage: Usage | undefined;
|
|
71
|
+
usageIncomplete: boolean;
|
|
66
72
|
usageUnavailableReason: string | undefined;
|
|
67
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;
|
|
68
84
|
terminal: TerminalLatch | undefined;
|
|
69
85
|
committed: boolean;
|
|
70
86
|
}
|
|
@@ -82,6 +98,16 @@ interface TerminalLatch {
|
|
|
82
98
|
message: string;
|
|
83
99
|
}
|
|
84
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
|
+
|
|
85
111
|
/**
|
|
86
112
|
* Stop reasons that may be committed as a complete answer.
|
|
87
113
|
*
|
|
@@ -115,6 +141,85 @@ function utf8(value: string): Buffer {
|
|
|
115
141
|
return Buffer.from(value, 'utf8');
|
|
116
142
|
}
|
|
117
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
|
+
|
|
118
223
|
function finiteNonNegative(source: object, key: string): number | undefined {
|
|
119
224
|
const value: unknown = Reflect.get(source, key);
|
|
120
225
|
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
|
|
@@ -305,8 +410,19 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
305
410
|
spilled: [],
|
|
306
411
|
attestations: [],
|
|
307
412
|
usage: undefined,
|
|
413
|
+
usageIncomplete: false,
|
|
308
414
|
usageUnavailableReason: 'the child produced no assistant message carrying usage',
|
|
309
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,
|
|
310
426
|
terminal: undefined,
|
|
311
427
|
committed: false,
|
|
312
428
|
};
|
|
@@ -316,13 +432,92 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
316
432
|
}
|
|
317
433
|
|
|
318
434
|
function usageReport(): DelegateUsageReport {
|
|
319
|
-
if (state.usage !== undefined)
|
|
435
|
+
if (!state.usageIncomplete && state.usage !== undefined) {
|
|
436
|
+
return { status: 'observed', usage: state.usage };
|
|
437
|
+
}
|
|
320
438
|
return {
|
|
321
439
|
status: 'unavailable',
|
|
322
440
|
reason: state.usageUnavailableReason ?? 'usage was not reported by the provider',
|
|
323
441
|
};
|
|
324
442
|
}
|
|
325
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
|
+
|
|
326
521
|
/**
|
|
327
522
|
* Commit exactly one result package.
|
|
328
523
|
*
|
|
@@ -335,13 +530,6 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
335
530
|
writeTerminalRecord(state.terminal);
|
|
336
531
|
return;
|
|
337
532
|
}
|
|
338
|
-
if (state.answerBlocks.length === 0) {
|
|
339
|
-
writeTerminalRecord({
|
|
340
|
-
code: 'child_exited_without_commit',
|
|
341
|
-
message: 'the delegate child produced no assistant answer text',
|
|
342
|
-
});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
533
|
// A hash proves the bytes are intact; it cannot prove they are complete.
|
|
346
534
|
// Only an approved terminal stop reason may be committed as success, so a
|
|
347
535
|
// response cut short by the output-token limit, a content filter, an
|
|
@@ -349,7 +537,14 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
349
537
|
if (!ACCEPTED_STOP_REASONS.has(stopReason)) {
|
|
350
538
|
writeTerminalRecord({
|
|
351
539
|
code: stopReason === 'length' ? 'child_model_output_limit' : 'child_result_invalid',
|
|
352
|
-
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',
|
|
353
548
|
});
|
|
354
549
|
return;
|
|
355
550
|
}
|
|
@@ -360,6 +555,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
360
555
|
});
|
|
361
556
|
return;
|
|
362
557
|
}
|
|
558
|
+
writeRuntimeBudgetRecord();
|
|
363
559
|
const pkg = buildDelegateResultPackage({
|
|
364
560
|
taskId: seed.task_id,
|
|
365
561
|
launchNonce: seed.launch_nonce,
|
|
@@ -379,6 +575,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
379
575
|
}
|
|
380
576
|
|
|
381
577
|
function writeTerminalRecord(terminal: TerminalLatch): void {
|
|
578
|
+
writeRuntimeBudgetRecord();
|
|
382
579
|
commitFileSync(
|
|
383
580
|
join(artifactDirAbs, 'child-terminal.json'),
|
|
384
581
|
utf8(
|
|
@@ -404,10 +601,11 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
404
601
|
name: 'delegate_read_artifact',
|
|
405
602
|
label: 'Delegate Artifact Read',
|
|
406
603
|
description:
|
|
407
|
-
'Read an exact byte range from a spilled tool-result artifact. Returns
|
|
408
|
-
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',
|
|
409
606
|
promptGuidelines: [
|
|
410
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.',
|
|
411
609
|
'Request a bounded range. A request past the end of the artifact fails loudly rather than returning a short read.',
|
|
412
610
|
],
|
|
413
611
|
parameters: ArtifactReadParams,
|
|
@@ -445,8 +643,32 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
445
643
|
`delegate_read_artifact returned ${String(slice.length)} of ${String(params.length)} requested bytes`,
|
|
446
644
|
);
|
|
447
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
|
+
}
|
|
448
670
|
return Promise.resolve({
|
|
449
|
-
content: [{ type: 'text' as const, text:
|
|
671
|
+
content: [{ type: 'text' as const, text: responseText }],
|
|
450
672
|
details: {
|
|
451
673
|
artifact: params.artifact,
|
|
452
674
|
offset: params.offset,
|
|
@@ -458,12 +680,15 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
458
680
|
});
|
|
459
681
|
|
|
460
682
|
pi.on('context', (event, ctx) => {
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// 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.
|
|
466
687
|
try {
|
|
688
|
+
if (state.terminal !== undefined) {
|
|
689
|
+
ctx.abort();
|
|
690
|
+
return { messages: suppressedMessages(event.messages) };
|
|
691
|
+
}
|
|
467
692
|
const measurement = retainedInputMeasurement(event.messages, ctx.getSystemPrompt());
|
|
468
693
|
const verdict = evaluateDelegateRuntimeBudget(
|
|
469
694
|
{
|
|
@@ -474,32 +699,74 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
474
699
|
seed.limits.allowed_input_tokens,
|
|
475
700
|
seed.route,
|
|
476
701
|
);
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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] };
|
|
487
734
|
} catch (error) {
|
|
488
735
|
latch(
|
|
489
736
|
'child_result_invalid',
|
|
490
|
-
`delegate context
|
|
737
|
+
`delegate context measurement failed and the run was stopped rather than dispatched unguarded: ${error instanceof Error ? error.message : String(error)}`,
|
|
491
738
|
);
|
|
492
739
|
try {
|
|
493
740
|
ctx.abort();
|
|
494
741
|
} catch {
|
|
495
|
-
//
|
|
496
|
-
// latch above already prevents a success commit, and the suppressed
|
|
497
|
-
// replacement below still removes the content from this request.
|
|
742
|
+
// The terminal latch prevents success even if abort itself fails.
|
|
498
743
|
}
|
|
499
744
|
return { messages: suppressedMessages(event.messages) };
|
|
500
745
|
}
|
|
501
746
|
});
|
|
502
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
|
+
|
|
503
770
|
pi.on('tool_result', (event) => {
|
|
504
771
|
// Fail closed for the same reason as the context guard: a throw here would
|
|
505
772
|
// let the ORIGINAL oversized payload flow into the transcript.
|
|
@@ -525,31 +792,36 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
525
792
|
function guardToolResult(event: {
|
|
526
793
|
toolName: string;
|
|
527
794
|
toolCallId: string;
|
|
528
|
-
content: ReadonlyArray<
|
|
795
|
+
content: ReadonlyArray<DelegateToolResultPart>;
|
|
529
796
|
}): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } | undefined {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
const texts = event.content.flatMap((part) =>
|
|
538
|
-
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
539
|
-
);
|
|
540
|
-
const joined = texts.join('');
|
|
541
|
-
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;
|
|
542
803
|
state.totalToolOutputBytes += payload.length;
|
|
543
804
|
if (state.totalToolOutputBytes > seed.limits.max_total_tool_output_bytes) {
|
|
544
805
|
latch(
|
|
545
806
|
'aggregate_tool_output_cap',
|
|
546
807
|
`delegate child accumulated ${String(state.totalToolOutputBytes)} bytes of tool output, exceeding its ${String(seed.limits.max_total_tool_output_bytes)}-byte cap`,
|
|
547
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;
|
|
548
820
|
}
|
|
549
|
-
if (payload.length <= seed.limits.max_tool_result_bytes) return undefined;
|
|
550
821
|
|
|
551
|
-
//
|
|
552
|
-
//
|
|
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.
|
|
553
825
|
const turnSequence = state.turns;
|
|
554
826
|
const sourceCallIndex = state.spilled.length;
|
|
555
827
|
const safeCallId = event.toolCallId.replace(/[^a-zA-Z0-9_.-]+/g, '-').slice(0, 64);
|
|
@@ -595,20 +867,22 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
595
867
|
source_call_index: sourceCallIndex,
|
|
596
868
|
byte_length: payload.length,
|
|
597
869
|
sha256: sha256(payload),
|
|
870
|
+
content_format: encodedContent.contentFormat,
|
|
598
871
|
};
|
|
599
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`);
|
|
600
884
|
return {
|
|
601
|
-
content: [
|
|
602
|
-
{
|
|
603
|
-
type: 'text' as const,
|
|
604
|
-
text: [
|
|
605
|
-
`[delegate spill receipt] The ${event.toolName} result was ${String(payload.length)} bytes, over the ${String(seed.limits.max_tool_result_bytes)}-byte transcript cap.`,
|
|
606
|
-
`It was written in full to ${relPath} (sha256 ${receipt.sha256}).`,
|
|
607
|
-
'Nothing was truncated: the complete bytes are on disk.',
|
|
608
|
-
`Read an exact range with delegate_read_artifact({artifact:"${relPath}", offset, length}).`,
|
|
609
|
-
].join('\n'),
|
|
610
|
-
},
|
|
611
|
-
],
|
|
885
|
+
content: [{ type: 'text' as const, text: receiptText }],
|
|
612
886
|
};
|
|
613
887
|
}
|
|
614
888
|
|
|
@@ -644,21 +918,46 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
644
918
|
}
|
|
645
919
|
const observedUsage = readUsage(Reflect.get(event.message, 'usage'));
|
|
646
920
|
if (observedUsage === undefined) {
|
|
647
|
-
// Never synthesize zero usage.
|
|
648
|
-
//
|
|
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;
|
|
649
924
|
state.usageUnavailableReason =
|
|
650
|
-
'
|
|
925
|
+
'at least one provider turn did not report a complete token/cost usage record';
|
|
651
926
|
} else {
|
|
652
|
-
state.
|
|
653
|
-
|
|
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;
|
|
654
933
|
}
|
|
655
934
|
const content: unknown = Reflect.get(event.message, 'content');
|
|
656
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;
|
|
657
946
|
for (const part of content) {
|
|
658
947
|
if (typeof part !== 'object' || part === null) continue;
|
|
659
948
|
if (Reflect.get(part, 'type') !== 'text') continue;
|
|
660
949
|
const text: unknown = Reflect.get(part, 'text');
|
|
661
|
-
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);
|
|
662
961
|
}
|
|
663
962
|
});
|
|
664
963
|
|
|
@@ -466,7 +466,7 @@ export function registerDelegateExtension(
|
|
|
466
466
|
child_session_id: prepared.preflight.childSessionId,
|
|
467
467
|
artifact_dir: prepared.facts.artifactDir,
|
|
468
468
|
seed_sha256: prepared.facts.seedSha256,
|
|
469
|
-
seed_utf8_bytes: prepared.preflight.
|
|
469
|
+
seed_utf8_bytes: Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'),
|
|
470
470
|
budget: prepared.facts.budget,
|
|
471
471
|
extension_mode: extensionMode,
|
|
472
472
|
auto_deliver: autoDeliver,
|
|
@@ -480,7 +480,8 @@ export function registerDelegateExtension(
|
|
|
480
480
|
`Route pinned: ${route.qualified_id} (${route.origin}); it is never substituted.`,
|
|
481
481
|
`Child session: ${prepared.preflight.childSessionId} (separate from this session)`,
|
|
482
482
|
`Artifacts: ${prepared.facts.artifactDir}`,
|
|
483
|
-
`Seed: ${String(prepared.preflight.
|
|
483
|
+
`Seed: ${String(Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'))} bytes, sha256 ${prepared.facts.seedSha256}`,
|
|
484
|
+
`Child prompt: ${String(prepared.preflight.plan.child_prompt_utf8_bytes)} bytes; launch estimate ${String(prepared.preflight.plan.launch_input_tokens_upper_bound)} / ${String(prepared.preflight.plan.route.allowed_input_tokens)} allowed input tokens; protected retained-growth runway ${String(prepared.preflight.plan.retained_growth_budget_tokens)} tokens.`,
|
|
484
485
|
`Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
|
|
485
486
|
`Capability: ${capability} (read/search/list only)`,
|
|
486
487
|
`Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
|
|
@@ -698,6 +699,8 @@ export function registerDelegateExtension(
|
|
|
698
699
|
route: { provider: facts.route.provider, model: facts.route.model },
|
|
699
700
|
taskStatus: task.status === 'completed' ? 'completed' : task.status,
|
|
700
701
|
taskError: task.error,
|
|
702
|
+
taskOutputPath: task.outputPath,
|
|
703
|
+
taskOutputAbsPath: task.outputAbsPath,
|
|
701
704
|
});
|
|
702
705
|
|
|
703
706
|
if (terminal.error !== undefined || terminal.result === undefined) {
|