pi-background-tasks 2.3.0 → 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.
@@ -4,7 +4,6 @@ import {
4
4
  TOKEN_BUDGET_RATE_SCALE,
5
5
  estimateInputTokens,
6
6
  knownTextSegment,
7
- maxKnownTextBytesForTokens,
8
7
  resolveTokenBudgetFamily,
9
8
  utf8ByteClassBreakdown,
10
9
  allowedInputTokens,
@@ -28,11 +27,15 @@ import {
28
27
  * A delegate child is a multi-turn, tool-using agent, so its budget has two
29
28
  * distinct phases rather than Fusion's single-shot stage forecast:
30
29
  *
31
- * 1. Launch admission checks the frozen seed, framing, and child system prompt.
32
- * 2. The runtime governor checks the complete retained input before each call.
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.
33
36
  *
34
- * Nothing here clamps, downgrades, or silently reduces. An input that does not
35
- * fit is a typed refusal.
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.
36
39
  */
37
40
 
38
41
  /** Output tokens reserved so the child can always finish an answer. */
@@ -54,10 +57,14 @@ export const DELEGATE_DEFAULT_TIMEOUT_SECONDS = 1200;
54
57
  export const DELEGATE_MAX_TOOL_RESULT_BYTES = 64 * 1024;
55
58
  export const DELEGATE_MAX_TOTAL_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024;
56
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;
57
64
  /** Answers at or under this serialize inline; larger ones degrade explicitly. */
58
65
  export const DELEGATE_INLINE_ANSWER_BYTES = 48 * 1024;
59
66
 
60
- export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v2';
67
+ export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v3';
61
68
 
62
69
  export interface DelegateBudgetPolicyDescriptor {
63
70
  id: typeof DELEGATE_BUDGET_POLICY_ID;
@@ -68,7 +75,11 @@ export interface DelegateBudgetPolicyDescriptor {
68
75
  safety_reserve_tokens: number;
69
76
  min_usable_input_tokens: number;
70
77
  inline_answer_bytes: number;
71
- estimator_scope: 'delegate';
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';
72
83
  }
73
84
 
74
85
  export const DELEGATE_BUDGET_POLICY: DelegateBudgetPolicyDescriptor = {
@@ -80,7 +91,11 @@ export const DELEGATE_BUDGET_POLICY: DelegateBudgetPolicyDescriptor = {
80
91
  safety_reserve_tokens: DELEGATE_SAFETY_RESERVE_TOKENS,
81
92
  min_usable_input_tokens: DELEGATE_MIN_USABLE_INPUT_TOKENS,
82
93
  inline_answer_bytes: DELEGATE_INLINE_ANSWER_BYTES,
83
- estimator_scope: 'delegate',
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',
84
99
  };
85
100
 
86
101
  export interface DelegateAdmissionPlanV1 {
@@ -97,17 +112,22 @@ export interface DelegateAdmissionPlanV1 {
97
112
  rate_source: TokenBudgetRateSource;
98
113
  byte_capacity_utf8_bytes: number;
99
114
  };
100
- seed_utf8_bytes: number;
101
- seed_multibyte_utf8_bytes: number;
115
+ child_prompt_utf8_bytes: number;
116
+ child_prompt_multibyte_utf8_bytes: number;
102
117
  system_prompt_utf8_bytes: number;
103
118
  system_prompt_multibyte_utf8_bytes: number;
104
119
  launch_utf8_bytes: number;
105
120
  launch_input_tokens_upper_bound: number;
121
+ conservative_launch_input_tokens: number;
122
+ conservative_launch_fits: boolean;
106
123
  signed_headroom_tokens: number;
107
124
  utilization_basis_points: number;
125
+ retained_growth_budget_tokens: number;
126
+ finalization_input_reserve_tokens: number;
108
127
  byte_class_breakdown: TokenBudgetByteClassBreakdown;
109
128
  dominant_byte_class: EstimateInputTokensResult['rateSource']['dominant_byte_class'];
110
129
  estimate: EstimateInputTokensResult;
130
+ conservative_estimate: EstimateInputTokensResult;
111
131
  fits: boolean;
112
132
  limits: DelegateLimits;
113
133
  }
@@ -167,7 +187,7 @@ export function delegateAllowedInputTokens(route: DelegatePinnedRoute): number {
167
187
 
168
188
  export interface DelegateAdmissionInput {
169
189
  route: DelegatePinnedRoute;
170
- seedSerialized: string;
190
+ childPrompt: string;
171
191
  childSystemPrompt: string;
172
192
  limits: DelegateLimits;
173
193
  }
@@ -176,20 +196,39 @@ export interface DelegateAdmissionInput {
176
196
  export function planDelegateAdmission(input: DelegateAdmissionInput): DelegateAdmissionPlanV1 {
177
197
  const allowed = delegateAllowedInputTokens(input.route);
178
198
  const family = routeFamily(input.route);
179
- const seed = utf8ByteClassBreakdown(input.seedSerialized);
199
+ const childPrompt = utf8ByteClassBreakdown(input.childPrompt);
180
200
  const system = utf8ByteClassBreakdown(input.childSystemPrompt);
201
+ const segments = [knownTextSegment(input.childPrompt), knownTextSegment(input.childSystemPrompt)];
181
202
  const estimate = estimateInputTokens({
182
203
  family: family.family,
183
204
  calibrationBacked: family.backed,
184
205
  familyResolution: family.resolution,
185
206
  allowedInputTokens: allowed,
186
- scope: 'delegate',
187
- segments: [knownTextSegment(input.seedSerialized), knownTextSegment(input.childSystemPrompt)],
207
+ scope: 'delegate_launch',
208
+ segments,
188
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;
189
224
  const byteCapacity = Math.floor(
190
225
  (allowed * estimate.rateSource.effective_rate_bytes_per_token_x100) / TOKEN_BUDGET_RATE_SCALE,
191
226
  );
192
- const launchBytes = seed.bytes + system.bytes;
227
+ const launchBytes = childPrompt.bytes + system.bytes;
228
+ const retainedGrowthBudget = Math.max(
229
+ 0,
230
+ allowed - estimate.tokens - DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
231
+ );
193
232
  return {
194
233
  schema_version: DELEGATE_BUDGET_PLAN_SCHEMA_VERSION,
195
234
  policy: DELEGATE_BUDGET_POLICY,
@@ -204,17 +243,22 @@ export function planDelegateAdmission(input: DelegateAdmissionInput): DelegateAd
204
243
  rate_source: estimate.rateSource,
205
244
  byte_capacity_utf8_bytes: byteCapacity,
206
245
  },
207
- seed_utf8_bytes: seed.bytes,
208
- seed_multibyte_utf8_bytes: seed.multibyteBytes,
246
+ child_prompt_utf8_bytes: childPrompt.bytes,
247
+ child_prompt_multibyte_utf8_bytes: childPrompt.multibyteBytes,
209
248
  system_prompt_utf8_bytes: system.bytes,
210
249
  system_prompt_multibyte_utf8_bytes: system.multibyteBytes,
211
250
  launch_utf8_bytes: launchBytes,
212
251
  launch_input_tokens_upper_bound: estimate.tokens,
252
+ conservative_launch_input_tokens: provableConservativeLaunchTokens,
253
+ conservative_launch_fits: provableConservativeLaunchTokens <= allowed,
213
254
  signed_headroom_tokens: allowed - estimate.tokens,
214
255
  utilization_basis_points: utilizationBasisPoints(estimate.tokens, allowed),
256
+ retained_growth_budget_tokens: retainedGrowthBudget,
257
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
215
258
  byte_class_breakdown: estimate.byte_class_breakdown,
216
259
  dominant_byte_class: estimate.rateSource.dominant_byte_class,
217
260
  estimate,
261
+ conservative_estimate: conservativeEstimate,
218
262
  fits: estimate.tokens <= allowed,
219
263
  limits: input.limits,
220
264
  };
@@ -226,17 +270,15 @@ function rateWarningText(rateSource: TokenBudgetRateSource, qualifiedId: string)
226
270
  }
227
271
 
228
272
  function requiredByteReduction(plan: DelegateAdmissionPlanV1): number {
229
- return Math.max(
230
- 0,
231
- plan.launch_utf8_bytes -
232
- maxKnownTextBytesForTokens({
233
- family: plan.route.family,
234
- calibrationBacked: plan.route.rate_source.backed,
235
- familyResolution: plan.route.rate_source.model_resolution,
236
- allowedInputTokens: plan.route.allowed_input_tokens,
237
- scope: 'delegate',
238
- }),
239
- );
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);
240
282
  }
241
283
 
242
284
  /**
@@ -249,7 +291,7 @@ export function assertDelegateAdmission(plan: DelegateAdmissionPlanV1): void {
249
291
  if (plan.fits) return;
250
292
  const overage = plan.launch_input_tokens_upper_bound - plan.route.allowed_input_tokens;
251
293
  throw new DelegateError(
252
- `bg_delegate seed does not fit the pinned route before launch. Route ${plan.route.qualified_id} allows ${String(plan.route.allowed_input_tokens)} input tokens; the frozen seed plus the 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.`,
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.`,
253
295
  {
254
296
  code: 'seed_budget_exceeded',
255
297
  childCreated: false,
@@ -291,10 +333,13 @@ export interface DelegateGovernorVerdict {
291
333
  }
292
334
 
293
335
  /**
294
- * Runtime governor decision for one prospective model call.
336
+ * Advisory runtime measurement for one prospective model call.
295
337
  *
296
- * Pure and total, so the child guard can call it from inside a hook with no
297
- * possibility of throwing where a throw would be swallowed.
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.
298
343
  */
299
344
  export function evaluateDelegateRuntimeBudget(
300
345
  measurement: DelegateRuntimeMeasurement,
@@ -307,7 +352,7 @@ export function evaluateDelegateRuntimeBudget(
307
352
  calibrationBacked: family.backed,
308
353
  familyResolution: family.resolution,
309
354
  allowedInputTokens: allowedTokens,
310
- scope: 'delegate',
355
+ scope: 'delegate_launch',
311
356
  segments: [
312
357
  {
313
358
  kind: 'known_text',
@@ -380,7 +380,9 @@ export function buildDelegateChildSystemPrompt(seedPathHint: string): string {
380
380
  '',
381
381
  'You are inspect-only. You can read, search, and list files. You cannot run shell commands, edit or write files, reach the network, or start further delegates. Do not claim to have done so.',
382
382
  '',
383
- 'If a tool result is replaced by a spill receipt, the complete bytes are on disk and nothing was truncated. Use delegate_read_artifact with an exact offset and length when you genuinely need them.',
383
+ 'If a tool result is replaced by a spill receipt, the complete encoded content is on disk and nothing was truncated. Use delegate_read_artifact with an exact offset and length when you genuinely need lossless base64 bytes, then interpret them using the receipt content_format.',
384
+ '',
385
+ 'The child controls retained context by spilling tool results before they consume protected final-answer runway. A spill is not a failure. If a finalization-runway notice appears, all investigation tools are finished: stop investigating and answer immediately from the evidence already gathered.',
384
386
  '',
385
387
  'Finish with a single, direct, self-contained answer to the directive. Your final assistant message is the answer that will be returned to the parent.',
386
388
  ].join('\n');
@@ -468,7 +470,7 @@ export function preflightDelegateLaunch(input: DelegatePreflightInput): Delegate
468
470
  route: input.route,
469
471
  // The seed reaches the child inside its prompt, so the admission forecast
470
472
  // must measure the prompt that is actually sent, not the seed alone.
471
- seedSerialized: childPrompt,
473
+ childPrompt,
472
474
  childSystemPrompt,
473
475
  limits,
474
476
  });
@@ -247,6 +247,21 @@ function parseAttestations(
247
247
  });
248
248
  }
249
249
 
250
+ function parseSpillContentFormat(
251
+ value: unknown,
252
+ taskId: string,
253
+ ): DelegateSpillReceipt['content_format'] {
254
+ if (value === undefined) return undefined;
255
+ if (
256
+ value !== 'single_text_utf8' &&
257
+ value !== 'tool_result_content_json_v1' &&
258
+ value !== 'opaque_bytes'
259
+ ) {
260
+ fail('delegate spill receipt content_format is invalid', 'child_result_invalid', taskId);
261
+ }
262
+ return value;
263
+ }
264
+
250
265
  function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSpillReceipt[] {
251
266
  if (!Array.isArray(value))
252
267
  fail('delegate result package spilled_artifacts must be an array', 'child_result_invalid', taskId);
@@ -264,6 +279,7 @@ function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSp
264
279
  source_call_index: requireInteger(entry, 'source_call_index', taskId),
265
280
  byte_length: requireInteger(entry, 'byte_length', taskId),
266
281
  sha256: requireSha256(entry, 'sha256', taskId),
282
+ content_format: parseSpillContentFormat(entry['content_format'], taskId),
267
283
  };
268
284
  });
269
285
  }
@@ -160,6 +160,7 @@ export async function prepareDelegateLaunch(
160
160
  budget: {
161
161
  family: preflight.plan.route.family,
162
162
  rate_source: preflight.plan.route.rate_source,
163
+ conservative_rate_source: preflight.plan.conservative_estimate.rateSource,
163
164
  },
164
165
  extensionMode: input.extensionMode,
165
166
  autoDeliver: input.autoDeliver,
@@ -200,6 +201,9 @@ export interface EvaluateDelegateTerminalInput {
200
201
  /** Terminal status observed by the background task registry. */
201
202
  taskStatus: 'completed' | 'failed' | 'killed';
202
203
  taskError: string | undefined;
204
+ /** Real merged child output owned by the background-task registry. */
205
+ taskOutputPath?: string | undefined;
206
+ taskOutputAbsPath?: string | undefined;
203
207
  }
204
208
 
205
209
  /**
@@ -251,6 +255,21 @@ async function adjudicateDelegateTerminal(
251
255
  recorded?.message ??
252
256
  input.taskError ??
253
257
  'the delegate child exited without committing a result package';
258
+ const preserved = ['seed.json', 'budget-plan.json', 'child-terminal.json', 'runtime-budget.json']
259
+ .filter((name) => existsSync(join(input.artifactDirAbs, name)));
260
+ if (
261
+ input.taskOutputPath !== undefined &&
262
+ input.taskOutputAbsPath !== undefined &&
263
+ existsSync(input.taskOutputAbsPath)
264
+ ) {
265
+ preserved.push(input.taskOutputPath);
266
+ }
267
+ const diagnosticTargets = preserved.filter(
268
+ (name) => name === 'child-terminal.json' || name === 'runtime-budget.json' || name === input.taskOutputPath,
269
+ );
270
+ const diagnostic = diagnosticTargets.length === 0
271
+ ? 'No child terminal record or merged task output exists; inspect the preserved launch artifacts listed above.'
272
+ : `Inspect the preserved diagnostic evidence: ${diagnosticTargets.join(', ')}.`;
254
273
  const error = new DelegateError(
255
274
  `bg_delegate produced no committed answer: ${detail}`,
256
275
  {
@@ -258,9 +277,9 @@ async function adjudicateDelegateTerminal(
258
277
  childCreated: true,
259
278
  taskId: input.taskId,
260
279
  artifactDir: input.artifactDirAbs,
261
- preserved: ['seed.json', 'budget-plan.json', 'child.stdout.txt', 'child.stderr.txt'],
280
+ preserved,
262
281
  remediation: [
263
- 'Inspect child.stderr.txt and child-terminal.json in the artifact directory.',
282
+ diagnostic,
264
283
  'No partial answer is returned; nothing was truncated to look like success.',
265
284
  ],
266
285
  },
@@ -18,7 +18,7 @@ export const DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION =
18
18
  'pi-background-tasks.delegate-result.v1' as const;
19
19
  export const DELEGATE_RECEIPT_SCHEMA_VERSION = 'pi-background-tasks.delegate-receipt.v1' as const;
20
20
  export const DELEGATE_BUDGET_PLAN_SCHEMA_VERSION =
21
- 'pi-background-tasks.delegate-budget-plan.v2' as const;
21
+ 'pi-background-tasks.delegate-budget-plan.v3' as const;
22
22
  export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.v2' as const;
23
23
 
24
24
  /**
@@ -64,6 +64,7 @@ export interface DelegatePinnedRoute extends DelegateRoute {
64
64
  export interface DelegateBudgetRouteSource {
65
65
  family: TokenBudgetFamily;
66
66
  rate_source: TokenBudgetRateSource;
67
+ conservative_rate_source?: TokenBudgetRateSource | undefined;
67
68
  }
68
69
 
69
70
  export interface DelegateContextPolicyDescriptor {
@@ -192,6 +193,11 @@ export interface DelegateResultPackageV1 {
192
193
  spilled_artifacts: readonly DelegateSpillReceipt[];
193
194
  }
194
195
 
196
+ export type DelegateSpillContentFormat =
197
+ | 'single_text_utf8'
198
+ | 'tool_result_content_json_v1'
199
+ | 'opaque_bytes';
200
+
195
201
  export interface DelegateSpillReceipt {
196
202
  schema_version: typeof DELEGATE_RECEIPT_SCHEMA_VERSION;
197
203
  artifact: string;
@@ -201,6 +207,11 @@ export interface DelegateSpillReceipt {
201
207
  source_call_index: number;
202
208
  byte_length: number;
203
209
  sha256: string;
210
+ /**
211
+ * Encoding of the hashed artifact bytes. Optional only for compatibility
212
+ * with v1 receipts written before content formats were recorded.
213
+ */
214
+ content_format?: DelegateSpillContentFormat | undefined;
204
215
  }
205
216
 
206
217
  export const DELEGATE_ERROR_CODES = [