pi-background-tasks 0.7.6 → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  import { randomBytes as nodeRandomBytes } from 'node:crypto';
2
2
  import { parseJsonText } from '../common.js';
3
- import { FusionBudget, assertChildOutputWithinContract } from './budget.js';
3
+ import { FUSION_BUDGET_POLICY, FusionBudget, assertChildOutputWithinContract } from './budget.js';
4
4
  import {
5
5
  FusionArtifactStore,
6
6
  type CreateFusionArtifactStoreOptions,
@@ -13,10 +13,6 @@ import {
13
13
  } from './evaluation.js';
14
14
  import { FusionChildRunError, runPiChild, type RunPiChildOptions } from './pi-child.js';
15
15
  import {
16
- FUSION_CANDIDATE_SYSTEM_PROMPT,
17
- FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
18
- FUSION_EVALUATOR_SYSTEM_PROMPT,
19
- FUSION_MERGER_SYSTEM_PROMPT,
20
16
  buildBlindEvaluationInput,
21
17
  buildCandidatePrompt,
22
18
  buildEvaluationPrompt,
@@ -26,10 +22,18 @@ import {
26
22
  type AnonymousFusionCandidate,
27
23
  } from './prompts.js';
28
24
  import {
25
+ FUSION_BRAINSTORM_WORKFLOW,
26
+ resolveWorkflowCapability,
27
+ type FusionWorkflowProfile,
28
+ } from './workflows.js';
29
+ import {
30
+ FUSION_DEFAULT_CAPABILITY,
29
31
  FUSION_RESULT_SCHEMA_VERSION,
30
32
  FusionError,
31
33
  addFusionUsage,
32
34
  createEmptyFusionUsage,
35
+ type FusionCalibrationViolation,
36
+ type FusionCapability,
33
37
  type FusionCanonicalInputV3,
34
38
  type FusionCandidateId,
35
39
  type FusionContextOmissionLedgerV2,
@@ -61,6 +65,9 @@ export interface FusionWorkflowInput {
61
65
  contextLedger: FusionContextOmissionLedgerV2;
62
66
  config: FusionModelConfigV1;
63
67
  models: ResolvedFusionModels;
68
+ candidateCapability?: FusionCapability | undefined;
69
+ /** Stage framing and capability policy. Defaults to the brainstorm workflow. */
70
+ profile?: FusionWorkflowProfile | undefined;
64
71
  signal?: AbortSignal | undefined;
65
72
  onProgress?: FusionProgressSink | undefined;
66
73
  }
@@ -178,21 +185,25 @@ function childOptions(
178
185
  model: ResolvedFusionModel,
179
186
  stage: FusionStage,
180
187
  attempt: number,
188
+ capability: FusionCapability,
181
189
  systemPrompt: string,
182
190
  userPrompt: string,
183
191
  signal: AbortSignal,
184
192
  slot?: CandidateSlot,
193
+ toolCallLogPath?: string,
185
194
  ): RunPiChildOptions {
186
195
  const out: RunPiChildOptions = {
187
196
  stage,
188
197
  attempt,
189
198
  cwd: input.cwd,
190
199
  model,
200
+ capability,
191
201
  systemPrompt,
192
202
  userPrompt,
193
203
  signal,
194
204
  };
195
205
  if (slot !== undefined) out.slot = slot;
206
+ if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
196
207
  return out;
197
208
  }
198
209
 
@@ -323,17 +334,29 @@ export class FusionOrchestrator {
323
334
  }
324
335
 
325
336
  async run(input: FusionWorkflowInput): Promise<FusionRunResult> {
337
+ const profile = input.profile ?? FUSION_BRAINSTORM_WORKFLOW;
338
+ // Workflow policy, not caller input: a fixed-capability workflow rejects each
339
+ // other capability here, before a single child exists, rather than silently
340
+ // substituting its own and running a review that never read the code.
341
+ const candidateCapability = resolveWorkflowCapability(profile, input.candidateCapability);
326
342
  const storeOptions: CreateFusionArtifactStoreOptions = {
327
343
  cwd: input.cwd,
344
+ profile,
328
345
  source: input.source,
329
346
  config: input.config,
330
347
  models: input.models,
348
+ capabilities: {
349
+ candidate: candidateCapability,
350
+ evaluation: FUSION_DEFAULT_CAPABILITY,
351
+ merge: FUSION_DEFAULT_CAPABILITY,
352
+ },
331
353
  };
332
354
  if (input.sessionId !== undefined) storeOptions.sessionId = input.sessionId;
333
355
  if (this.now !== undefined) storeOptions.now = this.now;
334
356
  const store = await this.createArtifactStore(storeOptions);
335
357
  input.onProgress?.({ type: 'state', state: 'initializing' });
336
358
  const usage = createEmptyFusionUsage();
359
+ const calibrationWarnings: FusionCalibrationViolation[] = [];
337
360
  try {
338
361
  await store.writeCanonicalInput(input.canonicalInputSerialized);
339
362
  await store.writeContextLedger(input.contextLedger);
@@ -342,6 +365,8 @@ export class FusionOrchestrator {
342
365
  const budget = new FusionBudget(
343
366
  input.models,
344
367
  input.canonicalInput.conversation_projection.policy.id,
368
+ candidateCapability,
369
+ profile,
345
370
  );
346
371
  const budgetPlan = budget.plan(input.canonicalInput);
347
372
  await store.writeBudgetPlan(budgetPlan);
@@ -355,7 +380,15 @@ export class FusionOrchestrator {
355
380
  }
356
381
  await store.transition('candidates_running');
357
382
  input.onProgress?.({ type: 'state', state: 'candidates_running' });
358
- const candidateResults = await this.runCandidates(input, store, usage, budget);
383
+ const candidateResults = await this.runCandidates(
384
+ input,
385
+ store,
386
+ usage,
387
+ budget,
388
+ calibrationWarnings,
389
+ profile,
390
+ candidateCapability,
391
+ );
359
392
  await store.transition('candidates_complete');
360
393
  input.onProgress?.({ type: 'state', state: 'candidates_complete' });
361
394
 
@@ -366,7 +399,15 @@ export class FusionOrchestrator {
366
399
 
367
400
  await store.transition('evaluating');
368
401
  input.onProgress?.({ type: 'state', state: 'evaluating' });
369
- const evaluation = await this.runEvaluation(input, store, usage, blindInput, budget);
402
+ const evaluation = await this.runEvaluation(
403
+ input,
404
+ store,
405
+ usage,
406
+ blindInput,
407
+ budget,
408
+ calibrationWarnings,
409
+ profile,
410
+ );
370
411
  await store.writeEvaluationJson(evaluation);
371
412
  await store.transition('evaluation_complete');
372
413
  input.onProgress?.({ type: 'state', state: 'evaluation_complete' });
@@ -375,7 +416,7 @@ export class FusionOrchestrator {
375
416
  input.onProgress?.({ type: 'state', state: 'merging' });
376
417
  const mergeInput = buildMergeInput(input.canonicalInput, shuffled.candidates, evaluation);
377
418
  const mergePrompt = buildMergePrompt(mergeInput);
378
- budget.assertStagePrompt('merge', FUSION_MERGER_SYSTEM_PROMPT, mergePrompt);
419
+ budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
379
420
  input.onProgress?.({ type: 'merge_started' });
380
421
  const merged = await this.runChildWithRetry(
381
422
  input,
@@ -383,14 +424,26 @@ export class FusionOrchestrator {
383
424
  usage,
384
425
  input.models.merger,
385
426
  'merge',
386
- FUSION_MERGER_SYSTEM_PROMPT,
427
+ profile.mergerSystemPrompt,
387
428
  mergePrompt,
388
429
  input.signal ?? new AbortController().signal,
430
+ // Stage policy, not caller input: evaluator and merger are always reasoning-only.
431
+ FUSION_DEFAULT_CAPABILITY,
389
432
  undefined,
390
433
  'md',
391
434
  );
392
435
  addFusionUsage(usage, merged.usage);
393
436
  await store.recordChildAttempt({ result: merged, prompt: mergePrompt, responseKind: 'md' });
437
+ await this.recordCalibrationObservation(
438
+ input,
439
+ store,
440
+ budget,
441
+ calibrationWarnings,
442
+ 'merge',
443
+ profile.mergerSystemPrompt,
444
+ mergePrompt,
445
+ merged,
446
+ );
394
447
  assertChildOutputWithinContract('merge', merged.text);
395
448
  await store.writeMerged(merged.text);
396
449
  await store.setUsage(usage);
@@ -401,6 +454,7 @@ export class FusionOrchestrator {
401
454
  details: {
402
455
  schema_version: FUSION_RESULT_SCHEMA_VERSION,
403
456
  run_id: store.runId,
457
+ workflow: profile.id,
404
458
  source: input.source,
405
459
  status: 'completed',
406
460
  artifact_dir: store.artifactDir,
@@ -409,6 +463,14 @@ export class FusionOrchestrator {
409
463
  .snapshot()
410
464
  .attempts.filter((attempt) => attempt.stage === 'evaluation').length,
411
465
  usage,
466
+ budget: {
467
+ policy_id: FUSION_BUDGET_POLICY.id,
468
+ calibration_version: budgetPlan.policy.calibration_version,
469
+ route_table: budget.routes,
470
+ rate_sources: budget.resultRateSources,
471
+ unknown_provider_warnings: budget.unknownProviderWarnings,
472
+ calibration_warnings: calibrationWarnings,
473
+ },
412
474
  },
413
475
  };
414
476
  } catch (error) {
@@ -446,14 +508,18 @@ export class FusionOrchestrator {
446
508
  store: FusionArtifactStore,
447
509
  usage: FusionUsage,
448
510
  budget: FusionBudget,
511
+ calibrationWarnings: FusionCalibrationViolation[],
512
+ profile: FusionWorkflowProfile,
513
+ candidateCapability: FusionCapability,
449
514
  ): Promise<readonly CandidateResult[]> {
450
515
  const controller = new AbortController();
451
516
  const abortListener = () => controller.abort();
452
517
  input.signal?.addEventListener('abort', abortListener, { once: true });
453
518
  if (input.signal?.aborted) controller.abort();
519
+ const systemPrompt = profile.candidateSystemPrompt(candidateCapability);
454
520
  const prompt = buildCandidatePrompt(input.canonicalInput);
455
521
  for (const slot of [1, 2, 3] as const) {
456
- budget.assertStagePrompt('candidate', FUSION_CANDIDATE_SYSTEM_PROMPT, prompt, slot);
522
+ budget.assertStagePrompt('candidate', systemPrompt, prompt, slot);
457
523
  }
458
524
  let primaryError: unknown;
459
525
  let completed = 0;
@@ -473,13 +539,25 @@ export class FusionOrchestrator {
473
539
  usage,
474
540
  model,
475
541
  'candidate',
476
- FUSION_CANDIDATE_SYSTEM_PROMPT,
542
+ systemPrompt,
477
543
  prompt,
478
544
  controller.signal,
545
+ candidateCapability,
479
546
  slot,
480
547
  'md',
481
548
  ).then(async (result) => {
482
549
  await store.recordChildAttempt({ result, prompt, responseKind: 'md' });
550
+ await this.recordCalibrationObservation(
551
+ input,
552
+ store,
553
+ budget,
554
+ calibrationWarnings,
555
+ 'candidate',
556
+ systemPrompt,
557
+ prompt,
558
+ result,
559
+ slot,
560
+ );
483
561
  // The response is durable before the contract check, so an oversized
484
562
  // answer is preserved as evidence rather than lost.
485
563
  assertChildOutputWithinContract('candidate', result.text);
@@ -516,10 +594,22 @@ export class FusionOrchestrator {
516
594
  usage: FusionUsage,
517
595
  blindInput: Parameters<typeof buildEvaluationPrompt>[0],
518
596
  budget: FusionBudget,
597
+ calibrationWarnings: FusionCalibrationViolation[],
598
+ profile: FusionWorkflowProfile,
519
599
  ): Promise<FusionEvaluationV1> {
520
600
  const firstPrompt = buildEvaluationPrompt(blindInput);
521
- budget.assertStagePrompt('evaluation', FUSION_EVALUATOR_SYSTEM_PROMPT, firstPrompt);
522
- const first = await this.runEvaluationAttempt(input, store, usage, firstPrompt, 1, false);
601
+ budget.assertStagePrompt('evaluation', profile.evaluatorSystemPrompt, firstPrompt);
602
+ const first = await this.runEvaluationAttempt(
603
+ input,
604
+ store,
605
+ usage,
606
+ budget,
607
+ calibrationWarnings,
608
+ firstPrompt,
609
+ 1,
610
+ false,
611
+ profile,
612
+ );
523
613
  if (first.evaluation !== undefined) return first.evaluation;
524
614
  const errors = boundedEvaluationErrors(first.errors);
525
615
  input.onProgress?.({ type: 'evaluation_retry', errors });
@@ -531,10 +621,20 @@ export class FusionOrchestrator {
531
621
  });
532
622
  budget.assertStagePrompt(
533
623
  'evaluation_repair',
534
- FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
624
+ profile.evaluationRepairSystemPrompt,
625
+ repairPrompt,
626
+ );
627
+ const second = await this.runEvaluationAttempt(
628
+ input,
629
+ store,
630
+ usage,
631
+ budget,
632
+ calibrationWarnings,
535
633
  repairPrompt,
634
+ 2,
635
+ true,
636
+ profile,
536
637
  );
537
- const second = await this.runEvaluationAttempt(input, store, usage, repairPrompt, 2, true);
538
638
  if (second.evaluation !== undefined) return second.evaluation;
539
639
  throw new FusionError(
540
640
  `evaluation schema repair failed: ${formatEvaluationErrors(second.errors)}`,
@@ -550,14 +650,17 @@ export class FusionOrchestrator {
550
650
  input: FusionWorkflowInput,
551
651
  store: FusionArtifactStore,
552
652
  usage: FusionUsage,
653
+ budget: FusionBudget,
654
+ calibrationWarnings: FusionCalibrationViolation[],
553
655
  prompt: string,
554
656
  attempt: 1 | 2,
555
657
  repair: boolean,
658
+ profile: FusionWorkflowProfile,
556
659
  ): Promise<EvaluationAttemptResult> {
557
660
  input.onProgress?.({ type: 'evaluation_started', attempt, repair });
558
661
  const systemPrompt = repair
559
- ? FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT
560
- : FUSION_EVALUATOR_SYSTEM_PROMPT;
662
+ ? profile.evaluationRepairSystemPrompt
663
+ : profile.evaluatorSystemPrompt;
561
664
  const result = await this.runChildWithRetry(
562
665
  input,
563
666
  store,
@@ -567,12 +670,24 @@ export class FusionOrchestrator {
567
670
  systemPrompt,
568
671
  prompt,
569
672
  input.signal ?? new AbortController().signal,
673
+ // Stage policy, not caller input: evaluator and merger are always reasoning-only.
674
+ FUSION_DEFAULT_CAPABILITY,
570
675
  undefined,
571
676
  'txt',
572
677
  attempt,
573
678
  );
574
679
  addFusionUsage(usage, result.usage);
575
680
  await store.recordChildAttempt({ result, prompt, responseKind: 'txt' });
681
+ await this.recordCalibrationObservation(
682
+ input,
683
+ store,
684
+ budget,
685
+ calibrationWarnings,
686
+ 'evaluation',
687
+ systemPrompt,
688
+ prompt,
689
+ result,
690
+ );
576
691
  await store.setUsage(usage);
577
692
  // Bound the evaluator output before it can be embedded in a repair prompt.
578
693
  assertChildOutputWithinContract('evaluation', result.text);
@@ -580,6 +695,41 @@ export class FusionOrchestrator {
580
695
  return { result, evaluation: parsed.evaluation, errors: parsed.errors };
581
696
  }
582
697
 
698
+ private async recordCalibrationObservation(
699
+ input: FusionWorkflowInput,
700
+ store: FusionArtifactStore,
701
+ budget: FusionBudget,
702
+ calibrationWarnings: FusionCalibrationViolation[],
703
+ stage: FusionStage,
704
+ systemPrompt: string,
705
+ userPrompt: string,
706
+ result: FusionChildRunResult,
707
+ slot?: CandidateSlot,
708
+ ): Promise<void> {
709
+ const violation = budget.calibrationViolationForCompletedChild(
710
+ stage,
711
+ systemPrompt,
712
+ userPrompt,
713
+ result,
714
+ slot,
715
+ );
716
+ if (violation === undefined) return;
717
+ calibrationWarnings.push(violation);
718
+ let artifact = 'calibration-violation artifact was not written';
719
+ try {
720
+ const ref = await store.recordCalibrationViolation({
721
+ stage,
722
+ attempt: result.attempt,
723
+ violation,
724
+ ...(slot === undefined ? {} : { slot }),
725
+ });
726
+ artifact = ref.path;
727
+ } catch (error) {
728
+ artifact = `calibration-violation artifact write failed: ${errorText(error)}`;
729
+ }
730
+ input.onProgress?.({ type: 'calibration_warning', warning: violation, artifact });
731
+ }
732
+
583
733
  private async runChildWithRetry(
584
734
  input: FusionWorkflowInput,
585
735
  store: FusionArtifactStore,
@@ -589,6 +739,7 @@ export class FusionOrchestrator {
589
739
  systemPrompt: string,
590
740
  userPrompt: string,
591
741
  signal: AbortSignal,
742
+ capability: FusionCapability,
592
743
  slot: CandidateSlot | undefined,
593
744
  responseKind: 'md' | 'txt',
594
745
  fixedAttempt?: 1 | 2,
@@ -598,9 +749,24 @@ export class FusionOrchestrator {
598
749
  if (stage === 'candidate' && slot !== undefined) {
599
750
  input.onProgress?.({ type: 'candidate_started', slot, attempt: logicalAttempt });
600
751
  }
752
+ const toolCallLogPath =
753
+ capability !== 'reason'
754
+ ? store.childToolCallLogPath(stage, slot, logicalAttempt)
755
+ : undefined;
601
756
  try {
602
757
  return await this.childRunner(
603
- childOptions(input, model, stage, logicalAttempt, systemPrompt, userPrompt, signal, slot),
758
+ childOptions(
759
+ input,
760
+ model,
761
+ stage,
762
+ logicalAttempt,
763
+ capability,
764
+ systemPrompt,
765
+ userPrompt,
766
+ signal,
767
+ slot,
768
+ toolCallLogPath,
769
+ ),
604
770
  );
605
771
  } catch (error) {
606
772
  if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;