@sublang/playbook 17.8.1 → 17.10.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.
@@ -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'>,
@@ -1800,19 +1874,25 @@ export function renderGovernedOutcomeContract(
1800
1874
  for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
1801
1875
  if (authority === 'semantic') {
1802
1876
  const entry = authored.get(field);
1803
- // DR-089: the value has the shape the compiled machine admits.
1877
+ // DR-089: the value has the shape the compiled machine admits; a
1878
+ // shape the probe did not reveal is the machine's to judge, so the
1879
+ // example asserts none (DR-091).
1804
1880
  const shape =
1805
1881
  shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
1806
1882
  ? shapes[field]!
1807
- : 'string';
1808
- const words = describePlaybookSemanticValueShape(shape);
1883
+ : undefined;
1884
+ const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
1809
1885
  replyMembers.push(
1810
1886
  `${JSON.stringify(field)}: ${
1811
1887
  entry?.placeholder ??
1812
- (shape === 'string' ? '<string>' : `<${words.replace(/^a /, '')}>`)
1888
+ (shape === undefined
1889
+ ? '<value>'
1890
+ : shape === 'string'
1891
+ ? '<string>'
1892
+ : `<${words!.replace(/^a /, '')}>`)
1813
1893
  }`,
1814
1894
  );
1815
- if (shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
1895
+ if (shape !== undefined && shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
1816
1896
  semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
1817
1897
  } else {
1818
1898
  runtimeSupplied.push(`\`${field}\` (${authority}-owned)`);
@@ -3273,8 +3353,9 @@ function snapshotOutcomeAuthority(
3273
3353
  // the machine's transition selection — guards only, no action runs — with
3274
3354
  // sample values, and the first combination of shapes whose arm carries that
3275
3355
  // outcome's accepted-outcome marker is the contract the judge is shown and the
3276
- // reconciler enforces. A field whose shape the machine does not reveal, or
3277
- // that it admits as a string, stays a string.
3356
+ // reconciler enforces. A field the machine admits as a string is recorded as
3357
+ // one; a field whose shape it does not reveal is recorded with none, so the
3358
+ // reconciler leaves its value to the machine's own validator (DR-091).
3278
3359
  // ---------------------------------------------------------------------------
3279
3360
 
3280
3361
  const SEMANTIC_SHAPE_SAMPLES: readonly (readonly [
@@ -3337,9 +3418,10 @@ function shapeCombinations(fieldCount: number): readonly number[][] {
3337
3418
  }
3338
3419
 
3339
3420
  /**
3340
- * DR-089: the non-string JSON value shape the compiled machine admits for
3341
- * each semantic-owned field of each governed outcome, keyed by state id and
3342
- * then by outcome. Exported so linked machinery of an artifact's own reads
3421
+ * DR-089: the JSON value shape the compiled machine admits for each
3422
+ * semantic-owned field of each governed outcome, keyed by state id and then
3423
+ * by outcome. A field, outcome, or state the probe reveals no shape for is
3424
+ * left out (DR-091). Exported so linked machinery of an artifact's own reads
3343
3425
  * the same contract the shared factory enforces.
3344
3426
  */
3345
3427
  export function inferSemanticValueShapes(
@@ -3425,17 +3507,17 @@ export function inferSemanticValueShapes(
3425
3507
  values[field] = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![1];
3426
3508
  });
3427
3509
  if (!admits(values)) continue;
3510
+ // DR-091: an admitted string is recorded too, so only a field the
3511
+ // probe revealed nothing for goes unrecorded.
3428
3512
  const shapes: Record<string, PlaybookSemanticValueShape> = {};
3429
3513
  semanticFields.forEach((field, index) => {
3430
3514
  let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![0];
3431
3515
  if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
3432
3516
  shape = 'string-or-number-array';
3433
3517
  }
3434
- if (shape !== 'string') shapes[field] = shape;
3518
+ shapes[field] = shape;
3435
3519
  });
3436
- if (Object.keys(shapes).length > 0) {
3437
- stateShapes[guard] = Object.freeze(shapes);
3438
- }
3520
+ stateShapes[guard] = Object.freeze(shapes);
3439
3521
  break;
3440
3522
  }
3441
3523
  }
@@ -4269,10 +4351,16 @@ export function createXStatePlaybookRuntime<
4269
4351
  spec.composePlayerPrompt ??
4270
4352
  ((input: PlaybookPlayerInput, _identity: XStatePromptIdentity, resuming = false) =>
4271
4353
  defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
4272
- const composeCaptainPrompt =
4354
+ const composeArtifactCaptainPrompt =
4273
4355
  spec.composeCaptainPrompt ??
4274
4356
  ((input: PlaybookCaptainInput) =>
4275
4357
  defaultComposeCaptainPrompt(input, spec.placeholderFields));
4358
+ // DR-090 §2: a direct-Captain continuation is marked by the runtime too.
4359
+ const composeCaptainPrompt = (input: PlaybookCaptainInput): string =>
4360
+ [
4361
+ ...markedContinuationBlocks(input),
4362
+ composeArtifactCaptainPrompt(withoutContinuation(input)),
4363
+ ].join('\n\n');
4276
4364
  const extractFields =
4277
4365
  spec.extractRequiredFields ?? defaultExtractRequiredFields;
4278
4366
  const verbatimPayloadFields: ReadonlySet<string> = new Set(
@@ -4508,6 +4596,9 @@ export function createXStatePlaybookRuntime<
4508
4596
  );
4509
4597
  let effectLedgerMirror = currentEffectLedger();
4510
4598
  let recoveryCheckpoint: PlaybookRuntimeSnapshot['recoveryCheckpoint'];
4599
+ // DR-090 §1: the run's Boss exchanges, in order. A run begins at the
4600
+ // artifact's entry event; the list travels in the snapshot.
4601
+ let bossExchanges: readonly PlaybookBossExchange[] = [];
4511
4602
  let retainedEffectSourceSessionId: string | undefined;
4512
4603
  let retainedEffectReconciliation:
4513
4604
  | PlaybookRuntimeSnapshot['retainedEffectReconciliation']
@@ -5406,7 +5497,18 @@ export function createXStatePlaybookRuntime<
5406
5497
  return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
5407
5498
  }
5408
5499
 
5500
+ // DR-090 §3: the first block names the role and, where the host bound
5501
+ // it, the model the call runs on as configured, so a player can name
5502
+ // both where its project asks (a commit trailer, say).
5503
+ function playerIdentityBlock(roleId: string): string {
5504
+ const binding = session?.roleBindings?.[roleId];
5505
+ return binding === undefined
5506
+ ? `Role: ${roleId}`
5507
+ : `Role: ${roleId} · Model: ${binding.promptIdentity}`;
5508
+ }
5509
+
5409
5510
  function composeBoundPlayerPrompt(input: PlaybookPlayerInput, resuming = false): string {
5511
+ const roleId = requireRoleId(input);
5410
5512
  let active = true;
5411
5513
  const lookup: XStatePromptIdentity = (roleId) => {
5412
5514
  if (!active) {
@@ -5416,11 +5518,49 @@ export function createXStatePlaybookRuntime<
5416
5518
  }
5417
5519
  return promptIdentity(roleId);
5418
5520
  };
5521
+ let body: string;
5419
5522
  try {
5420
- return composePlayerPrompt(input, lookup, resuming);
5523
+ body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
5421
5524
  } finally {
5422
5525
  active = false;
5423
5526
  }
5527
+ // DR-090 §1, §2: every role hears the Boss — the run's exchanges come
5528
+ // before the body whatever the role, and the asking role's marked
5529
+ // continuation carries the exchange it asked.
5530
+ return [
5531
+ playerIdentityBlock(roleId),
5532
+ ...bossExchangeBlocks(bossExchanges, input),
5533
+ ...markedContinuationBlocks(input, resuming),
5534
+ body,
5535
+ ].join('\n\n');
5536
+ }
5537
+
5538
+ /** DR-090 §1: record a Boss reply the machine is about to receive. */
5539
+ function recordBossExchange(
5540
+ event: EventObject,
5541
+ pending: readonly PlaybookPendingBossQuestionContext[],
5542
+ ): void {
5543
+ if (event.type !== 'BOSS_REPLY') return;
5544
+ const { questionId, answer } = event as {
5545
+ readonly questionId?: unknown;
5546
+ readonly answer?: unknown;
5547
+ };
5548
+ if (typeof answer !== 'string' || answer.trim().length === 0) return;
5549
+ const question =
5550
+ pending.find((candidate) => candidate.questionId === questionId) ??
5551
+ (questionId === undefined && pending.length === 1 ? pending[0] : undefined);
5552
+ if (question === undefined) return;
5553
+ bossExchanges = Object.freeze([
5554
+ ...bossExchanges,
5555
+ Object.freeze({ question: question.question, reply: answer }),
5556
+ ]);
5557
+ }
5558
+
5559
+ /** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
5560
+ function beginRunIfEntry(event: EventObject): void {
5561
+ if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
5562
+ bossExchanges = [];
5563
+ }
5424
5564
  }
5425
5565
 
5426
5566
  function continuationKey(
@@ -6706,7 +6846,10 @@ export function createXStatePlaybookRuntime<
6706
6846
  try {
6707
6847
  await claim.assertOwner();
6708
6848
  const restored = snapshotJsonValue(await repositoryCapability!.observe!()) as unknown as PlaybookRepositoryReceipt['baseline'];
6709
- if (!isDeepStrictEqual(restored, saved.baseline)) {
6849
+ // DR-090 §4: the branch an observation names is never compared.
6850
+ const { branch: _restoredBranch, ...restoredState } = restored;
6851
+ const { branch: _savedBranch, ...savedState } = saved.baseline;
6852
+ if (!isDeepStrictEqual(restoredState, savedState)) {
6710
6853
  throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
6711
6854
  }
6712
6855
  signal.throwIfAborted();
@@ -6954,6 +7097,14 @@ export function createXStatePlaybookRuntime<
6954
7097
  ...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
6955
7098
  ? { recoveryCheckpoint } : {}),
6956
7099
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
7100
+ ...(bossExchanges.length === 0
7101
+ ? {}
7102
+ : {
7103
+ bossExchanges: bossExchanges.map(({ question, reply }) => ({
7104
+ question,
7105
+ reply,
7106
+ })),
7107
+ }),
6957
7108
  };
6958
7109
  }
6959
7110
 
@@ -9224,6 +9375,7 @@ export function createXStatePlaybookRuntime<
9224
9375
  deferInspectionEmissions = false;
9225
9376
  deferredInspectionEmissions = [];
9226
9377
  recoveryCheckpoint = undefined;
9378
+ bossExchanges = [];
9227
9379
  suppressInspectionEmissions = false;
9228
9380
  initialized = false;
9229
9381
  traceSequence = 0;
@@ -9671,6 +9823,13 @@ export function createXStatePlaybookRuntime<
9671
9823
  }
9672
9824
  restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
9673
9825
  }
9826
+ // DR-090 §1: a restored or adopted run keeps the Boss exchanges it
9827
+ // heard; a snapshot without them heard none.
9828
+ bossExchanges = Object.freeze(
9829
+ (boundSnapshot.bossExchanges ?? []).map(({ question, reply }) =>
9830
+ Object.freeze({ question, reply }),
9831
+ ),
9832
+ );
9674
9833
  nestedBridge.prepareRestore(suspendedCall);
9675
9834
  if (suspendedCall !== undefined) {
9676
9835
  playbookCallTurnIds.set(
@@ -10870,13 +11029,20 @@ export function createXStatePlaybookRuntime<
10870
11029
  )
10871
11030
  ) {
10872
11031
  const statusLine = classificationStatus(event);
11032
+ const acceptedPending = deferredPending;
11033
+ const acceptedEvent = event;
10873
11034
  const continuation = await continueBoundDeferredOperation(
10874
11035
  deferredOperation,
10875
11036
  event,
10876
11037
  signal,
10877
11038
  turnId,
10878
11039
  statusLine,
10879
- onAccepted,
11040
+ () => {
11041
+ // DR-090 §1: the host started the continuation, so the
11042
+ // machine receives this reply now.
11043
+ recordBossExchange(acceptedEvent, [acceptedPending]);
11044
+ onAccepted?.();
11045
+ },
10880
11046
  );
10881
11047
  if (continuation === 'continued') {
10882
11048
  if (controlPlaneError !== undefined) {
@@ -10921,13 +11087,25 @@ export function createXStatePlaybookRuntime<
10921
11087
  signal.throwIfAborted();
10922
11088
  // 3. A final actor cannot accept new events; reconstruct only
10923
11089
  // after classification produced a real event.
11090
+ const acceptedPending =
11091
+ classifiedSnapshot === undefined
11092
+ ? []
11093
+ : pendingBossQuestionsFor(
11094
+ normalizePlaybookSnapshot(classifiedSnapshot),
11095
+ ((classifiedSnapshot as { context?: unknown }).context ??
11096
+ {}) as Record<string, unknown>,
11097
+ );
10924
11098
  if (actor.getSnapshot().status === 'done') {
10925
11099
  stopActor();
10926
11100
  actor = buildActor(runtimePorts!);
10927
11101
  // The replacement actor's snapshots are real state entries.
10928
11102
  suppressInspectionEmissions = false;
10929
11103
  actor.start();
11104
+ // DR-090 §1: a finished run's exchanges end with it.
11105
+ bossExchanges = [];
10930
11106
  }
11107
+ beginRunIfEntry(event);
11108
+ recordBossExchange(event, acceptedPending);
10931
11109
  onAccepted?.();
10932
11110
  actor.send(event);
10933
11111
  await waitForPlaybookQuiescence(actor, {
@@ -11236,6 +11414,7 @@ export function createXStatePlaybookRuntime<
11236
11414
  failedGovernedAttemptId = undefined;
11237
11415
  controlPlaneError = undefined;
11238
11416
  emissionFailure = undefined;
11417
+ bossExchanges = [];
11239
11418
  retainedEffectSourceSessionId = undefined;
11240
11419
  retainedEffectReconciliation = undefined;
11241
11420
  retainedEffectReconciliationRequired = false;
@@ -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, or whose shape the
60
- * machine does not reveal, is `string`.
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 non-string shapes of its semantic-owned fields; a field
65
- * left out is a string.
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; every field it leaves out is a string.
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
  }
@@ -681,6 +681,13 @@ function effectInteger(value, path, minimum = 0) {
681
681
  }
682
682
  return value;
683
683
  }
684
+ // DR-090 §4: an observation's branch is evidence for the Boss only; two
685
+ // observations of one repository state are equal whatever branch each names.
686
+ function observedStatesEqual(left, right) {
687
+ const { branch: _left, ...leftState } = left;
688
+ const { branch: _right, ...rightState } = right;
689
+ return jsonValuesEqual(leftState, rightState);
690
+ }
684
691
  function jsonValuesEqual(left, right) {
685
692
  if (Object.is(left, right))
686
693
  return true;
@@ -760,13 +767,17 @@ function effectPreExisting(value, path, baseline) {
760
767
  function effectObservation(value, path) {
761
768
  if (!isRecord(value))
762
769
  throw new TypeError(`${path} must be an object`);
763
- rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'projection', 'projectionDigest'], path);
770
+ rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'branch', 'projection', 'projectionDigest'], path);
764
771
  requireNonEmptyString(value.worktree, `${path}.worktree`);
765
772
  requireNonEmptyString(value.gitDir, `${path}.gitDir`);
766
773
  const head = requireNonEmptyString(value.head, `${path}.head`);
767
774
  if (!EFFECT_OID_PATTERN.test(head)) {
768
775
  throw new TypeError(`${path}.head must be a canonical Git commit OID`);
769
776
  }
777
+ // DR-090 §4: the branch HEAD names, evidence for the Boss only.
778
+ if (own(value, 'branch')) {
779
+ requireNonEmptyString(value.branch, `${path}.branch`);
780
+ }
770
781
  if (!isRecord(value.projection)) {
771
782
  throw new TypeError(`${path}.projection must be a path-keyed object`);
772
783
  }
@@ -854,7 +865,7 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
854
865
  }
855
866
  if (classification === 'unchanged' &&
856
867
  after !== undefined &&
857
- !jsonValuesEqual(baseline, after)) {
868
+ !observedStatesEqual(baseline, after)) {
858
869
  throw new TypeError(`${path} classified unchanged observations that differ`);
859
870
  }
860
871
  // DR-062 §3: a receipt records the fate of every baseline entry, so the
@@ -997,12 +1008,13 @@ function snapshotSemanticCandidate(value, outcomes, shapes) {
997
1008
  (extra.length === 0 ? '' : `; extra or wrongly owned ${extra.join(', ')}`));
998
1009
  }
999
1010
  // DR-089: each semantic-owned field has the shape the compiled machine
1000
- // admits for it, a string unless the machine admits another.
1011
+ // admits for it. DR-091: a field whose shape the machine did not reveal
1012
+ // takes any JSON value, and the machine's own validator decides it.
1001
1013
  const outcomeShapes = shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
1002
1014
  for (const field of semanticFields) {
1003
- const shape = outcomeShapes !== undefined && own(outcomeShapes, field)
1004
- ? outcomeShapes[field]
1005
- : 'string';
1015
+ if (outcomeShapes === undefined || !own(outcomeShapes, field))
1016
+ continue;
1017
+ const shape = outcomeShapes[field];
1006
1018
  if (!isPlaybookSemanticValueShape(detached[field], shape)) {
1007
1019
  semanticCandidateStructureError(shape === 'string'
1008
1020
  ? `field ${JSON.stringify(field)} must be a string`
@@ -1417,7 +1429,7 @@ function effectBoundary(value, index) {
1417
1429
  }
1418
1430
  if (own(value, 'restored')) {
1419
1431
  const restored = effectObservation(value.restored, `${path}.restored`);
1420
- if (!jsonValuesEqual(restored, baseline) ||
1432
+ if (!observedStatesEqual(restored, baseline) ||
1421
1433
  !isRecord(value.physicalReceipt) || after?.head !== baseline.head ||
1422
1434
  value.dispositions.some((item) => item !== 'unchanged') ||
1423
1435
  value.cohortId !== undefined || value.logicalOperationId !== undefined) {
@@ -1506,7 +1518,7 @@ function effectLogicalOperation(value, index, boundaryById) {
1506
1518
  const previous = boundaries[boundaryIndex - 1];
1507
1519
  if (previous.physicalReceipt === undefined ||
1508
1520
  previous.after === undefined ||
1509
- !jsonValuesEqual(boundary.baseline, previous.after)) {
1521
+ !observedStatesEqual(boundary.baseline, previous.after)) {
1510
1522
  throw new TypeError(`${path}.boundaryIds must form one completed checkpoint chain`);
1511
1523
  }
1512
1524
  }
@@ -1517,7 +1529,7 @@ function effectLogicalOperation(value, index, boundaryById) {
1517
1529
  if (checkpoint !== undefined) {
1518
1530
  assertObservationIdentity(checkpoint, originalBaseline, `${path}.checkpoint`);
1519
1531
  if (latestAfter === undefined ||
1520
- !jsonValuesEqual(checkpoint, latestAfter)) {
1532
+ !observedStatesEqual(checkpoint, latestAfter)) {
1521
1533
  throw new TypeError(`${path}.checkpoint must equal its latest boundary after`);
1522
1534
  }
1523
1535
  }
@@ -1810,6 +1822,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
1810
1822
  'failedEffectAttempt',
1811
1823
  'recoveryCheckpoint',
1812
1824
  'suspendedCall',
1825
+ 'bossExchanges',
1813
1826
  ], 'runtime snapshot');
1814
1827
  let suspendedCall;
1815
1828
  if (own(snapshot, 'suspendedCall')) {
@@ -1994,6 +2007,23 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
1994
2007
  }
1995
2008
  recoveryCheckpoint = Object.freeze({ stateId, prompt, boundaryPrefix, machine: snapshotJsonValue(checkpoint.machine), ...(own(checkpoint, 'id') ? { id: effectUuid(checkpoint.id, 'recoveryCheckpoint.id') } : {}), ...(own(checkpoint, 'result') ? { result: snapshotJsonValue(checkpoint.result) } : {}), ...(checkpoint.delivered === true ? { delivered: true } : {}) });
1996
2009
  }
2010
+ // DR-090 §1: the run's Boss exchanges, present only when there is one.
2011
+ let bossExchanges;
2012
+ if (own(snapshot, 'bossExchanges')) {
2013
+ const list = snapshot.bossExchanges;
2014
+ if (!Array.isArray(list) || list.length === 0) {
2015
+ throw new TypeError('runtime snapshot bossExchanges must be a nonempty list');
2016
+ }
2017
+ bossExchanges = Object.freeze(list.map((exchange, index) => {
2018
+ const path = `runtime snapshot bossExchanges[${index}]`;
2019
+ if (!isRecord(exchange))
2020
+ throw new TypeError(`${path} must be an object`);
2021
+ rejectUnknownKeys(exchange, ['question', 'reply'], path);
2022
+ const question = requireNonEmptyString(exchange.question, `${path}.question`);
2023
+ const reply = requireNonEmptyString(exchange.reply, `${path}.reply`);
2024
+ return Object.freeze({ question, reply });
2025
+ }));
2026
+ }
1997
2027
  const fields = {
1998
2028
  playbookId,
1999
2029
  machine,
@@ -2017,6 +2047,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
2017
2047
  schemaVersion: 4,
2018
2048
  ...fields,
2019
2049
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
2050
+ ...(bossExchanges === undefined ? {} : { bossExchanges }),
2020
2051
  });
2021
2052
  }
2022
2053
  export class NestedPlaybookCallError extends Error {