@akagilnc/pi-workflow-roles 0.1.5663 → 0.1.5671
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/dist/acp-host/production-host.js +180 -105
- package/dist/headless-host/production-host.js +201 -126
- package/dist/migrate-book-topology.js +11 -10
- package/dist/navigator-attendance.js +61 -78
- package/dist/navigator-public-session.js +17 -12
- package/dist/package-contracts/navigator-output.js +101 -15
- package/dist/public-cli/main.js +18 -10
- package/dist/public-cli/post-admission.js +9 -0
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/public-role-summons.js +1 -0
- package/package.json +1 -1
- package/resources/navigator-route-playbook.md +3 -3
- package/src/headless-host/description.ts +2 -1
- package/src/headless-host/role-turn-host.ts +18 -15
- package/src/host-contracts.ts +6 -0
- package/src/navigator-attendance.ts +149 -171
- package/src/navigator-public-session.ts +23 -16
- package/src/navigator-session-contracts.ts +0 -1
- package/src/package-contracts/navigator-output.ts +146 -20
- package/src/public-cli/post-admission.ts +11 -0
- package/src/public-cli/turn-request.ts +5 -0
- package/src/public-role-summons.ts +8 -0
- package/src/role-runtime.ts +3 -2
|
@@ -392,7 +392,7 @@ function terminalFromSpawned(
|
|
|
392
392
|
function buildTurnArgs(options: {
|
|
393
393
|
readonly description: HeadlessHostDescription;
|
|
394
394
|
readonly systemPromptPath: string;
|
|
395
|
-
/** Open schema; omit for #959 navigator prose-exit seats.
|
|
395
|
+
/** Open schema for Claude --json-schema; omit for #959 navigator prose-exit seats. */
|
|
396
396
|
readonly jsonSchema?: Readonly<Record<string, unknown>>;
|
|
397
397
|
readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
|
|
398
398
|
readonly mcpConfigPath?: string;
|
|
@@ -408,7 +408,7 @@ function buildTurnArgs(options: {
|
|
|
408
408
|
if (options.sessionKind === "resume" && !options.sessionId) {
|
|
409
409
|
throw new Error("codex resume requires a bound thread_id");
|
|
410
410
|
}
|
|
411
|
-
//
|
|
411
|
+
// Codex always mounts --output-schema when the path is supplied (incl. navigator byStatus #1160).
|
|
412
412
|
return codexTurnArgs({
|
|
413
413
|
systemPromptPath: options.systemPromptPath,
|
|
414
414
|
...(options.outputSchemaPath === undefined ? {} : { outputSchemaPath: options.outputSchemaPath }),
|
|
@@ -443,7 +443,12 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
443
443
|
return createSerializedRoleTurnHost(async (request): Promise<RoleTurnResult> => {
|
|
444
444
|
const prepared = await config.prepare(request);
|
|
445
445
|
const systemPrompt = renderSystemPromptOverride(prepared.systemPrompt);
|
|
446
|
-
|
|
446
|
+
const codex = isCodexExecDescription(config.description);
|
|
447
|
+
// #1160: one exit-mode decision per turn — direct navigator free prose vs structured.
|
|
448
|
+
// Attendance sets request.navigatorByStatusPrepare; direct ak-role navigator does not.
|
|
449
|
+
const navigatorProseExit =
|
|
450
|
+
prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME
|
|
451
|
+
&& request.navigatorByStatusPrepare !== true;
|
|
447
452
|
let outcome: RoleTurnResult = failure("session", "HeadlessNoOutcome", "no-outcome");
|
|
448
453
|
try {
|
|
449
454
|
// Claude mints a package UUID for --session-id; codex waits for thread.started.
|
|
@@ -485,7 +490,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
485
490
|
if (codex) {
|
|
486
491
|
// #1148: Codex --output-schema requires a native strict transport schema.
|
|
487
492
|
// Project from the unique open declaration; do not alter package receipt rules.
|
|
488
|
-
if (
|
|
493
|
+
if (!navigatorProseExit) {
|
|
489
494
|
outputSchemaPath = join(inputsDirectory, "output-schema.json");
|
|
490
495
|
deliveredSchema = closeJsonSchemaForCodex(prepared.jsonSchema);
|
|
491
496
|
await writeFile(outputSchemaPath, `${JSON.stringify(deliveredSchema, null, 2)}\n`, "utf8");
|
|
@@ -497,8 +502,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
497
502
|
`${JSON.stringify(headlessMcpConfigDocument(prepared.mcpServers), null, 2)}\n`,
|
|
498
503
|
"utf8",
|
|
499
504
|
);
|
|
500
|
-
|
|
501
|
-
if (prepared.terminatingToolName !== NAVIGATOR_OUTPUT_TOOL_NAME) deliveredSchema = prepared.jsonSchema;
|
|
505
|
+
if (!navigatorProseExit) deliveredSchema = prepared.jsonSchema;
|
|
502
506
|
}
|
|
503
507
|
startedWithSchema = deliveredSchema;
|
|
504
508
|
} catch (setupError) {
|
|
@@ -526,10 +530,8 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
526
530
|
args = buildTurnArgs({
|
|
527
531
|
description: config.description,
|
|
528
532
|
systemPromptPath,
|
|
529
|
-
//
|
|
530
|
-
...(prepared.
|
|
531
|
-
? {}
|
|
532
|
-
: { jsonSchema: prepared.jsonSchema }),
|
|
533
|
+
// Direct navigator prose exit omits --json-schema; byStatus prepare mounts it.
|
|
534
|
+
...(navigatorProseExit ? {} : { jsonSchema: prepared.jsonSchema }),
|
|
533
535
|
mcpServers: prepared.mcpServers,
|
|
534
536
|
...(mcpConfigPath === undefined ? {} : { mcpConfigPath }),
|
|
535
537
|
...(outputSchemaPath === undefined ? {} : { outputSchemaPath }),
|
|
@@ -665,14 +667,15 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
665
667
|
});
|
|
666
668
|
}
|
|
667
669
|
|
|
668
|
-
const navigator = prepared.terminatingToolName === NAVIGATOR_OUTPUT_TOOL_NAME;
|
|
669
670
|
const deliverReply = async (): Promise<ReturnType<typeof deliveredFromSpawned> | undefined> => {
|
|
670
671
|
if (!observation.turnCompleted || observation.finalMessage === undefined) return undefined;
|
|
671
|
-
if (
|
|
672
|
+
if (navigatorProseExit) {
|
|
673
|
+
// Direct ak-role navigator: free-form agent_message is the prose body.
|
|
672
674
|
if (observation.finalMessage.trim() === "") return undefined;
|
|
673
675
|
await prepared.ingestStructuredOutput({ prose: observation.finalMessage });
|
|
674
676
|
return deliveredFromSpawned(spawned);
|
|
675
677
|
}
|
|
678
|
+
// Structured seats (incl. #1160 byStatus prepare): native JSON receipt.
|
|
676
679
|
let receipt: unknown;
|
|
677
680
|
try {
|
|
678
681
|
receipt = JSON.parse(observation.finalMessage);
|
|
@@ -721,12 +724,12 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
|
|
|
721
724
|
if (envelope.structured_output !== undefined) {
|
|
722
725
|
await prepared.ingestStructuredOutput(envelope.structured_output);
|
|
723
726
|
} else if (
|
|
724
|
-
|
|
727
|
+
navigatorProseExit
|
|
725
728
|
&& typeof envelope.result === "string"
|
|
726
729
|
&& envelope.result.trim() !== ""
|
|
727
730
|
) {
|
|
728
|
-
// #959:
|
|
729
|
-
// when structured_output is absent (json-schema omitted
|
|
731
|
+
// #959: direct navigator prose exit — free-form result text is the body
|
|
732
|
+
// when structured_output is absent (json-schema omitted).
|
|
730
733
|
await prepared.ingestStructuredOutput({ prose: envelope.result });
|
|
731
734
|
}
|
|
732
735
|
return deliveredFromSpawned(spawned);
|
package/src/host-contracts.ts
CHANGED
|
@@ -193,6 +193,12 @@ export type RoleTurnRequest = {
|
|
|
193
193
|
readonly host?: string;
|
|
194
194
|
/** Station child role run (#840): omit automatic navigator attendance. */
|
|
195
195
|
readonly stationChild?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* #1160 attendance auto-prepare: host mounts native structured schema so
|
|
198
|
+
* byStatus returns as structured fields. Direct `ak-role navigator` omits
|
|
199
|
+
* this and keeps the free-form prose exit (#959).
|
|
200
|
+
*/
|
|
201
|
+
readonly navigatorByStatusPrepare?: boolean;
|
|
196
202
|
/**
|
|
197
203
|
* #1132: the effective delivery-request ceiling resolved once by the caller
|
|
198
204
|
* from the single configured `autoResumeLimit` value and projected here, so
|
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import { type Static } from "typebox";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
NAVIGATOR_INVOCATION_ENTRY,
|
|
@@ -60,7 +60,14 @@ export { createNativeNavigatorSessionFactory };
|
|
|
60
60
|
export { resolveNavigatorSeatSelection };
|
|
61
61
|
import { issueRoot, subjectPath } from "./work-subject-identity.ts";
|
|
62
62
|
import { createReceiptDeliveryPolicy } from "./receipt-delivery-policy.ts";
|
|
63
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
byStatusToPlainObject,
|
|
65
|
+
mergePreparedAdvice,
|
|
66
|
+
navigatorAdviceBodySchema,
|
|
67
|
+
pickPreparedProse,
|
|
68
|
+
preparedAdviceFromUnknown,
|
|
69
|
+
type PreparedNavigatorAdvice,
|
|
70
|
+
} from "./package-contracts/navigator-output.ts";
|
|
64
71
|
import { sha256Hex } from "./sha256.ts";
|
|
65
72
|
import { isRecord } from "./unknown-value.ts";
|
|
66
73
|
|
|
@@ -133,13 +140,10 @@ export type NavigatorEvent = {
|
|
|
133
140
|
arrivalMessage?: string;
|
|
134
141
|
};
|
|
135
142
|
|
|
136
|
-
// #959: prepare
|
|
137
|
-
// never a gate — every object root
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
description: "游奕使散文建议,原样呈现。不要求 candidates/next 结构。",
|
|
141
|
-
})),
|
|
142
|
-
}, { additionalProperties: true });
|
|
143
|
+
// #959 / #1160: prepare reuses the single navigator advice field owner.
|
|
144
|
+
// Object root only (ADR 0060); nested shape is never a gate — every object root
|
|
145
|
+
// reaches execute exactly once (Rule 0).
|
|
146
|
+
const prepareSchema = navigatorAdviceBodySchema;
|
|
143
147
|
type PrepareOutput = Static<typeof prepareSchema>;
|
|
144
148
|
|
|
145
149
|
export type NavigatorAttendanceOptions = {
|
|
@@ -200,13 +204,13 @@ function rejectedPrepareReason(entries: readonly unknown[], start: number): stri
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/**
|
|
203
|
-
* #959: extract
|
|
204
|
-
* Never a rejection — shape is not an admission gate.
|
|
205
|
-
* Production
|
|
207
|
+
* #959 / #1160: extract prepared advice from any prepare submission shape.
|
|
208
|
+
* Missing/empty → undefined. Never a rejection — shape is not an admission gate.
|
|
209
|
+
* Production payload arrives via nested public summon → prepare tool.execute only;
|
|
206
210
|
* archivist entries() never carries assistant message text, so no parallel harvest.
|
|
207
211
|
*/
|
|
208
|
-
function
|
|
209
|
-
return
|
|
212
|
+
function normalizePreparedAdvice(value: unknown): PreparedNavigatorAdvice | undefined {
|
|
213
|
+
return preparedAdviceFromUnknown(value);
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
export function navigatorSubjectKey(
|
|
@@ -268,7 +272,7 @@ export function formatNavigatorReport(report: NavigatorReport): string {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
271
|
-
let preparation: Promise<
|
|
275
|
+
let preparation: Promise<PreparedNavigatorAdvice | undefined> | undefined;
|
|
272
276
|
let preparationInFlight = false;
|
|
273
277
|
let sessionReady: Promise<NavigatorPreparationSession> | undefined;
|
|
274
278
|
let session: NavigatorPreparationSession | undefined;
|
|
@@ -276,7 +280,8 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
276
280
|
let subject = options.subject;
|
|
277
281
|
let authority = options.authority;
|
|
278
282
|
let contextError = options.contextError;
|
|
279
|
-
|
|
283
|
+
/** Parallel-prepare result: status-keyed prose picked at settle (#1160). */
|
|
284
|
+
let preparedAdvice: PreparedNavigatorAdvice | undefined;
|
|
280
285
|
// Shared lifecycle owns the principal when supplied; otherwise mint once per attendance.
|
|
281
286
|
const invocationPrincipal = options.invocationId ?? mintNavigatorInvocationId();
|
|
282
287
|
let activeInvocationId: string | undefined = invocationPrincipal;
|
|
@@ -311,11 +316,10 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
311
316
|
};
|
|
312
317
|
};
|
|
313
318
|
/**
|
|
314
|
-
*
|
|
319
|
+
* #1160: prepare runs the model in parallel from parent start and stores byStatus.
|
|
320
|
+
* settle picks by settlement.status — no second model round.
|
|
315
321
|
* Soul and route playbook stay on the navigator system prompt, not this user turn.
|
|
316
322
|
*/
|
|
317
|
-
let prepareBoundSettlement: NavigatorSettlement | undefined;
|
|
318
|
-
/** Session only — no host prompt (standby attendance, or cold settle book-before-feed). */
|
|
319
323
|
const loadMaterialsAndSession = async (invocationId: string): Promise<NavigatorPreparationSession> => {
|
|
320
324
|
if (contextError !== undefined) throw navigatorUnavailableError("context", contextError);
|
|
321
325
|
if (typeof authority !== "string" || authority.trim() === "") {
|
|
@@ -384,133 +388,127 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
384
388
|
if (session === undefined) throw new Error("Navigator session was not created");
|
|
385
389
|
return session;
|
|
386
390
|
};
|
|
387
|
-
const prepare = async (
|
|
391
|
+
const prepare = async (): Promise<PreparedNavigatorAdvice | undefined> => {
|
|
388
392
|
// Exact principal is owned by shared lifecycle (or one mint per attendance).
|
|
389
393
|
// Model/tool/advice paths cannot override it; role-session persistence is
|
|
390
394
|
// pi.appendEntry at lifecycle start — not optional sessionManager probing.
|
|
391
|
-
const boundSettlement = prepareBoundSettlement;
|
|
392
|
-
prepareBoundSettlement = undefined;
|
|
393
395
|
const invocationId = invocationPrincipal;
|
|
394
396
|
activeInvocationId = invocationId;
|
|
395
397
|
let output: PrepareOutput | undefined;
|
|
396
398
|
let prepareBatchRejected = false;
|
|
397
399
|
outputSink = (value) => {
|
|
398
|
-
// #836: extra prepare calls
|
|
400
|
+
// #836 / #1160: extra prepare calls merge byStatus (same key concatenates) and prose.
|
|
401
|
+
const next = normalizePreparedAdvice(value);
|
|
399
402
|
if (output === undefined) {
|
|
400
403
|
output = value;
|
|
401
404
|
return;
|
|
402
405
|
}
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
406
|
+
const merged = mergePreparedAdvice(normalizePreparedAdvice(output), next);
|
|
407
|
+
output = {
|
|
408
|
+
...(merged?.prose === undefined ? {} : { prose: merged.prose }),
|
|
409
|
+
...(merged === undefined || merged.byStatus.size === 0
|
|
410
|
+
? {}
|
|
411
|
+
: { byStatus: byStatusToPlainObject(merged.byStatus) }),
|
|
412
|
+
};
|
|
407
413
|
};
|
|
408
|
-
const activeSession =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const activeSession = await loadMaterialsAndSession(invocationId);
|
|
415
|
+
// #1160: parallel prepare from parent start — model runs now; settle only picks.
|
|
416
|
+
// Pass already-loaded subject/authority so the prepare run can judge progress
|
|
417
|
+
// (ticket text lives in those materials when present — no path invention).
|
|
418
|
+
const request = JSON.stringify({
|
|
419
|
+
kind: "prepare",
|
|
420
|
+
role: options.role,
|
|
421
|
+
phase: options.phase,
|
|
422
|
+
invocationId,
|
|
423
|
+
subjectKey,
|
|
424
|
+
subject,
|
|
425
|
+
authority,
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
416
428
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
promptFailure = error;
|
|
432
|
-
}
|
|
433
|
-
const providerFailure = activeSession.providerFailure?.();
|
|
434
|
-
if (providerFailure !== undefined) {
|
|
435
|
-
throw navigatorUnavailableError(providerFailure.source, promptFailure ?? "Navigator provider failure", providerFailure.cause);
|
|
436
|
-
}
|
|
437
|
-
const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
|
|
438
|
-
if (rejectedReason !== undefined) {
|
|
439
|
-
// A rejected call makes every provisional output from this prompt
|
|
440
|
-
// ineligible for publication before the correction turn starts.
|
|
441
|
-
output = undefined;
|
|
442
|
-
prepareBatchRejected = true;
|
|
443
|
-
// The correction prompt below is the one spend for this rejection.
|
|
444
|
-
delivery.recordRejected(rejectedReason);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (promptFailure !== undefined) throw promptFailure;
|
|
448
|
-
const sessionNoReceipt = activeSession.noReceipt?.();
|
|
449
|
-
if (sessionNoReceipt !== undefined) {
|
|
450
|
-
// This session already settled without an accepted receipt. Keep the
|
|
451
|
-
// larger issued count and stop; a nested zero must not wipe prompts
|
|
452
|
-
// this layer already sent (#675 / #1132).
|
|
453
|
-
delivery.recordNestedNoReceipt(sessionNoReceipt);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
await promptAllowingRejectedPrepare(request);
|
|
458
|
-
// Bound output only: correction after rejected prepare. Early ready-wait
|
|
459
|
-
// does not 催交 final advice (owner: prepare then wait for settlement feed).
|
|
460
|
-
if (boundSettlement !== undefined) {
|
|
461
|
-
while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
|
|
462
|
-
delivery.recordDeliveryRequest();
|
|
463
|
-
await promptAllowingRejectedPrepare(JSON.stringify(delivery.deliveryState()));
|
|
464
|
-
}
|
|
465
|
-
if (output === undefined && delivery.nextAction() === "request-delivery") {
|
|
466
|
-
delivery.closeBudget();
|
|
467
|
-
}
|
|
468
|
-
if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
|
|
469
|
-
// Nested @navigator run already holds lawful no_receipt + runPointer when
|
|
470
|
-
// present. Do not write a parallel parent lifecycle off a dead side-branch
|
|
471
|
-
// pointer (#1178). 催交 / nested settle / no-advice delivery stay above.
|
|
472
|
-
preparedProse = undefined;
|
|
473
|
-
return undefined;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
} catch (error) {
|
|
477
|
-
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
|
|
478
|
-
}
|
|
479
|
-
if (output === undefined) {
|
|
480
|
-
// Early host round: ready and wait — no final prose until settlement is fed.
|
|
481
|
-
if (boundSettlement === undefined) {
|
|
482
|
-
preparedProse = undefined;
|
|
483
|
-
return undefined;
|
|
429
|
+
if (disposed) throw navigatorUnavailableError("session", new Error("Navigator attendance was disposed"));
|
|
430
|
+
// #1132: the ceiling this turn already resolved. Absent = package default.
|
|
431
|
+
const delivery = createReceiptDeliveryPolicy(options.deliveryRequestLimit);
|
|
432
|
+
// Production payload arrives only via nested summon → prepare tool.execute
|
|
433
|
+
// (navigator-public-session). No assistant-entry harvest — entries() is
|
|
434
|
+
// archivist custom-only on the wired factory (#959).
|
|
435
|
+
const promptAllowingRejectedPrepare = async (text: string) => {
|
|
436
|
+
const entryStart = activeSession.entries().length;
|
|
437
|
+
prepareBatchRejected = false;
|
|
438
|
+
let promptFailure: unknown;
|
|
439
|
+
try {
|
|
440
|
+
await activeSession.prompt(text);
|
|
441
|
+
} catch (error) {
|
|
442
|
+
promptFailure = error;
|
|
484
443
|
}
|
|
485
|
-
const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
|
|
486
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) return false;
|
|
487
|
-
return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
|
|
488
|
-
});
|
|
489
|
-
const nativeMessage = isRecord(nativeFailure) && isRecord(nativeFailure.message) ? nativeFailure.message : undefined;
|
|
490
|
-
const errorMessage = nativeMessage !== undefined && typeof nativeMessage.errorMessage === "string"
|
|
491
|
-
? nativeMessage.errorMessage
|
|
492
|
-
: "Navigator did not submit direction advice";
|
|
493
|
-
// Classification originates only at the native provider stream seam.
|
|
494
|
-
// AssistantMessage metadata is a human diagnostic surface, not an acceptance oracle.
|
|
495
444
|
const providerFailure = activeSession.providerFailure?.();
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
445
|
+
if (providerFailure !== undefined) {
|
|
446
|
+
throw navigatorUnavailableError(providerFailure.source, promptFailure ?? "Navigator provider failure", providerFailure.cause);
|
|
447
|
+
}
|
|
448
|
+
const rejectedReason = rejectedPrepareReason(activeSession.entries(), entryStart);
|
|
449
|
+
if (rejectedReason !== undefined) {
|
|
450
|
+
// A rejected call makes every provisional output from this prompt
|
|
451
|
+
// ineligible for publication before the correction turn starts.
|
|
452
|
+
output = undefined;
|
|
453
|
+
prepareBatchRejected = true;
|
|
454
|
+
// The correction prompt below is the one spend for this rejection.
|
|
455
|
+
delivery.recordRejected(rejectedReason);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (promptFailure !== undefined) throw promptFailure;
|
|
459
|
+
const sessionNoReceipt = activeSession.noReceipt?.();
|
|
460
|
+
if (sessionNoReceipt !== undefined) {
|
|
461
|
+
// This session already settled without an accepted receipt. Keep the
|
|
462
|
+
// larger issued count and stop; a nested zero must not wipe prompts
|
|
463
|
+
// this layer already sent (#675 / #1132).
|
|
464
|
+
delivery.recordNestedNoReceipt(sessionNoReceipt);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
await promptAllowingRejectedPrepare(request);
|
|
469
|
+
// Correction after rejected prepare on the sole model round (#1160).
|
|
470
|
+
while (output === undefined && prepareBatchRejected && delivery.nextAction() === "request-delivery") {
|
|
471
|
+
delivery.recordDeliveryRequest();
|
|
472
|
+
await promptAllowingRejectedPrepare(JSON.stringify(delivery.deliveryState()));
|
|
473
|
+
}
|
|
474
|
+
if (output === undefined && delivery.nextAction() === "request-delivery") {
|
|
475
|
+
delivery.closeBudget();
|
|
499
476
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
477
|
+
if (output === undefined && delivery.nextAction() === "no-receipt" && activeSession.providerFailure?.() === undefined) {
|
|
478
|
+
// Nested @navigator run already holds lawful no_receipt + runPointer when
|
|
479
|
+
// present. Do not write a parallel parent lifecycle off a dead side-branch
|
|
480
|
+
// pointer (#1178). Honest empty prep → settle reports no-advice.
|
|
481
|
+
preparedAdvice = undefined;
|
|
503
482
|
return undefined;
|
|
504
483
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
484
|
+
} catch (error) {
|
|
485
|
+
throw error instanceof NavigatorUnavailableError ? error : navigatorUnavailableError("transport", error);
|
|
486
|
+
}
|
|
487
|
+
if (output === undefined) {
|
|
488
|
+
const nativeFailure = [...activeSession.entries()].reverse().find((entry: unknown) => {
|
|
489
|
+
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) return false;
|
|
490
|
+
return entry.message.role === "assistant" && typeof entry.message.errorMessage === "string" && entry.message.errorMessage.trim() !== "";
|
|
491
|
+
});
|
|
492
|
+
const nativeMessage = isRecord(nativeFailure) && isRecord(nativeFailure.message) ? nativeFailure.message : undefined;
|
|
493
|
+
const errorMessage = nativeMessage !== undefined && typeof nativeMessage.errorMessage === "string"
|
|
494
|
+
? nativeMessage.errorMessage
|
|
495
|
+
: "Navigator did not submit direction advice";
|
|
496
|
+
// Classification originates only at the native provider stream seam.
|
|
497
|
+
// AssistantMessage metadata is a human diagnostic surface, not an acceptance oracle.
|
|
498
|
+
const providerFailure = activeSession.providerFailure?.();
|
|
499
|
+
const source = providerFailure?.source ?? "unknown";
|
|
500
|
+
const cause = providerFailure?.cause ?? source;
|
|
501
|
+
throw navigatorUnavailableError(source, errorMessage, cause);
|
|
513
502
|
}
|
|
503
|
+
preparedAdvice = normalizePreparedAdvice(output);
|
|
504
|
+
return preparedAdvice;
|
|
505
|
+
} finally {
|
|
506
|
+
const playbookFailure = activeSession.routePlaybookReadFailure?.();
|
|
507
|
+
observedRoutePlaybookFailure = typeof playbookFailure === "string" && playbookFailure.trim() !== ""
|
|
508
|
+
? playbookFailure
|
|
509
|
+
: undefined;
|
|
510
|
+
outputSink = undefined;
|
|
511
|
+
}
|
|
514
512
|
};
|
|
515
513
|
|
|
516
514
|
return {
|
|
@@ -530,8 +528,9 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
530
528
|
prepare(): void {
|
|
531
529
|
if (disposed || preparation !== undefined) return;
|
|
532
530
|
preparationFailure = undefined;
|
|
531
|
+
preparedAdvice = undefined;
|
|
533
532
|
// Track live in-flight only — a resolved preparation still held until settle
|
|
534
|
-
// drain must not read as preparing (test helpers release
|
|
533
|
+
// drain must not read as preparing (test helpers release by this).
|
|
535
534
|
preparationInFlight = true;
|
|
536
535
|
preparation = prepare().finally(() => {
|
|
537
536
|
preparationInFlight = false;
|
|
@@ -575,22 +574,9 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
575
574
|
async function settleOnce(settlement: NavigatorSettlement): Promise<void> {
|
|
576
575
|
// Dispose during post-role grace must ignore late completion entirely (#675).
|
|
577
576
|
if (disposed) return;
|
|
578
|
-
|
|
577
|
+
// Keep playbook failure observed on the prepare round; settle does not re-prompt (#1160).
|
|
579
578
|
const invocationId = activeInvocationId ?? invocationPrincipal;
|
|
580
579
|
let report: NavigatorReport;
|
|
581
|
-
// Drain in-flight standby attendance (record only). Then feed the typed
|
|
582
|
-
// settlement as the only model round on the same host session.
|
|
583
|
-
if (sessionReady !== undefined) {
|
|
584
|
-
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
585
|
-
}
|
|
586
|
-
if (preparation !== undefined) {
|
|
587
|
-
try {
|
|
588
|
-
await preparation;
|
|
589
|
-
} catch (error) {
|
|
590
|
-
preparationFailure ??= error;
|
|
591
|
-
}
|
|
592
|
-
preparation = undefined;
|
|
593
|
-
}
|
|
594
580
|
const settlementFact = {
|
|
595
581
|
invocationId,
|
|
596
582
|
subjectKey,
|
|
@@ -601,9 +587,11 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
601
587
|
? { status: settlement.status }
|
|
602
588
|
: {}),
|
|
603
589
|
};
|
|
604
|
-
// Arrival is presentation-only: never
|
|
605
|
-
let drainedProse: string | undefined;
|
|
590
|
+
// Arrival is presentation-only: wait only for nest open, never for the model round.
|
|
606
591
|
if (settlement.kind === "arrival") {
|
|
592
|
+
if (sessionReady !== undefined) {
|
|
593
|
+
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
594
|
+
}
|
|
607
595
|
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
608
596
|
if (preparationFailure !== undefined) {
|
|
609
597
|
report = unavailable(invocationId, preparationFailure);
|
|
@@ -614,45 +602,34 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
614
602
|
};
|
|
615
603
|
}
|
|
616
604
|
} else {
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (session !== undefined) preparationFailure = undefined;
|
|
621
|
-
// Cold settle: session only (no unbound host prompt), then book + feed.
|
|
622
|
-
// Pass the session into prepare so setModel/INVOCATION run once.
|
|
623
|
-
let coldSession: NavigatorPreparationSession | undefined;
|
|
624
|
-
if (session === undefined && preparationFailure === undefined) {
|
|
625
|
-
try {
|
|
626
|
-
coldSession = await loadMaterialsAndSession(invocationId);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
preparationFailure = error;
|
|
629
|
-
}
|
|
605
|
+
// Drain in-flight parallel prepare, then pick by status — no second model round (#1160).
|
|
606
|
+
if (sessionReady !== undefined) {
|
|
607
|
+
try { await sessionReady; } catch (error) { preparationFailure ??= error; }
|
|
630
608
|
}
|
|
631
|
-
|
|
632
|
-
if (preparationFailure === undefined) {
|
|
633
|
-
prepareBoundSettlement = settlement;
|
|
634
|
-
preparation = prepare(coldSession);
|
|
635
|
-
void preparation.catch((error) => { preparationFailure = error; });
|
|
609
|
+
if (preparation !== undefined) {
|
|
636
610
|
try {
|
|
637
|
-
|
|
611
|
+
await preparation;
|
|
638
612
|
} catch (error) {
|
|
639
613
|
preparationFailure ??= error;
|
|
640
614
|
}
|
|
641
615
|
preparation = undefined;
|
|
642
616
|
}
|
|
617
|
+
session?.appendEntry(SETTLEMENT_ENTRY, settlementFact);
|
|
643
618
|
if (preparationFailure !== undefined) {
|
|
644
619
|
// Contract: README.md#Navigator-attendance — failed attendance is typed
|
|
645
620
|
// unavailable without invalidating the role Receipt; retain the cause.
|
|
646
621
|
report = unavailable(invocationId, preparationFailure);
|
|
647
|
-
} else if (typeof drainedProse === "string" && drainedProse.trim() !== "") {
|
|
648
|
-
// #959: present prose as-is on every parent outcome — including
|
|
649
|
-
// human_decision / escalate. Old path wiped prose on escalate and left
|
|
650
|
-
// auto-attendance looking empty after a successful nested summon.
|
|
651
|
-
// Prior words stay on the host session (CLI resume); no package advice ledger.
|
|
652
|
-
report = { disposition: "advice", prose: drainedProse };
|
|
653
622
|
} else {
|
|
654
|
-
|
|
655
|
-
|
|
623
|
+
const status = "status" in settlement ? settlement.status : undefined;
|
|
624
|
+
const picked = pickPreparedProse(preparedAdvice, status);
|
|
625
|
+
if (typeof picked === "string" && picked.trim() !== "") {
|
|
626
|
+
// #959 / #1160: present the pre-written prose as-is on every parent outcome
|
|
627
|
+
// including human_decision / escalate. No settlement model round.
|
|
628
|
+
report = { disposition: "advice", prose: picked };
|
|
629
|
+
} else {
|
|
630
|
+
// Missing key / empty prepare / no prior prepare → affirmative no-advice.
|
|
631
|
+
report = { disposition: "no-advice" };
|
|
632
|
+
}
|
|
656
633
|
}
|
|
657
634
|
}
|
|
658
635
|
if (observedRoutePlaybookFailure !== undefined) {
|
|
@@ -685,8 +662,9 @@ export function createNavigatorAttendance(options: NavigatorAttendanceOptions) {
|
|
|
685
662
|
}
|
|
686
663
|
preparation = undefined;
|
|
687
664
|
sessionReady = undefined;
|
|
688
|
-
|
|
665
|
+
preparedAdvice = undefined;
|
|
689
666
|
preparationFailure = undefined;
|
|
667
|
+
observedRoutePlaybookFailure = undefined;
|
|
690
668
|
}
|
|
691
669
|
}
|
|
692
670
|
|