@sublang/playbook 0.9.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -151
- package/package.json +50 -6
- package/reference/sdlc/captain.md +102 -0
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
- package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
- package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
- package/reference/sdlc/code.playbook/bin/run.js +999 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
- package/reference/sdlc/code.playbook/code.fsm.js +334 -102
- package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
- package/reference/sdlc/code.playbook/code.gears.md +11 -10
- package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
- package/reference/sdlc/code.playbook/code.playbook.js +199 -488
- package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
- package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
- package/reference/sdlc/code.playbook/code.registry.js +0 -3
- package/reference/sdlc/code.playbook/code.registry.ts +0 -6
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
- package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
- package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
- package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
- package/slc/gears2fsm.md +557 -57
- package/slc/link.md +1165 -89
- package/slc/optimize.md +92 -0
- package/slc/text2gears.md +255 -7
- package/src/runtime.d.ts +146 -3
- package/src/runtime.ts +201 -2
- package/src/xstate-playbook-runtime.d.ts +201 -0
- package/src/xstate-playbook-runtime.js +2058 -0
- package/src/xstate-playbook-runtime.ts +2792 -0
- package/src/xstate-runtime.d.ts +95 -0
- package/src/xstate-runtime.js +1258 -0
- package/src/xstate-runtime.ts +1816 -0
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import PQueue from 'p-queue';
|
|
5
|
+
import { registerPlaybookAbortCleanup } from '../../../src/xstate-runtime.js';
|
|
6
|
+
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
7
|
+
class VisibilityControlError extends Error {
|
|
8
|
+
constructor(cause) {
|
|
9
|
+
super(`playbook visibility request failed: ${String(cause?.message ?? cause)}`, { cause });
|
|
10
|
+
this.name = 'VisibilityControlError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
3
13
|
const SUB_RUNTIME_FSM_TOPIC = 'playbook.fsm.state';
|
|
4
14
|
const SHELL_FSM_TOPIC = 'playbook.captain.fsm.state';
|
|
15
|
+
const INTERNAL_CAPTAIN_ID = 'captain';
|
|
16
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
5
17
|
function parseRegisteredCommand(prompt) {
|
|
6
18
|
const match = /^\/([A-Za-z][A-Za-z0-9_-]*)(?:\s+([\s\S]*))?$/.exec(prompt.trim());
|
|
7
19
|
if (!match)
|
|
@@ -11,14 +23,14 @@ function parseRegisteredCommand(prompt) {
|
|
|
11
23
|
function visibleChatEnvelope(message) {
|
|
12
24
|
return [
|
|
13
25
|
'You are the Playbook Captain shell.',
|
|
14
|
-
'This is visible Boss chat. Do not reveal hidden control JSON, hidden
|
|
26
|
+
'This is visible Boss chat. Do not reveal hidden control JSON, hidden lifecycle decisions, or hidden judge replies.',
|
|
15
27
|
message,
|
|
16
28
|
].join('\n\n');
|
|
17
29
|
}
|
|
18
30
|
function visibleTurnSummaryEnvelope(input) {
|
|
19
31
|
return [
|
|
20
32
|
'You are the Playbook Captain shell.',
|
|
21
|
-
'This is visible Boss chat after a sub-playbook command completed. Do not reveal hidden control JSON, hidden
|
|
33
|
+
'This is visible Boss chat after a sub-playbook command completed. Do not reveal hidden control JSON, hidden lifecycle decisions, or hidden judge replies.',
|
|
22
34
|
'Write a brief, clearly formatted turn-summary block for Boss.',
|
|
23
35
|
'Use a natural, chat-like tone and no more than two short sentences before the saved-counts line.',
|
|
24
36
|
'State only what was done or what changed; do not explain how it was done.',
|
|
@@ -38,9 +50,6 @@ function visibleTurnSummaryEnvelope(input) {
|
|
|
38
50
|
].join('\n\n');
|
|
39
51
|
}
|
|
40
52
|
function stateCountLabel(stateId, entry) {
|
|
41
|
-
if (stateId === entry.idleStateId || stateId === entry.finalStateId) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
53
|
const registryLabel = entry.summaryPolicy?.stateCountLabels?.[stateId]?.trim();
|
|
45
54
|
return registryLabel || undefined;
|
|
46
55
|
}
|
|
@@ -74,9 +83,6 @@ function isValidRegistryEntry(value) {
|
|
|
74
83
|
typeof e.command === 'string' &&
|
|
75
84
|
typeof e.intent === 'string' &&
|
|
76
85
|
Array.isArray(e.requiredRoleIds) &&
|
|
77
|
-
typeof e.idleStateId === 'string' &&
|
|
78
|
-
typeof e.finalStateId === 'string' &&
|
|
79
|
-
Array.isArray(e.parkStateIds) &&
|
|
80
86
|
typeof e.validateOptions === 'function' &&
|
|
81
87
|
typeof e.createRuntime === 'function');
|
|
82
88
|
}
|
|
@@ -106,6 +112,9 @@ async function buildEnablements(options, players, loadModule) {
|
|
|
106
112
|
throw new Error('captain.options.playbooks must enable at least one playbook');
|
|
107
113
|
}
|
|
108
114
|
for (const id of ids) {
|
|
115
|
+
if (id === INTERNAL_CAPTAIN_ID) {
|
|
116
|
+
throw new Error(`captain.options.playbooks.${id} collides with the reserved internal Captain id`);
|
|
117
|
+
}
|
|
109
118
|
const block = config[id];
|
|
110
119
|
if (typeof block !== 'object' || block === null || Array.isArray(block)) {
|
|
111
120
|
throw new Error(`captain.options.playbooks.${id} must be an object`);
|
|
@@ -135,12 +144,19 @@ async function buildEnablements(options, players, loadModule) {
|
|
|
135
144
|
const command = typeof record.command === 'string' && record.command.length > 0
|
|
136
145
|
? record.command
|
|
137
146
|
: entry.command;
|
|
147
|
+
if (command === INTERNAL_CAPTAIN_ID) {
|
|
148
|
+
throw new Error(`captain.options.playbooks.${id} command collides with the reserved internal Captain command`);
|
|
149
|
+
}
|
|
138
150
|
if (byCommand.has(command)) {
|
|
139
151
|
throw new Error(`captain.options.playbooks has a duplicate effective command "${command}"`);
|
|
140
152
|
}
|
|
141
153
|
const boundPlayers = entry.requiredRoleIds.map((role) => {
|
|
142
154
|
const host = players.find((p) => p.id === `${entry.id}-${role}`);
|
|
143
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
id: role,
|
|
157
|
+
...(host?.adapter !== undefined ? { adapter: host.adapter } : {}),
|
|
158
|
+
...(host?.model !== undefined ? { model: host.model } : {}),
|
|
159
|
+
};
|
|
144
160
|
});
|
|
145
161
|
entries.push(entry);
|
|
146
162
|
byId.set(entry.id, entry);
|
|
@@ -158,52 +174,75 @@ async function buildEnablements(options, players, loadModule) {
|
|
|
158
174
|
}
|
|
159
175
|
export function createPlaybookCaptainShell(options, deps = {}) {
|
|
160
176
|
const loadModule = deps.loadModule ?? ((specifier) => import(specifier));
|
|
177
|
+
const createSessionId = deps.createSessionId ?? randomUUID;
|
|
178
|
+
const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
161
179
|
let entries = [];
|
|
162
180
|
let byCommand = new Map();
|
|
163
181
|
let byId = new Map();
|
|
164
182
|
let enablementById = new Map();
|
|
183
|
+
let internalCaptainEnablement;
|
|
165
184
|
let session;
|
|
166
185
|
let players = [];
|
|
167
186
|
let activeContext;
|
|
168
|
-
|
|
187
|
+
const frames = [];
|
|
169
188
|
let mode = 'chat';
|
|
170
|
-
let
|
|
171
|
-
let pendingBossQuestion;
|
|
189
|
+
let pendingBossQuestions;
|
|
172
190
|
let lastError;
|
|
173
191
|
let lastRouteDecision;
|
|
174
|
-
let finalDisposalRequested;
|
|
175
192
|
let activeTurnSummary;
|
|
193
|
+
let activeTurnHostCalls;
|
|
194
|
+
const issuedSessionIds = new Set();
|
|
195
|
+
const pendingChildParents = new Set();
|
|
196
|
+
const captainQueue = new PQueue({ concurrency: 1 });
|
|
197
|
+
let disposing = false;
|
|
198
|
+
const rootFrame = () => frames[0];
|
|
199
|
+
const leafFrame = () => frames.at(-1);
|
|
200
|
+
const frameLabel = (frame) => frame.internal ? 'Captain' : `/${frame.enablement.command}`;
|
|
176
201
|
const requireSession = () => {
|
|
177
202
|
if (!session) {
|
|
178
203
|
throw new Error('init must be called first');
|
|
179
204
|
}
|
|
180
205
|
return session;
|
|
181
206
|
};
|
|
182
|
-
const ledgerSnapshot = (playbookId =
|
|
207
|
+
const ledgerSnapshot = (playbookId = leafFrame()?.entry.id, activeSessionId = leafFrame()?.sessionId) => ({
|
|
183
208
|
...(playbookId ? { activePlaybookId: playbookId } : {}),
|
|
209
|
+
...(activeSessionId ? { activeSessionId } : {}),
|
|
210
|
+
...(rootFrame()
|
|
211
|
+
? {
|
|
212
|
+
rootPlaybookId: rootFrame().entry.id,
|
|
213
|
+
rootSessionId: rootFrame().sessionId,
|
|
214
|
+
}
|
|
215
|
+
: {}),
|
|
216
|
+
stackDepth: frames.length,
|
|
217
|
+
stackPath: frames.map((frame) => frame.entry.id),
|
|
184
218
|
mode,
|
|
185
|
-
...(
|
|
186
|
-
|
|
219
|
+
...(leafFrame()?.state?.stateId
|
|
220
|
+
? { latestSubRuntimeStateId: leafFrame().state.stateId }
|
|
221
|
+
: {}),
|
|
222
|
+
...(leafFrame()?.state
|
|
223
|
+
? { latestSubRuntimeState: leafFrame().state }
|
|
224
|
+
: {}),
|
|
225
|
+
...(pendingBossQuestions !== undefined ? { pendingBossQuestions } : {}),
|
|
187
226
|
...(lastError ? { lastError } : {}),
|
|
188
227
|
...(lastRouteDecision ? { lastRouteDecision } : {}),
|
|
189
228
|
});
|
|
190
|
-
const emitShellTelemetry = async (from, to, event, playbookId =
|
|
229
|
+
const emitShellTelemetry = async (from, to, event, playbookId = leafFrame()?.entry.id, activeSessionId = leafFrame()?.sessionId) => {
|
|
191
230
|
await requireSession().emitTelemetry({
|
|
192
231
|
topic: SHELL_FSM_TOPIC,
|
|
193
232
|
payload: {
|
|
194
233
|
from,
|
|
195
234
|
to,
|
|
196
235
|
event,
|
|
197
|
-
ledger: ledgerSnapshot(playbookId),
|
|
236
|
+
ledger: ledgerSnapshot(playbookId, activeSessionId),
|
|
198
237
|
},
|
|
199
238
|
});
|
|
200
239
|
};
|
|
201
|
-
const setMode = async (nextMode, event, playbookId =
|
|
240
|
+
const setMode = async (nextMode, event, playbookId = leafFrame()?.entry.id, activeSessionId = leafFrame()?.sessionId) => {
|
|
202
241
|
if (mode === nextMode)
|
|
203
242
|
return;
|
|
204
243
|
const from = mode;
|
|
205
244
|
mode = nextMode;
|
|
206
|
-
await emitShellTelemetry(from, nextMode, event, playbookId);
|
|
245
|
+
await emitShellTelemetry(from, nextMode, event, playbookId, activeSessionId);
|
|
207
246
|
};
|
|
208
247
|
const normalizeErrorCompact = (value) => {
|
|
209
248
|
if (value === undefined || value === null)
|
|
@@ -225,62 +264,141 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
225
264
|
const payloadRecord = (payload) => typeof payload === 'object' && payload !== null && !Array.isArray(payload)
|
|
226
265
|
? payload
|
|
227
266
|
: undefined;
|
|
228
|
-
const
|
|
229
|
-
const record = payloadRecord(
|
|
230
|
-
if (!record
|
|
267
|
+
const playbookState = (value) => {
|
|
268
|
+
const record = payloadRecord(value);
|
|
269
|
+
if (!record ||
|
|
270
|
+
!Array.isArray(record.activeStateIds) ||
|
|
271
|
+
!record.activeStateIds.every((id) => typeof id === 'string') ||
|
|
272
|
+
!Array.isArray(record.tags) ||
|
|
273
|
+
!record.tags.every((tag) => typeof tag === 'string') ||
|
|
274
|
+
typeof record.status !== 'string' ||
|
|
275
|
+
typeof record.quiescent !== 'boolean' ||
|
|
276
|
+
!('value' in record)) {
|
|
231
277
|
return undefined;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return typeof record.state === 'string' ? record.state : undefined;
|
|
278
|
+
}
|
|
279
|
+
return record;
|
|
235
280
|
};
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
238
|
-
return;
|
|
281
|
+
const stateValueContains = (value, stateId) => {
|
|
282
|
+
if (typeof value === 'string')
|
|
283
|
+
return value === stateId;
|
|
284
|
+
const record = payloadRecord(value);
|
|
285
|
+
if (!record)
|
|
286
|
+
return false;
|
|
287
|
+
return Object.entries(record).some(([key, nested]) => key === stateId || stateValueContains(nested, stateId));
|
|
288
|
+
};
|
|
289
|
+
const drainHostCalls = async (calls) => {
|
|
290
|
+
while (calls.size > 0) {
|
|
291
|
+
await Promise.allSettled([...calls]);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const trackHostCall = (frame, call) => {
|
|
295
|
+
// Cligent's host methods are scoped to the whole Boss turn, while an
|
|
296
|
+
// XState invocation can carry a narrower sibling-cancellation signal.
|
|
297
|
+
// Keep both frame and turn ownership after XState stops awaiting the
|
|
298
|
+
// promise so the host cannot outlive frame disposal or turn settlement.
|
|
299
|
+
const turnCalls = activeTurnHostCalls;
|
|
300
|
+
let tracked;
|
|
301
|
+
tracked = call.finally(() => {
|
|
302
|
+
frame.inFlightHostCalls.delete(tracked);
|
|
303
|
+
turnCalls?.delete(tracked);
|
|
304
|
+
});
|
|
305
|
+
frame.inFlightHostCalls.add(tracked);
|
|
306
|
+
turnCalls?.add(tracked);
|
|
307
|
+
return tracked;
|
|
308
|
+
};
|
|
309
|
+
const callCaptainQueued = (frame, context, prompt, options, signal) => {
|
|
310
|
+
const queued = captainQueue.add(async () => {
|
|
311
|
+
signal.throwIfAborted();
|
|
312
|
+
const result = await trackHostCall(frame, context.callCaptain(prompt, options));
|
|
313
|
+
signal.throwIfAborted();
|
|
314
|
+
return result;
|
|
315
|
+
});
|
|
316
|
+
return trackHostCall(frame, queued);
|
|
317
|
+
};
|
|
318
|
+
const mirrorSubRuntimeTelemetry = async (frame, payload) => {
|
|
239
319
|
const record = payloadRecord(payload);
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
return;
|
|
243
|
-
const countLabel = stateCountLabel(stateId, active.entry);
|
|
244
|
-
if (activeTurnSummary && countLabel) {
|
|
245
|
-
activeTurnSummary.stateCounts.set(countLabel, (activeTurnSummary.stateCounts.get(countLabel) ?? 0) + 1);
|
|
246
|
-
}
|
|
247
|
-
latestSubRuntimeStateId = stateId;
|
|
248
|
-
pendingBossQuestion = record?.pendingBossQuestion;
|
|
249
|
-
lastError = normalizeErrorCompact(record?.lastError);
|
|
250
|
-
if (stateId === active.entry.finalStateId) {
|
|
251
|
-
finalDisposalRequested = active;
|
|
320
|
+
const state = playbookState(record?.state);
|
|
321
|
+
if (!record || !state)
|
|
252
322
|
return;
|
|
323
|
+
const previousActiveIds = new Set(frame.state?.activeStateIds ?? []);
|
|
324
|
+
frame.state = state;
|
|
325
|
+
if (activeTurnSummary?.owner === frame) {
|
|
326
|
+
for (const stateId of state.activeStateIds) {
|
|
327
|
+
const newlyActive = !previousActiveIds.has(stateId);
|
|
328
|
+
const structuredEntry = stateValueContains(record.to, stateId) &&
|
|
329
|
+
!stateValueContains(record.from, stateId);
|
|
330
|
+
if (!newlyActive && !structuredEntry)
|
|
331
|
+
continue;
|
|
332
|
+
const countLabel = stateCountLabel(stateId, frame.entry);
|
|
333
|
+
if (countLabel) {
|
|
334
|
+
activeTurnSummary.stateCounts.set(countLabel, (activeTurnSummary.stateCounts.get(countLabel) ?? 0) + 1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
253
337
|
}
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
338
|
+
if (leafFrame() === frame) {
|
|
339
|
+
pendingBossQuestions =
|
|
340
|
+
record.pendingBossQuestions ?? record.pendingBossQuestion;
|
|
341
|
+
lastError = normalizeErrorCompact(record.lastError);
|
|
342
|
+
if (state.quiescent && state.tags.includes('playbook.parked')) {
|
|
343
|
+
await setMode('engaged.parked', `sub-runtime:${state.stateId ?? 'structured'}`);
|
|
344
|
+
}
|
|
257
345
|
}
|
|
258
346
|
};
|
|
259
|
-
|
|
260
|
-
|
|
347
|
+
let callNestedPlaybook;
|
|
348
|
+
const createPorts = (frame) => ({
|
|
349
|
+
callPlayer: async (playerId, prompt, signal, options) => {
|
|
261
350
|
if (!activeContext) {
|
|
262
351
|
throw new Error('callPlayer invoked outside a Boss turn');
|
|
263
352
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const result = await
|
|
268
|
-
|
|
353
|
+
const context = activeContext;
|
|
354
|
+
signal.throwIfAborted();
|
|
355
|
+
const hostPlayerId = frame.enablement.hostPlayerId(playerId);
|
|
356
|
+
const result = await trackHostCall(frame, context.callPlayer(hostPlayerId, prompt, {
|
|
357
|
+
resume: options.resume,
|
|
358
|
+
}));
|
|
359
|
+
// CaptainContext is turn-scoped and cannot accept a narrower XState
|
|
360
|
+
// invocation signal. Recheck after the host call so a sibling
|
|
361
|
+
// cancellation is still reported as aborted and cannot rotate a
|
|
362
|
+
// stopped branch's player token in the linked runtime.
|
|
363
|
+
signal.throwIfAborted();
|
|
364
|
+
if (activeTurnSummary?.owner === frame) {
|
|
269
365
|
activeTurnSummary.counts.interruptions++;
|
|
270
366
|
}
|
|
271
367
|
return {
|
|
272
368
|
status: result.status,
|
|
273
|
-
|
|
274
|
-
|
|
369
|
+
...(result.resumeToken !== undefined
|
|
370
|
+
? { resumeToken: result.resumeToken }
|
|
371
|
+
: {}),
|
|
372
|
+
...(result.finalText !== undefined
|
|
373
|
+
? { finalText: result.finalText }
|
|
374
|
+
: {}),
|
|
375
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
275
376
|
};
|
|
276
377
|
},
|
|
277
|
-
|
|
378
|
+
callCaptain: async (prompt, signal, options) => {
|
|
379
|
+
if (!activeContext) {
|
|
380
|
+
throw new Error('callCaptain invoked outside a Boss turn');
|
|
381
|
+
}
|
|
382
|
+
const result = await callCaptainQueued(frame, activeContext, prompt, {
|
|
383
|
+
visibility: options.visibility,
|
|
384
|
+
resume: options.resume,
|
|
385
|
+
...(options.allowedTools === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: { allowedTools: options.allowedTools }),
|
|
388
|
+
}, signal);
|
|
389
|
+
return {
|
|
390
|
+
status: result.status,
|
|
391
|
+
...(result.finalText !== undefined
|
|
392
|
+
? { finalText: result.finalText }
|
|
393
|
+
: {}),
|
|
394
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
callJudge: async (prompt, signal) => {
|
|
278
398
|
if (!activeContext) {
|
|
279
399
|
throw new Error('callJudge invoked outside a Boss turn');
|
|
280
400
|
}
|
|
281
|
-
const result = await activeContext
|
|
282
|
-
visibility: 'hidden',
|
|
283
|
-
});
|
|
401
|
+
const result = await callCaptainQueued(frame, activeContext, prompt, { visibility: 'hidden', resume: false, allowedTools: [] }, signal);
|
|
284
402
|
if (result.status !== 'ok') {
|
|
285
403
|
throw new Error(result.error ?? `callCaptain status "${result.status}"`);
|
|
286
404
|
}
|
|
@@ -289,18 +407,34 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
289
407
|
}
|
|
290
408
|
const guard = guardFromJudgeReply(result.finalText);
|
|
291
409
|
if (guard &&
|
|
292
|
-
|
|
293
|
-
|
|
410
|
+
activeTurnSummary?.owner === frame &&
|
|
411
|
+
frame.entry.summaryPolicy?.copyPasteGuardNames.includes(guard)) {
|
|
294
412
|
activeTurnSummary.counts.copyPastes++;
|
|
295
413
|
}
|
|
296
414
|
return result.finalText;
|
|
297
415
|
},
|
|
416
|
+
callPlaybook: (request, signal) => {
|
|
417
|
+
const opening = callNestedPlaybook(frame, request, signal);
|
|
418
|
+
let exposed;
|
|
419
|
+
const registerOpeningCleanup = () => {
|
|
420
|
+
registerPlaybookAbortCleanup(signal, exposed);
|
|
421
|
+
};
|
|
422
|
+
exposed = opening.finally(() => {
|
|
423
|
+
signal.removeEventListener('abort', registerOpeningCleanup);
|
|
424
|
+
});
|
|
425
|
+
signal.addEventListener('abort', registerOpeningCleanup, { once: true });
|
|
426
|
+
if (signal.aborted)
|
|
427
|
+
registerOpeningCleanup();
|
|
428
|
+
return exposed;
|
|
429
|
+
},
|
|
298
430
|
emitStatus: async (message, data) => {
|
|
431
|
+
if (frame.internal)
|
|
432
|
+
return;
|
|
299
433
|
await requireSession().emitStatus(message, data);
|
|
300
434
|
},
|
|
301
435
|
emitTelemetry: async (event) => {
|
|
302
436
|
if (event.topic === SUB_RUNTIME_FSM_TOPIC) {
|
|
303
|
-
await mirrorSubRuntimeTelemetry(event.payload);
|
|
437
|
+
await mirrorSubRuntimeTelemetry(frame, event.payload);
|
|
304
438
|
}
|
|
305
439
|
await requireSession().emitTelemetry(event);
|
|
306
440
|
},
|
|
@@ -313,60 +447,651 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
313
447
|
const ids = enablement.visiblePlayerIds;
|
|
314
448
|
if (!ids || ids.length === 0 || !activeContext)
|
|
315
449
|
return;
|
|
316
|
-
|
|
450
|
+
try {
|
|
451
|
+
await activeContext.setVisiblePlayers(ids);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
throw new VisibilityControlError(error);
|
|
455
|
+
}
|
|
317
456
|
};
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
457
|
+
const allocateSessionId = () => {
|
|
458
|
+
const sessionId = createSessionId();
|
|
459
|
+
if (!UUID_PATTERN.test(sessionId)) {
|
|
460
|
+
throw new Error(`playbook session id generator returned a non-UUID value: ${JSON.stringify(sessionId)}`);
|
|
461
|
+
}
|
|
462
|
+
if (issuedSessionIds.has(sessionId)) {
|
|
463
|
+
throw new Error(`playbook session id collision: ${sessionId}`);
|
|
464
|
+
}
|
|
465
|
+
issuedSessionIds.add(sessionId);
|
|
466
|
+
return sessionId;
|
|
467
|
+
};
|
|
468
|
+
const normalizeErrorFull = (value) => {
|
|
469
|
+
const compact = normalizeErrorCompact(value) ?? {
|
|
470
|
+
name: 'Error',
|
|
471
|
+
message: String(value),
|
|
472
|
+
};
|
|
473
|
+
const stack = value instanceof Error
|
|
474
|
+
? value.stack
|
|
475
|
+
: typeof value === 'object' && value !== null
|
|
476
|
+
? value.stack
|
|
477
|
+
: undefined;
|
|
478
|
+
return typeof stack === 'string' ? { ...compact, stack } : compact;
|
|
479
|
+
};
|
|
480
|
+
const makeFrame = (enablement, parent, internal = false) => {
|
|
481
|
+
const entry = enablement.entry;
|
|
482
|
+
const sessionId = allocateSessionId();
|
|
322
483
|
const runtime = entry.createRuntime({
|
|
323
484
|
captainOptions: enablement.optionInput,
|
|
324
485
|
players: enablement.boundPlayers,
|
|
325
486
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
487
|
+
return {
|
|
488
|
+
entry,
|
|
489
|
+
enablement,
|
|
490
|
+
runtime,
|
|
491
|
+
sessionId,
|
|
492
|
+
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
493
|
+
depth: parent ? parent.frame.depth + 1 : 0,
|
|
494
|
+
...(parent ? { parent } : {}),
|
|
495
|
+
inFlightHostCalls: new Set(),
|
|
496
|
+
internal,
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
const initFrame = async (frame) => {
|
|
500
|
+
await frame.runtime.init({
|
|
501
|
+
sessionId: frame.sessionId,
|
|
502
|
+
playbookId: frame.entry.id,
|
|
503
|
+
rootSessionId: frame.rootSessionId,
|
|
504
|
+
...(frame.parent
|
|
505
|
+
? {
|
|
506
|
+
parentSessionId: frame.parent.frame.sessionId,
|
|
507
|
+
parentCallId: frame.parent.callId,
|
|
508
|
+
}
|
|
509
|
+
: {}),
|
|
510
|
+
depth: frame.depth,
|
|
511
|
+
ports: createPorts(frame),
|
|
512
|
+
});
|
|
513
|
+
};
|
|
514
|
+
const clearLeafLedger = () => {
|
|
515
|
+
pendingBossQuestions = undefined;
|
|
329
516
|
lastError = undefined;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
517
|
+
};
|
|
518
|
+
const engageEnablement = async (enablement, internal) => {
|
|
519
|
+
const entry = enablement.entry;
|
|
520
|
+
const existing = rootFrame();
|
|
521
|
+
if (existing?.entry.id === entry.id && frames.length === 1) {
|
|
522
|
+
return existing;
|
|
523
|
+
}
|
|
524
|
+
if (existing) {
|
|
525
|
+
throw new Error('cannot engage a second root playbook');
|
|
526
|
+
}
|
|
527
|
+
const frame = makeFrame(enablement, undefined, internal);
|
|
528
|
+
frames.push(frame);
|
|
529
|
+
clearLeafLedger();
|
|
530
|
+
try {
|
|
531
|
+
await setMode('engaged.parked', 'engage', entry.id, frame.sessionId);
|
|
532
|
+
await initFrame(frame);
|
|
533
|
+
if (!internal) {
|
|
534
|
+
await requireSession().emitStatus(`◇ ${frameLabel(frame)} started`);
|
|
535
|
+
}
|
|
536
|
+
return frame;
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (leafFrame() === frame)
|
|
540
|
+
frames.pop();
|
|
541
|
+
clearLeafLedger();
|
|
542
|
+
try {
|
|
543
|
+
await frame.runtime.dispose();
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// Preserve the initialization failure while still making a
|
|
547
|
+
// best-effort attempt to release partially acquired resources.
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
await setMode('chat', 'engage.failed');
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// setMode updates the authoritative mode before telemetry; preserve
|
|
554
|
+
// the initialization failure if that recovery emission also fails.
|
|
555
|
+
mode = 'chat';
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const engage = async (entry) => engageEnablement(enablementById.get(entry.id), false);
|
|
561
|
+
const createInternalCaptainEnablement = () => {
|
|
562
|
+
const catalog = Object.freeze(entries.map((entry) => Object.freeze({
|
|
563
|
+
id: entry.id,
|
|
564
|
+
command: enablementById.get(entry.id).command,
|
|
565
|
+
intent: entry.intent,
|
|
566
|
+
})));
|
|
567
|
+
const entry = {
|
|
568
|
+
id: INTERNAL_CAPTAIN_ID,
|
|
569
|
+
command: INTERNAL_CAPTAIN_ID,
|
|
570
|
+
intent: 'internal orchestration policy',
|
|
571
|
+
requiredRoleIds: [],
|
|
572
|
+
validateOptions: () => undefined,
|
|
573
|
+
createRuntime: () => createCaptainRuntime({ enabledPlaybooks: catalog }),
|
|
574
|
+
};
|
|
575
|
+
return {
|
|
576
|
+
entry,
|
|
577
|
+
command: INTERNAL_CAPTAIN_ID,
|
|
578
|
+
optionInput: undefined,
|
|
579
|
+
boundPlayers: [],
|
|
580
|
+
hostPlayerId(localRole) {
|
|
581
|
+
throw new Error(`internal Captain has no player binding for ${JSON.stringify(localRole)}`);
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
const engageInternalCaptain = async () => {
|
|
586
|
+
if (!internalCaptainEnablement) {
|
|
587
|
+
throw new Error('internal Captain enablement is unavailable before init');
|
|
588
|
+
}
|
|
589
|
+
return engageEnablement(internalCaptainEnablement, true);
|
|
590
|
+
};
|
|
591
|
+
const disposeFrame = (frame) => {
|
|
592
|
+
if (frame.disposePromise)
|
|
593
|
+
return frame.disposePromise;
|
|
594
|
+
const operation = (async () => {
|
|
595
|
+
if (frame.invocationSignal && frame.abortListener) {
|
|
596
|
+
frame.invocationSignal.removeEventListener('abort', frame.abortListener);
|
|
597
|
+
}
|
|
598
|
+
frame.invocationSignal = undefined;
|
|
599
|
+
frame.abortListener = undefined;
|
|
600
|
+
let disposeError;
|
|
601
|
+
try {
|
|
602
|
+
await frame.runtime.dispose();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
disposeError = error;
|
|
606
|
+
}
|
|
607
|
+
await drainHostCalls(frame.inFlightHostCalls);
|
|
608
|
+
if (disposeError !== undefined)
|
|
609
|
+
throw disposeError;
|
|
610
|
+
})();
|
|
611
|
+
frame.disposePromise = operation;
|
|
612
|
+
return operation;
|
|
613
|
+
};
|
|
614
|
+
const removeTopFrame = (frame, reason) => {
|
|
615
|
+
if (frame.removal) {
|
|
616
|
+
return {
|
|
617
|
+
claimed: false,
|
|
618
|
+
reason: frame.removal.reason,
|
|
619
|
+
promise: frame.removal.promise,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const operation = (async () => {
|
|
623
|
+
if (leafFrame() !== frame) {
|
|
624
|
+
throw new Error('nested playbook stack is not LIFO');
|
|
625
|
+
}
|
|
626
|
+
let removalError;
|
|
627
|
+
try {
|
|
628
|
+
await disposeFrame(frame);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
removalError = error;
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
if (leafFrame() === frame) {
|
|
635
|
+
frames.pop();
|
|
636
|
+
if (frame.parent) {
|
|
637
|
+
pendingChildParents.delete(frame.parent.frame);
|
|
638
|
+
}
|
|
639
|
+
pendingChildParents.delete(frame);
|
|
640
|
+
}
|
|
641
|
+
else if (frames.includes(frame)) {
|
|
642
|
+
const stackError = new Error('nested playbook stack changed during frame removal');
|
|
643
|
+
removalError =
|
|
644
|
+
removalError === undefined
|
|
645
|
+
? stackError
|
|
646
|
+
: new AggregateError([removalError, stackError], 'nested playbook frame removal failed');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (removalError !== undefined)
|
|
650
|
+
throw removalError;
|
|
651
|
+
})();
|
|
652
|
+
frame.removal = { reason, promise: operation };
|
|
653
|
+
return { claimed: true, reason, promise: operation };
|
|
654
|
+
};
|
|
655
|
+
const unwindFramesFrom = async (frame, reason = 'stack') => {
|
|
656
|
+
const index = frames.indexOf(frame);
|
|
657
|
+
if (index < 0)
|
|
658
|
+
return;
|
|
659
|
+
const failures = [];
|
|
660
|
+
while (frames.length > index) {
|
|
661
|
+
const current = leafFrame();
|
|
662
|
+
const removal = removeTopFrame(current, reason);
|
|
663
|
+
try {
|
|
664
|
+
await removal.promise;
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
failures.push(error);
|
|
668
|
+
}
|
|
669
|
+
if (frames.includes(current)) {
|
|
670
|
+
failures.push(new Error('nested playbook frame remained after removal attempt'));
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
clearLeafLedger();
|
|
675
|
+
if (failures.length === 1)
|
|
676
|
+
throw failures[0];
|
|
677
|
+
if (failures.length > 1) {
|
|
678
|
+
throw new AggregateError(failures, 'nested playbook stack disposal failed');
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const popChild = async (frame, status) => {
|
|
682
|
+
if (!frame.parent || (leafFrame() !== frame && !frame.removal)) {
|
|
683
|
+
throw new Error('nested playbook stack is not LIFO');
|
|
684
|
+
}
|
|
685
|
+
const parent = frame.parent.frame;
|
|
686
|
+
const removal = removeTopFrame(frame, 'return');
|
|
687
|
+
if (!removal.claimed) {
|
|
688
|
+
await removal.promise;
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
let cleanupError;
|
|
692
|
+
try {
|
|
693
|
+
await removal.promise;
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
cleanupError = error;
|
|
697
|
+
}
|
|
698
|
+
const message = status === 'returned'
|
|
699
|
+
? `◇ ${frameLabel(frame)} returned to ${frameLabel(parent)}`
|
|
700
|
+
: `◇ ${frameLabel(frame)} stopped; returning to ${frameLabel(parent)}`;
|
|
701
|
+
try {
|
|
702
|
+
await requireSession().emitStatus(message);
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
cleanupError =
|
|
706
|
+
cleanupError === undefined
|
|
707
|
+
? error
|
|
708
|
+
: new AggregateError([cleanupError, error], 'nested playbook return cleanup failed');
|
|
709
|
+
}
|
|
710
|
+
let visibilityError;
|
|
711
|
+
try {
|
|
712
|
+
await requestVisibility(parent.enablement);
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
visibilityError = error;
|
|
716
|
+
}
|
|
717
|
+
if (visibilityError !== undefined) {
|
|
718
|
+
if (cleanupError !== undefined) {
|
|
719
|
+
throw new VisibilityControlError(new AggregateError([cleanupError, visibilityError], 'nested playbook return and visibility failed'));
|
|
720
|
+
}
|
|
721
|
+
throw visibilityError;
|
|
722
|
+
}
|
|
723
|
+
if (cleanupError !== undefined)
|
|
724
|
+
throw cleanupError;
|
|
725
|
+
return true;
|
|
726
|
+
};
|
|
727
|
+
const disposeStack = async (reason) => {
|
|
728
|
+
const root = rootFrame();
|
|
729
|
+
if (!root)
|
|
730
|
+
return;
|
|
731
|
+
const rootId = root.entry.id;
|
|
732
|
+
const rootSessionId = root.sessionId;
|
|
733
|
+
const failures = [];
|
|
734
|
+
disposing = true;
|
|
735
|
+
try {
|
|
736
|
+
if (reason !== 'dispose') {
|
|
737
|
+
try {
|
|
738
|
+
await setMode('chat', reason, rootId, rootSessionId);
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
failures.push(error);
|
|
742
|
+
mode = 'chat';
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
mode = 'chat';
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
await unwindFramesFrom(root);
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
failures.push(error);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
disposing = false;
|
|
757
|
+
pendingChildParents.clear();
|
|
758
|
+
clearLeafLedger();
|
|
759
|
+
}
|
|
760
|
+
if (!root.internal) {
|
|
761
|
+
try {
|
|
762
|
+
if (reason === 'dismiss') {
|
|
763
|
+
await requireSession().emitStatus(`◇ ${frameLabel(root)} stopped`);
|
|
764
|
+
}
|
|
765
|
+
else if (reason === 'final') {
|
|
766
|
+
await requireSession().emitStatus(`◇ ${frameLabel(root)} finished`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
failures.push(error);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (failures.length === 1)
|
|
774
|
+
throw failures[0];
|
|
775
|
+
if (failures.length > 1) {
|
|
776
|
+
throw new AggregateError(failures, 'playbook stack disposal failed');
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const callResultFor = (frame, result) => {
|
|
780
|
+
if (result.outcome === 'terminal') {
|
|
781
|
+
return {
|
|
782
|
+
status: 'ok',
|
|
783
|
+
playbookId: frame.entry.id,
|
|
784
|
+
childSessionId: frame.sessionId,
|
|
785
|
+
state: result.state,
|
|
786
|
+
...(result.output !== undefined ? { output: result.output } : {}),
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
if (result.outcome === 'aborted') {
|
|
790
|
+
return {
|
|
791
|
+
status: 'aborted',
|
|
792
|
+
playbookId: frame.entry.id,
|
|
793
|
+
childSessionId: frame.sessionId,
|
|
794
|
+
state: result.state,
|
|
795
|
+
...(result.error ? { error: result.error } : {}),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
throw new Error(`playbook ${frame.entry.id} has not returned`);
|
|
799
|
+
};
|
|
800
|
+
const assertRetainableResult = (frame, result) => {
|
|
801
|
+
if (result.outcome === 'suspended')
|
|
802
|
+
return;
|
|
803
|
+
if (result.state.quiescent &&
|
|
804
|
+
result.state.tags.includes('playbook.parked')) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
throw new Error(`playbook ${frame.entry.id} returned outcome "${result.outcome}" ` +
|
|
808
|
+
'without a quiescent playbook.parked state');
|
|
809
|
+
};
|
|
810
|
+
const driveFrame = async (frame, text, context, signal = context.signal) => {
|
|
811
|
+
if (leafFrame() !== frame) {
|
|
812
|
+
throw new Error('only the active leaf may receive Boss input');
|
|
813
|
+
}
|
|
814
|
+
await requestVisibility(frame.enablement);
|
|
815
|
+
await setMode('engaged.driving', 'submit');
|
|
816
|
+
const result = await frame.runtime.handleBossInput({
|
|
817
|
+
text,
|
|
818
|
+
signal,
|
|
819
|
+
});
|
|
820
|
+
frame.state = result.state;
|
|
821
|
+
return result;
|
|
822
|
+
};
|
|
823
|
+
async function resumeParent(child, callResult, context, status = 'returned') {
|
|
824
|
+
const parentLink = child.parent;
|
|
825
|
+
if (!parentLink)
|
|
826
|
+
throw new Error('root playbook has no caller');
|
|
827
|
+
const parent = parentLink.frame;
|
|
828
|
+
const invocationSignal = child.invocationSignal;
|
|
829
|
+
let effectiveResult = callResult;
|
|
830
|
+
let ownsReturn = false;
|
|
831
|
+
let visibilityControlError;
|
|
832
|
+
try {
|
|
833
|
+
ownsReturn = await popChild(child, status);
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
ownsReturn = child.removal?.reason === 'return';
|
|
837
|
+
if (error instanceof VisibilityControlError) {
|
|
838
|
+
visibilityControlError = error;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
effectiveResult = {
|
|
842
|
+
status: context.signal.aborted ? 'aborted' : 'error',
|
|
843
|
+
playbookId: child.entry.id,
|
|
844
|
+
childSessionId: child.sessionId,
|
|
845
|
+
...(child.state ? { state: child.state } : {}),
|
|
846
|
+
error: normalizeErrorFull(error),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!ownsReturn ||
|
|
851
|
+
disposing ||
|
|
852
|
+
invocationSignal?.aborted ||
|
|
853
|
+
!frames.includes(parent)) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
let result;
|
|
857
|
+
try {
|
|
858
|
+
result = await parent.runtime.resumePlaybookCall({
|
|
859
|
+
callId: parentLink.callId,
|
|
860
|
+
result: effectiveResult,
|
|
861
|
+
signal: context.signal,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
catch (error) {
|
|
865
|
+
if (disposing || invocationSignal?.aborted)
|
|
866
|
+
return;
|
|
867
|
+
await returnBoundaryFailure(parent, error, context);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
parent.state = result.state;
|
|
871
|
+
await processFrameResult(parent, result, context);
|
|
872
|
+
if (visibilityControlError !== undefined)
|
|
873
|
+
throw visibilityControlError;
|
|
874
|
+
}
|
|
875
|
+
async function returnBoundaryFailure(frame, error, context) {
|
|
876
|
+
if (!frame.parent)
|
|
877
|
+
throw error;
|
|
878
|
+
await resumeParent(frame, {
|
|
879
|
+
status: context.signal.aborted ? 'aborted' : 'error',
|
|
880
|
+
playbookId: frame.entry.id,
|
|
881
|
+
childSessionId: frame.sessionId,
|
|
882
|
+
...(frame.state ? { state: frame.state } : {}),
|
|
883
|
+
error: normalizeErrorFull(error),
|
|
884
|
+
}, context);
|
|
885
|
+
}
|
|
886
|
+
async function processFrameResult(frame, result, context) {
|
|
887
|
+
if (result.outcome === 'terminal') {
|
|
888
|
+
if (frame.parent) {
|
|
889
|
+
await resumeParent(frame, callResultFor(frame, result), context);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
await disposeStack('final');
|
|
893
|
+
}
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (result.outcome === 'aborted' && frame.parent) {
|
|
897
|
+
await resumeParent(frame, callResultFor(frame, result), context);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
assertRetainableResult(frame, result);
|
|
901
|
+
if (leafFrame()) {
|
|
902
|
+
await setMode('engaged.parked', `turn:${result.outcome}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const disposeAbandonedChild = async (child) => {
|
|
906
|
+
if (disposing || !frames.includes(child) || !child.parent)
|
|
907
|
+
return;
|
|
908
|
+
if (child.removal) {
|
|
909
|
+
await child.removal.promise;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const parent = child.parent.frame;
|
|
913
|
+
let cleanupError;
|
|
914
|
+
try {
|
|
915
|
+
await unwindFramesFrom(child, 'abandoned');
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
cleanupError = error;
|
|
919
|
+
}
|
|
920
|
+
if (frames.includes(parent)) {
|
|
921
|
+
await requestVisibility(parent.enablement);
|
|
922
|
+
}
|
|
923
|
+
if (cleanupError !== undefined)
|
|
924
|
+
throw cleanupError;
|
|
925
|
+
};
|
|
926
|
+
callNestedPlaybook = async (parent, request, invocationSignal) => {
|
|
927
|
+
if (!activeContext) {
|
|
928
|
+
throw new Error('callPlaybook invoked outside a Boss turn');
|
|
929
|
+
}
|
|
930
|
+
invocationSignal.throwIfAborted();
|
|
931
|
+
if (leafFrame() !== parent) {
|
|
932
|
+
throw new Error('only the active leaf may call a child playbook');
|
|
933
|
+
}
|
|
934
|
+
if (pendingChildParents.has(parent)) {
|
|
935
|
+
throw new Error('playbook frame already has an outstanding child');
|
|
936
|
+
}
|
|
937
|
+
if (typeof request.callId !== 'string' || request.callId.trim() === '') {
|
|
938
|
+
throw new Error('nested playbook call id must be a non-empty string');
|
|
939
|
+
}
|
|
940
|
+
if (typeof request.playbookId !== 'string' ||
|
|
941
|
+
request.playbookId.trim() === '') {
|
|
942
|
+
throw new Error('nested playbook id must be a non-empty string');
|
|
943
|
+
}
|
|
944
|
+
if (typeof request.text !== 'string') {
|
|
945
|
+
throw new Error('nested playbook input text must be a string');
|
|
946
|
+
}
|
|
947
|
+
if (request.playbookId === INTERNAL_CAPTAIN_ID) {
|
|
948
|
+
throw new Error('the internal Captain playbook cannot call itself');
|
|
949
|
+
}
|
|
950
|
+
const entry = byId.get(request.playbookId);
|
|
951
|
+
if (!entry) {
|
|
952
|
+
throw new Error(`playbook "${request.playbookId}" is not enabled`);
|
|
953
|
+
}
|
|
954
|
+
if (frames.some((frame) => frame.entry.id === request.playbookId)) {
|
|
955
|
+
throw new Error(`nested playbook cycle: ${[
|
|
956
|
+
...frames.map((frame) => frame.entry.id),
|
|
957
|
+
request.playbookId,
|
|
958
|
+
].join(' -> ')}`);
|
|
959
|
+
}
|
|
960
|
+
pendingChildParents.add(parent);
|
|
961
|
+
let child;
|
|
962
|
+
try {
|
|
963
|
+
child = makeFrame(enablementById.get(entry.id), {
|
|
964
|
+
frame: parent,
|
|
965
|
+
callId: request.callId,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
pendingChildParents.delete(parent);
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
frames.push(child);
|
|
973
|
+
clearLeafLedger();
|
|
974
|
+
let calledStatusEmitted = false;
|
|
975
|
+
let returnStatusHandled = false;
|
|
976
|
+
try {
|
|
977
|
+
await initFrame(child);
|
|
978
|
+
invocationSignal.throwIfAborted();
|
|
979
|
+
await requireSession().emitStatus(`◇ ${frameLabel(child)} called by ${frameLabel(parent)}`);
|
|
980
|
+
calledStatusEmitted = true;
|
|
981
|
+
const result = await driveFrame(child, request.text, activeContext, AbortSignal.any([invocationSignal, activeContext.signal]));
|
|
982
|
+
if (result.outcome === 'terminal' || result.outcome === 'aborted') {
|
|
983
|
+
const callResult = callResultFor(child, result);
|
|
984
|
+
returnStatusHandled = true;
|
|
985
|
+
const returned = await popChild(child, result.outcome === 'aborted' ? 'stopped' : 'returned');
|
|
986
|
+
if (!returned) {
|
|
987
|
+
throw new Error('nested playbook return lost its active frame');
|
|
988
|
+
}
|
|
989
|
+
return { state: 'settled', result: callResult };
|
|
990
|
+
}
|
|
991
|
+
assertRetainableResult(child, result);
|
|
992
|
+
if (invocationSignal.aborted) {
|
|
993
|
+
const callResult = {
|
|
994
|
+
status: 'aborted',
|
|
995
|
+
playbookId: child.entry.id,
|
|
996
|
+
childSessionId: child.sessionId,
|
|
997
|
+
state: result.state,
|
|
998
|
+
};
|
|
999
|
+
returnStatusHandled = true;
|
|
1000
|
+
const returned = await popChild(child, 'stopped');
|
|
1001
|
+
if (!returned) {
|
|
1002
|
+
throw new Error('nested playbook abort lost its active frame');
|
|
1003
|
+
}
|
|
1004
|
+
return { state: 'settled', result: callResult };
|
|
1005
|
+
}
|
|
1006
|
+
const abortListener = () => {
|
|
1007
|
+
registerPlaybookAbortCleanup(invocationSignal, disposeAbandonedChild(child));
|
|
1008
|
+
};
|
|
1009
|
+
child.invocationSignal = invocationSignal;
|
|
1010
|
+
child.abortListener = abortListener;
|
|
1011
|
+
invocationSignal.addEventListener('abort', abortListener, { once: true });
|
|
1012
|
+
return { state: 'suspended', childSessionId: child.sessionId };
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
let boundaryError = error;
|
|
1016
|
+
let visibilityControlFailure = error instanceof VisibilityControlError;
|
|
1017
|
+
if (frames.includes(child)) {
|
|
1018
|
+
try {
|
|
1019
|
+
await unwindFramesFrom(child, 'stack');
|
|
1020
|
+
}
|
|
1021
|
+
catch (cleanupError) {
|
|
1022
|
+
boundaryError = new AggregateError([error, cleanupError], 'nested playbook call and cleanup failed');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
pendingChildParents.delete(parent);
|
|
1026
|
+
if (calledStatusEmitted && !returnStatusHandled) {
|
|
1027
|
+
try {
|
|
1028
|
+
await requireSession().emitStatus(`◇ ${frameLabel(child)} stopped; returning to ${frameLabel(parent)}`);
|
|
1029
|
+
}
|
|
1030
|
+
catch (statusError) {
|
|
1031
|
+
boundaryError = new AggregateError([boundaryError, statusError], 'nested playbook failure status emission failed');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (frames.includes(parent)) {
|
|
1035
|
+
try {
|
|
1036
|
+
await requestVisibility(parent.enablement);
|
|
1037
|
+
}
|
|
1038
|
+
catch (visibilityError) {
|
|
1039
|
+
visibilityControlFailure = true;
|
|
1040
|
+
boundaryError = new AggregateError([boundaryError, visibilityError], 'nested playbook call return failed');
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (visibilityControlFailure)
|
|
1044
|
+
throw boundaryError;
|
|
1045
|
+
return {
|
|
1046
|
+
state: 'settled',
|
|
1047
|
+
result: {
|
|
1048
|
+
status: invocationSignal.aborted ? 'aborted' : 'error',
|
|
1049
|
+
playbookId: request.playbookId,
|
|
1050
|
+
childSessionId: child.sessionId,
|
|
1051
|
+
error: normalizeErrorFull(boundaryError),
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
const submitToActive = async (frame, text, context) => {
|
|
1057
|
+
const policy = frame.entry.summaryPolicy;
|
|
339
1058
|
const summaryCounts = {
|
|
340
1059
|
interruptions: 0,
|
|
341
1060
|
copyPastes: 0,
|
|
342
1061
|
};
|
|
343
1062
|
const summaryStateCounts = new Map();
|
|
344
|
-
let shouldSummarize = false;
|
|
345
1063
|
activeTurnSummary = policy
|
|
346
|
-
? {
|
|
1064
|
+
? {
|
|
1065
|
+
owner: frame,
|
|
1066
|
+
counts: summaryCounts,
|
|
1067
|
+
stateCounts: summaryStateCounts,
|
|
1068
|
+
}
|
|
347
1069
|
: undefined;
|
|
348
|
-
|
|
1070
|
+
let completed = false;
|
|
349
1071
|
try {
|
|
350
|
-
await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1072
|
+
const result = await driveFrame(frame, text, context);
|
|
1073
|
+
await processFrameResult(frame, result, context);
|
|
1074
|
+
completed = true;
|
|
1075
|
+
}
|
|
1076
|
+
catch (error) {
|
|
1077
|
+
if (frame.parent && frames.includes(frame)) {
|
|
1078
|
+
await returnBoundaryFailure(frame, error, context);
|
|
1079
|
+
completed = true;
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
355
1084
|
}
|
|
356
1085
|
finally {
|
|
357
1086
|
activeTurnSummary = undefined;
|
|
358
|
-
if (
|
|
359
|
-
finalDisposalRequested = undefined;
|
|
360
|
-
await disposeActive('final');
|
|
361
|
-
}
|
|
362
|
-
else if (active === engagement && mode === 'engaged.driving') {
|
|
1087
|
+
if (leafFrame() && mode === 'engaged.driving') {
|
|
363
1088
|
await setMode('engaged.parked', 'turn.settled');
|
|
364
1089
|
}
|
|
365
1090
|
}
|
|
366
|
-
if (
|
|
1091
|
+
if (completed && policy) {
|
|
367
1092
|
const progressRounds = summaryProgressRoundCount(summaryStateCounts);
|
|
368
|
-
await callVisibleTurnSummary(context, {
|
|
369
|
-
playbookId:
|
|
1093
|
+
await callVisibleTurnSummary(frame, context, {
|
|
1094
|
+
playbookId: frame.entry.id,
|
|
370
1095
|
submittedText: text,
|
|
371
1096
|
counts: summaryCounts,
|
|
372
1097
|
progressPhrase: summaryProgressPhrase(summaryStateCounts),
|
|
@@ -375,68 +1100,30 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
375
1100
|
});
|
|
376
1101
|
}
|
|
377
1102
|
};
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
if (!engagement)
|
|
381
|
-
return;
|
|
382
|
-
const playbookId = engagement.entry.id;
|
|
383
|
-
const commandLabel = `/${engagement.enablement.command}`;
|
|
384
|
-
active = undefined;
|
|
385
|
-
finalDisposalRequested = undefined;
|
|
386
|
-
if (reason === 'dispose') {
|
|
387
|
-
mode = 'chat';
|
|
388
|
-
await engagement.runtime.dispose();
|
|
389
|
-
latestSubRuntimeStateId = undefined;
|
|
390
|
-
pendingBossQuestion = undefined;
|
|
391
|
-
lastError = undefined;
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
await setMode('chat', reason, playbookId);
|
|
395
|
-
await engagement.runtime.dispose();
|
|
396
|
-
if (reason === 'dismiss') {
|
|
397
|
-
await requireSession().emitStatus(`◇ ${commandLabel} stopped`);
|
|
398
|
-
}
|
|
399
|
-
else if (reason === 'final') {
|
|
400
|
-
await requireSession().emitStatus(`◇ ${commandLabel} finished`);
|
|
401
|
-
}
|
|
402
|
-
latestSubRuntimeStateId = undefined;
|
|
403
|
-
pendingBossQuestion = undefined;
|
|
404
|
-
lastError = undefined;
|
|
405
|
-
};
|
|
406
|
-
const callVisibleChat = async (context, message) => {
|
|
407
|
-
const result = await context.callCaptain(visibleChatEnvelope(message));
|
|
1103
|
+
const callVisibleChat = async (frame, context, message) => {
|
|
1104
|
+
const result = await callCaptainQueued(frame, context, visibleChatEnvelope(message), { visibility: 'visible', resume: false, allowedTools: [] }, context.signal);
|
|
408
1105
|
if (result.status !== 'ok') {
|
|
409
1106
|
throw new Error(result.error ?? `callCaptain status "${result.status}"`);
|
|
410
1107
|
}
|
|
411
1108
|
};
|
|
412
|
-
const callVisibleTurnSummary = async (context, input) => {
|
|
413
|
-
const result = await context
|
|
1109
|
+
const callVisibleTurnSummary = async (frame, context, input) => {
|
|
1110
|
+
const result = await callCaptainQueued(frame, context, visibleTurnSummaryEnvelope(input), { visibility: 'visible', resume: false, allowedTools: [] }, context.signal);
|
|
414
1111
|
if (result.status !== 'ok') {
|
|
415
1112
|
throw new Error(result.error ?? `callCaptain status "${result.status}"`);
|
|
416
1113
|
}
|
|
417
1114
|
};
|
|
418
|
-
const
|
|
419
|
-
'You are the Playbook Captain shell
|
|
1115
|
+
const hiddenLifecycleEnvelope = (prompt) => [
|
|
1116
|
+
'You are the Playbook Captain shell lifecycle classifier.',
|
|
420
1117
|
'This is hidden control work. Return only one JSON object and no prose.',
|
|
421
1118
|
'Allowed decisions:',
|
|
422
|
-
'{"decision":"
|
|
423
|
-
'{"decision":"
|
|
424
|
-
'
|
|
425
|
-
'
|
|
426
|
-
'
|
|
427
|
-
'Treat unregistered slash-prefixed input as ordinary router input.',
|
|
428
|
-
`Ledger:\n${JSON.stringify(ledgerSnapshot())}`,
|
|
429
|
-
`Registry:\n${JSON.stringify(entries.map((entry) => ({
|
|
430
|
-
id: entry.id,
|
|
431
|
-
command: enablementById.get(entry.id)?.command ?? entry.command,
|
|
432
|
-
intent: entry.intent,
|
|
433
|
-
})))}`,
|
|
1119
|
+
'{"decision":"deliver"}',
|
|
1120
|
+
'{"decision":"dismiss"}',
|
|
1121
|
+
'Choose dismiss only when Boss explicitly asks to stop or dismiss the current active engagement.',
|
|
1122
|
+
'Choose deliver for every task instruction, answer, clarification, continuation, command-like near miss, or ambiguous message.',
|
|
1123
|
+
'Do not rewrite, summarize, or copy the Boss message into the result.',
|
|
434
1124
|
`Boss message:\n${prompt}`,
|
|
435
1125
|
].join('\n\n');
|
|
436
|
-
const
|
|
437
|
-
await callVisibleChat(context, "I'm not sure whether this should be Captain chat or a /code task. Please clarify.");
|
|
438
|
-
};
|
|
439
|
-
const parseRouterDecision = (finalText) => {
|
|
1126
|
+
const parseLifecycleDecision = (finalText) => {
|
|
440
1127
|
let parsed;
|
|
441
1128
|
try {
|
|
442
1129
|
parsed = JSON.parse(finalText);
|
|
@@ -451,87 +1138,57 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
451
1138
|
}
|
|
452
1139
|
const record = parsed;
|
|
453
1140
|
const decision = record.decision;
|
|
454
|
-
if (decision === '
|
|
455
|
-
return
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
if (decision === 'dispatch') {
|
|
460
|
-
return typeof record.playbookId === 'string' &&
|
|
461
|
-
byId.has(record.playbookId) &&
|
|
462
|
-
typeof record.text === 'string' &&
|
|
463
|
-
record.text.trim()
|
|
464
|
-
? {
|
|
465
|
-
decision,
|
|
466
|
-
playbookId: record.playbookId,
|
|
467
|
-
text: record.text.trim(),
|
|
468
|
-
}
|
|
469
|
-
: undefined;
|
|
470
|
-
}
|
|
471
|
-
if (decision === 'sub') {
|
|
472
|
-
return typeof record.text === 'string' && record.text.trim()
|
|
473
|
-
? { decision, text: record.text.trim() }
|
|
474
|
-
: undefined;
|
|
475
|
-
}
|
|
476
|
-
if (decision === 'dismiss') {
|
|
477
|
-
return typeof record.text === 'string' && record.text.trim()
|
|
478
|
-
? { decision, text: record.text.trim() }
|
|
479
|
-
: { decision };
|
|
480
|
-
}
|
|
1141
|
+
if (decision === 'deliver')
|
|
1142
|
+
return { decision };
|
|
1143
|
+
if (decision === 'dismiss')
|
|
1144
|
+
return { decision };
|
|
481
1145
|
return undefined;
|
|
482
1146
|
};
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
return;
|
|
1147
|
+
const routeEngaged = async (turn, context) => {
|
|
1148
|
+
const leaf = leafFrame();
|
|
1149
|
+
if (!leaf) {
|
|
1150
|
+
throw new Error('engaged lifecycle routing requires an active leaf');
|
|
488
1151
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
await
|
|
492
|
-
|
|
1152
|
+
let decision;
|
|
1153
|
+
try {
|
|
1154
|
+
const result = await callCaptainQueued(leaf, context, hiddenLifecycleEnvelope(turn.prompt), { visibility: 'hidden', resume: false, allowedTools: [] }, context.signal);
|
|
1155
|
+
if (result.status === 'ok' && result.finalText !== undefined) {
|
|
1156
|
+
decision = parseLifecycleDecision(result.finalText);
|
|
1157
|
+
}
|
|
493
1158
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return;
|
|
1159
|
+
catch {
|
|
1160
|
+
// Lifecycle classification is advisory. Delivery is fail-open so an
|
|
1161
|
+
// unavailable classifier can never consume a parked leaf's Boss reply.
|
|
498
1162
|
}
|
|
499
|
-
if (decision
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
await routerClarification(context);
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
const engagement = await engage(entry);
|
|
506
|
-
await submitToActive(engagement, decision.text, context);
|
|
1163
|
+
if (decision?.decision !== 'dismiss') {
|
|
1164
|
+
lastRouteDecision = 'deliver';
|
|
1165
|
+
await submitToActive(leaf, turn.prompt, context);
|
|
507
1166
|
return;
|
|
508
1167
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1168
|
+
lastRouteDecision = 'dismiss';
|
|
1169
|
+
if (leaf.parent) {
|
|
1170
|
+
await resumeParent(leaf, {
|
|
1171
|
+
status: 'aborted',
|
|
1172
|
+
playbookId: leaf.entry.id,
|
|
1173
|
+
childSessionId: leaf.sessionId,
|
|
1174
|
+
...(leaf.state ? { state: leaf.state } : {}),
|
|
1175
|
+
}, context, 'stopped');
|
|
516
1176
|
}
|
|
517
|
-
|
|
518
|
-
await
|
|
519
|
-
return;
|
|
1177
|
+
else {
|
|
1178
|
+
await disposeStack('dismiss');
|
|
520
1179
|
}
|
|
521
|
-
const dismissedCommandLabel = `/${active.enablement.command}`;
|
|
522
|
-
await disposeActive('dismiss');
|
|
523
|
-
await callVisibleChat(context, decision.text ?? `${dismissedCommandLabel} stopped.`);
|
|
524
1180
|
};
|
|
525
1181
|
const handleRegisteredCommand = async (entry, text, context) => {
|
|
526
1182
|
const enablement = enablementById.get(entry.id);
|
|
527
|
-
|
|
528
|
-
|
|
1183
|
+
const leaf = leafFrame();
|
|
1184
|
+
if (leaf && leaf.entry.id !== entry.id) {
|
|
1185
|
+
await callVisibleChat(leaf, context, `${frameLabel(leaf)} is already running. Finish or stop it before starting /${enablement.command}.`);
|
|
529
1186
|
return;
|
|
530
1187
|
}
|
|
531
|
-
const engagement = await engage(entry);
|
|
1188
|
+
const engagement = leaf ?? (await engage(entry));
|
|
532
1189
|
if (text.length === 0) {
|
|
533
1190
|
await requestVisibility(engagement.enablement);
|
|
534
|
-
await callVisibleChat(context, `Ask what task to run with /${enablement.command}.`);
|
|
1191
|
+
await callVisibleChat(engagement, context, `Ask what task to run with /${enablement.command}.`);
|
|
535
1192
|
return;
|
|
536
1193
|
}
|
|
537
1194
|
await submitToActive(engagement, text, context);
|
|
@@ -548,10 +1205,16 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
548
1205
|
for (const enablement of enablementById.values()) {
|
|
549
1206
|
enablement.entry.validateOptions(enablement.optionInput);
|
|
550
1207
|
}
|
|
1208
|
+
internalCaptainEnablement = createInternalCaptainEnablement();
|
|
551
1209
|
await setMode('chat', 'init');
|
|
552
1210
|
},
|
|
553
1211
|
async handleBossTurn(turn, context) {
|
|
554
1212
|
requireSession();
|
|
1213
|
+
if (activeTurnHostCalls !== undefined) {
|
|
1214
|
+
throw new Error('cannot handle concurrent Boss turns');
|
|
1215
|
+
}
|
|
1216
|
+
const turnHostCalls = new Set();
|
|
1217
|
+
activeTurnHostCalls = turnHostCalls;
|
|
555
1218
|
activeContext = context;
|
|
556
1219
|
try {
|
|
557
1220
|
const command = parseRegisteredCommand(turn.prompt);
|
|
@@ -562,15 +1225,31 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
562
1225
|
return;
|
|
563
1226
|
}
|
|
564
1227
|
}
|
|
565
|
-
|
|
1228
|
+
const leaf = leafFrame();
|
|
1229
|
+
if (leaf) {
|
|
1230
|
+
await routeEngaged(turn, context);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (turn.prompt.trim().length === 0)
|
|
1234
|
+
return;
|
|
1235
|
+
const captain = await engageInternalCaptain();
|
|
1236
|
+
await submitToActive(captain, turn.prompt, context);
|
|
566
1237
|
}
|
|
567
1238
|
finally {
|
|
1239
|
+
await drainHostCalls(turnHostCalls);
|
|
1240
|
+
if (activeTurnHostCalls === turnHostCalls) {
|
|
1241
|
+
activeTurnHostCalls = undefined;
|
|
1242
|
+
}
|
|
568
1243
|
activeContext = undefined;
|
|
569
1244
|
}
|
|
570
1245
|
},
|
|
1246
|
+
async prepareDispose() {
|
|
1247
|
+
activeContext = undefined;
|
|
1248
|
+
await disposeStack('dispose');
|
|
1249
|
+
},
|
|
571
1250
|
async dispose() {
|
|
572
1251
|
activeContext = undefined;
|
|
573
|
-
await
|
|
1252
|
+
await disposeStack('dispose');
|
|
574
1253
|
},
|
|
575
1254
|
};
|
|
576
1255
|
}
|