@sublang/playbook 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/slc/link.md +27 -11
- package/src/xstate-playbook-runtime.js +60 -19
- package/src/xstate-playbook-runtime.ts +81 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, and DISCUSS workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"dependencies": {
|
|
127
127
|
"@anthropic-ai/claude-agent-sdk": "^0.3.154",
|
|
128
128
|
"@openai/codex-sdk": "^0.139.0",
|
|
129
|
-
"@sublang/cligent": "^0.
|
|
129
|
+
"@sublang/cligent": "^0.16.0",
|
|
130
130
|
"@sublang/spex": "^0.3.0",
|
|
131
131
|
"p-queue": "^9.3.1",
|
|
132
132
|
"xstate": "^5.19.4",
|
package/slc/link.md
CHANGED
|
@@ -257,15 +257,21 @@ array requests a tool-free call, while omission preserves the host Captain's
|
|
|
257
257
|
configured tools.
|
|
258
258
|
`CaptainResult` carries no resume token or player-continuation selection.
|
|
259
259
|
A non-`ok`
|
|
260
|
-
result, or an `ok` result without `finalText`, shall
|
|
261
|
-
the
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
drive it to quiescence,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
result, or an `ok` result without non-empty `finalText`, shall record that
|
|
261
|
+
failure on the call's single finish trace and reject the actor through the
|
|
262
|
+
FSM's error path. These structured host-result failures are recoverable
|
|
263
|
+
workflow failures, not control-plane failures: the runtime shall let the actor
|
|
264
|
+
take `onError`, drive it to quiescence, drain ordered emissions, and resolve
|
|
265
|
+
the public method with `{ outcome: 'failed' }` carrying the failure state's
|
|
266
|
+
error. This matches the delegated-player result boundary.
|
|
267
|
+
A non-abort thrown `callCaptain` port, a malformed host result, and a rejecting
|
|
268
|
+
trace sink remain control-plane failures that reject the public method. If the
|
|
269
|
+
required finish sink rejects after a structured host-result failure, the
|
|
270
|
+
actor's error and failure-state evidence shall remain the host-result failure,
|
|
271
|
+
while the public method rejects with the sink failure surfaced by the turn's
|
|
272
|
+
emission drain. Absent such a control-plane failure, if the combined signal
|
|
273
|
+
has aborted, ordinary abort settlement remains authoritative after the actor
|
|
274
|
+
reaches its error path.
|
|
269
275
|
|
|
270
276
|
Every linked runtime owns a map from resolved player id to its latest non-empty `resumeToken`.
|
|
271
277
|
Before reading a resolved direct-Captain or delegated-player result, the
|
|
@@ -753,8 +759,11 @@ Adjudicator failures are control-plane errors.
|
|
|
753
759
|
The runtime shall propagate them by throwing out of `handleBossInput` after attempting cleanup.
|
|
754
760
|
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)).
|
|
755
761
|
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
|
|
757
|
-
reported as player failures
|
|
762
|
+
Direct-Captain host-result failures stay on the Captain actor boundary and
|
|
763
|
+
shall not be reported as player failures; they follow the recoverable FSM
|
|
764
|
+
failure path specified above. Captain transport, result-shape, trace-sink, and
|
|
765
|
+
adjudication failures remain control-plane errors unless the transport failure
|
|
766
|
+
is causally identical to the active abort signal.
|
|
758
767
|
Because XState still needs the invoked promise to settle, the linked runtime
|
|
759
768
|
shall latch an adjudicator, actor-output JSON-validation, or nested-boundary
|
|
760
769
|
control error outside machine context, allow the invocation's `onError` path to
|
|
@@ -1316,6 +1325,13 @@ The emitted module:
|
|
|
1316
1325
|
weaken, runtime-derived entry text ownership or closed interrupt targets.
|
|
1317
1326
|
A conflicting duplicate field contract is a linker/runtime construction
|
|
1318
1327
|
error.
|
|
1328
|
+
`NO_ACTION` and `BOSS_REPLY` are runtime-owned event types the factory
|
|
1329
|
+
supplies itself — `NO_ACTION` as exactly `{ type: 'NO_ACTION' }`, and
|
|
1330
|
+
`BOSS_REPLY` as an optional judge-selected `questionId` plus the exact-text
|
|
1331
|
+
`answer` the runtime attaches. `bossEvents` shall carry no entry for either
|
|
1332
|
+
type; supplying one is a construction error, so a linker that judges a
|
|
1333
|
+
runtime-owned arm to have lost payload detail under erasure shall report
|
|
1334
|
+
that gap rather than emit the entry.
|
|
1319
1335
|
- Default-exports the factory call as `createPlaybookRuntime`, typed
|
|
1320
1336
|
`PlaybookRuntimeFactory<PlaybookRuntimeOptions>`.
|
|
1321
1337
|
- Exposes, under an `_internal` export, the pure helpers verification
|
|
@@ -23,6 +23,25 @@ export const BOSS_REPLY_ERRORS = {
|
|
|
23
23
|
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
24
24
|
};
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
|
+
// A host agent result that is not `ok` (or is `ok` with no final text) is a
|
|
27
|
+
// recoverable FSM failure, not a control-plane error: it travels the invoked
|
|
28
|
+
// actor's XState error path to the failure state and the public boundary
|
|
29
|
+
// resolves `failed` (PBRT-47, matching the player boundary's PBRT-9). The
|
|
30
|
+
// direct-Captain boundary has to emit its paired finish trace before
|
|
31
|
+
// rethrowing, so it needs to tell that failure apart from the control-plane
|
|
32
|
+
// errors it does latch — a thrown port, a malformed result, a rejecting sink.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const fsmResultFailures = new WeakSet();
|
|
35
|
+
function markFsmResultFailure(error) {
|
|
36
|
+
fsmResultFailures.add(error);
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
function isFsmResultFailure(error) {
|
|
40
|
+
return (typeof error === 'object' &&
|
|
41
|
+
error !== null &&
|
|
42
|
+
fsmResultFailures.has(error));
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
26
45
|
// Tolerant judge-JSON recovery (slc/link.md §Boss-event mapping).
|
|
27
46
|
// ---------------------------------------------------------------------------
|
|
28
47
|
function isPlainObject(value) {
|
|
@@ -587,10 +606,20 @@ function configuredEventTypesForState(machine, stateId) {
|
|
|
587
606
|
}
|
|
588
607
|
return configured;
|
|
589
608
|
}
|
|
609
|
+
// Derived contracts merge into whatever the machine already yielded for the
|
|
610
|
+
// same type, so a deterministic entry event that shares a type with another
|
|
611
|
+
// derived contract keeps its exact-text ownership instead of being replaced.
|
|
612
|
+
function mergeDerivedContract(contracts, contract) {
|
|
613
|
+
const existing = contracts.get(contract.type);
|
|
614
|
+
contracts.set(contract.type, {
|
|
615
|
+
type: contract.type,
|
|
616
|
+
fields: { ...(existing?.fields ?? {}), ...(contract.fields ?? {}) },
|
|
617
|
+
});
|
|
618
|
+
}
|
|
590
619
|
function defaultBossEventSpecs(machine, entryEvent, supplied) {
|
|
591
620
|
const contracts = new Map();
|
|
592
621
|
if (entryEvent !== undefined) {
|
|
593
|
-
contracts
|
|
622
|
+
mergeDerivedContract(contracts, {
|
|
594
623
|
type: entryEvent.type,
|
|
595
624
|
fields: { [entryEvent.textField]: { source: 'text', required: true } },
|
|
596
625
|
});
|
|
@@ -601,7 +630,7 @@ function defaultBossEventSpecs(machine, entryEvent, supplied) {
|
|
|
601
630
|
: undefined;
|
|
602
631
|
const interruptTargets = rootInterrupt === undefined ? [] : transitionTargets(rootInterrupt);
|
|
603
632
|
if (interruptTargets.length > 0) {
|
|
604
|
-
contracts
|
|
633
|
+
mergeDerivedContract(contracts, {
|
|
605
634
|
type: 'BOSS_INTERRUPT',
|
|
606
635
|
fields: {
|
|
607
636
|
targetId: {
|
|
@@ -609,6 +638,10 @@ function defaultBossEventSpecs(machine, entryEvent, supplied) {
|
|
|
609
638
|
required: true,
|
|
610
639
|
values: [...new Set(interruptTargets)],
|
|
611
640
|
},
|
|
641
|
+
// slc/link.md §Boss-event mapping: for BOSS_INTENT and
|
|
642
|
+
// BOSS_INTERRUPT the runtime, never the judge, attaches the exact
|
|
643
|
+
// original Boss text as `bossIntent`.
|
|
644
|
+
bossIntent: { source: 'text', required: true },
|
|
612
645
|
},
|
|
613
646
|
});
|
|
614
647
|
}
|
|
@@ -839,8 +872,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
839
872
|
: {}),
|
|
840
873
|
};
|
|
841
874
|
const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
842
|
-
|
|
843
|
-
|
|
875
|
+
// Build the derived classifier unconditionally: it is the sole validator of
|
|
876
|
+
// supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
|
|
877
|
+
// fail factory construction whether or not this spec overrides the
|
|
878
|
+
// classifier that would have consumed the contracts.
|
|
879
|
+
const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? []);
|
|
880
|
+
const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
|
|
844
881
|
const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
|
|
845
882
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
846
883
|
const statusesForState = spec.statusesForState ?? defaultStatusesForState;
|
|
@@ -1221,18 +1258,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1221
1258
|
await emitTrace('captain.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1222
1259
|
throw error;
|
|
1223
1260
|
}
|
|
1261
|
+
// A non-`ok` host result is a recoverable FSM failure (PBRT-47), so
|
|
1262
|
+
// it is never latched as a control-plane error; it is still
|
|
1263
|
+
// authoritative for the actor's error path even when the required
|
|
1264
|
+
// finish emission fails or a coincident boundary abort lands.
|
|
1224
1265
|
let resultFailure;
|
|
1225
1266
|
if (result.status !== 'ok') {
|
|
1226
|
-
resultFailure = new Error(result.error ??
|
|
1227
|
-
`captainActor: callCaptain status "${result.status}"`);
|
|
1267
|
+
resultFailure = markFsmResultFailure(new Error(result.error ??
|
|
1268
|
+
`captainActor: callCaptain status "${result.status}"`));
|
|
1228
1269
|
}
|
|
1229
1270
|
else if (result.finalText === undefined || result.finalText === '') {
|
|
1230
|
-
resultFailure = new Error('captainActor: callCaptain returned status=ok with no finalText');
|
|
1231
|
-
}
|
|
1232
|
-
if (resultFailure !== undefined) {
|
|
1233
|
-
// The host result is authoritative even when the required finish
|
|
1234
|
-
// emission fails or triggers a coincident boundary abort.
|
|
1235
|
-
controlPlaneError ??= resultFailure;
|
|
1271
|
+
resultFailure = markFsmResultFailure(new Error('captainActor: callCaptain returned status=ok with no finalText'));
|
|
1236
1272
|
}
|
|
1237
1273
|
try {
|
|
1238
1274
|
await emitTrace('captain.call.finished', {
|
|
@@ -1290,12 +1326,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1290
1326
|
await drainEmissions();
|
|
1291
1327
|
const prompt = composeCaptainPrompt(input);
|
|
1292
1328
|
const result = await boundary.callCaptain(input, prompt, active);
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
if (result.
|
|
1298
|
-
throw new Error('captainActor:
|
|
1329
|
+
// The boundary owns result validation (PBRT-47) and throws the
|
|
1330
|
+
// authoritative failure itself, so a returned result is always
|
|
1331
|
+
// `ok` with visible text. Assert that invariant rather than
|
|
1332
|
+
// restating the failure semantics, which would drift.
|
|
1333
|
+
if (result.status !== 'ok' || !result.finalText) {
|
|
1334
|
+
throw new Error('captainActor: boundary returned an unvalidated Captain result');
|
|
1299
1335
|
}
|
|
1300
1336
|
const judgePrompt = buildCaptainJudgePrompt(input, result.finalText);
|
|
1301
1337
|
const raw = await boundary.callJudge('captain-output-adjudication', input.stateId, judgePrompt, active);
|
|
@@ -1304,8 +1340,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1304
1340
|
return output;
|
|
1305
1341
|
}
|
|
1306
1342
|
catch (error) {
|
|
1307
|
-
|
|
1343
|
+
// A host-reported Captain result failure routes to the FSM's
|
|
1344
|
+
// failure state (PBRT-47); everything else here — a drained
|
|
1345
|
+
// emission failure, prompt composition, the port itself,
|
|
1346
|
+
// adjudication — is control plane.
|
|
1347
|
+
if (!active.aborted && !isFsmResultFailure(error)) {
|
|
1308
1348
|
controlPlaneError ??= error;
|
|
1349
|
+
}
|
|
1309
1350
|
throw error;
|
|
1310
1351
|
}
|
|
1311
1352
|
});
|
|
@@ -152,6 +152,31 @@ export const BOSS_REPLY_ERRORS = {
|
|
|
152
152
|
`state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
153
153
|
} as const;
|
|
154
154
|
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// A host agent result that is not `ok` (or is `ok` with no final text) is a
|
|
157
|
+
// recoverable FSM failure, not a control-plane error: it travels the invoked
|
|
158
|
+
// actor's XState error path to the failure state and the public boundary
|
|
159
|
+
// resolves `failed` (PBRT-47, matching the player boundary's PBRT-9). The
|
|
160
|
+
// direct-Captain boundary has to emit its paired finish trace before
|
|
161
|
+
// rethrowing, so it needs to tell that failure apart from the control-plane
|
|
162
|
+
// errors it does latch — a thrown port, a malformed result, a rejecting sink.
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const fsmResultFailures = new WeakSet<object>();
|
|
166
|
+
|
|
167
|
+
function markFsmResultFailure(error: Error): Error {
|
|
168
|
+
fsmResultFailures.add(error);
|
|
169
|
+
return error;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isFsmResultFailure(error: unknown): boolean {
|
|
173
|
+
return (
|
|
174
|
+
typeof error === 'object' &&
|
|
175
|
+
error !== null &&
|
|
176
|
+
fsmResultFailures.has(error as object)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
155
180
|
// ---------------------------------------------------------------------------
|
|
156
181
|
// The per-workflow spec. Every strategy member has a generic default derived
|
|
157
182
|
// from the FSM artifact's own data, so a linker-emitted thin module normally
|
|
@@ -940,6 +965,20 @@ function configuredEventTypesForState(
|
|
|
940
965
|
return configured;
|
|
941
966
|
}
|
|
942
967
|
|
|
968
|
+
// Derived contracts merge into whatever the machine already yielded for the
|
|
969
|
+
// same type, so a deterministic entry event that shares a type with another
|
|
970
|
+
// derived contract keeps its exact-text ownership instead of being replaced.
|
|
971
|
+
function mergeDerivedContract(
|
|
972
|
+
contracts: Map<string, XStateBossEventSpec>,
|
|
973
|
+
contract: XStateBossEventSpec,
|
|
974
|
+
): void {
|
|
975
|
+
const existing = contracts.get(contract.type);
|
|
976
|
+
contracts.set(contract.type, {
|
|
977
|
+
type: contract.type,
|
|
978
|
+
fields: { ...(existing?.fields ?? {}), ...(contract.fields ?? {}) },
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
943
982
|
function defaultBossEventSpecs(
|
|
944
983
|
machine: AnyStateMachine,
|
|
945
984
|
entryEvent: { type: string; textField: string } | undefined,
|
|
@@ -947,7 +986,7 @@ function defaultBossEventSpecs(
|
|
|
947
986
|
): ReadonlyMap<string, XStateBossEventSpec> {
|
|
948
987
|
const contracts = new Map<string, XStateBossEventSpec>();
|
|
949
988
|
if (entryEvent !== undefined) {
|
|
950
|
-
contracts
|
|
989
|
+
mergeDerivedContract(contracts, {
|
|
951
990
|
type: entryEvent.type,
|
|
952
991
|
fields: { [entryEvent.textField]: { source: 'text', required: true } },
|
|
953
992
|
});
|
|
@@ -961,7 +1000,7 @@ function defaultBossEventSpecs(
|
|
|
961
1000
|
const interruptTargets =
|
|
962
1001
|
rootInterrupt === undefined ? [] : transitionTargets(rootInterrupt);
|
|
963
1002
|
if (interruptTargets.length > 0) {
|
|
964
|
-
contracts
|
|
1003
|
+
mergeDerivedContract(contracts, {
|
|
965
1004
|
type: 'BOSS_INTERRUPT',
|
|
966
1005
|
fields: {
|
|
967
1006
|
targetId: {
|
|
@@ -969,6 +1008,10 @@ function defaultBossEventSpecs(
|
|
|
969
1008
|
required: true,
|
|
970
1009
|
values: [...new Set(interruptTargets)],
|
|
971
1010
|
},
|
|
1011
|
+
// slc/link.md §Boss-event mapping: for BOSS_INTENT and
|
|
1012
|
+
// BOSS_INTERRUPT the runtime, never the judge, attaches the exact
|
|
1013
|
+
// original Boss text as `bossIntent`.
|
|
1014
|
+
bossIntent: { source: 'text', required: true },
|
|
972
1015
|
},
|
|
973
1016
|
});
|
|
974
1017
|
}
|
|
@@ -1313,9 +1356,16 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1313
1356
|
};
|
|
1314
1357
|
const extractFields =
|
|
1315
1358
|
spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1359
|
+
// Build the derived classifier unconditionally: it is the sole validator of
|
|
1360
|
+
// supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
|
|
1361
|
+
// fail factory construction whether or not this spec overrides the
|
|
1362
|
+
// classifier that would have consumed the contracts.
|
|
1363
|
+
const derivedClassifyBossText = makeDefaultClassifyBossText(
|
|
1364
|
+
machine,
|
|
1365
|
+
spec.entryEvent,
|
|
1366
|
+
spec.bossEvents ?? [],
|
|
1367
|
+
);
|
|
1368
|
+
const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
|
|
1319
1369
|
const normalizeTransitionEvent =
|
|
1320
1370
|
spec.normalizeTransitionEvent ??
|
|
1321
1371
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
@@ -1815,22 +1865,25 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1815
1865
|
);
|
|
1816
1866
|
throw error;
|
|
1817
1867
|
}
|
|
1868
|
+
// A non-`ok` host result is a recoverable FSM failure (PBRT-47), so
|
|
1869
|
+
// it is never latched as a control-plane error; it is still
|
|
1870
|
+
// authoritative for the actor's error path even when the required
|
|
1871
|
+
// finish emission fails or a coincident boundary abort lands.
|
|
1818
1872
|
let resultFailure: Error | undefined;
|
|
1819
1873
|
if (result.status !== 'ok') {
|
|
1820
|
-
resultFailure =
|
|
1821
|
-
|
|
1822
|
-
|
|
1874
|
+
resultFailure = markFsmResultFailure(
|
|
1875
|
+
new Error(
|
|
1876
|
+
result.error ??
|
|
1877
|
+
`captainActor: callCaptain status "${result.status}"`,
|
|
1878
|
+
),
|
|
1823
1879
|
);
|
|
1824
1880
|
} else if (result.finalText === undefined || result.finalText === '') {
|
|
1825
|
-
resultFailure =
|
|
1826
|
-
|
|
1881
|
+
resultFailure = markFsmResultFailure(
|
|
1882
|
+
new Error(
|
|
1883
|
+
'captainActor: callCaptain returned status=ok with no finalText',
|
|
1884
|
+
),
|
|
1827
1885
|
);
|
|
1828
1886
|
}
|
|
1829
|
-
if (resultFailure !== undefined) {
|
|
1830
|
-
// The host result is authoritative even when the required finish
|
|
1831
|
-
// emission fails or triggers a coincident boundary abort.
|
|
1832
|
-
controlPlaneError ??= resultFailure;
|
|
1833
|
-
}
|
|
1834
1887
|
try {
|
|
1835
1888
|
await emitTrace(
|
|
1836
1889
|
'captain.call.finished',
|
|
@@ -1903,15 +1956,13 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1903
1956
|
await drainEmissions();
|
|
1904
1957
|
const prompt = composeCaptainPrompt(input);
|
|
1905
1958
|
const result = await boundary.callCaptain!(input, prompt, active);
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
}
|
|
1912
|
-
if (result.finalText === undefined || result.finalText === '') {
|
|
1959
|
+
// The boundary owns result validation (PBRT-47) and throws the
|
|
1960
|
+
// authoritative failure itself, so a returned result is always
|
|
1961
|
+
// `ok` with visible text. Assert that invariant rather than
|
|
1962
|
+
// restating the failure semantics, which would drift.
|
|
1963
|
+
if (result.status !== 'ok' || !result.finalText) {
|
|
1913
1964
|
throw new Error(
|
|
1914
|
-
'captainActor:
|
|
1965
|
+
'captainActor: boundary returned an unvalidated Captain result',
|
|
1915
1966
|
);
|
|
1916
1967
|
}
|
|
1917
1968
|
const judgePrompt = buildCaptainJudgePrompt(
|
|
@@ -1933,7 +1984,13 @@ export function createXStatePlaybookRuntime<TOptions>(
|
|
|
1933
1984
|
validateBossReplyOutput(input, output, resumableStateIds);
|
|
1934
1985
|
return output;
|
|
1935
1986
|
} catch (error) {
|
|
1936
|
-
|
|
1987
|
+
// A host-reported Captain result failure routes to the FSM's
|
|
1988
|
+
// failure state (PBRT-47); everything else here — a drained
|
|
1989
|
+
// emission failure, prompt composition, the port itself,
|
|
1990
|
+
// adjudication — is control plane.
|
|
1991
|
+
if (!active.aborted && !isFsmResultFailure(error)) {
|
|
1992
|
+
controlPlaneError ??= error;
|
|
1993
|
+
}
|
|
1937
1994
|
throw error;
|
|
1938
1995
|
}
|
|
1939
1996
|
},
|