@sublang/playbook 4.0.0 → 6.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 +69 -122
- package/docs/assets/playbook-venn.svg +13 -0
- package/docs/cli.md +43 -26
- package/docs/configuration.md +63 -18
- package/docs/embedding.md +24 -16
- package/package.json +43 -22
- package/reference/sdlc/captain.md +70 -83
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
- package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
- package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
- package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
- package/reference/sdlc/captain.playbook/captain.playbook.js +466 -976
- package/reference/sdlc/captain.playbook/captain.playbook.ts +698 -1001
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -639
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -850
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +1877 -251
- package/reference/sdlc/code.playbook/playbook-captain.ts +2385 -352
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +511 -370
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +616 -451
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +67 -6
- package/slc/link.md +339 -25
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +36 -1
- package/src/runtime.ts +59 -0
- package/src/xstate-playbook-runtime.d.ts +96 -7
- package/src/xstate-playbook-runtime.js +1018 -49
- package/src/xstate-playbook-runtime.ts +1283 -59
- package/src/xstate-runtime.js +25 -0
- package/src/xstate-runtime.ts +51 -0
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts}
RENAMED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
//
|
|
4
|
-
// PlaybookRuntime for the
|
|
4
|
+
// PlaybookRuntime for the decide playbook, linked from the FSM artifact by
|
|
5
5
|
// the slc FSM-to-runtime link phase.
|
|
6
6
|
//
|
|
7
7
|
// Linker inputs:
|
|
8
|
-
// FSM artifact: ./
|
|
9
|
-
// Link target: @sublang/playbook/
|
|
10
|
-
// Player binding:
|
|
11
|
-
// Committer -> committer
|
|
8
|
+
// FSM artifact: ./decide.fsm.ts
|
|
9
|
+
// Link target: @sublang/playbook/runtime
|
|
10
|
+
// Player binding: Coder -> coder, Reviewer -> reviewer
|
|
12
11
|
// (default binding: lowercased player name)
|
|
13
|
-
// Composite players: Committer = Host | Participant. DISCUSS-14 and
|
|
14
|
-
// DISCUSS-15 keep PlayerInput.player = 'Committer';
|
|
15
|
-
// callPlayer resolution uses options.committer when
|
|
16
|
-
// supplied, otherwise falls back to Host, the first
|
|
17
|
-
// listed alias alternative.
|
|
18
12
|
// Adjudication: LLM-judge per state (default)
|
|
19
13
|
// Boss-event mapping: free-text judge classification (default)
|
|
20
14
|
// Abort strategy: natural rejection; every player-invoking state's
|
|
21
15
|
// onError routes to the quiescent failed state.
|
|
16
|
+
// Output profile: bespoke parallel runtime with the shared nested-call
|
|
17
|
+
// bridge (slc/link.md §Output)
|
|
22
18
|
|
|
23
19
|
import PQueue from 'p-queue';
|
|
24
20
|
import { createActor, fromPromise } from 'xstate';
|
|
@@ -28,6 +24,7 @@ import {
|
|
|
28
24
|
assertJsonSafe,
|
|
29
25
|
assertPlaybookRuntimeSnapshot,
|
|
30
26
|
combineAbortSignals,
|
|
27
|
+
createNestedPlaybookBridge,
|
|
31
28
|
detachPersistedMachineSnapshot,
|
|
32
29
|
normalizeError,
|
|
33
30
|
normalizePlaybookSnapshot,
|
|
@@ -36,14 +33,16 @@ import {
|
|
|
36
33
|
validatePlayerResult,
|
|
37
34
|
waitForPlaybookQuiescence,
|
|
38
35
|
} from '../../../src/xstate-runtime.js';
|
|
36
|
+
import type { NestedPlaybookBridge } from '../../../src/xstate-runtime.js';
|
|
39
37
|
|
|
40
|
-
import
|
|
38
|
+
import decideMachine, {
|
|
41
39
|
type PlayerInput,
|
|
42
40
|
type PlayerOutput,
|
|
43
|
-
type
|
|
44
|
-
type
|
|
41
|
+
type DecideEvent,
|
|
42
|
+
type DecideInput,
|
|
45
43
|
type PendingBossQuestion,
|
|
46
|
-
|
|
44
|
+
type PlaybookInput,
|
|
45
|
+
} from './decide.fsm.js';
|
|
47
46
|
|
|
48
47
|
import type {
|
|
49
48
|
CaptainCallOptions,
|
|
@@ -52,9 +51,13 @@ import type {
|
|
|
52
51
|
NormalizedError,
|
|
53
52
|
PlayerCallOptions,
|
|
54
53
|
PlayerResult,
|
|
54
|
+
PlayerSessionStore,
|
|
55
55
|
PlaybookCallRequest,
|
|
56
56
|
PlaybookCallResult,
|
|
57
57
|
PlaybookCallStart,
|
|
58
|
+
PlaybookControlAction,
|
|
59
|
+
PlaybookControlReceipt,
|
|
60
|
+
PlaybookControlView,
|
|
58
61
|
PlaybookPendingCall,
|
|
59
62
|
PlaybookPorts,
|
|
60
63
|
PlaybookRunResult,
|
|
@@ -75,9 +78,13 @@ export type {
|
|
|
75
78
|
NormalizedError,
|
|
76
79
|
PlayerCallOptions,
|
|
77
80
|
PlayerResult,
|
|
81
|
+
PlayerSessionStore,
|
|
78
82
|
PlaybookCallRequest,
|
|
79
83
|
PlaybookCallResult,
|
|
80
84
|
PlaybookCallStart,
|
|
85
|
+
PlaybookControlAction,
|
|
86
|
+
PlaybookControlReceipt,
|
|
87
|
+
PlaybookControlView,
|
|
81
88
|
PlaybookPendingCall,
|
|
82
89
|
PlaybookPorts,
|
|
83
90
|
PlaybookRunResult,
|
|
@@ -91,68 +98,53 @@ export type {
|
|
|
91
98
|
PlaybookTraceType,
|
|
92
99
|
};
|
|
93
100
|
|
|
94
|
-
type PlayerName = '
|
|
95
|
-
type ReviewScope = 'specItems' | 'decisionRecords' | 'mixed';
|
|
101
|
+
type PlayerName = 'Coder' | 'Reviewer';
|
|
96
102
|
|
|
97
|
-
export interface PlaybookRuntimeOptions extends
|
|
103
|
+
export interface PlaybookRuntimeOptions extends DecideInput {
|
|
98
104
|
playerBinding?: Partial<Record<PlayerName, string>>;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
const DEFAULT_PLAYER_BINDING: Readonly<Record<PlayerName, string>> = {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
Committer: 'committer',
|
|
108
|
+
Coder: 'coder',
|
|
109
|
+
Reviewer: 'reviewer',
|
|
105
110
|
};
|
|
106
111
|
|
|
107
|
-
|
|
108
|
-
'
|
|
109
|
-
'Committer = Host | Participant; uses input.committerPlayer when supplied, otherwise Host.',
|
|
110
|
-
'DISCUSS-15':
|
|
111
|
-
'Committer = Host | Participant; uses input.committerPlayer when supplied, otherwise Host.',
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
function snapshotDiscussRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
115
|
-
const captured = snapshotJsonValue(value, 'DISCUSS runtime options');
|
|
112
|
+
function snapshotDecideRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
113
|
+
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
116
114
|
if (!isPlainObject(captured)) {
|
|
117
|
-
throw new TypeError('
|
|
115
|
+
throw new TypeError('DECIDE runtime options must be an object');
|
|
118
116
|
}
|
|
119
|
-
const allowed = new Set([
|
|
120
|
-
'host',
|
|
121
|
-
'participant',
|
|
122
|
-
'committer',
|
|
123
|
-
'playerBinding',
|
|
124
|
-
]);
|
|
117
|
+
const allowed = new Set(['coderLlm', 'playerBinding']);
|
|
125
118
|
for (const key of Object.keys(captured)) {
|
|
126
119
|
if (!allowed.has(key)) {
|
|
127
|
-
throw new TypeError(`
|
|
120
|
+
throw new TypeError(`DECIDE runtime options.${key} is not declared`);
|
|
128
121
|
}
|
|
129
122
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
123
|
+
if (
|
|
124
|
+
typeof captured.coderLlm !== 'string' ||
|
|
125
|
+
captured.coderLlm.trim().length === 0
|
|
126
|
+
) {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
'DECIDE runtime options.coderLlm must be a non-empty string',
|
|
129
|
+
);
|
|
134
130
|
}
|
|
135
131
|
if ('playerBinding' in captured) {
|
|
136
132
|
const playerBinding = captured.playerBinding;
|
|
137
133
|
if (!isPlainObject(playerBinding)) {
|
|
138
134
|
throw new TypeError(
|
|
139
|
-
'
|
|
135
|
+
'DECIDE runtime options.playerBinding must be an object',
|
|
140
136
|
);
|
|
141
137
|
}
|
|
142
|
-
const playerNames = new Set<PlayerName>([
|
|
143
|
-
'Host',
|
|
144
|
-
'Participant',
|
|
145
|
-
'Committer',
|
|
146
|
-
]);
|
|
138
|
+
const playerNames = new Set<PlayerName>(['Coder', 'Reviewer']);
|
|
147
139
|
for (const [player, playerId] of Object.entries(playerBinding)) {
|
|
148
140
|
if (!playerNames.has(player as PlayerName)) {
|
|
149
141
|
throw new TypeError(
|
|
150
|
-
`
|
|
142
|
+
`DECIDE runtime options.playerBinding.${player} is not declared`,
|
|
151
143
|
);
|
|
152
144
|
}
|
|
153
145
|
if (typeof playerId !== 'string' || playerId.trim().length === 0) {
|
|
154
146
|
throw new TypeError(
|
|
155
|
-
`
|
|
147
|
+
`DECIDE runtime options.playerBinding.${player} must be a non-empty string`,
|
|
156
148
|
);
|
|
157
149
|
}
|
|
158
150
|
}
|
|
@@ -161,140 +153,40 @@ function snapshotDiscussRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
|
161
153
|
}
|
|
162
154
|
|
|
163
155
|
const STATE_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
164
|
-
ready: '
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
'
|
|
175
|
-
|
|
176
|
-
'Committer commits the changes produced at the end of initial discussion.',
|
|
177
|
-
reviewSpecInitialCommit:
|
|
178
|
-
'Participant reviews newly committed spec-item changes.',
|
|
179
|
-
reviewSpecHostChanges:
|
|
180
|
-
'Participant reviews Host changes to spec items after findings.',
|
|
181
|
-
reviewDrInitialCommit:
|
|
182
|
-
'Participant reviews newly committed decision-record changes.',
|
|
183
|
-
reviewDrHostChanges:
|
|
184
|
-
'Participant reviews Host changes to decision records after findings.',
|
|
185
|
-
reviewMixedInitialCommit:
|
|
186
|
-
'Participant reviews newly committed mixed spec-item and DR changes.',
|
|
187
|
-
reviewMixedHostChanges:
|
|
188
|
-
'Participant reviews Host changes to mixed spec items and DRs after findings.',
|
|
189
|
-
hostAddressesFindings:
|
|
190
|
-
'Host accepts or challenges review findings and stages repo changes.',
|
|
191
|
-
participantAddressesRebuttals:
|
|
192
|
-
'Participant accepts or challenges Host rebuttals.',
|
|
193
|
-
commitReviewedChanges:
|
|
194
|
-
'Committer commits reviewed changes once Participant raises no findings.',
|
|
195
|
-
awaitBossReply: 'Waiting for Boss to answer a player question.',
|
|
196
|
-
failed: 'The discussion workflow failed and is waiting for Boss recovery.',
|
|
197
|
-
done: 'The discussion workflow completed with a reviewed commit.',
|
|
156
|
+
ready: 'Waiting for a topic to decide.',
|
|
157
|
+
askCoderProposal: 'Coder independently proposes a spec design.',
|
|
158
|
+
askReviewerProposal: 'Reviewer independently proposes a spec design.',
|
|
159
|
+
waitCoderProposalReply: 'Coder waits for Boss to answer a question.',
|
|
160
|
+
waitReviewerProposalReply: 'Reviewer waits for Boss to answer a question.',
|
|
161
|
+
commitCoderProposal: 'Coder writes and commits Coder’s independent proposal.',
|
|
162
|
+
awaitBossReply: 'Waiting for Boss to answer Coder’s question.',
|
|
163
|
+
reviewCommit: 'REVIEW examines the committed proposal.',
|
|
164
|
+
failed: 'DECIDE failed and is waiting for a new topic.',
|
|
165
|
+
reportedReviewFailure:
|
|
166
|
+
'DECIDE reports REVIEW’s failure and its last commit.',
|
|
167
|
+
done: 'DECIDE completed with an approved commit.',
|
|
198
168
|
};
|
|
199
169
|
|
|
200
|
-
const
|
|
201
|
-
{ stateId: '
|
|
202
|
-
{
|
|
203
|
-
stateId: 'askParticipantInitial',
|
|
204
|
-
player: 'Participant',
|
|
205
|
-
sourceItem: 'DISCUSS-2',
|
|
206
|
-
},
|
|
207
|
-
{ stateId: 'hostInitialRound', player: 'Host', sourceItem: 'DISCUSS-3' },
|
|
170
|
+
const PLAYER_STATES = [
|
|
171
|
+
{ stateId: 'askCoderProposal', player: 'Coder', sourceItem: 'DECIDE-1' },
|
|
208
172
|
{
|
|
209
|
-
stateId: '
|
|
210
|
-
player: '
|
|
211
|
-
sourceItem: '
|
|
212
|
-
},
|
|
213
|
-
{ stateId: 'hostWritesAgreement', player: 'Host', sourceItem: 'DISCUSS-5' },
|
|
214
|
-
{
|
|
215
|
-
stateId: 'commitInitialChanges',
|
|
216
|
-
player: 'Committer',
|
|
217
|
-
sourceItem: 'DISCUSS-14',
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
stateId: 'reviewSpecInitialCommit',
|
|
221
|
-
player: 'Participant',
|
|
222
|
-
sourceItem: 'DISCUSS-6',
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
stateId: 'reviewSpecHostChanges',
|
|
226
|
-
player: 'Participant',
|
|
227
|
-
sourceItem: 'DISCUSS-7',
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
stateId: 'reviewDrInitialCommit',
|
|
231
|
-
player: 'Participant',
|
|
232
|
-
sourceItem: 'DISCUSS-8',
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
stateId: 'reviewDrHostChanges',
|
|
236
|
-
player: 'Participant',
|
|
237
|
-
sourceItem: 'DISCUSS-9',
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
stateId: 'reviewMixedInitialCommit',
|
|
241
|
-
player: 'Participant',
|
|
242
|
-
sourceItem: 'DISCUSS-10',
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
stateId: 'reviewMixedHostChanges',
|
|
246
|
-
player: 'Participant',
|
|
247
|
-
sourceItem: 'DISCUSS-11',
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
stateId: 'hostAddressesFindings',
|
|
251
|
-
player: 'Host',
|
|
252
|
-
sourceItem: 'DISCUSS-12',
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
stateId: 'participantAddressesRebuttals',
|
|
256
|
-
player: 'Participant',
|
|
257
|
-
sourceItem: 'DISCUSS-13',
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
stateId: 'commitReviewedChanges',
|
|
261
|
-
player: 'Committer',
|
|
262
|
-
sourceItem: 'DISCUSS-15',
|
|
173
|
+
stateId: 'askReviewerProposal',
|
|
174
|
+
player: 'Reviewer',
|
|
175
|
+
sourceItem: 'DECIDE-2',
|
|
263
176
|
},
|
|
177
|
+
{ stateId: 'commitCoderProposal', player: 'Coder', sourceItem: 'DECIDE-3' },
|
|
264
178
|
] as const;
|
|
265
179
|
|
|
266
|
-
const
|
|
267
|
-
|
|
180
|
+
const PLAYER_STATE_IDS: ReadonlySet<string> = new Set(
|
|
181
|
+
PLAYER_STATES.map((state) => state.stateId),
|
|
268
182
|
);
|
|
269
183
|
|
|
270
|
-
const BOSS_INTERRUPT_TARGETS = [
|
|
271
|
-
'ready',
|
|
272
|
-
'initialProposalRound',
|
|
273
|
-
'reconciliationRound',
|
|
274
|
-
'hostWritesAgreement',
|
|
275
|
-
'commitInitialChanges',
|
|
276
|
-
'reviewSpecInitialCommit',
|
|
277
|
-
'reviewSpecHostChanges',
|
|
278
|
-
'reviewDrInitialCommit',
|
|
279
|
-
'reviewDrHostChanges',
|
|
280
|
-
'reviewMixedInitialCommit',
|
|
281
|
-
'reviewMixedHostChanges',
|
|
282
|
-
'hostAddressesFindings',
|
|
283
|
-
'participantAddressesRebuttals',
|
|
284
|
-
'commitReviewedChanges',
|
|
285
|
-
'failed',
|
|
286
|
-
] as const;
|
|
184
|
+
const BOSS_INTERRUPT_TARGETS = ['independentProposals'] as const;
|
|
287
185
|
|
|
288
186
|
const BOSS_INTERRUPT_TARGET_IDS: ReadonlySet<string> = new Set(
|
|
289
187
|
BOSS_INTERRUPT_TARGETS,
|
|
290
188
|
);
|
|
291
189
|
|
|
292
|
-
const REVIEW_SCOPES: ReadonlySet<string> = new Set([
|
|
293
|
-
'specItems',
|
|
294
|
-
'decisionRecords',
|
|
295
|
-
'mixed',
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
190
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
299
191
|
const TRACE_TOPIC = 'playbook.trace';
|
|
300
192
|
|
|
@@ -303,19 +195,16 @@ const CONTINUATION_PREAMBLE =
|
|
|
303
195
|
|
|
304
196
|
const PLACEHOLDER_FIELDS: ReadonlyArray<readonly [string, keyof PlayerInput]> =
|
|
305
197
|
[
|
|
306
|
-
['<topic>', '
|
|
307
|
-
['<
|
|
308
|
-
['<host-previous-proposal>', 'hostProposal'],
|
|
309
|
-
['<host-proposal>', 'hostProposal'],
|
|
310
|
-
['<participant-previous-proposal>', 'participantProposal'],
|
|
311
|
-
['<agreement>', 'agreement'],
|
|
312
|
-
['<changes>', 'latestChanges'],
|
|
313
|
-
['<review-items>', 'reviewItems'],
|
|
314
|
-
['<rebuttals>', 'rebuttals'],
|
|
315
|
-
['<host-llm>', 'hostLlm'],
|
|
316
|
-
['<participant-llm>', 'participantLlm'],
|
|
198
|
+
['<caller-topic>', 'callerTopic'],
|
|
199
|
+
['<coder-llm>', 'coderLlm'],
|
|
317
200
|
];
|
|
318
201
|
|
|
202
|
+
const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
|
|
203
|
+
'coderProposal',
|
|
204
|
+
'reviewerProposal',
|
|
205
|
+
'coderOutput',
|
|
206
|
+
]);
|
|
207
|
+
|
|
319
208
|
function composePlayerPrompt(input: PlayerInput): string {
|
|
320
209
|
const blocks: string[] = [];
|
|
321
210
|
|
|
@@ -333,13 +222,22 @@ function composePlayerPrompt(input: PlayerInput): string {
|
|
|
333
222
|
);
|
|
334
223
|
}
|
|
335
224
|
|
|
336
|
-
|
|
225
|
+
const replacements = new Map<string, string>();
|
|
337
226
|
for (const [placeholder, field] of PLACEHOLDER_FIELDS) {
|
|
338
227
|
const value = input[field];
|
|
339
|
-
if (typeof value === 'string')
|
|
340
|
-
body = body.replaceAll(placeholder, value);
|
|
341
|
-
}
|
|
228
|
+
if (typeof value === 'string') replacements.set(placeholder, value);
|
|
342
229
|
}
|
|
230
|
+
const body = input.prompt.replace(
|
|
231
|
+
/<caller-topic>|<coder-llm>/g,
|
|
232
|
+
(placeholder, offset: number, source: string) => {
|
|
233
|
+
const value = replacements.get(placeholder);
|
|
234
|
+
if (value === undefined) return placeholder;
|
|
235
|
+
const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
|
|
236
|
+
return source.slice(lineStart, offset) === '> '
|
|
237
|
+
? value.replaceAll('\n', '\n> ')
|
|
238
|
+
: value;
|
|
239
|
+
},
|
|
240
|
+
);
|
|
343
241
|
|
|
344
242
|
blocks.push(body);
|
|
345
243
|
return blocks.join('\n\n');
|
|
@@ -350,12 +248,10 @@ function resolvePlayerId(
|
|
|
350
248
|
binding: Record<PlayerName, string>,
|
|
351
249
|
): string {
|
|
352
250
|
switch (input.player) {
|
|
353
|
-
case '
|
|
354
|
-
return binding.
|
|
355
|
-
case '
|
|
356
|
-
return binding.
|
|
357
|
-
case 'Committer':
|
|
358
|
-
return input.committerPlayer ?? binding.Host;
|
|
251
|
+
case 'Coder':
|
|
252
|
+
return binding.Coder;
|
|
253
|
+
case 'Reviewer':
|
|
254
|
+
return binding.Reviewer;
|
|
359
255
|
default: {
|
|
360
256
|
const exhaustive: never = input.player;
|
|
361
257
|
throw new Error(`unknown player ${String(exhaustive)}`);
|
|
@@ -363,11 +259,8 @@ function resolvePlayerId(
|
|
|
363
259
|
}
|
|
364
260
|
}
|
|
365
261
|
|
|
366
|
-
// A `result` description names required payload fields in
|
|
367
|
-
// "Output shall include ..." sentence.
|
|
368
|
-
// fields — DISCUSS-5's wroteChanges names both `latestChanges` and
|
|
369
|
-
// `reviewScope` — so every backticked `field:` token in the sentence
|
|
370
|
-
// span is required, not just the one adjacent to the phrase.
|
|
262
|
+
// A `result` description names required payload fields in its
|
|
263
|
+
// "Output shall include ..." sentence.
|
|
371
264
|
function requiredFieldsFor(description: string): string[] {
|
|
372
265
|
const fields: string[] = [];
|
|
373
266
|
const sentence = /Output shall include([^.]*)/g;
|
|
@@ -492,7 +385,7 @@ function buildClassifierPrompt(
|
|
|
492
385
|
},
|
|
493
386
|
): string {
|
|
494
387
|
const lines: string[] = [];
|
|
495
|
-
lines.push('You are the Boss-input classifier for the
|
|
388
|
+
lines.push('You are the Boss-input classifier for the decide playbook.');
|
|
496
389
|
lines.push(
|
|
497
390
|
'Classify the Boss message into exactly one FSM event, or into no event.',
|
|
498
391
|
);
|
|
@@ -503,7 +396,7 @@ function buildClassifierPrompt(
|
|
|
503
396
|
lines.push('Pending Boss questions:');
|
|
504
397
|
for (const pending of ctx.pendingQuestions) {
|
|
505
398
|
lines.push(
|
|
506
|
-
`- ${pending.questionId} (${pending.player}
|
|
399
|
+
`- ${pending.questionId} (${pending.player}): ${pending.question}`,
|
|
507
400
|
);
|
|
508
401
|
}
|
|
509
402
|
lines.push(
|
|
@@ -513,98 +406,77 @@ function buildClassifierPrompt(
|
|
|
513
406
|
lines.push('');
|
|
514
407
|
lines.push('Events and payload contracts:');
|
|
515
408
|
lines.push(
|
|
516
|
-
|
|
517
|
-
);
|
|
518
|
-
lines.push(
|
|
519
|
-
'- START_REVIEW: required latestChanges string, required reviewScope string ("specItems" | "decisionRecords" | "mixed"), optional rebuttals string.',
|
|
409
|
+
`- BOSS_INTERRUPT: required targetId string, exactly ${BOSS_INTERRUPT_TARGETS[0]}; the runtime attaches the exact Boss text as bossIntent.`,
|
|
520
410
|
);
|
|
521
411
|
lines.push(
|
|
522
|
-
|
|
523
|
-
);
|
|
524
|
-
lines.push(
|
|
525
|
-
'- BOSS_REPLY: required answer string and questionId when several questions are pending; valid only while at least one Boss question is pending.',
|
|
412
|
+
'- BOSS_REPLY: optional questionId when exactly one question is pending and required when several are pending; the runtime attaches the exact Boss text as answer.',
|
|
526
413
|
);
|
|
414
|
+
lines.push('- NO_ACTION: no fields.');
|
|
527
415
|
lines.push('');
|
|
528
416
|
lines.push('Boss message:');
|
|
529
417
|
lines.push(text);
|
|
530
418
|
lines.push('');
|
|
531
419
|
lines.push(
|
|
532
|
-
'Reply with a single JSON object: { "
|
|
420
|
+
'Reply with a single JSON object: { "type": "<EVENT_TYPE>", ...declared fields }.',
|
|
533
421
|
);
|
|
534
|
-
lines.push('Use
|
|
422
|
+
lines.push('Use NO_ACTION when no FSM action applies.');
|
|
535
423
|
return lines.join('\n');
|
|
536
424
|
}
|
|
537
425
|
|
|
538
426
|
function parseClassification(
|
|
539
427
|
raw: string,
|
|
428
|
+
text: string,
|
|
540
429
|
pendingQuestionIds: readonly string[] = [],
|
|
541
|
-
):
|
|
430
|
+
): DecideEvent | { type: 'NO_ACTION' } | null {
|
|
542
431
|
const obj = extractJson(raw);
|
|
543
432
|
if (!obj) return null;
|
|
544
|
-
const
|
|
433
|
+
const exactKeys = (...keys: string[]): boolean => {
|
|
434
|
+
const ownKeys = Reflect.ownKeys(obj);
|
|
435
|
+
return (
|
|
436
|
+
ownKeys.length === keys.length &&
|
|
437
|
+
ownKeys.every(
|
|
438
|
+
(key) => typeof key === 'string' && keys.includes(key),
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
};
|
|
442
|
+
const eventType = obj.type;
|
|
545
443
|
|
|
546
|
-
if (eventType === '
|
|
547
|
-
|
|
548
|
-
// normally flow in via the machine input (run options).
|
|
549
|
-
if (typeof obj.topic === 'string') {
|
|
550
|
-
return {
|
|
551
|
-
type: 'START_DISCUSSION',
|
|
552
|
-
topic: obj.topic,
|
|
553
|
-
...(typeof obj.hostLlm === 'string' ? { hostLlm: obj.hostLlm } : {}),
|
|
554
|
-
...(typeof obj.participantLlm === 'string'
|
|
555
|
-
? { participantLlm: obj.participantLlm }
|
|
556
|
-
: {}),
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (eventType === 'START_REVIEW') {
|
|
563
|
-
if (
|
|
564
|
-
typeof obj.latestChanges === 'string' &&
|
|
565
|
-
typeof obj.reviewScope === 'string' &&
|
|
566
|
-
REVIEW_SCOPES.has(obj.reviewScope)
|
|
567
|
-
) {
|
|
568
|
-
return {
|
|
569
|
-
type: 'START_REVIEW',
|
|
570
|
-
latestChanges: obj.latestChanges,
|
|
571
|
-
reviewScope: obj.reviewScope as ReviewScope,
|
|
572
|
-
...(typeof obj.rebuttals === 'string'
|
|
573
|
-
? { rebuttals: obj.rebuttals }
|
|
574
|
-
: {}),
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
return null;
|
|
444
|
+
if (eventType === 'NO_ACTION') {
|
|
445
|
+
return exactKeys('type') ? { type: 'NO_ACTION' } : null;
|
|
578
446
|
}
|
|
579
447
|
|
|
580
448
|
if (eventType === 'BOSS_INTERRUPT') {
|
|
581
449
|
if (
|
|
450
|
+
exactKeys('type', 'targetId') &&
|
|
582
451
|
typeof obj.targetId === 'string' &&
|
|
583
452
|
BOSS_INTERRUPT_TARGET_IDS.has(obj.targetId)
|
|
584
453
|
) {
|
|
585
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
type: 'BOSS_INTERRUPT',
|
|
456
|
+
targetId: obj.targetId as 'independentProposals',
|
|
457
|
+
bossIntent: text,
|
|
458
|
+
};
|
|
586
459
|
}
|
|
587
460
|
return null;
|
|
588
461
|
}
|
|
589
462
|
|
|
590
463
|
if (eventType === 'BOSS_REPLY') {
|
|
591
|
-
if (
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
464
|
+
if (pendingQuestionIds.length === 0) return null;
|
|
594
465
|
if (typeof obj.questionId === 'string') {
|
|
595
|
-
return
|
|
466
|
+
return exactKeys('type', 'questionId') &&
|
|
467
|
+
pendingQuestionIds.includes(obj.questionId)
|
|
596
468
|
? {
|
|
597
469
|
type: 'BOSS_REPLY',
|
|
598
470
|
questionId: obj.questionId as never,
|
|
599
|
-
answer:
|
|
471
|
+
answer: text,
|
|
600
472
|
}
|
|
601
473
|
: null;
|
|
602
474
|
}
|
|
603
|
-
return pendingQuestionIds.length === 1
|
|
475
|
+
return exactKeys('type') && pendingQuestionIds.length === 1
|
|
604
476
|
? {
|
|
605
477
|
type: 'BOSS_REPLY',
|
|
606
478
|
questionId: pendingQuestionIds[0] as never,
|
|
607
|
-
answer:
|
|
479
|
+
answer: text,
|
|
608
480
|
}
|
|
609
481
|
: null;
|
|
610
482
|
}
|
|
@@ -639,6 +511,18 @@ function buildAdjudicatorPrompt(
|
|
|
639
511
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
640
512
|
lines.push(`- ${guard}: ${description}`);
|
|
641
513
|
}
|
|
514
|
+
const runtimeOwnedFields = new Set<string>();
|
|
515
|
+
for (const description of Object.values(input.result)) {
|
|
516
|
+
for (const field of requiredFieldsFor(description)) {
|
|
517
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) runtimeOwnedFields.add(field);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (runtimeOwnedFields.size > 0) {
|
|
521
|
+
lines.push('');
|
|
522
|
+
lines.push(
|
|
523
|
+
`The runtime owns these verbatim fields; do not include them in your JSON: ${[...runtimeOwnedFields].join(', ')}.`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
642
526
|
lines.push('');
|
|
643
527
|
lines.push(
|
|
644
528
|
'Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.',
|
|
@@ -646,7 +530,11 @@ function buildAdjudicatorPrompt(
|
|
|
646
530
|
return lines.join('\n');
|
|
647
531
|
}
|
|
648
532
|
|
|
649
|
-
function parseAdjudication(
|
|
533
|
+
function parseAdjudication(
|
|
534
|
+
raw: string,
|
|
535
|
+
input: PlayerInput,
|
|
536
|
+
finalText: string,
|
|
537
|
+
): PlayerOutput {
|
|
650
538
|
const obj = extractJson(raw);
|
|
651
539
|
if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
|
|
652
540
|
throw new Error('adjudicator returned empty or malformed JSON');
|
|
@@ -659,15 +547,22 @@ function parseAdjudication(raw: string, input: PlayerInput): PlayerOutput {
|
|
|
659
547
|
);
|
|
660
548
|
}
|
|
661
549
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
550
|
+
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
551
|
+
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
552
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
553
|
+
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
554
|
+
throw new Error(
|
|
555
|
+
`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const output: Record<string, unknown> = { guard };
|
|
560
|
+
for (const field of requiredFields) {
|
|
561
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
562
|
+
output[field] = finalText;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const value = obj[field];
|
|
671
566
|
if (
|
|
672
567
|
value === undefined ||
|
|
673
568
|
value === null ||
|
|
@@ -677,19 +572,9 @@ function parseAdjudication(raw: string, input: PlayerInput): PlayerOutput {
|
|
|
677
572
|
`adjudicator response for guard "${guard}" missing required field "${field}"`,
|
|
678
573
|
);
|
|
679
574
|
}
|
|
575
|
+
output[field] = value;
|
|
680
576
|
}
|
|
681
|
-
|
|
682
|
-
if (
|
|
683
|
-
'reviewScope' in output &&
|
|
684
|
-
typeof output.reviewScope === 'string' &&
|
|
685
|
-
!REVIEW_SCOPES.has(output.reviewScope)
|
|
686
|
-
) {
|
|
687
|
-
throw new Error(
|
|
688
|
-
`adjudicator returned invalid reviewScope "${output.reviewScope}"`,
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return output;
|
|
577
|
+
return output as PlayerOutput;
|
|
693
578
|
}
|
|
694
579
|
|
|
695
580
|
function combineSignals(
|
|
@@ -713,10 +598,16 @@ function normalizeErrorFull(
|
|
|
713
598
|
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
714
599
|
}
|
|
715
600
|
|
|
601
|
+
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
602
|
+
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
603
|
+
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
604
|
+
// the shared engine's predicate (PBRT-9).
|
|
605
|
+
function isEmptyFinalText(finalText: string | undefined): boolean {
|
|
606
|
+
return finalText === undefined || finalText.trim().length === 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
716
609
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
717
|
-
|
|
718
|
-
if (Object.is(error, signal.reason)) return true;
|
|
719
|
-
return normalizeErrorCompact(error)?.name === 'AbortError';
|
|
610
|
+
return signal.aborted && Object.is(error, signal.reason);
|
|
720
611
|
}
|
|
721
612
|
|
|
722
613
|
function pendingQuestionsFromContext(
|
|
@@ -753,10 +644,8 @@ function pendingQuestionsFromContext(
|
|
|
753
644
|
}
|
|
754
645
|
|
|
755
646
|
const WAIT_STATE_RESUME_IDS: Readonly<Record<string, string>> = {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
waitHostReconciliationReply: 'hostInitialRound',
|
|
759
|
-
waitParticipantReconciliationReply: 'participantInitialRound',
|
|
647
|
+
waitCoderProposalReply: 'askCoderProposal',
|
|
648
|
+
waitReviewerProposalReply: 'askReviewerProposal',
|
|
760
649
|
};
|
|
761
650
|
|
|
762
651
|
const WAIT_STATE_IDS: ReadonlySet<string> = new Set([
|
|
@@ -765,7 +654,7 @@ const WAIT_STATE_IDS: ReadonlySet<string> = new Set([
|
|
|
765
654
|
]);
|
|
766
655
|
|
|
767
656
|
const STATUS_STATE_IDS: ReadonlySet<string> = new Set([
|
|
768
|
-
...
|
|
657
|
+
...PLAYER_STATE_IDS,
|
|
769
658
|
...WAIT_STATE_IDS,
|
|
770
659
|
'failed',
|
|
771
660
|
]);
|
|
@@ -785,25 +674,36 @@ function questionForWaitState(
|
|
|
785
674
|
: undefined;
|
|
786
675
|
}
|
|
787
676
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
677
|
+
const TRANSITION_EVENT_FIELDS: readonly string[] = [
|
|
678
|
+
'callerTopic',
|
|
679
|
+
'targetId',
|
|
680
|
+
'bossIntent',
|
|
681
|
+
'questionId',
|
|
682
|
+
'answer',
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
function normalizedTransitionEvent(event: unknown): JsonValue {
|
|
686
|
+
if (event === null || typeof event !== 'object') {
|
|
687
|
+
return snapshotJsonValue(event ?? null, 'FSM event');
|
|
797
688
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
(typeof
|
|
803
|
-
|
|
804
|
-
|
|
689
|
+
const source = event as Record<string, unknown>;
|
|
690
|
+
const descriptor: Record<string, JsonValue> = {};
|
|
691
|
+
if (typeof source.type === 'string') descriptor.type = source.type;
|
|
692
|
+
for (const field of TRANSITION_EVENT_FIELDS) {
|
|
693
|
+
if (typeof source[field] === 'string') {
|
|
694
|
+
descriptor[field] = source[field] as string;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (source.output !== undefined) {
|
|
698
|
+
descriptor.output = snapshotJsonValue(source.output, 'FSM event output');
|
|
699
|
+
}
|
|
700
|
+
if (source.error !== undefined) {
|
|
701
|
+
descriptor.error = snapshotJsonValue(
|
|
702
|
+
normalizeError(source.error),
|
|
703
|
+
'FSM event error',
|
|
704
|
+
);
|
|
805
705
|
}
|
|
806
|
-
return
|
|
706
|
+
return snapshotJsonValue(descriptor, 'FSM event');
|
|
807
707
|
}
|
|
808
708
|
|
|
809
709
|
function telemetryPayload(
|
|
@@ -812,22 +712,15 @@ function telemetryPayload(
|
|
|
812
712
|
event: unknown,
|
|
813
713
|
context: Record<string, unknown>,
|
|
814
714
|
): JsonValue {
|
|
815
|
-
const eventError =
|
|
816
|
-
event !== null &&
|
|
817
|
-
typeof event === 'object' &&
|
|
818
|
-
!Array.isArray(event) &&
|
|
819
|
-
'error' in event
|
|
820
|
-
? normalizeErrorFull((event as { error: unknown }).error)
|
|
821
|
-
: undefined;
|
|
822
715
|
const pendingBossQuestions = pendingQuestionsFromContext(context);
|
|
716
|
+
const prior = previousState ?? state;
|
|
823
717
|
const payload = {
|
|
824
|
-
from:
|
|
718
|
+
from: prior.value,
|
|
825
719
|
to: state.value,
|
|
826
|
-
event:
|
|
827
|
-
previousState:
|
|
720
|
+
event: normalizedTransitionEvent(event),
|
|
721
|
+
previousState: prior,
|
|
828
722
|
state,
|
|
829
723
|
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
830
|
-
...(eventError !== undefined ? { error: eventError } : {}),
|
|
831
724
|
...(state.activeStateIds.includes('failed')
|
|
832
725
|
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
833
726
|
: {}),
|
|
@@ -839,15 +732,13 @@ function telemetryPayload(
|
|
|
839
732
|
export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
840
733
|
PlaybookRuntimeOptions
|
|
841
734
|
> = (options) => {
|
|
842
|
-
const boundOptions =
|
|
735
|
+
const boundOptions = snapshotDecideRuntimeOptions(options);
|
|
843
736
|
const binding: Record<PlayerName, string> = {
|
|
844
737
|
...DEFAULT_PLAYER_BINDING,
|
|
845
738
|
...(boundOptions.playerBinding ?? {}),
|
|
846
739
|
};
|
|
847
|
-
const fsmInput:
|
|
848
|
-
|
|
849
|
-
participant: boundOptions.participant,
|
|
850
|
-
committer: boundOptions.committer,
|
|
740
|
+
const fsmInput: DecideInput = {
|
|
741
|
+
coderLlm: boundOptions.coderLlm,
|
|
851
742
|
};
|
|
852
743
|
|
|
853
744
|
type SessionIdentity = Readonly<PlaybookSession>;
|
|
@@ -864,12 +755,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
864
755
|
let turnSequence = 0;
|
|
865
756
|
let judgeCallSequence = 0;
|
|
866
757
|
let playerCallSequence = 0;
|
|
758
|
+
let playbookCallSequence = 0;
|
|
867
759
|
let lifecycleStarted = false;
|
|
868
760
|
let initInFlight: Promise<void> | undefined;
|
|
869
761
|
let disposed = false;
|
|
870
762
|
let disposalPromise: Promise<void> | undefined;
|
|
871
763
|
let controlPlaneError: unknown;
|
|
764
|
+
let nestedBridge: NestedPlaybookBridge<PlaybookInput>;
|
|
872
765
|
const playerResumeTokens = new Map<string, string>();
|
|
766
|
+
const playbookCallTurnIds = new Map<string, number | undefined>();
|
|
873
767
|
const inFlightPlayerIds = new Set<string>();
|
|
874
768
|
const activeBoundaryCalls = new Set<Promise<unknown>>();
|
|
875
769
|
const activeEmissionCalls = new Set<Promise<void>>();
|
|
@@ -891,6 +785,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
891
785
|
): void => {
|
|
892
786
|
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
893
787
|
};
|
|
788
|
+
const latchInspectionError = (error: unknown): void => {
|
|
789
|
+
if (currentSignal !== undefined) {
|
|
790
|
+
latchControlPlaneError(error, currentSignal);
|
|
791
|
+
} else {
|
|
792
|
+
collectFailure(emissionFailures, error);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
894
795
|
const enqueue = (fn: () => Promise<void>): Promise<void> => {
|
|
895
796
|
const queued = emissionQueue.add(fn);
|
|
896
797
|
activeEmissionCalls.add(queued);
|
|
@@ -920,7 +821,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
920
821
|
const failures = emissionFailures;
|
|
921
822
|
emissionFailures = [];
|
|
922
823
|
if (failures.length === 1) throw failures[0];
|
|
923
|
-
throw new AggregateError(failures, '
|
|
824
|
+
throw new AggregateError(failures, 'decide runtime emissions failed');
|
|
924
825
|
};
|
|
925
826
|
const drainBoundaryCallsAndEmissions = async (): Promise<void> => {
|
|
926
827
|
while (true) {
|
|
@@ -941,22 +842,92 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
941
842
|
};
|
|
942
843
|
const requirePorts = (): PlaybookPorts => {
|
|
943
844
|
if (!ports) {
|
|
944
|
-
throw new Error('
|
|
845
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
945
846
|
}
|
|
946
847
|
return ports;
|
|
947
848
|
};
|
|
948
849
|
const requireSessionIdentity = (): SessionIdentity => {
|
|
949
850
|
if (!sessionIdentity) {
|
|
950
|
-
throw new Error('
|
|
851
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
951
852
|
}
|
|
952
853
|
return sessionIdentity;
|
|
953
854
|
};
|
|
855
|
+
const selectPlayerResume = (playerId: string): string | false => {
|
|
856
|
+
const session = requireSessionIdentity();
|
|
857
|
+
const selected = session.playerSessions
|
|
858
|
+
? session.playerSessions.select(playerId)
|
|
859
|
+
: playerResumeTokens.get(playerId) ?? false;
|
|
860
|
+
if (
|
|
861
|
+
selected !== false &&
|
|
862
|
+
(typeof selected !== 'string' || selected.trim().length === 0)
|
|
863
|
+
) {
|
|
864
|
+
throw new TypeError(
|
|
865
|
+
`player session store returned an invalid resume token for ${playerId}`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
return selected;
|
|
869
|
+
};
|
|
870
|
+
const updatePlayerResume = (
|
|
871
|
+
playerId: string,
|
|
872
|
+
resumeToken: string | undefined,
|
|
873
|
+
): void => {
|
|
874
|
+
const session = requireSessionIdentity();
|
|
875
|
+
if (session.playerSessions) {
|
|
876
|
+
session.playerSessions.update(playerId, resumeToken);
|
|
877
|
+
} else if (resumeToken !== undefined && resumeToken.trim().length > 0) {
|
|
878
|
+
playerResumeTokens.set(playerId, resumeToken);
|
|
879
|
+
} else {
|
|
880
|
+
playerResumeTokens.delete(playerId);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const snapshotPlayerResumeTokens = (): Record<string, string> => {
|
|
884
|
+
const session = requireSessionIdentity();
|
|
885
|
+
const captured = snapshotJsonValue(
|
|
886
|
+
session.playerSessions
|
|
887
|
+
? session.playerSessions.snapshot()
|
|
888
|
+
: Object.fromEntries(playerResumeTokens),
|
|
889
|
+
'player session store snapshot',
|
|
890
|
+
);
|
|
891
|
+
if (!isPlainObject(captured)) {
|
|
892
|
+
throw new TypeError('player session store snapshot must be an object');
|
|
893
|
+
}
|
|
894
|
+
const tokens: Record<string, string> = {};
|
|
895
|
+
for (const [playerId, token] of Object.entries(captured)) {
|
|
896
|
+
if (playerId.trim().length === 0) {
|
|
897
|
+
throw new TypeError(
|
|
898
|
+
'player session store snapshot player ids must be non-empty',
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
902
|
+
throw new TypeError(
|
|
903
|
+
`player session store snapshot token for ${playerId} must be a non-empty string`,
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
tokens[playerId] = token;
|
|
907
|
+
}
|
|
908
|
+
return tokens;
|
|
909
|
+
};
|
|
910
|
+
const restorePlayerResumeTokens = (
|
|
911
|
+
tokens: Readonly<Record<string, string>>,
|
|
912
|
+
): void => {
|
|
913
|
+
const session = requireSessionIdentity();
|
|
914
|
+
if (session.playerSessions) {
|
|
915
|
+
session.playerSessions.restore(tokens);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
playerResumeTokens.clear();
|
|
919
|
+
for (const [playerId, token] of Object.entries(tokens)) {
|
|
920
|
+
playerResumeTokens.set(playerId, token);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
954
923
|
const currentState = (): PlaybookState => {
|
|
955
924
|
const live = actor;
|
|
956
925
|
if (!live) {
|
|
957
|
-
throw new Error('
|
|
926
|
+
throw new Error('decide runtime: actor is not initialized');
|
|
958
927
|
}
|
|
959
|
-
return normalizePlaybookSnapshot(live.getSnapshot()
|
|
928
|
+
return normalizePlaybookSnapshot(live.getSnapshot(), {
|
|
929
|
+
pendingCall: nestedBridge.getPendingCall(),
|
|
930
|
+
});
|
|
960
931
|
};
|
|
961
932
|
const stateIdentity = (state: PlaybookState): { stateId?: string } => {
|
|
962
933
|
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
@@ -1062,7 +1033,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1062
1033
|
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
1063
1034
|
};
|
|
1064
1035
|
const queued = await judgeQueue.add(async () => {
|
|
1065
|
-
// Keep the complete queue task pending until an active
|
|
1036
|
+
// Keep the complete queue task pending until an active coder promise
|
|
1066
1037
|
// settles. PQueue's signal option may reject add() while that task is
|
|
1067
1038
|
// still running, which would let the turn drain race a late finish.
|
|
1068
1039
|
signal.throwIfAborted();
|
|
@@ -1134,10 +1105,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1134
1105
|
`resolved player "${playerId}" already has an in-flight call`,
|
|
1135
1106
|
);
|
|
1136
1107
|
}
|
|
1137
|
-
inFlightPlayerIds.add(playerId);
|
|
1138
1108
|
const prompt = composePlayerPrompt(input);
|
|
1139
|
-
|
|
1140
|
-
|
|
1109
|
+
let resume: PlayerCallOptions['resume'];
|
|
1110
|
+
try {
|
|
1111
|
+
signal.throwIfAborted();
|
|
1112
|
+
resume = selectPlayerResume(playerId);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
latchControlPlaneError(error, signal);
|
|
1115
|
+
throw error;
|
|
1116
|
+
}
|
|
1141
1117
|
const callId = `player-${++playerCallSequence}`;
|
|
1142
1118
|
const identity = {
|
|
1143
1119
|
purpose: 'captain',
|
|
@@ -1159,6 +1135,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1159
1135
|
},
|
|
1160
1136
|
{ turnId: currentTurnId, callId },
|
|
1161
1137
|
);
|
|
1138
|
+
inFlightPlayerIds.add(playerId);
|
|
1162
1139
|
try {
|
|
1163
1140
|
await emitCallStarted(
|
|
1164
1141
|
'player.call.started',
|
|
@@ -1175,13 +1152,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1175
1152
|
requirePorts().callPlayer(playerId, prompt, signal, { resume }),
|
|
1176
1153
|
);
|
|
1177
1154
|
rawResult = await boundary;
|
|
1178
|
-
// An XState sibling cancellation does not cancel an arbitrary
|
|
1155
|
+
// An XState sibling cancellation does not cancel an arbitrary coder
|
|
1179
1156
|
// promise. Re-check before a late resolution can mutate continuity or
|
|
1180
1157
|
// masquerade as a successful boundary finish.
|
|
1181
1158
|
signal.throwIfAborted();
|
|
1182
1159
|
} catch (error) {
|
|
1183
1160
|
// A rejected call produced no authoritative result, so the previous
|
|
1184
|
-
// token remains untouched. This also covers a
|
|
1161
|
+
// token remains untouched. This also covers a coder promise that
|
|
1185
1162
|
// resolves after its invocation signal was cancelled.
|
|
1186
1163
|
latchControlPlaneError(error, signal);
|
|
1187
1164
|
try {
|
|
@@ -1200,20 +1177,29 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1200
1177
|
try {
|
|
1201
1178
|
await emitFailure(error);
|
|
1202
1179
|
} catch {
|
|
1203
|
-
// The malformed
|
|
1180
|
+
// The malformed coder result remains authoritative.
|
|
1204
1181
|
}
|
|
1205
1182
|
throw error;
|
|
1206
1183
|
}
|
|
1207
1184
|
|
|
1208
1185
|
// The resolved result is authoritative even on aborted/error status.
|
|
1209
1186
|
// Update continuation state before interpreting that status.
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1187
|
+
try {
|
|
1188
|
+
updatePlayerResume(
|
|
1189
|
+
playerId,
|
|
1190
|
+
typeof result.resumeToken === 'string' &&
|
|
1191
|
+
result.resumeToken.trim().length > 0
|
|
1192
|
+
? result.resumeToken
|
|
1193
|
+
: undefined,
|
|
1194
|
+
);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
latchControlPlaneError(error, signal);
|
|
1197
|
+
try {
|
|
1198
|
+
await emitFailure(error);
|
|
1199
|
+
} catch {
|
|
1200
|
+
// The continuation-store failure remains authoritative.
|
|
1201
|
+
}
|
|
1202
|
+
throw error;
|
|
1217
1203
|
}
|
|
1218
1204
|
|
|
1219
1205
|
// Keep this finish outside the boundary catch. A trace sink can record
|
|
@@ -1263,7 +1249,19 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1263
1249
|
}
|
|
1264
1250
|
combined.throwIfAborted();
|
|
1265
1251
|
|
|
1266
|
-
|
|
1252
|
+
let { playerId, result } = await callPlayer(input, combined);
|
|
1253
|
+
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
1254
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1255
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1256
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
1257
|
+
// player-call pair, with the resume selection re-read from the
|
|
1258
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
1259
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
1260
|
+
// are never retried), and a rejecting finish emission rejects
|
|
1261
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1262
|
+
combined.throwIfAborted();
|
|
1263
|
+
({ playerId, result } = await callPlayer(input, combined));
|
|
1264
|
+
}
|
|
1267
1265
|
if (result.status !== 'ok') {
|
|
1268
1266
|
throw new Error(
|
|
1269
1267
|
`player "${playerId}" returned status "${result.status}"${
|
|
@@ -1271,7 +1269,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1271
1269
|
}`,
|
|
1272
1270
|
);
|
|
1273
1271
|
}
|
|
1274
|
-
|
|
1272
|
+
const finalText = result.finalText ?? '';
|
|
1273
|
+
if (isEmptyFinalText(finalText)) {
|
|
1275
1274
|
throw new Error(
|
|
1276
1275
|
`player "${playerId}" returned status "ok" with no finalText`,
|
|
1277
1276
|
);
|
|
@@ -1279,7 +1278,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1279
1278
|
combined.throwIfAborted();
|
|
1280
1279
|
|
|
1281
1280
|
try {
|
|
1282
|
-
const prompt = buildAdjudicatorPrompt(input,
|
|
1281
|
+
const prompt = buildAdjudicatorPrompt(input, finalText);
|
|
1283
1282
|
return parseAdjudication(
|
|
1284
1283
|
await callJudge(
|
|
1285
1284
|
prompt,
|
|
@@ -1288,6 +1287,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1288
1287
|
input.stateId,
|
|
1289
1288
|
),
|
|
1290
1289
|
input,
|
|
1290
|
+
finalText,
|
|
1291
1291
|
);
|
|
1292
1292
|
} catch (error) {
|
|
1293
1293
|
latchControlPlaneError(error, combined);
|
|
@@ -1296,89 +1296,162 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1296
1296
|
},
|
|
1297
1297
|
);
|
|
1298
1298
|
|
|
1299
|
-
|
|
1299
|
+
nestedBridge = createNestedPlaybookBridge<PlaybookInput>({
|
|
1300
|
+
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
1301
|
+
getBoundarySignal: () => currentSignal,
|
|
1302
|
+
callPlaybook: (request, signal) =>
|
|
1303
|
+
trackBoundaryCall(
|
|
1304
|
+
Promise.resolve(requirePorts().callPlaybook(request, signal)),
|
|
1305
|
+
),
|
|
1306
|
+
emitStarted: async (event) => {
|
|
1307
|
+
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
1308
|
+
await emitTrace(
|
|
1309
|
+
'playbook.call.started',
|
|
1310
|
+
{
|
|
1311
|
+
stateId: event.stateId,
|
|
1312
|
+
playbookId: event.playbookId,
|
|
1313
|
+
text: event.text,
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
1317
|
+
callId: event.callId,
|
|
1318
|
+
},
|
|
1319
|
+
);
|
|
1320
|
+
},
|
|
1321
|
+
emitFinished: async (event) => {
|
|
1322
|
+
const turnId = playbookCallTurnIds.get(event.callId);
|
|
1323
|
+
try {
|
|
1324
|
+
await emitTrace(
|
|
1325
|
+
'playbook.call.finished',
|
|
1326
|
+
{
|
|
1327
|
+
stateId: event.stateId,
|
|
1328
|
+
playbookId: event.playbookId,
|
|
1329
|
+
text: event.text,
|
|
1330
|
+
result: event.result,
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
1334
|
+
callId: event.callId,
|
|
1335
|
+
},
|
|
1336
|
+
);
|
|
1337
|
+
} finally {
|
|
1338
|
+
playbookCallTurnIds.delete(event.callId);
|
|
1339
|
+
}
|
|
1340
|
+
},
|
|
1341
|
+
drain: flush,
|
|
1342
|
+
bindResumeSignal: (signal) => {
|
|
1343
|
+
currentSignal = signal;
|
|
1344
|
+
},
|
|
1345
|
+
onControlPlaneError: (error) => {
|
|
1346
|
+
const signal = currentSignal;
|
|
1347
|
+
if (!signal || !isAbortFailure(error, signal)) {
|
|
1348
|
+
controlPlaneError ??= error;
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
onBackgroundError: (error) => {
|
|
1352
|
+
collectFailure(emissionFailures, error);
|
|
1353
|
+
},
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
const providedMachine = decideMachine.provide({
|
|
1357
|
+
actors: { player, playbook: nestedBridge.actorLogic },
|
|
1358
|
+
});
|
|
1300
1359
|
|
|
1301
1360
|
const inspect = (event: InspectionEvent): void => {
|
|
1302
1361
|
if (event.type !== '@xstate.snapshot') return;
|
|
1303
1362
|
if (actor === undefined || event.actorRef !== actor) return;
|
|
1304
1363
|
if (suppressInspectionEmissions) return;
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
fsmPayload,
|
|
1316
|
-
'described FSM telemetry',
|
|
1317
|
-
);
|
|
1364
|
+
try {
|
|
1365
|
+
const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
|
|
1366
|
+
const state = normalizePlaybookSnapshot(snapshot);
|
|
1367
|
+
const prior = previousState ?? state;
|
|
1368
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1369
|
+
const fsmPayload = telemetryPayload(prior, state, event.event, context);
|
|
1370
|
+
const describedFsmPayload = snapshotJsonValue(
|
|
1371
|
+
fsmPayload,
|
|
1372
|
+
'described FSM telemetry',
|
|
1373
|
+
);
|
|
1318
1374
|
|
|
1319
|
-
void enqueueTracedEmission(
|
|
1320
|
-
'fsm.transition',
|
|
1321
|
-
fsmPayload,
|
|
1322
|
-
{ turnId: currentTurnId },
|
|
1323
|
-
(emissionPorts) =>
|
|
1324
|
-
emissionPorts.emitTelemetry({
|
|
1325
|
-
topic: TELEMETRY_TOPIC,
|
|
1326
|
-
payload: describedFsmPayload,
|
|
1327
|
-
}),
|
|
1328
|
-
).catch(() => undefined);
|
|
1329
|
-
|
|
1330
|
-
const priorIds = new Set(prior?.activeStateIds ?? []);
|
|
1331
|
-
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1332
|
-
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1333
|
-
STATUS_STATE_IDS.has(stateId),
|
|
1334
|
-
);
|
|
1335
|
-
const scheduleStatus = (
|
|
1336
|
-
message: string,
|
|
1337
|
-
stateId: string,
|
|
1338
|
-
data?: JsonValue,
|
|
1339
|
-
): void => {
|
|
1340
|
-
const tracePayload = {
|
|
1341
|
-
...(bossRelevantStateIds.length === 1 ? { stateId } : {}),
|
|
1342
|
-
message,
|
|
1343
|
-
state,
|
|
1344
|
-
...(data !== undefined ? { data } : {}),
|
|
1345
|
-
};
|
|
1346
|
-
assertJsonSafe(tracePayload);
|
|
1347
1375
|
void enqueueTracedEmission(
|
|
1348
|
-
'
|
|
1349
|
-
|
|
1376
|
+
'fsm.transition',
|
|
1377
|
+
fsmPayload,
|
|
1350
1378
|
{ turnId: currentTurnId },
|
|
1351
|
-
(emissionPorts) =>
|
|
1379
|
+
(emissionPorts) =>
|
|
1380
|
+
emissionPorts.emitTelemetry({
|
|
1381
|
+
topic: TELEMETRY_TOPIC,
|
|
1382
|
+
payload: describedFsmPayload,
|
|
1383
|
+
}),
|
|
1352
1384
|
).catch(() => undefined);
|
|
1353
|
-
};
|
|
1354
1385
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1386
|
+
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1387
|
+
previousState = state;
|
|
1388
|
+
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1389
|
+
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1390
|
+
STATUS_STATE_IDS.has(stateId),
|
|
1391
|
+
);
|
|
1392
|
+
const scheduleStatus = (
|
|
1393
|
+
message: string,
|
|
1394
|
+
stateId: string,
|
|
1395
|
+
data?: JsonValue,
|
|
1396
|
+
): void => {
|
|
1397
|
+
const tracePayload = {
|
|
1398
|
+
...(bossRelevantStateIds.length === 1 ? { stateId } : {}),
|
|
1399
|
+
message,
|
|
1400
|
+
state,
|
|
1401
|
+
...(data !== undefined ? { data } : {}),
|
|
1402
|
+
};
|
|
1403
|
+
assertJsonSafe(tracePayload);
|
|
1404
|
+
void enqueueTracedEmission(
|
|
1405
|
+
'status.emitted',
|
|
1406
|
+
tracePayload,
|
|
1407
|
+
{ turnId: currentTurnId },
|
|
1408
|
+
(emissionPorts) => emissionPorts.emitStatus(message, data),
|
|
1409
|
+
).catch(() => undefined);
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
for (const activeStateId of state.activeStateIds) {
|
|
1413
|
+
if (
|
|
1414
|
+
priorIds.has(activeStateId) ||
|
|
1415
|
+
!STATUS_STATE_IDS.has(activeStateId)
|
|
1416
|
+
) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (WAIT_STATE_IDS.has(activeStateId)) {
|
|
1420
|
+
const pending = questionForWaitState(
|
|
1368
1421
|
activeStateId,
|
|
1422
|
+
pendingQuestions,
|
|
1369
1423
|
);
|
|
1424
|
+
if (pending) {
|
|
1425
|
+
scheduleStatus(
|
|
1426
|
+
`${pending.player} asks: ${pending.question}`,
|
|
1427
|
+
activeStateId,
|
|
1428
|
+
);
|
|
1429
|
+
scheduleStatus(
|
|
1430
|
+
`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.player} · ${pending.sourceItem}`,
|
|
1431
|
+
activeStateId,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
continue;
|
|
1370
1435
|
}
|
|
1371
|
-
|
|
1436
|
+
const lastError =
|
|
1437
|
+
activeStateId === 'failed'
|
|
1438
|
+
? normalizeErrorCompact(context.lastError)
|
|
1439
|
+
: undefined;
|
|
1440
|
+
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
1441
|
+
if (description === undefined) continue;
|
|
1442
|
+
const playerState = PLAYER_STATES.find(
|
|
1443
|
+
(candidate) => candidate.stateId === activeStateId,
|
|
1444
|
+
);
|
|
1445
|
+
scheduleStatus(
|
|
1446
|
+
playerState === undefined
|
|
1447
|
+
? '◆ workflow failed; awaiting Boss recovery.'
|
|
1448
|
+
: `⤷ ${playerState.player}: ${description}`,
|
|
1449
|
+
activeStateId,
|
|
1450
|
+
lastError === undefined ? undefined : { lastError },
|
|
1451
|
+
);
|
|
1372
1452
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
? normalizeErrorCompact(context.lastError)
|
|
1376
|
-
: undefined;
|
|
1377
|
-
scheduleStatus(
|
|
1378
|
-
STATE_DESCRIPTIONS[activeStateId] ?? activeStateId,
|
|
1379
|
-
activeStateId,
|
|
1380
|
-
lastError === undefined ? undefined : { lastError },
|
|
1381
|
-
);
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
latchInspectionError(error);
|
|
1382
1455
|
}
|
|
1383
1456
|
};
|
|
1384
1457
|
|
|
@@ -1392,34 +1465,60 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1392
1465
|
? {}
|
|
1393
1466
|
: {
|
|
1394
1467
|
snapshot: machineSnapshot as unknown as SnapshotFrom<
|
|
1395
|
-
typeof
|
|
1468
|
+
typeof decideMachine
|
|
1396
1469
|
>,
|
|
1397
1470
|
}),
|
|
1398
1471
|
inspect,
|
|
1399
1472
|
});
|
|
1400
1473
|
};
|
|
1401
1474
|
|
|
1475
|
+
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
1476
|
+
// still-running actor fires one more `@xstate.snapshot` for the *unchanged*
|
|
1477
|
+
// state value with `status: 'stopped'`; `inspect` cannot tell that disposal
|
|
1478
|
+
// artifact from a state entry, so unsuppressed it re-emits the parked
|
|
1479
|
+
// state's telemetry and a phantom self-loop transition. Suppression is a
|
|
1480
|
+
// property of stopping, not a rule each caller must remember — every stop
|
|
1481
|
+
// goes through here so no later site can reintroduce the omission.
|
|
1482
|
+
const stopActor = (): void => {
|
|
1483
|
+
if (!actor) return;
|
|
1484
|
+
suppressInspectionEmissions = true;
|
|
1485
|
+
actor.stop();
|
|
1486
|
+
};
|
|
1487
|
+
|
|
1402
1488
|
const startActor = (): void => {
|
|
1403
1489
|
createRuntimeActor();
|
|
1490
|
+
// A fresh actor's emissions are real state entries again.
|
|
1491
|
+
suppressInspectionEmissions = false;
|
|
1404
1492
|
actor?.start();
|
|
1405
1493
|
};
|
|
1406
1494
|
|
|
1407
1495
|
const driveToQuiescence = async (): Promise<void> => {
|
|
1408
1496
|
const live = actor;
|
|
1409
|
-
if (!live) throw new Error('
|
|
1410
|
-
await waitForPlaybookQuiescence(live);
|
|
1497
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1498
|
+
await waitForPlaybookQuiescence(live, { pendingCalls: nestedBridge });
|
|
1411
1499
|
};
|
|
1412
1500
|
|
|
1413
1501
|
const classify = async (
|
|
1414
1502
|
text: string,
|
|
1415
1503
|
signal: AbortSignal,
|
|
1416
|
-
): Promise<
|
|
1504
|
+
): Promise<DecideEvent | { type: 'NO_ACTION' } | null> => {
|
|
1417
1505
|
const live = actor;
|
|
1418
|
-
if (!live) throw new Error('
|
|
1419
|
-
const snapshot = live.getSnapshot() as SnapshotFrom<typeof
|
|
1420
|
-
const context = snapshot.context as Record<string, unknown>;
|
|
1421
|
-
const state = normalizePlaybookSnapshot(snapshot
|
|
1506
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1507
|
+
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
1508
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1509
|
+
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1510
|
+
pendingCall: nestedBridge.getPendingCall(),
|
|
1511
|
+
});
|
|
1422
1512
|
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1513
|
+
if (
|
|
1514
|
+
pendingQuestions.length === 0 &&
|
|
1515
|
+
(snapshot.status === 'done' ||
|
|
1516
|
+
state.activeStateIds.includes('ready') ||
|
|
1517
|
+
state.activeStateIds.includes('failed'))
|
|
1518
|
+
) {
|
|
1519
|
+
return { type: 'START_DECIDE', callerTopic: text };
|
|
1520
|
+
}
|
|
1521
|
+
if (pendingQuestions.length === 0) return null;
|
|
1423
1522
|
const prompt = buildClassifierPrompt(text, {
|
|
1424
1523
|
state,
|
|
1425
1524
|
pendingQuestions,
|
|
@@ -1432,16 +1531,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1432
1531
|
);
|
|
1433
1532
|
return parseClassification(
|
|
1434
1533
|
raw,
|
|
1534
|
+
text,
|
|
1435
1535
|
pendingQuestions.map(({ questionId }) => questionId),
|
|
1436
1536
|
);
|
|
1437
1537
|
};
|
|
1438
1538
|
|
|
1439
1539
|
const resultForSnapshot = (signal?: AbortSignal): PlaybookRunResult => {
|
|
1440
1540
|
const live = actor;
|
|
1441
|
-
if (!live) throw new Error('
|
|
1442
|
-
const snapshot = live.getSnapshot() as SnapshotFrom<typeof
|
|
1443
|
-
const
|
|
1444
|
-
const
|
|
1541
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1542
|
+
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
1543
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
1544
|
+
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
1545
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1445
1546
|
if (signal?.aborted) {
|
|
1446
1547
|
return {
|
|
1447
1548
|
outcome: 'aborted',
|
|
@@ -1468,7 +1569,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1468
1569
|
if (snapshot.status === 'error') {
|
|
1469
1570
|
throw (
|
|
1470
1571
|
(snapshot as { error?: unknown }).error ??
|
|
1471
|
-
new Error('
|
|
1572
|
+
new Error('decide runtime actor entered error status')
|
|
1472
1573
|
);
|
|
1473
1574
|
}
|
|
1474
1575
|
if (state.activeStateIds.includes('failed')) {
|
|
@@ -1479,6 +1580,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1479
1580
|
...(error === undefined ? {} : { error }),
|
|
1480
1581
|
};
|
|
1481
1582
|
}
|
|
1583
|
+
if (pendingCall) {
|
|
1584
|
+
return { outcome: 'suspended', state, pendingCall };
|
|
1585
|
+
}
|
|
1482
1586
|
return { outcome: 'quiescent', state };
|
|
1483
1587
|
};
|
|
1484
1588
|
|
|
@@ -1499,9 +1603,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1499
1603
|
// A state that cannot even normalize has no disposal descriptor.
|
|
1500
1604
|
}
|
|
1501
1605
|
}
|
|
1502
|
-
suppressInspectionEmissions = true;
|
|
1503
1606
|
try {
|
|
1504
|
-
|
|
1607
|
+
stopActor();
|
|
1505
1608
|
} catch {
|
|
1506
1609
|
// Preserve the original startup failure.
|
|
1507
1610
|
}
|
|
@@ -1542,6 +1645,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1542
1645
|
turnSequence = 0;
|
|
1543
1646
|
judgeCallSequence = 0;
|
|
1544
1647
|
playerCallSequence = 0;
|
|
1648
|
+
playbookCallSequence = 0;
|
|
1649
|
+
playbookCallTurnIds.clear();
|
|
1545
1650
|
lifecycleStarted = false;
|
|
1546
1651
|
};
|
|
1547
1652
|
|
|
@@ -1554,7 +1659,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1554
1659
|
disposalPromise !== undefined
|
|
1555
1660
|
) {
|
|
1556
1661
|
throw new Error(
|
|
1557
|
-
'
|
|
1662
|
+
'decide runtime: init(session) may only be called once',
|
|
1558
1663
|
);
|
|
1559
1664
|
}
|
|
1560
1665
|
const identity = snapshotPlaybookSession(session);
|
|
@@ -1588,8 +1693,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1588
1693
|
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked session.
|
|
1589
1694
|
// Defined only at a safe capture point — initialized, not disposing
|
|
1590
1695
|
// or disposed, no active public boundary, and the actor quiescent
|
|
1591
|
-
// with status `active
|
|
1592
|
-
// so no pending-call guard applies.
|
|
1696
|
+
// with status `active` and no pending nested REVIEW call.
|
|
1593
1697
|
exportSnapshot(): PlaybookRuntimeSnapshot | undefined {
|
|
1594
1698
|
if (
|
|
1595
1699
|
!actor ||
|
|
@@ -1602,25 +1706,26 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1602
1706
|
if (currentTurnId !== undefined || currentSignal !== undefined) {
|
|
1603
1707
|
return undefined;
|
|
1604
1708
|
}
|
|
1709
|
+
if (nestedBridge.getPendingCall()) return undefined;
|
|
1605
1710
|
const state = currentState();
|
|
1606
1711
|
if (state.status !== 'active' || !state.quiescent) return undefined;
|
|
1607
1712
|
const machine = detachPersistedMachineSnapshot(
|
|
1608
1713
|
actor.getPersistedSnapshot(),
|
|
1609
1714
|
);
|
|
1610
1715
|
const context = (
|
|
1611
|
-
actor.getSnapshot() as SnapshotFrom<typeof
|
|
1612
|
-
).context as Record<string, unknown>;
|
|
1716
|
+
actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
1717
|
+
).context as unknown as Record<string, unknown>;
|
|
1613
1718
|
return {
|
|
1614
1719
|
schemaVersion: 1,
|
|
1615
1720
|
playbookId: sessionIdentity.playbookId,
|
|
1616
1721
|
machine,
|
|
1617
|
-
playerResumeTokens:
|
|
1722
|
+
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
1618
1723
|
sequences: {
|
|
1619
1724
|
trace: traceSequence,
|
|
1620
1725
|
turn: turnSequence,
|
|
1621
1726
|
judgeCall: judgeCallSequence,
|
|
1622
1727
|
playerCall: playerCallSequence,
|
|
1623
|
-
playbookCall:
|
|
1728
|
+
playbookCall: playbookCallSequence,
|
|
1624
1729
|
},
|
|
1625
1730
|
state,
|
|
1626
1731
|
pendingBossQuestions: pendingQuestionsFromContext(context).map(
|
|
@@ -1650,7 +1755,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1650
1755
|
disposalPromise !== undefined
|
|
1651
1756
|
) {
|
|
1652
1757
|
throw new Error(
|
|
1653
|
-
'
|
|
1758
|
+
'decide runtime: restore(session, snapshot) may only be called once',
|
|
1654
1759
|
);
|
|
1655
1760
|
}
|
|
1656
1761
|
const identity = snapshotPlaybookSession(session);
|
|
@@ -1666,32 +1771,45 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1666
1771
|
lifecycleStarted = true;
|
|
1667
1772
|
ports = identity.ports;
|
|
1668
1773
|
sessionIdentity = identity;
|
|
1774
|
+
let priorExternalPlayerTokens:
|
|
1775
|
+
| Readonly<Record<string, string>>
|
|
1776
|
+
| undefined;
|
|
1669
1777
|
try {
|
|
1670
1778
|
traceSequence = boundSnapshot.sequences.trace;
|
|
1671
1779
|
turnSequence = boundSnapshot.sequences.turn;
|
|
1672
1780
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1673
1781
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
)) {
|
|
1678
|
-
playerResumeTokens.set(playerId, token);
|
|
1782
|
+
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
1783
|
+
if (identity.playerSessions) {
|
|
1784
|
+
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
1679
1785
|
}
|
|
1786
|
+
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
1680
1787
|
suppressInspectionEmissions = true;
|
|
1681
1788
|
createRuntimeActor(boundSnapshot.machine);
|
|
1682
1789
|
actor?.start();
|
|
1683
1790
|
const restoredState = currentState();
|
|
1684
1791
|
if (restoredState.status !== 'active') {
|
|
1685
1792
|
throw new Error(
|
|
1686
|
-
`
|
|
1793
|
+
`decide runtime: restored actor status is ${restoredState.status}, expected active`,
|
|
1687
1794
|
);
|
|
1688
1795
|
}
|
|
1689
1796
|
suppressInspectionEmissions = false;
|
|
1690
1797
|
previousState = restoredState;
|
|
1691
1798
|
await flush();
|
|
1692
1799
|
} catch (error) {
|
|
1800
|
+
let failure = error;
|
|
1801
|
+
if (priorExternalPlayerTokens !== undefined) {
|
|
1802
|
+
try {
|
|
1803
|
+
identity.playerSessions!.restore(priorExternalPlayerTokens);
|
|
1804
|
+
} catch (rollbackError) {
|
|
1805
|
+
failure = new AggregateError(
|
|
1806
|
+
[error, rollbackError],
|
|
1807
|
+
'DECIDE restore and player continuation rollback failed',
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1693
1811
|
await cleanupFailedStart({ emitDisposal: false });
|
|
1694
|
-
throw
|
|
1812
|
+
throw failure;
|
|
1695
1813
|
} finally {
|
|
1696
1814
|
finishInitialization();
|
|
1697
1815
|
if (initInFlight === initialization) initInFlight = undefined;
|
|
@@ -1703,16 +1821,16 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1703
1821
|
signal: AbortSignal;
|
|
1704
1822
|
}): Promise<PlaybookRunResult> {
|
|
1705
1823
|
if (disposalPromise !== undefined) {
|
|
1706
|
-
throw new Error('
|
|
1824
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
1707
1825
|
}
|
|
1708
1826
|
requirePorts();
|
|
1709
1827
|
if (!actor) {
|
|
1710
1828
|
throw new Error(
|
|
1711
|
-
'
|
|
1829
|
+
'decide runtime: init(session) must be called before handleBossInput',
|
|
1712
1830
|
);
|
|
1713
1831
|
}
|
|
1714
|
-
if (
|
|
1715
|
-
throw new Error('
|
|
1832
|
+
if (currentSignal !== undefined) {
|
|
1833
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
1716
1834
|
}
|
|
1717
1835
|
|
|
1718
1836
|
const turnId = ++turnSequence;
|
|
@@ -1733,10 +1851,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1733
1851
|
const state = currentState();
|
|
1734
1852
|
await emitBoundaryStatus('No playbook action classified.', state);
|
|
1735
1853
|
result = { outcome: 'no-action', state };
|
|
1854
|
+
} else if (event.type === 'NO_ACTION') {
|
|
1855
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
1736
1856
|
} else {
|
|
1737
1857
|
await emitBoundaryStatus(event.type, currentState());
|
|
1738
1858
|
if (actor.getSnapshot().status === 'done') {
|
|
1739
|
-
|
|
1859
|
+
stopActor();
|
|
1740
1860
|
startActor();
|
|
1741
1861
|
}
|
|
1742
1862
|
|
|
@@ -1817,36 +1937,77 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1817
1937
|
|
|
1818
1938
|
if (failures.length === 1) throw failures[0];
|
|
1819
1939
|
if (failures.length > 1) {
|
|
1820
|
-
throw new AggregateError(failures, '
|
|
1940
|
+
throw new AggregateError(failures, 'decide runtime turn failed');
|
|
1821
1941
|
}
|
|
1822
1942
|
return result;
|
|
1823
1943
|
},
|
|
1824
1944
|
|
|
1825
1945
|
async resumePlaybookCall({
|
|
1826
1946
|
callId,
|
|
1947
|
+
result: childResult,
|
|
1948
|
+
signal,
|
|
1827
1949
|
}: {
|
|
1828
1950
|
callId: string;
|
|
1829
1951
|
result: PlaybookCallResult;
|
|
1830
1952
|
signal: AbortSignal;
|
|
1831
1953
|
}): Promise<PlaybookRunResult> {
|
|
1832
1954
|
if (disposalPromise !== undefined) {
|
|
1833
|
-
throw new Error('
|
|
1955
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
1834
1956
|
}
|
|
1835
1957
|
requirePorts();
|
|
1836
1958
|
if (!actor) {
|
|
1837
1959
|
throw new Error(
|
|
1838
|
-
'
|
|
1960
|
+
'decide runtime: init(session) must be called before resumePlaybookCall',
|
|
1839
1961
|
);
|
|
1840
1962
|
}
|
|
1841
|
-
|
|
1963
|
+
if (currentSignal !== undefined) {
|
|
1964
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
1965
|
+
}
|
|
1966
|
+
currentTurnId = playbookCallTurnIds.get(callId);
|
|
1967
|
+
currentSignal = signal;
|
|
1968
|
+
controlPlaneError = undefined;
|
|
1969
|
+
let runResult: PlaybookRunResult | undefined;
|
|
1970
|
+
let operationError: unknown;
|
|
1971
|
+
try {
|
|
1972
|
+
await nestedBridge.resume({
|
|
1973
|
+
callId,
|
|
1974
|
+
result: childResult,
|
|
1975
|
+
signal,
|
|
1976
|
+
});
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
operationError = error;
|
|
1979
|
+
}
|
|
1980
|
+
try {
|
|
1981
|
+
await waitForPlaybookQuiescence(actor, {
|
|
1982
|
+
pendingCalls: nestedBridge,
|
|
1983
|
+
});
|
|
1984
|
+
runResult = resultForSnapshot(signal);
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
operationError ??= error;
|
|
1987
|
+
}
|
|
1988
|
+
let drainError: unknown;
|
|
1989
|
+
try {
|
|
1990
|
+
await drainBoundaryCallsAndEmissions();
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
drainError = error;
|
|
1993
|
+
}
|
|
1994
|
+
const failure = controlPlaneError ?? drainError ?? operationError;
|
|
1995
|
+
currentSignal = undefined;
|
|
1996
|
+
currentTurnId = undefined;
|
|
1997
|
+
controlPlaneError = undefined;
|
|
1998
|
+
if (failure !== undefined) throw failure;
|
|
1999
|
+
if (runResult === undefined) {
|
|
2000
|
+
throw new Error('decide runtime: playbook resume produced no result');
|
|
2001
|
+
}
|
|
2002
|
+
return runResult;
|
|
1842
2003
|
},
|
|
1843
2004
|
|
|
1844
2005
|
dispose(): Promise<void> {
|
|
1845
2006
|
if (disposalPromise !== undefined) return disposalPromise;
|
|
1846
|
-
if (
|
|
2007
|
+
if (currentSignal !== undefined) {
|
|
1847
2008
|
return Promise.reject(
|
|
1848
2009
|
new Error(
|
|
1849
|
-
'
|
|
2010
|
+
'decide runtime: cannot dispose while a runtime turn is active',
|
|
1850
2011
|
),
|
|
1851
2012
|
);
|
|
1852
2013
|
}
|
|
@@ -1859,8 +2020,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1859
2020
|
}
|
|
1860
2021
|
const finalState = currentState();
|
|
1861
2022
|
const failures: unknown[] = [];
|
|
1862
|
-
|
|
1863
|
-
|
|
2023
|
+
stopActor();
|
|
2024
|
+
try {
|
|
2025
|
+
await nestedBridge.dispose();
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
collectFailure(failures, error);
|
|
1864
2028
|
}
|
|
1865
2029
|
try {
|
|
1866
2030
|
await drainBoundaryCallsAndEmissions();
|
|
@@ -1881,6 +2045,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1881
2045
|
collectFailure(failures, error);
|
|
1882
2046
|
} finally {
|
|
1883
2047
|
playerResumeTokens.clear();
|
|
2048
|
+
playbookCallTurnIds.clear();
|
|
1884
2049
|
inFlightPlayerIds.clear();
|
|
1885
2050
|
activeBoundaryCalls.clear();
|
|
1886
2051
|
activeEmissionCalls.clear();
|
|
@@ -1897,7 +2062,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1897
2062
|
}
|
|
1898
2063
|
if (failures.length === 1) throw failures[0];
|
|
1899
2064
|
if (failures.length > 1) {
|
|
1900
|
-
throw new AggregateError(failures, '
|
|
2065
|
+
throw new AggregateError(failures, 'decide runtime disposal failed');
|
|
1901
2066
|
}
|
|
1902
2067
|
})();
|
|
1903
2068
|
return disposalPromise;
|
|
@@ -1919,10 +2084,10 @@ export const _internal = {
|
|
|
1919
2084
|
normalizeErrorCompact,
|
|
1920
2085
|
normalizeErrorFull,
|
|
1921
2086
|
DEFAULT_PLAYER_BINDING,
|
|
1922
|
-
ALIAS_RESOLUTION,
|
|
1923
2087
|
STATE_DESCRIPTIONS,
|
|
1924
|
-
|
|
1925
|
-
|
|
2088
|
+
PLAYER_STATES,
|
|
2089
|
+
PLAYER_STATE_IDS,
|
|
2090
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
1926
2091
|
BOSS_INTERRUPT_TARGETS,
|
|
1927
2092
|
CONTINUATION_PREAMBLE,
|
|
1928
2093
|
TELEMETRY_TOPIC,
|