@sublang/playbook 4.0.0 → 5.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 +65 -122
- package/docs/assets/playbook-venn.svg +13 -0
- package/docs/cli.md +21 -7
- package/docs/configuration.md +5 -3
- package/package.json +4 -2
- package/reference/sdlc/captain.md +70 -83
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +127 -142
- package/reference/sdlc/captain.playbook/captain.fsm.js +349 -470
- package/reference/sdlc/captain.playbook/captain.fsm.ts +535 -598
- package/reference/sdlc/captain.playbook/captain.gears.md +37 -41
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +90 -15
- package/reference/sdlc/captain.playbook/captain.playbook.js +464 -976
- package/reference/sdlc/captain.playbook/captain.playbook.ts +696 -1001
- package/reference/sdlc/code.playbook/code.playbook.js +17 -0
- package/reference/sdlc/code.playbook/code.playbook.ts +17 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +1785 -237
- package/reference/sdlc/code.playbook/playbook-captain.ts +2281 -344
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +41 -9
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +42 -9
- package/slc/gears2fsm.md +54 -2
- package/slc/link.md +293 -25
- package/src/runtime.d.ts +29 -1
- package/src/runtime.ts +47 -0
- package/src/xstate-playbook-runtime.d.ts +87 -5
- package/src/xstate-playbook-runtime.js +763 -28
- package/src/xstate-playbook-runtime.ts +950 -31
package/slc/link.md
CHANGED
|
@@ -143,7 +143,7 @@ exactly one `pendingCall` is active.
|
|
|
143
143
|
Control-plane exceptions reject the runtime method rather than masquerade as a
|
|
144
144
|
recoverable workflow `failed` result.
|
|
145
145
|
|
|
146
|
-
`PlaybookRuntimeOptions` is host-agnostic and carries only _per-run_ knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders)
|
|
146
|
+
`PlaybookRuntimeOptions` is host-agnostic and carries only _per-run_ knobs such as identity strings (e.g., model names a playbook substitutes into prompt placeholders), strategy overrides the linker exposes, and — where the compiled playbook's policy needs a host seam — host-supplied port-shaped callbacks the linker exposes as option members whose types the artifact itself declares, so the six-member `PlaybookPorts` contract and the shared contract module stay free of host types.
|
|
147
147
|
The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
|
|
148
148
|
The CLI's absence of `--link-option` values does not mean that
|
|
149
149
|
`PlaybookRuntimeOptions` is empty. CLI link options are compile-time inputs;
|
|
@@ -240,6 +240,10 @@ type PlaybookCallStart =
|
|
|
240
240
|
|
|
241
241
|
`PlayerResult` mirrors the status, resume token, final text, and error fields of cligent's `PlayerRunResult` ([TMUX-033](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-033)).
|
|
242
242
|
The runtime treats `status !== 'ok'` as a player failure and routes it through the FSM's error path (§Abort).
|
|
243
|
+
An `ok` player result whose `finalText` is missing, empty, or whitespace-only
|
|
244
|
+
earns exactly one corrective re-ask: the same player call repeated under the
|
|
245
|
+
stored resume selection, traced as its own player-call pair, before a second
|
|
246
|
+
such result routes through the same error path.
|
|
243
247
|
|
|
244
248
|
`callCaptain` runs a direct-Captain FSM actor against the host's Captain
|
|
245
249
|
agent. The linked runtime shall pass
|
|
@@ -257,9 +261,13 @@ array requests a tool-free call, while omission preserves the host Captain's
|
|
|
257
261
|
configured tools.
|
|
258
262
|
`CaptainResult` carries no resume token or player-continuation selection.
|
|
259
263
|
A non-`ok`
|
|
260
|
-
result, or an `ok` result
|
|
261
|
-
failure on the call's single finish trace
|
|
262
|
-
|
|
264
|
+
result, or an `ok` result whose `finalText` is missing, empty, or
|
|
265
|
+
whitespace-only, shall record that failure on the call's single finish trace.
|
|
266
|
+
A non-`ok` result shall then reject the actor through the FSM's error path
|
|
267
|
+
with no corrective re-ask; an empty `ok` result shall first earn exactly one
|
|
268
|
+
corrective re-ask — the same call repeated, traced as its own
|
|
269
|
+
started/finished pair — and only a second such result shall reject the actor
|
|
270
|
+
the same way. These structured host-result failures are recoverable
|
|
263
271
|
workflow failures, not control-plane failures: the runtime shall let the actor
|
|
264
272
|
take `onError`, drive it to quiescence, drain ordered emissions, and resolve
|
|
265
273
|
the public method with `{ outcome: 'failed' }` carrying the failure state's
|
|
@@ -347,6 +355,8 @@ type PlaybookTraceType =
|
|
|
347
355
|
| 'captain.call.finished'
|
|
348
356
|
| 'playbook.call.started'
|
|
349
357
|
| 'playbook.call.finished'
|
|
358
|
+
| 'apply.started'
|
|
359
|
+
| 'apply.finished'
|
|
350
360
|
| 'fsm.transition'
|
|
351
361
|
| 'status.emitted'
|
|
352
362
|
| 'boss.input.settled'
|
|
@@ -373,10 +383,19 @@ The trace types are `session.started`, `boss.input.received`,
|
|
|
373
383
|
`judge.call.started`, `judge.call.finished`, `player.call.started`,
|
|
374
384
|
`player.call.finished`, `captain.call.started`, `captain.call.finished`,
|
|
375
385
|
`playbook.call.started`,
|
|
376
|
-
`playbook.call.finished`, `
|
|
386
|
+
`playbook.call.finished`, `apply.started`, `apply.finished`,
|
|
387
|
+
`fsm.transition`, `status.emitted`,
|
|
377
388
|
`boss.input.settled`, and `session.disposed`.
|
|
378
389
|
Call pairs carry exact prompts and replies, normalized failures, actor and state
|
|
379
390
|
identity, and their boundary-specific options.
|
|
391
|
+
`apply.started` and `apply.finished` are the paired schema-2 boundary of an
|
|
392
|
+
executed `apply()` call on a runtime implementing the optional control surface
|
|
393
|
+
(§Control surface): both carry the action id and idempotency `key` (plus the
|
|
394
|
+
singular `stateId` on start when one exists), the pair shares one
|
|
395
|
+
session-unique `apply-<n>` call id and the boundary's turn id, and the finish
|
|
396
|
+
adds the receipt `disposition` with its `reason`, normalized `error`, or
|
|
397
|
+
projected `run` result, all JSON-safe. A repeated idempotency key returns the
|
|
398
|
+
recorded receipt without a new pair.
|
|
380
399
|
Direct-Captain start and finish payloads shall carry `allowedTools` exactly when
|
|
381
400
|
the originating `CaptainCallOptions` selects it and shall omit the member when
|
|
382
401
|
the call preserves the host Captain's configured tools.
|
|
@@ -392,16 +411,23 @@ its message and optional data; consumers shall not have to recover state
|
|
|
392
411
|
identity from a nested ad hoc object.
|
|
393
412
|
Judge results use `reply`; player start and finish payloads both carry the
|
|
394
413
|
selected `resume`; Captain start and finish payloads both carry
|
|
395
|
-
the exact composed prompt, `visibility
|
|
396
|
-
`stateId` and `sourceItem`, and no player resume selection or
|
|
414
|
+
the exact composed prompt, the boundary's selected `visibility`, the direct
|
|
415
|
+
invocation's `stateId` and `sourceItem`, and no player resume selection or
|
|
416
|
+
resume token — a visible workflow call carries its runtime-owned
|
|
417
|
+
`resume: false` selection, while a hidden controller call
|
|
418
|
+
(§Captain adjudication) omits the `resume` member altogether, its
|
|
419
|
+
durable-conversation selection being host-owned;
|
|
397
420
|
judge `purpose` is
|
|
398
421
|
`boss-input-classification`, `player-output-adjudication`, or
|
|
399
422
|
`captain-output-adjudication`; and every error uses
|
|
400
423
|
`{ name, message, stack? }` rather than a raw string or `Error` instance.
|
|
401
424
|
The Captain finish payload shall preserve the exact `CaptainResult` status and
|
|
402
425
|
final text when present, while carrying any failure in normalized form.
|
|
403
|
-
An `ok` result without `finalText` therefore retains status `ok` but
|
|
404
|
-
carries the normalized missing-text failure
|
|
426
|
+
An `ok` result without non-empty `finalText` therefore retains status `ok` but
|
|
427
|
+
also carries the normalized missing-text failure; the corrective re-ask that
|
|
428
|
+
follows (§PlaybookPorts contract) traces as its own started/finished pair, and,
|
|
429
|
+
when that corrective call happens, only a second such finish makes the actor
|
|
430
|
+
reject.
|
|
405
431
|
If the Captain port rejects before returning a result, the finish instead
|
|
406
432
|
carries explicit `status: 'aborted'` when the combined signal has aborted or
|
|
407
433
|
`status: 'error'` otherwise. A finish boundary never omits status merely
|
|
@@ -596,8 +622,17 @@ Where the current ready or reconstructed terminal machine accepts exactly one
|
|
|
596
622
|
ordinary textual entry event and no Boss question is pending, the runtime
|
|
597
623
|
shall send that event deterministically and attach the exact original text to
|
|
598
624
|
its declared textual payload field without invoking `callJudge`.
|
|
599
|
-
The default Captain
|
|
600
|
-
|
|
625
|
+
The default Captain — the controller playbook of
|
|
626
|
+
[gears2fsm "Setup"](gears2fsm.md#setup) — is deterministic at every parked
|
|
627
|
+
entry: the runtime maps each Boss turn from the exact text and the host's
|
|
628
|
+
deterministic command-parse resolution, supplied through the linked options'
|
|
629
|
+
controller port, to the rewritten machine's hub entry union —
|
|
630
|
+
`{ type: 'BOSS_TURN', bossText: turn.text }` for an undecided turn,
|
|
631
|
+
`{ type: 'PARSED_RESPOND', bossText: turn.text }` for a parse-resolved
|
|
632
|
+
`respond`, `{ type: 'PARSED_ACTION', bossText: turn.text, decision }` carrying
|
|
633
|
+
the injected parse-resolved decision object, and `{ type: 'SHUTDOWN' }` for the
|
|
634
|
+
host's teardown resolution — and invokes no classifier judge call; the exact
|
|
635
|
+
original text still rides only the runtime-owned textual payload field.
|
|
601
636
|
All other non-empty turns shall use `callJudge` only to choose one of the FSM's
|
|
602
637
|
event kinds and non-text routing fields, or no FSM action.
|
|
603
638
|
The classifier prompt shall include the exact, unmodified `turn.text` in a
|
|
@@ -677,7 +712,7 @@ If a host forwards text beginning with `/` to `handleBossInput`, the runtime tre
|
|
|
677
712
|
Hosts that receive structured control input shall resolve host-level concerns before choosing a playbook runtime.
|
|
678
713
|
Once they call `handleBossInput`, they shall pass the Boss content as text and shall not pre-classify in-playbook FSM events or rely on slash forms as a runtime protocol.
|
|
679
714
|
|
|
680
|
-
`BOSS_INTERRUPT` (or the FSM's equivalent explicit-state-jump event) is reached only by the judge choosing it and supplying its required target payload.
|
|
715
|
+
Within `handleBossInput` classification, `BOSS_INTERRUPT` (or the FSM's equivalent explicit-state-jump event) is reached only by the judge choosing it and supplying its required target payload; `apply()` of a runtime-advertised action (§Control surface) is the second, runtime-validated path to the same events, and on neither path does the host fabricate an FSM event itself.
|
|
681
716
|
It is _not_ an abort surface; aborts go through the abort signal and the strategies in §Abort.
|
|
682
717
|
Hosts where the abort signal is terminal (e.g., SIGINT runs shutdown) shall not route abort to `BOSS_INTERRUPT`.
|
|
683
718
|
|
|
@@ -746,6 +781,55 @@ presentation. The linked
|
|
|
746
781
|
runtime shall not make a second visible Captain call or expose the hidden
|
|
747
782
|
structured adjudication merely to present the same response.
|
|
748
783
|
|
|
784
|
+
That visible-call presentation — visible Captain prose as the Boss
|
|
785
|
+
presentation, a separate hidden adjudicator that never authors the
|
|
786
|
+
`question`/`response` fields, and runtime injection of the visible
|
|
787
|
+
`finalText` — stays scoped to visible-presentation playbooks such as CODE
|
|
788
|
+
and DISCUSS. For a controller playbook, whose FSM declares the controller
|
|
789
|
+
decision-state class of [gears2fsm "Setup"](gears2fsm.md#setup), the
|
|
790
|
+
Captain-call presentation admits the hidden controller form instead
|
|
791
|
+
(DR-029): the decision and closing-reply Captain calls run
|
|
792
|
+
`{ visibility: 'hidden' }` on the host's durable conversation, whose resume
|
|
793
|
+
token the host pins and rotates (DR-029), and the decision call's reply
|
|
794
|
+
is the `{ action, … }` control JSON itself — validated by the linked runtime
|
|
795
|
+
against the declared decision-state contract rather than adjudicated through
|
|
796
|
+
a separate judge call, with exactly one corrective re-ask appending the
|
|
797
|
+
rejection reason and the restated reply contract (the DR-025 corrective
|
|
798
|
+
pattern). Because the host owns that conversation, the `resume` member the
|
|
799
|
+
runtime is required to pass on a controller call carries no continuity
|
|
800
|
+
meaning: the runtime passes `resume: false` because it holds no token, and
|
|
801
|
+
the host's pinned durable selection overrides it — a controller call shall
|
|
802
|
+
never be read as a request for a fresh conversation.
|
|
803
|
+
|
|
804
|
+
The validated selection is not itself an effect. The linked runtime shall
|
|
805
|
+
submit it through the host-supplied controller port the linker exposed as an
|
|
806
|
+
option member (§PlaybookRuntime contract) and shall take the returned
|
|
807
|
+
settlement as the only evidence of what happened. That settlement, carried
|
|
808
|
+
beside the selected action as the guard discriminant, is the decision
|
|
809
|
+
invocation's own result — the actor output the decision state's `onDone` arms
|
|
810
|
+
select on and its evidence action records — so the effect reports through the
|
|
811
|
+
same invocation that decided it, with no second boundary and no host-sent
|
|
812
|
+
event. A controller prose state (a parse-resolved `respond` reply, an acting
|
|
813
|
+
turn's closing reply) settles on its declared single outcome and returns no
|
|
814
|
+
prose to the machine at all.
|
|
815
|
+
|
|
816
|
+
That reply is control data, never Boss presentation: the runtime
|
|
817
|
+
shall not inject the `question` or `response` presentation fields into a
|
|
818
|
+
controller result — no visible Captain call exists to own them. Controller
|
|
819
|
+
prose reaches the Boss only as host-validated captain speech surfaced
|
|
820
|
+
through the host's presentation seam, cligent `CaptainContext.emitReply`
|
|
821
|
+
(DR-029). Because no prose returns to the machine and no presentation
|
|
822
|
+
field is injected, the host's own `callCaptain` implementation is that seam:
|
|
823
|
+
it already holds the `CaptainResult` of the call it just served, and it
|
|
824
|
+
identifies which call that is from the paired `captain.call.started` boundary
|
|
825
|
+
the runtime emits before invoking the port, whose identity carries the
|
|
826
|
+
invoking `stateId` and `sourceItem` (§Playbook trace). No prose therefore
|
|
827
|
+
needs a return path through the machine, and none exists.
|
|
828
|
+
The visible-call `question`/`response` injection rule above and
|
|
829
|
+
the `{ visibility: 'visible', resume: false }` workflow-call selection
|
|
830
|
+
(§PlaybookPorts contract, §Captain prompt composition) stay the
|
|
831
|
+
visible-presentation shape for non-controller playbooks.
|
|
832
|
+
|
|
749
833
|
The adjudicator shall fail loudly on:
|
|
750
834
|
|
|
751
835
|
- A guard the state does not declare,
|
|
@@ -791,9 +875,9 @@ independent cleanup evidence.
|
|
|
791
875
|
## Script execution
|
|
792
876
|
|
|
793
877
|
Where the FSM declares the typed `script` actor from
|
|
794
|
-
[gears2fsm "Setup"](gears2fsm.md#setup),
|
|
795
|
-
its implementation through the shared factory (§Output); the linker
|
|
796
|
-
not regenerate a script executor inside
|
|
878
|
+
[gears2fsm "Setup"](gears2fsm.md#setup), a factory-backed linked runtime shall
|
|
879
|
+
provide its implementation through the shared factory (§Output); the linker
|
|
880
|
+
shall not regenerate a script executor inside a factory-backed emitted module.
|
|
797
881
|
A script invocation is the one actor kind that runs without any agent:
|
|
798
882
|
it makes no `callPlayer`, `callCaptain`, or `callJudge` call and needs no
|
|
799
883
|
adjudication.
|
|
@@ -1170,6 +1254,162 @@ not `active` or whose state descriptor cannot be normalized shall fail
|
|
|
1170
1254
|
A restore failure shall leave the runtime unbound so `dispose` remains
|
|
1171
1255
|
callable and terminal.
|
|
1172
1256
|
|
|
1257
|
+
## Control surface (optional)
|
|
1258
|
+
|
|
1259
|
+
A linked runtime may implement the optional control-surface capability of
|
|
1260
|
+
`@sublang/playbook/runtime` — `describe()` and `apply(...)` — so a host can
|
|
1261
|
+
observe the parked machine and execute a runtime-advertised recovery or jump
|
|
1262
|
+
action without fabricating an FSM event (DR-029). A runtime that
|
|
1263
|
+
implements either member shall implement both; every runtime the shared
|
|
1264
|
+
`createXStatePlaybookRuntime` factory constructs implements the pair. A
|
|
1265
|
+
runtime lacking the pair advertises no actions, and plain text delivery is
|
|
1266
|
+
the only verb against it. Presence is feature-detected like the
|
|
1267
|
+
parked-session snapshot capability; the pair changes no runtime ABI and no
|
|
1268
|
+
artifact or snapshot schema.
|
|
1269
|
+
|
|
1270
|
+
```typescript
|
|
1271
|
+
interface PlaybookControlAction {
|
|
1272
|
+
id: string; // stable within the returned view
|
|
1273
|
+
label: string; // runtime-written, Boss-appropriate
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
interface PlaybookControlView {
|
|
1277
|
+
state: PlaybookState;
|
|
1278
|
+
stateDescription?: string; // runtime-published meaning of the current state
|
|
1279
|
+
context?: JsonValue; // the runtime's authored projection, sanitized
|
|
1280
|
+
pendingQuestions: readonly PlaybookPendingBossQuestion[];
|
|
1281
|
+
lastError?: NormalizedError;
|
|
1282
|
+
actions: readonly PlaybookControlAction[];
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
type PlaybookControlReceipt =
|
|
1286
|
+
| { disposition: 'rejected'; reason: string } // before any effect
|
|
1287
|
+
| { disposition: 'executed'; run: PlaybookRunResult }
|
|
1288
|
+
| { disposition: 'failed'; error: NormalizedError }; // effects may exist
|
|
1289
|
+
|
|
1290
|
+
// Optional PlaybookRuntime members — both or neither:
|
|
1291
|
+
describe?(): PlaybookControlView;
|
|
1292
|
+
apply?(input: { actionId: string; key: string; signal: AbortSignal }): Promise<PlaybookControlReceipt>;
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
`describe()` shall be side-effect free — it emits no trace, status, or
|
|
1296
|
+
telemetry and moves no machine state — and is valid at parked quiescence
|
|
1297
|
+
outside an active `handleBossInput`/`resumePlaybookCall`/`apply` boundary;
|
|
1298
|
+
during an active boundary, before `init`, or after disposal begins it shall
|
|
1299
|
+
throw. The view carries the current normalized state descriptor, the state
|
|
1300
|
+
description defined below, the authored context projection defined below, the
|
|
1301
|
+
pending Boss questions with their stable ids, the last recorded error in
|
|
1302
|
+
normalized form, and the currently valid actions.
|
|
1303
|
+
|
|
1304
|
+
`stateDescription` is the runtime's own Boss-facing statement of what its
|
|
1305
|
+
current state means, taken from the same source state descriptions the action
|
|
1306
|
+
labels below are written from. A controller host has no other grounding for a
|
|
1307
|
+
status answer, and an internal state id is not text a reply may repeat, so the
|
|
1308
|
+
runtime publishes the meaning rather than leaving the host to substitute the
|
|
1309
|
+
identifier for it. A state whose source declares no description carries no
|
|
1310
|
+
`stateDescription`: an id is never promoted into a description, so a host is
|
|
1311
|
+
never handed an identifier dressed as meaning.
|
|
1312
|
+
|
|
1313
|
+
The view's `context` is an explicit projection the linked runtime **authors**,
|
|
1314
|
+
never an allow-by-default serialization of the FSM context (PBRT-52).
|
|
1315
|
+
Only the runtime knows which of its context members are safe and relevant
|
|
1316
|
+
for a controller prompt, while the host receiving the view cannot inspect an
|
|
1317
|
+
opaque blob for the player rosters, option values, and raw player output its
|
|
1318
|
+
own prompts must exclude; exporting by default makes the two obligations
|
|
1319
|
+
unsatisfiable together and gives every member added to an FSM later the wrong
|
|
1320
|
+
default. The rules:
|
|
1321
|
+
|
|
1322
|
+
- The emitted module declares the projection in its `spec` as
|
|
1323
|
+
`controlContextFields` — the FSM context member names its view exposes, in
|
|
1324
|
+
the order it names them — and the factory exports those and nothing else.
|
|
1325
|
+
- A runtime naming no member carries no `context` at all, so a member is
|
|
1326
|
+
private until an artifact names it and extending an FSM leaks nothing by
|
|
1327
|
+
omission.
|
|
1328
|
+
- Sanitization sits on top of the projection, not in place of it: a named
|
|
1329
|
+
member is still normalized (raw `Error` values normalized) and dropped when
|
|
1330
|
+
it cannot be made JSON-safe, rather than thrown, since `describe` stays
|
|
1331
|
+
side-effect free and total.
|
|
1332
|
+
- The two members the view surfaces first-class — the pending Boss question
|
|
1333
|
+
and the last error — cannot be named. A projection naming either is a
|
|
1334
|
+
construction error, failing runtime construction rather than being silently
|
|
1335
|
+
ignored.
|
|
1336
|
+
- The host still composes its own prompt block from the projection rather than
|
|
1337
|
+
pasting the projection in, so no runtime's exported value can forge a block
|
|
1338
|
+
into an envelope the host owns.
|
|
1339
|
+
|
|
1340
|
+
Actions derive from the live snapshot, only at the same safe point the
|
|
1341
|
+
parked-session snapshot uses (actor status `active`, quiescent, no pending
|
|
1342
|
+
nested call); anywhere else `actions` is empty while the rest of the view
|
|
1343
|
+
still describes the state. Two families exist, labeled from source state
|
|
1344
|
+
descriptions:
|
|
1345
|
+
|
|
1346
|
+
- **Failure-state retry** — while the singular state id is the recoverable
|
|
1347
|
+
failure state and the runtime holds a recorded last classified event (the
|
|
1348
|
+
event a public Boss boundary sent that drove the run into `failed`, kept
|
|
1349
|
+
with its recorded payload), and the live snapshot accepts that event, the
|
|
1350
|
+
runtime shall advertise `retry:<EVENT_TYPE>` replaying exactly that
|
|
1351
|
+
recorded event.
|
|
1352
|
+
- **Jump entries** — for each registered resumable state id whose
|
|
1353
|
+
explicit-state-jump event (`BOSS_INTERRUPT` with that `targetId`, optional
|
|
1354
|
+
textual fields omitted) the live snapshot accepts, guards included, the
|
|
1355
|
+
runtime shall advertise `jump:<stateId>`.
|
|
1356
|
+
|
|
1357
|
+
A candidate whose event requires a payload the runtime cannot source from
|
|
1358
|
+
recorded state shall be excluded from `actions` — `apply` never invents free
|
|
1359
|
+
text and never enters Boss-input classification. A candidate whose *label*
|
|
1360
|
+
could only be an identifier is excluded on the same terms: a label never falls
|
|
1361
|
+
back to a target id or to the replayed event type, because a controller host
|
|
1362
|
+
names an executed or refused action by its label and never by its id, so an
|
|
1363
|
+
identifier used as a label defeats that substitution. A jump whose target
|
|
1364
|
+
publishes no description is therefore not advertised — borrowing another
|
|
1365
|
+
state's description would name the wrong state — and a retry falls back from
|
|
1366
|
+
its target's description to its own source state's, and is not advertised when
|
|
1367
|
+
neither exists.
|
|
1368
|
+
|
|
1369
|
+
`apply({ actionId, key, signal })` shall revalidate the action against the
|
|
1370
|
+
live state and settle `{ disposition: 'rejected', reason }` with no effect
|
|
1371
|
+
when it is no longer advertised. It shall execute an accepted action at most
|
|
1372
|
+
once per idempotency `key`: the receipt is recorded at acceptance, before
|
|
1373
|
+
the settlement emissions, and a repeated key returns the recorded receipt
|
|
1374
|
+
verbatim with no revalidation, no execution, and no new trace pair within the
|
|
1375
|
+
runtime instance.
|
|
1376
|
+
Only accepted receipts (`executed` or `failed`) are recorded and final for
|
|
1377
|
+
their key. A rejection settles before acceptance and records nothing under
|
|
1378
|
+
its key — a later call with that key revalidates afresh, traces its own
|
|
1379
|
+
pair, and may execute once the action is advertised — and a key whose call
|
|
1380
|
+
threw before reaching acceptance (lifecycle misuse, invalid input, a
|
|
1381
|
+
pre-acceptance abort, a rejected start-boundary sink) likewise records
|
|
1382
|
+
nothing, so a later call with that key may execute. Executing sends the
|
|
1383
|
+
validated event through the same actor drive as `handleBossInput` — state
|
|
1384
|
+
transitions, player/judge boundaries, statuses, and traces flow unchanged —
|
|
1385
|
+
and settles `executed` with the projected run result, or `failed` with the
|
|
1386
|
+
normalized error when the run settles in the failure state, aborts, or a
|
|
1387
|
+
post-acceptance control-plane error lands (effects may exist). `signal`
|
|
1388
|
+
follows §Abort exactly as a Boss-turn signal does; an abort after acceptance
|
|
1389
|
+
settles the `failed` receipt rather than rejecting. The boundary traces as
|
|
1390
|
+
the paired `apply.started` / `apply.finished` events of §Playbook trace, and
|
|
1391
|
+
`apply` shares the single active-boundary sentinel with `handleBossInput`
|
|
1392
|
+
and `resumePlaybookCall`.
|
|
1393
|
+
|
|
1394
|
+
Acceptance is also the line past which `apply` does not throw, and
|
|
1395
|
+
publication — the `apply.finished` emission — is the line past which its
|
|
1396
|
+
receipt no longer changes. A settlement failure after acceptance but before
|
|
1397
|
+
publication (a rejecting emission drain) settles the `failed` receipt carrying
|
|
1398
|
+
its normalized error, replacing the one recorded at acceptance so the finish
|
|
1399
|
+
trace, the returned receipt, and any replay of the key report one settlement.
|
|
1400
|
+
A settlement failure at or after publication (a rejecting `apply.finished`
|
|
1401
|
+
sink) does not: the disposition is already emitted, so no rewrite can make the
|
|
1402
|
+
trace and the return agree, and a receipt states what happened to the effect
|
|
1403
|
+
rather than what happened to its telemetry. The published receipt stands, is
|
|
1404
|
+
returned and replayed verbatim, and the delivery failure travels on the
|
|
1405
|
+
runtime's emission-failure channel to surface from the next public boundary
|
|
1406
|
+
that drains.
|
|
1407
|
+
|
|
1408
|
+
The recorded receipts and the recorded last classified event are
|
|
1409
|
+
process-local: the schema-1 parked-session snapshot persists neither, and a
|
|
1410
|
+
restored runtime advertises a retry again only after its next classified
|
|
1411
|
+
event.
|
|
1412
|
+
|
|
1173
1413
|
## Abort
|
|
1174
1414
|
|
|
1175
1415
|
`handleBossInput.signal` is the abort surface.
|
|
@@ -1245,19 +1485,25 @@ The `playbook.trace` copies are the host-agnostic runtime-boundary record requir
|
|
|
1245
1485
|
|
|
1246
1486
|
## Output
|
|
1247
1487
|
|
|
1248
|
-
The link compiler emits
|
|
1488
|
+
The link compiler emits one TypeScript module per playbook.
|
|
1489
|
+
For an FSM that declares no `type: 'parallel'` state, it shall emit the thin
|
|
1490
|
+
shared-factory module defined below.
|
|
1491
|
+
For an FSM that declares a parallel state, it shall emit bespoke linked
|
|
1492
|
+
machinery satisfying this document's runtime contract and shall not invoke
|
|
1493
|
+
`createXStatePlaybookRuntime`, whose supported domain is single-region FSMs
|
|
1494
|
+
under [DR-019](../specs/decisions/019-shared-linked-runtime-factory.md).
|
|
1249
1495
|
The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
|
|
1250
1496
|
mapping, adjudication, script execution, nested-playbook bridging, session
|
|
1251
1497
|
lifecycle, abort handling, and the optional parked-session snapshot
|
|
1252
|
-
capability — is not regenerated
|
|
1253
|
-
`createXStatePlaybookRuntime(machine, spec)` factory exported by
|
|
1498
|
+
capability — is not regenerated for a factory-backed artifact: it ships once
|
|
1499
|
+
as the shared `createXStatePlaybookRuntime(machine, spec)` factory exported by
|
|
1254
1500
|
`@sublang/playbook/xstate-runtime`, and the emitted module hands its FSM and
|
|
1255
1501
|
a small per-playbook `spec` to that factory. Every behavioral section of
|
|
1256
1502
|
this definition still binds the emitted module's runtime; the shared factory
|
|
1257
1503
|
is how the emitted module satisfies them, so a runtime fix ships as a
|
|
1258
1504
|
package release instead of a re-link of every artifact.
|
|
1259
1505
|
|
|
1260
|
-
The emitted module:
|
|
1506
|
+
The thin emitted module:
|
|
1261
1507
|
|
|
1262
1508
|
- Imports the FSM artifact by relative path with an extension-bearing
|
|
1263
1509
|
runtime specifier. When the linked TypeScript is part of a package that
|
|
@@ -1297,10 +1543,23 @@ The emitted module:
|
|
|
1297
1543
|
exceptions not covered by the canonical kebab-token-to-camel-field mapping
|
|
1298
1544
|
and the canonical `<#>` → `irNumber` special case; the
|
|
1299
1545
|
transition-event payload fields the FSM's Boss union declares; a
|
|
1300
|
-
non-default player binding where the linker inputs supplied one;
|
|
1546
|
+
non-default player binding where the linker inputs supplied one; the
|
|
1547
|
+
`controlContextFields` projection of §Control surface; and any
|
|
1301
1548
|
per-playbook strategy override (classifier, prompt composers,
|
|
1302
1549
|
required-field extraction, status formatting) an earlier section of this
|
|
1303
|
-
definition requires for that playbook.
|
|
1550
|
+
definition requires for that playbook.
|
|
1551
|
+
`controlContextFields` is authored, not derived: the linker names the FSM
|
|
1552
|
+
context members the playbook's controller view exposes and no others, in the
|
|
1553
|
+
order the view should render them, omitting the member entirely where the
|
|
1554
|
+
playbook exposes no context. It is the one spec member whose default is
|
|
1555
|
+
*nothing* rather than everything — the factory exports no context for a
|
|
1556
|
+
module that supplies none — so a module emitted without it advertises a
|
|
1557
|
+
playbook with no Boss-visible context rather than one whose whole FSM
|
|
1558
|
+
context is Boss-visible. The linker shall not name a member the view
|
|
1559
|
+
surfaces first-class (the pending Boss question, the last error), which is a
|
|
1560
|
+
construction error, and shall not name a member carrying a resolved player
|
|
1561
|
+
roster, an option value, or player-authored text, which a controller host's
|
|
1562
|
+
prompts are required to exclude or to fence. The metadata shall keep the shared
|
|
1304
1563
|
classifier's reply contract exactly flat `{ type, ...declaredFields }` and
|
|
1305
1564
|
distinguish judge-authored routing fields from exact-text fields the
|
|
1306
1565
|
runtime attaches itself. Everything else — player/script/captain/nested actor
|
|
@@ -1352,10 +1611,16 @@ The emitted module:
|
|
|
1352
1611
|
- Default-exports the factory call as `createPlaybookRuntime`, typed
|
|
1353
1612
|
`PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
|
|
1354
1613
|
- Exposes, under an `_internal` export, the pure helpers verification
|
|
1355
|
-
needs — at least the
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1614
|
+
needs — at least the prompt composers its own machine uses, which may
|
|
1615
|
+
re-export the shared defaults when the spec does not override composition —
|
|
1616
|
+
so compilation-correctness tests can exercise composition without a host.
|
|
1617
|
+
A playbook that calls players exposes `composePlayerPrompt`; a playbook
|
|
1618
|
+
whose states make direct-Captain calls exposes `composeCaptainPrompt`. A
|
|
1619
|
+
controller playbook that calls no players exposes no player composer:
|
|
1620
|
+
there is no composition to verify, and a stub under that name would
|
|
1621
|
+
describe work the module cannot do. `_internal` is not a public API — the
|
|
1622
|
+
leading underscore says so — and nothing in it is semver-stable; a helper
|
|
1623
|
+
a host is meant to call is a top-level export and is governed as one.
|
|
1359
1624
|
- Holds no host-specific types and no host primitive calls. The runtime
|
|
1360
1625
|
speaks only `PlaybookPorts` for every agent and host concern; the
|
|
1361
1626
|
`node:child_process` dependency of §Script execution lives in the shared
|
|
@@ -1375,6 +1640,9 @@ The emitted module:
|
|
|
1375
1640
|
dependency runs one way — from each linked module to the shared
|
|
1376
1641
|
engine and contract, never the reverse.
|
|
1377
1642
|
|
|
1643
|
+
Both output profiles remain subject to the behavioral sections above and the
|
|
1644
|
+
verification requirements below.
|
|
1645
|
+
|
|
1378
1646
|
When a co-located integration test for the linked runtime already exists, the
|
|
1379
1647
|
link compiler shall run it before reporting success and treat any failure as a
|
|
1380
1648
|
generation failure. It shall not delete, skip, or weaken that suite to make a
|
package/src/runtime.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export interface PlaybookSession {
|
|
|
108
108
|
depth: number;
|
|
109
109
|
ports: PlaybookPorts;
|
|
110
110
|
}
|
|
111
|
-
export type PlaybookTraceType = 'session.started' | 'boss.input.received' | 'judge.call.started' | 'judge.call.finished' | 'player.call.started' | 'player.call.finished' | 'captain.call.started' | 'captain.call.finished' | 'playbook.call.started' | 'playbook.call.finished' | 'fsm.transition' | 'status.emitted' | 'boss.input.settled' | 'session.disposed';
|
|
111
|
+
export type PlaybookTraceType = 'session.started' | 'boss.input.received' | 'judge.call.started' | 'judge.call.finished' | 'player.call.started' | 'player.call.finished' | 'captain.call.started' | 'captain.call.finished' | 'playbook.call.started' | 'playbook.call.finished' | 'apply.started' | 'apply.finished' | 'fsm.transition' | 'status.emitted' | 'boss.input.settled' | 'session.disposed';
|
|
112
112
|
export interface PlaybookTraceEvent {
|
|
113
113
|
schemaVersion: 2;
|
|
114
114
|
sessionId: string;
|
|
@@ -148,10 +148,38 @@ export interface PlaybookRuntimeSnapshot {
|
|
|
148
148
|
state: PlaybookState;
|
|
149
149
|
pendingBossQuestions: readonly PlaybookPendingBossQuestion[];
|
|
150
150
|
}
|
|
151
|
+
export interface PlaybookControlAction {
|
|
152
|
+
id: string;
|
|
153
|
+
label: string;
|
|
154
|
+
}
|
|
155
|
+
export interface PlaybookControlView {
|
|
156
|
+
state: PlaybookState;
|
|
157
|
+
stateDescription?: string;
|
|
158
|
+
context?: JsonValue;
|
|
159
|
+
pendingQuestions: readonly PlaybookPendingBossQuestion[];
|
|
160
|
+
lastError?: NormalizedError;
|
|
161
|
+
actions: readonly PlaybookControlAction[];
|
|
162
|
+
}
|
|
163
|
+
export type PlaybookControlReceipt = {
|
|
164
|
+
disposition: 'rejected';
|
|
165
|
+
reason: string;
|
|
166
|
+
} | {
|
|
167
|
+
disposition: 'executed';
|
|
168
|
+
run: PlaybookRunResult;
|
|
169
|
+
} | {
|
|
170
|
+
disposition: 'failed';
|
|
171
|
+
error: NormalizedError;
|
|
172
|
+
};
|
|
151
173
|
export interface PlaybookRuntime {
|
|
152
174
|
init(session: PlaybookSession): Promise<void>;
|
|
153
175
|
exportSnapshot?(): PlaybookRuntimeSnapshot | undefined;
|
|
154
176
|
restore?(session: PlaybookSession, snapshot: PlaybookRuntimeSnapshot): Promise<void>;
|
|
177
|
+
describe?(): PlaybookControlView;
|
|
178
|
+
apply?(input: {
|
|
179
|
+
actionId: string;
|
|
180
|
+
key: string;
|
|
181
|
+
signal: AbortSignal;
|
|
182
|
+
}): Promise<PlaybookControlReceipt>;
|
|
155
183
|
handleBossInput(turn: {
|
|
156
184
|
text: string;
|
|
157
185
|
signal: AbortSignal;
|
package/src/runtime.ts
CHANGED
|
@@ -157,6 +157,8 @@ export type PlaybookTraceType =
|
|
|
157
157
|
| 'captain.call.finished'
|
|
158
158
|
| 'playbook.call.started'
|
|
159
159
|
| 'playbook.call.finished'
|
|
160
|
+
| 'apply.started'
|
|
161
|
+
| 'apply.finished'
|
|
160
162
|
| 'fsm.transition'
|
|
161
163
|
| 'status.emitted'
|
|
162
164
|
| 'boss.input.settled'
|
|
@@ -206,6 +208,38 @@ export interface PlaybookRuntimeSnapshot {
|
|
|
206
208
|
pendingBossQuestions: readonly PlaybookPendingBossQuestion[];
|
|
207
209
|
}
|
|
208
210
|
|
|
211
|
+
// DR-029: one currently valid, runtime-advertised control action. The id
|
|
212
|
+
// is stable within the returned view; the label is runtime-written,
|
|
213
|
+
// Boss-appropriate text derived from source state descriptions.
|
|
214
|
+
export interface PlaybookControlAction {
|
|
215
|
+
id: string;
|
|
216
|
+
label: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// DR-029: the sanitized control view `describe()` returns — current
|
|
220
|
+
// state and the runtime-written description of what that state means,
|
|
221
|
+
// the authored context projection, pending Boss questions, the last
|
|
222
|
+
// recorded error, and the currently valid actions. `stateDescription` is
|
|
223
|
+
// the Boss-appropriate grounding a host may speak from; the state id is
|
|
224
|
+
// internal and is absent from it whenever the runtime's source declares
|
|
225
|
+
// no description for the state it is in.
|
|
226
|
+
export interface PlaybookControlView {
|
|
227
|
+
state: PlaybookState;
|
|
228
|
+
stateDescription?: string;
|
|
229
|
+
context?: JsonValue;
|
|
230
|
+
pendingQuestions: readonly PlaybookPendingBossQuestion[];
|
|
231
|
+
lastError?: NormalizedError;
|
|
232
|
+
actions: readonly PlaybookControlAction[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// DR-029: the receipt `apply()` returns says which of three things
|
|
236
|
+
// happened — rejected before any effect, executed with the settled run
|
|
237
|
+
// result, or failed after effects may exist.
|
|
238
|
+
export type PlaybookControlReceipt =
|
|
239
|
+
| { disposition: 'rejected'; reason: string }
|
|
240
|
+
| { disposition: 'executed'; run: PlaybookRunResult }
|
|
241
|
+
| { disposition: 'failed'; error: NormalizedError };
|
|
242
|
+
|
|
209
243
|
export interface PlaybookRuntime {
|
|
210
244
|
init(session: PlaybookSession): Promise<void>;
|
|
211
245
|
// DR-014 §1 optional durable-session capability: a runtime implements
|
|
@@ -218,6 +252,19 @@ export interface PlaybookRuntime {
|
|
|
218
252
|
session: PlaybookSession,
|
|
219
253
|
snapshot: PlaybookRuntimeSnapshot,
|
|
220
254
|
): Promise<void>;
|
|
255
|
+
// DR-029 optional control-surface capability: a runtime implements
|
|
256
|
+
// both members or neither. `describe` is side-effect free and valid at
|
|
257
|
+
// parked quiescence outside an active boundary; `apply` revalidates the
|
|
258
|
+
// named action against the live state, executes it at most once per
|
|
259
|
+
// idempotency key within that runtime instance, and returns a receipt. A
|
|
260
|
+
// runtime lacking the pair advertises no actions; plain text delivery is
|
|
261
|
+
// the only verb against it.
|
|
262
|
+
describe?(): PlaybookControlView;
|
|
263
|
+
apply?(input: {
|
|
264
|
+
actionId: string;
|
|
265
|
+
key: string;
|
|
266
|
+
signal: AbortSignal;
|
|
267
|
+
}): Promise<PlaybookControlReceipt>;
|
|
221
268
|
handleBossInput(turn: {
|
|
222
269
|
text: string;
|
|
223
270
|
signal: AbortSignal;
|