@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.
Files changed (51) hide show
  1. package/README.md +190 -151
  2. package/package.json +50 -6
  3. package/reference/sdlc/captain.md +102 -0
  4. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +227 -0
  5. package/reference/sdlc/captain.playbook/captain.fsm.js +628 -0
  6. package/reference/sdlc/captain.playbook/captain.fsm.ts +851 -0
  7. package/reference/sdlc/captain.playbook/captain.gears.md +60 -0
  8. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +23 -0
  9. package/reference/sdlc/captain.playbook/captain.playbook.js +1053 -0
  10. package/reference/sdlc/captain.playbook/captain.playbook.ts +1144 -0
  11. package/reference/sdlc/code.playbook/bin/playbook.js +158 -12
  12. package/reference/sdlc/code.playbook/bin/run.js +999 -0
  13. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -4
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +2 -2
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.js +1 -1
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +6 -6
  17. package/reference/sdlc/code.playbook/code.fsm.js +334 -102
  18. package/reference/sdlc/code.playbook/code.fsm.ts +467 -180
  19. package/reference/sdlc/code.playbook/code.gears.md +11 -10
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +16 -19
  21. package/reference/sdlc/code.playbook/code.playbook.js +199 -488
  22. package/reference/sdlc/code.playbook/code.playbook.ts +327 -566
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +0 -3
  24. package/reference/sdlc/code.playbook/code.registry.js +0 -3
  25. package/reference/sdlc/code.playbook/code.registry.ts +0 -6
  26. package/reference/sdlc/code.playbook/playbook-captain.d.ts +9 -4
  27. package/reference/sdlc/code.playbook/playbook-captain.js +889 -210
  28. package/reference/sdlc/code.playbook/playbook-captain.ts +1136 -257
  29. package/reference/sdlc/code.playbook/playbook.config.template.yaml +21 -0
  30. package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +396 -0
  31. package/reference/sdlc/discuss.playbook/discuss.fsm.js +2066 -0
  32. package/reference/sdlc/discuss.playbook/discuss.fsm.ts +2464 -0
  33. package/reference/sdlc/discuss.playbook/discuss.gears.md +251 -0
  34. package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +113 -0
  35. package/reference/sdlc/discuss.playbook/discuss.playbook.js +1514 -0
  36. package/reference/sdlc/discuss.playbook/discuss.playbook.ts +1926 -0
  37. package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +58 -0
  38. package/reference/sdlc/discuss.playbook/discuss.registry.js +97 -0
  39. package/reference/sdlc/discuss.playbook/discuss.registry.ts +153 -0
  40. package/slc/gears2fsm.md +557 -57
  41. package/slc/link.md +1165 -89
  42. package/slc/optimize.md +92 -0
  43. package/slc/text2gears.md +255 -7
  44. package/src/runtime.d.ts +146 -3
  45. package/src/runtime.ts +201 -2
  46. package/src/xstate-playbook-runtime.d.ts +201 -0
  47. package/src/xstate-playbook-runtime.js +2058 -0
  48. package/src/xstate-playbook-runtime.ts +2792 -0
  49. package/src/xstate-runtime.d.ts +95 -0
  50. package/src/xstate-runtime.js +1258 -0
  51. package/src/xstate-runtime.ts +1816 -0
@@ -0,0 +1,628 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+ import { assign, fromPromise, setup } from 'xstate';
4
+ const ROUTING_PROMPT = [
5
+ 'Boss intent: <boss-intent>',
6
+ 'Enabled playbooks: <enabled-playbooks>',
7
+ 'You are routing this intent, not performing the requested work.',
8
+ 'Use only the Boss intent and enabled-playbooks catalog supplied here.',
9
+ 'Do not investigate the task, inspect files or project state, use tools, or attempt the specialized work yourself.',
10
+ "Preserve Boss's intended outcome and constraints.",
11
+ 'If the supplied evidence identifies a useful route, select an enabled playbook; do not finish the intent yourself.',
12
+ 'Ask exactly one concise question only when its answer is necessary to choose a useful route or call order.',
13
+ 'For a complex intent, divide it into the smallest finite ordered plan of useful playbook calls.',
14
+ 'Name the selected first playbook and state its complete standalone request containing only the context it needs.',
15
+ 'List any later playbook calls in their intended order after the selected first call.',
16
+ 'Do not call a playbook merely to restate or classify the intent.',
17
+ 'Write only concise human-facing routing prose or the one routing question.',
18
+ 'Do not emit JSON, guard names, result property names, or control instructions.',
19
+ 'Do not expose internal state ids, session ids, call ids, stack data, hidden control data, or private reasoning.',
20
+ ].join('\n');
21
+ const REASSESS_PROMPT = [
22
+ 'Boss intent: <boss-intent>',
23
+ 'Enabled playbooks: <enabled-playbooks>',
24
+ 'Remaining plan: <remaining-plan>',
25
+ 'Completed call results: <completed-call-results>',
26
+ "Preserve Boss's intended outcome and constraints.",
27
+ 'Treat each returned result as evidence and revise the remaining plan when needed.',
28
+ 'A continuing decision must strictly reduce the remaining plan length.',
29
+ 'Do not repeat an equivalent failed or completed call without new information.',
30
+ 'If the intent is fulfilled, give Boss one concise final response that states the result or actionable conclusion.',
31
+ 'Do not finish with a bare acknowledgement, a promise to act, or an announcement that the round is complete.',
32
+ 'If information from Boss is now necessary, ask exactly one concise question.',
33
+ 'Otherwise name exactly one next enabled playbook and state its complete standalone request containing only the context it needs.',
34
+ 'List any still-later playbook calls in their intended order after the selected next call.',
35
+ 'Write only concise human-facing final, question, or routing prose.',
36
+ 'Do not emit JSON, guard names, result property names, or control instructions.',
37
+ 'Do not expose internal state ids, session ids, call ids, stack data, hidden control data, or private reasoning.',
38
+ ].join('\n');
39
+ 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>`.";
40
+ const ROUTING_RESULTS = {
41
+ question: 'Captain asked the one material routing question. Output shall include `question: <verbatim final text from the visible Captain call>`.',
42
+ delegation: 'Captain selected the first useful call. Output shall include `remainingPlan: <finite JSON-safe array of only later calls>`, `nextPlaybookId: <selected stable enabled-playbook id>`, and `nextPlaybookInput: <complete standalone request>`.',
43
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
44
+ };
45
+ const REASSESS_RESULTS = {
46
+ final: 'Captain gave Boss the concrete result or actionable conclusion. Output shall include `response: <verbatim final text from the visible Captain call>`.',
47
+ followUpQuestion: 'Captain asked one necessary follow-up question. Output shall include `question: <verbatim final text from the visible Captain call>`.',
48
+ continuing: 'Captain selected another useful call. Output shall include `remainingPlan: <strictly shorter finite JSON-safe array of only later calls>`, `nextPlaybookId: <selected stable enabled-playbook id>`, and `nextPlaybookInput: <complete standalone request>`.',
49
+ needsBossReply: NEEDS_BOSS_REPLY_DESCRIPTION,
50
+ };
51
+ function isPlainRecord(value) {
52
+ if (value === null || typeof value !== 'object') {
53
+ return false;
54
+ }
55
+ const prototype = Object.getPrototypeOf(value);
56
+ return prototype === Object.prototype || prototype === null;
57
+ }
58
+ function isJsonValue(value, seen = []) {
59
+ if (value === null || typeof value === 'boolean' || typeof value === 'string') {
60
+ return true;
61
+ }
62
+ if (typeof value === 'number') {
63
+ return Number.isFinite(value);
64
+ }
65
+ if (typeof value !== 'object') {
66
+ return false;
67
+ }
68
+ if (seen.includes(value)) {
69
+ return false;
70
+ }
71
+ if (Array.isArray(value)) {
72
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
73
+ return false;
74
+ }
75
+ const keys = Reflect.ownKeys(value);
76
+ if (keys.length !== value.length + 1 || !keys.includes('length')) {
77
+ return false;
78
+ }
79
+ for (let index = 0; index < value.length; index += 1) {
80
+ const key = String(index);
81
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
82
+ return false;
83
+ }
84
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
85
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
86
+ return false;
87
+ }
88
+ if (!isJsonValue(descriptor.value, [...seen, value])) {
89
+ return false;
90
+ }
91
+ }
92
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
93
+ return Boolean(lengthDescriptor && !lengthDescriptor.enumerable && !lengthDescriptor.configurable && lengthDescriptor.value === value.length);
94
+ }
95
+ if (!isPlainRecord(value)) {
96
+ return false;
97
+ }
98
+ for (const key of Reflect.ownKeys(value)) {
99
+ if (typeof key !== 'string') {
100
+ return false;
101
+ }
102
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
103
+ if (!descriptor || !descriptor.enumerable || !('value' in descriptor)) {
104
+ return false;
105
+ }
106
+ if (!isJsonValue(descriptor.value, [...seen, value])) {
107
+ return false;
108
+ }
109
+ }
110
+ return true;
111
+ }
112
+ function isJsonArray(value) {
113
+ return Array.isArray(value) && isJsonValue(value);
114
+ }
115
+ function hasDoneOutput(event) {
116
+ return isPlainRecord(event) && 'output' in event;
117
+ }
118
+ function hasErrorValue(event) {
119
+ return isPlainRecord(event) && 'error' in event;
120
+ }
121
+ function isNonEmptyString(value) {
122
+ return typeof value === 'string' && value.trim().length > 0;
123
+ }
124
+ function isRoutingQuestionOutput(output) {
125
+ return isPlainRecord(output) && (output.guard === 'question' || output.guard === 'needsBossReply') && isNonEmptyString(output.question);
126
+ }
127
+ function isReassessQuestionOutput(output) {
128
+ return isPlainRecord(output) && (output.guard === 'followUpQuestion' || output.guard === 'needsBossReply') && isNonEmptyString(output.question);
129
+ }
130
+ function isDelegationOutput(output) {
131
+ return (isPlainRecord(output) &&
132
+ output.guard === 'delegation' &&
133
+ isJsonArray(output.remainingPlan) &&
134
+ isNonEmptyString(output.nextPlaybookId) &&
135
+ isNonEmptyString(output.nextPlaybookInput));
136
+ }
137
+ function isContinuingOutput(output) {
138
+ return (isPlainRecord(output) &&
139
+ output.guard === 'continuing' &&
140
+ isJsonArray(output.remainingPlan) &&
141
+ isNonEmptyString(output.nextPlaybookId) &&
142
+ isNonEmptyString(output.nextPlaybookInput));
143
+ }
144
+ function isFinalOutput(output) {
145
+ return isPlainRecord(output) && output.guard === 'final' && isNonEmptyString(output.response);
146
+ }
147
+ function outputFrom(event) {
148
+ return hasDoneOutput(event) ? event.output : undefined;
149
+ }
150
+ function errorFrom(event) {
151
+ return hasErrorValue(event) ? event.error : undefined;
152
+ }
153
+ function targetInCatalog(context, playbookId) {
154
+ return context.enabledPlaybooks.some((entry) => entry.id === playbookId);
155
+ }
156
+ function callSignature(playbookId, text) {
157
+ return JSON.stringify([playbookId, text]);
158
+ }
159
+ function canEnterDynamicCall(context, playbookId, text) {
160
+ return (isNonEmptyString(playbookId) &&
161
+ isNonEmptyString(text) &&
162
+ playbookId !== context.selfPlaybookId &&
163
+ targetInCatalog(context, playbookId) &&
164
+ !context.callHistory.includes(callSignature(playbookId, text)));
165
+ }
166
+ function normalizeError(error) {
167
+ if (error instanceof Error) {
168
+ const normalized = {
169
+ name: error.name || 'Error',
170
+ message: error.message || 'Unknown error',
171
+ };
172
+ if (typeof error.stack === 'string') {
173
+ normalized.stack = error.stack;
174
+ }
175
+ return normalized;
176
+ }
177
+ if (isPlainRecord(error) && typeof error.name === 'string' && typeof error.message === 'string') {
178
+ const normalized = {
179
+ name: error.name,
180
+ message: error.message,
181
+ };
182
+ if (typeof error.stack === 'string') {
183
+ normalized.stack = error.stack;
184
+ }
185
+ return normalized;
186
+ }
187
+ return { name: 'Error', message: String(error) };
188
+ }
189
+ function isNormalizedError(value) {
190
+ const allowed = new Set(['name', 'message', 'stack']);
191
+ return (isPlainRecord(value) &&
192
+ Reflect.ownKeys(value).every((key) => typeof key === 'string' && allowed.has(key)) &&
193
+ isNonEmptyString(value.name) &&
194
+ typeof value.message === 'string' &&
195
+ (!('stack' in value) || typeof value.stack === 'string'));
196
+ }
197
+ function isStringArray(value) {
198
+ return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
199
+ }
200
+ function isPlaybookStateValue(value, seen = []) {
201
+ if (typeof value === 'string') {
202
+ return true;
203
+ }
204
+ if (!isPlainRecord(value) || seen.includes(value)) {
205
+ return false;
206
+ }
207
+ return Reflect.ownKeys(value).every((key) => {
208
+ if (typeof key !== 'string') {
209
+ return false;
210
+ }
211
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
212
+ return Boolean(descriptor &&
213
+ descriptor.enumerable &&
214
+ 'value' in descriptor &&
215
+ isPlaybookStateValue(descriptor.value, [...seen, value]));
216
+ });
217
+ }
218
+ function isPlaybookState(value) {
219
+ if (!isPlainRecord(value)) {
220
+ return false;
221
+ }
222
+ const allowed = new Set(['value', 'activeStateIds', 'tags', 'status', 'quiescent', 'stateId']);
223
+ if (Reflect.ownKeys(value).some((key) => typeof key !== 'string' || !allowed.has(key))) {
224
+ return false;
225
+ }
226
+ return (isPlaybookStateValue(value.value) &&
227
+ isStringArray(value.activeStateIds) &&
228
+ isStringArray(value.tags) &&
229
+ (value.status === 'active' || value.status === 'done' || value.status === 'error' || value.status === 'stopped') &&
230
+ typeof value.quiescent === 'boolean' &&
231
+ (!('stateId' in value) || typeof value.stateId === 'string'));
232
+ }
233
+ function publicChildResult(error) {
234
+ if (!(error instanceof Error) || !('result' in error)) {
235
+ return undefined;
236
+ }
237
+ const result = error.result;
238
+ return isPlainRecord(result) ? result : undefined;
239
+ }
240
+ function isValidPublicChildResult(error, context) {
241
+ const result = publicChildResult(error);
242
+ if (!result) {
243
+ return false;
244
+ }
245
+ const allowed = new Set(['playbookId', 'status', 'error', 'output', 'childSessionId', 'state']);
246
+ if (Reflect.ownKeys(result).some((key) => typeof key !== 'string' || !allowed.has(key))) {
247
+ return false;
248
+ }
249
+ if (result.playbookId !== context.nextPlaybookId || (result.status !== 'aborted' && result.status !== 'error')) {
250
+ return false;
251
+ }
252
+ if ('output' in result) {
253
+ return false;
254
+ }
255
+ if ('childSessionId' in result && !isNonEmptyString(result.childSessionId)) {
256
+ return false;
257
+ }
258
+ if ('state' in result && !isPlaybookState(result.state)) {
259
+ return false;
260
+ }
261
+ if (result.status === 'error') {
262
+ return isNormalizedError(result.error);
263
+ }
264
+ return !('error' in result) || isNormalizedError(result.error);
265
+ }
266
+ function compactChildError(error) {
267
+ const result = publicChildResult(error);
268
+ if (result && isNormalizedError(result.error)) {
269
+ return { name: result.error.name, message: result.error.message };
270
+ }
271
+ return { name: 'AbortError', message: 'Child playbook aborted.' };
272
+ }
273
+ function childStatus(error) {
274
+ const result = publicChildResult(error);
275
+ return result?.status === 'error' ? 'error' : 'aborted';
276
+ }
277
+ function makePendingQuestion(stateId, sourceItem, question) {
278
+ return {
279
+ questionId: stateId,
280
+ resumeStateId: stateId,
281
+ sourceItem,
282
+ player: 'Captain',
283
+ question,
284
+ };
285
+ }
286
+ function bossIntentFromEvent(event) {
287
+ return event.type === 'BOSS_INTENT' || event.type === 'BOSS_INTERRUPT' ? event.bossIntent : '';
288
+ }
289
+ function answerMatchesPending(context, event) {
290
+ if (event.type !== 'BOSS_REPLY' || !context.pendingBossQuestion || !isNonEmptyString(event.answer)) {
291
+ return false;
292
+ }
293
+ return event.questionId === undefined || event.questionId === context.pendingBossQuestion.questionId;
294
+ }
295
+ function bossInterrupts(ids) {
296
+ return ids.map((id) => ({
297
+ guard: 'isRoutingInterrupt',
298
+ target: `#${id}`,
299
+ reenter: true,
300
+ actions: 'startRoutingFromBoss',
301
+ }));
302
+ }
303
+ function resumableStates() {
304
+ return [
305
+ {
306
+ guard: 'canResumeRouting',
307
+ target: '#routing',
308
+ actions: 'storeBossReply',
309
+ },
310
+ {
311
+ guard: 'canResumeReassessing',
312
+ target: '#reassessing',
313
+ actions: 'storeBossReply',
314
+ },
315
+ {
316
+ target: 'failed',
317
+ actions: 'rememberInvalidBossReply',
318
+ },
319
+ ];
320
+ }
321
+ export const captainMachine = setup({
322
+ types: {},
323
+ actors: {
324
+ captain: fromPromise(() => {
325
+ throw new Error('captain actor must be provided by the runner');
326
+ }),
327
+ playbook: fromPromise(() => {
328
+ throw new Error('playbook actor must be provided by the runner');
329
+ }),
330
+ },
331
+ guards: {
332
+ hasBossIntent: ({ event }) => event.type === 'BOSS_INTENT' && isNonEmptyString(event.bossIntent),
333
+ isRoutingInterrupt: ({ event }) => event.type === 'BOSS_INTERRUPT' && event.targetId === 'routing' && isNonEmptyString(event.bossIntent),
334
+ canResumeRouting: ({ context, event }) => answerMatchesPending(context, event) && context.pendingBossQuestion?.resumeStateId === 'routing',
335
+ canResumeReassessing: ({ context, event }) => answerMatchesPending(context, event) && context.pendingBossQuestion?.resumeStateId === 'reassessing',
336
+ isRoutingQuestion: ({ event }) => isRoutingQuestionOutput(outputFrom(event)),
337
+ isRoutingDelegation: ({ context, event }) => {
338
+ const output = outputFrom(event);
339
+ return isDelegationOutput(output) && canEnterDynamicCall(context, output.nextPlaybookId, output.nextPlaybookInput);
340
+ },
341
+ isReassessFinal: ({ event }) => isFinalOutput(outputFrom(event)),
342
+ isReassessQuestion: ({ event }) => isReassessQuestionOutput(outputFrom(event)),
343
+ isReassessContinuing: ({ context, event }) => {
344
+ const output = outputFrom(event);
345
+ return (isContinuingOutput(output) &&
346
+ output.remainingPlan.length < context.remainingPlan.length &&
347
+ canEnterDynamicCall(context, output.nextPlaybookId, output.nextPlaybookInput));
348
+ },
349
+ isPlaybookSuccessOutput: ({ event }) => {
350
+ const output = outputFrom(event);
351
+ return output === undefined || isJsonValue(output);
352
+ },
353
+ isAuthoredChildError: ({ context, event }) => isValidPublicChildResult(errorFrom(event), context),
354
+ },
355
+ actions: {
356
+ startRoutingFromBoss: assign(({ event }) => ({
357
+ bossIntent: bossIntentFromEvent(event),
358
+ remainingPlan: [],
359
+ completedCallResults: [],
360
+ nextPlaybookId: '',
361
+ nextPlaybookInput: '',
362
+ callHistory: [],
363
+ response: undefined,
364
+ pendingBossQuestion: undefined,
365
+ bossReply: undefined,
366
+ lastError: undefined,
367
+ })),
368
+ storeBossReply: assign(({ event }) => ({
369
+ bossReply: event.type === 'BOSS_REPLY' ? event.answer : undefined,
370
+ lastError: undefined,
371
+ })),
372
+ setRoutingQuestion: assign(({ event }) => {
373
+ const output = outputFrom(event);
374
+ return {
375
+ pendingBossQuestion: isRoutingQuestionOutput(output) ? makePendingQuestion('routing', 'CAPTAIN-1', output.question) : undefined,
376
+ bossReply: undefined,
377
+ };
378
+ }),
379
+ storeRoutingDelegation: assign(({ context, event }) => {
380
+ const output = outputFrom(event);
381
+ if (!isDelegationOutput(output)) {
382
+ return {};
383
+ }
384
+ return {
385
+ remainingPlan: output.remainingPlan,
386
+ nextPlaybookId: output.nextPlaybookId,
387
+ nextPlaybookInput: output.nextPlaybookInput,
388
+ callHistory: [...context.callHistory, callSignature(output.nextPlaybookId, output.nextPlaybookInput)],
389
+ pendingBossQuestion: undefined,
390
+ bossReply: undefined,
391
+ lastError: undefined,
392
+ };
393
+ }),
394
+ appendSuccessfulChildResult: assign(({ context, event }) => {
395
+ const output = outputFrom(event);
396
+ const result = isJsonValue(output)
397
+ ? { playbookId: context.nextPlaybookId, status: 'ok', output }
398
+ : { playbookId: context.nextPlaybookId, status: 'ok' };
399
+ return {
400
+ completedCallResults: [...context.completedCallResults, result],
401
+ lastError: undefined,
402
+ };
403
+ }),
404
+ appendRejectedChildResult: assign(({ context, event }) => {
405
+ const error = errorFrom(event);
406
+ const result = {
407
+ playbookId: context.nextPlaybookId,
408
+ status: childStatus(error),
409
+ error: compactChildError(error),
410
+ };
411
+ return {
412
+ completedCallResults: [...context.completedCallResults, result],
413
+ lastError: undefined,
414
+ };
415
+ }),
416
+ storeFinalResponse: assign(({ event }) => {
417
+ const output = outputFrom(event);
418
+ return isFinalOutput(output)
419
+ ? {
420
+ response: output.response,
421
+ pendingBossQuestion: undefined,
422
+ bossReply: undefined,
423
+ lastError: undefined,
424
+ }
425
+ : {};
426
+ }),
427
+ setReassessQuestion: assign(({ event }) => {
428
+ const output = outputFrom(event);
429
+ return {
430
+ pendingBossQuestion: isReassessQuestionOutput(output) ? makePendingQuestion('reassessing', 'CAPTAIN-3', output.question) : undefined,
431
+ bossReply: undefined,
432
+ };
433
+ }),
434
+ storeContinuingCall: assign(({ context, event }) => {
435
+ const output = outputFrom(event);
436
+ if (!isContinuingOutput(output)) {
437
+ return {};
438
+ }
439
+ return {
440
+ remainingPlan: output.remainingPlan,
441
+ nextPlaybookId: output.nextPlaybookId,
442
+ nextPlaybookInput: output.nextPlaybookInput,
443
+ callHistory: [...context.callHistory, callSignature(output.nextPlaybookId, output.nextPlaybookInput)],
444
+ pendingBossQuestion: undefined,
445
+ bossReply: undefined,
446
+ lastError: undefined,
447
+ };
448
+ }),
449
+ rememberInvalidActorOutput: assign({
450
+ lastError: () => ({ name: 'ActorOutputError', message: 'Actor output did not match any declared result contract.' }),
451
+ }),
452
+ rememberInvalidBossReply: assign({
453
+ lastError: () => ({ name: 'BossReplyError', message: 'BOSS_REPLY did not match a pending question or carried an empty answer.' }),
454
+ }),
455
+ rememberActorError: assign(({ event }) => ({
456
+ lastError: normalizeError(errorFrom(event)),
457
+ })),
458
+ },
459
+ }).createMachine({
460
+ id: 'captain',
461
+ initial: 'ready',
462
+ context: ({ input }) => ({
463
+ bossIntent: input.bossIntent ?? '',
464
+ enabledPlaybooks: input.enabledPlaybooks,
465
+ selfPlaybookId: input.selfPlaybookId,
466
+ remainingPlan: [],
467
+ completedCallResults: [],
468
+ nextPlaybookId: '',
469
+ nextPlaybookInput: '',
470
+ callHistory: [],
471
+ }),
472
+ output: ({ context }) => ({
473
+ response: context.response ?? '',
474
+ }),
475
+ on: {
476
+ BOSS_INTERRUPT: bossInterrupts(['routing']),
477
+ },
478
+ states: {
479
+ ready: {
480
+ id: 'ready',
481
+ description: 'Idle hub waiting for Boss to provide a new intent.',
482
+ tags: ['playbook.parked'],
483
+ meta: { playbook: { stateId: 'ready', description: 'Idle hub waiting for Boss to provide a new intent.' } },
484
+ on: {
485
+ BOSS_INTENT: {
486
+ guard: 'hasBossIntent',
487
+ target: 'routing',
488
+ actions: 'startRoutingFromBoss',
489
+ },
490
+ },
491
+ },
492
+ routing: {
493
+ id: 'routing',
494
+ description: 'Captain routes the Boss intent to a first enabled playbook or asks one routing question.',
495
+ tags: ['playbook.busy'],
496
+ meta: {
497
+ playbook: {
498
+ stateId: 'routing',
499
+ description: 'Captain routes the Boss intent to a first enabled playbook or asks one routing question.',
500
+ },
501
+ },
502
+ invoke: {
503
+ src: 'captain',
504
+ input: ({ context }) => ({
505
+ ...{
506
+ stateId: 'routing',
507
+ sourceItem: 'CAPTAIN-1',
508
+ prompt: ROUTING_PROMPT,
509
+ result: ROUTING_RESULTS,
510
+ bossIntent: context.bossIntent,
511
+ enabledPlaybooks: context.enabledPlaybooks,
512
+ },
513
+ ...(context.pendingBossQuestion ? { pendingBossQuestion: context.pendingBossQuestion } : {}),
514
+ ...(context.bossReply ? { bossReply: context.bossReply } : {}),
515
+ }),
516
+ onDone: [
517
+ { guard: 'isRoutingQuestion', target: 'awaitBossReply', actions: 'setRoutingQuestion' },
518
+ { guard: 'isRoutingDelegation', target: 'callPlaybook', actions: 'storeRoutingDelegation' },
519
+ { target: 'failed', actions: 'rememberInvalidActorOutput' },
520
+ ],
521
+ onError: { target: 'failed', actions: 'rememberActorError' },
522
+ },
523
+ },
524
+ callPlaybook: {
525
+ id: 'callPlaybook',
526
+ description: 'Captain calls the selected enabled playbook with the selected standalone request.',
527
+ tags: ['playbook.suspended'],
528
+ meta: {
529
+ playbook: {
530
+ stateId: 'callPlaybook',
531
+ description: 'Captain calls the selected enabled playbook with the selected standalone request.',
532
+ },
533
+ },
534
+ invoke: {
535
+ src: 'playbook',
536
+ input: ({ context }) => ({
537
+ stateId: 'callPlaybook',
538
+ sourceItem: 'CAPTAIN-2',
539
+ playbookId: context.nextPlaybookId,
540
+ text: context.nextPlaybookInput,
541
+ playbookIdContext: 'nextPlaybookId',
542
+ textContext: 'nextPlaybookInput',
543
+ }),
544
+ onDone: [
545
+ { guard: 'isPlaybookSuccessOutput', target: 'reassessing', actions: 'appendSuccessfulChildResult' },
546
+ { target: 'failed', actions: 'rememberInvalidActorOutput' },
547
+ ],
548
+ onError: [
549
+ { guard: 'isAuthoredChildError', target: 'reassessing', actions: 'appendRejectedChildResult' },
550
+ { target: 'failed', actions: 'rememberActorError' },
551
+ ],
552
+ },
553
+ },
554
+ reassessing: {
555
+ id: 'reassessing',
556
+ description: 'Captain reassesses the original intent, remaining plan, and completed call results.',
557
+ tags: ['playbook.busy'],
558
+ meta: {
559
+ playbook: {
560
+ stateId: 'reassessing',
561
+ description: 'Captain reassesses the original intent, remaining plan, and completed call results.',
562
+ },
563
+ },
564
+ invoke: {
565
+ src: 'captain',
566
+ input: ({ context }) => ({
567
+ ...{
568
+ stateId: 'reassessing',
569
+ sourceItem: 'CAPTAIN-3',
570
+ prompt: REASSESS_PROMPT,
571
+ result: REASSESS_RESULTS,
572
+ bossIntent: context.bossIntent,
573
+ enabledPlaybooks: context.enabledPlaybooks,
574
+ remainingPlan: context.remainingPlan,
575
+ completedCallResults: context.completedCallResults,
576
+ },
577
+ ...(context.pendingBossQuestion ? { pendingBossQuestion: context.pendingBossQuestion } : {}),
578
+ ...(context.bossReply ? { bossReply: context.bossReply } : {}),
579
+ }),
580
+ onDone: [
581
+ { guard: 'isReassessFinal', target: 'done', actions: 'storeFinalResponse' },
582
+ { guard: 'isReassessQuestion', target: 'awaitBossReply', actions: 'setReassessQuestion' },
583
+ { guard: 'isReassessContinuing', target: 'callPlaybook', actions: 'storeContinuingCall' },
584
+ { target: 'failed', actions: 'rememberInvalidActorOutput' },
585
+ ],
586
+ onError: { target: 'failed', actions: 'rememberActorError' },
587
+ },
588
+ },
589
+ awaitBossReply: {
590
+ id: 'awaitBossReply',
591
+ description: "Waiting for Boss to answer the acting agent's question.",
592
+ tags: ['playbook.parked'],
593
+ meta: {
594
+ playbook: {
595
+ stateId: 'awaitBossReply',
596
+ description: "Waiting for Boss to answer the acting agent's question.",
597
+ },
598
+ },
599
+ on: {
600
+ BOSS_REPLY: resumableStates(),
601
+ BOSS_INTENT: {
602
+ guard: 'hasBossIntent',
603
+ target: 'routing',
604
+ actions: 'startRoutingFromBoss',
605
+ },
606
+ },
607
+ },
608
+ failed: {
609
+ id: 'failed',
610
+ description: 'Recoverable failure retaining context for Boss recovery.',
611
+ tags: ['playbook.parked'],
612
+ meta: { playbook: { stateId: 'failed', description: 'Recoverable failure retaining context for Boss recovery.' } },
613
+ on: {
614
+ BOSS_INTENT: {
615
+ guard: 'hasBossIntent',
616
+ target: 'routing',
617
+ actions: 'startRoutingFromBoss',
618
+ },
619
+ },
620
+ },
621
+ done: {
622
+ id: 'done',
623
+ type: 'final',
624
+ description: 'Captain completed with a concise response for Boss.',
625
+ meta: { playbook: { stateId: 'done', description: 'Captain completed with a concise response for Boss.' } },
626
+ },
627
+ },
628
+ });