@akagilnc/pi-workflow-roles 0.1.4586 → 0.1.4620
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/acp-host/production-host.js +951 -497
- package/dist/archivist-record-topology.js +27 -35
- package/dist/headless-host/description.js +7 -4
- package/dist/headless-host/production-host.js +783 -393
- package/dist/migrate-book-topology.js +3 -6
- package/dist/navigator-attendance.js +146 -241
- package/dist/navigator-public-session.js +81 -15
- package/dist/package-contracts/navigator-output.js +30 -12
- package/dist/public-cli/judge-run.js +2 -2
- package/dist/public-cli/main.js +69 -94
- package/dist/public-cli/option-definitions.js +1 -1
- package/dist/public-cli/post-admission.js +2 -2
- package/dist/public-cli/run-lifecycle.js +29 -7
- package/dist/public-cli/settlement.js +46 -32
- package/dist/public-cli/terminal.js +8 -27
- package/dist/public-role-summons.js +22 -4
- package/dist/submission-ledger.js +56 -23
- package/package.json +1 -1
- package/resources/836-deleted-machine-instruction-inventory.md +2 -0
- package/resources/navigator-route-playbook.md +9 -0
- package/scripts/build-package.mjs +6 -1
- package/src/acp-host/role-turn-host.ts +129 -13
- package/src/headless-host/description.ts +17 -7
- package/src/headless-host/role-turn-host.ts +42 -11
- package/src/navigator-attendance.ts +290 -425
- package/src/navigator-public-session.ts +126 -25
- package/src/navigator-role.ts +2 -2
- package/src/package-contracts/navigator-output.ts +41 -25
- package/src/public-cli/coder-run.ts +2 -2
- package/src/public-cli/fixer-run.ts +2 -2
- package/src/public-cli/judge-run.ts +2 -2
- package/src/public-cli/merger-run.ts +2 -2
- package/src/public-cli/option-definitions.ts +1 -1
- package/src/public-cli/post-admission.ts +2 -1
- package/src/public-cli/run-lifecycle.ts +37 -7
- package/src/public-cli/settlement.ts +42 -33
- package/src/public-cli/terminal.ts +12 -37
- package/src/public-role-summons.ts +34 -5
- package/src/role-envelope.ts +89 -4
- package/src/role-runtime.ts +74 -14
- package/src/submission-ledger.ts +63 -21
- package/dist/public-cli/command-renderer.js +0 -5
- package/dist/public-command-renderer.js +0 -29
- package/src/public-cli/command-renderer.ts +0 -9
- package/src/public-command-renderer.ts +0 -55
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
NAVIGATOR_INVOCATION_ENTRY,
|
|
11
11
|
mintNavigatorInvocationId,
|
|
12
12
|
} from "./navigator-invocation-identity.ts";
|
|
13
|
-
import { PACKAGED_ROLE_REGISTRY, type PackagedRole
|
|
13
|
+
import { PACKAGED_ROLE_REGISTRY, type PackagedRole } from "./packaged-role-registry.ts";
|
|
14
14
|
import {
|
|
15
15
|
activationBookDirectory,
|
|
16
16
|
resolveActivationLedgerHome,
|
|
@@ -58,9 +58,9 @@ export {
|
|
|
58
58
|
};
|
|
59
59
|
export { createNativeNavigatorSessionFactory };
|
|
60
60
|
export { resolveNavigatorSeatSelection };
|
|
61
|
-
import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
|
|
62
61
|
import { issueRoot, subjectPath } from "./work-subject-identity.ts";
|
|
63
62
|
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
|
|
63
|
+
import { navigatorProseFromUnknown } from "./package-contracts/navigator-output.ts";
|
|
64
64
|
|
|
65
65
|
export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
|
|
66
66
|
export const NAVIGATOR_PREPARE_ACCEPTED_TEXT = "游奕使准备已接受";
|
|
@@ -100,24 +100,15 @@ export type NavigatorWorkContext = {
|
|
|
100
100
|
contextError?: unknown;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
matches?: { role: string; phase: NavigatorPhase; kind: "accepted"; statuses?: string[] };
|
|
108
|
-
route?: NavigatorRouteTarget[];
|
|
109
|
-
next?: NavigatorRouteTarget;
|
|
110
|
-
reason?: string;
|
|
111
|
-
command?: string;
|
|
112
|
-
};
|
|
113
|
-
|
|
103
|
+
/**
|
|
104
|
+
* #959: Navigator speaks free-form prose. Code does not parse, rank, or judge
|
|
105
|
+
* the advice — only whether attendance itself failed (host/process).
|
|
106
|
+
*/
|
|
114
107
|
export type NavigatorReport = {
|
|
115
108
|
/** Affirmative attendance only. Lawful no-advice is typed, never inferred from absence. */
|
|
116
|
-
disposition: "
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
reason?: string;
|
|
120
|
-
command?: string;
|
|
109
|
+
disposition: "advice" | "no-advice" | "unavailable" | "arrival";
|
|
110
|
+
/** Present when disposition is advice — navigator words, presented as-is. */
|
|
111
|
+
prose?: string;
|
|
121
112
|
unavailableReason?: string;
|
|
122
113
|
unavailableSource?: NavigatorUnavailableKey;
|
|
123
114
|
unavailableCause?: NavigatorUnavailableKey;
|
|
@@ -132,10 +123,7 @@ export type NavigatorEvent = {
|
|
|
132
123
|
role: string;
|
|
133
124
|
phase: NavigatorPhase;
|
|
134
125
|
subjectKey: string;
|
|
135
|
-
|
|
136
|
-
next?: NavigatorRouteTarget;
|
|
137
|
-
reason?: string;
|
|
138
|
-
command?: string;
|
|
126
|
+
prose?: string;
|
|
139
127
|
unavailableReason?: string;
|
|
140
128
|
unavailableSource?: NavigatorUnavailableKey;
|
|
141
129
|
unavailableCause?: NavigatorUnavailableKey;
|
|
@@ -149,21 +137,17 @@ export type NavigatorContextProjection = {
|
|
|
149
137
|
subject: string;
|
|
150
138
|
authority: string;
|
|
151
139
|
currentRole: { role: string; phase: NavigatorPhase };
|
|
152
|
-
/** Present
|
|
140
|
+
/** Present on the unique settlement-bound advice prompt. */
|
|
153
141
|
currentSettlement?: NavigatorSettlement;
|
|
154
|
-
priorRoute: NavigatorRouteTarget[] | null;
|
|
155
142
|
publicSettlementHistory: NavigatorSettlementFact[];
|
|
156
143
|
liveRoleHelp: Array<{ role: NavigatorTargetRole; help: string }>;
|
|
157
144
|
};
|
|
158
145
|
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// root reaches the unique execute/normalize path exactly once.
|
|
162
|
-
// Field guidance only — candidates shape is never an acceptance gate (Rule 0).
|
|
146
|
+
// #959: prepare is a prose vehicle. Object root only (ADR 0060); nested shape is
|
|
147
|
+
// never a gate — every object root reaches execute exactly once (Rule 0).
|
|
163
148
|
const prepareSchema = Type.Object({
|
|
164
|
-
|
|
165
|
-
description:
|
|
166
|
-
"方向候选;candidates[].next.role 必填,phase 可选,route/matches/reason/command 可选上下文,非受理闸",
|
|
149
|
+
prose: Type.Optional(Type.Unknown({
|
|
150
|
+
description: "游奕使散文建议,原样呈现。不要求 candidates/next 结构。非受理闸",
|
|
167
151
|
})),
|
|
168
152
|
}, { additionalProperties: true });
|
|
169
153
|
type PrepareOutput = Static<typeof prepareSchema>;
|
|
@@ -186,11 +170,9 @@ export type NavigatorAttendanceOptions = {
|
|
|
186
170
|
onEvent: (event: NavigatorEvent, report: NavigatorReport) => void | Promise<void>;
|
|
187
171
|
};
|
|
188
172
|
|
|
189
|
-
const ROUTE_ENTRY = "ak-navigator-route";
|
|
190
173
|
const CONTEXT_ENTRY = "ak-navigator-context";
|
|
191
174
|
const INVOCATION_ENTRY = NAVIGATOR_INVOCATION_ENTRY;
|
|
192
175
|
const SETTLEMENT_ENTRY = "ak-navigator-settlement";
|
|
193
|
-
const targetRoles = new Set<string>(NAVIGATOR_TARGETS.map(({ role }) => role));
|
|
194
176
|
const unavailableKeys = new Set<NavigatorUnavailableKey>(["context", "session", "model", "thinking", "auth", "quota", "transport", "unknown"]);
|
|
195
177
|
|
|
196
178
|
function unavailableKey(value: unknown): NavigatorUnavailableKey | undefined {
|
|
@@ -232,85 +214,15 @@ function rejectedPrepareReason(entries: readonly unknown[], start: number): stri
|
|
|
232
214
|
}
|
|
233
215
|
return reason;
|
|
234
216
|
}
|
|
235
|
-
function targetIsValid(value: unknown): value is NavigatorRouteTarget {
|
|
236
|
-
if (!exactRecord(value) || !targetRoles.has(String(value.role))) return false;
|
|
237
|
-
const metadata = packagedRoleMetadata(String(value.role));
|
|
238
|
-
return metadata !== undefined && metadata.phases.includes(value.phase as never);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/** Keep the role/phase the model wrote. Unknown seats stay on the advice. */
|
|
242
|
-
function normalizeTarget(value: unknown): NavigatorRouteTarget | undefined {
|
|
243
|
-
if (!exactRecord(value)) return undefined;
|
|
244
|
-
const role = typeof value.role === "string" ? value.role.trim() : "";
|
|
245
|
-
if (role === "") return undefined;
|
|
246
|
-
if (value.phase === undefined || value.phase === null) {
|
|
247
|
-
return { role, phase: null };
|
|
248
|
-
}
|
|
249
|
-
if (value.phase === "plan" || value.phase === "apply") {
|
|
250
|
-
return { role, phase: value.phase };
|
|
251
|
-
}
|
|
252
|
-
return { role, phase: null };
|
|
253
|
-
}
|
|
254
217
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (value.statuses !== undefined) {
|
|
264
|
-
if (!Array.isArray(value.statuses) || value.statuses.some((status) => typeof status !== "string" || status.trim() === "")) {
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
return {
|
|
268
|
-
role: value.role,
|
|
269
|
-
phase,
|
|
270
|
-
kind: "accepted",
|
|
271
|
-
statuses: [...value.statuses],
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
return { role: value.role, phase, kind: "accepted" };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Normalize one submitted candidate. Broken ancillary fields are dropped, never a rejection. */
|
|
278
|
-
function normalizeCandidate(value: unknown): NavigatorCandidate | undefined {
|
|
279
|
-
if (!exactRecord(value)) return undefined;
|
|
280
|
-
const next = normalizeTarget(value.next);
|
|
281
|
-
const route = Array.isArray(value.route)
|
|
282
|
-
? value.route.map(normalizeTarget).filter((target): target is NavigatorRouteTarget => target !== undefined)
|
|
283
|
-
: undefined;
|
|
284
|
-
const matches = normalizeMatches(value.matches);
|
|
285
|
-
const id = typeof value.id === "string" && value.id.trim() !== "" ? value.id : undefined;
|
|
286
|
-
const reason = typeof value.reason === "string" && value.reason.trim() !== "" ? value.reason : undefined;
|
|
287
|
-
const command = typeof value.command === "string" ? value.command : undefined;
|
|
288
|
-
return {
|
|
289
|
-
...(id === undefined ? {} : { id }),
|
|
290
|
-
...(matches === undefined ? {} : { matches }),
|
|
291
|
-
...(route === undefined || route.length === 0 ? {} : { route }),
|
|
292
|
-
...(next === undefined ? {} : { next }),
|
|
293
|
-
...(reason === undefined ? {} : { reason }),
|
|
294
|
-
...(command === undefined ? {} : { command }),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Accept any prepare submission shape; missing/malformed candidates become empty advice. */
|
|
299
|
-
function normalizePrepareOutput(value: unknown): NavigatorCandidate[] {
|
|
300
|
-
if (!exactRecord(value) || !Array.isArray(value.candidates)) return [];
|
|
301
|
-
return value.candidates
|
|
302
|
-
.map(normalizeCandidate)
|
|
303
|
-
.filter((candidate): candidate is NavigatorCandidate => candidate !== undefined);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function routeEqual(a: readonly NavigatorRouteTarget[] | undefined, b: readonly NavigatorRouteTarget[]): boolean {
|
|
307
|
-
return a !== undefined && a.length === b.length && a.every((target, index) => target.role === b[index]!.role && target.phase === b[index]!.phase);
|
|
308
|
-
}
|
|
309
|
-
function routeText(route: readonly NavigatorRouteTarget[]): string {
|
|
310
|
-
return route.map((target) => target.phase === null ? target.role : `${target.role} ${target.phase}`).join(" → ");
|
|
311
|
-
}
|
|
312
|
-
function targetText(target: NavigatorRouteTarget): string {
|
|
313
|
-
return target.phase === null ? target.role : `${target.role} ${target.phase}`;
|
|
218
|
+
/**
|
|
219
|
+
* #959: extract prose from any prepare submission shape. Missing/empty → undefined.
|
|
220
|
+
* Never a rejection — shape is not an admission gate.
|
|
221
|
+
* Production prose arrives via nested public summon → prepare tool.execute only;
|
|
222
|
+
* archivist entries() never carries assistant message text, so no parallel harvest.
|
|
223
|
+
*/
|
|
224
|
+
function normalizePrepareProse(value: unknown): string | undefined {
|
|
225
|
+
return navigatorProseFromUnknown(value);
|
|
314
226
|
}
|
|
315
227
|
|
|
316
228
|
export function navigatorSubjectKey(
|
|
@@ -346,67 +258,16 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
|
|
|
346
258
|
return {
|
|
347
259
|
name: NAVIGATOR_PREPARE_TOOL_NAME,
|
|
348
260
|
label: "游奕使准备",
|
|
349
|
-
description: "
|
|
261
|
+
description: "提交游奕使散文建议。",
|
|
350
262
|
parameters: prepareSchema,
|
|
351
263
|
async execute(_id, value) {
|
|
352
|
-
// Rule 0: the unique prepare submission is accepted once.
|
|
353
|
-
// normalized later; never open a format-correction retry loop here.
|
|
264
|
+
// Rule 0: the unique prepare submission is accepted once. Shape is never a gate.
|
|
354
265
|
onOutput(value as PrepareOutput);
|
|
355
266
|
return { content: [{ type: "text" as const, text: NAVIGATOR_PREPARE_ACCEPTED_TEXT }], details: value, terminate: true as const };
|
|
356
267
|
},
|
|
357
268
|
};
|
|
358
269
|
}
|
|
359
270
|
|
|
360
|
-
/**
|
|
361
|
-
* Candidate pick plus whether matches keyed it to this settlement.
|
|
362
|
-
* Single owner for role/phase/status match truth (selection ranking + stale-context rebind).
|
|
363
|
-
* Structural only — never inspects next.role for routing legality.
|
|
364
|
-
*/
|
|
365
|
-
export type NavigatorCandidateSelection = {
|
|
366
|
-
readonly candidate: NavigatorCandidate;
|
|
367
|
-
/** True when matches keyed this candidate to the settlement (no stale-context rebind). */
|
|
368
|
-
readonly matchedToSettlement: boolean;
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
export function selectNavigatorCandidate(
|
|
372
|
-
candidates: readonly NavigatorCandidate[],
|
|
373
|
-
settlement: NavigatorSettlement,
|
|
374
|
-
): NavigatorCandidateSelection | undefined {
|
|
375
|
-
if (settlement.kind !== "accepted") return undefined;
|
|
376
|
-
const usable = candidates.filter((candidate) => candidate.next !== undefined);
|
|
377
|
-
if (usable.length === 0) return undefined;
|
|
378
|
-
const rolePhaseMatched = usable.filter((candidate) =>
|
|
379
|
-
candidate.matches !== undefined
|
|
380
|
-
&& candidate.matches.role === settlement.role
|
|
381
|
-
&& candidate.matches.phase === settlement.phase,
|
|
382
|
-
);
|
|
383
|
-
// Status-specific candidates outrank role/phase generics regardless of declaration order.
|
|
384
|
-
if (rolePhaseMatched.length > 0) {
|
|
385
|
-
if (settlement.status !== undefined) {
|
|
386
|
-
const statusSpecific = rolePhaseMatched.find(
|
|
387
|
-
(candidate) =>
|
|
388
|
-
candidate.matches?.statuses !== undefined &&
|
|
389
|
-
candidate.matches.statuses.includes(settlement.status!),
|
|
390
|
-
);
|
|
391
|
-
if (statusSpecific !== undefined) {
|
|
392
|
-
return { candidate: statusSpecific, matchedToSettlement: true };
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const rolePhaseGeneric = rolePhaseMatched.find(
|
|
396
|
-
(candidate) => candidate.matches?.statuses === undefined,
|
|
397
|
-
);
|
|
398
|
-
if (rolePhaseGeneric !== undefined) {
|
|
399
|
-
return { candidate: rolePhaseGeneric, matchedToSettlement: true };
|
|
400
|
-
}
|
|
401
|
-
return undefined;
|
|
402
|
-
}
|
|
403
|
-
// v1 direction-only / broken matches: absent match metadata must not drop a usable next.
|
|
404
|
-
// Not settlement-keyed → caller may run one stale-context rebind.
|
|
405
|
-
const unbound = usable.find((candidate) => candidate.matches === undefined);
|
|
406
|
-
if (unbound === undefined) return undefined;
|
|
407
|
-
return { candidate: unbound, matchedToSettlement: false };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
271
|
export function formatNavigatorReport(report: NavigatorReport): string {
|
|
411
272
|
const playbookFailure = report.routePlaybookReadFailure === undefined
|
|
412
273
|
? []
|
|
@@ -418,54 +279,27 @@ export function formatNavigatorReport(report: NavigatorReport): string {
|
|
|
418
279
|
if (report.disposition === "arrival") {
|
|
419
280
|
return [...playbookFailure, ...(report.arrivalMessage ? [report.arrivalMessage] : [])].join("\n");
|
|
420
281
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
...(report.route === undefined ? [] : [`路线:${routeText(report.route)}`]),
|
|
424
|
-
...(report.next === undefined ? [] : [`下一步:${targetText(report.next)}`]),
|
|
425
|
-
...(report.reason === undefined || report.reason.trim() === "" ? [] : [`理由:${report.reason}`]),
|
|
426
|
-
...(report.command === undefined || report.command.trim() === "" ? [] : [`命令:${report.command}`]),
|
|
427
|
-
].join("\n");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
export type SettlementNavigation = {
|
|
431
|
-
disposition: "recommendation";
|
|
432
|
-
route?: NavigatorRouteTarget[];
|
|
433
|
-
next: NavigatorRouteTarget;
|
|
434
|
-
reason?: string;
|
|
435
|
-
command?: string;
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
/** Recommendation essentials for the one mandatory last-ak_*_output extraction. */
|
|
439
|
-
export function settlementNavigationFromEvent(event: NavigatorEvent): SettlementNavigation | undefined {
|
|
440
|
-
if (event.disposition !== "recommendation") return undefined;
|
|
441
|
-
if (event.next === undefined) return undefined;
|
|
442
|
-
return {
|
|
443
|
-
disposition: "recommendation",
|
|
444
|
-
...(event.route === undefined ? {} : { route: event.route }),
|
|
445
|
-
next: event.next,
|
|
446
|
-
...(event.reason === undefined ? {} : { reason: event.reason }),
|
|
447
|
-
...(event.command === undefined ? {} : { command: event.command }),
|
|
448
|
-
};
|
|
282
|
+
// advice — prose as-is
|
|
283
|
+
return [...playbookFailure, ...(report.prose ? [report.prose] : [])].join("\n");
|
|
449
284
|
}
|
|
450
285
|
|
|
451
286
|
export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
452
|
-
let preparation: Promise<
|
|
287
|
+
let preparation: Promise<string | undefined> | undefined;
|
|
288
|
+
let preparationInFlight = false;
|
|
453
289
|
let sessionReady: Promise<NavigatorPreparationSession> | undefined;
|
|
454
290
|
let session: NavigatorPreparationSession | undefined;
|
|
455
291
|
let subjectKey = options.subjectKey;
|
|
456
292
|
let subject = options.subject;
|
|
457
293
|
let authority = options.authority;
|
|
458
294
|
let contextError = options.contextError;
|
|
459
|
-
let
|
|
295
|
+
let preparedProse: string | undefined;
|
|
460
296
|
// Shared lifecycle owns the principal when supplied; otherwise mint once per attendance.
|
|
461
297
|
const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
|
|
462
298
|
let activeInvocationId: string | undefined = invocationPrincipal;
|
|
463
|
-
let previousRoute: NavigatorRouteTarget[] | undefined;
|
|
464
299
|
let outputSink: ((value: PrepareOutput) => void) | undefined;
|
|
465
300
|
let settlementTail: Promise<void> = Promise.resolve();
|
|
466
301
|
let settlementFailure: unknown;
|
|
467
302
|
let preparationFailure: unknown;
|
|
468
|
-
let preparationNoReceipt = false;
|
|
469
303
|
let routePlaybookReadFailure: string | undefined;
|
|
470
304
|
let disposed = false;
|
|
471
305
|
/** One-shot live-help warm; consumed by the next prepare so later prepares reread live help. */
|
|
@@ -496,19 +330,23 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
496
330
|
};
|
|
497
331
|
};
|
|
498
332
|
let routePlaybookSettlement: Promise<void> | undefined;
|
|
499
|
-
/**
|
|
333
|
+
/**
|
|
334
|
+
* Settlement fed for the output prompt. Early prepare() runs the host round
|
|
335
|
+
* from parent start (ready and wait); settleOnce feeds currentSettlement and
|
|
336
|
+
* lets navigator speak. No candidates/next parse (owner 2026-09-17 #959).
|
|
337
|
+
*/
|
|
500
338
|
let prepareBoundSettlement: NavigatorSettlement | undefined;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
339
|
+
type MaterialsBundle = {
|
|
340
|
+
soul: string;
|
|
341
|
+
routePlaybook: string;
|
|
342
|
+
helpContext: string;
|
|
343
|
+
modelSetting: string;
|
|
344
|
+
model: ReturnType<typeof parseNavigatorModelSetting>;
|
|
345
|
+
help: Array<{ role: NavigatorTargetRole; help: string }>;
|
|
346
|
+
activeSession: NavigatorPreparationSession;
|
|
347
|
+
};
|
|
348
|
+
/** Materials + session only — no host prompt (cold settle book-before-feed). */
|
|
349
|
+
const loadMaterialsAndSession = async (invocationId: string): Promise<MaterialsBundle> => {
|
|
512
350
|
if (contextError !== undefined) throw navigatorUnavailableError("context", contextError);
|
|
513
351
|
if (typeof authority !== "string" || authority.trim() === "") {
|
|
514
352
|
throw navigatorUnavailableError(
|
|
@@ -516,161 +354,173 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
516
354
|
new Error("controlling authority content was not supplied as typed work context"),
|
|
517
355
|
);
|
|
518
356
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
357
|
+
let soul: string;
|
|
358
|
+
let modelSetting: string;
|
|
359
|
+
let help: Array<{ role: NavigatorTargetRole; help: string }>;
|
|
360
|
+
let routePlaybook = "";
|
|
361
|
+
routePlaybookReadFailure = undefined;
|
|
362
|
+
const soulPromise = (async () => {
|
|
363
|
+
try {
|
|
364
|
+
const text = (await options.loadSoul()).trim();
|
|
365
|
+
if (!text) throw new Error("Navigator soul is empty");
|
|
366
|
+
return text;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
throw navigatorUnavailableError("context", error);
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
const routePlaybookPromise = (async () => {
|
|
372
|
+
if (options.loadRoutePlaybook === undefined) return "";
|
|
373
|
+
try {
|
|
374
|
+
return await options.loadRoutePlaybook();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
routePlaybookReadFailure = error instanceof Error ? error.message : String(error);
|
|
377
|
+
return "";
|
|
378
|
+
}
|
|
379
|
+
})();
|
|
380
|
+
routePlaybookSettlement = routePlaybookPromise.then(() => undefined);
|
|
381
|
+
const modelPromise = (async () => {
|
|
382
|
+
try {
|
|
383
|
+
const resolved = await resolveNavigatorSeatSelection(options.context);
|
|
384
|
+
return resolved.configuredLabel;
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof NavigatorUnavailableError) throw error;
|
|
387
|
+
throw navigatorUnavailableError("model", error);
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
const helpPromise = warmedHelp ?? loadLiveHelp();
|
|
391
|
+
warmedHelp = undefined;
|
|
392
|
+
[soul, routePlaybook, modelSetting, help] = await Promise.all([
|
|
393
|
+
soulPromise,
|
|
394
|
+
routePlaybookPromise,
|
|
395
|
+
modelPromise,
|
|
396
|
+
helpPromise,
|
|
397
|
+
]);
|
|
398
|
+
let model: ReturnType<typeof parseNavigatorModelSetting>;
|
|
399
|
+
try {
|
|
400
|
+
model = parseNavigatorModelSetting(modelSetting);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw navigatorUnavailableError("model", error);
|
|
403
|
+
}
|
|
404
|
+
const helpContext = help.map(({ role, help: text }) => `<role_help role="${role}">\n${text}\n</role_help>`).join("\n");
|
|
405
|
+
const tool = createNavigatorPrepareTool((value) => { outputSink?.(value); });
|
|
406
|
+
if (session === undefined) {
|
|
407
|
+
sessionReady = (async () => {
|
|
408
|
+
let created: NavigatorPreparationSession;
|
|
531
409
|
try {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
410
|
+
created = await options.createSession({
|
|
411
|
+
context: options.context,
|
|
412
|
+
subject: subjectKey,
|
|
413
|
+
...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }),
|
|
414
|
+
tool,
|
|
415
|
+
});
|
|
535
416
|
} catch (error) {
|
|
536
|
-
|
|
537
|
-
throw navigatorUnavailableError("context", error);
|
|
417
|
+
throw navigatorUnavailableError("session", error);
|
|
538
418
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
return await options.loadRoutePlaybook();
|
|
544
|
-
} catch (error) {
|
|
545
|
-
routePlaybookReadFailure = error instanceof Error ? error.message : String(error);
|
|
546
|
-
return "";
|
|
419
|
+
if (disposed) {
|
|
420
|
+
await created.dispose();
|
|
421
|
+
throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
547
422
|
}
|
|
548
|
-
})();
|
|
549
|
-
// Preparation is fail-fast for its primary dependencies, but settlement
|
|
550
|
-
// independently drains this optional diagnostic before emitting attendance.
|
|
551
|
-
routePlaybookSettlement = routePlaybookPromise.then(() => undefined);
|
|
552
|
-
const modelPromise = (async () => {
|
|
553
423
|
try {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
424
|
+
await created.setModel?.(modelSetting, model.thinkingLevel);
|
|
425
|
+
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
426
|
+
created.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
|
|
427
|
+
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
428
|
+
session = created;
|
|
429
|
+
return created;
|
|
557
430
|
} catch (error) {
|
|
558
|
-
if (
|
|
559
|
-
throw navigatorUnavailableError("
|
|
431
|
+
if (session !== created) await created.dispose();
|
|
432
|
+
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
560
433
|
}
|
|
561
434
|
})();
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
[soul, routePlaybook, modelSetting, help] = await Promise.all([
|
|
566
|
-
soulPromise,
|
|
567
|
-
routePlaybookPromise,
|
|
568
|
-
modelPromise,
|
|
569
|
-
helpPromise,
|
|
570
|
-
]);
|
|
571
|
-
|
|
572
|
-
let model: ReturnType<typeof parseNavigatorModelSetting>;
|
|
435
|
+
await sessionReady;
|
|
436
|
+
sessionReady = undefined;
|
|
437
|
+
} else {
|
|
573
438
|
try {
|
|
574
|
-
|
|
439
|
+
await session.setModel?.(modelSetting, model.thinkingLevel);
|
|
575
440
|
} catch (error) {
|
|
576
|
-
throw navigatorUnavailableError("
|
|
441
|
+
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
577
442
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
try {
|
|
600
|
-
created = await options.createSession({ context: options.context, subject: subjectKey, ...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }), tool });
|
|
601
|
-
} catch (error) {
|
|
602
|
-
throw navigatorUnavailableError("session", error);
|
|
603
|
-
}
|
|
604
|
-
if (disposed) {
|
|
605
|
-
await created.dispose();
|
|
606
|
-
throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
607
|
-
}
|
|
608
|
-
try {
|
|
609
|
-
await created.setModel?.(modelSetting, model.thinkingLevel);
|
|
610
|
-
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
611
|
-
created.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
|
|
612
|
-
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
613
|
-
session = created;
|
|
614
|
-
return created;
|
|
615
|
-
} catch (error) {
|
|
616
|
-
if (session !== created) await created.dispose();
|
|
617
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
618
|
-
}
|
|
619
|
-
})();
|
|
620
|
-
await sessionReady;
|
|
621
|
-
sessionReady = undefined;
|
|
622
|
-
} else {
|
|
623
|
-
try {
|
|
624
|
-
await session.setModel?.(modelSetting, model.thinkingLevel);
|
|
625
|
-
} catch (error) {
|
|
626
|
-
// Contract: README.md#Navigator-attendance — resumed-session configuration failures remain typed unavailable and retain the original cause.
|
|
627
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
628
|
-
}
|
|
629
|
-
session.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
|
|
630
|
-
}
|
|
631
|
-
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
632
|
-
const activeSession = session;
|
|
633
|
-
if (activeSession === undefined) throw new Error("Navigator session was not created");
|
|
634
|
-
const prior = activeSession.entries().filter((entry): entry is { type: "custom"; customType: string; data?: unknown } => exactRecord(entry) && entry.type === "custom" && entry.customType === ROUTE_ENTRY && exactRecord(entry.data) && entry.data.subjectKey === subjectKey).at(-1)?.data;
|
|
635
|
-
if (exactRecord(prior) && Array.isArray(prior.route) && prior.route.every((target) => targetIsValid(target))) {
|
|
636
|
-
previousRoute = prior.route.map((target) => ({ role: target.role as NavigatorTargetRole, phase: target.phase as NavigatorPhase }));
|
|
443
|
+
session.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
|
|
444
|
+
}
|
|
445
|
+
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
446
|
+
if (session === undefined) throw new Error("Navigator session was not created");
|
|
447
|
+
return { soul, routePlaybook, helpContext, modelSetting, model, help, activeSession: session };
|
|
448
|
+
};
|
|
449
|
+
const prepare = async (materialsBundle?: MaterialsBundle): Promise<string | undefined> => {
|
|
450
|
+
// Exact principal is owned by shared lifecycle (or one mint per attendance).
|
|
451
|
+
// Model/tool/advice paths cannot override it; role-session persistence is
|
|
452
|
+
// pi.appendEntry at lifecycle start — not optional sessionManager probing.
|
|
453
|
+
const boundSettlement = prepareBoundSettlement;
|
|
454
|
+
prepareBoundSettlement = undefined;
|
|
455
|
+
const invocationId = invocationPrincipal;
|
|
456
|
+
activeInvocationId = invocationId;
|
|
457
|
+
let output: PrepareOutput | undefined;
|
|
458
|
+
let prepareBatchRejected = false;
|
|
459
|
+
outputSink = (value) => {
|
|
460
|
+
// #836: extra prepare calls keep the first prose; later calls append as more prose.
|
|
461
|
+
if (output === undefined) {
|
|
462
|
+
output = value;
|
|
463
|
+
return;
|
|
637
464
|
}
|
|
465
|
+
const prior = normalizePrepareProse(output) ?? "";
|
|
466
|
+
const next = normalizePrepareProse(value) ?? "";
|
|
467
|
+
const merged = [prior, next].filter((part) => part.trim() !== "").join("\n\n");
|
|
468
|
+
output = { prose: merged };
|
|
469
|
+
};
|
|
470
|
+
// Cold settle passes its first-load bundle; early/warm prepare loads here.
|
|
471
|
+
const { soul, routePlaybook, helpContext, help, activeSession } =
|
|
472
|
+
materialsBundle ?? await loadMaterialsAndSession(invocationId);
|
|
638
473
|
const publicSettlementHistory = activeSession.entries()
|
|
639
474
|
.filter((entry): entry is { type: "custom"; customType: string; data?: unknown } => exactRecord(entry) && entry.type === "custom" && entry.customType === SETTLEMENT_ENTRY && exactRecord(entry.data))
|
|
640
475
|
.map((entry) => entry.data as NavigatorSettlementFact);
|
|
476
|
+
// Prior advice is the host session's own history (CLI resume), not a package ledger.
|
|
641
477
|
const projection: NavigatorContextProjection = {
|
|
642
478
|
subjectKey,
|
|
643
479
|
subject,
|
|
644
480
|
authority,
|
|
645
481
|
currentRole: { role: options.role, phase: options.phase },
|
|
646
482
|
...(boundSettlement === undefined ? {} : { currentSettlement: boundSettlement }),
|
|
647
|
-
priorRoute: exactRecord(prior) && Array.isArray(prior.route) && prior.route.every((target) => targetIsValid(target))
|
|
648
|
-
? prior.route.map((target) => ({ role: target.role as NavigatorTargetRole, phase: target.phase as NavigatorPhase }))
|
|
649
|
-
: null,
|
|
650
483
|
publicSettlementHistory,
|
|
651
484
|
liveRoleHelp: help,
|
|
652
485
|
};
|
|
653
486
|
activeSession.appendEntry(CONTEXT_ENTRY, projection);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
487
|
+
// Early (parent start): full host round — ready and wait. Settlement-bound:
|
|
488
|
+
// feed the result and let navigator speak (owner 2026-09-17 #959).
|
|
489
|
+
// ADR 0073: phase behavior text lives in resources/navigator-route-playbook.md;
|
|
490
|
+
// code only binds a neutral phase pointer + typed materials.
|
|
491
|
+
const request = boundSettlement === undefined
|
|
492
|
+
? [
|
|
493
|
+
"本轮为待命轮。",
|
|
494
|
+
`<navigator_soul>\n${soul}\n</navigator_soul>`,
|
|
495
|
+
...(routePlaybookReadFailure === undefined
|
|
496
|
+
? [`<route_playbook>\n${routePlaybook}\n</route_playbook>`]
|
|
497
|
+
: ["可选路线手册未能读取。"]),
|
|
498
|
+
`<work_subject>\n${subject}\n</work_subject>`,
|
|
499
|
+
`<controlling_authority>\n${authority}\n</controlling_authority>`,
|
|
500
|
+
`<current_role>\n${JSON.stringify({ role: options.role, phase: options.phase })}\n</current_role>`,
|
|
501
|
+
`<public_settlement_history>\n${JSON.stringify(projection.publicSettlementHistory)}\n</public_settlement_history>`,
|
|
502
|
+
`<live_role_help>\n${helpContext}\n</live_role_help>`,
|
|
503
|
+
].join("\n\n")
|
|
504
|
+
: [
|
|
505
|
+
"本轮为结算投喂轮。",
|
|
506
|
+
`<navigator_soul>\n${soul}\n</navigator_soul>`,
|
|
507
|
+
...(routePlaybookReadFailure === undefined
|
|
508
|
+
? [`<route_playbook>\n${routePlaybook}\n</route_playbook>`]
|
|
509
|
+
: ["可选路线手册未能读取。"]),
|
|
510
|
+
`<work_subject>\n${subject}\n</work_subject>`,
|
|
511
|
+
`<controlling_authority>\n${authority}\n</controlling_authority>`,
|
|
512
|
+
`<current_role>\n${JSON.stringify({ role: options.role, phase: options.phase })}\n</current_role>`,
|
|
513
|
+
`<current_settlement>\n${JSON.stringify(boundSettlement)}\n</current_settlement>`,
|
|
514
|
+
`<public_settlement_history>\n${JSON.stringify(projection.publicSettlementHistory)}\n</public_settlement_history>`,
|
|
515
|
+
`<live_role_help>\n${helpContext}\n</live_role_help>`,
|
|
516
|
+
].join("\n\n");
|
|
670
517
|
try {
|
|
671
518
|
try {
|
|
672
519
|
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
673
520
|
const delivery = createReceiptDeliveryPolicy();
|
|
521
|
+
// Production prose arrives only via nested summon → prepare tool.execute
|
|
522
|
+
// (navigator-public-session). No assistant-entry harvest — entries() is
|
|
523
|
+
// archivist custom-only on the wired factory (#959).
|
|
674
524
|
const promptAllowingRejectedPrepare = async (text: string, deliveryRequest: boolean) => {
|
|
675
525
|
const entryStart = activeSession.entries().length;
|
|
676
526
|
prepareBatchRejected = false;
|
|
@@ -705,20 +555,33 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
705
555
|
if (deliveryRequest && output === undefined) delivery.recordDeliveryRequest();
|
|
706
556
|
};
|
|
707
557
|
await promptAllowingRejectedPrepare(request, false);
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
558
|
+
// Bound output only: correction after rejected prepare. Early ready-wait
|
|
559
|
+
// does not 催交 final advice (owner: prepare then wait for settlement feed).
|
|
560
|
+
if (boundSettlement !== undefined) {
|
|
561
|
+
while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
|
|
562
|
+
await promptAllowingRejectedPrepare(RECEIPT_DELIVERY_PROMPT, true);
|
|
563
|
+
}
|
|
564
|
+
if (output === undefined && delivery.nextAction() === "request-delivery") {
|
|
565
|
+
while (delivery.nextAction() === "request-delivery") {
|
|
566
|
+
delivery.recordDeliveryRequest();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
|
|
570
|
+
const facts = delivery.facts({ runPointer: activeSession.recordPointer(), attemptPointer: invocationId });
|
|
571
|
+
activeSession.appendEntry(NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, facts);
|
|
572
|
+
preparedProse = undefined;
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
717
575
|
}
|
|
718
576
|
} catch (error) {
|
|
719
577
|
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
|
|
720
578
|
}
|
|
721
579
|
if (output === undefined) {
|
|
580
|
+
// Early host round: ready and wait — no final prose until settlement is fed.
|
|
581
|
+
if (boundSettlement === undefined) {
|
|
582
|
+
preparedProse = undefined;
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
722
585
|
const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
|
|
723
586
|
if (!exactRecord(entry) || entry.type !== "message" || !exactRecord(entry.message)) return false;
|
|
724
587
|
return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
|
|
@@ -734,8 +597,13 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
734
597
|
const cause = providerFailure?.cause ?? source;
|
|
735
598
|
throw navigatorUnavailableError(source, errorMessage, cause);
|
|
736
599
|
}
|
|
737
|
-
|
|
738
|
-
|
|
600
|
+
// Early tool/prose is not final advice — discard; settlement feed owns output.
|
|
601
|
+
if (boundSettlement === undefined) {
|
|
602
|
+
preparedProse = undefined;
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
preparedProse = normalizePrepareProse(output);
|
|
606
|
+
return preparedProse;
|
|
739
607
|
} finally {
|
|
740
608
|
outputSink = undefined;
|
|
741
609
|
}
|
|
@@ -747,7 +615,6 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
747
615
|
if (next.subjectKey !== subjectKey && session !== undefined) {
|
|
748
616
|
const previous = session;
|
|
749
617
|
session = undefined;
|
|
750
|
-
previousRoute = undefined;
|
|
751
618
|
closing = previous.dispose();
|
|
752
619
|
}
|
|
753
620
|
subjectKey = next.subjectKey;
|
|
@@ -770,12 +637,17 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
770
637
|
prepare(): void {
|
|
771
638
|
if (disposed || preparation !== undefined) return;
|
|
772
639
|
preparationFailure = undefined;
|
|
773
|
-
preparation
|
|
640
|
+
// Track live in-flight only — a resolved preparation still held until settle
|
|
641
|
+
// drain must not read as preparing (test helpers release early ready-wait by this).
|
|
642
|
+
preparationInFlight = true;
|
|
643
|
+
preparation = prepare().finally(() => {
|
|
644
|
+
preparationInFlight = false;
|
|
645
|
+
});
|
|
774
646
|
// Contract: README.md#Navigator-attendance — background preparation rejection is drained so the later typed settlement can report unavailable; retain the exact cause until settlement.
|
|
775
647
|
void preparation.catch((error) => { preparationFailure = error; });
|
|
776
648
|
},
|
|
777
649
|
isPreparing(): boolean {
|
|
778
|
-
return
|
|
650
|
+
return preparationInFlight;
|
|
779
651
|
},
|
|
780
652
|
knownRoutePlaybookReadFailure(): string | undefined {
|
|
781
653
|
return routePlaybookReadFailure;
|
|
@@ -802,84 +674,81 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
802
674
|
if (disposed) return;
|
|
803
675
|
const invocationId = activeInvocationId ?? invocationPrincipal;
|
|
804
676
|
let report: NavigatorReport;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
677
|
+
// Drain in-flight early prepare (parent-start host round: ready and wait).
|
|
678
|
+
// Then feed settlement for the output prompt on the same host session.
|
|
679
|
+
if (sessionReady !== undefined) {
|
|
680
|
+
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
681
|
+
}
|
|
682
|
+
if (preparation !== undefined) {
|
|
683
|
+
try {
|
|
684
|
+
await preparation;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
preparationFailure ??= error;
|
|
814
687
|
}
|
|
815
|
-
|
|
688
|
+
preparation = undefined;
|
|
689
|
+
}
|
|
690
|
+
const settlementFact = {
|
|
691
|
+
invocationId,
|
|
692
|
+
subjectKey,
|
|
693
|
+
role: settlement.role,
|
|
694
|
+
phase: settlement.phase,
|
|
695
|
+
kind: settlement.kind,
|
|
696
|
+
...("status" in settlement && settlement.status !== undefined
|
|
697
|
+
? { status: settlement.status }
|
|
698
|
+
: {}),
|
|
699
|
+
};
|
|
700
|
+
// Arrival is presentation-only: never start soul/model/help/session just to book.
|
|
701
|
+
let drainedProse: string | undefined;
|
|
702
|
+
if (settlement.kind === "arrival") {
|
|
703
|
+
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
816
704
|
if (preparationFailure !== undefined) {
|
|
817
705
|
report = unavailable(invocationId, preparationFailure);
|
|
818
706
|
} else {
|
|
819
|
-
// Affirmative no-advice attendance — never inferred later from absence.
|
|
820
|
-
report = { disposition: "no-advice" };
|
|
821
|
-
}
|
|
822
|
-
} else if (settlement.kind === "arrival") {
|
|
823
|
-
// Contract: README.md#Navigator-attendance — failed attendance is reported as typed unavailable rather than silently discarded.
|
|
824
|
-
if (sessionReady !== undefined) {
|
|
825
|
-
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
826
|
-
}
|
|
827
|
-
if (preparation !== undefined) {
|
|
828
|
-
try { await preparation; } catch (error) { preparationFailure ??= error; }
|
|
829
|
-
}
|
|
830
|
-
report = preparationFailure === undefined
|
|
831
|
-
? { disposition: "arrival", ...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }) }
|
|
832
|
-
: unavailable(invocationId, preparationFailure);
|
|
833
|
-
} else if (preparation === undefined) {
|
|
834
|
-
report = unavailable(invocationId, "Navigator preparation did not start");
|
|
835
|
-
} else {
|
|
836
|
-
try {
|
|
837
|
-
if (sessionReady !== undefined) await sessionReady;
|
|
838
|
-
let prepared = await preparation;
|
|
839
|
-
session?.appendEntry(SETTLEMENT_ENTRY, { invocationId, subjectKey, role: settlement.role, phase: settlement.phase, kind: settlement.kind, ...(settlement.status === undefined ? {} : { status: settlement.status }) });
|
|
840
|
-
let selected = selectNavigatorCandidate(prepared, settlement);
|
|
841
|
-
// Speculative prepare runs before this terminal exists and may treat prior
|
|
842
|
-
// history as decisive. When selection provenance says matches did not key
|
|
843
|
-
// this candidate to the settlement, rebind once with currentSettlement.
|
|
844
|
-
// Stale-context repair only — reachable without any next.role legality
|
|
845
|
-
// table. After selection (speculative or rebound), advice is passed
|
|
846
|
-
// through as-is (ADR 0010 / ADR 0061: caller may ignore).
|
|
847
|
-
if (selected?.candidate.next !== undefined && !selected.matchedToSettlement) {
|
|
848
|
-
if (disposed) return;
|
|
849
|
-
prepareBoundSettlement = settlement;
|
|
850
|
-
prepared = await prepare();
|
|
851
|
-
if (disposed) return;
|
|
852
|
-
selected = selectNavigatorCandidate(prepared, settlement);
|
|
853
|
-
}
|
|
854
|
-
// Budget exhaustion is affirmative typed no-advice; malformed submitted
|
|
855
|
-
// advice remains the existing unavailable path.
|
|
856
|
-
const selectedCandidate = selected?.candidate;
|
|
857
|
-
if (selectedCandidate?.next === undefined && preparationNoReceipt) {
|
|
858
|
-
report = { disposition: "no-advice" };
|
|
859
|
-
} else if (selectedCandidate?.next === undefined) {
|
|
860
|
-
throw new Error("Navigator prepared no machine-usable next direction");
|
|
861
|
-
} else {
|
|
862
|
-
const selectedRoute = selectedCandidate.route;
|
|
863
|
-
const routeChanged = selectedRoute !== undefined && !routeEqual(previousRoute, selectedRoute);
|
|
864
|
-
const command =
|
|
865
|
-
typeof selectedCandidate.command === "string" && selectedCandidate.command.trim() !== ""
|
|
866
|
-
? selectedCandidate.command
|
|
867
|
-
: renderPublicAkRoleCommand(selectedCandidate.next);
|
|
868
707
|
report = {
|
|
869
|
-
disposition: "
|
|
870
|
-
...(
|
|
871
|
-
next: selectedCandidate.next,
|
|
872
|
-
...(selectedCandidate.reason === undefined ? {} : { reason: selectedCandidate.reason }),
|
|
873
|
-
...(command === undefined ? {} : { command }),
|
|
708
|
+
disposition: "arrival",
|
|
709
|
+
...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }),
|
|
874
710
|
};
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
// Feed settlement; navigator speaks. Clear early-path failure only when a
|
|
714
|
+
// session already exists so the output prompt can run (owner: 准备好了就等着,
|
|
715
|
+
// 结果出来喂给它让它自己输出).
|
|
716
|
+
if (session !== undefined) preparationFailure = undefined;
|
|
717
|
+
// Cold settle: materials+session only (no unbound host prompt), then book + feed.
|
|
718
|
+
// Pass the bundle into prepare so loadLiveHelp/setModel/INVOCATION run once.
|
|
719
|
+
let coldMaterials: MaterialsBundle | undefined;
|
|
720
|
+
if (session === undefined && preparationFailure === undefined) {
|
|
721
|
+
try {
|
|
722
|
+
coldMaterials = await loadMaterialsAndSession(invocationId);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
preparationFailure = error;
|
|
878
725
|
}
|
|
726
|
+
}
|
|
727
|
+
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
728
|
+
if (preparationFailure === undefined) {
|
|
729
|
+
prepareBoundSettlement = settlement;
|
|
730
|
+
preparation = prepare(coldMaterials);
|
|
731
|
+
void preparation.catch((error) => { preparationFailure = error; });
|
|
732
|
+
try {
|
|
733
|
+
drainedProse = await preparation;
|
|
734
|
+
} catch (error) {
|
|
735
|
+
preparationFailure ??= error;
|
|
879
736
|
}
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
737
|
+
preparation = undefined;
|
|
738
|
+
}
|
|
739
|
+
if (preparationFailure !== undefined) {
|
|
740
|
+
// Contract: README.md#Navigator-attendance — failed attendance is typed
|
|
741
|
+
// unavailable without invalidating the role Receipt; retain the cause.
|
|
742
|
+
report = unavailable(invocationId, preparationFailure);
|
|
743
|
+
} else if (typeof drainedProse === "string" && drainedProse.trim() !== "") {
|
|
744
|
+
// #959: present prose as-is on every parent outcome — including
|
|
745
|
+
// human_decision / escalate. Old path wiped prose on escalate and left
|
|
746
|
+
// auto-attendance looking empty after a successful nested summon.
|
|
747
|
+
// Prior words stay on the host session (CLI resume); no package advice ledger.
|
|
748
|
+
report = { disposition: "advice", prose: drainedProse };
|
|
749
|
+
} else {
|
|
750
|
+
// Empty/no-receipt prepare → affirmative no-advice (never inferred later).
|
|
751
|
+
report = { disposition: "no-advice" };
|
|
883
752
|
}
|
|
884
753
|
}
|
|
885
754
|
// A primary preparation failure may reject Promise.all before the optional
|
|
@@ -896,10 +765,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
896
765
|
role: options.role,
|
|
897
766
|
phase: options.phase,
|
|
898
767
|
subjectKey,
|
|
899
|
-
...(report.
|
|
900
|
-
...(report.next === undefined ? {} : { next: report.next }),
|
|
901
|
-
...(report.reason === undefined ? {} : { reason: report.reason }),
|
|
902
|
-
...(report.command === undefined ? {} : { command: report.command }),
|
|
768
|
+
...(report.prose === undefined ? {} : { prose: report.prose }),
|
|
903
769
|
...(report.unavailableReason === undefined ? {} : { unavailableReason: report.unavailableReason }),
|
|
904
770
|
...(report.unavailableSource === undefined ? {} : { unavailableSource: report.unavailableSource }),
|
|
905
771
|
...(report.unavailableCause === undefined ? {} : { unavailableCause: report.unavailableCause }),
|
|
@@ -907,7 +773,7 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
907
773
|
...(report.arrivalMessage === undefined ? {} : { arrivalMessage: report.arrivalMessage }),
|
|
908
774
|
};
|
|
909
775
|
// Dispose during post-role grace must ignore late completion (ADR 0052 / #106 / #675).
|
|
910
|
-
// Every settled disposition (
|
|
776
|
+
// Every settled disposition (advice | no-advice | unavailable | arrival) is affirmative.
|
|
911
777
|
// Only the disposed bit is typed authority to drop — never match free-text Error.message.
|
|
912
778
|
if (disposed) return;
|
|
913
779
|
try {
|
|
@@ -919,9 +785,8 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
919
785
|
}
|
|
920
786
|
preparation = undefined;
|
|
921
787
|
sessionReady = undefined;
|
|
922
|
-
|
|
788
|
+
preparedProse = undefined;
|
|
923
789
|
preparationFailure = undefined;
|
|
924
|
-
preparationNoReceipt = false;
|
|
925
790
|
routePlaybookSettlement = undefined;
|
|
926
791
|
routePlaybookReadFailure = undefined;
|
|
927
792
|
}
|