@sublang/playbook 0.9.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 +190 -151
- package/package.json +50 -6
- package/reference/sdlc/captain.md +102 -0
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
- package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
- package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
- package/reference/sdlc/code.playbook/bin/run.js +999 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
- package/reference/sdlc/code.playbook/code.fsm.js +334 -102
- package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
- package/reference/sdlc/code.playbook/code.gears.md +11 -10
- package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
- package/reference/sdlc/code.playbook/code.playbook.js +199 -488
- package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
- package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
- package/reference/sdlc/code.playbook/code.registry.js +0 -3
- package/reference/sdlc/code.playbook/code.registry.ts +0 -6
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
- package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
- package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
- package/slc/gears2fsm.md +557 -57
- package/slc/link.md +1165 -89
- package/slc/optimize.md +92 -0
- package/slc/text2gears.md +255 -7
- package/src/runtime.d.ts +146 -3
- package/src/runtime.ts +201 -2
- 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 +95 -0
- package/src/xstate-runtime.js +1258 -0
- package/src/xstate-runtime.ts +1816 -0
|
@@ -11,16 +11,44 @@
|
|
|
11
11
|
// alias's first alternative)
|
|
12
12
|
// Boss event: free-text judge classification
|
|
13
13
|
// Adjudication: LLM-judge per state
|
|
14
|
-
// Contract: PlayerResult / PlaybookPorts /
|
|
15
|
-
// and re-exported from
|
|
14
|
+
// Contract: PlayerResult / PlaybookPorts / PlaybookSession /
|
|
15
|
+
// PlaybookRuntime imported and re-exported from
|
|
16
|
+
// @sublang/playbook/runtime
|
|
16
17
|
// (slc/link.md §Output, DR-004 Addendum A4)
|
|
17
|
-
|
|
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';
|
|
18
25
|
import { codingMachine, } from './code.fsm.js';
|
|
19
26
|
import { enumerateAwaitBossReply, enumerateCaptainStates, enumerateRootEvents, } from './code.fsm.introspect.js';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
function snapshotCodePlaybookOptions(value) {
|
|
28
|
+
const captured = snapshotJsonValue(value, 'CODE runtime options');
|
|
29
|
+
if (captured === null ||
|
|
30
|
+
typeof captured !== 'object' ||
|
|
31
|
+
Array.isArray(captured)) {
|
|
32
|
+
throw new TypeError('CODE runtime options must be an object');
|
|
33
|
+
}
|
|
34
|
+
const record = captured;
|
|
35
|
+
const allowed = new Set([
|
|
36
|
+
'intent',
|
|
37
|
+
'irNumber',
|
|
38
|
+
'coderPlayer',
|
|
39
|
+
'reviewerPlayer',
|
|
40
|
+
'committerPlayer',
|
|
41
|
+
]);
|
|
42
|
+
for (const [key, option] of Object.entries(record)) {
|
|
43
|
+
if (!allowed.has(key)) {
|
|
44
|
+
throw new TypeError(`CODE runtime options.${key} is not declared`);
|
|
45
|
+
}
|
|
46
|
+
if (typeof option !== 'string') {
|
|
47
|
+
throw new TypeError(`CODE runtime options.${key} must be a string`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return captured;
|
|
51
|
+
}
|
|
24
52
|
// Required-payload fields whose value is the player's verbatim long-form
|
|
25
53
|
// prose. The runtime carries `finalText.trim()` into these fields rather
|
|
26
54
|
// than asking the judge to round-trip the text through JSON. Short
|
|
@@ -30,59 +58,47 @@ const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
|
30
58
|
'reviews',
|
|
31
59
|
'challenges',
|
|
32
60
|
]);
|
|
33
|
-
// Normalize
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (err === undefined || err === null)
|
|
61
|
+
// Normalize any `error` field inside a telemetry event so failed
|
|
62
|
+
// transitions don't leak raw Error instances through the channel.
|
|
63
|
+
function normalizeEventForTelemetry(event) {
|
|
64
|
+
if (event === undefined)
|
|
38
65
|
return undefined;
|
|
39
|
-
|
|
40
|
-
return { name: err.name, message: err.message };
|
|
41
|
-
}
|
|
42
|
-
if (typeof err === 'object') {
|
|
43
|
-
const o = err;
|
|
44
|
-
if (typeof o.message === 'string') {
|
|
45
|
-
return {
|
|
46
|
-
name: typeof o.name === 'string' ? o.name : 'Error',
|
|
47
|
-
message: o.message,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return { name: 'Error', message: String(err) };
|
|
66
|
+
return normalizeEventValue(event, 'FSM event', new Set());
|
|
52
67
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
function normalizeEventValue(value, path, ancestors) {
|
|
69
|
+
if (Array.isArray(value))
|
|
70
|
+
return snapshotJsonValue(value, path);
|
|
71
|
+
if (value === null || typeof value !== 'object') {
|
|
72
|
+
return snapshotJsonValue(value, path);
|
|
73
|
+
}
|
|
74
|
+
if (ancestors.has(value)) {
|
|
75
|
+
throw new TypeError(`${path} must not contain a JSON cycle`);
|
|
76
|
+
}
|
|
77
|
+
const prototype = Object.getPrototypeOf(value);
|
|
78
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
79
|
+
return snapshotJsonValue(value, path);
|
|
80
|
+
}
|
|
81
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
82
|
+
return snapshotJsonValue(value, path);
|
|
83
|
+
}
|
|
84
|
+
const nextAncestors = new Set(ancestors).add(value);
|
|
85
|
+
const normalized = {};
|
|
86
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
|
|
87
|
+
if (!descriptor.enumerable) {
|
|
88
|
+
throw new TypeError(`${path}.${key} must be an enumerable JSON property`);
|
|
89
|
+
}
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
91
|
+
throw new TypeError(`${path}.${key} must be a JSON data property`);
|
|
67
92
|
}
|
|
93
|
+
if (descriptor.value === undefined)
|
|
94
|
+
continue;
|
|
95
|
+
normalized[key] =
|
|
96
|
+
key === 'error'
|
|
97
|
+
? snapshotJsonValue(normalizeError(descriptor.value), `${path}.error`)
|
|
98
|
+
: normalizeEventValue(descriptor.value, `${path}.${key}`, nextAncestors);
|
|
68
99
|
}
|
|
69
|
-
return
|
|
100
|
+
return snapshotJsonValue(normalized, path);
|
|
70
101
|
}
|
|
71
|
-
// Normalize any `error` field inside a telemetry event so failed
|
|
72
|
-
// transitions don't leak raw Error instances through the channel.
|
|
73
|
-
function normalizeEventForTelemetry(event) {
|
|
74
|
-
if (event === null || typeof event !== 'object' || Array.isArray(event)) {
|
|
75
|
-
return event;
|
|
76
|
-
}
|
|
77
|
-
const e = event;
|
|
78
|
-
if (!('error' in e))
|
|
79
|
-
return event;
|
|
80
|
-
const normalized = normalizeErrorFull(e.error);
|
|
81
|
-
return { ...e, error: normalized };
|
|
82
|
-
}
|
|
83
|
-
// Internal capabilities (DR-004 §10). Each ships with its final
|
|
84
|
-
// signature; behavior lands in the per-capability task noted by the
|
|
85
|
-
// TODO marker.
|
|
86
102
|
// Player-prompt composer — DR-004 §6.
|
|
87
103
|
// Substitutes the three placeholder tokens in `input.prompt` (literal
|
|
88
104
|
// string replace, no escaping) and arranges labelled blocks around
|
|
@@ -163,52 +179,6 @@ function resolvePlayerId(input) {
|
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
|
-
// LLM judge — DR-004 §4. Builds a prompt that lists each declared
|
|
167
|
-
// outcome verbatim, asks ports.callJudge for a JSON
|
|
168
|
-
// `{ guard, …payloadFields }` response, and returns the parsed
|
|
169
|
-
// object once the chosen guard is one of the input.result keys.
|
|
170
|
-
// Adjudicator failures (malformed JSON, missing/unknown guard) are
|
|
171
|
-
// control-plane errors and propagate via throw per slc/link.md.
|
|
172
|
-
async function adjudicate(input, finalText, ports, signal) {
|
|
173
|
-
const prompt = buildJudgePrompt(input, finalText);
|
|
174
|
-
const raw = await ports.callJudge(prompt, signal);
|
|
175
|
-
const parsed = parseJudgeJson(raw);
|
|
176
|
-
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
177
|
-
throw new Error('adjudicate: judge response is not a JSON object');
|
|
178
|
-
}
|
|
179
|
-
const obj = parsed;
|
|
180
|
-
const guard = obj.guard;
|
|
181
|
-
if (typeof guard !== 'string') {
|
|
182
|
-
throw new Error('adjudicate: judge response missing string "guard" field');
|
|
183
|
-
}
|
|
184
|
-
if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
|
|
185
|
-
throw new Error(`adjudicate: unknown guard "${guard}" — declared guards: ${Object.keys(input.result).join(', ')}`);
|
|
186
|
-
}
|
|
187
|
-
// Per slc/link.md, a missing payload field the state's `result`
|
|
188
|
-
// description requires is a control-plane error. The FSM names
|
|
189
|
-
// required fields with the literal phrase
|
|
190
|
-
// Output shall include `<fieldName>: <...>`
|
|
191
|
-
// so we extract those tokens and require each to be a string in
|
|
192
|
-
// the judge response — except for VERBATIM_PAYLOAD_FIELDS
|
|
193
|
-
// (`reviews`, `challenges`), where the runtime substitutes
|
|
194
|
-
// `finalText.trim()` so the long-form prose is not round-tripped
|
|
195
|
-
// through judge JSON. Short extracted fields like `question` and
|
|
196
|
-
// `taskDescription` keep the existing extract-and-validate path.
|
|
197
|
-
const verbatim = finalText.trim();
|
|
198
|
-
for (const field of extractRequiredFields(input.result[guard])) {
|
|
199
|
-
if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
|
|
200
|
-
obj[field] = verbatim;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (typeof obj[field] !== 'string') {
|
|
204
|
-
if (guard === 'needsBossReply' && field === 'question') {
|
|
205
|
-
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
206
|
-
}
|
|
207
|
-
throw new Error(`adjudicate: judge response missing required field "${field}" for guard "${guard}"`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return obj;
|
|
211
|
-
}
|
|
212
182
|
function extractRequiredFields(description) {
|
|
213
183
|
const fields = [];
|
|
214
184
|
const re = /Output shall include `([A-Za-z_][A-Za-z0-9_]*):/g;
|
|
@@ -237,145 +207,23 @@ function buildJudgePrompt(input, finalText) {
|
|
|
237
207
|
}
|
|
238
208
|
return lines.join('\n');
|
|
239
209
|
}
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
// the
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// own object check. Only a reply from which no JSON value can be
|
|
258
|
-
// recovered is treated as malformed and throws, preserving the
|
|
259
|
-
// control-plane error contract (PBRT-7, PBRT-10).
|
|
260
|
-
function parseJudgeJson(raw) {
|
|
261
|
-
const fenced = stripCodeFence(raw.trim());
|
|
262
|
-
// Fast path: a well-formed (optionally fenced) JSON body.
|
|
263
|
-
try {
|
|
264
|
-
return JSON.parse(fenced);
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
// Fall through to lenient extraction + repair.
|
|
268
|
-
}
|
|
269
|
-
const starts = [];
|
|
270
|
-
for (let i = 0; i < fenced.length; i++) {
|
|
271
|
-
const ch = fenced[i];
|
|
272
|
-
if (ch === '{' || ch === '[')
|
|
273
|
-
starts.push(i);
|
|
274
|
-
}
|
|
275
|
-
// Walk starts in document order. At each start prefer a strict
|
|
276
|
-
// balanced span (most trustworthy) and fall back to a repaired one
|
|
277
|
-
// for a trailing-comma / truncated tail, so the earliest intended
|
|
278
|
-
// object wins even when it needs repair. Return the first plain
|
|
279
|
-
// object; remember the first value of any shape so a legitimately
|
|
280
|
-
// array/scalar reply still surfaces to the caller's own object check.
|
|
281
|
-
let firstValue;
|
|
282
|
-
for (const start of starts) {
|
|
283
|
-
let parsedHere;
|
|
284
|
-
for (const repair of [false, true]) {
|
|
285
|
-
const candidate = extractJsonValue(fenced, start, repair);
|
|
286
|
-
if (candidate === undefined)
|
|
287
|
-
continue;
|
|
288
|
-
try {
|
|
289
|
-
parsedHere = { value: JSON.parse(candidate) };
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
continue; // not parseable this way — try repair, then next start
|
|
293
|
-
}
|
|
294
|
-
break; // prefer the strict span at this start over its repair
|
|
295
|
-
}
|
|
296
|
-
if (parsedHere === undefined)
|
|
297
|
-
continue;
|
|
298
|
-
if (isPlainObject(parsedHere.value))
|
|
299
|
-
return parsedHere.value;
|
|
300
|
-
if (firstValue === undefined)
|
|
301
|
-
firstValue = parsedHere;
|
|
302
|
-
}
|
|
303
|
-
if (firstValue !== undefined)
|
|
304
|
-
return firstValue.value;
|
|
305
|
-
throw new Error('adjudicate: judge response is not valid JSON');
|
|
306
|
-
}
|
|
307
|
-
function isPlainObject(value) {
|
|
308
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
309
|
-
}
|
|
310
|
-
// Strip a single Markdown code fence that wraps the whole string.
|
|
311
|
-
function stripCodeFence(text) {
|
|
312
|
-
const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/);
|
|
313
|
-
return fence ? fence[1].trim() : text;
|
|
314
|
-
}
|
|
315
|
-
// Scan from `start` (a `{`/`[` index), tracking string and
|
|
316
|
-
// bracket-nesting state, and emit the balanced JSON value rooted
|
|
317
|
-
// there. Anything after the top-level value closes is ignored (so a
|
|
318
|
-
// trailing code fence or commentary does not matter). With
|
|
319
|
-
// `repair === false` the span is returned only if it actually closes,
|
|
320
|
-
// and trailing commas are left intact — so the caller can prefer a
|
|
321
|
-
// cleanly-balanced span before attempting repair; if input ends
|
|
322
|
-
// before the value closes, undefined is returned. With
|
|
323
|
-
// `repair === true` common damage is fixed: a trailing comma before a
|
|
324
|
-
// close is removed, an unterminated string is closed, and any
|
|
325
|
-
// brackets still open at end-of-input are closed in order.
|
|
326
|
-
function extractJsonValue(text, start, repair) {
|
|
327
|
-
const stack = [];
|
|
328
|
-
let out = '';
|
|
329
|
-
let inString = false;
|
|
330
|
-
let escaped = false;
|
|
331
|
-
for (let i = start; i < text.length; i++) {
|
|
332
|
-
const ch = text[i];
|
|
333
|
-
if (inString) {
|
|
334
|
-
out += ch;
|
|
335
|
-
if (escaped)
|
|
336
|
-
escaped = false;
|
|
337
|
-
else if (ch === '\\')
|
|
338
|
-
escaped = true;
|
|
339
|
-
else if (ch === '"')
|
|
340
|
-
inString = false;
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
if (ch === '"') {
|
|
344
|
-
inString = true;
|
|
345
|
-
out += ch;
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (ch === '{' || ch === '[') {
|
|
349
|
-
stack.push(ch === '{' ? '}' : ']');
|
|
350
|
-
out += ch;
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
if (ch === '}' || ch === ']') {
|
|
354
|
-
if (repair)
|
|
355
|
-
out = dropTrailingComma(out);
|
|
356
|
-
out += ch;
|
|
357
|
-
stack.pop();
|
|
358
|
-
if (stack.length === 0)
|
|
359
|
-
return out; // top-level value complete
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
out += ch;
|
|
363
|
-
}
|
|
364
|
-
// End of input before the top-level value closed.
|
|
365
|
-
if (!repair)
|
|
366
|
-
return undefined; // strict pass: no balanced span here
|
|
367
|
-
if (inString)
|
|
368
|
-
out += '"';
|
|
369
|
-
out = dropTrailingComma(out);
|
|
370
|
-
while (stack.length > 0)
|
|
371
|
-
out += stack.pop();
|
|
372
|
-
return out;
|
|
373
|
-
}
|
|
374
|
-
// Remove a trailing comma (and any whitespace after it) at the end of
|
|
375
|
-
// the accumulated output, so `{"a":1,}` / `[1,2,]` and truncated
|
|
376
|
-
// `{"a":1,` repair to valid JSON.
|
|
377
|
-
function dropTrailingComma(out) {
|
|
378
|
-
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));
|
|
379
227
|
}
|
|
380
228
|
// Boss-event classifier — DR-004 §3.
|
|
381
229
|
// Every non-empty Boss turn goes through ports.callJudge. Slash-prefixed
|
|
@@ -384,19 +232,22 @@ function dropTrailingComma(out) {
|
|
|
384
232
|
// classifier is state-aware so awaitBossReply can distinguish a direct
|
|
385
233
|
// answer (BOSS_REPLY) from a fresh directive that abandons the pending
|
|
386
234
|
// question through the FSM's existing transitions.
|
|
387
|
-
async function classifyBossText(text, ports, signal, snapshotOrState) {
|
|
235
|
+
async function classifyBossText(text, ports, signal, snapshotOrState, boundary) {
|
|
388
236
|
const trimmed = text.trim();
|
|
389
237
|
if (trimmed === '')
|
|
390
238
|
return undefined;
|
|
391
|
-
return classifyWithLlm(text, ports, signal, snapshotOrState);
|
|
239
|
+
return classifyWithLlm(text, ports, signal, snapshotOrState, boundary);
|
|
392
240
|
}
|
|
393
241
|
const rootEvents = enumerateRootEvents(codingMachine);
|
|
394
242
|
const bossInterruptTargets = rootEvents.bossInterruptTargetDescriptions;
|
|
395
243
|
const bossInterruptTargetIds = new Set(bossInterruptTargets.map((target) => target.stateId));
|
|
396
|
-
async function classifyWithLlm(text, ports, signal, snapshotOrState) {
|
|
244
|
+
async function classifyWithLlm(text, ports, signal, snapshotOrState, boundary) {
|
|
397
245
|
const state = classifierState(snapshotOrState);
|
|
398
246
|
const prompt = buildClassifierPrompt(text, state);
|
|
399
|
-
const
|
|
247
|
+
const stateId = typeof state.value === 'string' ? state.value : undefined;
|
|
248
|
+
const raw = boundary
|
|
249
|
+
? await boundary.callJudge('boss-input-classification', stateId, prompt, signal)
|
|
250
|
+
: await ports.callJudge(prompt, signal);
|
|
400
251
|
let parsed;
|
|
401
252
|
try {
|
|
402
253
|
parsed = parseJudgeJson(raw);
|
|
@@ -479,7 +330,26 @@ async function classifyWithLlm(text, ports, signal, snapshotOrState) {
|
|
|
479
330
|
await ports.emitStatus('Classifier omitted answer for BOSS_REPLY');
|
|
480
331
|
return undefined;
|
|
481
332
|
}
|
|
482
|
-
|
|
333
|
+
const pending = pendingBossQuestionFromContext(state.context);
|
|
334
|
+
if (!pending) {
|
|
335
|
+
await ports.emitStatus('Classifier returned BOSS_REPLY without a pending question');
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
if (payload.questionId !== undefined &&
|
|
339
|
+
typeof payload.questionId !== 'string') {
|
|
340
|
+
await ports.emitStatus('Classifier supplied a non-string questionId for BOSS_REPLY');
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
if (typeof payload.questionId === 'string' &&
|
|
344
|
+
payload.questionId !== pending.questionId) {
|
|
345
|
+
await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${payload.questionId}`);
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
type: 'BOSS_REPLY',
|
|
350
|
+
answer: payload.answer,
|
|
351
|
+
questionId: pending.questionId,
|
|
352
|
+
};
|
|
483
353
|
}
|
|
484
354
|
default:
|
|
485
355
|
await ports.emitStatus(`Classifier returned unknown event type: ${eventType}`);
|
|
@@ -503,7 +373,9 @@ function classifierState(snapshotOrState) {
|
|
|
503
373
|
return { value: snapshotOrState, context: {} };
|
|
504
374
|
}
|
|
505
375
|
function buildClassifierPrompt(text, state) {
|
|
506
|
-
const currentState = typeof state.value === 'string'
|
|
376
|
+
const currentState = typeof state.value === 'string'
|
|
377
|
+
? state.value
|
|
378
|
+
: JSON.stringify(state.value ?? null);
|
|
507
379
|
const pendingBossQuestion = pendingBossQuestionFromContext(state.context);
|
|
508
380
|
const lines = [
|
|
509
381
|
'Classify the following Boss message into exactly one of these events.',
|
|
@@ -513,14 +385,14 @@ function buildClassifierPrompt(text, state) {
|
|
|
513
385
|
`Current state: ${currentState}`,
|
|
514
386
|
];
|
|
515
387
|
if (pendingBossQuestion !== undefined) {
|
|
516
|
-
lines.push(`Pending
|
|
388
|
+
lines.push(`Pending question id: ${pendingBossQuestion.questionId}`, `Pending asking player: ${pendingBossQuestion.player}`, `Pending Boss question: ${pendingBossQuestion.question}`);
|
|
517
389
|
}
|
|
518
390
|
lines.push('', 'Events:', '- START_CODING: payload { intent: "<free-form goal>" }', '- CONTINUE_IR: payload { irNumber: "<number>" }', '- SUMMARIZE_IR: payload { irNumber: "<number>" }', '- BOSS_INTERRUPT: payload { targetId: "<stateId>", intent?: "<free-form goal>", irNumber?: "<number>" }', ' targetId must be one of these jumpable states:');
|
|
519
391
|
for (const target of bossInterruptTargets) {
|
|
520
392
|
lines.push(` - ${target.stateId}: ${target.description}`);
|
|
521
393
|
}
|
|
522
394
|
if (currentState === 'awaitBossReply') {
|
|
523
|
-
lines.push('- BOSS_REPLY: payload { answer: "<verbatim Boss answer>" }');
|
|
395
|
+
lines.push('- BOSS_REPLY: payload { answer: "<verbatim Boss answer>", questionId?: "<pending question id>" }');
|
|
524
396
|
}
|
|
525
397
|
else {
|
|
526
398
|
lines.push('- BOSS_REPLY: valid only when Current state is awaitBossReply');
|
|
@@ -528,36 +400,28 @@ function buildClassifierPrompt(text, state) {
|
|
|
528
400
|
lines.push('', 'Boss message:', '```', text, '```');
|
|
529
401
|
return lines.join('\n');
|
|
530
402
|
}
|
|
531
|
-
//
|
|
532
|
-
// codingMachine invokes from every
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
403
|
+
// Delegated-player actor bridge — DR-004 §7. One PromiseActorLogic that the
|
|
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
|
|
410
|
+
// failures). Captain remains the orchestrator and adjudicator; it is not
|
|
411
|
+
// encoded as the delegated FSM actor.
|
|
538
412
|
//
|
|
539
413
|
// `getActiveSignal` is the runtime's hook for flowing the Boss's
|
|
540
414
|
// `handleBossInput.signal` into the host port calls — fromPromise
|
|
541
415
|
// hands the bridge XState's actor-scoped signal, which only fires
|
|
542
416
|
// on actor.stop(), not on Boss abort. When omitted (e.g. direct
|
|
543
417
|
// captainBridge tests), the bridge falls back to XState's signal.
|
|
544
|
-
function captainBridge(ports, getActiveSignal) {
|
|
545
|
-
return
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
throw new Error(result.error ??
|
|
552
|
-
`captainBridge: callPlayer status "${result.status}"`);
|
|
553
|
-
}
|
|
554
|
-
if (result.finalText === undefined) {
|
|
555
|
-
throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
|
|
556
|
-
}
|
|
557
|
-
const output = await adjudicate(input, result.finalText, ports, activeSignal);
|
|
558
|
-
validateBossReplyOutput(input, output);
|
|
559
|
-
return output;
|
|
560
|
-
});
|
|
418
|
+
function captainBridge(ports, getActiveSignal, boundary, onControlPlaneError) {
|
|
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);
|
|
561
425
|
}
|
|
562
426
|
// Captain pane display — PBRT-3 / PBRT-14.
|
|
563
427
|
// The Captain pane is a stream of three glyphs plus one bare
|
|
@@ -565,7 +429,7 @@ function captainBridge(ports, getActiveSignal) {
|
|
|
565
429
|
// glance:
|
|
566
430
|
// (no glyph) bare FSM event type — host renders as captain speech
|
|
567
431
|
// (e.g., `captain> START_CODING`)
|
|
568
|
-
// ⤷
|
|
432
|
+
// ⤷ player-invoking state entry: `<Player>: <label>`
|
|
569
433
|
// → transition guard outcome (`· field=N` tallies
|
|
570
434
|
// appended); the host presenter owns any visual
|
|
571
435
|
// nesting under the preceding ⤷ entry
|
|
@@ -602,29 +466,14 @@ const stateMetadata = (() => {
|
|
|
602
466
|
for (const s of enumerateCaptainStates(codingMachine)) {
|
|
603
467
|
const label = STATE_LABELS[s.stateId];
|
|
604
468
|
if (!label) {
|
|
605
|
-
throw new Error(`code.playbook.ts: STATE_LABELS missing entry for
|
|
469
|
+
throw new Error(`code.playbook.ts: STATE_LABELS missing entry for player-invoking state '${s.stateId}'`);
|
|
606
470
|
}
|
|
607
471
|
const input = s.getInput({});
|
|
608
472
|
m.set(s.stateId, { player: input.player, sourceItem: s.sourceItem, label });
|
|
609
473
|
}
|
|
610
474
|
return m;
|
|
611
475
|
})();
|
|
612
|
-
const stateIdBySourceItem = new Map([...stateMetadata.entries()].map(([stateId, meta]) => [
|
|
613
|
-
meta.sourceItem,
|
|
614
|
-
stateId,
|
|
615
|
-
]));
|
|
616
476
|
const registeredResumableStateIds = new Set(enumerateAwaitBossReply(codingMachine).bossReplyTransitions.map((transition) => transition.target));
|
|
617
|
-
function validateBossReplyOutput(input, output) {
|
|
618
|
-
if (output.guard !== 'needsBossReply')
|
|
619
|
-
return;
|
|
620
|
-
if (typeof output.question !== 'string') {
|
|
621
|
-
throw new Error(BOSS_REPLY_ERRORS.missingQuestion);
|
|
622
|
-
}
|
|
623
|
-
const stateId = stateIdBySourceItem.get(input.sourceItem);
|
|
624
|
-
if (stateId === undefined || !registeredResumableStateIds.has(stateId)) {
|
|
625
|
-
throw new Error(BOSS_REPLY_ERRORS.unregisteredState(stateId ?? input.sourceItem));
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
477
|
const QUIESCENT_STATES = new Set([
|
|
629
478
|
'ready',
|
|
630
479
|
'awaitBossReply',
|
|
@@ -635,11 +484,8 @@ const QUIESCENT_STATES = new Set([
|
|
|
635
484
|
// pane per PBRT-3: the readline returning to its `boss>` prompt is
|
|
636
485
|
// the implicit "turn over" signal, so a `◆ ready` / `◆ done`
|
|
637
486
|
// tombstone is redundant.
|
|
638
|
-
const SUPPRESSED_ENTRY_STATES = new Set([
|
|
639
|
-
|
|
640
|
-
'done',
|
|
641
|
-
]);
|
|
642
|
-
// Captain-pane surface (PBRT-3): every captain-invoking state plus
|
|
487
|
+
const SUPPRESSED_ENTRY_STATES = new Set(['ready', 'done']);
|
|
488
|
+
// Captain-pane surface (PBRT-3): every player-invoking state plus
|
|
643
489
|
// the quiescent states whose entry still carries information
|
|
644
490
|
// (failure with `lastError`, awaitBossReply with the pending
|
|
645
491
|
// question). `ready` and `done` flow through the inspect handler
|
|
@@ -656,12 +502,14 @@ function pendingBossQuestionFromContext(context) {
|
|
|
656
502
|
}
|
|
657
503
|
const candidate = pending;
|
|
658
504
|
if (typeof candidate.resumeStateId !== 'string' ||
|
|
505
|
+
typeof candidate.questionId !== 'string' ||
|
|
659
506
|
typeof candidate.sourceItem !== 'string' ||
|
|
660
507
|
typeof candidate.player !== 'string' ||
|
|
661
508
|
typeof candidate.question !== 'string') {
|
|
662
509
|
return undefined;
|
|
663
510
|
}
|
|
664
511
|
return {
|
|
512
|
+
questionId: candidate.questionId,
|
|
665
513
|
resumeStateId: candidate.resumeStateId,
|
|
666
514
|
sourceItem: candidate.sourceItem,
|
|
667
515
|
player: candidate.player,
|
|
@@ -726,9 +574,9 @@ function formatClassification(eventType) {
|
|
|
726
574
|
}
|
|
727
575
|
function stateTelemetryPayload(from, to, event, context) {
|
|
728
576
|
const payload = {
|
|
729
|
-
from,
|
|
577
|
+
from: from ?? null,
|
|
730
578
|
to,
|
|
731
|
-
event: normalizeEventForTelemetry(event),
|
|
579
|
+
event: normalizeEventForTelemetry(event) ?? null,
|
|
732
580
|
};
|
|
733
581
|
if (to === 'awaitBossReply') {
|
|
734
582
|
const pendingBossQuestion = pendingBossQuestionFromContext(context);
|
|
@@ -744,10 +592,40 @@ function stateTelemetryPayload(from, to, event, context) {
|
|
|
744
592
|
}
|
|
745
593
|
return payload;
|
|
746
594
|
}
|
|
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 });
|
|
607
|
+
}
|
|
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
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return statuses;
|
|
626
|
+
}
|
|
747
627
|
// Internal export surface for tests. Not part of the stable public API;
|
|
748
|
-
// the leading underscore signals "subject to change."
|
|
749
|
-
// referenced here so `noUnusedLocals` stays clean while later tasks
|
|
750
|
-
// wire the factory body to use them.
|
|
628
|
+
// the leading underscore signals "subject to change."
|
|
751
629
|
export const _internal = {
|
|
752
630
|
composePlayerPrompt,
|
|
753
631
|
resolvePlayerId,
|
|
@@ -768,192 +646,25 @@ export const _internal = {
|
|
|
768
646
|
normalizeEventForTelemetry,
|
|
769
647
|
VERBATIM_PAYLOAD_FIELDS,
|
|
770
648
|
};
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
await emitQueue.shift()();
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
// Suppress host-side emission errors; the control plane
|
|
797
|
-
// surfaces real failures via handleBossInput throws.
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
drainer = undefined;
|
|
801
|
-
})();
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
function drainEmissions() {
|
|
805
|
-
return drainer ?? Promise.resolve();
|
|
806
|
-
}
|
|
807
|
-
function buildActor(ports) {
|
|
808
|
-
priorState = undefined;
|
|
809
|
-
return createActor(codingMachine.provide({
|
|
810
|
-
actors: { captain: captainBridge(ports, () => activeSignal) },
|
|
811
|
-
}), {
|
|
812
|
-
input: options,
|
|
813
|
-
inspect: (inspectionEvent) => {
|
|
814
|
-
if (inspectionEvent.type !== '@xstate.snapshot')
|
|
815
|
-
return;
|
|
816
|
-
const snap = inspectionEvent.snapshot;
|
|
817
|
-
// Filter out captain sub-actor (fromPromise) snapshots —
|
|
818
|
-
// only the root codingMachine snapshot has a string value.
|
|
819
|
-
if (typeof snap.value !== 'string')
|
|
820
|
-
return;
|
|
821
|
-
const to = snap.value;
|
|
822
|
-
if (priorState === to)
|
|
823
|
-
return;
|
|
824
|
-
const from = priorState;
|
|
825
|
-
priorState = to;
|
|
826
|
-
// Telemetry on every transition (PBRT-14).
|
|
827
|
-
const context = snap.context ?? {};
|
|
828
|
-
enqueueEmit(() => ports.emitTelemetry({
|
|
829
|
-
topic: 'playbook.fsm.state',
|
|
830
|
-
payload: stateTelemetryPayload(from, to, inspectionEvent.event, context),
|
|
831
|
-
}));
|
|
832
|
-
// Captain pane (PBRT-3 / PBRT-14): show the transition
|
|
833
|
-
// guard first (when this is an actor-done transition with
|
|
834
|
-
// a known guard), then the new state entry, then any
|
|
835
|
-
// context riders the entering state cares about. Terminal
|
|
836
|
-
// entry to `failed` carries `lastError` as the data arg.
|
|
837
|
-
if (!CAPTAIN_PANE_STATES.has(to))
|
|
838
|
-
return;
|
|
839
|
-
const transitionLine = formatTransition(inspectionEvent.event);
|
|
840
|
-
if (transitionLine !== undefined) {
|
|
841
|
-
enqueueEmit(() => ports.emitStatus(transitionLine));
|
|
842
|
-
}
|
|
843
|
-
// awaitBossReply surfaces two lines per PBRT-3 / PBRT-14: the
|
|
844
|
-
// full player question as captain speech, then the rider-less
|
|
845
|
-
// routing marker. The full-question telemetry rides
|
|
846
|
-
// stateTelemetryPayload above.
|
|
847
|
-
if (to === 'awaitBossReply') {
|
|
848
|
-
const questionLine = formatAwaitBossReplyQuestion(context);
|
|
849
|
-
const markerLine = formatAwaitBossReplyMarker(context);
|
|
850
|
-
enqueueEmit(() => ports.emitStatus(questionLine));
|
|
851
|
-
enqueueEmit(() => ports.emitStatus(markerLine));
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
const entryLine = formatStateEntry(to);
|
|
855
|
-
if (entryLine === undefined)
|
|
856
|
-
return;
|
|
857
|
-
if (to === 'failed') {
|
|
858
|
-
const lastError = snap.context
|
|
859
|
-
?.lastError;
|
|
860
|
-
const data = { lastError: normalizeErrorCompact(lastError) };
|
|
861
|
-
enqueueEmit(() => ports.emitStatus(entryLine, data));
|
|
862
|
-
}
|
|
863
|
-
else {
|
|
864
|
-
enqueueEmit(() => ports.emitStatus(entryLine));
|
|
865
|
-
}
|
|
866
|
-
},
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
const runtime = {
|
|
870
|
-
async init(ports) {
|
|
871
|
-
savedPorts = ports;
|
|
872
|
-
actor = buildActor(ports);
|
|
873
|
-
actor.start();
|
|
874
|
-
await drainEmissions();
|
|
875
|
-
},
|
|
876
|
-
async handleBossInput({ text, signal, }) {
|
|
877
|
-
if (!actor || !savedPorts) {
|
|
878
|
-
throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
|
|
879
|
-
}
|
|
880
|
-
activeSignal = signal;
|
|
881
|
-
try {
|
|
882
|
-
// 1. Classify non-empty text into an FSM event through the judge.
|
|
883
|
-
const event = await classifyBossText(text, savedPorts, signal, actor.getSnapshot());
|
|
884
|
-
// Empty input, no-action classifier output, or invalid classifier
|
|
885
|
-
// output — nothing to send.
|
|
886
|
-
if (event === undefined) {
|
|
887
|
-
await drainEmissions();
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
// 2. Captain-pane classification line (PBRT-14): the bare
|
|
891
|
-
// FSM event type, emitted before the FSM advances so the
|
|
892
|
-
// host can render it as captain speech (e.g.,
|
|
893
|
-
// `captain> START_CODING`). Enqueued so it interleaves
|
|
894
|
-
// cleanly with the inspect-driven transition emissions.
|
|
895
|
-
const echoPorts = savedPorts;
|
|
896
|
-
enqueueEmit(() => echoPorts.emitStatus(formatClassification(event.type)));
|
|
897
|
-
// 3. final state ('done') cannot accept new events — dispose
|
|
898
|
-
// and reconstruct per DR-004 §5.
|
|
899
|
-
if (actor.getSnapshot().status === 'done') {
|
|
900
|
-
actor.stop();
|
|
901
|
-
actor = buildActor(savedPorts);
|
|
902
|
-
actor.start();
|
|
903
|
-
}
|
|
904
|
-
// 4. Send the event.
|
|
905
|
-
actor.send(event);
|
|
906
|
-
// 5. Drive to quiescence. On signal-abort we take no FSM
|
|
907
|
-
// action: the captain bridge's awaited callPlayer rejects
|
|
908
|
-
// naturally, the bridge throws, XState routes through
|
|
909
|
-
// onError → #failed, and this loop sees the quiescent
|
|
910
|
-
// snapshot and returns (DR-004 §8 natural rejection).
|
|
911
|
-
await driveToQuiescence(actor);
|
|
912
|
-
// Drain transition emissions before returning so the Boss
|
|
913
|
-
// sees the final status line for this turn.
|
|
914
|
-
await drainEmissions();
|
|
915
|
-
}
|
|
916
|
-
finally {
|
|
917
|
-
activeSignal = undefined;
|
|
918
|
-
}
|
|
919
|
-
},
|
|
920
|
-
async dispose() {
|
|
921
|
-
if (actor) {
|
|
922
|
-
actor.stop();
|
|
923
|
-
actor = undefined;
|
|
924
|
-
}
|
|
925
|
-
// Drain any in-flight emissions per slc/link.md §Session
|
|
926
|
-
// lifecycle ("stop the actor and drain pending port emissions").
|
|
927
|
-
await drainEmissions();
|
|
928
|
-
savedPorts = undefined;
|
|
929
|
-
},
|
|
930
|
-
// @internal — test-only escape hatch for inspecting the
|
|
931
|
-
// underlying actor's snapshot. Most state assertions are now
|
|
932
|
-
// expressible via the recorded emitStatus / emitTelemetry
|
|
933
|
-
// calls (DR-004 §9); the hatch stays for the few cases where
|
|
934
|
-
// direct context inspection is clearer (e.g., the dispose
|
|
935
|
-
// teardown test).
|
|
936
|
-
_getActor() {
|
|
937
|
-
return actor;
|
|
938
|
-
},
|
|
939
|
-
};
|
|
940
|
-
return runtime;
|
|
941
|
-
}
|
|
942
|
-
function driveToQuiescence(actor) {
|
|
943
|
-
return new Promise((resolve) => {
|
|
944
|
-
if (isQuiescent(actor.getSnapshot())) {
|
|
945
|
-
resolve();
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
const sub = actor.subscribe((snap) => {
|
|
949
|
-
if (isQuiescent(snap)) {
|
|
950
|
-
sub.unsubscribe();
|
|
951
|
-
resolve();
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
});
|
|
955
|
-
}
|
|
956
|
-
function isQuiescent(snap) {
|
|
957
|
-
const v = snap.value;
|
|
958
|
-
return typeof v === 'string' && QUIESCENT_STATES.has(v);
|
|
959
|
-
}
|
|
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;
|