@sublang/playbook 5.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -14
- package/docs/cli.md +138 -90
- package/docs/configuration.md +113 -29
- package/docs/embedding.md +24 -16
- package/package.json +42 -21
- package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1171 -983
- package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
- package/reference/sdlc/code.playbook/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 -656
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
- 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 +70 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +954 -80
- package/reference/sdlc/code.playbook/playbook-captain.ts +1408 -80
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +37 -36
- 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} +545 -372
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +665 -454
- 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 +13 -4
- package/slc/link.md +83 -14
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +21 -2
- package/src/runtime.ts +38 -6
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +319 -35
- package/src/xstate-playbook-runtime.ts +412 -41
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +384 -57
- package/src/xstate-runtime.ts +542 -71
- 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' },
|
|
208
|
-
{
|
|
209
|
-
stateId: 'participantInitialRound',
|
|
210
|
-
player: 'Participant',
|
|
211
|
-
sourceItem: 'DISCUSS-4',
|
|
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
|
-
},
|
|
170
|
+
const PLAYER_STATES = [
|
|
171
|
+
{ stateId: 'askCoderProposal', player: 'Coder', sourceItem: 'DECIDE-1' },
|
|
229
172
|
{
|
|
230
|
-
stateId: '
|
|
231
|
-
player: '
|
|
232
|
-
sourceItem: '
|
|
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;
|
|
@@ -431,6 +324,23 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
431
324
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
432
325
|
}
|
|
433
326
|
|
|
327
|
+
function sortJson(value: JsonValue): JsonValue {
|
|
328
|
+
if (Array.isArray(value)) return value.map((entry) => sortJson(entry));
|
|
329
|
+
if (value !== null && typeof value === 'object') {
|
|
330
|
+
const record = value as { readonly [key: string]: JsonValue };
|
|
331
|
+
const sorted: Record<string, JsonValue> = {};
|
|
332
|
+
for (const key of Object.keys(record).sort()) {
|
|
333
|
+
sorted[key] = sortJson(record[key]);
|
|
334
|
+
}
|
|
335
|
+
return sorted;
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function stableJson(value: unknown, path: string): string {
|
|
341
|
+
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
342
|
+
}
|
|
343
|
+
|
|
434
344
|
function stripCodeFence(text: string): string {
|
|
435
345
|
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
436
346
|
return fence ? fence[1].trim() : text;
|
|
@@ -492,7 +402,7 @@ function buildClassifierPrompt(
|
|
|
492
402
|
},
|
|
493
403
|
): string {
|
|
494
404
|
const lines: string[] = [];
|
|
495
|
-
lines.push('You are the Boss-input classifier for the
|
|
405
|
+
lines.push('You are the Boss-input classifier for the decide playbook.');
|
|
496
406
|
lines.push(
|
|
497
407
|
'Classify the Boss message into exactly one FSM event, or into no event.',
|
|
498
408
|
);
|
|
@@ -503,7 +413,7 @@ function buildClassifierPrompt(
|
|
|
503
413
|
lines.push('Pending Boss questions:');
|
|
504
414
|
for (const pending of ctx.pendingQuestions) {
|
|
505
415
|
lines.push(
|
|
506
|
-
`- ${pending.questionId} (${pending.player}
|
|
416
|
+
`- ${pending.questionId} (${pending.player}): ${pending.question}`,
|
|
507
417
|
);
|
|
508
418
|
}
|
|
509
419
|
lines.push(
|
|
@@ -513,98 +423,77 @@ function buildClassifierPrompt(
|
|
|
513
423
|
lines.push('');
|
|
514
424
|
lines.push('Events and payload contracts:');
|
|
515
425
|
lines.push(
|
|
516
|
-
|
|
517
|
-
);
|
|
518
|
-
lines.push(
|
|
519
|
-
'- START_REVIEW: required latestChanges string, required reviewScope string ("specItems" | "decisionRecords" | "mixed"), optional rebuttals string.',
|
|
520
|
-
);
|
|
521
|
-
lines.push(
|
|
522
|
-
`- BOSS_INTERRUPT: required targetId string, one of ${BOSS_INTERRUPT_TARGETS.join(', ')}.`,
|
|
426
|
+
`- BOSS_INTERRUPT: required targetId string, exactly ${BOSS_INTERRUPT_TARGETS[0]}; the runtime attaches the exact Boss text as bossIntent.`,
|
|
523
427
|
);
|
|
524
428
|
lines.push(
|
|
525
|
-
'- BOSS_REPLY:
|
|
429
|
+
'- 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
430
|
);
|
|
431
|
+
lines.push('- NO_ACTION: no fields.');
|
|
527
432
|
lines.push('');
|
|
528
433
|
lines.push('Boss message:');
|
|
529
434
|
lines.push(text);
|
|
530
435
|
lines.push('');
|
|
531
436
|
lines.push(
|
|
532
|
-
'Reply with a single JSON object: { "
|
|
437
|
+
'Reply with a single JSON object: { "type": "<EVENT_TYPE>", ...declared fields }.',
|
|
533
438
|
);
|
|
534
|
-
lines.push('Use
|
|
439
|
+
lines.push('Use NO_ACTION when no FSM action applies.');
|
|
535
440
|
return lines.join('\n');
|
|
536
441
|
}
|
|
537
442
|
|
|
538
443
|
function parseClassification(
|
|
539
444
|
raw: string,
|
|
445
|
+
text: string,
|
|
540
446
|
pendingQuestionIds: readonly string[] = [],
|
|
541
|
-
):
|
|
447
|
+
): DecideEvent | { type: 'NO_ACTION' } | null {
|
|
542
448
|
const obj = extractJson(raw);
|
|
543
449
|
if (!obj) return null;
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
...(typeof obj.participantLlm === 'string'
|
|
555
|
-
? { participantLlm: obj.participantLlm }
|
|
556
|
-
: {}),
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
return null;
|
|
560
|
-
}
|
|
450
|
+
const exactKeys = (...keys: string[]): boolean => {
|
|
451
|
+
const ownKeys = Reflect.ownKeys(obj);
|
|
452
|
+
return (
|
|
453
|
+
ownKeys.length === keys.length &&
|
|
454
|
+
ownKeys.every(
|
|
455
|
+
(key) => typeof key === 'string' && keys.includes(key),
|
|
456
|
+
)
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
const eventType = obj.type;
|
|
561
460
|
|
|
562
|
-
if (eventType === '
|
|
563
|
-
|
|
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;
|
|
461
|
+
if (eventType === 'NO_ACTION') {
|
|
462
|
+
return exactKeys('type') ? { type: 'NO_ACTION' } : null;
|
|
578
463
|
}
|
|
579
464
|
|
|
580
465
|
if (eventType === 'BOSS_INTERRUPT') {
|
|
581
466
|
if (
|
|
467
|
+
exactKeys('type', 'targetId') &&
|
|
582
468
|
typeof obj.targetId === 'string' &&
|
|
583
469
|
BOSS_INTERRUPT_TARGET_IDS.has(obj.targetId)
|
|
584
470
|
) {
|
|
585
|
-
return {
|
|
471
|
+
return {
|
|
472
|
+
type: 'BOSS_INTERRUPT',
|
|
473
|
+
targetId: obj.targetId as 'independentProposals',
|
|
474
|
+
bossIntent: text,
|
|
475
|
+
};
|
|
586
476
|
}
|
|
587
477
|
return null;
|
|
588
478
|
}
|
|
589
479
|
|
|
590
480
|
if (eventType === 'BOSS_REPLY') {
|
|
591
|
-
if (
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
481
|
+
if (pendingQuestionIds.length === 0) return null;
|
|
594
482
|
if (typeof obj.questionId === 'string') {
|
|
595
|
-
return
|
|
483
|
+
return exactKeys('type', 'questionId') &&
|
|
484
|
+
pendingQuestionIds.includes(obj.questionId)
|
|
596
485
|
? {
|
|
597
486
|
type: 'BOSS_REPLY',
|
|
598
487
|
questionId: obj.questionId as never,
|
|
599
|
-
answer:
|
|
488
|
+
answer: text,
|
|
600
489
|
}
|
|
601
490
|
: null;
|
|
602
491
|
}
|
|
603
|
-
return pendingQuestionIds.length === 1
|
|
492
|
+
return exactKeys('type') && pendingQuestionIds.length === 1
|
|
604
493
|
? {
|
|
605
494
|
type: 'BOSS_REPLY',
|
|
606
495
|
questionId: pendingQuestionIds[0] as never,
|
|
607
|
-
answer:
|
|
496
|
+
answer: text,
|
|
608
497
|
}
|
|
609
498
|
: null;
|
|
610
499
|
}
|
|
@@ -639,6 +528,18 @@ function buildAdjudicatorPrompt(
|
|
|
639
528
|
for (const [guard, description] of Object.entries(input.result)) {
|
|
640
529
|
lines.push(`- ${guard}: ${description}`);
|
|
641
530
|
}
|
|
531
|
+
const runtimeOwnedFields = new Set<string>();
|
|
532
|
+
for (const description of Object.values(input.result)) {
|
|
533
|
+
for (const field of requiredFieldsFor(description)) {
|
|
534
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) runtimeOwnedFields.add(field);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (runtimeOwnedFields.size > 0) {
|
|
538
|
+
lines.push('');
|
|
539
|
+
lines.push(
|
|
540
|
+
`The runtime owns these verbatim fields; do not include them in your JSON: ${[...runtimeOwnedFields].join(', ')}.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
642
543
|
lines.push('');
|
|
643
544
|
lines.push(
|
|
644
545
|
'Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.',
|
|
@@ -646,7 +547,11 @@ function buildAdjudicatorPrompt(
|
|
|
646
547
|
return lines.join('\n');
|
|
647
548
|
}
|
|
648
549
|
|
|
649
|
-
function parseAdjudication(
|
|
550
|
+
function parseAdjudication(
|
|
551
|
+
raw: string,
|
|
552
|
+
input: PlayerInput,
|
|
553
|
+
finalText: string,
|
|
554
|
+
): PlayerOutput {
|
|
650
555
|
const obj = extractJson(raw);
|
|
651
556
|
if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
|
|
652
557
|
throw new Error('adjudicator returned empty or malformed JSON');
|
|
@@ -659,15 +564,22 @@ function parseAdjudication(raw: string, input: PlayerInput): PlayerOutput {
|
|
|
659
564
|
);
|
|
660
565
|
}
|
|
661
566
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
567
|
+
const requiredFields = requiredFieldsFor(input.result[guard]);
|
|
568
|
+
const allowedFields = new Set(['guard', ...requiredFields]);
|
|
569
|
+
for (const key of Reflect.ownKeys(obj)) {
|
|
570
|
+
if (typeof key !== 'string' || !allowedFields.has(key)) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const output: Record<string, unknown> = { guard };
|
|
577
|
+
for (const field of requiredFields) {
|
|
578
|
+
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
579
|
+
output[field] = finalText;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const value = obj[field];
|
|
671
583
|
if (
|
|
672
584
|
value === undefined ||
|
|
673
585
|
value === null ||
|
|
@@ -677,19 +589,9 @@ function parseAdjudication(raw: string, input: PlayerInput): PlayerOutput {
|
|
|
677
589
|
`adjudicator response for guard "${guard}" missing required field "${field}"`,
|
|
678
590
|
);
|
|
679
591
|
}
|
|
592
|
+
output[field] = value;
|
|
680
593
|
}
|
|
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;
|
|
594
|
+
return output as PlayerOutput;
|
|
693
595
|
}
|
|
694
596
|
|
|
695
597
|
function combineSignals(
|
|
@@ -722,9 +624,7 @@ function isEmptyFinalText(finalText: string | undefined): boolean {
|
|
|
722
624
|
}
|
|
723
625
|
|
|
724
626
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
725
|
-
|
|
726
|
-
if (Object.is(error, signal.reason)) return true;
|
|
727
|
-
return normalizeErrorCompact(error)?.name === 'AbortError';
|
|
627
|
+
return signal.aborted && Object.is(error, signal.reason);
|
|
728
628
|
}
|
|
729
629
|
|
|
730
630
|
function pendingQuestionsFromContext(
|
|
@@ -761,10 +661,8 @@ function pendingQuestionsFromContext(
|
|
|
761
661
|
}
|
|
762
662
|
|
|
763
663
|
const WAIT_STATE_RESUME_IDS: Readonly<Record<string, string>> = {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
waitHostReconciliationReply: 'hostInitialRound',
|
|
767
|
-
waitParticipantReconciliationReply: 'participantInitialRound',
|
|
664
|
+
waitCoderProposalReply: 'askCoderProposal',
|
|
665
|
+
waitReviewerProposalReply: 'askReviewerProposal',
|
|
768
666
|
};
|
|
769
667
|
|
|
770
668
|
const WAIT_STATE_IDS: ReadonlySet<string> = new Set([
|
|
@@ -773,7 +671,7 @@ const WAIT_STATE_IDS: ReadonlySet<string> = new Set([
|
|
|
773
671
|
]);
|
|
774
672
|
|
|
775
673
|
const STATUS_STATE_IDS: ReadonlySet<string> = new Set([
|
|
776
|
-
...
|
|
674
|
+
...PLAYER_STATE_IDS,
|
|
777
675
|
...WAIT_STATE_IDS,
|
|
778
676
|
'failed',
|
|
779
677
|
]);
|
|
@@ -793,25 +691,36 @@ function questionForWaitState(
|
|
|
793
691
|
: undefined;
|
|
794
692
|
}
|
|
795
693
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
694
|
+
const TRANSITION_EVENT_FIELDS: readonly string[] = [
|
|
695
|
+
'callerTopic',
|
|
696
|
+
'targetId',
|
|
697
|
+
'bossIntent',
|
|
698
|
+
'questionId',
|
|
699
|
+
'answer',
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
function normalizedTransitionEvent(event: unknown): JsonValue {
|
|
703
|
+
if (event === null || typeof event !== 'object') {
|
|
704
|
+
return snapshotJsonValue(event ?? null, 'FSM event');
|
|
805
705
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
(typeof
|
|
811
|
-
|
|
812
|
-
|
|
706
|
+
const source = event as Record<string, unknown>;
|
|
707
|
+
const descriptor: Record<string, JsonValue> = {};
|
|
708
|
+
if (typeof source.type === 'string') descriptor.type = source.type;
|
|
709
|
+
for (const field of TRANSITION_EVENT_FIELDS) {
|
|
710
|
+
if (typeof source[field] === 'string') {
|
|
711
|
+
descriptor[field] = source[field] as string;
|
|
712
|
+
}
|
|
813
713
|
}
|
|
814
|
-
|
|
714
|
+
if (source.output !== undefined) {
|
|
715
|
+
descriptor.output = snapshotJsonValue(source.output, 'FSM event output');
|
|
716
|
+
}
|
|
717
|
+
if (source.error !== undefined) {
|
|
718
|
+
descriptor.error = snapshotJsonValue(
|
|
719
|
+
normalizeError(source.error),
|
|
720
|
+
'FSM event error',
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return snapshotJsonValue(descriptor, 'FSM event');
|
|
815
724
|
}
|
|
816
725
|
|
|
817
726
|
function telemetryPayload(
|
|
@@ -820,22 +729,15 @@ function telemetryPayload(
|
|
|
820
729
|
event: unknown,
|
|
821
730
|
context: Record<string, unknown>,
|
|
822
731
|
): JsonValue {
|
|
823
|
-
const eventError =
|
|
824
|
-
event !== null &&
|
|
825
|
-
typeof event === 'object' &&
|
|
826
|
-
!Array.isArray(event) &&
|
|
827
|
-
'error' in event
|
|
828
|
-
? normalizeErrorFull((event as { error: unknown }).error)
|
|
829
|
-
: undefined;
|
|
830
732
|
const pendingBossQuestions = pendingQuestionsFromContext(context);
|
|
733
|
+
const prior = previousState ?? state;
|
|
831
734
|
const payload = {
|
|
832
|
-
from:
|
|
735
|
+
from: prior.value,
|
|
833
736
|
to: state.value,
|
|
834
|
-
event:
|
|
835
|
-
previousState:
|
|
737
|
+
event: normalizedTransitionEvent(event),
|
|
738
|
+
previousState: prior,
|
|
836
739
|
state,
|
|
837
740
|
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
838
|
-
...(eventError !== undefined ? { error: eventError } : {}),
|
|
839
741
|
...(state.activeStateIds.includes('failed')
|
|
840
742
|
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
841
743
|
: {}),
|
|
@@ -847,15 +749,13 @@ function telemetryPayload(
|
|
|
847
749
|
export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
848
750
|
PlaybookRuntimeOptions
|
|
849
751
|
> = (options) => {
|
|
850
|
-
const boundOptions =
|
|
752
|
+
const boundOptions = snapshotDecideRuntimeOptions(options);
|
|
851
753
|
const binding: Record<PlayerName, string> = {
|
|
852
754
|
...DEFAULT_PLAYER_BINDING,
|
|
853
755
|
...(boundOptions.playerBinding ?? {}),
|
|
854
756
|
};
|
|
855
|
-
const fsmInput:
|
|
856
|
-
|
|
857
|
-
participant: boundOptions.participant,
|
|
858
|
-
committer: boundOptions.committer,
|
|
757
|
+
const fsmInput: DecideInput = {
|
|
758
|
+
coderLlm: boundOptions.coderLlm,
|
|
859
759
|
};
|
|
860
760
|
|
|
861
761
|
type SessionIdentity = Readonly<PlaybookSession>;
|
|
@@ -872,12 +772,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
872
772
|
let turnSequence = 0;
|
|
873
773
|
let judgeCallSequence = 0;
|
|
874
774
|
let playerCallSequence = 0;
|
|
775
|
+
let playbookCallSequence = 0;
|
|
875
776
|
let lifecycleStarted = false;
|
|
876
777
|
let initInFlight: Promise<void> | undefined;
|
|
877
778
|
let disposed = false;
|
|
878
779
|
let disposalPromise: Promise<void> | undefined;
|
|
879
780
|
let controlPlaneError: unknown;
|
|
781
|
+
let nestedBridge: NestedPlaybookBridge<PlaybookInput>;
|
|
880
782
|
const playerResumeTokens = new Map<string, string>();
|
|
783
|
+
const playbookCallTurnIds = new Map<string, number | undefined>();
|
|
881
784
|
const inFlightPlayerIds = new Set<string>();
|
|
882
785
|
const activeBoundaryCalls = new Set<Promise<unknown>>();
|
|
883
786
|
const activeEmissionCalls = new Set<Promise<void>>();
|
|
@@ -899,6 +802,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
899
802
|
): void => {
|
|
900
803
|
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
901
804
|
};
|
|
805
|
+
const latchInspectionError = (error: unknown): void => {
|
|
806
|
+
if (currentSignal !== undefined) {
|
|
807
|
+
latchControlPlaneError(error, currentSignal);
|
|
808
|
+
} else {
|
|
809
|
+
collectFailure(emissionFailures, error);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
902
812
|
const enqueue = (fn: () => Promise<void>): Promise<void> => {
|
|
903
813
|
const queued = emissionQueue.add(fn);
|
|
904
814
|
activeEmissionCalls.add(queued);
|
|
@@ -928,7 +838,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
928
838
|
const failures = emissionFailures;
|
|
929
839
|
emissionFailures = [];
|
|
930
840
|
if (failures.length === 1) throw failures[0];
|
|
931
|
-
throw new AggregateError(failures, '
|
|
841
|
+
throw new AggregateError(failures, 'decide runtime emissions failed');
|
|
932
842
|
};
|
|
933
843
|
const drainBoundaryCallsAndEmissions = async (): Promise<void> => {
|
|
934
844
|
while (true) {
|
|
@@ -949,22 +859,95 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
949
859
|
};
|
|
950
860
|
const requirePorts = (): PlaybookPorts => {
|
|
951
861
|
if (!ports) {
|
|
952
|
-
throw new Error('
|
|
862
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
953
863
|
}
|
|
954
864
|
return ports;
|
|
955
865
|
};
|
|
956
866
|
const requireSessionIdentity = (): SessionIdentity => {
|
|
957
867
|
if (!sessionIdentity) {
|
|
958
|
-
throw new Error('
|
|
868
|
+
throw new Error('decide runtime: init(session) must be called first');
|
|
959
869
|
}
|
|
960
870
|
return sessionIdentity;
|
|
961
871
|
};
|
|
962
|
-
const
|
|
872
|
+
const selectPlayerResume = (playerId: string): string | false => {
|
|
873
|
+
const session = requireSessionIdentity();
|
|
874
|
+
const selected = session.playerSessions
|
|
875
|
+
? session.playerSessions.select(playerId)
|
|
876
|
+
: playerResumeTokens.get(playerId) ?? false;
|
|
877
|
+
if (
|
|
878
|
+
selected !== false &&
|
|
879
|
+
(typeof selected !== 'string' || selected.trim().length === 0)
|
|
880
|
+
) {
|
|
881
|
+
throw new TypeError(
|
|
882
|
+
`player session store returned an invalid resume token for ${playerId}`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return selected;
|
|
886
|
+
};
|
|
887
|
+
const updatePlayerResume = (
|
|
888
|
+
playerId: string,
|
|
889
|
+
resumeToken: string | undefined,
|
|
890
|
+
): void => {
|
|
891
|
+
const session = requireSessionIdentity();
|
|
892
|
+
if (session.playerSessions) {
|
|
893
|
+
session.playerSessions.update(playerId, resumeToken);
|
|
894
|
+
} else if (resumeToken !== undefined && resumeToken.trim().length > 0) {
|
|
895
|
+
playerResumeTokens.set(playerId, resumeToken);
|
|
896
|
+
} else {
|
|
897
|
+
playerResumeTokens.delete(playerId);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const snapshotPlayerResumeTokens = (): Record<string, string> => {
|
|
901
|
+
const session = requireSessionIdentity();
|
|
902
|
+
const captured = snapshotJsonValue(
|
|
903
|
+
session.playerSessions
|
|
904
|
+
? session.playerSessions.snapshot()
|
|
905
|
+
: Object.fromEntries(playerResumeTokens),
|
|
906
|
+
'player session store snapshot',
|
|
907
|
+
);
|
|
908
|
+
if (!isPlainObject(captured)) {
|
|
909
|
+
throw new TypeError('player session store snapshot must be an object');
|
|
910
|
+
}
|
|
911
|
+
const tokens: Record<string, string> = {};
|
|
912
|
+
for (const [playerId, token] of Object.entries(captured)) {
|
|
913
|
+
if (playerId.trim().length === 0) {
|
|
914
|
+
throw new TypeError(
|
|
915
|
+
'player session store snapshot player ids must be non-empty',
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
919
|
+
throw new TypeError(
|
|
920
|
+
`player session store snapshot token for ${playerId} must be a non-empty string`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
tokens[playerId] = token;
|
|
924
|
+
}
|
|
925
|
+
return tokens;
|
|
926
|
+
};
|
|
927
|
+
const restorePlayerResumeTokens = (
|
|
928
|
+
tokens: Readonly<Record<string, string>>,
|
|
929
|
+
): void => {
|
|
930
|
+
const session = requireSessionIdentity();
|
|
931
|
+
if (session.playerSessions) {
|
|
932
|
+
session.playerSessions.restore(tokens);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
playerResumeTokens.clear();
|
|
936
|
+
for (const [playerId, token] of Object.entries(tokens)) {
|
|
937
|
+
playerResumeTokens.set(playerId, token);
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const currentState = (
|
|
941
|
+
pendingCall: PlaybookPendingCall | undefined =
|
|
942
|
+
nestedBridge.getPendingCall(),
|
|
943
|
+
): PlaybookState => {
|
|
963
944
|
const live = actor;
|
|
964
945
|
if (!live) {
|
|
965
|
-
throw new Error('
|
|
946
|
+
throw new Error('decide runtime: actor is not initialized');
|
|
966
947
|
}
|
|
967
|
-
return normalizePlaybookSnapshot(live.getSnapshot()
|
|
948
|
+
return normalizePlaybookSnapshot(live.getSnapshot(), {
|
|
949
|
+
pendingCall,
|
|
950
|
+
});
|
|
968
951
|
};
|
|
969
952
|
const stateIdentity = (state: PlaybookState): { stateId?: string } => {
|
|
970
953
|
return state.stateId === undefined ? {} : { stateId: state.stateId };
|
|
@@ -1070,7 +1053,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1070
1053
|
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
1071
1054
|
};
|
|
1072
1055
|
const queued = await judgeQueue.add(async () => {
|
|
1073
|
-
// Keep the complete queue task pending until an active
|
|
1056
|
+
// Keep the complete queue task pending until an active coder promise
|
|
1074
1057
|
// settles. PQueue's signal option may reject add() while that task is
|
|
1075
1058
|
// still running, which would let the turn drain race a late finish.
|
|
1076
1059
|
signal.throwIfAborted();
|
|
@@ -1142,10 +1125,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1142
1125
|
`resolved player "${playerId}" already has an in-flight call`,
|
|
1143
1126
|
);
|
|
1144
1127
|
}
|
|
1145
|
-
inFlightPlayerIds.add(playerId);
|
|
1146
1128
|
const prompt = composePlayerPrompt(input);
|
|
1147
|
-
|
|
1148
|
-
|
|
1129
|
+
let resume: PlayerCallOptions['resume'];
|
|
1130
|
+
try {
|
|
1131
|
+
signal.throwIfAborted();
|
|
1132
|
+
resume = selectPlayerResume(playerId);
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
latchControlPlaneError(error, signal);
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1149
1137
|
const callId = `player-${++playerCallSequence}`;
|
|
1150
1138
|
const identity = {
|
|
1151
1139
|
purpose: 'captain',
|
|
@@ -1167,6 +1155,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1167
1155
|
},
|
|
1168
1156
|
{ turnId: currentTurnId, callId },
|
|
1169
1157
|
);
|
|
1158
|
+
inFlightPlayerIds.add(playerId);
|
|
1170
1159
|
try {
|
|
1171
1160
|
await emitCallStarted(
|
|
1172
1161
|
'player.call.started',
|
|
@@ -1183,13 +1172,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1183
1172
|
requirePorts().callPlayer(playerId, prompt, signal, { resume }),
|
|
1184
1173
|
);
|
|
1185
1174
|
rawResult = await boundary;
|
|
1186
|
-
// An XState sibling cancellation does not cancel an arbitrary
|
|
1175
|
+
// An XState sibling cancellation does not cancel an arbitrary coder
|
|
1187
1176
|
// promise. Re-check before a late resolution can mutate continuity or
|
|
1188
1177
|
// masquerade as a successful boundary finish.
|
|
1189
1178
|
signal.throwIfAborted();
|
|
1190
1179
|
} catch (error) {
|
|
1191
1180
|
// A rejected call produced no authoritative result, so the previous
|
|
1192
|
-
// token remains untouched. This also covers a
|
|
1181
|
+
// token remains untouched. This also covers a coder promise that
|
|
1193
1182
|
// resolves after its invocation signal was cancelled.
|
|
1194
1183
|
latchControlPlaneError(error, signal);
|
|
1195
1184
|
try {
|
|
@@ -1208,20 +1197,29 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1208
1197
|
try {
|
|
1209
1198
|
await emitFailure(error);
|
|
1210
1199
|
} catch {
|
|
1211
|
-
// The malformed
|
|
1200
|
+
// The malformed coder result remains authoritative.
|
|
1212
1201
|
}
|
|
1213
1202
|
throw error;
|
|
1214
1203
|
}
|
|
1215
1204
|
|
|
1216
1205
|
// The resolved result is authoritative even on aborted/error status.
|
|
1217
1206
|
// Update continuation state before interpreting that status.
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1207
|
+
try {
|
|
1208
|
+
updatePlayerResume(
|
|
1209
|
+
playerId,
|
|
1210
|
+
typeof result.resumeToken === 'string' &&
|
|
1211
|
+
result.resumeToken.trim().length > 0
|
|
1212
|
+
? result.resumeToken
|
|
1213
|
+
: undefined,
|
|
1214
|
+
);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
latchControlPlaneError(error, signal);
|
|
1217
|
+
try {
|
|
1218
|
+
await emitFailure(error);
|
|
1219
|
+
} catch {
|
|
1220
|
+
// The continuation-store failure remains authoritative.
|
|
1221
|
+
}
|
|
1222
|
+
throw error;
|
|
1225
1223
|
}
|
|
1226
1224
|
|
|
1227
1225
|
// Keep this finish outside the boundary catch. A trace sink can record
|
|
@@ -1309,6 +1307,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1309
1307
|
input.stateId,
|
|
1310
1308
|
),
|
|
1311
1309
|
input,
|
|
1310
|
+
finalText,
|
|
1312
1311
|
);
|
|
1313
1312
|
} catch (error) {
|
|
1314
1313
|
latchControlPlaneError(error, combined);
|
|
@@ -1317,89 +1316,162 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1317
1316
|
},
|
|
1318
1317
|
);
|
|
1319
1318
|
|
|
1320
|
-
|
|
1319
|
+
nestedBridge = createNestedPlaybookBridge<PlaybookInput>({
|
|
1320
|
+
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
1321
|
+
getBoundarySignal: () => currentSignal,
|
|
1322
|
+
callPlaybook: (request, signal) =>
|
|
1323
|
+
trackBoundaryCall(
|
|
1324
|
+
Promise.resolve(requirePorts().callPlaybook(request, signal)),
|
|
1325
|
+
),
|
|
1326
|
+
emitStarted: async (event) => {
|
|
1327
|
+
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
1328
|
+
await emitTrace(
|
|
1329
|
+
'playbook.call.started',
|
|
1330
|
+
{
|
|
1331
|
+
stateId: event.stateId,
|
|
1332
|
+
playbookId: event.playbookId,
|
|
1333
|
+
text: event.text,
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
1337
|
+
callId: event.callId,
|
|
1338
|
+
},
|
|
1339
|
+
);
|
|
1340
|
+
},
|
|
1341
|
+
emitFinished: async (event) => {
|
|
1342
|
+
const turnId = playbookCallTurnIds.get(event.callId);
|
|
1343
|
+
try {
|
|
1344
|
+
await emitTrace(
|
|
1345
|
+
'playbook.call.finished',
|
|
1346
|
+
{
|
|
1347
|
+
stateId: event.stateId,
|
|
1348
|
+
playbookId: event.playbookId,
|
|
1349
|
+
text: event.text,
|
|
1350
|
+
result: event.result,
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
1354
|
+
callId: event.callId,
|
|
1355
|
+
},
|
|
1356
|
+
);
|
|
1357
|
+
} finally {
|
|
1358
|
+
playbookCallTurnIds.delete(event.callId);
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
drain: flush,
|
|
1362
|
+
bindResumeSignal: (signal) => {
|
|
1363
|
+
currentSignal = signal;
|
|
1364
|
+
},
|
|
1365
|
+
onControlPlaneError: (error) => {
|
|
1366
|
+
const signal = currentSignal;
|
|
1367
|
+
if (!signal || !isAbortFailure(error, signal)) {
|
|
1368
|
+
controlPlaneError ??= error;
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
onBackgroundError: (error) => {
|
|
1372
|
+
collectFailure(emissionFailures, error);
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
const providedMachine = decideMachine.provide({
|
|
1377
|
+
actors: { player, playbook: nestedBridge.actorLogic },
|
|
1378
|
+
});
|
|
1321
1379
|
|
|
1322
1380
|
const inspect = (event: InspectionEvent): void => {
|
|
1323
1381
|
if (event.type !== '@xstate.snapshot') return;
|
|
1324
1382
|
if (actor === undefined || event.actorRef !== actor) return;
|
|
1325
1383
|
if (suppressInspectionEmissions) return;
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
fsmPayload,
|
|
1337
|
-
'described FSM telemetry',
|
|
1338
|
-
);
|
|
1384
|
+
try {
|
|
1385
|
+
const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
|
|
1386
|
+
const state = normalizePlaybookSnapshot(snapshot);
|
|
1387
|
+
const prior = previousState ?? state;
|
|
1388
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1389
|
+
const fsmPayload = telemetryPayload(prior, state, event.event, context);
|
|
1390
|
+
const describedFsmPayload = snapshotJsonValue(
|
|
1391
|
+
fsmPayload,
|
|
1392
|
+
'described FSM telemetry',
|
|
1393
|
+
);
|
|
1339
1394
|
|
|
1340
|
-
void enqueueTracedEmission(
|
|
1341
|
-
'fsm.transition',
|
|
1342
|
-
fsmPayload,
|
|
1343
|
-
{ turnId: currentTurnId },
|
|
1344
|
-
(emissionPorts) =>
|
|
1345
|
-
emissionPorts.emitTelemetry({
|
|
1346
|
-
topic: TELEMETRY_TOPIC,
|
|
1347
|
-
payload: describedFsmPayload,
|
|
1348
|
-
}),
|
|
1349
|
-
).catch(() => undefined);
|
|
1350
|
-
|
|
1351
|
-
const priorIds = new Set(prior?.activeStateIds ?? []);
|
|
1352
|
-
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1353
|
-
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1354
|
-
STATUS_STATE_IDS.has(stateId),
|
|
1355
|
-
);
|
|
1356
|
-
const scheduleStatus = (
|
|
1357
|
-
message: string,
|
|
1358
|
-
stateId: string,
|
|
1359
|
-
data?: JsonValue,
|
|
1360
|
-
): void => {
|
|
1361
|
-
const tracePayload = {
|
|
1362
|
-
...(bossRelevantStateIds.length === 1 ? { stateId } : {}),
|
|
1363
|
-
message,
|
|
1364
|
-
state,
|
|
1365
|
-
...(data !== undefined ? { data } : {}),
|
|
1366
|
-
};
|
|
1367
|
-
assertJsonSafe(tracePayload);
|
|
1368
1395
|
void enqueueTracedEmission(
|
|
1369
|
-
'
|
|
1370
|
-
|
|
1396
|
+
'fsm.transition',
|
|
1397
|
+
fsmPayload,
|
|
1371
1398
|
{ turnId: currentTurnId },
|
|
1372
|
-
(emissionPorts) =>
|
|
1399
|
+
(emissionPorts) =>
|
|
1400
|
+
emissionPorts.emitTelemetry({
|
|
1401
|
+
topic: TELEMETRY_TOPIC,
|
|
1402
|
+
payload: describedFsmPayload,
|
|
1403
|
+
}),
|
|
1373
1404
|
).catch(() => undefined);
|
|
1374
|
-
};
|
|
1375
1405
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1406
|
+
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
1407
|
+
previousState = state;
|
|
1408
|
+
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1409
|
+
const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
|
|
1410
|
+
STATUS_STATE_IDS.has(stateId),
|
|
1411
|
+
);
|
|
1412
|
+
const scheduleStatus = (
|
|
1413
|
+
message: string,
|
|
1414
|
+
stateId: string,
|
|
1415
|
+
data?: JsonValue,
|
|
1416
|
+
): void => {
|
|
1417
|
+
const tracePayload = {
|
|
1418
|
+
...(bossRelevantStateIds.length === 1 ? { stateId } : {}),
|
|
1419
|
+
message,
|
|
1420
|
+
state,
|
|
1421
|
+
...(data !== undefined ? { data } : {}),
|
|
1422
|
+
};
|
|
1423
|
+
assertJsonSafe(tracePayload);
|
|
1424
|
+
void enqueueTracedEmission(
|
|
1425
|
+
'status.emitted',
|
|
1426
|
+
tracePayload,
|
|
1427
|
+
{ turnId: currentTurnId },
|
|
1428
|
+
(emissionPorts) => emissionPorts.emitStatus(message, data),
|
|
1429
|
+
).catch(() => undefined);
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
for (const activeStateId of state.activeStateIds) {
|
|
1433
|
+
if (
|
|
1434
|
+
priorIds.has(activeStateId) ||
|
|
1435
|
+
!STATUS_STATE_IDS.has(activeStateId)
|
|
1436
|
+
) {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (WAIT_STATE_IDS.has(activeStateId)) {
|
|
1440
|
+
const pending = questionForWaitState(
|
|
1389
1441
|
activeStateId,
|
|
1442
|
+
pendingQuestions,
|
|
1390
1443
|
);
|
|
1444
|
+
if (pending) {
|
|
1445
|
+
scheduleStatus(
|
|
1446
|
+
`${pending.player} asks: ${pending.question}`,
|
|
1447
|
+
activeStateId,
|
|
1448
|
+
);
|
|
1449
|
+
scheduleStatus(
|
|
1450
|
+
`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.player} · ${pending.sourceItem}`,
|
|
1451
|
+
activeStateId,
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
continue;
|
|
1391
1455
|
}
|
|
1392
|
-
|
|
1456
|
+
const lastError =
|
|
1457
|
+
activeStateId === 'failed'
|
|
1458
|
+
? normalizeErrorCompact(context.lastError)
|
|
1459
|
+
: undefined;
|
|
1460
|
+
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
1461
|
+
if (description === undefined) continue;
|
|
1462
|
+
const playerState = PLAYER_STATES.find(
|
|
1463
|
+
(candidate) => candidate.stateId === activeStateId,
|
|
1464
|
+
);
|
|
1465
|
+
scheduleStatus(
|
|
1466
|
+
playerState === undefined
|
|
1467
|
+
? '◆ workflow failed; awaiting Boss recovery.'
|
|
1468
|
+
: `⤷ ${playerState.player}: ${description}`,
|
|
1469
|
+
activeStateId,
|
|
1470
|
+
lastError === undefined ? undefined : { lastError },
|
|
1471
|
+
);
|
|
1393
1472
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
? normalizeErrorCompact(context.lastError)
|
|
1397
|
-
: undefined;
|
|
1398
|
-
scheduleStatus(
|
|
1399
|
-
STATE_DESCRIPTIONS[activeStateId] ?? activeStateId,
|
|
1400
|
-
activeStateId,
|
|
1401
|
-
lastError === undefined ? undefined : { lastError },
|
|
1402
|
-
);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
latchInspectionError(error);
|
|
1403
1475
|
}
|
|
1404
1476
|
};
|
|
1405
1477
|
|
|
@@ -1413,7 +1485,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1413
1485
|
? {}
|
|
1414
1486
|
: {
|
|
1415
1487
|
snapshot: machineSnapshot as unknown as SnapshotFrom<
|
|
1416
|
-
typeof
|
|
1488
|
+
typeof decideMachine
|
|
1417
1489
|
>,
|
|
1418
1490
|
}),
|
|
1419
1491
|
inspect,
|
|
@@ -1442,20 +1514,31 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1442
1514
|
|
|
1443
1515
|
const driveToQuiescence = async (): Promise<void> => {
|
|
1444
1516
|
const live = actor;
|
|
1445
|
-
if (!live) throw new Error('
|
|
1446
|
-
await waitForPlaybookQuiescence(live);
|
|
1517
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1518
|
+
await waitForPlaybookQuiescence(live, { pendingCalls: nestedBridge });
|
|
1447
1519
|
};
|
|
1448
1520
|
|
|
1449
1521
|
const classify = async (
|
|
1450
1522
|
text: string,
|
|
1451
1523
|
signal: AbortSignal,
|
|
1452
|
-
): Promise<
|
|
1524
|
+
): Promise<DecideEvent | { type: 'NO_ACTION' } | null> => {
|
|
1453
1525
|
const live = actor;
|
|
1454
|
-
if (!live) throw new Error('
|
|
1455
|
-
const snapshot = live.getSnapshot() as SnapshotFrom<typeof
|
|
1456
|
-
const context = snapshot.context as Record<string, unknown>;
|
|
1457
|
-
const state = normalizePlaybookSnapshot(snapshot
|
|
1526
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1527
|
+
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
1528
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1529
|
+
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1530
|
+
pendingCall: nestedBridge.getPendingCall(),
|
|
1531
|
+
});
|
|
1458
1532
|
const pendingQuestions = pendingQuestionsFromContext(context);
|
|
1533
|
+
if (
|
|
1534
|
+
pendingQuestions.length === 0 &&
|
|
1535
|
+
(snapshot.status === 'done' ||
|
|
1536
|
+
state.activeStateIds.includes('ready') ||
|
|
1537
|
+
state.activeStateIds.includes('failed'))
|
|
1538
|
+
) {
|
|
1539
|
+
return { type: 'START_DECIDE', callerTopic: text };
|
|
1540
|
+
}
|
|
1541
|
+
if (pendingQuestions.length === 0) return null;
|
|
1459
1542
|
const prompt = buildClassifierPrompt(text, {
|
|
1460
1543
|
state,
|
|
1461
1544
|
pendingQuestions,
|
|
@@ -1468,16 +1551,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1468
1551
|
);
|
|
1469
1552
|
return parseClassification(
|
|
1470
1553
|
raw,
|
|
1554
|
+
text,
|
|
1471
1555
|
pendingQuestions.map(({ questionId }) => questionId),
|
|
1472
1556
|
);
|
|
1473
1557
|
};
|
|
1474
1558
|
|
|
1475
1559
|
const resultForSnapshot = (signal?: AbortSignal): PlaybookRunResult => {
|
|
1476
1560
|
const live = actor;
|
|
1477
|
-
if (!live) throw new Error('
|
|
1478
|
-
const snapshot = live.getSnapshot() as SnapshotFrom<typeof
|
|
1479
|
-
const
|
|
1480
|
-
const
|
|
1561
|
+
if (!live) throw new Error('decide runtime: actor is not initialized');
|
|
1562
|
+
const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
|
|
1563
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
1564
|
+
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
1565
|
+
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1481
1566
|
if (signal?.aborted) {
|
|
1482
1567
|
return {
|
|
1483
1568
|
outcome: 'aborted',
|
|
@@ -1504,7 +1589,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1504
1589
|
if (snapshot.status === 'error') {
|
|
1505
1590
|
throw (
|
|
1506
1591
|
(snapshot as { error?: unknown }).error ??
|
|
1507
|
-
new Error('
|
|
1592
|
+
new Error('decide runtime actor entered error status')
|
|
1508
1593
|
);
|
|
1509
1594
|
}
|
|
1510
1595
|
if (state.activeStateIds.includes('failed')) {
|
|
@@ -1515,6 +1600,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1515
1600
|
...(error === undefined ? {} : { error }),
|
|
1516
1601
|
};
|
|
1517
1602
|
}
|
|
1603
|
+
if (pendingCall) {
|
|
1604
|
+
return { outcome: 'suspended', state, pendingCall };
|
|
1605
|
+
}
|
|
1518
1606
|
return { outcome: 'quiescent', state };
|
|
1519
1607
|
};
|
|
1520
1608
|
|
|
@@ -1524,9 +1612,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1524
1612
|
// The caller rethrows its original failure. A restore failure skips
|
|
1525
1613
|
// the disposal trace — the parked session was never re-bound in this
|
|
1526
1614
|
// process, so its persisted snapshot stays authoritative (DR-014 §2).
|
|
1527
|
-
const cleanupFailedStart = async (
|
|
1528
|
-
|
|
1529
|
-
|
|
1615
|
+
const cleanupFailedStart = async (
|
|
1616
|
+
cause: unknown,
|
|
1617
|
+
options: { emitDisposal: boolean },
|
|
1618
|
+
): Promise<void> => {
|
|
1530
1619
|
let finalState: PlaybookState | undefined;
|
|
1531
1620
|
if (options.emitDisposal && actor) {
|
|
1532
1621
|
try {
|
|
@@ -1540,6 +1629,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1540
1629
|
} catch {
|
|
1541
1630
|
// Preserve the original startup failure.
|
|
1542
1631
|
}
|
|
1632
|
+
try {
|
|
1633
|
+
await nestedBridge.abortPending(cause);
|
|
1634
|
+
} catch {
|
|
1635
|
+
// Preserve the original startup failure.
|
|
1636
|
+
}
|
|
1543
1637
|
try {
|
|
1544
1638
|
await judgeQueue.onIdle();
|
|
1545
1639
|
await drainBoundaryCallsAndEmissions();
|
|
@@ -1577,6 +1671,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1577
1671
|
turnSequence = 0;
|
|
1578
1672
|
judgeCallSequence = 0;
|
|
1579
1673
|
playerCallSequence = 0;
|
|
1674
|
+
playbookCallSequence = 0;
|
|
1675
|
+
playbookCallTurnIds.clear();
|
|
1580
1676
|
lifecycleStarted = false;
|
|
1581
1677
|
};
|
|
1582
1678
|
|
|
@@ -1589,7 +1685,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1589
1685
|
disposalPromise !== undefined
|
|
1590
1686
|
) {
|
|
1591
1687
|
throw new Error(
|
|
1592
|
-
'
|
|
1688
|
+
'decide runtime: init(session) may only be called once',
|
|
1593
1689
|
);
|
|
1594
1690
|
}
|
|
1595
1691
|
const identity = snapshotPlaybookSession(session);
|
|
@@ -1612,7 +1708,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1612
1708
|
actor?.start();
|
|
1613
1709
|
await flush();
|
|
1614
1710
|
} catch (error) {
|
|
1615
|
-
await cleanupFailedStart({ emitDisposal: true });
|
|
1711
|
+
await cleanupFailedStart(error, { emitDisposal: true });
|
|
1616
1712
|
throw error;
|
|
1617
1713
|
} finally {
|
|
1618
1714
|
finishInitialization();
|
|
@@ -1620,11 +1716,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1620
1716
|
}
|
|
1621
1717
|
},
|
|
1622
1718
|
|
|
1623
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked
|
|
1719
|
+
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
1720
|
+
// session, including one already-started suspended REVIEW call.
|
|
1624
1721
|
// Defined only at a safe capture point — initialized, not disposing
|
|
1625
|
-
// or disposed, no active public boundary, and the actor quiescent
|
|
1626
|
-
//
|
|
1627
|
-
// so no pending-call guard applies.
|
|
1722
|
+
// or disposed, no active public boundary, and the actor quiescent with
|
|
1723
|
+
// status `active`.
|
|
1628
1724
|
exportSnapshot(): PlaybookRuntimeSnapshot | undefined {
|
|
1629
1725
|
if (
|
|
1630
1726
|
!actor ||
|
|
@@ -1637,25 +1733,56 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1637
1733
|
if (currentTurnId !== undefined || currentSignal !== undefined) {
|
|
1638
1734
|
return undefined;
|
|
1639
1735
|
}
|
|
1736
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
1737
|
+
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
1738
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
1739
|
+
return undefined;
|
|
1740
|
+
}
|
|
1741
|
+
if (
|
|
1742
|
+
pendingCall !== undefined &&
|
|
1743
|
+
bridgeSuspendedCall !== undefined &&
|
|
1744
|
+
(pendingCall.callId !== bridgeSuspendedCall.callId ||
|
|
1745
|
+
pendingCall.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
1746
|
+
pendingCall.childSessionId !== bridgeSuspendedCall.childSessionId)
|
|
1747
|
+
) {
|
|
1748
|
+
return undefined;
|
|
1749
|
+
}
|
|
1750
|
+
let suspendedCall: typeof bridgeSuspendedCall;
|
|
1751
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
1752
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
1753
|
+
return undefined;
|
|
1754
|
+
}
|
|
1755
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
1756
|
+
if (
|
|
1757
|
+
bridgeSuspendedCall.turnId !== undefined &&
|
|
1758
|
+
bridgeSuspendedCall.turnId !== turnId
|
|
1759
|
+
) {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
suspendedCall = {
|
|
1763
|
+
...bridgeSuspendedCall,
|
|
1764
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1640
1767
|
const state = currentState();
|
|
1641
1768
|
if (state.status !== 'active' || !state.quiescent) return undefined;
|
|
1642
1769
|
const machine = detachPersistedMachineSnapshot(
|
|
1643
1770
|
actor.getPersistedSnapshot(),
|
|
1644
1771
|
);
|
|
1645
1772
|
const context = (
|
|
1646
|
-
actor.getSnapshot() as SnapshotFrom<typeof
|
|
1647
|
-
).context as Record<string, unknown>;
|
|
1773
|
+
actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
1774
|
+
).context as unknown as Record<string, unknown>;
|
|
1648
1775
|
return {
|
|
1649
|
-
schemaVersion:
|
|
1776
|
+
schemaVersion: 2,
|
|
1650
1777
|
playbookId: sessionIdentity.playbookId,
|
|
1651
1778
|
machine,
|
|
1652
|
-
playerResumeTokens:
|
|
1779
|
+
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
1653
1780
|
sequences: {
|
|
1654
1781
|
trace: traceSequence,
|
|
1655
1782
|
turn: turnSequence,
|
|
1656
1783
|
judgeCall: judgeCallSequence,
|
|
1657
1784
|
playerCall: playerCallSequence,
|
|
1658
|
-
playbookCall:
|
|
1785
|
+
playbookCall: playbookCallSequence,
|
|
1659
1786
|
},
|
|
1660
1787
|
state,
|
|
1661
1788
|
pendingBossQuestions: pendingQuestionsFromContext(context).map(
|
|
@@ -1666,6 +1793,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1666
1793
|
sourceItem: pending.sourceItem,
|
|
1667
1794
|
}),
|
|
1668
1795
|
),
|
|
1796
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
1669
1797
|
};
|
|
1670
1798
|
},
|
|
1671
1799
|
|
|
@@ -1685,14 +1813,19 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1685
1813
|
disposalPromise !== undefined
|
|
1686
1814
|
) {
|
|
1687
1815
|
throw new Error(
|
|
1688
|
-
'
|
|
1816
|
+
'decide runtime: restore(session, snapshot) may only be called once',
|
|
1689
1817
|
);
|
|
1690
1818
|
}
|
|
1691
1819
|
const identity = snapshotPlaybookSession(session);
|
|
1692
1820
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(
|
|
1693
1821
|
snapshot,
|
|
1694
1822
|
identity.playbookId,
|
|
1823
|
+
{ allowSuspendedCall: true },
|
|
1695
1824
|
);
|
|
1825
|
+
const suspendedCall =
|
|
1826
|
+
boundSnapshot.schemaVersion === 2
|
|
1827
|
+
? boundSnapshot.suspendedCall
|
|
1828
|
+
: undefined;
|
|
1696
1829
|
let finishInitialization!: () => void;
|
|
1697
1830
|
const initialization = new Promise<void>((resolve) => {
|
|
1698
1831
|
finishInitialization = resolve;
|
|
@@ -1701,32 +1834,61 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1701
1834
|
lifecycleStarted = true;
|
|
1702
1835
|
ports = identity.ports;
|
|
1703
1836
|
sessionIdentity = identity;
|
|
1837
|
+
let priorExternalPlayerTokens:
|
|
1838
|
+
| Readonly<Record<string, string>>
|
|
1839
|
+
| undefined;
|
|
1704
1840
|
try {
|
|
1705
1841
|
traceSequence = boundSnapshot.sequences.trace;
|
|
1706
1842
|
turnSequence = boundSnapshot.sequences.turn;
|
|
1707
1843
|
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1708
1844
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1845
|
+
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
1846
|
+
if (identity.playerSessions) {
|
|
1847
|
+
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
1848
|
+
}
|
|
1849
|
+
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
1850
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
1851
|
+
if (suspendedCall !== undefined) {
|
|
1852
|
+
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
1714
1853
|
}
|
|
1715
1854
|
suppressInspectionEmissions = true;
|
|
1716
1855
|
createRuntimeActor(boundSnapshot.machine);
|
|
1717
1856
|
actor?.start();
|
|
1718
|
-
const restoredState = currentState();
|
|
1857
|
+
const restoredState = currentState(suspendedCall);
|
|
1719
1858
|
if (restoredState.status !== 'active') {
|
|
1720
1859
|
throw new Error(
|
|
1721
|
-
`
|
|
1860
|
+
`decide runtime: restored actor status is ${restoredState.status}, expected active`,
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
if (
|
|
1864
|
+
stableJson(restoredState, 'restored runtime state') !==
|
|
1865
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')
|
|
1866
|
+
) {
|
|
1867
|
+
throw new Error(
|
|
1868
|
+
'decide runtime: restored actor state does not match snapshot state',
|
|
1722
1869
|
);
|
|
1723
1870
|
}
|
|
1724
|
-
suppressInspectionEmissions = false;
|
|
1725
1871
|
previousState = restoredState;
|
|
1726
1872
|
await flush();
|
|
1873
|
+
suppressInspectionEmissions = false;
|
|
1874
|
+
// Final fallible step: after this publication the authoritative
|
|
1875
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
1876
|
+
// restore validation may trigger failed-start rollback.
|
|
1877
|
+
nestedBridge.confirmRestore();
|
|
1727
1878
|
} catch (error) {
|
|
1728
|
-
|
|
1729
|
-
|
|
1879
|
+
let failure = error;
|
|
1880
|
+
if (priorExternalPlayerTokens !== undefined) {
|
|
1881
|
+
try {
|
|
1882
|
+
identity.playerSessions!.restore(priorExternalPlayerTokens);
|
|
1883
|
+
} catch (rollbackError) {
|
|
1884
|
+
failure = new AggregateError(
|
|
1885
|
+
[error, rollbackError],
|
|
1886
|
+
'DECIDE restore and player continuation rollback failed',
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
1891
|
+
throw failure;
|
|
1730
1892
|
} finally {
|
|
1731
1893
|
finishInitialization();
|
|
1732
1894
|
if (initInFlight === initialization) initInFlight = undefined;
|
|
@@ -1738,16 +1900,16 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1738
1900
|
signal: AbortSignal;
|
|
1739
1901
|
}): Promise<PlaybookRunResult> {
|
|
1740
1902
|
if (disposalPromise !== undefined) {
|
|
1741
|
-
throw new Error('
|
|
1903
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
1742
1904
|
}
|
|
1743
1905
|
requirePorts();
|
|
1744
1906
|
if (!actor) {
|
|
1745
1907
|
throw new Error(
|
|
1746
|
-
'
|
|
1908
|
+
'decide runtime: init(session) must be called before handleBossInput',
|
|
1747
1909
|
);
|
|
1748
1910
|
}
|
|
1749
|
-
if (
|
|
1750
|
-
throw new Error('
|
|
1911
|
+
if (currentSignal !== undefined) {
|
|
1912
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
1751
1913
|
}
|
|
1752
1914
|
|
|
1753
1915
|
const turnId = ++turnSequence;
|
|
@@ -1768,6 +1930,8 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1768
1930
|
const state = currentState();
|
|
1769
1931
|
await emitBoundaryStatus('No playbook action classified.', state);
|
|
1770
1932
|
result = { outcome: 'no-action', state };
|
|
1933
|
+
} else if (event.type === 'NO_ACTION') {
|
|
1934
|
+
result = { outcome: 'no-action', state: currentState() };
|
|
1771
1935
|
} else {
|
|
1772
1936
|
await emitBoundaryStatus(event.type, currentState());
|
|
1773
1937
|
if (actor.getSnapshot().status === 'done') {
|
|
@@ -1852,36 +2016,77 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1852
2016
|
|
|
1853
2017
|
if (failures.length === 1) throw failures[0];
|
|
1854
2018
|
if (failures.length > 1) {
|
|
1855
|
-
throw new AggregateError(failures, '
|
|
2019
|
+
throw new AggregateError(failures, 'decide runtime turn failed');
|
|
1856
2020
|
}
|
|
1857
2021
|
return result;
|
|
1858
2022
|
},
|
|
1859
2023
|
|
|
1860
2024
|
async resumePlaybookCall({
|
|
1861
2025
|
callId,
|
|
2026
|
+
result: childResult,
|
|
2027
|
+
signal,
|
|
1862
2028
|
}: {
|
|
1863
2029
|
callId: string;
|
|
1864
2030
|
result: PlaybookCallResult;
|
|
1865
2031
|
signal: AbortSignal;
|
|
1866
2032
|
}): Promise<PlaybookRunResult> {
|
|
1867
2033
|
if (disposalPromise !== undefined) {
|
|
1868
|
-
throw new Error('
|
|
2034
|
+
throw new Error('decide runtime: runtime is disposing or disposed');
|
|
1869
2035
|
}
|
|
1870
2036
|
requirePorts();
|
|
1871
2037
|
if (!actor) {
|
|
1872
2038
|
throw new Error(
|
|
1873
|
-
'
|
|
2039
|
+
'decide runtime: init(session) must be called before resumePlaybookCall',
|
|
1874
2040
|
);
|
|
1875
2041
|
}
|
|
1876
|
-
|
|
2042
|
+
if (currentSignal !== undefined) {
|
|
2043
|
+
throw new Error('decide runtime: another runtime turn is active');
|
|
2044
|
+
}
|
|
2045
|
+
currentTurnId = playbookCallTurnIds.get(callId);
|
|
2046
|
+
currentSignal = signal;
|
|
2047
|
+
controlPlaneError = undefined;
|
|
2048
|
+
let runResult: PlaybookRunResult | undefined;
|
|
2049
|
+
let operationError: unknown;
|
|
2050
|
+
try {
|
|
2051
|
+
await nestedBridge.resume({
|
|
2052
|
+
callId,
|
|
2053
|
+
result: childResult,
|
|
2054
|
+
signal,
|
|
2055
|
+
});
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
operationError = error;
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
await waitForPlaybookQuiescence(actor, {
|
|
2061
|
+
pendingCalls: nestedBridge,
|
|
2062
|
+
});
|
|
2063
|
+
runResult = resultForSnapshot(signal);
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
operationError ??= error;
|
|
2066
|
+
}
|
|
2067
|
+
let drainError: unknown;
|
|
2068
|
+
try {
|
|
2069
|
+
await drainBoundaryCallsAndEmissions();
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
drainError = error;
|
|
2072
|
+
}
|
|
2073
|
+
const failure = controlPlaneError ?? drainError ?? operationError;
|
|
2074
|
+
currentSignal = undefined;
|
|
2075
|
+
currentTurnId = undefined;
|
|
2076
|
+
controlPlaneError = undefined;
|
|
2077
|
+
if (failure !== undefined) throw failure;
|
|
2078
|
+
if (runResult === undefined) {
|
|
2079
|
+
throw new Error('decide runtime: playbook resume produced no result');
|
|
2080
|
+
}
|
|
2081
|
+
return runResult;
|
|
1877
2082
|
},
|
|
1878
2083
|
|
|
1879
2084
|
dispose(): Promise<void> {
|
|
1880
2085
|
if (disposalPromise !== undefined) return disposalPromise;
|
|
1881
|
-
if (
|
|
2086
|
+
if (currentSignal !== undefined) {
|
|
1882
2087
|
return Promise.reject(
|
|
1883
2088
|
new Error(
|
|
1884
|
-
'
|
|
2089
|
+
'decide runtime: cannot dispose while a runtime turn is active',
|
|
1885
2090
|
),
|
|
1886
2091
|
);
|
|
1887
2092
|
}
|
|
@@ -1895,6 +2100,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1895
2100
|
const finalState = currentState();
|
|
1896
2101
|
const failures: unknown[] = [];
|
|
1897
2102
|
stopActor();
|
|
2103
|
+
try {
|
|
2104
|
+
await nestedBridge.dispose();
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
collectFailure(failures, error);
|
|
2107
|
+
}
|
|
1898
2108
|
try {
|
|
1899
2109
|
await drainBoundaryCallsAndEmissions();
|
|
1900
2110
|
} catch (error) {
|
|
@@ -1914,6 +2124,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1914
2124
|
collectFailure(failures, error);
|
|
1915
2125
|
} finally {
|
|
1916
2126
|
playerResumeTokens.clear();
|
|
2127
|
+
playbookCallTurnIds.clear();
|
|
1917
2128
|
inFlightPlayerIds.clear();
|
|
1918
2129
|
activeBoundaryCalls.clear();
|
|
1919
2130
|
activeEmissionCalls.clear();
|
|
@@ -1930,7 +2141,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1930
2141
|
}
|
|
1931
2142
|
if (failures.length === 1) throw failures[0];
|
|
1932
2143
|
if (failures.length > 1) {
|
|
1933
|
-
throw new AggregateError(failures, '
|
|
2144
|
+
throw new AggregateError(failures, 'decide runtime disposal failed');
|
|
1934
2145
|
}
|
|
1935
2146
|
})();
|
|
1936
2147
|
return disposalPromise;
|
|
@@ -1952,10 +2163,10 @@ export const _internal = {
|
|
|
1952
2163
|
normalizeErrorCompact,
|
|
1953
2164
|
normalizeErrorFull,
|
|
1954
2165
|
DEFAULT_PLAYER_BINDING,
|
|
1955
|
-
ALIAS_RESOLUTION,
|
|
1956
2166
|
STATE_DESCRIPTIONS,
|
|
1957
|
-
|
|
1958
|
-
|
|
2167
|
+
PLAYER_STATES,
|
|
2168
|
+
PLAYER_STATE_IDS,
|
|
2169
|
+
VERBATIM_PAYLOAD_FIELDS,
|
|
1959
2170
|
BOSS_INTERRUPT_TARGETS,
|
|
1960
2171
|
CONTINUATION_PREAMBLE,
|
|
1961
2172
|
TELEMETRY_TOPIC,
|