@sublang/playbook 6.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 +28 -11
- package/docs/cli.md +158 -68
- package/docs/configuration.md +246 -108
- package/docs/embedding.md +71 -25
- package/package.json +6 -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 +1900 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +573 -535
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1164 -991
- package/reference/sdlc/code.playbook/bin/session-store.js +1961 -0
- 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 +101 -9
- package/reference/sdlc/code.playbook/playbook-captain.js +1690 -213
- package/reference/sdlc/code.playbook/playbook-captain.ts +2492 -253
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +44 -62
- 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 +244 -143
- package/reference/sdlc/decide.playbook/decide.playbook.ts +326 -171
- 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 +140 -97
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +24 -8
- package/src/runtime.ts +29 -13
- package/src/xstate-playbook-runtime.d.ts +21 -17
- package/src/xstate-playbook-runtime.js +301 -159
- package/src/xstate-playbook-runtime.ts +405 -186
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +403 -62
- package/src/xstate-runtime.ts +566 -78
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// Linker inputs:
|
|
8
8
|
// FSM artifact: ./decide.fsm.ts
|
|
9
9
|
// Link target: @sublang/playbook/runtime
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// Role binding: canonical coder and reviewer roles; concrete players
|
|
11
|
+
// and prompt identities are supplied by the host session
|
|
12
12
|
// Adjudication: LLM-judge per state (default)
|
|
13
13
|
// Boss-event mapping: free-text judge classification (default)
|
|
14
14
|
// Abort strategy: natural rejection; every player-invoking state's
|
|
@@ -19,41 +19,16 @@ import PQueue from 'p-queue';
|
|
|
19
19
|
import { createActor, fromPromise } from 'xstate';
|
|
20
20
|
import { assertJsonSafe, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
21
21
|
import decideMachine from './decide.fsm.js';
|
|
22
|
-
const DEFAULT_PLAYER_BINDING = {
|
|
23
|
-
Coder: 'coder',
|
|
24
|
-
Reviewer: 'reviewer',
|
|
25
|
-
};
|
|
26
22
|
function snapshotDecideRuntimeOptions(value) {
|
|
27
23
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
28
24
|
if (!isPlainObject(captured)) {
|
|
29
25
|
throw new TypeError('DECIDE runtime options must be an object');
|
|
30
26
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new TypeError(`DECIDE runtime options.${key} is not declared`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (typeof captured.coderLlm !== 'string' ||
|
|
38
|
-
captured.coderLlm.trim().length === 0) {
|
|
39
|
-
throw new TypeError('DECIDE runtime options.coderLlm must be a non-empty string');
|
|
40
|
-
}
|
|
41
|
-
if ('playerBinding' in captured) {
|
|
42
|
-
const playerBinding = captured.playerBinding;
|
|
43
|
-
if (!isPlainObject(playerBinding)) {
|
|
44
|
-
throw new TypeError('DECIDE runtime options.playerBinding must be an object');
|
|
45
|
-
}
|
|
46
|
-
const playerNames = new Set(['Coder', 'Reviewer']);
|
|
47
|
-
for (const [player, playerId] of Object.entries(playerBinding)) {
|
|
48
|
-
if (!playerNames.has(player)) {
|
|
49
|
-
throw new TypeError(`DECIDE runtime options.playerBinding.${player} is not declared`);
|
|
50
|
-
}
|
|
51
|
-
if (typeof playerId !== 'string' || playerId.trim().length === 0) {
|
|
52
|
-
throw new TypeError(`DECIDE runtime options.playerBinding.${player} must be a non-empty string`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
27
|
+
const [unknown] = Object.keys(captured);
|
|
28
|
+
if (unknown !== undefined) {
|
|
29
|
+
throw new TypeError(`DECIDE runtime options.${unknown} is not declared`);
|
|
55
30
|
}
|
|
56
|
-
return
|
|
31
|
+
return Object.freeze({});
|
|
57
32
|
}
|
|
58
33
|
const STATE_DESCRIPTIONS = {
|
|
59
34
|
ready: 'Waiting for a topic to decide.',
|
|
@@ -68,31 +43,31 @@ const STATE_DESCRIPTIONS = {
|
|
|
68
43
|
reportedReviewFailure: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
69
44
|
done: 'DECIDE completed with an approved commit.',
|
|
70
45
|
};
|
|
71
|
-
const
|
|
72
|
-
{ stateId: 'askCoderProposal',
|
|
46
|
+
const ROLE_STATES = [
|
|
47
|
+
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
73
48
|
{
|
|
74
49
|
stateId: 'askReviewerProposal',
|
|
75
|
-
|
|
50
|
+
role: 'reviewer',
|
|
76
51
|
sourceItem: 'DECIDE-2',
|
|
77
52
|
},
|
|
78
|
-
{ stateId: 'commitCoderProposal',
|
|
53
|
+
{ stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
|
|
79
54
|
];
|
|
80
|
-
const
|
|
55
|
+
const ROLE_STATE_IDS = new Set(ROLE_STATES.map((state) => state.stateId));
|
|
56
|
+
const ROLE_IDS = ['coder', 'reviewer'];
|
|
57
|
+
const ROLE_ID_SET = new Set(ROLE_IDS);
|
|
58
|
+
const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
|
|
81
59
|
const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
|
|
82
60
|
const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
|
|
83
61
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
84
62
|
const TRACE_TOPIC = 'playbook.trace';
|
|
85
63
|
const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
|
|
86
|
-
const PLACEHOLDER_FIELDS = [
|
|
87
|
-
['<caller-topic>', 'callerTopic'],
|
|
88
|
-
['<coder-llm>', 'coderLlm'],
|
|
89
|
-
];
|
|
64
|
+
const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
|
|
90
65
|
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
91
66
|
'coderProposal',
|
|
92
67
|
'reviewerProposal',
|
|
93
68
|
'coderOutput',
|
|
94
69
|
]);
|
|
95
|
-
function composePlayerPrompt(input) {
|
|
70
|
+
function composePlayerPrompt(input, promptIdentity) {
|
|
96
71
|
const blocks = [];
|
|
97
72
|
if (input.pendingBossQuestion && input.bossReply !== undefined) {
|
|
98
73
|
blocks.push([
|
|
@@ -111,6 +86,9 @@ function composePlayerPrompt(input) {
|
|
|
111
86
|
if (typeof value === 'string')
|
|
112
87
|
replacements.set(placeholder, value);
|
|
113
88
|
}
|
|
89
|
+
if (input.prompt.includes('<coder-llm>')) {
|
|
90
|
+
replacements.set('<coder-llm>', promptIdentity('coder'));
|
|
91
|
+
}
|
|
114
92
|
const body = input.prompt.replace(/<caller-topic>|<coder-llm>/g, (placeholder, offset, source) => {
|
|
115
93
|
const value = replacements.get(placeholder);
|
|
116
94
|
if (value === undefined)
|
|
@@ -123,18 +101,6 @@ function composePlayerPrompt(input) {
|
|
|
123
101
|
blocks.push(body);
|
|
124
102
|
return blocks.join('\n\n');
|
|
125
103
|
}
|
|
126
|
-
function resolvePlayerId(input, binding) {
|
|
127
|
-
switch (input.player) {
|
|
128
|
-
case 'Coder':
|
|
129
|
-
return binding.Coder;
|
|
130
|
-
case 'Reviewer':
|
|
131
|
-
return binding.Reviewer;
|
|
132
|
-
default: {
|
|
133
|
-
const exhaustive = input.player;
|
|
134
|
-
throw new Error(`unknown player ${String(exhaustive)}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
104
|
// A `result` description names required payload fields in its
|
|
139
105
|
// "Output shall include ..." sentence.
|
|
140
106
|
function requiredFieldsFor(description) {
|
|
@@ -203,6 +169,22 @@ function parseJudgeJson(raw) {
|
|
|
203
169
|
function isPlainObject(value) {
|
|
204
170
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
205
171
|
}
|
|
172
|
+
function sortJson(value) {
|
|
173
|
+
if (Array.isArray(value))
|
|
174
|
+
return value.map((entry) => sortJson(entry));
|
|
175
|
+
if (value !== null && typeof value === 'object') {
|
|
176
|
+
const record = value;
|
|
177
|
+
const sorted = {};
|
|
178
|
+
for (const key of Object.keys(record).sort()) {
|
|
179
|
+
sorted[key] = sortJson(record[key]);
|
|
180
|
+
}
|
|
181
|
+
return sorted;
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
function stableJson(value, path) {
|
|
186
|
+
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
187
|
+
}
|
|
206
188
|
function stripCodeFence(text) {
|
|
207
189
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
208
190
|
return fence ? fence[1].trim() : text;
|
|
@@ -267,7 +249,7 @@ function buildClassifierPrompt(text, ctx) {
|
|
|
267
249
|
if (ctx.pendingQuestions.length > 0) {
|
|
268
250
|
lines.push('Pending Boss questions:');
|
|
269
251
|
for (const pending of ctx.pendingQuestions) {
|
|
270
|
-
lines.push(`- ${pending.questionId} (${pending.
|
|
252
|
+
lines.push(`- ${pending.questionId} (${pending.asker.roleId}): ${pending.question}`);
|
|
271
253
|
}
|
|
272
254
|
lines.push('If the Boss message answers a pending question, classify it as BOSS_REPLY; if it is a fresh directive, classify it accordingly.');
|
|
273
255
|
}
|
|
@@ -338,7 +320,7 @@ function buildAdjudicatorPrompt(input, playerOutput) {
|
|
|
338
320
|
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
339
321
|
'seek external evidence. Decide only from the supplied player output ' +
|
|
340
322
|
'and guard descriptions. Reply with exactly one JSON object and no prose.');
|
|
341
|
-
lines.push(`The
|
|
323
|
+
lines.push(`The role "${roleLabel(input.role)}" produced the output below for source item ${input.sourceItem}.`);
|
|
342
324
|
lines.push('Choose exactly one guard whose description matches that output.');
|
|
343
325
|
lines.push('');
|
|
344
326
|
lines.push('Player output (verbatim):');
|
|
@@ -436,7 +418,9 @@ function pendingQuestionsFromContext(context) {
|
|
|
436
418
|
obj.questionId === key &&
|
|
437
419
|
typeof obj.resumeStateId === 'string' &&
|
|
438
420
|
typeof obj.sourceItem === 'string' &&
|
|
439
|
-
|
|
421
|
+
isPlainObject(obj.asker) &&
|
|
422
|
+
obj.asker.kind === 'role' &&
|
|
423
|
+
ROLE_ID_SET.has(String(obj.asker.roleId)) &&
|
|
440
424
|
typeof obj.question === 'string') {
|
|
441
425
|
questions.push(obj);
|
|
442
426
|
}
|
|
@@ -452,7 +436,7 @@ const WAIT_STATE_IDS = new Set([
|
|
|
452
436
|
'awaitBossReply',
|
|
453
437
|
]);
|
|
454
438
|
const STATUS_STATE_IDS = new Set([
|
|
455
|
-
...
|
|
439
|
+
...ROLE_STATE_IDS,
|
|
456
440
|
...WAIT_STATE_IDS,
|
|
457
441
|
'failed',
|
|
458
442
|
]);
|
|
@@ -511,14 +495,7 @@ function telemetryPayload(previousState, state, event, context) {
|
|
|
511
495
|
return payload;
|
|
512
496
|
}
|
|
513
497
|
export const createPlaybookRuntime = (options) => {
|
|
514
|
-
const
|
|
515
|
-
const binding = {
|
|
516
|
-
...DEFAULT_PLAYER_BINDING,
|
|
517
|
-
...(boundOptions.playerBinding ?? {}),
|
|
518
|
-
};
|
|
519
|
-
const fsmInput = {
|
|
520
|
-
coderLlm: boundOptions.coderLlm,
|
|
521
|
-
};
|
|
498
|
+
const fsmInput = snapshotDecideRuntimeOptions(options);
|
|
522
499
|
let ports;
|
|
523
500
|
let sessionIdentity;
|
|
524
501
|
let actor;
|
|
@@ -538,9 +515,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
538
515
|
let disposalPromise;
|
|
539
516
|
let controlPlaneError;
|
|
540
517
|
let nestedBridge;
|
|
541
|
-
const
|
|
518
|
+
const privateResumeTokens = new Map();
|
|
542
519
|
const playbookCallTurnIds = new Map();
|
|
543
|
-
const
|
|
520
|
+
const inFlightPlayerKeys = new Set();
|
|
544
521
|
const activeBoundaryCalls = new Set();
|
|
545
522
|
const activeEmissionCalls = new Set();
|
|
546
523
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
@@ -623,67 +600,138 @@ export const createPlaybookRuntime = (options) => {
|
|
|
623
600
|
}
|
|
624
601
|
return sessionIdentity;
|
|
625
602
|
};
|
|
626
|
-
const
|
|
603
|
+
const bindSession = (nextSession) => {
|
|
604
|
+
const bound = snapshotPlaybookSession(nextSession);
|
|
605
|
+
if (bound.roleBindings === undefined)
|
|
606
|
+
return bound;
|
|
607
|
+
const actual = Object.keys(bound.roleBindings).sort();
|
|
608
|
+
const expected = [...ROLE_IDS].sort();
|
|
609
|
+
const missing = expected.filter((roleId) => !actual.includes(roleId));
|
|
610
|
+
const extra = actual.filter((roleId) => !ROLE_ID_SET.has(roleId));
|
|
611
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
612
|
+
throw new TypeError(`DECIDE session roleBindings must cover exactly [${expected.join(', ')}]` +
|
|
613
|
+
`${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
|
|
614
|
+
`${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
|
|
615
|
+
}
|
|
616
|
+
return bound;
|
|
617
|
+
};
|
|
618
|
+
const resolvedPlayerId = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.playerId;
|
|
619
|
+
const promptIdentity = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
620
|
+
const composeInvocationPrompt = (input) => {
|
|
621
|
+
let active = true;
|
|
622
|
+
const lookup = (roleId) => {
|
|
623
|
+
if (!active) {
|
|
624
|
+
throw new Error('DECIDE prompt identity lookup is no longer active for this invocation');
|
|
625
|
+
}
|
|
626
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
627
|
+
throw new TypeError(`DECIDE prompt identity lookup rejected undeclared role ${String(roleId)}`);
|
|
628
|
+
}
|
|
629
|
+
return promptIdentity(roleId);
|
|
630
|
+
};
|
|
631
|
+
try {
|
|
632
|
+
return composePlayerPrompt(input, lookup);
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
active = false;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const continuationKey = (roleId, playerId) => playerId ?? roleId;
|
|
639
|
+
const tokensByContinuationKey = (tokens) => {
|
|
640
|
+
const byKey = new Map();
|
|
641
|
+
for (const [roleId, token] of Object.entries(tokens)) {
|
|
642
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
643
|
+
throw new TypeError(`DECIDE role tokens contain unknown role ${roleId}`);
|
|
644
|
+
}
|
|
645
|
+
const typedRole = roleId;
|
|
646
|
+
const key = continuationKey(typedRole, resolvedPlayerId(typedRole));
|
|
647
|
+
const prior = byKey.get(key);
|
|
648
|
+
if (prior !== undefined && prior !== token) {
|
|
649
|
+
throw new TypeError(`DECIDE runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
|
|
650
|
+
}
|
|
651
|
+
byKey.set(key, token);
|
|
652
|
+
}
|
|
653
|
+
const rolesByKey = new Map();
|
|
654
|
+
for (const roleId of ROLE_IDS) {
|
|
655
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
656
|
+
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
657
|
+
}
|
|
658
|
+
for (const [key, roles] of rolesByKey) {
|
|
659
|
+
if (roles.length < 2)
|
|
660
|
+
continue;
|
|
661
|
+
const present = roles.filter((roleId) => tokens[roleId] !== undefined);
|
|
662
|
+
if (present.length !== 0 && present.length !== roles.length) {
|
|
663
|
+
throw new TypeError(`DECIDE role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return byKey;
|
|
667
|
+
};
|
|
668
|
+
const selectPlayerResume = (roleId, playerId) => {
|
|
627
669
|
const session = requireSessionIdentity();
|
|
628
670
|
const selected = session.playerSessions
|
|
629
|
-
? session.playerSessions.select(
|
|
630
|
-
:
|
|
671
|
+
? session.playerSessions.select(roleId)
|
|
672
|
+
: privateResumeTokens.get(continuationKey(roleId, playerId)) ?? false;
|
|
631
673
|
if (selected !== false &&
|
|
632
674
|
(typeof selected !== 'string' || selected.trim().length === 0)) {
|
|
633
|
-
throw new TypeError(`player session store returned an invalid resume token for ${
|
|
675
|
+
throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
|
|
634
676
|
}
|
|
635
677
|
return selected;
|
|
636
678
|
};
|
|
637
|
-
const updatePlayerResume = (playerId,
|
|
679
|
+
const updatePlayerResume = (roleId, playerId, result) => {
|
|
680
|
+
if (result.resumeToken === undefined && result.status !== 'ok')
|
|
681
|
+
return;
|
|
638
682
|
const session = requireSessionIdentity();
|
|
639
683
|
if (session.playerSessions) {
|
|
640
|
-
session.playerSessions.update(
|
|
684
|
+
session.playerSessions.update(roleId, result.resumeToken);
|
|
641
685
|
}
|
|
642
|
-
else if (resumeToken !== undefined
|
|
643
|
-
|
|
686
|
+
else if (result.resumeToken !== undefined) {
|
|
687
|
+
privateResumeTokens.set(continuationKey(roleId, playerId), result.resumeToken);
|
|
644
688
|
}
|
|
645
689
|
else {
|
|
646
|
-
|
|
690
|
+
privateResumeTokens.delete(continuationKey(roleId, playerId));
|
|
647
691
|
}
|
|
648
692
|
};
|
|
649
|
-
const
|
|
693
|
+
const snapshotRoleResumeTokens = () => {
|
|
650
694
|
const session = requireSessionIdentity();
|
|
651
695
|
const captured = snapshotJsonValue(session.playerSessions
|
|
652
696
|
? session.playerSessions.snapshot()
|
|
653
|
-
: Object.fromEntries(
|
|
697
|
+
: Object.fromEntries(ROLE_IDS.flatMap((roleId) => {
|
|
698
|
+
const token = privateResumeTokens.get(continuationKey(roleId, resolvedPlayerId(roleId)));
|
|
699
|
+
return token === undefined ? [] : [[roleId, token]];
|
|
700
|
+
})), 'player session store snapshot');
|
|
654
701
|
if (!isPlainObject(captured)) {
|
|
655
702
|
throw new TypeError('player session store snapshot must be an object');
|
|
656
703
|
}
|
|
657
704
|
const tokens = {};
|
|
658
|
-
for (const [
|
|
659
|
-
if (
|
|
660
|
-
throw new TypeError(
|
|
705
|
+
for (const [roleId, token] of Object.entries(captured)) {
|
|
706
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
707
|
+
throw new TypeError(`player session store snapshot contains unknown role ${roleId}`);
|
|
661
708
|
}
|
|
662
709
|
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
663
|
-
throw new TypeError(`player session store snapshot token for ${
|
|
710
|
+
throw new TypeError(`player session store snapshot token for ${roleId} must be a non-empty string`);
|
|
664
711
|
}
|
|
665
|
-
tokens[
|
|
712
|
+
tokens[roleId] = token;
|
|
666
713
|
}
|
|
714
|
+
tokensByContinuationKey(tokens);
|
|
667
715
|
return tokens;
|
|
668
716
|
};
|
|
669
|
-
const
|
|
717
|
+
const restoreRoleResumeTokens = (tokens) => {
|
|
718
|
+
const byKey = tokensByContinuationKey(tokens);
|
|
670
719
|
const session = requireSessionIdentity();
|
|
671
720
|
if (session.playerSessions) {
|
|
672
721
|
session.playerSessions.restore(tokens);
|
|
673
722
|
return;
|
|
674
723
|
}
|
|
675
|
-
|
|
676
|
-
for (const [
|
|
677
|
-
|
|
678
|
-
}
|
|
724
|
+
privateResumeTokens.clear();
|
|
725
|
+
for (const [key, token] of byKey)
|
|
726
|
+
privateResumeTokens.set(key, token);
|
|
679
727
|
};
|
|
680
|
-
const currentState = () => {
|
|
728
|
+
const currentState = (pendingCall = nestedBridge.getPendingCall()) => {
|
|
681
729
|
const live = actor;
|
|
682
730
|
if (!live) {
|
|
683
731
|
throw new Error('decide runtime: actor is not initialized');
|
|
684
732
|
}
|
|
685
733
|
return normalizePlaybookSnapshot(live.getSnapshot(), {
|
|
686
|
-
pendingCall
|
|
734
|
+
pendingCall,
|
|
687
735
|
});
|
|
688
736
|
};
|
|
689
737
|
const stateIdentity = (state) => {
|
|
@@ -694,7 +742,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
694
742
|
const identity = requireSessionIdentity();
|
|
695
743
|
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
696
744
|
const trace = Object.freeze({
|
|
697
|
-
schemaVersion:
|
|
745
|
+
schemaVersion: 3,
|
|
698
746
|
sessionId: identity.sessionId,
|
|
699
747
|
playbookId: identity.playbookId,
|
|
700
748
|
rootSessionId: identity.rootSessionId,
|
|
@@ -796,15 +844,17 @@ export const createPlaybookRuntime = (options) => {
|
|
|
796
844
|
};
|
|
797
845
|
const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
|
|
798
846
|
const runPlayerCall = async (input, signal) => {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
throw new Error(`resolved player "${playerId}" already has an in-flight call`);
|
|
847
|
+
if (!ROLE_ID_SET.has(input.role)) {
|
|
848
|
+
throw new TypeError(`DECIDE player input role must name a declared local role`);
|
|
802
849
|
}
|
|
803
|
-
const
|
|
850
|
+
const roleId = input.role;
|
|
851
|
+
const playerId = resolvedPlayerId(roleId);
|
|
852
|
+
const playerKey = continuationKey(roleId, playerId);
|
|
853
|
+
const prompt = composeInvocationPrompt(input);
|
|
804
854
|
let resume;
|
|
805
855
|
try {
|
|
806
856
|
signal.throwIfAborted();
|
|
807
|
-
resume = selectPlayerResume(playerId);
|
|
857
|
+
resume = selectPlayerResume(roleId, playerId);
|
|
808
858
|
}
|
|
809
859
|
catch (error) {
|
|
810
860
|
latchControlPlaneError(error, signal);
|
|
@@ -812,10 +862,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
812
862
|
}
|
|
813
863
|
const callId = `player-${++playerCallSequence}`;
|
|
814
864
|
const identity = {
|
|
815
|
-
purpose: 'captain',
|
|
816
865
|
stateId: input.stateId,
|
|
817
866
|
sourceItem: input.sourceItem,
|
|
818
|
-
|
|
867
|
+
roleId,
|
|
868
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
819
869
|
resume,
|
|
820
870
|
};
|
|
821
871
|
const emitFailure = (error) => emitTrace('player.call.finished', {
|
|
@@ -826,13 +876,19 @@ export const createPlaybookRuntime = (options) => {
|
|
|
826
876
|
message: String(error),
|
|
827
877
|
},
|
|
828
878
|
}, { turnId: currentTurnId, callId });
|
|
829
|
-
|
|
879
|
+
if (inFlightPlayerKeys.has(playerKey)) {
|
|
880
|
+
const error = new Error(`resolved player key "${playerKey}" already has an in-flight call`);
|
|
881
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
882
|
+
await emitFailure(error);
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
inFlightPlayerKeys.add(playerKey);
|
|
830
886
|
try {
|
|
831
887
|
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
832
888
|
let rawResult;
|
|
833
889
|
try {
|
|
834
890
|
signal.throwIfAborted();
|
|
835
|
-
const boundary = Promise.resolve(requirePorts().callPlayer(
|
|
891
|
+
const boundary = Promise.resolve(requirePorts().callPlayer(roleId, prompt, signal, { resume }));
|
|
836
892
|
rawResult = await boundary;
|
|
837
893
|
// An XState sibling cancellation does not cancel an arbitrary coder
|
|
838
894
|
// promise. Re-check before a late resolution can mutate continuity or
|
|
@@ -869,10 +925,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
869
925
|
// The resolved result is authoritative even on aborted/error status.
|
|
870
926
|
// Update continuation state before interpreting that status.
|
|
871
927
|
try {
|
|
872
|
-
updatePlayerResume(playerId,
|
|
873
|
-
result.resumeToken.trim().length > 0
|
|
874
|
-
? result.resumeToken
|
|
875
|
-
: undefined);
|
|
928
|
+
updatePlayerResume(roleId, playerId, result);
|
|
876
929
|
}
|
|
877
930
|
catch (error) {
|
|
878
931
|
latchControlPlaneError(error, signal);
|
|
@@ -900,10 +953,14 @@ export const createPlaybookRuntime = (options) => {
|
|
|
900
953
|
? { error: normalizeErrorFull(result.error) }
|
|
901
954
|
: {}),
|
|
902
955
|
}, { turnId: currentTurnId, callId });
|
|
903
|
-
return {
|
|
956
|
+
return {
|
|
957
|
+
roleId,
|
|
958
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
959
|
+
result,
|
|
960
|
+
};
|
|
904
961
|
}
|
|
905
962
|
finally {
|
|
906
|
-
|
|
963
|
+
inFlightPlayerKeys.delete(playerKey);
|
|
907
964
|
}
|
|
908
965
|
};
|
|
909
966
|
const callPlayer = (input, signal) => {
|
|
@@ -922,7 +979,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
922
979
|
throw error;
|
|
923
980
|
}
|
|
924
981
|
combined.throwIfAborted();
|
|
925
|
-
let { playerId, result } = await callPlayer(input, combined);
|
|
982
|
+
let { roleId, playerId, result } = await callPlayer(input, combined);
|
|
926
983
|
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
927
984
|
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
928
985
|
// whitespace-only earns exactly one corrective re-ask — the same
|
|
@@ -933,14 +990,14 @@ export const createPlaybookRuntime = (options) => {
|
|
|
933
990
|
// are never retried), and a rejecting finish emission rejects
|
|
934
991
|
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
935
992
|
combined.throwIfAborted();
|
|
936
|
-
({ playerId, result } = await callPlayer(input, combined));
|
|
993
|
+
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
937
994
|
}
|
|
938
995
|
if (result.status !== 'ok') {
|
|
939
|
-
throw new Error(`
|
|
996
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
940
997
|
}
|
|
941
998
|
const finalText = result.finalText ?? '';
|
|
942
999
|
if (isEmptyFinalText(finalText)) {
|
|
943
|
-
throw new Error(`
|
|
1000
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
|
|
944
1001
|
}
|
|
945
1002
|
combined.throwIfAborted();
|
|
946
1003
|
try {
|
|
@@ -1041,8 +1098,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1041
1098
|
if (WAIT_STATE_IDS.has(activeStateId)) {
|
|
1042
1099
|
const pending = questionForWaitState(activeStateId, pendingQuestions);
|
|
1043
1100
|
if (pending) {
|
|
1044
|
-
scheduleStatus(`${pending.
|
|
1045
|
-
scheduleStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.
|
|
1101
|
+
scheduleStatus(`${pending.asker.roleId} asks: ${pending.question}`, activeStateId);
|
|
1102
|
+
scheduleStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, activeStateId);
|
|
1046
1103
|
}
|
|
1047
1104
|
continue;
|
|
1048
1105
|
}
|
|
@@ -1052,10 +1109,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1052
1109
|
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
1053
1110
|
if (description === undefined)
|
|
1054
1111
|
continue;
|
|
1055
|
-
const
|
|
1056
|
-
scheduleStatus(
|
|
1112
|
+
const roleState = ROLE_STATES.find((candidate) => candidate.stateId === activeStateId);
|
|
1113
|
+
scheduleStatus(roleState === undefined
|
|
1057
1114
|
? '◆ workflow failed; awaiting Boss recovery.'
|
|
1058
|
-
: `⤷ ${
|
|
1115
|
+
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined ? undefined : { lastError });
|
|
1059
1116
|
}
|
|
1060
1117
|
}
|
|
1061
1118
|
catch (error) {
|
|
@@ -1181,7 +1238,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1181
1238
|
// The caller rethrows its original failure. A restore failure skips
|
|
1182
1239
|
// the disposal trace — the parked session was never re-bound in this
|
|
1183
1240
|
// process, so its persisted snapshot stays authoritative (DR-014 §2).
|
|
1184
|
-
const cleanupFailedStart = async (options) => {
|
|
1241
|
+
const cleanupFailedStart = async (cause, options) => {
|
|
1185
1242
|
let finalState;
|
|
1186
1243
|
if (options.emitDisposal && actor) {
|
|
1187
1244
|
try {
|
|
@@ -1197,6 +1254,12 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1197
1254
|
catch {
|
|
1198
1255
|
// Preserve the original startup failure.
|
|
1199
1256
|
}
|
|
1257
|
+
try {
|
|
1258
|
+
await nestedBridge.abortPending(cause);
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
// Preserve the original startup failure.
|
|
1262
|
+
}
|
|
1200
1263
|
try {
|
|
1201
1264
|
await judgeQueue.onIdle();
|
|
1202
1265
|
await drainBoundaryCallsAndEmissions();
|
|
@@ -1217,8 +1280,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1217
1280
|
// The session-start error remains authoritative.
|
|
1218
1281
|
}
|
|
1219
1282
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1283
|
+
privateResumeTokens.clear();
|
|
1284
|
+
inFlightPlayerKeys.clear();
|
|
1222
1285
|
activeBoundaryCalls.clear();
|
|
1223
1286
|
activeEmissionCalls.clear();
|
|
1224
1287
|
emissionQueue.clear();
|
|
@@ -1248,7 +1311,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1248
1311
|
disposalPromise !== undefined) {
|
|
1249
1312
|
throw new Error('decide runtime: init(session) may only be called once');
|
|
1250
1313
|
}
|
|
1251
|
-
const identity =
|
|
1314
|
+
const identity = bindSession(session);
|
|
1252
1315
|
let finishInitialization;
|
|
1253
1316
|
const initialization = new Promise((resolve) => {
|
|
1254
1317
|
finishInitialization = resolve;
|
|
@@ -1269,7 +1332,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1269
1332
|
await flush();
|
|
1270
1333
|
}
|
|
1271
1334
|
catch (error) {
|
|
1272
|
-
await cleanupFailedStart({ emitDisposal: true });
|
|
1335
|
+
await cleanupFailedStart(error, { emitDisposal: true });
|
|
1273
1336
|
throw error;
|
|
1274
1337
|
}
|
|
1275
1338
|
finally {
|
|
@@ -1278,10 +1341,11 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1278
1341
|
initInFlight = undefined;
|
|
1279
1342
|
}
|
|
1280
1343
|
},
|
|
1281
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked
|
|
1344
|
+
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1345
|
+
// session, including one already-started suspended REVIEW call.
|
|
1282
1346
|
// Defined only at a safe capture point — initialized, not disposing
|
|
1283
|
-
// or disposed, no active public boundary, and the actor quiescent
|
|
1284
|
-
//
|
|
1347
|
+
// or disposed, no active public boundary, and the actor quiescent with
|
|
1348
|
+
// status `active`.
|
|
1285
1349
|
exportSnapshot() {
|
|
1286
1350
|
if (!actor ||
|
|
1287
1351
|
!sessionIdentity ||
|
|
@@ -1292,18 +1356,43 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1292
1356
|
if (currentTurnId !== undefined || currentSignal !== undefined) {
|
|
1293
1357
|
return undefined;
|
|
1294
1358
|
}
|
|
1295
|
-
|
|
1359
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
1360
|
+
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
1361
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
1362
|
+
return undefined;
|
|
1363
|
+
}
|
|
1364
|
+
if (pendingCall !== undefined &&
|
|
1365
|
+
bridgeSuspendedCall !== undefined &&
|
|
1366
|
+
(pendingCall.callId !== bridgeSuspendedCall.callId ||
|
|
1367
|
+
pendingCall.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
1368
|
+
pendingCall.childSessionId !== bridgeSuspendedCall.childSessionId)) {
|
|
1296
1369
|
return undefined;
|
|
1370
|
+
}
|
|
1371
|
+
let suspendedCall;
|
|
1372
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
1373
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
1374
|
+
return undefined;
|
|
1375
|
+
}
|
|
1376
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
1377
|
+
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
1378
|
+
bridgeSuspendedCall.turnId !== turnId) {
|
|
1379
|
+
return undefined;
|
|
1380
|
+
}
|
|
1381
|
+
suspendedCall = {
|
|
1382
|
+
...bridgeSuspendedCall,
|
|
1383
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1297
1386
|
const state = currentState();
|
|
1298
1387
|
if (state.status !== 'active' || !state.quiescent)
|
|
1299
1388
|
return undefined;
|
|
1300
1389
|
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
1301
1390
|
const context = actor.getSnapshot().context;
|
|
1302
1391
|
return {
|
|
1303
|
-
schemaVersion:
|
|
1392
|
+
schemaVersion: 3,
|
|
1304
1393
|
playbookId: sessionIdentity.playbookId,
|
|
1305
1394
|
machine,
|
|
1306
|
-
|
|
1395
|
+
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
1307
1396
|
sequences: {
|
|
1308
1397
|
trace: traceSequence,
|
|
1309
1398
|
turn: turnSequence,
|
|
@@ -1314,10 +1403,11 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1314
1403
|
state,
|
|
1315
1404
|
pendingBossQuestions: pendingQuestionsFromContext(context).map((pending) => ({
|
|
1316
1405
|
questionId: pending.questionId,
|
|
1317
|
-
|
|
1406
|
+
asker: pending.asker,
|
|
1318
1407
|
question: pending.question,
|
|
1319
1408
|
sourceItem: pending.sourceItem,
|
|
1320
1409
|
})),
|
|
1410
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
1321
1411
|
};
|
|
1322
1412
|
},
|
|
1323
1413
|
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
@@ -1332,8 +1422,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1332
1422
|
disposalPromise !== undefined) {
|
|
1333
1423
|
throw new Error('decide runtime: restore(session, snapshot) may only be called once');
|
|
1334
1424
|
}
|
|
1335
|
-
const identity =
|
|
1336
|
-
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId);
|
|
1425
|
+
const identity = bindSession(session);
|
|
1426
|
+
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId, { allowSuspendedCall: true });
|
|
1427
|
+
const suspendedCall = boundSnapshot.suspendedCall;
|
|
1337
1428
|
let finishInitialization;
|
|
1338
1429
|
const initialization = new Promise((resolve) => {
|
|
1339
1430
|
finishInitialization = resolve;
|
|
@@ -1342,7 +1433,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1342
1433
|
lifecycleStarted = true;
|
|
1343
1434
|
ports = identity.ports;
|
|
1344
1435
|
sessionIdentity = identity;
|
|
1345
|
-
let
|
|
1436
|
+
let priorExternalRoleTokens;
|
|
1346
1437
|
try {
|
|
1347
1438
|
traceSequence = boundSnapshot.sequences.trace;
|
|
1348
1439
|
turnSequence = boundSnapshot.sequences.turn;
|
|
@@ -1350,31 +1441,43 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1350
1441
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1351
1442
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
1352
1443
|
if (identity.playerSessions) {
|
|
1353
|
-
|
|
1444
|
+
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
1445
|
+
}
|
|
1446
|
+
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
1447
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
1448
|
+
if (suspendedCall !== undefined) {
|
|
1449
|
+
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
1354
1450
|
}
|
|
1355
|
-
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
1356
1451
|
suppressInspectionEmissions = true;
|
|
1357
1452
|
createRuntimeActor(boundSnapshot.machine);
|
|
1358
1453
|
actor?.start();
|
|
1359
|
-
const restoredState = currentState();
|
|
1454
|
+
const restoredState = currentState(suspendedCall);
|
|
1360
1455
|
if (restoredState.status !== 'active') {
|
|
1361
1456
|
throw new Error(`decide runtime: restored actor status is ${restoredState.status}, expected active`);
|
|
1362
1457
|
}
|
|
1363
|
-
|
|
1458
|
+
if (stableJson(restoredState, 'restored runtime state') !==
|
|
1459
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
1460
|
+
throw new Error('decide runtime: restored actor state does not match snapshot state');
|
|
1461
|
+
}
|
|
1364
1462
|
previousState = restoredState;
|
|
1365
1463
|
await flush();
|
|
1464
|
+
suppressInspectionEmissions = false;
|
|
1465
|
+
// Final fallible step: after this publication the authoritative
|
|
1466
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
1467
|
+
// restore validation may trigger failed-start rollback.
|
|
1468
|
+
nestedBridge.confirmRestore();
|
|
1366
1469
|
}
|
|
1367
1470
|
catch (error) {
|
|
1368
1471
|
let failure = error;
|
|
1369
|
-
if (
|
|
1472
|
+
if (priorExternalRoleTokens !== undefined) {
|
|
1370
1473
|
try {
|
|
1371
|
-
identity.playerSessions.restore(
|
|
1474
|
+
identity.playerSessions.restore(priorExternalRoleTokens);
|
|
1372
1475
|
}
|
|
1373
1476
|
catch (rollbackError) {
|
|
1374
1477
|
failure = new AggregateError([error, rollbackError], 'DECIDE restore and player continuation rollback failed');
|
|
1375
1478
|
}
|
|
1376
1479
|
}
|
|
1377
|
-
await cleanupFailedStart({ emitDisposal: false });
|
|
1480
|
+
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
1378
1481
|
throw failure;
|
|
1379
1482
|
}
|
|
1380
1483
|
finally {
|
|
@@ -1607,9 +1710,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1607
1710
|
collectFailure(failures, error);
|
|
1608
1711
|
}
|
|
1609
1712
|
finally {
|
|
1610
|
-
|
|
1713
|
+
privateResumeTokens.clear();
|
|
1611
1714
|
playbookCallTurnIds.clear();
|
|
1612
|
-
|
|
1715
|
+
inFlightPlayerKeys.clear();
|
|
1613
1716
|
activeBoundaryCalls.clear();
|
|
1614
1717
|
activeEmissionCalls.clear();
|
|
1615
1718
|
emissionQueue.clear();
|
|
@@ -1635,7 +1738,6 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1635
1738
|
};
|
|
1636
1739
|
export const _internal = {
|
|
1637
1740
|
composePlayerPrompt,
|
|
1638
|
-
resolvePlayerId,
|
|
1639
1741
|
requiredFieldsFor,
|
|
1640
1742
|
extractJson,
|
|
1641
1743
|
buildClassifierPrompt,
|
|
@@ -1646,10 +1748,9 @@ export const _internal = {
|
|
|
1646
1748
|
pendingQuestionsFromContext,
|
|
1647
1749
|
normalizeErrorCompact,
|
|
1648
1750
|
normalizeErrorFull,
|
|
1649
|
-
DEFAULT_PLAYER_BINDING,
|
|
1650
1751
|
STATE_DESCRIPTIONS,
|
|
1651
|
-
|
|
1652
|
-
|
|
1752
|
+
ROLE_STATES,
|
|
1753
|
+
ROLE_STATE_IDS,
|
|
1653
1754
|
VERBATIM_PAYLOAD_FIELDS,
|
|
1654
1755
|
BOSS_INTERRUPT_TARGETS,
|
|
1655
1756
|
CONTINUATION_PREAMBLE,
|