@sublang/playbook 17.8.1 → 17.9.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/package.json +1 -1
- package/reference/sdlc/captain.md +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.js +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.gears.md +1 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +26 -2
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +1 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +157 -46
- package/reference/sdlc/code.playbook/playbook-captain.ts +210 -59
- package/slc/link.md +25 -3
- package/src/runtime.d.ts +13 -0
- package/src/runtime.ts +14 -0
- package/src/xstate-playbook-runtime.d.ts +4 -3
- package/src/xstate-playbook-runtime.js +152 -14
- package/src/xstate-playbook-runtime.ts +186 -13
- package/src/xstate-runtime.d.ts +7 -5
- package/src/xstate-runtime.js +40 -9
- package/src/xstate-runtime.ts +53 -24
|
@@ -569,6 +569,9 @@ function pendingBossQuestionRecord(pending) {
|
|
|
569
569
|
// Generic strategy defaults.
|
|
570
570
|
// ---------------------------------------------------------------------------
|
|
571
571
|
const CONTINUATION_PREAMBLE = 'Continue the same task using Boss’s reply below.';
|
|
572
|
+
// The labelled continuation a compiled artifact's composer renders, which
|
|
573
|
+
// slc's prompt-contract verifier pins. The runtime no longer sends it: it
|
|
574
|
+
// hands a composer no question and reply, and marks them itself (DR-090 §2).
|
|
572
575
|
function continuationBlocks(input, resuming = false) {
|
|
573
576
|
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
574
577
|
return [];
|
|
@@ -579,6 +582,59 @@ function continuationBlocks(input, resuming = false) {
|
|
|
579
582
|
`Boss reply:\n${input.bossReply}`,
|
|
580
583
|
];
|
|
581
584
|
}
|
|
585
|
+
// DR-090 §2: a Boss exchange reaches a player as structure, not as an
|
|
586
|
+
// English instruction — a marker line before each question and each reply,
|
|
587
|
+
// whatever language the run speaks.
|
|
588
|
+
const BOSS_QUESTION_MARKER = '? ⟶';
|
|
589
|
+
const BOSS_REPLY_MARKER = 'Boss ⟶';
|
|
590
|
+
function bossQuestionBlock(question) {
|
|
591
|
+
return `${BOSS_QUESTION_MARKER}\n${question}`;
|
|
592
|
+
}
|
|
593
|
+
function bossReplyBlock(reply) {
|
|
594
|
+
return `${BOSS_REPLY_MARKER}\n${reply}`;
|
|
595
|
+
}
|
|
596
|
+
/** DR-090 §2: the marked continuation of a call after a Boss reply. */
|
|
597
|
+
function markedContinuationBlocks(input, resuming = false) {
|
|
598
|
+
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
599
|
+
return [];
|
|
600
|
+
}
|
|
601
|
+
return [
|
|
602
|
+
...(resuming ? [] : [bossQuestionBlock(input.pendingBossQuestion.question)]),
|
|
603
|
+
bossReplyBlock(input.bossReply),
|
|
604
|
+
];
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* The input a composer receives: without the question and reply, which the
|
|
608
|
+
* runtime marks itself (DR-090 §2), so a composer adds no labelled blocks.
|
|
609
|
+
*/
|
|
610
|
+
function withoutContinuation(input) {
|
|
611
|
+
if (input.pendingBossQuestion === undefined && input.bossReply === undefined) {
|
|
612
|
+
return input;
|
|
613
|
+
}
|
|
614
|
+
const { pendingBossQuestion: _question, bossReply: _reply, ...rest } = input;
|
|
615
|
+
return rest;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* DR-090 §1: the run's Boss exchanges a player call carries before its body,
|
|
619
|
+
* leaving out the one exchange the call's own continuation blocks carry, so
|
|
620
|
+
* the asking role reads its answered question once.
|
|
621
|
+
*/
|
|
622
|
+
function bossExchangeBlocks(exchanges, input) {
|
|
623
|
+
let carried = -1;
|
|
624
|
+
if (input.pendingBossQuestion !== undefined && input.bossReply !== undefined) {
|
|
625
|
+
const question = input.pendingBossQuestion.question;
|
|
626
|
+
for (let index = exchanges.length - 1; index >= 0; index -= 1) {
|
|
627
|
+
const exchange = exchanges[index];
|
|
628
|
+
if (exchange.question === question && exchange.reply === input.bossReply) {
|
|
629
|
+
carried = index;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return exchanges.flatMap((exchange, index) => index === carried
|
|
635
|
+
? []
|
|
636
|
+
: [bossQuestionBlock(exchange.question), bossReplyBlock(exchange.reply)]);
|
|
637
|
+
}
|
|
582
638
|
/** Add clarification context without repeating the question in a live conversation. */
|
|
583
639
|
export function composePlayerContinuation(input, body, resuming = false) {
|
|
584
640
|
return [...continuationBlocks(input, resuming), body].join('\n\n');
|
|
@@ -2005,8 +2061,9 @@ function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPaylo
|
|
|
2005
2061
|
// the machine's transition selection — guards only, no action runs — with
|
|
2006
2062
|
// sample values, and the first combination of shapes whose arm carries that
|
|
2007
2063
|
// outcome's accepted-outcome marker is the contract the judge is shown and the
|
|
2008
|
-
// reconciler enforces. A field
|
|
2009
|
-
//
|
|
2064
|
+
// reconciler enforces. A field the machine admits as a string is recorded as
|
|
2065
|
+
// one; a field whose shape it does not reveal is recorded with none, so the
|
|
2066
|
+
// reconciler leaves its value to the machine's own validator (DR-091).
|
|
2010
2067
|
// ---------------------------------------------------------------------------
|
|
2011
2068
|
const SEMANTIC_SHAPE_SAMPLES = [
|
|
2012
2069
|
['string', 'sample'],
|
|
@@ -2045,9 +2102,10 @@ function shapeCombinations(fieldCount) {
|
|
|
2045
2102
|
left.join(',').localeCompare(right.join(',')));
|
|
2046
2103
|
}
|
|
2047
2104
|
/**
|
|
2048
|
-
* DR-089: the
|
|
2049
|
-
*
|
|
2050
|
-
*
|
|
2105
|
+
* DR-089: the JSON value shape the compiled machine admits for each
|
|
2106
|
+
* semantic-owned field of each governed outcome, keyed by state id and then
|
|
2107
|
+
* by outcome. A field, outcome, or state the probe reveals no shape for is
|
|
2108
|
+
* left out (DR-091). Exported so linked machinery of an artifact's own reads
|
|
2051
2109
|
* the same contract the shared factory enforces.
|
|
2052
2110
|
*/
|
|
2053
2111
|
export function inferSemanticValueShapes(machine, authority) {
|
|
@@ -2127,18 +2185,17 @@ export function inferSemanticValueShapes(machine, authority) {
|
|
|
2127
2185
|
});
|
|
2128
2186
|
if (!admits(values))
|
|
2129
2187
|
continue;
|
|
2188
|
+
// DR-091: an admitted string is recorded too, so only a field the
|
|
2189
|
+
// probe revealed nothing for goes unrecorded.
|
|
2130
2190
|
const shapes = {};
|
|
2131
2191
|
semanticFields.forEach((field, index) => {
|
|
2132
2192
|
let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]][0];
|
|
2133
2193
|
if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
|
|
2134
2194
|
shape = 'string-or-number-array';
|
|
2135
2195
|
}
|
|
2136
|
-
|
|
2137
|
-
shapes[field] = shape;
|
|
2196
|
+
shapes[field] = shape;
|
|
2138
2197
|
});
|
|
2139
|
-
|
|
2140
|
-
stateShapes[guard] = Object.freeze(shapes);
|
|
2141
|
-
}
|
|
2198
|
+
stateShapes[guard] = Object.freeze(shapes);
|
|
2142
2199
|
break;
|
|
2143
2200
|
}
|
|
2144
2201
|
}
|
|
@@ -2715,8 +2772,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2715
2772
|
}
|
|
2716
2773
|
const composePlayerPrompt = spec.composePlayerPrompt ??
|
|
2717
2774
|
((input, _identity, resuming = false) => defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
|
|
2718
|
-
const
|
|
2775
|
+
const composeArtifactCaptainPrompt = spec.composeCaptainPrompt ??
|
|
2719
2776
|
((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
2777
|
+
// DR-090 §2: a direct-Captain continuation is marked by the runtime too.
|
|
2778
|
+
const composeCaptainPrompt = (input) => [
|
|
2779
|
+
...markedContinuationBlocks(input),
|
|
2780
|
+
composeArtifactCaptainPrompt(withoutContinuation(input)),
|
|
2781
|
+
].join('\n\n');
|
|
2720
2782
|
const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
2721
2783
|
const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
|
|
2722
2784
|
const adjudication = {
|
|
@@ -2865,6 +2927,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2865
2927
|
const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
|
|
2866
2928
|
let effectLedgerMirror = currentEffectLedger();
|
|
2867
2929
|
let recoveryCheckpoint;
|
|
2930
|
+
// DR-090 §1: the run's Boss exchanges, in order. A run begins at the
|
|
2931
|
+
// artifact's entry event; the list travels in the snapshot.
|
|
2932
|
+
let bossExchanges = [];
|
|
2868
2933
|
let retainedEffectSourceSessionId;
|
|
2869
2934
|
let retainedEffectReconciliation;
|
|
2870
2935
|
let retainedEffectReconciliationRequired = false;
|
|
@@ -3495,7 +3560,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3495
3560
|
}
|
|
3496
3561
|
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
3497
3562
|
}
|
|
3563
|
+
// DR-090 §3: the first block names the role and, where the host bound
|
|
3564
|
+
// it, the model the call runs on as configured, so a player can name
|
|
3565
|
+
// both where its project asks (a commit trailer, say).
|
|
3566
|
+
function playerIdentityBlock(roleId) {
|
|
3567
|
+
const binding = session?.roleBindings?.[roleId];
|
|
3568
|
+
return binding === undefined
|
|
3569
|
+
? `Role: ${roleId}`
|
|
3570
|
+
: `Role: ${roleId} · Model: ${binding.promptIdentity}`;
|
|
3571
|
+
}
|
|
3498
3572
|
function composeBoundPlayerPrompt(input, resuming = false) {
|
|
3573
|
+
const roleId = requireRoleId(input);
|
|
3499
3574
|
let active = true;
|
|
3500
3575
|
const lookup = (roleId) => {
|
|
3501
3576
|
if (!active) {
|
|
@@ -3503,12 +3578,44 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3503
3578
|
}
|
|
3504
3579
|
return promptIdentity(roleId);
|
|
3505
3580
|
};
|
|
3581
|
+
let body;
|
|
3506
3582
|
try {
|
|
3507
|
-
|
|
3583
|
+
body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
|
|
3508
3584
|
}
|
|
3509
3585
|
finally {
|
|
3510
3586
|
active = false;
|
|
3511
3587
|
}
|
|
3588
|
+
// DR-090 §1, §2: every role hears the Boss — the run's exchanges come
|
|
3589
|
+
// before the body whatever the role, and the asking role's marked
|
|
3590
|
+
// continuation carries the exchange it asked.
|
|
3591
|
+
return [
|
|
3592
|
+
playerIdentityBlock(roleId),
|
|
3593
|
+
...bossExchangeBlocks(bossExchanges, input),
|
|
3594
|
+
...markedContinuationBlocks(input, resuming),
|
|
3595
|
+
body,
|
|
3596
|
+
].join('\n\n');
|
|
3597
|
+
}
|
|
3598
|
+
/** DR-090 §1: record a Boss reply the machine is about to receive. */
|
|
3599
|
+
function recordBossExchange(event, pending) {
|
|
3600
|
+
if (event.type !== 'BOSS_REPLY')
|
|
3601
|
+
return;
|
|
3602
|
+
const { questionId, answer } = event;
|
|
3603
|
+
if (typeof answer !== 'string' || answer.trim().length === 0)
|
|
3604
|
+
return;
|
|
3605
|
+
const question = pending.find((candidate) => candidate.questionId === questionId) ??
|
|
3606
|
+
(questionId === undefined && pending.length === 1 ? pending[0] : undefined);
|
|
3607
|
+
if (question === undefined)
|
|
3608
|
+
return;
|
|
3609
|
+
bossExchanges = Object.freeze([
|
|
3610
|
+
...bossExchanges,
|
|
3611
|
+
Object.freeze({ question: question.question, reply: answer }),
|
|
3612
|
+
]);
|
|
3613
|
+
}
|
|
3614
|
+
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
3615
|
+
function beginRunIfEntry(event) {
|
|
3616
|
+
if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
|
|
3617
|
+
bossExchanges = [];
|
|
3618
|
+
}
|
|
3512
3619
|
}
|
|
3513
3620
|
function continuationKey(roleId, playerId) {
|
|
3514
3621
|
return playerId ?? roleId;
|
|
@@ -4372,7 +4479,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4372
4479
|
try {
|
|
4373
4480
|
await claim.assertOwner();
|
|
4374
4481
|
const restored = snapshotJsonValue(await repositoryCapability.observe());
|
|
4375
|
-
|
|
4482
|
+
// DR-090 §4: the branch an observation names is never compared.
|
|
4483
|
+
const { branch: _restoredBranch, ...restoredState } = restored;
|
|
4484
|
+
const { branch: _savedBranch, ...savedState } = saved.baseline;
|
|
4485
|
+
if (!isDeepStrictEqual(restoredState, savedState)) {
|
|
4376
4486
|
throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
|
|
4377
4487
|
}
|
|
4378
4488
|
signal.throwIfAborted();
|
|
@@ -4580,6 +4690,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4580
4690
|
...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
|
|
4581
4691
|
? { recoveryCheckpoint } : {}),
|
|
4582
4692
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
4693
|
+
...(bossExchanges.length === 0
|
|
4694
|
+
? {}
|
|
4695
|
+
: {
|
|
4696
|
+
bossExchanges: bossExchanges.map(({ question, reply }) => ({
|
|
4697
|
+
question,
|
|
4698
|
+
reply,
|
|
4699
|
+
})),
|
|
4700
|
+
}),
|
|
4583
4701
|
};
|
|
4584
4702
|
}
|
|
4585
4703
|
async function runJournalledStep(kind, input, signal, execute) {
|
|
@@ -6354,6 +6472,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6354
6472
|
deferInspectionEmissions = false;
|
|
6355
6473
|
deferredInspectionEmissions = [];
|
|
6356
6474
|
recoveryCheckpoint = undefined;
|
|
6475
|
+
bossExchanges = [];
|
|
6357
6476
|
suppressInspectionEmissions = false;
|
|
6358
6477
|
initialized = false;
|
|
6359
6478
|
traceSequence = 0;
|
|
@@ -6706,6 +6825,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6706
6825
|
}
|
|
6707
6826
|
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
6708
6827
|
}
|
|
6828
|
+
// DR-090 §1: a restored or adopted run keeps the Boss exchanges it
|
|
6829
|
+
// heard; a snapshot without them heard none.
|
|
6830
|
+
bossExchanges = Object.freeze((boundSnapshot.bossExchanges ?? []).map(({ question, reply }) => Object.freeze({ question, reply })));
|
|
6709
6831
|
nestedBridge.prepareRestore(suspendedCall);
|
|
6710
6832
|
if (suspendedCall !== undefined) {
|
|
6711
6833
|
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
@@ -7671,7 +7793,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7671
7793
|
deferredOperation !== undefined) {
|
|
7672
7794
|
if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
|
|
7673
7795
|
const statusLine = classificationStatus(event);
|
|
7674
|
-
const
|
|
7796
|
+
const acceptedPending = deferredPending;
|
|
7797
|
+
const acceptedEvent = event;
|
|
7798
|
+
const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, () => {
|
|
7799
|
+
// DR-090 §1: the host started the continuation, so the
|
|
7800
|
+
// machine receives this reply now.
|
|
7801
|
+
recordBossExchange(acceptedEvent, [acceptedPending]);
|
|
7802
|
+
onAccepted?.();
|
|
7803
|
+
});
|
|
7675
7804
|
if (continuation === 'continued') {
|
|
7676
7805
|
if (controlPlaneError !== undefined) {
|
|
7677
7806
|
throw controlPlaneError;
|
|
@@ -7715,13 +7844,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7715
7844
|
signal.throwIfAborted();
|
|
7716
7845
|
// 3. A final actor cannot accept new events; reconstruct only
|
|
7717
7846
|
// after classification produced a real event.
|
|
7847
|
+
const acceptedPending = classifiedSnapshot === undefined
|
|
7848
|
+
? []
|
|
7849
|
+
: pendingBossQuestionsFor(normalizePlaybookSnapshot(classifiedSnapshot), (classifiedSnapshot.context ??
|
|
7850
|
+
{}));
|
|
7718
7851
|
if (actor.getSnapshot().status === 'done') {
|
|
7719
7852
|
stopActor();
|
|
7720
7853
|
actor = buildActor(runtimePorts);
|
|
7721
7854
|
// The replacement actor's snapshots are real state entries.
|
|
7722
7855
|
suppressInspectionEmissions = false;
|
|
7723
7856
|
actor.start();
|
|
7857
|
+
// DR-090 §1: a finished run's exchanges end with it.
|
|
7858
|
+
bossExchanges = [];
|
|
7724
7859
|
}
|
|
7860
|
+
beginRunIfEntry(event);
|
|
7861
|
+
recordBossExchange(event, acceptedPending);
|
|
7725
7862
|
onAccepted?.();
|
|
7726
7863
|
actor.send(event);
|
|
7727
7864
|
await waitForPlaybookQuiescence(actor, {
|
|
@@ -8002,6 +8139,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
8002
8139
|
failedGovernedAttemptId = undefined;
|
|
8003
8140
|
controlPlaneError = undefined;
|
|
8004
8141
|
emissionFailure = undefined;
|
|
8142
|
+
bossExchanges = [];
|
|
8005
8143
|
retainedEffectSourceSessionId = undefined;
|
|
8006
8144
|
retainedEffectReconciliation = undefined;
|
|
8007
8145
|
retainedEffectReconciliationRequired = false;
|
|
@@ -58,6 +58,7 @@ import type {
|
|
|
58
58
|
CaptainResult,
|
|
59
59
|
JsonValue,
|
|
60
60
|
PlaybookAdoptionContext,
|
|
61
|
+
PlaybookBossExchange,
|
|
61
62
|
PlaybookCallResult,
|
|
62
63
|
PlaybookControlAction,
|
|
63
64
|
PlaybookControlReceipt,
|
|
@@ -1327,6 +1328,9 @@ function pendingBossQuestionRecord(
|
|
|
1327
1328
|
const CONTINUATION_PREAMBLE =
|
|
1328
1329
|
'Continue the same task using Boss’s reply below.';
|
|
1329
1330
|
|
|
1331
|
+
// The labelled continuation a compiled artifact's composer renders, which
|
|
1332
|
+
// slc's prompt-contract verifier pins. The runtime no longer sends it: it
|
|
1333
|
+
// hands a composer no question and reply, and marks them itself (DR-090 §2).
|
|
1330
1334
|
function continuationBlocks(input: {
|
|
1331
1335
|
pendingBossQuestion?: { readonly question: string };
|
|
1332
1336
|
bossReply?: string;
|
|
@@ -1341,6 +1345,76 @@ function continuationBlocks(input: {
|
|
|
1341
1345
|
];
|
|
1342
1346
|
}
|
|
1343
1347
|
|
|
1348
|
+
// DR-090 §2: a Boss exchange reaches a player as structure, not as an
|
|
1349
|
+
// English instruction — a marker line before each question and each reply,
|
|
1350
|
+
// whatever language the run speaks.
|
|
1351
|
+
const BOSS_QUESTION_MARKER = '? ⟶';
|
|
1352
|
+
const BOSS_REPLY_MARKER = 'Boss ⟶';
|
|
1353
|
+
|
|
1354
|
+
function bossQuestionBlock(question: string): string {
|
|
1355
|
+
return `${BOSS_QUESTION_MARKER}\n${question}`;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function bossReplyBlock(reply: string): string {
|
|
1359
|
+
return `${BOSS_REPLY_MARKER}\n${reply}`;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** DR-090 §2: the marked continuation of a call after a Boss reply. */
|
|
1363
|
+
function markedContinuationBlocks(input: {
|
|
1364
|
+
pendingBossQuestion?: { readonly question: string };
|
|
1365
|
+
bossReply?: string;
|
|
1366
|
+
}, resuming = false): string[] {
|
|
1367
|
+
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
1368
|
+
return [];
|
|
1369
|
+
}
|
|
1370
|
+
return [
|
|
1371
|
+
...(resuming ? [] : [bossQuestionBlock(input.pendingBossQuestion.question)]),
|
|
1372
|
+
bossReplyBlock(input.bossReply),
|
|
1373
|
+
];
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* The input a composer receives: without the question and reply, which the
|
|
1378
|
+
* runtime marks itself (DR-090 §2), so a composer adds no labelled blocks.
|
|
1379
|
+
*/
|
|
1380
|
+
function withoutContinuation<T extends {
|
|
1381
|
+
pendingBossQuestion?: unknown;
|
|
1382
|
+
bossReply?: unknown;
|
|
1383
|
+
}>(input: T): T {
|
|
1384
|
+
if (input.pendingBossQuestion === undefined && input.bossReply === undefined) {
|
|
1385
|
+
return input;
|
|
1386
|
+
}
|
|
1387
|
+
const { pendingBossQuestion: _question, bossReply: _reply, ...rest } = input;
|
|
1388
|
+
return rest as T;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* DR-090 §1: the run's Boss exchanges a player call carries before its body,
|
|
1393
|
+
* leaving out the one exchange the call's own continuation blocks carry, so
|
|
1394
|
+
* the asking role reads its answered question once.
|
|
1395
|
+
*/
|
|
1396
|
+
function bossExchangeBlocks(
|
|
1397
|
+
exchanges: readonly PlaybookBossExchange[],
|
|
1398
|
+
input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>,
|
|
1399
|
+
): string[] {
|
|
1400
|
+
let carried = -1;
|
|
1401
|
+
if (input.pendingBossQuestion !== undefined && input.bossReply !== undefined) {
|
|
1402
|
+
const question = input.pendingBossQuestion.question;
|
|
1403
|
+
for (let index = exchanges.length - 1; index >= 0; index -= 1) {
|
|
1404
|
+
const exchange = exchanges[index]!;
|
|
1405
|
+
if (exchange.question === question && exchange.reply === input.bossReply) {
|
|
1406
|
+
carried = index;
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return exchanges.flatMap((exchange, index) =>
|
|
1412
|
+
index === carried
|
|
1413
|
+
? []
|
|
1414
|
+
: [bossQuestionBlock(exchange.question), bossReplyBlock(exchange.reply)],
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1344
1418
|
/** Add clarification context without repeating the question in a live conversation. */
|
|
1345
1419
|
export function composePlayerContinuation(
|
|
1346
1420
|
input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>,
|
|
@@ -3273,8 +3347,9 @@ function snapshotOutcomeAuthority(
|
|
|
3273
3347
|
// the machine's transition selection — guards only, no action runs — with
|
|
3274
3348
|
// sample values, and the first combination of shapes whose arm carries that
|
|
3275
3349
|
// outcome's accepted-outcome marker is the contract the judge is shown and the
|
|
3276
|
-
// reconciler enforces. A field
|
|
3277
|
-
//
|
|
3350
|
+
// reconciler enforces. A field the machine admits as a string is recorded as
|
|
3351
|
+
// one; a field whose shape it does not reveal is recorded with none, so the
|
|
3352
|
+
// reconciler leaves its value to the machine's own validator (DR-091).
|
|
3278
3353
|
// ---------------------------------------------------------------------------
|
|
3279
3354
|
|
|
3280
3355
|
const SEMANTIC_SHAPE_SAMPLES: readonly (readonly [
|
|
@@ -3337,9 +3412,10 @@ function shapeCombinations(fieldCount: number): readonly number[][] {
|
|
|
3337
3412
|
}
|
|
3338
3413
|
|
|
3339
3414
|
/**
|
|
3340
|
-
* DR-089: the
|
|
3341
|
-
*
|
|
3342
|
-
*
|
|
3415
|
+
* DR-089: the JSON value shape the compiled machine admits for each
|
|
3416
|
+
* semantic-owned field of each governed outcome, keyed by state id and then
|
|
3417
|
+
* by outcome. A field, outcome, or state the probe reveals no shape for is
|
|
3418
|
+
* left out (DR-091). Exported so linked machinery of an artifact's own reads
|
|
3343
3419
|
* the same contract the shared factory enforces.
|
|
3344
3420
|
*/
|
|
3345
3421
|
export function inferSemanticValueShapes(
|
|
@@ -3425,17 +3501,17 @@ export function inferSemanticValueShapes(
|
|
|
3425
3501
|
values[field] = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![1];
|
|
3426
3502
|
});
|
|
3427
3503
|
if (!admits(values)) continue;
|
|
3504
|
+
// DR-091: an admitted string is recorded too, so only a field the
|
|
3505
|
+
// probe revealed nothing for goes unrecorded.
|
|
3428
3506
|
const shapes: Record<string, PlaybookSemanticValueShape> = {};
|
|
3429
3507
|
semanticFields.forEach((field, index) => {
|
|
3430
3508
|
let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![0];
|
|
3431
3509
|
if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
|
|
3432
3510
|
shape = 'string-or-number-array';
|
|
3433
3511
|
}
|
|
3434
|
-
|
|
3512
|
+
shapes[field] = shape;
|
|
3435
3513
|
});
|
|
3436
|
-
|
|
3437
|
-
stateShapes[guard] = Object.freeze(shapes);
|
|
3438
|
-
}
|
|
3514
|
+
stateShapes[guard] = Object.freeze(shapes);
|
|
3439
3515
|
break;
|
|
3440
3516
|
}
|
|
3441
3517
|
}
|
|
@@ -4269,10 +4345,16 @@ export function createXStatePlaybookRuntime<
|
|
|
4269
4345
|
spec.composePlayerPrompt ??
|
|
4270
4346
|
((input: PlaybookPlayerInput, _identity: XStatePromptIdentity, resuming = false) =>
|
|
4271
4347
|
defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
|
|
4272
|
-
const
|
|
4348
|
+
const composeArtifactCaptainPrompt =
|
|
4273
4349
|
spec.composeCaptainPrompt ??
|
|
4274
4350
|
((input: PlaybookCaptainInput) =>
|
|
4275
4351
|
defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
4352
|
+
// DR-090 §2: a direct-Captain continuation is marked by the runtime too.
|
|
4353
|
+
const composeCaptainPrompt = (input: PlaybookCaptainInput): string =>
|
|
4354
|
+
[
|
|
4355
|
+
...markedContinuationBlocks(input),
|
|
4356
|
+
composeArtifactCaptainPrompt(withoutContinuation(input)),
|
|
4357
|
+
].join('\n\n');
|
|
4276
4358
|
const extractFields =
|
|
4277
4359
|
spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
4278
4360
|
const verbatimPayloadFields: ReadonlySet<string> = new Set(
|
|
@@ -4508,6 +4590,9 @@ export function createXStatePlaybookRuntime<
|
|
|
4508
4590
|
);
|
|
4509
4591
|
let effectLedgerMirror = currentEffectLedger();
|
|
4510
4592
|
let recoveryCheckpoint: PlaybookRuntimeSnapshot['recoveryCheckpoint'];
|
|
4593
|
+
// DR-090 §1: the run's Boss exchanges, in order. A run begins at the
|
|
4594
|
+
// artifact's entry event; the list travels in the snapshot.
|
|
4595
|
+
let bossExchanges: readonly PlaybookBossExchange[] = [];
|
|
4511
4596
|
let retainedEffectSourceSessionId: string | undefined;
|
|
4512
4597
|
let retainedEffectReconciliation:
|
|
4513
4598
|
| PlaybookRuntimeSnapshot['retainedEffectReconciliation']
|
|
@@ -5406,7 +5491,18 @@ export function createXStatePlaybookRuntime<
|
|
|
5406
5491
|
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
5407
5492
|
}
|
|
5408
5493
|
|
|
5494
|
+
// DR-090 §3: the first block names the role and, where the host bound
|
|
5495
|
+
// it, the model the call runs on as configured, so a player can name
|
|
5496
|
+
// both where its project asks (a commit trailer, say).
|
|
5497
|
+
function playerIdentityBlock(roleId: string): string {
|
|
5498
|
+
const binding = session?.roleBindings?.[roleId];
|
|
5499
|
+
return binding === undefined
|
|
5500
|
+
? `Role: ${roleId}`
|
|
5501
|
+
: `Role: ${roleId} · Model: ${binding.promptIdentity}`;
|
|
5502
|
+
}
|
|
5503
|
+
|
|
5409
5504
|
function composeBoundPlayerPrompt(input: PlaybookPlayerInput, resuming = false): string {
|
|
5505
|
+
const roleId = requireRoleId(input);
|
|
5410
5506
|
let active = true;
|
|
5411
5507
|
const lookup: XStatePromptIdentity = (roleId) => {
|
|
5412
5508
|
if (!active) {
|
|
@@ -5416,11 +5512,49 @@ export function createXStatePlaybookRuntime<
|
|
|
5416
5512
|
}
|
|
5417
5513
|
return promptIdentity(roleId);
|
|
5418
5514
|
};
|
|
5515
|
+
let body: string;
|
|
5419
5516
|
try {
|
|
5420
|
-
|
|
5517
|
+
body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
|
|
5421
5518
|
} finally {
|
|
5422
5519
|
active = false;
|
|
5423
5520
|
}
|
|
5521
|
+
// DR-090 §1, §2: every role hears the Boss — the run's exchanges come
|
|
5522
|
+
// before the body whatever the role, and the asking role's marked
|
|
5523
|
+
// continuation carries the exchange it asked.
|
|
5524
|
+
return [
|
|
5525
|
+
playerIdentityBlock(roleId),
|
|
5526
|
+
...bossExchangeBlocks(bossExchanges, input),
|
|
5527
|
+
...markedContinuationBlocks(input, resuming),
|
|
5528
|
+
body,
|
|
5529
|
+
].join('\n\n');
|
|
5530
|
+
}
|
|
5531
|
+
|
|
5532
|
+
/** DR-090 §1: record a Boss reply the machine is about to receive. */
|
|
5533
|
+
function recordBossExchange(
|
|
5534
|
+
event: EventObject,
|
|
5535
|
+
pending: readonly PlaybookPendingBossQuestionContext[],
|
|
5536
|
+
): void {
|
|
5537
|
+
if (event.type !== 'BOSS_REPLY') return;
|
|
5538
|
+
const { questionId, answer } = event as {
|
|
5539
|
+
readonly questionId?: unknown;
|
|
5540
|
+
readonly answer?: unknown;
|
|
5541
|
+
};
|
|
5542
|
+
if (typeof answer !== 'string' || answer.trim().length === 0) return;
|
|
5543
|
+
const question =
|
|
5544
|
+
pending.find((candidate) => candidate.questionId === questionId) ??
|
|
5545
|
+
(questionId === undefined && pending.length === 1 ? pending[0] : undefined);
|
|
5546
|
+
if (question === undefined) return;
|
|
5547
|
+
bossExchanges = Object.freeze([
|
|
5548
|
+
...bossExchanges,
|
|
5549
|
+
Object.freeze({ question: question.question, reply: answer }),
|
|
5550
|
+
]);
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
5554
|
+
function beginRunIfEntry(event: EventObject): void {
|
|
5555
|
+
if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
|
|
5556
|
+
bossExchanges = [];
|
|
5557
|
+
}
|
|
5424
5558
|
}
|
|
5425
5559
|
|
|
5426
5560
|
function continuationKey(
|
|
@@ -6706,7 +6840,10 @@ export function createXStatePlaybookRuntime<
|
|
|
6706
6840
|
try {
|
|
6707
6841
|
await claim.assertOwner();
|
|
6708
6842
|
const restored = snapshotJsonValue(await repositoryCapability!.observe!()) as unknown as PlaybookRepositoryReceipt['baseline'];
|
|
6709
|
-
|
|
6843
|
+
// DR-090 §4: the branch an observation names is never compared.
|
|
6844
|
+
const { branch: _restoredBranch, ...restoredState } = restored;
|
|
6845
|
+
const { branch: _savedBranch, ...savedState } = saved.baseline;
|
|
6846
|
+
if (!isDeepStrictEqual(restoredState, savedState)) {
|
|
6710
6847
|
throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
|
|
6711
6848
|
}
|
|
6712
6849
|
signal.throwIfAborted();
|
|
@@ -6954,6 +7091,14 @@ export function createXStatePlaybookRuntime<
|
|
|
6954
7091
|
...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
|
|
6955
7092
|
? { recoveryCheckpoint } : {}),
|
|
6956
7093
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
7094
|
+
...(bossExchanges.length === 0
|
|
7095
|
+
? {}
|
|
7096
|
+
: {
|
|
7097
|
+
bossExchanges: bossExchanges.map(({ question, reply }) => ({
|
|
7098
|
+
question,
|
|
7099
|
+
reply,
|
|
7100
|
+
})),
|
|
7101
|
+
}),
|
|
6957
7102
|
};
|
|
6958
7103
|
}
|
|
6959
7104
|
|
|
@@ -9224,6 +9369,7 @@ export function createXStatePlaybookRuntime<
|
|
|
9224
9369
|
deferInspectionEmissions = false;
|
|
9225
9370
|
deferredInspectionEmissions = [];
|
|
9226
9371
|
recoveryCheckpoint = undefined;
|
|
9372
|
+
bossExchanges = [];
|
|
9227
9373
|
suppressInspectionEmissions = false;
|
|
9228
9374
|
initialized = false;
|
|
9229
9375
|
traceSequence = 0;
|
|
@@ -9671,6 +9817,13 @@ export function createXStatePlaybookRuntime<
|
|
|
9671
9817
|
}
|
|
9672
9818
|
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
9673
9819
|
}
|
|
9820
|
+
// DR-090 §1: a restored or adopted run keeps the Boss exchanges it
|
|
9821
|
+
// heard; a snapshot without them heard none.
|
|
9822
|
+
bossExchanges = Object.freeze(
|
|
9823
|
+
(boundSnapshot.bossExchanges ?? []).map(({ question, reply }) =>
|
|
9824
|
+
Object.freeze({ question, reply }),
|
|
9825
|
+
),
|
|
9826
|
+
);
|
|
9674
9827
|
nestedBridge.prepareRestore(suspendedCall);
|
|
9675
9828
|
if (suspendedCall !== undefined) {
|
|
9676
9829
|
playbookCallTurnIds.set(
|
|
@@ -10870,13 +11023,20 @@ export function createXStatePlaybookRuntime<
|
|
|
10870
11023
|
)
|
|
10871
11024
|
) {
|
|
10872
11025
|
const statusLine = classificationStatus(event);
|
|
11026
|
+
const acceptedPending = deferredPending;
|
|
11027
|
+
const acceptedEvent = event;
|
|
10873
11028
|
const continuation = await continueBoundDeferredOperation(
|
|
10874
11029
|
deferredOperation,
|
|
10875
11030
|
event,
|
|
10876
11031
|
signal,
|
|
10877
11032
|
turnId,
|
|
10878
11033
|
statusLine,
|
|
10879
|
-
|
|
11034
|
+
() => {
|
|
11035
|
+
// DR-090 §1: the host started the continuation, so the
|
|
11036
|
+
// machine receives this reply now.
|
|
11037
|
+
recordBossExchange(acceptedEvent, [acceptedPending]);
|
|
11038
|
+
onAccepted?.();
|
|
11039
|
+
},
|
|
10880
11040
|
);
|
|
10881
11041
|
if (continuation === 'continued') {
|
|
10882
11042
|
if (controlPlaneError !== undefined) {
|
|
@@ -10921,13 +11081,25 @@ export function createXStatePlaybookRuntime<
|
|
|
10921
11081
|
signal.throwIfAborted();
|
|
10922
11082
|
// 3. A final actor cannot accept new events; reconstruct only
|
|
10923
11083
|
// after classification produced a real event.
|
|
11084
|
+
const acceptedPending =
|
|
11085
|
+
classifiedSnapshot === undefined
|
|
11086
|
+
? []
|
|
11087
|
+
: pendingBossQuestionsFor(
|
|
11088
|
+
normalizePlaybookSnapshot(classifiedSnapshot),
|
|
11089
|
+
((classifiedSnapshot as { context?: unknown }).context ??
|
|
11090
|
+
{}) as Record<string, unknown>,
|
|
11091
|
+
);
|
|
10924
11092
|
if (actor.getSnapshot().status === 'done') {
|
|
10925
11093
|
stopActor();
|
|
10926
11094
|
actor = buildActor(runtimePorts!);
|
|
10927
11095
|
// The replacement actor's snapshots are real state entries.
|
|
10928
11096
|
suppressInspectionEmissions = false;
|
|
10929
11097
|
actor.start();
|
|
11098
|
+
// DR-090 §1: a finished run's exchanges end with it.
|
|
11099
|
+
bossExchanges = [];
|
|
10930
11100
|
}
|
|
11101
|
+
beginRunIfEntry(event);
|
|
11102
|
+
recordBossExchange(event, acceptedPending);
|
|
10931
11103
|
onAccepted?.();
|
|
10932
11104
|
actor.send(event);
|
|
10933
11105
|
await waitForPlaybookQuiescence(actor, {
|
|
@@ -11236,6 +11408,7 @@ export function createXStatePlaybookRuntime<
|
|
|
11236
11408
|
failedGovernedAttemptId = undefined;
|
|
11237
11409
|
controlPlaneError = undefined;
|
|
11238
11410
|
emissionFailure = undefined;
|
|
11411
|
+
bossExchanges = [];
|
|
11239
11412
|
retainedEffectSourceSessionId = undefined;
|
|
11240
11413
|
retainedEffectReconciliation = undefined;
|
|
11241
11414
|
retainedEffectReconciliationRequired = false;
|
package/src/xstate-runtime.d.ts
CHANGED
|
@@ -56,13 +56,14 @@ export interface PlaybookSemanticOutcomeSpec {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* The JSON value shape a compiled machine admits for one semantic-owned field
|
|
59
|
-
* (DR-089). A field the machine admits as a string
|
|
60
|
-
* machine does not reveal
|
|
59
|
+
* (DR-089). A field the machine admits as a string is `string`; a field whose
|
|
60
|
+
* shape the machine does not reveal has none (DR-091).
|
|
61
61
|
*/
|
|
62
62
|
export type PlaybookSemanticValueShape = 'string' | 'string-array' | 'number-array' | 'string-or-number-array' | 'number' | 'boolean';
|
|
63
63
|
/**
|
|
64
|
-
* Per outcome, the
|
|
65
|
-
*
|
|
64
|
+
* Per outcome, the shapes of its semantic-owned fields; a field left out has
|
|
65
|
+
* no revealed shape and takes any JSON value, the machine's own validator
|
|
66
|
+
* deciding it (DR-091).
|
|
66
67
|
*/
|
|
67
68
|
export type PlaybookSemanticValueShapes = Readonly<Record<string, Readonly<Record<string, PlaybookSemanticValueShape>>>>;
|
|
68
69
|
/** The words a judge prompt or validation error uses for one value shape. */
|
|
@@ -76,7 +77,8 @@ export interface PlaybookSemanticEvidenceInput {
|
|
|
76
77
|
readonly runtimeFields?: unknown;
|
|
77
78
|
/**
|
|
78
79
|
* DR-089: the shape the compiled machine admits for each semantic-owned
|
|
79
|
-
* field, keyed by outcome;
|
|
80
|
+
* field, keyed by outcome; a field it leaves out takes any JSON value
|
|
81
|
+
* (DR-091).
|
|
80
82
|
*/
|
|
81
83
|
readonly semanticValueShapes?: PlaybookSemanticValueShapes;
|
|
82
84
|
}
|