@akagilnc/pi-workflow-roles 0.1.1794 → 0.1.1815

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.
@@ -95,6 +95,13 @@ export async function disposeComplianceDecision(decision, handlers, deliveredOut
95
95
  switch (decision.status) {
96
96
  case "pass":
97
97
  return await handlers.pass(decision.usage);
98
+ case "no-receipt":
99
+ // The parent candidate remains accepted, but its parallel audit leg is a
100
+ // public typed fact and must not be collapsed into an ordinary pass.
101
+ if (handlers.noReceipt === undefined) {
102
+ throw new Error("Compliance no-receipt projection handler is unavailable");
103
+ }
104
+ return await handlers.noReceipt(decision);
98
105
  case "revise":
99
106
  return await handlers.revise(decision.violations);
100
107
  case "escalate":
@@ -1,6 +1,7 @@
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";
4
5
  /** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
5
6
  export const AUDITOR_DOSSIER_PROMPT = "Audit the current run dossier.";
6
7
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
@@ -73,5 +74,10 @@ export async function runComplianceAudit(options) {
73
74
  ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
74
75
  ...(options.signal === undefined ? {} : { signal: options.signal }),
75
76
  });
76
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
77
+ try {
78
+ return { status: "no-receipt", ...parseNoReceiptLifecycleFacts(receipt.decision) };
79
+ }
80
+ catch {
81
+ return readComplianceCandidate(receipt.decision, receipt.response.usage);
82
+ }
77
83
  }
@@ -10,6 +10,7 @@ import { createAssistantMessageEventStream, InMemoryCredentialStore, } from "@ea
10
10
  import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, prepareComplianceDispatch, } from "./compliance-transport.js";
11
11
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
12
12
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
13
+ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
13
14
  // ── shared constants / types ──────────────────────────────────────────────
14
15
  export const AUDITOR_TURN_LIMIT = 32;
15
16
  export const DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES = 2;
@@ -430,6 +431,7 @@ export async function executeAuditorChild(options) {
430
431
  let decisionSubmitted = false;
431
432
  let decisionCallId;
432
433
  let decisionToolFailure;
434
+ const decisionToolFailures = new Map();
433
435
  const tool = wrapPackageOwnedToolDefinition({
434
436
  ...options.tool,
435
437
  label: options.roleLabel,
@@ -441,11 +443,14 @@ export async function executeAuditorChild(options) {
441
443
  const result = await options.tool.execute(...args);
442
444
  decision = args[1];
443
445
  decisionCallId = args[0];
446
+ decisionToolFailure = undefined;
447
+ decisionToolFailures.delete(args[0]);
444
448
  decisionSubmitted = true;
445
449
  return result;
446
450
  }
447
451
  catch (error) {
448
452
  decisionToolFailure = error;
453
+ decisionToolFailures.set(args[0], error);
449
454
  throw error;
450
455
  }
451
456
  },
@@ -488,6 +493,8 @@ export async function executeAuditorChild(options) {
488
493
  let boundaryResponse;
489
494
  let retentionFailure;
490
495
  let retainedResponse;
496
+ let rejectedDecisionResponse;
497
+ let promptNeighboringFailure;
491
498
  const registeredToolNames = new Set(session.getAllTools().map((entry) => entry.name));
492
499
  const evidenceToolFailures = new Map();
493
500
  for (const name of registeredToolNames) {
@@ -526,24 +533,39 @@ export async function executeAuditorChild(options) {
526
533
  catch (error) {
527
534
  retentionFailure = error;
528
535
  }
536
+ // A tool call in assistant output is only an observation. Preserve its
537
+ // candidate for typed malformed-decision settlement, but the wrapped
538
+ // execute path above is the sole owner of accepted-receipt state; a
539
+ // rejected execution must remain retryable in this same session.
529
540
  for (const part of event.message.content) {
530
- if (part.type !== "toolCall" || part.name !== tool.name)
531
- continue;
532
- if (!decisionSubmitted) {
533
- decision = part.arguments;
534
- decisionCallId = part.id;
535
- decisionSubmitted = true;
536
- }
537
- else if (decisionCallId !== part.id) {
538
- decisionToolFailure = new Error("Auditor decision was submitted more than once");
541
+ if (part.type === "toolCall" && part.name === tool.name) {
542
+ rejectedDecisionResponse = event.message;
543
+ if (decision === undefined) {
544
+ decision = part.arguments;
545
+ decisionCallId = part.id;
546
+ // Pi can reject malformed root arguments before invoking execute;
547
+ // that remains the existing typed audit-incomplete candidate path.
548
+ if (part.arguments === undefined)
549
+ decisionSubmitted = true;
550
+ }
539
551
  }
540
552
  }
541
553
  if (turns >= AUDITOR_TURN_LIMIT)
542
554
  boundaryResponse = event.message;
543
555
  }
544
- if (event.type === "turn_end" &&
545
- (decisionSubmitted || boundaryResponse !== undefined || retentionFailure !== undefined)) {
546
- void session.abort();
556
+ if (event.type === "turn_end") {
557
+ if (rejectedDecisionResponse !== undefined) {
558
+ 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
+ }
564
+ }
565
+ if (decisionSubmitted || promptNeighboringFailure !== undefined
566
+ || boundaryResponse !== undefined || retentionFailure !== undefined) {
567
+ void session.abort();
568
+ }
547
569
  }
548
570
  });
549
571
  const abort = () => { void session.abort(); };
@@ -553,7 +575,63 @@ export async function executeAuditorChild(options) {
553
575
  options.signal?.addEventListener("abort", abort, { once: true });
554
576
  try {
555
577
  try {
556
- await session.prompt(options.prompt);
578
+ const delivery = createReceiptDeliveryPolicy();
579
+ const promptAllowingRejectedDecision = async (prompt) => {
580
+ rejectedDecisionResponse = undefined;
581
+ promptNeighboringFailure = undefined;
582
+ decisionToolFailure = undefined;
583
+ let promptFailure;
584
+ try {
585
+ await session.prompt(prompt);
586
+ }
587
+ catch (error) {
588
+ promptFailure = error;
589
+ }
590
+ // Prefer turn_end correlation, but Pi may reject prompt() before that
591
+ // event. In that case correlate against this prompt's captured decision
592
+ // response and call-id maps at the catch boundary.
593
+ const correlatedResponse = rejectedDecisionResponse;
594
+ if (correlatedResponse !== undefined) {
595
+ 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
+ }
601
+ }
602
+ // An adjacent failure outranks correctable decision feedback.
603
+ if (promptNeighboringFailure !== undefined)
604
+ throw promptNeighboringFailure;
605
+ if (decisionToolFailure !== undefined)
606
+ return;
607
+ if (promptFailure !== undefined)
608
+ throw promptFailure;
609
+ };
610
+ await promptAllowingRejectedDecision(options.prompt);
611
+ while (!decisionSubmitted && boundaryResponse === undefined && inherited.streamFailure === undefined
612
+ && delivery.nextAction() === "request-delivery") {
613
+ if (decisionToolFailure !== undefined) {
614
+ delivery.recordRejected(decisionToolFailure instanceof Error ? decisionToolFailure.message : String(decisionToolFailure));
615
+ decisionToolFailure = undefined;
616
+ if (delivery.nextAction() === "request-delivery") {
617
+ await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
618
+ }
619
+ }
620
+ else {
621
+ delivery.recordDeliveryRequest();
622
+ await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
623
+ }
624
+ }
625
+ if (!decisionSubmitted && boundaryResponse === undefined && inherited.streamFailure === undefined
626
+ && delivery.nextAction() === "no-receipt") {
627
+ const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
628
+ const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
629
+ decision = delivery.facts({ runPointer, attemptPointer });
630
+ // Late turn_end feedback cannot overturn a lifecycle that has already
631
+ // charged this prompt to the exhausted shared budget.
632
+ decisionToolFailure = undefined;
633
+ auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, decision);
634
+ }
557
635
  }
558
636
  catch (error) {
559
637
  if (options.signal?.aborted)
@@ -647,7 +725,7 @@ export async function executeAuditorChild(options) {
647
725
  if (response === undefined
648
726
  || response.stopReason === "error"
649
727
  || response.stopReason === "aborted"
650
- || !decisionSubmitted) {
728
+ || (!decisionSubmitted && decision === undefined)) {
651
729
  throw new Error(`${options.roleLabel} exited without a readable decision receipt`);
652
730
  }
653
731
  return { decision, response };
@@ -17,6 +17,7 @@ import { openInProcessAgentSession } from "./in-process-session.js";
17
17
  import { renderPublicAkRoleCommand } from "./public-command-renderer.js";
18
18
  import { issueRoot, subjectPath } from "./work-subject-identity.js";
19
19
  import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
20
+ import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
20
21
  const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance";
21
22
  const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare";
22
23
  const NAVIGATOR_DEFAULT_MODEL = "openai-codex/gpt-5.6-luna:max";
@@ -105,6 +106,28 @@ function unavailableKey(value) {
105
106
  function exactRecord(value) {
106
107
  return typeof value === "object" && value !== null && !Array.isArray(value);
107
108
  }
109
+ function rejectedPrepareReason(entries, start) {
110
+ const recent = entries.slice(start);
111
+ const prepareCalls = /* @__PURE__ */ new Set();
112
+ for (const entry of recent) {
113
+ if (!exactRecord(entry) || entry.type !== "message" || !exactRecord(entry.message) || entry.message.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
114
+ for (const part of entry.message.content) {
115
+ if (exactRecord(part) && part.type === "toolCall" && part.name === NAVIGATOR_PREPARE_TOOL_NAME && typeof part.id === "string") prepareCalls.add(part.id);
116
+ }
117
+ }
118
+ let reason;
119
+ for (const entry of recent) {
120
+ if (!exactRecord(entry) || entry.type !== "message" || !exactRecord(entry.message) || entry.message.role !== "toolResult" || entry.message.isError !== true) continue;
121
+ const callId = entry.message.toolCallId;
122
+ if (entry.message.toolName !== NAVIGATOR_PREPARE_TOOL_NAME || typeof callId !== "string" || !prepareCalls.has(callId)) {
123
+ return void 0;
124
+ }
125
+ const content = entry.message.content;
126
+ const text = Array.isArray(content) ? content.flatMap((part) => exactRecord(part) && typeof part.text === "string" ? [part.text] : []).join("") : typeof content === "string" ? content : "";
127
+ if (text.trim() !== "") reason = text.trim();
128
+ }
129
+ return reason;
130
+ }
108
131
  function targetIsValid(value) {
109
132
  if (!exactRecord(value) || !targetRoles.has(String(value.role))) return false;
110
133
  const metadata = packagedRoleMetadata(String(value.role));
@@ -327,6 +350,7 @@ function createNavigatorAttendance(options) {
327
350
  let settlementTail = Promise.resolve();
328
351
  let settlementFailure;
329
352
  let preparationFailure;
353
+ let preparationNoReceipt = false;
330
354
  let routePlaybookReadFailure;
331
355
  let disposed = false;
332
356
  let warmedHelp;
@@ -526,7 +550,38 @@ ${helpContext}
526
550
  try {
527
551
  try {
528
552
  if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
529
- await activeSession.prompt(request);
553
+ const delivery = createReceiptDeliveryPolicy();
554
+ const promptAllowingRejectedPrepare = async (text, deliveryRequest) => {
555
+ const entryStart = activeSession.entries().length;
556
+ let promptFailure;
557
+ try {
558
+ await activeSession.prompt(text);
559
+ } catch (error) {
560
+ promptFailure = error;
561
+ }
562
+ const providerFailure = activeSession.providerFailure?.();
563
+ if (providerFailure !== void 0) {
564
+ throw navigatorUnavailableError(providerFailure.source, promptFailure ?? "Navigator provider failure", providerFailure.cause);
565
+ }
566
+ const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
567
+ if (rejectedReason !== void 0) {
568
+ delivery.recordRejected(rejectedReason);
569
+ return;
570
+ }
571
+ if (promptFailure !== void 0) throw promptFailure;
572
+ if (deliveryRequest && output === void 0) delivery.recordDeliveryRequest();
573
+ };
574
+ await promptAllowingRejectedPrepare(request, false);
575
+ while (output === void 0 && delivery.nextAction() === "request-delivery") {
576
+ await promptAllowingRejectedPrepare(RECEIPT_DELIVERY_PROMPT, true);
577
+ }
578
+ if (output === void 0 && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === void 0) {
579
+ const facts = delivery.facts({ runPointer: sessionDir, attemptPointer: invocationId });
580
+ activeSession.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
581
+ preparationNoReceipt = true;
582
+ candidates = [];
583
+ return candidates;
584
+ }
530
585
  } catch (error) {
531
586
  throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
532
587
  }
@@ -655,22 +710,25 @@ ${helpContext}
655
710
  throw new Error("Navigator advice contradicts the accepted settlement");
656
711
  }
657
712
  }
658
- if (selected?.next === void 0) {
713
+ if (selected?.next === void 0 && preparationNoReceipt) {
714
+ report = { disposition: "no-advice" };
715
+ } else if (selected?.next === void 0) {
659
716
  throw new Error("Navigator prepared no machine-usable next direction");
660
- }
661
- const selectedRoute = selected.route;
662
- const routeChanged = selectedRoute !== void 0 && !routeEqual(previousRoute, selectedRoute);
663
- const command = renderPublicAkRoleCommand(selected.next);
664
- report = {
665
- disposition: "recommendation",
666
- ...routeChanged ? { route: selectedRoute } : {},
667
- next: selected.next,
668
- ...selected.reason === void 0 ? {} : { reason: oneLine(selected.reason) },
669
- ...command === void 0 ? {} : { command }
670
- };
671
- if (selectedRoute !== void 0) {
672
- previousRoute = selectedRoute;
673
- session?.appendEntry(ROUTE_ENTRY, { invocationId, subjectKey, route: selectedRoute });
717
+ } else {
718
+ const selectedRoute = selected.route;
719
+ const routeChanged = selectedRoute !== void 0 && !routeEqual(previousRoute, selectedRoute);
720
+ const command = renderPublicAkRoleCommand(selected.next);
721
+ report = {
722
+ disposition: "recommendation",
723
+ ...routeChanged ? { route: selectedRoute } : {},
724
+ next: selected.next,
725
+ ...selected.reason === void 0 ? {} : { reason: oneLine(selected.reason) },
726
+ ...command === void 0 ? {} : { command }
727
+ };
728
+ if (selectedRoute !== void 0) {
729
+ previousRoute = selectedRoute;
730
+ session?.appendEntry(ROUTE_ENTRY, { invocationId, subjectKey, route: selectedRoute });
731
+ }
674
732
  }
675
733
  } catch (error) {
676
734
  report = unavailable(invocationId, error);
@@ -704,6 +762,7 @@ ${helpContext}
704
762
  sessionReady = void 0;
705
763
  candidates = void 0;
706
764
  preparationFailure = void 0;
765
+ preparationNoReceipt = false;
707
766
  routePlaybookSettlement = void 0;
708
767
  routePlaybookReadFailure = void 0;
709
768
  }