@sublang/playbook 10.0.0 → 11.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/README.md +1 -1
- package/docs/cli.md +58 -13
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +88 -0
- package/package.json +9 -2
- package/reference/sdlc/code.md +35 -15
- package/reference/sdlc/code.playbook/bin/interactive-session.js +58 -6
- package/reference/sdlc/code.playbook/bin/launch-config.js +499 -241
- package/reference/sdlc/code.playbook/bin/playbook.js +236 -187
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/run.js +355 -203
- package/reference/sdlc/code.playbook/bin/session-store.js +1512 -136
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +4 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +21 -3
- package/reference/sdlc/code.playbook/playbook-captain.ts +42 -6
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -15
- package/reference/sdlc/review.md +36 -18
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
|
|
4
|
+
const PLAYER_RECORD_TYPES = new Set([
|
|
5
|
+
'player_prompt',
|
|
6
|
+
'player_event',
|
|
7
|
+
'player_finished',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
export function createReplayRecordObserver({ lease, onIncomplete }) {
|
|
11
|
+
const activeFrames = new Map();
|
|
12
|
+
let incompleteReported = false;
|
|
13
|
+
let lastIncomplete = readIncomplete(lease);
|
|
14
|
+
|
|
15
|
+
const reportIfIncomplete = async (knownStatus) => {
|
|
16
|
+
if (incompleteReported) return;
|
|
17
|
+
const incomplete = readIncomplete(lease, knownStatus);
|
|
18
|
+
if (incomplete === undefined) return;
|
|
19
|
+
lastIncomplete = incomplete;
|
|
20
|
+
if (!incomplete) return;
|
|
21
|
+
incompleteReported = true;
|
|
22
|
+
try {
|
|
23
|
+
await onIncomplete();
|
|
24
|
+
} catch {
|
|
25
|
+
// Replay warnings are presentation-only and never change host outcome.
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const observer = Object.freeze({
|
|
30
|
+
async onRecord(record) {
|
|
31
|
+
let role;
|
|
32
|
+
try {
|
|
33
|
+
role = roleForReplayRecord(activeFrames, record);
|
|
34
|
+
} catch {
|
|
35
|
+
// The writer sanitizer remains authoritative for hostile values.
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
await lease.append(record, ...(role === undefined ? [] : [role]));
|
|
39
|
+
} catch {
|
|
40
|
+
// The replay writer records failure in its live latch. The observer
|
|
41
|
+
// remains installed so later host records and lifecycle work continue.
|
|
42
|
+
}
|
|
43
|
+
const incomplete = readIncomplete(lease);
|
|
44
|
+
if (incomplete === undefined) return;
|
|
45
|
+
const transitioned = lastIncomplete === false && incomplete;
|
|
46
|
+
lastIncomplete = incomplete;
|
|
47
|
+
if (transitioned) await reportIfIncomplete({ incomplete: true });
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return Object.freeze({ observer, reportIfIncomplete });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readIncomplete(lease, knownStatus) {
|
|
55
|
+
try {
|
|
56
|
+
const status = knownStatus ?? lease.streamStatus();
|
|
57
|
+
return typeof status?.incomplete === 'boolean'
|
|
58
|
+
? status.incomplete
|
|
59
|
+
: undefined;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function replayIncompleteMessage(sessionId) {
|
|
66
|
+
return `warning: replay history for session ${JSON.stringify(sessionId)} may be incomplete; recording has stopped`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function roleForReplayRecord(activeFrames, record) {
|
|
70
|
+
let trace;
|
|
71
|
+
try {
|
|
72
|
+
trace = playerTraceFrame(record);
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (trace !== undefined) {
|
|
77
|
+
const sessionFrames = activeFrames.get(trace.sessionId);
|
|
78
|
+
if (trace.type === 'player.call.started') {
|
|
79
|
+
if (sessionFrames?.has(trace.callId)) return undefined;
|
|
80
|
+
const frames = sessionFrames ?? new Map();
|
|
81
|
+
frames.set(trace.callId, {
|
|
82
|
+
playerId: trace.playerId,
|
|
83
|
+
roleId: trace.roleId,
|
|
84
|
+
});
|
|
85
|
+
if (sessionFrames === undefined) activeFrames.set(trace.sessionId, frames);
|
|
86
|
+
} else {
|
|
87
|
+
const frame = sessionFrames?.get(trace.callId);
|
|
88
|
+
if (
|
|
89
|
+
frame?.playerId === trace.playerId &&
|
|
90
|
+
frame.roleId === trace.roleId
|
|
91
|
+
) {
|
|
92
|
+
sessionFrames.delete(trace.callId);
|
|
93
|
+
if (sessionFrames.size === 0) activeFrames.delete(trace.sessionId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isRecord(record) || !PLAYER_RECORD_TYPES.has(record.type)) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
if (!isNonemptyString(record.playerId)) return undefined;
|
|
103
|
+
|
|
104
|
+
const roles = new Set();
|
|
105
|
+
for (const sessionFrames of activeFrames.values()) {
|
|
106
|
+
for (const frame of sessionFrames.values()) {
|
|
107
|
+
if (frame.playerId === record.playerId) roles.add(frame.roleId);
|
|
108
|
+
if (roles.size > 1) return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return roles.size === 1 ? roles.values().next().value : undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function playerTraceFrame(record) {
|
|
115
|
+
if (
|
|
116
|
+
!isRecord(record) ||
|
|
117
|
+
record.type !== 'captain_telemetry' ||
|
|
118
|
+
record.topic !== 'playbook.trace' ||
|
|
119
|
+
!isRecord(record.payload)
|
|
120
|
+
) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
const trace = record.payload;
|
|
124
|
+
if (
|
|
125
|
+
!isSchema4PlayerTraceEvent(trace) ||
|
|
126
|
+
(trace.type !== 'player.call.started' &&
|
|
127
|
+
trace.type !== 'player.call.finished') ||
|
|
128
|
+
(trace.type === 'player.call.started'
|
|
129
|
+
? typeof trace.payload.prompt !== 'string'
|
|
130
|
+
: !['ok', 'aborted', 'error'].includes(trace.payload.status))
|
|
131
|
+
) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
type: trace.type,
|
|
136
|
+
sessionId: trace.sessionId,
|
|
137
|
+
callId: trace.callId,
|
|
138
|
+
playerId: trace.payload.playerId,
|
|
139
|
+
roleId: trace.payload.roleId,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isSchema4PlayerTraceEvent(trace) {
|
|
144
|
+
if (
|
|
145
|
+
!hasOwnValue(trace, 'schemaVersion', 4) ||
|
|
146
|
+
!hasOwnNonemptyString(trace, 'sessionId') ||
|
|
147
|
+
!hasOwnNonemptyString(trace, 'playbookId') ||
|
|
148
|
+
!hasOwnNonemptyString(trace, 'rootSessionId') ||
|
|
149
|
+
!hasOwnNonnegativeSafeInteger(trace, 'depth') ||
|
|
150
|
+
!hasOwnPositiveSafeInteger(trace, 'sequence') ||
|
|
151
|
+
!hasOwnFiniteNonnegativeNumber(trace, 'timestamp') ||
|
|
152
|
+
!optionalPositiveSafeInteger(trace, 'turnId') ||
|
|
153
|
+
!hasOwnNonemptyString(trace, 'callId') ||
|
|
154
|
+
!optionalNonemptyString(trace, 'parentSessionId') ||
|
|
155
|
+
!optionalNonemptyString(trace, 'parentCallId') ||
|
|
156
|
+
!isRecord(trace.payload) ||
|
|
157
|
+
!hasOwnNonemptyString(trace.payload, 'playerId') ||
|
|
158
|
+
!hasOwnNonemptyString(trace.payload, 'roleId') ||
|
|
159
|
+
!hasOwnResumeSelection(trace.payload)
|
|
160
|
+
) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function hasOwnValue(value, key, expected) {
|
|
167
|
+
return Object.hasOwn(value, key) && value[key] === expected;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasOwnNonemptyString(value, key) {
|
|
171
|
+
return Object.hasOwn(value, key) && isNonemptyString(value[key]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function optionalNonemptyString(value, key) {
|
|
175
|
+
return !Object.hasOwn(value, key) || isNonemptyString(value[key]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function optionalPositiveSafeInteger(value, key) {
|
|
179
|
+
return !Object.hasOwn(value, key) || hasOwnPositiveSafeInteger(value, key);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function hasOwnNonnegativeSafeInteger(value, key) {
|
|
183
|
+
return (
|
|
184
|
+
Object.hasOwn(value, key) &&
|
|
185
|
+
Number.isSafeInteger(value[key]) &&
|
|
186
|
+
value[key] >= 0
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function hasOwnPositiveSafeInteger(value, key) {
|
|
191
|
+
return (
|
|
192
|
+
Object.hasOwn(value, key) &&
|
|
193
|
+
Number.isSafeInteger(value[key]) &&
|
|
194
|
+
value[key] > 0
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function hasOwnFiniteNonnegativeNumber(value, key) {
|
|
199
|
+
return (
|
|
200
|
+
Object.hasOwn(value, key) &&
|
|
201
|
+
typeof value[key] === 'number' &&
|
|
202
|
+
Number.isFinite(value[key]) &&
|
|
203
|
+
value[key] >= 0
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function hasOwnResumeSelection(value) {
|
|
208
|
+
return (
|
|
209
|
+
Object.hasOwn(value, 'resume') &&
|
|
210
|
+
(value.resume === false ||
|
|
211
|
+
(typeof value.resume === 'string' && value.resume.trim().length > 0))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isRecord(value) {
|
|
216
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isNonemptyString(value) {
|
|
220
|
+
return typeof value === 'string' && value.length > 0;
|
|
221
|
+
}
|