pi-background-tasks 0.7.4 → 0.7.7
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/PUBLISHING.md +7 -7
- package/README.md +199 -15
- package/TESTING.md +95 -1
- package/TEST_PLAN.md +21 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +4 -3
- package/src/core/common.ts +41 -0
- package/src/core/context/parent-snapshot.ts +142 -0
- package/src/core/context/token-budget.ts +890 -0
- package/src/core/context/visible-conversation-v2.ts +551 -0
- package/src/core/delegate/artifacts.ts +479 -0
- package/src/core/delegate/budget.ts +370 -0
- package/src/core/delegate/hook-contract-evidence.json +18 -0
- package/src/core/delegate/hook-contract.ts +153 -0
- package/src/core/delegate/launch.ts +459 -0
- package/src/core/delegate/result-package.ts +443 -0
- package/src/core/delegate/runner.ts +406 -0
- package/src/core/delegate/seed.ts +411 -0
- package/src/core/delegate/types.ts +304 -0
- package/src/core/fusion/artifacts.ts +17 -2
- package/src/core/fusion/budget.ts +884 -210
- package/src/core/fusion/context.ts +118 -520
- package/src/core/fusion/orchestrator.ts +133 -20
- package/src/core/fusion/prompts.ts +11 -7
- package/src/core/fusion/types.ts +230 -86
- package/src/core/registry.ts +174 -0
- package/src/delegate-child-extension.ts +673 -0
- package/src/delegate-extension.ts +587 -0
- package/src/extension.ts +10 -0
- package/src/fusion-extension.ts +4 -0
package/src/core/fusion/types.ts
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
2
|
+
import type {
|
|
3
|
+
EstimateInputTokensResult,
|
|
4
|
+
TokenBudgetByteClassBreakdown,
|
|
5
|
+
TokenBudgetDominantByteClass,
|
|
6
|
+
TokenBudgetFamily,
|
|
7
|
+
TokenBudgetFamilyCalibration,
|
|
8
|
+
TokenBudgetRateSource,
|
|
9
|
+
} from '../context/token-budget.js';
|
|
2
10
|
|
|
3
11
|
export type FusionThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
4
12
|
|
|
5
13
|
export const FUSION_MODEL_CONFIG_SCHEMA_VERSION = 'pi-background-tasks.fusion-models.v1';
|
|
6
|
-
export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.
|
|
14
|
+
export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v4';
|
|
7
15
|
export const FUSION_EVALUATION_SCHEMA_VERSION = 'pi-background-tasks.fusion-evaluation.v1';
|
|
8
|
-
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.
|
|
16
|
+
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v3';
|
|
9
17
|
export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v2';
|
|
10
|
-
export const FUSION_CONTEXT_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.fusion-context-ledger.
|
|
11
|
-
export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-budget-plan.
|
|
18
|
+
export const FUSION_CONTEXT_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.fusion-context-ledger.v2';
|
|
19
|
+
export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-budget-plan.v3';
|
|
20
|
+
export const FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION =
|
|
21
|
+
'pi-background-tasks.fusion-calibration-violation.v1';
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
24
|
* Conversation-projection transform shared by every Fusion entry point.
|
|
@@ -18,12 +28,12 @@ export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-bud
|
|
|
18
28
|
* hash-accounted omission receipts. It never truncates retained text and never
|
|
19
29
|
* forwards raw image bytes.
|
|
20
30
|
*/
|
|
21
|
-
export const FUSION_CONTEXT_TRANSFORM_ID = 'visible-conversation-ledger-
|
|
31
|
+
export const FUSION_CONTEXT_TRANSFORM_ID = 'visible-conversation-ledger-v2';
|
|
22
32
|
export const FUSION_BRANCH_FILTER_ID = 'exclude-active-fusion-subtree-v1';
|
|
23
33
|
|
|
24
34
|
/** Entry-point specific context policies. Both use the same payload-exclusion transform. */
|
|
25
|
-
export const FUSION_TOOL_CONTEXT_POLICY_ID = 'fusion-tool-explicit-
|
|
26
|
-
export const FUSION_COMMAND_CONTEXT_POLICY_ID = 'fusion-command-conversation-
|
|
35
|
+
export const FUSION_TOOL_CONTEXT_POLICY_ID = 'fusion-tool-explicit-v2';
|
|
36
|
+
export const FUSION_COMMAND_CONTEXT_POLICY_ID = 'fusion-command-conversation-v2';
|
|
27
37
|
|
|
28
38
|
export const FUSION_IMAGE_OMISSION_PREFIX = '[Image omitted from fusion text transcript: ';
|
|
29
39
|
|
|
@@ -112,7 +122,7 @@ export interface ResolvedFusionModels {
|
|
|
112
122
|
|
|
113
123
|
export type FusionRequestAuthority = 'explicit_text' | 'directive_over_projected_conversation';
|
|
114
124
|
|
|
115
|
-
export interface
|
|
125
|
+
export interface FusionCanonicalRequestV3 {
|
|
116
126
|
/** Entry point that produced this request. */
|
|
117
127
|
source: FusionSource;
|
|
118
128
|
/** How children must weigh `text` against the projected conversation. */
|
|
@@ -144,59 +154,60 @@ export interface FusionOmittedEventRecord {
|
|
|
144
154
|
mime_type?: string;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
|
-
export interface
|
|
157
|
+
export interface FusionOmittedActivityProjectionMapEntry {
|
|
158
|
+
canonical_entry_index: number;
|
|
159
|
+
entry_kind: 'omitted_activity';
|
|
160
|
+
ledger_index_first: number;
|
|
161
|
+
ledger_index_last: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface FusionLedgerOnlyImageProjectionMapEntry {
|
|
165
|
+
entry_kind: 'ledger_only_tool_result_image';
|
|
166
|
+
ledger_index_first: number;
|
|
167
|
+
ledger_index_last: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export type FusionContextProjectionMapEntry =
|
|
171
|
+
| FusionOmittedActivityProjectionMapEntry
|
|
172
|
+
| FusionLedgerOnlyImageProjectionMapEntry;
|
|
173
|
+
|
|
174
|
+
export interface FusionContextOmissionLedgerV2 {
|
|
148
175
|
schema_version: typeof FUSION_CONTEXT_LEDGER_SCHEMA_VERSION;
|
|
149
176
|
policy_id: string;
|
|
150
177
|
transform: typeof FUSION_CONTEXT_TRANSFORM_ID;
|
|
151
178
|
entries: readonly FusionOmittedEventRecord[];
|
|
179
|
+
projection_map: readonly FusionContextProjectionMapEntry[];
|
|
152
180
|
root_sha256: string;
|
|
153
181
|
}
|
|
154
182
|
|
|
155
|
-
export
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
text: string
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Per-kind counts for one omitted run. Zero-valued kinds are omitted from the
|
|
165
|
-
* serialized receipt by a fixed policy rule so receipt size does not scale with
|
|
166
|
-
* the number of tracked kinds; absent means exactly zero.
|
|
167
|
-
*/
|
|
168
|
-
export interface FusionOmittedRunCounts {
|
|
169
|
-
assistant_thinking?: number;
|
|
170
|
-
tool_calls?: number;
|
|
171
|
-
tool_result_texts?: number;
|
|
172
|
-
tool_result_images?: number;
|
|
173
|
-
}
|
|
183
|
+
export type FusionProjectionTextEntry = [
|
|
184
|
+
tag: 't',
|
|
185
|
+
role: 'u' | 'a',
|
|
186
|
+
sourceOrdinal: number,
|
|
187
|
+
blockOrdinal: number,
|
|
188
|
+
text: string,
|
|
189
|
+
];
|
|
174
190
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
tool_result_image?: number;
|
|
181
|
-
}
|
|
191
|
+
export type FusionProjectionOmissionCounts = [
|
|
192
|
+
assistantThinking: number,
|
|
193
|
+
toolCalls: number,
|
|
194
|
+
toolResults: number,
|
|
195
|
+
];
|
|
182
196
|
|
|
183
|
-
export
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
counts: FusionOmittedRunCounts;
|
|
190
|
-
payload_bytes: FusionOmittedRunBytes;
|
|
191
|
-
ledger_run_sha256: string;
|
|
192
|
-
}
|
|
197
|
+
export type FusionProjectionOmissionEntry = [
|
|
198
|
+
tag: 'o',
|
|
199
|
+
sourceOrdinalSpan: [first: number, last: number],
|
|
200
|
+
bytes: number,
|
|
201
|
+
counts: FusionProjectionOmissionCounts,
|
|
202
|
+
];
|
|
193
203
|
|
|
194
204
|
export type FusionProjectionEntry = FusionProjectionTextEntry | FusionProjectionOmissionEntry;
|
|
195
205
|
|
|
196
206
|
export interface FusionContextPolicyDescriptor {
|
|
197
207
|
id: string;
|
|
198
208
|
transform: typeof FUSION_CONTEXT_TRANSFORM_ID;
|
|
199
|
-
version:
|
|
209
|
+
version: 2;
|
|
210
|
+
receipt_format: 'omitted_activity.v2';
|
|
200
211
|
user_text: 'verbatim';
|
|
201
212
|
assistant_text: 'verbatim';
|
|
202
213
|
assistant_thinking: 'ledger_only';
|
|
@@ -238,23 +249,28 @@ export interface FusionProjectionAccounting {
|
|
|
238
249
|
tool_call_names: readonly FusionToolCallNameCount[];
|
|
239
250
|
ledger_entry_count: number;
|
|
240
251
|
ledger_root_sha256: string;
|
|
252
|
+
omission_receipt_utf8_bytes: number;
|
|
241
253
|
}
|
|
242
254
|
|
|
243
|
-
export interface
|
|
255
|
+
export interface FusionConversationProjectionV4 {
|
|
244
256
|
policy: FusionContextPolicyDescriptor;
|
|
245
257
|
branch_filter: FusionBranchFilterDescriptor;
|
|
246
258
|
entries: readonly FusionProjectionEntry[];
|
|
247
259
|
accounting: FusionProjectionAccounting;
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
export
|
|
262
|
+
export type FusionConversationProjectionV3 = FusionConversationProjectionV4;
|
|
263
|
+
|
|
264
|
+
export interface FusionCanonicalInputV4 {
|
|
251
265
|
schema_version: typeof FUSION_INPUT_SCHEMA_VERSION;
|
|
252
266
|
cwd: string;
|
|
253
267
|
system_prompt: string;
|
|
254
|
-
request:
|
|
255
|
-
conversation_projection:
|
|
268
|
+
request: FusionCanonicalRequestV3;
|
|
269
|
+
conversation_projection: FusionConversationProjectionV4;
|
|
256
270
|
}
|
|
257
271
|
|
|
272
|
+
export type FusionCanonicalInputV3 = FusionCanonicalInputV4;
|
|
273
|
+
|
|
258
274
|
export interface CandidateAssessment {
|
|
259
275
|
candidate_id: FusionCandidateId;
|
|
260
276
|
summary: string;
|
|
@@ -348,6 +364,15 @@ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
|
348
364
|
target.cost.total += delta.cost.total;
|
|
349
365
|
}
|
|
350
366
|
|
|
367
|
+
export interface FusionResultBudgetDetails {
|
|
368
|
+
policy_id: string;
|
|
369
|
+
calibration_version: string;
|
|
370
|
+
route_table: readonly FusionRouteCapacity[];
|
|
371
|
+
rate_sources: readonly TokenBudgetRateSource[];
|
|
372
|
+
unknown_provider_warnings: readonly string[];
|
|
373
|
+
calibration_warnings: readonly FusionCalibrationViolation[];
|
|
374
|
+
}
|
|
375
|
+
|
|
351
376
|
export interface FusionResultDetails {
|
|
352
377
|
schema_version: typeof FUSION_RESULT_SCHEMA_VERSION;
|
|
353
378
|
run_id: string;
|
|
@@ -362,6 +387,7 @@ export interface FusionResultDetails {
|
|
|
362
387
|
};
|
|
363
388
|
evaluator_attempts: number;
|
|
364
389
|
usage: FusionUsage;
|
|
390
|
+
budget: FusionResultBudgetDetails;
|
|
365
391
|
}
|
|
366
392
|
|
|
367
393
|
export type FusionProgressEvent =
|
|
@@ -370,6 +396,8 @@ export type FusionProgressEvent =
|
|
|
370
396
|
| { type: 'candidate_completed'; slot: 1 | 2 | 3; completed: number; total: 3 }
|
|
371
397
|
| { type: 'evaluation_started'; attempt: 1 | 2; repair: boolean }
|
|
372
398
|
| { type: 'evaluation_retry'; errors: readonly string[] }
|
|
399
|
+
| { type: 'budget_warning'; warnings: readonly FusionBudgetWarning[]; error: string }
|
|
400
|
+
| { type: 'calibration_warning'; warning: FusionCalibrationViolation; artifact: string }
|
|
373
401
|
| { type: 'merge_started' }
|
|
374
402
|
| { type: 'completed'; runId: string; artifactDir: string }
|
|
375
403
|
| { type: 'failed'; runId: string; artifactDir: string; error: string }
|
|
@@ -381,7 +409,8 @@ export type FusionErrorCode =
|
|
|
381
409
|
| 'model_unavailable'
|
|
382
410
|
| 'context_capture_failed'
|
|
383
411
|
| 'context_policy_unsupported_block'
|
|
384
|
-
| '
|
|
412
|
+
| 'prompt_budget_exceeded_forecast'
|
|
413
|
+
| 'prompt_budget_exceeded_measured'
|
|
385
414
|
| 'model_capacity_unknown'
|
|
386
415
|
| 'child_spawn_failed'
|
|
387
416
|
| 'child_stdin_failed'
|
|
@@ -395,14 +424,53 @@ export type FusionErrorCode =
|
|
|
395
424
|
| 'state_transition_invalid'
|
|
396
425
|
| 'orchestration_failed';
|
|
397
426
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
427
|
+
export type FusionBudgetCheckKind = 'input_only_preflight' | 'rendered_prompt';
|
|
428
|
+
|
|
429
|
+
export interface FusionBudgetComponentBreakdown {
|
|
430
|
+
visible_text: { bytes: number; tokens: number };
|
|
431
|
+
omission_receipts: { bytes: number; tokens: number };
|
|
432
|
+
projection_metadata: { bytes: number; tokens: number };
|
|
433
|
+
request: { bytes: number; tokens: number };
|
|
434
|
+
static_stage_framing: { bytes: number; tokens: number };
|
|
435
|
+
upstream_output_contracts: { bytes: number; tokens: number };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export interface FusionBudgetDenseRegion {
|
|
439
|
+
offset: number;
|
|
440
|
+
len: number;
|
|
441
|
+
detector: 'not_implemented_step_6';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface FusionBudgetRouteTableEntry {
|
|
445
|
+
role: FusionRouteCapacity['role'];
|
|
446
|
+
qualified_id: string;
|
|
447
|
+
allowed_input_tokens: number;
|
|
448
|
+
family: TokenBudgetFamily;
|
|
449
|
+
effective_rate_bytes_per_token_x100: number;
|
|
450
|
+
byte_capacity_utf8_bytes: number;
|
|
451
|
+
backed: boolean;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export interface FusionBudgetCounterfactuals {
|
|
455
|
+
empty_request: FusionBudgetEmptyRequestVerdict;
|
|
456
|
+
without_reservation: {
|
|
457
|
+
forecast_input_tokens_upper_bound: number;
|
|
458
|
+
signed_headroom_tokens: number;
|
|
459
|
+
fits: boolean;
|
|
460
|
+
};
|
|
461
|
+
at_median_rate: {
|
|
462
|
+
forecast_input_tokens_upper_bound: number | null;
|
|
463
|
+
signed_headroom_tokens: number | null;
|
|
464
|
+
fits: boolean | null;
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Structured detail attached to a split prompt-budget failure. */
|
|
403
469
|
export interface FusionBudgetErrorDetail {
|
|
404
470
|
budget_stage: FusionBudgetStage;
|
|
405
|
-
|
|
471
|
+
slot?: 1 | 2 | 3;
|
|
472
|
+
measurement_kind: 'stage_forecast' | 'rendered_prompt';
|
|
473
|
+
check_kind: FusionBudgetCheckKind;
|
|
406
474
|
measured_utf8_bytes: number;
|
|
407
475
|
measured_input_tokens_upper_bound: number;
|
|
408
476
|
allowed_input_tokens: number;
|
|
@@ -412,8 +480,24 @@ export interface FusionBudgetErrorDetail {
|
|
|
412
480
|
qualified_id: string;
|
|
413
481
|
context_window_tokens: number;
|
|
414
482
|
};
|
|
483
|
+
rate_source: TokenBudgetRateSource;
|
|
484
|
+
backed: boolean;
|
|
485
|
+
dominant_byte_class: TokenBudgetDominantByteClass;
|
|
486
|
+
component_breakdown: FusionBudgetComponentBreakdown;
|
|
487
|
+
byte_class_breakdown: TokenBudgetByteClassBreakdown;
|
|
488
|
+
dense_regions: readonly FusionBudgetDenseRegion[];
|
|
489
|
+
bytes_over: number;
|
|
490
|
+
tokens_over: number;
|
|
491
|
+
required_allowed_tokens: number;
|
|
492
|
+
route_table: readonly FusionBudgetRouteTableEntry[];
|
|
493
|
+
counterfactuals: FusionBudgetCounterfactuals;
|
|
494
|
+
stage_upstream_actuals: readonly { stage: FusionStage; bytes: number }[];
|
|
495
|
+
policy_id: string;
|
|
496
|
+
calibration_version: string;
|
|
415
497
|
context_policy_id: string;
|
|
416
498
|
remediation: readonly string[];
|
|
499
|
+
blockers: readonly FusionBudgetBlocker[];
|
|
500
|
+
artifact_dir: string;
|
|
417
501
|
}
|
|
418
502
|
|
|
419
503
|
export interface FusionErrorDetails {
|
|
@@ -533,50 +617,110 @@ export interface FusionRouteCapacity {
|
|
|
533
617
|
framing_reserve_tokens: number;
|
|
534
618
|
safety_reserve_tokens: number;
|
|
535
619
|
allowed_input_tokens: number;
|
|
620
|
+
family: TokenBudgetFamily;
|
|
621
|
+
rate_source: TokenBudgetRateSource;
|
|
622
|
+
byte_capacity_utf8_bytes: number;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export interface FusionBudgetStageComposition {
|
|
626
|
+
visible_text_bytes: number;
|
|
627
|
+
omission_receipt_bytes: number;
|
|
628
|
+
projection_metadata_bytes: number;
|
|
629
|
+
request_bytes: number;
|
|
630
|
+
static_stage_framing_bytes: number;
|
|
631
|
+
upstream_output_contract_bytes: number;
|
|
536
632
|
}
|
|
537
633
|
|
|
538
634
|
export interface FusionStageBudgetPlanEntry {
|
|
539
635
|
budget_stage: FusionBudgetStage;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
636
|
+
slot?: 1 | 2 | 3;
|
|
637
|
+
route: FusionRouteCapacity;
|
|
638
|
+
conditional: boolean;
|
|
639
|
+
check_kind: 'input_only_preflight';
|
|
640
|
+
input_utf8_bytes: number;
|
|
641
|
+
upstream_output_contract_bytes: number;
|
|
642
|
+
forecast_utf8_bytes: number;
|
|
643
|
+
input_only_input_tokens_upper_bound: number;
|
|
644
|
+
forecast_input_tokens_upper_bound: number;
|
|
543
645
|
allowed_input_tokens: number;
|
|
544
|
-
|
|
545
|
-
|
|
646
|
+
input_only_signed_headroom_tokens: number;
|
|
647
|
+
signed_headroom_tokens: number;
|
|
648
|
+
input_only_utilization_basis_points: number;
|
|
649
|
+
utilization_basis_points: number;
|
|
650
|
+
input_only_estimate: EstimateInputTokensResult;
|
|
651
|
+
reservation_estimate: EstimateInputTokensResult;
|
|
652
|
+
fits: boolean;
|
|
653
|
+
reservation_fits: boolean;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export interface FusionBudgetBlocker extends FusionStageBudgetPlanEntry {
|
|
657
|
+
overage_tokens: number;
|
|
658
|
+
bytes_over: number;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export interface FusionBudgetWarning extends FusionStageBudgetPlanEntry {
|
|
662
|
+
warning_kind: 'input_utilization' | 'worst_case_reservation';
|
|
663
|
+
threshold_basis_points: number;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export interface FusionBudgetEmptyRequestVerdict {
|
|
667
|
+
request_utf8_bytes: number;
|
|
668
|
+
still_fails_with_empty_request: boolean;
|
|
669
|
+
shortening_request_can_help: boolean;
|
|
670
|
+
minimum_request_byte_reduction: number;
|
|
671
|
+
maximum_safe_request_utf8_bytes: number;
|
|
672
|
+
blockers_with_empty_request: readonly FusionBudgetBlocker[];
|
|
546
673
|
}
|
|
547
674
|
|
|
548
675
|
export interface FusionBudgetPlanV1 {
|
|
549
676
|
schema_version: typeof FUSION_BUDGET_PLAN_SCHEMA_VERSION;
|
|
550
677
|
policy: FusionBudgetPolicyDescriptor;
|
|
551
678
|
routes: readonly FusionRouteCapacity[];
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
679
|
+
stages: readonly FusionStageBudgetPlanEntry[];
|
|
680
|
+
blockers: readonly FusionBudgetBlocker[];
|
|
681
|
+
primary_blocker?: FusionBudgetBlocker;
|
|
682
|
+
primary_blocker_composition?: FusionBudgetStageComposition;
|
|
683
|
+
empty_request: FusionBudgetEmptyRequestVerdict;
|
|
684
|
+
warnings: readonly FusionBudgetWarning[];
|
|
555
685
|
}
|
|
556
686
|
|
|
557
|
-
/**
|
|
558
|
-
* Documented, versioned budget policy.
|
|
559
|
-
*
|
|
560
|
-
* `bytes_per_token_divisor` is a conservative lower bound on UTF-8 bytes per
|
|
561
|
-
* token: token upper bound = ceil(utf8Bytes / divisor). It is deliberately far
|
|
562
|
-
* below the smallest ratio measured across real Fusion prompts so dense
|
|
563
|
-
* non-ASCII input cannot silently exceed a route's window.
|
|
564
|
-
*
|
|
565
|
-
* `downstream_reserve_bytes` is withheld from the canonical input so the
|
|
566
|
-
* evaluator, evaluation-repair, and merger prompts provably have room for the
|
|
567
|
-
* child outputs they embed. It is derived from the enforced per-stage output
|
|
568
|
-
* byte contracts, so it is a guarantee rather than an estimate: a response over
|
|
569
|
-
* its contract fails loudly instead of being embedded. `downstream_reserve_tokens`
|
|
570
|
-
* converts it with the same `bytes_per_token_divisor` used to measure prompts,
|
|
571
|
-
* because reserving output tokens directly would understate the cost of
|
|
572
|
-
* re-embedding those bytes. Neither value is adjusted to fit a particular input.
|
|
573
|
-
*/
|
|
687
|
+
/** Documented, versioned budget policy. */
|
|
574
688
|
export interface FusionBudgetPolicyDescriptor {
|
|
575
|
-
id: 'fusion-budget-policy-
|
|
576
|
-
|
|
689
|
+
id: 'fusion-budget-policy-v3';
|
|
690
|
+
calibration_version: string;
|
|
691
|
+
calibration_table: Readonly<Record<TokenBudgetFamily, TokenBudgetFamilyCalibration>>;
|
|
577
692
|
reserved_output_tokens: number;
|
|
578
693
|
framing_reserve_tokens: number;
|
|
579
694
|
safety_reserve_tokens: number;
|
|
580
|
-
|
|
581
|
-
|
|
695
|
+
candidate_output_contract_bytes: number;
|
|
696
|
+
evaluation_output_contract_bytes: number;
|
|
697
|
+
merge_output_contract_bytes: number;
|
|
698
|
+
diagnostics_contract_bytes: number;
|
|
699
|
+
utilization_warning_threshold_basis_points: 8000;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
export interface FusionCalibrationViolation {
|
|
703
|
+
schema_version: typeof FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION;
|
|
704
|
+
stage: FusionStage;
|
|
705
|
+
slot?: 1 | 2 | 3;
|
|
706
|
+
attempt: number;
|
|
707
|
+
route: {
|
|
708
|
+
provider: string;
|
|
709
|
+
model: string;
|
|
710
|
+
qualified_id: string;
|
|
711
|
+
};
|
|
712
|
+
family: TokenBudgetFamily;
|
|
713
|
+
rate_source: TokenBudgetRateSource;
|
|
714
|
+
prompt_utf8_bytes: number;
|
|
715
|
+
prompt_sha256: string;
|
|
716
|
+
forecast_input_tokens: number;
|
|
717
|
+
billed_input_tokens: number;
|
|
718
|
+
billed_input_breakdown: {
|
|
719
|
+
input: number;
|
|
720
|
+
cache_read: number;
|
|
721
|
+
cache_write: number;
|
|
722
|
+
};
|
|
723
|
+
under_forecast_tokens: number;
|
|
724
|
+
byte_class_breakdown: TokenBudgetByteClassBreakdown;
|
|
725
|
+
dominant_byte_class: TokenBudgetDominantByteClass;
|
|
582
726
|
}
|
package/src/core/registry.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type JsonObject,
|
|
28
28
|
type KillKind,
|
|
29
29
|
type StartAttestedPiTaskOptions,
|
|
30
|
+
type StartDelegateTaskOptions,
|
|
30
31
|
type StartTaskOptions,
|
|
31
32
|
type TaskContextUsage,
|
|
32
33
|
type TaskStatus,
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
} from './attested-pi-run.js';
|
|
53
54
|
import {
|
|
54
55
|
assertWindowsCommandLineWithinLimit,
|
|
56
|
+
piLaunchArgv,
|
|
55
57
|
resolvePiLaunch,
|
|
56
58
|
type PiLaunchSpec,
|
|
57
59
|
} from './pi-launch.js';
|
|
@@ -87,8 +89,15 @@ interface OutputEventSource {
|
|
|
87
89
|
on(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
interface ChildStdin {
|
|
93
|
+
write(data: Buffer, callback: (error?: Error | null) => void): boolean;
|
|
94
|
+
end(callback?: () => void): unknown;
|
|
95
|
+
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
export interface BackgroundTaskChildProcess {
|
|
91
99
|
pid?: number | undefined;
|
|
100
|
+
stdin?: ChildStdin | null | undefined;
|
|
92
101
|
stdout?: OutputEventSource | null | undefined;
|
|
93
102
|
stderr?: OutputEventSource | null | undefined;
|
|
94
103
|
kill(signal?: NodeJS.Signals): boolean;
|
|
@@ -678,6 +687,32 @@ function noopOnChange(): void {
|
|
|
678
687
|
return undefined;
|
|
679
688
|
}
|
|
680
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Deliver the delegate prompt bytes over stdin.
|
|
692
|
+
*
|
|
693
|
+
* A failure to deliver the seed is loud: the caller terminates the task rather
|
|
694
|
+
* than letting a child run without the context it was supposed to receive.
|
|
695
|
+
*/
|
|
696
|
+
function writeDelegateStdin(
|
|
697
|
+
child: BackgroundTaskChildProcess,
|
|
698
|
+
bytes: Buffer,
|
|
699
|
+
onError: (error: Error) => void,
|
|
700
|
+
): void {
|
|
701
|
+
const stdin = child.stdin;
|
|
702
|
+
if (stdin === undefined || stdin === null) {
|
|
703
|
+
onError(new Error('delegate child stdin pipe is unavailable'));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
stdin.once('error', onError);
|
|
707
|
+
stdin.write(bytes, (error?: Error | null) => {
|
|
708
|
+
if (error !== undefined && error !== null) {
|
|
709
|
+
onError(error);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
stdin.end();
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
681
716
|
export class BackgroundTaskRegistry {
|
|
682
717
|
private readonly tasks = new Map<string, BgTask>();
|
|
683
718
|
private runtimeDir: RuntimeDir | undefined;
|
|
@@ -927,6 +962,145 @@ export class BackgroundTaskRegistry {
|
|
|
927
962
|
}
|
|
928
963
|
}
|
|
929
964
|
|
|
965
|
+
/**
|
|
966
|
+
* Start a prepared delegate child.
|
|
967
|
+
*
|
|
968
|
+
* The caller has already completed preflight, so by the time this runs the
|
|
969
|
+
* seed, budget plan, and artifact directory exist and the argv is fixed. The
|
|
970
|
+
* child is launched directly, never through a shell, and its terminal state
|
|
971
|
+
* flows through the same durable notification path as `bg_run`.
|
|
972
|
+
*/
|
|
973
|
+
async startDelegateTask(
|
|
974
|
+
ctx: BackgroundTaskContext,
|
|
975
|
+
request: StartDelegateTaskOptions,
|
|
976
|
+
): Promise<BgTask> {
|
|
977
|
+
if (this.shuttingDown)
|
|
978
|
+
throw new Error('Cannot start a delegate task while Pi is shutting down');
|
|
979
|
+
|
|
980
|
+
const launch = resolvePiLaunch({ platform: this.platform });
|
|
981
|
+
assertWindowsCommandLineWithinLimit(launch, request.argv, this.platform, 'bg-delegate');
|
|
982
|
+
|
|
983
|
+
const dir = await this.ensureRuntimeDir(ctx);
|
|
984
|
+
const id = request.facts.taskId;
|
|
985
|
+
const outputAbsPath = join(dir.abs, `${id}.output`);
|
|
986
|
+
const metadataAbsPath = join(dir.abs, `${id}.json`);
|
|
987
|
+
const outputPath = join(dir.display, `${id}.output`);
|
|
988
|
+
|
|
989
|
+
const task: BgTask = {
|
|
990
|
+
id,
|
|
991
|
+
name: normalizeTaskName(request.name) ?? 'Delegate task',
|
|
992
|
+
command: ['pi', ...request.argv].map(shellQuote).join(' '),
|
|
993
|
+
status: 'running',
|
|
994
|
+
outputPath,
|
|
995
|
+
outputAbsPath,
|
|
996
|
+
metadataAbsPath,
|
|
997
|
+
cwd: ctx.cwd,
|
|
998
|
+
startTime: this.now(),
|
|
999
|
+
exitCode: undefined,
|
|
1000
|
+
pid: undefined,
|
|
1001
|
+
bytesWritten: 0,
|
|
1002
|
+
isAgent: true,
|
|
1003
|
+
notified: false,
|
|
1004
|
+
notifyOnCompletion: request.notifyOnCompletion,
|
|
1005
|
+
triggerOnCompletion: request.triggerOnCompletion,
|
|
1006
|
+
timeoutSeconds: request.timeoutSeconds,
|
|
1007
|
+
model: request.facts.route.qualifiedId,
|
|
1008
|
+
delegate: request.facts,
|
|
1009
|
+
waiters: [],
|
|
1010
|
+
};
|
|
1011
|
+
this.tasks.set(id, task);
|
|
1012
|
+
|
|
1013
|
+
const stream = createWriteStream(outputAbsPath, { flags: 'a', encoding: 'utf8' });
|
|
1014
|
+
task.stream = stream;
|
|
1015
|
+
stream.on('error', (error) => {
|
|
1016
|
+
task.error = `Output file write failed: ${error.message}`;
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const child = this.spawn(launch.executable, piLaunchArgv(launch, [...request.argv]), {
|
|
1021
|
+
cwd: ctx.cwd,
|
|
1022
|
+
detached: this.platform !== 'win32',
|
|
1023
|
+
shell: false,
|
|
1024
|
+
// The seed travels over stdin, never as a shell or positional argument,
|
|
1025
|
+
// so the bytes the child reads are exactly the bytes that were persisted
|
|
1026
|
+
// and hashed, with no quoting or command-line length limit in the path.
|
|
1027
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1028
|
+
env: request.env,
|
|
1029
|
+
windowsHide: true,
|
|
1030
|
+
});
|
|
1031
|
+
task.child = child;
|
|
1032
|
+
task.pid = child.pid;
|
|
1033
|
+
writeDelegateStdin(child, request.stdinBytes, (error) => {
|
|
1034
|
+
this.writeNotice(task, `\n[delegate stdin write failed: ${error.message}]\n`);
|
|
1035
|
+
if (task.status === 'running') {
|
|
1036
|
+
task.killKind = 'user';
|
|
1037
|
+
task.error = `Delegate seed could not be delivered: ${error.message}`;
|
|
1038
|
+
try {
|
|
1039
|
+
this.requestKill(task, 'SIGTERM');
|
|
1040
|
+
} catch {
|
|
1041
|
+
void this.finalizeTask(task, 'failed', null, undefined, task.error);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
child.stdout?.on('data', (data) => {
|
|
1047
|
+
this.appendChildOutput(task, data, 'stdout');
|
|
1048
|
+
});
|
|
1049
|
+
child.stderr?.on('data', (data) => {
|
|
1050
|
+
this.appendChildOutput(task, data, 'stderr');
|
|
1051
|
+
});
|
|
1052
|
+
child.on('error', (error) => {
|
|
1053
|
+
this.writeNotice(task, `\n[delegate spawn error: ${error.message}]\n`);
|
|
1054
|
+
void this.finalizeTask(task, 'failed', null, undefined, error.message);
|
|
1055
|
+
});
|
|
1056
|
+
child.on('close', (code, signalName) => {
|
|
1057
|
+
let status: TaskStatus;
|
|
1058
|
+
let error: string | undefined;
|
|
1059
|
+
if (task.killKind === 'user' || task.killKind === 'shutdown') {
|
|
1060
|
+
status = 'killed';
|
|
1061
|
+
} else if (task.killKind === 'timeout') {
|
|
1062
|
+
status = 'failed';
|
|
1063
|
+
error = task.error ?? `Timed out after ${String(request.timeoutSeconds ?? 0)}s`;
|
|
1064
|
+
} else if ((code ?? 0) === 0) {
|
|
1065
|
+
status = 'completed';
|
|
1066
|
+
} else {
|
|
1067
|
+
status = 'failed';
|
|
1068
|
+
error = `Exited with code ${code === null ? 'null' : String(code)}${signalName ? ` (${signalName})` : ''}`;
|
|
1069
|
+
}
|
|
1070
|
+
void this.finalizeTask(task, status, code, signalName, error);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
if (request.timeoutSeconds !== undefined) {
|
|
1074
|
+
task.timeoutHandle = setTimeout(() => {
|
|
1075
|
+
if (task.status !== 'running') return;
|
|
1076
|
+
task.killKind = 'timeout';
|
|
1077
|
+
task.error = `Timed out after ${String(request.timeoutSeconds)}s`;
|
|
1078
|
+
this.writeNotice(task, `\n[delegate timeout: ${task.error}]\n`);
|
|
1079
|
+
try {
|
|
1080
|
+
this.requestKill(task, 'SIGTERM');
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
void this.finalizeTask(
|
|
1083
|
+
task,
|
|
1084
|
+
'failed',
|
|
1085
|
+
null,
|
|
1086
|
+
undefined,
|
|
1087
|
+
`${task.error}; kill failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}, request.timeoutSeconds * 1000);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
await this.writeMetadata(task);
|
|
1094
|
+
this.onChange();
|
|
1095
|
+
return task;
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1098
|
+
this.writeNotice(task, `\n[delegate spawn exception: ${message}]\n`);
|
|
1099
|
+
await this.finalizeTask(task, 'failed', null, undefined, message);
|
|
1100
|
+
throw new Error(`Failed to start delegate task: ${message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
930
1104
|
async startAttestedPiTask(
|
|
931
1105
|
ctx: BackgroundTaskContext,
|
|
932
1106
|
request: StartAttestedPiTaskOptions,
|