@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.
@@ -6,7 +6,7 @@
6
6
  * no books/<book>/navigator/<hash>/ side-branch nest, work-context file,
7
7
  * current-session pointer, or attendance parallel ledger (#1178).
8
8
  * Host conversation continuity uses CLI resume (`resumeRunId`) within this
9
- * attendance lifetime (in-memory pointer); cross-turn design is #1160.
9
+ * attendance lifetime (in-memory pointer). Parallel byStatus prepare is #1160.
10
10
  */
11
11
  import { basename } from "node:path";
12
12
 
@@ -81,6 +81,8 @@ export type NavigatorPublicSummon = (options: {
81
81
  readonly resumeRunId?: string;
82
82
  /** Shared-lifecycle cancel forwarded from HostContext.signal (#675 / #959). */
83
83
  readonly signal?: AbortSignal;
84
+ /** #1160 attendance auto byStatus prepare — host mounts structured schema. */
85
+ readonly navigatorByStatusPrepare?: boolean;
84
86
  }) => Promise<PublicSummonResult>;
85
87
 
86
88
  type MemoryEntry = {
@@ -98,7 +100,6 @@ export function createNativeNavigatorSessionFactory(deps?: {
98
100
  return async ({ context, tool }) => {
99
101
  // Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
100
102
  // Factory open only holds in-memory attendance bookkeeping — no side-branch nest.
101
- let thinkingLevel: string | undefined;
102
103
  const entries: MemoryEntry[] = [];
103
104
  let providerFailure: NavigatorProviderFailureFact | undefined;
104
105
  let noReceipt: Partial<NoReceiptLifecycleFacts> | undefined;
@@ -137,6 +138,9 @@ export function createNativeNavigatorSessionFactory(deps?: {
137
138
  role: "navigator" as const,
138
139
  argv: [text] as const,
139
140
  cwd: context.cwd,
141
+ // Attendance prepare always asks for structured byStatus (#1160).
142
+ // Direct `ak-role navigator` never sets this flag.
143
+ navigatorByStatusPrepare: true as const,
140
144
  ...(summonHome === undefined ? {} : { home: summonHome }),
141
145
  ...(context.signal === undefined ? {} : { signal: context.signal }),
142
146
  };
@@ -222,18 +226,24 @@ export function createNativeNavigatorSessionFactory(deps?: {
222
226
  // Not a shape-unusable judgment on the navigator reply (#757).
223
227
  return;
224
228
  }
225
- // #959: present navigator words as prose. No candidates/next parsing.
226
- const { navigatorProseFromUnknown } = await import("./package-contracts/navigator-output.ts");
227
- const proseParts: string[] = [];
229
+ // #959 / #1160: pass through prose and byStatus. No route parsing.
230
+ const {
231
+ byStatusToPlainObject,
232
+ mergePreparedAdvice,
233
+ preparedAdviceFromUnknown,
234
+ } = await import("./package-contracts/navigator-output.ts");
235
+ let prepared = undefined as ReturnType<typeof preparedAdviceFromUnknown>;
228
236
  for (const payload of outcome.payloads ?? []) {
229
- const prose = navigatorProseFromUnknown(payload);
230
- if (prose !== undefined) proseParts.push(prose);
237
+ prepared = mergePreparedAdvice(prepared, preparedAdviceFromUnknown(payload));
231
238
  }
232
- if (proseParts.length === 0) return;
239
+ if (prepared === undefined) return;
233
240
  if (disposed) return;
234
241
  await tool.execute(
235
242
  "navigator-public-prepare",
236
- { prose: proseParts.join("\n\n") },
243
+ {
244
+ ...(prepared.prose === undefined ? {} : { prose: prepared.prose }),
245
+ ...(prepared.byStatus.size === 0 ? {} : { byStatus: byStatusToPlainObject(prepared.byStatus) }),
246
+ },
237
247
  undefined,
238
248
  undefined,
239
249
  context as never,
@@ -264,18 +274,15 @@ export function createNativeNavigatorSessionFactory(deps?: {
264
274
  });
265
275
  },
266
276
  entries: () => entries,
267
- setModel: async (next, nextThinking) => {
268
- let nextParsed: ReturnType<typeof parseNavigatorModelSetting>;
277
+ setModel: async (next) => {
278
+ // Validate only — resume/fresh mint both read the live seat table
279
+ // (#675 / #617 DK-3). No test-only thinking observation state (#1160 F1).
269
280
  try {
270
- nextParsed = parseNavigatorModelSetting(next);
281
+ parseNavigatorModelSetting(next);
271
282
  } catch (error) {
272
283
  throw navigatorUnavailableError("model", error);
273
284
  }
274
- // Resume and fresh mint both read the live seat table (#675 / #617 DK-3).
275
- // A seat edit between prepares applies on the next host turn.
276
- thinkingLevel = nextThinking ?? nextParsed.thinkingLevel;
277
285
  },
278
- getThinkingLevel: () => thinkingLevel,
279
286
  dispose: async () => {
280
287
  // Marker only — nested cancel and non-blocking teardown are owned by
281
288
  // navigator-attendance / role-runtime (ADR 0018 / #959).
@@ -203,7 +203,6 @@ export type NavigatorPreparationSession = {
203
203
  /** Native message from the nested playbook read, when that read failed. */
204
204
  routePlaybookReadFailure?(): string | undefined;
205
205
  setModel?(model: string, thinkingLevel?: string): Promise<void>;
206
- getThinkingLevel?(): string | undefined;
207
206
  dispose(): void | Promise<void>;
208
207
  };
209
208
 
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Public Navigator (游奕使) terminating receipt contracts (#639 / #959).
3
- * Navigator speaks free-form prose. The tool is only a lifecycle vehicle so the
4
- * host can seal a turn; the model-facing content is prose, not structured route
5
- * candidates. Code does not parse, rank, or judge the advice.
2
+ * Public Navigator (游奕使) terminating receipt contracts (#639 / #959 / #1160).
3
+ * Advice body is free-form prose. Status association for parallel prepare lives on
4
+ * `byStatus` — code picks by the settlement status field only (owner 3188b502).
5
+ * Code does not parse, rank, or judge advice semantics.
6
6
  * No usable result is infrastructure failure via public settlement, not a judgment status (#475).
7
7
  */
8
8
  import { Type } from "typebox";
@@ -13,20 +13,75 @@ import { withTerminatingOutputDeclarations } from "./terminating-infrastructure.
13
13
 
14
14
  export const NAVIGATOR_OUTPUT_TOOL_NAME = "ak_navigator_output";
15
15
 
16
- export const navigatorOutputSchema = withTerminatingOutputDeclarations(
17
- openToolObject(
18
- Type.Object({
19
- prose: Type.Unknown({
20
- description: "游奕使散文建议,原样呈现。不要求 candidates/next 等结构化字段。",
21
- }),
22
- }),
23
- ),
24
- );
16
+ /**
17
+ * Single owner of prose / byStatus field declarations (#1160).
18
+ * Output tool and attendance prepare tool both compose from this.
19
+ */
20
+ export const navigatorAdviceFields = {
21
+ prose: Type.Unknown({
22
+ description: "单条散文建议。无状态分叉时使用;原样呈现。",
23
+ }),
24
+ byStatus: Type.Unknown({
25
+ description:
26
+ "按主衙门结局 status 预写的散文建议。键为 status 字面量,值为建议正文。"
27
+ + "结算时代码只按实际 status 取对应原文,不解析建议语义。",
28
+ }),
29
+ } as const;
30
+
31
+ /** Open tool object for prose/byStatus (attendance prepare + public output body). */
32
+ export const navigatorAdviceBodySchema = openToolObject(Type.Object(navigatorAdviceFields));
33
+
34
+ export const navigatorOutputSchema = withTerminatingOutputDeclarations(navigatorAdviceBodySchema);
25
35
 
26
36
  export type NavigatorAdvice = { readonly prose: string };
27
37
 
38
+ /** Parallel-prepare payload: status-keyed prose plus optional single prose. */
39
+ export type PreparedNavigatorAdvice = {
40
+ /** Map keeps opaque status keys (including "__proto__") as own entries. */
41
+ readonly byStatus: ReadonlyMap<string, string>;
42
+ readonly prose?: string;
43
+ };
44
+
45
+ /** Serialize structured bodies with JSON; never String(object) → [object Object]. */
46
+ function nonEmptyProse(value: unknown): string | undefined {
47
+ if (typeof value === "string") {
48
+ return value.trim() === "" ? undefined : value;
49
+ }
50
+ if (value === undefined || value === null) return undefined;
51
+ if (typeof value === "object") {
52
+ // No catch: serialization failure must surface (failure honesty).
53
+ const text = JSON.stringify(value);
54
+ if (text === undefined || text.trim() === "" || text === "{}" || text === "[]") return undefined;
55
+ return text;
56
+ }
57
+ const text = String(value);
58
+ return text.trim() === "" ? undefined : text;
59
+ }
60
+
61
+ function setStatusProse(target: Map<string, string>, key: string, prose: string): void {
62
+ const prior = target.get(key);
63
+ target.set(key, prior === undefined ? prose : `${prior}\n\n${prose}`);
64
+ }
65
+
66
+ function byStatusFromUnknown(value: unknown): Map<string, string> {
67
+ const out = new Map<string, string>();
68
+ if (!isRecord(value)) return out;
69
+ for (const [key, raw] of Object.entries(value)) {
70
+ const prose = nonEmptyProse(raw);
71
+ if (prose !== undefined) out.set(key, prose);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** Plain object for tool.execute / host payloads; Object.fromEntries keeps opaque keys. */
77
+ export function byStatusToPlainObject(
78
+ byStatus: ReadonlyMap<string, string>,
79
+ ): Record<string, string> {
80
+ return Object.fromEntries(byStatus);
81
+ }
82
+
28
83
  /**
29
- * Project one Navigator advice receipt as prose.
84
+ * Project one Navigator advice receipt as prose (display / single-advice path).
30
85
  * Accepts { prose }, a bare string, or any object — objects stringify as the
31
86
  * prose body so historical / free-form submissions still present. Shape is not
32
87
  * an admission gate (ADR 0055 / 仓内 CLAUDE.md 开篇 / #959).
@@ -37,16 +92,13 @@ export function projectLawfulNavigatorOutput(value: unknown): NavigatorAdvice |
37
92
  if (isRecord(value)) {
38
93
  if (typeof value.prose === "string") return { prose: value.prose };
39
94
  if (value.prose !== undefined && value.prose !== null) {
40
- return { prose: typeof value.prose === "string" ? value.prose : String(value.prose) };
95
+ const serialized = nonEmptyProse(value.prose);
96
+ if (serialized !== undefined) return { prose: serialized };
41
97
  }
42
98
  // Free-form object submission: present the whole body as prose when it has content.
43
99
  const keys = Object.keys(value);
44
100
  if (keys.length === 0) return { prose: "" };
45
- try {
46
- return { prose: JSON.stringify(value) };
47
- } catch {
48
- return { prose: String(value) };
49
- }
101
+ return { prose: JSON.stringify(value) };
50
102
  }
51
103
  return { prose: String(value) };
52
104
  }
@@ -69,3 +121,77 @@ export function navigatorProseFromUnknown(value: unknown): string | undefined {
69
121
  const trimmed = projected.prose.trim();
70
122
  return trimmed === "" ? undefined : projected.prose;
71
123
  }
124
+
125
+ /**
126
+ * Extract parallel-prepare advice. Empty / missing → undefined.
127
+ * Never a rejection — shape is not an admission gate.
128
+ */
129
+ export function preparedAdviceFromUnknown(value: unknown): PreparedNavigatorAdvice | undefined {
130
+ if (value === undefined || value === null) return undefined;
131
+ if (typeof value === "string") {
132
+ const prose = nonEmptyProse(value);
133
+ return prose === undefined ? undefined : { byStatus: new Map(), prose };
134
+ }
135
+ if (!isRecord(value)) {
136
+ const prose = nonEmptyProse(value);
137
+ return prose === undefined ? undefined : { byStatus: new Map(), prose };
138
+ }
139
+ const byStatus = byStatusFromUnknown(value.byStatus);
140
+ const hasProseField = Object.prototype.hasOwnProperty.call(value, "prose");
141
+ const hasByStatusField = Object.prototype.hasOwnProperty.call(value, "byStatus");
142
+ const prose = nonEmptyProse(value.prose);
143
+ if (byStatus.size === 0 && prose === undefined) {
144
+ // Explicit empty prose / byStatus → no advice (do not re-stringify the shell).
145
+ if (hasProseField || hasByStatusField) return undefined;
146
+ // Free-form object without byStatus/prose: single-advice compat via stringify.
147
+ const keys = Object.keys(value);
148
+ if (keys.length === 0) return undefined;
149
+ const text = JSON.stringify(value);
150
+ return text.trim() === "" || text === "{}" ? undefined : { byStatus: new Map(), prose: text };
151
+ }
152
+ return {
153
+ byStatus,
154
+ ...(prose === undefined ? {} : { prose }),
155
+ };
156
+ }
157
+
158
+ /** Merge status keys; same-status prose concatenates (no silent drop). */
159
+ export function mergePreparedAdvice(
160
+ prior: PreparedNavigatorAdvice | undefined,
161
+ next: PreparedNavigatorAdvice | undefined,
162
+ ): PreparedNavigatorAdvice | undefined {
163
+ if (prior === undefined) return next;
164
+ if (next === undefined) return prior;
165
+ const byStatus = new Map(prior.byStatus);
166
+ for (const [key, prose] of next.byStatus) {
167
+ setStatusProse(byStatus, key, prose);
168
+ }
169
+ const proseParts = [prior.prose, next.prose]
170
+ .filter((part): part is string => typeof part === "string" && part.trim() !== "");
171
+ const prose = proseParts.length === 0 ? undefined : proseParts.join("\n\n");
172
+ if (byStatus.size === 0 && prose === undefined) return undefined;
173
+ return {
174
+ byStatus,
175
+ ...(prose === undefined ? {} : { prose }),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Pick prepared prose for a settlement status (#1160 / owner 3188b502).
181
+ * - byStatus present → only the matching status key (no prose fallback).
182
+ * - byStatus empty → single prose used as the only advice.
183
+ * Missing match → undefined (honest no-advice).
184
+ */
185
+ export function pickPreparedProse(
186
+ prepared: PreparedNavigatorAdvice | undefined,
187
+ status: string | undefined,
188
+ ): string | undefined {
189
+ if (prepared === undefined) return undefined;
190
+ if (prepared.byStatus.size > 0) {
191
+ if (status === undefined) return undefined;
192
+ const hit = prepared.byStatus.get(status);
193
+ return typeof hit === "string" && hit.trim() !== "" ? hit : undefined;
194
+ }
195
+ const prose = prepared.prose;
196
+ return typeof prose === "string" && prose.trim() !== "" ? prose : undefined;
197
+ }
@@ -487,6 +487,8 @@ export type PostAdmissionEnv = {
487
487
  boundTicketNumber?: number;
488
488
  /** Station child role run (#840): omit automatic navigator attendance. */
489
489
  stationChild?: boolean;
490
+ /** #1160 attendance auto byStatus prepare (nested navigator only). */
491
+ navigatorByStatusPrepare?: boolean;
490
492
  };
491
493
 
492
494
  /**
@@ -1379,6 +1381,9 @@ export async function dispatchPostAdmissionTurn<
1379
1381
  if (env.stationChild !== undefined) {
1380
1382
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
1381
1383
  }
1384
+ if (env.navigatorByStatusPrepare === true) {
1385
+ turnRequest = { ...turnRequest, navigatorByStatusPrepare: true };
1386
+ }
1382
1387
  // Selected host axis rides the shared Host envelope for in-turn tools
1383
1388
  // (detour usage ledger) — never a pre-spawn current.json reread.
1384
1389
  if (typeof env.host === "string" && env.host.trim() !== "") {
@@ -1735,6 +1740,9 @@ export function resumeTurnRequestProjectionOptions(
1735
1740
  prompt,
1736
1741
  },
1737
1742
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1743
+ ...(env.navigatorByStatusPrepare === true
1744
+ ? { navigatorByStatusPrepare: true as const }
1745
+ : {}),
1738
1746
  // #1132: one configured ceiling, already resolved by the caller (#422).
1739
1747
  deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1740
1748
  };
@@ -1755,6 +1763,9 @@ export function roleTurnOptions(
1755
1763
  : { correlationId }),
1756
1764
  continuation,
1757
1765
  ...(env.stationChild === undefined ? {} : { stationChild: env.stationChild }),
1766
+ ...(env.navigatorByStatusPrepare === true
1767
+ ? { navigatorByStatusPrepare: true as const }
1768
+ : {}),
1758
1769
  // #1132: one configured ceiling, already resolved by the caller (#422).
1759
1770
  deliveryRequestLimit: deliveryLimitFromConfig(env.autoResumeLimit),
1760
1771
  };
@@ -70,6 +70,8 @@ export type RoleTurnRequestProjectionOptions = {
70
70
  invocationScopeId?: string;
71
71
  /** Station child role run (#840): omit automatic navigator attendance. */
72
72
  readonly stationChild?: boolean;
73
+ /** #1160 attendance auto byStatus prepare (not direct navigator prose). */
74
+ readonly navigatorByStatusPrepare?: boolean;
73
75
  /**
74
76
  * #1132: the one effective delivery-request ceiling (#422 single resolve by
75
77
  * the caller). Projected so the host adapter's own re-ask loop and the AK
@@ -123,6 +125,9 @@ export function projectRoleTurnRequest(
123
125
  ? {}
124
126
  : { invocationScopeId: options.invocationScopeId }),
125
127
  ...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
128
+ ...(options.navigatorByStatusPrepare === undefined
129
+ ? {}
130
+ : { navigatorByStatusPrepare: options.navigatorByStatusPrepare }),
126
131
  ...(options.deliveryRequestLimit === undefined
127
132
  ? {}
128
133
  : { deliveryRequestLimit: options.deliveryRequestLimit }),
@@ -121,6 +121,11 @@ export type PublicSummonRequest = {
121
121
  * (#946 / ADR 0082).
122
122
  */
123
123
  readonly stationChild?: boolean;
124
+ /**
125
+ * #1160 attendance auto byStatus prepare. Nested navigator only — never set
126
+ * on direct `ak-role navigator` (keeps free-form prose exit).
127
+ */
128
+ readonly navigatorByStatusPrepare?: boolean;
124
129
  };
125
130
 
126
131
  const execFileAsync = promisify(execFile);
@@ -398,6 +403,9 @@ export async function summonPublicRole(
398
403
  // Nested court stations keep station-child semantics; dual-lens
399
404
  // ordinary Reviewer axes opt out (#946 / ADR 0082).
400
405
  ...(options.stationChild === false ? {} : { stationChild: true }),
406
+ ...(options.navigatorByStatusPrepare === true
407
+ ? { navigatorByStatusPrepare: true as const }
408
+ : {}),
401
409
  // Forward composition-root adapters so nested stations (e.g.
402
410
  // secretariat → diarist) select the same faux/production table (#924).
403
411
  ...(options.hostAdapters === undefined
@@ -1107,9 +1107,10 @@ export function createRoleRuntimeExtension(
1107
1107
  if (settlement === undefined || attendance === undefined) return;
1108
1108
  const workContext = navigatorWorkContext;
1109
1109
  const pending = (async () => {
1110
- // ADR 0052 / #959: every settlement that starts a post-role feed host round
1110
+ // ADR 0052 / #959 / #1160: every settlement that waits on parallel prepare
1111
1111
  // (accepted, human_decision, role_infrastructure_failure) shares one grace
1112
1112
  // and the same honest unavailable projection — no unbounded parallel branch.
1113
+ // Settle picks byStatus; grace is only the tail wait, not a cold model start.
1113
1114
  const settlePromise = attendance.settle(settlement);
1114
1115
  // Attach catch immediately so a late rejection after grace timeout cannot
1115
1116
  // surface as unhandledRejection / stale-ctx after session dispose (#675).
@@ -1775,7 +1776,7 @@ export function createRoleRuntimeExtension(
1775
1776
  }
1776
1777
  },
1777
1778
  });
1778
- // Concrete work context starts standby attendance (record only, no model).
1779
+ // Concrete work context starts parallel prepare (model runs now; #1160).
1779
1780
  // Placeholder subjects wait for before_agent_start (user prompt may replace the subject key).
1780
1781
  if (
1781
1782
  navigatorWorkContext.contextError === undefined &&