@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621

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.
Files changed (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -1,1140 +0,0 @@
1
- /**
2
- * Unique in-process child lifecycle helper (#236 established; #233 sinks auditor + navigator).
3
- * Owns scratch, inherited provider runtime, AgentSession, abort/dispose.
4
- * Not a subprocess RPC.
5
- */
6
- import { mkdtemp, rm } from "node:fs/promises";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import {
10
- type Api,
11
- type AssistantMessage,
12
- type Context,
13
- type Model,
14
- type ProviderStreamOptions,
15
- type Usage,
16
- } from "@earendil-works/pi-ai";
17
- import type {
18
- ExtensionContext,
19
- SessionManager,
20
- } from "@earendil-works/pi-coding-agent";
21
-
22
- import {
23
- AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE,
24
- AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
25
- type AuditorParentAttemptBinding,
26
- } from "./compliance-transport.ts";
27
- import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
28
- import { sitianReport } from "./sitian-facade.ts";
29
- import { createEngineDetourToolDefinition } from "./engine-detour-tool.ts";
30
- import { engineNameFromEnv } from "./engine-detour.ts";
31
- import {
32
- appendEngineSessionMaterial,
33
- engineSessionMaterialFromOptions,
34
- type EngineSessionMaterial,
35
- } from "./package-resources/engine-material.ts";
36
- import { readPackageMaterial } from "./session-opening-materials.ts";
37
- import {
38
- type GateOfficerSeat,
39
- } from "./public-cli/config.ts";
40
- import { readInstitutionalSeatSelection } from "./institutional-resolution.ts";
41
- import type {
42
- OpenPiInstitutionalSessionOptions,
43
- OpenPiInstitutionalSessionResult,
44
- } from "./pi/in-process-session.ts";
45
- import type { HostAssistantTurnResult, HostContext } from "./host-contracts.ts";
46
- import {
47
- NAVIGATOR_PREPARE_TOOL_NAME,
48
- NavigatorUnavailableError,
49
- navigatorModelSettingPath,
50
- navigatorProviderFailureFromDiagnostics,
51
- navigatorProviderFailureFromError,
52
- navigatorProviderFailureFromStatus,
53
- navigatorUnavailableError,
54
- parseNavigatorModelSetting,
55
- resolveNavigatorSeatSelection,
56
- type NavigatorProviderFailureFact,
57
- type NavigatorSessionFactory,
58
- } from "./navigator-session-contracts.ts";
59
- import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
60
- import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
61
- import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
62
- import {
63
- hasUpstreamErrorTestimony,
64
- isNonSuccessHttpStatus,
65
- projectConfirmedRemotePayload,
66
- } from "./upstream-error-testimony.ts";
67
-
68
- /**
69
- * Shared Standards/Spec evidence-child system materials — path roster only.
70
- * Builder consumes this unique roster; cadence prose stays in owner material (ADR 0073).
71
- * Not exported — tests must not mirror internal roster structure.
72
- */
73
- const EVIDENCE_CHILD_SESSION_MATERIALS = [
74
- "souls/quality-law.md",
75
- ] as const;
76
-
77
- /** Package-owned system prompt for Reviewer Standards/Spec evidence children (private carrier). */
78
- async function buildEvidenceChildSystemPrompt(
79
- engineMaterial?: EngineSessionMaterial,
80
- ): Promise<string> {
81
- // ADR 0073: verification cadence lives in owner material only; no machine prose copy.
82
- const materials: string[] = [];
83
- for (const relativePath of EVIDENCE_CHILD_SESSION_MATERIALS) {
84
- materials.push(await readPackageMaterial(relativePath));
85
- }
86
- return appendEngineSessionMaterial(materials, engineMaterial).join("\n");
87
- }
88
-
89
- // ── shared constants / types ──────────────────────────────────────────────
90
-
91
- export const DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES = 2;
92
-
93
- export type AuditorDecisionTool = {
94
- name: string;
95
- description: string;
96
- parameters: object;
97
- execute(...args: any[]): Promise<import("@earendil-works/pi-coding-agent").AgentToolResult<unknown>>;
98
- };
99
-
100
- export type InProcessScratchOptions = {
101
- readonly prefix: string;
102
- readonly parentDirectory?: string;
103
- };
104
-
105
- /** Shared scratch directory with guaranteed cleanup. */
106
- export async function withInProcessScratch<T>(
107
- options: InProcessScratchOptions,
108
- run: (scratch: string) => Promise<T>,
109
- ): Promise<T> {
110
- const scratch = await mkdtemp(join(options.parentDirectory ?? tmpdir(), options.prefix));
111
- let failure: unknown;
112
- try {
113
- return await run(scratch);
114
- } catch (error) {
115
- failure = error;
116
- throw error;
117
- } finally {
118
- try {
119
- await rm(scratch, { recursive: true, force: true });
120
- } catch (cleanupFailure) {
121
- if (failure !== undefined) {
122
- throw new AggregateError([failure, cleanupFailure], "in-process child scratch cleanup failed", { cause: failure });
123
- }
124
- throw cleanupFailure;
125
- }
126
- }
127
- }
128
-
129
- /**
130
- * Run every child cleanup, aggregating failures so one throwing cleanup never
131
- * skips the rest (e.g. handle.close must still run when unsubscribe throws).
132
- * If a primary failure is supplied and cleanup also failed, the two are combined
133
- * into an AggregateError; otherwise only the failing branch is surfaced.
134
- */
135
- async function runChildCleanup(
136
- cleanups: ReadonlyArray<() => void | Promise<void>>,
137
- primaryFailure: unknown,
138
- label: string,
139
- ): Promise<void> {
140
- let cleanupFailure: unknown;
141
- for (const cleanup of cleanups) {
142
- try {
143
- await cleanup();
144
- } catch (failure) {
145
- cleanupFailure = cleanupFailure === undefined
146
- ? failure
147
- : new AggregateError([cleanupFailure, failure], `${label} cleanup failed`, {
148
- cause: cleanupFailure,
149
- });
150
- }
151
- }
152
- if (cleanupFailure === undefined) return;
153
- if (primaryFailure !== undefined) {
154
- throw new AggregateError(
155
- [primaryFailure, cleanupFailure],
156
- `${label} execution and cleanup failed`,
157
- { cause: primaryFailure },
158
- );
159
- }
160
- throw new AggregateError([cleanupFailure], `${label} cleanup failed`, {
161
- cause: cleanupFailure,
162
- });
163
- }
164
-
165
- type EvidenceChildFailureClassification = "provider" | "child" | "unknown";
166
- type ClassifiedReviewerError = Error & Readonly<{
167
- evidenceChildFailure: EvidenceChildFailureClassification;
168
- evidenceChildOriginal?: unknown;
169
- }>;
170
- function numericHttpStatus(value: unknown): number | undefined {
171
- return isNonSuccessHttpStatus(value) ? value : undefined;
172
- }
173
-
174
- type StructuredRemoteProjection = {
175
- readonly hasTestimony: boolean;
176
- readonly httpStatus?: number;
177
- readonly diagnostics?: unknown;
178
- readonly body?: unknown;
179
- readonly code?: unknown;
180
- readonly errno?: unknown;
181
- };
182
-
183
- /**
184
- * Cause-chain reader over the shared upstream-testimony authority.
185
- * Shape walking stays here; testimony + confirmed-remote payload rules are shared.
186
- */
187
- function projectStructuredRemote(error: unknown): StructuredRemoteProjection {
188
- let httpStatus: number | undefined;
189
- let diagnostics: unknown;
190
- let body: unknown;
191
- let code: unknown;
192
- let errno: unknown;
193
- let cursor: unknown = error;
194
- const seen = new Set<unknown>();
195
- while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
196
- seen.add(cursor);
197
- const record = cursor as Record<string, unknown>;
198
- const nodeStatus = numericHttpStatus(record.statusCode)
199
- ?? numericHttpStatus(record.status)
200
- ?? numericHttpStatus(record.httpStatus);
201
- const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
202
- ? record.diagnostics
203
- : undefined;
204
- const nodeHasTestimony = hasUpstreamErrorTestimony({
205
- ...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
206
- ...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
207
- });
208
- if (httpStatus === undefined && nodeStatus !== undefined) httpStatus = nodeStatus;
209
- if (diagnostics === undefined && nodeDiagnostics !== undefined) diagnostics = nodeDiagnostics;
210
- // Payload only from confirmed-remote nodes — never arbitrary local Error.code.
211
- if (nodeHasTestimony) {
212
- const payload = projectConfirmedRemotePayload(record);
213
- if (body === undefined && payload.body !== undefined) body = payload.body;
214
- if (code === undefined && payload.code !== undefined) code = payload.code;
215
- if (errno === undefined && payload.errno !== undefined) errno = payload.errno;
216
- }
217
- cursor = record.cause;
218
- }
219
- return {
220
- hasTestimony: hasUpstreamErrorTestimony({
221
- ...(httpStatus === undefined ? {} : { httpStatus }),
222
- ...(diagnostics === undefined ? {} : { diagnostics }),
223
- }),
224
- ...(httpStatus === undefined ? {} : { httpStatus }),
225
- ...(diagnostics === undefined ? {} : { diagnostics }),
226
- ...(body === undefined ? {} : { body }),
227
- ...(code === undefined ? {} : { code }),
228
- ...(errno === undefined ? {} : { errno }),
229
- };
230
- }
231
-
232
- /**
233
- * Attach a directly observed HTTP status onto an error/aborted assistant message.
234
- * Does not invent status from errorMessage prose; skips when already held.
235
- */
236
- function attachObservedHttpStatus<T extends AssistantMessage>(
237
- message: T,
238
- observedHttpStatus: number | undefined,
239
- ): T {
240
- if (observedHttpStatus === undefined) return message;
241
- if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
242
- if (numericHttpStatus(observedHttpStatus) === undefined) return message;
243
- if (projectStructuredRemote(message).httpStatus !== undefined) return message;
244
- return Object.assign(message, {
245
- status: observedHttpStatus,
246
- statusCode: observedHttpStatus,
247
- });
248
- }
249
- function enrichStreamEvent(event: unknown, observedHttpStatus: number | undefined): unknown {
250
- if (observedHttpStatus === undefined || event === null || typeof event !== "object") return event;
251
- const record = event as Record<string, unknown>;
252
- if (record.type === "error" && record.error !== null && typeof record.error === "object") {
253
- return {
254
- ...record,
255
- error: attachObservedHttpStatus(record.error as AssistantMessage, observedHttpStatus),
256
- };
257
- }
258
- if (record.type === "done" && record.message !== null && typeof record.message === "object") {
259
- return {
260
- ...record,
261
- message: attachObservedHttpStatus(record.message as AssistantMessage, observedHttpStatus),
262
- };
263
- }
264
- if (record.partial !== null && typeof record.partial === "object") {
265
- return {
266
- ...record,
267
- partial: attachObservedHttpStatus(record.partial as AssistantMessage, observedHttpStatus),
268
- };
269
- }
270
- return event;
271
- }
272
- function classifiedError(error: unknown, evidenceChildFailure: EvidenceChildFailureClassification): ClassifiedReviewerError {
273
- const diagnostic = typeof error === "object" && error !== null && typeof (error as { errorMessage?: unknown }).errorMessage === "string"
274
- ? (error as { errorMessage: string }).errorMessage
275
- : error === undefined ? "" : String(error);
276
- const wrapped = error instanceof Error
277
- ? error
278
- : Object.assign(new Error(diagnostic, { cause: error }), { evidenceChildOriginal: error });
279
- const classification = "evidenceChildFailure" in wrapped
280
- ? (wrapped as ClassifiedReviewerError).evidenceChildFailure
281
- : evidenceChildFailure === "provider" && !projectStructuredRemote(error).hasTestimony
282
- ? "unknown"
283
- : evidenceChildFailure;
284
- return Object.assign(wrapped, { evidenceChildFailure: classification });
285
- }
286
-
287
- /** Extract the first text diagnostic from a flattened toolResult error `details`. */
288
- function extractToolResultText(details: unknown): string | undefined {
289
- if (typeof details !== "object" || details === null) return undefined;
290
- const record = details as Record<string, unknown>;
291
- const content = record.content;
292
- if (Array.isArray(content)) {
293
- for (const part of content) {
294
- if (typeof part === "object" && part !== null) {
295
- const text = (part as Record<string, unknown>).text;
296
- if (typeof text === "string" && text.trim() !== "") return text;
297
- }
298
- }
299
- }
300
- return undefined;
301
- }
302
-
303
- function emptyUsage(): Usage {
304
- return {
305
- input: 0,
306
- output: 0,
307
- cacheRead: 0,
308
- cacheWrite: 0,
309
- totalTokens: 0,
310
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
311
- };
312
- }
313
-
314
- function addUsage(total: Usage, next: Usage): void {
315
- total.input += next.input;
316
- total.output += next.output;
317
- total.cacheRead += next.cacheRead;
318
- total.cacheWrite += next.cacheWrite;
319
- total.totalTokens += next.totalTokens;
320
- total.cost.input += next.cost.input;
321
- total.cost.output += next.cost.output;
322
- total.cost.cacheRead += next.cost.cacheRead;
323
- total.cost.cacheWrite += next.cost.cacheWrite;
324
- total.cost.total += next.cost.total;
325
- }
326
-
327
- // ── evidence child (reviewer legs) ────────────────────────────────────────
328
-
329
- /** Evidence child execution options. */
330
- export type EvidenceChildExecuteOptions = Readonly<{
331
- signal?: AbortSignal;
332
- /** Parent directory for credential/config scratch. Defaults to os.tmpdir(). */
333
- credentialScratchParent?: string;
334
- /** Run directory carrying the institutional resolution page (#518). Derives
335
- * from context when absent. */
336
- runDirectory?: string;
337
- /**
338
- * Package root for optional engine method-material resolution (#378).
339
- * Required only when AK_ROLE_ENGINE is set and packaged notes should attach.
340
- */
341
- packageRoot?: string;
342
- }>;
343
-
344
- export async function executeEvidenceChild(
345
- workspace: string,
346
- prompt: ReviewerPromptText,
347
- context: ExtensionContext | HostContext,
348
- options: EvidenceChildExecuteOptions = {},
349
- ): Promise<{ report: string; usage: Usage; prompt: ReviewerPromptText }> {
350
- const signal = options.signal;
351
- const runDirectory = options.runDirectory ?? auditorRunDirectory(context);
352
- if (runDirectory === undefined) {
353
- throw new Error("Evidence child requires a run directory carrying the institutional resolution page");
354
- }
355
- const selection = await readInstitutionalSeatSelection(runDirectory, "evidenceChild");
356
- return withInProcessScratch(
357
- {
358
- prefix: "ak-evidence-child-",
359
- ...(options.credentialScratchParent === undefined
360
- ? {}
361
- : { parentDirectory: options.credentialScratchParent }),
362
- },
363
- async (childConfigDir) => {
364
- const { openPiInstitutionalSession } = await import("./pi/in-process-session.ts");
365
- const { createRecordSession } = await import("./archivist-record-entry.ts");
366
- // #378: when labor engine is configured, legs get the same detour tool + material
367
- // dual-path as the parent seat (ADR 0069 detour-rejoins-main-road).
368
- const engineName = engineNameFromEnv();
369
- const engineMaterial =
370
- engineName === undefined
371
- ? undefined
372
- : options.packageRoot === undefined || options.packageRoot.trim() === ""
373
- // Name-only when package root is unavailable (still a valid #376 path).
374
- ? Object.freeze({ name: engineName })
375
- : engineSessionMaterialFromOptions({
376
- engine: engineName,
377
- packageRoot: options.packageRoot,
378
- });
379
- // Evidence legs use the parent detour tool; retain any engine process cause
380
- // so the enclosing child boundary, rather than a tool-error result, terminates.
381
- let engineDetourFailure: Error | undefined;
382
- const engineDetourTool =
383
- engineName === undefined
384
- ? undefined
385
- : createEngineDetourToolDefinition({
386
- engineName,
387
- fail(error) {
388
- engineDetourFailure ??= error instanceof Error ? error : new Error(String(error));
389
- throw engineDetourFailure;
390
- },
391
- });
392
- // No tools allowlist — Pi defaults + unrestricted evidence surface (ADR 0064).
393
- // Single open seam owner: pi/in-process-session.ts.
394
- let opened: Awaited<ReturnType<typeof openPiInstitutionalSession>>;
395
- try {
396
- opened = await openPiInstitutionalSession({
397
- cwd: workspace,
398
- agentDir: childConfigDir,
399
- selection,
400
- systemPrompt: await buildEvidenceChildSystemPrompt(engineMaterial),
401
- ...(engineDetourTool === undefined
402
- ? {}
403
- : { customTools: [engineDetourTool] }),
404
- sessionManager: createRecordSession({
405
- cwd: workspace,
406
- kind: "evidence-children",
407
- ...(context.sessionManager === undefined ? {} : { parent: context.sessionManager }),
408
- }),
409
- ...(signal === undefined ? {} : { signal }),
410
- label: "Evidence child",
411
- });
412
- } catch (error) {
413
- throw classifiedError(error, "provider");
414
- }
415
- const { handle } = opened;
416
- const usage = emptyUsage();
417
- const unsubscribe = handle.subscribe((event) => {
418
- if (event.type === "message_end" && event.role === "assistant") {
419
- if (event.usage) addUsage(usage, event.usage);
420
- }
421
- });
422
- const abortChild = () => { handle.abort(); };
423
- if (signal?.aborted) abortChild();
424
- else signal?.addEventListener("abort", abortChild, { once: true });
425
- let primaryFailure: unknown;
426
- try {
427
- const delivered = prompt;
428
- let turnResult: HostAssistantTurnResult;
429
- try {
430
- turnResult = await handle.prompt(delivered);
431
- } catch (error) {
432
- if (engineDetourFailure !== undefined) {
433
- throw classifiedError(engineDetourFailure, "child");
434
- }
435
- throw classifiedError(error, "provider");
436
- }
437
- if (engineDetourFailure !== undefined) {
438
- throw classifiedError(engineDetourFailure, "child");
439
- }
440
- if (signal?.aborted) throw new Error("Evidence child was cancelled");
441
- const lastAssistant = turnResult.messages !== undefined
442
- ? [...turnResult.messages].reverse().find((message: any) => message?.role === "assistant") as AssistantMessage | undefined
443
- : undefined;
444
- // error|aborted assistant stops share the upstream-testimony rule: provider only
445
- // with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
446
- // for real local child/report failures (no assistant / blank report / cleanup).
447
- if (
448
- turnResult.stopReason === "error" || turnResult.stopReason === "aborted"
449
- || (lastAssistant?.role === "assistant" && (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted"))
450
- ) {
451
- const errMsg = turnResult.errorMessage ?? lastAssistant?.errorMessage ?? "";
452
- throw classifiedError(
453
- new Error(errMsg, { cause: lastAssistant }),
454
- lastAssistant && projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
455
- );
456
- }
457
- if (lastAssistant !== undefined && lastAssistant.role !== "assistant") {
458
- throw classifiedError(
459
- new Error("Evidence child child terminated without a report", {
460
- cause: lastAssistant ?? turnResult.messages,
461
- }),
462
- "child",
463
- );
464
- }
465
- const report = turnResult.text;
466
- if (report.trim().length === 0) {
467
- throw new Error("Evidence child returned a blank child report");
468
- }
469
- return { report, usage, prompt: delivered };
470
- } catch (error) {
471
- primaryFailure = classifiedError(error, "child");
472
- throw primaryFailure;
473
- } finally {
474
- signal?.removeEventListener("abort", abortChild);
475
- await runChildCleanup([() => unsubscribe(), () => handle.close()], primaryFailure, "Reviewer child");
476
- }
477
- },
478
- );
479
- }
480
-
481
- // ── auditor child ─────────────────────────────────────────────────────────
482
-
483
- export type AuditorRoleOptions = {
484
- systemPrompt: string;
485
- prompt: string;
486
- tool: AuditorDecisionTool;
487
- dossierTool: AuditorDecisionTool;
488
- roleLabel: string;
489
- context: ExtensionContext | HostContext;
490
- signal?: AbortSignal;
491
- retainResponse?(response: AssistantMessage): void;
492
- /** Province seat identity only — shared executor owns seat resolution, and
493
- * loud auth/availability failure (#453 / ADR 0018). */
494
- gateSeat?: GateOfficerSeat;
495
- /** Run directory carrying the institutional resolution page (#518). Derives
496
- * from context when absent. */
497
- runDirectory?: string;
498
- };
499
-
500
- /** Resolution-page seat key for an auditor invocation: province gate seats map
501
- * to their own page seats; doctor/judge compliance audits use the auditor seat. */
502
- function auditorSeatKey(gateSeat: GateOfficerSeat | undefined): string {
503
- return gateSeat ?? "auditor";
504
- }
505
-
506
- /**
507
- * Auditor lifecycle via the shared institutional sub-session adapter.
508
- * Adapter keeps role label / soul / decision tool / result projection only.
509
- * No tools allowlist (ADR 0064). Provider-stream idle-only retry (ADR 0059).
510
- * Durable child session via ADR 0065 archivist entry.
511
- */
512
- export async function executeAuditorChild(
513
- options: AuditorRoleOptions,
514
- ): Promise<{ decision: unknown; response: AssistantMessage; noReceiptLifecycle?: NoReceiptLifecycleFacts }> {
515
- const runDirectory = options.runDirectory ?? auditorRunDirectory(options.context);
516
- if (runDirectory === undefined) {
517
- throw new Error(`${options.roleLabel} requires a run directory carrying the institutional resolution page`);
518
- }
519
- const seat = auditorSeatKey(options.gateSeat);
520
- const selection = await readInstitutionalSeatSelection(runDirectory, seat);
521
-
522
- return withInProcessScratch({ prefix: "ak-auditor-role-" }, async (scratch) => {
523
- const cwd = options.context.cwd ?? process.cwd();
524
-
525
- let decision: unknown;
526
- let noReceiptLifecycle: NoReceiptLifecycleFacts | undefined;
527
- let decisionSubmitted = false;
528
- let decisionCallId: string | undefined;
529
- let decisionToolFailure: unknown;
530
- const decisionToolFailures = new Map<string, unknown>();
531
- const delivery = createReceiptDeliveryPolicy();
532
- const tool = {
533
- ...options.tool,
534
- label: options.roleLabel,
535
- async execute(...args: any[]) {
536
- if (decisionSubmitted && decisionCallId !== args[0]) {
537
- throw new Error("Auditor decision was submitted more than once");
538
- }
539
- // Pi may already have issued several decision calls in one assistant
540
- // response. Execute every issued call: the budget limits future
541
- // solicitations, not terminal calls already in flight.
542
- try {
543
- const result = await options.tool.execute(...args);
544
- delivery.recordAccepted();
545
- const rawDecision = args[1];
546
- const isMissingArgs = rawDecision === undefined
547
- || (typeof rawDecision === "object" && rawDecision !== null && !Array.isArray(rawDecision) && Object.keys(rawDecision).length === 0);
548
- decision = isMissingArgs ? undefined : rawDecision;
549
- decisionCallId = args[0];
550
- decisionToolFailure = undefined;
551
- decisionToolFailures.delete(args[0]);
552
- decisionSubmitted = true;
553
- return { ...result, terminate: true };
554
- } catch (error) {
555
- decisionToolFailure = error;
556
- decisionToolFailures.set(args[0], error);
557
- throw error;
558
- }
559
- },
560
- };
561
-
562
- const parentSessionManager = options.context.sessionManager;
563
- const parentHeader = parentSessionManager?.getHeader?.();
564
- const parentSessionFile = parentSessionManager?.getSessionFile?.();
565
- const parentAttemptEntryId = parentSessionManager?.getLeafId?.();
566
- // Institutional child under parent placement for gate/settlement discovery.
567
- // No ticket:seat subject nest (#636 deleted / ADR 0079). Public officer seats
568
- // resume via tryResumeSameTicketSeatRun on the seat's own run — not this nest.
569
- const { createRecordSession } = await import("./archivist-record-entry.ts");
570
- const auditorSessionManager: SessionManager = createRecordSession({
571
- cwd,
572
- kind: "auditor-roles",
573
- ...(parentSessionManager === undefined ? {} : { parent: parentSessionManager }),
574
- });
575
-
576
- // Shared session open — no tools allowlist (ADR 0064). Auth resolved
577
- // child-locally from the explicit seat selection; adapter owns runtime/provider.
578
- const { openPiInstitutionalSession } = await import("./pi/in-process-session.ts");
579
- const evidenceToolFailures = new Map<string, unknown>();
580
- const wrappedDossierTool = {
581
- ...options.dossierTool,
582
- label: options.roleLabel,
583
- async execute(...args: any[]) {
584
- try {
585
- return await options.dossierTool.execute(...args);
586
- } catch (error) {
587
- evidenceToolFailures.set(args[0], error);
588
- throw error;
589
- }
590
- },
591
- };
592
- const opened = await openPiInstitutionalSession({
593
- cwd,
594
- agentDir: scratch,
595
- selection,
596
- systemPrompt: options.systemPrompt,
597
- customTools: [wrappedDossierTool, tool],
598
- sessionManager: auditorSessionManager,
599
- ...(options.signal === undefined ? {} : { signal: options.signal }),
600
- idleRetry: true,
601
- label: options.roleLabel,
602
- });
603
- const { handle } = opened;
604
-
605
- const binding: AuditorParentAttemptBinding = {
606
- version: 1,
607
- parent: {
608
- ...(parentHeader?.id === undefined ? {} : { sessionId: parentHeader.id }),
609
- ...(parentSessionFile === undefined ? {} : { sessionFile: parentSessionFile }),
610
- ...(parentAttemptEntryId === null || parentAttemptEntryId === undefined
611
- ? {}
612
- : { attemptEntryId: parentAttemptEntryId }),
613
- },
614
- };
615
- // Durable binding is a prerequisite: never observe the provider when its
616
- // response could not later be tied to the current parent attempt.
617
- auditorSessionManager.appendCustomEntry(AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, binding);
618
- try {
619
- sitianReport({
620
- level: "event",
621
- kind: "auditor",
622
- cwd,
623
- sessionParent: parentSessionFile,
624
- payload: { type: AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, ...binding },
625
- source: "evidence-child-executor",
626
- });
627
- } catch {}
628
-
629
- const sessionUsage = emptyUsage();
630
- let boundaryResponse: AssistantMessage | undefined;
631
- let retentionFailure: unknown;
632
- let retainedResponse: AssistantMessage | undefined;
633
- let rejectedDecisionResponse: AssistantMessage | undefined;
634
- let promptNeighboringFailure: unknown;
635
- let promptDecisionFailures: unknown[] = [];
636
- const findToolFailure = (response: AssistantMessage): unknown => {
637
- const callIds = response.content.flatMap((part) =>
638
- part.type === "toolCall" && part.name !== tool.name ? [part.id] : []);
639
- for (const callId of callIds) {
640
- if (evidenceToolFailures.has(callId)) return evidenceToolFailures.get(callId);
641
- }
642
- return undefined;
643
- };
644
- const drainRejectedDecisionFailures = (response: AssistantMessage) => {
645
- for (const part of response.content) {
646
- if (part.type !== "toolCall" || part.name !== tool.name || !decisionToolFailures.has(part.id)) continue;
647
- decisionToolFailure = decisionToolFailures.get(part.id);
648
- promptDecisionFailures.push(decisionToolFailure);
649
- decisionToolFailures.delete(part.id);
650
- }
651
- };
652
- const retainedAssistants: AssistantMessage[] = [];
653
- const unsubscribe = handle.subscribe((event) => {
654
- // Capture evidence-tool (non-decision) failures from the handle's real
655
- // tool_result events. The old runCompletion path wrapped every child tool's
656
- // execute to observe failures; through the real handle the adapter forwards
657
- // tool_execution_end isError. Record the errored tool call id so an adjacent
658
- // evidence failure can outrank a settled/correctable decision feedback.
659
- if (event.type === "tool_result" && event.isError === true && event.toolName !== tool.name) {
660
- // Through the real handle the child flattens a throwing evidence tool
661
- // into a toolResult error whose content text carries the diagnostic
662
- // (e.g. "ENOENT: no such file or directory..."). Recover that text so
663
- // the adjacent evidence failure outranks a settled decision feedback
664
- // with its original diagnostic, mirroring the pre-migration wrapped
665
- // execute capture (which held the raw error object).
666
- const detailText = extractToolResultText(event.details);
667
- // A native unknown-tool receipt ("Tool <name> not found") is not an
668
- // evidence-tool failure: the child session has no such tool registered,
669
- // so its errored toolResult must not short-circuit the auditor (it keeps
670
- // prompting, mirroring the pre-migration registeredToolNames exclusion
671
- // in findToolFailure).
672
- if (detailText !== undefined && /^Tool\s+.+ not found$/.test(detailText.trim())) return;
673
- const failure = detailText === undefined
674
- ? new Error(event.toolName ?? "evidence tool failed")
675
- : new Error(detailText);
676
- // Recover the errno code from the flattened diagnostic so an evidence
677
- // failure's identity (e.g. code "ENOENT") is preserved across the real
678
- // HTTP round-trip, mirroring the pre-migration raw error object.
679
- const errno = /^([A-Z_]+):/.exec(detailText ?? "");
680
- if (errno !== null && errno[1] !== undefined) (failure as NodeJS.ErrnoException).code = errno[1];
681
- evidenceToolFailures.set(event.toolCallId, failure);
682
- }
683
- if (event.type === "message_end" && event.role === "assistant" && boundaryResponse === undefined) {
684
- if (event.usage) addUsage(sessionUsage, event.usage);
685
- const msg = event.message as AssistantMessage | undefined;
686
- retainedResponse = msg;
687
- if (msg) {
688
- retainedAssistants.push(msg);
689
- try { options.retainResponse?.(msg); } catch (error) { retentionFailure = error; }
690
- // A tool call in assistant output is only an observation. Preserve its
691
- // candidate for typed malformed-decision settlement, but the wrapped
692
- // execute path above is the sole owner of accepted-receipt state; a
693
- // rejected execution must remain retryable in this same session.
694
- for (const part of msg.content) {
695
- if (part.type === "toolCall" && part.name === tool.name) {
696
- rejectedDecisionResponse = msg;
697
- if (decision === undefined) {
698
- decision = (part.arguments === undefined
699
- || (typeof part.arguments === "object" && part.arguments !== null
700
- && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0))
701
- ? undefined
702
- : part.arguments;
703
- decisionCallId = part.id;
704
- // Pi can reject malformed root arguments before invoking execute;
705
- // that remains the existing unreadable-candidate failure path.
706
- // A missing root argument reaches the real provider adapter as an
707
- // empty object after serialization — treat it as missing too so a
708
- // one-shot typed missing-args settlement does not solicit another turn.
709
- if (part.arguments === undefined
710
- || (typeof part.arguments === "object" && part.arguments !== null
711
- && !Array.isArray(part.arguments) && Object.keys(part.arguments).length === 0)) {
712
- decisionSubmitted = true;
713
- }
714
- }
715
- }
716
- }
717
- if (msg.stopReason === "error") boundaryResponse = msg;
718
- }
719
- }
720
- if (event.type === "turn_end") {
721
- if (rejectedDecisionResponse !== undefined) {
722
- promptNeighboringFailure = findToolFailure(rejectedDecisionResponse);
723
- drainRejectedDecisionFailures(rejectedDecisionResponse);
724
- }
725
- if (decisionSubmitted || promptNeighboringFailure !== undefined
726
- || (boundaryResponse !== undefined && rejectedDecisionResponse === undefined)
727
- || retentionFailure !== undefined) {
728
- handle.abort();
729
- }
730
- }
731
- });
732
- const abort = () => { handle.abort(); };
733
- if (options.signal?.aborted) abort();
734
- else options.signal?.addEventListener("abort", abort, { once: true });
735
-
736
- let auditorFailure: unknown;
737
- try {
738
- try {
739
- const promptAllowingRejectedDecision = async (prompt: string) => {
740
- rejectedDecisionResponse = undefined;
741
- promptNeighboringFailure = undefined;
742
- decisionToolFailure = undefined;
743
- promptDecisionFailures = [];
744
- let promptFailure: unknown;
745
- try {
746
- await handle.prompt(prompt);
747
- } catch (error) {
748
- promptFailure = error;
749
- }
750
- // Prefer turn_end correlation, but Pi may reject prompt() before that
751
- // event. In that case correlate against this prompt's captured decision
752
- // response and call-id maps at the catch boundary.
753
- const correlatedResponse = rejectedDecisionResponse as AssistantMessage | undefined;
754
- if (correlatedResponse !== undefined) {
755
- promptNeighboringFailure ??= findToolFailure(correlatedResponse);
756
- drainRejectedDecisionFailures(correlatedResponse);
757
- }
758
- // An adjacent failure outranks correctable decision feedback.
759
- if (promptNeighboringFailure !== undefined) throw promptNeighboringFailure;
760
- // An accepted correction in the same response owns the terminal
761
- // outcome; correlated rejected siblings remain observations, not a
762
- // stale failure capable of replacing that accepted receipt.
763
- if (decisionSubmitted) {
764
- decisionToolFailure = undefined;
765
- return;
766
- }
767
- if (decisionToolFailure !== undefined) return;
768
- if (retentionFailure !== undefined) return;
769
- if (opened.streamFailure !== undefined) throw opened.streamFailure;
770
- if (promptFailure !== undefined) throw promptFailure;
771
- };
772
- const chargeAndClearRejectedDecisionFailures = (failures: unknown[]) => {
773
- for (const failure of failures) {
774
- delivery.recordRejected(failure instanceof Error ? failure.message : String(failure));
775
- }
776
- decisionToolFailure = undefined;
777
- promptDecisionFailures = [];
778
- };
779
- await promptAllowingRejectedDecision(options.prompt);
780
- while (!decisionSubmitted && retentionFailure === undefined && (boundaryResponse === undefined || decisionToolFailure !== undefined)
781
- && opened.streamFailure === undefined && delivery.nextAction() === "request-delivery") {
782
- if (decisionToolFailure !== undefined) {
783
- const failures = promptDecisionFailures.length === 0
784
- ? [decisionToolFailure]
785
- : promptDecisionFailures;
786
- chargeAndClearRejectedDecisionFailures(failures);
787
- if (delivery.nextAction() === "no-receipt") boundaryResponse = undefined;
788
- if (delivery.nextAction() === "request-delivery") {
789
- // A rejection and its correction solicitation are one budget unit;
790
- // recordRejected already charged it.
791
- if (retainedResponse === rejectedDecisionResponse) {
792
- await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
793
- chargeAndClearRejectedDecisionFailures(promptDecisionFailures);
794
- }
795
- }
796
- } else {
797
- delivery.recordDeliveryRequest();
798
- await promptAllowingRejectedDecision(RECEIPT_DELIVERY_PROMPT);
799
- }
800
- }
801
- if (!decisionSubmitted && retentionFailure === undefined && opened.streamFailure === undefined
802
- && delivery.nextAction() === "no-receipt") {
803
- const runPointer = options.context.sessionManager.getSessionFile() ?? options.context.cwd ?? process.cwd();
804
- const attemptPointer = binding.parent.attemptEntryId ?? binding.parent.sessionId ?? `current:${runPointer}`;
805
- const facts = delivery.facts({ runPointer, attemptPointer });
806
- decision = facts;
807
- // Late turn_end feedback cannot overturn a lifecycle that has already
808
- // charged this prompt to the exhausted shared budget.
809
- decisionToolFailure = undefined;
810
- auditorSessionManager.appendCustomEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
811
- try {
812
- sitianReport({
813
- level: "event",
814
- kind: "auditor",
815
- cwd,
816
- sessionParent: parentSessionFile,
817
- payload: { type: NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, ...facts },
818
- source: "evidence-child-executor",
819
- });
820
- } catch {}
821
- // Provenance is granted only after the lifecycle owner persisted the
822
- // current child record; accepted model arguments can never set it.
823
- noReceiptLifecycle = facts;
824
- }
825
- } catch (error) {
826
- if (options.signal?.aborted) throw options.signal.reason;
827
- if (retentionFailure === undefined && opened.streamFailure !== undefined) throw opened.streamFailure;
828
- if (retentionFailure === undefined) throw error;
829
- }
830
- if (options.signal?.aborted) throw options.signal.reason;
831
- if (retentionFailure === undefined && opened.streamFailure !== undefined) throw opened.streamFailure;
832
- if (!decisionSubmitted && decisionToolFailure !== undefined) throw decisionToolFailure;
833
- const relevantResponse = !decisionSubmitted
834
- ? boundaryResponse
835
- : (retainedResponse && retainedResponse.role === "assistant" && retainedResponse.content.some((part) => part.type === "toolCall" && part.name === tool.name)
836
- ? retainedResponse
837
- : undefined);
838
- if (relevantResponse !== undefined) {
839
- const toolFailure = findToolFailure(relevantResponse);
840
- if (toolFailure !== undefined) throw toolFailure;
841
- }
842
- const assistants = [...retainedAssistants].reverse();
843
- const response = !decisionSubmitted
844
- ? assistants[0]
845
- : assistants.find((message) =>
846
- message.content.some((part) => part.type === "toolCall" && part.name === tool.name));
847
-
848
- if (response !== undefined) {
849
- try {
850
- if (retentionFailure !== undefined) throw retentionFailure;
851
- if (retainedResponse === undefined) options.retainResponse?.(response);
852
- } catch (retentionFailure) {
853
- if (response.stopReason !== "error") throw retentionFailure;
854
- // Do not trim/rewrite the held errorMessage bytes.
855
- const diagnostic = typeof response.errorMessage === "string" && response.errorMessage.trim() !== ""
856
- ? response.errorMessage
857
- : undefined;
858
- const projected = projectStructuredRemote(response);
859
- const failure = new Error(
860
- diagnostic ?? "",
861
- { cause: retentionFailure },
862
- ) as Error & {
863
- knownCause: "provider" | "unrecognized";
864
- failureCode?: string;
865
- details: Record<string, unknown>;
866
- };
867
- if (projected.hasTestimony && (response.model || response.provider)) {
868
- failure.name = response.model || response.provider || "Error";
869
- failure.failureCode = response.provider || response.model;
870
- }
871
- failure.knownCause = projected.hasTestimony ? "provider" : "unrecognized";
872
- const retentionError = retentionFailure instanceof Error ? retentionFailure : undefined;
873
- const retentionCause = retentionError?.cause;
874
- failure.details = {
875
- ...(diagnostic === undefined ? {} : { errorMessage: diagnostic }),
876
- ...(projected.hasTestimony && response.provider ? { provider: response.provider } : {}),
877
- ...(projected.hasTestimony && response.model ? { model: response.model } : {}),
878
- ...(response.api ? { api: response.api } : {}),
879
- ...(response.rawStopReason ? { rawStopReason: response.rawStopReason } : {}),
880
- ...(projected.httpStatus === undefined ? {} : { httpStatus: projected.httpStatus }),
881
- ...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
882
- ...(projected.body === undefined ? {} : { body: projected.body }),
883
- ...(projected.code === undefined ? {} : { code: projected.code }),
884
- ...(projected.errno === undefined ? {} : { errno: projected.errno }),
885
- retentionFailure: {
886
- name: retentionError?.name ?? typeof retentionFailure,
887
- message: retentionError?.message ?? String(retentionFailure),
888
- ...((retentionError as Error & { code?: unknown } | undefined)?.code !== undefined
889
- ? { code: (retentionError as Error & { code?: unknown }).code }
890
- : {}),
891
- ...(retentionCause === undefined
892
- ? {}
893
- : {
894
- cause: retentionCause instanceof Error
895
- ? {
896
- name: retentionCause.name,
897
- message: retentionCause.message,
898
- ...((retentionCause as Error & { code?: unknown }).code === undefined
899
- ? {}
900
- : { code: (retentionCause as Error & { code?: unknown }).code }),
901
- }
902
- : retentionCause,
903
- }),
904
- },
905
- };
906
- const failureData = {
907
- version: 1 as const,
908
- parent: binding.parent,
909
- failure: {
910
- cause: failure.knownCause,
911
- ...(failure.failureCode === undefined ? {} : { identity: { name: failure.name, code: failure.failureCode } }),
912
- ...(failure.message === "" ? {} : { diagnostic: failure.message }),
913
- details: failure.details,
914
- },
915
- };
916
- auditorSessionManager.appendCustomEntry(AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, failureData);
917
- try {
918
- sitianReport({
919
- level: "event",
920
- kind: "auditor",
921
- cwd,
922
- sessionParent: parentSessionFile,
923
- payload: { type: AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, ...failureData },
924
- source: "evidence-child-executor",
925
- });
926
- } catch {}
927
- throw failure;
928
- }
929
- }
930
-
931
- if (
932
- response === undefined
933
- || response.stopReason === "error"
934
- || (!decisionSubmitted && (response.stopReason === "aborted" || decision === undefined))
935
- ) {
936
- throw new Error(response?.errorMessage ?? `${options.roleLabel} exited without a readable decision receipt`);
937
- }
938
- return {
939
- decision,
940
- response: { ...response, usage: sessionUsage },
941
- ...(noReceiptLifecycle === undefined ? {} : { noReceiptLifecycle }),
942
- };
943
- } catch (error) {
944
- auditorFailure = error;
945
- throw error;
946
- } finally {
947
- options.signal?.removeEventListener("abort", abort);
948
- await runChildCleanup([() => unsubscribe(), () => handle.close()], auditorFailure, options.roleLabel);
949
- }
950
- });
951
- }
952
-
953
- function exactRecord(value: unknown): value is Record<string, unknown> {
954
- return typeof value === "object" && value !== null && !Array.isArray(value);
955
- }
956
-
957
- /**
958
- * Navigator institutional child on this unique lifecycle seam (#590 / ADR 0018).
959
- * Owns record-session / open / subscribe / close. Caller keeps tool, label, and
960
- * failure projection only.
961
- */
962
- export function createNativeNavigatorSessionFactory(
963
- defaultModelSettingPath = navigatorModelSettingPath(),
964
- ): NavigatorSessionFactory {
965
- return async ({ context, subject, modelSettingPath, tool }) => {
966
- const resolved = await resolveNavigatorSeatSelection(context, modelSettingPath, defaultModelSettingPath);
967
- let selection = resolved.selection;
968
- let thinkingLevel: string | undefined = resolved.thinkingLevel;
969
- let configuredLabel = resolved.configuredLabel;
970
-
971
- const { createRecordSession } = await import("./archivist-record-entry.ts");
972
- const sessionManager = createRecordSession({
973
- cwd: context.cwd,
974
- kind: "navigator",
975
- subject,
976
- parent: context.sessionManager,
977
- });
978
-
979
- let providerFailure: NavigatorProviderFailureFact | undefined;
980
- let observationWrite: Promise<void> = Promise.resolve();
981
- const assignProviderFailure = (fact: NavigatorProviderFailureFact | undefined): void => {
982
- if (fact !== undefined) providerFailure = fact;
983
- };
984
- const classifyTerminalMessage = (message: unknown): void => {
985
- if (!exactRecord(message)) {
986
- assignProviderFailure(navigatorProviderFailureFromError(message));
987
- return;
988
- }
989
- assignProviderFailure(navigatorProviderFailureFromDiagnostics(message.diagnostics));
990
- if (providerFailure !== undefined) return;
991
- if (message.role !== "assistant") assignProviderFailure(navigatorProviderFailureFromError(message));
992
- if (providerFailure !== undefined) return;
993
- const status = typeof message.statusCode === "number"
994
- ? message.statusCode
995
- : typeof message.status === "number"
996
- ? message.status
997
- : typeof message.httpStatus === "number"
998
- ? message.httpStatus
999
- : undefined;
1000
- if (typeof status === "number") {
1001
- assignProviderFailure(navigatorProviderFailureFromStatus(status));
1002
- if (providerFailure === undefined && status >= 400 && status < 600) {
1003
- assignProviderFailure({ source: "transport", cause: "transport" });
1004
- }
1005
- const runDir = process.env.AK_ROLE_RUN_DIR;
1006
- const provider = typeof message.provider === "string" && message.provider.trim() !== ""
1007
- ? message.provider
1008
- : selection.provider;
1009
- if (typeof runDir === "string" && runDir.trim() !== "" && provider.trim() !== "") {
1010
- observationWrite = observationWrite.then(() =>
1011
- recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider }),
1012
- );
1013
- }
1014
- }
1015
- };
1016
-
1017
- const { openPiInstitutionalSession } = await import("./pi/in-process-session.ts");
1018
- let opened: Awaited<ReturnType<typeof openPiInstitutionalSession>>;
1019
- try {
1020
- opened = await openPiInstitutionalSession({
1021
- cwd: context.cwd,
1022
- selection,
1023
- systemPrompt: "",
1024
- noTools: "all",
1025
- toolsAllowlist: [NAVIGATOR_PREPARE_TOOL_NAME],
1026
- customTools: [tool],
1027
- sessionManager,
1028
- label: "Navigator",
1029
- });
1030
- } catch (error) {
1031
- const fact = navigatorProviderFailureFromError(error);
1032
- throw navigatorUnavailableError(fact?.source ?? "session", error, fact?.cause ?? "session");
1033
- }
1034
-
1035
- const unsubscribe = opened.handle.subscribe((event) => {
1036
- if (event.type === "message_end" && event.message !== undefined) {
1037
- const message = event.message;
1038
- if (exactRecord(message) && (message.stopReason === "error" || message.stopReason === "aborted")) {
1039
- classifyTerminalMessage(message);
1040
- }
1041
- }
1042
- });
1043
-
1044
- let disposal: Promise<void> | undefined;
1045
- const dispose = (): Promise<void> => {
1046
- if (disposal === undefined) {
1047
- disposal = runChildCleanup(
1048
- [() => unsubscribe(), () => opened.handle.close()],
1049
- undefined,
1050
- "Navigator",
1051
- );
1052
- }
1053
- return disposal;
1054
- };
1055
-
1056
- return {
1057
- prompt: async (text) => {
1058
- providerFailure = undefined;
1059
- observationWrite = Promise.resolve();
1060
- const failFrom = (error: unknown): never => {
1061
- if (providerFailure === undefined) {
1062
- assignProviderFailure({ source: "transport", cause: "transport" });
1063
- }
1064
- const fact = providerFailure!;
1065
- throw navigatorUnavailableError(fact.source, error, fact.cause);
1066
- };
1067
- let terminal: unknown;
1068
- try {
1069
- const turn = await opened.handle.prompt(text);
1070
- if (turn.stopReason === "error" || turn.stopReason === "aborted") {
1071
- const cause = turn.errorMessage ?? opened.streamFailure ?? "Navigator provider failure";
1072
- if (providerFailure === undefined && opened.streamFailure !== undefined) {
1073
- classifyTerminalMessage(opened.streamFailure);
1074
- }
1075
- if (providerFailure === undefined) {
1076
- assignProviderFailure(navigatorProviderFailureFromError(
1077
- typeof cause === "object" && cause !== null ? cause : new Error(String(cause)),
1078
- ));
1079
- }
1080
- terminal = cause;
1081
- } else if (opened.streamFailure !== undefined) {
1082
- if (providerFailure === undefined) classifyTerminalMessage(opened.streamFailure);
1083
- terminal = opened.streamFailure;
1084
- }
1085
- } catch (error) {
1086
- if (error instanceof NavigatorUnavailableError) throw error;
1087
- if (providerFailure === undefined) classifyTerminalMessage(error);
1088
- terminal = error;
1089
- }
1090
- await observationWrite;
1091
- if (terminal !== undefined) failFrom(terminal);
1092
- },
1093
- providerFailure: () => providerFailure,
1094
- appendEntry: (customType, data) => {
1095
- sessionManager.appendCustomEntry(customType, data);
1096
- try {
1097
- sitianReport({
1098
- level: "event",
1099
- kind: "attendance",
1100
- cwd: context.cwd,
1101
- sessionParent: sessionManager.getSessionFile(),
1102
- payload: { customType, data },
1103
- source: "evidence-child-executor",
1104
- });
1105
- } catch {
1106
- // best-effort
1107
- }
1108
- },
1109
- entries: () => sessionManager.getEntries(),
1110
- setModel: async (next) => {
1111
- let nextParsed: ReturnType<typeof parseNavigatorModelSetting>;
1112
- try {
1113
- nextParsed = parseNavigatorModelSetting(next);
1114
- } catch (error) {
1115
- throw navigatorUnavailableError("model", error);
1116
- }
1117
- if (
1118
- nextParsed.provider !== selection.provider
1119
- || nextParsed.model !== selection.model
1120
- ) {
1121
- throw new NavigatorUnavailableError(
1122
- "model",
1123
- `Navigator model switch requires a new session: ${configuredLabel} → ${next}`,
1124
- );
1125
- }
1126
- // Thinking is opaque pass-through; no stick/availability re-check.
1127
- selection = {
1128
- provider: nextParsed.provider,
1129
- model: nextParsed.model,
1130
- ...(nextParsed.thinkingLevel === undefined ? {} : { thinking: nextParsed.thinkingLevel }),
1131
- };
1132
- thinkingLevel = nextParsed.thinkingLevel;
1133
- configuredLabel = next;
1134
- },
1135
- getThinkingLevel: () => thinkingLevel,
1136
- recordPointer: () => sessionManager.getSessionDir(),
1137
- dispose,
1138
- };
1139
- };
1140
- }