@sublang/playbook 15.0.0 → 15.1.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 +1 -1
- package/docs/embedding.md +7 -5
- package/package.json +3 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -9
- package/reference/sdlc/decide.playbook/decide.playbook.js +158 -32
- package/reference/sdlc/decide.playbook/decide.playbook.ts +229 -46
- package/slc/prefix-prompts.mjs +190 -0
- package/slc/prefix.md +200 -0
- package/slc/slc.pin-inputs.json +1 -0
- package/src/xstate-playbook-runtime.d.ts +35 -1
- package/src/xstate-playbook-runtime.js +82 -47
- package/src/xstate-playbook-runtime.ts +107 -60
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ SLC's `playbook` pipeline has three phases:
|
|
|
107
107
|
2. **GEARS → FSM** ([slc/gears2fsm.md](slc/gears2fsm.md)) — maps each item to an XState state that invokes the Captain, a player, another playbook, or a local script.
|
|
108
108
|
3. **FSM → runtime** ([slc/link.md](slc/link.md)) — links the machine to a host-independent interface for user input, agent calls, status, and telemetry.
|
|
109
109
|
|
|
110
|
-
The default [optimization pass](slc/optimize.md) replaces eligible mechanical steps with local shell scripts; `--no-optimize` skips
|
|
110
|
+
The default passes run between the first two phases: the [optimization pass](slc/optimize.md) replaces eligible mechanical steps with local shell scripts, and the [prompt-prefix pass](slc/prefix.md) moves each prompt's relayed runtime values after its instructions so repeated runs share a cacheable prompt prefix; `--no-optimize` skips both.
|
|
111
111
|
Inspect the complete [Captain](reference/sdlc/captain.md), [CODE](reference/sdlc/code.md), [REVIEW](reference/sdlc/review.md), and [DECIDE](reference/sdlc/decide.md) examples.
|
|
112
112
|
|
|
113
113
|
## Contributing
|
package/docs/embedding.md
CHANGED
|
@@ -445,21 +445,23 @@ const url = import.meta.resolve('@sublang/playbook/slc/link.md');
|
|
|
445
445
|
const link = await readFile(fileURLToPath(url), 'utf8');
|
|
446
446
|
```
|
|
447
447
|
|
|
448
|
-
The
|
|
448
|
+
The five specs are [`slc/text2gears.md`](../slc/text2gears.md),
|
|
449
449
|
[`slc/gears2fsm.md`](../slc/gears2fsm.md),
|
|
450
450
|
[`slc/link.md`](../slc/link.md) — the FSM-to-runtime contract that
|
|
451
|
-
`@sublang/playbook/runtime` projects into TypeScript —
|
|
452
|
-
[`slc/optimize.md`](../slc/optimize.md)
|
|
451
|
+
`@sublang/playbook/runtime` projects into TypeScript —
|
|
452
|
+
[`slc/optimize.md`](../slc/optimize.md), and
|
|
453
|
+
[`slc/prefix.md`](../slc/prefix.md).
|
|
453
454
|
|
|
454
|
-
|
|
455
|
+
Six supporting assets use the same resolution mechanism:
|
|
455
456
|
|
|
456
457
|
| Asset | Purpose |
|
|
457
458
|
| --- | --- |
|
|
458
459
|
| [`slc/workflow-contracts.json`](../slc/workflow-contracts.json) | Public output schemas and default literal target bindings for the builtin REVIEW, DECIDE, CODE, BRANCH, and PR workflows; compilers and embedders can read them without importing workflow implementations. |
|
|
459
460
|
| [`slc/materialize-link.mjs`](../slc/materialize-link.mjs) | Optional CLI that emits a thin linked module from a supported FSM and JSON descriptor; see the profiles and invocation in `slc/link.md`. |
|
|
460
461
|
| [`slc/scaffold-fsm.mjs`](../slc/scaffold-fsm.mjs) | Optional CLI that initializes an incomplete typed FSM from supported GEARS source. |
|
|
462
|
+
| [`slc/prefix-prompts.mjs`](../slc/prefix-prompts.mjs) | CLI that performs the prompt-prefix pass's rewrite of a GEARS package exactly; see the invocation in `slc/prefix.md`. |
|
|
461
463
|
| [`slc/experiments/fsm-scaffold-guidance.md`](../slc/experiments/fsm-scaffold-guidance.md) | Opt-in compiler guidance for the initializer, outside ordinary phase discovery. |
|
|
462
|
-
| [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm` and `link`, including the workflow catalog and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
|
|
464
|
+
| [`slc/slc.pin-inputs.json`](../slc/slc.pin-inputs.json) | SLC semantic-input closures for `gears2fsm`, `prefix`, and `link`, including the workflow catalog, the prefix tool, and, for `link`, the materializer. Other phases use SLC's inline-input discovery. |
|
|
463
465
|
|
|
464
466
|
For example, resolve `@sublang/playbook/slc/workflow-contracts.json`, read it
|
|
465
467
|
with `readFile`, and parse the result as JSON to inspect the published workflow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"slc/gears2fsm.md",
|
|
47
47
|
"slc/text2gears.md",
|
|
48
48
|
"slc/optimize.md",
|
|
49
|
+
"slc/prefix.md",
|
|
50
|
+
"slc/prefix-prompts.mjs",
|
|
49
51
|
"reference/sdlc/captain.md",
|
|
50
52
|
"reference/sdlc/code.md",
|
|
51
53
|
"reference/sdlc/review.md",
|
|
@@ -520,8 +520,6 @@ function projectionText(projection) {
|
|
|
520
520
|
return JSON.stringify(projection);
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
const MISSING_CONTENT_IDENTITY = Object.freeze({ kind: 'missing' });
|
|
524
|
-
|
|
525
523
|
function hasPath(projection, path) {
|
|
526
524
|
return Object.prototype.hasOwnProperty.call(projection, path);
|
|
527
525
|
}
|
|
@@ -555,9 +553,9 @@ function sameTreeEntry(left, right) {
|
|
|
555
553
|
return left.mode === right.mode && left.oid === right.oid;
|
|
556
554
|
}
|
|
557
555
|
|
|
558
|
-
// The
|
|
556
|
+
// The entry records nothing at the path: a deleted worktree entry, or a
|
|
559
557
|
// staged deletion whose index holds no object.
|
|
560
|
-
function
|
|
558
|
+
function entryIsDeletion(entry) {
|
|
561
559
|
return entry.worktree === undefined
|
|
562
560
|
? entry.indexMode === '000000'
|
|
563
561
|
: entry.worktree.kind === 'missing';
|
|
@@ -615,14 +613,57 @@ function sameContentIdentity(left, right) {
|
|
|
615
613
|
);
|
|
616
614
|
}
|
|
617
615
|
|
|
616
|
+
// A worktree deletion loses the Boss's content only when neither the after
|
|
617
|
+
// index nor the after HEAD tree holds it; content the after HEAD cannot
|
|
618
|
+
// produce fails closed as lost.
|
|
619
|
+
async function deletionRetainsContent(baseline, after, path, entry, afterEntry) {
|
|
620
|
+
try {
|
|
621
|
+
const baselineIdentity = await baselineContentIdentity(after.worktree, entry);
|
|
622
|
+
if (
|
|
623
|
+
afterEntry.kind === 'ordinary' &&
|
|
624
|
+
afterEntry.indexMode !== undefined &&
|
|
625
|
+
afterEntry.indexMode !== '000000'
|
|
626
|
+
) {
|
|
627
|
+
const indexIdentity = await blobContentIdentity(
|
|
628
|
+
after.worktree,
|
|
629
|
+
afterEntry.indexMode,
|
|
630
|
+
afterEntry.indexOid,
|
|
631
|
+
);
|
|
632
|
+
if (sameContentIdentity(baselineIdentity, indexIdentity)) return true;
|
|
633
|
+
}
|
|
634
|
+
if (baseline.head === after.head) return false;
|
|
635
|
+
const treeEntry = await afterTreeEntry(after.worktree, after.head, path);
|
|
636
|
+
if (treeEntry === undefined || treeEntry.mode === '160000') return false;
|
|
637
|
+
const treeIdentity = await blobContentIdentity(
|
|
638
|
+
after.worktree,
|
|
639
|
+
treeEntry.mode,
|
|
640
|
+
treeEntry.oid,
|
|
641
|
+
);
|
|
642
|
+
return sameContentIdentity(baselineIdentity, treeIdentity);
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
618
648
|
// DR-062 §1: the fate of one baseline projection entry at the end of a
|
|
619
649
|
// governed call — `preserved`, `altered-uncommitted`, `altered-committed`,
|
|
620
|
-
// `absorbed`, or `lost`.
|
|
650
|
+
// `absorbed`, or `lost`. Loss is decided before alteration: a kind that
|
|
651
|
+
// admits no content matching, a worktree deletion whose content survives in
|
|
652
|
+
// neither the index nor the new HEAD, and a commit that dropped the path all
|
|
653
|
+
// leave the Boss's content nowhere.
|
|
621
654
|
async function baselineEntryFate(baseline, after, path, entry) {
|
|
622
655
|
if (hasPath(after.projection, path)) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
656
|
+
const afterEntry = after.projection[path];
|
|
657
|
+
if (JSON.stringify(afterEntry) === JSON.stringify(entry)) {
|
|
658
|
+
return 'preserved';
|
|
659
|
+
}
|
|
660
|
+
if (nonComparableBaselineEntry(entry)) return 'lost';
|
|
661
|
+
if (entryIsDeletion(afterEntry) && !entryIsDeletion(entry)) {
|
|
662
|
+
return (await deletionRetainsContent(baseline, after, path, entry, afterEntry))
|
|
663
|
+
? 'altered-uncommitted'
|
|
664
|
+
: 'lost';
|
|
665
|
+
}
|
|
666
|
+
return 'altered-uncommitted';
|
|
626
667
|
}
|
|
627
668
|
// The entry left the working tree, so only a commit this call made can hold
|
|
628
669
|
// the Boss's content; a call that moved no HEAD made none.
|
|
@@ -635,15 +676,15 @@ async function baselineEntryFate(baseline, after, path, entry) {
|
|
|
635
676
|
// carried no change of the Boss's and his content is nowhere.
|
|
636
677
|
return 'lost';
|
|
637
678
|
}
|
|
638
|
-
if (
|
|
679
|
+
if (entryIsDeletion(entry)) {
|
|
639
680
|
return treeEntry === undefined ? 'absorbed' : 'lost';
|
|
640
681
|
}
|
|
641
|
-
|
|
682
|
+
// A commit that dropped the path, or replaced it with a gitlink, carries
|
|
683
|
+
// no content of the Boss's to compare.
|
|
684
|
+
if (treeEntry === undefined || treeEntry.mode === '160000') return 'lost';
|
|
642
685
|
const [baselineIdentity, treeIdentity] = await Promise.all([
|
|
643
686
|
baselineContentIdentity(after.worktree, entry),
|
|
644
|
-
treeEntry
|
|
645
|
-
? MISSING_CONTENT_IDENTITY
|
|
646
|
-
: blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
|
|
687
|
+
blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
|
|
647
688
|
]);
|
|
648
689
|
return sameContentIdentity(baselineIdentity, treeIdentity)
|
|
649
690
|
? 'absorbed'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeErrorCompact, normalizeErrorFull } from '../../../src/xstate-runtime.js';
|
|
1
2
|
import { type PlayerInput, type DecideEvent, type DecideInput, type PendingBossQuestion } from './decide.fsm.js';
|
|
2
3
|
import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
|
|
3
4
|
import type { CaptainCallOptions, CaptainResult, JsonValue, NormalizedError, PlayerCallOptions, PlayerResult, PlayerSessionStore, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookControlAction, PlaybookControlReceipt, PlaybookControlView, PlaybookPendingCall, PlaybookPorts, PlaybookRunResult, PlaybookRuntime, PlaybookRuntimeFactory, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookStateValue, PlaybookTraceEvent, PlaybookTraceType } from '@sublang/playbook/runtime';
|
|
@@ -25,15 +26,6 @@ declare function buildAdjudicatorPrompt(input: PlayerInput, playerOutput: string
|
|
|
25
26
|
readonly error: string;
|
|
26
27
|
}): string;
|
|
27
28
|
declare function combineSignals(a: AbortSignal | undefined, b: AbortSignal | undefined): AbortSignal;
|
|
28
|
-
declare function normalizeErrorCompact(err: unknown): {
|
|
29
|
-
name: string;
|
|
30
|
-
message: string;
|
|
31
|
-
} | undefined;
|
|
32
|
-
declare function normalizeErrorFull(err: unknown): {
|
|
33
|
-
name: string;
|
|
34
|
-
message: string;
|
|
35
|
-
stack?: string;
|
|
36
|
-
} | undefined;
|
|
37
29
|
declare function pendingQuestionsFromContext(context: Record<string, unknown>): PendingBossQuestion[];
|
|
38
30
|
declare function pendingQuestionsForState(state: PlaybookState, context: Record<string, unknown>): PendingBossQuestion[];
|
|
39
31
|
export declare const createPlaybookRuntime: PlaybookRuntimeFactory<DecidePlaybookRuntimeConstruction>;
|
|
@@ -19,7 +19,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
19
19
|
import PQueue from 'p-queue';
|
|
20
20
|
import { createActor, fromPromise } from 'xstate';
|
|
21
21
|
import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
|
|
22
|
-
import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, composePlayerContinuation, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
22
|
+
import { ABORTED_FAILURE_CAUSE, assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, attachPlaybookFailureCause, combineAbortSignals, composePlayerContinuation, createFailureCauseRetention, createNestedPlaybookBridge, detachPersistedMachineSnapshot, effectAuthorizedPreExistingBlock, governedSettlementCause, normalizeError, normalizeErrorCompact, normalizeErrorFull, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, playerFailureCause, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, runtimeDefectCause, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
23
23
|
import decideMachine from './decide.fsm.js';
|
|
24
24
|
function snapshotDecideRuntimeOptions(value) {
|
|
25
25
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
@@ -453,15 +453,6 @@ function parseGovernedSemanticCandidate(raw) {
|
|
|
453
453
|
function combineSignals(a, b) {
|
|
454
454
|
return combineAbortSignals(a, b);
|
|
455
455
|
}
|
|
456
|
-
function normalizeErrorCompact(err) {
|
|
457
|
-
if (err === undefined || err === null)
|
|
458
|
-
return undefined;
|
|
459
|
-
const normalized = normalizeError(err);
|
|
460
|
-
return { name: normalized.name, message: normalized.message };
|
|
461
|
-
}
|
|
462
|
-
function normalizeErrorFull(err) {
|
|
463
|
-
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
464
|
-
}
|
|
465
456
|
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
466
457
|
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
467
458
|
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
@@ -691,7 +682,7 @@ function normalizedTransitionEvent(event) {
|
|
|
691
682
|
}
|
|
692
683
|
return snapshotJsonValue(descriptor, 'FSM event');
|
|
693
684
|
}
|
|
694
|
-
function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
|
|
685
|
+
function telemetryPayload(previousState, state, event, context, hiddenQuestionId, failedError) {
|
|
695
686
|
const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
696
687
|
const prior = previousState ?? state;
|
|
697
688
|
const payload = {
|
|
@@ -702,7 +693,7 @@ function telemetryPayload(previousState, state, event, context, hiddenQuestionId
|
|
|
702
693
|
state,
|
|
703
694
|
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
704
695
|
...(state.activeStateIds.includes('failed')
|
|
705
|
-
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
696
|
+
? { lastError: failedError ?? normalizeErrorFull(context.lastError) ?? null }
|
|
706
697
|
: {}),
|
|
707
698
|
};
|
|
708
699
|
assertJsonSafe(payload);
|
|
@@ -751,6 +742,59 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
751
742
|
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
752
743
|
const governedOutputsByBoundaryId = new Map();
|
|
753
744
|
const governedFailuresByBoundaryId = new Map();
|
|
745
|
+
// DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
746
|
+
// marker — a string, a frozen error — kept by identity for as long as the
|
|
747
|
+
// failure stands, so every surface that reads the FSM's own `lastError`
|
|
748
|
+
// publishes the cause decided for exactly that value (mirrors the shared
|
|
749
|
+
// factory).
|
|
750
|
+
const failureCauses = createFailureCauseRetention();
|
|
751
|
+
let failureSequence = 0;
|
|
752
|
+
const playerCallFailures = new Map();
|
|
753
|
+
const governedFailureOccurrences = new Map();
|
|
754
|
+
const recordFailure = (occurrences, key, cause) => {
|
|
755
|
+
occurrences.set(key, { cause, sequence: ++failureSequence });
|
|
756
|
+
};
|
|
757
|
+
const failedStateError = (context) => {
|
|
758
|
+
const normalized = normalizeErrorFull(context.lastError);
|
|
759
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
760
|
+
return normalized;
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
...normalized,
|
|
764
|
+
cause: failureCauses.causeOf(context.lastError) ??
|
|
765
|
+
runtimeDefectCause(normalized.message),
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
const playerResultFailureCause = (roleId, playerId, result) => result.status === 'aborted'
|
|
769
|
+
? ABORTED_FAILURE_CAUSE
|
|
770
|
+
: playerFailureCause({
|
|
771
|
+
roleId,
|
|
772
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
773
|
+
error: result.error ??
|
|
774
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
775
|
+
});
|
|
776
|
+
// A rejected player port or a result the runtime cannot read is a
|
|
777
|
+
// `player-failed` failure named by role; the turn's own abort is `aborted`.
|
|
778
|
+
// A cancellation the runtime itself issued — a proposal cancelled because
|
|
779
|
+
// its sibling failed — is not this player's failure and decides nothing, so
|
|
780
|
+
// the originator's cause stands; every other failure is decided afresh and
|
|
781
|
+
// recorded as this call's occurrence, whatever an earlier failure threw.
|
|
782
|
+
const decidePlayerCallFailure = (error, roleId, playerId, signal, callId) => {
|
|
783
|
+
const turn = currentSignal;
|
|
784
|
+
const turnAborted = turn !== undefined && turn.aborted;
|
|
785
|
+
if (!turnAborted && signal.aborted)
|
|
786
|
+
return;
|
|
787
|
+
const cause = turn !== undefined && turn.aborted && Object.is(error, turn.reason)
|
|
788
|
+
? ABORTED_FAILURE_CAUSE
|
|
789
|
+
: playerFailureCause({
|
|
790
|
+
roleId,
|
|
791
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
792
|
+
error,
|
|
793
|
+
});
|
|
794
|
+
attachPlaybookFailureCause(error, cause);
|
|
795
|
+
failureCauses.retain(error, cause);
|
|
796
|
+
recordFailure(playerCallFailures, callId, cause);
|
|
797
|
+
};
|
|
754
798
|
const governedEvidenceByBoundaryId = new Map();
|
|
755
799
|
const governedReceiptsByBoundaryId = new Map();
|
|
756
800
|
const governedPlayerOutputs = new WeakMap();
|
|
@@ -1325,7 +1369,12 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1325
1369
|
const roleId = input.role;
|
|
1326
1370
|
const playerId = resolvedPlayerId(roleId);
|
|
1327
1371
|
const playerKey = continuationKey(roleId, playerId);
|
|
1328
|
-
|
|
1372
|
+
// DR-062 §4: composed from this call's own baseline observation, so the
|
|
1373
|
+
// prompt the trace records is the prompt the player is sent.
|
|
1374
|
+
const withPreExisting = (text) => continuation?.preExistingBlock === undefined
|
|
1375
|
+
? text
|
|
1376
|
+
: `${text}\n\n${continuation.preExistingBlock}`;
|
|
1377
|
+
const freshPrompt = withPreExisting(composeInvocationPrompt(input));
|
|
1329
1378
|
let resume;
|
|
1330
1379
|
try {
|
|
1331
1380
|
signal.throwIfAborted();
|
|
@@ -1339,7 +1388,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1339
1388
|
latchControlPlaneError(error, signal);
|
|
1340
1389
|
throw error;
|
|
1341
1390
|
}
|
|
1342
|
-
const prompt = resume === false
|
|
1391
|
+
const prompt = resume === false
|
|
1392
|
+
? freshPrompt
|
|
1393
|
+
: withPreExisting(composeInvocationPrompt(input, true));
|
|
1343
1394
|
const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
1344
1395
|
const identity = {
|
|
1345
1396
|
stateId: input.stateId,
|
|
@@ -1382,6 +1433,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1382
1433
|
// A rejected call produced no authoritative result, so the previous
|
|
1383
1434
|
// token remains untouched. This also covers a coder promise that
|
|
1384
1435
|
// resolves after its invocation signal was cancelled.
|
|
1436
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
1385
1437
|
latchControlPlaneError(error, signal);
|
|
1386
1438
|
try {
|
|
1387
1439
|
await emitFailure(error);
|
|
@@ -1396,6 +1448,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1396
1448
|
result = validatePlayerResult(rawResult);
|
|
1397
1449
|
}
|
|
1398
1450
|
catch (error) {
|
|
1451
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
1399
1452
|
latchControlPlaneError(error, signal);
|
|
1400
1453
|
try {
|
|
1401
1454
|
await emitFailure(error);
|
|
@@ -1468,10 +1521,22 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1468
1521
|
correctionBudget: { limit: 1, spent: false },
|
|
1469
1522
|
};
|
|
1470
1523
|
};
|
|
1471
|
-
|
|
1472
|
-
|
|
1524
|
+
// DR-063 §1/§2: the cause of an unresolved governed settlement is decided
|
|
1525
|
+
// here — a reconciled mismatch supplies its receipt-read cause, an abort or
|
|
1526
|
+
// a refused adjudication its own code, anything else a runtime defect — and
|
|
1527
|
+
// rides the failure the boundary throws.
|
|
1528
|
+
const unresolvedGovernedEvidence = (boundaryId, reason, error, cause) => {
|
|
1529
|
+
const aborted = error !== undefined &&
|
|
1530
|
+
currentSignal?.aborted === true &&
|
|
1531
|
+
Object.is(error, currentSignal.reason);
|
|
1532
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
1533
|
+
const failure = error instanceof Error
|
|
1473
1534
|
? error
|
|
1474
|
-
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`)
|
|
1535
|
+
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`);
|
|
1536
|
+
attachPlaybookFailureCause(failure, decided);
|
|
1537
|
+
failureCauses.retain(failure, decided);
|
|
1538
|
+
recordFailure(governedFailureOccurrences, boundaryId, decided);
|
|
1539
|
+
governedFailuresByBoundaryId.set(boundaryId, failure);
|
|
1475
1540
|
};
|
|
1476
1541
|
const spendSemanticCorrectionBudget = async (boundary, receipt, finalText, semanticCandidate) => {
|
|
1477
1542
|
const ledger = readEffectLedger();
|
|
@@ -1700,7 +1765,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1700
1765
|
semanticCandidate,
|
|
1701
1766
|
});
|
|
1702
1767
|
if (reconciliation.status === 'unresolved') {
|
|
1703
|
-
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason);
|
|
1768
|
+
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason, undefined, reconciliation.cause);
|
|
1704
1769
|
return { finalText, semanticCandidate, unresolved: true };
|
|
1705
1770
|
}
|
|
1706
1771
|
const output = reconciliation.output;
|
|
@@ -1861,7 +1926,14 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1861
1926
|
member.envelope = envelope;
|
|
1862
1927
|
if (envelope.result.status !== 'ok' &&
|
|
1863
1928
|
!cohortOperations.signal.aborted) {
|
|
1864
|
-
|
|
1929
|
+
// DR-063 §2: the cancellation carries the cause of the result
|
|
1930
|
+
// that caused it, recorded as this member's occurrence, so the
|
|
1931
|
+
// cohort fails for this member.
|
|
1932
|
+
const cause = playerResultFailureCause(roleId, resolvedPlayerId(roleId), envelope.result);
|
|
1933
|
+
const cancellation = attachPlaybookFailureCause(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`), cause);
|
|
1934
|
+
failureCauses.retain(cancellation, cause);
|
|
1935
|
+
recordFailure(playerCallFailures, member.callId, cause);
|
|
1936
|
+
cohortOperations.abort(cancellation);
|
|
1865
1937
|
}
|
|
1866
1938
|
return envelope.result;
|
|
1867
1939
|
}
|
|
@@ -1928,9 +2000,38 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1928
2000
|
for (const result of acknowledgedResults.values()) {
|
|
1929
2001
|
governedPlayerOutputs.delete(result);
|
|
1930
2002
|
}
|
|
1931
|
-
|
|
2003
|
+
const failure = acknowledgementFailures.length === 1
|
|
1932
2004
|
? acknowledgementFailures[0]
|
|
1933
2005
|
: new AggregateError(acknowledgementFailures, 'DECIDE proposal cohort reconciliation failed');
|
|
2006
|
+
// DR-063 §2: a cohort fails for the member whose failure was decided
|
|
2007
|
+
// first — a rejected or non-`ok` player call, a refused adjudication,
|
|
2008
|
+
// a mismatched receipt — in decision order, not in the order the
|
|
2009
|
+
// players finished or the values they threw. That cause rides every
|
|
2010
|
+
// error the failure can surface as: the cancellation, each member's
|
|
2011
|
+
// own rejection, and the aggregate of both, never an abort.
|
|
2012
|
+
const origin = cohortOperations.signal.aborted
|
|
2013
|
+
? cohortOperations.signal.reason
|
|
2014
|
+
: undefined;
|
|
2015
|
+
const first = ROLE_IDS.flatMap((roleId) => {
|
|
2016
|
+
const member = members[roleId];
|
|
2017
|
+
const occurrence = member === undefined
|
|
2018
|
+
? undefined
|
|
2019
|
+
: (playerCallFailures.get(member.callId) ??
|
|
2020
|
+
governedFailureOccurrences.get(member.effectBoundary.boundaryId));
|
|
2021
|
+
return occurrence === undefined ? [] : [occurrence];
|
|
2022
|
+
}).sort((left, right) => left.sequence - right.sequence)[0];
|
|
2023
|
+
const originCause = first?.cause ??
|
|
2024
|
+
failureCauses.causeOf(failure) ??
|
|
2025
|
+
(origin === undefined ? undefined : failureCauses.causeOf(origin));
|
|
2026
|
+
if (originCause !== undefined) {
|
|
2027
|
+
for (const candidate of [failure, origin, ...acknowledgementFailures]) {
|
|
2028
|
+
if (candidate === undefined)
|
|
2029
|
+
continue;
|
|
2030
|
+
attachPlaybookFailureCause(candidate, originCause);
|
|
2031
|
+
failureCauses.retain(candidate, originCause);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
throw failure;
|
|
1934
2035
|
}
|
|
1935
2036
|
for (const roleId of releaseOrder) {
|
|
1936
2037
|
const member = members[roleId];
|
|
@@ -1993,6 +2094,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1993
2094
|
const envelope = await runPlayerCall(input, signal, {
|
|
1994
2095
|
callId: active.effectBoundary.callId,
|
|
1995
2096
|
resume: active.playerContinuation,
|
|
2097
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(active.effectBoundary, active.baseline),
|
|
1996
2098
|
});
|
|
1997
2099
|
active.result.resolve(envelope.result);
|
|
1998
2100
|
const acknowledgedResult = await active.acknowledged.promise;
|
|
@@ -2018,8 +2120,13 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2018
2120
|
const settled = await deferredEffects.repository.runExclusive({
|
|
2019
2121
|
signal,
|
|
2020
2122
|
effectBoundary,
|
|
2021
|
-
|
|
2022
|
-
|
|
2123
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
2124
|
+
// captured it under this exclusive claim.
|
|
2125
|
+
operation: async (context) => {
|
|
2126
|
+
envelope = await runPlayerCall(input, signal, {
|
|
2127
|
+
callId,
|
|
2128
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline),
|
|
2129
|
+
});
|
|
2023
2130
|
return envelope.result;
|
|
2024
2131
|
},
|
|
2025
2132
|
completeEffectBoundary: completionEvidenceFor(input, input.role, resolvedPlayerId(input.role), signal),
|
|
@@ -2064,7 +2171,10 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2064
2171
|
({ roleId, playerId, callId, result } = await callPlayer(input, combined));
|
|
2065
2172
|
}
|
|
2066
2173
|
if (result.status !== 'ok') {
|
|
2067
|
-
|
|
2174
|
+
// DR-063 §2: the failure Error built for a non-`ok` result carries
|
|
2175
|
+
// the cause decided for that exact result.
|
|
2176
|
+
const cause = playerResultFailureCause(roleId, playerId, result);
|
|
2177
|
+
throw attachPlaybookFailureCause(new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`), cause);
|
|
2068
2178
|
}
|
|
2069
2179
|
const finalText = result.finalText ?? '';
|
|
2070
2180
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -2181,7 +2291,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2181
2291
|
const context = snapshot.context;
|
|
2182
2292
|
const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
|
|
2183
2293
|
? undefined
|
|
2184
|
-
: 'commitCoderProposal')
|
|
2294
|
+
: 'commitCoderProposal', state.activeStateIds.includes('failed')
|
|
2295
|
+
? failedStateError(context)
|
|
2296
|
+
: undefined);
|
|
2185
2297
|
const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
|
|
2186
2298
|
void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
|
|
2187
2299
|
topic: TELEMETRY_TOPIC,
|
|
@@ -2218,7 +2330,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2218
2330
|
continue;
|
|
2219
2331
|
}
|
|
2220
2332
|
const lastError = activeStateId === 'failed'
|
|
2221
|
-
? normalizeErrorCompact(context
|
|
2333
|
+
? normalizeErrorCompact(failedStateError(context))
|
|
2222
2334
|
: undefined;
|
|
2223
2335
|
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
2224
2336
|
if (description === undefined)
|
|
@@ -2226,7 +2338,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2226
2338
|
const roleState = ROLE_STATES.find((candidate) => candidate.stateId === activeStateId);
|
|
2227
2339
|
scheduleStatus(roleState === undefined
|
|
2228
2340
|
? '◆ workflow failed; awaiting Boss recovery.'
|
|
2229
|
-
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined
|
|
2341
|
+
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined
|
|
2342
|
+
? undefined
|
|
2343
|
+
: snapshotJsonValue({ lastError }, 'DECIDE failed status data'));
|
|
2230
2344
|
}
|
|
2231
2345
|
}
|
|
2232
2346
|
catch (error) {
|
|
@@ -2357,13 +2471,15 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2357
2471
|
signal,
|
|
2358
2472
|
operationId,
|
|
2359
2473
|
effectBoundary,
|
|
2360
|
-
operation: async ({ playerContinuation }) => {
|
|
2474
|
+
operation: async ({ playerContinuation, baseline }) => {
|
|
2361
2475
|
if (playerContinuation !== false &&
|
|
2362
2476
|
(typeof playerContinuation !== 'string' ||
|
|
2363
2477
|
playerContinuation.trim() === '')) {
|
|
2364
2478
|
throw new TypeError('DECIDE bound deferred player continuation is invalid');
|
|
2365
2479
|
}
|
|
2366
2480
|
active.playerContinuation = playerContinuation;
|
|
2481
|
+
// DR-062 §4: the continuation's own baseline.
|
|
2482
|
+
active.baseline = baseline;
|
|
2367
2483
|
readiness.resolve({ status: 'ready' });
|
|
2368
2484
|
return active.result.promise;
|
|
2369
2485
|
},
|
|
@@ -2566,7 +2682,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2566
2682
|
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
2567
2683
|
...action,
|
|
2568
2684
|
}));
|
|
2569
|
-
const lastError =
|
|
2685
|
+
const lastError = state.stateId === 'failed'
|
|
2686
|
+
? failedStateError(context)
|
|
2687
|
+
: normalizeErrorFull(context.lastError);
|
|
2570
2688
|
const stateDescription = unresolved || state.stateId === undefined
|
|
2571
2689
|
? undefined
|
|
2572
2690
|
: STATE_DESCRIPTIONS[state.stateId];
|
|
@@ -2614,6 +2732,8 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2614
2732
|
const turnId = ++turnSequence;
|
|
2615
2733
|
const callId = `apply-${++applyCallSequence}`;
|
|
2616
2734
|
currentTurnId = turnId;
|
|
2735
|
+
playerCallFailures.clear();
|
|
2736
|
+
governedFailureOccurrences.clear();
|
|
2617
2737
|
currentSignal = signal;
|
|
2618
2738
|
currentAborts = abortReasonClassifier(signal);
|
|
2619
2739
|
controlPlaneError = undefined;
|
|
@@ -2862,7 +2982,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2862
2982
|
return abortedResult(signal);
|
|
2863
2983
|
}
|
|
2864
2984
|
if (state.activeStateIds.includes('failed')) {
|
|
2865
|
-
const error =
|
|
2985
|
+
const error = failedStateError(context);
|
|
2866
2986
|
return {
|
|
2867
2987
|
outcome: 'failed',
|
|
2868
2988
|
state,
|
|
@@ -3052,8 +3172,12 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3052
3172
|
const state = currentState();
|
|
3053
3173
|
if (state.status !== 'active' || !state.quiescent)
|
|
3054
3174
|
return undefined;
|
|
3055
|
-
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
3056
3175
|
const context = actor.getSnapshot().context;
|
|
3176
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
3177
|
+
// surface publishes it, completed from the runtime's own cause slot.
|
|
3178
|
+
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
|
|
3179
|
+
? { lastError: failedStateError(context) }
|
|
3180
|
+
: {});
|
|
3057
3181
|
if (deferredEffects !== undefined) {
|
|
3058
3182
|
synchronizeDeferredProjection(readEffectLedger());
|
|
3059
3183
|
}
|
|
@@ -3189,6 +3313,8 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3189
3313
|
}
|
|
3190
3314
|
const turnId = ++turnSequence;
|
|
3191
3315
|
currentTurnId = turnId;
|
|
3316
|
+
playerCallFailures.clear();
|
|
3317
|
+
governedFailureOccurrences.clear();
|
|
3192
3318
|
currentSignal = turn.signal;
|
|
3193
3319
|
currentAborts = abortReasonClassifier(turn.signal);
|
|
3194
3320
|
controlPlaneError = undefined;
|
|
@@ -3319,7 +3445,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3319
3445
|
: {
|
|
3320
3446
|
outcome: 'failed',
|
|
3321
3447
|
state,
|
|
3322
|
-
error:
|
|
3448
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
3323
3449
|
name: 'Error',
|
|
3324
3450
|
message: String(effectiveError),
|
|
3325
3451
|
},
|
|
@@ -3344,7 +3470,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3344
3470
|
result = {
|
|
3345
3471
|
outcome: drainAborted ? 'aborted' : 'failed',
|
|
3346
3472
|
state,
|
|
3347
|
-
error:
|
|
3473
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
3348
3474
|
name: 'Error',
|
|
3349
3475
|
message: String(effectiveError),
|
|
3350
3476
|
},
|