@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.
- 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 +187 -224
- package/dist/receipt-delivery-policy.js +14 -5
- package/dist/sitian-record-entry.js +24 -29
- package/dist/sitian-role-run-coordinates.js +28 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- 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/public-cli/invocation.ts +16 -72
- package/src/public-cli/merger-run.ts +4 -16
- package/src/public-cli/run-lifecycle.ts +8 -10
- package/src/receipt-delivery-policy.ts +20 -8
- package/src/reviewer-role.ts +2 -2
- package/src/role-runtime.ts +3 -2
- package/src/sitian-record-entry.ts +4 -0
- package/src/sitian-role-run-coordinates.ts +45 -0
|
@@ -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() {
|
|
@@ -1,33 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 司天台唯一 Pi session 记录落盘入口(ADR 0065)。
|
|
3
|
-
* 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
|
|
4
|
-
* 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
|
|
5
|
-
*/
|
|
6
1
|
import { dirname, resolve, join } from "node:path";
|
|
7
2
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
8
3
|
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
? join(dirname(parentResolved), options.kind)
|
|
30
|
-
: join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
31
|
-
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
32
|
-
return SessionManager.create(cwd, sessionDir, { parentSession: parentFile });
|
|
4
|
+
import {
|
|
5
|
+
roleRunSessionCoordinates
|
|
6
|
+
} from "./sitian-role-run-coordinates.js";
|
|
7
|
+
import {
|
|
8
|
+
activationBookDirectory,
|
|
9
|
+
ensureRealDirectoryTree,
|
|
10
|
+
physicallyContainedIn,
|
|
11
|
+
resolveActivationLedgerHome
|
|
12
|
+
} from "./activation-ledger-topology.js";
|
|
13
|
+
function createRecordSession(options) {
|
|
14
|
+
const cwd = options.cwd;
|
|
15
|
+
const parentFile = options.parent?.getSessionFile();
|
|
16
|
+
if (parentFile === void 0 || parentFile.length === 0) {
|
|
17
|
+
return SessionManager.inMemory(cwd);
|
|
18
|
+
}
|
|
19
|
+
const ledgerHome = resolveActivationLedgerHome();
|
|
20
|
+
const parentResolved = resolve(parentFile);
|
|
21
|
+
const sessionDir = physicallyContainedIn(ledgerHome, parentResolved) ? join(dirname(parentResolved), options.kind) : join(activationBookDirectory(ledgerHome, resolveBookKeyFromGit(cwd)), options.kind);
|
|
22
|
+
ensureRealDirectoryTree(ledgerHome, sessionDir);
|
|
23
|
+
return SessionManager.create(cwd, sessionDir, { parentSession: parentFile });
|
|
33
24
|
}
|
|
25
|
+
export {
|
|
26
|
+
createRecordSession,
|
|
27
|
+
roleRunSessionCoordinates
|
|
28
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
|
|
3
|
+
import {
|
|
4
|
+
activationBookDirectory,
|
|
5
|
+
resolveActivationLedgerHome
|
|
6
|
+
} from "./activation-ledger-topology.js";
|
|
7
|
+
function roleRunSessionCoordinates(options) {
|
|
8
|
+
const ledgerHome = resolveActivationLedgerHome(
|
|
9
|
+
options.home === void 0 ? void 0 : () => options.home
|
|
10
|
+
);
|
|
11
|
+
const bookKey = resolveBookKeyFromGit(options.cwd);
|
|
12
|
+
const runDirectory = join(
|
|
13
|
+
activationBookDirectory(ledgerHome, bookKey),
|
|
14
|
+
"runs",
|
|
15
|
+
`${options.runId}@${options.role}`
|
|
16
|
+
);
|
|
17
|
+
const sessionDirectory = join(runDirectory, "session");
|
|
18
|
+
return {
|
|
19
|
+
ledgerHome,
|
|
20
|
+
bookKey,
|
|
21
|
+
runDirectory,
|
|
22
|
+
sessionDirectory,
|
|
23
|
+
sessionFile: join(sessionDirectory, "session.jsonl")
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
roleRunSessionCoordinates
|
|
28
|
+
};
|
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
|
}
|