@akagilnc/pi-workflow-roles 0.1.1815 → 0.1.1831
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/dist/audit-escalation.js +1 -1
- package/dist/compliance-transport.js +7 -6
- package/dist/evidence-child-executor.js +65 -23
- package/dist/navigator-attendance.js +9 -1
- package/dist/public-cli/main.js +5 -3
- package/dist/receipt-delivery-policy.js +14 -5
- package/package.json +1 -1
- package/src/audit-escalation.ts +5 -1
- package/src/compliance-transport.ts +9 -6
- package/src/doctor-role.ts +1 -1
- package/src/evidence-child-executor.ts +65 -27
- package/src/judge-role.ts +2 -1
- package/src/navigator-attendance.ts +14 -1
- package/src/receipt-delivery-policy.ts +20 -8
- package/src/reviewer-role.ts +2 -2
- package/src/role-runtime.ts +3 -2
package/dist/audit-escalation.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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)
|
|
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
|
}
|
package/dist/public-cli/main.js
CHANGED
|
@@ -17909,12 +17909,15 @@ function isRecord4(value) {
|
|
|
17909
17909
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17910
17910
|
}
|
|
17911
17911
|
function parseNoReceiptLifecycleFacts(input) {
|
|
17912
|
-
if (!isRecord4(input) || typeof input.terminalToolCalled !== "boolean" || input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT || input.sessionCompletion !== "settled-without-accepted-receipt" || input.acceptedReceipt !== false || typeof input.runPointer !== "string" || input.runPointer.trim() === "" || typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === "" || !Array.isArray(input.rejectedReceipts) || !input.rejectedReceipts.every((item) => isRecord4(item) && typeof item.reason === "string"
|
|
17912
|
+
if (!isRecord4(input) || typeof input.terminalToolCalled !== "boolean" || input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT || input.sessionCompletion !== "settled-without-accepted-receipt" || input.acceptedReceipt !== false || typeof input.runPointer !== "string" || input.runPointer.trim() === "" || typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === "" || !Array.isArray(input.rejectedReceipts) || !input.rejectedReceipts.every((item) => isRecord4(item) && typeof item.reason === "string")) {
|
|
17913
17913
|
throw new TypeError("malformed no-receipt lifecycle facts");
|
|
17914
17914
|
}
|
|
17915
17915
|
return {
|
|
17916
17916
|
terminalToolCalled: input.terminalToolCalled,
|
|
17917
|
-
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
17917
|
+
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
17918
|
+
reason: item.reason,
|
|
17919
|
+
diagnosticAvailable: item.reason.trim() !== ""
|
|
17920
|
+
})),
|
|
17918
17921
|
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
17919
17922
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
17920
17923
|
runPointer: input.runPointer,
|
|
@@ -17967,7 +17970,6 @@ var init_compliance_transport = __esm({
|
|
|
17967
17970
|
init_build();
|
|
17968
17971
|
init_evidence_child_executor();
|
|
17969
17972
|
init_auditor_dossier_tool();
|
|
17970
|
-
init_receipt_delivery_policy();
|
|
17971
17973
|
nonblank2 = typebox_exports.String({ minLength: 1, pattern: "\\S" });
|
|
17972
17974
|
decisionGateSchema = typebox_exports.Object({ question: nonblank2, options: typebox_exports.Array(nonblank2, { minItems: 1 }) }, { additionalProperties: false });
|
|
17973
17975
|
complianceDecisionSchema = typebox_exports.Object({ status: typebox_exports.Unknown({ description: "Auditor decision status." }), violations: typebox_exports.Array(nonblank2, { description: "Observed compliance violations." }), conflicts: typebox_exports.Array(nonblank2, { description: "Unresolved authority or execution conflicts." }), decisionGate: typebox_exports.Union([decisionGateSchema, typebox_exports.Null()], { description: "Escalation question and available options." }) }, { additionalProperties: true, required: [] });
|
|
@@ -16,12 +16,15 @@ export function parseNoReceiptLifecycleFacts(input) {
|
|
|
16
16
|
|| typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === ""
|
|
17
17
|
|| !Array.isArray(input.rejectedReceipts)
|
|
18
18
|
|| !input.rejectedReceipts.every((item) => isRecord(item)
|
|
19
|
-
&& typeof item.reason === "string"
|
|
19
|
+
&& typeof item.reason === "string")) {
|
|
20
20
|
throw new TypeError("malformed no-receipt lifecycle facts");
|
|
21
21
|
}
|
|
22
22
|
return {
|
|
23
23
|
terminalToolCalled: input.terminalToolCalled,
|
|
24
|
-
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
24
|
+
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
25
|
+
reason: item.reason,
|
|
26
|
+
diagnosticAvailable: item.reason.trim() !== "",
|
|
27
|
+
})),
|
|
25
28
|
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
26
29
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
27
30
|
runPointer: input.runPointer,
|
|
@@ -35,7 +38,10 @@ export function noReceiptLifecycleFacts(input) {
|
|
|
35
38
|
}
|
|
36
39
|
return {
|
|
37
40
|
terminalToolCalled: input.terminalToolCalled,
|
|
38
|
-
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
41
|
+
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
42
|
+
reason,
|
|
43
|
+
diagnosticAvailable: reason.trim() !== "",
|
|
44
|
+
})),
|
|
39
45
|
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
40
46
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
41
47
|
runPointer: input.runPointer,
|
|
@@ -49,12 +55,15 @@ export function createReceiptDeliveryPolicy() {
|
|
|
49
55
|
let deliveryTurns = 0;
|
|
50
56
|
const rejectedReceipts = [];
|
|
51
57
|
return {
|
|
52
|
-
recordAccepted() {
|
|
58
|
+
recordAccepted() {
|
|
59
|
+
accepted = true;
|
|
60
|
+
terminalToolCalled = true;
|
|
61
|
+
},
|
|
53
62
|
/** Infrastructure owns terminality and must never trigger receipt催交. */
|
|
54
63
|
stopForInfrastructure() { accepted = true; },
|
|
55
64
|
recordRejected(reason) {
|
|
56
65
|
terminalToolCalled = true;
|
|
57
|
-
rejectedReceipts.push({ reason });
|
|
66
|
+
rejectedReceipts.push({ reason, diagnosticAvailable: reason.trim() !== "" });
|
|
58
67
|
deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
|
|
59
68
|
},
|
|
60
69
|
recordDeliveryRequest() {
|
package/package.json
CHANGED
package/src/audit-escalation.ts
CHANGED
|
@@ -148,6 +148,7 @@ export type ComplianceDecisionHandlers<T> = {
|
|
|
148
148
|
/** Project the accepted parent candidate beside the typed audit-leg facts. */
|
|
149
149
|
noReceipt?: (
|
|
150
150
|
facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
|
|
151
|
+
usageProjection: { usage?: Usage },
|
|
151
152
|
) => T | PromiseLike<T>;
|
|
152
153
|
revise: (violations: readonly unknown[]) => T | PromiseLike<T>;
|
|
153
154
|
escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
|
|
@@ -172,7 +173,10 @@ export async function disposeComplianceDecision<T>(
|
|
|
172
173
|
if (handlers.noReceipt === undefined) {
|
|
173
174
|
throw new Error("Compliance no-receipt projection handler is unavailable");
|
|
174
175
|
}
|
|
175
|
-
return await handlers.noReceipt(
|
|
176
|
+
return await handlers.noReceipt(
|
|
177
|
+
decision,
|
|
178
|
+
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
179
|
+
);
|
|
176
180
|
case "revise":
|
|
177
181
|
return await handlers.revise(decision.violations);
|
|
178
182
|
case "escalate":
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./evidence-child-executor.ts";
|
|
8
8
|
import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
|
|
9
9
|
import type { DossierObservation } from "./dossier-resolution.ts";
|
|
10
|
-
import {
|
|
10
|
+
import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
11
11
|
|
|
12
12
|
export type ComplianceCompletion = AuditorCompletion;
|
|
13
13
|
export type ComplianceArgumentRootType = "null" | "array" | "undefined" | "string" | "number" | "boolean" | "bigint" | "symbol" | "function";
|
|
@@ -16,7 +16,7 @@ export type ComplianceAuditObservation =
|
|
|
16
16
|
| { kind: "object-status-unreadable"; status: "missing" | "unknown" }
|
|
17
17
|
| DossierObservation;
|
|
18
18
|
export type ComplianceAuditIncomplete = { status: "audit-incomplete"; observation: ComplianceAuditObservation; candidate: unknown; usage?: Usage };
|
|
19
|
-
export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt" };
|
|
19
|
+
export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
|
|
20
20
|
export type ComplianceDecision = { status: "pass"; usage?: Usage } | { status: "revise"; violations: readonly unknown[]; usage?: Usage } | { status: "escalate"; conflicts?: unknown; decisionGate?: unknown; usage?: Usage } | ComplianceNoReceipt | ComplianceAuditIncomplete;
|
|
21
21
|
export type ComplianceDispatch = { model: Model<Api>; auth: { apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> } };
|
|
22
22
|
|
|
@@ -128,9 +128,12 @@ export async function runComplianceAudit(options: RunComplianceAuditOptions): Pr
|
|
|
128
128
|
...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
|
|
129
129
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
130
130
|
});
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
if (receipt.noReceiptLifecycle !== undefined) {
|
|
132
|
+
return {
|
|
133
|
+
status: "no-receipt",
|
|
134
|
+
...receipt.noReceiptLifecycle,
|
|
135
|
+
...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
|
|
136
|
+
};
|
|
135
137
|
}
|
|
138
|
+
return readComplianceCandidate(receipt.decision, receipt.response.usage);
|
|
136
139
|
}
|
package/src/doctor-role.ts
CHANGED
|
@@ -12,7 +12,7 @@ export function createDoctorRoleRuntime(pi: ExtensionAPI, dependencies: DoctorRo
|
|
|
12
12
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
13
13
|
return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
|
|
14
14
|
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "Doctor Evidence", description: "Read retained Pi session bytes with bounded pagination.", parameters: doctorEvidenceReadSchema, async execute(_id, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("Doctor is not activated"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
15
|
-
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...details, auditNoReceipt }, terminate: true as const }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result, auditIncomplete: (result) => result }, details); } });
|
|
15
|
+
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "Doctor Output", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("Doctor is not activated"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "doctor candidate retention is unavailable", failed: "doctor candidate retention failed" }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: "Doctor output accepted" }], details, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: "Doctor output accepted; compliance audit produced no receipt" }], details: { ...details, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`Doctor output violates its soul: ${violations.join("; ")}`); }, escalate: (result) => result, auditIncomplete: (result) => result }, details); } });
|
|
16
16
|
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("Doctor is not activated"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "runtime-derived from retained session bytes and sealed into the accepted receipt", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
|
|
17
17
|
const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
|
|
18
18
|
}
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
|
|
33
33
|
import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
|
|
34
34
|
import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
|
|
35
|
-
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
|
|
35
|
+
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
36
36
|
|
|
37
37
|
// ── shared constants / types ──────────────────────────────────────────────
|
|
38
38
|
|
|
@@ -532,7 +532,7 @@ export type AuditorRoleOptions = {
|
|
|
532
532
|
*/
|
|
533
533
|
export async function executeAuditorChild(
|
|
534
534
|
options: AuditorRoleOptions,
|
|
535
|
-
): Promise<{ decision: unknown; response: AssistantMessage }> {
|
|
535
|
+
): Promise<{ decision: unknown; response: AssistantMessage; noReceiptLifecycle?: NoReceiptLifecycleFacts }> {
|
|
536
536
|
const { createRecordSession } = await import("./sitian-record-entry.ts");
|
|
537
537
|
|
|
538
538
|
return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
|
|
@@ -549,10 +549,12 @@ export async function executeAuditorChild(
|
|
|
549
549
|
const cwd = options.context.cwd ?? process.cwd();
|
|
550
550
|
|
|
551
551
|
let decision: unknown;
|
|
552
|
+
let noReceiptLifecycle: NoReceiptLifecycleFacts | undefined;
|
|
552
553
|
let decisionSubmitted = false;
|
|
553
554
|
let decisionCallId: string | undefined;
|
|
554
555
|
let decisionToolFailure: unknown;
|
|
555
556
|
const decisionToolFailures = new Map<string, unknown>();
|
|
557
|
+
const delivery = createReceiptDeliveryPolicy();
|
|
556
558
|
const tool = wrapPackageOwnedToolDefinition({
|
|
557
559
|
...options.tool,
|
|
558
560
|
label: options.roleLabel,
|
|
@@ -560,8 +562,12 @@ export async function executeAuditorChild(
|
|
|
560
562
|
if (decisionSubmitted && decisionCallId !== args[0]) {
|
|
561
563
|
throw new Error("Auditor decision was submitted more than once");
|
|
562
564
|
}
|
|
565
|
+
// Pi may already have issued several decision calls in one assistant
|
|
566
|
+
// response. Execute every issued call: the budget limits future
|
|
567
|
+
// solicitations, not terminal calls already in flight.
|
|
563
568
|
try {
|
|
564
569
|
const result = await options.tool.execute(...args);
|
|
570
|
+
delivery.recordAccepted();
|
|
565
571
|
decision = args[1];
|
|
566
572
|
decisionCallId = args[0];
|
|
567
573
|
decisionToolFailure = undefined;
|
|
@@ -614,11 +620,13 @@ export async function executeAuditorChild(
|
|
|
614
620
|
auditorSessionManager.appendCustomEntry(AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, binding);
|
|
615
621
|
|
|
616
622
|
let turns = 0;
|
|
623
|
+
const sessionUsage = emptyUsage();
|
|
617
624
|
let boundaryResponse: AssistantMessage | undefined;
|
|
618
625
|
let retentionFailure: unknown;
|
|
619
626
|
let retainedResponse: AssistantMessage | undefined;
|
|
620
627
|
let rejectedDecisionResponse: AssistantMessage | undefined;
|
|
621
628
|
let promptNeighboringFailure: unknown;
|
|
629
|
+
let promptDecisionFailures: unknown[] = [];
|
|
622
630
|
const registeredToolNames = new Set(session.getAllTools().map((entry) => entry.name));
|
|
623
631
|
const evidenceToolFailures = new Map<string, unknown>();
|
|
624
632
|
for (const name of registeredToolNames) {
|
|
@@ -645,9 +653,18 @@ export async function executeAuditorChild(
|
|
|
645
653
|
return [...session.messages].reverse().find((message) =>
|
|
646
654
|
message.role === "toolResult" && callIdSet.has(message.toolCallId) && message.isError);
|
|
647
655
|
};
|
|
656
|
+
const drainRejectedDecisionFailures = (response: AssistantMessage) => {
|
|
657
|
+
for (const part of response.content) {
|
|
658
|
+
if (part.type !== "toolCall" || part.name !== tool.name || !decisionToolFailures.has(part.id)) continue;
|
|
659
|
+
decisionToolFailure = decisionToolFailures.get(part.id);
|
|
660
|
+
promptDecisionFailures.push(decisionToolFailure);
|
|
661
|
+
decisionToolFailures.delete(part.id);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
648
664
|
const unsubscribe = session.subscribe((event) => {
|
|
649
665
|
if (event.type === "message_end" && event.message.role === "assistant" && boundaryResponse === undefined) {
|
|
650
666
|
turns += 1;
|
|
667
|
+
addUsage(sessionUsage, event.message.usage);
|
|
651
668
|
retainedResponse = event.message;
|
|
652
669
|
try { options.retainResponse?.(event.message); } catch (error) { retentionFailure = error; }
|
|
653
670
|
// A tool call in assistant output is only an observation. Preserve its
|
|
@@ -671,15 +688,11 @@ export async function executeAuditorChild(
|
|
|
671
688
|
if (event.type === "turn_end") {
|
|
672
689
|
if (rejectedDecisionResponse !== undefined) {
|
|
673
690
|
promptNeighboringFailure = findToolFailure(rejectedDecisionResponse);
|
|
674
|
-
|
|
675
|
-
part.type === "toolCall" && part.name === tool.name && decisionToolFailures.has(part.id));
|
|
676
|
-
if (rejectedCall?.type === "toolCall") {
|
|
677
|
-
decisionToolFailure = decisionToolFailures.get(rejectedCall.id);
|
|
678
|
-
decisionToolFailures.delete(rejectedCall.id);
|
|
679
|
-
}
|
|
691
|
+
drainRejectedDecisionFailures(rejectedDecisionResponse);
|
|
680
692
|
}
|
|
681
693
|
if (decisionSubmitted || promptNeighboringFailure !== undefined
|
|
682
|
-
|| boundaryResponse !== undefined
|
|
694
|
+
|| (boundaryResponse !== undefined && rejectedDecisionResponse === undefined)
|
|
695
|
+
|| retentionFailure !== undefined) {
|
|
683
696
|
void session.abort();
|
|
684
697
|
}
|
|
685
698
|
}
|
|
@@ -690,11 +703,11 @@ export async function executeAuditorChild(
|
|
|
690
703
|
|
|
691
704
|
try {
|
|
692
705
|
try {
|
|
693
|
-
const delivery = createReceiptDeliveryPolicy();
|
|
694
706
|
const promptAllowingRejectedDecision = async (prompt: string) => {
|
|
695
707
|
rejectedDecisionResponse = undefined;
|
|
696
708
|
promptNeighboringFailure = undefined;
|
|
697
709
|
decisionToolFailure = undefined;
|
|
710
|
+
promptDecisionFailures = [];
|
|
698
711
|
let promptFailure: unknown;
|
|
699
712
|
try {
|
|
700
713
|
await session.prompt(prompt);
|
|
@@ -707,41 +720,62 @@ export async function executeAuditorChild(
|
|
|
707
720
|
const correlatedResponse = rejectedDecisionResponse as AssistantMessage | undefined;
|
|
708
721
|
if (correlatedResponse !== undefined) {
|
|
709
722
|
promptNeighboringFailure ??= findToolFailure(correlatedResponse);
|
|
710
|
-
|
|
711
|
-
part.type === "toolCall" && part.name === tool.name && decisionToolFailures.has(part.id));
|
|
712
|
-
if (rejectedCall?.type === "toolCall") {
|
|
713
|
-
decisionToolFailure = decisionToolFailures.get(rejectedCall.id);
|
|
714
|
-
decisionToolFailures.delete(rejectedCall.id);
|
|
715
|
-
}
|
|
723
|
+
drainRejectedDecisionFailures(correlatedResponse);
|
|
716
724
|
}
|
|
717
725
|
// An adjacent failure outranks correctable decision feedback.
|
|
718
726
|
if (promptNeighboringFailure !== undefined) throw promptNeighboringFailure;
|
|
727
|
+
// An accepted correction in the same response owns the terminal
|
|
728
|
+
// outcome; correlated rejected siblings remain observations, not a
|
|
729
|
+
// stale failure capable of replacing that accepted receipt.
|
|
730
|
+
if (decisionSubmitted) {
|
|
731
|
+
decisionToolFailure = undefined;
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
719
734
|
if (decisionToolFailure !== undefined) return;
|
|
720
735
|
if (promptFailure !== undefined) throw promptFailure;
|
|
721
736
|
};
|
|
737
|
+
const chargeAndClearRejectedDecisionFailures = (failures: unknown[]) => {
|
|
738
|
+
for (const failure of failures) {
|
|
739
|
+
delivery.recordRejected(failure instanceof Error ? failure.message : String(failure));
|
|
740
|
+
}
|
|
741
|
+
decisionToolFailure = undefined;
|
|
742
|
+
promptDecisionFailures = [];
|
|
743
|
+
};
|
|
722
744
|
await promptAllowingRejectedDecision(options.prompt);
|
|
723
|
-
while (!decisionSubmitted && boundaryResponse === undefined
|
|
724
|
-
&& delivery.nextAction() === "request-delivery") {
|
|
745
|
+
while (!decisionSubmitted && (boundaryResponse === undefined || decisionToolFailure !== undefined)
|
|
746
|
+
&& inherited.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
|
|
725
747
|
if (decisionToolFailure !== undefined) {
|
|
726
|
-
|
|
727
|
-
|
|
748
|
+
const failures = promptDecisionFailures.length === 0
|
|
749
|
+
? [decisionToolFailure]
|
|
750
|
+
: promptDecisionFailures;
|
|
751
|
+
chargeAndClearRejectedDecisionFailures(failures);
|
|
752
|
+
if (delivery.nextAction() === "no-receipt") boundaryResponse = undefined;
|
|
728
753
|
if (delivery.nextAction() === "request-delivery") {
|
|
729
|
-
|
|
754
|
+
// A rejection and its correction solicitation are one budget unit;
|
|
755
|
+
// recordRejected already charged it.
|
|
756
|
+
if (retainedResponse === rejectedDecisionResponse) {
|
|
757
|
+
await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
|
|
758
|
+
chargeAndClearRejectedDecisionFailures(promptDecisionFailures);
|
|
759
|
+
}
|
|
730
760
|
}
|
|
731
761
|
} else {
|
|
732
762
|
delivery.recordDeliveryRequest();
|
|
733
763
|
await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
|
|
734
764
|
}
|
|
735
765
|
}
|
|
736
|
-
if (!decisionSubmitted &&
|
|
766
|
+
if (!decisionSubmitted && inherited.streamFailure === undefined
|
|
737
767
|
&& delivery.nextAction() === "no-receipt") {
|
|
738
768
|
const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
|
|
739
769
|
const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
|
|
740
|
-
|
|
770
|
+
const facts = delivery.facts({ runPointer, attemptPointer });
|
|
771
|
+
decision = facts;
|
|
741
772
|
// Late turn_end feedback cannot overturn a lifecycle that has already
|
|
742
773
|
// charged this prompt to the exhausted shared budget.
|
|
743
774
|
decisionToolFailure = undefined;
|
|
744
|
-
auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
|
|
775
|
+
auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
|
|
776
|
+
// Provenance is granted only after the lifecycle owner persisted the
|
|
777
|
+
// current child record; accepted model arguments can never set it.
|
|
778
|
+
noReceiptLifecycle = facts;
|
|
745
779
|
}
|
|
746
780
|
} catch (error) {
|
|
747
781
|
if (options.signal?.aborted) throw options.signal.reason;
|
|
@@ -750,7 +784,7 @@ export async function executeAuditorChild(
|
|
|
750
784
|
}
|
|
751
785
|
if (options.signal?.aborted) throw options.signal.reason;
|
|
752
786
|
if (inherited.streamFailure !== undefined) throw inherited.streamFailure;
|
|
753
|
-
if (decisionToolFailure !== undefined) throw decisionToolFailure;
|
|
787
|
+
if (!decisionSubmitted && decisionToolFailure !== undefined) throw decisionToolFailure;
|
|
754
788
|
const relevantResponse = !decisionSubmitted
|
|
755
789
|
? boundaryResponse
|
|
756
790
|
: [...session.messages].reverse().find((message): message is AssistantMessage =>
|
|
@@ -760,7 +794,7 @@ export async function executeAuditorChild(
|
|
|
760
794
|
if (toolFailure !== undefined) throw toolFailure;
|
|
761
795
|
}
|
|
762
796
|
if (retentionFailure !== undefined && retainedResponse?.stopReason !== "error") throw retentionFailure;
|
|
763
|
-
if (boundaryResponse !== undefined && !decisionSubmitted) {
|
|
797
|
+
if (boundaryResponse !== undefined && !decisionSubmitted && noReceiptLifecycle === undefined) {
|
|
764
798
|
const toolNames = boundaryResponse.content.flatMap((part) => part.type === "toolCall" ? [part.name] : []);
|
|
765
799
|
throw new AuditorTurnLimitError(AUDITOR_TURN_LIMIT, turns, {
|
|
766
800
|
stopReason: boundaryResponse.stopReason,
|
|
@@ -841,7 +875,11 @@ export async function executeAuditorChild(
|
|
|
841
875
|
) {
|
|
842
876
|
throw new Error(`${options.roleLabel} exited without a readable decision receipt`);
|
|
843
877
|
}
|
|
844
|
-
return {
|
|
878
|
+
return {
|
|
879
|
+
decision,
|
|
880
|
+
response: { ...response, usage: sessionUsage },
|
|
881
|
+
...(noReceiptLifecycle === undefined ? {} : { noReceiptLifecycle }),
|
|
882
|
+
};
|
|
845
883
|
} finally {
|
|
846
884
|
options.signal?.removeEventListener("abort", abort);
|
|
847
885
|
unsubscribe();
|
package/src/judge-role.ts
CHANGED
|
@@ -135,10 +135,11 @@ export function createJudgeRoleRuntime(
|
|
|
135
135
|
terminate: true as const,
|
|
136
136
|
...(usage === undefined ? {} : { usage }),
|
|
137
137
|
}),
|
|
138
|
-
noReceipt: (auditNoReceipt) => ({
|
|
138
|
+
noReceipt: (auditNoReceipt, usageProjection) => ({
|
|
139
139
|
content: [{ type: "text" as const, text: "Judge verdict accepted; compliance audit produced no receipt" }],
|
|
140
140
|
details: { ...verdict, auditNoReceipt },
|
|
141
141
|
terminate: true as const,
|
|
142
|
+
...usageProjection,
|
|
142
143
|
}),
|
|
143
144
|
revise: (violations) => {
|
|
144
145
|
throw new Error(
|
|
@@ -704,8 +704,16 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
704
704
|
}
|
|
705
705
|
const helpContext = help.map(({ role, help: text }) => `<role_help role="${role}">\n${text}\n</role_help>`).join("\n");
|
|
706
706
|
let output: PrepareOutput | undefined;
|
|
707
|
+
let prepareBatchRejected = false;
|
|
707
708
|
outputSink = (value) => {
|
|
708
|
-
if (output !== undefined)
|
|
709
|
+
if (prepareBatchRejected || output !== undefined) {
|
|
710
|
+
// Tool executions in one assistant response are provisional until the
|
|
711
|
+
// whole response is known to contain exactly one submission. A
|
|
712
|
+
// duplicate invalidates the batch, including its first call.
|
|
713
|
+
output = undefined;
|
|
714
|
+
prepareBatchRejected = true;
|
|
715
|
+
throw new Error("Navigator preparation must submit exactly one typed candidate batch");
|
|
716
|
+
}
|
|
709
717
|
output = value;
|
|
710
718
|
};
|
|
711
719
|
const tool = createNavigatorPrepareTool((value) => { outputSink?.(value); });
|
|
@@ -805,6 +813,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
805
813
|
const delivery = createReceiptDeliveryPolicy();
|
|
806
814
|
const promptAllowingRejectedPrepare = async (text: string, deliveryRequest: boolean) => {
|
|
807
815
|
const entryStart = activeSession.entries().length;
|
|
816
|
+
prepareBatchRejected = false;
|
|
808
817
|
let promptFailure: unknown;
|
|
809
818
|
try {
|
|
810
819
|
await activeSession.prompt(text);
|
|
@@ -817,6 +826,10 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
817
826
|
}
|
|
818
827
|
const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
|
|
819
828
|
if (rejectedReason !== undefined) {
|
|
829
|
+
// A rejected call makes every provisional output from this prompt
|
|
830
|
+
// ineligible for publication before the correction turn starts.
|
|
831
|
+
output = undefined;
|
|
832
|
+
prepareBatchRejected = true;
|
|
820
833
|
delivery.recordRejected(rejectedReason);
|
|
821
834
|
return;
|
|
822
835
|
}
|
|
@@ -7,7 +7,7 @@ export const NO_RECEIPT_LIFECYCLE_ENTRY_TYPE = "ak-no-receipt-lifecycle" as cons
|
|
|
7
7
|
/** The sole schema shared by lifecycle owners and Terminal projections. */
|
|
8
8
|
export type NoReceiptLifecycleFacts = {
|
|
9
9
|
terminalToolCalled: boolean;
|
|
10
|
-
rejectedReceipts: readonly { reason: string }[];
|
|
10
|
+
rejectedReceipts: readonly { reason: string; diagnosticAvailable: boolean }[];
|
|
11
11
|
deliveryTurns: typeof RECEIPT_DELIVERY_TURN_LIMIT;
|
|
12
12
|
sessionCompletion: "settled-without-accepted-receipt";
|
|
13
13
|
runPointer: string;
|
|
@@ -30,12 +30,15 @@ export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycle
|
|
|
30
30
|
|| typeof input.attemptPointer !== "string" || input.attemptPointer.trim() === ""
|
|
31
31
|
|| !Array.isArray(input.rejectedReceipts)
|
|
32
32
|
|| !input.rejectedReceipts.every((item) => isRecord(item)
|
|
33
|
-
&& typeof item.reason === "string"
|
|
33
|
+
&& typeof item.reason === "string")) {
|
|
34
34
|
throw new TypeError("malformed no-receipt lifecycle facts");
|
|
35
35
|
}
|
|
36
36
|
return {
|
|
37
37
|
terminalToolCalled: input.terminalToolCalled,
|
|
38
|
-
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
38
|
+
rejectedReceipts: input.rejectedReceipts.map((item) => ({
|
|
39
|
+
reason: item.reason as string,
|
|
40
|
+
diagnosticAvailable: (item.reason as string).trim() !== "",
|
|
41
|
+
})),
|
|
39
42
|
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
40
43
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
41
44
|
runPointer: input.runPointer,
|
|
@@ -45,14 +48,20 @@ export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycle
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export function noReceiptLifecycleFacts(
|
|
48
|
-
input: Omit<NoReceiptLifecycleFacts, "deliveryTurns" | "sessionCompletion" | "acceptedReceipt"> & {
|
|
51
|
+
input: Omit<NoReceiptLifecycleFacts, "rejectedReceipts" | "deliveryTurns" | "sessionCompletion" | "acceptedReceipt"> & {
|
|
52
|
+
rejectedReceipts: readonly { reason: string }[];
|
|
53
|
+
deliveryTurns: number;
|
|
54
|
+
},
|
|
49
55
|
): NoReceiptLifecycleFacts {
|
|
50
56
|
if (input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT) {
|
|
51
57
|
throw new TypeError("no-receipt lifecycle requires an exhausted delivery budget");
|
|
52
58
|
}
|
|
53
59
|
return {
|
|
54
60
|
terminalToolCalled: input.terminalToolCalled,
|
|
55
|
-
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
61
|
+
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
62
|
+
reason,
|
|
63
|
+
diagnosticAvailable: reason.trim() !== "",
|
|
64
|
+
})),
|
|
56
65
|
deliveryTurns: RECEIPT_DELIVERY_TURN_LIMIT,
|
|
57
66
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
58
67
|
runPointer: input.runPointer,
|
|
@@ -65,14 +74,17 @@ export function createReceiptDeliveryPolicy() {
|
|
|
65
74
|
let accepted = false;
|
|
66
75
|
let terminalToolCalled = false;
|
|
67
76
|
let deliveryTurns = 0;
|
|
68
|
-
const rejectedReceipts: { reason: string }[] = [];
|
|
77
|
+
const rejectedReceipts: { reason: string; diagnosticAvailable: boolean }[] = [];
|
|
69
78
|
return {
|
|
70
|
-
recordAccepted() {
|
|
79
|
+
recordAccepted() {
|
|
80
|
+
accepted = true;
|
|
81
|
+
terminalToolCalled = true;
|
|
82
|
+
},
|
|
71
83
|
/** Infrastructure owns terminality and must never trigger receipt催交. */
|
|
72
84
|
stopForInfrastructure() { accepted = true; },
|
|
73
85
|
recordRejected(reason: string) {
|
|
74
86
|
terminalToolCalled = true;
|
|
75
|
-
rejectedReceipts.push({ reason });
|
|
87
|
+
rejectedReceipts.push({ reason, diagnosticAvailable: reason.trim() !== "" });
|
|
76
88
|
deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
|
|
77
89
|
},
|
|
78
90
|
recordDeliveryRequest() {
|
package/src/reviewer-role.ts
CHANGED
|
@@ -163,9 +163,9 @@ export function createReviewerRoleRuntime(pi: ExtensionAPI, dependencies: Review
|
|
|
163
163
|
try { await dependencies.shutdownAgent?.(); } catch (error) { hostActions.failInfrastructure(ledger.recordInfrastructureFailure(error), toolCtx, id); }
|
|
164
164
|
return { content: [{ type: "text" as const, text: "Reviewer report accepted" }], details: candidate, terminate: true as const, ...(usage === undefined ? {} : { usage }) };
|
|
165
165
|
},
|
|
166
|
-
noReceipt: async (auditNoReceipt) => {
|
|
166
|
+
noReceipt: async (auditNoReceipt, usageProjection) => {
|
|
167
167
|
try { await dependencies.shutdownAgent?.(); } catch (error) { hostActions.failInfrastructure(ledger.recordInfrastructureFailure(error), toolCtx, id); }
|
|
168
|
-
return { content: [{ type: "text" as const, text: "Reviewer report accepted; compliance audit produced no receipt" }], details: { ...candidate, auditNoReceipt }, terminate: true as const };
|
|
168
|
+
return { content: [{ type: "text" as const, text: "Reviewer report accepted; compliance audit produced no receipt" }], details: { ...candidate, auditNoReceipt }, terminate: true as const, ...usageProjection };
|
|
169
169
|
},
|
|
170
170
|
revise: (violations) => {
|
|
171
171
|
throw new AggregateError([], `Reviewer receipt rejected:\n${violations.join("\n")}`, { cause: Object.freeze([...violations]) });
|
package/src/role-runtime.ts
CHANGED
|
@@ -567,8 +567,9 @@ export function createRoleRuntimeExtension(
|
|
|
567
567
|
// whose queued next turn is consumed before settlement.
|
|
568
568
|
pi.on("agent_end", (event) => {
|
|
569
569
|
const lastMessage = event.messages.at(-1);
|
|
570
|
-
if (lastMessage?.role === "assistant"
|
|
571
|
-
|
|
570
|
+
if (lastMessage?.role === "assistant"
|
|
571
|
+
&& (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted")) {
|
|
572
|
+
// Provider failure/abort has no tool_result event; classify it here so the
|
|
572
573
|
// receipt policy cannot turn infrastructure death into an exit-0 lifecycle.
|
|
573
574
|
receiptDelivery.stopForInfrastructure();
|
|
574
575
|
return;
|