@sublang/playbook 17.3.0 → 17.4.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 +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.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 +139 -13
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +17 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +159 -18
- package/reference/sdlc/code.playbook/playbook-captain.ts +176 -23
- 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 +302 -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/workflow-contracts.json +43 -1
|
@@ -7,6 +7,8 @@ import { isAgentCallSettingsError, } from '@sublang/cligent/tmux-play';
|
|
|
7
7
|
import { assertPlaybookFailureCause } from '@sublang/playbook/runtime';
|
|
8
8
|
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, emptyPlaybookEffectLedger, hiddenControlEnvelope, isPlaybookEffectLedgerMonotonicExtension, normalizeError, parseJudgeJson, registerPlaybookAbortCleanup, snapshotJsonValue, validatePlayerResult, } from '../../../src/xstate-runtime.js';
|
|
9
9
|
import createDefaultCaptainRuntime from '../captain.playbook/captain.playbook.js';
|
|
10
|
+
import { validateAssetRef, validateAssetRefs } from './session-assets.js';
|
|
11
|
+
import { QUOTED_EVIDENCE_LIMIT, workerReportExcerpt, workerReportBlock } from './worker-evidence.js';
|
|
10
12
|
function retainedEffectLedgerCanRebase(checkpoint, current) {
|
|
11
13
|
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
12
14
|
return false;
|
|
@@ -124,7 +126,6 @@ function quoteEvidence(text) {
|
|
|
124
126
|
// into a fact and still knows it is foreign — never at the digest renderer,
|
|
125
127
|
// which sees an opaque payload and cannot tell quoted output from the Boss
|
|
126
128
|
// text, captain speech, or settlement facts the shell authored itself.
|
|
127
|
-
const QUOTED_EVIDENCE_LIMIT = 400;
|
|
128
129
|
// The same guard for strings the shell interpolates into a single-line fact:
|
|
129
130
|
// control characters collapse to spaces so a quoted message can never open a
|
|
130
131
|
// new line — and therefore never a new labeled block — inside a report.
|
|
@@ -1685,6 +1686,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1685
1686
|
'issuedSessionIds',
|
|
1686
1687
|
'sequences',
|
|
1687
1688
|
'journal',
|
|
1689
|
+
'pendingAttachments',
|
|
1688
1690
|
'lastAction',
|
|
1689
1691
|
'lastSettlementStatus',
|
|
1690
1692
|
'mode',
|
|
@@ -1858,6 +1860,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1858
1860
|
issuedSessionIds: issued,
|
|
1859
1861
|
sequences: { turn: turnSequence, journal: journalSequence },
|
|
1860
1862
|
journal: normalizedJournal,
|
|
1863
|
+
...(snapshot.pendingAttachments === undefined ? {} : { pendingAttachments: validateAssetRefs(snapshot.pendingAttachments) }),
|
|
1861
1864
|
...(lastAction === undefined ? {} : { lastAction }),
|
|
1862
1865
|
...(lastSettlementStatus === undefined
|
|
1863
1866
|
? {}
|
|
@@ -1892,6 +1895,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1892
1895
|
'parentCallId',
|
|
1893
1896
|
'request',
|
|
1894
1897
|
'inputs',
|
|
1898
|
+
'attachments',
|
|
1895
1899
|
'options',
|
|
1896
1900
|
'roleBindings',
|
|
1897
1901
|
'runtime',
|
|
@@ -1922,6 +1926,7 @@ export function assertPlaybookCaptainShellSnapshot(value) {
|
|
|
1922
1926
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
1923
1927
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
1924
1928
|
...(frame.inputs === undefined ? {} : { inputs: frame.inputs }),
|
|
1929
|
+
...(frame.attachments === undefined ? {} : { attachments: validateAssetRefs(frame.attachments) }),
|
|
1925
1930
|
options,
|
|
1926
1931
|
roleBindings,
|
|
1927
1932
|
runtime,
|
|
@@ -2114,6 +2119,7 @@ function snapshotSessionAgent(value, path) {
|
|
|
2114
2119
|
'fastMode',
|
|
2115
2120
|
'subagentModel',
|
|
2116
2121
|
'subagentEffort',
|
|
2122
|
+
'browser',
|
|
2117
2123
|
'instruction',
|
|
2118
2124
|
'permissions',
|
|
2119
2125
|
], path);
|
|
@@ -2121,7 +2127,8 @@ function snapshotSessionAgent(value, path) {
|
|
|
2121
2127
|
key !== 'effort' &&
|
|
2122
2128
|
key !== 'fastMode' &&
|
|
2123
2129
|
key !== 'subagentModel' &&
|
|
2124
|
-
key !== 'subagentEffort'
|
|
2130
|
+
key !== 'subagentEffort' &&
|
|
2131
|
+
key !== 'browser')), path);
|
|
2125
2132
|
return {
|
|
2126
2133
|
adapter: fixed.adapter,
|
|
2127
2134
|
...(fixed.instruction === undefined
|
|
@@ -2130,6 +2137,7 @@ function snapshotSessionAgent(value, path) {
|
|
|
2130
2137
|
...(fixed.permissions === undefined
|
|
2131
2138
|
? {}
|
|
2132
2139
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
2140
|
+
...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
|
|
2133
2141
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
2134
2142
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
2135
2143
|
...(agent.fastMode === undefined
|
|
@@ -2163,6 +2171,7 @@ function callSettings(agent, tuning = agent) {
|
|
|
2163
2171
|
return {
|
|
2164
2172
|
model: tuning.model,
|
|
2165
2173
|
effort: tuning.effort,
|
|
2174
|
+
browser: agent.browser ?? false,
|
|
2166
2175
|
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
2167
2176
|
...(tuning.subagentModel === undefined
|
|
2168
2177
|
? {}
|
|
@@ -2174,6 +2183,9 @@ function callSettings(agent, tuning = agent) {
|
|
|
2174
2183
|
...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
|
|
2175
2184
|
};
|
|
2176
2185
|
}
|
|
2186
|
+
function controlCallSettings(agent) {
|
|
2187
|
+
return { ...callSettings(agent), browser: false, mcpServers: {} };
|
|
2188
|
+
}
|
|
2177
2189
|
function promptIdentity(binding) {
|
|
2178
2190
|
return binding.model.kind === 'value'
|
|
2179
2191
|
? binding.model.value
|
|
@@ -2545,6 +2557,19 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2545
2557
|
const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
2546
2558
|
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
2547
2559
|
const continuity = deps.continuity;
|
|
2560
|
+
const resolveAttachments = deps.resolveAttachments;
|
|
2561
|
+
const beginPreparationMedia = deps.beginPreparationMedia;
|
|
2562
|
+
const onTurnAttachments = deps.onTurnAttachments;
|
|
2563
|
+
let nextTurnAttachments = [];
|
|
2564
|
+
let pendingAttachments = [];
|
|
2565
|
+
const mergeAttachments = (...groups) => [...new Map(groups.flat().map((reference) => [reference.assetId, reference])).values()];
|
|
2566
|
+
const attachmentPaths = async (references, signal) => {
|
|
2567
|
+
if (!references?.length)
|
|
2568
|
+
return undefined;
|
|
2569
|
+
if (!resolveAttachments)
|
|
2570
|
+
throw new Error('The session host cannot materialize required attachments');
|
|
2571
|
+
return resolveAttachments(references, signal);
|
|
2572
|
+
};
|
|
2548
2573
|
const abortPreparation = deps.abortPreparation;
|
|
2549
2574
|
let pendingHostCapabilities = deps.hostCapabilities;
|
|
2550
2575
|
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
@@ -2866,6 +2891,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2866
2891
|
'parentCallId',
|
|
2867
2892
|
'request',
|
|
2868
2893
|
'inputs',
|
|
2894
|
+
'attachments',
|
|
2869
2895
|
'options',
|
|
2870
2896
|
'roleBindings',
|
|
2871
2897
|
'runtime',
|
|
@@ -2951,6 +2977,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2951
2977
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
2952
2978
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
2953
2979
|
...(frame.inputs === undefined ? {} : { inputs: frame.inputs }),
|
|
2980
|
+
...(frame.attachments === undefined ? {} : { attachments: validateAssetRefs(frame.attachments) }),
|
|
2954
2981
|
options,
|
|
2955
2982
|
roleBindings,
|
|
2956
2983
|
runtime,
|
|
@@ -3324,12 +3351,20 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3324
3351
|
turnCalls?.add(tracked);
|
|
3325
3352
|
return tracked;
|
|
3326
3353
|
};
|
|
3327
|
-
const callCaptainQueued = (frame, context, prompt, options, signal) => {
|
|
3354
|
+
const callCaptainQueued = (frame, context, prompt, options, signal, media) => {
|
|
3328
3355
|
const queued = captainQueue.add(async () => {
|
|
3329
3356
|
signal.throwIfAborted();
|
|
3330
|
-
const
|
|
3357
|
+
const attachments = media?.attachments?.length ? await attachmentPaths(media.attachments, signal) : undefined;
|
|
3331
3358
|
signal.throwIfAborted();
|
|
3332
|
-
|
|
3359
|
+
const endMedia = media?.preparation ? beginPreparationMedia?.(frame.sessionId) : undefined;
|
|
3360
|
+
try {
|
|
3361
|
+
const result = await trackHostCall(frame, context.callCaptain(prompt, { ...options, ...(attachments === undefined ? {} : { attachments }) }));
|
|
3362
|
+
signal.throwIfAborted();
|
|
3363
|
+
return result;
|
|
3364
|
+
}
|
|
3365
|
+
finally {
|
|
3366
|
+
endMedia?.();
|
|
3367
|
+
}
|
|
3333
3368
|
});
|
|
3334
3369
|
return trackHostCall(frame, queued);
|
|
3335
3370
|
};
|
|
@@ -3398,9 +3433,17 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3398
3433
|
!Number.isSafeInteger(trace.turnId) ||
|
|
3399
3434
|
trace.turnId <= 0 ||
|
|
3400
3435
|
!Number.isSafeInteger(trace.sequence) ||
|
|
3401
|
-
trace.sequence <= 0
|
|
3402
|
-
|
|
3403
|
-
|
|
3436
|
+
trace.sequence <= 0) {
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
if (trace.type === 'player.call.finished') {
|
|
3440
|
+
const call = payloadRecord(trace.payload);
|
|
3441
|
+
if (typeof trace.callId === 'string' && call?.status === 'ok' &&
|
|
3442
|
+
typeof call.roleId === 'string' && typeof call.playerId === 'string' &&
|
|
3443
|
+
frame.playerBindings.get(call.roleId)?.playerId === call.playerId &&
|
|
3444
|
+
typeof call.stateId === 'string' && typeof call.finalText === 'string') {
|
|
3445
|
+
turn.workerCalls.set(`${frame.sessionId}:${trace.callId}`, { frame, callId: trace.callId, roleId: call.roleId, playerId: call.playerId, stateId: call.stateId, finalText: call.finalText });
|
|
3446
|
+
}
|
|
3404
3447
|
return;
|
|
3405
3448
|
}
|
|
3406
3449
|
if (trace.type !== 'outcome.accepted')
|
|
@@ -3419,6 +3462,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3419
3462
|
receipt.acceptedOutcome.trim().length === 0) {
|
|
3420
3463
|
return;
|
|
3421
3464
|
}
|
|
3465
|
+
const ledger = currentEffectLedger();
|
|
3466
|
+
const previous = turn.progressBase?.effectLedger;
|
|
3467
|
+
for (const call of turn.workerCalls.values()) {
|
|
3468
|
+
if (call.frame !== frame || call.stateId !== receipt.source)
|
|
3469
|
+
continue;
|
|
3470
|
+
const boundary = [...ledger.boundaries].reverse().find((entry) => entry.playbookId === frame.entry.id &&
|
|
3471
|
+
entry.runtimeSessionId === (frame.effectOwnerSessionId ?? frame.sessionId) &&
|
|
3472
|
+
entry.callId === call.callId && entry.roleId === call.roleId &&
|
|
3473
|
+
entry.sourceStateId === call.stateId && entry.finalText === call.finalText &&
|
|
3474
|
+
!isDeepStrictEqual(entry, previous?.boundaries[entry.sequence - 1]));
|
|
3475
|
+
if (!boundary || turn.workerReports.has(boundary.boundaryId))
|
|
3476
|
+
continue;
|
|
3477
|
+
const report = workerReportExcerpt(boundary, call.playerId, receipt.acceptedOutcome);
|
|
3478
|
+
if (report !== undefined)
|
|
3479
|
+
turn.workerReports.set(boundary.boundaryId, report);
|
|
3480
|
+
}
|
|
3481
|
+
if (summary === undefined || !summaryIncludes(frame))
|
|
3482
|
+
return;
|
|
3422
3483
|
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
3423
3484
|
if (summary.acceptedOutcomeTraceKeys.has(traceKey))
|
|
3424
3485
|
return;
|
|
@@ -3438,7 +3499,27 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3438
3499
|
const createPorts = (frame) => ({
|
|
3439
3500
|
...(recordProgress === undefined ? {} : { recordStep: async (step, position) => {
|
|
3440
3501
|
journalledFrames.add(frame);
|
|
3441
|
-
|
|
3502
|
+
const turn = activeTurn;
|
|
3503
|
+
if (step.kind === 'player' && step.result === undefined) {
|
|
3504
|
+
turn?.workerStepStarts.set(step.id, new Set(turn.workerCalls.keys()));
|
|
3505
|
+
}
|
|
3506
|
+
let workerEvidence;
|
|
3507
|
+
const guard = payloadRecord(step.result)?.guard;
|
|
3508
|
+
const before = turn?.workerStepStarts.get(step.id);
|
|
3509
|
+
if (step.kind === 'player' && typeof guard === 'string' && before !== undefined && turn) {
|
|
3510
|
+
const candidates = [...turn.workerCalls.entries()].flatMap(([key, call]) => {
|
|
3511
|
+
if (before.has(key) || call.frame !== frame || call.stateId !== step.stateId)
|
|
3512
|
+
return [];
|
|
3513
|
+
return currentEffectLedger().boundaries.filter((boundary) => boundary.playbookId === frame.entry.id && boundary.sourceStateId === step.stateId &&
|
|
3514
|
+
boundary.runtimeSessionId === (frame.effectOwnerSessionId ?? frame.sessionId) &&
|
|
3515
|
+
boundary.callId === call.callId && boundary.roleId === call.roleId && boundary.finalText === call.finalText &&
|
|
3516
|
+
!isDeepStrictEqual(boundary, turn.progressBase?.effectLedger.boundaries[boundary.sequence - 1]) &&
|
|
3517
|
+
workerReportExcerpt(boundary, call.playerId, guard) !== undefined).map((boundary) => ({ boundaryId: boundary.boundaryId, playerId: call.playerId }));
|
|
3518
|
+
});
|
|
3519
|
+
if (candidates.length === 1)
|
|
3520
|
+
workerEvidence = candidates[0];
|
|
3521
|
+
}
|
|
3522
|
+
await saveProgress(step.result === undefined ? { frame, position } : undefined, { ...step, runtimeSessionId: frame.sessionId, playbookId: frame.entry.id, ...(workerEvidence === undefined ? {} : { workerEvidence }) });
|
|
3442
3523
|
} }),
|
|
3443
3524
|
callPlayer: async (roleId, prompt, signal, options) => {
|
|
3444
3525
|
admitHostBoundary();
|
|
@@ -3465,6 +3546,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3465
3546
|
throw new Error(`session player ${JSON.stringify(binding.playerId)} already has a call in flight`);
|
|
3466
3547
|
}
|
|
3467
3548
|
const settings = callSettings(binding.agent, binding);
|
|
3549
|
+
const attachments = await attachmentPaths(frame.attachments, signal);
|
|
3468
3550
|
const calling = {
|
|
3469
3551
|
phase: 'calling',
|
|
3470
3552
|
frame,
|
|
@@ -3489,7 +3571,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3489
3571
|
const call = async (resume) => {
|
|
3490
3572
|
await continuity?.beforeCall(binding.playerId);
|
|
3491
3573
|
signal.throwIfAborted();
|
|
3492
|
-
const raw = await trackHostCall(frame, classifySettingsCall(() => context.callPlayer(binding.playerId, resume === false ? options.freshPrompt ?? prompt : prompt, { resume, settings })));
|
|
3574
|
+
const raw = await trackHostCall(frame, classifySettingsCall(() => context.callPlayer(binding.playerId, resume === false ? options.freshPrompt ?? prompt : prompt, { resume, settings, ...(attachments === undefined ? {} : { attachments }) })));
|
|
3493
3575
|
hostResolved = true;
|
|
3494
3576
|
return raw;
|
|
3495
3577
|
};
|
|
@@ -3611,8 +3693,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3611
3693
|
const result = await callCaptainQueued(frame, activeContext, prompt, {
|
|
3612
3694
|
visibility: options.visibility,
|
|
3613
3695
|
resume: options.resume,
|
|
3696
|
+
settings: options.allowedTools?.length === 0 ? controlCallSettings(captainAgent) : callSettings(captainAgent),
|
|
3614
3697
|
...forwardedToolOptions(options.allowedTools, captainAdapter),
|
|
3615
|
-
}, signal);
|
|
3698
|
+
}, signal, { attachments: options.allowedTools?.length === 0 ? undefined : frame.attachments });
|
|
3616
3699
|
const output = {
|
|
3617
3700
|
status: result.status,
|
|
3618
3701
|
...(result.finalText !== undefined
|
|
@@ -3630,6 +3713,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3630
3713
|
throw new Error('callJudge invoked outside a Boss turn');
|
|
3631
3714
|
}
|
|
3632
3715
|
const result = await callCaptainQueued(frame, activeContext, hiddenJudgeEnvelope(prompt), {
|
|
3716
|
+
settings: controlCallSettings(captainAgent),
|
|
3633
3717
|
visibility: 'hidden',
|
|
3634
3718
|
resume: false,
|
|
3635
3719
|
...controlCallToolOptions(captainAdapter),
|
|
@@ -3764,6 +3848,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3764
3848
|
runtime,
|
|
3765
3849
|
sessionId,
|
|
3766
3850
|
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
3851
|
+
...((parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn?.attachments)?.length ? { attachments: [...(parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn.attachments)] } : {}),
|
|
3767
3852
|
depth: parent ? parent.frame.depth + 1 : 0,
|
|
3768
3853
|
playerBindings,
|
|
3769
3854
|
...(parent ? { parent } : {}),
|
|
@@ -3779,12 +3864,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3779
3864
|
enablement,
|
|
3780
3865
|
runtime,
|
|
3781
3866
|
sessionId: snapshot.sessionId,
|
|
3867
|
+
effectOwnerSessionId: snapshot.runtime.retainedEffectSourceSessionId ?? snapshot.sessionId,
|
|
3782
3868
|
rootSessionId: snapshot.rootSessionId,
|
|
3783
3869
|
depth: snapshot.depth,
|
|
3784
3870
|
playerBindings,
|
|
3785
3871
|
...(parent ? { parent } : {}),
|
|
3786
3872
|
...(snapshot.request === undefined ? {} : { request: snapshot.request }),
|
|
3787
3873
|
...(snapshot.inputs === undefined ? {} : { inputs: [...snapshot.inputs] }),
|
|
3874
|
+
...(snapshot.attachments === undefined ? {} : { attachments: [...snapshot.attachments] }),
|
|
3788
3875
|
state: snapshot.runtime.state,
|
|
3789
3876
|
inFlightHostCalls: new Set(),
|
|
3790
3877
|
};
|
|
@@ -4065,6 +4152,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4065
4152
|
finally {
|
|
4066
4153
|
if (leafFrame() === frame) {
|
|
4067
4154
|
frames.pop();
|
|
4155
|
+
if (!frame.parent)
|
|
4156
|
+
pendingAttachments = [];
|
|
4068
4157
|
if (frame.parent) {
|
|
4069
4158
|
pendingChildParents.delete(frame.parent.frame);
|
|
4070
4159
|
}
|
|
@@ -5328,11 +5417,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5328
5417
|
await continuity?.beforeCall('captain');
|
|
5329
5418
|
context.signal.throwIfAborted();
|
|
5330
5419
|
attempt.providerBoundaryEntered = true;
|
|
5331
|
-
const result = await classifySettingsCall(() => context.callCaptain(prompt, {
|
|
5420
|
+
const result = await classifySettingsCall(() => 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, {
|
|
5332
5421
|
visibility: 'hidden',
|
|
5333
5422
|
resume,
|
|
5334
5423
|
...controlCallToolOptions(captainAdapter),
|
|
5335
|
-
settings:
|
|
5424
|
+
settings: controlCallSettings(captainAgent),
|
|
5336
5425
|
}));
|
|
5337
5426
|
context.signal.throwIfAborted();
|
|
5338
5427
|
return result;
|
|
@@ -5497,6 +5586,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5497
5586
|
...(kind === 'decision'
|
|
5498
5587
|
? [labeledBlock('Catalog digest', catalogDigest())]
|
|
5499
5588
|
: []),
|
|
5589
|
+
...(kind === 'closingReply' && turn?.workerReports.size
|
|
5590
|
+
? [workerReportBlock([...turn.workerReports.values()])]
|
|
5591
|
+
: []),
|
|
5500
5592
|
...(kind === 'closingReply' && turn?.report
|
|
5501
5593
|
? [
|
|
5502
5594
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
@@ -5821,6 +5913,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5821
5913
|
enablement,
|
|
5822
5914
|
runtime: offer.runtimes[index],
|
|
5823
5915
|
sessionId: targetSessionIds[index],
|
|
5916
|
+
effectOwnerSessionId: sourceFrame.runtime.retainedEffectSourceSessionId ?? sourceFrame.sessionId,
|
|
5824
5917
|
rootSessionId: targetRootSessionId,
|
|
5825
5918
|
depth: index,
|
|
5826
5919
|
playerBindings: makePlayerBindings(enablement),
|
|
@@ -5829,6 +5922,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5829
5922
|
: {}),
|
|
5830
5923
|
...(sourceFrame.request === undefined ? {} : { request: sourceFrame.request }),
|
|
5831
5924
|
...(sourceFrame.inputs === undefined ? {} : { inputs: [...sourceFrame.inputs] }),
|
|
5925
|
+
...(sourceFrame.attachments === undefined ? {} : { attachments: [...sourceFrame.attachments] }),
|
|
5832
5926
|
state: sourceFrame.runtime.state,
|
|
5833
5927
|
inFlightHostCalls: new Set(),
|
|
5834
5928
|
});
|
|
@@ -6383,6 +6477,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6383
6477
|
return rejectSelection(selection, `/${enablementById.get(entry.id).command} is already on the active path`);
|
|
6384
6478
|
}
|
|
6385
6479
|
}
|
|
6480
|
+
const selectedIds = selection.attachmentIds;
|
|
6481
|
+
const available = new Map(mergeAttachments(pendingAttachments, turn.attachments, leafFrame()?.attachments ?? []).map((reference) => [reference.assetId, reference]));
|
|
6482
|
+
if (selectedIds?.some((id) => !available.has(id)) || (selectedIds && new Set(selectedIds).size !== selectedIds.length))
|
|
6483
|
+
return rejectSelection(selection, 'the request selects unavailable attachment evidence');
|
|
6484
|
+
turn.selectedAttachments = selectedIds === undefined ? turn.attachments : selectedIds.map((id) => available.get(id));
|
|
6485
|
+
pendingAttachments = [];
|
|
6386
6486
|
turn.settled = true;
|
|
6387
6487
|
journalAction({
|
|
6388
6488
|
action: selection.action,
|
|
@@ -6515,7 +6615,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6515
6615
|
`Later Boss input (context only): ${JSON.stringify(laterInputs)}`,
|
|
6516
6616
|
`Existing instructions: ${JSON.stringify(instructions)}`,
|
|
6517
6617
|
`Pending questions (quoted evidence): ${JSON.stringify(before.pendingQuestions.map(({ question }) => question))}`,
|
|
6518
|
-
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings:
|
|
6618
|
+
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings: controlCallSettings(captainAgent) }, signal);
|
|
6519
6619
|
const answer = result.status === 'ok' ? controlReplyJson(result.finalText) : undefined;
|
|
6520
6620
|
const index = answer && typeof answer === 'object' && !Array.isArray(answer) && Object.keys(answer).join(',') === 'instructionIndex'
|
|
6521
6621
|
? answer.instructionIndex : undefined;
|
|
@@ -6573,7 +6673,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6573
6673
|
try {
|
|
6574
6674
|
claim = await capability.acquire({ signal: preparationSignal });
|
|
6575
6675
|
await claim.assertOwner();
|
|
6576
|
-
const result = await runEffect(() => callCaptainQueued(leaf, context, preparationPrompt, { visibility: 'hidden', resume: false, settings: callSettings(captainAgent) }, preparationSignal));
|
|
6676
|
+
const result = await runEffect(() => callCaptainQueued(leaf, context, preparationPrompt, { visibility: 'hidden', resume: false, settings: callSettings(captainAgent) }, preparationSignal, { attachments: leaf.attachments, preparation: true }));
|
|
6577
6677
|
preparationSignal.throwIfAborted();
|
|
6578
6678
|
await claim.assertOwner();
|
|
6579
6679
|
if (result.status !== 'ok') {
|
|
@@ -6616,6 +6716,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6616
6716
|
: { action: 'runtime', actionId: offer.continuation.actionId };
|
|
6617
6717
|
}
|
|
6618
6718
|
if (selection.action === 'deliver') {
|
|
6719
|
+
const available = new Map(mergeAttachments(pendingAttachments, turn.attachments, leaf.attachments ?? []).map((reference) => [reference.assetId, reference]));
|
|
6720
|
+
if (selection.attachmentIds?.some((id) => !available.has(id)) || (selection.attachmentIds && new Set(selection.attachmentIds).size !== selection.attachmentIds.length))
|
|
6721
|
+
return rejectSelection(selection, 'the request selects unavailable attachment evidence');
|
|
6722
|
+
const selected = selection.attachmentIds === undefined ? turn.attachments : selection.attachmentIds.map((id) => available.get(id));
|
|
6723
|
+
const nextAttachments = mergeAttachments(leaf.attachments ?? [], selected);
|
|
6724
|
+
if (nextAttachments.length)
|
|
6725
|
+
leaf.attachments = nextAttachments;
|
|
6619
6726
|
// CAPTAIN-8: delivery carries text only, and the shell is authoritative
|
|
6620
6727
|
// for that text — any text carried on the selection is ignored.
|
|
6621
6728
|
turn.settled = true;
|
|
@@ -7154,6 +7261,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7154
7261
|
: {}),
|
|
7155
7262
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
7156
7263
|
...(frame.inputs === undefined ? {} : { inputs: [...frame.inputs] }),
|
|
7264
|
+
...(frame.attachments === undefined ? {} : { attachments: [...frame.attachments] }),
|
|
7157
7265
|
options: frame.enablement.options,
|
|
7158
7266
|
roleBindings: Object.fromEntries([...frame.playerBindings].map(([role, binding]) => [
|
|
7159
7267
|
role,
|
|
@@ -7365,6 +7473,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7365
7473
|
issuedSessionIds: [...issuedSessionIds],
|
|
7366
7474
|
sequences: { turn: turnSequence, journal: journalSeq },
|
|
7367
7475
|
journal,
|
|
7476
|
+
...(pendingAttachments.length ? { pendingAttachments } : {}),
|
|
7368
7477
|
...(lastAction === undefined ? {} : { lastAction }),
|
|
7369
7478
|
...(lastSettlementStatus === undefined
|
|
7370
7479
|
? {}
|
|
@@ -7659,6 +7768,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7659
7768
|
for (const id of snapshot.issuedSessionIds)
|
|
7660
7769
|
issuedSessionIds.add(id);
|
|
7661
7770
|
journal.push(...snapshot.journal);
|
|
7771
|
+
pendingAttachments = [...(snapshot.pendingAttachments ?? [])];
|
|
7662
7772
|
journalSeq = snapshot.sequences.journal;
|
|
7663
7773
|
turnSequence = snapshot.sequences.turn;
|
|
7664
7774
|
conversation = snapshot.captain.conversation;
|
|
@@ -7790,10 +7900,29 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7790
7900
|
selectInterruptedReport(input, report) {
|
|
7791
7901
|
if (activeTurn)
|
|
7792
7902
|
throw new Error('A Captain turn is already active');
|
|
7793
|
-
if (!input.trim() || !report.text.trim())
|
|
7903
|
+
if ((!input.trim() && nextTurnAttachments.length === 0) || !report.text.trim())
|
|
7794
7904
|
throw new Error('Interruption report requires its recorded input and explanation');
|
|
7795
7905
|
pendingHostSelection = { kind: 'report', text: input, report };
|
|
7796
7906
|
},
|
|
7907
|
+
recordMediaEvidence(reference, runtimeSessionId) {
|
|
7908
|
+
const asset = validateAssetRef(reference);
|
|
7909
|
+
if (!activeTurn)
|
|
7910
|
+
throw new Error('Media evidence arrived outside an active turn');
|
|
7911
|
+
activeTurn.evidence = mergeAttachments(activeTurn.evidence ?? [], [asset]);
|
|
7912
|
+
const producingIndex = frames.findIndex((candidate) => candidate.sessionId === runtimeSessionId);
|
|
7913
|
+
// Observed output belongs to this engagement's reasoning context: a
|
|
7914
|
+
// resumed parent may inspect its child's figure. Boss input additions
|
|
7915
|
+
// still target only the addressed leaf, and a new root inherits none.
|
|
7916
|
+
for (const frame of frames.slice(0, producingIndex + 1)) {
|
|
7917
|
+
frame.attachments = mergeAttachments(frame.attachments ?? [], [asset]);
|
|
7918
|
+
}
|
|
7919
|
+
},
|
|
7920
|
+
setTurnAttachments(references) {
|
|
7921
|
+
if (activeTurn || activeTurnHostCalls)
|
|
7922
|
+
throw new Error('A Captain turn is already active');
|
|
7923
|
+
nextTurnAttachments = validateAssetRefs(references);
|
|
7924
|
+
onTurnAttachments?.(nextTurnAttachments);
|
|
7925
|
+
},
|
|
7797
7926
|
async handleBossTurn(turn, context) {
|
|
7798
7927
|
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
7799
7928
|
throw new Error('init must be called first, or restore must complete before handling a Boss turn');
|
|
@@ -7812,7 +7941,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7812
7941
|
// telemetry (CAPTAIN-7).
|
|
7813
7942
|
retentionSettlementReady = false;
|
|
7814
7943
|
abandonmentSettlementUnsafe = false;
|
|
7815
|
-
|
|
7944
|
+
const attachments = nextTurnAttachments;
|
|
7945
|
+
nextTurnAttachments = [];
|
|
7946
|
+
if (turn.prompt.trim().length === 0 && attachments.length === 0)
|
|
7816
7947
|
return;
|
|
7817
7948
|
const progressBase = exportShellSnapshot();
|
|
7818
7949
|
settledTurnUnresolvedEffects = undefined;
|
|
@@ -7840,6 +7971,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7840
7971
|
const hostGiveUp = decided?.kind === 'give-up' ? true : undefined;
|
|
7841
7972
|
const parsed = decided === undefined ? resolveCommandTurn(turn.prompt) : undefined;
|
|
7842
7973
|
activeTurn = {
|
|
7974
|
+
attachments,
|
|
7975
|
+
workerReports: new Map(),
|
|
7976
|
+
workerStepStarts: new Map(),
|
|
7977
|
+
workerCalls: new Map(),
|
|
7843
7978
|
appliedActionCount: 0,
|
|
7844
7979
|
stoppedFrames: new Set(),
|
|
7845
7980
|
...(progressBase === undefined ? {} : { progressBase }),
|
|
@@ -7873,7 +8008,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7873
8008
|
}
|
|
7874
8009
|
}
|
|
7875
8010
|
decisionCall = undefined;
|
|
7876
|
-
appendJournal('boss', turn.prompt);
|
|
8011
|
+
appendJournal('boss', attachments.length ? { text: turn.prompt, attachments } : turn.prompt);
|
|
8012
|
+
pendingAttachments = mergeAttachments(pendingAttachments, attachments);
|
|
7877
8013
|
try {
|
|
7878
8014
|
await drainRetiredRetainedRuntimes();
|
|
7879
8015
|
await prepareRetainedGenerationOffers();
|
|
@@ -7882,6 +8018,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7882
8018
|
await settleSelection({ action: 'respond', text: activeTurn.interruptedReport.text }, context.signal);
|
|
7883
8019
|
return;
|
|
7884
8020
|
}
|
|
8021
|
+
if (!turn.prompt.trim() && attachments.length) {
|
|
8022
|
+
markConversationCatchUp();
|
|
8023
|
+
await settleSelection({ action: 'respond', text: 'What would you like me to do with the attached material?' }, context.signal);
|
|
8024
|
+
return;
|
|
8025
|
+
}
|
|
7885
8026
|
const result = await captainRuntime.handleBossInput({
|
|
7886
8027
|
text: turn.prompt,
|
|
7887
8028
|
signal: context.signal,
|