@sublang/playbook 7.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/docs/cli.md +74 -29
- package/docs/configuration.md +209 -112
- package/docs/embedding.md +71 -25
- package/package.json +4 -3
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +3 -3
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +1078 -116
- package/reference/sdlc/code.playbook/bin/playbook.js +489 -34
- package/reference/sdlc/code.playbook/bin/run.js +283 -298
- package/reference/sdlc/code.playbook/bin/session-store.js +818 -26
- package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.js +7 -11
- package/reference/sdlc/code.playbook/code.fsm.ts +9 -17
- package/reference/sdlc/code.playbook/code.gears.md +1 -1
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
- package/reference/sdlc/code.playbook/code.playbook.js +12 -13
- package/reference/sdlc/code.playbook/code.playbook.ts +22 -15
- package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
- package/reference/sdlc/code.playbook/code.registry.js +3 -10
- package/reference/sdlc/code.playbook/code.registry.ts +7 -32
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +39 -14
- package/reference/sdlc/code.playbook/playbook-captain.js +970 -289
- package/reference/sdlc/code.playbook/playbook-captain.ts +1403 -396
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +41 -49
- package/reference/sdlc/decide.md +4 -4
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +9 -9
- package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
- package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
- package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +9 -13
- package/reference/sdlc/decide.playbook/decide.playbook.js +171 -134
- package/reference/sdlc/decide.playbook/decide.playbook.ts +238 -162
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
- package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
- package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
- package/reference/sdlc/review.md +4 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
- package/reference/sdlc/review.playbook/review.fsm.js +30 -24
- package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
- package/reference/sdlc/review.playbook/review.gears.md +6 -5
- package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
- package/reference/sdlc/review.playbook/review.playbook.js +16 -21
- package/reference/sdlc/review.playbook/review.playbook.ts +26 -26
- package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
- package/reference/sdlc/review.playbook/review.registry.js +3 -16
- package/reference/sdlc/review.playbook/review.registry.ts +7 -38
- package/slc/gears2fsm.md +27 -23
- package/slc/link.md +113 -93
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +20 -16
- package/src/runtime.ts +19 -23
- package/src/xstate-playbook-runtime.d.ts +21 -17
- package/src/xstate-playbook-runtime.js +241 -149
- package/src/xstate-playbook-runtime.ts +331 -178
- package/src/xstate-runtime.js +63 -24
- package/src/xstate-runtime.ts +96 -28
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// Linker inputs:
|
|
8
8
|
// FSM artifact: ./decide.fsm.ts
|
|
9
9
|
// Link target: @sublang/playbook/runtime
|
|
10
|
-
//
|
|
11
|
-
//
|
|
10
|
+
// Role binding: canonical coder and reviewer roles; concrete players
|
|
11
|
+
// and prompt identities are supplied by the host session
|
|
12
12
|
// Adjudication: LLM-judge per state (default)
|
|
13
13
|
// Boss-event mapping: free-text judge classification (default)
|
|
14
14
|
// Abort strategy: natural rejection; every player-invoking state's
|
|
@@ -19,41 +19,16 @@ import PQueue from 'p-queue';
|
|
|
19
19
|
import { createActor, fromPromise } from 'xstate';
|
|
20
20
|
import { assertJsonSafe, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
21
21
|
import decideMachine from './decide.fsm.js';
|
|
22
|
-
const DEFAULT_PLAYER_BINDING = {
|
|
23
|
-
Coder: 'coder',
|
|
24
|
-
Reviewer: 'reviewer',
|
|
25
|
-
};
|
|
26
22
|
function snapshotDecideRuntimeOptions(value) {
|
|
27
23
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
28
24
|
if (!isPlainObject(captured)) {
|
|
29
25
|
throw new TypeError('DECIDE runtime options must be an object');
|
|
30
26
|
}
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
throw new TypeError(`DECIDE runtime options.${key} is not declared`);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (typeof captured.coderLlm !== 'string' ||
|
|
38
|
-
captured.coderLlm.trim().length === 0) {
|
|
39
|
-
throw new TypeError('DECIDE runtime options.coderLlm must be a non-empty string');
|
|
27
|
+
const [unknown] = Object.keys(captured);
|
|
28
|
+
if (unknown !== undefined) {
|
|
29
|
+
throw new TypeError(`DECIDE runtime options.${unknown} is not declared`);
|
|
40
30
|
}
|
|
41
|
-
|
|
42
|
-
const playerBinding = captured.playerBinding;
|
|
43
|
-
if (!isPlainObject(playerBinding)) {
|
|
44
|
-
throw new TypeError('DECIDE runtime options.playerBinding must be an object');
|
|
45
|
-
}
|
|
46
|
-
const playerNames = new Set(['Coder', 'Reviewer']);
|
|
47
|
-
for (const [player, playerId] of Object.entries(playerBinding)) {
|
|
48
|
-
if (!playerNames.has(player)) {
|
|
49
|
-
throw new TypeError(`DECIDE runtime options.playerBinding.${player} is not declared`);
|
|
50
|
-
}
|
|
51
|
-
if (typeof playerId !== 'string' || playerId.trim().length === 0) {
|
|
52
|
-
throw new TypeError(`DECIDE runtime options.playerBinding.${player} must be a non-empty string`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return captured;
|
|
31
|
+
return Object.freeze({});
|
|
57
32
|
}
|
|
58
33
|
const STATE_DESCRIPTIONS = {
|
|
59
34
|
ready: 'Waiting for a topic to decide.',
|
|
@@ -68,31 +43,31 @@ const STATE_DESCRIPTIONS = {
|
|
|
68
43
|
reportedReviewFailure: 'DECIDE reports REVIEW’s failure and its last commit.',
|
|
69
44
|
done: 'DECIDE completed with an approved commit.',
|
|
70
45
|
};
|
|
71
|
-
const
|
|
72
|
-
{ stateId: 'askCoderProposal',
|
|
46
|
+
const ROLE_STATES = [
|
|
47
|
+
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
73
48
|
{
|
|
74
49
|
stateId: 'askReviewerProposal',
|
|
75
|
-
|
|
50
|
+
role: 'reviewer',
|
|
76
51
|
sourceItem: 'DECIDE-2',
|
|
77
52
|
},
|
|
78
|
-
{ stateId: 'commitCoderProposal',
|
|
53
|
+
{ stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
|
|
79
54
|
];
|
|
80
|
-
const
|
|
55
|
+
const ROLE_STATE_IDS = new Set(ROLE_STATES.map((state) => state.stateId));
|
|
56
|
+
const ROLE_IDS = ['coder', 'reviewer'];
|
|
57
|
+
const ROLE_ID_SET = new Set(ROLE_IDS);
|
|
58
|
+
const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
|
|
81
59
|
const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
|
|
82
60
|
const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
|
|
83
61
|
const TELEMETRY_TOPIC = 'playbook.fsm.state';
|
|
84
62
|
const TRACE_TOPIC = 'playbook.trace';
|
|
85
63
|
const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
|
|
86
|
-
const PLACEHOLDER_FIELDS = [
|
|
87
|
-
['<caller-topic>', 'callerTopic'],
|
|
88
|
-
['<coder-llm>', 'coderLlm'],
|
|
89
|
-
];
|
|
64
|
+
const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
|
|
90
65
|
const VERBATIM_PAYLOAD_FIELDS = new Set([
|
|
91
66
|
'coderProposal',
|
|
92
67
|
'reviewerProposal',
|
|
93
68
|
'coderOutput',
|
|
94
69
|
]);
|
|
95
|
-
function composePlayerPrompt(input) {
|
|
70
|
+
function composePlayerPrompt(input, promptIdentity) {
|
|
96
71
|
const blocks = [];
|
|
97
72
|
if (input.pendingBossQuestion && input.bossReply !== undefined) {
|
|
98
73
|
blocks.push([
|
|
@@ -111,6 +86,9 @@ function composePlayerPrompt(input) {
|
|
|
111
86
|
if (typeof value === 'string')
|
|
112
87
|
replacements.set(placeholder, value);
|
|
113
88
|
}
|
|
89
|
+
if (input.prompt.includes('<coder-llm>')) {
|
|
90
|
+
replacements.set('<coder-llm>', promptIdentity('coder'));
|
|
91
|
+
}
|
|
114
92
|
const body = input.prompt.replace(/<caller-topic>|<coder-llm>/g, (placeholder, offset, source) => {
|
|
115
93
|
const value = replacements.get(placeholder);
|
|
116
94
|
if (value === undefined)
|
|
@@ -123,18 +101,6 @@ function composePlayerPrompt(input) {
|
|
|
123
101
|
blocks.push(body);
|
|
124
102
|
return blocks.join('\n\n');
|
|
125
103
|
}
|
|
126
|
-
function resolvePlayerId(input, binding) {
|
|
127
|
-
switch (input.player) {
|
|
128
|
-
case 'Coder':
|
|
129
|
-
return binding.Coder;
|
|
130
|
-
case 'Reviewer':
|
|
131
|
-
return binding.Reviewer;
|
|
132
|
-
default: {
|
|
133
|
-
const exhaustive = input.player;
|
|
134
|
-
throw new Error(`unknown player ${String(exhaustive)}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
104
|
// A `result` description names required payload fields in its
|
|
139
105
|
// "Output shall include ..." sentence.
|
|
140
106
|
function requiredFieldsFor(description) {
|
|
@@ -283,7 +249,7 @@ function buildClassifierPrompt(text, ctx) {
|
|
|
283
249
|
if (ctx.pendingQuestions.length > 0) {
|
|
284
250
|
lines.push('Pending Boss questions:');
|
|
285
251
|
for (const pending of ctx.pendingQuestions) {
|
|
286
|
-
lines.push(`- ${pending.questionId} (${pending.
|
|
252
|
+
lines.push(`- ${pending.questionId} (${pending.asker.roleId}): ${pending.question}`);
|
|
287
253
|
}
|
|
288
254
|
lines.push('If the Boss message answers a pending question, classify it as BOSS_REPLY; if it is a fresh directive, classify it accordingly.');
|
|
289
255
|
}
|
|
@@ -354,7 +320,7 @@ function buildAdjudicatorPrompt(input, playerOutput) {
|
|
|
354
320
|
lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
|
|
355
321
|
'seek external evidence. Decide only from the supplied player output ' +
|
|
356
322
|
'and guard descriptions. Reply with exactly one JSON object and no prose.');
|
|
357
|
-
lines.push(`The
|
|
323
|
+
lines.push(`The role "${roleLabel(input.role)}" produced the output below for source item ${input.sourceItem}.`);
|
|
358
324
|
lines.push('Choose exactly one guard whose description matches that output.');
|
|
359
325
|
lines.push('');
|
|
360
326
|
lines.push('Player output (verbatim):');
|
|
@@ -452,7 +418,9 @@ function pendingQuestionsFromContext(context) {
|
|
|
452
418
|
obj.questionId === key &&
|
|
453
419
|
typeof obj.resumeStateId === 'string' &&
|
|
454
420
|
typeof obj.sourceItem === 'string' &&
|
|
455
|
-
|
|
421
|
+
isPlainObject(obj.asker) &&
|
|
422
|
+
obj.asker.kind === 'role' &&
|
|
423
|
+
ROLE_ID_SET.has(String(obj.asker.roleId)) &&
|
|
456
424
|
typeof obj.question === 'string') {
|
|
457
425
|
questions.push(obj);
|
|
458
426
|
}
|
|
@@ -468,7 +436,7 @@ const WAIT_STATE_IDS = new Set([
|
|
|
468
436
|
'awaitBossReply',
|
|
469
437
|
]);
|
|
470
438
|
const STATUS_STATE_IDS = new Set([
|
|
471
|
-
...
|
|
439
|
+
...ROLE_STATE_IDS,
|
|
472
440
|
...WAIT_STATE_IDS,
|
|
473
441
|
'failed',
|
|
474
442
|
]);
|
|
@@ -527,14 +495,7 @@ function telemetryPayload(previousState, state, event, context) {
|
|
|
527
495
|
return payload;
|
|
528
496
|
}
|
|
529
497
|
export const createPlaybookRuntime = (options) => {
|
|
530
|
-
const
|
|
531
|
-
const binding = {
|
|
532
|
-
...DEFAULT_PLAYER_BINDING,
|
|
533
|
-
...(boundOptions.playerBinding ?? {}),
|
|
534
|
-
};
|
|
535
|
-
const fsmInput = {
|
|
536
|
-
coderLlm: boundOptions.coderLlm,
|
|
537
|
-
};
|
|
498
|
+
const fsmInput = snapshotDecideRuntimeOptions(options);
|
|
538
499
|
let ports;
|
|
539
500
|
let sessionIdentity;
|
|
540
501
|
let actor;
|
|
@@ -554,9 +515,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
554
515
|
let disposalPromise;
|
|
555
516
|
let controlPlaneError;
|
|
556
517
|
let nestedBridge;
|
|
557
|
-
const
|
|
518
|
+
const privateResumeTokens = new Map();
|
|
558
519
|
const playbookCallTurnIds = new Map();
|
|
559
|
-
const
|
|
520
|
+
const inFlightPlayerKeys = new Set();
|
|
560
521
|
const activeBoundaryCalls = new Set();
|
|
561
522
|
const activeEmissionCalls = new Set();
|
|
562
523
|
const emissionQueue = new PQueue({ concurrency: 1 });
|
|
@@ -639,59 +600,130 @@ export const createPlaybookRuntime = (options) => {
|
|
|
639
600
|
}
|
|
640
601
|
return sessionIdentity;
|
|
641
602
|
};
|
|
642
|
-
const
|
|
603
|
+
const bindSession = (nextSession) => {
|
|
604
|
+
const bound = snapshotPlaybookSession(nextSession);
|
|
605
|
+
if (bound.roleBindings === undefined)
|
|
606
|
+
return bound;
|
|
607
|
+
const actual = Object.keys(bound.roleBindings).sort();
|
|
608
|
+
const expected = [...ROLE_IDS].sort();
|
|
609
|
+
const missing = expected.filter((roleId) => !actual.includes(roleId));
|
|
610
|
+
const extra = actual.filter((roleId) => !ROLE_ID_SET.has(roleId));
|
|
611
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
612
|
+
throw new TypeError(`DECIDE session roleBindings must cover exactly [${expected.join(', ')}]` +
|
|
613
|
+
`${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
|
|
614
|
+
`${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
|
|
615
|
+
}
|
|
616
|
+
return bound;
|
|
617
|
+
};
|
|
618
|
+
const resolvedPlayerId = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.playerId;
|
|
619
|
+
const promptIdentity = (roleId) => requireSessionIdentity().roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
620
|
+
const composeInvocationPrompt = (input) => {
|
|
621
|
+
let active = true;
|
|
622
|
+
const lookup = (roleId) => {
|
|
623
|
+
if (!active) {
|
|
624
|
+
throw new Error('DECIDE prompt identity lookup is no longer active for this invocation');
|
|
625
|
+
}
|
|
626
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
627
|
+
throw new TypeError(`DECIDE prompt identity lookup rejected undeclared role ${String(roleId)}`);
|
|
628
|
+
}
|
|
629
|
+
return promptIdentity(roleId);
|
|
630
|
+
};
|
|
631
|
+
try {
|
|
632
|
+
return composePlayerPrompt(input, lookup);
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
active = false;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const continuationKey = (roleId, playerId) => playerId ?? roleId;
|
|
639
|
+
const tokensByContinuationKey = (tokens) => {
|
|
640
|
+
const byKey = new Map();
|
|
641
|
+
for (const [roleId, token] of Object.entries(tokens)) {
|
|
642
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
643
|
+
throw new TypeError(`DECIDE role tokens contain unknown role ${roleId}`);
|
|
644
|
+
}
|
|
645
|
+
const typedRole = roleId;
|
|
646
|
+
const key = continuationKey(typedRole, resolvedPlayerId(typedRole));
|
|
647
|
+
const prior = byKey.get(key);
|
|
648
|
+
if (prior !== undefined && prior !== token) {
|
|
649
|
+
throw new TypeError(`DECIDE runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
|
|
650
|
+
}
|
|
651
|
+
byKey.set(key, token);
|
|
652
|
+
}
|
|
653
|
+
const rolesByKey = new Map();
|
|
654
|
+
for (const roleId of ROLE_IDS) {
|
|
655
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
656
|
+
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
657
|
+
}
|
|
658
|
+
for (const [key, roles] of rolesByKey) {
|
|
659
|
+
if (roles.length < 2)
|
|
660
|
+
continue;
|
|
661
|
+
const present = roles.filter((roleId) => tokens[roleId] !== undefined);
|
|
662
|
+
if (present.length !== 0 && present.length !== roles.length) {
|
|
663
|
+
throw new TypeError(`DECIDE role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return byKey;
|
|
667
|
+
};
|
|
668
|
+
const selectPlayerResume = (roleId, playerId) => {
|
|
643
669
|
const session = requireSessionIdentity();
|
|
644
670
|
const selected = session.playerSessions
|
|
645
|
-
? session.playerSessions.select(
|
|
646
|
-
:
|
|
671
|
+
? session.playerSessions.select(roleId)
|
|
672
|
+
: privateResumeTokens.get(continuationKey(roleId, playerId)) ?? false;
|
|
647
673
|
if (selected !== false &&
|
|
648
674
|
(typeof selected !== 'string' || selected.trim().length === 0)) {
|
|
649
|
-
throw new TypeError(`player session store returned an invalid resume token for ${
|
|
675
|
+
throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
|
|
650
676
|
}
|
|
651
677
|
return selected;
|
|
652
678
|
};
|
|
653
|
-
const updatePlayerResume = (playerId,
|
|
679
|
+
const updatePlayerResume = (roleId, playerId, result) => {
|
|
680
|
+
if (result.resumeToken === undefined && result.status !== 'ok')
|
|
681
|
+
return;
|
|
654
682
|
const session = requireSessionIdentity();
|
|
655
683
|
if (session.playerSessions) {
|
|
656
|
-
session.playerSessions.update(
|
|
684
|
+
session.playerSessions.update(roleId, result.resumeToken);
|
|
657
685
|
}
|
|
658
|
-
else if (resumeToken !== undefined
|
|
659
|
-
|
|
686
|
+
else if (result.resumeToken !== undefined) {
|
|
687
|
+
privateResumeTokens.set(continuationKey(roleId, playerId), result.resumeToken);
|
|
660
688
|
}
|
|
661
689
|
else {
|
|
662
|
-
|
|
690
|
+
privateResumeTokens.delete(continuationKey(roleId, playerId));
|
|
663
691
|
}
|
|
664
692
|
};
|
|
665
|
-
const
|
|
693
|
+
const snapshotRoleResumeTokens = () => {
|
|
666
694
|
const session = requireSessionIdentity();
|
|
667
695
|
const captured = snapshotJsonValue(session.playerSessions
|
|
668
696
|
? session.playerSessions.snapshot()
|
|
669
|
-
: Object.fromEntries(
|
|
697
|
+
: Object.fromEntries(ROLE_IDS.flatMap((roleId) => {
|
|
698
|
+
const token = privateResumeTokens.get(continuationKey(roleId, resolvedPlayerId(roleId)));
|
|
699
|
+
return token === undefined ? [] : [[roleId, token]];
|
|
700
|
+
})), 'player session store snapshot');
|
|
670
701
|
if (!isPlainObject(captured)) {
|
|
671
702
|
throw new TypeError('player session store snapshot must be an object');
|
|
672
703
|
}
|
|
673
704
|
const tokens = {};
|
|
674
|
-
for (const [
|
|
675
|
-
if (
|
|
676
|
-
throw new TypeError(
|
|
705
|
+
for (const [roleId, token] of Object.entries(captured)) {
|
|
706
|
+
if (!ROLE_ID_SET.has(roleId)) {
|
|
707
|
+
throw new TypeError(`player session store snapshot contains unknown role ${roleId}`);
|
|
677
708
|
}
|
|
678
709
|
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
679
|
-
throw new TypeError(`player session store snapshot token for ${
|
|
710
|
+
throw new TypeError(`player session store snapshot token for ${roleId} must be a non-empty string`);
|
|
680
711
|
}
|
|
681
|
-
tokens[
|
|
712
|
+
tokens[roleId] = token;
|
|
682
713
|
}
|
|
714
|
+
tokensByContinuationKey(tokens);
|
|
683
715
|
return tokens;
|
|
684
716
|
};
|
|
685
|
-
const
|
|
717
|
+
const restoreRoleResumeTokens = (tokens) => {
|
|
718
|
+
const byKey = tokensByContinuationKey(tokens);
|
|
686
719
|
const session = requireSessionIdentity();
|
|
687
720
|
if (session.playerSessions) {
|
|
688
721
|
session.playerSessions.restore(tokens);
|
|
689
722
|
return;
|
|
690
723
|
}
|
|
691
|
-
|
|
692
|
-
for (const [
|
|
693
|
-
|
|
694
|
-
}
|
|
724
|
+
privateResumeTokens.clear();
|
|
725
|
+
for (const [key, token] of byKey)
|
|
726
|
+
privateResumeTokens.set(key, token);
|
|
695
727
|
};
|
|
696
728
|
const currentState = (pendingCall = nestedBridge.getPendingCall()) => {
|
|
697
729
|
const live = actor;
|
|
@@ -710,7 +742,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
710
742
|
const identity = requireSessionIdentity();
|
|
711
743
|
const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
712
744
|
const trace = Object.freeze({
|
|
713
|
-
schemaVersion:
|
|
745
|
+
schemaVersion: 3,
|
|
714
746
|
sessionId: identity.sessionId,
|
|
715
747
|
playbookId: identity.playbookId,
|
|
716
748
|
rootSessionId: identity.rootSessionId,
|
|
@@ -812,15 +844,17 @@ export const createPlaybookRuntime = (options) => {
|
|
|
812
844
|
};
|
|
813
845
|
const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
|
|
814
846
|
const runPlayerCall = async (input, signal) => {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
throw new Error(`resolved player "${playerId}" already has an in-flight call`);
|
|
847
|
+
if (!ROLE_ID_SET.has(input.role)) {
|
|
848
|
+
throw new TypeError(`DECIDE player input role must name a declared local role`);
|
|
818
849
|
}
|
|
819
|
-
const
|
|
850
|
+
const roleId = input.role;
|
|
851
|
+
const playerId = resolvedPlayerId(roleId);
|
|
852
|
+
const playerKey = continuationKey(roleId, playerId);
|
|
853
|
+
const prompt = composeInvocationPrompt(input);
|
|
820
854
|
let resume;
|
|
821
855
|
try {
|
|
822
856
|
signal.throwIfAborted();
|
|
823
|
-
resume = selectPlayerResume(playerId);
|
|
857
|
+
resume = selectPlayerResume(roleId, playerId);
|
|
824
858
|
}
|
|
825
859
|
catch (error) {
|
|
826
860
|
latchControlPlaneError(error, signal);
|
|
@@ -828,10 +862,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
828
862
|
}
|
|
829
863
|
const callId = `player-${++playerCallSequence}`;
|
|
830
864
|
const identity = {
|
|
831
|
-
purpose: 'captain',
|
|
832
865
|
stateId: input.stateId,
|
|
833
866
|
sourceItem: input.sourceItem,
|
|
834
|
-
|
|
867
|
+
roleId,
|
|
868
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
835
869
|
resume,
|
|
836
870
|
};
|
|
837
871
|
const emitFailure = (error) => emitTrace('player.call.finished', {
|
|
@@ -842,13 +876,19 @@ export const createPlaybookRuntime = (options) => {
|
|
|
842
876
|
message: String(error),
|
|
843
877
|
},
|
|
844
878
|
}, { turnId: currentTurnId, callId });
|
|
845
|
-
|
|
879
|
+
if (inFlightPlayerKeys.has(playerKey)) {
|
|
880
|
+
const error = new Error(`resolved player key "${playerKey}" already has an in-flight call`);
|
|
881
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
882
|
+
await emitFailure(error);
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
inFlightPlayerKeys.add(playerKey);
|
|
846
886
|
try {
|
|
847
887
|
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
|
|
848
888
|
let rawResult;
|
|
849
889
|
try {
|
|
850
890
|
signal.throwIfAborted();
|
|
851
|
-
const boundary = Promise.resolve(requirePorts().callPlayer(
|
|
891
|
+
const boundary = Promise.resolve(requirePorts().callPlayer(roleId, prompt, signal, { resume }));
|
|
852
892
|
rawResult = await boundary;
|
|
853
893
|
// An XState sibling cancellation does not cancel an arbitrary coder
|
|
854
894
|
// promise. Re-check before a late resolution can mutate continuity or
|
|
@@ -885,10 +925,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
885
925
|
// The resolved result is authoritative even on aborted/error status.
|
|
886
926
|
// Update continuation state before interpreting that status.
|
|
887
927
|
try {
|
|
888
|
-
updatePlayerResume(playerId,
|
|
889
|
-
result.resumeToken.trim().length > 0
|
|
890
|
-
? result.resumeToken
|
|
891
|
-
: undefined);
|
|
928
|
+
updatePlayerResume(roleId, playerId, result);
|
|
892
929
|
}
|
|
893
930
|
catch (error) {
|
|
894
931
|
latchControlPlaneError(error, signal);
|
|
@@ -916,10 +953,14 @@ export const createPlaybookRuntime = (options) => {
|
|
|
916
953
|
? { error: normalizeErrorFull(result.error) }
|
|
917
954
|
: {}),
|
|
918
955
|
}, { turnId: currentTurnId, callId });
|
|
919
|
-
return {
|
|
956
|
+
return {
|
|
957
|
+
roleId,
|
|
958
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
959
|
+
result,
|
|
960
|
+
};
|
|
920
961
|
}
|
|
921
962
|
finally {
|
|
922
|
-
|
|
963
|
+
inFlightPlayerKeys.delete(playerKey);
|
|
923
964
|
}
|
|
924
965
|
};
|
|
925
966
|
const callPlayer = (input, signal) => {
|
|
@@ -938,7 +979,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
938
979
|
throw error;
|
|
939
980
|
}
|
|
940
981
|
combined.throwIfAborted();
|
|
941
|
-
let { playerId, result } = await callPlayer(input, combined);
|
|
982
|
+
let { roleId, playerId, result } = await callPlayer(input, combined);
|
|
942
983
|
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
943
984
|
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
944
985
|
// whitespace-only earns exactly one corrective re-ask — the same
|
|
@@ -949,14 +990,14 @@ export const createPlaybookRuntime = (options) => {
|
|
|
949
990
|
// are never retried), and a rejecting finish emission rejects
|
|
950
991
|
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
951
992
|
combined.throwIfAborted();
|
|
952
|
-
({ playerId, result } = await callPlayer(input, combined));
|
|
993
|
+
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
953
994
|
}
|
|
954
995
|
if (result.status !== 'ok') {
|
|
955
|
-
throw new Error(`
|
|
996
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
|
|
956
997
|
}
|
|
957
998
|
const finalText = result.finalText ?? '';
|
|
958
999
|
if (isEmptyFinalText(finalText)) {
|
|
959
|
-
throw new Error(`
|
|
1000
|
+
throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
|
|
960
1001
|
}
|
|
961
1002
|
combined.throwIfAborted();
|
|
962
1003
|
try {
|
|
@@ -1057,8 +1098,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1057
1098
|
if (WAIT_STATE_IDS.has(activeStateId)) {
|
|
1058
1099
|
const pending = questionForWaitState(activeStateId, pendingQuestions);
|
|
1059
1100
|
if (pending) {
|
|
1060
|
-
scheduleStatus(`${pending.
|
|
1061
|
-
scheduleStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.
|
|
1101
|
+
scheduleStatus(`${pending.asker.roleId} asks: ${pending.question}`, activeStateId);
|
|
1102
|
+
scheduleStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, activeStateId);
|
|
1062
1103
|
}
|
|
1063
1104
|
continue;
|
|
1064
1105
|
}
|
|
@@ -1068,10 +1109,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1068
1109
|
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
1069
1110
|
if (description === undefined)
|
|
1070
1111
|
continue;
|
|
1071
|
-
const
|
|
1072
|
-
scheduleStatus(
|
|
1112
|
+
const roleState = ROLE_STATES.find((candidate) => candidate.stateId === activeStateId);
|
|
1113
|
+
scheduleStatus(roleState === undefined
|
|
1073
1114
|
? '◆ workflow failed; awaiting Boss recovery.'
|
|
1074
|
-
: `⤷ ${
|
|
1115
|
+
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined ? undefined : { lastError });
|
|
1075
1116
|
}
|
|
1076
1117
|
}
|
|
1077
1118
|
catch (error) {
|
|
@@ -1239,8 +1280,8 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1239
1280
|
// The session-start error remains authoritative.
|
|
1240
1281
|
}
|
|
1241
1282
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1283
|
+
privateResumeTokens.clear();
|
|
1284
|
+
inFlightPlayerKeys.clear();
|
|
1244
1285
|
activeBoundaryCalls.clear();
|
|
1245
1286
|
activeEmissionCalls.clear();
|
|
1246
1287
|
emissionQueue.clear();
|
|
@@ -1270,7 +1311,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1270
1311
|
disposalPromise !== undefined) {
|
|
1271
1312
|
throw new Error('decide runtime: init(session) may only be called once');
|
|
1272
1313
|
}
|
|
1273
|
-
const identity =
|
|
1314
|
+
const identity = bindSession(session);
|
|
1274
1315
|
let finishInitialization;
|
|
1275
1316
|
const initialization = new Promise((resolve) => {
|
|
1276
1317
|
finishInitialization = resolve;
|
|
@@ -1348,10 +1389,10 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1348
1389
|
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
1349
1390
|
const context = actor.getSnapshot().context;
|
|
1350
1391
|
return {
|
|
1351
|
-
schemaVersion:
|
|
1392
|
+
schemaVersion: 3,
|
|
1352
1393
|
playbookId: sessionIdentity.playbookId,
|
|
1353
1394
|
machine,
|
|
1354
|
-
|
|
1395
|
+
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
1355
1396
|
sequences: {
|
|
1356
1397
|
trace: traceSequence,
|
|
1357
1398
|
turn: turnSequence,
|
|
@@ -1362,7 +1403,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1362
1403
|
state,
|
|
1363
1404
|
pendingBossQuestions: pendingQuestionsFromContext(context).map((pending) => ({
|
|
1364
1405
|
questionId: pending.questionId,
|
|
1365
|
-
|
|
1406
|
+
asker: pending.asker,
|
|
1366
1407
|
question: pending.question,
|
|
1367
1408
|
sourceItem: pending.sourceItem,
|
|
1368
1409
|
})),
|
|
@@ -1381,11 +1422,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1381
1422
|
disposalPromise !== undefined) {
|
|
1382
1423
|
throw new Error('decide runtime: restore(session, snapshot) may only be called once');
|
|
1383
1424
|
}
|
|
1384
|
-
const identity =
|
|
1425
|
+
const identity = bindSession(session);
|
|
1385
1426
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId, { allowSuspendedCall: true });
|
|
1386
|
-
const suspendedCall = boundSnapshot.
|
|
1387
|
-
? boundSnapshot.suspendedCall
|
|
1388
|
-
: undefined;
|
|
1427
|
+
const suspendedCall = boundSnapshot.suspendedCall;
|
|
1389
1428
|
let finishInitialization;
|
|
1390
1429
|
const initialization = new Promise((resolve) => {
|
|
1391
1430
|
finishInitialization = resolve;
|
|
@@ -1394,7 +1433,7 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1394
1433
|
lifecycleStarted = true;
|
|
1395
1434
|
ports = identity.ports;
|
|
1396
1435
|
sessionIdentity = identity;
|
|
1397
|
-
let
|
|
1436
|
+
let priorExternalRoleTokens;
|
|
1398
1437
|
try {
|
|
1399
1438
|
traceSequence = boundSnapshot.sequences.trace;
|
|
1400
1439
|
turnSequence = boundSnapshot.sequences.turn;
|
|
@@ -1402,9 +1441,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1402
1441
|
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
1403
1442
|
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
1404
1443
|
if (identity.playerSessions) {
|
|
1405
|
-
|
|
1444
|
+
priorExternalRoleTokens = snapshotRoleResumeTokens();
|
|
1406
1445
|
}
|
|
1407
|
-
|
|
1446
|
+
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
1408
1447
|
nestedBridge.prepareRestore(suspendedCall);
|
|
1409
1448
|
if (suspendedCall !== undefined) {
|
|
1410
1449
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
@@ -1430,9 +1469,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1430
1469
|
}
|
|
1431
1470
|
catch (error) {
|
|
1432
1471
|
let failure = error;
|
|
1433
|
-
if (
|
|
1472
|
+
if (priorExternalRoleTokens !== undefined) {
|
|
1434
1473
|
try {
|
|
1435
|
-
identity.playerSessions.restore(
|
|
1474
|
+
identity.playerSessions.restore(priorExternalRoleTokens);
|
|
1436
1475
|
}
|
|
1437
1476
|
catch (rollbackError) {
|
|
1438
1477
|
failure = new AggregateError([error, rollbackError], 'DECIDE restore and player continuation rollback failed');
|
|
@@ -1671,9 +1710,9 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1671
1710
|
collectFailure(failures, error);
|
|
1672
1711
|
}
|
|
1673
1712
|
finally {
|
|
1674
|
-
|
|
1713
|
+
privateResumeTokens.clear();
|
|
1675
1714
|
playbookCallTurnIds.clear();
|
|
1676
|
-
|
|
1715
|
+
inFlightPlayerKeys.clear();
|
|
1677
1716
|
activeBoundaryCalls.clear();
|
|
1678
1717
|
activeEmissionCalls.clear();
|
|
1679
1718
|
emissionQueue.clear();
|
|
@@ -1699,7 +1738,6 @@ export const createPlaybookRuntime = (options) => {
|
|
|
1699
1738
|
};
|
|
1700
1739
|
export const _internal = {
|
|
1701
1740
|
composePlayerPrompt,
|
|
1702
|
-
resolvePlayerId,
|
|
1703
1741
|
requiredFieldsFor,
|
|
1704
1742
|
extractJson,
|
|
1705
1743
|
buildClassifierPrompt,
|
|
@@ -1710,10 +1748,9 @@ export const _internal = {
|
|
|
1710
1748
|
pendingQuestionsFromContext,
|
|
1711
1749
|
normalizeErrorCompact,
|
|
1712
1750
|
normalizeErrorFull,
|
|
1713
|
-
DEFAULT_PLAYER_BINDING,
|
|
1714
1751
|
STATE_DESCRIPTIONS,
|
|
1715
|
-
|
|
1716
|
-
|
|
1752
|
+
ROLE_STATES,
|
|
1753
|
+
ROLE_STATE_IDS,
|
|
1717
1754
|
VERBATIM_PAYLOAD_FIELDS,
|
|
1718
1755
|
BOSS_INTERRUPT_TARGETS,
|
|
1719
1756
|
CONTINUATION_PREAMBLE,
|