@akagilnc/pi-workflow-roles 0.1.1817 → 0.1.1837

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.
@@ -101,7 +101,7 @@ export async function disposeComplianceDecision(decision, handlers, deliveredOut
101
101
  if (handlers.noReceipt === undefined) {
102
102
  throw new Error("Compliance no-receipt projection handler is unavailable");
103
103
  }
104
- return await handlers.noReceipt(decision);
104
+ return await handlers.noReceipt(decision, decision.usage === undefined ? {} : { usage: decision.usage });
105
105
  case "revise":
106
106
  return await handlers.revise(decision.violations);
107
107
  case "escalate":
@@ -1,7 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { executeAuditorChild, } from "./evidence-child-executor.js";
3
3
  import { createAuditorDossierTool } from "./auditor-dossier-tool.js";
4
- import { parseNoReceiptLifecycleFacts } from "./receipt-delivery-policy.js";
5
4
  /** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
6
5
  export const AUDITOR_DOSSIER_PROMPT = "Audit the current run dossier.";
7
6
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
@@ -74,10 +73,12 @@ export async function runComplianceAudit(options) {
74
73
  ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
75
74
  ...(options.signal === undefined ? {} : { signal: options.signal }),
76
75
  });
77
- try {
78
- return { status: "no-receipt", ...parseNoReceiptLifecycleFacts(receipt.decision) };
79
- }
80
- catch {
81
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
76
+ if (receipt.noReceiptLifecycle !== undefined) {
77
+ return {
78
+ status: "no-receipt",
79
+ ...receipt.noReceiptLifecycle,
80
+ ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
81
+ };
82
82
  }
83
+ return readComplianceCandidate(receipt.decision, receipt.response.usage);
83
84
  }
@@ -428,10 +428,12 @@ export async function executeAuditorChild(options) {
428
428
  });
429
429
  const cwd = options.context.cwd ?? process.cwd();
430
430
  let decision;
431
+ let noReceiptLifecycle;
431
432
  let decisionSubmitted = false;
432
433
  let decisionCallId;
433
434
  let decisionToolFailure;
434
435
  const decisionToolFailures = new Map();
436
+ const delivery = createReceiptDeliveryPolicy();
435
437
  const tool = wrapPackageOwnedToolDefinition({
436
438
  ...options.tool,
437
439
  label: options.roleLabel,
@@ -439,8 +441,12 @@ export async function executeAuditorChild(options) {
439
441
  if (decisionSubmitted && decisionCallId !== args[0]) {
440
442
  throw new Error("Auditor decision was submitted more than once");
441
443
  }
444
+ // Pi may already have issued several decision calls in one assistant
445
+ // response. Execute every issued call: the budget limits future
446
+ // solicitations, not terminal calls already in flight.
442
447
  try {
443
448
  const result = await options.tool.execute(...args);
449
+ delivery.recordAccepted();
444
450
  decision = args[1];
445
451
  decisionCallId = args[0];
446
452
  decisionToolFailure = undefined;
@@ -490,11 +496,13 @@ export async function executeAuditorChild(options) {
490
496
  // response could not later be tied to the current parent attempt.
491
497
  auditorSessionManager.appendCustomEntry(AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, binding);
492
498
  let turns = 0;
499
+ const sessionUsage = emptyUsage();
493
500
  let boundaryResponse;
494
501
  let retentionFailure;
495
502
  let retainedResponse;
496
503
  let rejectedDecisionResponse;
497
504
  let promptNeighboringFailure;
505
+ let promptDecisionFailures = [];
498
506
  const registeredToolNames = new Set(session.getAllTools().map((entry) => entry.name));
499
507
  const evidenceToolFailures = new Map();
500
508
  for (const name of registeredToolNames) {
@@ -523,9 +531,19 @@ export async function executeAuditorChild(options) {
523
531
  const callIdSet = new Set(callIds);
524
532
  return [...session.messages].reverse().find((message) => message.role === "toolResult" && callIdSet.has(message.toolCallId) && message.isError);
525
533
  };
534
+ const drainRejectedDecisionFailures = (response) => {
535
+ for (const part of response.content) {
536
+ if (part.type !== "toolCall" || part.name !== tool.name || !decisionToolFailures.has(part.id))
537
+ continue;
538
+ decisionToolFailure = decisionToolFailures.get(part.id);
539
+ promptDecisionFailures.push(decisionToolFailure);
540
+ decisionToolFailures.delete(part.id);
541
+ }
542
+ };
526
543
  const unsubscribe = session.subscribe((event) => {
527
544
  if (event.type === "message_end" && event.message.role === "assistant" && boundaryResponse === undefined) {
528
545
  turns += 1;
546
+ addUsage(sessionUsage, event.message.usage);
529
547
  retainedResponse = event.message;
530
548
  try {
531
549
  options.retainResponse?.(event.message);
@@ -556,14 +574,11 @@ export async function executeAuditorChild(options) {
556
574
  if (event.type === "turn_end") {
557
575
  if (rejectedDecisionResponse !== undefined) {
558
576
  promptNeighboringFailure = findToolFailure(rejectedDecisionResponse);
559
- const rejectedCall = rejectedDecisionResponse.content.find((part) => part.type === "toolCall" && part.name === tool.name && decisionToolFailures.has(part.id));
560
- if (rejectedCall?.type === "toolCall") {
561
- decisionToolFailure = decisionToolFailures.get(rejectedCall.id);
562
- decisionToolFailures.delete(rejectedCall.id);
563
- }
577
+ drainRejectedDecisionFailures(rejectedDecisionResponse);
564
578
  }
565
579
  if (decisionSubmitted || promptNeighboringFailure !== undefined
566
- || boundaryResponse !== undefined || retentionFailure !== undefined) {
580
+ || (boundaryResponse !== undefined && rejectedDecisionResponse === undefined)
581
+ || retentionFailure !== undefined) {
567
582
  void session.abort();
568
583
  }
569
584
  }
@@ -575,11 +590,11 @@ export async function executeAuditorChild(options) {
575
590
  options.signal?.addEventListener("abort", abort, { once: true });
576
591
  try {
577
592
  try {
578
- const delivery = createReceiptDeliveryPolicy();
579
593
  const promptAllowingRejectedDecision = async (prompt) => {
580
594
  rejectedDecisionResponse = undefined;
581
595
  promptNeighboringFailure = undefined;
582
596
  decisionToolFailure = undefined;
597
+ promptDecisionFailures = [];
583
598
  let promptFailure;
584
599
  try {
585
600
  await session.prompt(prompt);
@@ -593,28 +608,47 @@ export async function executeAuditorChild(options) {
593
608
  const correlatedResponse = rejectedDecisionResponse;
594
609
  if (correlatedResponse !== undefined) {
595
610
  promptNeighboringFailure ??= findToolFailure(correlatedResponse);
596
- const rejectedCall = correlatedResponse.content.find((part) => part.type === "toolCall" && part.name === tool.name && decisionToolFailures.has(part.id));
597
- if (rejectedCall?.type === "toolCall") {
598
- decisionToolFailure = decisionToolFailures.get(rejectedCall.id);
599
- decisionToolFailures.delete(rejectedCall.id);
600
- }
611
+ drainRejectedDecisionFailures(correlatedResponse);
601
612
  }
602
613
  // An adjacent failure outranks correctable decision feedback.
603
614
  if (promptNeighboringFailure !== undefined)
604
615
  throw promptNeighboringFailure;
616
+ // An accepted correction in the same response owns the terminal
617
+ // outcome; correlated rejected siblings remain observations, not a
618
+ // stale failure capable of replacing that accepted receipt.
619
+ if (decisionSubmitted) {
620
+ decisionToolFailure = undefined;
621
+ return;
622
+ }
605
623
  if (decisionToolFailure !== undefined)
606
624
  return;
607
625
  if (promptFailure !== undefined)
608
626
  throw promptFailure;
609
627
  };
628
+ const chargeAndClearRejectedDecisionFailures = (failures) => {
629
+ for (const failure of failures) {
630
+ delivery.recordRejected(failure instanceof Error ? failure.message : String(failure));
631
+ }
632
+ decisionToolFailure = undefined;
633
+ promptDecisionFailures = [];
634
+ };
610
635
  await promptAllowingRejectedDecision(options.prompt);
611
- while (!decisionSubmitted && boundaryResponse === undefined && inherited.streamFailure === undefined
612
- && delivery.nextAction() === "request-delivery") {
636
+ while (!decisionSubmitted && (boundaryResponse === undefined || decisionToolFailure !== undefined)
637
+ && inherited.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
613
638
  if (decisionToolFailure !== undefined) {
614
- delivery.recordRejected(decisionToolFailure instanceof Error ? decisionToolFailure.message : String(decisionToolFailure));
615
- decisionToolFailure = undefined;
639
+ const failures = promptDecisionFailures.length === 0
640
+ ? [decisionToolFailure]
641
+ : promptDecisionFailures;
642
+ chargeAndClearRejectedDecisionFailures(failures);
643
+ if (delivery.nextAction() === "no-receipt")
644
+ boundaryResponse = undefined;
616
645
  if (delivery.nextAction() === "request-delivery") {
617
- await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
646
+ // A rejection and its correction solicitation are one budget unit;
647
+ // recordRejected already charged it.
648
+ if (retainedResponse === rejectedDecisionResponse) {
649
+ await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
650
+ chargeAndClearRejectedDecisionFailures(promptDecisionFailures);
651
+ }
618
652
  }
619
653
  }
620
654
  else {
@@ -622,15 +656,19 @@ export async function executeAuditorChild(options) {
622
656
  await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
623
657
  }
624
658
  }
625
- if (!decisionSubmitted && boundaryResponse === undefined && inherited.streamFailure === undefined
659
+ if (!decisionSubmitted && inherited.streamFailure === undefined
626
660
  && delivery.nextAction() === "no-receipt") {
627
661
  const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
628
662
  const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
629
- decision = delivery.facts({ runPointer, attemptPointer });
663
+ const facts = delivery.facts({ runPointer, attemptPointer });
664
+ decision = facts;
630
665
  // Late turn_end feedback cannot overturn a lifecycle that has already
631
666
  // charged this prompt to the exhausted shared budget.
632
667
  decisionToolFailure = undefined;
633
- auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, decision);
668
+ auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
669
+ // Provenance is granted only after the lifecycle owner persisted the
670
+ // current child record; accepted model arguments can never set it.
671
+ noReceiptLifecycle = facts;
634
672
  }
635
673
  }
636
674
  catch (error) {
@@ -644,7 +682,7 @@ export async function executeAuditorChild(options) {
644
682
  throw options.signal.reason;
645
683
  if (inherited.streamFailure !== undefined)
646
684
  throw inherited.streamFailure;
647
- if (decisionToolFailure !== undefined)
685
+ if (!decisionSubmitted && decisionToolFailure !== undefined)
648
686
  throw decisionToolFailure;
649
687
  const relevantResponse = !decisionSubmitted
650
688
  ? boundaryResponse
@@ -656,7 +694,7 @@ export async function executeAuditorChild(options) {
656
694
  }
657
695
  if (retentionFailure !== undefined && retainedResponse?.stopReason !== "error")
658
696
  throw retentionFailure;
659
- if (boundaryResponse !== undefined && !decisionSubmitted) {
697
+ if (boundaryResponse !== undefined && !decisionSubmitted && noReceiptLifecycle === undefined) {
660
698
  const toolNames = boundaryResponse.content.flatMap((part) => part.type === "toolCall" ? [part.name] : []);
661
699
  throw new AuditorTurnLimitError(AUDITOR_TURN_LIMIT, turns, {
662
700
  stopReason: boundaryResponse.stopReason,
@@ -728,7 +766,11 @@ export async function executeAuditorChild(options) {
728
766
  || (!decisionSubmitted && decision === undefined)) {
729
767
  throw new Error(`${options.roleLabel} exited without a readable decision receipt`);
730
768
  }
731
- return { decision, response };
769
+ return {
770
+ decision,
771
+ response: { ...response, usage: sessionUsage },
772
+ ...(noReceiptLifecycle === undefined ? {} : { noReceiptLifecycle }),
773
+ };
732
774
  }
733
775
  finally {
734
776
  options.signal?.removeEventListener("abort", abort);
@@ -438,8 +438,13 @@ function createNavigatorAttendance(options) {
438
438
  ${text}
439
439
  </role_help>`).join("\n");
440
440
  let output;
441
+ let prepareBatchRejected = false;
441
442
  outputSink = (value) => {
442
- if (output !== void 0) throw new Error("Navigator preparation must submit exactly one typed candidate batch");
443
+ if (prepareBatchRejected || output !== void 0) {
444
+ output = void 0;
445
+ prepareBatchRejected = true;
446
+ throw new Error("Navigator preparation must submit exactly one typed candidate batch");
447
+ }
443
448
  output = value;
444
449
  };
445
450
  const tool = createNavigatorPrepareTool((value) => {
@@ -553,6 +558,7 @@ ${helpContext}
553
558
  const delivery = createReceiptDeliveryPolicy();
554
559
  const promptAllowingRejectedPrepare = async (text, deliveryRequest) => {
555
560
  const entryStart = activeSession.entries().length;
561
+ prepareBatchRejected = false;
556
562
  let promptFailure;
557
563
  try {
558
564
  await activeSession.prompt(text);
@@ -565,6 +571,8 @@ ${helpContext}
565
571
  }
566
572
  const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
567
573
  if (rejectedReason !== void 0) {
574
+ output = void 0;
575
+ prepareBatchRejected = true;
568
576
  delivery.recordRejected(rejectedReason);
569
577
  return;
570
578
  }