@sublang/playbook 1.0.0 → 1.3.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 +8 -1
- package/package.json +5 -1
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/playbook.js +6 -2
- package/reference/sdlc/code.playbook/bin/run.js +110 -4
- package/reference/sdlc/code.playbook/code.playbook.d.ts +4 -16
- package/reference/sdlc/code.playbook/code.playbook.js +88 -1272
- package/reference/sdlc/code.playbook/code.playbook.ts +127 -1547
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +11 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +1 -1
- package/slc/link.md +95 -36
- package/slc/optimize.md +7 -3
- package/slc/text2gears.md +8 -2
- package/src/runtime.d.ts +1 -0
- package/src/runtime.ts +1 -0
- package/src/xstate-playbook-runtime.d.ts +201 -0
- package/src/xstate-playbook-runtime.js +2058 -0
- package/src/xstate-playbook-runtime.ts +2792 -0
- package/src/xstate-runtime.d.ts +1 -0
- package/src/xstate-runtime.js +11 -0
- package/src/xstate-runtime.ts +14 -0
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
// PlaybookRuntime imported and re-exported from
|
|
16
16
|
// @sublang/playbook/runtime
|
|
17
17
|
// (slc/link.md §Output, DR-004 Addendum A4)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Runtime: the shared createXStatePlaybookRuntime factory from
|
|
19
|
+
// @sublang/playbook/xstate-runtime interprets the FSM
|
|
20
|
+
// (slc/link.md §Output, DR-019); this module carries only
|
|
21
|
+
// the CODE-specific spec — options validation, player
|
|
22
|
+
// binding, prompt composition, Boss-event classification,
|
|
23
|
+
// and Captain-pane status formatting.
|
|
24
|
+
import { createPlayerBridge, createXStatePlaybookRuntime, adjudicatePlayerOutput, normalizeError, normalizeErrorCompact, normalizeErrorFull, parseJudgeJson, snapshotJsonValue, } from '../../../src/xstate-runtime.js';
|
|
21
25
|
import { codingMachine, } from './code.fsm.js';
|
|
22
26
|
import { enumerateAwaitBossReply, enumerateCaptainStates, enumerateRootEvents, } from './code.fsm.introspect.js';
|
|
23
27
|
function snapshotCodePlaybookOptions(value) {
|
|
@@ -45,10 +49,6 @@ function snapshotCodePlaybookOptions(value) {
|
|
|
45
49
|
}
|
|
46
50
|
return captured;
|
|
47
51
|
}
|
|
48
|
-
const BOSS_REPLY_ERRORS = {
|
|
49
|
-
missingQuestion: "needsBossReply outcome missing 'question' field",
|
|
50
|
-
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
51
|
-
};
|
|
52
52
|
// Required-payload fields whose value is the player's verbatim long-form
|
|
53
53
|
// prose. The runtime carries `finalText.trim()` into these fields rather
|
|
54
54
|
// than asking the judge to round-trip the text through JSON. Short
|
|
@@ -58,27 +58,6 @@ const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
|
58
58
|
'reviews',
|
|
59
59
|
'challenges',
|
|
60
60
|
]);
|
|
61
|
-
// Normalize an unknown error value to the compact `{ name, message }`
|
|
62
|
-
// shape used by Captain-pane / status emissions. Returns `undefined`
|
|
63
|
-
// for nullish input so callers can omit absent errors.
|
|
64
|
-
function normalizeErrorCompact(err) {
|
|
65
|
-
if (err === undefined || err === null)
|
|
66
|
-
return undefined;
|
|
67
|
-
const normalized = normalizeError(err);
|
|
68
|
-
return { name: normalized.name, message: normalized.message };
|
|
69
|
-
}
|
|
70
|
-
// Normalize an unknown error value to the full `{ name, message, stack }`
|
|
71
|
-
// shape used by telemetry emissions. Returns `undefined` for nullish
|
|
72
|
-
// input. `stack` is omitted when not available on the source value.
|
|
73
|
-
function normalizeErrorFull(err) {
|
|
74
|
-
if (err === undefined || err === null)
|
|
75
|
-
return undefined;
|
|
76
|
-
return normalizeError(err);
|
|
77
|
-
}
|
|
78
|
-
function isAbortFailure(error, signal) {
|
|
79
|
-
return (signal.aborted &&
|
|
80
|
-
(error === signal.reason || normalizeError(error).name === 'AbortError'));
|
|
81
|
-
}
|
|
82
61
|
// Normalize any `error` field inside a telemetry event so failed
|
|
83
62
|
// transitions don't leak raw Error instances through the channel.
|
|
84
63
|
function normalizeEventForTelemetry(event) {
|
|
@@ -120,9 +99,6 @@ function normalizeEventValue(value, path, ancestors) {
|
|
|
120
99
|
}
|
|
121
100
|
return snapshotJsonValue(normalized, path);
|
|
122
101
|
}
|
|
123
|
-
// Internal capabilities (DR-004 §10). Each ships with its final
|
|
124
|
-
// signature; behavior lands in the per-capability task noted by the
|
|
125
|
-
// TODO marker.
|
|
126
102
|
// Player-prompt composer — DR-004 §6.
|
|
127
103
|
// Substitutes the three placeholder tokens in `input.prompt` (literal
|
|
128
104
|
// string replace, no escaping) and arranges labelled blocks around
|
|
@@ -203,54 +179,6 @@ function resolvePlayerId(input) {
|
|
|
203
179
|
}
|
|
204
180
|
}
|
|
205
181
|
}
|
|
206
|
-
// LLM judge — DR-004 §4. Builds a prompt that lists each declared
|
|
207
|
-
// outcome verbatim, asks ports.callJudge for a JSON
|
|
208
|
-
// `{ guard, …payloadFields }` response, and returns the parsed
|
|
209
|
-
// object once the chosen guard is one of the input.result keys.
|
|
210
|
-
// Adjudicator failures (malformed JSON, missing/unknown guard) are
|
|
211
|
-
// control-plane errors and propagate via throw per slc/link.md.
|
|
212
|
-
async function adjudicate(input, finalText, ports, signal, boundary) {
|
|
213
|
-
const prompt = buildJudgePrompt(input, finalText);
|
|
214
|
-
const raw = boundary
|
|
215
|
-
? await boundary.callJudge('player-output-adjudication', input.stateId, prompt, signal)
|
|
216
|
-
: await ports.callJudge(prompt, signal);
|
|
217
|
-
const parsed = parseJudgeJson(raw);
|
|
218
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
219
|
-
throw new Error('adjudicate: judge response is not a JSON object');
|
|
220
|
-
}
|
|
221
|
-
const obj = parsed;
|
|
222
|
-
const guard = obj.guard;
|
|
223
|
-
if (typeof guard !== 'string') {
|
|
224
|
-
throw new Error('adjudicate: judge response missing string "guard" field');
|
|
225
|
-
}
|
|
226
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
227
|
-
throw new Error(`adjudicate: unknown guard "${guard}" — declared guards: ${Object.keys(input.result).join(', ')}`);
|
|
228
|
-
}
|
|
229
|
-
// Per slc/link.md, a missing payload field the state's `result`
|
|
230
|
-
// description requires is a control-plane error. The FSM names
|
|
231
|
-
// required fields with the literal phrase
|
|
232
|
-
// Output shall include `<fieldName>: <...>`
|
|
233
|
-
// so we extract those tokens and require each to be a string in
|
|
234
|
-
// the judge response — except for VERBATIM_PAYLOAD_FIELDS
|
|
235
|
-
// (`reviews`, `challenges`), where the runtime substitutes
|
|
236
|
-
// `finalText.trim()` so the long-form prose is not round-tripped
|
|
237
|
-
// through judge JSON. Short extracted fields like `question` and
|
|
238
|
-
// `taskDescription` keep the existing extract-and-validate path.
|
|
239
|
-
const verbatim = finalText.trim();
|
|
240
|
-
for (const field of extractRequiredFields(input.result[guard])) {
|
|
241
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
242
|
-
obj[field] = verbatim;
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
if (typeof obj[field] !== 'string') {
|
|
246
|
-
if (guard === 'needsBossReply' && field === 'question') {
|
|
247
|
-
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
248
|
-
}
|
|
249
|
-
throw new Error(`adjudicate: judge response missing required field "${field}" for guard "${guard}"`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return obj;
|
|
253
|
-
}
|
|
254
182
|
function extractRequiredFields(description) {
|
|
255
183
|
const fields = [];
|
|
256
184
|
const re = /Output shall include `([A-Za-z_][A-Za-z0-9_]*):/g;
|
|
@@ -279,145 +207,23 @@ function buildJudgePrompt(input, finalText) {
|
|
|
279
207
|
}
|
|
280
208
|
return lines.join('\n');
|
|
281
209
|
}
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// the
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// own object check. Only a reply from which no JSON value can be
|
|
300
|
-
// recovered is treated as malformed and throws, preserving the
|
|
301
|
-
// control-plane error contract (PBRT-7, PBRT-10).
|
|
302
|
-
function parseJudgeJson(raw) {
|
|
303
|
-
const fenced = stripCodeFence(raw.trim());
|
|
304
|
-
// Fast path: a well-formed (optionally fenced) JSON body.
|
|
305
|
-
try {
|
|
306
|
-
return JSON.parse(fenced);
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// Fall through to lenient extraction + repair.
|
|
310
|
-
}
|
|
311
|
-
const starts = [];
|
|
312
|
-
for (let i = 0; i < fenced.length; i++) {
|
|
313
|
-
const ch = fenced[i];
|
|
314
|
-
if (ch === '{' || ch === '[')
|
|
315
|
-
starts.push(i);
|
|
316
|
-
}
|
|
317
|
-
// Walk starts in document order. At each start prefer a strict
|
|
318
|
-
// balanced span (most trustworthy) and fall back to a repaired one
|
|
319
|
-
// for a trailing-comma / truncated tail, so the earliest intended
|
|
320
|
-
// object wins even when it needs repair. Return the first plain
|
|
321
|
-
// object; remember the first value of any shape so a legitimately
|
|
322
|
-
// array/scalar reply still surfaces to the caller's own object check.
|
|
323
|
-
let firstValue;
|
|
324
|
-
for (const start of starts) {
|
|
325
|
-
let parsedHere;
|
|
326
|
-
for (const repair of [false, true]) {
|
|
327
|
-
const candidate = extractJsonValue(fenced, start, repair);
|
|
328
|
-
if (candidate === undefined)
|
|
329
|
-
continue;
|
|
330
|
-
try {
|
|
331
|
-
parsedHere = { value: JSON.parse(candidate) };
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
continue; // not parseable this way — try repair, then next start
|
|
335
|
-
}
|
|
336
|
-
break; // prefer the strict span at this start over its repair
|
|
337
|
-
}
|
|
338
|
-
if (parsedHere === undefined)
|
|
339
|
-
continue;
|
|
340
|
-
if (isPlainObject(parsedHere.value))
|
|
341
|
-
return parsedHere.value;
|
|
342
|
-
if (firstValue === undefined)
|
|
343
|
-
firstValue = parsedHere;
|
|
344
|
-
}
|
|
345
|
-
if (firstValue !== undefined)
|
|
346
|
-
return firstValue.value;
|
|
347
|
-
throw new Error('adjudicate: judge response is not valid JSON');
|
|
348
|
-
}
|
|
349
|
-
function isPlainObject(value) {
|
|
350
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
351
|
-
}
|
|
352
|
-
// Strip a single Markdown code fence that wraps the whole string.
|
|
353
|
-
function stripCodeFence(text) {
|
|
354
|
-
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
355
|
-
return fence ? fence[1].trim() : text;
|
|
356
|
-
}
|
|
357
|
-
// Scan from `start` (a `{`/`[` index), tracking string and
|
|
358
|
-
// bracket-nesting state, and emit the balanced JSON value rooted
|
|
359
|
-
// there. Anything after the top-level value closes is ignored (so a
|
|
360
|
-
// trailing code fence or commentary does not matter). With
|
|
361
|
-
// `repair === false` the span is returned only if it actually closes,
|
|
362
|
-
// and trailing commas are left intact — so the caller can prefer a
|
|
363
|
-
// cleanly-balanced span before attempting repair; if input ends
|
|
364
|
-
// before the value closes, undefined is returned. With
|
|
365
|
-
// `repair === true` common damage is fixed: a trailing comma before a
|
|
366
|
-
// close is removed, an unterminated string is closed, and any
|
|
367
|
-
// brackets still open at end-of-input are closed in order.
|
|
368
|
-
function extractJsonValue(text, start, repair) {
|
|
369
|
-
const stack = [];
|
|
370
|
-
let out = '';
|
|
371
|
-
let inString = false;
|
|
372
|
-
let escaped = false;
|
|
373
|
-
for (let i = start; i < text.length; i++) {
|
|
374
|
-
const ch = text[i];
|
|
375
|
-
if (inString) {
|
|
376
|
-
out += ch;
|
|
377
|
-
if (escaped)
|
|
378
|
-
escaped = false;
|
|
379
|
-
else if (ch === '\\')
|
|
380
|
-
escaped = true;
|
|
381
|
-
else if (ch === '"')
|
|
382
|
-
inString = false;
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (ch === '"') {
|
|
386
|
-
inString = true;
|
|
387
|
-
out += ch;
|
|
388
|
-
continue;
|
|
389
|
-
}
|
|
390
|
-
if (ch === '{' || ch === '[') {
|
|
391
|
-
stack.push(ch === '{' ? '}' : ']');
|
|
392
|
-
out += ch;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
if (ch === '}' || ch === ']') {
|
|
396
|
-
if (repair)
|
|
397
|
-
out = dropTrailingComma(out);
|
|
398
|
-
out += ch;
|
|
399
|
-
stack.pop();
|
|
400
|
-
if (stack.length === 0)
|
|
401
|
-
return out; // top-level value complete
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
out += ch;
|
|
405
|
-
}
|
|
406
|
-
// End of input before the top-level value closed.
|
|
407
|
-
if (!repair)
|
|
408
|
-
return undefined; // strict pass: no balanced span here
|
|
409
|
-
if (inString)
|
|
410
|
-
out += '"';
|
|
411
|
-
out = dropTrailingComma(out);
|
|
412
|
-
while (stack.length > 0)
|
|
413
|
-
out += stack.pop();
|
|
414
|
-
return out;
|
|
415
|
-
}
|
|
416
|
-
// Remove a trailing comma (and any whitespace after it) at the end of
|
|
417
|
-
// the accumulated output, so `{"a":1,}` / `[1,2,]` and truncated
|
|
418
|
-
// `{"a":1,` repair to valid JSON.
|
|
419
|
-
function dropTrailingComma(out) {
|
|
420
|
-
return out.replace(/,(\s*)$/, '$1');
|
|
210
|
+
// CODE-specific adjudication strategy: the CODE judge prompt above, the
|
|
211
|
+
// DR-004 `Output shall include` required-field extraction, and the
|
|
212
|
+
// verbatim long-form payload fields.
|
|
213
|
+
const CODE_ADJUDICATION = {
|
|
214
|
+
buildJudgePrompt: (input, finalText) => buildJudgePrompt(input, finalText),
|
|
215
|
+
extractRequiredFields,
|
|
216
|
+
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
217
|
+
};
|
|
218
|
+
// LLM judge — DR-004 §4. Delegates to the shared adjudicator with the
|
|
219
|
+
// CODE strategy: it lists each declared outcome verbatim, asks
|
|
220
|
+
// ports.callJudge for a JSON `{ guard, …payloadFields }` response, and
|
|
221
|
+
// returns the parsed object once the chosen guard is one of the
|
|
222
|
+
// input.result keys. Adjudicator failures (malformed JSON,
|
|
223
|
+
// missing/unknown guard) are control-plane errors and propagate via
|
|
224
|
+
// throw per slc/link.md.
|
|
225
|
+
async function adjudicate(input, finalText, ports, signal, boundary) {
|
|
226
|
+
return (await adjudicatePlayerOutput(CODE_ADJUDICATION, input, finalText, ports, signal, boundary));
|
|
421
227
|
}
|
|
422
228
|
// Boss-event classifier — DR-004 §3.
|
|
423
229
|
// Every non-empty Boss turn goes through ports.callJudge. Slash-prefixed
|
|
@@ -595,11 +401,12 @@ function buildClassifierPrompt(text, state) {
|
|
|
595
401
|
return lines.join('\n');
|
|
596
402
|
}
|
|
597
403
|
// Delegated-player actor bridge — DR-004 §7. One PromiseActorLogic that the
|
|
598
|
-
// codingMachine invokes from every player-invoking state
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
404
|
+
// codingMachine invokes from every player-invoking state, built by the
|
|
405
|
+
// shared createPlayerBridge with the CODE binding, composer, and
|
|
406
|
+
// adjudication strategy. Per turn: resolve playerId, compose the player
|
|
407
|
+
// prompt, await ports.callPlayer, adjudicate the finalText. PlayerResult
|
|
408
|
+
// status of 'aborted' or 'error' throws so XState routes via onError →
|
|
409
|
+
// #failed (the single fail-stop sink for both Captain errors and player
|
|
603
410
|
// failures). Captain remains the orchestrator and adjudicator; it is not
|
|
604
411
|
// encoded as the delegated FSM actor.
|
|
605
412
|
//
|
|
@@ -609,31 +416,12 @@ function buildClassifierPrompt(text, state) {
|
|
|
609
416
|
// on actor.stop(), not on Boss abort. When omitted (e.g. direct
|
|
610
417
|
// captainBridge tests), the bridge falls back to XState's signal.
|
|
611
418
|
function captainBridge(ports, getActiveSignal, boundary, onControlPlaneError) {
|
|
612
|
-
return
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
: await ports.callPlayer(playerId, prompt, activeSignal, {
|
|
619
|
-
resume: false,
|
|
620
|
-
});
|
|
621
|
-
if (result.status !== 'ok') {
|
|
622
|
-
throw new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
|
|
623
|
-
}
|
|
624
|
-
if (result.finalText === undefined) {
|
|
625
|
-
throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
|
|
626
|
-
}
|
|
627
|
-
try {
|
|
628
|
-
const output = await adjudicate(input, result.finalText, ports, activeSignal, boundary);
|
|
629
|
-
validateBossReplyOutput(input, output);
|
|
630
|
-
return output;
|
|
631
|
-
}
|
|
632
|
-
catch (error) {
|
|
633
|
-
onControlPlaneError?.(error);
|
|
634
|
-
throw error;
|
|
635
|
-
}
|
|
636
|
-
});
|
|
419
|
+
return createPlayerBridge({
|
|
420
|
+
resolvePlayerId: (input) => resolvePlayerId(input),
|
|
421
|
+
composePlayerPrompt: (input) => composePlayerPrompt(input),
|
|
422
|
+
adjudication: CODE_ADJUDICATION,
|
|
423
|
+
resumableStateIds: registeredResumableStateIds,
|
|
424
|
+
}, ports, getActiveSignal, boundary, onControlPlaneError);
|
|
637
425
|
}
|
|
638
426
|
// Captain pane display — PBRT-3 / PBRT-14.
|
|
639
427
|
// The Captain pane is a stream of three glyphs plus one bare
|
|
@@ -686,17 +474,6 @@ const stateMetadata = (() => {
|
|
|
686
474
|
return m;
|
|
687
475
|
})();
|
|
688
476
|
const registeredResumableStateIds = new Set(enumerateAwaitBossReply(codingMachine).bossReplyTransitions.map((transition) => transition.target));
|
|
689
|
-
function validateBossReplyOutput(input, output) {
|
|
690
|
-
if (output.guard !== 'needsBossReply')
|
|
691
|
-
return;
|
|
692
|
-
if (typeof output.question !== 'string') {
|
|
693
|
-
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
694
|
-
}
|
|
695
|
-
const stateId = input.stateId;
|
|
696
|
-
if (!registeredResumableStateIds.has(stateId)) {
|
|
697
|
-
throw new Error(BOSS_REPLY_ERRORS.unregisteredState(stateId));
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
477
|
const QUIESCENT_STATES = new Set([
|
|
701
478
|
'ready',
|
|
702
479
|
'awaitBossReply',
|
|
@@ -815,31 +592,40 @@ function stateTelemetryPayload(from, to, event, context) {
|
|
|
815
592
|
}
|
|
816
593
|
return payload;
|
|
817
594
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
}
|
|
595
|
+
// Captain-pane status lines for a root transition (PBRT-3 / PBRT-14):
|
|
596
|
+
// the `→ guard` outcome line for the settling transition, then either
|
|
597
|
+
// the awaitBossReply question + rider-less marker pair or the state's
|
|
598
|
+
// entry line (with `lastError` data on `failed`).
|
|
599
|
+
function statusesForState(state, context, event) {
|
|
600
|
+
const to = state.stateId;
|
|
601
|
+
if (to === undefined || !CAPTAIN_PANE_STATES.has(to))
|
|
602
|
+
return [];
|
|
603
|
+
const statuses = [];
|
|
604
|
+
const transitionLine = formatTransition(event);
|
|
605
|
+
if (transitionLine !== undefined) {
|
|
606
|
+
statuses.push({ message: transitionLine });
|
|
831
607
|
}
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
608
|
+
if (to === 'awaitBossReply') {
|
|
609
|
+
statuses.push({ message: formatAwaitBossReplyQuestion(context) }, { message: formatAwaitBossReplyMarker(context) });
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
const entryLine = formatStateEntry(to);
|
|
613
|
+
if (entryLine !== undefined) {
|
|
614
|
+
const lastError = to === 'failed' ? normalizeErrorCompact(context.lastError) : undefined;
|
|
615
|
+
statuses.push({
|
|
616
|
+
message: entryLine,
|
|
617
|
+
...(lastError === undefined
|
|
618
|
+
? {}
|
|
619
|
+
: {
|
|
620
|
+
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
621
|
+
}),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
836
624
|
}
|
|
837
|
-
return
|
|
625
|
+
return statuses;
|
|
838
626
|
}
|
|
839
627
|
// Internal export surface for tests. Not part of the stable public API;
|
|
840
|
-
// the leading underscore signals "subject to change."
|
|
841
|
-
// referenced here so `noUnusedLocals` stays clean while later tasks
|
|
842
|
-
// wire the factory body to use them.
|
|
628
|
+
// the leading underscore signals "subject to change."
|
|
843
629
|
export const _internal = {
|
|
844
630
|
composePlayerPrompt,
|
|
845
631
|
resolvePlayerId,
|
|
@@ -860,995 +646,25 @@ export const _internal = {
|
|
|
860
646
|
normalizeEventForTelemetry,
|
|
861
647
|
VERBATIM_PAYLOAD_FIELDS,
|
|
862
648
|
};
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
let judgeCallSequence = 0;
|
|
886
|
-
let playerCallSequence = 0;
|
|
887
|
-
let playbookCallSequence = 0;
|
|
888
|
-
const playerResumeTokens = new Map();
|
|
889
|
-
const activePlayerIds = new Set();
|
|
890
|
-
const playbookCallTurnIds = new Map();
|
|
891
|
-
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
892
|
-
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
893
|
-
const activeEmissionCalls = new Set();
|
|
894
|
-
// All trace, state-telemetry, and status work shares this one queue.
|
|
895
|
-
// Inspection callbacks enqueue a complete ordered batch synchronously;
|
|
896
|
-
// imperative boundaries await their queued work directly.
|
|
897
|
-
let emissionFailure;
|
|
898
|
-
function enqueueEmission(fn) {
|
|
899
|
-
const queued = emissionQueue.add(fn).then(() => undefined);
|
|
900
|
-
activeEmissionCalls.add(queued);
|
|
901
|
-
void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
|
|
902
|
-
activeEmissionCalls.delete(queued);
|
|
903
|
-
emissionFailure ??= error;
|
|
904
|
-
});
|
|
905
|
-
return queued;
|
|
906
|
-
}
|
|
907
|
-
async function drainEmissions() {
|
|
908
|
-
while (true) {
|
|
909
|
-
const active = [...activeEmissionCalls];
|
|
910
|
-
if (active.length > 0)
|
|
911
|
-
await Promise.allSettled(active);
|
|
912
|
-
await emissionQueue.onIdle();
|
|
913
|
-
if (activeEmissionCalls.size === 0 &&
|
|
914
|
-
emissionQueue.size === 0 &&
|
|
915
|
-
emissionQueue.pending === 0) {
|
|
916
|
-
break;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
if (emissionFailure !== undefined) {
|
|
920
|
-
const error = emissionFailure;
|
|
921
|
-
emissionFailure = undefined;
|
|
922
|
-
throw error;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
function requireSession() {
|
|
926
|
-
if (!session) {
|
|
927
|
-
throw new Error('createPlaybookRuntime: init must be called first');
|
|
928
|
-
}
|
|
929
|
-
return session;
|
|
930
|
-
}
|
|
931
|
-
function requireHostPorts() {
|
|
932
|
-
if (!savedPorts) {
|
|
933
|
-
throw new Error('createPlaybookRuntime: init must be called first');
|
|
934
|
-
}
|
|
935
|
-
return savedPorts;
|
|
936
|
-
}
|
|
937
|
-
function createTraceEvent(type, payload, position = {}) {
|
|
938
|
-
const currentSession = requireSession();
|
|
939
|
-
const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
940
|
-
return {
|
|
941
|
-
schemaVersion: 2,
|
|
942
|
-
sessionId: currentSession.sessionId,
|
|
943
|
-
playbookId: currentSession.playbookId,
|
|
944
|
-
rootSessionId: currentSession.rootSessionId,
|
|
945
|
-
...(currentSession.parentSessionId !== undefined
|
|
946
|
-
? { parentSessionId: currentSession.parentSessionId }
|
|
947
|
-
: {}),
|
|
948
|
-
...(currentSession.parentCallId !== undefined
|
|
949
|
-
? { parentCallId: currentSession.parentCallId }
|
|
950
|
-
: {}),
|
|
951
|
-
depth: currentSession.depth,
|
|
952
|
-
sequence: ++traceSequence,
|
|
953
|
-
timestamp: Date.now(),
|
|
954
|
-
type,
|
|
955
|
-
...(position.turnId !== undefined ? { turnId: position.turnId } : {}),
|
|
956
|
-
...(position.callId !== undefined ? { callId: position.callId } : {}),
|
|
957
|
-
payload: safePayload,
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
function emitTrace(type, payload, position = {}) {
|
|
961
|
-
const currentSession = requireSession();
|
|
962
|
-
const event = createTraceEvent(type, payload, position);
|
|
963
|
-
return enqueueEmission(() => currentSession.ports.emitTelemetry({
|
|
964
|
-
topic: 'playbook.trace',
|
|
965
|
-
payload: event,
|
|
966
|
-
}));
|
|
967
|
-
}
|
|
968
|
-
function stateIdentity(stateId) {
|
|
969
|
-
return stateId === undefined ? {} : { stateId };
|
|
970
|
-
}
|
|
971
|
-
function currentState() {
|
|
972
|
-
if (!actor) {
|
|
973
|
-
throw new Error('createPlaybookRuntime: actor is not initialized');
|
|
974
|
-
}
|
|
975
|
-
return normalizePlaybookSnapshot(actor.getSnapshot(), {
|
|
976
|
-
pendingCall: nestedBridge.getPendingCall(),
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
function stateTracePayload(state = currentState()) {
|
|
980
|
-
return {
|
|
981
|
-
state,
|
|
982
|
-
...stateIdentity(state.stateId),
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function createRuntimePorts(hostPorts) {
|
|
986
|
-
return {
|
|
987
|
-
callPlayer: (playerId, prompt, signal, callOptions) => hostPorts.callPlayer(playerId, prompt, signal, callOptions),
|
|
988
|
-
callCaptain: (prompt, signal, callOptions) => hostPorts.callCaptain(prompt, signal, callOptions),
|
|
989
|
-
callJudge: (prompt, signal) => hostPorts.callJudge(prompt, signal),
|
|
990
|
-
callPlaybook: (request, signal) => hostPorts.callPlaybook(request, signal),
|
|
991
|
-
emitStatus: (message, data) => {
|
|
992
|
-
const descriptor = actor ? currentState() : undefined;
|
|
993
|
-
const safeData = data === undefined
|
|
994
|
-
? undefined
|
|
995
|
-
: snapshotJsonValue(data, 'status data');
|
|
996
|
-
const trace = createTraceEvent('status.emitted', {
|
|
997
|
-
message,
|
|
998
|
-
...(safeData !== undefined ? { data: safeData } : {}),
|
|
999
|
-
...(descriptor !== undefined
|
|
1000
|
-
? {
|
|
1001
|
-
state: descriptor,
|
|
1002
|
-
...stateIdentity(descriptor.stateId),
|
|
1003
|
-
}
|
|
1004
|
-
: {}),
|
|
1005
|
-
}, activeTurnId !== undefined ? { turnId: activeTurnId } : {});
|
|
1006
|
-
return enqueueEmission(async () => {
|
|
1007
|
-
await hostPorts.emitTelemetry({
|
|
1008
|
-
topic: 'playbook.trace',
|
|
1009
|
-
payload: trace,
|
|
1010
|
-
});
|
|
1011
|
-
await hostPorts.emitStatus(message, safeData);
|
|
1012
|
-
});
|
|
1013
|
-
},
|
|
1014
|
-
emitTelemetry: (event) => {
|
|
1015
|
-
if (typeof event.topic !== 'string' || event.topic.length === 0) {
|
|
1016
|
-
throw new TypeError('telemetry topic must be a non-empty string');
|
|
1017
|
-
}
|
|
1018
|
-
const payload = snapshotJsonValue(event.payload, 'telemetry payload');
|
|
1019
|
-
return enqueueEmission(() => hostPorts.emitTelemetry({ topic: event.topic, payload }));
|
|
1020
|
-
},
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
async function emitCallStarted(startedType, finishedType, identity, position) {
|
|
1024
|
-
try {
|
|
1025
|
-
await emitTrace(startedType, identity, position);
|
|
1026
|
-
}
|
|
1027
|
-
catch (error) {
|
|
1028
|
-
controlPlaneError ??= error;
|
|
1029
|
-
try {
|
|
1030
|
-
await emitTrace(finishedType, { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1031
|
-
}
|
|
1032
|
-
catch {
|
|
1033
|
-
// Preserve the start failure after one best-effort finish attempt.
|
|
1034
|
-
}
|
|
1035
|
-
throw error;
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
const boundary = {
|
|
1039
|
-
async callPlayer(input, playerId, prompt, signal) {
|
|
1040
|
-
// State-entry telemetry/status must precede the call they describe.
|
|
1041
|
-
await drainEmissions();
|
|
1042
|
-
const turnId = activeTurnId;
|
|
1043
|
-
const callId = `player-${++playerCallSequence}`;
|
|
1044
|
-
const stateId = input.stateId;
|
|
1045
|
-
const resume = playerResumeTokens.get(playerId) ?? false;
|
|
1046
|
-
const identity = {
|
|
1047
|
-
purpose: 'captain',
|
|
1048
|
-
...stateIdentity(stateId),
|
|
1049
|
-
sourceItem: input.sourceItem,
|
|
1050
|
-
playerId,
|
|
1051
|
-
resume,
|
|
1052
|
-
};
|
|
1053
|
-
if (activePlayerIds.has(playerId)) {
|
|
1054
|
-
const error = new Error(`simultaneous calls to resolved player ${playerId} are not allowed`);
|
|
1055
|
-
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, {
|
|
1056
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1057
|
-
callId,
|
|
1058
|
-
});
|
|
1059
|
-
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, {
|
|
1060
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1061
|
-
callId,
|
|
1062
|
-
});
|
|
1063
|
-
throw error;
|
|
1064
|
-
}
|
|
1065
|
-
activePlayerIds.add(playerId);
|
|
1066
|
-
try {
|
|
1067
|
-
await emitTrace('player.call.started', { ...identity, prompt }, {
|
|
1068
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1069
|
-
callId,
|
|
1070
|
-
});
|
|
1071
|
-
let rawResult;
|
|
1072
|
-
try {
|
|
1073
|
-
rawResult = await requireHostPorts().callPlayer(playerId, prompt, signal, { resume });
|
|
1074
|
-
// A host promise is not required to honor cancellation. Do not let
|
|
1075
|
-
// a late result mutate continuity or publish a successful finish.
|
|
1076
|
-
signal.throwIfAborted();
|
|
1077
|
-
}
|
|
1078
|
-
catch (error) {
|
|
1079
|
-
if (!signal.aborted)
|
|
1080
|
-
controlPlaneError ??= error;
|
|
1081
|
-
try {
|
|
1082
|
-
await emitTrace('player.call.finished', {
|
|
1083
|
-
...identity,
|
|
1084
|
-
status: signal.aborted ? 'aborted' : 'error',
|
|
1085
|
-
error: normalizeError(error),
|
|
1086
|
-
}, {
|
|
1087
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1088
|
-
callId,
|
|
1089
|
-
});
|
|
1090
|
-
}
|
|
1091
|
-
catch {
|
|
1092
|
-
// The original non-abort port rejection remains authoritative.
|
|
1093
|
-
}
|
|
1094
|
-
// A thrown port call carries no authoritative result, so the
|
|
1095
|
-
// prior token remains available for a later explicit resume.
|
|
1096
|
-
throw error;
|
|
1097
|
-
}
|
|
1098
|
-
let result;
|
|
1099
|
-
try {
|
|
1100
|
-
result = validatePlayerResult(rawResult);
|
|
1101
|
-
}
|
|
1102
|
-
catch (error) {
|
|
1103
|
-
if (!signal.aborted)
|
|
1104
|
-
controlPlaneError ??= error;
|
|
1105
|
-
try {
|
|
1106
|
-
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, {
|
|
1107
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1108
|
-
callId,
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
catch {
|
|
1112
|
-
// The malformed host result remains authoritative.
|
|
1113
|
-
}
|
|
1114
|
-
throw error;
|
|
1115
|
-
}
|
|
1116
|
-
if (typeof result.resumeToken === 'string' &&
|
|
1117
|
-
result.resumeToken.trim().length > 0) {
|
|
1118
|
-
playerResumeTokens.set(playerId, result.resumeToken);
|
|
1119
|
-
}
|
|
1120
|
-
else {
|
|
1121
|
-
playerResumeTokens.delete(playerId);
|
|
1122
|
-
}
|
|
1123
|
-
await emitTrace('player.call.finished', {
|
|
1124
|
-
...identity,
|
|
1125
|
-
status: result.status,
|
|
1126
|
-
...(result.finalText !== undefined
|
|
1127
|
-
? { finalText: result.finalText }
|
|
1128
|
-
: {}),
|
|
1129
|
-
...(result.error !== undefined
|
|
1130
|
-
? { error: normalizeError(result.error) }
|
|
1131
|
-
: {}),
|
|
1132
|
-
...(result.resumeToken !== undefined
|
|
1133
|
-
? { resumeToken: result.resumeToken }
|
|
1134
|
-
: {}),
|
|
1135
|
-
}, {
|
|
1136
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1137
|
-
callId,
|
|
1138
|
-
});
|
|
1139
|
-
return result;
|
|
1140
|
-
}
|
|
1141
|
-
finally {
|
|
1142
|
-
activePlayerIds.delete(playerId);
|
|
1143
|
-
}
|
|
1144
|
-
},
|
|
1145
|
-
async callJudge(purpose, stateId, prompt, signal) {
|
|
1146
|
-
return judgeQueue.add(async () => {
|
|
1147
|
-
signal.throwIfAborted();
|
|
1148
|
-
// A transition/status queued synchronously by XState must reach
|
|
1149
|
-
// the host before the judge call that follows it.
|
|
1150
|
-
await drainEmissions();
|
|
1151
|
-
signal.throwIfAborted();
|
|
1152
|
-
const turnId = activeTurnId;
|
|
1153
|
-
const callId = `judge-${++judgeCallSequence}`;
|
|
1154
|
-
const identity = { purpose, ...stateIdentity(stateId) };
|
|
1155
|
-
await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, {
|
|
1156
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1157
|
-
callId,
|
|
1158
|
-
});
|
|
1159
|
-
let reply;
|
|
1160
|
-
try {
|
|
1161
|
-
reply = await requireHostPorts().callJudge(prompt, signal);
|
|
1162
|
-
signal.throwIfAborted();
|
|
1163
|
-
}
|
|
1164
|
-
catch (error) {
|
|
1165
|
-
if (!isAbortFailure(error, signal)) {
|
|
1166
|
-
controlPlaneError ??= error;
|
|
1167
|
-
}
|
|
1168
|
-
await emitTrace('judge.call.finished', {
|
|
1169
|
-
...identity,
|
|
1170
|
-
status: signal.aborted ? 'aborted' : 'error',
|
|
1171
|
-
error: normalizeError(error),
|
|
1172
|
-
}, {
|
|
1173
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1174
|
-
callId,
|
|
1175
|
-
});
|
|
1176
|
-
throw error;
|
|
1177
|
-
}
|
|
1178
|
-
if (typeof reply !== 'string') {
|
|
1179
|
-
const error = new TypeError('judge reply must be a string');
|
|
1180
|
-
controlPlaneError ??= error;
|
|
1181
|
-
await emitTrace('judge.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, {
|
|
1182
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1183
|
-
callId,
|
|
1184
|
-
});
|
|
1185
|
-
throw error;
|
|
1186
|
-
}
|
|
1187
|
-
// Keep the success finish outside the port-call catch. If a
|
|
1188
|
-
// telemetry sink records this boundary and then rejects, that sink
|
|
1189
|
-
// failure must not synthesize a second, contradictory finish.
|
|
1190
|
-
await emitTrace('judge.call.finished', { ...identity, status: 'ok', reply }, {
|
|
1191
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1192
|
-
callId,
|
|
1193
|
-
});
|
|
1194
|
-
return reply;
|
|
1195
|
-
});
|
|
1196
|
-
},
|
|
1197
|
-
};
|
|
1198
|
-
const nestedBridge = createNestedPlaybookBridge({
|
|
1199
|
-
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
1200
|
-
getBoundarySignal: () => activeSignal,
|
|
1201
|
-
callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
|
|
1202
|
-
emitStarted: async (event) => {
|
|
1203
|
-
playbookCallTurnIds.set(event.callId, activeTurnId);
|
|
1204
|
-
await emitTrace('playbook.call.started', {
|
|
1205
|
-
stateId: event.stateId,
|
|
1206
|
-
playbookId: event.playbookId,
|
|
1207
|
-
text: event.text,
|
|
1208
|
-
}, {
|
|
1209
|
-
...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
|
|
1210
|
-
callId: event.callId,
|
|
1211
|
-
});
|
|
1212
|
-
},
|
|
1213
|
-
emitFinished: async (event) => {
|
|
1214
|
-
const turnId = playbookCallTurnIds.get(event.callId);
|
|
1215
|
-
try {
|
|
1216
|
-
await emitTrace('playbook.call.finished', {
|
|
1217
|
-
stateId: event.stateId,
|
|
1218
|
-
playbookId: event.playbookId,
|
|
1219
|
-
text: event.text,
|
|
1220
|
-
result: event.result,
|
|
1221
|
-
}, {
|
|
1222
|
-
...(turnId !== undefined ? { turnId } : {}),
|
|
1223
|
-
callId: event.callId,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
finally {
|
|
1227
|
-
playbookCallTurnIds.delete(event.callId);
|
|
1228
|
-
}
|
|
1229
|
-
},
|
|
1230
|
-
drain: drainEmissions,
|
|
1231
|
-
bindResumeSignal: (signal) => {
|
|
1232
|
-
activeSignal = signal;
|
|
1233
|
-
},
|
|
1234
|
-
onControlPlaneError: (error) => {
|
|
1235
|
-
if (!activeSignal?.aborted)
|
|
1236
|
-
controlPlaneError ??= error;
|
|
1237
|
-
},
|
|
1238
|
-
onBackgroundError: (error) => {
|
|
1239
|
-
emissionFailure ??= error;
|
|
1240
|
-
},
|
|
1241
|
-
});
|
|
1242
|
-
function tracePositionForActiveTurn() {
|
|
1243
|
-
return activeTurnId === undefined ? {} : { turnId: activeTurnId };
|
|
1244
|
-
}
|
|
1245
|
-
function enqueueTransitionEmission(payload, state, statuses, position) {
|
|
1246
|
-
const currentSession = requireSession();
|
|
1247
|
-
const transitionTrace = createTraceEvent('fsm.transition', payload, position);
|
|
1248
|
-
const statusEmissions = statuses.map(({ message, data }) => ({
|
|
1249
|
-
message,
|
|
1250
|
-
data,
|
|
1251
|
-
trace: createTraceEvent('status.emitted', {
|
|
1252
|
-
message,
|
|
1253
|
-
...(data === undefined ? {} : { data }),
|
|
1254
|
-
state,
|
|
1255
|
-
...stateIdentity(state.stateId),
|
|
1256
|
-
}, position),
|
|
1257
|
-
}));
|
|
1258
|
-
void enqueueEmission(async () => {
|
|
1259
|
-
await currentSession.ports.emitTelemetry({
|
|
1260
|
-
topic: 'playbook.trace',
|
|
1261
|
-
payload: transitionTrace,
|
|
1262
|
-
});
|
|
1263
|
-
await currentSession.ports.emitTelemetry({
|
|
1264
|
-
topic: 'playbook.fsm.state',
|
|
1265
|
-
payload,
|
|
1266
|
-
});
|
|
1267
|
-
for (const status of statusEmissions) {
|
|
1268
|
-
await currentSession.ports.emitTelemetry({
|
|
1269
|
-
topic: 'playbook.trace',
|
|
1270
|
-
payload: status.trace,
|
|
1271
|
-
});
|
|
1272
|
-
await currentSession.ports.emitStatus(status.message, status.data);
|
|
1273
|
-
}
|
|
1274
|
-
}).catch(() => undefined);
|
|
1275
|
-
}
|
|
1276
|
-
function latchInspectionError(error) {
|
|
1277
|
-
if (activeSignal !== undefined)
|
|
1278
|
-
controlPlaneError ??= error;
|
|
1279
|
-
else
|
|
1280
|
-
emissionFailure ??= error;
|
|
1281
|
-
}
|
|
1282
|
-
function buildActor(ports, machineSnapshot) {
|
|
1283
|
-
priorState = undefined;
|
|
1284
|
-
let builtActor;
|
|
1285
|
-
builtActor = createActor(codingMachine.provide({
|
|
1286
|
-
actors: {
|
|
1287
|
-
player: captainBridge(ports, () => activeSignal, boundary, (error) => {
|
|
1288
|
-
if (!activeSignal?.aborted)
|
|
1289
|
-
controlPlaneError ??= error;
|
|
1290
|
-
}),
|
|
1291
|
-
},
|
|
1292
|
-
}), {
|
|
1293
|
-
input: boundOptions,
|
|
1294
|
-
// DR-014 §1: a restore rehydrates the persisted machine snapshot;
|
|
1295
|
-
// XState derives context/value from it and ignores `input` then.
|
|
1296
|
-
...(machineSnapshot === undefined
|
|
1297
|
-
? {}
|
|
1298
|
-
: {
|
|
1299
|
-
snapshot: machineSnapshot,
|
|
1300
|
-
}),
|
|
1301
|
-
inspect: (inspectionEvent) => {
|
|
1302
|
-
if (inspectionEvent.type !== '@xstate.snapshot')
|
|
1303
|
-
return;
|
|
1304
|
-
if (inspectionEvent.actorRef !== builtActor)
|
|
1305
|
-
return;
|
|
1306
|
-
if (suppressInspectionEmissions)
|
|
1307
|
-
return;
|
|
1308
|
-
try {
|
|
1309
|
-
const snap = inspectionEvent.snapshot;
|
|
1310
|
-
const state = normalizePlaybookSnapshot(snap);
|
|
1311
|
-
const to = state.stateId;
|
|
1312
|
-
if (to === undefined) {
|
|
1313
|
-
throw new Error('CODE root snapshot must expose exactly one playbook state id');
|
|
1314
|
-
}
|
|
1315
|
-
const previousState = priorState;
|
|
1316
|
-
const context = snap.context;
|
|
1317
|
-
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
1318
|
-
const statuses = [];
|
|
1319
|
-
if (CAPTAIN_PANE_STATES.has(to)) {
|
|
1320
|
-
const transitionLine = formatTransition(inspectionEvent.event);
|
|
1321
|
-
if (transitionLine !== undefined) {
|
|
1322
|
-
statuses.push({ message: transitionLine });
|
|
1323
|
-
}
|
|
1324
|
-
if (to === 'awaitBossReply') {
|
|
1325
|
-
statuses.push({ message: formatAwaitBossReplyQuestion(context) }, { message: formatAwaitBossReplyMarker(context) });
|
|
1326
|
-
}
|
|
1327
|
-
else {
|
|
1328
|
-
const entryLine = formatStateEntry(to);
|
|
1329
|
-
if (entryLine !== undefined) {
|
|
1330
|
-
const lastError = to === 'failed'
|
|
1331
|
-
? normalizeErrorCompact(snap.context.lastError)
|
|
1332
|
-
: undefined;
|
|
1333
|
-
statuses.push({
|
|
1334
|
-
message: entryLine,
|
|
1335
|
-
...(lastError === undefined
|
|
1336
|
-
? {}
|
|
1337
|
-
: {
|
|
1338
|
-
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
1339
|
-
}),
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
enqueueTransitionEmission(payload, state, statuses, tracePositionForActiveTurn());
|
|
1345
|
-
priorState = state;
|
|
1346
|
-
}
|
|
1347
|
-
catch (error) {
|
|
1348
|
-
latchInspectionError(error);
|
|
1349
|
-
}
|
|
1350
|
-
},
|
|
1351
|
-
});
|
|
1352
|
-
return builtActor;
|
|
1353
|
-
}
|
|
1354
|
-
function runResultFor(outcome, error) {
|
|
1355
|
-
const state = currentState();
|
|
1356
|
-
if (outcome === 'quiescent' || outcome === 'no-action') {
|
|
1357
|
-
return { outcome, state };
|
|
1358
|
-
}
|
|
1359
|
-
if (outcome === 'suspended') {
|
|
1360
|
-
const pendingCall = nestedBridge.getPendingCall();
|
|
1361
|
-
if (!pendingCall) {
|
|
1362
|
-
throw new Error('suspended runtime has no pending playbook call');
|
|
1363
|
-
}
|
|
1364
|
-
return { outcome, state, pendingCall };
|
|
1365
|
-
}
|
|
1366
|
-
if (outcome === 'terminal') {
|
|
1367
|
-
const output = actor?.getSnapshot()
|
|
1368
|
-
?.output;
|
|
1369
|
-
if (output !== undefined) {
|
|
1370
|
-
return {
|
|
1371
|
-
outcome,
|
|
1372
|
-
state,
|
|
1373
|
-
output: snapshotJsonValue(output, 'terminal playbook output'),
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
return { outcome, state };
|
|
1377
|
-
}
|
|
1378
|
-
const failure = error ??
|
|
1379
|
-
(outcome === 'failed'
|
|
1380
|
-
? actor?.getSnapshot()
|
|
1381
|
-
?.context?.lastError
|
|
1382
|
-
: outcome === 'aborted'
|
|
1383
|
-
? activeSignal?.reason
|
|
1384
|
-
: undefined);
|
|
1385
|
-
return {
|
|
1386
|
-
outcome,
|
|
1387
|
-
state,
|
|
1388
|
-
...(failure !== undefined ? { error: normalizeError(failure) } : {}),
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
function settledOutcome(signal) {
|
|
1392
|
-
if (nestedBridge.getPendingCall())
|
|
1393
|
-
return 'suspended';
|
|
1394
|
-
if (signal.aborted)
|
|
1395
|
-
return 'aborted';
|
|
1396
|
-
const state = currentState();
|
|
1397
|
-
if (state.status === 'error') {
|
|
1398
|
-
const actorError = actor?.getSnapshot()?.error;
|
|
1399
|
-
throw actorError ?? new Error('CODE actor entered error status');
|
|
1400
|
-
}
|
|
1401
|
-
if (state.status === 'done')
|
|
1402
|
-
return 'terminal';
|
|
1403
|
-
if (state.stateId === 'failed')
|
|
1404
|
-
return 'failed';
|
|
1405
|
-
return 'quiescent';
|
|
1406
|
-
}
|
|
1407
|
-
function settlementTracePayload(result) {
|
|
1408
|
-
return {
|
|
1409
|
-
...result,
|
|
1410
|
-
...stateIdentity(result.state.stateId),
|
|
1411
|
-
};
|
|
1412
|
-
}
|
|
1413
|
-
// Shared failed-start cleanup for init and restore: stop the actor,
|
|
1414
|
-
// abort/drain nested and host work, optionally emit one best-effort
|
|
1415
|
-
// session.disposed boundary, and unbind every closure field so dispose
|
|
1416
|
-
// stays callable. The caller rethrows its original failure. A restore
|
|
1417
|
-
// failure skips the disposal trace — the parked session was never
|
|
1418
|
-
// re-bound in this process, so its persisted snapshot stays
|
|
1419
|
-
// authoritative (DR-014 §2).
|
|
1420
|
-
async function cleanupFailedStart(cause, options) {
|
|
1421
|
-
let finalState;
|
|
1422
|
-
if (options.emitDisposal && actor) {
|
|
1423
|
-
try {
|
|
1424
|
-
finalState = currentState();
|
|
1425
|
-
}
|
|
1426
|
-
catch {
|
|
1427
|
-
// A state that cannot even normalize has no disposal descriptor.
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
suppressInspectionEmissions = true;
|
|
1431
|
-
try {
|
|
1432
|
-
actor?.stop();
|
|
1433
|
-
}
|
|
1434
|
-
catch {
|
|
1435
|
-
// Preserve the original startup failure.
|
|
1436
|
-
}
|
|
1437
|
-
try {
|
|
1438
|
-
await nestedBridge.abortPending(cause);
|
|
1439
|
-
}
|
|
1440
|
-
catch {
|
|
1441
|
-
// Preserve the original startup failure.
|
|
1442
|
-
}
|
|
1443
|
-
try {
|
|
1444
|
-
await judgeQueue.onIdle();
|
|
1445
|
-
await drainEmissions();
|
|
1446
|
-
}
|
|
1447
|
-
catch {
|
|
1448
|
-
// Preserve the original startup failure.
|
|
1449
|
-
}
|
|
1450
|
-
if (options.emitDisposal) {
|
|
1451
|
-
try {
|
|
1452
|
-
await emitTrace('session.disposed', finalState === undefined
|
|
1453
|
-
? {}
|
|
1454
|
-
: {
|
|
1455
|
-
state: finalState,
|
|
1456
|
-
...stateIdentity(finalState.stateId),
|
|
1457
|
-
});
|
|
1458
|
-
await drainEmissions();
|
|
1459
|
-
}
|
|
1460
|
-
catch {
|
|
1461
|
-
// The session-start error remains authoritative.
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
playerResumeTokens.clear();
|
|
1465
|
-
activePlayerIds.clear();
|
|
1466
|
-
playbookCallTurnIds.clear();
|
|
1467
|
-
activeEmissionCalls.clear();
|
|
1468
|
-
emissionQueue.clear();
|
|
1469
|
-
judgeQueue.clear();
|
|
1470
|
-
actor = undefined;
|
|
1471
|
-
session = undefined;
|
|
1472
|
-
savedPorts = undefined;
|
|
1473
|
-
runtimePorts = undefined;
|
|
1474
|
-
activeSignal = undefined;
|
|
1475
|
-
activeTurnId = undefined;
|
|
1476
|
-
controlPlaneError = undefined;
|
|
1477
|
-
emissionFailure = undefined;
|
|
1478
|
-
priorState = undefined;
|
|
1479
|
-
suppressInspectionEmissions = false;
|
|
1480
|
-
initialized = false;
|
|
1481
|
-
traceSequence = 0;
|
|
1482
|
-
turnSequence = 0;
|
|
1483
|
-
judgeCallSequence = 0;
|
|
1484
|
-
playerCallSequence = 0;
|
|
1485
|
-
playbookCallSequence = 0;
|
|
1486
|
-
}
|
|
1487
|
-
const runtime = {
|
|
1488
|
-
async init(nextSession) {
|
|
1489
|
-
if (initialized || disposed || disposalPromise !== undefined) {
|
|
1490
|
-
throw new Error('createPlaybookRuntime.init: already initialized');
|
|
1491
|
-
}
|
|
1492
|
-
const boundSession = snapshotPlaybookSession(nextSession);
|
|
1493
|
-
initialized = true;
|
|
1494
|
-
let finishInitialization;
|
|
1495
|
-
const initialization = new Promise((resolve) => {
|
|
1496
|
-
finishInitialization = resolve;
|
|
1497
|
-
});
|
|
1498
|
-
initInFlight = initialization;
|
|
1499
|
-
const initTask = (async () => {
|
|
1500
|
-
session = boundSession;
|
|
1501
|
-
savedPorts = boundSession.ports;
|
|
1502
|
-
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
1503
|
-
suppressInspectionEmissions = false;
|
|
1504
|
-
actor = buildActor(runtimePorts);
|
|
1505
|
-
await emitTrace('session.started', stateTracePayload());
|
|
1506
|
-
actor.start();
|
|
1507
|
-
await drainEmissions();
|
|
1508
|
-
})();
|
|
1509
|
-
try {
|
|
1510
|
-
await initTask;
|
|
1511
|
-
}
|
|
1512
|
-
catch (error) {
|
|
1513
|
-
await cleanupFailedStart(error, { emitDisposal: true });
|
|
1514
|
-
throw error;
|
|
1515
|
-
}
|
|
1516
|
-
finally {
|
|
1517
|
-
finishInitialization();
|
|
1518
|
-
if (initInFlight === initialization)
|
|
1519
|
-
initInFlight = undefined;
|
|
1520
|
-
}
|
|
1521
|
-
},
|
|
1522
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked session.
|
|
1523
|
-
// Defined only at a safe capture point — initialized, not disposing
|
|
1524
|
-
// or disposed, no active public boundary, no pending nested call,
|
|
1525
|
-
// and the actor quiescent with status `active`.
|
|
1526
|
-
exportSnapshot() {
|
|
1527
|
-
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
1528
|
-
return undefined;
|
|
1529
|
-
}
|
|
1530
|
-
if (activeSignal !== undefined)
|
|
1531
|
-
return undefined;
|
|
1532
|
-
if (nestedBridge.getPendingCall())
|
|
1533
|
-
return undefined;
|
|
1534
|
-
const state = currentState();
|
|
1535
|
-
if (state.status !== 'active' || !state.quiescent)
|
|
1536
|
-
return undefined;
|
|
1537
|
-
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
1538
|
-
const context = actor.getSnapshot()
|
|
1539
|
-
.context;
|
|
1540
|
-
const pending = pendingBossQuestionFromContext(context ?? {});
|
|
1541
|
-
return {
|
|
1542
|
-
schemaVersion: 1,
|
|
1543
|
-
playbookId: session.playbookId,
|
|
1544
|
-
machine,
|
|
1545
|
-
playerResumeTokens: Object.fromEntries(playerResumeTokens),
|
|
1546
|
-
sequences: {
|
|
1547
|
-
trace: traceSequence,
|
|
1548
|
-
turn: turnSequence,
|
|
1549
|
-
judgeCall: judgeCallSequence,
|
|
1550
|
-
playerCall: playerCallSequence,
|
|
1551
|
-
playbookCall: playbookCallSequence,
|
|
1552
|
-
},
|
|
1553
|
-
state,
|
|
1554
|
-
pendingBossQuestions: pending === undefined
|
|
1555
|
-
? []
|
|
1556
|
-
: [
|
|
1557
|
-
{
|
|
1558
|
-
questionId: pending.questionId,
|
|
1559
|
-
player: pending.player,
|
|
1560
|
-
question: pending.question,
|
|
1561
|
-
sourceItem: pending.sourceItem,
|
|
1562
|
-
},
|
|
1563
|
-
],
|
|
1564
|
-
};
|
|
1565
|
-
},
|
|
1566
|
-
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
1567
|
-
// exported snapshot under the same immutable session identity.
|
|
1568
|
-
// Emits no `session.started`, transition trace, or human status —
|
|
1569
|
-
// the session already started; the next public boundary continues
|
|
1570
|
-
// the contiguous trace sequence.
|
|
1571
|
-
async restore(nextSession, snapshot) {
|
|
1572
|
-
if (initialized || disposed || disposalPromise !== undefined) {
|
|
1573
|
-
throw new Error('createPlaybookRuntime.restore: already initialized');
|
|
1574
|
-
}
|
|
1575
|
-
const boundSession = snapshotPlaybookSession(nextSession);
|
|
1576
|
-
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId);
|
|
1577
|
-
initialized = true;
|
|
1578
|
-
let finishInitialization;
|
|
1579
|
-
const initialization = new Promise((resolve) => {
|
|
1580
|
-
finishInitialization = resolve;
|
|
1581
|
-
});
|
|
1582
|
-
initInFlight = initialization;
|
|
1583
|
-
const initTask = (async () => {
|
|
1584
|
-
session = boundSession;
|
|
1585
|
-
savedPorts = boundSession.ports;
|
|
1586
|
-
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
1587
|
-
traceSequence = boundSnapshot.sequences.trace;
|
|
1588
|
-
turnSequence = boundSnapshot.sequences.turn;
|
|
1589
|
-
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
1590
|
-
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1591
|
-
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
1592
|
-
playerResumeTokens.clear();
|
|
1593
|
-
for (const [playerId, token] of Object.entries(boundSnapshot.playerResumeTokens)) {
|
|
1594
|
-
playerResumeTokens.set(playerId, token);
|
|
1595
|
-
}
|
|
1596
|
-
suppressInspectionEmissions = true;
|
|
1597
|
-
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
1598
|
-
actor.start();
|
|
1599
|
-
const restoredState = currentState();
|
|
1600
|
-
if (restoredState.status !== 'active') {
|
|
1601
|
-
throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
|
|
1602
|
-
}
|
|
1603
|
-
suppressInspectionEmissions = false;
|
|
1604
|
-
priorState = restoredState;
|
|
1605
|
-
await drainEmissions();
|
|
1606
|
-
})();
|
|
1607
|
-
try {
|
|
1608
|
-
await initTask;
|
|
1609
|
-
}
|
|
1610
|
-
catch (error) {
|
|
1611
|
-
await cleanupFailedStart(error, { emitDisposal: false });
|
|
1612
|
-
throw error;
|
|
1613
|
-
}
|
|
1614
|
-
finally {
|
|
1615
|
-
finishInitialization();
|
|
1616
|
-
if (initInFlight === initialization)
|
|
1617
|
-
initInFlight = undefined;
|
|
1618
|
-
}
|
|
1619
|
-
},
|
|
1620
|
-
async handleBossInput({ text, signal, }) {
|
|
1621
|
-
if (!actor || !savedPorts) {
|
|
1622
|
-
throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
|
|
1623
|
-
}
|
|
1624
|
-
if (disposed || disposalPromise !== undefined) {
|
|
1625
|
-
throw new Error('createPlaybookRuntime.handleBossInput: runtime is disposing or disposed');
|
|
1626
|
-
}
|
|
1627
|
-
if (activeSignal !== undefined) {
|
|
1628
|
-
throw new Error('createPlaybookRuntime.handleBossInput: another runtime turn is active');
|
|
1629
|
-
}
|
|
1630
|
-
const turnId = ++turnSequence;
|
|
1631
|
-
activeTurnId = turnId;
|
|
1632
|
-
activeSignal = signal;
|
|
1633
|
-
controlPlaneError = undefined;
|
|
1634
|
-
let result;
|
|
1635
|
-
let operationError;
|
|
1636
|
-
try {
|
|
1637
|
-
await emitTrace('boss.input.received', { text }, { turnId });
|
|
1638
|
-
// 1. Classify non-empty text into an FSM event through the judge.
|
|
1639
|
-
const event = await classifyBossText(text, runtimePorts, signal, actor.getSnapshot(), boundary);
|
|
1640
|
-
// Empty input, no-action classifier output, or invalid classifier
|
|
1641
|
-
// output — nothing to send.
|
|
1642
|
-
if (event === undefined) {
|
|
1643
|
-
result = runResultFor('no-action');
|
|
1644
|
-
}
|
|
1645
|
-
else {
|
|
1646
|
-
// 2. Captain-pane classification line (PBRT-14): the bare
|
|
1647
|
-
// FSM event type, emitted before the FSM advances.
|
|
1648
|
-
await runtimePorts.emitStatus(formatClassification(event.type));
|
|
1649
|
-
// 3. A final actor cannot accept new events; reconstruct only after
|
|
1650
|
-
// classification produced a real event.
|
|
1651
|
-
if (actor.getSnapshot().status === 'done') {
|
|
1652
|
-
actor.stop();
|
|
1653
|
-
actor = buildActor(runtimePorts);
|
|
1654
|
-
actor.start();
|
|
1655
|
-
}
|
|
1656
|
-
actor.send(event);
|
|
1657
|
-
await waitForPlaybookQuiescence(actor, {
|
|
1658
|
-
pendingCalls: nestedBridge,
|
|
1659
|
-
});
|
|
1660
|
-
if (controlPlaneError !== undefined)
|
|
1661
|
-
throw controlPlaneError;
|
|
1662
|
-
result = runResultFor(settledOutcome(signal));
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
catch (error) {
|
|
1666
|
-
operationError = error;
|
|
1667
|
-
}
|
|
1668
|
-
let drainError;
|
|
1669
|
-
try {
|
|
1670
|
-
await drainEmissions();
|
|
1671
|
-
}
|
|
1672
|
-
catch (error) {
|
|
1673
|
-
drainError = error;
|
|
1674
|
-
}
|
|
1675
|
-
const latchedControlError = controlPlaneError;
|
|
1676
|
-
const primaryError = latchedControlError ?? drainError ?? operationError;
|
|
1677
|
-
const abortError = latchedControlError === undefined &&
|
|
1678
|
-
drainError === undefined &&
|
|
1679
|
-
operationError !== undefined &&
|
|
1680
|
-
isAbortFailure(operationError, signal);
|
|
1681
|
-
const settlementResult = primaryError === undefined
|
|
1682
|
-
? (result ?? runResultFor('no-action'))
|
|
1683
|
-
: runResultFor(abortError ? 'aborted' : 'failed', primaryError);
|
|
1684
|
-
let settlementEmissionError;
|
|
1685
|
-
try {
|
|
1686
|
-
await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
|
|
1687
|
-
}
|
|
1688
|
-
catch (error) {
|
|
1689
|
-
settlementEmissionError = error;
|
|
1690
|
-
}
|
|
1691
|
-
try {
|
|
1692
|
-
await drainEmissions();
|
|
1693
|
-
}
|
|
1694
|
-
catch (error) {
|
|
1695
|
-
settlementEmissionError ??= error;
|
|
1696
|
-
}
|
|
1697
|
-
const failure = controlPlaneError ??
|
|
1698
|
-
latchedControlError ??
|
|
1699
|
-
drainError ??
|
|
1700
|
-
(abortError
|
|
1701
|
-
? (settlementEmissionError ?? operationError)
|
|
1702
|
-
: (operationError ?? settlementEmissionError));
|
|
1703
|
-
activeSignal = undefined;
|
|
1704
|
-
activeTurnId = undefined;
|
|
1705
|
-
controlPlaneError = undefined;
|
|
1706
|
-
if (failure !== undefined &&
|
|
1707
|
-
!(abortError && settlementEmissionError === undefined)) {
|
|
1708
|
-
throw failure;
|
|
1709
|
-
}
|
|
1710
|
-
return settlementResult;
|
|
1711
|
-
},
|
|
1712
|
-
async resumePlaybookCall(input) {
|
|
1713
|
-
if (!actor || !savedPorts) {
|
|
1714
|
-
throw new Error('createPlaybookRuntime.resumePlaybookCall: init must be called first');
|
|
1715
|
-
}
|
|
1716
|
-
if (disposed || disposalPromise !== undefined) {
|
|
1717
|
-
throw new Error('createPlaybookRuntime.resumePlaybookCall: runtime is disposing or disposed');
|
|
1718
|
-
}
|
|
1719
|
-
if (activeSignal !== undefined) {
|
|
1720
|
-
throw new Error('createPlaybookRuntime.resumePlaybookCall: another runtime turn is active');
|
|
1721
|
-
}
|
|
1722
|
-
activeTurnId = playbookCallTurnIds.get(input.callId);
|
|
1723
|
-
activeSignal = input.signal;
|
|
1724
|
-
controlPlaneError = undefined;
|
|
1725
|
-
let result;
|
|
1726
|
-
let operationError;
|
|
1727
|
-
try {
|
|
1728
|
-
await nestedBridge.resume(input);
|
|
1729
|
-
}
|
|
1730
|
-
catch (error) {
|
|
1731
|
-
operationError = error;
|
|
1732
|
-
}
|
|
1733
|
-
try {
|
|
1734
|
-
await waitForPlaybookQuiescence(actor, {
|
|
1735
|
-
pendingCalls: nestedBridge,
|
|
1736
|
-
});
|
|
1737
|
-
result = runResultFor(settledOutcome(input.signal));
|
|
1738
|
-
}
|
|
1739
|
-
catch (error) {
|
|
1740
|
-
operationError ??= error;
|
|
1741
|
-
}
|
|
1742
|
-
let drainError;
|
|
1743
|
-
try {
|
|
1744
|
-
await drainEmissions();
|
|
1745
|
-
}
|
|
1746
|
-
catch (error) {
|
|
1747
|
-
drainError = error;
|
|
1748
|
-
}
|
|
1749
|
-
const failure = controlPlaneError ?? drainError ?? operationError;
|
|
1750
|
-
activeSignal = undefined;
|
|
1751
|
-
activeTurnId = undefined;
|
|
1752
|
-
controlPlaneError = undefined;
|
|
1753
|
-
if (failure !== undefined)
|
|
1754
|
-
throw failure;
|
|
1755
|
-
if (result === undefined) {
|
|
1756
|
-
throw new Error('playbook resume produced no runtime result');
|
|
1757
|
-
}
|
|
1758
|
-
return result;
|
|
1759
|
-
},
|
|
1760
|
-
dispose() {
|
|
1761
|
-
if (disposalPromise !== undefined)
|
|
1762
|
-
return disposalPromise;
|
|
1763
|
-
if (disposed)
|
|
1764
|
-
return Promise.resolve();
|
|
1765
|
-
if (activeSignal !== undefined) {
|
|
1766
|
-
return Promise.reject(new Error('createPlaybookRuntime.dispose: cannot dispose during an active runtime boundary'));
|
|
1767
|
-
}
|
|
1768
|
-
const task = (async () => {
|
|
1769
|
-
const failures = [];
|
|
1770
|
-
try {
|
|
1771
|
-
if (initInFlight !== undefined) {
|
|
1772
|
-
try {
|
|
1773
|
-
await initInFlight;
|
|
1774
|
-
}
|
|
1775
|
-
catch {
|
|
1776
|
-
// Dispose still releases whatever an unsuccessful init bound.
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
const finalState = actor ? currentState() : undefined;
|
|
1780
|
-
// Stop the root before settling a suspended child. Its rejection
|
|
1781
|
-
// must not re-enter CODE and start fresh work during disposal.
|
|
1782
|
-
if (actor)
|
|
1783
|
-
actor.stop();
|
|
1784
|
-
try {
|
|
1785
|
-
await nestedBridge.dispose();
|
|
1786
|
-
}
|
|
1787
|
-
catch (error) {
|
|
1788
|
-
failures.push(error);
|
|
1789
|
-
}
|
|
1790
|
-
try {
|
|
1791
|
-
await drainEmissions();
|
|
1792
|
-
}
|
|
1793
|
-
catch (error) {
|
|
1794
|
-
failures.push(error);
|
|
1795
|
-
}
|
|
1796
|
-
if (session !== undefined) {
|
|
1797
|
-
try {
|
|
1798
|
-
await emitTrace('session.disposed', finalState === undefined
|
|
1799
|
-
? {}
|
|
1800
|
-
: {
|
|
1801
|
-
state: finalState,
|
|
1802
|
-
...stateIdentity(finalState.stateId),
|
|
1803
|
-
});
|
|
1804
|
-
await drainEmissions();
|
|
1805
|
-
}
|
|
1806
|
-
catch (error) {
|
|
1807
|
-
failures.push(error);
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
finally {
|
|
1812
|
-
playerResumeTokens.clear();
|
|
1813
|
-
activePlayerIds.clear();
|
|
1814
|
-
playbookCallTurnIds.clear();
|
|
1815
|
-
activeEmissionCalls.clear();
|
|
1816
|
-
emissionQueue.clear();
|
|
1817
|
-
judgeQueue.clear();
|
|
1818
|
-
actor = undefined;
|
|
1819
|
-
activeSignal = undefined;
|
|
1820
|
-
activeTurnId = undefined;
|
|
1821
|
-
controlPlaneError = undefined;
|
|
1822
|
-
emissionFailure = undefined;
|
|
1823
|
-
savedPorts = undefined;
|
|
1824
|
-
runtimePorts = undefined;
|
|
1825
|
-
session = undefined;
|
|
1826
|
-
disposed = true;
|
|
1827
|
-
}
|
|
1828
|
-
if (failures.length === 1)
|
|
1829
|
-
throw failures[0];
|
|
1830
|
-
if (failures.length > 1) {
|
|
1831
|
-
throw new AggregateError(failures, 'playbook runtime disposal failed');
|
|
1832
|
-
}
|
|
1833
|
-
})();
|
|
1834
|
-
disposalPromise = task;
|
|
1835
|
-
return task;
|
|
1836
|
-
},
|
|
1837
|
-
// @internal — test-only escape hatch for inspecting the
|
|
1838
|
-
// underlying actor's snapshot. Most state assertions are now
|
|
1839
|
-
// expressible via the recorded emitStatus / emitTelemetry
|
|
1840
|
-
// calls (DR-004 §9); the hatch stays for the few cases where
|
|
1841
|
-
// direct context inspection is clearer (e.g., the dispose
|
|
1842
|
-
// teardown test).
|
|
1843
|
-
_getActor() {
|
|
1844
|
-
return actor;
|
|
1845
|
-
},
|
|
1846
|
-
_getBoundary() {
|
|
1847
|
-
return boundary;
|
|
1848
|
-
},
|
|
1849
|
-
_getNestedBridge() {
|
|
1850
|
-
return nestedBridge;
|
|
1851
|
-
},
|
|
1852
|
-
};
|
|
1853
|
-
return runtime;
|
|
1854
|
-
}
|
|
649
|
+
// The CODE-specific spec handed to the shared runtime factory
|
|
650
|
+
// (slc/link.md §Output, DR-019). The generic machinery — actor wiring,
|
|
651
|
+
// boundary tracing, Boss-turn lifecycle, nested-playbook bridge, and the
|
|
652
|
+
// DR-014 parked-session snapshot capability — lives in
|
|
653
|
+
// @sublang/playbook/xstate-runtime; this spec carries only what is
|
|
654
|
+
// CODE-specific.
|
|
655
|
+
const runtimeSpec = {
|
|
656
|
+
label: 'CODE',
|
|
657
|
+
snapshotOptions: snapshotCodePlaybookOptions,
|
|
658
|
+
resolvePlayerId: (input) => resolvePlayerId(input),
|
|
659
|
+
composePlayerPrompt: (input) => composePlayerPrompt(input),
|
|
660
|
+
buildJudgePrompt: CODE_ADJUDICATION.buildJudgePrompt,
|
|
661
|
+
extractRequiredFields,
|
|
662
|
+
verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
|
|
663
|
+
resumableStateIds: registeredResumableStateIds,
|
|
664
|
+
classifyBossText: (text, ports, signal, snapshotOrState, boundary) => classifyBossText(text, ports, signal, snapshotOrState, boundary),
|
|
665
|
+
classificationStatus: (event) => formatClassification(event.type),
|
|
666
|
+
statusesForState,
|
|
667
|
+
normalizeTransitionEvent: (event) => normalizeEventForTelemetry(event),
|
|
668
|
+
};
|
|
669
|
+
const createPlaybookRuntime = createXStatePlaybookRuntime(codingMachine, runtimeSpec);
|
|
670
|
+
export default createPlaybookRuntime;
|