@sublang/playbook 17.10.1 → 17.11.1
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/docs/configuration.md +16 -0
- package/docs/releases/17.11.1-preparation.md +37 -0
- package/package.json +2 -2
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/code.fsm.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +18 -1
- package/reference/sdlc/code.playbook/playbook-captain.ts +19 -1
- package/slc/link.md +12 -3
- package/slc/text2gears.md +6 -3
- package/src/xstate-playbook-runtime.js +33 -5
- package/src/xstate-playbook-runtime.ts +63 -14
package/docs/configuration.md
CHANGED
|
@@ -304,6 +304,22 @@ files, so keep their locations available for resumed runs. Ordinary Captain
|
|
|
304
304
|
routing, judging and replies receive `skills: []`; bounded recovery preparation
|
|
305
305
|
can use the Captain's configured skills.
|
|
306
306
|
|
|
307
|
+
Agents also find skills by themselves in their project and user skill folders,
|
|
308
|
+
such as `.claude/skills` and `.agents/skills`. Captain routing, judging and
|
|
309
|
+
replies turn that discovery off as far as the agent allows, so a skill
|
|
310
|
+
installed there no longer steers them; what an agent keeps regardless stays,
|
|
311
|
+
such as OpenCode's configuration-directory skills and plugin or extension
|
|
312
|
+
skills. Kimi Code has no switch for this, so a Kimi Captain still reads those
|
|
313
|
+
folders. Working players and recovery preparation keep them.
|
|
314
|
+
|
|
315
|
+
A session whose Captain conversation began before Captain control turned this
|
|
316
|
+
discovery off, with the Playbook release that requires Cligent 0.36.0, keeps the
|
|
317
|
+
skill listings already in that conversation; start a new session for full
|
|
318
|
+
isolation. A Captain whose agent offers the switch but cannot apply it where it
|
|
319
|
+
runs now fails its control calls with an error, for example a Gemini CLI Captain
|
|
320
|
+
under its native sandbox or run from the home directory, or a Codex Captain with
|
|
321
|
+
an unreadable skill root.
|
|
322
|
+
|
|
307
323
|
## Browser capability
|
|
308
324
|
|
|
309
325
|
A Captain or player agent block may set `browser: true` to make Cligent's managed
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai> -->
|
|
3
|
+
|
|
4
|
+
# Playbook 17.11.1 preparation
|
|
5
|
+
|
|
6
|
+
This record captures the initial preparation checkpoint on 2026-10-21,
|
|
7
|
+
before tagging or release. Subsequent gate outcomes are recorded separately.
|
|
8
|
+
|
|
9
|
+
This patch release carries the classified-entry lifecycle fix from
|
|
10
|
+
`f31250de548d78f2576f753e5f564e1988604b6f` ([IR-116](https://github.com/sublang-ai/playbook/blob/main/specs/intents/116-classified-entry-lifecycle.md)).
|
|
11
|
+
It preserves required classification fields and exact Boss text on terminal
|
|
12
|
+
restart, and clears earlier run exchanges only for a valid new entry.
|
|
13
|
+
Refused entries, replies and global interrupts retain the current run.
|
|
14
|
+
Dependencies and the locked Cligent 0.36.0 binding are unchanged.
|
|
15
|
+
|
|
16
|
+
The fix candidate passed the canonical model-free integration checks: build,
|
|
17
|
+
spec lint, 2,842 main tests with 24 skips, the separate 146 capability tests,
|
|
18
|
+
link checks and SPDX checks. The shared-runtime segment includes 261 tests.
|
|
19
|
+
Those results precede this version/changelog/record-only preparation; they do
|
|
20
|
+
not establish packed or real-agent acceptance for 17.11.1.
|
|
21
|
+
The historical failed broad run remains qualified in IR-116.
|
|
22
|
+
|
|
23
|
+
At that checkpoint, the remaining pre-tag release gates were the full canonical
|
|
24
|
+
`pnpm smoke:release` followed by `pnpm test:acceptance`, compiled-sibling and CI
|
|
25
|
+
verification, and review/commit/push to main under
|
|
26
|
+
[release-10](https://github.com/sublang-ai/playbook/blob/main/specs/packages/release.md#release-10).
|
|
27
|
+
Neither smoke nor real-agent acceptance had run at that checkpoint.
|
|
28
|
+
The real-agent gate must request `gpt-6.1-sol` and `claude-opus-5-5` through
|
|
29
|
+
`PLAYBOOK_ACCEPTANCE_CODEX_MODEL` and `PLAYBOOK_ACCEPTANCE_CLAUDE_MODEL`.
|
|
30
|
+
Requested model names alone do not independently identify the serving backend.
|
|
31
|
+
|
|
32
|
+
This change does not alter interactive presentation/layout or the declared or
|
|
33
|
+
locked Cligent version, so it does not itself trigger the conditional manual
|
|
34
|
+
presentation check in [release-26](https://github.com/sublang-ai/playbook/blob/main/specs/packages/release.md#release-26).
|
|
35
|
+
Reassess that condition if the release inputs change.
|
|
36
|
+
Raw live logs and retained scratch evidence remain private outside the repository.
|
|
37
|
+
No publication, installation or adoption is claimed by this record.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.11.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, PR, and INSPECT workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
"provenance": true
|
|
281
281
|
},
|
|
282
282
|
"dependencies": {
|
|
283
|
-
"@sublang/cligent": "^0.
|
|
283
|
+
"@sublang/cligent": "^0.36.0",
|
|
284
284
|
"@sublang/spex": "^3.0.0",
|
|
285
285
|
"p-queue": "^9.3.1",
|
|
286
286
|
"xstate": "^5.19.4",
|
package/reference/sdlc/code.md
CHANGED
|
@@ -91,7 +91,6 @@ Every new-intent phase has two semantic outcomes: direct implementation and new
|
|
|
91
91
|
Every IR-task phase, including the first phase for an existing IR, identifies the IR and the implemented task, and has two semantic outcomes: more tasks and final task.
|
|
92
92
|
The first phase therefore has four semantic outcomes — direct implementation, new IR, more tasks, and final task — plus the Boss question when the continued IR is ambiguous.
|
|
93
93
|
Each semantic outcome requires affirmative support in Coder's result, but no phase transition shall depend on a fixed presentation format of Coder's reply.
|
|
94
|
-
The Boss-question outcome means that, at the end of this call, Coder has an unanswered clarifying question for Boss whose answer is required to complete the current coding phase. Earlier interim questions resolved or superseded by later affirmative current-phase completion, and prerequisites only for later review, acceptance or release stages, do not establish this outcome.
|
|
95
94
|
A commit statement alone does not establish semantic completion, and contradictory or insufficient evidence supports no forced commit outcome.
|
|
96
95
|
Captain shall use the repository-effect receipt as the authoritative identity of the phase's new commit.
|
|
97
96
|
|
|
@@ -84,7 +84,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
|
|
|
84
84
|
'',
|
|
85
85
|
'> Current IR task: <ir-task>',
|
|
86
86
|
].join('\n');
|
|
87
|
-
const NEEDS_BOSS_REPLY_DESCRIPTION = "
|
|
87
|
+
const NEEDS_BOSS_REPLY_DESCRIPTION = "The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
88
88
|
const FIRST_PHASE_RESULT = {
|
|
89
89
|
directCommit: "Coder completed the new coding intent as one direct implementation phase and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>` and `latestCommit: <commit identity>`.",
|
|
90
90
|
irCommit: "Coder decomposed the new coding intent into a new IR, implemented no IR task, and made its one new commit; Coder's result affirmatively supports this outcome, and no fixed presentation format of the reply is required. Output shall include `coderOutput: <verbatim final text>`, `latestCommit: <commit identity>`, and `irNumber` identifying the created IR.",
|
|
@@ -326,7 +326,7 @@ const REVIEW_IR_TASK_PHASE_TEMPLATE = [
|
|
|
326
326
|
].join('\n');
|
|
327
327
|
|
|
328
328
|
const NEEDS_BOSS_REPLY_DESCRIPTION =
|
|
329
|
-
"
|
|
329
|
+
"The acting agent's prose surfaces a clarifying question for Boss that the agent cannot answer alone. Output shall include `question: <verbatim question text from the acting agent's prose>`.";
|
|
330
330
|
|
|
331
331
|
const FIRST_PHASE_RESULT = {
|
|
332
332
|
directCommit:
|
|
@@ -5,6 +5,7 @@ import { posix, win32 } from 'node:path';
|
|
|
5
5
|
import { isDeepStrictEqual } from 'node:util';
|
|
6
6
|
import PQueue from 'p-queue';
|
|
7
7
|
import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
|
|
8
|
+
import { getNativeSkillSupport } from '@sublang/cligent';
|
|
8
9
|
import { assertPlaybookFailureCause } from '@sublang/playbook/runtime';
|
|
9
10
|
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, normalizeError, parseJudgeJson, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
10
11
|
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
@@ -2395,7 +2396,23 @@ function callSettings(agent, tuning = agent) {
|
|
|
2395
2396
|
};
|
|
2396
2397
|
}
|
|
2397
2398
|
function controlCallSettings(agent) {
|
|
2398
|
-
return {
|
|
2399
|
+
return {
|
|
2400
|
+
...callSettings(agent),
|
|
2401
|
+
browser: false,
|
|
2402
|
+
skills: [],
|
|
2403
|
+
mcpServers: {},
|
|
2404
|
+
...controlNativeSkills(agent.adapter),
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
// The native-skill half of a decision, Judge or reporting call's settings
|
|
2408
|
+
// (playbook-captain-88). Only an adapter Cligent reports can honor the switch
|
|
2409
|
+
// is asked to; tmux-play rejects `false` for any other, so a Kimi Captain
|
|
2410
|
+
// keeps its runtime's own skills rather than failing every control call
|
|
2411
|
+
// (DR-093). Working calls never carry it.
|
|
2412
|
+
function controlNativeSkills(adapter) {
|
|
2413
|
+
return getNativeSkillSupport(adapter)?.disableSupported === true
|
|
2414
|
+
? { nativeSkills: false }
|
|
2415
|
+
: {};
|
|
2399
2416
|
}
|
|
2400
2417
|
function promptIdentity(binding) {
|
|
2401
2418
|
return binding.model.kind === 'value'
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type PlayerAdapterName,
|
|
17
17
|
type TuningSelection,
|
|
18
18
|
} from '@sublang/cligent/tmux-play';
|
|
19
|
+
import { getNativeSkillSupport } from '@sublang/cligent';
|
|
19
20
|
import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
20
21
|
import type {
|
|
21
22
|
JsonValue,
|
|
@@ -4173,7 +4174,24 @@ function callSettings(
|
|
|
4173
4174
|
}
|
|
4174
4175
|
|
|
4175
4176
|
function controlCallSettings(agent: SessionAgent): AgentCallSettings {
|
|
4176
|
-
return {
|
|
4177
|
+
return {
|
|
4178
|
+
...callSettings(agent),
|
|
4179
|
+
browser: false,
|
|
4180
|
+
skills: [],
|
|
4181
|
+
mcpServers: {},
|
|
4182
|
+
...controlNativeSkills(agent.adapter),
|
|
4183
|
+
};
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
// The native-skill half of a decision, Judge or reporting call's settings
|
|
4187
|
+
// (playbook-captain-88). Only an adapter Cligent reports can honor the switch
|
|
4188
|
+
// is asked to; tmux-play rejects `false` for any other, so a Kimi Captain
|
|
4189
|
+
// keeps its runtime's own skills rather than failing every control call
|
|
4190
|
+
// (DR-093). Working calls never carry it.
|
|
4191
|
+
function controlNativeSkills(adapter: string): { nativeSkills?: false } {
|
|
4192
|
+
return getNativeSkillSupport(adapter)?.disableSupported === true
|
|
4193
|
+
? { nativeSkills: false }
|
|
4194
|
+
: {};
|
|
4177
4195
|
}
|
|
4178
4196
|
|
|
4179
4197
|
function promptIdentity(binding: EffectivePlayerBinding): string {
|
package/slc/link.md
CHANGED
|
@@ -1099,7 +1099,7 @@ Before the composer's result it places, in order:
|
|
|
1099
1099
|
- once the run has heard a Boss reply, the run's Boss exchanges so far, each a `? ⟶` line and the verbatim question, then a `Boss ⟶` line and the verbatim reply, whatever the role and whether or not the Source retains them, leaving out the one exchange the call's own continuation carries;
|
|
1100
1100
|
- for a call after a Boss reply, the marked verbatim question, only for a fresh conversation, and the marked verbatim reply.
|
|
1101
1101
|
|
|
1102
|
-
A run begins at
|
|
1102
|
+
A run begins at a deterministic entry or an event declared on the initial parked state itself, including a classified entry from recoverable failure, or at a finished machine's restart; a reply, global interrupt alone, or refused event starts no run. Its exchanges travel in the runtime snapshot as `bossExchanges`.
|
|
1103
1103
|
|
|
1104
1104
|
The runtime chooses the compact prompt only after selecting a conversation token and carries the complete prompt as optional `PlayerCallOptions.freshPrompt`.
|
|
1105
1105
|
A host starting fresh after a definite pre-execution token rejection uses `freshPrompt`, when supplied, instead of the compact prompt.
|
|
@@ -1181,6 +1181,15 @@ host's teardown resolution — and invokes no classifier judge call; the exact
|
|
|
1181
1181
|
original text still rides only the runtime-owned textual payload field.
|
|
1182
1182
|
All other non-empty turns shall use `callJudge` only to choose one of the FSM's
|
|
1183
1183
|
event kinds and non-text routing fields, or no FSM action.
|
|
1184
|
+
An entry requiring a judge-selected routing field therefore omits deterministic
|
|
1185
|
+
`entryEvent` metadata and declares its exact fields through `bossEvents`.
|
|
1186
|
+
The shared engine derives that entry from the initial parked state's own
|
|
1187
|
+
transitions and resets the run's exchanges when it dispatches the entry.
|
|
1188
|
+
At a terminal state, the classifier uses those initial entry transitions and
|
|
1189
|
+
their guards under detached bound machine input. This evaluates pure context
|
|
1190
|
+
and assignment initialization under the supported FSM contract; it does not
|
|
1191
|
+
run returned executable actions or start actors. Global interrupts and reply
|
|
1192
|
+
events are not terminal entries.
|
|
1184
1193
|
The classifier prompt shall include the exact, unmodified `turn.text` in a
|
|
1185
1194
|
clearly labelled Boss-message block so the judge can make that choice. Omitting
|
|
1186
1195
|
the message makes a parked-state classifier unable to distinguish an answer,
|
|
@@ -1849,9 +1858,9 @@ The `PlaybookRuntime` shall:
|
|
|
1849
1858
|
cannot be recovered or validated, emit the one recovery status required
|
|
1850
1859
|
by §Boss-event mapping, send no event, leave the actor unchanged, and
|
|
1851
1860
|
return `no-action` after the ordinary settled boundary drains.
|
|
1852
|
-
3. Only after classification produces a
|
|
1861
|
+
3. Only after classification produces a valid initial entry event, if the actor is in a
|
|
1853
1862
|
`final` state, dispose and reconstruct it — `final` is terminal and cannot
|
|
1854
|
-
accept new events. `NO_ACTION`, classifier rejection, and malformed
|
|
1863
|
+
accept new events. `NO_ACTION`, classifier rejection, invalid initial guards, and malformed
|
|
1855
1864
|
classification shall leave a terminal actor untouched.
|
|
1856
1865
|
4. Bind the active public-boundary signal and send the classified event to
|
|
1857
1866
|
the actor.
|
package/slc/text2gears.md
CHANGED
|
@@ -229,7 +229,7 @@ The annotation makes the field runtime-owned: the adjudicator selects the result
|
|
|
229
229
|
A distinct typed field extracted from that response remains judge-authored even when a later prompt quotes its exact value; quoting a field does not turn it into the player's whole final response.
|
|
230
230
|
One property name shall not be annotated as verbatim in one result contract and judge-authored in another; text2gears shall choose distinct properties or report that the Source cannot be represented by the current contract.
|
|
231
231
|
|
|
232
|
-
A Source statement that qualifies an outcome's evidence — what affirmatively
|
|
232
|
+
A Source statement that qualifies an authored domain outcome's evidence — what affirmatively
|
|
233
233
|
supports it, what supports no outcome, such as "a progress report, status
|
|
234
234
|
update, or promise of a later result supports no review outcome" — shall be
|
|
235
235
|
carried in that outcome's result description, the only text the adjudicator
|
|
@@ -244,7 +244,10 @@ explicitly requires the acting agent to show that machine syntax to the user.
|
|
|
244
244
|
It shall move Source's outcome contract into `Results:` while preserving the
|
|
245
245
|
human domain instructions in the blockquote.
|
|
246
246
|
It shall not emit the framework-owned `needsBossReply` result; gears2fsm adds
|
|
247
|
-
that universal result for every Captain- or player-invoking state.
|
|
247
|
+
that universal result with its standard description for every Captain- or player-invoking state.
|
|
248
|
+
The framework-owned question is interpreted under the runtime's current-call
|
|
249
|
+
adjudication rule; it is not an authored domain outcome whose description
|
|
250
|
+
Source overrides.
|
|
248
251
|
|
|
249
252
|
Where Source restricts an initial Captain to routing, text2gears shall preserve
|
|
250
253
|
only the authored question and delegation outcomes and shall not infer a
|
|
@@ -255,7 +258,7 @@ outcome Source leaves unqualified carries no `Results:` label; downstream,
|
|
|
255
258
|
[gears2fsm](gears2fsm.md#setup) gives its state the default single-outcome
|
|
256
259
|
contract, so text2gears shall not invent a one-bullet `Results:` block for it.
|
|
257
260
|
When a later item does consume its output, the produced-value rule above
|
|
258
|
-
applies instead; when Source qualifies the one outcome's evidence — what
|
|
261
|
+
applies instead; when Source qualifies the one authored domain outcome's evidence — what
|
|
259
262
|
affirmatively supports it, what supports no outcome — the behavior carries
|
|
260
263
|
exactly one `Results:` bullet naming that outcome and carrying the
|
|
261
264
|
qualification, with an output property only where a consumer requires one,
|
|
@@ -19,7 +19,7 @@ import { spawn } from 'node:child_process';
|
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
20
|
import { isDeepStrictEqual } from 'node:util';
|
|
21
21
|
import PQueue from 'p-queue';
|
|
22
|
-
import { createActor, fromPromise } from 'xstate';
|
|
22
|
+
import { createActor, fromPromise, initialTransition } from 'xstate';
|
|
23
23
|
import { ACCEPTED_OUTCOME_ACTION_TYPE, createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
|
|
24
24
|
import { assertPlaybookFailureCause } from './runtime.js';
|
|
25
25
|
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, attachPlaybookFailureCause, combineAbortSignals, createNestedPlaybookBridge, describePlaybookSemanticValueShape, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
|
|
@@ -2490,7 +2490,7 @@ function activeStateIdsOf(snapshotOrState) {
|
|
|
2490
2490
|
}
|
|
2491
2491
|
function makeDefaultClassifyBossText(machine, entryEvent, bossEvents, parallelProfile) {
|
|
2492
2492
|
const contracts = defaultBossEventSpecs(machine, entryEvent, bossEvents);
|
|
2493
|
-
return async (text, ports, signal, snapshotOrState, boundary) => {
|
|
2493
|
+
return async (text, ports, signal, snapshotOrState, boundary, allowedEventTypes) => {
|
|
2494
2494
|
const trimmed = text.trim();
|
|
2495
2495
|
if (trimmed === '')
|
|
2496
2496
|
return undefined;
|
|
@@ -2523,6 +2523,7 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents, parallelPr
|
|
|
2523
2523
|
const severalPending = pendingQuestions.length > 1;
|
|
2524
2524
|
const applicable = [...contracts.values()]
|
|
2525
2525
|
.filter((contract) => configuredTypes.has(contract.type) &&
|
|
2526
|
+
(allowedEventTypes === undefined || allowedEventTypes.has(contract.type)) &&
|
|
2526
2527
|
(contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0))
|
|
2527
2528
|
.map((contract) => contract.type === 'BOSS_REPLY' && severalPending
|
|
2528
2529
|
? {
|
|
@@ -2807,7 +2808,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2807
2808
|
// fail factory construction whether or not this spec overrides the
|
|
2808
2809
|
// classifier that would have consumed the contracts.
|
|
2809
2810
|
const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? [], parallelProfile);
|
|
2810
|
-
|
|
2811
|
+
// Initial-state-owned events describe new runs independently of whether
|
|
2812
|
+
// their payload is deterministic. Root interrupts are deliberately absent.
|
|
2813
|
+
const initialEntryEventTypes = new Set((machine.root.initial.target ?? []).flatMap((node) => Object.keys(node.config.on ?? {}).filter((type) => type !== 'BOSS_REPLY' && type !== 'NO_ACTION')));
|
|
2811
2814
|
const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
|
|
2812
2815
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
2813
2816
|
const statusesForState = spec.statusesForState ??
|
|
@@ -3619,7 +3622,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3619
3622
|
}
|
|
3620
3623
|
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
3621
3624
|
function beginRunIfEntry(event) {
|
|
3622
|
-
if (
|
|
3625
|
+
if ((event.type === spec.entryEvent?.type || initialEntryEventTypes.has(event.type)) &&
|
|
3626
|
+
snapshotCan(actor?.getSnapshot(), event)) {
|
|
3623
3627
|
bossExchanges = [];
|
|
3624
3628
|
}
|
|
3625
3629
|
}
|
|
@@ -7780,7 +7784,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
7780
7784
|
};
|
|
7781
7785
|
}
|
|
7782
7786
|
else {
|
|
7783
|
-
|
|
7787
|
+
// Classify a finished run against its fresh entry context,
|
|
7788
|
+
// without starting/replacing an actor or running the returned
|
|
7789
|
+
// executable actions. Pure context/assign initialization is
|
|
7790
|
+
// evaluated under the supported FSM contract.
|
|
7791
|
+
// Detach the input so pure initialization cannot mutate the
|
|
7792
|
+
// current actor or the host's bound input through an alias.
|
|
7793
|
+
const input = terminal
|
|
7794
|
+
? machineInput(boundOptions, requireSession())
|
|
7795
|
+
: undefined;
|
|
7796
|
+
const classificationSnapshot = terminal
|
|
7797
|
+
? initialTransition(machine, input === undefined
|
|
7798
|
+
? undefined
|
|
7799
|
+
: snapshotJsonValue(input, 'initial classification input'))[0]
|
|
7800
|
+
: snapshot;
|
|
7801
|
+
event = spec.classifyBossText !== undefined
|
|
7802
|
+
? await spec.classifyBossText(text, runtimePorts, signal, classificationSnapshot, boundary, boundOptions)
|
|
7803
|
+
: await derivedClassifyBossText(text, runtimePorts, signal, classificationSnapshot, boundary, terminal ? initialEntryEventTypes : undefined);
|
|
7804
|
+
if (terminal &&
|
|
7805
|
+
event !== undefined &&
|
|
7806
|
+
(!initialEntryEventTypes.has(event.type) ||
|
|
7807
|
+
(spec.classifyBossText !== undefined &&
|
|
7808
|
+
!snapshotCan(classificationSnapshot, event)))) {
|
|
7809
|
+
await runtimePorts.emitStatus('Classifier selected an invalid initial entry event');
|
|
7810
|
+
event = undefined;
|
|
7811
|
+
}
|
|
7784
7812
|
}
|
|
7785
7813
|
signal.throwIfAborted();
|
|
7786
7814
|
}
|
|
@@ -20,7 +20,7 @@ import { spawn } from 'node:child_process';
|
|
|
20
20
|
import { randomUUID } from 'node:crypto';
|
|
21
21
|
import { isDeepStrictEqual } from 'node:util';
|
|
22
22
|
import PQueue from 'p-queue';
|
|
23
|
-
import { createActor, fromPromise } from 'xstate';
|
|
23
|
+
import { createActor, fromPromise, initialTransition } from 'xstate';
|
|
24
24
|
import type {
|
|
25
25
|
AnyStateMachine,
|
|
26
26
|
EventObject,
|
|
@@ -3915,9 +3915,10 @@ function makeDefaultClassifyBossText(
|
|
|
3915
3915
|
signal: AbortSignal,
|
|
3916
3916
|
snapshotOrState: unknown,
|
|
3917
3917
|
boundary?: RuntimeBoundaryCalls,
|
|
3918
|
+
allowedEventTypes?: ReadonlySet<string>,
|
|
3918
3919
|
) => Promise<EventObject | undefined> {
|
|
3919
3920
|
const contracts = defaultBossEventSpecs(machine, entryEvent, bossEvents);
|
|
3920
|
-
return async (text, ports, signal, snapshotOrState, boundary) => {
|
|
3921
|
+
return async (text, ports, signal, snapshotOrState, boundary, allowedEventTypes) => {
|
|
3921
3922
|
const trimmed = text.trim();
|
|
3922
3923
|
if (trimmed === '') return undefined;
|
|
3923
3924
|
const state = classifierState(snapshotOrState);
|
|
@@ -3959,6 +3960,7 @@ function makeDefaultClassifyBossText(
|
|
|
3959
3960
|
.filter(
|
|
3960
3961
|
(contract) =>
|
|
3961
3962
|
configuredTypes.has(contract.type) &&
|
|
3963
|
+
(allowedEventTypes === undefined || allowedEventTypes.has(contract.type)) &&
|
|
3962
3964
|
(contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0),
|
|
3963
3965
|
)
|
|
3964
3966
|
.map((contract): XStateBossEventSpec =>
|
|
@@ -4407,9 +4409,15 @@ export function createXStatePlaybookRuntime<
|
|
|
4407
4409
|
spec.bossEvents ?? [],
|
|
4408
4410
|
parallelProfile,
|
|
4409
4411
|
);
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4412
|
+
// Initial-state-owned events describe new runs independently of whether
|
|
4413
|
+
// their payload is deterministic. Root interrupts are deliberately absent.
|
|
4414
|
+
const initialEntryEventTypes = new Set(
|
|
4415
|
+
(machine.root.initial.target ?? []).flatMap((node) =>
|
|
4416
|
+
Object.keys(node.config.on ?? {}).filter(
|
|
4417
|
+
(type) => type !== 'BOSS_REPLY' && type !== 'NO_ACTION',
|
|
4418
|
+
),
|
|
4419
|
+
),
|
|
4420
|
+
);
|
|
4413
4421
|
const normalizeTransitionEvent =
|
|
4414
4422
|
spec.normalizeTransitionEvent ??
|
|
4415
4423
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
@@ -5558,7 +5566,10 @@ export function createXStatePlaybookRuntime<
|
|
|
5558
5566
|
|
|
5559
5567
|
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
5560
5568
|
function beginRunIfEntry(event: EventObject): void {
|
|
5561
|
-
if (
|
|
5569
|
+
if (
|
|
5570
|
+
(event.type === spec.entryEvent?.type || initialEntryEventTypes.has(event.type)) &&
|
|
5571
|
+
snapshotCan(actor?.getSnapshot(), event)
|
|
5572
|
+
) {
|
|
5562
5573
|
bossExchanges = [];
|
|
5563
5574
|
}
|
|
5564
5575
|
}
|
|
@@ -10991,14 +11002,52 @@ export function createXStatePlaybookRuntime<
|
|
|
10991
11002
|
[spec.entryEvent.textField]: text,
|
|
10992
11003
|
};
|
|
10993
11004
|
} else {
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11005
|
+
// Classify a finished run against its fresh entry context,
|
|
11006
|
+
// without starting/replacing an actor or running the returned
|
|
11007
|
+
// executable actions. Pure context/assign initialization is
|
|
11008
|
+
// evaluated under the supported FSM contract.
|
|
11009
|
+
// Detach the input so pure initialization cannot mutate the
|
|
11010
|
+
// current actor or the host's bound input through an alias.
|
|
11011
|
+
const input = terminal
|
|
11012
|
+
? machineInput(boundOptions, requireSession())
|
|
11013
|
+
: undefined;
|
|
11014
|
+
const classificationSnapshot = terminal
|
|
11015
|
+
? initialTransition(
|
|
11016
|
+
machine,
|
|
11017
|
+
input === undefined
|
|
11018
|
+
? undefined
|
|
11019
|
+
: snapshotJsonValue(input, 'initial classification input'),
|
|
11020
|
+
)[0]
|
|
11021
|
+
: snapshot;
|
|
11022
|
+
event = spec.classifyBossText !== undefined
|
|
11023
|
+
? await spec.classifyBossText(
|
|
11024
|
+
text,
|
|
11025
|
+
runtimePorts!,
|
|
11026
|
+
signal,
|
|
11027
|
+
classificationSnapshot,
|
|
11028
|
+
boundary,
|
|
11029
|
+
boundOptions,
|
|
11030
|
+
)
|
|
11031
|
+
: await derivedClassifyBossText(
|
|
11032
|
+
text,
|
|
11033
|
+
runtimePorts!,
|
|
11034
|
+
signal,
|
|
11035
|
+
classificationSnapshot,
|
|
11036
|
+
boundary,
|
|
11037
|
+
terminal ? initialEntryEventTypes : undefined,
|
|
11038
|
+
);
|
|
11039
|
+
if (
|
|
11040
|
+
terminal &&
|
|
11041
|
+
event !== undefined &&
|
|
11042
|
+
(!initialEntryEventTypes.has(event.type) ||
|
|
11043
|
+
(spec.classifyBossText !== undefined &&
|
|
11044
|
+
!snapshotCan(classificationSnapshot, event)))
|
|
11045
|
+
) {
|
|
11046
|
+
await runtimePorts!.emitStatus(
|
|
11047
|
+
'Classifier selected an invalid initial entry event',
|
|
11048
|
+
);
|
|
11049
|
+
event = undefined;
|
|
11050
|
+
}
|
|
11002
11051
|
}
|
|
11003
11052
|
signal.throwIfAborted();
|
|
11004
11053
|
}
|