@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.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/acp-host/production-host.js +6381 -5684
- package/dist/archivist-record-pointer.js +38 -0
- package/dist/collector-tool-schemas.js +18 -0
- package/dist/compliance-transport.js +5 -0
- package/dist/diarist-contracts.js +26 -0
- package/dist/doctor-auditor.js +1 -0
- package/dist/engine-detour-usage.js +6 -6
- package/dist/filed-submission.js +28 -0
- package/dist/gatekeeper-role.js +6 -10
- package/dist/gleaner-left-contracts.js +16 -0
- package/dist/headless-host/production-host.js +1644 -947
- package/dist/judge-role.js +21 -16
- package/dist/migrate-book-topology.js +452 -81
- package/dist/navigator-attendance.js +10 -9
- package/dist/navigator-invocation-identity.js +18 -0
- package/dist/package-contracts/reviewer-output.js +16 -0
- package/dist/package-contracts/worker-output.js +18 -2
- package/dist/pi/role-turn-host.js +9 -0
- package/dist/public-cli/cli.js +1 -0
- package/dist/public-cli/instruction-seat-run.js +258 -55
- package/dist/public-cli/main.js +1209 -346
- package/dist/public-cli/post-admission.js +427 -244
- package/dist/public-cli/settlement.js +111 -48
- package/dist/public-cli/terminal.js +12 -1
- package/dist/public-cli/turn-request.js +3 -0
- package/dist/receipt-delivery-policy.js +152 -17
- package/dist/role-submission-declarations.js +131 -0
- package/dist/secretariat-contracts.js +23 -1
- package/dist/submission-errors.js +11 -1
- package/dist/submission-gate.js +32 -12
- package/package.json +1 -1
- package/src/archivist-record-pointer.ts +48 -0
- package/src/auditor-role.ts +2 -8
- package/src/collector-role.ts +5 -28
- package/src/compliance-transport.ts +10 -2
- package/src/countersign-role.ts +2 -7
- package/src/diarist-contracts.ts +37 -0
- package/src/diarist-role.ts +4 -44
- package/src/doctor-auditor.ts +3 -0
- package/src/doctor-role.ts +16 -11
- package/src/engine-detour-usage.ts +7 -6
- package/src/external-host-turn-loop.ts +24 -12
- package/src/filed-submission.ts +44 -0
- package/src/gatekeeper-role.ts +8 -16
- package/src/gleaner-left-contracts.ts +26 -1
- package/src/gleaner-left-role.ts +4 -32
- package/src/host-contracts.ts +7 -0
- package/src/inspector-role.ts +2 -7
- package/src/judge-role.ts +31 -24
- package/src/merger-role.ts +5 -8
- package/src/navigator-attendance.ts +17 -12
- package/src/navigator-invocation-identity.ts +31 -0
- package/src/navigator-role.ts +2 -8
- package/src/notary-role.ts +5 -20
- package/src/package-contracts/reviewer-output.ts +25 -0
- package/src/package-contracts/worker-output.ts +43 -5
- package/src/pi/adapter.ts +33 -8
- package/src/pi/role-turn-host.ts +7 -0
- package/src/public-cli/cli.ts +1 -0
- package/src/public-cli/instruction-seat-run.ts +292 -61
- package/src/public-cli/post-admission.ts +556 -324
- package/src/public-cli/settlement.ts +112 -50
- package/src/public-cli/terminal.ts +16 -4
- package/src/public-cli/turn-request.ts +10 -1
- package/src/receipt-delivery-policy.ts +209 -17
- package/src/reviewer-role.ts +7 -35
- package/src/role-envelope.ts +5 -1
- package/src/role-runtime-dependencies.ts +1 -0
- package/src/role-runtime.ts +72 -56
- package/src/role-submission-declarations.ts +143 -0
- package/src/secretariat-contracts.ts +35 -0
- package/src/secretariat-role.ts +4 -40
- package/src/submission-correctable-error.ts +5 -2
- package/src/submission-errors.ts +12 -1
- package/src/submission-gate.ts +41 -12
- package/src/worker-role.ts +81 -86
- package/src/worker-submission-gates.ts +32 -7
|
@@ -92,9 +92,10 @@ import {
|
|
|
92
92
|
} from "../navigator-invocation-identity.ts";
|
|
93
93
|
import {
|
|
94
94
|
NO_RECEIPT_LIFECYCLE_ENTRY_TYPE,
|
|
95
|
-
RECEIPT_DELIVERY_TURN_LIMIT,
|
|
96
95
|
noReceiptLifecycleFacts,
|
|
97
96
|
parseNoReceiptLifecycleFacts,
|
|
97
|
+
priorReceiptContinuation,
|
|
98
|
+
receiptAttemptPointer,
|
|
98
99
|
type NoReceiptLifecycleFacts,
|
|
99
100
|
} from "../receipt-delivery-policy.ts";
|
|
100
101
|
import type {
|
|
@@ -290,17 +291,101 @@ export async function attachRecordedSubmissions<T extends TerminalResult>(
|
|
|
290
291
|
* Does not invent an output failure for an empty ledger.
|
|
291
292
|
* #953: empty public terminal face — clear every reader-adoptable prior face.
|
|
292
293
|
*/
|
|
294
|
+
/**
|
|
295
|
+
* Lifecycle fact the in-process runtime already wrote for this run and attempt.
|
|
296
|
+
* Absent, malformed, or a previous attempt's bytes are not a count.
|
|
297
|
+
*/
|
|
298
|
+
function lifecycleFactsFromEntry(entry: SessionEntry): unknown {
|
|
299
|
+
const customType = entry.customType ?? entry.message?.customType;
|
|
300
|
+
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return undefined;
|
|
301
|
+
return entry.data ?? entry.message?.details;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function readCurrentAttemptNoReceiptFacts(
|
|
305
|
+
admitted: AdmittedRoleInvocation,
|
|
306
|
+
authority: DurablePrincipalAuthority,
|
|
307
|
+
scope?: SettlementCourtScope,
|
|
308
|
+
): Promise<NoReceiptLifecycleFacts | undefined> {
|
|
309
|
+
const { sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
310
|
+
const entries = await readBoundSessionEntries(sessionFile).catch(() => undefined);
|
|
311
|
+
if (entries === undefined) return undefined;
|
|
312
|
+
const scopeId = scope?.invocationScopeId?.trim() ?? "";
|
|
313
|
+
if (scopeId.length > 0) {
|
|
314
|
+
const pointer = receiptAttemptPointer(admitted.runDirectory, scopeId);
|
|
315
|
+
let selected: NoReceiptLifecycleFacts | undefined;
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const raw = lifecycleFactsFromEntry(entry);
|
|
318
|
+
if (raw === undefined) continue;
|
|
319
|
+
try {
|
|
320
|
+
const facts = parseNoReceiptLifecycleFacts(raw);
|
|
321
|
+
if (facts.runPointer !== admitted.runDirectory) continue;
|
|
322
|
+
if (facts.attemptPointer !== pointer) continue;
|
|
323
|
+
if (selected === undefined || facts.deliveryTurns >= selected.deliveryTurns) {
|
|
324
|
+
selected = facts;
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// A malformed historical entry is not this call's count.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return selected;
|
|
331
|
+
}
|
|
332
|
+
let attemptStart = 0;
|
|
333
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
334
|
+
if (entries[index]?.type === "message" && entries[index]?.message?.role === "user") {
|
|
335
|
+
attemptStart = index;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const lifecycleEntry = entries.slice(attemptStart).reverse().find((entry: SessionEntry) =>
|
|
340
|
+
lifecycleFactsFromEntry(entry) !== undefined);
|
|
341
|
+
const raw = lifecycleEntry === undefined ? undefined : lifecycleFactsFromEntry(lifecycleEntry);
|
|
342
|
+
if (raw === undefined) return undefined;
|
|
343
|
+
let facts: NoReceiptLifecycleFacts;
|
|
344
|
+
try {
|
|
345
|
+
facts = parseNoReceiptLifecycleFacts(raw);
|
|
346
|
+
} catch {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
if (facts.runPointer !== admitted.runDirectory) return undefined;
|
|
350
|
+
if (facts.attemptPointer !== receiptAttemptPointer(admitted.runDirectory)) return undefined;
|
|
351
|
+
return facts;
|
|
352
|
+
}
|
|
353
|
+
|
|
293
354
|
export async function settleHostEndedNoReceipt(
|
|
294
355
|
admitted: AdmittedRoleInvocation,
|
|
295
356
|
authority: DurablePrincipalAuthority,
|
|
296
357
|
scope?: SettlementCourtScope,
|
|
358
|
+
/**
|
|
359
|
+
* #1132: delivery requests this run actually issued. Zero stays zero when
|
|
360
|
+
* nothing was sent and this attempt has no lifecycle fact. A fact already
|
|
361
|
+
* written for this run and attempt supplies the count when it is larger.
|
|
362
|
+
* The budget is never the count, and another loop's resumes are not催交.
|
|
363
|
+
*/
|
|
364
|
+
issuedDeliveryRequests = 0,
|
|
297
365
|
): Promise<TerminalResult> {
|
|
366
|
+
const scopeId = scope?.invocationScopeId?.trim() ?? "";
|
|
367
|
+
const persisted = scopeId.length > 0
|
|
368
|
+
? undefined
|
|
369
|
+
: await readCurrentAttemptNoReceiptFacts(admitted, authority, scope);
|
|
370
|
+
let terminalToolCalled = persisted?.terminalToolCalled ?? false;
|
|
371
|
+
let rejectedReceipts: readonly { reason: string }[] = persisted?.rejectedReceipts ?? [];
|
|
372
|
+
let recordedTurns = persisted?.deliveryTurns ?? 0;
|
|
373
|
+
if (scopeId.length > 0) {
|
|
374
|
+
const { sessionFile } = coordinatesFromAdmitted(authority, admitted);
|
|
375
|
+
const entries = await readBoundSessionEntries(sessionFile).catch(() => undefined);
|
|
376
|
+
if (entries !== undefined) {
|
|
377
|
+
const prior = priorReceiptContinuation(entries, scopeId);
|
|
378
|
+
terminalToolCalled = prior.terminalToolCalled;
|
|
379
|
+
rejectedReceipts = prior.rejectedReceipts;
|
|
380
|
+
recordedTurns = prior.deliveryTurns;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
298
383
|
const facts = noReceiptLifecycleFacts({
|
|
299
|
-
terminalToolCalled
|
|
300
|
-
rejectedReceipts
|
|
301
|
-
deliveryTurns:
|
|
384
|
+
terminalToolCalled,
|
|
385
|
+
rejectedReceipts,
|
|
386
|
+
deliveryTurns: Math.max(issuedDeliveryRequests, recordedTurns),
|
|
302
387
|
runPointer: admitted.runDirectory,
|
|
303
|
-
attemptPointer:
|
|
388
|
+
attemptPointer: receiptAttemptPointer(admitted.runDirectory, scope?.invocationScopeId),
|
|
304
389
|
});
|
|
305
390
|
const coordinates = coordinatesFromAdmitted(authority, admitted);
|
|
306
391
|
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
@@ -3191,51 +3276,28 @@ export async function settleFailureTerminalResult(
|
|
|
3191
3276
|
// current-attempt fact. Transcript reconstruction must not turn arbitrary output
|
|
3192
3277
|
// failures (or bytes retained from a prior resume attempt) into exit zero.
|
|
3193
3278
|
if (failure.cause === "output") {
|
|
3194
|
-
const
|
|
3195
|
-
if (
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
) {
|
|
3217
|
-
let decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
|
|
3218
|
-
// #478: no_receipt is still a public Terminal — project accepted gate facts.
|
|
3219
|
-
// #953: empty public terminal face — clear every reader-adoptable prior face.
|
|
3220
|
-
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
3221
|
-
return withOptionalGateProjection(
|
|
3222
|
-
{
|
|
3223
|
-
roleOutcome: {
|
|
3224
|
-
kind: "no_receipt",
|
|
3225
|
-
role: admitted.role,
|
|
3226
|
-
status: "no-accepted-receipt",
|
|
3227
|
-
...facts,
|
|
3228
|
-
decisiveFacts,
|
|
3229
|
-
},
|
|
3230
|
-
navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
|
|
3231
|
-
artifacts: [],
|
|
3232
|
-
runId: admitted.runId,
|
|
3233
|
-
},
|
|
3234
|
-
sessionDirectory,
|
|
3235
|
-
detourGateContext(admitted, options),
|
|
3236
|
-
);
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3279
|
+
const facts = await readCurrentAttemptNoReceiptFacts(admitted, authority, options);
|
|
3280
|
+
if (facts !== undefined) {
|
|
3281
|
+
const decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
|
|
3282
|
+
// #478: no_receipt is still a public Terminal — project accepted gate facts.
|
|
3283
|
+
// #953: empty public terminal face — clear every reader-adoptable prior face.
|
|
3284
|
+
await clearOppositeTerminalArtifactFace(admitted.runDirectory);
|
|
3285
|
+
return withOptionalGateProjection(
|
|
3286
|
+
{
|
|
3287
|
+
roleOutcome: {
|
|
3288
|
+
kind: "no_receipt",
|
|
3289
|
+
role: admitted.role,
|
|
3290
|
+
status: "no-accepted-receipt",
|
|
3291
|
+
...facts,
|
|
3292
|
+
decisiveFacts,
|
|
3293
|
+
},
|
|
3294
|
+
navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
|
|
3295
|
+
artifacts: [],
|
|
3296
|
+
runId: admitted.runId,
|
|
3297
|
+
},
|
|
3298
|
+
sessionDirectory,
|
|
3299
|
+
detourGateContext(admitted, options),
|
|
3300
|
+
);
|
|
3239
3301
|
}
|
|
3240
3302
|
}
|
|
3241
3303
|
// Exact-session attendance only — never infer no-advice from caller omission.
|
|
@@ -263,6 +263,19 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
263
263
|
`diagnostic\t${encodeTerminalField(result.roleOutcome.diagnostic)}`,
|
|
264
264
|
);
|
|
265
265
|
}
|
|
266
|
+
// Accepted volumes do not dump decisiveFacts unless a failed attempt is
|
|
267
|
+
// recorded. The unsettled-direction fact still has to be visible on its own.
|
|
268
|
+
// audit_escalation already dumps every decisiveFact below.
|
|
269
|
+
if (
|
|
270
|
+
result.roleOutcome.kind === "accepted"
|
|
271
|
+
&& result.roleOutcome.decisiveFacts?.directionUnsettled === true
|
|
272
|
+
) {
|
|
273
|
+
lines.push("fact\tdirectionUnsettled\ttrue");
|
|
274
|
+
const subsequent = result.roleOutcome.decisiveFacts.subsequentAudit;
|
|
275
|
+
if (typeof subsequent === "string") {
|
|
276
|
+
lines.push(`fact\tsubsequentAudit\t${encodeTerminalField(subsequent)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
266
279
|
if (result.roleOutcome.kind === "failure" || result.roleOutcome.kind === "no_receipt" || result.roleOutcome.kind === "audit_escalation" || result.roleOutcome.decisiveFacts?.failedAttempts !== undefined) {
|
|
267
280
|
const facts = result.roleOutcome.kind === "accepted"
|
|
268
281
|
? { failedAttempts: result.roleOutcome.decisiveFacts?.failedAttempts }
|
|
@@ -337,11 +350,10 @@ export function formatTerminalResult(result: TerminalResult): string {
|
|
|
337
350
|
lines.push(`recorded-submission\t${encodeTerminalField(rendered)}`);
|
|
338
351
|
}
|
|
339
352
|
} else {
|
|
340
|
-
const payloads =
|
|
341
|
-
result.roleOutcome.kind === "accepted" ||
|
|
353
|
+
const payloads = result.roleOutcome.kind === "accepted" ||
|
|
342
354
|
result.roleOutcome.kind === "audit_escalation"
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
? coalesceSubmissionRows(result.submissions, result.roleOutcome.payloads)
|
|
356
|
+
: result.submissions ?? [];
|
|
345
357
|
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
|
346
358
|
const payload = payloads[i]!;
|
|
347
359
|
const rendered =
|
|
@@ -70,7 +70,13 @@ export type RoleTurnRequestProjectionOptions = {
|
|
|
70
70
|
/** #537 public-invocation scope (one ak-role call). */
|
|
71
71
|
invocationScopeId?: string;
|
|
72
72
|
/** Station child role run (#840): omit automatic navigator attendance. */
|
|
73
|
-
stationChild?: boolean;
|
|
73
|
+
readonly stationChild?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* #1132: the one effective delivery-request ceiling (#422 single resolve by
|
|
76
|
+
* the caller). Projected so the host adapter's own re-ask loop and the AK
|
|
77
|
+
* execution seam count with the same configured number.
|
|
78
|
+
*/
|
|
79
|
+
readonly deliveryRequestLimit?: number;
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
export type AdmittedTurnInvocation = {
|
|
@@ -118,6 +124,9 @@ export function projectRoleTurnRequest(
|
|
|
118
124
|
? {}
|
|
119
125
|
: { invocationScopeId: options.invocationScopeId }),
|
|
120
126
|
...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
|
|
127
|
+
...(options.deliveryRequestLimit === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { deliveryRequestLimit: options.deliveryRequestLimit }),
|
|
121
130
|
};
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -1,14 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared accepted-receipt delivery budget for role, auditor, and Navigator sessions (#288).
|
|
3
|
+
*
|
|
4
|
+
* #1132: the ceiling is the one configured auto-resume value (public-cli.json
|
|
5
|
+
* top-level `autoResumeLimit`). This module owns no number of its own — the one
|
|
6
|
+
* package default stays with AUTO_RESUME_LIMIT (#422), and `deliveryTurns`
|
|
7
|
+
* records the delivery requests actually issued, never the budget
|
|
8
|
+
* (零次即记零,不把额度或其他续跑次数冒充催交次数).
|
|
9
|
+
*/
|
|
10
|
+
import { AUTO_RESUME_LIMIT } from "./public-cli/run-lifecycle.ts";
|
|
11
|
+
import { parseAutoResumeLimit } from "./public-cli/config.ts";
|
|
1
12
|
import { isRecord } from "./unknown-value.ts";
|
|
2
|
-
/** Shared accepted-receipt delivery budget for role, auditor, and Navigator sessions (#288). */
|
|
3
|
-
export const RECEIPT_DELIVERY_TURN_LIMIT = 2 as const;
|
|
4
13
|
|
|
5
14
|
export const NO_RECEIPT_LIFECYCLE_ENTRY_TYPE = "ak-no-receipt-lifecycle" as const;
|
|
6
15
|
|
|
16
|
+
/** One in-process催交 send, tagged with the public call that issued it. */
|
|
17
|
+
export const RECEIPT_DELIVERY_REQUEST_ENTRY = "ak-receipt-delivery-request" as const;
|
|
18
|
+
|
|
19
|
+
/** A rejection observed by the output-tool hook, bound before any delivery turn. */
|
|
20
|
+
export const RECEIPT_REJECTION_ENTRY = "ak-receipt-rejection" as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Facts of one public call. A call with an invocation scope does not share
|
|
24
|
+
* `current:<runDirectory>` with the previous call on the same run.
|
|
25
|
+
*/
|
|
26
|
+
export function receiptAttemptPointer(runPointer: string, invocationScopeId?: string): string {
|
|
27
|
+
const scope = invocationScopeId?.trim() ?? "";
|
|
28
|
+
if (scope.length > 0) return `invocation:${scope}`;
|
|
29
|
+
return `current:${runPointer}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Child-process transport for the effective ceiling (#1132). */
|
|
33
|
+
export const RECEIPT_DELIVERY_LIMIT_ENV = "AK_ROLE_RECEIPT_DELIVERY_LIMIT" as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The effective receipt-delivery ceiling: the caller's already-resolved
|
|
37
|
+
* configured value, else the single package default. Domain-validated at this
|
|
38
|
+
* one seam (#422) so NaN/negative/fractional/Infinity never silently disable
|
|
39
|
+
* or bypass the comparison downstream.
|
|
40
|
+
*/
|
|
41
|
+
export function deliveryLimitFromConfig(value: number | undefined): number {
|
|
42
|
+
return parseAutoResumeLimit(value ?? AUTO_RESUME_LIMIT);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Effective ceiling for an in-child role runtime reading its own env (#1132). */
|
|
46
|
+
export function deliveryLimitFromEnv(env: NodeJS.ProcessEnv): number {
|
|
47
|
+
const raw = env[RECEIPT_DELIVERY_LIMIT_ENV];
|
|
48
|
+
if (raw === undefined || raw.trim() === "") return deliveryLimitFromConfig(undefined);
|
|
49
|
+
const parsed = Number(raw);
|
|
50
|
+
if (!Number.isInteger(parsed)) return deliveryLimitFromConfig(undefined);
|
|
51
|
+
return deliveryLimitFromConfig(parsed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deliveryTurnsFact(value: unknown): number {
|
|
55
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
56
|
+
throw new TypeError("deliveryTurns must be a non-negative integer");
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
7
61
|
/** The sole schema shared by lifecycle owners and Terminal projections. */
|
|
8
62
|
export type NoReceiptLifecycleFacts = {
|
|
9
63
|
terminalToolCalled: boolean;
|
|
10
64
|
rejectedReceipts: readonly { reason: string; diagnosticAvailable: boolean }[];
|
|
11
|
-
|
|
65
|
+
/** Delivery requests actually issued on this run (#1132 — not a budget). */
|
|
66
|
+
deliveryTurns: number;
|
|
12
67
|
sessionCompletion: "settled-without-accepted-receipt";
|
|
13
68
|
runPointer: string;
|
|
14
69
|
attemptPointer: string;
|
|
@@ -19,7 +74,6 @@ export type NoReceiptLifecycleFacts = {
|
|
|
19
74
|
export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycleFacts {
|
|
20
75
|
if (!isRecord(input)
|
|
21
76
|
|| typeof input.terminalToolCalled !== "boolean"
|
|
22
|
-
|| input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT
|
|
23
77
|
|| input.sessionCompletion !== "settled-without-accepted-receipt"
|
|
24
78
|
|| input.acceptedReceipt !== false
|
|
25
79
|
|| typeof input.runPointer !== "string" || input.runPointer.trim() === ""
|
|
@@ -35,7 +89,7 @@ export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycle
|
|
|
35
89
|
reason: item.reason as string,
|
|
36
90
|
diagnosticAvailable: (item.reason as string).trim() !== "",
|
|
37
91
|
})),
|
|
38
|
-
deliveryTurns:
|
|
92
|
+
deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
|
|
39
93
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
40
94
|
runPointer: input.runPointer,
|
|
41
95
|
attemptPointer: input.attemptPointer,
|
|
@@ -43,22 +97,24 @@ export function parseNoReceiptLifecycleFacts(input: unknown): NoReceiptLifecycle
|
|
|
43
97
|
};
|
|
44
98
|
}
|
|
45
99
|
|
|
100
|
+
/**
|
|
101
|
+
* #1132: exhaustion is the caller's decision — the receipt-delivery loop only
|
|
102
|
+
* settles after its budget is actually spent — so this records the issued
|
|
103
|
+
* count verbatim instead of demanding it equal a package constant.
|
|
104
|
+
*/
|
|
46
105
|
export function noReceiptLifecycleFacts(
|
|
47
106
|
input: Omit<NoReceiptLifecycleFacts, "rejectedReceipts" | "deliveryTurns" | "sessionCompletion" | "acceptedReceipt"> & {
|
|
48
107
|
rejectedReceipts: readonly { reason: string }[];
|
|
49
108
|
deliveryTurns: number;
|
|
50
109
|
},
|
|
51
110
|
): NoReceiptLifecycleFacts {
|
|
52
|
-
if (input.deliveryTurns !== RECEIPT_DELIVERY_TURN_LIMIT) {
|
|
53
|
-
throw new TypeError("no-receipt lifecycle requires an exhausted delivery budget");
|
|
54
|
-
}
|
|
55
111
|
return {
|
|
56
112
|
terminalToolCalled: input.terminalToolCalled,
|
|
57
113
|
rejectedReceipts: input.rejectedReceipts.map(({ reason }) => ({
|
|
58
114
|
reason,
|
|
59
115
|
diagnosticAvailable: reason.trim() !== "",
|
|
60
116
|
})),
|
|
61
|
-
deliveryTurns:
|
|
117
|
+
deliveryTurns: deliveryTurnsFact(input.deliveryTurns),
|
|
62
118
|
sessionCompletion: "settled-without-accepted-receipt",
|
|
63
119
|
runPointer: input.runPointer,
|
|
64
120
|
attemptPointer: input.attemptPointer,
|
|
@@ -66,39 +122,66 @@ export function noReceiptLifecycleFacts(
|
|
|
66
122
|
};
|
|
67
123
|
}
|
|
68
124
|
|
|
69
|
-
export function createReceiptDeliveryPolicy() {
|
|
125
|
+
export function createReceiptDeliveryPolicy(limit?: number) {
|
|
126
|
+
const deliveryLimit = deliveryLimitFromConfig(limit);
|
|
70
127
|
let accepted = false;
|
|
128
|
+
let closed = false;
|
|
71
129
|
let terminalToolCalled = false;
|
|
130
|
+
/** Requests actually sent. Rejections and a nested close do not invent sends. */
|
|
72
131
|
let deliveryTurns = 0;
|
|
132
|
+
/** Stop counter. Separate from the issued-count fact (#1132). */
|
|
133
|
+
let budgetSpent = 0;
|
|
73
134
|
const rejectedReceipts: { reason: string; diagnosticAvailable: boolean }[] = [];
|
|
135
|
+
const spend = (): void => {
|
|
136
|
+
if (budgetSpent < deliveryLimit) budgetSpent += 1;
|
|
137
|
+
};
|
|
74
138
|
return {
|
|
139
|
+
/** The effective ceiling this budget spends (#1132 — never re-read). */
|
|
140
|
+
limit: deliveryLimit,
|
|
75
141
|
recordAccepted() {
|
|
76
142
|
accepted = true;
|
|
77
143
|
terminalToolCalled = true;
|
|
78
144
|
},
|
|
79
145
|
/** Infrastructure owns terminality and must never trigger receipt催交. */
|
|
80
146
|
stopForInfrastructure() { accepted = true; },
|
|
147
|
+
/**
|
|
148
|
+
* Record the rejection this turn already produced.
|
|
149
|
+
* The fact does not spend a budget. The seam that sends the next prompt
|
|
150
|
+
* owns that count.
|
|
151
|
+
*/
|
|
81
152
|
recordRejected(reason: string) {
|
|
82
153
|
terminalToolCalled = true;
|
|
83
154
|
rejectedReceipts.push({ reason, diagnosticAvailable: reason.trim() !== "" });
|
|
84
|
-
deliveryTurns = Math.min(RECEIPT_DELIVERY_TURN_LIMIT, deliveryTurns + 1);
|
|
85
155
|
},
|
|
86
156
|
recordDeliveryRequest() {
|
|
87
|
-
deliveryTurns
|
|
157
|
+
deliveryTurns += 1;
|
|
158
|
+
spend();
|
|
88
159
|
},
|
|
89
160
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
161
|
+
* Stop asking. The issued count stays whatever was actually sent (#1132 —
|
|
162
|
+
* do not fill the remainder up to the ceiling).
|
|
163
|
+
*/
|
|
164
|
+
closeBudget() { closed = true; },
|
|
165
|
+
/**
|
|
166
|
+
* A nested session settled without an accepted receipt. Close so this layer
|
|
167
|
+
* does not open another prompt on that session. The issued count stays the
|
|
168
|
+
* larger of the two seams: a nested zero must not wipe prompts this layer
|
|
169
|
+
* already sent, and adding the two counts would bill one send twice.
|
|
93
170
|
*/
|
|
94
171
|
recordNestedNoReceipt(facts: NoReceiptLifecycleFacts) {
|
|
95
172
|
terminalToolCalled = terminalToolCalled || facts.terminalToolCalled;
|
|
96
173
|
rejectedReceipts.push(...facts.rejectedReceipts);
|
|
97
|
-
deliveryTurns =
|
|
174
|
+
deliveryTurns = Math.max(deliveryTurns, facts.deliveryTurns);
|
|
175
|
+
closed = true;
|
|
98
176
|
},
|
|
99
177
|
nextAction(): "accepted" | "request-delivery" | "no-receipt" {
|
|
100
178
|
if (accepted) return "accepted";
|
|
101
|
-
|
|
179
|
+
if (closed) return "no-receipt";
|
|
180
|
+
return budgetSpent < deliveryLimit ? "request-delivery" : "no-receipt";
|
|
181
|
+
},
|
|
182
|
+
/** Delivery requests actually issued so far (#1132). */
|
|
183
|
+
issuedDeliveryRequests(): number {
|
|
184
|
+
return deliveryTurns;
|
|
102
185
|
},
|
|
103
186
|
/** Current typed delivery facts. No prose. */
|
|
104
187
|
deliveryState() {
|
|
@@ -112,5 +195,114 @@ export function createReceiptDeliveryPolicy() {
|
|
|
112
195
|
facts(binding: { runPointer: string; attemptPointer: string }): NoReceiptLifecycleFacts {
|
|
113
196
|
return noReceiptLifecycleFacts({ terminalToolCalled, rejectedReceipts: [...rejectedReceipts], deliveryTurns, ...binding });
|
|
114
197
|
},
|
|
198
|
+
/**
|
|
199
|
+
* Continue a public call after the in-memory policy was rebuilt.
|
|
200
|
+
* Sends already issued spend the same budget; copied rejections keep
|
|
201
|
+
* the fact and do not spend again.
|
|
202
|
+
*/
|
|
203
|
+
continueIssued(prior: {
|
|
204
|
+
readonly deliveryTurns: number;
|
|
205
|
+
readonly rejectedReceipts: readonly { reason: string }[];
|
|
206
|
+
readonly terminalToolCalled?: boolean;
|
|
207
|
+
}) {
|
|
208
|
+
for (const receipt of prior.rejectedReceipts) {
|
|
209
|
+
this.recordRejected(receipt.reason);
|
|
210
|
+
}
|
|
211
|
+
if (prior.terminalToolCalled === true) terminalToolCalled = true;
|
|
212
|
+
for (let index = 0; index < prior.deliveryTurns; index += 1) {
|
|
213
|
+
this.recordDeliveryRequest();
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function entryRecord(entry: unknown): Record<string, unknown> | undefined {
|
|
220
|
+
return isRecord(entry) ? entry : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Sends, observed rejections, and the latest lifecycle fact for this public call. */
|
|
224
|
+
export type ReceiptContinuation = {
|
|
225
|
+
readonly deliveryTurns: number;
|
|
226
|
+
readonly rejectedReceipts: readonly { reason: string }[];
|
|
227
|
+
readonly terminalToolCalled: boolean;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
function customTypeOf(record: Record<string, unknown>, message: Record<string, unknown> | undefined): unknown {
|
|
231
|
+
return record.customType ?? message?.customType;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function dataOf(record: Record<string, unknown>, message: Record<string, unknown> | undefined): unknown {
|
|
235
|
+
return record.data ?? message?.details;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Scope id carried by a delivery request or an invocation-scoped lifecycle fact. */
|
|
239
|
+
function scopeTag(record: Record<string, unknown>): string | undefined {
|
|
240
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
241
|
+
const customType = customTypeOf(record, message);
|
|
242
|
+
const data = dataOf(record, message);
|
|
243
|
+
if (
|
|
244
|
+
(customType === RECEIPT_DELIVERY_REQUEST_ENTRY || customType === RECEIPT_REJECTION_ENTRY)
|
|
245
|
+
&& isRecord(data)
|
|
246
|
+
&& typeof data.invocationScopeId === "string"
|
|
247
|
+
&& data.invocationScopeId.trim() !== ""
|
|
248
|
+
) {
|
|
249
|
+
return data.invocationScopeId;
|
|
250
|
+
}
|
|
251
|
+
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) return undefined;
|
|
252
|
+
try {
|
|
253
|
+
const facts = parseNoReceiptLifecycleFacts(data);
|
|
254
|
+
const prefix = "invocation:";
|
|
255
|
+
if (!facts.attemptPointer.startsWith(prefix)) return undefined;
|
|
256
|
+
const scope = facts.attemptPointer.slice(prefix.length);
|
|
257
|
+
return scope.trim() === "" ? undefined : scope;
|
|
258
|
+
} catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Restore only facts explicitly bound to this public call, never inferred from dialogue. */
|
|
264
|
+
export function priorReceiptContinuation(
|
|
265
|
+
entries: Iterable<unknown>,
|
|
266
|
+
invocationScopeId: string,
|
|
267
|
+
): ReceiptContinuation {
|
|
268
|
+
const pointer = receiptAttemptPointer("", invocationScopeId);
|
|
269
|
+
let entryTurns = 0;
|
|
270
|
+
let factTurns = 0;
|
|
271
|
+
let rejectedReceipts: { reason: string }[] = [];
|
|
272
|
+
let snapshotTerminal = false;
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
const record = entryRecord(entry);
|
|
275
|
+
if (record === undefined) continue;
|
|
276
|
+
const tagged = scopeTag(record);
|
|
277
|
+
if (tagged !== undefined) {
|
|
278
|
+
const message = isRecord(record.message) ? record.message : undefined;
|
|
279
|
+
const customType = customTypeOf(record, message);
|
|
280
|
+
if (tagged !== invocationScopeId) continue;
|
|
281
|
+
if (customType === RECEIPT_DELIVERY_REQUEST_ENTRY) entryTurns += 1;
|
|
282
|
+
if (customType === RECEIPT_REJECTION_ENTRY) {
|
|
283
|
+
const data = dataOf(record, message);
|
|
284
|
+
if (isRecord(data) && typeof data.reason === "string") rejectedReceipts.push({ reason: data.reason });
|
|
285
|
+
}
|
|
286
|
+
if (customType !== NO_RECEIPT_LIFECYCLE_ENTRY_TYPE) continue;
|
|
287
|
+
try {
|
|
288
|
+
const facts = parseNoReceiptLifecycleFacts(dataOf(record, message));
|
|
289
|
+
if (facts.attemptPointer !== pointer) continue;
|
|
290
|
+
factTurns = Math.max(factTurns, facts.deliveryTurns);
|
|
291
|
+
// Snapshots include all preceding observations. Replay later events,
|
|
292
|
+
// rather than treating their free-text reasons as deduplication keys.
|
|
293
|
+
if (facts.rejectedReceipts.length >= rejectedReceipts.length) {
|
|
294
|
+
rejectedReceipts = facts.rejectedReceipts.map(({ reason }) => ({ reason }));
|
|
295
|
+
}
|
|
296
|
+
snapshotTerminal ||= facts.terminalToolCalled;
|
|
297
|
+
} catch {
|
|
298
|
+
// A malformed historical entry is not this call's count.
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
deliveryTurns: Math.max(entryTurns, factTurns),
|
|
305
|
+
rejectedReceipts,
|
|
306
|
+
terminalToolCalled: snapshotTerminal || rejectedReceipts.length > 0,
|
|
115
307
|
};
|
|
116
308
|
}
|
package/src/reviewer-role.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import type { RoleHost, HostContext
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { openToolObject } from "./open-tool-schema.ts";
|
|
4
|
-
import { withTerminatingOutputDeclarations } from "./package-contracts/terminating-infrastructure.ts";
|
|
1
|
+
import type { RoleHost, HostContext } from "./host-contracts.ts";
|
|
5
2
|
|
|
6
|
-
import {
|
|
3
|
+
import { registerFiledSubmissionTool } from "./filed-submission.ts";
|
|
4
|
+
import { roleSubmissionDeclaration } from "./role-submission-declarations.ts";
|
|
7
5
|
|
|
8
|
-
export { REVIEWER_OUTPUT_TOOL_NAME };
|
|
9
|
-
export type { ReviewerIntent };
|
|
6
|
+
export { REVIEWER_OUTPUT_TOOL_NAME, reviewerOutputSchema, type ReviewerIntent } from "./package-contracts/reviewer-output.ts";
|
|
10
7
|
|
|
11
8
|
/** Frozen admitted inputs the behavior layer may consume — no flag surface. */
|
|
12
9
|
export type ReviewerAdmittedInputs = Readonly<{
|
|
@@ -17,25 +14,6 @@ export type ReviewerAdmittedInputs = Readonly<{
|
|
|
17
14
|
ticketNumber?: number;
|
|
18
15
|
}>;
|
|
19
16
|
|
|
20
|
-
const REVIEWER_AMENDMENTS_DESCRIPTION =
|
|
21
|
-
"已运行 lens 的 amendments 承载 candidates、逐条处置与 verdict 的完整报告(非仅 verdict 行);显式单轴时另一轴可省略(completeness / correctness)。hard-stop/usage error 为 refused 时,已产出报告仍照录对应 lens 字段。" as const;
|
|
22
|
-
// #1134: `status` alone is the trajectory field (#917 §6 two normal lens verdicts →
|
|
23
|
-
// completed; hard-stop / usage error → refused); its words ride the description.
|
|
24
|
-
// amendments/diagnostic are LLM/human-read narrative content returned as
|
|
25
|
-
// submitted — declaration keeps name + semantic description only, no nested
|
|
26
|
-
// object shape, no string type, no required constraint.
|
|
27
|
-
const REVIEWER_STATUS_DESCRIPTION =
|
|
28
|
-
"completed | refused。两种正常 lens verdict(completeness / correctness)均 completed;hard-stop 与 usage error 为 refused(已产出报告仍照录进所选 lens amendments)。" as const;
|
|
29
|
-
const reviewerOutputObject = Type.Object({
|
|
30
|
-
status: Type.Unknown({ description: REVIEWER_STATUS_DESCRIPTION }),
|
|
31
|
-
amendments: Type.Unknown({ description: REVIEWER_AMENDMENTS_DESCRIPTION }),
|
|
32
|
-
diagnostic: Type.Unknown({
|
|
33
|
-
description: "拒绝诊断说明;hard-stop/usage error 原因进此字段,已产出报告另照录 amendments",
|
|
34
|
-
}),
|
|
35
|
-
});
|
|
36
|
-
export const reviewerOutputSchema = withTerminatingOutputDeclarations(
|
|
37
|
-
openToolObject(reviewerOutputObject),
|
|
38
|
-
);
|
|
39
17
|
export type ReviewerRoleDependencies = {
|
|
40
18
|
loadSoul(): Promise<string>;
|
|
41
19
|
};
|
|
@@ -70,15 +48,9 @@ export function createReviewerRoleRuntime(
|
|
|
70
48
|
|
|
71
49
|
if (!registered) {
|
|
72
50
|
registered = true;
|
|
73
|
-
pi
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
content: [],
|
|
78
|
-
details: parameters,
|
|
79
|
-
terminate: true as const,
|
|
80
|
-
};
|
|
81
|
-
} });
|
|
51
|
+
registerFiledSubmissionTool(pi, roleSubmissionDeclaration("reviewer"), {
|
|
52
|
+
readyError: () => (soul === undefined || soul.length === 0 ? "御史台输入未装载" : undefined),
|
|
53
|
+
});
|
|
82
54
|
}
|
|
83
55
|
const activatedSoul = soul;
|
|
84
56
|
const activatedBase = fixedBaseRevision;
|
package/src/role-envelope.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
RoleTurnRequest,
|
|
20
20
|
} from "./host-contracts.ts";
|
|
21
21
|
import { packagedRoleOutputTool } from "./packaged-role-registry.ts";
|
|
22
|
+
import { deliveryLimitFromConfig } from "./receipt-delivery-policy.ts";
|
|
22
23
|
import {
|
|
23
24
|
createRoleRuntimeExtension,
|
|
24
25
|
type RoleRuntimeDependencies,
|
|
@@ -250,7 +251,10 @@ export async function prepareRoleEnvelope(options: {
|
|
|
250
251
|
startKeepalive() {},
|
|
251
252
|
stopKeepalive() {},
|
|
252
253
|
};
|
|
253
|
-
createRoleRuntimeExtension(
|
|
254
|
+
createRoleRuntimeExtension(
|
|
255
|
+
options.dependencies,
|
|
256
|
+
deliveryLimitFromConfig(options.request.deliveryRequestLimit),
|
|
257
|
+
)(envelope);
|
|
254
258
|
|
|
255
259
|
const token = randomUUID();
|
|
256
260
|
const server = createServer((socket) => serveSocket(socket));
|
|
@@ -64,6 +64,7 @@ export function createRoleRuntimeDependencies(packageRoot: string): RoleRuntimeD
|
|
|
64
64
|
authority: options.authority,
|
|
65
65
|
invocationId: options.invocationId,
|
|
66
66
|
createSession: navigatorSessionFactory,
|
|
67
|
+
...(options.deliveryRequestLimit === undefined ? {} : { deliveryRequestLimit: options.deliveryRequestLimit }),
|
|
67
68
|
...(options.contextError === undefined ? {} : { contextError: options.contextError }),
|
|
68
69
|
onEvent: options.onEvent,
|
|
69
70
|
}),
|