@sublang/playbook 12.2.2 → 12.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/package.json +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +11 -7
- package/reference/sdlc/code.playbook/bin/session-store.js +16 -5
- package/reference/sdlc/code.playbook/code.fsm.js +43 -5
- package/reference/sdlc/code.playbook/code.fsm.ts +66 -9
- package/reference/sdlc/code.playbook/playbook-captain.js +22 -5
- package/reference/sdlc/code.playbook/playbook-captain.ts +25 -5
- package/reference/sdlc/decide.playbook/decide.fsm.js +4 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +4 -0
- package/reference/sdlc/decide.playbook/decide.playbook.js +23 -5
- package/reference/sdlc/decide.playbook/decide.playbook.ts +25 -4
- package/reference/sdlc/dev.playbook/dev.fsm.js +57 -7
- package/reference/sdlc/dev.playbook/dev.fsm.ts +80 -11
- package/reference/sdlc/review.playbook/review.fsm.js +11 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +15 -1
- package/slc/gears2fsm.md +21 -3
- package/slc/link.md +21 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +13 -0
- package/src/xstate-playbook-runtime.js +63 -0
- package/src/xstate-playbook-runtime.ts +73 -0
- package/src/xstate-runtime.js +35 -4
- package/src/xstate-runtime.ts +49 -4
|
@@ -47,6 +47,18 @@ function playbookMeta(stateId, role) {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
51
|
+
// workflow succeeded or failed, so DEV's own caller learns that from the
|
|
52
|
+
// machine rather than from DEV's output fields.
|
|
53
|
+
function terminalMeta(stateId, terminal) {
|
|
54
|
+
return {
|
|
55
|
+
playbook: {
|
|
56
|
+
stateId,
|
|
57
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
58
|
+
terminal,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
50
62
|
function isRecord(value) {
|
|
51
63
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
52
64
|
return false;
|
|
@@ -172,6 +184,16 @@ function nestedResultFromError(error) {
|
|
|
172
184
|
const result = error.result;
|
|
173
185
|
return isRecord(result) ? result : undefined;
|
|
174
186
|
}
|
|
187
|
+
function isFailureTerminalRecord(value) {
|
|
188
|
+
if (!isRecord(value))
|
|
189
|
+
return false;
|
|
190
|
+
const allowed = new Set(['stateId', 'kind', 'description']);
|
|
191
|
+
return (Reflect.ownKeys(value).every((key) => typeof key === 'string' && allowed.has(key)) &&
|
|
192
|
+
isNonEmptyString(value.stateId) &&
|
|
193
|
+
value.kind === 'failure' &&
|
|
194
|
+
(!Object.prototype.hasOwnProperty.call(value, 'description') ||
|
|
195
|
+
typeof value.description === 'string'));
|
|
196
|
+
}
|
|
175
197
|
function normalizedChildFailure(error, playbookId) {
|
|
176
198
|
const result = nestedResultFromError(error);
|
|
177
199
|
if (result === undefined)
|
|
@@ -181,10 +203,12 @@ function normalizedChildFailure(error, playbookId) {
|
|
|
181
203
|
'playbookId',
|
|
182
204
|
'childSessionId',
|
|
183
205
|
'state',
|
|
184
|
-
'error',
|
|
206
|
+
...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
|
|
185
207
|
]);
|
|
186
208
|
if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key)) ||
|
|
187
|
-
(result.status !== 'aborted' &&
|
|
209
|
+
(result.status !== 'aborted' &&
|
|
210
|
+
result.status !== 'error' &&
|
|
211
|
+
result.status !== 'ok') ||
|
|
188
212
|
result.playbookId !== playbookId) {
|
|
189
213
|
return undefined;
|
|
190
214
|
}
|
|
@@ -192,6 +216,21 @@ function normalizedChildFailure(error, playbookId) {
|
|
|
192
216
|
!isNonEmptyString(result.childSessionId)) {
|
|
193
217
|
return undefined;
|
|
194
218
|
}
|
|
219
|
+
if (result.status === 'ok') {
|
|
220
|
+
if (!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
|
|
221
|
+
!isFailureTerminalRecord(result.terminal) ||
|
|
222
|
+
!isNonEmptyString(result.childSessionId) ||
|
|
223
|
+
(Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
224
|
+
!isPlaybookState(result.state))) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
status: 'ok',
|
|
229
|
+
...(result.output === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: { output: result.output }),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
195
234
|
if (Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
196
235
|
!isPlaybookState(result.state)) {
|
|
197
236
|
return undefined;
|
|
@@ -233,8 +272,9 @@ function authoredChildFailureGuard(playbookId) {
|
|
|
233
272
|
function authoredChildError(event, playbookId) {
|
|
234
273
|
const outer = isRecord(event) ? event.error : undefined;
|
|
235
274
|
const failure = normalizedChildFailure(outer, playbookId);
|
|
236
|
-
if (failure
|
|
275
|
+
if (failure !== undefined && failure.status !== 'ok' && failure.error) {
|
|
237
276
|
return failure.error;
|
|
277
|
+
}
|
|
238
278
|
if (failure?.status === 'aborted') {
|
|
239
279
|
return {
|
|
240
280
|
name: 'AbortError',
|
|
@@ -249,9 +289,19 @@ function authoredChildError(event, playbookId) {
|
|
|
249
289
|
function relayedChildFailure(event, playbookId) {
|
|
250
290
|
const outer = isRecord(event) ? event.error : undefined;
|
|
251
291
|
const failure = normalizedChildFailure(outer, playbookId);
|
|
292
|
+
// DR-048: a completed child that reached an authored failure terminal
|
|
293
|
+
// relays its own output, exactly as a terminal result that did not prove
|
|
294
|
+
// success did before it was routed through the error path.
|
|
295
|
+
if (failure?.status === 'ok') {
|
|
296
|
+
return {
|
|
297
|
+
playbookId,
|
|
298
|
+
status: 'ok',
|
|
299
|
+
...(failure.output === undefined ? {} : { output: failure.output }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
252
302
|
return {
|
|
253
303
|
playbookId,
|
|
254
|
-
status: failure?.status
|
|
304
|
+
status: failure?.status === 'aborted' ? 'aborted' : 'error',
|
|
255
305
|
error: authoredChildError(event, playbookId),
|
|
256
306
|
};
|
|
257
307
|
}
|
|
@@ -703,19 +753,19 @@ export const devMachine = machineSetup.createMachine({
|
|
|
703
753
|
discussionComplete: {
|
|
704
754
|
id: 'discussionComplete',
|
|
705
755
|
description: STATE_DESCRIPTIONS.discussionComplete,
|
|
706
|
-
meta:
|
|
756
|
+
meta: terminalMeta('discussionComplete', 'success'),
|
|
707
757
|
type: 'final',
|
|
708
758
|
},
|
|
709
759
|
done: {
|
|
710
760
|
id: 'done',
|
|
711
761
|
description: STATE_DESCRIPTIONS.done,
|
|
712
|
-
meta:
|
|
762
|
+
meta: terminalMeta('done', 'success'),
|
|
713
763
|
type: 'final',
|
|
714
764
|
},
|
|
715
765
|
reportedChildFailure: {
|
|
716
766
|
id: 'reportedChildFailure',
|
|
717
767
|
description: STATE_DESCRIPTIONS.reportedChildFailure,
|
|
718
|
-
meta:
|
|
768
|
+
meta: terminalMeta('reportedChildFailure', 'failure'),
|
|
719
769
|
type: 'final',
|
|
720
770
|
},
|
|
721
771
|
},
|
|
@@ -197,6 +197,22 @@ function playbookMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
201
|
+
// workflow succeeded or failed, so DEV's own caller learns that from the
|
|
202
|
+
// machine rather than from DEV's output fields.
|
|
203
|
+
function terminalMeta<StateId extends keyof typeof STATE_DESCRIPTIONS>(
|
|
204
|
+
stateId: StateId,
|
|
205
|
+
terminal: 'success' | 'failure',
|
|
206
|
+
) {
|
|
207
|
+
return {
|
|
208
|
+
playbook: {
|
|
209
|
+
stateId,
|
|
210
|
+
description: STATE_DESCRIPTIONS[stateId],
|
|
211
|
+
terminal,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
201
217
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
202
218
|
return false;
|
|
@@ -367,10 +383,32 @@ function nestedResultFromError(
|
|
|
367
383
|
return isRecord(result) ? result : undefined;
|
|
368
384
|
}
|
|
369
385
|
|
|
370
|
-
type AuthoredChildFailure =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
386
|
+
type AuthoredChildFailure =
|
|
387
|
+
| {
|
|
388
|
+
readonly status: 'aborted' | 'error';
|
|
389
|
+
readonly error?: CompactError;
|
|
390
|
+
}
|
|
391
|
+
// DR-048: a child that completed at an authored failure terminal. The
|
|
392
|
+
// bridge rejects it through the same error path, so DEV recognizes the
|
|
393
|
+
// failure from the child's own machine and never from its output fields.
|
|
394
|
+
| {
|
|
395
|
+
readonly status: 'ok';
|
|
396
|
+
readonly output?: JsonValue;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
function isFailureTerminalRecord(value: unknown): boolean {
|
|
400
|
+
if (!isRecord(value)) return false;
|
|
401
|
+
const allowed = new Set(['stateId', 'kind', 'description']);
|
|
402
|
+
return (
|
|
403
|
+
Reflect.ownKeys(value).every(
|
|
404
|
+
(key) => typeof key === 'string' && allowed.has(key),
|
|
405
|
+
) &&
|
|
406
|
+
isNonEmptyString(value.stateId) &&
|
|
407
|
+
value.kind === 'failure' &&
|
|
408
|
+
(!Object.prototype.hasOwnProperty.call(value, 'description') ||
|
|
409
|
+
typeof value.description === 'string')
|
|
410
|
+
);
|
|
411
|
+
}
|
|
374
412
|
|
|
375
413
|
function normalizedChildFailure(
|
|
376
414
|
error: unknown,
|
|
@@ -383,13 +421,15 @@ function normalizedChildFailure(
|
|
|
383
421
|
'playbookId',
|
|
384
422
|
'childSessionId',
|
|
385
423
|
'state',
|
|
386
|
-
'error',
|
|
424
|
+
...(result.status === 'ok' ? ['output', 'terminal'] : ['error']),
|
|
387
425
|
]);
|
|
388
426
|
if (
|
|
389
427
|
Reflect.ownKeys(result).some(
|
|
390
428
|
(key) => typeof key !== 'string' || !allowed.has(key),
|
|
391
429
|
) ||
|
|
392
|
-
(result.status !== 'aborted' &&
|
|
430
|
+
(result.status !== 'aborted' &&
|
|
431
|
+
result.status !== 'error' &&
|
|
432
|
+
result.status !== 'ok') ||
|
|
393
433
|
result.playbookId !== playbookId
|
|
394
434
|
) {
|
|
395
435
|
return undefined;
|
|
@@ -400,6 +440,23 @@ function normalizedChildFailure(
|
|
|
400
440
|
) {
|
|
401
441
|
return undefined;
|
|
402
442
|
}
|
|
443
|
+
if (result.status === 'ok') {
|
|
444
|
+
if (
|
|
445
|
+
!Object.prototype.hasOwnProperty.call(result, 'terminal') ||
|
|
446
|
+
!isFailureTerminalRecord(result.terminal) ||
|
|
447
|
+
!isNonEmptyString(result.childSessionId) ||
|
|
448
|
+
(Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
449
|
+
!isPlaybookState(result.state))
|
|
450
|
+
) {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
status: 'ok',
|
|
455
|
+
...(result.output === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: { output: result.output as JsonValue }),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
403
460
|
if (
|
|
404
461
|
Object.prototype.hasOwnProperty.call(result, 'state') &&
|
|
405
462
|
!isPlaybookState(result.state)
|
|
@@ -453,7 +510,9 @@ function authoredChildError(
|
|
|
453
510
|
): CompactError {
|
|
454
511
|
const outer = isRecord(event) ? event.error : undefined;
|
|
455
512
|
const failure = normalizedChildFailure(outer, playbookId);
|
|
456
|
-
if (failure
|
|
513
|
+
if (failure !== undefined && failure.status !== 'ok' && failure.error) {
|
|
514
|
+
return failure.error;
|
|
515
|
+
}
|
|
457
516
|
if (failure?.status === 'aborted') {
|
|
458
517
|
return {
|
|
459
518
|
name: 'AbortError',
|
|
@@ -472,9 +531,19 @@ function relayedChildFailure(
|
|
|
472
531
|
): CompletedChildResult {
|
|
473
532
|
const outer = isRecord(event) ? event.error : undefined;
|
|
474
533
|
const failure = normalizedChildFailure(outer, playbookId);
|
|
534
|
+
// DR-048: a completed child that reached an authored failure terminal
|
|
535
|
+
// relays its own output, exactly as a terminal result that did not prove
|
|
536
|
+
// success did before it was routed through the error path.
|
|
537
|
+
if (failure?.status === 'ok') {
|
|
538
|
+
return {
|
|
539
|
+
playbookId,
|
|
540
|
+
status: 'ok',
|
|
541
|
+
...(failure.output === undefined ? {} : { output: failure.output }),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
475
544
|
return {
|
|
476
545
|
playbookId,
|
|
477
|
-
status: failure?.status
|
|
546
|
+
status: failure?.status === 'aborted' ? 'aborted' : 'error',
|
|
478
547
|
error: authoredChildError(event, playbookId),
|
|
479
548
|
};
|
|
480
549
|
}
|
|
@@ -967,19 +1036,19 @@ export const devMachine = machineSetup.createMachine({
|
|
|
967
1036
|
discussionComplete: {
|
|
968
1037
|
id: 'discussionComplete',
|
|
969
1038
|
description: STATE_DESCRIPTIONS.discussionComplete,
|
|
970
|
-
meta:
|
|
1039
|
+
meta: terminalMeta('discussionComplete', 'success'),
|
|
971
1040
|
type: 'final',
|
|
972
1041
|
},
|
|
973
1042
|
done: {
|
|
974
1043
|
id: 'done',
|
|
975
1044
|
description: STATE_DESCRIPTIONS.done,
|
|
976
|
-
meta:
|
|
1045
|
+
meta: terminalMeta('done', 'success'),
|
|
977
1046
|
type: 'final',
|
|
978
1047
|
},
|
|
979
1048
|
reportedChildFailure: {
|
|
980
1049
|
id: 'reportedChildFailure',
|
|
981
1050
|
description: STATE_DESCRIPTIONS.reportedChildFailure,
|
|
982
|
-
meta:
|
|
1051
|
+
meta: terminalMeta('reportedChildFailure', 'failure'),
|
|
983
1052
|
type: 'final',
|
|
984
1053
|
},
|
|
985
1054
|
},
|
|
@@ -104,6 +104,16 @@ const playbookMeta = (stateId, role) => ({
|
|
|
104
104
|
...(role === undefined ? {} : { role }),
|
|
105
105
|
},
|
|
106
106
|
});
|
|
107
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
108
|
+
// workflow succeeded or failed, so a caller learns that from the machine
|
|
109
|
+
// rather than from REVIEW's output fields.
|
|
110
|
+
const terminalMeta = (stateId, terminal) => ({
|
|
111
|
+
playbook: {
|
|
112
|
+
stateId,
|
|
113
|
+
description: stateDescriptions[stateId],
|
|
114
|
+
terminal,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
107
117
|
const outputOf = (event) => event.output;
|
|
108
118
|
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
109
119
|
const stateMetadata = {
|
|
@@ -685,7 +695,7 @@ export const reviewMachine = setup({
|
|
|
685
695
|
done: {
|
|
686
696
|
id: 'done',
|
|
687
697
|
description: stateDescriptions.done,
|
|
688
|
-
meta:
|
|
698
|
+
meta: terminalMeta('done', 'success'),
|
|
689
699
|
type: 'final',
|
|
690
700
|
},
|
|
691
701
|
},
|
|
@@ -212,6 +212,20 @@ const playbookMeta = <StateId extends keyof typeof stateDescriptions>(
|
|
|
212
212
|
},
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
// DR-048: a final state additionally declares whether its outcome means the
|
|
216
|
+
// workflow succeeded or failed, so a caller learns that from the machine
|
|
217
|
+
// rather than from REVIEW's output fields.
|
|
218
|
+
const terminalMeta = <StateId extends keyof typeof stateDescriptions>(
|
|
219
|
+
stateId: StateId,
|
|
220
|
+
terminal: 'success' | 'failure',
|
|
221
|
+
) => ({
|
|
222
|
+
playbook: {
|
|
223
|
+
stateId,
|
|
224
|
+
description: stateDescriptions[stateId],
|
|
225
|
+
terminal,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
215
229
|
const outputOf = (event: unknown): PlayerOutput =>
|
|
216
230
|
(event as PlayerDoneEvent).output;
|
|
217
231
|
|
|
@@ -832,7 +846,7 @@ export const reviewMachine = setup({
|
|
|
832
846
|
done: {
|
|
833
847
|
id: 'done',
|
|
834
848
|
description: stateDescriptions.done,
|
|
835
|
-
meta:
|
|
849
|
+
meta: terminalMeta('done', 'success'),
|
|
836
850
|
type: 'final',
|
|
837
851
|
},
|
|
838
852
|
},
|
package/slc/gears2fsm.md
CHANGED
|
@@ -431,8 +431,11 @@ behavior. The generic `failed` state is the default only when Source declares
|
|
|
431
431
|
no recovery or reassessment path for a rejected child.
|
|
432
432
|
That recovering `onError` shall be an ordered transition array. Its first arm
|
|
433
433
|
shall use a typed structural guard that accepts only an `Error` carrying a
|
|
434
|
-
validated public child `result`
|
|
435
|
-
|
|
434
|
+
validated public child `result` that is either `status: 'aborted' | 'error'`
|
|
435
|
+
or `status: 'ok'` with `terminal.kind === 'failure'` — a child that completed
|
|
436
|
+
at its own authored failure terminal, which the bridge delivers through this
|
|
437
|
+
same error path; only that arm appends sanitized child evidence and
|
|
438
|
+
continues. A fallback arm shall retain
|
|
436
439
|
the control error normalized as JSON-safe `{ name, message, stack? }` in
|
|
437
440
|
`lastError` and route to `failed` without appending a completed child result;
|
|
438
441
|
the linked runtime alone retains the original error in its out-of-machine
|
|
@@ -444,9 +447,14 @@ result, the error action shall inspect whether its status was `aborted` or
|
|
|
444
447
|
`error`; it shall not collapse both into an invented success/failure enum. The
|
|
445
448
|
FSM may inspect that public structural data without importing the runner or
|
|
446
449
|
constructing runtime call identities.
|
|
450
|
+
Because a failure terminal reaches `onError`, `onDone` alone proves the child
|
|
451
|
+
succeeded: a caller shall not decide success by inspecting the callee's output
|
|
452
|
+
fields, and shall read those fields only where its own Source relays them.
|
|
447
453
|
For a workflow that reassesses child results, use a typed JSON-safe record such
|
|
448
454
|
as `{ playbookId, status: 'ok', output }` on `onDone` and
|
|
449
|
-
`{ playbookId, status: 'aborted' | 'error', error }` on `onError
|
|
455
|
+
`{ playbookId, status: 'aborted' | 'error', error }` on `onError`, with a
|
|
456
|
+
rejected `status: 'ok'` failure terminal recorded in that same `ok` shape so
|
|
457
|
+
the child's own output is relayed. Because the
|
|
450
458
|
runtime rejection is an `Error` with a public `result` property, normalization
|
|
451
459
|
shall inspect `result.status` and `result.error` before applying a generic
|
|
452
460
|
`Error` normalizer. It shall persist only the current context target id, the
|
|
@@ -773,6 +781,16 @@ This constrains only published meaning: the declared machine `output` still
|
|
|
773
781
|
derives its status and fields from typed context, so a caller that does read
|
|
774
782
|
the output is unaffected.
|
|
775
783
|
|
|
784
|
+
Each final state shall additionally declare `terminal: 'success' | 'failure'`
|
|
785
|
+
in its `meta.playbook`, derived from the Source's own outcome wording exactly
|
|
786
|
+
as the description is: an outcome Source states as the workflow completing is
|
|
787
|
+
`'success'`, and one Source states as a failure the workflow reports instead
|
|
788
|
+
of parking is `'failure'`. The kind is fixed at compile time and adds no agent
|
|
789
|
+
call. The runtime publishes it, with the reached state's id and description, as
|
|
790
|
+
the completed run's terminal record, so this workflow's own caller learns
|
|
791
|
+
whether it succeeded from the machine it reached rather than from these output
|
|
792
|
+
fields.
|
|
793
|
+
|
|
776
794
|
Where Source declares a JSON-safe terminal result, the setup types shall
|
|
777
795
|
declare that output and the root machine shall derive it from typed context
|
|
778
796
|
through XState's machine `output` function. A final-state transition alone does
|
package/slc/link.md
CHANGED
|
@@ -338,6 +338,7 @@ type PlaybookRunResult =
|
|
|
338
338
|
outcome: 'terminal';
|
|
339
339
|
state: PlaybookState;
|
|
340
340
|
stateDescription?: string;
|
|
341
|
+
terminal?: PlaybookTerminalOutcome;
|
|
341
342
|
output?: JsonValue;
|
|
342
343
|
}
|
|
343
344
|
| {
|
|
@@ -515,6 +516,12 @@ interface PlaybookCallRequest {
|
|
|
515
516
|
text: string;
|
|
516
517
|
}
|
|
517
518
|
|
|
519
|
+
interface PlaybookTerminalOutcome {
|
|
520
|
+
stateId: string;
|
|
521
|
+
kind: 'success' | 'failure';
|
|
522
|
+
description?: string;
|
|
523
|
+
}
|
|
524
|
+
|
|
518
525
|
type PlaybookCallResult =
|
|
519
526
|
| {
|
|
520
527
|
status: 'ok';
|
|
@@ -522,6 +529,7 @@ type PlaybookCallResult =
|
|
|
522
529
|
childSessionId: string;
|
|
523
530
|
state?: PlaybookState;
|
|
524
531
|
output?: JsonValue;
|
|
532
|
+
terminal?: PlaybookTerminalOutcome;
|
|
525
533
|
}
|
|
526
534
|
| {
|
|
527
535
|
status: 'aborted';
|
|
@@ -1448,12 +1456,23 @@ playbook id, and child session id; bind its new turn signal for work resumed in
|
|
|
1448
1456
|
the parent; emit and drain the call-finish trace; settle the bridge deferred;
|
|
1449
1457
|
and use XState `waitFor` to drive the parent to its next
|
|
1450
1458
|
quiescent, suspended, failed, aborted, or terminal result.
|
|
1451
|
-
An `ok` result
|
|
1452
|
-
`
|
|
1459
|
+
An `ok` result whose `terminal.kind` is `success`, and an `ok` result carrying
|
|
1460
|
+
no `terminal` record at all, resolve the actor and reach `invoke.onDone`;
|
|
1461
|
+
`aborted` and `error` results, and an `ok` result whose `terminal.kind` is
|
|
1462
|
+
`failure`, reject it and reach `invoke.onError`.
|
|
1453
1463
|
The rejection shall be an `Error` whose public readonly `result` property is
|
|
1454
1464
|
the exact normalized `PlaybookCallResult`; throwing the result object directly
|
|
1455
1465
|
or discarding its status prevents the FSM from distinguishing abort from
|
|
1456
1466
|
failure during recovery.
|
|
1467
|
+
A completed child's `ok` result carries `terminal` exactly when the child's
|
|
1468
|
+
artifact declares its reached final state's kind: the runtime reads
|
|
1469
|
+
`meta.playbook.terminal` and that state's authored description from the
|
|
1470
|
+
artifact, never from an agent reply, and a declared value other than `success`
|
|
1471
|
+
or `failure` is a control-plane error rather than a child outcome.
|
|
1472
|
+
Because the bridge routes a failure terminal to the error path, `onDone` alone
|
|
1473
|
+
proves the child succeeded and a caller never inspects a callee's output fields
|
|
1474
|
+
to decide that; a caller reads those fields only when its own Source relays
|
|
1475
|
+
them.
|
|
1457
1476
|
Unknown, duplicate, or stale call ids reject without changing actor state.
|
|
1458
1477
|
The finish trace shall therefore precede any parent FSM transition caused by
|
|
1459
1478
|
the child return.
|
package/src/runtime.d.ts
CHANGED
|
@@ -62,12 +62,18 @@ export interface PlaybookCallRequest {
|
|
|
62
62
|
playbookId: string;
|
|
63
63
|
text: string;
|
|
64
64
|
}
|
|
65
|
+
export interface PlaybookTerminalOutcome {
|
|
66
|
+
stateId: string;
|
|
67
|
+
kind: 'success' | 'failure';
|
|
68
|
+
description?: string;
|
|
69
|
+
}
|
|
65
70
|
export type PlaybookCallResult = {
|
|
66
71
|
status: 'ok';
|
|
67
72
|
playbookId: string;
|
|
68
73
|
childSessionId: string;
|
|
69
74
|
state?: PlaybookState;
|
|
70
75
|
output?: JsonValue;
|
|
76
|
+
terminal?: PlaybookTerminalOutcome;
|
|
71
77
|
} | {
|
|
72
78
|
status: 'aborted';
|
|
73
79
|
playbookId: string;
|
|
@@ -102,6 +108,7 @@ export type PlaybookRunResult = {
|
|
|
102
108
|
outcome: 'terminal';
|
|
103
109
|
state: PlaybookState;
|
|
104
110
|
stateDescription?: string;
|
|
111
|
+
terminal?: PlaybookTerminalOutcome;
|
|
105
112
|
output?: JsonValue;
|
|
106
113
|
} | {
|
|
107
114
|
outcome: 'suspended';
|
package/src/runtime.ts
CHANGED
|
@@ -98,6 +98,16 @@ export interface PlaybookCallRequest {
|
|
|
98
98
|
text: string;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// DR-048: the reached final state's compiled terminal meaning. `kind` is
|
|
102
|
+
// authored metadata read from the artifact, never from an agent reply, and
|
|
103
|
+
// `description` repeats that state's authored description when it declares
|
|
104
|
+
// one. An artifact whose final states declare no kind carries no such record.
|
|
105
|
+
export interface PlaybookTerminalOutcome {
|
|
106
|
+
stateId: string;
|
|
107
|
+
kind: 'success' | 'failure';
|
|
108
|
+
description?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
export type PlaybookCallResult =
|
|
102
112
|
| {
|
|
103
113
|
status: 'ok';
|
|
@@ -105,6 +115,7 @@ export type PlaybookCallResult =
|
|
|
105
115
|
childSessionId: string;
|
|
106
116
|
state?: PlaybookState;
|
|
107
117
|
output?: JsonValue;
|
|
118
|
+
terminal?: PlaybookTerminalOutcome;
|
|
108
119
|
}
|
|
109
120
|
| {
|
|
110
121
|
status: 'aborted';
|
|
@@ -137,6 +148,7 @@ export type PlaybookRunResult =
|
|
|
137
148
|
outcome: 'terminal';
|
|
138
149
|
state: PlaybookState;
|
|
139
150
|
stateDescription?: string;
|
|
151
|
+
terminal?: PlaybookTerminalOutcome;
|
|
140
152
|
output?: JsonValue;
|
|
141
153
|
}
|
|
142
154
|
| {
|
|
@@ -468,6 +468,19 @@ export declare function resumableStateIdsFromMachine(machine: AnyStateMachine):
|
|
|
468
468
|
* these descriptions (DR-029); a state without one has no entry.
|
|
469
469
|
*/
|
|
470
470
|
export declare function stateDescriptionsFromMachine(machine: AnyStateMachine): ReadonlyMap<string, string>;
|
|
471
|
+
/**
|
|
472
|
+
* DR-048: each root final state's declared terminal kind, read from
|
|
473
|
+
* `meta.playbook.terminal` in `machine.config`. The kind is compiled
|
|
474
|
+
* metadata — the compiler derives it from the Source's own outcome wording,
|
|
475
|
+
* exactly as it derives the state's description — so a caller learns whether
|
|
476
|
+
* a completed child succeeded from the machine it reached, never from the
|
|
477
|
+
* child's output fields or an agent's prose.
|
|
478
|
+
*
|
|
479
|
+
* A machine whose final states declare no kind yields an empty map and keeps
|
|
480
|
+
* the pre-DR-048 delivery. A `terminal` on a non-final state, or a value
|
|
481
|
+
* other than `success` or `failure`, is a malformed artifact and throws.
|
|
482
|
+
*/
|
|
483
|
+
export declare function terminalOutcomesFromMachine(machine: AnyStateMachine, label?: string): ReadonlyMap<string, 'success' | 'failure'>;
|
|
471
484
|
/**
|
|
472
485
|
* Build a `PlaybookRuntimeFactory` that interprets the given FSM artifact
|
|
473
486
|
* under the slc/link.md contract. The factory provides every actor kind the
|
|
@@ -1073,6 +1073,45 @@ export function stateDescriptionsFromMachine(machine) {
|
|
|
1073
1073
|
}
|
|
1074
1074
|
return descriptions;
|
|
1075
1075
|
}
|
|
1076
|
+
/**
|
|
1077
|
+
* DR-048: each root final state's declared terminal kind, read from
|
|
1078
|
+
* `meta.playbook.terminal` in `machine.config`. The kind is compiled
|
|
1079
|
+
* metadata — the compiler derives it from the Source's own outcome wording,
|
|
1080
|
+
* exactly as it derives the state's description — so a caller learns whether
|
|
1081
|
+
* a completed child succeeded from the machine it reached, never from the
|
|
1082
|
+
* child's output fields or an agent's prose.
|
|
1083
|
+
*
|
|
1084
|
+
* A machine whose final states declare no kind yields an empty map and keeps
|
|
1085
|
+
* the pre-DR-048 delivery. A `terminal` on a non-final state, or a value
|
|
1086
|
+
* other than `success` or `failure`, is a malformed artifact and throws.
|
|
1087
|
+
*/
|
|
1088
|
+
export function terminalOutcomesFromMachine(machine, label = 'playbook') {
|
|
1089
|
+
const kinds = new Map();
|
|
1090
|
+
const config = machine.config;
|
|
1091
|
+
if (!isPlainObject(config) || !isPlainObject(config.states))
|
|
1092
|
+
return kinds;
|
|
1093
|
+
for (const [key, stateDef] of Object.entries(config.states)) {
|
|
1094
|
+
if (!isPlainObject(stateDef))
|
|
1095
|
+
continue;
|
|
1096
|
+
const playbook = isPlainObject(stateDef.meta)
|
|
1097
|
+
? stateDef.meta.playbook
|
|
1098
|
+
: undefined;
|
|
1099
|
+
const declared = isPlainObject(playbook) ? playbook.terminal : undefined;
|
|
1100
|
+
if (declared === undefined)
|
|
1101
|
+
continue;
|
|
1102
|
+
if (declared !== 'success' && declared !== 'failure') {
|
|
1103
|
+
throw new TypeError(`${label} state ${key} declares meta.playbook.terminal ` +
|
|
1104
|
+
`${JSON.stringify(declared)}; only 'success' or 'failure' is a ` +
|
|
1105
|
+
'terminal kind');
|
|
1106
|
+
}
|
|
1107
|
+
if (stateDef.type !== 'final') {
|
|
1108
|
+
throw new TypeError(`${label} state ${key} declares meta.playbook.terminal but is not ` +
|
|
1109
|
+
'a final state');
|
|
1110
|
+
}
|
|
1111
|
+
kinds.set(key, declared);
|
|
1112
|
+
}
|
|
1113
|
+
return kinds;
|
|
1114
|
+
}
|
|
1076
1115
|
/**
|
|
1077
1116
|
* First configured target of `eventType` from the state with `stateId`,
|
|
1078
1117
|
* falling back to the machine root's own transitions. Used only to pick the
|
|
@@ -1834,6 +1873,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1834
1873
|
// DR-029: source state descriptions label the control actions the
|
|
1835
1874
|
// runtime advertises through `describe()`.
|
|
1836
1875
|
const stateDescriptions = stateDescriptionsFromMachine(machine);
|
|
1876
|
+
// DR-048: a malformed terminal declaration is a control-plane defect of the
|
|
1877
|
+
// artifact, so it fails construction rather than at the one run that
|
|
1878
|
+
// happens to reach that final state.
|
|
1879
|
+
const terminalKinds = terminalOutcomesFromMachine(machine, label);
|
|
1837
1880
|
const roleStatesDescriptor = specDescriptors.roleStates;
|
|
1838
1881
|
if (roleStatesDescriptor !== undefined &&
|
|
1839
1882
|
!Object.prototype.hasOwnProperty.call(roleStatesDescriptor, 'value')) {
|
|
@@ -4275,10 +4318,18 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4275
4318
|
const stateDescription = !hasUnresolvedReconciliation()
|
|
4276
4319
|
? stateDescriptionFor(state)
|
|
4277
4320
|
: undefined;
|
|
4321
|
+
// DR-048: the reached final state's compiled terminal meaning, read
|
|
4322
|
+
// from the artifact. It is withheld exactly when the published
|
|
4323
|
+
// description is, so an unresolved reconciliation publishes no
|
|
4324
|
+
// terminal meaning at all.
|
|
4325
|
+
const terminal = hasUnresolvedReconciliation() || state.stateId === undefined
|
|
4326
|
+
? undefined
|
|
4327
|
+
: terminalOutcomeFor(state.stateId, stateDescription);
|
|
4278
4328
|
return {
|
|
4279
4329
|
outcome,
|
|
4280
4330
|
state,
|
|
4281
4331
|
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
4332
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
4282
4333
|
...(output === undefined
|
|
4283
4334
|
? {}
|
|
4284
4335
|
: {
|
|
@@ -4629,6 +4680,18 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4629
4680
|
}
|
|
4630
4681
|
return undefined;
|
|
4631
4682
|
}
|
|
4683
|
+
// DR-048: the public terminal record for a reached final state, present
|
|
4684
|
+
// only for an artifact that declares that state's kind.
|
|
4685
|
+
function terminalOutcomeFor(stateId, description) {
|
|
4686
|
+
const kind = terminalKinds.get(stateId);
|
|
4687
|
+
if (kind === undefined)
|
|
4688
|
+
return undefined;
|
|
4689
|
+
return {
|
|
4690
|
+
stateId,
|
|
4691
|
+
kind,
|
|
4692
|
+
...(description === undefined ? {} : { description }),
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4632
4695
|
function receiptTracePayload(receipt) {
|
|
4633
4696
|
return {
|
|
4634
4697
|
disposition: receipt.disposition,
|