@sublang/playbook 7.0.0 → 8.0.0
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 +17 -4
- package/docs/cli.md +74 -29
- package/docs/configuration.md +209 -112
- package/docs/embedding.md +71 -25
- package/package.json +4 -3
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +3 -3
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
- package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
- package/reference/sdlc/code.playbook/bin/run.js +283 -298
- package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
- package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.js +7 -11
- package/reference/sdlc/code.playbook/code.fsm.ts +9 -17
- package/reference/sdlc/code.playbook/code.gears.md +1 -1
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
- package/reference/sdlc/code.playbook/code.playbook.js +12 -13
- package/reference/sdlc/code.playbook/code.playbook.ts +22 -15
- package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
- package/reference/sdlc/code.playbook/code.registry.js +3 -10
- package/reference/sdlc/code.playbook/code.registry.ts +7 -32
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
- package/reference/sdlc/code.playbook/playbook-captain.js +970 -289
- package/reference/sdlc/code.playbook/playbook-captain.ts +1403 -396
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
- package/reference/sdlc/decide.md +4 -4
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +9 -9
- package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
- package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
- package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +9 -13
- package/reference/sdlc/decide.playbook/decide.playbook.js +171 -134
- package/reference/sdlc/decide.playbook/decide.playbook.ts +238 -162
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
- package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
- package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
- package/reference/sdlc/review.md +4 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
- package/reference/sdlc/review.playbook/review.fsm.js +30 -24
- package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
- package/reference/sdlc/review.playbook/review.gears.md +6 -5
- package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
- package/reference/sdlc/review.playbook/review.playbook.js +16 -21
- package/reference/sdlc/review.playbook/review.playbook.ts +26 -26
- package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
- package/reference/sdlc/review.playbook/review.registry.js +3 -16
- package/reference/sdlc/review.playbook/review.registry.ts +7 -38
- package/slc/gears2fsm.md +27 -23
- package/slc/link.md +113 -93
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +20 -16
- package/src/runtime.ts +19 -23
- package/src/xstate-playbook-runtime.d.ts +21 -17
- package/src/xstate-playbook-runtime.js +241 -149
- package/src/xstate-playbook-runtime.ts +331 -178
- package/src/xstate-runtime.js +63 -24
- package/src/xstate-runtime.ts +96 -28
|
@@ -66,13 +66,13 @@ export interface PlaybookPendingBossQuestionContext {
|
|
|
66
66
|
questionId: string;
|
|
67
67
|
resumeStateId: string;
|
|
68
68
|
sourceItem: string;
|
|
69
|
-
|
|
69
|
+
asker: { kind: 'captain' } | { kind: 'role'; roleId: string };
|
|
70
70
|
question: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
export interface PlaybookPlayerInput {
|
|
74
74
|
stateId: string;
|
|
75
|
-
|
|
75
|
+
role: string;
|
|
76
76
|
sourceItem: string;
|
|
77
77
|
prompt: string;
|
|
78
78
|
result: Readonly<Record<string, string>>;
|
|
@@ -113,7 +113,7 @@ export type JudgePurpose =
|
|
|
113
113
|
export interface RuntimeBoundaryCalls {
|
|
114
114
|
callPlayer(
|
|
115
115
|
input: PlaybookPlayerInput,
|
|
116
|
-
|
|
116
|
+
roleId: string,
|
|
117
117
|
prompt: string,
|
|
118
118
|
signal: AbortSignal,
|
|
119
119
|
): Promise<PlayerResult>;
|
|
@@ -151,11 +151,14 @@ export interface ScheduledStatus {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/** Boss-facing identity for one FSM state whose invoked actor is `player`. */
|
|
154
|
-
export interface
|
|
155
|
-
|
|
154
|
+
export interface XStateRoleStateStatus {
|
|
155
|
+
role: string;
|
|
156
156
|
label: string;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/** Invocation-scoped lookup exposed only while composing a player prompt. */
|
|
160
|
+
export type XStatePromptIdentity = (roleId: string) => string;
|
|
161
|
+
|
|
159
162
|
export interface XStateBossEventFieldSpec {
|
|
160
163
|
/** The judge supplies routing data; the runtime supplies exact Boss text. */
|
|
161
164
|
source: 'judge' | 'text';
|
|
@@ -247,7 +250,7 @@ export const RUNTIME_ABI = 1;
|
|
|
247
250
|
|
|
248
251
|
/** The linked-artifact schema versions this engine accepts (DR-022). */
|
|
249
252
|
export const SUPPORTED_ARTIFACT_SCHEMAS: readonly number[] = Object.freeze([
|
|
250
|
-
|
|
253
|
+
2,
|
|
251
254
|
]);
|
|
252
255
|
|
|
253
256
|
/** A linked artifact's declared link-time compatibility values (DR-022). */
|
|
@@ -259,14 +262,17 @@ export interface XStatePlaybookRuntimeCompat {
|
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
// PBRT-50: validate a declaration against the loaded engine, schema first,
|
|
262
|
-
// so one clear diagnostic covers a fully skewed artifact.
|
|
263
|
-
//
|
|
264
|
-
// loading unchanged (DR-019 §4), so there is nothing to check.
|
|
265
|
+
// so one clear diagnostic covers a fully skewed artifact. Declaration-free
|
|
266
|
+
// artifacts are schema 1 and cannot be interpreted as local-role artifacts.
|
|
265
267
|
function assertRuntimeCompat(
|
|
266
268
|
compat: XStatePlaybookRuntimeCompat | undefined,
|
|
267
269
|
label: string,
|
|
268
270
|
): void {
|
|
269
|
-
if (compat === undefined)
|
|
271
|
+
if (compat === undefined) {
|
|
272
|
+
throw new TypeError(
|
|
273
|
+
`${label} spec.compat is required for local-role artifacts`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
270
276
|
if (compat === null || typeof compat !== 'object') {
|
|
271
277
|
throw new TypeError(`${label} spec.compat must be an object`);
|
|
272
278
|
}
|
|
@@ -355,8 +361,8 @@ export interface XStatePlaybookRuntimeSpec<TOptions> {
|
|
|
355
361
|
label?: string;
|
|
356
362
|
/**
|
|
357
363
|
* Link-time compatibility declaration checked at construction against the
|
|
358
|
-
* loaded engine's self-report (DR-022). Absent
|
|
359
|
-
*
|
|
364
|
+
* loaded engine's self-report (DR-022). Absent declarations reject because
|
|
365
|
+
* their overloaded player metadata has no safe local-role interpretation.
|
|
360
366
|
*/
|
|
361
367
|
compat?: XStatePlaybookRuntimeCompat;
|
|
362
368
|
/** Validate and JSON-snapshot the caller's per-run options. */
|
|
@@ -397,14 +403,15 @@ export interface XStatePlaybookRuntimeSpec<TOptions> {
|
|
|
397
403
|
* recoverable FSM-result failures instead.
|
|
398
404
|
*/
|
|
399
405
|
captainStrategy?: XStateCaptainStrategy<TOptions>;
|
|
400
|
-
/** Status line emitted after classification;
|
|
406
|
+
/** Status line emitted after classification; metadata defaults to the event type. */
|
|
401
407
|
classificationStatus?: (event: EventObject) => string | undefined;
|
|
402
|
-
/** Complete FSM-derived Boss-facing metadata for every `player` state
|
|
403
|
-
|
|
404
|
-
/** Map a player-invoking state's input to the host player id. Default: lowercased player name. */
|
|
405
|
-
resolvePlayerId?: (input: PlaybookPlayerInput, options: TOptions) => string;
|
|
408
|
+
/** Complete FSM-derived Boss-facing metadata for every `player` state. */
|
|
409
|
+
roleStates?: Readonly<Record<string, XStateRoleStateStatus>>;
|
|
406
410
|
/** Compose the player prompt. Default: continuation blocks + `<field>` placeholder substitution. */
|
|
407
|
-
composePlayerPrompt?: (
|
|
411
|
+
composePlayerPrompt?: (
|
|
412
|
+
input: PlaybookPlayerInput,
|
|
413
|
+
promptIdentity: XStatePromptIdentity,
|
|
414
|
+
) => string;
|
|
408
415
|
/** Compose the direct-Captain prompt. Default: continuation blocks + placeholder substitution with deterministic JSON rendering. */
|
|
409
416
|
composeCaptainPrompt?: (input: PlaybookCaptainInput) => string;
|
|
410
417
|
/** Linker-known exceptions to the default kebab-token → camel-field mapping. */
|
|
@@ -594,16 +601,34 @@ export function pendingBossQuestionFromContext(
|
|
|
594
601
|
typeof candidate.questionId !== 'string' ||
|
|
595
602
|
typeof candidate.resumeStateId !== 'string' ||
|
|
596
603
|
typeof candidate.sourceItem !== 'string' ||
|
|
597
|
-
|
|
604
|
+
!isPlainObject(candidate.asker) ||
|
|
598
605
|
typeof candidate.question !== 'string'
|
|
599
606
|
) {
|
|
600
607
|
return undefined;
|
|
601
608
|
}
|
|
609
|
+
let asker: PlaybookPendingBossQuestionContext['asker'];
|
|
610
|
+
if (candidate.asker.kind === 'captain') {
|
|
611
|
+
if (Object.keys(candidate.asker).some((key) => key !== 'kind')) {
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
asker = { kind: 'captain' };
|
|
615
|
+
} else if (
|
|
616
|
+
candidate.asker.kind === 'role' &&
|
|
617
|
+
typeof candidate.asker.roleId === 'string' &&
|
|
618
|
+
candidate.asker.roleId.trim().length > 0 &&
|
|
619
|
+
Object.keys(candidate.asker).every(
|
|
620
|
+
(key) => key === 'kind' || key === 'roleId',
|
|
621
|
+
)
|
|
622
|
+
) {
|
|
623
|
+
asker = { kind: 'role', roleId: candidate.asker.roleId };
|
|
624
|
+
} else {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
602
627
|
return {
|
|
603
628
|
questionId: candidate.questionId,
|
|
604
629
|
resumeStateId: candidate.resumeStateId,
|
|
605
630
|
sourceItem: candidate.sourceItem,
|
|
606
|
-
|
|
631
|
+
asker,
|
|
607
632
|
question: candidate.question,
|
|
608
633
|
};
|
|
609
634
|
}
|
|
@@ -707,11 +732,6 @@ export function defaultComposeCaptainPrompt(
|
|
|
707
732
|
return blocks.join('\n\n');
|
|
708
733
|
}
|
|
709
734
|
|
|
710
|
-
/** Default player binding: each player to its lowercased name. */
|
|
711
|
-
export function defaultResolvePlayerId(input: PlaybookPlayerInput): string {
|
|
712
|
-
return input.player.toLowerCase();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
735
|
/**
|
|
716
736
|
* Default required-field extraction (slc/link.md §Captain adjudication).
|
|
717
737
|
* Limited to the description's `Output shall include` / `输出应包含` clause;
|
|
@@ -748,7 +768,7 @@ export function defaultBuildJudgePrompt(
|
|
|
748
768
|
'and outcome descriptions. Reply with exactly one JSON object and no prose.',
|
|
749
769
|
);
|
|
750
770
|
lines.push('');
|
|
751
|
-
lines.push(`The ${input.
|
|
771
|
+
lines.push(`The ${input.role} role just produced this output:`);
|
|
752
772
|
lines.push('');
|
|
753
773
|
lines.push('```');
|
|
754
774
|
lines.push(finalText);
|
|
@@ -856,7 +876,8 @@ function validateBossReplyOutput(
|
|
|
856
876
|
|
|
857
877
|
// ---------------------------------------------------------------------------
|
|
858
878
|
// Delegated-player actor bridge. One PromiseActorLogic the machine invokes
|
|
859
|
-
// from every player-invoking state:
|
|
879
|
+
// from every player-invoking state: retain the role, resolve any bound player
|
|
880
|
+
// identity privately, compose the prompt through the ephemeral identity lookup,
|
|
860
881
|
// await callPlayer, adjudicate the finalText. An `ok` result with a missing,
|
|
861
882
|
// empty, or whitespace-only finalText earns exactly one corrective re-ask of
|
|
862
883
|
// the same composed call (DR-028); a non-`ok` result, or a second such empty
|
|
@@ -867,8 +888,8 @@ function validateBossReplyOutput(
|
|
|
867
888
|
// which only fires on actor.stop(), not on Boss abort.
|
|
868
889
|
// ---------------------------------------------------------------------------
|
|
869
890
|
|
|
870
|
-
|
|
871
|
-
|
|
891
|
+
interface PlayerBridgeSpec {
|
|
892
|
+
resolveRoleId: (input: PlaybookPlayerInput) => string;
|
|
872
893
|
composePlayerPrompt: (input: PlaybookPlayerInput) => string;
|
|
873
894
|
adjudication: PlayerAdjudicationSpec;
|
|
874
895
|
resumableStateIds: ReadonlySet<string>;
|
|
@@ -884,12 +905,24 @@ export function createPlayerBridge(
|
|
|
884
905
|
return fromPromise<PlaybookActorOutput, PlaybookPlayerInput>(
|
|
885
906
|
async ({ input, signal }) => {
|
|
886
907
|
const activeSignal = combineAbortSignals(signal, getActiveSignal?.());
|
|
887
|
-
|
|
888
|
-
|
|
908
|
+
let roleId: string;
|
|
909
|
+
let prompt: string;
|
|
910
|
+
try {
|
|
911
|
+
roleId = spec.resolveRoleId(input);
|
|
912
|
+
prompt = spec.composePlayerPrompt(input);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
onControlPlaneError?.(error);
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
889
917
|
const callPlayer = (resume: string | false) =>
|
|
890
918
|
boundary
|
|
891
|
-
? boundary.callPlayer(
|
|
892
|
-
|
|
919
|
+
? boundary.callPlayer(
|
|
920
|
+
input,
|
|
921
|
+
roleId,
|
|
922
|
+
prompt,
|
|
923
|
+
activeSignal,
|
|
924
|
+
)
|
|
925
|
+
: ports.callPlayer(roleId, prompt, activeSignal, { resume });
|
|
893
926
|
let result = await callPlayer(false);
|
|
894
927
|
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
895
928
|
// An abort that lands between the empty first result and the
|
|
@@ -1063,10 +1096,10 @@ function collectInvokeSources(machine: AnyStateMachine): ReadonlySet<string> {
|
|
|
1063
1096
|
return sources;
|
|
1064
1097
|
}
|
|
1065
1098
|
|
|
1066
|
-
function
|
|
1099
|
+
function collectPlayerStateRoles(
|
|
1067
1100
|
machine: AnyStateMachine,
|
|
1068
1101
|
): ReadonlyMap<string, string> {
|
|
1069
|
-
const
|
|
1102
|
+
const roles = new Map<string, string>();
|
|
1070
1103
|
const visit = (stateDef: unknown, stateKey: string): void => {
|
|
1071
1104
|
if (!isPlainObject(stateDef)) return;
|
|
1072
1105
|
const invoke = stateDef.invoke;
|
|
@@ -1092,15 +1125,15 @@ function collectPlayerStatePlayers(
|
|
|
1092
1125
|
'player state metadata must use a non-empty state id',
|
|
1093
1126
|
);
|
|
1094
1127
|
}
|
|
1095
|
-
const
|
|
1096
|
-
? playbookMeta.
|
|
1128
|
+
const role = isPlainObject(playbookMeta)
|
|
1129
|
+
? playbookMeta.role
|
|
1097
1130
|
: undefined;
|
|
1098
|
-
if (typeof
|
|
1131
|
+
if (typeof role !== 'string' || role.trim().length === 0) {
|
|
1099
1132
|
throw new TypeError(
|
|
1100
|
-
`player state ${stateId} meta.playbook.
|
|
1133
|
+
`player state ${stateId} meta.playbook.role must be a non-empty string`,
|
|
1101
1134
|
);
|
|
1102
1135
|
}
|
|
1103
|
-
|
|
1136
|
+
roles.set(stateId, role);
|
|
1104
1137
|
}
|
|
1105
1138
|
if (isPlainObject(stateDef.states)) {
|
|
1106
1139
|
for (const [childKey, child] of Object.entries(stateDef.states)) {
|
|
@@ -1114,7 +1147,7 @@ function collectPlayerStatePlayers(
|
|
|
1114
1147
|
visit(stateDef, stateKey);
|
|
1115
1148
|
}
|
|
1116
1149
|
}
|
|
1117
|
-
return
|
|
1150
|
+
return roles;
|
|
1118
1151
|
}
|
|
1119
1152
|
|
|
1120
1153
|
function transitionTargets(transition: unknown): string[] {
|
|
@@ -1268,55 +1301,68 @@ function makeDefaultNormalizeTransitionEvent(
|
|
|
1268
1301
|
};
|
|
1269
1302
|
}
|
|
1270
1303
|
|
|
1271
|
-
function
|
|
1272
|
-
value:
|
|
1304
|
+
function snapshotRoleStateStatuses(
|
|
1305
|
+
value: unknown,
|
|
1273
1306
|
label: string,
|
|
1274
1307
|
machine: AnyStateMachine,
|
|
1275
1308
|
stateDescriptions: ReadonlyMap<string, string>,
|
|
1276
|
-
): ReadonlyMap<string,
|
|
1277
|
-
if (value === undefined)
|
|
1278
|
-
|
|
1279
|
-
|
|
1309
|
+
): ReadonlyMap<string, XStateRoleStateStatus> {
|
|
1310
|
+
if (value === undefined) {
|
|
1311
|
+
throw new TypeError(`${label} roleStates must be supplied for schema 2`);
|
|
1312
|
+
}
|
|
1313
|
+
const captured = snapshotJsonValue(value, `${label} roleStates`);
|
|
1314
|
+
if (!isPlainObject(captured)) {
|
|
1315
|
+
throw new TypeError(`${label} roleStates must be an object`);
|
|
1280
1316
|
}
|
|
1281
|
-
const declared =
|
|
1282
|
-
const statuses = new Map<string,
|
|
1283
|
-
for (const [stateId, candidate] of Object.entries(
|
|
1317
|
+
const declared = collectPlayerStateRoles(machine);
|
|
1318
|
+
const statuses = new Map<string, XStateRoleStateStatus>();
|
|
1319
|
+
for (const [stateId, candidate] of Object.entries(captured)) {
|
|
1284
1320
|
if (!declared.has(stateId)) {
|
|
1285
1321
|
throw new TypeError(
|
|
1286
|
-
`${label}
|
|
1322
|
+
`${label} roleStates.${stateId} does not name a player state`,
|
|
1287
1323
|
);
|
|
1288
1324
|
}
|
|
1325
|
+
if (isPlainObject(candidate)) {
|
|
1326
|
+
const extra = Object.keys(candidate).find(
|
|
1327
|
+
(key) => key !== 'role' && key !== 'label',
|
|
1328
|
+
);
|
|
1329
|
+
if (extra !== undefined) {
|
|
1330
|
+
throw new TypeError(
|
|
1331
|
+
`${label} roleStates.${stateId}.${extra} is not allowed`,
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1289
1335
|
if (
|
|
1290
1336
|
!isPlainObject(candidate) ||
|
|
1291
|
-
typeof candidate.
|
|
1292
|
-
candidate.
|
|
1337
|
+
typeof candidate.role !== 'string' ||
|
|
1338
|
+
candidate.role.trim().length === 0 ||
|
|
1293
1339
|
typeof candidate.label !== 'string' ||
|
|
1294
1340
|
candidate.label.trim().length === 0
|
|
1295
1341
|
) {
|
|
1296
1342
|
throw new TypeError(
|
|
1297
|
-
`${label}
|
|
1343
|
+
`${label} roleStates.${stateId} must carry non-empty role and label strings`,
|
|
1298
1344
|
);
|
|
1299
1345
|
}
|
|
1300
1346
|
const expectedLabel = stateDescriptions.get(stateId);
|
|
1301
1347
|
if (candidate.label !== expectedLabel) {
|
|
1302
1348
|
throw new TypeError(
|
|
1303
|
-
`${label}
|
|
1349
|
+
`${label} roleStates.${stateId}.label must equal its FSM description`,
|
|
1304
1350
|
);
|
|
1305
1351
|
}
|
|
1306
|
-
if (candidate.
|
|
1352
|
+
if (candidate.role !== declared.get(stateId)) {
|
|
1307
1353
|
throw new TypeError(
|
|
1308
|
-
`${label}
|
|
1354
|
+
`${label} roleStates.${stateId}.role must equal its FSM role`,
|
|
1309
1355
|
);
|
|
1310
1356
|
}
|
|
1311
1357
|
statuses.set(stateId, {
|
|
1312
|
-
|
|
1358
|
+
role: candidate.role,
|
|
1313
1359
|
label: candidate.label,
|
|
1314
1360
|
});
|
|
1315
1361
|
}
|
|
1316
1362
|
for (const stateId of declared.keys()) {
|
|
1317
1363
|
if (!statuses.has(stateId)) {
|
|
1318
1364
|
throw new TypeError(
|
|
1319
|
-
`${label}
|
|
1365
|
+
`${label} roleStates must declare player state ${stateId}`,
|
|
1320
1366
|
);
|
|
1321
1367
|
}
|
|
1322
1368
|
}
|
|
@@ -1331,39 +1377,14 @@ function settlingGuard(event: unknown): string | undefined {
|
|
|
1331
1377
|
: undefined;
|
|
1332
1378
|
}
|
|
1333
1379
|
|
|
1334
|
-
function
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
const stateId = state.stateId;
|
|
1339
|
-
if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) return [];
|
|
1340
|
-
if (stateId === 'awaitBossReply') {
|
|
1341
|
-
const pending = pendingBossQuestionFromContext(context);
|
|
1342
|
-
return [
|
|
1343
|
-
{
|
|
1344
|
-
message:
|
|
1345
|
-
pending === undefined
|
|
1346
|
-
? 'Awaiting Boss reply.'
|
|
1347
|
-
: `${pending.player} asks: ${pending.question}`,
|
|
1348
|
-
},
|
|
1349
|
-
];
|
|
1350
|
-
}
|
|
1351
|
-
if (stateId === 'failed') {
|
|
1352
|
-
const lastError = normalizeErrorFull(context.lastError);
|
|
1353
|
-
return [
|
|
1354
|
-
{
|
|
1355
|
-
message: 'Workflow failed; awaiting Boss recovery.',
|
|
1356
|
-
...(lastError === undefined
|
|
1357
|
-
? {}
|
|
1358
|
-
: { data: snapshotJsonValue({ lastError }, 'failed status data') }),
|
|
1359
|
-
},
|
|
1360
|
-
];
|
|
1361
|
-
}
|
|
1362
|
-
return [{ message: `Entered ${stateId}.` }];
|
|
1380
|
+
function askerLabel(
|
|
1381
|
+
asker: PlaybookPendingBossQuestionContext['asker'],
|
|
1382
|
+
): string {
|
|
1383
|
+
return asker.kind === 'captain' ? 'Captain' : asker.roleId;
|
|
1363
1384
|
}
|
|
1364
1385
|
|
|
1365
1386
|
function makeDefaultStatusesForState(
|
|
1366
|
-
|
|
1387
|
+
roleStates: ReadonlyMap<string, XStateRoleStateStatus>,
|
|
1367
1388
|
): NonNullable<XStatePlaybookRuntimeSpec<unknown>['statusesForState']> {
|
|
1368
1389
|
return (state, context, event): ScheduledStatus[] => {
|
|
1369
1390
|
const statuses: ScheduledStatus[] = [];
|
|
@@ -1381,11 +1402,11 @@ function makeDefaultStatusesForState(
|
|
|
1381
1402
|
}
|
|
1382
1403
|
return [
|
|
1383
1404
|
...statuses,
|
|
1384
|
-
{ message: `${pending.
|
|
1405
|
+
{ message: `${askerLabel(pending.asker)} asks: ${pending.question}` },
|
|
1385
1406
|
{
|
|
1386
1407
|
message:
|
|
1387
1408
|
`◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
1388
|
-
`${pending.
|
|
1409
|
+
`${askerLabel(pending.asker)} · ${pending.sourceItem}`,
|
|
1389
1410
|
},
|
|
1390
1411
|
];
|
|
1391
1412
|
}
|
|
@@ -1406,10 +1427,10 @@ function makeDefaultStatusesForState(
|
|
|
1406
1427
|
},
|
|
1407
1428
|
];
|
|
1408
1429
|
}
|
|
1409
|
-
const
|
|
1410
|
-
if (
|
|
1430
|
+
const roleState = roleStates.get(stateId);
|
|
1431
|
+
if (roleState !== undefined) {
|
|
1411
1432
|
statuses.push({
|
|
1412
|
-
message: `⤷ ${
|
|
1433
|
+
message: `⤷ ${roleState.role}: ${roleState.label}`,
|
|
1413
1434
|
});
|
|
1414
1435
|
}
|
|
1415
1436
|
return statuses;
|
|
@@ -1672,7 +1693,7 @@ function makeDefaultClassifyBossText(
|
|
|
1672
1693
|
if (pending !== undefined) {
|
|
1673
1694
|
lines.push(
|
|
1674
1695
|
`Pending question id: ${pending.questionId}`,
|
|
1675
|
-
`Pending
|
|
1696
|
+
`Pending asker: ${askerLabel(pending.asker)}`,
|
|
1676
1697
|
`Pending Boss question: ${pending.question}`,
|
|
1677
1698
|
);
|
|
1678
1699
|
}
|
|
@@ -1846,6 +1867,17 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1846
1867
|
// DR-022 / PBRT-50: reject an incompatible artifact declaration before any
|
|
1847
1868
|
// machine interpretation, against this loaded engine's own self-report.
|
|
1848
1869
|
assertRuntimeCompat(spec.compat, label);
|
|
1870
|
+
const specDescriptors = Object.getOwnPropertyDescriptors(spec);
|
|
1871
|
+
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
|
|
1872
|
+
throw new TypeError(
|
|
1873
|
+
`${label} schema-2 artifacts must supply roleStates, not playerStates`,
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
|
|
1877
|
+
throw new TypeError(
|
|
1878
|
+
`${label} schema-2 artifacts must not derive concrete player bindings`,
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1849
1881
|
if (machineDeclaresParallelState(machine)) {
|
|
1850
1882
|
throw new Error(
|
|
1851
1883
|
`${label} uses a parallel state; the shared runtime supports only single-region FSMs`,
|
|
@@ -1857,13 +1889,22 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1857
1889
|
// DR-029: source state descriptions label the control actions the
|
|
1858
1890
|
// runtime advertises through `describe()`.
|
|
1859
1891
|
const stateDescriptions = stateDescriptionsFromMachine(machine);
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1892
|
+
const roleStatesDescriptor = specDescriptors.roleStates;
|
|
1893
|
+
if (
|
|
1894
|
+
roleStatesDescriptor !== undefined &&
|
|
1895
|
+
!Object.prototype.hasOwnProperty.call(roleStatesDescriptor, 'value')
|
|
1896
|
+
) {
|
|
1897
|
+
throw new TypeError(`${label} roleStates must be an own data property`);
|
|
1898
|
+
}
|
|
1899
|
+
const roleStates = snapshotRoleStateStatuses(
|
|
1900
|
+
roleStatesDescriptor?.value,
|
|
1863
1901
|
label,
|
|
1864
1902
|
machine,
|
|
1865
1903
|
stateDescriptions,
|
|
1866
1904
|
);
|
|
1905
|
+
const declaredRoleIds = Object.freeze([
|
|
1906
|
+
...new Set([...roleStates.values()].map(({ role }) => role)),
|
|
1907
|
+
]);
|
|
1867
1908
|
// PBRT-52: the artifact's own ControlView context projection. Nothing is
|
|
1868
1909
|
// exported by default, so an FSM context member — including one added
|
|
1869
1910
|
// after this artifact was linked — is private until named here. The two
|
|
@@ -1880,7 +1921,6 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1880
1921
|
);
|
|
1881
1922
|
}
|
|
1882
1923
|
}
|
|
1883
|
-
const resolvePlayerIdSpec = spec.resolvePlayerId;
|
|
1884
1924
|
const composePlayerPrompt =
|
|
1885
1925
|
spec.composePlayerPrompt ??
|
|
1886
1926
|
((input: PlaybookPlayerInput) =>
|
|
@@ -1919,14 +1959,10 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1919
1959
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
1920
1960
|
const statusesForState =
|
|
1921
1961
|
spec.statusesForState ??
|
|
1922
|
-
(
|
|
1923
|
-
? makeDefaultStatusesForState(playerStates)
|
|
1924
|
-
: legacyStatusesForState);
|
|
1962
|
+
makeDefaultStatusesForState(roleStates);
|
|
1925
1963
|
const classificationStatus =
|
|
1926
1964
|
spec.classificationStatus ??
|
|
1927
|
-
(
|
|
1928
|
-
? (event: EventObject) => event.type
|
|
1929
|
-
: () => undefined);
|
|
1965
|
+
((event: EventObject) => event.type);
|
|
1930
1966
|
const machineInput =
|
|
1931
1967
|
spec.machineInput ?? ((options: TOptions) => options as unknown);
|
|
1932
1968
|
const scriptCwd =
|
|
@@ -1977,8 +2013,8 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1977
2013
|
// acceptance records nothing, so a later call with that key may still
|
|
1978
2014
|
// execute.
|
|
1979
2015
|
const appliedReceipts = new Map<string, PlaybookControlReceipt>();
|
|
1980
|
-
const
|
|
1981
|
-
const
|
|
2016
|
+
const privateResumeTokens = new Map<string, string>();
|
|
2017
|
+
const activePlayerKeys = new Set<string>();
|
|
1982
2018
|
const playbookCallTurnIds = new Map<string, number | undefined>();
|
|
1983
2019
|
// Captain and judge work share one serialized lane (slc/link.md
|
|
1984
2020
|
// §Session lifecycle).
|
|
@@ -1991,69 +2027,191 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1991
2027
|
// imperative boundaries await their queued work directly.
|
|
1992
2028
|
let emissionFailure: unknown;
|
|
1993
2029
|
|
|
1994
|
-
function
|
|
2030
|
+
function bindSession(nextSession: PlaybookSession): PlaybookSession {
|
|
2031
|
+
const bound = snapshotPlaybookSession(nextSession);
|
|
2032
|
+
if (bound.roleBindings === undefined) return bound;
|
|
2033
|
+
const actual = Object.keys(bound.roleBindings).sort();
|
|
2034
|
+
const expected = [...declaredRoleIds].sort();
|
|
2035
|
+
const missing = expected.filter((roleId) => !actual.includes(roleId));
|
|
2036
|
+
const extra = actual.filter((roleId) => !expected.includes(roleId));
|
|
2037
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
2038
|
+
throw new TypeError(
|
|
2039
|
+
`${label} session roleBindings must cover exactly [${expected.join(', ')}]` +
|
|
2040
|
+
`${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
|
|
2041
|
+
`${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`,
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
return bound;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function requireRoleId(input: PlaybookPlayerInput): string {
|
|
2048
|
+
const roleId = input.role;
|
|
2049
|
+
if (
|
|
2050
|
+
typeof roleId !== 'string' ||
|
|
2051
|
+
roleId.trim().length === 0 ||
|
|
2052
|
+
!declaredRoleIds.includes(roleId)
|
|
2053
|
+
) {
|
|
2054
|
+
throw new TypeError(
|
|
2055
|
+
`${label} player input role must name a declared local role`,
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
return roleId;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function resolvedPlayerId(roleId: string): string | undefined {
|
|
2062
|
+
return session?.roleBindings?.[roleId]?.playerId;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
function promptIdentity(roleId: string): string {
|
|
2066
|
+
if (!declaredRoleIds.includes(roleId)) {
|
|
2067
|
+
throw new TypeError(
|
|
2068
|
+
`${label} prompt identity lookup rejected undeclared role ${roleId}`,
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
function composeBoundPlayerPrompt(input: PlaybookPlayerInput): string {
|
|
2075
|
+
let active = true;
|
|
2076
|
+
const lookup: XStatePromptIdentity = (roleId) => {
|
|
2077
|
+
if (!active) {
|
|
2078
|
+
throw new Error(
|
|
2079
|
+
`${label} prompt identity lookup is no longer active`,
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
return promptIdentity(roleId);
|
|
2083
|
+
};
|
|
2084
|
+
try {
|
|
2085
|
+
return composePlayerPrompt(input, lookup);
|
|
2086
|
+
} finally {
|
|
2087
|
+
active = false;
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function continuationKey(
|
|
2092
|
+
roleId: string,
|
|
2093
|
+
playerId: string | undefined,
|
|
2094
|
+
): string {
|
|
2095
|
+
return playerId ?? roleId;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function roleTokensByContinuationKey(
|
|
2099
|
+
tokens: Readonly<Record<string, string>>,
|
|
2100
|
+
): Map<string, string> {
|
|
2101
|
+
const byKey = new Map<string, string>();
|
|
2102
|
+
for (const [roleId, token] of Object.entries(tokens)) {
|
|
2103
|
+
if (!declaredRoleIds.includes(roleId)) {
|
|
2104
|
+
throw new TypeError(
|
|
2105
|
+
`runtime role tokens contain unknown role ${roleId}`,
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
2109
|
+
const existing = byKey.get(key);
|
|
2110
|
+
if (existing !== undefined && existing !== token) {
|
|
2111
|
+
throw new TypeError(
|
|
2112
|
+
`runtime snapshot assigns conflicting tokens to roles bound to player ${key}`,
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
byKey.set(key, token);
|
|
2116
|
+
}
|
|
2117
|
+
const rolesByKey = new Map<string, string[]>();
|
|
2118
|
+
for (const roleId of declaredRoleIds) {
|
|
2119
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
2120
|
+
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
2121
|
+
}
|
|
2122
|
+
for (const [key, roles] of rolesByKey) {
|
|
2123
|
+
if (roles.length < 2) continue;
|
|
2124
|
+
const present = roles.filter((roleId) => tokens[roleId] !== undefined);
|
|
2125
|
+
if (present.length !== 0 && present.length !== roles.length) {
|
|
2126
|
+
throw new TypeError(
|
|
2127
|
+
`runtime role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`,
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return byKey;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function selectPlayerResume(
|
|
2135
|
+
roleId: string,
|
|
2136
|
+
playerId: string | undefined,
|
|
2137
|
+
): string | false {
|
|
2138
|
+
const key = continuationKey(roleId, playerId);
|
|
1995
2139
|
const selected = session?.playerSessions
|
|
1996
|
-
? session.playerSessions.select(
|
|
1997
|
-
:
|
|
2140
|
+
? session.playerSessions.select(roleId)
|
|
2141
|
+
: privateResumeTokens.get(key) ?? false;
|
|
1998
2142
|
if (
|
|
1999
2143
|
selected !== false &&
|
|
2000
2144
|
(typeof selected !== 'string' || selected.trim().length === 0)
|
|
2001
2145
|
) {
|
|
2002
2146
|
throw new TypeError(
|
|
2003
|
-
`player session store returned an invalid resume token for ${
|
|
2147
|
+
`player session store returned an invalid resume token for role ${roleId}`,
|
|
2004
2148
|
);
|
|
2005
2149
|
}
|
|
2006
2150
|
return selected;
|
|
2007
2151
|
}
|
|
2008
2152
|
|
|
2009
2153
|
function updatePlayerResume(
|
|
2010
|
-
|
|
2011
|
-
|
|
2154
|
+
roleId: string,
|
|
2155
|
+
playerId: string | undefined,
|
|
2156
|
+
result: PlayerResult,
|
|
2012
2157
|
): void {
|
|
2158
|
+
const resumeToken = result.resumeToken;
|
|
2159
|
+
if (resumeToken === undefined && result.status !== 'ok') return;
|
|
2160
|
+
const key = continuationKey(roleId, playerId);
|
|
2013
2161
|
if (session?.playerSessions) {
|
|
2014
|
-
session.playerSessions.update(
|
|
2015
|
-
} else if (resumeToken !== undefined
|
|
2016
|
-
|
|
2162
|
+
session.playerSessions.update(roleId, resumeToken);
|
|
2163
|
+
} else if (resumeToken !== undefined) {
|
|
2164
|
+
privateResumeTokens.set(key, resumeToken);
|
|
2017
2165
|
} else {
|
|
2018
|
-
|
|
2166
|
+
privateResumeTokens.delete(key);
|
|
2019
2167
|
}
|
|
2020
2168
|
}
|
|
2021
2169
|
|
|
2022
|
-
function
|
|
2023
|
-
const raw =
|
|
2024
|
-
|
|
2025
|
-
|
|
2170
|
+
function snapshotRoleResumeTokens(): Record<string, string> {
|
|
2171
|
+
const raw = snapshotJsonValue(
|
|
2172
|
+
session?.playerSessions
|
|
2173
|
+
? session.playerSessions.snapshot()
|
|
2174
|
+
: Object.fromEntries(
|
|
2175
|
+
declaredRoleIds.flatMap((roleId) => {
|
|
2176
|
+
const token = privateResumeTokens.get(
|
|
2177
|
+
continuationKey(roleId, resolvedPlayerId(roleId)),
|
|
2178
|
+
);
|
|
2179
|
+
return token === undefined ? [] : [[roleId, token]];
|
|
2180
|
+
}),
|
|
2181
|
+
),
|
|
2182
|
+
'player session store snapshot',
|
|
2183
|
+
);
|
|
2026
2184
|
if (!isPlainObject(raw)) {
|
|
2027
2185
|
throw new TypeError('player session store snapshot must be an object');
|
|
2028
2186
|
}
|
|
2029
2187
|
const detached: Record<string, string> = {};
|
|
2030
|
-
for (const [
|
|
2031
|
-
if (
|
|
2188
|
+
for (const [roleId, token] of Object.entries(raw)) {
|
|
2189
|
+
if (!declaredRoleIds.includes(roleId)) {
|
|
2032
2190
|
throw new TypeError(
|
|
2033
|
-
|
|
2191
|
+
`player session store snapshot contains unknown role ${roleId}`,
|
|
2034
2192
|
);
|
|
2035
2193
|
}
|
|
2036
2194
|
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
2037
2195
|
throw new TypeError(
|
|
2038
|
-
`player session store snapshot token for ${
|
|
2196
|
+
`player session store snapshot token for ${roleId} must be a non-empty string`,
|
|
2039
2197
|
);
|
|
2040
2198
|
}
|
|
2041
|
-
detached[
|
|
2199
|
+
detached[roleId] = token;
|
|
2042
2200
|
}
|
|
2201
|
+
roleTokensByContinuationKey(detached);
|
|
2043
2202
|
return detached;
|
|
2044
2203
|
}
|
|
2045
2204
|
|
|
2046
|
-
function
|
|
2205
|
+
function restoreRoleResumeTokens(
|
|
2047
2206
|
tokens: Readonly<Record<string, string>>,
|
|
2048
2207
|
): void {
|
|
2208
|
+
const byKey = roleTokensByContinuationKey(tokens);
|
|
2049
2209
|
if (session?.playerSessions) {
|
|
2050
2210
|
session.playerSessions.restore(tokens);
|
|
2051
2211
|
return;
|
|
2052
2212
|
}
|
|
2053
|
-
|
|
2054
|
-
for (const [
|
|
2055
|
-
playerResumeTokens.set(playerId, token);
|
|
2056
|
-
}
|
|
2213
|
+
privateResumeTokens.clear();
|
|
2214
|
+
for (const [key, token] of byKey) privateResumeTokens.set(key, token);
|
|
2057
2215
|
}
|
|
2058
2216
|
|
|
2059
2217
|
function enqueueEmission(fn: () => Promise<void>): Promise<void> {
|
|
@@ -2111,7 +2269,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2111
2269
|
const currentSession = requireSession();
|
|
2112
2270
|
const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
2113
2271
|
return {
|
|
2114
|
-
schemaVersion:
|
|
2272
|
+
schemaVersion: 3,
|
|
2115
2273
|
sessionId: currentSession.sessionId,
|
|
2116
2274
|
playbookId: currentSession.playbookId,
|
|
2117
2275
|
rootSessionId: currentSession.rootSessionId,
|
|
@@ -2260,25 +2418,31 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2260
2418
|
}
|
|
2261
2419
|
|
|
2262
2420
|
const boundary: RuntimeBoundaryCalls = {
|
|
2263
|
-
async callPlayer(
|
|
2421
|
+
async callPlayer(
|
|
2422
|
+
input,
|
|
2423
|
+
roleId,
|
|
2424
|
+
prompt,
|
|
2425
|
+
signal,
|
|
2426
|
+
): Promise<PlayerResult> {
|
|
2264
2427
|
// State-entry telemetry/status must precede the call they describe.
|
|
2265
2428
|
await drainEmissions();
|
|
2266
2429
|
const turnId = activeTurnId;
|
|
2267
2430
|
const stateId = input.stateId;
|
|
2431
|
+
const playerId = resolvedPlayerId(roleId);
|
|
2268
2432
|
let resume: string | false;
|
|
2269
2433
|
try {
|
|
2270
2434
|
signal.throwIfAborted();
|
|
2271
|
-
resume = selectPlayerResume(playerId);
|
|
2435
|
+
resume = selectPlayerResume(roleId, playerId);
|
|
2272
2436
|
} catch (error) {
|
|
2273
2437
|
if (!signal.aborted) controlPlaneError ??= error;
|
|
2274
2438
|
throw error;
|
|
2275
2439
|
}
|
|
2276
2440
|
const callId = `player-${++playerCallSequence}`;
|
|
2277
2441
|
const identity = {
|
|
2278
|
-
purpose: 'captain' as const,
|
|
2279
2442
|
...stateIdentity(stateId),
|
|
2280
2443
|
sourceItem: input.sourceItem,
|
|
2281
|
-
|
|
2444
|
+
roleId,
|
|
2445
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
2282
2446
|
resume,
|
|
2283
2447
|
};
|
|
2284
2448
|
const position: TracePosition = {
|
|
@@ -2286,9 +2450,10 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2286
2450
|
callId,
|
|
2287
2451
|
};
|
|
2288
2452
|
|
|
2289
|
-
|
|
2453
|
+
const playerKey = continuationKey(roleId, playerId);
|
|
2454
|
+
if (activePlayerKeys.has(playerKey)) {
|
|
2290
2455
|
const error = new Error(
|
|
2291
|
-
`simultaneous calls to
|
|
2456
|
+
`simultaneous calls to player key ${playerKey} are not allowed`,
|
|
2292
2457
|
);
|
|
2293
2458
|
await emitCallStarted(
|
|
2294
2459
|
'player.call.started',
|
|
@@ -2303,7 +2468,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2303
2468
|
);
|
|
2304
2469
|
throw error;
|
|
2305
2470
|
}
|
|
2306
|
-
|
|
2471
|
+
activePlayerKeys.add(playerKey);
|
|
2307
2472
|
|
|
2308
2473
|
try {
|
|
2309
2474
|
await emitTrace(
|
|
@@ -2320,7 +2485,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2320
2485
|
// as `aborted` through the catch below.
|
|
2321
2486
|
signal.throwIfAborted();
|
|
2322
2487
|
rawResult = await requireHostPorts().callPlayer(
|
|
2323
|
-
|
|
2488
|
+
roleId,
|
|
2324
2489
|
prompt,
|
|
2325
2490
|
signal,
|
|
2326
2491
|
{ resume },
|
|
@@ -2366,13 +2531,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2366
2531
|
}
|
|
2367
2532
|
|
|
2368
2533
|
try {
|
|
2369
|
-
updatePlayerResume(
|
|
2370
|
-
playerId,
|
|
2371
|
-
typeof result.resumeToken === 'string' &&
|
|
2372
|
-
result.resumeToken.trim().length > 0
|
|
2373
|
-
? result.resumeToken
|
|
2374
|
-
: undefined,
|
|
2375
|
-
);
|
|
2534
|
+
updatePlayerResume(roleId, playerId, result);
|
|
2376
2535
|
} catch (error) {
|
|
2377
2536
|
if (!signal.aborted) controlPlaneError ??= error;
|
|
2378
2537
|
try {
|
|
@@ -2406,7 +2565,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2406
2565
|
);
|
|
2407
2566
|
return result;
|
|
2408
2567
|
} finally {
|
|
2409
|
-
|
|
2568
|
+
activePlayerKeys.delete(playerKey);
|
|
2410
2569
|
}
|
|
2411
2570
|
},
|
|
2412
2571
|
|
|
@@ -2611,19 +2770,13 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
2611
2770
|
},
|
|
2612
2771
|
};
|
|
2613
2772
|
|
|
2614
|
-
function resolvePlayerId(input: PlaybookPlayerInput): string {
|
|
2615
|
-
return resolvePlayerIdSpec
|
|
2616
|
-
? resolvePlayerIdSpec(input, boundOptions)
|
|
2617
|
-
: defaultResolvePlayerId(input);
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
2773
|
function playerActor(
|
|
2621
2774
|
ports: PlaybookPorts,
|
|
2622
2775
|
): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput> {
|
|
2623
2776
|
return createPlayerBridge(
|
|
2624
2777
|
{
|
|
2625
|
-
|
|
2626
|
-
composePlayerPrompt,
|
|
2778
|
+
resolveRoleId: requireRoleId,
|
|
2779
|
+
composePlayerPrompt: composeBoundPlayerPrompt,
|
|
2627
2780
|
adjudication,
|
|
2628
2781
|
resumableStateIds,
|
|
2629
2782
|
},
|
|
@@ -3123,8 +3276,8 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3123
3276
|
// The session-start error remains authoritative.
|
|
3124
3277
|
}
|
|
3125
3278
|
}
|
|
3126
|
-
|
|
3127
|
-
|
|
3279
|
+
privateResumeTokens.clear();
|
|
3280
|
+
activePlayerKeys.clear();
|
|
3128
3281
|
playbookCallTurnIds.clear();
|
|
3129
3282
|
activeEmissionCalls.clear();
|
|
3130
3283
|
emissionQueue.clear();
|
|
@@ -3332,7 +3485,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3332
3485
|
if (initialized || disposed || disposalPromise !== undefined) {
|
|
3333
3486
|
throw new Error('createPlaybookRuntime.init: already initialized');
|
|
3334
3487
|
}
|
|
3335
|
-
const boundSession =
|
|
3488
|
+
const boundSession = bindSession(nextSession);
|
|
3336
3489
|
initialized = true;
|
|
3337
3490
|
let finishInitialization!: () => void;
|
|
3338
3491
|
const initialization = new Promise<void>((resolve) => {
|
|
@@ -3408,10 +3561,10 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3408
3561
|
.context as Record<string, unknown>;
|
|
3409
3562
|
const pending = pendingBossQuestionFromContext(context ?? {});
|
|
3410
3563
|
return {
|
|
3411
|
-
schemaVersion:
|
|
3564
|
+
schemaVersion: 3,
|
|
3412
3565
|
playbookId: session.playbookId,
|
|
3413
3566
|
machine: machineSnapshot,
|
|
3414
|
-
|
|
3567
|
+
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
3415
3568
|
sequences: {
|
|
3416
3569
|
trace: traceSequence,
|
|
3417
3570
|
turn: turnSequence,
|
|
@@ -3429,7 +3582,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3429
3582
|
: [
|
|
3430
3583
|
{
|
|
3431
3584
|
questionId: pending.questionId,
|
|
3432
|
-
|
|
3585
|
+
asker: pending.asker,
|
|
3433
3586
|
question: pending.question,
|
|
3434
3587
|
sourceItem: pending.sourceItem,
|
|
3435
3588
|
},
|
|
@@ -3450,16 +3603,21 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3450
3603
|
if (initialized || disposed || disposalPromise !== undefined) {
|
|
3451
3604
|
throw new Error('createPlaybookRuntime.restore: already initialized');
|
|
3452
3605
|
}
|
|
3453
|
-
const boundSession =
|
|
3606
|
+
const boundSession = bindSession(nextSession);
|
|
3454
3607
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(
|
|
3455
3608
|
snapshot,
|
|
3456
3609
|
boundSession.playbookId,
|
|
3457
3610
|
{ allowSuspendedCall: true },
|
|
3458
3611
|
);
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3612
|
+
if (
|
|
3613
|
+
declaredActors.has('captain') &&
|
|
3614
|
+
boundSnapshot.sequences.captainCall === undefined
|
|
3615
|
+
) {
|
|
3616
|
+
throw new TypeError(
|
|
3617
|
+
'runtime snapshot sequences.captainCall is required for a direct-Captain artifact',
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
const suspendedCall = boundSnapshot.suspendedCall;
|
|
3463
3621
|
let priorExternalPlayerTokens:
|
|
3464
3622
|
| Readonly<Record<string, string>>
|
|
3465
3623
|
| undefined;
|
|
@@ -3479,22 +3637,17 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3479
3637
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
3480
3638
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
3481
3639
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
3482
|
-
captainCallSequence =
|
|
3483
|
-
boundSnapshot.sequences.captainCall ??
|
|
3484
|
-
// Legacy schema-v1 snapshots predate this dedicated counter.
|
|
3485
|
-
// Every Captain call already consumed at least one trace number,
|
|
3486
|
-
// so the global trace counter is a collision-safe id floor.
|
|
3487
|
-
boundSnapshot.sequences.trace;
|
|
3640
|
+
captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
|
|
3488
3641
|
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
3489
3642
|
// apply boundary consumed trace numbers, so the persisted trace
|
|
3490
3643
|
// counter is a collision-safe id floor here too, keeping
|
|
3491
3644
|
// `apply-<n>` call ids unique across restore.
|
|
3492
3645
|
applyCallSequence = boundSnapshot.sequences.trace;
|
|
3493
3646
|
if (boundSession.playerSessions) {
|
|
3494
|
-
priorExternalPlayerTokens =
|
|
3647
|
+
priorExternalPlayerTokens = snapshotRoleResumeTokens();
|
|
3495
3648
|
externalStoreRestoreAttempted = true;
|
|
3496
3649
|
}
|
|
3497
|
-
|
|
3650
|
+
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
3498
3651
|
nestedBridge.prepareRestore(suspendedCall);
|
|
3499
3652
|
if (suspendedCall !== undefined) {
|
|
3500
3653
|
playbookCallTurnIds.set(
|
|
@@ -3604,7 +3757,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3604
3757
|
: [
|
|
3605
3758
|
{
|
|
3606
3759
|
questionId: pending.questionId,
|
|
3607
|
-
|
|
3760
|
+
asker: pending.asker,
|
|
3608
3761
|
question: pending.question,
|
|
3609
3762
|
sourceItem: pending.sourceItem,
|
|
3610
3763
|
},
|
|
@@ -4176,9 +4329,9 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
4176
4329
|
// engagement tree. Child disposal must not erase a token its
|
|
4177
4330
|
// caller will resume. The private fallback remains runtime-owned.
|
|
4178
4331
|
if (session?.playerSessions === undefined) {
|
|
4179
|
-
|
|
4332
|
+
privateResumeTokens.clear();
|
|
4180
4333
|
}
|
|
4181
|
-
|
|
4334
|
+
activePlayerKeys.clear();
|
|
4182
4335
|
playbookCallTurnIds.clear();
|
|
4183
4336
|
activeEmissionCalls.clear();
|
|
4184
4337
|
emissionQueue.clear();
|