@sublang/playbook 6.0.0 → 7.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 +14 -10
- package/docs/cli.md +102 -57
- package/docs/configuration.md +57 -16
- package/package.json +3 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1171 -983
- package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +70 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +864 -68
- package/reference/sdlc/code.playbook/playbook-captain.ts +1296 -64
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +4 -14
- package/reference/sdlc/decide.playbook/decide.playbook.js +77 -13
- package/reference/sdlc/decide.playbook/decide.playbook.ts +93 -14
- package/slc/link.md +35 -12
- package/src/runtime.d.ts +14 -2
- package/src/runtime.ts +26 -6
- package/src/xstate-playbook-runtime.js +64 -14
- package/src/xstate-playbook-runtime.ts +79 -13
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +359 -57
- package/src/xstate-runtime.ts +491 -71
|
@@ -1285,8 +1285,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1285
1285
|
// DR-029: the last event a public Boss boundary sent into the
|
|
1286
1286
|
// machine — classified, deterministic entry, or Boss reply — kept with
|
|
1287
1287
|
// its recorded payload so a failure-state retry action can replay the
|
|
1288
|
-
// event that drove the run into `failed`. Process-local: the
|
|
1289
|
-
//
|
|
1288
|
+
// event that drove the run into `failed`. Process-local: the durable
|
|
1289
|
+
// runtime snapshot does not persist it (PBRT-50).
|
|
1290
1290
|
let lastBossEvent;
|
|
1291
1291
|
// DR-029: process-local at-most-once `apply` execution — the accepted receipt
|
|
1292
1292
|
// recorded for each idempotency key, returned verbatim on a repeated
|
|
@@ -2424,18 +2424,42 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2424
2424
|
initInFlight = undefined;
|
|
2425
2425
|
}
|
|
2426
2426
|
},
|
|
2427
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked
|
|
2427
|
+
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
2428
|
+
// session, including one already-started suspended nested call.
|
|
2428
2429
|
// Defined only at a safe capture point — initialized, not disposing
|
|
2429
|
-
// or disposed, no active public boundary,
|
|
2430
|
-
//
|
|
2430
|
+
// or disposed, no active public boundary, and the actor quiescent with
|
|
2431
|
+
// status `active`.
|
|
2431
2432
|
exportSnapshot() {
|
|
2432
2433
|
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
2433
2434
|
return undefined;
|
|
2434
2435
|
}
|
|
2435
2436
|
if (activeSignal !== undefined)
|
|
2436
2437
|
return undefined;
|
|
2437
|
-
|
|
2438
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
2439
|
+
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
2440
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
2438
2441
|
return undefined;
|
|
2442
|
+
}
|
|
2443
|
+
let suspendedCall;
|
|
2444
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
2445
|
+
if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
|
|
2446
|
+
pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
2447
|
+
pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
|
|
2448
|
+
return undefined;
|
|
2449
|
+
}
|
|
2450
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
2451
|
+
return undefined;
|
|
2452
|
+
}
|
|
2453
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
2454
|
+
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
2455
|
+
bridgeSuspendedCall.turnId !== turnId) {
|
|
2456
|
+
return undefined;
|
|
2457
|
+
}
|
|
2458
|
+
suspendedCall = {
|
|
2459
|
+
...bridgeSuspendedCall,
|
|
2460
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2439
2463
|
const state = currentState();
|
|
2440
2464
|
if (state.status !== 'active' || !state.quiescent)
|
|
2441
2465
|
return undefined;
|
|
@@ -2444,7 +2468,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2444
2468
|
.context;
|
|
2445
2469
|
const pending = pendingBossQuestionFromContext(context ?? {});
|
|
2446
2470
|
return {
|
|
2447
|
-
schemaVersion:
|
|
2471
|
+
schemaVersion: 2,
|
|
2448
2472
|
playbookId: session.playbookId,
|
|
2449
2473
|
machine: machineSnapshot,
|
|
2450
2474
|
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
@@ -2469,6 +2493,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2469
2493
|
sourceItem: pending.sourceItem,
|
|
2470
2494
|
},
|
|
2471
2495
|
],
|
|
2496
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2472
2497
|
};
|
|
2473
2498
|
},
|
|
2474
2499
|
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
@@ -2481,7 +2506,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2481
2506
|
throw new Error('createPlaybookRuntime.restore: already initialized');
|
|
2482
2507
|
}
|
|
2483
2508
|
const boundSession = snapshotPlaybookSession(nextSession);
|
|
2484
|
-
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId);
|
|
2509
|
+
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
|
|
2510
|
+
const suspendedCall = boundSnapshot.schemaVersion === 2
|
|
2511
|
+
? boundSnapshot.suspendedCall
|
|
2512
|
+
: undefined;
|
|
2485
2513
|
let priorExternalPlayerTokens;
|
|
2486
2514
|
let externalStoreRestoreAttempted = false;
|
|
2487
2515
|
initialized = true;
|
|
@@ -2505,26 +2533,48 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2505
2533
|
// Every Captain call already consumed at least one trace number,
|
|
2506
2534
|
// so the global trace counter is a collision-safe id floor.
|
|
2507
2535
|
boundSnapshot.sequences.trace;
|
|
2508
|
-
// The
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2511
|
-
//
|
|
2536
|
+
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
2537
|
+
// apply boundary consumed trace numbers, so the persisted trace
|
|
2538
|
+
// counter is a collision-safe id floor here too, keeping
|
|
2539
|
+
// `apply-<n>` call ids unique across restore.
|
|
2512
2540
|
applyCallSequence = boundSnapshot.sequences.trace;
|
|
2513
2541
|
if (boundSession.playerSessions) {
|
|
2514
2542
|
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
2515
2543
|
externalStoreRestoreAttempted = true;
|
|
2516
2544
|
}
|
|
2517
2545
|
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
2546
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
2547
|
+
if (suspendedCall !== undefined) {
|
|
2548
|
+
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
2549
|
+
}
|
|
2518
2550
|
suppressInspectionEmissions = true;
|
|
2519
2551
|
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
2520
2552
|
actor.start();
|
|
2521
|
-
|
|
2553
|
+
if (controlPlaneError !== undefined)
|
|
2554
|
+
throw controlPlaneError;
|
|
2555
|
+
const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
|
|
2556
|
+
? {}
|
|
2557
|
+
: {
|
|
2558
|
+
pendingCall: {
|
|
2559
|
+
callId: suspendedCall.callId,
|
|
2560
|
+
playbookId: suspendedCall.playbookId,
|
|
2561
|
+
childSessionId: suspendedCall.childSessionId,
|
|
2562
|
+
},
|
|
2563
|
+
});
|
|
2522
2564
|
if (restoredState.status !== 'active') {
|
|
2523
2565
|
throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
|
|
2524
2566
|
}
|
|
2525
|
-
|
|
2567
|
+
if (stableJson(restoredState, 'restored runtime state') !==
|
|
2568
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
2569
|
+
throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
|
|
2570
|
+
}
|
|
2526
2571
|
priorState = restoredState;
|
|
2527
2572
|
await drainEmissions();
|
|
2573
|
+
suppressInspectionEmissions = false;
|
|
2574
|
+
// Final fallible step: after this publication the authoritative
|
|
2575
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
2576
|
+
// restore validation may trigger failed-start rollback.
|
|
2577
|
+
nestedBridge.confirmRestore();
|
|
2528
2578
|
})();
|
|
2529
2579
|
try {
|
|
2530
2580
|
await initTask;
|
|
@@ -1968,8 +1968,8 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1968
1968
|
// DR-029: the last event a public Boss boundary sent into the
|
|
1969
1969
|
// machine — classified, deterministic entry, or Boss reply — kept with
|
|
1970
1970
|
// its recorded payload so a failure-state retry action can replay the
|
|
1971
|
-
// event that drove the run into `failed`. Process-local: the
|
|
1972
|
-
//
|
|
1971
|
+
// event that drove the run into `failed`. Process-local: the durable
|
|
1972
|
+
// runtime snapshot does not persist it (PBRT-50).
|
|
1973
1973
|
let lastBossEvent: EventObject | undefined;
|
|
1974
1974
|
// DR-029: process-local at-most-once `apply` execution — the accepted receipt
|
|
1975
1975
|
// recorded for each idempotency key, returned verbatim on a repeated
|
|
@@ -3360,16 +3360,45 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3360
3360
|
}
|
|
3361
3361
|
},
|
|
3362
3362
|
|
|
3363
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked
|
|
3363
|
+
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
3364
|
+
// session, including one already-started suspended nested call.
|
|
3364
3365
|
// Defined only at a safe capture point — initialized, not disposing
|
|
3365
|
-
// or disposed, no active public boundary,
|
|
3366
|
-
//
|
|
3366
|
+
// or disposed, no active public boundary, and the actor quiescent with
|
|
3367
|
+
// status `active`.
|
|
3367
3368
|
exportSnapshot(): PlaybookRuntimeSnapshot | undefined {
|
|
3368
3369
|
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
3369
3370
|
return undefined;
|
|
3370
3371
|
}
|
|
3371
3372
|
if (activeSignal !== undefined) return undefined;
|
|
3372
|
-
|
|
3373
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
3374
|
+
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
3375
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
3376
|
+
return undefined;
|
|
3377
|
+
}
|
|
3378
|
+
let suspendedCall: typeof bridgeSuspendedCall;
|
|
3379
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
3380
|
+
if (
|
|
3381
|
+
pendingCall?.callId !== bridgeSuspendedCall.callId ||
|
|
3382
|
+
pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
3383
|
+
pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId
|
|
3384
|
+
) {
|
|
3385
|
+
return undefined;
|
|
3386
|
+
}
|
|
3387
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
3388
|
+
return undefined;
|
|
3389
|
+
}
|
|
3390
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
3391
|
+
if (
|
|
3392
|
+
bridgeSuspendedCall.turnId !== undefined &&
|
|
3393
|
+
bridgeSuspendedCall.turnId !== turnId
|
|
3394
|
+
) {
|
|
3395
|
+
return undefined;
|
|
3396
|
+
}
|
|
3397
|
+
suspendedCall = {
|
|
3398
|
+
...bridgeSuspendedCall,
|
|
3399
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3373
3402
|
const state = currentState();
|
|
3374
3403
|
if (state.status !== 'active' || !state.quiescent) return undefined;
|
|
3375
3404
|
const machineSnapshot = detachPersistedMachineSnapshot(
|
|
@@ -3379,7 +3408,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3379
3408
|
.context as Record<string, unknown>;
|
|
3380
3409
|
const pending = pendingBossQuestionFromContext(context ?? {});
|
|
3381
3410
|
return {
|
|
3382
|
-
schemaVersion:
|
|
3411
|
+
schemaVersion: 2,
|
|
3383
3412
|
playbookId: session.playbookId,
|
|
3384
3413
|
machine: machineSnapshot,
|
|
3385
3414
|
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
@@ -3405,6 +3434,7 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3405
3434
|
sourceItem: pending.sourceItem,
|
|
3406
3435
|
},
|
|
3407
3436
|
],
|
|
3437
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
3408
3438
|
};
|
|
3409
3439
|
},
|
|
3410
3440
|
|
|
@@ -3424,7 +3454,12 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3424
3454
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(
|
|
3425
3455
|
snapshot,
|
|
3426
3456
|
boundSession.playbookId,
|
|
3457
|
+
{ allowSuspendedCall: true },
|
|
3427
3458
|
);
|
|
3459
|
+
const suspendedCall =
|
|
3460
|
+
boundSnapshot.schemaVersion === 2
|
|
3461
|
+
? boundSnapshot.suspendedCall
|
|
3462
|
+
: undefined;
|
|
3428
3463
|
let priorExternalPlayerTokens:
|
|
3429
3464
|
| Readonly<Record<string, string>>
|
|
3430
3465
|
| undefined;
|
|
@@ -3450,28 +3485,59 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
3450
3485
|
// Every Captain call already consumed at least one trace number,
|
|
3451
3486
|
// so the global trace counter is a collision-safe id floor.
|
|
3452
3487
|
boundSnapshot.sequences.trace;
|
|
3453
|
-
// The
|
|
3454
|
-
//
|
|
3455
|
-
//
|
|
3456
|
-
//
|
|
3488
|
+
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
3489
|
+
// apply boundary consumed trace numbers, so the persisted trace
|
|
3490
|
+
// counter is a collision-safe id floor here too, keeping
|
|
3491
|
+
// `apply-<n>` call ids unique across restore.
|
|
3457
3492
|
applyCallSequence = boundSnapshot.sequences.trace;
|
|
3458
3493
|
if (boundSession.playerSessions) {
|
|
3459
3494
|
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
3460
3495
|
externalStoreRestoreAttempted = true;
|
|
3461
3496
|
}
|
|
3462
3497
|
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
3498
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
3499
|
+
if (suspendedCall !== undefined) {
|
|
3500
|
+
playbookCallTurnIds.set(
|
|
3501
|
+
suspendedCall.callId,
|
|
3502
|
+
suspendedCall.turnId,
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3463
3505
|
suppressInspectionEmissions = true;
|
|
3464
3506
|
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
3465
3507
|
actor.start();
|
|
3466
|
-
|
|
3508
|
+
if (controlPlaneError !== undefined) throw controlPlaneError;
|
|
3509
|
+
const restoredState = normalizePlaybookSnapshot(
|
|
3510
|
+
actor.getSnapshot(),
|
|
3511
|
+
suspendedCall === undefined
|
|
3512
|
+
? {}
|
|
3513
|
+
: {
|
|
3514
|
+
pendingCall: {
|
|
3515
|
+
callId: suspendedCall.callId,
|
|
3516
|
+
playbookId: suspendedCall.playbookId,
|
|
3517
|
+
childSessionId: suspendedCall.childSessionId,
|
|
3518
|
+
},
|
|
3519
|
+
},
|
|
3520
|
+
);
|
|
3467
3521
|
if (restoredState.status !== 'active') {
|
|
3468
3522
|
throw new Error(
|
|
3469
3523
|
`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`,
|
|
3470
3524
|
);
|
|
3471
3525
|
}
|
|
3472
|
-
|
|
3526
|
+
if (
|
|
3527
|
+
stableJson(restoredState, 'restored runtime state') !==
|
|
3528
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')
|
|
3529
|
+
) {
|
|
3530
|
+
throw new Error(
|
|
3531
|
+
'createPlaybookRuntime.restore: restored actor state does not match snapshot state',
|
|
3532
|
+
);
|
|
3533
|
+
}
|
|
3473
3534
|
priorState = restoredState;
|
|
3474
3535
|
await drainEmissions();
|
|
3536
|
+
suppressInspectionEmissions = false;
|
|
3537
|
+
// Final fallible step: after this publication the authoritative
|
|
3538
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
3539
|
+
// restore validation may trigger failed-start rollback.
|
|
3540
|
+
nestedBridge.confirmRestore();
|
|
3475
3541
|
})();
|
|
3476
3542
|
try {
|
|
3477
3543
|
await initTask;
|
package/src/xstate-runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AnyActorRef, type PromiseActorLogic, type SnapshotFrom } from 'xstate';
|
|
2
|
-
import type { CaptainResult, JsonValue, NormalizedError, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlayerResult } from './runtime.js';
|
|
2
|
+
import type { CaptainResult, JsonValue, NormalizedError, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookSuspendedCall, PlayerResult } from './runtime.js';
|
|
3
3
|
export * from './xstate-playbook-runtime.js';
|
|
4
4
|
/**
|
|
5
5
|
* Compose invocation-lifetime and imperative-boundary cancellation without
|
|
@@ -31,7 +31,15 @@ export interface SnapshotNormalizationOptions {
|
|
|
31
31
|
}
|
|
32
32
|
export declare function normalizePlaybookSnapshot(snapshot: unknown, options?: SnapshotNormalizationOptions): PlaybookState;
|
|
33
33
|
export declare function detachPersistedMachineSnapshot(persisted: unknown): JsonValue;
|
|
34
|
-
export
|
|
34
|
+
export interface PlaybookRuntimeSnapshotValidationOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Opt in only when the restore path will prepare and confirm the suspended
|
|
37
|
+
* call transaction. The default is fail-closed so a legacy restore cannot
|
|
38
|
+
* reopen or ignore it.
|
|
39
|
+
*/
|
|
40
|
+
allowSuspendedCall?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare function assertPlaybookRuntimeSnapshot(value: unknown, expectedPlaybookId: string, options?: PlaybookRuntimeSnapshotValidationOptions): PlaybookRuntimeSnapshot;
|
|
35
43
|
export interface NestedPlaybookInput {
|
|
36
44
|
stateId: string;
|
|
37
45
|
playbookId: string;
|
|
@@ -68,6 +76,15 @@ export interface PendingCallObserver {
|
|
|
68
76
|
}
|
|
69
77
|
export interface NestedPlaybookBridge<TInput extends NestedPlaybookInput = NestedPlaybookInput> extends PendingCallObserver {
|
|
70
78
|
actorLogic: PromiseActorLogic<JsonValue | undefined, TInput>;
|
|
79
|
+
/** Arm fail-closed actor startup for a snapshot with zero or one nested call. */
|
|
80
|
+
prepareRestore(call?: PlaybookSuspendedCall): void;
|
|
81
|
+
/**
|
|
82
|
+
* Commit restore startup after the persisted machine recreated exactly the
|
|
83
|
+
* expected zero or one nested invocation.
|
|
84
|
+
*/
|
|
85
|
+
confirmRestore(): void;
|
|
86
|
+
/** Complete durable identity; undefined until a normal or restored call suspends. */
|
|
87
|
+
getSuspendedCall(): PlaybookSuspendedCall | undefined;
|
|
71
88
|
resume(input: {
|
|
72
89
|
callId: string;
|
|
73
90
|
result: PlaybookCallResult;
|