@akagilnc/pi-workflow-roles 0.1.5845 → 0.1.5855

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 (66) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/dist/acp-host/production-host.js +348 -1254
  4. package/dist/acp-host/role-turn-host.js +4 -98
  5. package/dist/headless-host/production-host.js +15731 -16582
  6. package/dist/headless-host/role-turn-host.js +6 -31
  7. package/dist/migrate-book-topology.js +31 -16
  8. package/dist/navigator-attendance.js +54 -404
  9. package/dist/navigator-invocation-identity.js +0 -2
  10. package/dist/navigator-lookup-host.js +41 -0
  11. package/dist/navigator-lookup.js +68 -0
  12. package/dist/navigator-route-table.js +42 -0
  13. package/dist/package-contracts/navigator-output.js +3 -11
  14. package/dist/packaged-role-registry.js +3 -27
  15. package/dist/public-cli/instruction-seat-run.js +4 -1
  16. package/dist/public-cli/invocation.js +13 -26
  17. package/dist/public-cli/main.js +286 -114
  18. package/dist/public-cli/option-definitions.js +24 -4
  19. package/dist/public-cli/post-admission.js +0 -9
  20. package/dist/public-cli/role-seat-runtime.js +14 -0
  21. package/dist/public-cli/settlement.js +14 -40
  22. package/dist/public-cli/terminal.js +1 -8
  23. package/dist/public-cli/turn-request.js +0 -3
  24. package/dist/public-role-summons.js +0 -1
  25. package/dist/receipt-delivery-policy.js +0 -23
  26. package/dist/role-runtime-dependencies.js +4 -11
  27. package/dist/role-runtime.js +18 -194
  28. package/dist/submission-ledger.js +19 -15
  29. package/dist/ticket-progress.js +23 -5
  30. package/extensions/role-runtime.ts +0 -16
  31. package/package.json +1 -1
  32. package/scripts/build-package.mjs +3 -7
  33. package/src/acp-host/role-turn-host.ts +8 -97
  34. package/src/headless-host/description.ts +0 -2
  35. package/src/headless-host/role-turn-host.ts +7 -32
  36. package/src/host-contracts.ts +5 -7
  37. package/src/navigator-attendance.ts +86 -549
  38. package/src/navigator-invocation-identity.ts +0 -2
  39. package/src/navigator-lookup-host.ts +45 -0
  40. package/src/navigator-lookup.ts +98 -0
  41. package/src/navigator-route-table.ts +92 -0
  42. package/src/package-contracts/navigator-output.ts +4 -15
  43. package/src/packaged-role-registry.ts +3 -32
  44. package/src/public-cli/instruction-seat-run.ts +3 -0
  45. package/src/public-cli/invocation.ts +18 -30
  46. package/src/public-cli/option-definitions.ts +25 -4
  47. package/src/public-cli/post-admission.ts +0 -11
  48. package/src/public-cli/role-seat-runtime.ts +14 -0
  49. package/src/public-cli/settlement.ts +23 -51
  50. package/src/public-cli/terminal.ts +3 -18
  51. package/src/public-cli/turn-request.ts +0 -5
  52. package/src/public-role-summons.ts +0 -8
  53. package/src/receipt-delivery-policy.ts +0 -21
  54. package/src/role-runtime-dependencies.ts +4 -11
  55. package/src/role-runtime.ts +19 -210
  56. package/src/submission-ledger.ts +23 -16
  57. package/src/ticket-progress.ts +32 -5
  58. package/dist/navigator-public-session.js +0 -205
  59. package/dist/navigator-role.js +0 -6
  60. package/dist/navigator-session-contracts.js +0 -227
  61. package/dist/navigator-work-context.js +0 -72
  62. package/resources/navigator-route-playbook.md +0 -33
  63. package/src/navigator-public-session.ts +0 -305
  64. package/src/navigator-role.ts +0 -15
  65. package/src/navigator-session-contracts.ts +0 -320
  66. package/src/navigator-work-context.ts +0 -97
@@ -1,305 +0,0 @@
1
- /**
2
- * Navigator attendance session factory (#675 r3 / #1178).
3
- * Each prepare turn uses the public navigator activation path (summonPublicRole) —
4
- * same seat table and shared envelope as `ak-role navigator`.
5
- * Advice and run facts stay on the nested @navigator run dossier only —
6
- * no books/<book>/navigator/<hash>/ side-branch nest, work-context file,
7
- * current-session pointer, or attendance parallel ledger (#1178).
8
- * Host conversation continuity uses CLI resume (`resumeRunId`) within this
9
- * attendance lifetime (in-memory pointer). Parallel byStatus prepare is #1160.
10
- */
11
- import { basename } from "node:path";
12
-
13
- import { sitianReportSafe } from "./host-session-record.ts";
14
- import {
15
- NavigatorUnavailableError,
16
- navigatorProviderFailureFromPublicTerminal,
17
- navigatorProviderFailureFromError,
18
- navigatorUnavailableError,
19
- parseNavigatorModelSetting,
20
- type NavigatorProviderFailureFact,
21
- type NavigatorSessionFactory,
22
- } from "./navigator-session-contracts.ts";
23
- import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
24
- import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
25
- import { parseRunLeaf } from "./role-run-placement.ts";
26
- import type { PublicSummonResult } from "./public-role-summons.ts";
27
- import { CliUsageError } from "./public-cli/cli-errors.ts";
28
- import { isNavigatorSeat } from "./packaged-role-registry.ts";
29
-
30
- /**
31
- * Ledger process home for navigator summon: admitted HostContext.runDirectory
32
- * first, else a parent session file under .ak-roles. Never context.home / env HOME /
33
- * passwd guesses (#852).
34
- */
35
- async function resolveNavigatorLedgerHome(context: HostContext): Promise<string | undefined> {
36
- const { tryHomeFromAkRolesPath } = await import(
37
- "./activation-ledger-topology.ts"
38
- );
39
- const runDirectory = runDirectoryFromHostContext(context);
40
- if (runDirectory !== undefined) {
41
- const fromRun = tryHomeFromAkRolesPath(runDirectory);
42
- if (fromRun !== undefined && fromRun.length > 0) return fromRun;
43
- }
44
- const parentFile = context.sessionManager?.getSessionFile?.();
45
- if (typeof parentFile === "string" && parentFile.trim() !== "") {
46
- const fromParent = tryHomeFromAkRolesPath(parentFile);
47
- if (fromParent !== undefined && fromParent.length > 0) return fromParent;
48
- }
49
- return undefined;
50
- }
51
-
52
- export function runIdFromNavigatorDirectory(runDirectory: string): string | undefined {
53
- const parsed = parseRunLeaf(basename(runDirectory));
54
- if (parsed === undefined || parsed.role !== "navigator") return undefined;
55
- return parsed.runId;
56
- }
57
-
58
- /**
59
- * Typed preflight via public CLI load path: can this run reopen as navigator?
60
- * CliUsageError (code AK_ROLE_USAGE) from loadResumablePublicRole means
61
- * unknown id or principal unavailable — not resumable. Any other disk role
62
- * is not this navigator host run. Other throws propagate. Never reads stderr prose.
63
- */
64
- export async function navigatorHostRunResumable(
65
- home: string,
66
- runId: string,
67
- runDirectory?: string,
68
- ): Promise<boolean> {
69
- const { piDurablePrincipalAuthority } = await import("./pi/durable-principal.ts");
70
- const { loadResumablePublicRole } = await import("./public-cli/run-lifecycle.ts");
71
- try {
72
- const loaded = await loadResumablePublicRole(
73
- home, runId, piDurablePrincipalAuthority,
74
- runDirectory === undefined ? undefined : { runDirectory },
75
- );
76
- return isNavigatorSeat(loaded.admitted.role);
77
- } catch (error) {
78
- if (error instanceof CliUsageError) return false;
79
- throw error;
80
- }
81
- }
82
-
83
- export type NavigatorPublicSummon = (options: {
84
- readonly role: "navigator";
85
- readonly argv: readonly string[];
86
- readonly cwd: string;
87
- readonly home?: string;
88
- readonly resumeRunId?: string;
89
- readonly resumeRunDirectory?: string;
90
- /** Shared-lifecycle cancel forwarded from HostContext.signal (#675 / #959). */
91
- readonly signal?: AbortSignal;
92
- /** #1160 attendance auto byStatus prepare — host mounts structured schema. */
93
- readonly navigatorByStatusPrepare?: boolean;
94
- }) => Promise<PublicSummonResult>;
95
-
96
- type MemoryEntry = {
97
- readonly type: string;
98
- readonly customType?: string;
99
- readonly data?: unknown;
100
- };
101
-
102
- export function createNativeNavigatorSessionFactory(deps?: {
103
- /** Test/composition inject — production leaves unset and uses public-role-summons. */
104
- readonly summonPublicRole?: NavigatorPublicSummon;
105
- /** Test inject — production uses navigatorHostRunResumable (typed CLI load). */
106
- readonly hostRunResumable?: (home: string, runId: string, runDirectory?: string) => Promise<boolean>;
107
- }): NavigatorSessionFactory {
108
- return async ({ context, tool }) => {
109
- // Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
110
- // Factory open only holds in-memory attendance bookkeeping — no side-branch nest.
111
- const entries: MemoryEntry[] = [];
112
- let providerFailure: NavigatorProviderFailureFact | undefined;
113
- let noReceipt: Partial<NoReceiptLifecycleFacts> | undefined;
114
- let routePlaybookReadFailure: string | undefined;
115
- let disposed = false;
116
- /** Existing in-memory host continuity, retaining the directory already selected. */
117
- let hostRun: { runId: string; runDirectory: string } | undefined;
118
-
119
- const summon: NavigatorPublicSummon = deps?.summonPublicRole
120
- ?? (async (options) => {
121
- const { summonPublicRole } = await import("./public-role-summons.ts");
122
- return summonPublicRole(options);
123
- });
124
-
125
- const parentSessionFile = (): string | undefined =>
126
- context.sessionManager?.getSessionFile?.();
127
-
128
- return {
129
- prompt: async (text) => {
130
- if (disposed) {
131
- throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
132
- }
133
- providerFailure = undefined;
134
- noReceipt = undefined;
135
- routePlaybookReadFailure = undefined;
136
- try {
137
- const summonHome = await resolveNavigatorLedgerHome(context);
138
- // Admission after every await: dispose during preflight must not start summon
139
- // (ADR 0018 / #959 — legal HostContext may omit signal).
140
- if (disposed) return;
141
- const resumeRunId = hostRun?.runId;
142
-
143
- // Call contract only: forward shared-lifecycle signal; do not own AbortController here
144
- // (ADR 0018 / #959 — cancel ownership stays on the attendance/envelope seam).
145
- const baseSummon = {
146
- role: "navigator" as const,
147
- argv: [text] as const,
148
- cwd: context.cwd,
149
- // Attendance prepare always asks for structured byStatus (#1160).
150
- // Direct `ak-role navigator` never sets this flag.
151
- navigatorByStatusPrepare: true as const,
152
- ...(summonHome === undefined ? {} : { home: summonHome }),
153
- ...(context.signal === undefined ? {} : { signal: context.signal }),
154
- };
155
-
156
- // Prefer host CLI resume so prior advice stays on the host session.
157
- // Fresh mint only when typed load says the principal cannot reopen.
158
- const resumable = deps?.hostRunResumable ?? navigatorHostRunResumable;
159
- let summoned: PublicSummonResult;
160
- if (resumeRunId === undefined || summonHome === undefined) {
161
- if (disposed) return;
162
- summoned = await summon(baseSummon);
163
- } else {
164
- const canResume = await resumable(summonHome, resumeRunId, hostRun?.runDirectory);
165
- if (disposed) return;
166
- if (canResume) {
167
- summoned = await summon({
168
- ...baseSummon,
169
- resumeRunId,
170
- ...(hostRun === undefined ? {} : { resumeRunDirectory: hostRun.runDirectory }),
171
- });
172
- } else {
173
- hostRun = undefined;
174
- summoned = await summon(baseSummon);
175
- }
176
- }
177
-
178
- // Dispose won the race (with or without HostContext.signal): no late pointer/prepare.
179
- // Cancel ownership stays on attendance/envelope; this only closes side effects (#959).
180
- if (disposed) return;
181
-
182
- const playbookFailure = summoned.terminal?.navigator?.advisoryDiagnostic;
183
- routePlaybookReadFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
184
- ? playbookFailure
185
- : undefined;
186
- const outcome = summoned.terminal?.roleOutcome;
187
- if (outcome === undefined) {
188
- const detail = summoned.stderr?.trim() || `exit ${summoned.exitCode}`;
189
- // Known source is the public-summon transport path; unconfirmed cause stays unknown.
190
- providerFailure = { source: "transport", cause: "unknown" };
191
- throw navigatorUnavailableError(
192
- providerFailure.source,
193
- new Error(`Navigator public summon produced no terminal (${detail})`),
194
- providerFailure.cause,
195
- );
196
- }
197
- if (outcome.kind === "failure") {
198
- providerFailure = navigatorProviderFailureFromPublicTerminal(outcome);
199
- const failedAttempts = outcome.decisiveFacts.failedAttempts;
200
- const attemptReasons = Array.isArray(failedAttempts)
201
- ? failedAttempts.map((entry: unknown, index: number) => {
202
- const fact = entry !== null && typeof entry === "object"
203
- ? entry as { attempt?: unknown; diagnostic?: unknown }
204
- : undefined;
205
- return `${typeof fact?.attempt === "number" ? fact.attempt + 1 : index + 1}: ${
206
- typeof fact?.diagnostic === "string" ? fact.diagnostic : outcome.diagnostic
207
- }`;
208
- })
209
- : [];
210
- throw navigatorUnavailableError(
211
- providerFailure.source,
212
- new Error(attemptReasons.length > 0 ? attemptReasons.join("\n") : outcome.diagnostic),
213
- providerFailure.cause,
214
- );
215
- }
216
- if (outcome.kind === "no_receipt") {
217
- // The nested session spent its own delivery budget; attendance settles
218
- // no-receipt on these facts instead of opening another summon (#675).
219
- noReceipt = outcome;
220
- return;
221
- }
222
-
223
- // Pin host run for the next attendance prompt in this lifetime (CLI resume key).
224
- const runDirectory = summoned.runDirectory
225
- ?? (typeof summoned.admitted?.runDirectory === "string"
226
- ? summoned.admitted.runDirectory
227
- : undefined);
228
- if (typeof runDirectory === "string" && runDirectory.trim() !== "") {
229
- const nextRunId = runIdFromNavigatorDirectory(runDirectory);
230
- if (nextRunId !== undefined) {
231
- // In-memory only for this attendance lifetime (#1178) — no side-branch pointer ledger.
232
- hostRun = { runId: nextRunId, runDirectory };
233
- }
234
- }
235
-
236
- if (outcome.kind !== "accepted") {
237
- // Non-accepted kinds (e.g. audit_escalation): no prose advice this turn.
238
- // Not a shape-unusable judgment on the navigator reply (#757).
239
- return;
240
- }
241
- // #959 / #1160: pass through prose and byStatus. No route parsing.
242
- const {
243
- byStatusToPlainObject,
244
- mergePreparedAdvice,
245
- preparedAdviceFromUnknown,
246
- } = await import("./package-contracts/navigator-output.ts");
247
- let prepared = undefined as ReturnType<typeof preparedAdviceFromUnknown>;
248
- for (const payload of outcome.payloads ?? []) {
249
- prepared = mergePreparedAdvice(prepared, preparedAdviceFromUnknown(payload));
250
- }
251
- if (prepared === undefined) return;
252
- if (disposed) return;
253
- await tool.execute(
254
- "navigator-public-prepare",
255
- {
256
- ...(prepared.prose === undefined ? {} : { prose: prepared.prose }),
257
- ...(prepared.byStatus.size === 0 ? {} : { byStatus: byStatusToPlainObject(prepared.byStatus) }),
258
- },
259
- undefined,
260
- undefined,
261
- context as never,
262
- );
263
- } catch (error) {
264
- // Closed session: drain late nest errors without providerFailure side effects.
265
- if (disposed) return;
266
- if (error instanceof NavigatorUnavailableError) throw error;
267
- const fact = navigatorProviderFailureFromError(error);
268
- // Catch path is the public-summon seam (source transport); untyped cause stays unknown.
269
- providerFailure = fact ?? { source: "transport", cause: "unknown" };
270
- throw navigatorUnavailableError(providerFailure.source, error, providerFailure.cause);
271
- }
272
- },
273
- providerFailure: () => providerFailure,
274
- noReceipt: () => noReceipt,
275
- routePlaybookReadFailure: () => routePlaybookReadFailure,
276
- appendEntry: (customType, data) => {
277
- entries.push({ type: "custom", customType, data });
278
- const parent = parentSessionFile();
279
- sitianReportSafe({
280
- level: "event",
281
- kind: "attendance",
282
- cwd: context.cwd,
283
- ...(parent === undefined ? {} : { sessionParent: parent }),
284
- payload: { customType, data },
285
- source: "navigator-public-session",
286
- });
287
- },
288
- entries: () => entries,
289
- setModel: async (next) => {
290
- // Validate only — resume/fresh mint both read the live seat table
291
- // (#675 / #617 DK-3). No test-only thinking observation state (#1160 F1).
292
- try {
293
- parseNavigatorModelSetting(next);
294
- } catch (error) {
295
- throw navigatorUnavailableError("model", error);
296
- }
297
- },
298
- dispose: async () => {
299
- // Marker only — nested cancel and non-blocking teardown are owned by
300
- // navigator-attendance / role-runtime (ADR 0018 / #959).
301
- disposed = true;
302
- },
303
- };
304
- };
305
- }
@@ -1,15 +0,0 @@
1
- import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
2
-
3
- /**
4
- * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
5
- * #959: navigator speaks free-form prose; tool is a lifecycle vehicle only.
6
- */
7
- export const NAVIGATOR_TOOL_SPEC = roleSubmissionDeclaration("navigator");
8
-
9
- export type NavigatorRuntimeDependencies = {
10
- loadSoul(): Promise<string>;
11
- /** Standing route playbook. Production omits this and reads the package file once. */
12
- loadRoutePlaybook?(): Promise<string>;
13
- /** Same read's native failure, for the existing routePlaybookReadFailure outlet. */
14
- recordRoutePlaybookReadFailure?(message: string | undefined): void;
15
- };
@@ -1,320 +0,0 @@
1
- /**
2
- * Navigator session contracts — pure types, model setting, seat resolution, failure classification.
3
- * No lifecycle. Shared by attendance (consumer) and the unique institutional-child seam.
4
- */
5
- import { mkdir, readFile, writeFile } from "node:fs/promises";
6
- import { homedir } from "node:os";
7
- import { dirname, join } from "node:path";
8
-
9
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
10
- import { Type } from "typebox";
11
- import { Value } from "typebox/value";
12
-
13
- import type { HostContext, HostInstitutionalModelSelection } from "./host-contracts.ts";
14
- import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
15
-
16
- import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
17
-
18
- export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
19
-
20
- export type NavigatorUnavailableKey =
21
- | "context" | "session" | "model" | "thinking" | "auth" | "quota" | "transport" | "unknown";
22
-
23
- const NAVIGATOR_UNAVAILABLE_KEYS = new Set<NavigatorUnavailableKey>([
24
- "context", "session", "model", "thinking", "auth", "quota", "transport", "unknown",
25
- ]);
26
-
27
- function navigatorUnavailableKey(value: unknown): NavigatorUnavailableKey | undefined {
28
- return typeof value === "string" && NAVIGATOR_UNAVAILABLE_KEYS.has(value as NavigatorUnavailableKey)
29
- ? value as NavigatorUnavailableKey
30
- : undefined;
31
- }
32
-
33
- export class NavigatorUnavailableError extends Error {
34
- readonly unavailableSource: NavigatorUnavailableKey;
35
- readonly unavailableCause: NavigatorUnavailableKey;
36
- readonly originalCause: unknown;
37
-
38
- constructor(source: NavigatorUnavailableKey, message: string, cause: NavigatorUnavailableKey = source, originalCause?: unknown) {
39
- super(message);
40
- this.name = "NavigatorUnavailableError";
41
- this.unavailableSource = source;
42
- this.unavailableCause = cause;
43
- this.originalCause = originalCause;
44
- }
45
- }
46
-
47
- export type NavigatorProviderFailureFact = {
48
- source: NavigatorUnavailableKey;
49
- cause: NavigatorUnavailableKey;
50
- };
51
-
52
- export function navigatorUnavailableError(
53
- source: NavigatorUnavailableKey,
54
- error: unknown,
55
- cause: NavigatorUnavailableKey = source,
56
- ): NavigatorUnavailableError {
57
- const message = errorText(error);
58
- return error instanceof NavigatorUnavailableError
59
- ? error
60
- : new NavigatorUnavailableError(source, message, cause, error);
61
- }
62
-
63
- export function navigatorProviderFailureFromStatus(status: number | undefined): NavigatorProviderFailureFact | undefined {
64
- if (status === 401 || status === 403) return { source: "auth", cause: "auth" };
65
- if (status === 429) return { source: "quota", cause: "quota" };
66
- return undefined;
67
- }
68
-
69
- function navigatorProviderFailureFromCode(code: unknown): NavigatorProviderFailureFact | undefined {
70
- if (typeof code === "number") return navigatorProviderFailureFromStatus(code);
71
- if (typeof code !== "string") return undefined;
72
- if (code === "unauthorized" || code === "authentication_failed") return { source: "auth", cause: "auth" };
73
- if (code === "insufficient_quota" || code === "quota_exhausted") return { source: "quota", cause: "quota" };
74
- if (code === "transport_error") return { source: "transport", cause: "transport" };
75
- return undefined;
76
- }
77
-
78
- export function navigatorProviderFailureFromError(error: unknown): NavigatorProviderFailureFact | undefined {
79
- let cursor: unknown = error;
80
- const seen = new Set<unknown>();
81
- while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
82
- seen.add(cursor);
83
- if (cursor instanceof NavigatorUnavailableError) {
84
- return { source: cursor.unavailableSource, cause: cursor.unavailableCause };
85
- }
86
- if (!isRecord(cursor)) {
87
- cursor = cursor instanceof Error ? cursor.cause : undefined;
88
- continue;
89
- }
90
- const fromReason = navigatorUnavailableKey(cursor.reason);
91
- if (fromReason !== undefined) return { source: fromReason, cause: fromReason };
92
- const statusCode = typeof cursor.statusCode === "number"
93
- ? cursor.statusCode
94
- : typeof cursor.status === "number"
95
- ? cursor.status
96
- : typeof cursor.httpStatus === "number"
97
- ? cursor.httpStatus
98
- : undefined;
99
- const fromStatus = navigatorProviderFailureFromStatus(statusCode);
100
- if (fromStatus !== undefined) return fromStatus;
101
- const fromCode = navigatorProviderFailureFromCode(cursor.code);
102
- if (fromCode !== undefined) return fromCode;
103
- cursor = cursor.cause;
104
- }
105
- return undefined;
106
- }
107
-
108
- export function navigatorProviderFailureFromDiagnostics(diagnostics: unknown): NavigatorProviderFailureFact | undefined {
109
- if (!Array.isArray(diagnostics)) return undefined;
110
- for (const diagnostic of diagnostics) {
111
- if (!isRecord(diagnostic)) continue;
112
- if (diagnostic.type === "provider_transport_failure") return { source: "transport", cause: "transport" };
113
- if (isRecord(diagnostic.error)) {
114
- const fromCode = navigatorProviderFailureFromCode(diagnostic.error.code);
115
- if (fromCode !== undefined) return fromCode;
116
- }
117
- if (isRecord(diagnostic.details)) {
118
- const status = typeof diagnostic.details.status === "number"
119
- ? diagnostic.details.status
120
- : typeof diagnostic.details.statusCode === "number"
121
- ? diagnostic.details.statusCode
122
- : undefined;
123
- const fromStatus = navigatorProviderFailureFromStatus(status);
124
- if (fromStatus !== undefined) return fromStatus;
125
- const fromCode = navigatorProviderFailureFromCode(diagnostic.details.code);
126
- if (fromCode !== undefined) return fromCode;
127
- }
128
- }
129
- return undefined;
130
- }
131
-
132
- /**
133
- * Classify a public-navigator failure terminal from structured facts only.
134
- * Untyped cause stays unknown — never relabeled session.
135
- */
136
- export function navigatorProviderFailureFromPublicTerminal(outcome: {
137
- readonly cause?: string;
138
- readonly diagnostic: string;
139
- readonly decisiveFacts: Readonly<Record<string, unknown>>;
140
- }): NavigatorProviderFailureFact {
141
- const facts = outcome.decisiveFacts;
142
- const secondary =
143
- typeof facts.secondaryEvidence === "object" && facts.secondaryEvidence !== null
144
- ? (facts.secondaryEvidence as Record<string, unknown>)
145
- : undefined;
146
- const httpStatus =
147
- typeof secondary?.httpStatus === "number"
148
- ? secondary.httpStatus
149
- : typeof facts.httpStatus === "number"
150
- ? facts.httpStatus
151
- : typeof facts.errorCode === "number"
152
- ? facts.errorCode
153
- : undefined;
154
- const fromStatus = navigatorProviderFailureFromStatus(httpStatus);
155
- if (fromStatus !== undefined) return fromStatus;
156
- const diagnostics = secondary?.diagnostics ?? facts.diagnostics;
157
- const fromDiagnostics = navigatorProviderFailureFromDiagnostics(diagnostics);
158
- if (fromDiagnostics !== undefined) return fromDiagnostics;
159
- const fromCode = navigatorProviderFailureFromError({
160
- code: secondary?.code ?? facts.errorCode,
161
- });
162
- if (fromCode !== undefined) return fromCode;
163
- // cause=provider names the known source region only; without status/diagnostics/code
164
- // the specific cause stays unknown (#881 — never invent transport/session labels).
165
- if (outcome.cause === "provider") return { source: "transport", cause: "unknown" };
166
- const typed = navigatorUnavailableKey(outcome.cause);
167
- if (typed !== undefined) return { source: typed, cause: typed };
168
- return { source: "unknown", cause: "unknown" };
169
- }
170
-
171
- const navigatorProviderFailureSchema = Type.Object({
172
- source: Type.Union([
173
- Type.Literal("context"), Type.Literal("session"), Type.Literal("model"), Type.Literal("thinking"),
174
- Type.Literal("auth"), Type.Literal("quota"), Type.Literal("transport"), Type.Literal("unknown"),
175
- ]),
176
- cause: Type.Union([
177
- Type.Literal("context"), Type.Literal("session"), Type.Literal("model"), Type.Literal("thinking"),
178
- Type.Literal("auth"), Type.Literal("quota"), Type.Literal("transport"), Type.Literal("unknown"),
179
- ]),
180
- }, { additionalProperties: false });
181
-
182
- export function navigatorProviderFailure<T extends object>(
183
- message: T,
184
- source: NavigatorUnavailableKey,
185
- cause: NavigatorUnavailableKey = source,
186
- ): T & { navigatorFailure: NavigatorProviderFailureFact } {
187
- const fact = { source, cause } satisfies NavigatorProviderFailureFact;
188
- if (!Value.Check(navigatorProviderFailureSchema, fact)) throw new TypeError("Navigator provider failure fact is not typed");
189
- return Object.assign(message, { navigatorFailure: fact });
190
- }
191
-
192
- export type NavigatorPreparationSession = {
193
- prompt(text: string): Promise<void>;
194
- appendEntry(customType: string, data: unknown): void;
195
- entries(): readonly unknown[];
196
- providerFailure?(): NavigatorProviderFailureFact | undefined;
197
- /**
198
- * Facts of a nested session that already settled without an accepted receipt.
199
- * Present means its own delivery budget is spent — another prompt would open an
200
- * independent summon rather than press the session that owes the receipt.
201
- */
202
- noReceipt?(): Partial<NoReceiptLifecycleFacts> | undefined;
203
- /** Native message from the nested playbook read, when that read failed. */
204
- routePlaybookReadFailure?(): string | undefined;
205
- setModel?(model: string, thinkingLevel?: string): Promise<void>;
206
- dispose(): void | Promise<void>;
207
- };
208
-
209
- export type NavigatorSessionFactory = (options: {
210
- context: HostContext;
211
- subject: string;
212
- modelSettingPath?: string;
213
- tool: ToolDefinition;
214
- }) => Promise<NavigatorPreparationSession>;
215
-
216
- export function navigatorModelSettingPath(): string {
217
- return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "navigator-model.json");
218
- }
219
-
220
- export async function readNavigatorModelSetting(path = navigatorModelSettingPath()): Promise<string> {
221
- try {
222
- const raw = JSON.parse(await readFile(path, "utf8")) as unknown;
223
- if (!isRecord(raw) || typeof raw.model !== "string" || raw.model.trim() === "") {
224
- throw new Error("Navigator model setting is malformed");
225
- }
226
- return raw.model;
227
- } catch (error) {
228
- // #178: missing file is absence (no package default).
229
- if (isEnoent(error)) {
230
- throw new Error("Navigator model setting is missing");
231
- }
232
- throw error;
233
- }
234
- }
235
-
236
- export async function writeNavigatorModelSetting(model: string, path = navigatorModelSettingPath()): Promise<void> {
237
- const normalized = model.trim();
238
- parseNavigatorModelSetting(normalized);
239
- await mkdir(dirname(path), { recursive: true });
240
- await writeFile(path, `${JSON.stringify({ model: normalized })}\n`, "utf8");
241
- }
242
-
243
- export function parseNavigatorModelSetting(value: string): {
244
- provider: string;
245
- model: string;
246
- /** Host thinking passthrough — omit when bare provider/model (#675 ⑥). */
247
- thinkingLevel?: string;
248
- } {
249
- const slash = value.indexOf("/");
250
- if (slash <= 0 || slash === value.length - 1) {
251
- throw new Error("Navigator model setting must be provider/model[:thinking]");
252
- }
253
- const provider = value.slice(0, slash);
254
- const modelWithThinking = value.slice(slash + 1);
255
- const colon = modelWithThinking.lastIndexOf(":");
256
- const suffix = colon < 0 ? undefined : modelWithThinking.slice(colon + 1);
257
- const model = colon < 0 ? modelWithThinking : modelWithThinking.slice(0, colon);
258
- if (model === "") throw new Error("Navigator model setting must include a model");
259
- if (suffix !== undefined && suffix.trim() === "") {
260
- throw new Error("Navigator model setting thinking suffix must be non-blank");
261
- }
262
- return {
263
- provider,
264
- model,
265
- ...(suffix === undefined ? {} : { thinkingLevel: suffix }),
266
- };
267
- }
268
-
269
- /**
270
- * Navigator model selection from the public seat table only (#675 / #617 DK-3).
271
- * No legacy navigator-model.json fallback — missing seat is a real model failure.
272
- * Host/engine axes ride the shared public summons path; attendance open consumes
273
- * model/thinking here and defers host selection to the shared envelope when present.
274
- */
275
- export async function resolveNavigatorSeatSelection(
276
- context: HostContext,
277
- ): Promise<{ selection: HostInstitutionalModelSelection; configuredLabel: string; thinkingLevel?: string }> {
278
- try {
279
- const { loadPublicCliConfig } = await import("./public-cli/config.ts");
280
- const { seatModelOnly } = await import("./public-cli/registry.ts");
281
- const { packageMachineHome } = await import("./activation-ledger-topology.ts");
282
- const homeCandidate = (context as unknown as { home?: unknown }).home;
283
- const envHome = process.env.HOME;
284
- // Prefer explicit context home, then process HOME (hermetic tests), never
285
- // jump straight to the real machine home when a temp HOME is armed.
286
- const home =
287
- typeof homeCandidate === "string"
288
- ? homeCandidate
289
- : typeof envHome === "string" && envHome.trim() !== ""
290
- ? envHome
291
- : packageMachineHome();
292
- const config = await loadPublicCliConfig(home);
293
- const modelOnly = seatModelOnly(config.seats.navigator);
294
- // Seat table only — missing navigator seat → typed model-unavailable (#178).
295
- if (modelOnly === undefined) {
296
- const { missingResolvedSeatModelMessage } = await import("./public-cli/config.ts");
297
- throw navigatorUnavailableError(
298
- "model",
299
- new Error(missingResolvedSeatModelMessage("navigator")),
300
- );
301
- }
302
- // Host params passthrough only — no map/validate/default (#675 ⑥).
303
- const thinkingRaw = modelOnly.thinking;
304
- const configuredLabel =
305
- thinkingRaw === undefined
306
- ? `${modelOnly.provider}/${modelOnly.model}`
307
- : `${modelOnly.provider}/${modelOnly.model}:${thinkingRaw}`;
308
- return {
309
- selection: {
310
- provider: modelOnly.provider,
311
- model: modelOnly.model,
312
- ...(thinkingRaw === undefined ? {} : { thinking: thinkingRaw }),
313
- },
314
- configuredLabel,
315
- ...(thinkingRaw === undefined ? {} : { thinkingLevel: thinkingRaw }),
316
- };
317
- } catch (error) {
318
- throw navigatorUnavailableError("model", error);
319
- }
320
- }