@pi-unipi/background-tasks 2.6.1

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.
Files changed (116) hide show
  1. package/README.md +87 -0
  2. package/extensions/anthropic-attribution.ts +1 -0
  3. package/extensions/delegate-child.ts +1 -0
  4. package/extensions/fusion-child.ts +1 -0
  5. package/package.json +40 -0
  6. package/src/__tests__/anthropic-attribution.test.ts +195 -0
  7. package/src/__tests__/config.test.ts +137 -0
  8. package/src/__tests__/core.test.ts +493 -0
  9. package/src/__tests__/delegate-artifacts.test.ts +528 -0
  10. package/src/__tests__/delegate-budget.test.ts +456 -0
  11. package/src/__tests__/delegate-launch.test.ts +676 -0
  12. package/src/__tests__/delegate-result-package.test.ts +350 -0
  13. package/src/__tests__/delegate-seed.test.ts +392 -0
  14. package/src/__tests__/durable-fs.test.ts +559 -0
  15. package/src/__tests__/extension-api.test.ts +579 -0
  16. package/src/__tests__/fusion-artifacts.test.ts +1039 -0
  17. package/src/__tests__/fusion-budget.test.ts +1356 -0
  18. package/src/__tests__/fusion-claude-cache.test.ts +320 -0
  19. package/src/__tests__/fusion-config.test.ts +335 -0
  20. package/src/__tests__/fusion-context-prompts.test.ts +670 -0
  21. package/src/__tests__/fusion-evaluation.test.ts +315 -0
  22. package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
  23. package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
  24. package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
  25. package/src/__tests__/fusion-model-selector.test.ts +205 -0
  26. package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
  27. package/src/__tests__/fusion-rpc.test.ts +369 -0
  28. package/src/__tests__/fusion-sdk.test.ts +1226 -0
  29. package/src/__tests__/fusion-v5-core.test.ts +219 -0
  30. package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
  31. package/src/__tests__/fusion-web-fetch.test.ts +485 -0
  32. package/src/__tests__/fusion-workflows.test.ts +59 -0
  33. package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
  34. package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
  35. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
  36. package/src/__tests__/helpers/fusion-canonical.ts +140 -0
  37. package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
  38. package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
  39. package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
  40. package/src/__tests__/helpers/normalize.ts +22 -0
  41. package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
  42. package/src/__tests__/pi-launch.test.ts +202 -0
  43. package/src/__tests__/registry.test.ts +1580 -0
  44. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
  45. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
  46. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
  47. package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
  48. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
  49. package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
  50. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
  51. package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
  52. package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
  53. package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
  54. package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
  55. package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
  56. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
  57. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
  58. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
  59. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
  60. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
  61. package/src/__tests__/task-manager.test.ts +479 -0
  62. package/src/__tests__/windows-taskkill.test.ts +161 -0
  63. package/src/anthropic-attribution-path.ts +21 -0
  64. package/src/anthropic-attribution.ts +1983 -0
  65. package/src/attested-pi-run.ts +612 -0
  66. package/src/child-process.ts +55 -0
  67. package/src/common.ts +8 -0
  68. package/src/config.ts +292 -0
  69. package/src/context-parent-snapshot.ts +142 -0
  70. package/src/context-token-budget.ts +903 -0
  71. package/src/context-visible-conversation-v2.ts +551 -0
  72. package/src/delegate/artifacts.ts +487 -0
  73. package/src/delegate/budget.ts +415 -0
  74. package/src/delegate/hook-contract-evidence.json +18 -0
  75. package/src/delegate/hook-contract.ts +154 -0
  76. package/src/delegate/launch.ts +497 -0
  77. package/src/delegate/result-package.ts +459 -0
  78. package/src/delegate/runner.ts +449 -0
  79. package/src/delegate/seed.ts +423 -0
  80. package/src/delegate/types.ts +323 -0
  81. package/src/delegate-child-extension.ts +978 -0
  82. package/src/delegate-extension.ts +806 -0
  83. package/src/durable-fs.ts +386 -0
  84. package/src/extension-api.ts +548 -0
  85. package/src/fixtures/delegate-context-incident.json +17 -0
  86. package/src/fixtures/fusion-golden-bytes.json +310 -0
  87. package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
  88. package/src/fusion/artifacts.ts +967 -0
  89. package/src/fusion/budget.ts +1162 -0
  90. package/src/fusion/child-protocol.ts +305 -0
  91. package/src/fusion/claude-cache.ts +207 -0
  92. package/src/fusion/clean-context.ts +91 -0
  93. package/src/fusion/config.ts +449 -0
  94. package/src/fusion/context.ts +265 -0
  95. package/src/fusion/evaluation.ts +800 -0
  96. package/src/fusion/orchestrator.ts +1288 -0
  97. package/src/fusion/output-contract.ts +34 -0
  98. package/src/fusion/pi-child.ts +2373 -0
  99. package/src/fusion/prompts.ts +345 -0
  100. package/src/fusion/result-package.ts +959 -0
  101. package/src/fusion/source-policy.ts +257 -0
  102. package/src/fusion/types.ts +1139 -0
  103. package/src/fusion/web-fetch.ts +1060 -0
  104. package/src/fusion/workflows.ts +184 -0
  105. package/src/fusion-child-extension.ts +1052 -0
  106. package/src/fusion-extension.ts +1293 -0
  107. package/src/index.ts +295 -0
  108. package/src/pi-launch.ts +225 -0
  109. package/src/registry.ts +2424 -0
  110. package/src/settings-overlay.ts +208 -0
  111. package/src/task-manager.ts +774 -0
  112. package/src/tools.ts +530 -0
  113. package/src/turndown.d.ts +15 -0
  114. package/src/types.ts +963 -0
  115. package/src/ui/fusion-model-selector.ts +322 -0
  116. package/src/windows-taskkill.ts +250 -0
@@ -0,0 +1,415 @@
1
+ import {
2
+ TOKEN_BUDGET_CALIBRATION_VERSION,
3
+ TOKEN_BUDGET_FAMILY_CALIBRATIONS,
4
+ TOKEN_BUDGET_RATE_SCALE,
5
+ estimateInputTokens,
6
+ knownTextSegment,
7
+ resolveTokenBudgetFamily,
8
+ utf8ByteClassBreakdown,
9
+ allowedInputTokens,
10
+ isUsableContextWindow,
11
+ type EstimateInputTokensResult,
12
+ type TokenBudgetByteClassBreakdown,
13
+ type TokenBudgetFamily,
14
+ type TokenBudgetFamilyCalibration,
15
+ type TokenBudgetRateSource,
16
+ } from '../context-token-budget.js';
17
+ import {
18
+ DELEGATE_BUDGET_PLAN_SCHEMA_VERSION,
19
+ DelegateError,
20
+ type DelegateLimits,
21
+ type DelegatePinnedRoute,
22
+ } from './types.js';
23
+
24
+ /**
25
+ * Delegate budgeting.
26
+ *
27
+ * A delegate child is a multi-turn, tool-using agent, so its budget has two
28
+ * distinct phases rather than Fusion's single-shot stage forecast:
29
+ *
30
+ * 1. Launch admission checks the frozen seed, framing, and child system prompt
31
+ * with the same backed family calibration used by Fusion for large prompts.
32
+ * 2. A separate provable 1 B/token forecast sizes the transcript-growth runway
33
+ * used for explicit tool-result spilling.
34
+ * 3. Runtime measurements are advisory. Package-owned growth is controlled
35
+ * before transcript entry; Pi and the provider own live context handling.
36
+ *
37
+ * Nothing here clips, substitutes, or silently reduces content. Tool bytes that
38
+ * do not fit the retained-growth runway are preserved as hashed spill artifacts.
39
+ */
40
+
41
+ /** Output tokens reserved so the child can always finish an answer. */
42
+ export const DELEGATE_RESERVED_OUTPUT_TOKENS = 16_384;
43
+ /** Provider/tool-schema framing the package does not directly control. */
44
+ export const DELEGATE_FRAMING_RESERVE_TOKENS = 8_192;
45
+ export const DELEGATE_SAFETY_RESERVE_TOKENS = 4_096;
46
+ /** Below this, a route cannot hold a useful seed plus real investigation. */
47
+ export const DELEGATE_MIN_USABLE_INPUT_TOKENS = 8_192;
48
+ export const DELEGATE_MIN_CONTEXT_WINDOW_TOKENS =
49
+ DELEGATE_MIN_USABLE_INPUT_TOKENS +
50
+ DELEGATE_RESERVED_OUTPUT_TOKENS +
51
+ DELEGATE_FRAMING_RESERVE_TOKENS +
52
+ DELEGATE_SAFETY_RESERVE_TOKENS;
53
+
54
+ export const DELEGATE_DEFAULT_MAX_TURNS = 24;
55
+ export const DELEGATE_DEFAULT_MAX_TOOL_CALLS = 120;
56
+ export const DELEGATE_DEFAULT_TIMEOUT_SECONDS = 1200;
57
+ export const DELEGATE_MAX_TOOL_RESULT_BYTES = 64 * 1024;
58
+ export const DELEGATE_MAX_TOTAL_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024;
59
+ export const DELEGATE_MAX_ANSWER_BYTES = 4 * 1024 * 1024;
60
+ /** Input runway held back for a final no-tool answer after investigation. */
61
+ export const DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS = 32 * 1024;
62
+ /** Remaining retained-growth runway at which the child disables tools. */
63
+ export const DELEGATE_FINALIZATION_TRIGGER_TOKENS = 8 * 1024;
64
+ /** Answers at or under this serialize inline; larger ones degrade explicitly. */
65
+ export const DELEGATE_INLINE_ANSWER_BYTES = 48 * 1024;
66
+
67
+ export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v3';
68
+
69
+ export interface DelegateBudgetPolicyDescriptor {
70
+ id: typeof DELEGATE_BUDGET_POLICY_ID;
71
+ calibration_version: string;
72
+ calibration_table: Readonly<Record<TokenBudgetFamily, TokenBudgetFamilyCalibration>>;
73
+ reserved_output_tokens: number;
74
+ framing_reserve_tokens: number;
75
+ safety_reserve_tokens: number;
76
+ min_usable_input_tokens: number;
77
+ inline_answer_bytes: number;
78
+ finalization_input_reserve_tokens: number;
79
+ finalization_trigger_tokens: number;
80
+ launch_estimator_scope: 'calibrated_large_prompt';
81
+ retained_growth_estimator_scope: 'provable_1_byte_per_token';
82
+ live_provider_context_owner: 'pi_and_provider';
83
+ }
84
+
85
+ export const DELEGATE_BUDGET_POLICY: DelegateBudgetPolicyDescriptor = {
86
+ id: DELEGATE_BUDGET_POLICY_ID,
87
+ calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
88
+ calibration_table: TOKEN_BUDGET_FAMILY_CALIBRATIONS,
89
+ reserved_output_tokens: DELEGATE_RESERVED_OUTPUT_TOKENS,
90
+ framing_reserve_tokens: DELEGATE_FRAMING_RESERVE_TOKENS,
91
+ safety_reserve_tokens: DELEGATE_SAFETY_RESERVE_TOKENS,
92
+ min_usable_input_tokens: DELEGATE_MIN_USABLE_INPUT_TOKENS,
93
+ inline_answer_bytes: DELEGATE_INLINE_ANSWER_BYTES,
94
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
95
+ finalization_trigger_tokens: DELEGATE_FINALIZATION_TRIGGER_TOKENS,
96
+ launch_estimator_scope: 'calibrated_large_prompt',
97
+ retained_growth_estimator_scope: 'provable_1_byte_per_token',
98
+ live_provider_context_owner: 'pi_and_provider',
99
+ };
100
+
101
+ export interface DelegateAdmissionPlanV1 {
102
+ schema_version: typeof DELEGATE_BUDGET_PLAN_SCHEMA_VERSION;
103
+ policy: DelegateBudgetPolicyDescriptor;
104
+ route: {
105
+ provider: string;
106
+ model: string;
107
+ qualified_id: string;
108
+ context_window_tokens: number;
109
+ allowed_input_tokens: number;
110
+ family: TokenBudgetFamily;
111
+ backed: boolean;
112
+ rate_source: TokenBudgetRateSource;
113
+ byte_capacity_utf8_bytes: number;
114
+ };
115
+ child_prompt_utf8_bytes: number;
116
+ child_prompt_multibyte_utf8_bytes: number;
117
+ system_prompt_utf8_bytes: number;
118
+ system_prompt_multibyte_utf8_bytes: number;
119
+ launch_utf8_bytes: number;
120
+ launch_input_tokens_upper_bound: number;
121
+ conservative_launch_input_tokens: number;
122
+ conservative_launch_fits: boolean;
123
+ signed_headroom_tokens: number;
124
+ utilization_basis_points: number;
125
+ retained_growth_budget_tokens: number;
126
+ finalization_input_reserve_tokens: number;
127
+ byte_class_breakdown: TokenBudgetByteClassBreakdown;
128
+ dominant_byte_class: EstimateInputTokensResult['rateSource']['dominant_byte_class'];
129
+ estimate: EstimateInputTokensResult;
130
+ conservative_estimate: EstimateInputTokensResult;
131
+ fits: boolean;
132
+ limits: DelegateLimits;
133
+ }
134
+
135
+ function utilizationBasisPoints(tokens: number, allowed: number): number {
136
+ if (!Number.isSafeInteger(tokens) || tokens < 0) {
137
+ throw new TypeError('tokens must be a non-negative safe integer');
138
+ }
139
+ if (!Number.isSafeInteger(allowed) || allowed <= 0) {
140
+ throw new TypeError('allowed must be a positive safe integer');
141
+ }
142
+ return Math.floor(((tokens * 10_000) + allowed - 1) / allowed);
143
+ }
144
+
145
+ function routeFamily(route: DelegatePinnedRoute): ReturnType<typeof resolveTokenBudgetFamily> {
146
+ return resolveTokenBudgetFamily({ provider: route.provider, model: route.model });
147
+ }
148
+
149
+ /**
150
+ * Usable input tokens for a pinned route.
151
+ *
152
+ * A route with an unknown, non-integral, or non-positive context window is a
153
+ * loud `route_capacity_unknown` refusal. The delegate never assumes a default
154
+ * window, because assuming one is how oversized prompts reach a provider.
155
+ */
156
+ export function delegateAllowedInputTokens(route: DelegatePinnedRoute): number {
157
+ if (!isUsableContextWindow(route.context_window_tokens)) {
158
+ throw new DelegateError(
159
+ `bg_delegate route ${route.qualified_id} reports no usable context-window capacity`,
160
+ {
161
+ code: 'route_capacity_unknown',
162
+ childCreated: false,
163
+ remediation: [
164
+ 'Pin an explicit route whose model catalogue entry declares a context window.',
165
+ 'No child was created and no capacity was assumed.',
166
+ ],
167
+ },
168
+ );
169
+ }
170
+ const allowed = allowedInputTokens(route.context_window_tokens, {
171
+ reservedOutputTokens: DELEGATE_RESERVED_OUTPUT_TOKENS,
172
+ framingReserveTokens: DELEGATE_FRAMING_RESERVE_TOKENS,
173
+ safetyReserveTokens: DELEGATE_SAFETY_RESERVE_TOKENS,
174
+ });
175
+ if (allowed < DELEGATE_MIN_USABLE_INPUT_TOKENS) {
176
+ throw new DelegateError(
177
+ `bg_delegate route ${route.qualified_id} has a ${String(route.context_window_tokens)}-token context window, but a delegate child requires at least ${String(DELEGATE_MIN_CONTEXT_WINDOW_TOKENS)} tokens: ${String(DELEGATE_RESERVED_OUTPUT_TOKENS)} output + ${String(DELEGATE_FRAMING_RESERVE_TOKENS)} framing + ${String(DELEGATE_SAFETY_RESERVE_TOKENS)} safety + ${String(DELEGATE_MIN_USABLE_INPUT_TOKENS)} usable input`,
178
+ {
179
+ code: 'route_capacity_unknown',
180
+ childCreated: false,
181
+ remediation: ['Pin a larger-context route for this delegate.'],
182
+ },
183
+ );
184
+ }
185
+ return allowed;
186
+ }
187
+
188
+ export interface DelegateAdmissionInput {
189
+ route: DelegatePinnedRoute;
190
+ childPrompt: string;
191
+ childSystemPrompt: string;
192
+ limits: DelegateLimits;
193
+ }
194
+
195
+ /** Deterministic launch-admission forecast. Pure; creates nothing. */
196
+ export function planDelegateAdmission(input: DelegateAdmissionInput): DelegateAdmissionPlanV1 {
197
+ const allowed = delegateAllowedInputTokens(input.route);
198
+ const family = routeFamily(input.route);
199
+ const childPrompt = utf8ByteClassBreakdown(input.childPrompt);
200
+ const system = utf8ByteClassBreakdown(input.childSystemPrompt);
201
+ const segments = [knownTextSegment(input.childPrompt), knownTextSegment(input.childSystemPrompt)];
202
+ const estimate = estimateInputTokens({
203
+ family: family.family,
204
+ calibrationBacked: family.backed,
205
+ familyResolution: family.resolution,
206
+ allowedInputTokens: allowed,
207
+ scope: 'delegate_launch',
208
+ segments,
209
+ });
210
+ const conservativeEstimate = estimateInputTokens({
211
+ family: family.family,
212
+ calibrationBacked: family.backed,
213
+ familyResolution: family.resolution,
214
+ allowedInputTokens: allowed,
215
+ scope: 'conservative',
216
+ segments,
217
+ });
218
+ // `conservativeEstimate.tokens` intentionally uses the shared estimator's
219
+ // calibrated multibyte diagnostic rate. The counter-forecast published as
220
+ // "provable" must instead use that estimator's explicit 1 B/token ceiling
221
+ // for multibyte bytes as well as normal/dense bytes.
222
+ const provableConservativeLaunchTokens =
223
+ conservativeEstimate.advisory.input_tokens_if_multibyte_used_provable_ceiling;
224
+ const byteCapacity = Math.floor(
225
+ (allowed * estimate.rateSource.effective_rate_bytes_per_token_x100) / TOKEN_BUDGET_RATE_SCALE,
226
+ );
227
+ const launchBytes = childPrompt.bytes + system.bytes;
228
+ const retainedGrowthBudget = Math.max(
229
+ 0,
230
+ allowed - estimate.tokens - DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
231
+ );
232
+ return {
233
+ schema_version: DELEGATE_BUDGET_PLAN_SCHEMA_VERSION,
234
+ policy: DELEGATE_BUDGET_POLICY,
235
+ route: {
236
+ provider: input.route.provider,
237
+ model: input.route.model,
238
+ qualified_id: input.route.qualified_id,
239
+ context_window_tokens: input.route.context_window_tokens,
240
+ allowed_input_tokens: allowed,
241
+ family: family.family,
242
+ backed: estimate.rateSource.backed,
243
+ rate_source: estimate.rateSource,
244
+ byte_capacity_utf8_bytes: byteCapacity,
245
+ },
246
+ child_prompt_utf8_bytes: childPrompt.bytes,
247
+ child_prompt_multibyte_utf8_bytes: childPrompt.multibyteBytes,
248
+ system_prompt_utf8_bytes: system.bytes,
249
+ system_prompt_multibyte_utf8_bytes: system.multibyteBytes,
250
+ launch_utf8_bytes: launchBytes,
251
+ launch_input_tokens_upper_bound: estimate.tokens,
252
+ conservative_launch_input_tokens: provableConservativeLaunchTokens,
253
+ conservative_launch_fits: provableConservativeLaunchTokens <= allowed,
254
+ signed_headroom_tokens: allowed - estimate.tokens,
255
+ utilization_basis_points: utilizationBasisPoints(estimate.tokens, allowed),
256
+ retained_growth_budget_tokens: retainedGrowthBudget,
257
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
258
+ byte_class_breakdown: estimate.byte_class_breakdown,
259
+ dominant_byte_class: estimate.rateSource.dominant_byte_class,
260
+ estimate,
261
+ conservative_estimate: conservativeEstimate,
262
+ fits: estimate.tokens <= allowed,
263
+ limits: input.limits,
264
+ };
265
+ }
266
+
267
+ function rateWarningText(rateSource: TokenBudgetRateSource, qualifiedId: string): string {
268
+ if (rateSource.warning === null) return '';
269
+ return ` Estimator warning for ${qualifiedId}: ${rateSource.warning}.`;
270
+ }
271
+
272
+ function requiredByteReduction(plan: DelegateAdmissionPlanV1): number {
273
+ const variableTokens =
274
+ plan.route.allowed_input_tokens - plan.route.rate_source.affine_f_tokens;
275
+ const maximumBytes = variableTokens <= 0
276
+ ? 0
277
+ : Math.floor(
278
+ (variableTokens * plan.route.rate_source.effective_rate_bytes_per_token_x100) /
279
+ TOKEN_BUDGET_RATE_SCALE,
280
+ );
281
+ return Math.max(0, plan.launch_utf8_bytes - maximumBytes);
282
+ }
283
+
284
+ /**
285
+ * Enforce the admission plan.
286
+ *
287
+ * Called before the child process, the child session, and the artifact
288
+ * directory exist, so a refusal leaves zero children and zero artifacts.
289
+ */
290
+ export function assertDelegateAdmission(plan: DelegateAdmissionPlanV1): void {
291
+ if (plan.fits) return;
292
+ const overage = plan.launch_input_tokens_upper_bound - plan.route.allowed_input_tokens;
293
+ throw new DelegateError(
294
+ `bg_delegate child prompt does not fit the pinned route before launch. Route ${plan.route.qualified_id} allows ${String(plan.route.allowed_input_tokens)} input tokens; the exact child prompt plus child system prompt measure ${String(plan.launch_utf8_bytes)} UTF-8 bytes (<= ${String(plan.launch_input_tokens_upper_bound)} input tokens), over by ${String(overage)} tokens. Estimator family ${plan.route.family}, source ${plan.route.rate_source.source}, backed=${String(plan.route.rate_source.backed)}, dominant_byte_class=${plan.dominant_byte_class}, rate ${String(plan.route.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(plan.route.rate_source.affine_f_tokens)} tokens.${rateWarningText(plan.route.rate_source, plan.route.qualified_id)} Required reduction is at least ${String(requiredByteReduction(plan))} UTF-8 bytes. No child process, child session, or artifact was created. Nothing was clipped, dropped, or substituted.`,
295
+ {
296
+ code: 'seed_budget_exceeded',
297
+ childCreated: false,
298
+ budget: {
299
+ measurement_kind: 'launch_admission',
300
+ measured_utf8_bytes: plan.launch_utf8_bytes,
301
+ measured_input_tokens_upper_bound: plan.launch_input_tokens_upper_bound,
302
+ allowed_input_tokens: plan.route.allowed_input_tokens,
303
+ rate_source: plan.route.rate_source,
304
+ backed: plan.route.rate_source.backed,
305
+ dominant_byte_class: plan.dominant_byte_class,
306
+ byte_class_breakdown: plan.byte_class_breakdown,
307
+ },
308
+ remediation: [
309
+ 'Pin a larger-context route with the route argument.',
310
+ 'Delegate earlier in the session, or start a fresh conversation, so less history is projected.',
311
+ 'Restate only the required findings as visible conversation text; omitted tool payloads are not what is large here.',
312
+ ],
313
+ },
314
+ );
315
+ }
316
+
317
+ export interface DelegateRuntimeMeasurement {
318
+ /** Complete retained input for the next model call, in UTF-8 bytes. */
319
+ retainedInputBytes: number;
320
+ retainedInputMultibyteBytes: number;
321
+ retainedInputDenseBytes: number;
322
+ }
323
+
324
+ export interface DelegateGovernorVerdict {
325
+ withinBudget: boolean;
326
+ measuredTokens: number;
327
+ allowedTokens: number;
328
+ overageTokens: number;
329
+ byteClassBreakdown: TokenBudgetByteClassBreakdown;
330
+ dominantByteClass: EstimateInputTokensResult['rateSource']['dominant_byte_class'];
331
+ backed: boolean;
332
+ rateSource: TokenBudgetRateSource;
333
+ }
334
+
335
+ /**
336
+ * Advisory runtime measurement for one prospective model call.
337
+ *
338
+ * This deliberately uses the calibrated large-prompt policy and never decides
339
+ * whether transport may occur. Fusion's BUG-185 proved that a package-local
340
+ * estimator must not reject a live provider payload by subtracting hypothetical
341
+ * output. The delegate child uses this result for evidence and graceful
342
+ * finalization while proactive spilling controls package-owned growth.
343
+ */
344
+ export function evaluateDelegateRuntimeBudget(
345
+ measurement: DelegateRuntimeMeasurement,
346
+ allowedTokens: number,
347
+ route: { provider: string; model: string },
348
+ ): DelegateGovernorVerdict {
349
+ const family = resolveTokenBudgetFamily(route);
350
+ const estimate = estimateInputTokens({
351
+ family: family.family,
352
+ calibrationBacked: family.backed,
353
+ familyResolution: family.resolution,
354
+ allowedInputTokens: allowedTokens,
355
+ scope: 'delegate_launch',
356
+ segments: [
357
+ {
358
+ kind: 'known_text',
359
+ bytes: measurement.retainedInputBytes,
360
+ multibyteBytes: measurement.retainedInputMultibyteBytes,
361
+ denseBytes: measurement.retainedInputDenseBytes,
362
+ },
363
+ ],
364
+ });
365
+ return {
366
+ withinBudget: estimate.tokens <= allowedTokens,
367
+ measuredTokens: estimate.tokens,
368
+ allowedTokens,
369
+ overageTokens: Math.max(0, estimate.tokens - allowedTokens),
370
+ byteClassBreakdown: estimate.byte_class_breakdown,
371
+ dominantByteClass: estimate.rateSource.dominant_byte_class,
372
+ backed: estimate.rateSource.backed,
373
+ rateSource: estimate.rateSource,
374
+ };
375
+ }
376
+
377
+ export interface DelegateLimitOverrides {
378
+ maxTurns?: number | undefined;
379
+ maxToolCalls?: number | undefined;
380
+ timeoutSeconds?: number | undefined;
381
+ }
382
+
383
+ function positiveInteger(value: number | undefined, fallback: number, label: string): number {
384
+ if (value === undefined) return fallback;
385
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
386
+ throw new DelegateError(`bg_delegate ${label} must be a positive integer`, {
387
+ code: 'invalid_arguments',
388
+ childCreated: false,
389
+ });
390
+ }
391
+ return value;
392
+ }
393
+
394
+ export function resolveDelegateLimits(
395
+ route: DelegatePinnedRoute,
396
+ overrides: DelegateLimitOverrides = {},
397
+ ): DelegateLimits {
398
+ return {
399
+ max_turns: positiveInteger(overrides.maxTurns, DELEGATE_DEFAULT_MAX_TURNS, 'maxTurns'),
400
+ max_tool_calls: positiveInteger(
401
+ overrides.maxToolCalls,
402
+ DELEGATE_DEFAULT_MAX_TOOL_CALLS,
403
+ 'maxToolCalls',
404
+ ),
405
+ timeout_seconds: positiveInteger(
406
+ overrides.timeoutSeconds,
407
+ DELEGATE_DEFAULT_TIMEOUT_SECONDS,
408
+ 'timeoutSeconds',
409
+ ),
410
+ max_tool_result_bytes: DELEGATE_MAX_TOOL_RESULT_BYTES,
411
+ max_total_tool_output_bytes: DELEGATE_MAX_TOTAL_TOOL_OUTPUT_BYTES,
412
+ max_answer_bytes: DELEGATE_MAX_ANSWER_BYTES,
413
+ allowed_input_tokens: delegateAllowedInputTokens(route),
414
+ };
415
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "schema_version": "pi-background-tasks.delegate-hook-contract.v1",
3
+ "contract_id": "context-measure-abort-v1+tool-result-spill-v1",
4
+ "guarantees": {
5
+ "context_fires_before_every_model_call": true,
6
+ "context_result_messages_reach_provider": true,
7
+ "context_abort_blocks_provider_call": true,
8
+ "context_abort_skips_stream_invocation": false,
9
+ "context_abort_terminates_run": true,
10
+ "context_throw_blocks_provider_call": false,
11
+ "context_throw_isolated_to_throwing_handler": true,
12
+ "tool_result_fires_before_transcript_entry": true,
13
+ "tool_result_replacement_reaches_provider": true,
14
+ "tool_result_replacement_preserves_identity": true,
15
+ "tool_result_chains_in_load_order": true,
16
+ "handlers_run_in_extension_load_order": true
17
+ }
18
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Pi extension-hook contract required by the `bg_delegate` child-side guard.
3
+ *
4
+ * The guard installed inside a delegate child depends on runtime behaviour of
5
+ * Pi's `context` and `tool_result` hooks. That behaviour is proven by the
6
+ * `tests/scripted-provider/pi-hook-contract.test.ts` characterisation gate,
7
+ * which drives a real Pi agent loop and writes the observed guarantees to
8
+ * `tests/scripted-provider/pi-hook-contract-evidence.json`.
9
+ *
10
+ * Nothing here infers behaviour from type declarations. A guarantee is either
11
+ * observed by that gate or the delegate launch refuses to spawn a child.
12
+ */
13
+
14
+ export const DELEGATE_HOOK_CONTRACT_SCHEMA_VERSION =
15
+ 'pi-background-tasks.delegate-hook-contract.v1' as const;
16
+
17
+ /**
18
+ * Identifier for the exact guard mechanism the child installs.
19
+ *
20
+ * `context-measure-abort-v1`: measure the outgoing message set inside the
21
+ * `context` hook and, when it would exceed the pinned route window, call
22
+ * `ctx.abort()` so the request is never issued, then report a typed failure over
23
+ * the child result channel.
24
+ *
25
+ * Empirically established across the supported Pi lines by the characterisation
26
+ * and exact-version compatibility gates: Pi 0.81.1-0.83.0 invoke the provider
27
+ * entry point with an already-aborted `AbortSignal`, while Pi 0.84.0 propagates
28
+ * that signal through auth resolution and skips the entry point. No network
29
+ * request is issued in either mode, and the run terminates. Throwing from a
30
+ * `context` handler is NOT a barrier at all: Pi catches the exception, reports
31
+ * it as an extension error, and continues dispatch. The guard therefore uses
32
+ * abort, never a throw, and additionally suppresses the oversized content itself
33
+ * so a non-conforming provider cannot transmit it.
34
+ *
35
+ * `tool-result-spill-v1`: replace an oversized `tool_result` payload with an
36
+ * explicit hash-accounted receipt before it enters the transcript.
37
+ */
38
+ export const DELEGATE_HOOK_CONTRACT_ID = 'context-measure-abort-v1+tool-result-spill-v1' as const;
39
+
40
+ export const DELEGATE_HOOK_GUARANTEE_NAMES = [
41
+ 'context_fires_before_every_model_call',
42
+ 'context_result_messages_reach_provider',
43
+ 'context_abort_blocks_provider_call',
44
+ 'context_abort_skips_stream_invocation',
45
+ 'context_abort_terminates_run',
46
+ 'context_throw_blocks_provider_call',
47
+ 'context_throw_isolated_to_throwing_handler',
48
+ 'tool_result_fires_before_transcript_entry',
49
+ 'tool_result_replacement_reaches_provider',
50
+ 'tool_result_replacement_preserves_identity',
51
+ 'tool_result_chains_in_load_order',
52
+ 'handlers_run_in_extension_load_order',
53
+ ] as const;
54
+
55
+ export type DelegateHookGuaranteeName = (typeof DELEGATE_HOOK_GUARANTEE_NAMES)[number];
56
+
57
+ export type DelegateHookGuarantees = Readonly<Record<DelegateHookGuaranteeName, boolean>>;
58
+
59
+ export interface DelegateHookContractEvidence {
60
+ schema_version: typeof DELEGATE_HOOK_CONTRACT_SCHEMA_VERSION;
61
+ contract_id: typeof DELEGATE_HOOK_CONTRACT_ID;
62
+ guarantees: DelegateHookGuarantees;
63
+ }
64
+
65
+ /**
66
+ * Guarantees the child guard actually depends on.
67
+ *
68
+ * `context_throw_blocks_provider_call` and
69
+ * `context_abort_skips_stream_invocation` are deliberately absent. Throws do
70
+ * not block dispatch, and skipping the stream entry point is not shared by every
71
+ * supported Pi line. The guard needs neither behavior: it aborts the run AND
72
+ * removes the oversized content from the outgoing message set, so the request
73
+ * cannot be issued and could not carry the content even if it were.
74
+ */
75
+ export const DELEGATE_REQUIRED_HOOK_GUARANTEES: readonly DelegateHookGuaranteeName[] = [
76
+ 'context_fires_before_every_model_call',
77
+ 'context_result_messages_reach_provider',
78
+ 'context_abort_blocks_provider_call',
79
+ 'context_abort_terminates_run',
80
+ 'context_throw_isolated_to_throwing_handler',
81
+ 'tool_result_fires_before_transcript_entry',
82
+ 'tool_result_replacement_reaches_provider',
83
+ 'tool_result_replacement_preserves_identity',
84
+ 'tool_result_chains_in_load_order',
85
+ 'handlers_run_in_extension_load_order',
86
+ ];
87
+
88
+ export interface DelegateHookContractVerdict {
89
+ supported: boolean;
90
+ missing: readonly DelegateHookGuaranteeName[];
91
+ }
92
+
93
+ export function evaluateDelegateHookContract(
94
+ evidence: DelegateHookContractEvidence,
95
+ ): DelegateHookContractVerdict {
96
+ const missing = DELEGATE_REQUIRED_HOOK_GUARANTEES.filter(
97
+ (guarantee) => evidence.guarantees[guarantee] !== true,
98
+ );
99
+ return { supported: missing.length === 0, missing };
100
+ }
101
+
102
+ function isRecord(value: unknown): value is Record<PropertyKey, unknown> {
103
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ /** Strict parse. A malformed or partial evidence file is a loud failure, never a default-allow. */
107
+ export function parseDelegateHookContractEvidence(value: unknown): DelegateHookContractEvidence {
108
+ if (!isRecord(value)) throw new Error('delegate hook-contract evidence must be an object');
109
+ if (value['schema_version'] !== DELEGATE_HOOK_CONTRACT_SCHEMA_VERSION) {
110
+ throw new Error(
111
+ `delegate hook-contract evidence schema_version must be ${DELEGATE_HOOK_CONTRACT_SCHEMA_VERSION}`,
112
+ );
113
+ }
114
+ if (value['contract_id'] !== DELEGATE_HOOK_CONTRACT_ID) {
115
+ throw new Error(
116
+ `delegate hook-contract evidence contract_id must be ${DELEGATE_HOOK_CONTRACT_ID}`,
117
+ );
118
+ }
119
+ const raw = value['guarantees'];
120
+ if (!isRecord(raw)) throw new Error('delegate hook-contract evidence guarantees must be an object');
121
+ const keys = Object.keys(raw).sort();
122
+ const expected = [...DELEGATE_HOOK_GUARANTEE_NAMES].sort();
123
+ if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
124
+ throw new Error(
125
+ `delegate hook-contract evidence guarantees keys mismatch: expected ${expected.join(', ')}`,
126
+ );
127
+ }
128
+ const flag = (name: DelegateHookGuaranteeName): boolean => {
129
+ const observed = raw[name];
130
+ if (typeof observed !== 'boolean') {
131
+ throw new Error(`delegate hook-contract evidence guarantee ${name} must be a boolean`);
132
+ }
133
+ return observed;
134
+ };
135
+ const guarantees: DelegateHookGuarantees = {
136
+ context_fires_before_every_model_call: flag('context_fires_before_every_model_call'),
137
+ context_result_messages_reach_provider: flag('context_result_messages_reach_provider'),
138
+ context_abort_blocks_provider_call: flag('context_abort_blocks_provider_call'),
139
+ context_abort_skips_stream_invocation: flag('context_abort_skips_stream_invocation'),
140
+ context_abort_terminates_run: flag('context_abort_terminates_run'),
141
+ context_throw_blocks_provider_call: flag('context_throw_blocks_provider_call'),
142
+ context_throw_isolated_to_throwing_handler: flag('context_throw_isolated_to_throwing_handler'),
143
+ tool_result_fires_before_transcript_entry: flag('tool_result_fires_before_transcript_entry'),
144
+ tool_result_replacement_reaches_provider: flag('tool_result_replacement_reaches_provider'),
145
+ tool_result_replacement_preserves_identity: flag('tool_result_replacement_preserves_identity'),
146
+ tool_result_chains_in_load_order: flag('tool_result_chains_in_load_order'),
147
+ handlers_run_in_extension_load_order: flag('handlers_run_in_extension_load_order'),
148
+ };
149
+ return {
150
+ schema_version: DELEGATE_HOOK_CONTRACT_SCHEMA_VERSION,
151
+ contract_id: DELEGATE_HOOK_CONTRACT_ID,
152
+ guarantees,
153
+ };
154
+ }