@akagilnc/pi-workflow-roles 0.1.5842 → 0.1.5853
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/dist/acp-host/production-host.js +348 -1254
- package/dist/acp-host/role-turn-host.js +4 -98
- package/dist/headless-host/production-host.js +15731 -16582
- package/dist/headless-host/role-turn-host.js +6 -31
- package/dist/migrate-book-topology.js +31 -16
- package/dist/navigator-attendance.js +54 -404
- package/dist/navigator-invocation-identity.js +0 -2
- package/dist/navigator-lookup-host.js +41 -0
- package/dist/navigator-lookup.js +68 -0
- package/dist/navigator-route-table.js +42 -0
- package/dist/package-contracts/navigator-output.js +3 -11
- package/dist/packaged-role-registry.js +3 -27
- package/dist/public-cli/instruction-seat-run.js +4 -1
- package/dist/public-cli/invocation.js +13 -26
- package/dist/public-cli/main.js +286 -114
- package/dist/public-cli/option-definitions.js +24 -4
- package/dist/public-cli/post-admission.js +0 -9
- package/dist/public-cli/role-seat-runtime.js +14 -0
- package/dist/public-cli/settlement.js +14 -40
- package/dist/public-cli/terminal.js +1 -8
- package/dist/public-cli/turn-request.js +0 -3
- package/dist/public-role-summons.js +0 -1
- package/dist/receipt-delivery-policy.js +0 -23
- package/dist/role-runtime-dependencies.js +4 -11
- package/dist/role-runtime.js +18 -194
- package/dist/submission-ledger.js +19 -15
- package/dist/ticket-progress.js +23 -5
- package/extensions/role-runtime.ts +0 -16
- package/package.json +1 -1
- package/scripts/build-package.mjs +3 -7
- package/souls/notary.md +1 -1
- package/src/acp-host/role-turn-host.ts +8 -97
- package/src/headless-host/description.ts +0 -2
- package/src/headless-host/role-turn-host.ts +7 -32
- package/src/host-contracts.ts +5 -7
- package/src/navigator-attendance.ts +86 -549
- package/src/navigator-invocation-identity.ts +0 -2
- package/src/navigator-lookup-host.ts +45 -0
- package/src/navigator-lookup.ts +98 -0
- package/src/navigator-route-table.ts +92 -0
- package/src/package-contracts/navigator-output.ts +4 -15
- package/src/packaged-role-registry.ts +3 -32
- package/src/public-cli/instruction-seat-run.ts +3 -0
- package/src/public-cli/invocation.ts +18 -30
- package/src/public-cli/option-definitions.ts +25 -4
- package/src/public-cli/post-admission.ts +0 -11
- package/src/public-cli/role-seat-runtime.ts +14 -0
- package/src/public-cli/settlement.ts +23 -51
- package/src/public-cli/terminal.ts +3 -18
- package/src/public-cli/turn-request.ts +0 -5
- package/src/public-role-summons.ts +0 -8
- package/src/receipt-delivery-policy.ts +0 -21
- package/src/role-runtime-dependencies.ts +4 -11
- package/src/role-runtime.ts +19 -210
- package/src/submission-ledger.ts +23 -16
- package/src/ticket-progress.ts +32 -5
- package/dist/navigator-public-session.js +0 -205
- package/dist/navigator-role.js +0 -6
- package/dist/navigator-session-contracts.js +0 -227
- package/dist/navigator-work-context.js +0 -72
- package/resources/navigator-route-playbook.md +0 -33
- package/src/navigator-public-session.ts +0 -305
- package/src/navigator-role.ts +0 -15
- package/src/navigator-session-contracts.ts +0 -320
- package/src/navigator-work-context.ts +0 -97
|
@@ -1,82 +1,18 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { type Static } from "typebox";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
NAVIGATOR_INVOCATION_ENTRY,
|
|
10
|
-
NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY,
|
|
11
|
-
mintNavigatorInvocationId,
|
|
12
|
-
} from "./navigator-invocation-identity.ts";
|
|
13
|
-
import { PACKAGED_ROLE_REGISTRY, type PackagedRole } from "./packaged-role-registry.ts";
|
|
14
|
-
import {
|
|
15
|
-
activationBookDirectory,
|
|
16
|
-
resolveActivationLedgerHome,
|
|
17
|
-
} from "./activation-ledger-topology.ts";
|
|
18
1
|
import type { HostContext } from "./host-contracts.ts";
|
|
19
|
-
import {
|
|
2
|
+
import { mintNavigatorInvocationId, NAVIGATOR_INVOCATION_ENTRY } from "./navigator-invocation-identity.ts";
|
|
20
3
|
import {
|
|
21
|
-
NAVIGATOR_PREPARE_TOOL_NAME,
|
|
22
4
|
NavigatorUnavailableError,
|
|
23
|
-
navigatorModelSettingPath,
|
|
24
|
-
navigatorProviderFailure,
|
|
25
|
-
navigatorProviderFailureFromDiagnostics,
|
|
26
|
-
navigatorProviderFailureFromError,
|
|
27
|
-
navigatorProviderFailureFromPublicTerminal,
|
|
28
|
-
navigatorProviderFailureFromStatus,
|
|
29
5
|
navigatorUnavailableError,
|
|
30
|
-
|
|
31
|
-
readNavigatorModelSetting,
|
|
32
|
-
resolveNavigatorSeatSelection,
|
|
33
|
-
writeNavigatorModelSetting,
|
|
34
|
-
type NavigatorPreparationSession,
|
|
35
|
-
type NavigatorProviderFailureFact,
|
|
36
|
-
type NavigatorSessionFactory,
|
|
6
|
+
type NavigatorLookup,
|
|
37
7
|
type NavigatorUnavailableKey,
|
|
38
|
-
} from "./navigator-
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
NAVIGATOR_PREPARE_TOOL_NAME,
|
|
43
|
-
NavigatorUnavailableError,
|
|
44
|
-
navigatorModelSettingPath,
|
|
45
|
-
navigatorProviderFailure,
|
|
46
|
-
navigatorProviderFailureFromDiagnostics,
|
|
47
|
-
navigatorProviderFailureFromError,
|
|
48
|
-
navigatorProviderFailureFromPublicTerminal,
|
|
49
|
-
navigatorProviderFailureFromStatus,
|
|
50
|
-
navigatorUnavailableError,
|
|
51
|
-
parseNavigatorModelSetting,
|
|
52
|
-
readNavigatorModelSetting,
|
|
53
|
-
writeNavigatorModelSetting,
|
|
54
|
-
type NavigatorPreparationSession,
|
|
55
|
-
type NavigatorProviderFailureFact,
|
|
56
|
-
type NavigatorSessionFactory,
|
|
57
|
-
type NavigatorUnavailableKey,
|
|
58
|
-
};
|
|
59
|
-
export { createNativeNavigatorSessionFactory };
|
|
60
|
-
export { resolveNavigatorSeatSelection };
|
|
61
|
-
import { issueRoot, subjectPath } from "./work-subject-identity.ts";
|
|
62
|
-
import { createReceiptDeliveryPolicy } from "./receipt-delivery-policy.ts";
|
|
63
|
-
import {
|
|
64
|
-
byStatusToPlainObject,
|
|
65
|
-
mergePreparedAdvice,
|
|
66
|
-
navigatorAdviceBodySchema,
|
|
67
|
-
pickPreparedProse,
|
|
68
|
-
preparedAdviceFromUnknown,
|
|
69
|
-
type PreparedNavigatorAdvice,
|
|
70
|
-
} from "./package-contracts/navigator-output.ts";
|
|
71
|
-
import { sha256Hex } from "./sha256.ts";
|
|
72
|
-
import { isRecord } from "./unknown-value.ts";
|
|
8
|
+
} from "./navigator-lookup.ts";
|
|
9
|
+
import type { PackagedRole } from "./packaged-role-registry.ts";
|
|
10
|
+
import { pickPreparedProse, type PreparedNavigatorAdvice } from "./package-contracts/navigator-output.ts";
|
|
11
|
+
import { subjectPath } from "./work-subject-identity.ts";
|
|
73
12
|
|
|
74
13
|
export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
|
|
75
|
-
export {
|
|
76
|
-
|
|
77
|
-
/** Every public role is a lawful navigator route target (#675 — no nested-only seats). */
|
|
78
|
-
export const NAVIGATOR_TARGETS = PACKAGED_ROLE_REGISTRY
|
|
79
|
-
.map(({ role, phases }) => ({ role, phases }));
|
|
14
|
+
export { NAVIGATOR_INVOCATION_ENTRY, NavigatorUnavailableError, navigatorUnavailableError };
|
|
15
|
+
export type { NavigatorUnavailableKey };
|
|
80
16
|
|
|
81
17
|
export type NavigatorTargetRole = PackagedRole;
|
|
82
18
|
export type NavigatorPhase = "plan" | "apply" | null;
|
|
@@ -86,28 +22,17 @@ export type NavigatorSettlement =
|
|
|
86
22
|
| { kind: "role_infrastructure_failure"; role: string; phase: NavigatorPhase }
|
|
87
23
|
| { kind: "arrival"; role: "lander"; phase: null; message?: string };
|
|
88
24
|
|
|
89
|
-
export type NavigatorSubjectProvenance = "placeholder" | "role_input" | "user_prompt";
|
|
90
|
-
|
|
91
|
-
/** #1187: auto prepare identity only — no subject/authority material fields. */
|
|
92
|
-
export type NavigatorWorkContext = {
|
|
93
|
-
subjectKey: string;
|
|
94
|
-
subjectProvenance: NavigatorSubjectProvenance;
|
|
95
|
-
contextError?: unknown;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
25
|
/**
|
|
99
|
-
* #959:
|
|
100
|
-
*
|
|
26
|
+
* #959 / #1192: the navigator speaks the table's own words. Code does not parse,
|
|
27
|
+
* rank, or judge them — only whether the lookup itself failed.
|
|
101
28
|
*/
|
|
102
29
|
export type NavigatorReport = {
|
|
103
30
|
/** Affirmative attendance only. Lawful no-advice is typed, never inferred from absence. */
|
|
104
31
|
disposition: "advice" | "no-advice" | "unavailable" | "arrival";
|
|
105
|
-
/** Present when disposition is advice —
|
|
32
|
+
/** Present when disposition is advice — presented as-is. */
|
|
106
33
|
prose?: string;
|
|
107
34
|
unavailableReason?: string;
|
|
108
35
|
unavailableSource?: NavigatorUnavailableKey;
|
|
109
|
-
unavailableCause?: NavigatorUnavailableKey;
|
|
110
|
-
routePlaybookReadFailure?: string;
|
|
111
36
|
arrivalMessage?: string;
|
|
112
37
|
};
|
|
113
38
|
|
|
@@ -121,165 +46,40 @@ export type NavigatorEvent = {
|
|
|
121
46
|
prose?: string;
|
|
122
47
|
unavailableReason?: string;
|
|
123
48
|
unavailableSource?: NavigatorUnavailableKey;
|
|
124
|
-
unavailableCause?: NavigatorUnavailableKey;
|
|
125
|
-
routePlaybookReadFailure?: string;
|
|
126
49
|
arrivalMessage?: string;
|
|
127
50
|
};
|
|
128
51
|
|
|
129
|
-
// #959 / #1160: prepare reuses the single navigator advice field owner.
|
|
130
|
-
// Object root only (ADR 0060); nested shape is never a gate — every object root
|
|
131
|
-
// reaches execute exactly once (Rule 0).
|
|
132
|
-
const prepareSchema = navigatorAdviceBodySchema;
|
|
133
|
-
type PrepareOutput = Static<typeof prepareSchema>;
|
|
134
|
-
|
|
135
52
|
export type NavigatorAttendanceOptions = {
|
|
136
53
|
context: HostContext;
|
|
137
54
|
role: string;
|
|
138
55
|
phase: NavigatorPhase;
|
|
139
56
|
subjectKey: string;
|
|
140
|
-
|
|
141
|
-
modelSettingPath?: string;
|
|
142
|
-
contextError?: unknown;
|
|
57
|
+
lookup: NavigatorLookup;
|
|
143
58
|
/** Exact principal owned by shared role lifecycle; attendance never overrides it. */
|
|
144
59
|
invocationId?: string;
|
|
145
|
-
/** Effective ceiling already resolved for this turn (#1132). */
|
|
146
|
-
deliveryRequestLimit?: number;
|
|
147
60
|
onEvent: (event: NavigatorEvent, report: NavigatorReport) => void | Promise<void>;
|
|
148
61
|
};
|
|
149
62
|
|
|
150
|
-
const INVOCATION_ENTRY = NAVIGATOR_INVOCATION_ENTRY;
|
|
151
|
-
const SETTLEMENT_ENTRY = "ak-navigator-settlement";
|
|
152
|
-
const unavailableKeys = new Set<NavigatorUnavailableKey>(["context", "session", "model", "thinking", "auth", "quota", "transport", "unknown"]);
|
|
153
|
-
|
|
154
|
-
function unavailableKey(value: unknown): NavigatorUnavailableKey | undefined {
|
|
155
|
-
return typeof value === "string" && unavailableKeys.has(value as NavigatorUnavailableKey)
|
|
156
|
-
? value as NavigatorUnavailableKey
|
|
157
|
-
: undefined;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** Correlate one rejected prepare call/result inside the just-finished prompt. */
|
|
161
|
-
function rejectedPrepareReason(entries: readonly unknown[], start: number): string | undefined {
|
|
162
|
-
const recent = entries.slice(start);
|
|
163
|
-
const prepareCalls = new Set<string>();
|
|
164
|
-
for (const entry of recent) {
|
|
165
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
|
|
166
|
-
|| entry.message.role !== "assistant" || !Array.isArray(entry.message.content)) continue;
|
|
167
|
-
for (const part of entry.message.content) {
|
|
168
|
-
if (isRecord(part) && part.type === "toolCall" && part.name === NAVIGATOR_PREPARE_TOOL_NAME
|
|
169
|
-
&& typeof part.id === "string") prepareCalls.add(part.id);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
let reason: string | undefined;
|
|
173
|
-
for (const entry of recent) {
|
|
174
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)
|
|
175
|
-
|| entry.message.role !== "toolResult" || entry.message.isError !== true) continue;
|
|
176
|
-
const callId = entry.message.toolCallId;
|
|
177
|
-
if (entry.message.toolName !== NAVIGATOR_PREPARE_TOOL_NAME
|
|
178
|
-
|| typeof callId !== "string" || !prepareCalls.has(callId)) {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
const content = entry.message.content;
|
|
182
|
-
const text = Array.isArray(content)
|
|
183
|
-
? content.flatMap((part) => isRecord(part) && typeof part.text === "string" ? [part.text] : []).join("")
|
|
184
|
-
: typeof content === "string" ? content : "";
|
|
185
|
-
if (text.trim() !== "") reason = text.trim();
|
|
186
|
-
}
|
|
187
|
-
return reason;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* #959 / #1160: extract prepared advice from any prepare submission shape.
|
|
192
|
-
* Missing/empty → undefined. Never a rejection — shape is not an admission gate.
|
|
193
|
-
* Production payload arrives via nested public summon → prepare tool.execute only;
|
|
194
|
-
* archivist entries() never carries assistant message text, so no parallel harvest.
|
|
195
|
-
*/
|
|
196
|
-
function normalizePreparedAdvice(value: unknown): PreparedNavigatorAdvice | undefined {
|
|
197
|
-
return preparedAdviceFromUnknown(value);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function navigatorSubjectKey(
|
|
201
|
-
subjectRoot: string,
|
|
202
|
-
subject: string,
|
|
203
|
-
provenance: NavigatorSubjectProvenance = "role_input",
|
|
204
|
-
): string {
|
|
205
|
-
if (issueRoot(subjectRoot) !== undefined || !subjectRoot.includes("/.ak/work/")) return subjectRoot;
|
|
206
|
-
if (provenance === "placeholder") return subjectRoot;
|
|
207
|
-
const normalized = subject.trim().replace(/\s+/g, " ");
|
|
208
|
-
if (normalized === "") return subjectRoot;
|
|
209
|
-
return `${subjectRoot}#${sha256Hex(normalized).slice(0, 32)}`;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Role inputs for one ad-hoc work item live below role-specific run folders.
|
|
214
|
-
* The folder and filename are transport, not identity: the shared work root
|
|
215
|
-
* keeps natural inputs on one subject.
|
|
216
|
-
*/
|
|
217
|
-
export function navigatorSubjectKeyForInput(subjectRoot: string, reference: string, cwd = process.cwd()): string {
|
|
218
|
-
if (issueRoot(subjectRoot) !== undefined || !subjectRoot.includes("/.ak/work/")) return subjectRoot;
|
|
219
|
-
const resolvedReference = resolve(cwd, reference);
|
|
220
|
-
const marker = "/runs/";
|
|
221
|
-
if (resolvedReference.includes(marker)) {
|
|
222
|
-
// The work root, not a role-input filename, is the stable subject.
|
|
223
|
-
// Different roots remain isolated without inventing a filename convention.
|
|
224
|
-
return subjectRoot;
|
|
225
|
-
}
|
|
226
|
-
return navigatorSubjectKey(subjectRoot, resolvedReference);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => void): ToolDefinition {
|
|
230
|
-
return {
|
|
231
|
-
name: NAVIGATOR_PREPARE_TOOL_NAME,
|
|
232
|
-
label: "游奕使准备",
|
|
233
|
-
description: "提交游奕使散文建议。",
|
|
234
|
-
parameters: prepareSchema,
|
|
235
|
-
async execute(_id, value) {
|
|
236
|
-
// Rule 0: the unique prepare submission is accepted once. Shape is never a gate.
|
|
237
|
-
onOutput(value as PrepareOutput);
|
|
238
|
-
return { content: [], details: value, terminate: true as const };
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
63
|
export function formatNavigatorReport(report: NavigatorReport): string {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (report.disposition === "unavailable") {
|
|
249
|
-
return [...playbookFailure, ...(report.unavailableReason ? [report.unavailableReason] : [])].join("\n");
|
|
250
|
-
}
|
|
251
|
-
if (report.disposition === "arrival") {
|
|
252
|
-
return [...playbookFailure, ...(report.arrivalMessage ? [report.arrivalMessage] : [])].join("\n");
|
|
253
|
-
}
|
|
254
|
-
// advice — prose as-is
|
|
255
|
-
return [...playbookFailure, ...(report.prose ? [report.prose] : [])].join("\n");
|
|
64
|
+
if (report.disposition === "no-advice") return "";
|
|
65
|
+
if (report.disposition === "unavailable") return report.unavailableReason ?? "";
|
|
66
|
+
if (report.disposition === "arrival") return report.arrivalMessage ?? "";
|
|
67
|
+
return report.prose ?? "";
|
|
256
68
|
}
|
|
257
69
|
|
|
258
70
|
export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
71
|
+
const { subjectKey } = options;
|
|
259
72
|
let preparation: Promise<PreparedNavigatorAdvice | undefined> | undefined;
|
|
260
73
|
let preparationInFlight = false;
|
|
261
|
-
|
|
262
|
-
let session: NavigatorPreparationSession | undefined;
|
|
263
|
-
let subjectKey = options.subjectKey;
|
|
264
|
-
// #1187: auto prepare is identity-only (role/phase/subjectKey).
|
|
265
|
-
let contextError = options.contextError;
|
|
266
|
-
/** Parallel-prepare result: status-keyed prose picked at settle (#1160). */
|
|
74
|
+
/** Looked-up rows: status-keyed prose picked at settle (#1160). */
|
|
267
75
|
let preparedAdvice: PreparedNavigatorAdvice | undefined;
|
|
268
|
-
// Shared lifecycle owns the principal when supplied; otherwise mint once per attendance.
|
|
269
|
-
const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
|
|
270
|
-
let activeInvocationId: string | undefined = invocationPrincipal;
|
|
271
|
-
let outputSink: ((value: PrepareOutput) => void) | undefined;
|
|
272
|
-
let settlementTail: Promise<void> = Promise.resolve();
|
|
273
|
-
let settlementFailure: unknown;
|
|
274
76
|
let preparationFailure: unknown;
|
|
275
|
-
let
|
|
77
|
+
let settlementTail: Promise<void> = Promise.resolve();
|
|
276
78
|
let disposed = false;
|
|
277
|
-
|
|
278
|
-
let closing: Promise<void> | undefined;
|
|
79
|
+
const invocationId = options.invocationId ?? mintNavigatorInvocationId();
|
|
279
80
|
// Shared attendance seam owns nested summon cancel (ADR 0018 / #959).
|
|
280
|
-
// Role session factory only forwards HostContext.signal — never its own controller.
|
|
281
81
|
const nestCancel = new AbortController();
|
|
282
|
-
const
|
|
82
|
+
const lookupContext = (): HostContext => {
|
|
283
83
|
const parentSignal = options.context.signal;
|
|
284
84
|
const signal = parentSignal === undefined
|
|
285
85
|
? nestCancel.signal
|
|
@@ -287,231 +87,88 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
287
87
|
return { ...options.context, signal };
|
|
288
88
|
};
|
|
289
89
|
|
|
290
|
-
const unavailable = (
|
|
291
|
-
const failure = reason
|
|
292
|
-
? reason
|
|
293
|
-
: navigatorUnavailableError("unknown", reason);
|
|
90
|
+
const unavailable = (reason: unknown): NavigatorReport => {
|
|
91
|
+
const failure = navigatorUnavailableError("unknown", reason);
|
|
294
92
|
return {
|
|
295
93
|
disposition: "unavailable",
|
|
296
94
|
unavailableReason: failure.message,
|
|
297
95
|
unavailableSource: failure.unavailableSource,
|
|
298
|
-
unavailableCause: failure.unavailableCause,
|
|
299
96
|
};
|
|
300
97
|
};
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
throw navigatorUnavailableError("model", error);
|
|
317
|
-
}
|
|
318
|
-
})();
|
|
319
|
-
const modelSetting = await modelPromise;
|
|
320
|
-
let model: ReturnType<typeof parseNavigatorModelSetting>;
|
|
321
|
-
try {
|
|
322
|
-
model = parseNavigatorModelSetting(modelSetting);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
throw navigatorUnavailableError("model", error);
|
|
325
|
-
}
|
|
326
|
-
const tool = createNavigatorPrepareTool((value) => { outputSink?.(value); });
|
|
327
|
-
if (session === undefined) {
|
|
328
|
-
sessionReady = (async () => {
|
|
329
|
-
let created: NavigatorPreparationSession;
|
|
330
|
-
try {
|
|
331
|
-
created = await options.createSession({
|
|
332
|
-
context: sessionHostContext(),
|
|
333
|
-
subject: subjectKey,
|
|
334
|
-
...(options.modelSettingPath === undefined ? {} : { modelSettingPath: options.modelSettingPath }),
|
|
335
|
-
tool,
|
|
336
|
-
});
|
|
337
|
-
} catch (error) {
|
|
338
|
-
throw navigatorUnavailableError("session", error);
|
|
339
|
-
}
|
|
340
|
-
if (disposed) {
|
|
341
|
-
await created.dispose();
|
|
342
|
-
throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
343
|
-
}
|
|
98
|
+
|
|
99
|
+
async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
|
|
100
|
+
// Dispose during post-role grace must ignore late completion entirely (#675).
|
|
101
|
+
if (disposed) return;
|
|
102
|
+
let report: NavigatorReport;
|
|
103
|
+
if (settlement.kind === "arrival") {
|
|
104
|
+
// Arrival is presentation-only: never waits for the lookup.
|
|
105
|
+
report = preparationFailure !== undefined
|
|
106
|
+
? unavailable(preparationFailure)
|
|
107
|
+
: {
|
|
108
|
+
disposition: "arrival",
|
|
109
|
+
...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }),
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
if (preparation !== undefined) {
|
|
344
113
|
try {
|
|
345
|
-
await
|
|
346
|
-
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
347
|
-
created.appendEntry(INVOCATION_ENTRY, { invocationId, role: options.role, phase: options.phase, subjectKey });
|
|
348
|
-
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
349
|
-
session = created;
|
|
350
|
-
return created;
|
|
114
|
+
await preparation;
|
|
351
115
|
} catch (error) {
|
|
352
|
-
|
|
353
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
116
|
+
preparationFailure ??= error;
|
|
354
117
|
}
|
|
355
|
-
|
|
356
|
-
await sessionReady;
|
|
357
|
-
sessionReady = undefined;
|
|
358
|
-
} else {
|
|
359
|
-
try {
|
|
360
|
-
await session.setModel?.(modelSetting, model.thinkingLevel);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("session", error);
|
|
118
|
+
preparation = undefined;
|
|
363
119
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
activeInvocationId = invocationId;
|
|
376
|
-
let output: PrepareOutput | undefined;
|
|
377
|
-
let prepareBatchRejected = false;
|
|
378
|
-
outputSink = (value) => {
|
|
379
|
-
// #836 / #1160: extra prepare calls merge byStatus (same key concatenates) and prose.
|
|
380
|
-
const next = normalizePreparedAdvice(value);
|
|
381
|
-
if (output === undefined) {
|
|
382
|
-
output = value;
|
|
383
|
-
return;
|
|
120
|
+
if (preparationFailure !== undefined) {
|
|
121
|
+
// Contract: README.md#Navigator-attendance — failed attendance is typed
|
|
122
|
+
// unavailable without invalidating the role Receipt; retain the cause.
|
|
123
|
+
report = unavailable(preparationFailure);
|
|
124
|
+
} else {
|
|
125
|
+
const status = "status" in settlement ? settlement.status : undefined;
|
|
126
|
+
const picked = pickPreparedProse(preparedAdvice, status);
|
|
127
|
+
// Missing key / no rows → affirmative no-advice; never an invented line.
|
|
128
|
+
report = typeof picked === "string" && picked.trim() !== ""
|
|
129
|
+
? { disposition: "advice", prose: picked }
|
|
130
|
+
: { disposition: "no-advice" };
|
|
384
131
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
: { byStatus: byStatusToPlainObject(merged.byStatus) }),
|
|
391
|
-
};
|
|
392
|
-
};
|
|
393
|
-
const activeSession = await loadMaterialsAndSession(invocationId);
|
|
394
|
-
// #1160: parallel prepare from parent start — model runs now; settle only picks.
|
|
395
|
-
// #1187: identity only. Parent dispatch / authority copies stay off this wire;
|
|
396
|
-
// navigator reads ticket and station records itself when needed.
|
|
397
|
-
const request = JSON.stringify({
|
|
398
|
-
kind: "prepare",
|
|
132
|
+
}
|
|
133
|
+
const event: NavigatorEvent = {
|
|
134
|
+
version: 1,
|
|
135
|
+
disposition: report.disposition,
|
|
136
|
+
invocationId,
|
|
399
137
|
role: options.role,
|
|
400
138
|
phase: options.phase,
|
|
401
|
-
invocationId,
|
|
402
139
|
subjectKey,
|
|
403
|
-
|
|
140
|
+
...(report.prose === undefined ? {} : { prose: report.prose }),
|
|
141
|
+
...(report.unavailableReason === undefined ? {} : { unavailableReason: report.unavailableReason }),
|
|
142
|
+
...(report.unavailableSource === undefined ? {} : { unavailableSource: report.unavailableSource }),
|
|
143
|
+
...(report.arrivalMessage === undefined ? {} : { arrivalMessage: report.arrivalMessage }),
|
|
144
|
+
};
|
|
145
|
+
if (disposed) return;
|
|
404
146
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
// (navigator-public-session). No assistant-entry harvest — entries() is
|
|
411
|
-
// archivist custom-only on the wired factory (#959).
|
|
412
|
-
const promptAllowingRejectedPrepare = async (text: string) => {
|
|
413
|
-
const entryStart = activeSession.entries().length;
|
|
414
|
-
prepareBatchRejected = false;
|
|
415
|
-
let promptFailure: unknown;
|
|
416
|
-
try {
|
|
417
|
-
await activeSession.prompt(text);
|
|
418
|
-
} catch (error) {
|
|
419
|
-
promptFailure = error;
|
|
420
|
-
}
|
|
421
|
-
const providerFailure = activeSession.providerFailure?.();
|
|
422
|
-
if (providerFailure !== undefined) {
|
|
423
|
-
throw navigatorUnavailableError(providerFailure.source, promptFailure ?? "Navigator provider failure", providerFailure.cause);
|
|
424
|
-
}
|
|
425
|
-
const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
|
|
426
|
-
if (rejectedReason !== undefined) {
|
|
427
|
-
// A rejected call makes every provisional output from this prompt
|
|
428
|
-
// ineligible for publication before the correction turn starts.
|
|
429
|
-
output = undefined;
|
|
430
|
-
prepareBatchRejected = true;
|
|
431
|
-
// The correction prompt below is the one spend for this rejection.
|
|
432
|
-
delivery.recordRejected(rejectedReason);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
if (promptFailure !== undefined) throw promptFailure;
|
|
436
|
-
const sessionNoReceipt = activeSession.noReceipt?.();
|
|
437
|
-
if (sessionNoReceipt !== undefined) {
|
|
438
|
-
// This session already settled without an accepted receipt. Keep the
|
|
439
|
-
// larger issued count and stop; a nested zero must not wipe prompts
|
|
440
|
-
// this layer already sent (#675 / #1132).
|
|
441
|
-
delivery.recordNestedNoReceipt(sessionNoReceipt);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
await promptAllowingRejectedPrepare(request);
|
|
446
|
-
// Correction after rejected prepare on the sole model round (#1160).
|
|
447
|
-
// #1208: resend this turn's prepare request; delivery-state stays on policy facts.
|
|
448
|
-
while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
|
|
449
|
-
delivery.recordDeliveryRequest();
|
|
450
|
-
await promptAllowingRejectedPrepare(request);
|
|
451
|
-
}
|
|
452
|
-
if (output === undefined && delivery.nextAction() === "request-delivery") {
|
|
453
|
-
delivery.closeBudget();
|
|
454
|
-
}
|
|
455
|
-
if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
|
|
456
|
-
// Nested @navigator run already holds lawful no_receipt + runPointer when
|
|
457
|
-
// present. Do not write a parallel parent lifecycle off a dead side-branch
|
|
458
|
-
// pointer (#1178). Honest empty prep → settle reports no-advice.
|
|
459
|
-
preparedAdvice = undefined;
|
|
460
|
-
return undefined;
|
|
461
|
-
}
|
|
462
|
-
} catch (error) {
|
|
463
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
|
|
464
|
-
}
|
|
465
|
-
if (output === undefined) {
|
|
466
|
-
const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
|
|
467
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) return false;
|
|
468
|
-
return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
|
|
469
|
-
});
|
|
470
|
-
const nativeMessage = isRecord(nativeFailure) && isRecord(nativeFailure.message) ? nativeFailure.message : undefined;
|
|
471
|
-
const errorMessage = nativeMessage !== undefined && typeof nativeMessage.errorMessage === "string"
|
|
472
|
-
? nativeMessage.errorMessage
|
|
473
|
-
: "Navigator did not submit direction advice";
|
|
474
|
-
// Classification originates only at the native provider stream seam.
|
|
475
|
-
// AssistantMessage metadata is a human diagnostic surface, not an acceptance oracle.
|
|
476
|
-
const providerFailure = activeSession.providerFailure?.();
|
|
477
|
-
const source = providerFailure?.source ?? "unknown";
|
|
478
|
-
const cause = providerFailure?.cause ?? source;
|
|
479
|
-
throw navigatorUnavailableError(source, errorMessage, cause);
|
|
480
|
-
}
|
|
481
|
-
preparedAdvice = normalizePreparedAdvice(output);
|
|
482
|
-
return preparedAdvice;
|
|
483
|
-
} finally {
|
|
484
|
-
const playbookFailure = activeSession.routePlaybookReadFailure?.();
|
|
485
|
-
observedRoutePlaybookFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
|
|
486
|
-
? playbookFailure
|
|
487
|
-
: undefined;
|
|
488
|
-
outputSink = undefined;
|
|
147
|
+
await options.onEvent(event, report);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// Race: dispose landed between the check and onEvent; drop only then.
|
|
150
|
+
if (disposed) return;
|
|
151
|
+
throw error;
|
|
489
152
|
}
|
|
490
|
-
|
|
153
|
+
preparation = undefined;
|
|
154
|
+
preparedAdvice = undefined;
|
|
155
|
+
preparationFailure = undefined;
|
|
156
|
+
}
|
|
491
157
|
|
|
492
158
|
return {
|
|
493
|
-
setWorkContext(next: NavigatorWorkContext): void | Promise<void> {
|
|
494
|
-
let closing: void | Promise<void> | undefined;
|
|
495
|
-
if (next.subjectKey !== subjectKey && session !== undefined) {
|
|
496
|
-
const previous = session;
|
|
497
|
-
session = undefined;
|
|
498
|
-
closing = previous.dispose();
|
|
499
|
-
}
|
|
500
|
-
subjectKey = next.subjectKey;
|
|
501
|
-
contextError = next.contextError;
|
|
502
|
-
return closing;
|
|
503
|
-
},
|
|
504
159
|
prepare(): void {
|
|
505
|
-
|
|
160
|
+
// Soul: 「plan 阶段停用」— a plan-phase parent gets no lookup at all.
|
|
161
|
+
if (disposed || preparation !== undefined || options.phase === "plan") return;
|
|
506
162
|
preparationFailure = undefined;
|
|
507
163
|
preparedAdvice = undefined;
|
|
508
|
-
// Track live in-flight only — a resolved preparation still held until settle
|
|
509
|
-
// drain must not read as preparing (test helpers release by this).
|
|
510
164
|
preparationInFlight = true;
|
|
511
|
-
preparation =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
165
|
+
preparation = options.lookup({ context: lookupContext(), role: options.role })
|
|
166
|
+
.then((prepared) => {
|
|
167
|
+
preparedAdvice = prepared;
|
|
168
|
+
return prepared;
|
|
169
|
+
})
|
|
170
|
+
.finally(() => { preparationInFlight = false; });
|
|
171
|
+
// Contract: README.md#Navigator-attendance — background lookup rejection is drained so the later typed settlement can report unavailable; the exact cause is retained until settlement.
|
|
515
172
|
void preparation.catch((error) => { preparationFailure = error; });
|
|
516
173
|
},
|
|
517
174
|
isPreparing(): boolean {
|
|
@@ -519,140 +176,20 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
519
176
|
},
|
|
520
177
|
settle(settlement: NavigatorSettlement): Promise<void> {
|
|
521
178
|
const next = settlementTail.then(() => settleOnce(settlement));
|
|
522
|
-
// Contract: README.md#Navigator-attendance — rejected attendance settlements are drained only to serialize later attendance;
|
|
523
|
-
settlementTail = next.catch((
|
|
179
|
+
// Contract: README.md#Navigator-attendance — rejected attendance settlements are drained only to serialize later attendance; the rejection still reaches the caller.
|
|
180
|
+
settlementTail = next.catch(() => undefined);
|
|
524
181
|
return next;
|
|
525
182
|
},
|
|
526
|
-
dispose(): void
|
|
183
|
+
dispose(): void {
|
|
527
184
|
disposed = true;
|
|
528
|
-
// Abort nested
|
|
529
|
-
// close. Parent grace/shutdown must choose not to await (#959 reopen).
|
|
185
|
+
// Abort the nested lookup summon via the shared signal (ADR 0018 / #959).
|
|
530
186
|
if (!nestCancel.signal.aborted) {
|
|
531
|
-
nestCancel.abort(navigatorUnavailableError(
|
|
532
|
-
"session",
|
|
533
|
-
new Error("Navigator attendance was disposed"),
|
|
534
|
-
));
|
|
187
|
+
nestCancel.abort(navigatorUnavailableError("session", new Error("Navigator attendance was disposed")));
|
|
535
188
|
}
|
|
536
|
-
// Leave sessionReady so an in-flight createSession observes disposed and drains exactly once.
|
|
537
|
-
// Late settleOnce completion observes disposed and skips onEvent.
|
|
538
|
-
activeInvocationId = undefined;
|
|
539
|
-
if (closing === undefined) {
|
|
540
|
-
const current = session;
|
|
541
|
-
session = undefined;
|
|
542
|
-
// Preserve rejection for non-blocking recordDisposeFailure; resolve void on success.
|
|
543
|
-
closing = Promise.resolve(current?.dispose()).then(() => undefined);
|
|
544
|
-
}
|
|
545
|
-
// Same in-flight teardown on repeat dispose — awaiting here re-blocks the parent court.
|
|
546
|
-
return closing;
|
|
547
189
|
},
|
|
548
190
|
};
|
|
549
|
-
|
|
550
|
-
async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
|
|
551
|
-
// Dispose during post-role grace must ignore late completion entirely (#675).
|
|
552
|
-
if (disposed) return;
|
|
553
|
-
// Keep playbook failure observed on the prepare round; settle does not re-prompt (#1160).
|
|
554
|
-
const invocationId = activeInvocationId ?? invocationPrincipal;
|
|
555
|
-
let report: NavigatorReport;
|
|
556
|
-
const settlementFact = {
|
|
557
|
-
invocationId,
|
|
558
|
-
subjectKey,
|
|
559
|
-
role: settlement.role,
|
|
560
|
-
phase: settlement.phase,
|
|
561
|
-
kind: settlement.kind,
|
|
562
|
-
...("status" in settlement && settlement.status !== undefined
|
|
563
|
-
? { status: settlement.status }
|
|
564
|
-
: {}),
|
|
565
|
-
};
|
|
566
|
-
// Arrival is presentation-only: wait only for nest open, never for the model round.
|
|
567
|
-
if (settlement.kind === "arrival") {
|
|
568
|
-
if (sessionReady !== undefined) {
|
|
569
|
-
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
570
|
-
}
|
|
571
|
-
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
572
|
-
if (preparationFailure !== undefined) {
|
|
573
|
-
report = unavailable(invocationId, preparationFailure);
|
|
574
|
-
} else {
|
|
575
|
-
report = {
|
|
576
|
-
disposition: "arrival",
|
|
577
|
-
...(settlement.message === undefined ? {} : { arrivalMessage: settlement.message }),
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
} else {
|
|
581
|
-
// Drain in-flight parallel prepare, then pick by status — no second model round (#1160).
|
|
582
|
-
if (sessionReady !== undefined) {
|
|
583
|
-
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
584
|
-
}
|
|
585
|
-
if (preparation !== undefined) {
|
|
586
|
-
try {
|
|
587
|
-
await preparation;
|
|
588
|
-
} catch (error) {
|
|
589
|
-
preparationFailure ??= error;
|
|
590
|
-
}
|
|
591
|
-
preparation = undefined;
|
|
592
|
-
}
|
|
593
|
-
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
594
|
-
if (preparationFailure !== undefined) {
|
|
595
|
-
// Contract: README.md#Navigator-attendance — failed attendance is typed
|
|
596
|
-
// unavailable without invalidating the role Receipt; retain the cause.
|
|
597
|
-
report = unavailable(invocationId, preparationFailure);
|
|
598
|
-
} else {
|
|
599
|
-
const status = "status" in settlement ? settlement.status : undefined;
|
|
600
|
-
const picked = pickPreparedProse(preparedAdvice, status);
|
|
601
|
-
if (typeof picked === "string" && picked.trim() !== "") {
|
|
602
|
-
// #959 / #1160: present the pre-written prose as-is on every parent outcome
|
|
603
|
-
// including human_decision / escalate. No settlement model round.
|
|
604
|
-
report = { disposition: "advice", prose: picked };
|
|
605
|
-
} else {
|
|
606
|
-
// Missing key / empty prepare / no prior prepare → affirmative no-advice.
|
|
607
|
-
report = { disposition: "no-advice" };
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (observedRoutePlaybookFailure !== undefined) {
|
|
612
|
-
report = { ...report, routePlaybookReadFailure: observedRoutePlaybookFailure };
|
|
613
|
-
}
|
|
614
|
-
const event: NavigatorEvent = {
|
|
615
|
-
version: 1,
|
|
616
|
-
disposition: report.disposition,
|
|
617
|
-
invocationId,
|
|
618
|
-
role: options.role,
|
|
619
|
-
phase: options.phase,
|
|
620
|
-
subjectKey,
|
|
621
|
-
...(report.prose === undefined ? {} : { prose: report.prose }),
|
|
622
|
-
...(report.unavailableReason === undefined ? {} : { unavailableReason: report.unavailableReason }),
|
|
623
|
-
...(report.unavailableSource === undefined ? {} : { unavailableSource: report.unavailableSource }),
|
|
624
|
-
...(report.unavailableCause === undefined ? {} : { unavailableCause: report.unavailableCause }),
|
|
625
|
-
...(report.routePlaybookReadFailure === undefined ? {} : { routePlaybookReadFailure: report.routePlaybookReadFailure }),
|
|
626
|
-
...(report.arrivalMessage === undefined ? {} : { arrivalMessage: report.arrivalMessage }),
|
|
627
|
-
};
|
|
628
|
-
// Dispose during post-role grace must ignore late completion (ADR 0052 / #106 / #675).
|
|
629
|
-
// Every settled disposition (advice | no-advice | unavailable | arrival) is affirmative.
|
|
630
|
-
// Only the disposed bit is typed authority to drop — never match free-text Error.message.
|
|
631
|
-
if (disposed) return;
|
|
632
|
-
try {
|
|
633
|
-
await options.onEvent(event, report);
|
|
634
|
-
} catch (error) {
|
|
635
|
-
// Race: dispose landed between the check and onEvent; drop only then.
|
|
636
|
-
if (disposed) return;
|
|
637
|
-
throw error;
|
|
638
|
-
}
|
|
639
|
-
preparation = undefined;
|
|
640
|
-
sessionReady = undefined;
|
|
641
|
-
preparedAdvice = undefined;
|
|
642
|
-
preparationFailure = undefined;
|
|
643
|
-
observedRoutePlaybookFailure = undefined;
|
|
644
|
-
}
|
|
645
191
|
}
|
|
646
192
|
|
|
647
193
|
export type NavigatorAttendance = ReturnType<typeof createNavigatorAttendance>;
|
|
648
194
|
|
|
649
|
-
export function registerNavigatorModelCommand(pi: ExtensionAPI, path = navigatorModelSettingPath()): void {
|
|
650
|
-
pi.registerCommand("navigator-model", {
|
|
651
|
-
description: "Set the persistent Navigator model (provider/model[:max]).",
|
|
652
|
-
handler: async (args) => {
|
|
653
|
-
await writeNavigatorModelSetting(args.trim(), path);
|
|
654
|
-
},
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
|
|
658
195
|
export { subjectPath };
|