@akagilnc/pi-workflow-roles 0.1.5375 → 0.1.5406

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.
Files changed (79) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +6381 -5684
  4. package/dist/archivist-record-pointer.js +38 -0
  5. package/dist/collector-tool-schemas.js +18 -0
  6. package/dist/compliance-transport.js +5 -0
  7. package/dist/diarist-contracts.js +26 -0
  8. package/dist/doctor-auditor.js +1 -0
  9. package/dist/engine-detour-usage.js +6 -6
  10. package/dist/filed-submission.js +28 -0
  11. package/dist/gatekeeper-role.js +6 -10
  12. package/dist/gleaner-left-contracts.js +16 -0
  13. package/dist/headless-host/production-host.js +1644 -947
  14. package/dist/judge-role.js +21 -16
  15. package/dist/migrate-book-topology.js +452 -81
  16. package/dist/navigator-attendance.js +10 -9
  17. package/dist/navigator-invocation-identity.js +18 -0
  18. package/dist/package-contracts/reviewer-output.js +16 -0
  19. package/dist/package-contracts/worker-output.js +18 -2
  20. package/dist/pi/role-turn-host.js +9 -0
  21. package/dist/public-cli/cli.js +1 -0
  22. package/dist/public-cli/instruction-seat-run.js +258 -55
  23. package/dist/public-cli/main.js +1209 -346
  24. package/dist/public-cli/post-admission.js +427 -244
  25. package/dist/public-cli/settlement.js +111 -48
  26. package/dist/public-cli/terminal.js +12 -1
  27. package/dist/public-cli/turn-request.js +3 -0
  28. package/dist/receipt-delivery-policy.js +152 -17
  29. package/dist/role-submission-declarations.js +131 -0
  30. package/dist/secretariat-contracts.js +23 -1
  31. package/dist/submission-errors.js +11 -1
  32. package/dist/submission-gate.js +32 -12
  33. package/package.json +1 -1
  34. package/src/archivist-record-pointer.ts +48 -0
  35. package/src/auditor-role.ts +2 -8
  36. package/src/collector-role.ts +5 -28
  37. package/src/compliance-transport.ts +10 -2
  38. package/src/countersign-role.ts +2 -7
  39. package/src/diarist-contracts.ts +37 -0
  40. package/src/diarist-role.ts +4 -44
  41. package/src/doctor-auditor.ts +3 -0
  42. package/src/doctor-role.ts +16 -11
  43. package/src/engine-detour-usage.ts +7 -6
  44. package/src/external-host-turn-loop.ts +24 -12
  45. package/src/filed-submission.ts +44 -0
  46. package/src/gatekeeper-role.ts +8 -16
  47. package/src/gleaner-left-contracts.ts +26 -1
  48. package/src/gleaner-left-role.ts +4 -32
  49. package/src/host-contracts.ts +7 -0
  50. package/src/inspector-role.ts +2 -7
  51. package/src/judge-role.ts +31 -24
  52. package/src/merger-role.ts +5 -8
  53. package/src/navigator-attendance.ts +17 -12
  54. package/src/navigator-invocation-identity.ts +31 -0
  55. package/src/navigator-role.ts +2 -8
  56. package/src/notary-role.ts +5 -20
  57. package/src/package-contracts/reviewer-output.ts +25 -0
  58. package/src/package-contracts/worker-output.ts +43 -5
  59. package/src/pi/adapter.ts +33 -8
  60. package/src/pi/role-turn-host.ts +7 -0
  61. package/src/public-cli/cli.ts +1 -0
  62. package/src/public-cli/instruction-seat-run.ts +292 -61
  63. package/src/public-cli/post-admission.ts +556 -324
  64. package/src/public-cli/settlement.ts +112 -50
  65. package/src/public-cli/terminal.ts +16 -4
  66. package/src/public-cli/turn-request.ts +10 -1
  67. package/src/receipt-delivery-policy.ts +209 -17
  68. package/src/reviewer-role.ts +7 -35
  69. package/src/role-envelope.ts +5 -1
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +72 -56
  72. package/src/role-submission-declarations.ts +143 -0
  73. package/src/secretariat-contracts.ts +35 -0
  74. package/src/secretariat-role.ts +4 -40
  75. package/src/submission-correctable-error.ts +5 -2
  76. package/src/submission-errors.ts +12 -1
  77. package/src/submission-gate.ts +41 -12
  78. package/src/worker-role.ts +81 -86
  79. package/src/worker-submission-gates.ts +32 -7
@@ -1,6 +1,8 @@
1
1
  import { bookDirectOfficerRunPointer } from "./archivist-record-pointer.js";
2
2
  import { GatekeeperDecisionError, officerConclusionReask, projectGatekeeperRun, } from "./gatekeeper-role.js";
3
- /** Queue word of the latest this-terminal payload. History does not outrank it. */
3
+ import { receivedDiscriminator } from "./submission-errors.js";
4
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.js";
5
+ /** Received discriminator of the latest payload, retained verbatim for re-ask. */
4
6
  export function latestQueueStatus(terminal) {
5
7
  const outcome = terminal?.roleOutcome;
6
8
  if (outcome === undefined)
@@ -10,14 +12,10 @@ export function latestQueueStatus(terminal) {
10
12
  if (outcome.kind !== "accepted")
11
13
  return undefined;
12
14
  const latest = outcome.payloads?.[outcome.payloads.length - 1];
13
- if (isRecord(latest)) {
14
- const status = latest.status;
15
- if (typeof status === "string" && status.trim() !== "")
16
- return status;
15
+ if (isRecord(latest) && Object.hasOwn(latest, "status")) {
16
+ return receivedDiscriminator(latest, "status");
17
17
  }
18
- return typeof outcome.status === "string" && outcome.status.trim() !== ""
19
- ? outcome.status
20
- : undefined;
18
+ return outcome.status;
21
19
  }
22
20
  export function latestQueuePayload(terminal) {
23
21
  const outcome = terminal?.roleOutcome;
@@ -57,7 +55,7 @@ export function createDefaultGateOfficerSummon(options) {
57
55
  * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
58
56
  * Offline mocks without a real session leave no nested volume (lawful zero).
59
57
  */
60
- function bookDirectOfficerPointer(context, officer, result, summoned) {
58
+ function bookDirectOfficerPointer(context, officer, result, summoned, toolCallId) {
61
59
  if (result.status !== "converged"
62
60
  && result.status !== "continue"
63
61
  && result.status !== "escalate"
@@ -80,16 +78,20 @@ function bookDirectOfficerPointer(context, officer, result, summoned) {
80
78
  ...(typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== ""
81
79
  ? { runDirectory: summoned.runDirectory }
82
80
  : {}),
81
+ ...(toolCallId.trim() === "" ? {} : { submissionToolCallId: toolCallId }),
83
82
  });
84
83
  }
85
84
  /**
86
85
  * Shared envelope: project gate, book officer pointer, map onto host actions.
87
- * Review loop has no round cap (#753 no-round-cap).
86
+ * A real converged / continue / escalate returns immediately. An unreadable
87
+ * conclusion reasks that officer against the configured ceiling; exhaustion
88
+ * returns the receipt actually received.
88
89
  * On converged, returns the officer snapshot (receipt + nested runId) for seat projection.
89
90
  */
90
91
  export async function requireSubmissionGate(options) {
91
92
  let reask;
92
- // No round cap — end only on converged, continue/escalate, or real failure.
93
+ let reasksSpent = 0;
94
+ const reaskLimit = deliveryLimitFromConfig(options.autoResumeLimit);
93
95
  const summonOfficer = options.summonOfficer ??
94
96
  createDefaultGateOfficerSummon({
95
97
  cwd: options.context.cwd ?? process.cwd(),
@@ -107,7 +109,7 @@ export async function requireSubmissionGate(options) {
107
109
  // Envelope-owned pointer book. Failure is host infrastructure — single face.
108
110
  if (projected.summoned !== undefined) {
109
111
  try {
110
- bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned);
112
+ bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned, options.toolCallId);
111
113
  }
112
114
  catch (error) {
113
115
  options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
@@ -134,6 +136,24 @@ export async function requireSubmissionGate(options) {
134
136
  };
135
137
  }
136
138
  if (gatekeeper.status === "needs_reask") {
139
+ if (reasksSpent >= reaskLimit) {
140
+ return {
141
+ status: "needs_reask",
142
+ officer: projected.officer,
143
+ receipt: gatekeeper.receipt,
144
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
145
+ ? { runId: gatekeeper.runId }
146
+ : {}),
147
+ ...(typeof projected.summoned?.runDirectory === "string"
148
+ && projected.summoned.runDirectory.trim() !== ""
149
+ ? { runDirectory: projected.summoned.runDirectory }
150
+ : {}),
151
+ ...(projected.summoned?.terminal === undefined
152
+ ? {}
153
+ : { terminal: projected.summoned.terminal }),
154
+ };
155
+ }
156
+ reasksSpent += 1;
137
157
  reask = officerConclusionReask(gatekeeper.receivedStatus);
138
158
  continue;
139
159
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5375",
3
+ "version": "0.1.5406",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
2
3
  import { dirname, join } from "node:path";
4
+ import { isRecord } from "./unknown-value.ts";
3
5
 
4
6
  /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
5
7
  export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
@@ -12,6 +14,11 @@ export type DirectOfficerRunPointer = {
12
14
  readonly sessionFile: string;
13
15
  /** Officer run directory when known. */
14
16
  readonly runDirectory?: string;
17
+ /**
18
+ * Parent submission tool call this officer run was summoned for.
19
+ * Absent on pointers booked before that binding existed.
20
+ */
21
+ readonly submissionToolCallId?: string;
15
22
  };
16
23
 
17
24
  /**
@@ -28,9 +35,11 @@ export function bookDirectOfficerRunPointer(options: {
28
35
  readonly officer: "inspector" | "notary" | "auditor" | "countersign";
29
36
  readonly sessionFile: string;
30
37
  readonly runDirectory?: string;
38
+ readonly submissionToolCallId?: string;
31
39
  }): DirectOfficerRunPointer {
32
40
  const nest = join(dirname(options.parentSessionFile), "auditor-roles");
33
41
  mkdirSync(nest, { recursive: true });
42
+ const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
34
43
  const pointer: DirectOfficerRunPointer = {
35
44
  version: 1,
36
45
  kind: DIRECT_OFFICER_RUN_POINTER_KIND,
@@ -39,6 +48,7 @@ export function bookDirectOfficerRunPointer(options: {
39
48
  ...(options.runDirectory !== undefined && options.runDirectory.trim() !== ""
40
49
  ? { runDirectory: options.runDirectory }
41
50
  : {}),
51
+ ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
42
52
  };
43
53
  writeFileSync(
44
54
  join(nest, `${options.officer}.pointer.json`),
@@ -47,3 +57,41 @@ export function bookDirectOfficerRunPointer(options: {
47
57
  );
48
58
  return pointer;
49
59
  }
60
+
61
+ /**
62
+ * Read the one officer pointer. Missing or unreadable is not a pass:
63
+ * the caller re-runs the gate.
64
+ */
65
+ export async function readDirectOfficerRunPointer(
66
+ parentSessionFile: string,
67
+ officer: DirectOfficerRunPointer["officer"],
68
+ ): Promise<DirectOfficerRunPointer | undefined> {
69
+ try {
70
+ const raw: unknown = JSON.parse(await readFile(
71
+ join(dirname(parentSessionFile), "auditor-roles", `${officer}.pointer.json`),
72
+ "utf8",
73
+ ));
74
+ if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
75
+ return undefined;
76
+ }
77
+ if (raw.officer !== officer) return undefined;
78
+ if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") return undefined;
79
+ const runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
80
+ ? raw.runDirectory
81
+ : undefined;
82
+ const submissionToolCallId = typeof raw.submissionToolCallId === "string"
83
+ && raw.submissionToolCallId.trim() !== ""
84
+ ? raw.submissionToolCallId
85
+ : undefined;
86
+ return {
87
+ version: 1,
88
+ kind: DIRECT_OFFICER_RUN_POINTER_KIND,
89
+ officer,
90
+ sessionFile: raw.sessionFile,
91
+ ...(runDirectory === undefined ? {} : { runDirectory }),
92
+ ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
93
+ };
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ }
@@ -3,8 +3,8 @@
3
3
  */
4
4
  import {
5
5
  AUDITOR_OUTPUT_TOOL_NAME,
6
- auditorOutputSchema,
7
6
  } from "./package-contracts/auditor-output.ts";
7
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
8
8
 
9
9
  export { AUDITOR_OUTPUT_TOOL_NAME };
10
10
 
@@ -12,10 +12,4 @@ export type AuditorRuntimeDependencies = {
12
12
  loadSoul(): Promise<string>;
13
13
  };
14
14
 
15
- export const AUDITOR_TOOL_SPEC = {
16
- name: AUDITOR_OUTPUT_TOOL_NAME,
17
- label: "审刑院输出",
18
- description: "审刑院终局回执,status 为 converged、continue 或 escalate。",
19
- promptSnippet: "审刑院终局回执",
20
- parameters: auditorOutputSchema,
21
- } as const;
15
+ export const AUDITOR_TOOL_SPEC = roleSubmissionDeclaration("auditor");
@@ -3,8 +3,9 @@
3
3
  * #1088: code no longer observes/requests/merges GitHub findings — the LLM
4
4
  * uses host CLI tools. Envelope owns host-surface + construction seatbelt.
5
5
  */
6
- import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
7
- import type { Static } from "typebox";
6
+ import type { RoleHost, HostContext } from "./host-contracts.ts";
7
+ import { registerFiledSubmissionTool } from "./filed-submission.ts";
8
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
8
9
 
9
10
  import {
10
11
  emptyCollectorManifest,
@@ -17,8 +18,6 @@ import {
17
18
  import {
18
19
  COLLECTOR_OUTPUT_TOOL,
19
20
  } from "./package-contracts/collector-output.ts";
20
- import { collectorOutputArgsSchema } from "./collector-tool-schemas.ts";
21
-
22
21
  export { COLLECTOR_OUTPUT_TOOL };
23
22
 
24
23
  export const COLLECTOR_REQUIRED_TOOLS = [COLLECTOR_OUTPUT_TOOL] as const;
@@ -54,8 +53,6 @@ export const COLLECTOR_TRANSPORT_FLAGS = Object.freeze([
54
53
  }),
55
54
  ] as const);
56
55
 
57
- const outputSchema = collectorOutputArgsSchema;
58
- type OutputParams = Static<typeof outputSchema>;
59
56
 
60
57
  export type CollectorRoleDependencies = {
61
58
  loadSoul(): Promise<string>;
@@ -164,28 +161,8 @@ export function createCollectorRoleRuntime(
164
161
  if (toolsRegistered) return;
165
162
  toolsRegistered = true;
166
163
 
167
- pi.registerTool({
168
- name: COLLECTOR_OUTPUT_TOOL,
169
- label: "通进司输出",
170
- description: "提交通进司回执。",
171
- promptSnippet: "提交通进司回执",
172
- parameters: outputSchema,
173
- async execute(
174
- _toolCallId: string,
175
- params: OutputParams,
176
- _signal: AbortSignal | undefined,
177
- _onUpdate: unknown,
178
- _ctx: HostContext,
179
- ): Promise<HostToolResult<unknown>> {
180
- const activation = getActivation();
181
- if (activation === undefined) throw new Error("通进司未激活");
182
- // #1088 / #836: record as submitted — runtime does not merge findings.
183
- return {
184
- content: [],
185
- details: params,
186
- terminate: true as const,
187
- };
188
- },
164
+ registerFiledSubmissionTool(pi, roleSubmissionDeclaration("collector"), {
165
+ readyError: () => (getActivation() === undefined ? "通进司未激活" : undefined),
189
166
  });
190
167
  },
191
168
  };
@@ -8,14 +8,16 @@ import { officerConclusionReask } from "./gatekeeper-role.ts";
8
8
  import { receivedDiscriminator } from "./submission-errors.ts";
9
9
  import { coalesceSubmissionRows } from "./public-cli/terminal.ts";
10
10
  import { readableGateItem } from "./readable-gate-item.ts";
11
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
11
12
  import { isRecord } from "./unknown-value.ts";
12
13
 
13
14
  export type ComplianceNoReceipt = NoReceiptLifecycleFacts & { status: "no-receipt"; usage?: Usage };
14
15
  /**
15
16
  * #757 / #750: no unreadable/unusable judgment on auditor replies.
16
17
  * Known three-state (converged/continue/escalate) is read for queueing only.
17
- * Unknown accepted shape rides as `received` with the raw reply — parent stands,
18
- * no forged pass, no shape-death label. Resume-speaker for three pairs is #753/#756.
18
+ * Unknown accepted shape rides as `received` with the raw reply. The summon
19
+ * loop reasks until the configured ceiling, then keeps that reply. No forged
20
+ * pass, no shape-death label.
19
21
  */
20
22
  export type ComplianceReceived = {
21
23
  readonly status: "received";
@@ -120,6 +122,8 @@ export type RunComplianceAuditOptions = {
120
122
  submission?: unknown;
121
123
  runDirectory?: string | undefined;
122
124
  signal?: AbortSignal;
125
+ /** This loop's ceiling. Absent uses the package default. */
126
+ autoResumeLimit?: number;
123
127
  /** Test seam — production uses summonPublicRole({ role: "auditor", argv: ["--subject", subject, "--source-run", …] }). */
124
128
  summonAuditor?: AuditorSummon;
125
129
  };
@@ -224,10 +228,14 @@ export async function runComplianceAudit(options: RunComplianceAuditOptions): Pr
224
228
  });
225
229
  });
226
230
  let reask: string | undefined;
231
+ let reasksSpent = 0;
232
+ const reaskLimit = deliveryLimitFromConfig(options.autoResumeLimit);
227
233
  for (;;) {
228
234
  const summoned = await summon(subject, runDirectory, options.signal, reask, submission);
229
235
  const decision = await projectAuditorTerminal(summoned);
230
236
  if (decision.status === "received") {
237
+ if (reasksSpent >= reaskLimit) return decision;
238
+ reasksSpent += 1;
231
239
  reask = officerConclusionReask(receivedDiscriminator(decision.reply, "status"));
232
240
  continue;
233
241
  }
@@ -1,4 +1,5 @@
1
1
  import { reviewSubmissionSchema } from "./review-submission.ts";
2
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
2
3
  import {
3
4
  COUNTERSIGN_OUTPUT_TOOL_NAME,
4
5
  validateRecordedCountersignOutput,
@@ -26,10 +27,4 @@ export type CountersignRuntimeDependencies = {
26
27
  * 决定工具规格。生命周期装配(注册、activate、prompt 注入、singleton 检查、
27
28
  * terminate)归注册信封 owner——src/role-runtime.ts(ADR 0018 / #572 R2 判词)。
28
29
  */
29
- export const COUNTERSIGN_TOOL_SPEC = {
30
- name: COUNTERSIGN_OUTPUT_TOOL_NAME,
31
- label: "给事中输出",
32
- description: "给事中决议。",
33
- promptSnippet: "给事中决议",
34
- parameters: countersignVerdictSchema,
35
- } as const;
30
+ export const COUNTERSIGN_TOOL_SPEC = roleSubmissionDeclaration("countersign");
@@ -7,6 +7,10 @@
7
7
  * #901: LLM submits bounds; the mechanical layer projects source bytes.
8
8
  */
9
9
 
10
+ import { Type } from "typebox";
11
+
12
+ import { openToolObject } from "./open-tool-schema.ts";
13
+ import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
10
14
  import {
11
15
  projectTicketProvenanceSessions,
12
16
  type TicketProvenanceSession,
@@ -88,3 +92,36 @@ export function projectDiaristTicketSessions(
88
92
  }
89
93
  return Array.from(perTicket, ([ticketNumber, sessions]) => ({ ticketNumber, sessions }));
90
94
  }
95
+
96
+ /**
97
+ * 起居郎交卷形状。
98
+ * #901:交边界(sessions);正文与不可解析原字节由机械投影。
99
+ * status 的合法词(completed | escalate)写在 description,结算认这些词。
100
+ * sessions / ticketSessions 是投影所读的边界:卷路径或端点读不出时走既有 reask,不中止本轮。
101
+ * 声明只留字段名和语义说明,不留类型、嵌套、长度、必填。
102
+ */
103
+ const DIARIST_SESSIONS_DESCRIPTION =
104
+ "对话边界。每卷含 path(会话卷绝对或可读路径)与 ranges(本卷本轮各区间)。每段区间含 from / to;起点与终点各写 id(原生 id)或 line(本轮行号),二选一。空列表=本轮无对话可划。端点无法指名时走 reask,不中止。" as const;
105
+
106
+ export const diaristOutputSchema = withTerminatingOutputDeclarations(
107
+ openToolObject(
108
+ Type.Object({
109
+ status: Type.Unknown({
110
+ description: "completed | escalate",
111
+ }),
112
+ ticketNumber: Type.Optional(
113
+ Type.Unknown({
114
+ description:
115
+ "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
116
+ }),
117
+ ),
118
+ reason: Type.Unknown({
119
+ description: "status 为 escalate 时:认不出本庭对象的原因",
120
+ }),
121
+ sessions: Type.Optional(Type.Unknown({ description: DIARIST_SESSIONS_DESCRIPTION })),
122
+ ticketSessions: Type.Unknown({
123
+ description: `多票庭逐票对话边界;每票含 ticketNumber(本条边界所归票号,正整数)与 sessions(${DIARIST_SESSIONS_DESCRIPTION})。一次交卷各票各自分录。与单票 sessions 二选一。`,
124
+ }),
125
+ }),
126
+ ),
127
+ );
@@ -1,53 +1,20 @@
1
1
  import type { Static } from "typebox";
2
- import { Type } from "typebox";
3
2
 
4
- import { openToolObject } from "./open-tool-schema.ts";
5
- import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
6
3
  import {
7
4
  DIARIST_OUTPUT_TOOL_NAME,
5
+ diaristOutputSchema,
8
6
  validateRecordedDiaristOutput,
9
7
  type DiaristOutput,
10
8
  } from "./diarist-contracts.ts";
9
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
11
10
 
12
11
  export {
13
12
  DIARIST_OUTPUT_TOOL_NAME,
13
+ diaristOutputSchema,
14
14
  } from "./diarist-contracts.ts";
15
15
  export type { DiaristOutput };
16
16
  export { validateRecordedDiaristOutput };
17
17
 
18
- /**
19
- * 起居郎交卷形状。
20
- * #901:交边界(sessions);正文与不可解析原字节由机械投影。
21
- * status 的合法词(completed | escalate)写在 description,结算认这些词。
22
- * sessions / ticketSessions 是投影所读的边界:卷路径或端点读不出时走既有 reask,不中止本轮。
23
- * 声明只留字段名和语义说明,不留类型、嵌套、长度、必填。
24
- */
25
- const DIARIST_SESSIONS_DESCRIPTION =
26
- "对话边界。每卷含 path(会话卷绝对或可读路径)与 ranges(本卷本轮各区间)。每段区间含 from / to;起点与终点各写 id(原生 id)或 line(本轮行号),二选一。空列表=本轮无对话可划。端点无法指名时走 reask,不中止。" as const;
27
-
28
- export const diaristOutputSchema = withTerminatingOutputDeclarations(
29
- openToolObject(
30
- Type.Object({
31
- status: Type.Unknown({
32
- description: "completed | escalate",
33
- }),
34
- ticketNumber: Type.Optional(
35
- Type.Unknown({
36
- description:
37
- "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
38
- }),
39
- ),
40
- reason: Type.Unknown({
41
- description: "status 为 escalate 时:认不出本庭对象的原因",
42
- }),
43
- sessions: Type.Optional(Type.Unknown({ description: DIARIST_SESSIONS_DESCRIPTION })),
44
- ticketSessions: Type.Unknown({
45
- description: `多票庭逐票对话边界;每票含 ticketNumber(本条边界所归票号,正整数)与 sessions(${DIARIST_SESSIONS_DESCRIPTION})。一次交卷各票各自分录。与单票 sessions 二选一。`,
46
- }),
47
- }),
48
- ),
49
- );
50
-
51
18
  export type DiaristOutputParameters = Static<typeof diaristOutputSchema>;
52
19
 
53
20
  export type DiaristRuntimeDependencies = {
@@ -57,11 +24,4 @@ export type DiaristRuntimeDependencies = {
57
24
  /**
58
25
  * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
59
26
  */
60
- export const DIARIST_TOOL_SPEC = {
61
- name: DIARIST_OUTPUT_TOOL_NAME,
62
- label: "起居郎输出",
63
- description:
64
- "起居郎交单票 sessions 或多票逐票 ticketSessions 边界;认不出本庭对象则 escalate。",
65
- promptSnippet: "起居郎交边界",
66
- parameters: diaristOutputSchema,
67
- } as const;
27
+ export const DIARIST_TOOL_SPEC = roleSubmissionDeclaration("diarist");
@@ -16,6 +16,8 @@ export type DoctorAuditOptions = {
16
16
  context: HostContext;
17
17
  submission: unknown;
18
18
  signal?: AbortSignal;
19
+ /** Same ceiling the public execution seam already resolved. */
20
+ autoResumeLimit?: number;
19
21
  /** Same seam as runComplianceAudit options — offline tracers only. */
20
22
  summonAuditor?: AuditorSummon;
21
23
  };
@@ -39,6 +41,7 @@ export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promis
39
41
  ? {}
40
42
  : { runDirectory: auditorRunDirectory(options.context) }),
41
43
  ...(options.signal === undefined ? {} : { signal: options.signal }),
44
+ ...(options.autoResumeLimit === undefined ? {} : { autoResumeLimit: options.autoResumeLimit }),
42
45
  ...(options.summonAuditor === undefined ? {} : { summonAuditor: options.summonAuditor }),
43
46
  });
44
47
  };
@@ -1,7 +1,9 @@
1
- import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
1
+ import type { RoleHost, HostContext } from "./host-contracts.ts";
2
+ import { registerFiledSubmissionTool } from "./filed-submission.ts";
3
+ import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
2
4
  import { ComplianceResponseRetentionError } from "./compliance-transport.ts";
3
5
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
4
- import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
6
+ import { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
5
7
 
6
8
  import { sitianReport } from "./sitian-facade.ts";
7
9
 
@@ -36,15 +38,18 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
36
38
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
37
39
  return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
38
40
  if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
39
- pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, _signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> {
40
- if (!activation) throw new Error("太医署未激活");
41
- const active = activation;
42
- const testimony = validateDoctorOutput(params, active.patient, active.store);
43
- try {
44
- appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity });
45
- } catch (error) { host.failInfrastructure(error, ctx, id); }
46
- return { content: [], details: testimony, terminate: true };
47
- } });
41
+ registerFiledSubmissionTool(pi, roleSubmissionDeclaration("doctor"), {
42
+ readyError: () => (activation === undefined ? "太医署未激活" : undefined),
43
+ beforeAccept: async ({ toolCallId, parameters, ctx }) => {
44
+ if (activation === undefined) throw new Error("太医署未激活");
45
+ const active = activation;
46
+ const testimony = validateDoctorOutput(parameters, active.patient, active.store);
47
+ try {
48
+ appendCandidate(ctx, { version: 1, testimony, cost: active.patient.cost, readRecord: active.store.readRecord(), patientIdentity: active.patient.identity });
49
+ } catch (error) { host.failInfrastructure(error, ctx, toolCallId); }
50
+ return testimony;
51
+ },
52
+ });
48
53
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
49
54
  const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
50
55
  }
@@ -329,15 +329,16 @@ export function sessionFileFromSessionDirectory(sessionDirectory: string): strin
329
329
  }
330
330
 
331
331
  /**
332
- * Mint one public-invocation scope id when an engine is mounted.
332
+ * Mint one public-invocation scope id.
333
333
  * Call once at the public-entry boundary — never inside the auto-resume loop.
334
- * The id lives on RoleTurnRequest / HostContext only (no detour sidecar file).
334
+ * Auto-resume reuses it; an explicit new public call, including manual resume,
335
+ * mints another. The id lives on RoleTurnRequest / HostContext only.
336
+ * Receipt facts and detour rows both use this id, including calls with no
337
+ * engine mounted — a missing engine must not merge two public calls.
335
338
  */
336
- export function mintEngineDetourInvocationScope(input: {
339
+ export function mintEngineDetourInvocationScope(_input: {
337
340
  readonly effectiveEngine?: string;
338
- }): string | undefined {
339
- const engine = input.effectiveEngine?.trim();
340
- if (engine === undefined || engine.length === 0) return undefined;
341
+ }): string {
341
342
  return randomUUID();
342
343
  }
343
344
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Middle external-host role-turn loop (#820 / ADR 0082).
3
- * One copy: serial, abort merge/race, 8-round closeRound retry,
3
+ * One copy: serial, abort merge/race, closeRound re-ask rounds,
4
4
  * host-aborted, round-limit. Last hop = ExternalHostTurnDriver (four verbs).
5
5
  */
6
6
  import type {
@@ -10,11 +10,10 @@ import type {
10
10
  RoleTurnResult,
11
11
  } from "./host-contracts.ts";
12
12
  import type { PreparedRoleTurn } from "./prepared-role-turn.ts";
13
-
13
+ import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
14
+ import { isOneShotWorkerReminderCode } from "./submission-errors.ts";
14
15
  import { errorText } from "./unknown-value.ts";
15
16
 
16
- export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
17
-
18
17
  export type ExternalPreparedTurn = Pick<PreparedRoleTurn, "prompt" | "abortSignal" | "closeRound">;
19
18
 
20
19
  export type ExternalHostRoundOutcome =
@@ -144,8 +143,14 @@ export async function driveExternalRoleTurnRounds(
144
143
  let prompt = prepared.prompt;
145
144
  const abortSignal = mergeRoleTurnAbortSignals(prepared.abortSignal, request.signal);
146
145
  let stderr = "";
147
-
148
- for (let attempt = 0; attempt < EXTERNAL_ROLE_TURN_ROUND_LIMIT; attempt += 1) {
146
+ // #1132: the first round is the initial delivery. Each later counted re-ask
147
+ // spends the configured ceiling. The one commit reminder and the one prefix
148
+ // reminder are not counted re-asks (ADR 0066/0070).
149
+ const countedReaskLimit = deliveryLimitFromConfig(request.deliveryRequestLimit);
150
+ const exemptReminders = new Set<string>();
151
+ let countedReasks = 0;
152
+
153
+ for (let attempt = 0; ; attempt += 1) {
149
154
  if (abortSignal?.aborted) return settleHostAborted(prepared, driver.currentSessionId());
150
155
 
151
156
  let round: ExternalHostRoundOutcome;
@@ -168,15 +173,22 @@ export async function driveExternalRoleTurnRounds(
168
173
  return { code: 0, stderr, timedOut: false };
169
174
  }
170
175
  if ("failure" in closure) return asFailure(closure.failure, stderr);
176
+ const reminderCode = closure.retry.code;
177
+ const exempt = isOneShotWorkerReminderCode(reminderCode) && !exemptReminders.has(reminderCode);
178
+ if (exempt) exemptReminders.add(reminderCode);
179
+ else {
180
+ countedReasks += 1;
181
+ if (countedReasks > countedReaskLimit) {
182
+ return asFailure({
183
+ cause: "output",
184
+ identity: { name: driver.roundLimitName, code: "round-retry-limit" },
185
+ ...(driver.currentSessionId() === undefined ? {} : { details: { sessionId: driver.currentSessionId() } }),
186
+ });
187
+ }
188
+ }
171
189
  prompt = closure.retry.message;
172
190
  driver.afterRetry?.();
173
191
  }
174
-
175
- return asFailure({
176
- cause: "output",
177
- identity: { name: driver.roundLimitName, code: "round-retry-limit" },
178
- ...(driver.currentSessionId() === undefined ? {} : { details: { sessionId: driver.currentSessionId() } }),
179
- });
180
192
  }
181
193
 
182
194
  export function createSerializedRoleTurnHost(
@@ -0,0 +1,44 @@
1
+ import type { HostContext, HostToolResult, RoleHost } from "./host-contracts.ts";
2
+ import type { RoleSubmissionDeclaration } from "./role-submission-declarations.ts";
3
+
4
+ export type FiledSubmissionBeforeAccept = (input: {
5
+ readonly toolCallId: string;
6
+ readonly parameters: unknown;
7
+ readonly signal: AbortSignal | undefined;
8
+ readonly ctx: HostContext;
9
+ }) => Promise<unknown>;
10
+
11
+ /**
12
+ * One host-neutral submission execute for every public seat.
13
+ * Records nothing itself — the ledger seam already appends the original payload.
14
+ * Ends this tool call. Does not summon an auditor and does not end the role run.
15
+ * Shape is not an admission gate (ADR 0055). Seat reminders stay on beforeAccept.
16
+ */
17
+ export function registerFiledSubmissionTool(
18
+ roleHost: RoleHost,
19
+ tool: RoleSubmissionDeclaration,
20
+ options: {
21
+ readonly readyError: () => string | undefined;
22
+ readonly beforeAccept?: FiledSubmissionBeforeAccept;
23
+ },
24
+ ): void {
25
+ roleHost.registerTool({
26
+ name: tool.name,
27
+ label: tool.label,
28
+ description: tool.description,
29
+ ...(tool.promptSnippet === undefined ? {} : { promptSnippet: tool.promptSnippet }),
30
+ parameters: tool.parameters as never,
31
+ async execute(toolCallId, parameters, signal, _onUpdate, ctx): Promise<HostToolResult<unknown>> {
32
+ const blocked = options.readyError();
33
+ if (blocked !== undefined) throw new Error(blocked);
34
+ const projected = options.beforeAccept === undefined
35
+ ? undefined
36
+ : await options.beforeAccept({ toolCallId, parameters, signal, ctx });
37
+ return {
38
+ content: [],
39
+ details: projected === undefined ? parameters : projected,
40
+ terminate: true as const,
41
+ };
42
+ },
43
+ });
44
+ }