@akagilnc/pi-workflow-roles 0.1.4243 → 0.1.4274
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/README.md +27 -23
- package/README.zh-CN.md +24 -21
- package/dist/acp-host/production-host.js +4607 -3524
- package/dist/engine-detour-tool.js +64 -5
- package/dist/engine-detour-usage.js +285 -0
- package/dist/headless-host/production-host.js +6057 -4974
- package/dist/navigator-attendance.js +0 -2
- package/dist/navigator-public-session.js +2 -4
- package/dist/navigator-session-contracts.js +6 -13
- package/dist/pi/role-turn-host.js +10 -0
- package/dist/public-cli/config.js +16 -20
- package/dist/public-cli/main.js +935 -613
- package/dist/public-cli/post-admission.js +72 -43
- package/dist/public-cli/registry.js +0 -57
- package/dist/public-cli/settlement.js +80 -24
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +121 -96
- package/package.json +1 -1
- package/src/engine-detour-tool.ts +106 -8
- package/src/engine-detour-usage.ts +373 -0
- package/src/host-contracts.ts +12 -1
- package/src/navigator-attendance.ts +0 -2
- package/src/navigator-public-session.ts +3 -3
- package/src/navigator-session-contracts.ts +8 -14
- package/src/pi/adapter.ts +6 -0
- package/src/pi/role-turn-host.ts +6 -0
- package/src/public-cli/cli.ts +115 -307
- package/src/public-cli/config.ts +35 -23
- package/src/public-cli/post-admission.ts +91 -49
- package/src/public-cli/registry.ts +2 -66
- package/src/public-cli/settlement.ts +148 -23
- package/src/public-cli/turn-request.ts +5 -0
- package/src/public-role-summons.ts +197 -125
- package/src/role-envelope.ts +2 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #537 typed ak_engine_detour tool usage ledger.
|
|
3
|
+
*
|
|
4
|
+
* Observes only the package detour tool path. Bash/CLI ordinary path
|
|
5
|
+
* (resources/engine-dispatch.md ordinary path) stays outside this ledger — a
|
|
6
|
+
* permanent blind spot that must never be read as "the seat did not use an engine".
|
|
7
|
+
*
|
|
8
|
+
* Runtime owns these facts beside role payloads (ADR 0042); they land in
|
|
9
|
+
* TerminalResult.decisiveFacts and are written live via sitian (ADR 0077).
|
|
10
|
+
* No gate, no required field, no index bytes (ADR 0049 / 0057).
|
|
11
|
+
*
|
|
12
|
+
* Invocation scope is one public ak-role call (#537), owned by the shared Host
|
|
13
|
+
* execution envelope (RoleTurnRequest / HostContext). Auto-resume attempts inside
|
|
14
|
+
* that call share the same scope; only an explicit new public call (including
|
|
15
|
+
* `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
|
|
16
|
+
* toolResult join keys, and never a detour-owned sidecar file.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
|
|
23
|
+
import {
|
|
24
|
+
readSitianRecords,
|
|
25
|
+
resolveSitianRecordPath,
|
|
26
|
+
sitianReport,
|
|
27
|
+
type RecordPointer,
|
|
28
|
+
} from "./sitian-facade.ts";
|
|
29
|
+
|
|
30
|
+
/** Sitian event kind for one detour-tool call (volume under session/). */
|
|
31
|
+
export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call" as const;
|
|
32
|
+
|
|
33
|
+
/** decisiveFacts key — name states detour-tool scope, not full engine usage. */
|
|
34
|
+
export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run-relative sitian volume path for one detour call.
|
|
38
|
+
* Openable once the run directory is known (resume.command / top-level runId);
|
|
39
|
+
* contains no runId bytes itself (#108 + #537 AC8).
|
|
40
|
+
*/
|
|
41
|
+
export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE =
|
|
42
|
+
`session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl` as const;
|
|
43
|
+
|
|
44
|
+
/** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
|
|
45
|
+
export function engineDetourStdoutByteLength(stdout: string): number {
|
|
46
|
+
return Buffer.byteLength(stdout, "utf8");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One observed ak_engine_detour call. code/stdoutByteLength absent on spawn failure. */
|
|
50
|
+
export type EngineDetourCallFact = {
|
|
51
|
+
readonly toolCallId: string;
|
|
52
|
+
readonly durationMs: number;
|
|
53
|
+
readonly code?: number;
|
|
54
|
+
readonly stdoutByteLength?: number;
|
|
55
|
+
readonly recordPointer: Readonly<RecordPointer>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-invocation aggregate for the detour tool only.
|
|
60
|
+
* callCount 0 is lawful when engine is mounted (ADR 0071 soft rule).
|
|
61
|
+
*/
|
|
62
|
+
export type EngineDetourToolUsageFact = {
|
|
63
|
+
readonly callCount: number;
|
|
64
|
+
readonly calls: readonly EngineDetourCallFact[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ReportEngineDetourCallInput = {
|
|
68
|
+
readonly toolCallId: string;
|
|
69
|
+
readonly durationMs: number;
|
|
70
|
+
/** Present only when the child closed (not spawn failure). */
|
|
71
|
+
readonly code?: number;
|
|
72
|
+
/** Present only when the child closed; empty stdout → 0. */
|
|
73
|
+
readonly stdoutByteLength?: number;
|
|
74
|
+
readonly cwd: string;
|
|
75
|
+
readonly sessionParent: string;
|
|
76
|
+
readonly home?: string;
|
|
77
|
+
/** Real selected host (ADR 0077 / 0082). Never invent "pi". */
|
|
78
|
+
readonly host?: string;
|
|
79
|
+
readonly runId?: string;
|
|
80
|
+
/** Public-invocation scope id from the shared Host envelope. */
|
|
81
|
+
readonly invocationScopeId?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Deterministic sitian identity: invocation scope + toolCallId.
|
|
86
|
+
* Must not embed runId — public Terminal decisiveFacts re-expose identity (#108).
|
|
87
|
+
*/
|
|
88
|
+
export function engineDetourCallIdentity(input: {
|
|
89
|
+
readonly toolCallId: string;
|
|
90
|
+
readonly invocationScopeId?: string;
|
|
91
|
+
}): string {
|
|
92
|
+
const scope = input.invocationScopeId ?? "";
|
|
93
|
+
return `engine-detour-call:${scope}:${input.toolCallId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Live sitian write for one detour call. Returns the call fact with pointer. */
|
|
97
|
+
export function reportEngineDetourCall(
|
|
98
|
+
input: ReportEngineDetourCallInput,
|
|
99
|
+
): EngineDetourCallFact {
|
|
100
|
+
const payload: Record<string, unknown> = {
|
|
101
|
+
tool: ENGINE_DETOUR_TOOL_NAME,
|
|
102
|
+
toolCallId: input.toolCallId,
|
|
103
|
+
durationMs: input.durationMs,
|
|
104
|
+
};
|
|
105
|
+
if (input.code !== undefined) payload.code = input.code;
|
|
106
|
+
if (input.stdoutByteLength !== undefined) {
|
|
107
|
+
payload.stdoutByteLength = input.stdoutByteLength;
|
|
108
|
+
}
|
|
109
|
+
if (input.invocationScopeId !== undefined) {
|
|
110
|
+
payload.invocationScopeId = input.invocationScopeId;
|
|
111
|
+
}
|
|
112
|
+
if (input.runId !== undefined) payload.runId = input.runId;
|
|
113
|
+
|
|
114
|
+
// SitianSubject object form requires runId; invocation scope rides payload always.
|
|
115
|
+
const subject =
|
|
116
|
+
input.runId === undefined
|
|
117
|
+
? undefined
|
|
118
|
+
: {
|
|
119
|
+
runId: input.runId,
|
|
120
|
+
...(input.invocationScopeId === undefined
|
|
121
|
+
? {}
|
|
122
|
+
: { invocationScopeId: input.invocationScopeId }),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const pointer = sitianReport({
|
|
126
|
+
level: "event",
|
|
127
|
+
kind: ENGINE_DETOUR_CALL_KIND,
|
|
128
|
+
identity: engineDetourCallIdentity({
|
|
129
|
+
toolCallId: input.toolCallId,
|
|
130
|
+
...(input.invocationScopeId === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { invocationScopeId: input.invocationScopeId }),
|
|
133
|
+
}),
|
|
134
|
+
cwd: input.cwd,
|
|
135
|
+
sessionParent: input.sessionParent,
|
|
136
|
+
source: "engine-detour-tool",
|
|
137
|
+
payload,
|
|
138
|
+
raw: {
|
|
139
|
+
sessionFile: input.sessionParent,
|
|
140
|
+
entryId: input.toolCallId,
|
|
141
|
+
},
|
|
142
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
143
|
+
...(input.host === undefined ? {} : { host: input.host }),
|
|
144
|
+
...(subject === undefined ? {} : { subject }),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
toolCallId: input.toolCallId,
|
|
149
|
+
durationMs: input.durationMs,
|
|
150
|
+
...(input.code === undefined ? {} : { code: input.code }),
|
|
151
|
+
...(input.stdoutByteLength === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { stdoutByteLength: input.stdoutByteLength }),
|
|
154
|
+
recordPointer: pointer,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
159
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function callFactFromSitianPayload(
|
|
163
|
+
payload: unknown,
|
|
164
|
+
pointer: RecordPointer,
|
|
165
|
+
): EngineDetourCallFact | undefined {
|
|
166
|
+
if (!isRecord(payload)) return undefined;
|
|
167
|
+
if (payload.tool !== ENGINE_DETOUR_TOOL_NAME) return undefined;
|
|
168
|
+
if (typeof payload.toolCallId !== "string" || payload.toolCallId.length === 0) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (typeof payload.durationMs !== "number" || !Number.isFinite(payload.durationMs)) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
toolCallId: payload.toolCallId,
|
|
176
|
+
durationMs: payload.durationMs,
|
|
177
|
+
...(typeof payload.code === "number" ? { code: payload.code } : {}),
|
|
178
|
+
...(typeof payload.stdoutByteLength === "number"
|
|
179
|
+
? { stdoutByteLength: payload.stdoutByteLength }
|
|
180
|
+
: {}),
|
|
181
|
+
recordPointer: pointer,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function invocationScopeIdOfRecord(record: {
|
|
186
|
+
readonly subject?: unknown;
|
|
187
|
+
readonly payload?: unknown;
|
|
188
|
+
}): string | undefined {
|
|
189
|
+
if (isRecord(record.subject)) {
|
|
190
|
+
const fromSubject = record.subject.invocationScopeId;
|
|
191
|
+
if (typeof fromSubject === "string" && fromSubject.length > 0) return fromSubject;
|
|
192
|
+
}
|
|
193
|
+
if (isRecord(record.payload)) {
|
|
194
|
+
const fromPayload = record.payload.invocationScopeId;
|
|
195
|
+
if (typeof fromPayload === "string" && fromPayload.length > 0) return fromPayload;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read this-invocation detour-tool usage from sitian volume.
|
|
202
|
+
* When `invocationScopeId` is provided, only records bound to that public call count
|
|
203
|
+
* (host-neutral boundary — not session toolResult join keys, not courtAttemptId).
|
|
204
|
+
* When engineMounted is false, returns undefined (field absent).
|
|
205
|
+
* When engineMounted is true and no calls, returns callCount 0.
|
|
206
|
+
*/
|
|
207
|
+
export async function readEngineDetourToolUsage(input: {
|
|
208
|
+
readonly sessionParent: string;
|
|
209
|
+
readonly engineMounted: boolean;
|
|
210
|
+
readonly invocationScopeId?: string;
|
|
211
|
+
readonly home?: string;
|
|
212
|
+
readonly cwd?: string;
|
|
213
|
+
}): Promise<EngineDetourToolUsageFact | undefined> {
|
|
214
|
+
if (!input.engineMounted) return undefined;
|
|
215
|
+
|
|
216
|
+
const { recordFile } = resolveSitianRecordPath({
|
|
217
|
+
level: "event",
|
|
218
|
+
kind: ENGINE_DETOUR_CALL_KIND,
|
|
219
|
+
sessionParent: input.sessionParent,
|
|
220
|
+
...(input.home === undefined ? {} : { home: input.home }),
|
|
221
|
+
...(input.cwd === undefined ? {} : { cwd: input.cwd }),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const { records } = await readSitianRecords(recordFile);
|
|
225
|
+
const calls: EngineDetourCallFact[] = [];
|
|
226
|
+
for (const record of records) {
|
|
227
|
+
if (record.kind !== ENGINE_DETOUR_CALL_KIND) continue;
|
|
228
|
+
const boundScope = invocationScopeIdOfRecord(record);
|
|
229
|
+
if (input.invocationScopeId !== undefined && input.invocationScopeId.length > 0) {
|
|
230
|
+
if (boundScope !== input.invocationScopeId) continue;
|
|
231
|
+
} else if (boundScope !== undefined) {
|
|
232
|
+
// Unscoped settlement must not pull invocation-bound rows across resume.
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const pointer: RecordPointer = {
|
|
236
|
+
identity: record.identity,
|
|
237
|
+
recordFile,
|
|
238
|
+
kind: record.kind,
|
|
239
|
+
level: record.level,
|
|
240
|
+
};
|
|
241
|
+
const fact = callFactFromSitianPayload(record.payload, pointer);
|
|
242
|
+
if (fact === undefined) continue;
|
|
243
|
+
calls.push(fact);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { callCount: calls.length, calls };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Project usage onto the public Terminal face.
|
|
251
|
+
* Non-resumable: keep absolute recordFile (runId already public via top-level runId).
|
|
252
|
+
* Resumable: keep an openable run-relative recordFile and identity with no runId
|
|
253
|
+
* bytes (#108 single disclosure + #537 AC8 reopen).
|
|
254
|
+
*/
|
|
255
|
+
export function projectEngineDetourToolUsageForPublicTerminal(
|
|
256
|
+
usage: EngineDetourToolUsageFact,
|
|
257
|
+
options: { readonly discloseRecordFile: boolean },
|
|
258
|
+
): EngineDetourToolUsageFact {
|
|
259
|
+
if (options.discloseRecordFile) return usage;
|
|
260
|
+
return {
|
|
261
|
+
callCount: usage.callCount,
|
|
262
|
+
calls: usage.calls.map((call) => ({
|
|
263
|
+
toolCallId: call.toolCallId,
|
|
264
|
+
durationMs: call.durationMs,
|
|
265
|
+
...(call.code === undefined ? {} : { code: call.code }),
|
|
266
|
+
...(call.stdoutByteLength === undefined
|
|
267
|
+
? {}
|
|
268
|
+
: { stdoutByteLength: call.stdoutByteLength }),
|
|
269
|
+
recordPointer: {
|
|
270
|
+
identity: call.recordPointer.identity,
|
|
271
|
+
kind: call.recordPointer.kind,
|
|
272
|
+
level: call.recordPointer.level,
|
|
273
|
+
recordFile: ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE,
|
|
274
|
+
},
|
|
275
|
+
})),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
|
|
280
|
+
function readInvocationRecord(
|
|
281
|
+
runDirectory: string,
|
|
282
|
+
): Record<string, unknown> | undefined {
|
|
283
|
+
try {
|
|
284
|
+
const raw = JSON.parse(
|
|
285
|
+
readFileSync(join(runDirectory, "invocation.json"), "utf8"),
|
|
286
|
+
) as unknown;
|
|
287
|
+
return isRecord(raw) ? raw : undefined;
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** True when invocation.json carries a non-empty engine axis. */
|
|
295
|
+
export async function readInvocationEngineMounted(
|
|
296
|
+
runDirectory: string,
|
|
297
|
+
): Promise<boolean> {
|
|
298
|
+
const raw = readInvocationRecord(runDirectory);
|
|
299
|
+
if (raw === undefined) return false;
|
|
300
|
+
return typeof raw.engine === "string" && raw.engine.trim() !== "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Selected host from the admission invocation page — for post-admission
|
|
305
|
+
* host-transition classification only. In-turn tools take host from the shared
|
|
306
|
+
* Host envelope (RoleTurnRequest / HostContext), never this reader.
|
|
307
|
+
* Undefined when absent — callers must not invent "pi".
|
|
308
|
+
*/
|
|
309
|
+
export function readInvocationSelectedHost(runDirectory: string): string | undefined {
|
|
310
|
+
const raw = readInvocationRecord(runDirectory);
|
|
311
|
+
if (raw === undefined) return undefined;
|
|
312
|
+
return typeof raw.host === "string" && raw.host.trim() !== ""
|
|
313
|
+
? raw.host.trim()
|
|
314
|
+
: undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Merge usage into decisiveFacts without touching role payloads. */
|
|
318
|
+
export function withEngineDetourToolUsageFact<
|
|
319
|
+
T extends { readonly decisiveFacts?: Readonly<Record<string, unknown>> },
|
|
320
|
+
>(
|
|
321
|
+
outcome: T,
|
|
322
|
+
usage: EngineDetourToolUsageFact | undefined,
|
|
323
|
+
): T {
|
|
324
|
+
if (usage === undefined) return outcome;
|
|
325
|
+
const prior = isRecord(outcome.decisiveFacts) ? outcome.decisiveFacts : {};
|
|
326
|
+
return {
|
|
327
|
+
...outcome,
|
|
328
|
+
decisiveFacts: {
|
|
329
|
+
...prior,
|
|
330
|
+
[ENGINE_DETOUR_TOOL_USAGE_FACT_KEY]: usage,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** runDirectory owning a session directory (.../runs/<id>@role/session). */
|
|
336
|
+
export function runDirectoryFromSessionDirectory(sessionDirectory: string): string {
|
|
337
|
+
return dirname(sessionDirectory);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** session.jsonl under a session directory. */
|
|
341
|
+
export function sessionFileFromSessionDirectory(sessionDirectory: string): string {
|
|
342
|
+
return join(sessionDirectory, "session.jsonl");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Mint one public-invocation scope id when an engine is mounted.
|
|
347
|
+
* Call once at the public-entry boundary — never inside the auto-resume loop.
|
|
348
|
+
* The id lives on RoleTurnRequest / HostContext only (no detour sidecar file).
|
|
349
|
+
*/
|
|
350
|
+
export function mintEngineDetourInvocationScope(input: {
|
|
351
|
+
readonly effectiveEngine?: string;
|
|
352
|
+
}): string | undefined {
|
|
353
|
+
const engine = input.effectiveEngine?.trim();
|
|
354
|
+
if (engine === undefined || engine.length === 0) return undefined;
|
|
355
|
+
return randomUUID();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Attach a minted scope onto a turn request (shared Host envelope field). */
|
|
359
|
+
export function withEngineDetourInvocationScope<T extends object>(
|
|
360
|
+
request: T & { readonly invocationScopeId?: string },
|
|
361
|
+
invocationScopeId: string | undefined,
|
|
362
|
+
): T & { readonly invocationScopeId?: string } {
|
|
363
|
+
if (invocationScopeId === undefined || invocationScopeId.length === 0) {
|
|
364
|
+
return request;
|
|
365
|
+
}
|
|
366
|
+
if (
|
|
367
|
+
typeof request.invocationScopeId === "string" &&
|
|
368
|
+
request.invocationScopeId.length > 0
|
|
369
|
+
) {
|
|
370
|
+
return request;
|
|
371
|
+
}
|
|
372
|
+
return { ...request, invocationScopeId };
|
|
373
|
+
}
|
package/src/host-contracts.ts
CHANGED
|
@@ -194,6 +194,17 @@ export type RoleTurnRequest = {
|
|
|
194
194
|
* and resume-with-message set this; bare resume without an open court omits it.
|
|
195
195
|
*/
|
|
196
196
|
readonly courtAttemptId?: string;
|
|
197
|
+
/**
|
|
198
|
+
* Public-invocation scope (#537): one ak-role call. Auto-resume reuses it;
|
|
199
|
+
* explicit resume mints a new one. Owned by this shared Host envelope — not
|
|
200
|
+
* courtAttemptId and not a detour sidecar file.
|
|
201
|
+
*/
|
|
202
|
+
readonly invocationScopeId?: string;
|
|
203
|
+
/**
|
|
204
|
+
* Selected host axis for this turn (#537 / ADR 0082). Projected from the
|
|
205
|
+
* public-entry seat resolution — never re-read from invocation.json by tools.
|
|
206
|
+
*/
|
|
207
|
+
readonly host?: string;
|
|
197
208
|
/** Station child role run (#840): omit automatic navigator attendance. */
|
|
198
209
|
readonly stationChild?: boolean;
|
|
199
210
|
};
|
|
@@ -246,7 +257,7 @@ export interface DurablePrincipalAuthority {
|
|
|
246
257
|
type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLeafId(): string | null | undefined; getEntries(): Iterable<HostSessionEntry>; getSessionDir(): string; getSessionFile(): string | undefined; getHeader?(): { readonly type: string; readonly id?: string } | null; setSessionFile?(path: string): void; appendCustomEntry?(customType: string, data?: unknown): unknown; };
|
|
247
258
|
|
|
248
259
|
/** Context supplied by a host for one activation and its interceptable events. */
|
|
249
|
-
export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
|
|
260
|
+
export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
|
|
250
261
|
|
|
251
262
|
/** Per-turn run directory; adapters must project any child-process identity. */
|
|
252
263
|
export function runDirectoryFromHostContext(context: HostContext): string | undefined {
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import type { HostContext } from "./host-contracts.ts";
|
|
19
19
|
import { createNativeNavigatorSessionFactory } from "./navigator-public-session.ts";
|
|
20
20
|
import {
|
|
21
|
-
NAVIGATOR_DEFAULT_MODEL,
|
|
22
21
|
NAVIGATOR_PREPARE_TOOL_NAME,
|
|
23
22
|
NavigatorUnavailableError,
|
|
24
23
|
navigatorModelSettingPath,
|
|
@@ -40,7 +39,6 @@ import {
|
|
|
40
39
|
import { sitianReport } from "./sitian-facade.ts";
|
|
41
40
|
|
|
42
41
|
export {
|
|
43
|
-
NAVIGATOR_DEFAULT_MODEL,
|
|
44
42
|
NAVIGATOR_PREPARE_TOOL_NAME,
|
|
45
43
|
NavigatorUnavailableError,
|
|
46
44
|
navigatorModelSettingPath,
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
navigatorProviderFailureFromError,
|
|
13
13
|
navigatorUnavailableError,
|
|
14
14
|
parseNavigatorModelSetting,
|
|
15
|
-
resolveNavigatorSeatSelection,
|
|
16
15
|
type NavigatorProviderFailureFact,
|
|
17
16
|
type NavigatorSessionFactory,
|
|
18
17
|
} from "./navigator-session-contracts.ts";
|
|
@@ -43,8 +42,9 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
|
|
|
43
42
|
|
|
44
43
|
export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
|
|
45
44
|
return async ({ context, subject, tool }) => {
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
// Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
|
|
46
|
+
// Factory open only books the archivist nest — no package default, no eager seat read.
|
|
47
|
+
let thinkingLevel: string | undefined;
|
|
48
48
|
|
|
49
49
|
// Archivist nest for attendance route memory only (ADR 0018 / 0065) — not a session open.
|
|
50
50
|
// #852: navigator/<work-subject> is the sole book-top exception; always pass subject so
|
|
@@ -15,7 +15,6 @@ import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
|
|
18
|
-
export const NAVIGATOR_DEFAULT_MODEL = "openai-codex/gpt-5.6-luna:max" as const;
|
|
19
18
|
|
|
20
19
|
export type NavigatorUnavailableKey =
|
|
21
20
|
| "context" | "session" | "model" | "thinking" | "auth" | "quota" | "transport" | "unknown";
|
|
@@ -229,8 +228,9 @@ export async function readNavigatorModelSetting(path = navigatorModelSettingPath
|
|
|
229
228
|
}
|
|
230
229
|
return raw.model;
|
|
231
230
|
} catch (error) {
|
|
231
|
+
// #178: missing file is absence (no package default).
|
|
232
232
|
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
233
|
-
|
|
233
|
+
throw new Error("Navigator model setting is missing");
|
|
234
234
|
}
|
|
235
235
|
throw error;
|
|
236
236
|
}
|
|
@@ -294,19 +294,13 @@ export async function resolveNavigatorSeatSelection(
|
|
|
294
294
|
: packageMachineHome();
|
|
295
295
|
const config = await loadPublicCliConfig(home);
|
|
296
296
|
const modelOnly = seatModelOnly(config.seats.navigator);
|
|
297
|
-
// Seat table
|
|
298
|
-
// Bare missing seat uses the package default constant (not a file path).
|
|
297
|
+
// Seat table only — missing navigator seat → typed model-unavailable (#178).
|
|
299
298
|
if (modelOnly === undefined) {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
...(parsed.thinkingLevel === undefined ? {} : { thinking: parsed.thinkingLevel }),
|
|
306
|
-
},
|
|
307
|
-
configuredLabel: NAVIGATOR_DEFAULT_MODEL,
|
|
308
|
-
...(parsed.thinkingLevel === undefined ? {} : { thinkingLevel: parsed.thinkingLevel }),
|
|
309
|
-
};
|
|
299
|
+
const { missingResolvedSeatModelMessage } = await import("./public-cli/config.ts");
|
|
300
|
+
throw navigatorUnavailableError(
|
|
301
|
+
"model",
|
|
302
|
+
new Error(missingResolvedSeatModelMessage("navigator")),
|
|
303
|
+
);
|
|
310
304
|
}
|
|
311
305
|
// Host params passthrough only — no map/validate/default (#675 ⑥).
|
|
312
306
|
const thinkingRaw = modelOnly.thinking;
|
package/src/pi/adapter.ts
CHANGED
|
@@ -52,6 +52,12 @@ function projectPiContext(context: ExtensionContext, transcriptFromContext?: (co
|
|
|
52
52
|
...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
|
|
53
53
|
? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
|
|
54
54
|
: {}),
|
|
55
|
+
...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
|
|
56
|
+
? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
|
|
57
|
+
: {}),
|
|
58
|
+
...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
|
|
59
|
+
? { host: process.env.AK_ROLE_HOST.trim() }
|
|
60
|
+
: {}),
|
|
55
61
|
sessionManager: {
|
|
56
62
|
getLeafEntry: () => context.sessionManager.getLeafEntry(),
|
|
57
63
|
getLeafId: () => context.sessionManager.getLeafId(),
|
package/src/pi/role-turn-host.ts
CHANGED
|
@@ -447,6 +447,12 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
|
|
|
447
447
|
};
|
|
448
448
|
if (request.courtAttemptId === undefined) delete env.AK_ROLE_COURT_ATTEMPT;
|
|
449
449
|
else env.AK_ROLE_COURT_ATTEMPT = request.courtAttemptId;
|
|
450
|
+
// Public-invocation scope (#537): omit must not inherit a parent env value.
|
|
451
|
+
if (request.invocationScopeId === undefined) delete env.AK_ROLE_INVOCATION_SCOPE;
|
|
452
|
+
else env.AK_ROLE_INVOCATION_SCOPE = request.invocationScopeId;
|
|
453
|
+
// Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
|
|
454
|
+
if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
|
|
455
|
+
else env.AK_ROLE_HOST = request.host.trim();
|
|
450
456
|
applyEngineChildEnv(env, request.engine);
|
|
451
457
|
// Nested auditor dossier tool binds the parent run pointer when published.
|
|
452
458
|
if (
|