@sublang/playbook 0.9.0 → 1.3.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 +190 -151
- package/package.json +50 -6
- package/reference/sdlc/captain.md +102 -0
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
- package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
- package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
- package/reference/sdlc/code.playbook/bin/run.js +999 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
- package/reference/sdlc/code.playbook/code.fsm.js +334 -102
- package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
- package/reference/sdlc/code.playbook/code.gears.md +11 -10
- package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
- package/reference/sdlc/code.playbook/code.playbook.js +199 -488
- package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
- package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
- package/reference/sdlc/code.playbook/code.registry.js +0 -3
- package/reference/sdlc/code.playbook/code.registry.ts +0 -6
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
- package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
- package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
- package/slc/gears2fsm.md +557 -57
- package/slc/link.md +1165 -89
- package/slc/optimize.md +92 -0
- package/slc/text2gears.md +255 -7
- package/src/runtime.d.ts +146 -3
- package/src/runtime.ts +201 -2
- package/src/xstate-playbook-runtime.d.ts +201 -0
- package/src/xstate-playbook-runtime.js +2058 -0
- package/src/xstate-playbook-runtime.ts +2792 -0
- package/src/xstate-runtime.d.ts +95 -0
- package/src/xstate-runtime.js +1258 -0
- package/src/xstate-runtime.ts +1816 -0
package/slc/link.md
CHANGED
|
@@ -8,28 +8,30 @@ Compiles the [gears2fsm](gears2fsm.md) artifact into a **`PlaybookRuntime`**: a
|
|
|
8
8
|
|
|
9
9
|
- Drives the FSM.
|
|
10
10
|
- Classifies Boss input into typed events.
|
|
11
|
-
- Runs
|
|
12
|
-
-
|
|
11
|
+
- Runs direct-Captain, delegated-player, and nested-playbook actors.
|
|
12
|
+
- Executes deterministic script actors locally, without any agent.
|
|
13
|
+
- Adjudicates Captain and player output into FSM guards.
|
|
13
14
|
- Surfaces transitions as status/telemetry.
|
|
14
15
|
|
|
15
16
|
The runtime is invoked through the stable `PlaybookPorts` contract.
|
|
16
|
-
Presentation layers (tmux-play, web, CLI, tests) implement the
|
|
17
|
+
Presentation layers (tmux-play, web, CLI, tests) implement the six ports once
|
|
18
|
+
and inherit every playbook.
|
|
17
19
|
|
|
18
20
|
- Source: an XState v5 machine artifact (`.fsm.ts`) produced by gears2fsm.
|
|
19
21
|
- Target: a `PlaybookRuntime` factory module — TypeScript, host-agnostic.
|
|
20
22
|
|
|
21
23
|
Hosts are out of scope for this phase.
|
|
22
|
-
Each host has
|
|
24
|
+
Each host has an adapter that loads a `PlaybookRuntime` module and supplies the host's primitives as `PlaybookPorts`.
|
|
23
25
|
The adapter shall speak only `PlaybookPorts` to the runtime and shall not leak host types back into it.
|
|
24
26
|
|
|
25
27
|
The link compiler shall not modify the FSM artifact and shall not re-derive Captain prompts, result keys, or guard semantics — those are fixed by the FSM.
|
|
26
28
|
|
|
27
29
|
## Formats
|
|
28
30
|
|
|
29
|
-
| Role
|
|
30
|
-
|
|
|
31
|
-
| source | fsm
|
|
32
|
-
| target | playbook | .ts
|
|
31
|
+
| Role | Format | Extension |
|
|
32
|
+
| ------ | -------- | --------- |
|
|
33
|
+
| source | fsm | .ts |
|
|
34
|
+
| target | playbook | .ts |
|
|
33
35
|
|
|
34
36
|
## PlaybookRuntime contract
|
|
35
37
|
|
|
@@ -37,11 +39,80 @@ The emitted module shall default-export a factory of the following shape:
|
|
|
37
39
|
|
|
38
40
|
```typescript
|
|
39
41
|
interface PlaybookRuntime {
|
|
40
|
-
init(
|
|
41
|
-
handleBossInput(turn: {
|
|
42
|
+
init(session: PlaybookSession): Promise<void>;
|
|
43
|
+
handleBossInput(turn: {
|
|
44
|
+
text: string;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
}): Promise<PlaybookRunResult>;
|
|
47
|
+
resumePlaybookCall(input: {
|
|
48
|
+
callId: string;
|
|
49
|
+
result: PlaybookCallResult;
|
|
50
|
+
signal: AbortSignal;
|
|
51
|
+
}): Promise<PlaybookRunResult>;
|
|
42
52
|
dispose(): Promise<void>;
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
interface PlaybookSession {
|
|
56
|
+
sessionId: string;
|
|
57
|
+
playbookId: string;
|
|
58
|
+
rootSessionId: string;
|
|
59
|
+
parentSessionId?: string;
|
|
60
|
+
parentCallId?: string;
|
|
61
|
+
depth: number;
|
|
62
|
+
ports: PlaybookPorts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type JsonValue =
|
|
66
|
+
| null
|
|
67
|
+
| boolean
|
|
68
|
+
| number
|
|
69
|
+
| string
|
|
70
|
+
| readonly JsonValue[]
|
|
71
|
+
| { readonly [key: string]: JsonValue };
|
|
72
|
+
|
|
73
|
+
interface NormalizedError {
|
|
74
|
+
name: string;
|
|
75
|
+
message: string;
|
|
76
|
+
stack?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type PlaybookStateValue =
|
|
80
|
+
| string
|
|
81
|
+
| { readonly [key: string]: PlaybookStateValue };
|
|
82
|
+
|
|
83
|
+
interface PlaybookState {
|
|
84
|
+
value: PlaybookStateValue;
|
|
85
|
+
activeStateIds: readonly string[];
|
|
86
|
+
tags: readonly string[];
|
|
87
|
+
status: 'active' | 'done' | 'error' | 'stopped';
|
|
88
|
+
quiescent: boolean;
|
|
89
|
+
stateId?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface PlaybookPendingCall {
|
|
93
|
+
callId: string;
|
|
94
|
+
playbookId: string;
|
|
95
|
+
childSessionId: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type PlaybookRunResult =
|
|
99
|
+
| { outcome: 'quiescent' | 'no-action'; state: PlaybookState }
|
|
100
|
+
| {
|
|
101
|
+
outcome: 'failed' | 'aborted';
|
|
102
|
+
state: PlaybookState;
|
|
103
|
+
error?: NormalizedError;
|
|
104
|
+
}
|
|
105
|
+
| {
|
|
106
|
+
outcome: 'terminal';
|
|
107
|
+
state: PlaybookState;
|
|
108
|
+
output?: JsonValue;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
outcome: 'suspended';
|
|
112
|
+
state: PlaybookState;
|
|
113
|
+
pendingCall: PlaybookPendingCall;
|
|
114
|
+
};
|
|
115
|
+
|
|
45
116
|
type PlaybookRuntimeFactory<Options = unknown> = (
|
|
46
117
|
options: Options,
|
|
47
118
|
) => PlaybookRuntime;
|
|
@@ -53,46 +124,330 @@ export default function createPlaybookRuntime(
|
|
|
53
124
|
|
|
54
125
|
The default export conforms to `PlaybookRuntimeFactory<PlaybookRuntimeOptions>`, the generic factory type the shared contract module exposes (§Output).
|
|
55
126
|
|
|
56
|
-
`init` receives the host
|
|
127
|
+
`init` receives the host-owned playbook session identity and ports, constructs the XState actor with FSM `input` derived from `options`, and starts the actor.
|
|
57
128
|
The runtime owns the actor for its lifetime; `handleBossInput` runs one turn, and `dispose` stops the actor and drains pending port emissions.
|
|
58
|
-
|
|
59
|
-
|
|
129
|
+
The host shall generate a non-empty, globally unique `sessionId` for each init-to-dispose lifecycle and shall supply the stable registry or authored playbook id as `playbookId`.
|
|
130
|
+
The runtime shall validate non-empty session, playbook, and root ids, a safe
|
|
131
|
+
non-negative integer depth, root identity (`depth === 0` and
|
|
132
|
+
`rootSessionId === sessionId` with no parent fields), and child identity
|
|
133
|
+
(`depth > 0` with non-empty parent session and call ids). It shall copy those
|
|
134
|
+
identity scalars and the port references into its own immutable record rather
|
|
135
|
+
than retaining the caller's mutable session object. A child `sessionId` shall
|
|
136
|
+
differ from both its `rootSessionId` and `parentSessionId`.
|
|
137
|
+
|
|
138
|
+
Run outcomes are exact: `no-action` means no FSM event was sent;
|
|
139
|
+
`quiescent` means a non-failure parked/idle state; `failed` means the FSM is in
|
|
140
|
+
a recoverable failure state; `terminal` means top-level final with optional
|
|
141
|
+
JSON output; `aborted` means the turn signal ended work; and `suspended` means
|
|
142
|
+
exactly one `pendingCall` is active.
|
|
143
|
+
Control-plane exceptions reject the runtime method rather than masquerade as a
|
|
144
|
+
recoverable workflow `failed` result.
|
|
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) and strategy overrides the linker exposes.
|
|
60
147
|
The link compiler emits a typed options interface per playbook based on the FSM's `CodingInput` (or equivalent).
|
|
61
|
-
|
|
62
|
-
|
|
148
|
+
The CLI's absence of `--link-option` values does not mean that
|
|
149
|
+
`PlaybookRuntimeOptions` is empty. CLI link options are compile-time inputs;
|
|
150
|
+
the runtime options interface is independently derived from every required FSM
|
|
151
|
+
input field that is not supplied by `PlaybookSession` or another linker-owned
|
|
152
|
+
source. In particular, a required immutable `enabledPlaybooks` catalog shall
|
|
153
|
+
remain a required readonly runtime option passed through to machine input; the
|
|
154
|
+
linker shall neither invent an empty catalog nor require it to be baked into a
|
|
155
|
+
CLI link option.
|
|
156
|
+
|
|
157
|
+
Player binding is a _linker-time_ input baked into the emitted runtime by default.
|
|
63
158
|
A linker may also expose it via `PlaybookRuntimeOptions` for per-run remapping; the contract requires only that the runtime ship with a deterministic binding it applies at every `callPlayer` site.
|
|
64
159
|
|
|
65
160
|
## PlaybookPorts contract
|
|
66
161
|
|
|
67
162
|
```typescript
|
|
68
163
|
interface PlaybookPorts {
|
|
69
|
-
callPlayer(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
164
|
+
callPlayer(
|
|
165
|
+
playerId: string,
|
|
166
|
+
prompt: string,
|
|
167
|
+
signal: AbortSignal,
|
|
168
|
+
options: PlayerCallOptions,
|
|
169
|
+
): Promise<PlayerResult>;
|
|
170
|
+
callCaptain(
|
|
171
|
+
prompt: string,
|
|
172
|
+
signal: AbortSignal,
|
|
173
|
+
options: CaptainCallOptions,
|
|
174
|
+
): Promise<CaptainResult>;
|
|
175
|
+
callJudge(prompt: string, signal: AbortSignal): Promise<string>;
|
|
176
|
+
callPlaybook(
|
|
177
|
+
request: PlaybookCallRequest,
|
|
178
|
+
signal: AbortSignal,
|
|
179
|
+
): Promise<PlaybookCallStart>;
|
|
73
180
|
emitStatus(message: string, data?: unknown): Promise<void>;
|
|
74
181
|
emitTelemetry(event: { topic: string; payload: unknown }): Promise<void>;
|
|
75
182
|
}
|
|
76
183
|
|
|
184
|
+
interface PlayerCallOptions {
|
|
185
|
+
resume: string | false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface CaptainCallOptions {
|
|
189
|
+
visibility: 'visible' | 'hidden';
|
|
190
|
+
resume: string | false;
|
|
191
|
+
allowedTools?: readonly string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
77
194
|
interface PlayerResult {
|
|
195
|
+
status: 'ok' | 'aborted' | 'error';
|
|
196
|
+
resumeToken?: string;
|
|
197
|
+
finalText?: string;
|
|
198
|
+
error?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface CaptainResult {
|
|
78
202
|
status: 'ok' | 'aborted' | 'error';
|
|
79
203
|
finalText?: string;
|
|
80
204
|
error?: string;
|
|
81
205
|
}
|
|
206
|
+
|
|
207
|
+
interface PlaybookCallRequest {
|
|
208
|
+
callId: string;
|
|
209
|
+
playbookId: string;
|
|
210
|
+
text: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
type PlaybookCallResult =
|
|
214
|
+
| {
|
|
215
|
+
status: 'ok';
|
|
216
|
+
playbookId: string;
|
|
217
|
+
childSessionId: string;
|
|
218
|
+
state?: PlaybookState;
|
|
219
|
+
output?: JsonValue;
|
|
220
|
+
}
|
|
221
|
+
| {
|
|
222
|
+
status: 'aborted';
|
|
223
|
+
playbookId: string;
|
|
224
|
+
childSessionId?: string;
|
|
225
|
+
state?: PlaybookState;
|
|
226
|
+
error?: NormalizedError;
|
|
227
|
+
}
|
|
228
|
+
| {
|
|
229
|
+
status: 'error';
|
|
230
|
+
playbookId: string;
|
|
231
|
+
childSessionId?: string;
|
|
232
|
+
state?: PlaybookState;
|
|
233
|
+
error: NormalizedError;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
type PlaybookCallStart =
|
|
237
|
+
| { state: 'settled'; result: PlaybookCallResult }
|
|
238
|
+
| { state: 'suspended'; childSessionId: string };
|
|
82
239
|
```
|
|
83
240
|
|
|
84
|
-
`PlayerResult` mirrors cligent's `PlayerRunResult`
|
|
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)).
|
|
85
242
|
The runtime treats `status !== 'ok'` as a player failure and routes it through the FSM's error path (§Abort).
|
|
86
243
|
|
|
244
|
+
`callCaptain` runs a direct-Captain FSM actor against the host's Captain
|
|
245
|
+
agent. The linked runtime shall pass
|
|
246
|
+
`{ visibility: 'visible', resume: false }` for authored workflow calls so
|
|
247
|
+
XState context, rather than an agent conversation, owns workflow continuity.
|
|
248
|
+
The tool restriction is source-owned: the runtime shall additionally pass
|
|
249
|
+
`allowedTools: []` exactly when the GEARS source itself restricts the acting
|
|
250
|
+
Captain from tools (a routing-only Captain policy such as the default generic
|
|
251
|
+
Captain). A transformation-performing Captain — e.g. a compiler phase compiled
|
|
252
|
+
from a transformation-spec source, whose behavior writes a declared target
|
|
253
|
+
artifact — works through the host Captain's own tools, so its calls shall
|
|
254
|
+
carry no `allowedTools` restriction.
|
|
255
|
+
Accordingly, `CaptainCallOptions.allowedTools` is optional: an explicit empty
|
|
256
|
+
array requests a tool-free call, while omission preserves the host Captain's
|
|
257
|
+
configured tools.
|
|
258
|
+
`CaptainResult` carries no resume token or player-continuation selection.
|
|
259
|
+
A non-`ok`
|
|
260
|
+
result, or an `ok` result without `finalText`, shall reject the actor through
|
|
261
|
+
the FSM's error path.
|
|
262
|
+
Outside a signal-driven abort, those invalid direct-Captain results are
|
|
263
|
+
latched control-plane failures. The runtime shall let the actor take `onError`,
|
|
264
|
+
drive it to quiescence, and drain ordered emissions before the public method
|
|
265
|
+
rejects with the original failure. It shall never translate either case into
|
|
266
|
+
a recoverable workflow `{ outcome: 'failed' }` result. If the combined signal
|
|
267
|
+
has aborted, an aborted host result follows the ordinary abort settlement
|
|
268
|
+
instead of being promoted to a control-plane failure.
|
|
269
|
+
|
|
270
|
+
Every linked runtime owns a map from resolved player id to its latest non-empty `resumeToken`.
|
|
271
|
+
Before reading a resolved direct-Captain or delegated-player result, the
|
|
272
|
+
runtime shall validate, detach, and freeze it through the shared
|
|
273
|
+
`validateCaptainResult` or `validatePlayerResult` helper. The accepted object
|
|
274
|
+
shape is exact: only the declared status and optional string fields are
|
|
275
|
+
allowed, JSON-unsafe members reject, and caller mutation after resolution
|
|
276
|
+
cannot change trace evidence or player continuity. Validation happens before
|
|
277
|
+
adopting a resume token or reading final text.
|
|
278
|
+
The first call to each player in a playbook session shall pass `{ resume: false }`; later calls shall pass the exact stored token.
|
|
279
|
+
After a resolved call, the runtime shall replace the token when the result carries one or clear it when absent before interpreting `status`; a rejected call with no result leaves the prior token unchanged.
|
|
280
|
+
After awaiting a host Captain or player promise, the runtime shall re-check the
|
|
281
|
+
combined invocation/public-boundary signal before validating the result,
|
|
282
|
+
adopting a resume token, or emitting a successful finish. A host promise that
|
|
283
|
+
ignores cancellation and resolves late shall be paired as aborted and shall
|
|
284
|
+
not mutate continuity or masquerade as success.
|
|
285
|
+
The map survives actor reconstruction inside the same runtime and is discarded at `dispose`.
|
|
286
|
+
The runtime shall keep an in-flight set keyed by resolved player id and reject
|
|
287
|
+
a second concurrent call to the same id before crossing the host port. Calls
|
|
288
|
+
to distinct resolved player ids may overlap.
|
|
289
|
+
|
|
87
290
|
`callJudge` returns free-form text.
|
|
88
291
|
The runtime parses it per the state's adjudication strategy (§Captain adjudication).
|
|
89
292
|
One port serves both classifier and adjudicator — they vary only in prompt.
|
|
293
|
+
Concurrent `callJudge` attempts within one linked runtime shall pass through
|
|
294
|
+
one abort-aware local FIFO. After the host promise resolves, the runtime shall
|
|
295
|
+
require a string reply and re-check the combined signal before tracing or
|
|
296
|
+
parsing success, so a non-cooperative late judge cannot outlive cancellation.
|
|
297
|
+
The host shall serialize `callCaptain` and `callJudge` together through one
|
|
298
|
+
shared abort-aware concurrency-one FIFO because both use the same single-flight
|
|
299
|
+
Captain lane, even when distinct player ports overlap [[4]]. A direct Captain
|
|
300
|
+
call's subsequent adjudication shall enter that same queue only after the
|
|
301
|
+
visible call has settled; the linked runtime shall not hold one queue lease
|
|
302
|
+
while requesting the other port.
|
|
303
|
+
Use one shared `PQueue({ concurrency: 1 })` for the individual host
|
|
304
|
+
`callCaptain` and `callJudge` promises. Do not pass an invocation or public
|
|
305
|
+
boundary signal as `PQueue.add(..., { signal })`: PQueue may release a running
|
|
306
|
+
slot as soon as that signal aborts even though a non-cooperative host promise
|
|
307
|
+
is still executing, which permits overlap. Instead check the combined signal
|
|
308
|
+
inside the queued task before crossing the host port, await the host promise
|
|
309
|
+
without releasing the queue lease, and check the signal again afterward.
|
|
310
|
+
|
|
311
|
+
`callPlaybook` starts a function-style child call.
|
|
312
|
+
The caller runtime supplies its stable call id and the XState invocation's
|
|
313
|
+
lifetime signal.
|
|
314
|
+
The host drives the child's initial text before resolving the port with either
|
|
315
|
+
an immediate settled result or a suspended child session.
|
|
316
|
+
Suspension is resumed later through `PlaybookRuntime.resumePlaybookCall`; the
|
|
317
|
+
port promise itself shall not remain pending across Boss turns.
|
|
90
318
|
|
|
91
319
|
`emitStatus` is human-readable; `emitTelemetry` is structured.
|
|
92
320
|
Both are async and shall be ordered, awaited, and never-dropped; the runtime awaits each emission before issuing the next.
|
|
93
321
|
|
|
94
322
|
The runtime never speaks to LLMs directly and never touches host types beyond `PlaybookPorts`.
|
|
95
323
|
|
|
324
|
+
## Playbook trace
|
|
325
|
+
|
|
326
|
+
Every linked runtime shall emit a boundary-complete, ordered trace through `emitTelemetry` topic `playbook.trace`.
|
|
327
|
+
Each payload shall carry `schemaVersion: 2`, the immutable session identity and
|
|
328
|
+
causality, a contiguous one-based `sequence`, a Unix-millisecond `timestamp`, a
|
|
329
|
+
trace `type`, event `payload`, and the runtime-local `turnId` / paired `callId`
|
|
330
|
+
where applicable.
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
type PlaybookTraceType =
|
|
334
|
+
| 'session.started'
|
|
335
|
+
| 'boss.input.received'
|
|
336
|
+
| 'judge.call.started'
|
|
337
|
+
| 'judge.call.finished'
|
|
338
|
+
| 'player.call.started'
|
|
339
|
+
| 'player.call.finished'
|
|
340
|
+
| 'captain.call.started'
|
|
341
|
+
| 'captain.call.finished'
|
|
342
|
+
| 'playbook.call.started'
|
|
343
|
+
| 'playbook.call.finished'
|
|
344
|
+
| 'fsm.transition'
|
|
345
|
+
| 'status.emitted'
|
|
346
|
+
| 'boss.input.settled'
|
|
347
|
+
| 'session.disposed';
|
|
348
|
+
|
|
349
|
+
interface PlaybookTraceEvent {
|
|
350
|
+
schemaVersion: 2;
|
|
351
|
+
sessionId: string;
|
|
352
|
+
playbookId: string;
|
|
353
|
+
rootSessionId: string;
|
|
354
|
+
parentSessionId?: string;
|
|
355
|
+
parentCallId?: string;
|
|
356
|
+
depth: number;
|
|
357
|
+
sequence: number;
|
|
358
|
+
timestamp: number;
|
|
359
|
+
type: PlaybookTraceType;
|
|
360
|
+
turnId?: number;
|
|
361
|
+
callId?: string;
|
|
362
|
+
payload: JsonValue;
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The trace types are `session.started`, `boss.input.received`,
|
|
367
|
+
`judge.call.started`, `judge.call.finished`, `player.call.started`,
|
|
368
|
+
`player.call.finished`, `captain.call.started`, `captain.call.finished`,
|
|
369
|
+
`playbook.call.started`,
|
|
370
|
+
`playbook.call.finished`, `fsm.transition`, `status.emitted`,
|
|
371
|
+
`boss.input.settled`, and `session.disposed`.
|
|
372
|
+
Call pairs carry exact prompts and replies, normalized failures, actor and state
|
|
373
|
+
identity, and their boundary-specific options.
|
|
374
|
+
Direct-Captain start and finish payloads shall carry `allowedTools` exactly when
|
|
375
|
+
the originating `CaptainCallOptions` selects it and shall omit the member when
|
|
376
|
+
the call preserves the host Captain's configured tools.
|
|
377
|
+
`session.started` and `session.disposed` carry their descriptor as top-level
|
|
378
|
+
`state` and its singular `stateId` when present. Every judge start and finish
|
|
379
|
+
carries the working snapshot's singular `stateId` when one exists;
|
|
380
|
+
classification uses the current descriptor and adjudication uses the invoking
|
|
381
|
+
actor input. The default Captain always has such a singular id, while a
|
|
382
|
+
parallel snapshot may omit it. Every judge finish also carries
|
|
383
|
+
`status: 'ok' | 'aborted' | 'error'`. Every `status.emitted` carries the
|
|
384
|
+
described top-level `state` and its singular `stateId` when present, as well as
|
|
385
|
+
its message and optional data; consumers shall not have to recover state
|
|
386
|
+
identity from a nested ad hoc object.
|
|
387
|
+
Judge results use `reply`; player start and finish payloads both carry the
|
|
388
|
+
selected `resume`; Captain start and finish payloads both carry
|
|
389
|
+
the exact composed prompt, `visibility: 'visible'`, the direct invocation's
|
|
390
|
+
`stateId` and `sourceItem`, and no player resume selection or resume token;
|
|
391
|
+
judge `purpose` is
|
|
392
|
+
`boss-input-classification`, `player-output-adjudication`, or
|
|
393
|
+
`captain-output-adjudication`; and every error uses
|
|
394
|
+
`{ name, message, stack? }` rather than a raw string or `Error` instance.
|
|
395
|
+
The Captain finish payload shall preserve the exact `CaptainResult` status and
|
|
396
|
+
final text when present, while carrying any failure in normalized form.
|
|
397
|
+
An `ok` result without `finalText` therefore retains status `ok` but also
|
|
398
|
+
carries the normalized missing-text failure that makes the actor reject.
|
|
399
|
+
If the Captain port rejects before returning a result, the finish instead
|
|
400
|
+
carries explicit `status: 'aborted'` when the combined signal has aborted or
|
|
401
|
+
`status: 'error'` otherwise. A finish boundary never omits status merely
|
|
402
|
+
because there was no structured host result.
|
|
403
|
+
The pair obligation also applies when a port promise rejects or throws: the
|
|
404
|
+
linked runtime shall emit and drain one normalized finish boundary before it
|
|
405
|
+
propagates the failure. No started call boundary may be left without its
|
|
406
|
+
matching finished boundary.
|
|
407
|
+
If a started-boundary sink records the event and then rejects, the runtime
|
|
408
|
+
shall make one best-effort normalized error-finish attempt with the same call
|
|
409
|
+
id and then reject the original start error. It shall not retry either event or
|
|
410
|
+
let a failure of that finish attempt replace the start error.
|
|
411
|
+
When a call boundary carries `callId`, that id shall be unique within the
|
|
412
|
+
runtime session. A stable FSM `stateId` is identity metadata in the payload,
|
|
413
|
+
not a call id and shall not be reused as one across repeated invocations.
|
|
414
|
+
Optional trace and run-result members shall be omitted when absent; the runtime
|
|
415
|
+
shall not create own `turnId`, `callId`, parent identity, output, or error
|
|
416
|
+
properties with value `undefined` and then rely on JSON serialization to drop
|
|
417
|
+
them.
|
|
418
|
+
A `boss.input.settled` payload shall project the complete structured run
|
|
419
|
+
result: its outcome must be one of the `PlaybookRunResult` discriminants (never
|
|
420
|
+
an invented `error` outcome), and it shall include `state`, singular
|
|
421
|
+
`stateId`, `pendingCall`, `output`, and normalized `error` whenever the matching
|
|
422
|
+
result arm carries them.
|
|
423
|
+
One runtime-owned concurrency-one emission queue shall serialize every trace,
|
|
424
|
+
human status, and state telemetry call. Sequence allocation and enqueueing
|
|
425
|
+
shall occur atomically, and every public method shall drain that queue before
|
|
426
|
+
resolving or rejecting. A state transition emission queued on entry shall be
|
|
427
|
+
observed before the invoked boundary's `*.started` event, even when a host
|
|
428
|
+
delays `emitTelemetry`.
|
|
429
|
+
The linked module shall use `PQueue({ concurrency: 1 })` from `p-queue` for
|
|
430
|
+
this ordering and drain it with `onIdle()` rather than recreate a promise-queue
|
|
431
|
+
implementation in every generated artifact.
|
|
432
|
+
An XState inspection callback shall synchronously enqueue the transition
|
|
433
|
+
trace, state telemetry, status trace, and human status in that order before it
|
|
434
|
+
returns. `emitStatus` likewise enqueues its trace and port emission in the same
|
|
435
|
+
synchronous call. Do not enqueue state telemetry or the status port from a
|
|
436
|
+
`trace(...).then(...)` continuation: the queue can become momentarily idle,
|
|
437
|
+
letting an invoked actor's `await drain()` overtake those dependent enqueues.
|
|
438
|
+
All validation happens before these synchronous enqueues; later sink failures
|
|
439
|
+
are caught into the appropriate latch without changing their queue position.
|
|
440
|
+
FSM trace events carry the same transition, pending-question, and normalized-error fields as state telemetry.
|
|
441
|
+
Trace emissions are awaited and sequenced before the boundary operation or human status/state telemetry they describe.
|
|
442
|
+
Every event in one session carries the same root/parent/depth identity.
|
|
443
|
+
A parent call start precedes its child `session.started`; the child's
|
|
444
|
+
`session.disposed` precedes the parent call finish.
|
|
445
|
+
Parallel call finishes may occur in either order, so consumers shall use call
|
|
446
|
+
ids for pairing and sequence for the observed total order.
|
|
447
|
+
|
|
448
|
+
This trace covers everything observable through `PlaybookRuntime`; host-specific adapter streaming remains in the host record stream.
|
|
449
|
+
Trace payloads never become Boss-visible status or prompt text.
|
|
450
|
+
|
|
96
451
|
## Linker inputs
|
|
97
452
|
|
|
98
453
|
The link compiler shall accept:
|
|
@@ -101,6 +456,9 @@ The link compiler shall accept:
|
|
|
101
456
|
- A **player binding** mapping GEARS players (declared in the
|
|
102
457
|
[text2gears](text2gears.md#players) source) to opaque player-identifier
|
|
103
458
|
strings.
|
|
459
|
+
Where no binding is supplied, the linker shall apply the default
|
|
460
|
+
binding — each player to its lowercased name (e.g. `Coder` → `coder`)
|
|
461
|
+
— and record the applied binding in the emitted header.
|
|
104
462
|
- An **adjudication strategy** (default: LLM-judge per state) and a
|
|
105
463
|
**Boss-event mapping** (default: free-text judge classification).
|
|
106
464
|
Both strategies are host-agnostic.
|
|
@@ -109,12 +467,21 @@ The host's identity does not enter compilation; the linked module runs unchanged
|
|
|
109
467
|
|
|
110
468
|
## Player binding
|
|
111
469
|
|
|
112
|
-
Each GEARS state names exactly one player
|
|
113
|
-
|
|
470
|
+
Each delegated GEARS state names exactly one player
|
|
471
|
+
(`player` actor `invoke.input.player`).
|
|
472
|
+
The linker shall map every named player to a `playerId` string used in
|
|
473
|
+
`PlaybookPorts.callPlayer(playerId, …)`.
|
|
114
474
|
The host adapter routes that opaque string to its concrete primitive.
|
|
475
|
+
Every direct-Captain and delegated-player invocation shall also carry its
|
|
476
|
+
working leaf's explicit
|
|
477
|
+
`stateId`; a linked runtime shall use that field for call identity and shall
|
|
478
|
+
not infer one leaf from a structured root snapshot.
|
|
479
|
+
Direct `captain` actor states bypass player binding and call
|
|
480
|
+
`PlaybookPorts.callCaptain`; the linker shall not synthesize a player id named
|
|
481
|
+
`captain` for them.
|
|
115
482
|
|
|
116
483
|
For composite players declared with aliases (e.g., `Committer = Coder | Reviewer`), the linker shall resolve the alias **per source item**.
|
|
117
|
-
Resolution inspects the `
|
|
484
|
+
Resolution inspects the `PlayerInput` fields populated at that state:
|
|
118
485
|
|
|
119
486
|
- If only one `<playerName>Player` field is present, bind to that player.
|
|
120
487
|
- If multiple are present, prefer the first-listed alternative in the alias declaration order.
|
|
@@ -122,20 +489,28 @@ Resolution inspects the `CaptainInput` fields populated at that state:
|
|
|
122
489
|
|
|
123
490
|
Resolution shall be deterministic and recorded in the emitted module so future maintainers can audit it without re-running the linker.
|
|
124
491
|
|
|
125
|
-
The linker shall not invent player identifiers
|
|
492
|
+
The linker shall not invent player identifiers beyond the recorded default
|
|
493
|
+
binding, and shall not silently collapse aliases at the FSM level — composite
|
|
494
|
+
players keep their `player: 'Committer'` value on `PlayerInput`; resolution
|
|
495
|
+
decides only the `callPlayer` invocation.
|
|
126
496
|
|
|
127
497
|
## Player prompt composition
|
|
128
498
|
|
|
129
|
-
The runtime shall compose the actual player prompt from the state's
|
|
499
|
+
The runtime shall compose the actual player prompt from the state's
|
|
500
|
+
`PlayerInput`.
|
|
130
501
|
`input.prompt` is the GEARS-derived domain prompt body and shall not be mutated, re-flowed, or treated as a place to store framework control instructions.
|
|
131
502
|
|
|
132
|
-
The composer may prepend structured labelled blocks from typed `
|
|
503
|
+
The composer may prepend structured labelled blocks from typed `PlayerInput`
|
|
504
|
+
fields the FSM exposes (for example `Boss intent:`, `Review items:`,
|
|
505
|
+
`Rebuttals:`, or `Task description:`).
|
|
133
506
|
Those blocks are outside the domain prompt body.
|
|
134
507
|
|
|
135
508
|
The composer shall not inject a player-visible Boss-question instruction.
|
|
136
509
|
Boss-question detection is adjudicator-facing: it comes from the state's `needsBossReply` result description, not from extra prompt text.
|
|
137
510
|
|
|
138
|
-
When `
|
|
511
|
+
When `PlayerInput` carries both `pendingBossQuestion` and `bossReply`, the
|
|
512
|
+
composer shall prepend the continuation preamble and labelled Q&A blocks before
|
|
513
|
+
ordinary structured blocks and before the domain prompt body:
|
|
139
514
|
|
|
140
515
|
```text
|
|
141
516
|
You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.
|
|
@@ -145,52 +520,222 @@ Boss question:
|
|
|
145
520
|
|
|
146
521
|
Boss reply:
|
|
147
522
|
<bossReply>
|
|
523
|
+
|
|
148
524
|
```
|
|
149
525
|
|
|
150
526
|
The continuation preamble is framework text supplied by the runtime.
|
|
151
527
|
It is not part of the GEARS blockquote and shall not appear in `invoke.input.prompt`.
|
|
528
|
+
The composer shall retain the blank line after the Boss reply before the next
|
|
529
|
+
structured block or domain prompt, producing exactly two newline characters at
|
|
530
|
+
that boundary.
|
|
531
|
+
When implementing the prefix as an array joined with `"\n"`, the array needs
|
|
532
|
+
two trailing empty strings after `bossReply`; one trailing empty string emits
|
|
533
|
+
only one newline and is nonconformant. Equivalently, append `"\n\n"` exactly
|
|
534
|
+
once before the following block or domain body.
|
|
535
|
+
|
|
536
|
+
## Captain prompt composition
|
|
537
|
+
|
|
538
|
+
The runtime shall compose a direct Captain prompt from the state's
|
|
539
|
+
`CaptainInput` under the same prompt-integrity rules: `input.prompt` remains the
|
|
540
|
+
verbatim GEARS domain body, while specific typed fields may be supplied as
|
|
541
|
+
labelled blocks and substituted for their declared placeholders.
|
|
542
|
+
It shall not introduce a player binding or player resume instruction.
|
|
543
|
+
String fields substitute verbatim. Arrays and objects such as the sanitized
|
|
544
|
+
enabled-playbook catalog, remaining plan, and completed child results shall be
|
|
545
|
+
validated as JSON-safe and rendered as deterministic JSON; they shall never be
|
|
546
|
+
coerced through default JavaScript string conversion or expose untyped context.
|
|
547
|
+
Deterministic rendering shall sort object keys lexicographically at every
|
|
548
|
+
depth while preserving array order, so equivalent JSON values produce the same
|
|
549
|
+
prompt independent of host property insertion order.
|
|
550
|
+
At construction, a structured host-owned catalog shall be validated against its
|
|
551
|
+
declared exact entry shape, copied, and frozen recursively so later caller
|
|
552
|
+
mutation or extra properties cannot alter a prompt or machine decision.
|
|
553
|
+
For the default Captain catalog, every entry has exactly the own enumerable
|
|
554
|
+
data keys `id`, `command`, and `intent`; all three values are non-empty strings,
|
|
555
|
+
and `id` values are unique. Empty values, duplicate ids, extra keys, accessors,
|
|
556
|
+
non-plain objects, and non-JSON data reject runtime construction rather than
|
|
557
|
+
being silently repaired or discarded.
|
|
558
|
+
|
|
559
|
+
When a direct Captain task resumes from its own Boss question, the composer
|
|
560
|
+
shall prepend the same continuation preamble and labelled Q&A blocks defined in
|
|
561
|
+
§Player prompt composition. The runtime shall pass the complete composed prompt
|
|
562
|
+
once to `callCaptain` with `{ visibility: 'visible', resume: false }` and the
|
|
563
|
+
same source-owned tool restriction as the originating call; it shall not
|
|
564
|
+
expose the
|
|
565
|
+
subsequent adjudicator prompt or structured judge reply through that visible
|
|
566
|
+
call.
|
|
567
|
+
The composed prompt shall contain only the GEARS blockquote, typed runtime
|
|
568
|
+
evidence blocks, and the continuation preamble.
|
|
569
|
+
It shall not append the state's result map, guard names, result-property
|
|
570
|
+
schema, adjudication request, workspace context, or tool instructions.
|
|
571
|
+
The shared Captain composer shall replace every known placeholder whose
|
|
572
|
+
matching typed field is present in the supplied input. It shall not choose one
|
|
573
|
+
exclusive replacement set from `stateId`, source-item identity, or another
|
|
574
|
+
variant discriminator. Verification may deliberately combine catalog,
|
|
575
|
+
intent, plan, result, question, and reply fields in one synthetic input; every
|
|
576
|
+
matching placeholder in that template still has to be rendered.
|
|
577
|
+
Construct the replacement table from field presence alone. In particular,
|
|
578
|
+
populate `<remaining-plan>` when `remainingPlan` is supplied and
|
|
579
|
+
`<completed-call-results>` when `completedCallResults` is supplied, regardless
|
|
580
|
+
of the input's `stateId` or `sourceItem`. An implementation branch such as
|
|
581
|
+
`if (input.stateId === 'reassessment')` around either replacement is
|
|
582
|
+
nonconformant.
|
|
152
583
|
|
|
153
584
|
## Boss-event mapping
|
|
154
585
|
|
|
155
586
|
The FSM's `events` union enumerates every Boss-originated event.
|
|
156
|
-
The runtime receives Boss input as a free-form string
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
587
|
+
The runtime receives Boss input as a free-form string
|
|
588
|
+
(`handleBossInput.text`).
|
|
589
|
+
Where the current ready or reconstructed terminal machine accepts exactly one
|
|
590
|
+
ordinary textual entry event and no Boss question is pending, the runtime
|
|
591
|
+
shall send that event deterministically and attach the exact original text to
|
|
592
|
+
its declared textual payload field without invoking `callJudge`.
|
|
593
|
+
The default Captain's ready entry is
|
|
594
|
+
`{ type: 'BOSS_INTENT', bossIntent: turn.text }`.
|
|
595
|
+
All other non-empty turns shall use `callJudge` only to choose one of the FSM's
|
|
596
|
+
event kinds and non-text routing fields, or no FSM action.
|
|
597
|
+
The classifier prompt shall include the exact, unmodified `turn.text` in a
|
|
598
|
+
clearly labelled Boss-message block so the judge can make that choice. Omitting
|
|
599
|
+
the message makes a parked-state classifier unable to distinguish an answer,
|
|
600
|
+
a fresh directive, and no action; including it does not authorize the judge to
|
|
601
|
+
rewrite the runtime-owned textual payload fields.
|
|
602
|
+
For `BOSS_INTENT` and `BOSS_INTERRUPT`, the runtime shall attach the exact
|
|
603
|
+
original text as `bossIntent`; for `BOSS_REPLY`, it shall attach the exact
|
|
604
|
+
original text as `answer`.
|
|
605
|
+
The classifier prompt shall neither request nor accept a copy of those fields,
|
|
606
|
+
and classifier-authored paraphrases shall never become machine context.
|
|
607
|
+
Empty or whitespace-only text produces no event, judge call, Captain call,
|
|
608
|
+
player call, status emission, or FSM transition; its received and settled
|
|
609
|
+
session-trace events are still emitted.
|
|
610
|
+
|
|
611
|
+
The classifier prompt shall demand JSON against the FSM's typed event union and any state-specific Boss input contract, including non-text routing payload fields required for each event but excluding the runtime-owned textual fields above.
|
|
612
|
+
Fields the FSM's event union declares optional shall stay optional in the classifier contract and the reply parser; the classifier shall not promote them to required.
|
|
613
|
+
The runtime shall parse the judge reply tolerantly before validating the
|
|
614
|
+
event. It shall recover the intended JSON object from surrounding prose or a
|
|
615
|
+
Markdown fence, ignore earlier non-JSON bracketed prose, remove a trailing
|
|
616
|
+
comma before a closing brace or bracket, and complete a truncated
|
|
617
|
+
unterminated string or unclosed object/array. When several values are
|
|
618
|
+
recoverable, it shall choose the first object in document order, preferring a
|
|
619
|
+
strict parse at each candidate position before repairing that same candidate.
|
|
620
|
+
For each opening-brace position, first scan strings and nesting to find that
|
|
621
|
+
candidate's earliest balanced closing boundary. Both the strict parse and the
|
|
622
|
+
trailing-comma repair shall operate on only that bounded substring. If no
|
|
623
|
+
closing boundary exists, repair may complete the unterminated suffix. The
|
|
624
|
+
implementation shall never repair the entire remaining document after a
|
|
625
|
+
balanced candidate, because later prose or a later clean object would make the
|
|
626
|
+
earlier repair fail. Advance to the next opening brace only after strict and
|
|
627
|
+
repaired parsing of the current bounded candidate both fail; an earlier
|
|
628
|
+
repairable object therefore wins over every later strict object.
|
|
629
|
+
When no object is recoverable or the recovered event/payload is invalid, the
|
|
630
|
+
runtime shall emit exactly one status and send no FSM event; a malformed
|
|
631
|
+
classification is recoverable control input, not a public boundary rejection.
|
|
632
|
+
If a recovered `BOSS_REPLY` names no question that is currently pending, it is
|
|
633
|
+
such a malformed classification: emit the one recovery status, send no event,
|
|
634
|
+
leave the actor unchanged, and return `no-action` after emissions drain.
|
|
635
|
+
Host-owned runtime options, player bindings, and enabled-playbook catalogs are
|
|
636
|
+
not Boss-event payload. The classifier schema and parser shall not invite or
|
|
637
|
+
accept them, and classified prose shall never overwrite their machine context.
|
|
638
|
+
Every recovered classifier object shall have exactly `type` plus the declared
|
|
639
|
+
non-text routing keys for its selected event arm. Extra own keys, including a
|
|
640
|
+
classifier-authored `bossIntent` or `answer`, reject the classification; the
|
|
641
|
+
parser shall not accept and discard injected catalog, option, state, or
|
|
642
|
+
routing fields.
|
|
643
|
+
`NO_ACTION` in particular is exactly `{ type: 'NO_ACTION' }`.
|
|
644
|
+
A valid `NO_ACTION` returns `no-action` without an invalid-classification
|
|
645
|
+
status and leaves the actor untouched. It is a successful classifier choice,
|
|
646
|
+
not the same parser result as malformed or unrecoverable classifier output.
|
|
647
|
+
After any successful classifier call drains, re-check the active Boss signal
|
|
648
|
+
before reconstructing a terminal actor or sending the selected event. If it
|
|
649
|
+
aborted while the classifier finish emission was pending, return and trace the
|
|
650
|
+
same structured `aborted` result against the unchanged actor.
|
|
651
|
+
When the FSM supports a Boss-reply suspension state, the prompt shall inspect
|
|
652
|
+
the actor snapshot context and include each exact pending Boss question,
|
|
653
|
+
question id, and asking player so the judge can distinguish a reply from a
|
|
654
|
+
fresh directive. With one pending question, a classified `BOSS_REPLY` that
|
|
655
|
+
omits its optional id shall be filled with that sole id. With several pending
|
|
656
|
+
questions, the classifier shall require a known id. A reply shall re-enter only
|
|
657
|
+
its recorded resume state and preserve the original intent, plan, prior child
|
|
658
|
+
results, and Q+A continuation context.
|
|
659
|
+
The classifier-facing pending-question block contains only `questionId`,
|
|
660
|
+
`player`, and `question`. Internal `resumeStateId`, source-item identity, and
|
|
661
|
+
other machine-routing fields remain authoritative in snapshot context and
|
|
662
|
+
shall not be serialized into the judge prompt.
|
|
663
|
+
The allowed fresh directives while parked include every applicable root entry
|
|
664
|
+
event and `BOSS_INTERRUPT`; accepting one shall abandon and clear the pending
|
|
665
|
+
question and reply context before new work begins.
|
|
161
666
|
|
|
162
667
|
A playbook runtime shall not define slash-prefix commands for states or features inside that playbook.
|
|
163
668
|
The `/command` namespace is reserved for host-level or playbook-selection UX before a turn reaches `handleBossInput`.
|
|
164
|
-
If a host forwards text beginning with `/` to `handleBossInput`, the runtime treats it as ordinary Boss text and
|
|
669
|
+
If a host forwards text beginning with `/` to `handleBossInput`, the runtime treats it as ordinary Boss text and maps it through the same deterministic-or-classified Boss-event rules.
|
|
165
670
|
|
|
166
671
|
Hosts that receive structured control input shall resolve host-level concerns before choosing a playbook runtime.
|
|
167
672
|
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.
|
|
168
673
|
|
|
169
674
|
`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.
|
|
170
|
-
It is
|
|
675
|
+
It is _not_ an abort surface; aborts go through the abort signal and the strategies in §Abort.
|
|
171
676
|
Hosts where the abort signal is terminal (e.g., SIGINT runs shutdown) shall not route abort to `BOSS_INTERRUPT`.
|
|
172
677
|
|
|
173
678
|
## Captain adjudication
|
|
174
679
|
|
|
175
|
-
After a player call returns, the runtime shall
|
|
680
|
+
After a direct Captain or delegated player call returns, the runtime shall
|
|
681
|
+
coerce `result.finalText` into one of the **per-state**
|
|
682
|
+
`invoke.input.result` keys.
|
|
176
683
|
It shall also extract any payload fields the state's `result` description names as required.
|
|
684
|
+
Required-field extraction shall recognize both an exact backticked property
|
|
685
|
+
name such as `` `question` `` and the standard annotated form
|
|
686
|
+
`` `question: <verbatim question text>` ``; in either form only `question` is
|
|
687
|
+
the JSON property name.
|
|
688
|
+
Extraction is limited to the description's explicit `Output shall include`
|
|
689
|
+
clause (or equivalent typed output metadata). Backticked prose before that
|
|
690
|
+
clause can name statuses, guards, or concepts such as `ok`, `aborted`, and
|
|
691
|
+
`error`; those names are not output properties and shall never become required
|
|
692
|
+
judge fields.
|
|
693
|
+
For a direct Captain result, `question` and `response` are human-presentation
|
|
694
|
+
fields owned by the visible call rather than fields authored by the hidden
|
|
695
|
+
judge.
|
|
696
|
+
The adjudicator shall select the guard and supply only other structural fields
|
|
697
|
+
required by that guard.
|
|
698
|
+
After validating that selection, the runtime shall inject the exact non-empty
|
|
699
|
+
`CaptainResult.finalText` as the selected output's `question` or `response`.
|
|
700
|
+
It shall reject a judge reply that supplies either presentation field as an
|
|
701
|
+
undeclared extra key, so hidden adjudication cannot replace, paraphrase, or
|
|
702
|
+
decorate prose Boss already saw.
|
|
703
|
+
Delegated-player adjudication retains extraction of every required field from
|
|
704
|
+
the judge reply, including a player-authored Boss question.
|
|
705
|
+
The adjudicator shall use the same document-order tolerant JSON recovery as
|
|
706
|
+
the Boss classifier. Unlike invalid classification, a reply from which no
|
|
707
|
+
object can be recovered, an undeclared guard, or a missing required field is a
|
|
708
|
+
control-plane error and shall throw after the invocation reaches its FSM error
|
|
709
|
+
path and ordered emissions drain.
|
|
177
710
|
|
|
178
711
|
Two default adjudication strategies, in selection order:
|
|
179
712
|
|
|
180
713
|
- **LLM-judge** (default): construct a fresh prompt for `callJudge` that
|
|
181
|
-
names the source item's
|
|
714
|
+
names the source item's actor (and delegated player where applicable),
|
|
715
|
+
includes the actor's verbatim output,
|
|
182
716
|
lists the `result` keys with their descriptions, and demands a JSON
|
|
183
|
-
`{ guard, …
|
|
184
|
-
declared guards
|
|
717
|
+
`{ guard, …structuralPayloadFields }` answer keyed to exactly one of the
|
|
718
|
+
declared guards, excluding the runtime-owned direct-Captain `question` and
|
|
719
|
+
`response` fields above. The judge prompt shall not interpret the player's
|
|
185
720
|
output, paraphrase it, or alter the FSM's `result` text — it carries
|
|
186
721
|
the description verbatim.
|
|
187
|
-
- **Marker-parse** (alternative): a deterministic parser that
|
|
188
|
-
player output for a terminal control line such as
|
|
722
|
+
- **Marker-parse** (delegated-player alternative): a deterministic parser that
|
|
723
|
+
scans the player output for a terminal control line such as
|
|
189
724
|
`FSM-RESULT: { "guard": "...", ... }`. Useful when player adapters can
|
|
190
725
|
be steered to emit structured trailers and the operator wants to avoid
|
|
191
726
|
the extra LLM call.
|
|
192
727
|
|
|
193
|
-
The linker may select different strategies per state; the
|
|
728
|
+
The linker may select different strategies per delegated-player state; the
|
|
729
|
+
default is **LLM-judge for every state**. Direct-Captain states shall use the
|
|
730
|
+
LLM judge so their visible prose remains human-readable and carries no marker
|
|
731
|
+
or control JSON. Their adjudicator call uses purpose
|
|
732
|
+
`captain-output-adjudication` and remains hidden at the host adapter.
|
|
733
|
+
|
|
734
|
+
When the direct Captain result selects a terminal `response`, the exact
|
|
735
|
+
already-visible `CaptainResult.finalText` is the machine response and Boss
|
|
736
|
+
presentation. The linked
|
|
737
|
+
runtime shall not make a second visible Captain call or expose the hidden
|
|
738
|
+
structured adjudication merely to present the same response.
|
|
194
739
|
|
|
195
740
|
The adjudicator shall fail loudly on:
|
|
196
741
|
|
|
@@ -198,49 +743,450 @@ The adjudicator shall fail loudly on:
|
|
|
198
743
|
- A missing payload field the state's `result` description requires,
|
|
199
744
|
- An empty / malformed response.
|
|
200
745
|
|
|
746
|
+
These cases shall remain distinguishable in the thrown error: malformed JSON
|
|
747
|
+
recovery shall identify the missing JSON object, an unknown selection shall
|
|
748
|
+
identify an undeclared guard, and an incomplete selection shall identify the
|
|
749
|
+
missing required field. A generic “no declared guard selected” error for all
|
|
750
|
+
three cases is nonconformant.
|
|
751
|
+
|
|
201
752
|
Adjudicator failures are control-plane errors.
|
|
202
753
|
The runtime shall propagate them by throwing out of `handleBossInput` after attempting cleanup.
|
|
203
754
|
The host adapter surfaces the throw on its control-plane channel (cligent surfaces such throws as `runtime_error` per [TMUX-025](https://github.com/sublang-ai/cligent/blob/main/specs/user/tmux-play.md#tmux-025)).
|
|
204
755
|
The host's player-result channels (`player_finished` and equivalents) are reserved for failures the player itself produced; the host emits them when `callPlayer` resolves with `status !== 'ok'`.
|
|
756
|
+
Captain call failures stay on the Captain/control boundary and shall not be
|
|
757
|
+
reported as player failures.
|
|
758
|
+
Because XState still needs the invoked promise to settle, the linked runtime
|
|
759
|
+
shall latch an adjudicator, actor-output JSON-validation, or nested-boundary
|
|
760
|
+
control error outside machine context, allow the invocation's `onError` path to
|
|
761
|
+
reach quiescence, drain all emissions, and then reject the public runtime
|
|
762
|
+
method with that original error. It shall not return such a failure as a
|
|
763
|
+
recoverable `{ outcome: 'failed' }` workflow result.
|
|
764
|
+
The first latched non-abort control error takes precedence over a coincident
|
|
765
|
+
boundary-signal abort. Read and clear the latch only in the public boundary's
|
|
766
|
+
`finally` cleanup after XState and emissions have settled, so it cannot leak
|
|
767
|
+
into a later Boss turn or be erased before rejection.
|
|
768
|
+
An `AbortError`-named transport, validation, or trace-sink failure is still a
|
|
769
|
+
non-abort control error unless it is causally identical to the applicable
|
|
770
|
+
signal reason. Error names shall never change original-error or first-latch
|
|
771
|
+
precedence.
|
|
772
|
+
When a host port or structured-result validator fails after a call-start
|
|
773
|
+
boundary, latch that original error before attempting the required finish
|
|
774
|
+
trace. If the finish sink records the event and then rejects, do not emit a
|
|
775
|
+
second finish and do not let the sink failure replace the earlier control
|
|
776
|
+
error returned by the public boundary; retain the sink failure only as
|
|
777
|
+
independent cleanup evidence.
|
|
778
|
+
|
|
779
|
+
## Script execution
|
|
780
|
+
|
|
781
|
+
Where the FSM declares the typed `script` actor from
|
|
782
|
+
[gears2fsm "Setup"](gears2fsm.md#setup), the linked runtime shall provide
|
|
783
|
+
its implementation through the shared factory (§Output); the linker shall
|
|
784
|
+
not regenerate a script executor inside each emitted module.
|
|
785
|
+
A script invocation is the one actor kind that runs without any agent:
|
|
786
|
+
it makes no `callPlayer`, `callCaptain`, or `callJudge` call and needs no
|
|
787
|
+
adjudication.
|
|
788
|
+
|
|
789
|
+
The provided actor shall:
|
|
790
|
+
|
|
791
|
+
- Execute `input.command` verbatim through the platform's POSIX shell
|
|
792
|
+
(`sh -c`), with the working directory taken from the emitted
|
|
793
|
+
`PlaybookRuntimeOptions.cwd` when the caller supplies it, else the process
|
|
794
|
+
working directory. The linker shall declare the optional `cwd` option on the
|
|
795
|
+
emitted options interface whenever the FSM contains a script state; the
|
|
796
|
+
validated option reaches the shared script actor through the spec.
|
|
797
|
+
- Resolve deterministically from the child's exit status: status zero resolves
|
|
798
|
+
`{ guard: <first declared guard>, exitStatus: 0 }`; any nonzero status
|
|
799
|
+
resolves the second declared guard with that status. Guard selection is
|
|
800
|
+
mechanical; the runtime shall not route script output through the judge.
|
|
801
|
+
- Reject only when the command cannot be spawned at all, routing through the
|
|
802
|
+
state's ordinary `onError` path.
|
|
803
|
+
- Honor the active turn's abort signal by terminating the child process and
|
|
804
|
+
rejecting per §Abort.
|
|
805
|
+
- Emit, after the child settles and before the invocation resolves, one status
|
|
806
|
+
line `Executed script for <stateId> (exit <status>).` and one telemetry
|
|
807
|
+
event under topic `playbook.script` with payload
|
|
808
|
+
`{ stateId, sourceItem, exitStatus }`, through the ordinary serialized
|
|
809
|
+
emission channel.
|
|
810
|
+
|
|
811
|
+
Script execution emits no `*.call.*` trace pair: the surrounding FSM
|
|
812
|
+
transition trace and the `playbook.script` telemetry are its record, so trace
|
|
813
|
+
schema consumers see no new event types.
|
|
814
|
+
Script stdout and stderr are not workflow data: the runtime shall not place
|
|
815
|
+
them in machine context, prompts, or trace payloads.
|
|
816
|
+
|
|
817
|
+
## Nested playbook bridge
|
|
818
|
+
|
|
819
|
+
Where the FSM declares the typed `playbook` actor from
|
|
820
|
+
[gears2fsm](gears2fsm.md#nested-playbook-calls), the linked runtime shall provide
|
|
821
|
+
it with the shared `createNestedPlaybookBridge(...).actorLogic` — wired by the
|
|
822
|
+
shared factory per §Output — and shall not regenerate a second pending-call,
|
|
823
|
+
identity-validation, or abort-cleanup substrate inside each linked artifact.
|
|
824
|
+
Instantiate the generic bridge with the FSM-exported `PlaybookInput` type so
|
|
825
|
+
XState `.provide(...)` receives the exact declared actor input rather than a
|
|
826
|
+
structurally similar local type.
|
|
827
|
+
Construct one bridge per runtime and wire every integration hook: allocate ids
|
|
828
|
+
with `nextCallId`; return the currently active public-boundary signal from
|
|
829
|
+
`getBoundarySignal`; bind `resumePlaybookCall.signal` before settling the
|
|
830
|
+
deferred actor through `bindResumeSignal`; enqueue the exact start/finish trace
|
|
831
|
+
through `emitStarted` / `emitFinished`; drain the global emission queue through
|
|
832
|
+
`drain`; latch the original control error through `onControlPlaneError`; and
|
|
833
|
+
retain any cleanup/observer failure through `onBackgroundError` for the next
|
|
834
|
+
public boundary or disposal rejection. The runtime shall not leave these
|
|
835
|
+
optional API hooks unwired merely because their TypeScript properties are
|
|
836
|
+
optional for simpler bridge consumers.
|
|
837
|
+
On invocation the bridge allocates a runtime-local call id, traces the start,
|
|
838
|
+
and calls `PlaybookPorts.callPlaybook` with the composed target/text and the
|
|
839
|
+
bridge signal combined from the XState invocation lifetime, the active public
|
|
840
|
+
boundary, and the bridge's own disposal controller.
|
|
841
|
+
|
|
842
|
+
For a literal invocation, target and text retain their existing static/composed
|
|
843
|
+
values. For a dynamic invocation, the bridge shall use the evaluated
|
|
844
|
+
`PlaybookInput.playbookId` and `PlaybookInput.text` values, require both to be
|
|
845
|
+
strings with non-empty target and text, and preserve the exact resolved values in the
|
|
846
|
+
request and trace. The linker shall preserve the FSM's static
|
|
847
|
+
`playbookIdContext` and `textContext` metadata for conformance; it shall not
|
|
848
|
+
parse function source, treat either metadata name as the runtime value, or
|
|
849
|
+
freeze a dynamic call to the value observed during artifact inspection.
|
|
850
|
+
|
|
851
|
+
If the port returns `state: 'settled'`, the bridge validates the result,
|
|
852
|
+
emits and drains `playbook.call.finished`, then resolves successful output or
|
|
853
|
+
rejects an aborted/error result.
|
|
854
|
+
If the port returns `state: 'suspended'`, the bridge records one pending call
|
|
855
|
+
and awaits a runtime-owned deferred result.
|
|
856
|
+
Only after that pending record exists may the drive boundary treat the call
|
|
857
|
+
state's `playbook.suspended` tag as quiescent.
|
|
858
|
+
One runtime supports at most one pending child call; a second shall reject.
|
|
859
|
+
The pending record shall also retain the call-start `turnId`. A resumed finish
|
|
860
|
+
and every parent transition, Captain reassessment, and status caused by that
|
|
861
|
+
return shall use this retained id, not an absent or newly allocated
|
|
862
|
+
current-turn value. The finish callback shall receive or close over that stored
|
|
863
|
+
id rather than read a mutable global turn id at resume time.
|
|
864
|
+
The bridge shall strictly validate the start discriminant, non-empty suspended
|
|
865
|
+
child session id, settled target identity, optional state descriptor,
|
|
866
|
+
normalized error, and JSON-safe output. A malformed start, malformed result,
|
|
867
|
+
identity mismatch, or non-JSON value is a control-plane error. Once a start
|
|
868
|
+
trace exists, every thrown port, validation failure, immediate result,
|
|
869
|
+
suspension resume, invocation abort, and disposal path shall emit and drain
|
|
870
|
+
exactly one matching finish trace; malformed data shall neither create a
|
|
871
|
+
pending identity nor be reassessed as ordinary child evidence.
|
|
872
|
+
The bridge shall detach and recursively freeze a validated start/result before
|
|
873
|
+
tracing it or delivering it to the FSM, so caller mutation after port
|
|
874
|
+
resolution cannot alter identity, evidence, or trace payloads. A non-abort
|
|
875
|
+
`callPlaybook` throw/rejection is a control-plane failure: pair its finish,
|
|
876
|
+
latch and rethrow the original error, and take the FSM fallback error path. A
|
|
877
|
+
rejection caused by the combined abort signal remains an authored `aborted`
|
|
878
|
+
child result.
|
|
879
|
+
The optional output field may be absent from an otherwise valid successful
|
|
880
|
+
child result. Generated event and trace descriptors shall omit an absent or
|
|
881
|
+
`undefined` output instead of attempting to snapshot it as a JSON value.
|
|
882
|
+
When cancellation wins while the host's opening promise is still pending, the
|
|
883
|
+
shared bridge shall retain and drain that exact promise before emitting the
|
|
884
|
+
matching finish boundary. It shall ignore an abort-reason rejection from that
|
|
885
|
+
opening promise, surface any other late rejection as a control-plane cleanup
|
|
886
|
+
failure, and recover a child session identity from a late resolved start when
|
|
887
|
+
available. Generated runtimes shall pass the host port directly to the shared
|
|
888
|
+
bridge rather than recreate this opening-promise drainage locally.
|
|
889
|
+
In particular, aborting a public turn during that opening promise shall abort
|
|
890
|
+
the combined bridge signal, wait for opening cleanup and the paired finish,
|
|
891
|
+
let the promise actor reach its `onError` quiescent state, and only then return
|
|
892
|
+
an aborted run result. It shall neither hang waiting for a child-resume path
|
|
893
|
+
that was never registered nor return while the opening promise or finish
|
|
894
|
+
emission remains live.
|
|
895
|
+
The pending record shall retain a one-shot invocation-signal listener. If the
|
|
896
|
+
call state is stopped, that listener shall settle and clear the deferred call
|
|
897
|
+
as an aborted `NestedPlaybookCallError`, drain the matching finish boundary after
|
|
898
|
+
host abort cleanup, and make a later nested invocation possible; it shall not
|
|
899
|
+
leave a permanently pending record merely because XState stopped observing the
|
|
900
|
+
promise actor.
|
|
901
|
+
|
|
902
|
+
`resumePlaybookCall` shall accept only the matching pending call id, target
|
|
903
|
+
playbook id, and child session id; bind its new turn signal for work resumed in
|
|
904
|
+
the parent; emit and drain the call-finish trace; settle the bridge deferred;
|
|
905
|
+
and use XState `waitFor` to drive the parent to its next
|
|
906
|
+
quiescent, suspended, failed, aborted, or terminal result.
|
|
907
|
+
An `ok` result resolves the actor and reaches `invoke.onDone`; `aborted` and
|
|
908
|
+
`error` results reject it and reach `invoke.onError`.
|
|
909
|
+
The rejection shall be an `Error` whose public readonly `result` property is
|
|
910
|
+
the exact normalized `PlaybookCallResult`; throwing the result object directly
|
|
911
|
+
or discarding its status prevents the FSM from distinguishing abort from
|
|
912
|
+
failure during recovery.
|
|
913
|
+
Unknown, duplicate, or stale call ids reject without changing actor state.
|
|
914
|
+
The finish trace shall therefore precede any parent FSM transition caused by
|
|
915
|
+
the child return.
|
|
916
|
+
The host independently validates every evaluated target against its enabled
|
|
917
|
+
registry; linker-time metadata is not authorization to call a target.
|
|
918
|
+
|
|
919
|
+
Disposal shall settle an outstanding call as aborted and drain its finish
|
|
920
|
+
trace before `session.disposed`.
|
|
921
|
+
If registered child abort cleanup rejects, the bridge shall emit the paired
|
|
922
|
+
finish with an error result and reject `abortPending` or disposal with that
|
|
923
|
+
original cleanup error; it shall not swallow the failure merely because the
|
|
924
|
+
promise actor also observes a `NestedPlaybookCallError`. Parent disposal shall
|
|
925
|
+
still drain, emit its one `session.disposed` boundary, and clear the bound
|
|
926
|
+
session before rejecting with that preserved cleanup error.
|
|
927
|
+
Child output and errors must be JSON-safe; a non-JSON-safe result is a
|
|
928
|
+
control-plane error.
|
|
205
929
|
|
|
206
930
|
## Session lifecycle
|
|
207
931
|
|
|
208
932
|
The `PlaybookRuntime` shall:
|
|
209
933
|
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
934
|
+
- Reject use before `init`, a second active turn or resume, and re-initializing
|
|
935
|
+
a live session. `handleBossInput` and `resumePlaybookCall` share one active
|
|
936
|
+
turn sentinel; neither may overlap the other, and disposal shall not race a
|
|
937
|
+
live boundary. A dispose request made during an active public boundary shall
|
|
938
|
+
reject without beginning teardown. Idle concurrent dispose requests shall
|
|
939
|
+
share one disposal promise; later calls after disposal shall return that
|
|
940
|
+
settled disposal outcome without emitting another boundary. Once disposal
|
|
941
|
+
begins, no new turn or resume may start.
|
|
942
|
+
Disposal requested during initialization shall retain one teardown promise,
|
|
943
|
+
wait for initialization's success or failure cleanup, and emit at most one
|
|
944
|
+
`session.disposed` boundary. Disposal before initialization is terminal and
|
|
945
|
+
coalesced: later initialization rejects and every later disposal call
|
|
946
|
+
returns the first retained promise.
|
|
947
|
+
Represent in-flight initialization with a cleanup-complete latch resolved by
|
|
948
|
+
`init`'s outer `finally`, after either successful startup or the complete
|
|
949
|
+
failed-start cleanup. Do not expose the fallible inner startup promise as
|
|
950
|
+
that latch: it rejects before the outer cleanup and lets concurrent disposal
|
|
951
|
+
race the cleanup's own `session.disposed` attempt.
|
|
952
|
+
Put session validation and snapshotting, bridge/actor construction, initial
|
|
953
|
+
state reads, and startup emissions inside that guarded outer `try`; none may
|
|
954
|
+
throw before the cleanup-complete latch's `finally` can resolve. A rejected
|
|
955
|
+
session identity must not leave later disposal waiting forever.
|
|
956
|
+
The generated `dispose` method shall not be declared `async`, because an
|
|
957
|
+
async wrapper returns a distinct promise and breaks identity coalescing; it
|
|
958
|
+
shall return the retained teardown promise directly and use
|
|
959
|
+
`Promise.reject(...)` for precondition failures.
|
|
960
|
+
|
|
961
|
+
- In `init`, bind the immutable `PlaybookSession`, emit
|
|
962
|
+
`session.started` with the initial normalized state descriptor, and
|
|
963
|
+
construct the XState actor with FSM `input` derived
|
|
964
|
+
from `options`. The actor is session-scoped, not turn-scoped. Use XState v5's
|
|
965
|
+
public actor inspection `@xstate.snapshot` event for the root actor so each
|
|
966
|
+
transition's triggering event and snapshot can be surfaced via `emitStatus`
|
|
967
|
+
and `emitTelemetry` before the next event fires; do not consult private actor
|
|
968
|
+
nodes or infer the event later from context. Filter inspection events by
|
|
969
|
+
`inspectionEvent.actorRef === rootActor`, not merely by the actor-system root
|
|
970
|
+
id, so promise-child snapshots are not emitted as root FSM transitions. The
|
|
971
|
+
inspection callback shall only validate and synchronously enqueue emission
|
|
972
|
+
work, catching validation/enqueue failures into the control/background-error
|
|
973
|
+
latch; it shall not let an exception escape or call an async port directly.
|
|
974
|
+
Its transition `event` field shall be a detached JSON-safe descriptor, never
|
|
975
|
+
the raw XState inspection event. Preserve the string `type` (or use
|
|
976
|
+
`unknown` when absent); copy only declared Boss-union payload fields and a
|
|
977
|
+
validated actor `output`, and normalize an `error` member before inclusion.
|
|
978
|
+
Omit `input`, `actorId`, system/ref data, and every other XState-internal
|
|
979
|
+
field even when it happens to be JSON-safe, so `xstate.init.input` cannot
|
|
980
|
+
leak the host catalog into transition telemetry. In particular, do not call
|
|
981
|
+
`snapshotJsonValue(event)` on an `xstate.error.actor.*` event that contains a
|
|
982
|
+
raw `Error`.
|
|
983
|
+
Construct the actor without starting it, read its public initial snapshot,
|
|
984
|
+
emit and drain `session.started`, and only then call `actor.start()`. The
|
|
985
|
+
initial inspection-driven transition/status emissions shall not precede the
|
|
986
|
+
session-start trace. Have any actor-construction helper return the actor and
|
|
987
|
+
assign it at the call site; TypeScript does not narrow a captured optional
|
|
988
|
+
actor variable from assignment hidden inside a helper. Retain a non-optional
|
|
989
|
+
local actor reference across terminal reconstruction and event sending.
|
|
990
|
+
An actor-construction helper may read the already-bound immutable session
|
|
991
|
+
directly for machine input such as `session.playbookId`, but it shall not
|
|
992
|
+
call a lifecycle assertion that also requires the actor to exist. The actor
|
|
993
|
+
does not exist until that helper returns, so coupling session access to actor
|
|
994
|
+
availability makes every valid `init` fail before construction completes.
|
|
995
|
+
Generated code shall pass the repository's full strict `tsc` build with no
|
|
996
|
+
unused helper or destructured parameter, not only a transpile-only or
|
|
997
|
+
target-local syntax check.
|
|
998
|
+
For the default Captain runtime, the initial quiescent `ready` snapshot may
|
|
999
|
+
emit the ordinary structured transition trace and telemetry, but it is not a
|
|
1000
|
+
Boss-relevant transition and shall emit no human status. Any initial
|
|
1001
|
+
transition-trace or telemetry sink failure is part of `init`: initialization
|
|
1002
|
+
shall reject, stop the actor, and perform the failed-start cleanup below
|
|
1003
|
+
rather than swallowing it as a later background error.
|
|
1004
|
+
Where the FSM input declares `selfPlaybookId`, seed it from the immutable
|
|
1005
|
+
`session.playbookId`; do not expose a caller option or reuse a working leaf's
|
|
1006
|
+
`stateId` as the self-call identity.
|
|
1007
|
+
- If initialization fails after attempting `session.started`, stop the actor,
|
|
1008
|
+
abort/drain nested and host work, and make one best-effort
|
|
1009
|
+
`session.disposed` attempt before clearing the bound session. Preserve the
|
|
1010
|
+
original initialization error if cleanup or disposal emission also fails.
|
|
1011
|
+
Suppress root inspection emissions before stopping the failed actor because
|
|
1012
|
+
XState emits a stop snapshot; that teardown snapshot shall not retry a
|
|
1013
|
+
transition/status sink that already failed initialization. Reset the
|
|
1014
|
+
inspection gate, queues, error latches, prior state, and all per-session
|
|
1015
|
+
sequence counters so a permitted retry starts with trace sequence `1`.
|
|
214
1016
|
- Per `handleBossInput`:
|
|
215
|
-
1.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
actor,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
-
|
|
1017
|
+
1. Allocate a runtime-local turn id and trace the exact Boss text.
|
|
1018
|
+
2. Map `turn.text` through the Boss-event mapping, using deterministic exact
|
|
1019
|
+
entry where applicable and classification otherwise.
|
|
1020
|
+
If mapping produces no event, return after draining any port emissions.
|
|
1021
|
+
If the classifier port rejects, emit and drain the Boss-settled error
|
|
1022
|
+
boundary, send no event, leave the actor unchanged (including a terminal
|
|
1023
|
+
actor), and reject the original error. If that rejection is caused by the
|
|
1024
|
+
active Boss abort signal, return and trace the same structured `aborted`
|
|
1025
|
+
result instead of tracing `no-action`. If the port resolves but its reply
|
|
1026
|
+
cannot be recovered or validated, emit the one recovery status required
|
|
1027
|
+
by §Boss-event mapping, send no event, leave the actor unchanged, and
|
|
1028
|
+
return `no-action` after the ordinary settled boundary drains.
|
|
1029
|
+
3. Only after classification produces a real event, if the actor is in a
|
|
1030
|
+
`final` state, dispose and reconstruct it — `final` is terminal and cannot
|
|
1031
|
+
accept new events. `NO_ACTION`, classifier rejection, and malformed
|
|
1032
|
+
classification shall leave a terminal actor untouched.
|
|
1033
|
+
4. Bind the active public-boundary signal and send the classified event to
|
|
1034
|
+
the actor.
|
|
1035
|
+
5. **Drive to quiescence**: provide each invoked actor according to its
|
|
1036
|
+
declared kind. For `player`, build a player prompt, call `callPlayer`,
|
|
1037
|
+
adjudicate, and resolve the invoke. For `captain`, build a direct Captain
|
|
1038
|
+
prompt, call `callCaptain` visibly, adjudicate through the shared hidden
|
|
1039
|
+
judge path, and resolve the invoke. For `playbook`, use §Nested playbook
|
|
1040
|
+
bridge. For `script`, use §Script execution — no port call and no
|
|
1041
|
+
adjudication. Parallel regions may run distinct resolved players independently;
|
|
1042
|
+
Captain and judge work remains serialized by the shared host queue. Use
|
|
1043
|
+
XState `waitFor` over public tags/status until no `playbook.busy` state is
|
|
1044
|
+
active, a registered child call is suspended, or the actor is
|
|
1045
|
+
terminal/error. Pass `pendingCalls: nestedBridge` so a suspended tag is
|
|
1046
|
+
quiescent only after its child identity exists. Under natural rejection,
|
|
1047
|
+
do not pass the already-aborted public turn signal as wait cancellation:
|
|
1048
|
+
it has already been combined into the invoked boundary, and the runtime
|
|
1049
|
+
must now wait for XState's `onError` transition and quiescence.
|
|
1050
|
+
6. Return a structured `PlaybookRunResult` after all in-flight calls and
|
|
1051
|
+
ordered emissions caused by the turn drain.
|
|
1052
|
+
- Per `resumePlaybookCall`, follow §Nested playbook bridge and return the same
|
|
1053
|
+
structured run-result boundary without classifying new Boss text. Drain the
|
|
1054
|
+
transition/status/telemetry queue before returning, just as
|
|
1055
|
+
`handleBossInput` does. A resume shall not allocate a new Boss-input
|
|
1056
|
+
`turnId`; retain the original call-start turn id for its matching finish and
|
|
1057
|
+
for the parent continuation caused by that return. Every success and
|
|
1058
|
+
exceptional path shall drain ordered emissions, select the first latched
|
|
1059
|
+
non-abort control error before considering abort, and clear its boundary
|
|
1060
|
+
latches in `finally`, so a failed resume cannot leak an emission error into a
|
|
1061
|
+
later turn.
|
|
1062
|
+
A resume is not a Boss-input turn and shall emit neither
|
|
1063
|
+
`boss.input.received` nor `boss.input.settled`; the structured result is the
|
|
1064
|
+
method return. Reusing the originating turn id on the child finish and
|
|
1065
|
+
continuation emissions does not create a second Boss trace pair.
|
|
1066
|
+
This quiescence and drain path is mandatory even when
|
|
1067
|
+
`nestedBridge.resume(...)` rejects: capture that operation error, allow the
|
|
1068
|
+
promise actor's `onError` transition to settle, and select the first latched
|
|
1069
|
+
control error only after all ordered emissions have drained.
|
|
1070
|
+
- In `dispose`, capture the final public state and stop the root actor before
|
|
1071
|
+
settling or aborting a suspended nested bridge, so the bridge rejection
|
|
1072
|
+
cannot reenter the FSM and start new actor work during disposal. Then drain
|
|
1073
|
+
pending port emissions and every in-flight Captain/player/judge/child opening, emit
|
|
1074
|
+
`session.disposed` with the final descriptor, and discard player resume
|
|
1075
|
+
tokens. Host child abort cleanup and child `session.disposed` shall drain
|
|
1076
|
+
before the parent call finish, which shall drain before parent
|
|
1077
|
+
`session.disposed`. Use cleanup/finally structure so a bridge or emission
|
|
1078
|
+
failure cannot skip the parent disposal boundary or leave the runtime bound.
|
|
226
1079
|
|
|
227
1080
|
The actor's `lastError` field shall be surfaced via `emitStatus` when the machine enters its `failed` state.
|
|
1081
|
+
For the default Captain runtime, an initial `ready` state and a terminal `done`
|
|
1082
|
+
state shall not emit human status. The terminal response is already visible
|
|
1083
|
+
Captain prose; a synthetic “entered done” message would present it twice.
|
|
1084
|
+
Structured transition trace and telemetry still apply to both states.
|
|
1085
|
+
Every provided actor boundary shall first drain the queued state-entry
|
|
1086
|
+
transition/status/telemetry caused by entering its working leaf, so a call's
|
|
1087
|
+
`*.started` trace cannot overtake the transition that explains it. Every public
|
|
1088
|
+
runtime method shall drain that queue before it resolves or rejects.
|
|
1089
|
+
This initial `await drain()` is required inside each provided `fromPromise`
|
|
1090
|
+
body: XState may begin that body before publishing the root snapshot, and the
|
|
1091
|
+
await yields so the synchronous inspection callback can enqueue the entering
|
|
1092
|
+
transition first.
|
|
1093
|
+
|
|
1094
|
+
If a `*.call.started` trace records and then its sink rejects, no host call may
|
|
1095
|
+
begin. The runtime shall still enqueue exactly one synthetic paired
|
|
1096
|
+
`*.call.finished` trace with `status: 'error'`, preserving the original call
|
|
1097
|
+
id, turn id, actor visibility, state/source identity, and prompt or request
|
|
1098
|
+
metadata from the start boundary. It shall then follow the same latched
|
|
1099
|
+
control-error, FSM settlement, and ordered-drain path as any other call-start
|
|
1100
|
+
failure; the synthetic finish must not replace the original sink error.
|
|
1101
|
+
|
|
1102
|
+
## Parked-session snapshot (optional)
|
|
1103
|
+
|
|
1104
|
+
A linked runtime may implement the optional durable-session capability of
|
|
1105
|
+
`@sublang/playbook/runtime` — `exportSnapshot()` and
|
|
1106
|
+
`restore(session, snapshot)` — so a host can persist a parked session and
|
|
1107
|
+
rehydrate it in a later process (DR-014). A runtime that implements either
|
|
1108
|
+
member shall implement both. When generated for a runtime whose host needs
|
|
1109
|
+
durability, the pair shall behave as follows.
|
|
1110
|
+
|
|
1111
|
+
`exportSnapshot()` shall return `undefined` unless the runtime is at a safe
|
|
1112
|
+
capture point: initialized, not disposing or disposed, no active
|
|
1113
|
+
`handleBossInput`/`resumePlaybookCall` boundary, no pending nested playbook
|
|
1114
|
+
call, and the root actor at a quiescent state with actor status `active`.
|
|
1115
|
+
At a safe capture point it shall return a JSON-safe
|
|
1116
|
+
`PlaybookRuntimeSnapshot` carrying:
|
|
1117
|
+
|
|
1118
|
+
- `schemaVersion`: literal `1`.
|
|
1119
|
+
- `playbookId`: the bound session's playbook id.
|
|
1120
|
+
- `machine`: the root actor's `getPersistedSnapshot()` result, passed
|
|
1121
|
+
through the shared JSON detachment with any raw `Error` context value
|
|
1122
|
+
(for example FSM `lastError`) normalized to `{ name, message, stack? }`
|
|
1123
|
+
first. The value is opaque to hosts.
|
|
1124
|
+
- `playerResumeTokens`: the resume-token map as a plain object
|
|
1125
|
+
(§PlaybookPorts contract).
|
|
1126
|
+
- `sequences`: the live `trace`, `turn`, `judgeCall`, `playerCall`, and
|
|
1127
|
+
`playbookCall` counters, plus `captainCall` when the runtime supports direct
|
|
1128
|
+
Captain calls. `captainCall` remains optional under schema version `1` for
|
|
1129
|
+
backward compatibility; a direct-Captain-capable runtime shall persist it.
|
|
1130
|
+
- `state`: the current normalized state descriptor.
|
|
1131
|
+
- `pendingBossQuestions`: the pending Boss question(s) from FSM context as
|
|
1132
|
+
a list of `{ questionId, player, question, sourceItem? }`, empty when the
|
|
1133
|
+
parked state awaits no reply. This list exists so hosts can surface the
|
|
1134
|
+
question without parsing status lines or telemetry.
|
|
1135
|
+
|
|
1136
|
+
`restore(session, snapshot)` is an alternative to `init` under the same
|
|
1137
|
+
lifecycle guards (§Session lifecycle): it shall reject when already
|
|
1138
|
+
initialized, disposing, or disposed, and shall validate
|
|
1139
|
+
`snapshot.schemaVersion` and that `snapshot.playbookId` equals
|
|
1140
|
+
`session.playbookId` before touching state.
|
|
1141
|
+
The host supplies the same immutable `PlaybookSession` identity the
|
|
1142
|
+
snapshot was exported under and recreates the runtime through the same
|
|
1143
|
+
factory with equivalent options; the runtime does not diff options, and
|
|
1144
|
+
module identity — that the factory constructing this runtime still
|
|
1145
|
+
belongs to the snapshot's playbook — is likewise the host's check to
|
|
1146
|
+
make before calling `restore`.
|
|
1147
|
+
`restore` shall bind the session, restore the resume-token map, the
|
|
1148
|
+
sequence counters (using the persisted global `trace` counter as a
|
|
1149
|
+
collision-safe floor for an absent legacy `captainCall`), and the
|
|
1150
|
+
prior-state descriptor from the snapshot,
|
|
1151
|
+
construct the actor with the persisted `machine` snapshot, and start it
|
|
1152
|
+
with root inspection emissions suppressed so rehydration emits no
|
|
1153
|
+
`session.started` trace, no transition trace, and no human status — the
|
|
1154
|
+
session already started, and the next public boundary continues the
|
|
1155
|
+
contiguous trace sequence. After start, a restored actor whose status is
|
|
1156
|
+
not `active` or whose state descriptor cannot be normalized shall fail
|
|
1157
|
+
`restore` through the same failed-start cleanup path as `init`.
|
|
1158
|
+
A restore failure shall leave the runtime unbound so `dispose` remains
|
|
1159
|
+
callable and terminal.
|
|
228
1160
|
|
|
229
1161
|
## Abort
|
|
230
1162
|
|
|
231
1163
|
`handleBossInput.signal` is the abort surface.
|
|
232
|
-
The runtime shall honor it at every `callPlayer`/`callJudge` and
|
|
233
|
-
|
|
1164
|
+
The runtime shall honor it at every `callPlayer`/`callCaptain`/`callJudge` and
|
|
1165
|
+
at every poll between transitions.
|
|
1166
|
+
Each provided Captain, player, judge, or nested-playbook boundary shall receive
|
|
1167
|
+
a signal combined from its XState invocation-lifetime signal and the currently
|
|
1168
|
+
active `handleBossInput` or `resumePlaybookCall` signal (for example with
|
|
1169
|
+
the shared `combineAbortSignals`). Classify a rejection as cancellation by its
|
|
1170
|
+
causal identity with the applicable signal reason, not by an `AbortError` name
|
|
1171
|
+
or by observing only that the signal is also aborted. Signals may carry an
|
|
1172
|
+
ordinary `Error`, while a distinct transport or sink failure that occurs after
|
|
1173
|
+
abort remains a non-abort control error and takes precedence. On abort, the
|
|
1174
|
+
runtime shall not merely race the imperative
|
|
1175
|
+
wait and return while an invocation remains live: it shall let the selected
|
|
1176
|
+
rejection path settle and drive the actor to a quiescent state before returning
|
|
1177
|
+
from the turn. No trace, status, state, or call completion caused by that turn
|
|
1178
|
+
may appear after the public method returns.
|
|
234
1179
|
Three strategies are permitted; the linker selects per FSM:
|
|
235
1180
|
|
|
236
|
-
- **Natural rejection** — the runtime's Captain actor (e.g.,
|
|
1181
|
+
- **Natural rejection** — the runtime's Captain or player actor (e.g.,
|
|
237
1182
|
`fromPromise`) ends the invocation by rejecting, and the FSM routes
|
|
238
1183
|
the rejection through `onError` to a quiescent sink. The cancelled
|
|
239
|
-
port call may
|
|
240
|
-
`PlayerResult
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
the
|
|
1184
|
+
port call may _itself_ reject, or it may resolve with
|
|
1185
|
+
`PlayerResult` or `CaptainResult` with
|
|
1186
|
+
`{ status: 'aborted' | 'error' }` that the runtime
|
|
1187
|
+
inspects and converts into an actor rejection. Either shape
|
|
1188
|
+
is permitted — the contract is on the actor boundary, not on
|
|
1189
|
+
the port's promise behavior. Preferred when every Captain- or player-invoking
|
|
244
1190
|
state's `onError` lands somewhere quiescent; the FSM's own error
|
|
245
1191
|
wiring is the abort path.
|
|
246
1192
|
- **Synthetic pre-emption to a quiescent target** — send the FSM's
|
|
@@ -249,7 +1195,7 @@ Three strategies are permitted; the linker selects per FSM:
|
|
|
249
1195
|
The runtime shall not pick the active state as the target:
|
|
250
1196
|
`gears2fsm.md` prescribes `reenter: true` for `bossInterrupts`, so
|
|
251
1197
|
re-entering the active state restarts its `invoke` and spawns a
|
|
252
|
-
fresh
|
|
1198
|
+
fresh agent call.
|
|
253
1199
|
- **Programmatic stop** — `actor.stop()` and report the turn as aborted
|
|
254
1200
|
via `emitStatus`. Reserved for FSMs with neither `onError` wiring nor
|
|
255
1201
|
a pre-emption event.
|
|
@@ -266,33 +1212,154 @@ The runtime shall emit, at minimum:
|
|
|
266
1212
|
default is to emit on every transition and let the host filter; hosts
|
|
267
1213
|
may bind a stricter rule.
|
|
268
1214
|
- One `emitTelemetry` per state transition under a namespaced topic
|
|
269
|
-
(recommended `playbook.fsm.state`), with
|
|
1215
|
+
(recommended `playbook.fsm.state`), with structured `from`, `to`, `event`,
|
|
1216
|
+
`previousState`, and `state` fields. Descriptors carry the JSON-safe XState
|
|
1217
|
+
value, active stable ids from public state metadata, tags, status, and
|
|
1218
|
+
quiescence; they do not inspect private XState nodes.
|
|
1219
|
+
The payload shall additionally carry the exact pending Boss question or
|
|
1220
|
+
keyed questions selected from public snapshot context and normalize any
|
|
1221
|
+
transition error without retaining a raw `Error` instance.
|
|
1222
|
+
Do not reduce this payload to the current state: `from` and `previousState`
|
|
1223
|
+
are the authoritative prior descriptor, while `to` and `state` are the new
|
|
1224
|
+
descriptor. On the first observed transition, use the initialized state as
|
|
1225
|
+
both the prior and new descriptor when no earlier transition exists.
|
|
1226
|
+
Snapshot and recursively freeze the complete described telemetry payload
|
|
1227
|
+
independently from the state retained as `previousState`, so an observer
|
|
1228
|
+
cannot mutate a later transition's authoritative `from` state.
|
|
270
1229
|
Observers consume telemetry; the runtime never interprets the topic.
|
|
271
1230
|
|
|
272
|
-
Player prompts and adjudicator JSON ride the host's own record channels when the host has them (cligent's `captain_*` / `player_*`).
|
|
273
|
-
The runtime
|
|
1231
|
+
Player prompts and adjudicator JSON may additionally ride the host's own record channels when the host has them (cligent's `captain_*` / `player_*`).
|
|
1232
|
+
The `playbook.trace` copies are the host-agnostic runtime-boundary record required by §Playbook trace.
|
|
274
1233
|
|
|
275
1234
|
## Output
|
|
276
1235
|
|
|
277
|
-
The link compiler emits **one** TypeScript module
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
1236
|
+
The link compiler emits **one thin** TypeScript module per playbook.
|
|
1237
|
+
The FSM-interpreter machinery — actor wiring, boundary tracing, Boss-event
|
|
1238
|
+
mapping, adjudication, script execution, nested-playbook bridging, session
|
|
1239
|
+
lifecycle, abort handling, and the optional parked-session snapshot
|
|
1240
|
+
capability — is not regenerated per artifact: it ships once as the shared
|
|
1241
|
+
`createXStatePlaybookRuntime(machine, spec)` factory exported by
|
|
1242
|
+
`@sublang/playbook/xstate-runtime`, and the emitted module hands its FSM and
|
|
1243
|
+
a small per-playbook `spec` to that factory. Every behavioral section of
|
|
1244
|
+
this definition still binds the emitted module's runtime; the shared factory
|
|
1245
|
+
is how the emitted module satisfies them, so a runtime fix ships as a
|
|
1246
|
+
package release instead of a re-link of every artifact.
|
|
1247
|
+
|
|
1248
|
+
The emitted module:
|
|
1249
|
+
|
|
1250
|
+
- Imports the FSM artifact by relative path with an extension-bearing
|
|
1251
|
+
runtime specifier. When the linked TypeScript is part of a package that
|
|
1252
|
+
compiles and ships JavaScript siblings, the source shall use the
|
|
1253
|
+
NodeNext-compatible `.js` specifier (for example `./code.fsm.js`), never a
|
|
1254
|
+
`.ts` specifier that the package's supported Node versions cannot load.
|
|
1255
|
+
An explicitly source-only host may instead retain `.ts` only when that
|
|
1256
|
+
host supports direct TypeScript loading and no JavaScript build is shipped.
|
|
1257
|
+
- Restricts itself to erasable TypeScript syntax — type annotations
|
|
1258
|
+
that strip cleanly, no constructor parameter properties, `enum`s, or
|
|
1259
|
+
namespaces — so a host running under type stripping loads it
|
|
1260
|
+
directly.
|
|
1261
|
+
- Imports `createXStatePlaybookRuntime` (plus any shared strategy defaults
|
|
1262
|
+
its `_internal` surface re-exports) from the shared engine module through
|
|
1263
|
+
its bare package specifier `@sublang/playbook/xstate-runtime`, and the
|
|
1264
|
+
contract types through `@sublang/playbook/runtime`. It shall not copy,
|
|
1265
|
+
inline, or re-derive interpreter machinery — actor bridges, trace
|
|
1266
|
+
emission, judge-JSON recovery, lifecycle guards — beside the factory
|
|
1267
|
+
call, and shall not import `xstate`, `p-queue`, or `node:child_process`
|
|
1268
|
+
itself; those are the shared engine's dependencies.
|
|
1269
|
+
- Declares and exports the typed `PlaybookRuntimeOptions` interface for that
|
|
1270
|
+
playbook, derived from every required FSM input field that is not supplied
|
|
1271
|
+
by `PlaybookSession` or another linker-owned source (§PlaybookRuntime
|
|
1272
|
+
contract), plus the optional `cwd` option whenever the FSM contains a
|
|
1273
|
+
`script` state (§Script execution).
|
|
1274
|
+
- Supplies the spec's `snapshotOptions` with the same options-validation
|
|
1275
|
+
semantics previously generated inline: validate and JSON-snapshot the
|
|
1276
|
+
caller's options, rejecting undeclared keys and non-conforming values, so
|
|
1277
|
+
the factory binds an immutable options record before constructing any
|
|
1278
|
+
actor.
|
|
1279
|
+
- Supplies in `spec` only what the factory cannot read from the FSM
|
|
1280
|
+
artifact's own data: the deterministic textual entry event where
|
|
1281
|
+
§Boss-event mapping prescribes deterministic entry; compact `bossEvents`
|
|
1282
|
+
metadata for each additional Boss-union arm whose exact required/optional
|
|
1283
|
+
judge fields, runtime-owned text fields, or closed string values disappear
|
|
1284
|
+
under TypeScript erasure; `placeholderFields` only for authored token/field
|
|
1285
|
+
exceptions not covered by the canonical kebab-token-to-camel-field mapping
|
|
1286
|
+
and the canonical `<#>` → `irNumber` special case; the
|
|
1287
|
+
transition-event payload fields the FSM's Boss union declares; a
|
|
1288
|
+
non-default player binding where the linker inputs supplied one; and any
|
|
1289
|
+
per-playbook strategy override (classifier, prompt composers,
|
|
1290
|
+
required-field extraction, status formatting) an earlier section of this
|
|
1291
|
+
definition requires for that playbook. The metadata shall keep the shared
|
|
1292
|
+
classifier's reply contract exactly flat `{ type, ...declaredFields }` and
|
|
1293
|
+
distinguish judge-authored routing fields from exact-text fields the
|
|
1294
|
+
runtime attaches itself. Everything else — player/script/captain/nested actor
|
|
1295
|
+
provisioning, prompt composition, classification, adjudication, statuses,
|
|
1296
|
+
resumable-state derivation — comes from the factory's generic defaults,
|
|
1297
|
+
which implement the behavioral sections of this definition.
|
|
1298
|
+
|
|
1299
|
+
```ts
|
|
1300
|
+
interface XStateBossEventFieldSpec {
|
|
1301
|
+
source: 'judge' | 'text';
|
|
1302
|
+
required?: boolean;
|
|
1303
|
+
values?: readonly string[];
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
interface XStateBossEventSpec {
|
|
1307
|
+
type: string;
|
|
1308
|
+
fields?: Readonly<Record<string, XStateBossEventFieldSpec>>;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
bossEvents?: readonly XStateBossEventSpec[];
|
|
1312
|
+
placeholderFields?: Readonly<Record<string, string>>;
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
Supplied `bossEvents` metadata shall merge with, and shall not replace or
|
|
1316
|
+
weaken, runtime-derived entry text ownership or closed interrupt targets.
|
|
1317
|
+
A conflicting duplicate field contract is a linker/runtime construction
|
|
1318
|
+
error.
|
|
1319
|
+
- Default-exports the factory call as `createPlaybookRuntime`, typed
|
|
1320
|
+
`PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
|
|
1321
|
+
- Exposes, under an `_internal` export, the pure helpers verification
|
|
1322
|
+
needs — at least the player-prompt and Captain-prompt composers
|
|
1323
|
+
(`composePlayerPrompt` and `composeCaptainPrompt`), which may re-export
|
|
1324
|
+
the shared defaults when the spec does not override composition — so
|
|
1325
|
+
compilation-correctness tests can exercise composition without a host.
|
|
284
1326
|
- Holds no host-specific types and no host primitive calls. The runtime
|
|
285
|
-
speaks only `PlaybookPorts
|
|
1327
|
+
speaks only `PlaybookPorts` for every agent and host concern; the
|
|
1328
|
+
`node:child_process` dependency of §Script execution lives in the shared
|
|
1329
|
+
factory, not in the emitted module.
|
|
286
1330
|
- Records the linker inputs (FSM path, player binding, strategies) in a
|
|
287
1331
|
top-of-file header comment so the file is reproducible from the same
|
|
288
1332
|
inputs.
|
|
289
|
-
- Sources the contract types (`PlayerResult`, `
|
|
290
|
-
`
|
|
1333
|
+
- Sources the contract types (`PlayerResult`, `PlayerCallOptions`,
|
|
1334
|
+
`CaptainResult`, `CaptainCallOptions`, `PlaybookPorts`, `PlaybookSession`,
|
|
1335
|
+
`PlaybookTraceEvent`,
|
|
1336
|
+
`PlaybookCallRequest`, `PlaybookCallResult`, `PlaybookCallStart`,
|
|
1337
|
+
`PlaybookStateValue`, `PlaybookState`, `PlaybookRunResult`,
|
|
1338
|
+
`PlaybookRuntime`, `PlaybookRuntimeFactory`) from the single shared
|
|
291
1339
|
type-only module instead of redefining them, and re-exports the names
|
|
292
1340
|
its consumers import, so every linked playbook shares one contract
|
|
293
|
-
definition. The shared
|
|
1341
|
+
definition. The shared modules import no FSM or host types, so the
|
|
294
1342
|
dependency runs one way — from each linked module to the shared
|
|
295
|
-
contract, never the reverse.
|
|
1343
|
+
engine and contract, never the reverse.
|
|
1344
|
+
|
|
1345
|
+
When a co-located integration test for the linked runtime already exists, the
|
|
1346
|
+
link compiler shall run it before reporting success and treat any failure as a
|
|
1347
|
+
generation failure. It shall not delete, skip, or weaken that suite to make a
|
|
1348
|
+
new artifact pass; the suite is executable evidence for lifecycle, ordering,
|
|
1349
|
+
error-propagation, and host-boundary requirements that static artifact checks
|
|
1350
|
+
cannot establish.
|
|
1351
|
+
|
|
1352
|
+
Internal trace/status helpers may accept `unknown`, validate it with the same
|
|
1353
|
+
JSON-safety rules as the public boundary, and only then emit a `JsonValue`.
|
|
1354
|
+
They shall not require nominally typed public interfaces such as
|
|
1355
|
+
`NormalizedError`, `PlaybookState`, `PlaybookCallRequest`, or
|
|
1356
|
+
`PlaybookCallResult` to satisfy a `JsonValue` index signature at compile time,
|
|
1357
|
+
and they shall not silence that mismatch with an unchecked cast.
|
|
1358
|
+
Prompt placeholder substitution shall make one callback-based pass over the
|
|
1359
|
+
original template. Replacement strings are literal: placeholder-looking text
|
|
1360
|
+
inside Boss/catalog/plan/result values and JavaScript replacement tokens such
|
|
1361
|
+
as `$&`, `$$`, dollar-backtick, and `$'` shall not be interpreted or
|
|
1362
|
+
substituted again.
|
|
296
1363
|
|
|
297
1364
|
## Host adaptation (informative, not normative)
|
|
298
1365
|
|
|
@@ -304,14 +1371,18 @@ A host integrates with playbooks via a small adapter that:
|
|
|
304
1371
|
2. Imports the module and constructs the runtime with options forwarded
|
|
305
1372
|
verbatim from the host config.
|
|
306
1373
|
3. Implements `PlaybookPorts` by wrapping the host's own primitives —
|
|
307
|
-
for cligent/tmux-play this is `callPlayer ← context.callPlayer`,
|
|
308
|
-
`
|
|
309
|
-
`
|
|
310
|
-
|
|
311
|
-
|
|
1374
|
+
for cligent/tmux-play this is `callPlayer ← context.callPlayer`, visible
|
|
1375
|
+
`callCaptain ← context.callCaptain`, hidden
|
|
1376
|
+
`callJudge ← context.callCaptain`, nested `callPlaybook ←` the Captain
|
|
1377
|
+
session stack, and `emitStatus`/`emitTelemetry` ←
|
|
1378
|
+
`session.emitStatus`/`session.emitTelemetry`. The two Captain-backed ports
|
|
1379
|
+
share one abort-aware concurrency-one queue.
|
|
1380
|
+
4. Generates a unique playbook-session id, calls
|
|
1381
|
+
`runtime.init({ sessionId, rootSessionId: sessionId, depth: 0,
|
|
1382
|
+
playbookId, ports })` once at session start, forwards each Boss turn to
|
|
1383
|
+
`runtime.handleBossInput`, and calls
|
|
312
1384
|
`runtime.dispose()` at session end.
|
|
313
1385
|
|
|
314
|
-
The adapter is ~30 lines regardless of which playbook is loaded.
|
|
315
1386
|
Its location is a project-organization choice:
|
|
316
1387
|
|
|
317
1388
|
- **Playbook repo** — simplest when the playbook author owns the integration; keeps host primitives a lower-layer dependency.
|
|
@@ -328,9 +1399,13 @@ This spec is silent on the choice; the contract is the same in any location.
|
|
|
328
1399
|
layouts — where these live is a per-project decision (see
|
|
329
1400
|
§Host adaptation); this spec only constrains the `PlaybookPorts`
|
|
330
1401
|
contract they satisfy.
|
|
331
|
-
-
|
|
332
|
-
|
|
333
|
-
changing this spec.
|
|
1402
|
+
- Trace persistence, multiple Boss-selected root engagements, recursive
|
|
1403
|
+
playbook calls, multi-Boss orchestration, or visualizer rendering —
|
|
1404
|
+
separate hosts/observers may add them without changing this spec. A host
|
|
1405
|
+
may persist the emitted trace, but the runtime does not rehydrate a
|
|
1406
|
+
disposed actor from it. Parked-session durability is in scope only
|
|
1407
|
+
through the optional snapshot surface of §Parked-session snapshot
|
|
1408
|
+
(DR-014); everything beyond it remains out of scope.
|
|
334
1409
|
|
|
335
1410
|
New behavior in any of these areas requires a separate slc spec.
|
|
336
1411
|
|
|
@@ -339,3 +1414,4 @@ New behavior in any of these areas requires a separate slc spec.
|
|
|
339
1414
|
[1]: [text2gears](text2gears.md) "First phase: text → GEARS spec items."
|
|
340
1415
|
[2]: [gears2fsm](gears2fsm.md) "Second phase: GEARS items → FSM artifact."
|
|
341
1416
|
[3]: https://stately.ai/docs/actors "XState actors — `createActor`, snapshots, abort signal handling."
|
|
1417
|
+
[4]: https://github.com/sindresorhus/p-queue#readme "p-queue concurrency and AbortSignal support."
|