@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.
@@ -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');
@@ -939,14 +995,20 @@ export function renderGovernedOutcomeContract(guard, description, outcome, shape
939
995
  for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
940
996
  if (authority === 'semantic') {
941
997
  const entry = authored.get(field);
942
- // DR-089: the value has the shape the compiled machine admits.
998
+ // DR-089: the value has the shape the compiled machine admits; a
999
+ // shape the probe did not reveal is the machine's to judge, so the
1000
+ // example asserts none (DR-091).
943
1001
  const shape = shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
944
1002
  ? shapes[field]
945
- : 'string';
946
- const words = describePlaybookSemanticValueShape(shape);
1003
+ : undefined;
1004
+ const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
947
1005
  replyMembers.push(`${JSON.stringify(field)}: ${entry?.placeholder ??
948
- (shape === 'string' ? '<string>' : `<${words.replace(/^a /, '')}>`)}`);
949
- if (shape !== 'string')
1006
+ (shape === undefined
1007
+ ? '<value>'
1008
+ : shape === 'string'
1009
+ ? '<string>'
1010
+ : `<${words.replace(/^a /, '')}>`)}`);
1011
+ if (shape !== undefined && shape !== 'string')
950
1012
  valueShapes.push(`\`${field}\` is ${words}`);
951
1013
  semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
952
1014
  }
@@ -2005,8 +2067,9 @@ function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPaylo
2005
2067
  // the machine's transition selection — guards only, no action runs — with
2006
2068
  // sample values, and the first combination of shapes whose arm carries that
2007
2069
  // outcome's accepted-outcome marker is the contract the judge is shown and the
2008
- // reconciler enforces. A field whose shape the machine does not reveal, or
2009
- // that it admits as a string, stays a string.
2070
+ // reconciler enforces. A field the machine admits as a string is recorded as
2071
+ // one; a field whose shape it does not reveal is recorded with none, so the
2072
+ // reconciler leaves its value to the machine's own validator (DR-091).
2010
2073
  // ---------------------------------------------------------------------------
2011
2074
  const SEMANTIC_SHAPE_SAMPLES = [
2012
2075
  ['string', 'sample'],
@@ -2045,9 +2108,10 @@ function shapeCombinations(fieldCount) {
2045
2108
  left.join(',').localeCompare(right.join(',')));
2046
2109
  }
2047
2110
  /**
2048
- * DR-089: the non-string JSON value shape the compiled machine admits for
2049
- * each semantic-owned field of each governed outcome, keyed by state id and
2050
- * then by outcome. Exported so linked machinery of an artifact's own reads
2111
+ * DR-089: the JSON value shape the compiled machine admits for each
2112
+ * semantic-owned field of each governed outcome, keyed by state id and then
2113
+ * by outcome. A field, outcome, or state the probe reveals no shape for is
2114
+ * left out (DR-091). Exported so linked machinery of an artifact's own reads
2051
2115
  * the same contract the shared factory enforces.
2052
2116
  */
2053
2117
  export function inferSemanticValueShapes(machine, authority) {
@@ -2127,18 +2191,17 @@ export function inferSemanticValueShapes(machine, authority) {
2127
2191
  });
2128
2192
  if (!admits(values))
2129
2193
  continue;
2194
+ // DR-091: an admitted string is recorded too, so only a field the
2195
+ // probe revealed nothing for goes unrecorded.
2130
2196
  const shapes = {};
2131
2197
  semanticFields.forEach((field, index) => {
2132
2198
  let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]][0];
2133
2199
  if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
2134
2200
  shape = 'string-or-number-array';
2135
2201
  }
2136
- if (shape !== 'string')
2137
- shapes[field] = shape;
2202
+ shapes[field] = shape;
2138
2203
  });
2139
- if (Object.keys(shapes).length > 0) {
2140
- stateShapes[guard] = Object.freeze(shapes);
2141
- }
2204
+ stateShapes[guard] = Object.freeze(shapes);
2142
2205
  break;
2143
2206
  }
2144
2207
  }
@@ -2715,8 +2778,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
2715
2778
  }
2716
2779
  const composePlayerPrompt = spec.composePlayerPrompt ??
2717
2780
  ((input, _identity, resuming = false) => defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
2718
- const composeCaptainPrompt = spec.composeCaptainPrompt ??
2781
+ const composeArtifactCaptainPrompt = spec.composeCaptainPrompt ??
2719
2782
  ((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
2783
+ // DR-090 §2: a direct-Captain continuation is marked by the runtime too.
2784
+ const composeCaptainPrompt = (input) => [
2785
+ ...markedContinuationBlocks(input),
2786
+ composeArtifactCaptainPrompt(withoutContinuation(input)),
2787
+ ].join('\n\n');
2720
2788
  const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
2721
2789
  const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
2722
2790
  const adjudication = {
@@ -2865,6 +2933,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2865
2933
  const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
2866
2934
  let effectLedgerMirror = currentEffectLedger();
2867
2935
  let recoveryCheckpoint;
2936
+ // DR-090 §1: the run's Boss exchanges, in order. A run begins at the
2937
+ // artifact's entry event; the list travels in the snapshot.
2938
+ let bossExchanges = [];
2868
2939
  let retainedEffectSourceSessionId;
2869
2940
  let retainedEffectReconciliation;
2870
2941
  let retainedEffectReconciliationRequired = false;
@@ -3495,7 +3566,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
3495
3566
  }
3496
3567
  return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
3497
3568
  }
3569
+ // DR-090 §3: the first block names the role and, where the host bound
3570
+ // it, the model the call runs on as configured, so a player can name
3571
+ // both where its project asks (a commit trailer, say).
3572
+ function playerIdentityBlock(roleId) {
3573
+ const binding = session?.roleBindings?.[roleId];
3574
+ return binding === undefined
3575
+ ? `Role: ${roleId}`
3576
+ : `Role: ${roleId} · Model: ${binding.promptIdentity}`;
3577
+ }
3498
3578
  function composeBoundPlayerPrompt(input, resuming = false) {
3579
+ const roleId = requireRoleId(input);
3499
3580
  let active = true;
3500
3581
  const lookup = (roleId) => {
3501
3582
  if (!active) {
@@ -3503,12 +3584,44 @@ export function createXStatePlaybookRuntime(machine, spec) {
3503
3584
  }
3504
3585
  return promptIdentity(roleId);
3505
3586
  };
3587
+ let body;
3506
3588
  try {
3507
- return composePlayerPrompt(input, lookup, resuming);
3589
+ body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
3508
3590
  }
3509
3591
  finally {
3510
3592
  active = false;
3511
3593
  }
3594
+ // DR-090 §1, §2: every role hears the Boss — the run's exchanges come
3595
+ // before the body whatever the role, and the asking role's marked
3596
+ // continuation carries the exchange it asked.
3597
+ return [
3598
+ playerIdentityBlock(roleId),
3599
+ ...bossExchangeBlocks(bossExchanges, input),
3600
+ ...markedContinuationBlocks(input, resuming),
3601
+ body,
3602
+ ].join('\n\n');
3603
+ }
3604
+ /** DR-090 §1: record a Boss reply the machine is about to receive. */
3605
+ function recordBossExchange(event, pending) {
3606
+ if (event.type !== 'BOSS_REPLY')
3607
+ return;
3608
+ const { questionId, answer } = event;
3609
+ if (typeof answer !== 'string' || answer.trim().length === 0)
3610
+ return;
3611
+ const question = pending.find((candidate) => candidate.questionId === questionId) ??
3612
+ (questionId === undefined && pending.length === 1 ? pending[0] : undefined);
3613
+ if (question === undefined)
3614
+ return;
3615
+ bossExchanges = Object.freeze([
3616
+ ...bossExchanges,
3617
+ Object.freeze({ question: question.question, reply: answer }),
3618
+ ]);
3619
+ }
3620
+ /** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
3621
+ function beginRunIfEntry(event) {
3622
+ if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
3623
+ bossExchanges = [];
3624
+ }
3512
3625
  }
3513
3626
  function continuationKey(roleId, playerId) {
3514
3627
  return playerId ?? roleId;
@@ -4372,7 +4485,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
4372
4485
  try {
4373
4486
  await claim.assertOwner();
4374
4487
  const restored = snapshotJsonValue(await repositoryCapability.observe());
4375
- if (!isDeepStrictEqual(restored, saved.baseline)) {
4488
+ // DR-090 §4: the branch an observation names is never compared.
4489
+ const { branch: _restoredBranch, ...restoredState } = restored;
4490
+ const { branch: _savedBranch, ...savedState } = saved.baseline;
4491
+ if (!isDeepStrictEqual(restoredState, savedState)) {
4376
4492
  throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
4377
4493
  }
4378
4494
  signal.throwIfAborted();
@@ -4580,6 +4696,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
4580
4696
  ...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
4581
4697
  ? { recoveryCheckpoint } : {}),
4582
4698
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
4699
+ ...(bossExchanges.length === 0
4700
+ ? {}
4701
+ : {
4702
+ bossExchanges: bossExchanges.map(({ question, reply }) => ({
4703
+ question,
4704
+ reply,
4705
+ })),
4706
+ }),
4583
4707
  };
4584
4708
  }
4585
4709
  async function runJournalledStep(kind, input, signal, execute) {
@@ -6354,6 +6478,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6354
6478
  deferInspectionEmissions = false;
6355
6479
  deferredInspectionEmissions = [];
6356
6480
  recoveryCheckpoint = undefined;
6481
+ bossExchanges = [];
6357
6482
  suppressInspectionEmissions = false;
6358
6483
  initialized = false;
6359
6484
  traceSequence = 0;
@@ -6706,6 +6831,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
6706
6831
  }
6707
6832
  restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
6708
6833
  }
6834
+ // DR-090 §1: a restored or adopted run keeps the Boss exchanges it
6835
+ // heard; a snapshot without them heard none.
6836
+ bossExchanges = Object.freeze((boundSnapshot.bossExchanges ?? []).map(({ question, reply }) => Object.freeze({ question, reply })));
6709
6837
  nestedBridge.prepareRestore(suspendedCall);
6710
6838
  if (suspendedCall !== undefined) {
6711
6839
  playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
@@ -7671,7 +7799,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
7671
7799
  deferredOperation !== undefined) {
7672
7800
  if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
7673
7801
  const statusLine = classificationStatus(event);
7674
- const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, onAccepted);
7802
+ const acceptedPending = deferredPending;
7803
+ const acceptedEvent = event;
7804
+ const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, () => {
7805
+ // DR-090 §1: the host started the continuation, so the
7806
+ // machine receives this reply now.
7807
+ recordBossExchange(acceptedEvent, [acceptedPending]);
7808
+ onAccepted?.();
7809
+ });
7675
7810
  if (continuation === 'continued') {
7676
7811
  if (controlPlaneError !== undefined) {
7677
7812
  throw controlPlaneError;
@@ -7715,13 +7850,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
7715
7850
  signal.throwIfAborted();
7716
7851
  // 3. A final actor cannot accept new events; reconstruct only
7717
7852
  // after classification produced a real event.
7853
+ const acceptedPending = classifiedSnapshot === undefined
7854
+ ? []
7855
+ : pendingBossQuestionsFor(normalizePlaybookSnapshot(classifiedSnapshot), (classifiedSnapshot.context ??
7856
+ {}));
7718
7857
  if (actor.getSnapshot().status === 'done') {
7719
7858
  stopActor();
7720
7859
  actor = buildActor(runtimePorts);
7721
7860
  // The replacement actor's snapshots are real state entries.
7722
7861
  suppressInspectionEmissions = false;
7723
7862
  actor.start();
7863
+ // DR-090 §1: a finished run's exchanges end with it.
7864
+ bossExchanges = [];
7724
7865
  }
7866
+ beginRunIfEntry(event);
7867
+ recordBossExchange(event, acceptedPending);
7725
7868
  onAccepted?.();
7726
7869
  actor.send(event);
7727
7870
  await waitForPlaybookQuiescence(actor, {
@@ -8002,6 +8145,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
8002
8145
  failedGovernedAttemptId = undefined;
8003
8146
  controlPlaneError = undefined;
8004
8147
  emissionFailure = undefined;
8148
+ bossExchanges = [];
8005
8149
  retainedEffectSourceSessionId = undefined;
8006
8150
  retainedEffectReconciliation = undefined;
8007
8151
  retainedEffectReconciliationRequired = false;