@akagilnc/pi-workflow-roles 0.1.5153 → 0.1.5194
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/acp-host/production-host.js +13086 -13034
- package/dist/archivist-record-entry.js +4 -20
- package/dist/archivist-record-pointer.js +28 -0
- package/dist/audit-escalation.js +3 -119
- package/dist/auditor-soul.js +32 -0
- package/dist/doctor-auditor.js +26 -0
- package/dist/headless-host/production-host.js +11218 -11166
- package/dist/judge-role.js +73 -0
- package/dist/migrate-book-topology.js +8 -0
- package/dist/package-contracts/judge-output.js +1 -18
- package/dist/package-contracts/terminating-tools.js +2 -2
- package/dist/public-cli/cli.js +2 -2
- package/dist/public-cli/countersign-run.js +11 -0
- package/dist/public-cli/instruction-seat-run.js +428 -48
- package/dist/public-cli/invocation.js +10 -5
- package/dist/public-cli/main.js +10049 -8911
- package/dist/public-cli/settlement.js +28 -8
- package/dist/public-role-summons.js +6 -4
- package/dist/submission-errors.js +1 -1
- package/dist/submission-gate.js +165 -0
- package/dist/submission-ledger.js +0 -27
- package/dist/worker-submission-contracts.js +8 -0
- package/package.json +1 -1
- package/src/archivist-record-entry.ts +5 -49
- package/src/archivist-record-pointer.ts +49 -0
- package/src/audit-escalation.ts +4 -217
- package/src/auditor-soul.ts +33 -0
- package/src/doctor-role.ts +11 -5
- package/src/gatekeeper-role.ts +2 -2
- package/src/host-contracts.ts +0 -25
- package/src/judge-role.ts +63 -90
- package/src/package-contracts/judge-output.ts +0 -17
- package/src/package-contracts/terminating-tools.ts +0 -2
- package/src/pi/adapter.ts +1 -33
- package/src/public-cli/cli.ts +2 -2
- package/src/public-cli/countersign-run.ts +11 -0
- package/src/public-cli/instruction-seat-run.ts +433 -16
- package/src/public-cli/invocation.ts +9 -3
- package/src/public-cli/post-admission.ts +0 -1
- package/src/public-cli/settlement.ts +32 -8
- package/src/public-role-summons.ts +9 -6
- package/src/role-envelope.ts +0 -32
- package/src/role-runtime-dependencies.ts +0 -4
- package/src/role-runtime.ts +15 -218
- package/src/submission-errors.ts +1 -1
- package/src/submission-gate.ts +46 -10
- package/src/submission-ledger.ts +0 -27
- package/src/worker-role.ts +23 -102
- package/src/worker-submission-contracts.ts +8 -0
- package/src/worker-submission-gates.ts +2 -2
package/src/audit-escalation.ts
CHANGED
|
@@ -1,220 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
ComplianceDecision,
|
|
5
|
-
} from "./compliance-transport.ts";
|
|
6
|
-
import { GatekeeperDecisionError } from "./submission-errors.ts";
|
|
7
|
-
import type { SubmissionGateNonPassResult } from "./gatekeeper-role.ts";
|
|
8
|
-
|
|
1
|
+
/** Historical audit-escalation discriminator retained for reading pre-#1057 run artifacts. */
|
|
9
2
|
export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const AUDIT_ESCALATION_LIVE_REGISTRY = new WeakSet<object>();
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Escalation delivery face.
|
|
18
|
-
* `kind` / `conflicts` / `auditDecisionGate` are audit-owned fields.
|
|
19
|
-
* Role-delivered fields ride beside them as open content (ADR 0055) — including
|
|
20
|
-
* a role `decisionGate` when present. The index signature tells that truth so
|
|
21
|
-
* callers never need a cast to retain role output.
|
|
22
|
-
*/
|
|
23
|
-
export type AuditEscalationResult = {
|
|
24
|
-
readonly kind: typeof AUDIT_ESCALATION_KIND;
|
|
25
|
-
/** Raw audit-owned conflicts field, when the auditor supplied one. */
|
|
26
|
-
readonly conflicts?: unknown;
|
|
27
|
-
/** Raw audit-owned gate field, when the auditor supplied one. */
|
|
28
|
-
readonly auditDecisionGate?: unknown;
|
|
29
|
-
readonly [key: string]: unknown;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type AuditEscalationToolResult = {
|
|
33
|
-
content: [];
|
|
34
|
-
details: AuditEscalationResult;
|
|
35
|
-
terminate: true;
|
|
36
|
-
usage?: Usage;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export type OfficerAuditEscalationDecision = {
|
|
40
|
-
readonly status: "escalate";
|
|
41
|
-
readonly officer?: "inspector" | "notary" | "auditor" | "countersign";
|
|
42
|
-
readonly reason?: unknown;
|
|
43
|
-
readonly findings?: unknown;
|
|
44
|
-
readonly conflicts?: unknown;
|
|
45
|
-
readonly decisionGate?: unknown;
|
|
46
|
-
readonly usage?: Usage;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type AuditEscalationDecision =
|
|
50
|
-
| Extract<ComplianceDecision, { status: "escalate" }>
|
|
51
|
-
| OfficerAuditEscalationDecision;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Build the escalation delivery face.
|
|
55
|
-
* Role-delivered fields ride under the escalation discriminator (ADR 0055).
|
|
56
|
-
* `kind` always wins so the discriminator cannot be laundered.
|
|
57
|
-
* `conflicts` and `auditDecisionGate`, when present, always come from the
|
|
58
|
-
* audit (why we escalated and its gate). Raw ancillary values are not repaired.
|
|
59
|
-
* A role `decisionGate`, when present, stays at its own key via spread and is
|
|
60
|
-
* never overwritten (not folded, not dropped, not swapped into the audit home).
|
|
61
|
-
*/
|
|
62
|
-
export function buildAuditEscalationResult(
|
|
63
|
-
decision: AuditEscalationDecision,
|
|
64
|
-
deliveredOutput?: unknown,
|
|
65
|
-
): AuditEscalationResult {
|
|
66
|
-
const auditOwned: Record<string, unknown> = {
|
|
67
|
-
kind: AUDIT_ESCALATION_KIND,
|
|
68
|
-
};
|
|
69
|
-
if (Object.hasOwn(decision, "receipt")) {
|
|
70
|
-
auditOwned.receipt = (decision as { receipt?: unknown }).receipt;
|
|
71
|
-
}
|
|
72
|
-
if (Object.hasOwn(decision, "conflicts")) {
|
|
73
|
-
auditOwned.conflicts = (decision as { conflicts?: unknown }).conflicts;
|
|
74
|
-
}
|
|
75
|
-
if (Object.hasOwn(decision, "decisionGate")) {
|
|
76
|
-
auditOwned.auditDecisionGate = (decision as { decisionGate?: unknown }).decisionGate;
|
|
77
|
-
}
|
|
78
|
-
if (Object.hasOwn(decision, "reason") && (decision as { reason?: unknown }).reason !== undefined) {
|
|
79
|
-
auditOwned.reason = (decision as { reason?: unknown }).reason;
|
|
80
|
-
}
|
|
81
|
-
if (Object.hasOwn(decision, "officer") && (decision as { officer?: unknown }).officer !== undefined) {
|
|
82
|
-
auditOwned.officer = (decision as { officer?: unknown }).officer;
|
|
83
|
-
}
|
|
84
|
-
if (Object.hasOwn(decision, "findings") && (decision as { findings?: unknown }).findings !== undefined) {
|
|
85
|
-
auditOwned.findings = (decision as { findings?: unknown }).findings;
|
|
86
|
-
}
|
|
87
|
-
const result = {
|
|
88
|
-
kind: AUDIT_ESCALATION_KIND,
|
|
89
|
-
...(deliveredOutput !== undefined ? { receipt: deliveredOutput } : {}),
|
|
90
|
-
audit: auditOwned,
|
|
91
|
-
} as AuditEscalationResult;
|
|
92
|
-
AUDIT_ESCALATION_LIVE_REGISTRY.add(result);
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** True only for the audit-owned live projection, never for role-shaped data. */
|
|
97
|
-
export function isAuditEscalationProjection(
|
|
98
|
-
value: unknown,
|
|
99
|
-
): value is AuditEscalationResult {
|
|
100
|
-
if (!isAuditEscalationResult(value)) return false;
|
|
101
|
-
return AUDIT_ESCALATION_LIVE_REGISTRY.has(value);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function projectAuditEscalation(
|
|
105
|
-
decision: AuditEscalationDecision,
|
|
106
|
-
deliveredOutput?: unknown,
|
|
107
|
-
): AuditEscalationToolResult {
|
|
108
|
-
const details = buildAuditEscalationResult(decision, deliveredOutput);
|
|
109
|
-
return {
|
|
110
|
-
content: [],
|
|
111
|
-
details,
|
|
112
|
-
terminate: true,
|
|
113
|
-
...(decision.usage === undefined ? {} : { usage: decision.usage }),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Preserve the escalating officer's own gate alongside the raw receipt. */
|
|
118
|
-
export function projectGatekeeperEscalation(
|
|
119
|
-
result: Extract<SubmissionGateNonPassResult, { status: "escalate" }>,
|
|
120
|
-
deliveredOutput?: unknown,
|
|
121
|
-
): AuditEscalationToolResult {
|
|
122
|
-
const receipt = result.receipt;
|
|
123
|
-
const record = receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
|
|
124
|
-
? receipt as Record<string, unknown>
|
|
125
|
-
: undefined;
|
|
126
|
-
return projectAuditEscalation({
|
|
127
|
-
status: "escalate",
|
|
128
|
-
officer: result.officer,
|
|
129
|
-
conflicts: receipt,
|
|
130
|
-
...(record !== undefined && Object.hasOwn(record, "decisionGate")
|
|
131
|
-
? { decisionGate: record.decisionGate }
|
|
132
|
-
: {}),
|
|
133
|
-
}, deliveredOutput);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Discriminator-only recognition (ADR 0040). Shape of conflicts/options/gate
|
|
138
|
-
* is not a reject gate — element types and cardinality are delivery content.
|
|
139
|
-
*/
|
|
140
|
-
export function isAuditEscalationResult(
|
|
141
|
-
value: unknown,
|
|
142
|
-
): value is AuditEscalationResult {
|
|
143
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
return (value as Record<string, unknown>).kind === AUDIT_ESCALATION_KIND;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export type ComplianceDecisionHandlers<T> = {
|
|
150
|
-
converged: (usage: Usage | undefined) => T | PromiseLike<T>;
|
|
151
|
-
/** Project the accepted parent candidate beside the typed audit-leg facts. */
|
|
152
|
-
noReceipt?: (
|
|
153
|
-
facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
|
|
154
|
-
usageProjection: { usage?: Usage },
|
|
155
|
-
) => T | PromiseLike<T>;
|
|
156
|
-
/**
|
|
157
|
-
* Parent work stands with the auditor's raw reply (#757).
|
|
158
|
-
* Not a shape-unreadable judgment — the reply is what the auditor said.
|
|
159
|
-
*/
|
|
160
|
-
received?: (
|
|
161
|
-
facts: Extract<ComplianceDecision, { status: "received" }>,
|
|
162
|
-
usageProjection: { usage?: Usage },
|
|
163
|
-
) => T | PromiseLike<T>;
|
|
164
|
-
continue: (violations: readonly unknown[]) => T | PromiseLike<T>;
|
|
165
|
-
escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
|
|
166
|
-
/** Host failure beside any recorded auditor payloads — parent is not accepted. */
|
|
167
|
-
transportFailure?: (
|
|
168
|
-
facts: Extract<ComplianceDecision, { status: "transport_failure" }>,
|
|
169
|
-
) => T | PromiseLike<T>;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Dispose a parsed audit decision without repeating status handling in roles.
|
|
174
|
-
* Role output already delivered is preserved on the escalate face (ADR 0055).
|
|
175
|
-
*/
|
|
176
|
-
export async function disposeComplianceDecision<T>(
|
|
177
|
-
decision: ComplianceDecision,
|
|
178
|
-
handlers: ComplianceDecisionHandlers<T>,
|
|
179
|
-
deliveredOutput?: unknown,
|
|
180
|
-
): Promise<Awaited<T>> {
|
|
181
|
-
switch (decision.status) {
|
|
182
|
-
case "converged":
|
|
183
|
-
return await handlers.converged(decision.usage);
|
|
184
|
-
case "no-receipt":
|
|
185
|
-
// The parent candidate remains accepted, but its parallel audit leg is a
|
|
186
|
-
// public typed fact and must not be collapsed into an ordinary pass.
|
|
187
|
-
if (handlers.noReceipt === undefined) {
|
|
188
|
-
throw new Error("Compliance no-receipt projection handler is unavailable");
|
|
189
|
-
}
|
|
190
|
-
return await handlers.noReceipt(
|
|
191
|
-
decision,
|
|
192
|
-
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
193
|
-
);
|
|
194
|
-
case "received":
|
|
195
|
-
// Parent candidate stands; raw auditor reply rides beside — not judged unreadable (#757).
|
|
196
|
-
if (handlers.received === undefined) {
|
|
197
|
-
throw new Error("Compliance received projection handler is unavailable");
|
|
198
|
-
}
|
|
199
|
-
return await handlers.received(
|
|
200
|
-
decision,
|
|
201
|
-
decision.usage === undefined ? {} : { usage: decision.usage },
|
|
202
|
-
);
|
|
203
|
-
case "continue":
|
|
204
|
-
return await handlers.continue(decision.violations);
|
|
205
|
-
case "escalate":
|
|
206
|
-
return await handlers.escalate(
|
|
207
|
-
projectAuditEscalation(decision, deliveredOutput),
|
|
208
|
-
);
|
|
209
|
-
case "transport_failure":
|
|
210
|
-
if (handlers.transportFailure !== undefined) {
|
|
211
|
-
return await handlers.transportFailure(decision);
|
|
212
|
-
}
|
|
213
|
-
throw new GatekeeperDecisionError({
|
|
214
|
-
status: "transport_failure",
|
|
215
|
-
stage: "auditor",
|
|
216
|
-
reason: decision.diagnostic,
|
|
217
|
-
submission: decision.submissions ?? decision.terminal,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
4
|
+
export function isAuditEscalationResult(value: unknown): value is { readonly kind: typeof AUDIT_ESCALATION_KIND } {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6
|
+
&& (value as Record<string, unknown>).kind === AUDIT_ESCALATION_KIND;
|
|
220
7
|
}
|
package/src/auditor-soul.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
|
|
1
4
|
import {
|
|
2
5
|
joinPackageMaterials,
|
|
3
6
|
readPackageMaterial,
|
|
@@ -102,3 +105,33 @@ export async function loadAuditorSoulFromSubjectInput(raw?: string): Promise<str
|
|
|
102
105
|
export function loadAuditorReferenceMaterialsFromSubjectInput(raw?: string): Promise<string> {
|
|
103
106
|
return loadAuditorReferenceMaterials(resolveAuditorSubject(raw));
|
|
104
107
|
}
|
|
108
|
+
|
|
109
|
+
function subjectFromSourceDirectory(sourceRunDirectory: string): AuditorSoulRole | undefined {
|
|
110
|
+
const role = basename(sourceRunDirectory).split("@")[1];
|
|
111
|
+
return isAuditorSoulRole(role) ? role : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resume binding already on the admitted page.
|
|
116
|
+
* Explicit subject wins; otherwise the source-run role is the audited object.
|
|
117
|
+
*/
|
|
118
|
+
export async function readAuditorResumeBinding(runDirectory: string): Promise<
|
|
119
|
+
| { readonly subject: AuditorSoulRole; readonly sourceRunDirectory: string }
|
|
120
|
+
| undefined
|
|
121
|
+
> {
|
|
122
|
+
let raw: unknown;
|
|
123
|
+
try {
|
|
124
|
+
raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8")) as unknown;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
130
|
+
const record = raw as { sourceRunPath?: unknown; auditorSubject?: unknown };
|
|
131
|
+
if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "") return undefined;
|
|
132
|
+
const subject = isAuditorSoulRole(record.auditorSubject)
|
|
133
|
+
? record.auditorSubject
|
|
134
|
+
: subjectFromSourceDirectory(record.sourceRunPath);
|
|
135
|
+
if (subject === undefined) return undefined;
|
|
136
|
+
return { subject, sourceRunDirectory: record.sourceRunPath };
|
|
137
|
+
}
|
package/src/doctor-role.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
|
|
2
|
-
import {
|
|
3
|
-
import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
|
|
2
|
+
import { ComplianceResponseRetentionError } from "./compliance-transport.ts";
|
|
4
3
|
import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
|
|
5
|
-
import { readableGateItem } from "./readable-gate-item.ts";
|
|
6
4
|
import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
|
|
7
5
|
|
|
8
6
|
import { sitianReport } from "./sitian-facade.ts";
|
|
9
7
|
|
|
10
8
|
export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
|
|
11
9
|
export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/<n>/runs directory", type: "string" as const } } as const;
|
|
12
|
-
export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase
|
|
10
|
+
export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase> };
|
|
13
11
|
function appendCandidate(ctx: HostContext, data: unknown): void {
|
|
14
12
|
try {
|
|
15
13
|
sitianReport({
|
|
@@ -36,7 +34,15 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
|
|
|
36
34
|
let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
|
|
37
35
|
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) };
|
|
38
36
|
if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
|
|
39
|
-
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown,
|
|
37
|
+
pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, _signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
|
|
38
|
+
if (!activation) throw new Error("太医署未激活");
|
|
39
|
+
const active = activation;
|
|
40
|
+
const testimony = validateDoctorOutput(params, active.patient, active.store);
|
|
41
|
+
try {
|
|
42
|
+
appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity });
|
|
43
|
+
} catch (error) { host.failInfrastructure(error, ctx, id); }
|
|
44
|
+
return { content: [], details: testimony, terminate: true };
|
|
45
|
+
} });
|
|
40
46
|
pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { 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>` }; }); }
|
|
41
47
|
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"); } };
|
|
42
48
|
}
|
package/src/gatekeeper-role.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type GatekeeperResult =
|
|
|
43
43
|
readonly receipt: unknown;
|
|
44
44
|
readonly runId?: string;
|
|
45
45
|
}
|
|
46
|
-
/** continue
|
|
46
|
+
/** continue returns the receipt for a subject resubmission; escalate resumes this officer directly. */
|
|
47
47
|
| {
|
|
48
48
|
readonly status: "continue";
|
|
49
49
|
readonly officer: GateOfficer;
|
|
@@ -81,7 +81,7 @@ export type GatekeeperResult =
|
|
|
81
81
|
/** Non-pass faces returned to the parent session (correctable; #836 never kill leg). */
|
|
82
82
|
export type SubmissionGateNonPassResult = Extract<
|
|
83
83
|
GatekeeperResult,
|
|
84
|
-
{ status: "continue" | "
|
|
84
|
+
{ status: "continue" | "no_receipt" | "transport_failure" }
|
|
85
85
|
>;
|
|
86
86
|
|
|
87
87
|
function gateSeatLabel(stage: GateOfficer): string {
|
package/src/host-contracts.ts
CHANGED
|
@@ -340,14 +340,6 @@ type HostEventResultMap = {
|
|
|
340
340
|
type HostEventHandler<K extends keyof HostEventMap> = (event: HostEventMap[K], ctx: HostContext) => HostEventResultMap[K] | void | Promise<HostEventResultMap[K] | void>;
|
|
341
341
|
export type HostEventRegistration = { [K in keyof HostEventMap]: [event: K, handler: HostEventHandler<K>] }[keyof HostEventMap];
|
|
342
342
|
|
|
343
|
-
type HostGatekeeperSubject = {
|
|
344
|
-
readonly kind:
|
|
345
|
-
| "worker_completion"
|
|
346
|
-
| "judge_draft"
|
|
347
|
-
| "judge_compliance"
|
|
348
|
-
| "countersign_verdict"
|
|
349
|
-
| "secretariat_verdict";
|
|
350
|
-
};
|
|
351
343
|
/** Gatekeeper non-pass faces returned to parent (#836 includes transport_failure; never kill leg). */
|
|
352
344
|
type HostGatekeeperNonPass = { readonly status: "continue" | "escalate" | "no_receipt" | "transport_failure" } & Record<string, unknown>;
|
|
353
345
|
export type HostSubmissionNonPass = HostGatekeeperNonPass;
|
|
@@ -378,23 +370,6 @@ export interface RoleHost {
|
|
|
378
370
|
getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
|
|
379
371
|
setActiveTools(names: string[]): void;
|
|
380
372
|
getActiveTools(): string[];
|
|
381
|
-
/**
|
|
382
|
-
* Shared submission gate. On converged/continue returns officer snapshot (receipt + nested
|
|
383
|
-
* runId) for seat public-terminal projection (#969); callers may ignore it.
|
|
384
|
-
*/
|
|
385
|
-
requireSubmissionGate?(options: {
|
|
386
|
-
context: HostContext;
|
|
387
|
-
subject: HostGatekeeperSubject;
|
|
388
|
-
signal?: AbortSignal;
|
|
389
|
-
hostActions: HostGatekeeperActions;
|
|
390
|
-
toolCallId: string;
|
|
391
|
-
submission?: unknown;
|
|
392
|
-
}): Promise<void | {
|
|
393
|
-
readonly status: "converged" | "continue";
|
|
394
|
-
readonly officer: string;
|
|
395
|
-
readonly receipt: unknown;
|
|
396
|
-
readonly runId?: string;
|
|
397
|
-
}>;
|
|
398
373
|
on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
|
|
399
374
|
on(event: "input", handler: HostEventHandler<"input">): void;
|
|
400
375
|
on(event: "tool_call", handler: HostEventHandler<"tool_call">): void;
|
package/src/judge-role.ts
CHANGED
|
@@ -1,19 +1,71 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type RoleHost,
|
|
3
|
+
type HostContext,
|
|
4
|
+
type HostToolResult,
|
|
5
|
+
} from "./host-contracts.ts";
|
|
2
6
|
import type { Static } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { projectGatekeeperEscalation } from "./audit-escalation.ts";
|
|
6
|
-
|
|
7
|
-
import { readableGateItem } from "./readable-gate-item.ts";
|
|
7
|
+
import { reviewSubmissionSchema } from "./review-submission.ts";
|
|
8
|
+
import { type GatekeeperSubject } from "./gatekeeper-role.ts";
|
|
8
9
|
import {
|
|
9
10
|
JUDGE_OUTPUT_TOOL_NAME,
|
|
10
11
|
type JudgeVerdict,
|
|
11
12
|
} from "./package-contracts/judge-output.ts";
|
|
12
13
|
|
|
13
|
-
|
|
14
14
|
export { JUDGE_OUTPUT_TOOL_NAME };
|
|
15
15
|
export type { JudgeVerdict };
|
|
16
16
|
|
|
17
|
+
/** The only judge audit order: 符宝郎, then 审刑院. */
|
|
18
|
+
const JUDGE_GATES: readonly GatekeeperSubject[] = [
|
|
19
|
+
{ kind: "judge_draft" },
|
|
20
|
+
{ kind: "judge_compliance" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export async function runJudgeGates(input: {
|
|
24
|
+
readonly gateAlreadyConverged: (subject: GatekeeperSubject) => Promise<boolean>;
|
|
25
|
+
readonly runGate: (
|
|
26
|
+
subject: GatekeeperSubject,
|
|
27
|
+
) => Promise<{
|
|
28
|
+
readonly status?: unknown;
|
|
29
|
+
readonly receipt?: unknown;
|
|
30
|
+
readonly runId?: string;
|
|
31
|
+
readonly runDirectory?: string;
|
|
32
|
+
} | void>;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
readonly status: "converged" | "continue" | "escalate";
|
|
35
|
+
readonly passes: readonly {
|
|
36
|
+
readonly subject: GatekeeperSubject;
|
|
37
|
+
readonly status: "converged" | "continue" | "escalate";
|
|
38
|
+
readonly receipt: unknown;
|
|
39
|
+
readonly runId?: string;
|
|
40
|
+
readonly runDirectory?: string;
|
|
41
|
+
}[];
|
|
42
|
+
}> {
|
|
43
|
+
const passes: {
|
|
44
|
+
subject: GatekeeperSubject;
|
|
45
|
+
status: "converged" | "continue" | "escalate";
|
|
46
|
+
receipt: unknown;
|
|
47
|
+
runId?: string;
|
|
48
|
+
runDirectory?: string;
|
|
49
|
+
}[] = [];
|
|
50
|
+
for (const subject of JUDGE_GATES) {
|
|
51
|
+
if (await input.gateAlreadyConverged(subject)) continue;
|
|
52
|
+
const pass = await input.runGate(subject);
|
|
53
|
+
const status = pass?.status;
|
|
54
|
+
if (pass === undefined || (status !== "converged" && status !== "continue" && status !== "escalate")) {
|
|
55
|
+
throw new Error("judge gate returned no conclusion");
|
|
56
|
+
}
|
|
57
|
+
passes.push({
|
|
58
|
+
subject,
|
|
59
|
+
status,
|
|
60
|
+
receipt: pass.receipt,
|
|
61
|
+
...(pass.runId === undefined ? {} : { runId: pass.runId }),
|
|
62
|
+
...(pass.runDirectory === undefined ? {} : { runDirectory: pass.runDirectory }),
|
|
63
|
+
});
|
|
64
|
+
if (status === "continue" || status === "escalate") return { status, passes };
|
|
65
|
+
}
|
|
66
|
+
return { status: "converged", passes };
|
|
67
|
+
}
|
|
68
|
+
|
|
17
69
|
// #836 r16 class 1: fix/classes/note/decisionGate are LLM/human-read narrative
|
|
18
70
|
// content — 符宝郎/审刑院 read the raw receipt, no code branches on their length
|
|
19
71
|
// or nested presence. `status` alone is the machine discriminator the
|
|
@@ -25,16 +77,9 @@ export type JudgeRoleDependencies = {
|
|
|
25
77
|
loadSoul(): Promise<string>;
|
|
26
78
|
};
|
|
27
79
|
|
|
28
|
-
export type JudgeRoleHostActions = HostGatekeeperActions & {
|
|
29
|
-
/** Preserve the first officer's pass as a separate model-visible result item if the second gate fails. */
|
|
30
|
-
bindPriorGatePass(toolCallId: string, receipt: unknown): void;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
|
|
34
80
|
export function createJudgeRoleRuntime(
|
|
35
81
|
pi: RoleHost,
|
|
36
82
|
dependencies: JudgeRoleDependencies,
|
|
37
|
-
hostActions: JudgeRoleHostActions,
|
|
38
83
|
): { activate(): Promise<void> } {
|
|
39
84
|
let soul: string | undefined;
|
|
40
85
|
let lifecycleRegistered = false;
|
|
@@ -48,86 +93,14 @@ export function createJudgeRoleRuntime(
|
|
|
48
93
|
pi.registerTool({
|
|
49
94
|
name: JUDGE_OUTPUT_TOOL_NAME,
|
|
50
95
|
label: "大理寺输出",
|
|
51
|
-
description: "
|
|
96
|
+
description: "提交大理寺终局判词;交卷调用结束后,由公开调用接缝按判词状态执行适用审核。",
|
|
52
97
|
promptSnippet: "提交大理寺终局判词",
|
|
53
98
|
parameters: judgeVerdictSchema,
|
|
54
99
|
async execute(toolCallId: string, parameters: Static<typeof judgeVerdictSchema>, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
|
|
55
100
|
if (soul === undefined) throw new Error("大理寺职分未装载");
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
|
|
60
|
-
&& typeof (parameters as Record<string, unknown>).status === "string"
|
|
61
|
-
? (parameters as Record<string, unknown>).status as string
|
|
62
|
-
: undefined;
|
|
63
|
-
if (rawStatus === undefined || !REVIEW_QUEUE_STATUSES.has(rawStatus)) {
|
|
64
|
-
const received = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
|
|
65
|
-
? (parameters as Record<string, unknown>).status
|
|
66
|
-
: undefined;
|
|
67
|
-
throw new ParentQueueReaskError(unreadableDiscriminatorNotice("status", received));
|
|
68
|
-
}
|
|
69
|
-
if (rawStatus === "escalate") {
|
|
70
|
-
// Parent escalate → throw to caller as-is; officers do not attend (#753 / #756).
|
|
71
|
-
return {
|
|
72
|
-
content: [],
|
|
73
|
-
details: parameters,
|
|
74
|
-
terminate: true as const,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
// Queue membership above is the only three-state check. The receipt is not re-judged.
|
|
78
|
-
// Candidate verdict is already on the parent session books as this
|
|
79
|
-
// tool-call leaf (first-record-then-audit; run 019fea05 L61/L62).
|
|
80
|
-
// #753: 符宝郎内闸 — continue returns the raw receipt as a nonterminal tool result.
|
|
81
|
-
let draftPass: { status: "converged" | "continue"; receipt: unknown } | undefined;
|
|
82
|
-
try {
|
|
83
|
-
const obtainedDraftPass = await pi.requireSubmissionGate!({
|
|
84
|
-
context: ctx,
|
|
85
|
-
subject: { kind: "judge_draft" },
|
|
86
|
-
...(signal === undefined ? {} : { signal }),
|
|
87
|
-
hostActions,
|
|
88
|
-
toolCallId,
|
|
89
|
-
// #879: this-turn typed payload — identity-bound at submit site.
|
|
90
|
-
submission: parameters,
|
|
91
|
-
});
|
|
92
|
-
draftPass = obtainedDraftPass || undefined;
|
|
93
|
-
if (draftPass?.status === "continue") {
|
|
94
|
-
return {
|
|
95
|
-
content: [{ type: "text" as const, text: readableGateItem(draftPass.receipt) }],
|
|
96
|
-
details: parameters,
|
|
97
|
-
terminate: false,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (draftPass !== undefined) hostActions.bindPriorGatePass(toolCallId, draftPass.receipt);
|
|
101
|
-
// #756: 审刑院合规路径 — same review-queue law as 符宝郎/台院.
|
|
102
|
-
// converged → accept; continue → raw auditor receipt; escalate → pause;
|
|
103
|
-
// not three-state → resume auditor; no round cap; no disposeCompliance mapping.
|
|
104
|
-
const compliancePass = await pi.requireSubmissionGate!({
|
|
105
|
-
context: ctx,
|
|
106
|
-
subject: { kind: "judge_compliance" },
|
|
107
|
-
...(signal === undefined ? {} : { signal }),
|
|
108
|
-
hostActions,
|
|
109
|
-
toolCallId,
|
|
110
|
-
// #879: same parent payload for 审刑院; not recovered from session latest.
|
|
111
|
-
submission: parameters,
|
|
112
|
-
});
|
|
113
|
-
if (compliancePass?.status === "continue") {
|
|
114
|
-
return {
|
|
115
|
-
content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
|
|
116
|
-
details: parameters,
|
|
117
|
-
terminate: false,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
content: [draftPass, compliancePass].filter((pass) => pass !== undefined).map((pass) => ({ type: "text" as const, text: readableGateItem(pass.receipt) })),
|
|
122
|
-
details: parameters,
|
|
123
|
-
terminate: true as const,
|
|
124
|
-
};
|
|
125
|
-
} catch (error) {
|
|
126
|
-
if (error instanceof GatekeeperDecisionError && error.result.status === "escalate") {
|
|
127
|
-
return projectGatekeeperEscalation(error.result, parameters);
|
|
128
|
-
}
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
101
|
+
// ADR 0003: record the original receipt and end the tool call first.
|
|
102
|
+
// The public seam reads status and chooses the next route.
|
|
103
|
+
return { content: [], details: parameters, terminate: true };
|
|
131
104
|
},
|
|
132
105
|
});
|
|
133
106
|
pi.on("before_agent_start", (event) => {
|
|
@@ -27,20 +27,3 @@ export type JudgeVerdict =
|
|
|
27
27
|
note?: string;
|
|
28
28
|
evidence?: unknown;
|
|
29
29
|
};
|
|
30
|
-
|
|
31
|
-
export function validateAcceptedJudgeDetails(verdict: unknown): JudgeVerdict {
|
|
32
|
-
if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) throw new Error("Judge verdict has no execution discriminator");
|
|
33
|
-
let status: unknown;
|
|
34
|
-
try {
|
|
35
|
-
status = (verdict as Record<string, unknown>).status;
|
|
36
|
-
} catch {
|
|
37
|
-
throw new Error("Judge verdict has no execution discriminator");
|
|
38
|
-
}
|
|
39
|
-
if (typeof status !== "string") {
|
|
40
|
-
throw new Error("Judge verdict has no execution discriminator");
|
|
41
|
-
}
|
|
42
|
-
if (REVIEW_QUEUE_STATUSES.has(status)) {
|
|
43
|
-
return verdict as JudgeVerdict;
|
|
44
|
-
}
|
|
45
|
-
throw new Error("Judge verdict has no execution discriminator");
|
|
46
|
-
}
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
} from "./collector-output.ts";
|
|
11
11
|
import {
|
|
12
12
|
JUDGE_OUTPUT_TOOL_NAME,
|
|
13
|
-
validateAcceptedJudgeDetails,
|
|
14
13
|
type JudgeVerdict,
|
|
15
14
|
} from "./judge-output.ts";
|
|
16
15
|
import {
|
|
@@ -46,7 +45,6 @@ export {
|
|
|
46
45
|
REVIEWER_OUTPUT_TOOL_NAME,
|
|
47
46
|
MERGER_OUTPUT_TOOL_NAME,
|
|
48
47
|
validateAcceptedCollectorReceipt,
|
|
49
|
-
validateAcceptedJudgeDetails,
|
|
50
48
|
validateReviewerIntent,
|
|
51
49
|
validateAcceptedWorkerDetails,
|
|
52
50
|
validateDoctorSubmissionShape,
|
package/src/pi/adapter.ts
CHANGED
|
@@ -6,12 +6,10 @@ import {
|
|
|
6
6
|
type ToolDefinition,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { Static, TSchema } from "typebox";
|
|
9
|
-
import type { SubmissionGateNonPassResult
|
|
10
|
-
import { requireSubmissionGate } from "../submission-gate.ts";
|
|
9
|
+
import type { SubmissionGateNonPassResult } from "../gatekeeper-role.ts";
|
|
11
10
|
import type {
|
|
12
11
|
HostContext,
|
|
13
12
|
HostEventRegistration,
|
|
14
|
-
HostGatekeeperActions,
|
|
15
13
|
HostToolDefinition,
|
|
16
14
|
HostToolResult,
|
|
17
15
|
RoleEnvelopeHost,
|
|
@@ -83,35 +81,6 @@ export function toPiContext(context: HostContext): ExtensionContext {
|
|
|
83
81
|
return piContext;
|
|
84
82
|
}
|
|
85
83
|
|
|
86
|
-
/** Gatekeeper receives HostContext directly (#518 §1③) without recovering raw ExtensionContext via WeakMap. */
|
|
87
|
-
function requirePiSubmissionGate(options: {
|
|
88
|
-
context: HostContext;
|
|
89
|
-
subject: GatekeeperSubject;
|
|
90
|
-
signal?: AbortSignal;
|
|
91
|
-
hostActions: HostGatekeeperActions;
|
|
92
|
-
toolCallId: string;
|
|
93
|
-
submission?: unknown;
|
|
94
|
-
}): Promise<void | {
|
|
95
|
-
readonly status: "converged" | "continue";
|
|
96
|
-
readonly officer: string;
|
|
97
|
-
readonly receipt: unknown;
|
|
98
|
-
readonly runId?: string;
|
|
99
|
-
}> {
|
|
100
|
-
return requireSubmissionGate({
|
|
101
|
-
context: options.context,
|
|
102
|
-
subject: options.subject,
|
|
103
|
-
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
104
|
-
hostActions: {
|
|
105
|
-
failInfrastructure(error, _context, toolCallId) {
|
|
106
|
-
options.hostActions.failInfrastructure(error, options.context, toolCallId);
|
|
107
|
-
},
|
|
108
|
-
bindSubmissionNonPass: options.hostActions.bindSubmissionNonPass,
|
|
109
|
-
},
|
|
110
|
-
toolCallId: options.toolCallId,
|
|
111
|
-
...(options.submission === undefined ? {} : { submission: options.submission }),
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
84
|
function toPiResult<D>(result: HostToolResult<D>): HostToolResult<D> {
|
|
116
85
|
return result;
|
|
117
86
|
}
|
|
@@ -177,7 +146,6 @@ export function createPiRoleHostAdapter(
|
|
|
177
146
|
})),
|
|
178
147
|
setActiveTools: (names) => pi.setActiveTools(names),
|
|
179
148
|
getActiveTools: () => pi.getActiveTools(),
|
|
180
|
-
requireSubmissionGate: requirePiSubmissionGate,
|
|
181
149
|
on(...registration: HostEventRegistration) {
|
|
182
150
|
const context = (value: ExtensionContext) => projectPiContext(value, options.transcriptFromContext);
|
|
183
151
|
if (registration[0] === "before_agent_start") {
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
} from "./option-definitions.ts";
|
|
83
83
|
import { TurnDispatchedFailure } from "./auto-resume.ts";
|
|
84
84
|
import { continueParentAfterChild, runPublicInstructionSeat, runPublicInstructionSeatResume } from "./instruction-seat-run.ts";
|
|
85
|
-
import {
|
|
85
|
+
import { latestPayloadEscalated } from "./countersign-run.ts";
|
|
86
86
|
import { runPublicAnalyst } from "./analyst-run.ts";
|
|
87
87
|
import {
|
|
88
88
|
AUTO_RESUME_LIMIT,
|
|
@@ -1251,7 +1251,7 @@ export async function runAkRole(
|
|
|
1251
1251
|
while (
|
|
1252
1252
|
current.exitCode === 0 && current.admitted?.correlationId !== undefined
|
|
1253
1253
|
&& current.terminal?.roleOutcome.kind === "accepted"
|
|
1254
|
-
&& !
|
|
1254
|
+
&& !latestPayloadEscalated(current.terminal.roleOutcome)
|
|
1255
1255
|
) {
|
|
1256
1256
|
const parentRunId = current.admitted.correlationId;
|
|
1257
1257
|
resumeFailureContext = {
|