@sublang/playbook 17.3.0 → 17.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/docs/configuration.md +45 -4
- package/docs/embedding.md +94 -0
- package/package.json +34 -5
- package/reference/sdlc/captain.md +6 -4
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +5 -0
- package/reference/sdlc/captain.playbook/captain.fsm.js +4 -3
- package/reference/sdlc/captain.playbook/captain.fsm.ts +9 -4
- package/reference/sdlc/captain.playbook/captain.gears.md +6 -3
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.js +7 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +8 -4
- package/reference/sdlc/code.md +24 -4
- package/reference/sdlc/code.playbook/asset-types.d.ts +32 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +2 -0
- package/reference/sdlc/code.playbook/bin/portable-codec.js +12 -5
- package/reference/sdlc/code.playbook/bin/run.js +68 -4
- package/reference/sdlc/code.playbook/bin/session-host.js +16 -4
- package/reference/sdlc/code.playbook/bin/session-media.js +46 -0
- package/reference/sdlc/code.playbook/bin/session-store.js +145 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +12 -0
- package/reference/sdlc/code.playbook/code.fsm.js +40 -6
- package/reference/sdlc/code.playbook/code.fsm.ts +53 -6
- package/reference/sdlc/code.playbook/code.gears.md +32 -4
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +17 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +201 -19
- package/reference/sdlc/code.playbook/playbook-captain.ts +224 -24
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +16 -3
- package/reference/sdlc/code.playbook/session-assets.d.ts +53 -0
- package/reference/sdlc/code.playbook/session-assets.js +303 -0
- package/reference/sdlc/code.playbook/session-host.d.ts +9 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +26 -2
- package/reference/sdlc/code.playbook/worker-evidence.d.ts +8 -0
- package/reference/sdlc/code.playbook/worker-evidence.js +51 -0
- package/reference/sdlc/inspect.md +40 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.d.ts +168 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.js +354 -0
- package/reference/sdlc/inspect.playbook/inspect.fsm.ts +455 -0
- package/reference/sdlc/inspect.playbook/inspect.gears.md +36 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.d.ts +17 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.js +142 -0
- package/reference/sdlc/inspect.playbook/inspect.playbook.ts +184 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.d.ts +41 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.js +51 -0
- package/reference/sdlc/inspect.playbook/inspect.registry.ts +110 -0
- package/slc/gears2fsm.md +17 -3
- package/slc/text2gears.md +7 -0
- package/slc/workflow-contracts.json +43 -1
- package/src/xstate-playbook-runtime.js +5 -0
- package/src/xstate-playbook-runtime.ts +7 -0
|
@@ -61,8 +61,14 @@ import createDefaultCaptainRuntime, {
|
|
|
61
61
|
type SettlementEvidence,
|
|
62
62
|
} from '../captain.playbook/captain.playbook.js';
|
|
63
63
|
import type { PlaybookSummaryPolicy } from './code.registry.js';
|
|
64
|
+
import type { Attachment } from '@sublang/cligent';
|
|
65
|
+
import { validateAssetRef, validateAssetRefs } from './session-assets.js';
|
|
66
|
+
import type { SessionAssetRef } from './session-assets.js';
|
|
67
|
+
import { QUOTED_EVIDENCE_LIMIT, workerReportExcerpt, workerReportBlock } from './worker-evidence.js';
|
|
64
68
|
|
|
65
69
|
interface SessionAgent {
|
|
70
|
+
/** Mutable next-turn execution capability; never structural identity. */
|
|
71
|
+
readonly browser?: boolean;
|
|
66
72
|
readonly adapter: string;
|
|
67
73
|
readonly model: TuningSelection;
|
|
68
74
|
readonly effort: TuningSelection<Effort>;
|
|
@@ -121,13 +127,13 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
|
|
|
121
127
|
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey>>;
|
|
130
|
+
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
|
|
125
131
|
|
|
126
132
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
127
133
|
|
|
128
134
|
export interface ProgressChange {
|
|
129
135
|
snapshot?: PlaybookCaptainShellSnapshot | null;
|
|
130
|
-
step?: Omit<PlaybookStepRecord, 'kind'> & { kind: PlaybookStepRecord['kind'] | 'preparation' | 'completion' | 'answer'; runtimeSessionId: string; playbookId: string };
|
|
136
|
+
step?: Omit<PlaybookStepRecord, 'kind'> & { kind: PlaybookStepRecord['kind'] | 'preparation' | 'completion' | 'answer'; runtimeSessionId: string; playbookId: string; workerEvidence?: { boundaryId: string; playerId: string } };
|
|
131
137
|
}
|
|
132
138
|
export interface InterruptedReport {
|
|
133
139
|
text: string;
|
|
@@ -137,6 +143,9 @@ export interface InterruptedReport {
|
|
|
137
143
|
}
|
|
138
144
|
|
|
139
145
|
export interface PlaybookCaptainDeps {
|
|
146
|
+
onTurnAttachments?: (references: readonly SessionAssetRef[]) => void;
|
|
147
|
+
beginPreparationMedia?: (runtimeSessionId: string) => () => void;
|
|
148
|
+
resolveAttachments?: (references: readonly SessionAssetRef[], signal: AbortSignal) => Promise<readonly Attachment[]>;
|
|
140
149
|
/** Stop the host's active turn, including admitted tool calls, on preparation expiry or a required save failure. */
|
|
141
150
|
abortPreparation?: (reason?: string) => void;
|
|
142
151
|
recordProgress?: (change: ProgressChange) => Promise<void>;
|
|
@@ -259,6 +268,7 @@ export interface PlaybookCaptainFrameSnapshot {
|
|
|
259
268
|
readonly parentCallId?: string;
|
|
260
269
|
readonly request?: string;
|
|
261
270
|
readonly inputs?: readonly string[];
|
|
271
|
+
readonly attachments?: readonly SessionAssetRef[];
|
|
262
272
|
readonly options: JsonValue;
|
|
263
273
|
readonly roleBindings: Readonly<Record<string, string>>;
|
|
264
274
|
readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
|
|
@@ -282,6 +292,7 @@ interface PlaybookCaptainShellSnapshotFields {
|
|
|
282
292
|
readonly journal: number;
|
|
283
293
|
};
|
|
284
294
|
readonly journal: readonly PlaybookCaptainJournalRecord[];
|
|
295
|
+
readonly pendingAttachments?: readonly SessionAssetRef[];
|
|
285
296
|
readonly lastAction?:
|
|
286
297
|
| 'respond'
|
|
287
298
|
| 'start'
|
|
@@ -388,6 +399,9 @@ export interface PlaybookCaptainSettlement {
|
|
|
388
399
|
|
|
389
400
|
/** tmux and headless front ends share this one durable Captain shell API. */
|
|
390
401
|
export interface PlaybookCaptainShell extends Captain {
|
|
402
|
+
/** Bind already-owned evidence to the next visible Boss turn without changing its text. */
|
|
403
|
+
setTurnAttachments(references: readonly SessionAssetRef[]): void;
|
|
404
|
+
recordMediaEvidence(reference: SessionAssetRef, runtimeSessionId?: string): void;
|
|
391
405
|
installRetainedGenerations(
|
|
392
406
|
generations: Readonly<
|
|
393
407
|
Record<string, PlaybookCaptainRetainedGeneration>
|
|
@@ -432,12 +446,27 @@ export interface PlaybookCaptainShell extends Captain {
|
|
|
432
446
|
// normalized `captain.options.playbooks.<id>` role map.
|
|
433
447
|
interface Enablement {
|
|
434
448
|
entry: PlaybookCaptainRegistryEntry;
|
|
449
|
+
readonly configuredFrom: string;
|
|
435
450
|
artifactSchema: 3;
|
|
436
451
|
command: string;
|
|
437
452
|
options: JsonValue;
|
|
438
453
|
roleBindings: ReadonlyMap<string, EffectivePlayerBinding>;
|
|
439
454
|
}
|
|
440
455
|
|
|
456
|
+
function missingPackagedReview(
|
|
457
|
+
enablementById: ReadonlyMap<string, Enablement>,
|
|
458
|
+
playbookId: string,
|
|
459
|
+
): string | undefined {
|
|
460
|
+
const selected = enablementById.get(playbookId);
|
|
461
|
+
if (
|
|
462
|
+
selected === undefined ||
|
|
463
|
+
enablementById.has('review') ||
|
|
464
|
+
!['code', 'decide'].includes(playbookId) ||
|
|
465
|
+
selected.configuredFrom !== `@sublang/playbook/${playbookId}/registry`
|
|
466
|
+
) return undefined;
|
|
467
|
+
return `/${selected.command} requires enabled playbook "review"; enable REVIEW before starting this packaged workflow`;
|
|
468
|
+
}
|
|
469
|
+
|
|
441
470
|
function createRuntimeForEnablement(
|
|
442
471
|
enablement: Enablement,
|
|
443
472
|
hostCapabilitiesById: ReadonlyMap<
|
|
@@ -488,6 +517,8 @@ type PlayerTransaction =
|
|
|
488
517
|
});
|
|
489
518
|
|
|
490
519
|
interface EngagementFrame {
|
|
520
|
+
effectOwnerSessionId?: string;
|
|
521
|
+
attachments?: readonly SessionAssetRef[];
|
|
491
522
|
request?: string;
|
|
492
523
|
inputs?: string[];
|
|
493
524
|
pauseCancellation?: () => () => void;
|
|
@@ -677,6 +708,12 @@ interface ActiveTurnSummary {
|
|
|
677
708
|
|
|
678
709
|
/** The shell state of one Boss turn (DR-029). */
|
|
679
710
|
interface ActiveTurn {
|
|
711
|
+
readonly workerReports: Map<string, string>;
|
|
712
|
+
readonly workerStepStarts: Map<string, ReadonlySet<string>>;
|
|
713
|
+
readonly workerCalls: Map<string, { frame: EngagementFrame; callId: string; roleId: string; playerId: string; stateId: string; finalText: string }>;
|
|
714
|
+
evidence?: readonly SessionAssetRef[];
|
|
715
|
+
attachments: readonly SessionAssetRef[];
|
|
716
|
+
selectedAttachments?: readonly SessionAssetRef[];
|
|
680
717
|
appliedActionCount: number;
|
|
681
718
|
startedEngagement?: boolean;
|
|
682
719
|
countedSummary?: ActiveTurnSummary;
|
|
@@ -847,7 +884,6 @@ function quoteEvidence(text: string): string {
|
|
|
847
884
|
// into a fact and still knows it is foreign — never at the digest renderer,
|
|
848
885
|
// which sees an opaque payload and cannot tell quoted output from the Boss
|
|
849
886
|
// text, captain speech, or settlement facts the shell authored itself.
|
|
850
|
-
const QUOTED_EVIDENCE_LIMIT = 400;
|
|
851
887
|
|
|
852
888
|
// The same guard for strings the shell interpolates into a single-line fact:
|
|
853
889
|
// control characters collapse to spaces so a quoted message can never open a
|
|
@@ -3011,6 +3047,7 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
3011
3047
|
'issuedSessionIds',
|
|
3012
3048
|
'sequences',
|
|
3013
3049
|
'journal',
|
|
3050
|
+
'pendingAttachments',
|
|
3014
3051
|
'lastAction',
|
|
3015
3052
|
'lastSettlementStatus',
|
|
3016
3053
|
'mode',
|
|
@@ -3339,6 +3376,7 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
3339
3376
|
issuedSessionIds: issued,
|
|
3340
3377
|
sequences: { turn: turnSequence, journal: journalSequence },
|
|
3341
3378
|
journal: normalizedJournal,
|
|
3379
|
+
...(snapshot.pendingAttachments === undefined ? {} : { pendingAttachments: validateAssetRefs(snapshot.pendingAttachments) }),
|
|
3342
3380
|
...(lastAction === undefined ? {} : { lastAction }),
|
|
3343
3381
|
...(lastSettlementStatus === undefined
|
|
3344
3382
|
? {}
|
|
@@ -3390,6 +3428,7 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
3390
3428
|
'parentCallId',
|
|
3391
3429
|
'request',
|
|
3392
3430
|
'inputs',
|
|
3431
|
+
'attachments',
|
|
3393
3432
|
'options',
|
|
3394
3433
|
'roleBindings',
|
|
3395
3434
|
'runtime',
|
|
@@ -3447,6 +3486,7 @@ export function assertPlaybookCaptainShellSnapshot(
|
|
|
3447
3486
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
3448
3487
|
...(frame.request === undefined ? {} : { request: frame.request as string }),
|
|
3449
3488
|
...(frame.inputs === undefined ? {} : { inputs: frame.inputs as string[] }),
|
|
3489
|
+
...(frame.attachments === undefined ? {} : { attachments: validateAssetRefs(frame.attachments) }),
|
|
3450
3490
|
options,
|
|
3451
3491
|
roleBindings,
|
|
3452
3492
|
runtime,
|
|
@@ -3763,6 +3803,7 @@ function snapshotSessionAgent(
|
|
|
3763
3803
|
'fastMode',
|
|
3764
3804
|
'subagentModel',
|
|
3765
3805
|
'subagentEffort',
|
|
3806
|
+
'browser',
|
|
3766
3807
|
'instruction',
|
|
3767
3808
|
'permissions',
|
|
3768
3809
|
],
|
|
@@ -3776,7 +3817,8 @@ function snapshotSessionAgent(
|
|
|
3776
3817
|
key !== 'effort' &&
|
|
3777
3818
|
key !== 'fastMode' &&
|
|
3778
3819
|
key !== 'subagentModel' &&
|
|
3779
|
-
key !== 'subagentEffort'
|
|
3820
|
+
key !== 'subagentEffort' &&
|
|
3821
|
+
key !== 'browser',
|
|
3780
3822
|
),
|
|
3781
3823
|
) as JsonValue,
|
|
3782
3824
|
path,
|
|
@@ -3789,6 +3831,7 @@ function snapshotSessionAgent(
|
|
|
3789
3831
|
...(fixed.permissions === undefined
|
|
3790
3832
|
? {}
|
|
3791
3833
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
3834
|
+
...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
|
|
3792
3835
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
3793
3836
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
3794
3837
|
...(agent.fastMode === undefined
|
|
@@ -3813,7 +3856,7 @@ function snapshotSessionAgent(
|
|
|
3813
3856
|
};
|
|
3814
3857
|
}
|
|
3815
3858
|
|
|
3816
|
-
function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey> {
|
|
3859
|
+
function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey | 'browser'> {
|
|
3817
3860
|
return {
|
|
3818
3861
|
adapter: agent.adapter,
|
|
3819
3862
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
@@ -3833,6 +3876,7 @@ function callSettings(
|
|
|
3833
3876
|
return {
|
|
3834
3877
|
model: tuning.model,
|
|
3835
3878
|
effort: tuning.effort,
|
|
3879
|
+
browser: agent.browser ?? false,
|
|
3836
3880
|
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
3837
3881
|
...(tuning.subagentModel === undefined
|
|
3838
3882
|
? {}
|
|
@@ -3845,6 +3889,10 @@ function callSettings(
|
|
|
3845
3889
|
};
|
|
3846
3890
|
}
|
|
3847
3891
|
|
|
3892
|
+
function controlCallSettings(agent: SessionAgent): AgentCallSettings {
|
|
3893
|
+
return { ...callSettings(agent), browser: false, mcpServers: {} };
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3848
3896
|
function promptIdentity(binding: EffectivePlayerBinding): string {
|
|
3849
3897
|
return binding.model.kind === 'value'
|
|
3850
3898
|
? binding.model.value
|
|
@@ -4148,6 +4196,7 @@ async function buildEnablements(
|
|
|
4148
4196
|
hostCapabilitiesById.set(entry.id, hostCapability);
|
|
4149
4197
|
enablementById.set(entry.id, {
|
|
4150
4198
|
entry,
|
|
4199
|
+
configuredFrom: from,
|
|
4151
4200
|
artifactSchema,
|
|
4152
4201
|
command,
|
|
4153
4202
|
options: validatedOptions,
|
|
@@ -4421,6 +4470,17 @@ export function createPlaybookCaptainShell(
|
|
|
4421
4470
|
> = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
4422
4471
|
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
4423
4472
|
const continuity = deps.continuity;
|
|
4473
|
+
const resolveAttachments = deps.resolveAttachments;
|
|
4474
|
+
const beginPreparationMedia = deps.beginPreparationMedia;
|
|
4475
|
+
const onTurnAttachments = deps.onTurnAttachments;
|
|
4476
|
+
let nextTurnAttachments: readonly SessionAssetRef[] = [];
|
|
4477
|
+
let pendingAttachments: readonly SessionAssetRef[] = [];
|
|
4478
|
+
const mergeAttachments = (...groups: readonly (readonly SessionAssetRef[])[]) => [...new Map(groups.flat().map((reference) => [reference.assetId, reference])).values()];
|
|
4479
|
+
const attachmentPaths = async (references: readonly SessionAssetRef[] | undefined, signal: AbortSignal) => {
|
|
4480
|
+
if (!references?.length) return undefined;
|
|
4481
|
+
if (!resolveAttachments) throw new Error('The session host cannot materialize required attachments');
|
|
4482
|
+
return resolveAttachments(references, signal);
|
|
4483
|
+
};
|
|
4424
4484
|
const abortPreparation = deps.abortPreparation;
|
|
4425
4485
|
let pendingHostCapabilities = deps.hostCapabilities;
|
|
4426
4486
|
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
@@ -4879,6 +4939,7 @@ export function createPlaybookCaptainShell(
|
|
|
4879
4939
|
'parentCallId',
|
|
4880
4940
|
'request',
|
|
4881
4941
|
'inputs',
|
|
4942
|
+
'attachments',
|
|
4882
4943
|
'options',
|
|
4883
4944
|
'roleBindings',
|
|
4884
4945
|
'runtime',
|
|
@@ -5028,6 +5089,7 @@ export function createPlaybookCaptainShell(
|
|
|
5028
5089
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
5029
5090
|
...(frame.request === undefined ? {} : { request: frame.request as string }),
|
|
5030
5091
|
...(frame.inputs === undefined ? {} : { inputs: frame.inputs as string[] }),
|
|
5092
|
+
...(frame.attachments === undefined ? {} : { attachments: validateAssetRefs(frame.attachments) }),
|
|
5031
5093
|
options,
|
|
5032
5094
|
roleBindings,
|
|
5033
5095
|
runtime,
|
|
@@ -5527,15 +5589,21 @@ export function createPlaybookCaptainShell(
|
|
|
5527
5589
|
prompt: string,
|
|
5528
5590
|
options: Parameters<CaptainContext['callCaptain']>[1],
|
|
5529
5591
|
signal: AbortSignal,
|
|
5592
|
+
media?: { readonly attachments?: readonly SessionAssetRef[]; readonly preparation?: true },
|
|
5530
5593
|
): ReturnType<CaptainContext['callCaptain']> => {
|
|
5531
5594
|
const queued = captainQueue.add(async () => {
|
|
5532
5595
|
signal.throwIfAborted();
|
|
5533
|
-
const
|
|
5534
|
-
frame,
|
|
5535
|
-
context.callCaptain(prompt, options),
|
|
5536
|
-
);
|
|
5596
|
+
const attachments = media?.attachments?.length ? await attachmentPaths(media.attachments, signal) : undefined;
|
|
5537
5597
|
signal.throwIfAborted();
|
|
5538
|
-
|
|
5598
|
+
const endMedia = media?.preparation ? beginPreparationMedia?.(frame.sessionId) : undefined;
|
|
5599
|
+
try {
|
|
5600
|
+
const result = await trackHostCall(
|
|
5601
|
+
frame,
|
|
5602
|
+
context.callCaptain(prompt, { ...options, ...(attachments === undefined ? {} : { attachments }) }),
|
|
5603
|
+
);
|
|
5604
|
+
signal.throwIfAborted();
|
|
5605
|
+
return result;
|
|
5606
|
+
} finally { endMedia?.(); }
|
|
5539
5607
|
});
|
|
5540
5608
|
return trackHostCall(frame, queued);
|
|
5541
5609
|
};
|
|
@@ -5621,12 +5689,20 @@ export function createPlaybookCaptainShell(
|
|
|
5621
5689
|
!Number.isSafeInteger(trace.turnId) ||
|
|
5622
5690
|
(trace.turnId as number) <= 0 ||
|
|
5623
5691
|
!Number.isSafeInteger(trace.sequence) ||
|
|
5624
|
-
(trace.sequence as number) <= 0
|
|
5625
|
-
summary === undefined ||
|
|
5626
|
-
!summaryIncludes(frame)
|
|
5692
|
+
(trace.sequence as number) <= 0
|
|
5627
5693
|
) {
|
|
5628
5694
|
return;
|
|
5629
5695
|
}
|
|
5696
|
+
if (trace.type === 'player.call.finished') {
|
|
5697
|
+
const call = payloadRecord(trace.payload);
|
|
5698
|
+
if (typeof trace.callId === 'string' && call?.status === 'ok' &&
|
|
5699
|
+
typeof call.roleId === 'string' && typeof call.playerId === 'string' &&
|
|
5700
|
+
frame.playerBindings.get(call.roleId)?.playerId === call.playerId &&
|
|
5701
|
+
typeof call.stateId === 'string' && typeof call.finalText === 'string') {
|
|
5702
|
+
turn.workerCalls.set(`${frame.sessionId}:${trace.callId}`, { frame, callId: trace.callId, roleId: call.roleId, playerId: call.playerId, stateId: call.stateId, finalText: call.finalText });
|
|
5703
|
+
}
|
|
5704
|
+
return;
|
|
5705
|
+
}
|
|
5630
5706
|
if (trace.type !== 'outcome.accepted') return;
|
|
5631
5707
|
const receipt = exactOwnDataRecord(trace.payload, [
|
|
5632
5708
|
'source',
|
|
@@ -5644,6 +5720,21 @@ export function createPlaybookCaptainShell(
|
|
|
5644
5720
|
) {
|
|
5645
5721
|
return;
|
|
5646
5722
|
}
|
|
5723
|
+
const ledger = currentEffectLedger();
|
|
5724
|
+
const previous = turn.progressBase?.effectLedger;
|
|
5725
|
+
for (const call of turn.workerCalls.values()) {
|
|
5726
|
+
if (call.frame !== frame || call.stateId !== receipt.source) continue;
|
|
5727
|
+
const boundary = [...ledger.boundaries].reverse().find((entry) =>
|
|
5728
|
+
entry.playbookId === frame.entry.id &&
|
|
5729
|
+
entry.runtimeSessionId === (frame.effectOwnerSessionId ?? frame.sessionId) &&
|
|
5730
|
+
entry.callId === call.callId && entry.roleId === call.roleId &&
|
|
5731
|
+
entry.sourceStateId === call.stateId && entry.finalText === call.finalText &&
|
|
5732
|
+
!isDeepStrictEqual(entry, previous?.boundaries[entry.sequence - 1]));
|
|
5733
|
+
if (!boundary || turn.workerReports.has(boundary.boundaryId)) continue;
|
|
5734
|
+
const report = workerReportExcerpt(boundary, call.playerId, receipt.acceptedOutcome);
|
|
5735
|
+
if (report !== undefined) turn.workerReports.set(boundary.boundaryId, report);
|
|
5736
|
+
}
|
|
5737
|
+
if (summary === undefined || !summaryIncludes(frame)) return;
|
|
5647
5738
|
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
5648
5739
|
if (summary.acceptedOutcomeTraceKeys.has(traceKey)) return;
|
|
5649
5740
|
summary.acceptedOutcomeTraceKeys.add(traceKey);
|
|
@@ -5671,8 +5762,28 @@ export function createPlaybookCaptainShell(
|
|
|
5671
5762
|
const createPorts = (frame: EngagementFrame): PlaybookPorts => ({
|
|
5672
5763
|
...(recordProgress === undefined ? {} : { recordStep: async (step: PlaybookStepRecord, position?: PlaybookRuntimeSnapshot) => {
|
|
5673
5764
|
journalledFrames.add(frame);
|
|
5765
|
+
const turn = activeTurn;
|
|
5766
|
+
if (step.kind === 'player' && step.result === undefined) {
|
|
5767
|
+
turn?.workerStepStarts.set(step.id, new Set(turn.workerCalls.keys()));
|
|
5768
|
+
}
|
|
5769
|
+
let workerEvidence: { boundaryId: string; playerId: string } | undefined;
|
|
5770
|
+
const guard = payloadRecord(step.result)?.guard;
|
|
5771
|
+
const before = turn?.workerStepStarts.get(step.id);
|
|
5772
|
+
if (step.kind === 'player' && typeof guard === 'string' && before !== undefined && turn) {
|
|
5773
|
+
const candidates = [...turn.workerCalls.entries()].flatMap(([key, call]) => {
|
|
5774
|
+
if (before.has(key) || call.frame !== frame || call.stateId !== step.stateId) return [];
|
|
5775
|
+
return currentEffectLedger().boundaries.filter((boundary) =>
|
|
5776
|
+
boundary.playbookId === frame.entry.id && boundary.sourceStateId === step.stateId &&
|
|
5777
|
+
boundary.runtimeSessionId === (frame.effectOwnerSessionId ?? frame.sessionId) &&
|
|
5778
|
+
boundary.callId === call.callId && boundary.roleId === call.roleId && boundary.finalText === call.finalText &&
|
|
5779
|
+
!isDeepStrictEqual(boundary, turn.progressBase?.effectLedger.boundaries[boundary.sequence - 1]) &&
|
|
5780
|
+
workerReportExcerpt(boundary, call.playerId, guard) !== undefined
|
|
5781
|
+
).map((boundary) => ({ boundaryId: boundary.boundaryId, playerId: call.playerId }));
|
|
5782
|
+
});
|
|
5783
|
+
if (candidates.length === 1) workerEvidence = candidates[0];
|
|
5784
|
+
}
|
|
5674
5785
|
await saveProgress(step.result === undefined ? { frame, position } : undefined,
|
|
5675
|
-
{ ...step, runtimeSessionId: frame.sessionId, playbookId: frame.entry.id });
|
|
5786
|
+
{ ...step, runtimeSessionId: frame.sessionId, playbookId: frame.entry.id, ...(workerEvidence === undefined ? {} : { workerEvidence }) });
|
|
5676
5787
|
} }),
|
|
5677
5788
|
callPlayer: async (roleId, prompt, signal, options) => {
|
|
5678
5789
|
admitHostBoundary();
|
|
@@ -5705,6 +5816,7 @@ export function createPlaybookCaptainShell(
|
|
|
5705
5816
|
);
|
|
5706
5817
|
}
|
|
5707
5818
|
const settings = callSettings(binding.agent, binding);
|
|
5819
|
+
const attachments = await attachmentPaths(frame.attachments, signal);
|
|
5708
5820
|
const calling: PlayerTransaction = {
|
|
5709
5821
|
phase: 'calling',
|
|
5710
5822
|
frame,
|
|
@@ -5731,7 +5843,7 @@ export function createPlaybookCaptainShell(
|
|
|
5731
5843
|
const raw = await trackHostCall(
|
|
5732
5844
|
frame,
|
|
5733
5845
|
classifySettingsCall(() =>
|
|
5734
|
-
context.callPlayer(binding.playerId, resume === false ? options.freshPrompt ?? prompt : prompt, { resume, settings }),
|
|
5846
|
+
context.callPlayer(binding.playerId, resume === false ? options.freshPrompt ?? prompt : prompt, { resume, settings, ...(attachments === undefined ? {} : { attachments }) }),
|
|
5735
5847
|
),
|
|
5736
5848
|
);
|
|
5737
5849
|
hostResolved = true;
|
|
@@ -5862,9 +5974,11 @@ export function createPlaybookCaptainShell(
|
|
|
5862
5974
|
{
|
|
5863
5975
|
visibility: options.visibility,
|
|
5864
5976
|
resume: options.resume,
|
|
5977
|
+
settings: options.allowedTools?.length === 0 ? controlCallSettings(captainAgent!) : callSettings(captainAgent!),
|
|
5865
5978
|
...forwardedToolOptions(options.allowedTools, captainAdapter),
|
|
5866
5979
|
},
|
|
5867
5980
|
signal,
|
|
5981
|
+
{ attachments: options.allowedTools?.length === 0 ? undefined : frame.attachments },
|
|
5868
5982
|
);
|
|
5869
5983
|
const output = {
|
|
5870
5984
|
status: result.status,
|
|
@@ -5886,6 +6000,7 @@ export function createPlaybookCaptainShell(
|
|
|
5886
6000
|
activeContext,
|
|
5887
6001
|
hiddenJudgeEnvelope(prompt),
|
|
5888
6002
|
{
|
|
6003
|
+
settings: controlCallSettings(captainAgent!),
|
|
5889
6004
|
visibility: 'hidden',
|
|
5890
6005
|
resume: false,
|
|
5891
6006
|
...controlCallToolOptions(captainAdapter),
|
|
@@ -6043,12 +6158,16 @@ export function createPlaybookCaptainShell(
|
|
|
6043
6158
|
enablement,
|
|
6044
6159
|
hostCapabilitiesById,
|
|
6045
6160
|
);
|
|
6161
|
+
// A nested child inherits only its parent's selection, never the turn's
|
|
6162
|
+
// unselected references; only a root reads the turn's selection.
|
|
6163
|
+
const inherited = parent ? parent.frame.attachments : (activeTurn?.selectedAttachments ?? activeTurn?.attachments);
|
|
6046
6164
|
return {
|
|
6047
6165
|
entry,
|
|
6048
6166
|
enablement,
|
|
6049
6167
|
runtime,
|
|
6050
6168
|
sessionId,
|
|
6051
6169
|
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
6170
|
+
...(inherited?.length ? { attachments: [...inherited] } : {}),
|
|
6052
6171
|
depth: parent ? parent.frame.depth + 1 : 0,
|
|
6053
6172
|
playerBindings,
|
|
6054
6173
|
...(parent ? { parent } : {}),
|
|
@@ -6072,12 +6191,14 @@ export function createPlaybookCaptainShell(
|
|
|
6072
6191
|
enablement,
|
|
6073
6192
|
runtime,
|
|
6074
6193
|
sessionId: snapshot.sessionId,
|
|
6194
|
+
effectOwnerSessionId: snapshot.runtime.retainedEffectSourceSessionId ?? snapshot.sessionId,
|
|
6075
6195
|
rootSessionId: snapshot.rootSessionId,
|
|
6076
6196
|
depth: snapshot.depth,
|
|
6077
6197
|
playerBindings,
|
|
6078
6198
|
...(parent ? { parent } : {}),
|
|
6079
6199
|
...(snapshot.request === undefined ? {} : { request: snapshot.request }),
|
|
6080
6200
|
...(snapshot.inputs === undefined ? {} : { inputs: [...snapshot.inputs] }),
|
|
6201
|
+
...(snapshot.attachments === undefined ? {} : { attachments: [...snapshot.attachments] }),
|
|
6081
6202
|
state: snapshot.runtime.state,
|
|
6082
6203
|
inFlightHostCalls: new Set(),
|
|
6083
6204
|
};
|
|
@@ -6827,6 +6948,23 @@ export function createPlaybookCaptainShell(
|
|
|
6827
6948
|
return;
|
|
6828
6949
|
}
|
|
6829
6950
|
assertRetainableResult(frame, result);
|
|
6951
|
+
// A deferred checkpoint refusal changes the runtime's control overlay
|
|
6952
|
+
// without publishing an FSM transition. Refresh its derived question
|
|
6953
|
+
// mirror from an exact safe snapshot before reporting or saving the stop.
|
|
6954
|
+
if (leafFrame() === frame && !frame.disposing &&
|
|
6955
|
+
retainedEffectReconciliation === undefined) {
|
|
6956
|
+
try {
|
|
6957
|
+
const exported = frame.runtime.exportSnapshot?.();
|
|
6958
|
+
if (exported !== undefined) {
|
|
6959
|
+
const snapshot = assertPlaybookRuntimeSnapshot(exported, frame.entry.id);
|
|
6960
|
+
if (isDeepStrictEqual(snapshot.state, result.state) &&
|
|
6961
|
+
isDeepStrictEqual(snapshot.effectLedger, currentEffectLedger())) {
|
|
6962
|
+
pendingBossQuestions = snapshot.pendingBossQuestions.length === 0 && pendingBossQuestions === undefined
|
|
6963
|
+
? undefined : mirroredBossQuestions(snapshot.pendingBossQuestions);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
} catch { /* An inconsistent runtime must still fail exact capture. */ }
|
|
6967
|
+
}
|
|
6830
6968
|
if (result.outcome === 'aborted' && runFailureFacts) {
|
|
6831
6969
|
runFailureFacts.push(
|
|
6832
6970
|
`${frameLabel(frame)} was aborted before its outcome could be confirmed; it was not repeated automatically.`,
|
|
@@ -6908,6 +7046,9 @@ export function createPlaybookCaptainShell(
|
|
|
6908
7046
|
);
|
|
6909
7047
|
}
|
|
6910
7048
|
|
|
7049
|
+
const reviewRefusal = missingPackagedReview(enablementById, entry.id);
|
|
7050
|
+
if (reviewRefusal !== undefined) throw new Error(reviewRefusal);
|
|
7051
|
+
|
|
6911
7052
|
pendingChildParents.add(parent);
|
|
6912
7053
|
let child: EngagementFrame;
|
|
6913
7054
|
try {
|
|
@@ -7899,11 +8040,11 @@ export function createPlaybookCaptainShell(
|
|
|
7899
8040
|
context.signal.throwIfAborted();
|
|
7900
8041
|
attempt.providerBoundaryEntered = true;
|
|
7901
8042
|
const result = await classifySettingsCall(() =>
|
|
7902
|
-
context.callCaptain(prompt, {
|
|
8043
|
+
context.callCaptain(pendingAttachments.length || activeTurn?.attachments.length || activeTurn?.evidence?.length || leafFrame()?.attachments?.length ? `${prompt}\n\nAttachment context (metadata only; do not claim to have viewed these bytes): ${JSON.stringify({ current: activeTurn?.attachments ?? [], pending: pendingAttachments, engagement: leafFrame()?.attachments ?? [], evidence: activeTurn?.evidence ?? [] })}` : prompt, {
|
|
7903
8044
|
visibility: 'hidden',
|
|
7904
8045
|
resume,
|
|
7905
8046
|
...controlCallToolOptions(captainAdapter),
|
|
7906
|
-
settings:
|
|
8047
|
+
settings: controlCallSettings(captainAgent!),
|
|
7907
8048
|
}),
|
|
7908
8049
|
);
|
|
7909
8050
|
context.signal.throwIfAborted();
|
|
@@ -8122,6 +8263,9 @@ export function createPlaybookCaptainShell(
|
|
|
8122
8263
|
...(kind === 'decision'
|
|
8123
8264
|
? [labeledBlock('Catalog digest', catalogDigest())]
|
|
8124
8265
|
: []),
|
|
8266
|
+
...(kind === 'closingReply' && turn?.workerReports.size
|
|
8267
|
+
? [workerReportBlock([...turn.workerReports.values()])!]
|
|
8268
|
+
: []),
|
|
8125
8269
|
...(kind === 'closingReply' && turn?.report
|
|
8126
8270
|
? [
|
|
8127
8271
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
@@ -8506,6 +8650,7 @@ export function createPlaybookCaptainShell(
|
|
|
8506
8650
|
enablement,
|
|
8507
8651
|
runtime: offer.runtimes[index]!,
|
|
8508
8652
|
sessionId: targetSessionIds[index]!,
|
|
8653
|
+
effectOwnerSessionId: sourceFrame.runtime.retainedEffectSourceSessionId ?? sourceFrame.sessionId,
|
|
8509
8654
|
rootSessionId: targetRootSessionId,
|
|
8510
8655
|
depth: index,
|
|
8511
8656
|
playerBindings: makePlayerBindings(enablement),
|
|
@@ -8514,6 +8659,7 @@ export function createPlaybookCaptainShell(
|
|
|
8514
8659
|
: {}),
|
|
8515
8660
|
...(sourceFrame.request === undefined ? {} : { request: sourceFrame.request }),
|
|
8516
8661
|
...(sourceFrame.inputs === undefined ? {} : { inputs: [...sourceFrame.inputs] }),
|
|
8662
|
+
...(sourceFrame.attachments === undefined ? {} : { attachments: [...sourceFrame.attachments] }),
|
|
8517
8663
|
state: sourceFrame.runtime.state,
|
|
8518
8664
|
inFlightHostCalls: new Set(),
|
|
8519
8665
|
});
|
|
@@ -8896,7 +9042,13 @@ export function createPlaybookCaptainShell(
|
|
|
8896
9042
|
// its decision-call prose crosses the presentation boundary. Acting
|
|
8897
9043
|
// selections freeze after their work and before reporting begins.
|
|
8898
9044
|
if (selection.action === 'respond') freezeControllerEvidence();
|
|
8899
|
-
|
|
9045
|
+
signal.throwIfAborted();
|
|
9046
|
+
const reviewRefusal = selection.action === 'start' || selection.action === 'switch'
|
|
9047
|
+
? missingPackagedReview(enablementById, selection.playbookId)
|
|
9048
|
+
: undefined;
|
|
9049
|
+
let settlement = reviewRefusal === undefined
|
|
9050
|
+
? await executeSelection(selection, signal, automaticRecovery)
|
|
9051
|
+
: await rejectSelection(selection, reviewRefusal);
|
|
8900
9052
|
// One controller action can include bounded prerequisite recovery.
|
|
8901
9053
|
// Neither the model nor a retry manufactures an unadvertised transition.
|
|
8902
9054
|
if (turn && abortPreparation && settlement.status !== 'rejected' && !['respond', 'dismiss', 'recover'].includes(selection.action)) {
|
|
@@ -9177,6 +9329,11 @@ export function createPlaybookCaptainShell(
|
|
|
9177
9329
|
);
|
|
9178
9330
|
}
|
|
9179
9331
|
}
|
|
9332
|
+
const selectedIds = selection.attachmentIds;
|
|
9333
|
+
const available = new Map(mergeAttachments(pendingAttachments, turn.attachments, leafFrame()?.attachments ?? []).map((reference) => [reference.assetId, reference]));
|
|
9334
|
+
if (selectedIds?.some((id) => !available.has(id as SessionAssetRef['assetId'])) || (selectedIds && new Set(selectedIds).size !== selectedIds.length)) return rejectSelection(selection, 'the request selects unavailable attachment evidence');
|
|
9335
|
+
turn.selectedAttachments = selectedIds === undefined ? turn.attachments : selectedIds.map((id) => available.get(id as SessionAssetRef['assetId'])!);
|
|
9336
|
+
pendingAttachments = [];
|
|
9180
9337
|
turn.settled = true;
|
|
9181
9338
|
journalAction({
|
|
9182
9339
|
action: selection.action,
|
|
@@ -9324,7 +9481,7 @@ export function createPlaybookCaptainShell(
|
|
|
9324
9481
|
`Later Boss input (context only): ${JSON.stringify(laterInputs)}`,
|
|
9325
9482
|
`Existing instructions: ${JSON.stringify(instructions)}`,
|
|
9326
9483
|
`Pending questions (quoted evidence): ${JSON.stringify(before.pendingQuestions.map(({ question }) => question))}`,
|
|
9327
|
-
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings:
|
|
9484
|
+
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings: controlCallSettings(captainAgent!) }, signal);
|
|
9328
9485
|
const answer = result.status === 'ok' ? controlReplyJson(result.finalText) : undefined;
|
|
9329
9486
|
const index = answer && typeof answer === 'object' && !Array.isArray(answer) && Object.keys(answer).join(',') === 'instructionIndex'
|
|
9330
9487
|
? (answer as { instructionIndex: unknown }).instructionIndex : undefined;
|
|
@@ -9383,8 +9540,9 @@ export function createPlaybookCaptainShell(
|
|
|
9383
9540
|
claim = await capability.acquire({ signal: preparationSignal }) as NonNullable<typeof claim>;
|
|
9384
9541
|
await claim.assertOwner();
|
|
9385
9542
|
const result = await runEffect(() => callCaptainQueued(
|
|
9386
|
-
|
|
9387
|
-
|
|
9543
|
+
leaf, context, preparationPrompt,
|
|
9544
|
+
{ visibility: 'hidden', resume: false, settings: callSettings(captainAgent!) }, preparationSignal,
|
|
9545
|
+
{ attachments: leaf.attachments, preparation: true },
|
|
9388
9546
|
));
|
|
9389
9547
|
preparationSignal.throwIfAborted();
|
|
9390
9548
|
await claim.assertOwner();
|
|
@@ -9429,6 +9587,14 @@ export function createPlaybookCaptainShell(
|
|
|
9429
9587
|
}
|
|
9430
9588
|
|
|
9431
9589
|
if (selection.action === 'deliver') {
|
|
9590
|
+
const available = new Map(mergeAttachments(pendingAttachments, turn.attachments, leaf.attachments ?? []).map((reference) => [reference.assetId, reference]));
|
|
9591
|
+
if (selection.attachmentIds?.some((id) => !available.has(id as SessionAssetRef['assetId'])) || (selection.attachmentIds && new Set(selection.attachmentIds).size !== selection.attachmentIds.length)) return rejectSelection(selection, 'the request selects unavailable attachment evidence');
|
|
9592
|
+
const selected = selection.attachmentIds === undefined ? turn.attachments : selection.attachmentIds.map((id) => available.get(id as SessionAssetRef['assetId'])!);
|
|
9593
|
+
const nextAttachments = mergeAttachments(leaf.attachments ?? [], selected);
|
|
9594
|
+
if (nextAttachments.length) leaf.attachments = nextAttachments;
|
|
9595
|
+
// Delivered references now belong to the leaf; the rest stay pending.
|
|
9596
|
+
const delivered = new Set(selected.map((reference) => reference.assetId));
|
|
9597
|
+
pendingAttachments = pendingAttachments.filter((reference) => !delivered.has(reference.assetId));
|
|
9432
9598
|
// CAPTAIN-8: delivery carries text only, and the shell is authoritative
|
|
9433
9599
|
// for that text — any text carried on the selection is ignored.
|
|
9434
9600
|
turn.settled = true;
|
|
@@ -10077,6 +10243,7 @@ export function createPlaybookCaptainShell(
|
|
|
10077
10243
|
: {}),
|
|
10078
10244
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
10079
10245
|
...(frame.inputs === undefined ? {} : { inputs: [...frame.inputs] }),
|
|
10246
|
+
...(frame.attachments === undefined ? {} : { attachments: [...frame.attachments] }),
|
|
10080
10247
|
options: frame.enablement.options,
|
|
10081
10248
|
roleBindings: Object.fromEntries(
|
|
10082
10249
|
[...frame.playerBindings].map(([role, binding]) => [
|
|
@@ -10345,6 +10512,7 @@ export function createPlaybookCaptainShell(
|
|
|
10345
10512
|
issuedSessionIds: [...issuedSessionIds],
|
|
10346
10513
|
sequences: { turn: turnSequence, journal: journalSeq },
|
|
10347
10514
|
journal,
|
|
10515
|
+
...(pendingAttachments.length ? { pendingAttachments } : {}),
|
|
10348
10516
|
...(lastAction === undefined ? {} : { lastAction }),
|
|
10349
10517
|
...(lastSettlementStatus === undefined
|
|
10350
10518
|
? {}
|
|
@@ -10683,6 +10851,7 @@ export function createPlaybookCaptainShell(
|
|
|
10683
10851
|
}
|
|
10684
10852
|
for (const id of snapshot.issuedSessionIds) issuedSessionIds.add(id);
|
|
10685
10853
|
journal.push(...snapshot.journal);
|
|
10854
|
+
pendingAttachments = [...(snapshot.pendingAttachments ?? [])];
|
|
10686
10855
|
journalSeq = snapshot.sequences.journal;
|
|
10687
10856
|
turnSequence = snapshot.sequences.turn;
|
|
10688
10857
|
conversation = snapshot.captain.conversation;
|
|
@@ -10841,10 +11010,29 @@ export function createPlaybookCaptainShell(
|
|
|
10841
11010
|
submitShellAction: selectShellAction,
|
|
10842
11011
|
selectInterruptedReport(input, report) {
|
|
10843
11012
|
if (activeTurn) throw new Error('A Captain turn is already active');
|
|
10844
|
-
if (!input.trim() || !report.text.trim()) throw new Error('Interruption report requires its recorded input and explanation');
|
|
11013
|
+
if ((!input.trim() && nextTurnAttachments.length === 0) || !report.text.trim()) throw new Error('Interruption report requires its recorded input and explanation');
|
|
10845
11014
|
pendingHostSelection = { kind: 'report', text: input, report };
|
|
10846
11015
|
},
|
|
10847
11016
|
|
|
11017
|
+
recordMediaEvidence(reference, runtimeSessionId) {
|
|
11018
|
+
const asset = validateAssetRef(reference);
|
|
11019
|
+
if (!activeTurn) throw new Error('Media evidence arrived outside an active turn');
|
|
11020
|
+
activeTurn.evidence = mergeAttachments(activeTurn.evidence ?? [], [asset]);
|
|
11021
|
+
const producingIndex = frames.findIndex((candidate) => candidate.sessionId === runtimeSessionId);
|
|
11022
|
+
// Observed output belongs to this engagement's reasoning context: a
|
|
11023
|
+
// resumed parent may inspect its child's figure. Boss input additions
|
|
11024
|
+
// still target only the addressed leaf, and a new root inherits none.
|
|
11025
|
+
for (const frame of frames.slice(0, producingIndex + 1)) {
|
|
11026
|
+
frame.attachments = mergeAttachments(frame.attachments ?? [], [asset]);
|
|
11027
|
+
}
|
|
11028
|
+
},
|
|
11029
|
+
|
|
11030
|
+
setTurnAttachments(references) {
|
|
11031
|
+
if (activeTurn || activeTurnHostCalls) throw new Error('A Captain turn is already active');
|
|
11032
|
+
nextTurnAttachments = validateAssetRefs(references);
|
|
11033
|
+
onTurnAttachments?.(nextTurnAttachments);
|
|
11034
|
+
},
|
|
11035
|
+
|
|
10848
11036
|
async handleBossTurn(
|
|
10849
11037
|
turn: BossTurn,
|
|
10850
11038
|
context: CaptainContext,
|
|
@@ -10870,7 +11058,9 @@ export function createPlaybookCaptainShell(
|
|
|
10870
11058
|
// telemetry (CAPTAIN-7).
|
|
10871
11059
|
retentionSettlementReady = false;
|
|
10872
11060
|
abandonmentSettlementUnsafe = false;
|
|
10873
|
-
|
|
11061
|
+
const attachments = nextTurnAttachments;
|
|
11062
|
+
nextTurnAttachments = [];
|
|
11063
|
+
if (turn.prompt.trim().length === 0 && attachments.length === 0) return;
|
|
10874
11064
|
const progressBase = exportShellSnapshot();
|
|
10875
11065
|
settledTurnUnresolvedEffects = undefined;
|
|
10876
11066
|
retainedGenerationInstallationClosed = true;
|
|
@@ -10900,6 +11090,10 @@ export function createPlaybookCaptainShell(
|
|
|
10900
11090
|
const parsed =
|
|
10901
11091
|
decided === undefined ? resolveCommandTurn(turn.prompt) : undefined;
|
|
10902
11092
|
activeTurn = {
|
|
11093
|
+
attachments,
|
|
11094
|
+
workerReports: new Map(),
|
|
11095
|
+
workerStepStarts: new Map(),
|
|
11096
|
+
workerCalls: new Map(),
|
|
10903
11097
|
appliedActionCount: 0,
|
|
10904
11098
|
stoppedFrames: new Set<string>(),
|
|
10905
11099
|
...(progressBase === undefined ? {} : { progressBase }),
|
|
@@ -10932,7 +11126,8 @@ export function createPlaybookCaptainShell(
|
|
|
10932
11126
|
}
|
|
10933
11127
|
}
|
|
10934
11128
|
decisionCall = undefined;
|
|
10935
|
-
appendJournal('boss', turn.prompt);
|
|
11129
|
+
appendJournal('boss', attachments.length ? { text: turn.prompt, attachments } as unknown as JsonValue : turn.prompt);
|
|
11130
|
+
pendingAttachments = mergeAttachments(pendingAttachments, attachments);
|
|
10936
11131
|
try {
|
|
10937
11132
|
await drainRetiredRetainedRuntimes();
|
|
10938
11133
|
await prepareRetainedGenerationOffers();
|
|
@@ -10941,6 +11136,11 @@ export function createPlaybookCaptainShell(
|
|
|
10941
11136
|
await settleSelection({ action: 'respond', text: activeTurn.interruptedReport.text }, context.signal);
|
|
10942
11137
|
return;
|
|
10943
11138
|
}
|
|
11139
|
+
if (!turn.prompt.trim() && attachments.length) {
|
|
11140
|
+
markConversationCatchUp();
|
|
11141
|
+
await settleSelection({ action: 'respond', text: 'What would you like me to do with the attached material?' }, context.signal);
|
|
11142
|
+
return;
|
|
11143
|
+
}
|
|
10944
11144
|
const result = await captainRuntime.handleBossInput({
|
|
10945
11145
|
text: turn.prompt,
|
|
10946
11146
|
signal: context.signal,
|