pi-background-tasks 1.0.6 → 2.0.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.
Files changed (49) hide show
  1. package/README.md +7 -7
  2. package/TESTING.md +3 -3
  3. package/TEST_PLAN.md +2 -2
  4. package/docs/INDEX.md +25 -25
  5. package/docs/choose-a-workflow.md +4 -4
  6. package/docs/commands/bg-clear.md +1 -1
  7. package/docs/commands/bg-update.md +1 -1
  8. package/docs/commands/bg.md +1 -1
  9. package/docs/commands/fusion-models.md +1 -1
  10. package/docs/commands/fusion.md +5 -8
  11. package/docs/commands/jobs.md +1 -1
  12. package/docs/commands/kill.md +1 -1
  13. package/docs/commands/logs.md +1 -1
  14. package/docs/commands/task-manager.md +2 -2
  15. package/docs/concepts/completion-delivery.md +1 -0
  16. package/docs/getting-started.md +1 -1
  17. package/docs/manifest.json +59 -50
  18. package/docs/read-before-edit.md +1 -0
  19. package/docs/reference/runtime-contracts.md +55 -51
  20. package/docs/reference/shortcuts-and-dock.md +2 -2
  21. package/docs/subsystems/background-task-runtime.md +7 -1
  22. package/docs/subsystems/docs-freshness-gate.md +4 -4
  23. package/docs/subsystems/fusion.md +15 -11
  24. package/docs/subsystems/host-ui-and-telemetry.md +1 -1
  25. package/docs/tools/bg_delegate.md +1 -1
  26. package/docs/tools/bg_kill.md +1 -1
  27. package/docs/tools/bg_logs.md +1 -1
  28. package/docs/tools/bg_result.md +14 -10
  29. package/docs/tools/bg_run.md +1 -1
  30. package/docs/tools/bg_run_pi_attested.md +1 -1
  31. package/docs/tools/bg_status.md +1 -1
  32. package/docs/tools/fusion_investigate.md +6 -4
  33. package/docs/tools/fusion_reason.md +5 -5
  34. package/docs/tools/fusion_research.md +6 -2
  35. package/docs/tools/fusion_validate.md +5 -3
  36. package/package.json +1 -1
  37. package/src/core/common.ts +50 -2
  38. package/src/core/fusion/artifacts.ts +106 -13
  39. package/src/core/fusion/budget.ts +12 -4
  40. package/src/core/fusion/evaluation.ts +61 -0
  41. package/src/core/fusion/orchestrator.ts +270 -73
  42. package/src/core/fusion/pi-child.ts +6 -0
  43. package/src/core/fusion/prompts.ts +1 -0
  44. package/src/core/fusion/result-package.ts +385 -0
  45. package/src/core/fusion/types.ts +19 -1
  46. package/src/core/registry.ts +187 -20
  47. package/src/delegate-extension.ts +130 -24
  48. package/src/extension.ts +17 -6
  49. package/src/fusion-extension.ts +308 -154
@@ -6,7 +6,9 @@ import { sanitizePathSegment } from '../common.js';
6
6
  import { replaceFileDurable } from '../durable-fs.js';
7
7
  import {
8
8
  EMPTY_FUSION_USAGE,
9
+ FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
9
10
  FUSION_MANIFEST_SCHEMA_VERSION,
11
+ FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
10
12
  FusionError,
11
13
  cloneFusionUsage,
12
14
  type FusionArtifactManifest,
@@ -18,6 +20,8 @@ import {
18
20
  type FusionCapability,
19
21
  type FusionContextOmissionLedgerV2,
20
22
  type FusionChildRunResult,
23
+ type FusionCommittedResultV1,
24
+ type FusionResultDetails,
21
25
  type FusionModelConfigV1,
22
26
  type FusionSource,
23
27
  type FusionStage,
@@ -56,8 +60,17 @@ interface MutableFusionArtifactManifest {
56
60
  evaluation: FusionCapability;
57
61
  merge: FusionCapability;
58
62
  };
59
- context: { kind: import('./types.js').FusionContextKind; policy_id: string; ledger_artifact?: string; source_policy_artifact?: string };
60
- tool_policy: { candidate_tools: readonly string[]; evaluation_tools: readonly []; merge_tools: readonly [] };
63
+ context: {
64
+ kind: import('./types.js').FusionContextKind;
65
+ policy_id: string;
66
+ ledger_artifact?: string;
67
+ source_policy_artifact?: string;
68
+ };
69
+ tool_policy: {
70
+ candidate_tools: readonly string[];
71
+ evaluation_tools: readonly [];
72
+ merge_tools: readonly [];
73
+ };
61
74
  usage: FusionUsage;
62
75
  attempts: FusionAttemptArtifactRecord[];
63
76
  artifacts: Record<string, FusionArtifactRef>;
@@ -83,14 +96,39 @@ export interface CreateFusionArtifactStoreOptions {
83
96
 
84
97
  export interface RecordFusionChildAttemptInput {
85
98
  result: FusionChildRunResult;
99
+ systemPrompt: string;
86
100
  prompt: string;
87
101
  responseKind: 'md' | 'txt';
88
102
  }
89
103
 
104
+ export type RecordValidationCandidateContractEventInput =
105
+ | {
106
+ candidateId: FusionCandidateId;
107
+ slot: 1 | 2 | 3;
108
+ status: 'normalized';
109
+ detail: {
110
+ normalization: 'markdown_json_fence' | 'prose_then_markdown_json_fence';
111
+ original_sha256: string;
112
+ forwarded_sha256: string;
113
+ warning: string;
114
+ };
115
+ }
116
+ | {
117
+ candidateId: FusionCandidateId;
118
+ slot: 1 | 2 | 3;
119
+ status: 'dropped';
120
+ detail: {
121
+ response_sha256: string;
122
+ error: string;
123
+ warning: string;
124
+ };
125
+ };
126
+
90
127
  export interface RecordFusionFailedAttemptInput {
91
128
  stage: FusionStage;
92
129
  slot?: 1 | 2 | 3;
93
130
  attempt: number;
131
+ systemPrompt: string;
94
132
  prompt: string;
95
133
  events: Buffer;
96
134
  partialResponse: Buffer;
@@ -180,7 +218,11 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
180
218
  models: manifest.models,
181
219
  capabilities: manifest.capabilities,
182
220
  context: { ...manifest.context },
183
- tool_policy: { candidate_tools: [...manifest.tool_policy.candidate_tools], evaluation_tools: [], merge_tools: [] },
221
+ tool_policy: {
222
+ candidate_tools: [...manifest.tool_policy.candidate_tools],
223
+ evaluation_tools: [],
224
+ merge_tools: [],
225
+ },
184
226
  usage: cloneFusionUsage(manifest.usage),
185
227
  attempts: [...manifest.attempts],
186
228
  artifacts: { ...manifest.artifacts },
@@ -269,8 +311,18 @@ export class FusionArtifactStore {
269
311
  evaluation: 'reason',
270
312
  merge: 'reason',
271
313
  },
272
- context: { kind: profile.contextKind, policy_id: profile.contextKind === 'session_projection' ? 'fusion-session-projection-v1' : 'fusion-clean-task-v1' },
273
- tool_policy: { candidate_tools: profile.candidateTools, evaluation_tools: [], merge_tools: [] },
314
+ context: {
315
+ kind: profile.contextKind,
316
+ policy_id:
317
+ profile.contextKind === 'session_projection'
318
+ ? 'fusion-session-projection-v1'
319
+ : 'fusion-clean-task-v1',
320
+ },
321
+ tool_policy: {
322
+ candidate_tools: profile.candidateTools,
323
+ evaluation_tools: [],
324
+ merge_tools: [],
325
+ },
274
326
  usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
275
327
  attempts: [],
276
328
  artifacts: {},
@@ -313,11 +365,18 @@ export class FusionArtifactStore {
313
365
  childCreated: false,
314
366
  });
315
367
  }
316
- if (to === 'completed' && manifest.artifacts['merged.md'] === undefined) {
317
- throw new FusionError('fusion cannot complete before merged.md is durable', {
318
- code: 'state_transition_invalid',
319
- childCreated: false,
320
- });
368
+ if (
369
+ to === 'completed' &&
370
+ (manifest.artifacts['merged.md'] === undefined ||
371
+ manifest.artifacts['result.json'] === undefined)
372
+ ) {
373
+ throw new FusionError(
374
+ 'fusion cannot complete before merged.md and result.json are durable',
375
+ {
376
+ code: 'state_transition_invalid',
377
+ childCreated: false,
378
+ },
379
+ );
321
380
  }
322
381
  manifest.state = to;
323
382
  });
@@ -377,8 +436,21 @@ export class FusionArtifactStore {
377
436
  await this.writeArtifact('evaluation.json', canonicalJson(value));
378
437
  }
379
438
 
380
- async writeMerged(text: string): Promise<void> {
381
- await this.writeArtifact('merged.md', text);
439
+ async writeMerged(text: string): Promise<FusionArtifactRef> {
440
+ return this.writeArtifact('merged.md', text);
441
+ }
442
+
443
+ async writeCommittedResult(
444
+ merged: FusionArtifactRef,
445
+ details: FusionResultDetails,
446
+ ): Promise<FusionArtifactRef> {
447
+ const value: FusionCommittedResultV1 = {
448
+ schema_version: FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
449
+ run_id: this.runId,
450
+ merged,
451
+ details,
452
+ };
453
+ return this.writeArtifact('result.json', `${canonicalJson(value)}\n`);
382
454
  }
383
455
 
384
456
  async writeError(state: Exclude<FusionTerminalState, 'completed'>, error: string): Promise<void> {
@@ -398,6 +470,7 @@ export class FusionArtifactStore {
398
470
 
399
471
  async recordChildAttempt(input: RecordFusionChildAttemptInput): Promise<void> {
400
472
  const prefix = attemptPrefix(input.result.stage, input.result.slot, input.result.attempt);
473
+ await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
401
474
  const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
402
475
  const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.result.events);
403
476
  const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.result.stderr);
@@ -439,11 +512,31 @@ export class FusionArtifactStore {
439
512
  violation: FusionCalibrationViolation;
440
513
  }): Promise<FusionArtifactRef> {
441
514
  const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
442
- return this.writeArtifact(calibrationViolationName(prefix), `${canonicalJson(input.violation)}\n`);
515
+ return this.writeArtifact(
516
+ calibrationViolationName(prefix),
517
+ `${canonicalJson(input.violation)}\n`,
518
+ );
519
+ }
520
+
521
+ async recordValidationCandidateContractEvent(
522
+ input: RecordValidationCandidateContractEventInput,
523
+ ): Promise<FusionArtifactRef> {
524
+ const name = `candidate-${String(input.slot)}.output-contract-${input.status}.json`;
525
+ return this.writeArtifact(
526
+ name,
527
+ `${canonicalJson({
528
+ schema_version: FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
529
+ ...input.detail,
530
+ candidate_id: input.candidateId,
531
+ slot: input.slot,
532
+ status: input.status,
533
+ })}\n`,
534
+ );
443
535
  }
444
536
 
445
537
  async recordFailedAttempt(input: RecordFusionFailedAttemptInput): Promise<void> {
446
538
  const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
539
+ await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
447
540
  const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
448
541
  const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.events);
449
542
  const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.stderr);
@@ -1122,7 +1122,13 @@ export class FusionBudget {
1122
1122
  const inputSegments = [knownTextSegment(systemPrompt), knownTextSegment(userPrompt)];
1123
1123
  const promptUtf8Bytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
1124
1124
  const estimate = estimateRouteInput(route, inputSegments);
1125
- const billedInput = result.usage.input + result.usage.cacheRead + result.usage.cacheWrite;
1125
+ // The forecast is a one-request admission estimate. Compare it only with
1126
+ // the first provider request, never with aggregate agent-loop/cache usage.
1127
+ // Custom child runners predating this observation field remain compatible,
1128
+ // but cannot produce a calibration verdict without like-for-like evidence.
1129
+ const observedUsage = result.firstRequestUsage;
1130
+ if (observedUsage === undefined) return undefined;
1131
+ const billedInput = observedUsage.input + observedUsage.cacheRead + observedUsage.cacheWrite;
1126
1132
  if (billedInput <= estimate.tokens) return undefined;
1127
1133
  const violation: FusionCalibrationViolation = {
1128
1134
  schema_version: FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
@@ -1137,12 +1143,14 @@ export class FusionBudget {
1137
1143
  rate_source: estimate.rateSource,
1138
1144
  prompt_utf8_bytes: promptUtf8Bytes,
1139
1145
  prompt_sha256: sha256Hex(`${systemPrompt}\u0000${userPrompt}`),
1146
+ observation_scope: 'first_provider_request',
1147
+ provider_request_count: result.providerRequestCount ?? 1,
1140
1148
  forecast_input_tokens: estimate.tokens,
1141
1149
  billed_input_tokens: billedInput,
1142
1150
  billed_input_breakdown: {
1143
- input: result.usage.input,
1144
- cache_read: result.usage.cacheRead,
1145
- cache_write: result.usage.cacheWrite,
1151
+ input: observedUsage.input,
1152
+ cache_read: observedUsage.cacheRead,
1153
+ cache_write: observedUsage.cacheWrite,
1146
1154
  },
1147
1155
  under_forecast_tokens: billedInput - estimate.tokens,
1148
1156
  byte_class_breakdown: estimate.byte_class_breakdown,
@@ -525,6 +525,67 @@ export interface ParsedFusionValidationCandidateReport {
525
525
  limitations: readonly string[];
526
526
  }
527
527
 
528
+ export type FusionValidationCandidateNormalization =
529
+ | 'markdown_json_fence'
530
+ | 'prose_then_markdown_json_fence';
531
+
532
+ export interface RecoveredFusionValidationCandidateReport {
533
+ report: ParsedFusionValidationCandidateReport;
534
+ /** Bare JSON forwarded to the evaluator after explicit, audited recovery. */
535
+ response: string;
536
+ normalization: FusionValidationCandidateNormalization;
537
+ }
538
+
539
+ /**
540
+ * Recognize exactly one complete Markdown JSON fence, optionally preceded by a
541
+ * short prose preamble. This is deliberately narrower than generic substring
542
+ * extraction: trailing prose, nested fences, unlabelled fences, and oversized
543
+ * preambles remain contract failures.
544
+ */
545
+ function fencedValidationCandidateJson(text: string): {
546
+ payload: string;
547
+ normalization: FusionValidationCandidateNormalization;
548
+ } | undefined {
549
+ const trimmed = text.trim();
550
+ const openingPattern = /```json[ \t]*\r?\n/giu;
551
+ const openings = [...trimmed.matchAll(openingPattern)];
552
+ if (openings.length !== 1) return undefined;
553
+ const opening = openings[0];
554
+ if (opening === undefined) return undefined;
555
+ const headerEnd = opening.index + opening[0].length;
556
+ const closing = trimmed.indexOf('```', headerEnd);
557
+ if (closing < 0 || trimmed.slice(closing + 3).includes('```')) return undefined;
558
+ if (trimmed.slice(closing + 3).trim().length > 0) return undefined;
559
+ const preamble = trimmed.slice(0, opening.index).trim();
560
+ if (Buffer.byteLength(preamble, 'utf8') > 2_000 || preamble.includes('```')) return undefined;
561
+ const payload = trimmed.slice(headerEnd, closing).trim();
562
+ if (payload.length === 0 || payload.includes('```')) return undefined;
563
+ return {
564
+ payload,
565
+ normalization: preamble.length === 0
566
+ ? 'markdown_json_fence'
567
+ : 'prose_then_markdown_json_fence',
568
+ };
569
+ }
570
+
571
+ /**
572
+ * Defensive recovery for the one observed contract violation shape. Callers
573
+ * must persist/surface the returned normalization; this function intentionally
574
+ * does not make the strict parser permissive.
575
+ */
576
+ export function recoverFencedFusionValidationCandidateReport(
577
+ text: string,
578
+ candidateId: FusionCandidateId,
579
+ ): RecoveredFusionValidationCandidateReport | undefined {
580
+ const recovered = fencedValidationCandidateJson(text);
581
+ if (recovered === undefined) return undefined;
582
+ return {
583
+ report: parseFusionValidationCandidateReport(recovered.payload, candidateId),
584
+ response: recovered.payload,
585
+ normalization: recovered.normalization,
586
+ };
587
+ }
588
+
528
589
  export function parseFusionValidationCandidateReport(text: string, candidateId: FusionCandidateId): ParsedFusionValidationCandidateReport {
529
590
  let parsed: unknown;
530
591
  try {