@sublang/playbook 17.2.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/cli.md +6 -5
- package/docs/configuration.md +72 -18
- 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 +84 -31
- package/reference/sdlc/code.playbook/bin/playbook.js +5 -3
- package/reference/sdlc/code.playbook/bin/portable-codec.js +12 -5
- package/reference/sdlc/code.playbook/bin/run.js +74 -7
- 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 +164 -25
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +23 -4
- package/reference/sdlc/code.playbook/playbook-captain.js +188 -23
- package/reference/sdlc/code.playbook/playbook-captain.ts +223 -31
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +22 -5
- 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,
|
|
@@ -2098,7 +2103,7 @@ function snapshotFastMode(value, path) {
|
|
|
2098
2103
|
}
|
|
2099
2104
|
return value;
|
|
2100
2105
|
}
|
|
2101
|
-
function
|
|
2106
|
+
function snapshotNonblankTuning(value, path) {
|
|
2102
2107
|
if (value !== undefined &&
|
|
2103
2108
|
(typeof value !== 'string' || value.trim().length === 0)) {
|
|
2104
2109
|
throw new TypeError(`${path} must be a nonblank string`);
|
|
@@ -2113,13 +2118,17 @@ function snapshotSessionAgent(value, path) {
|
|
|
2113
2118
|
'effort',
|
|
2114
2119
|
'fastMode',
|
|
2115
2120
|
'subagentModel',
|
|
2121
|
+
'subagentEffort',
|
|
2122
|
+
'browser',
|
|
2116
2123
|
'instruction',
|
|
2117
2124
|
'permissions',
|
|
2118
2125
|
], path);
|
|
2119
2126
|
const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' &&
|
|
2120
2127
|
key !== 'effort' &&
|
|
2121
2128
|
key !== 'fastMode' &&
|
|
2122
|
-
key !== 'subagentModel'
|
|
2129
|
+
key !== 'subagentModel' &&
|
|
2130
|
+
key !== 'subagentEffort' &&
|
|
2131
|
+
key !== 'browser')), path);
|
|
2123
2132
|
return {
|
|
2124
2133
|
adapter: fixed.adapter,
|
|
2125
2134
|
...(fixed.instruction === undefined
|
|
@@ -2128,6 +2137,7 @@ function snapshotSessionAgent(value, path) {
|
|
|
2128
2137
|
...(fixed.permissions === undefined
|
|
2129
2138
|
? {}
|
|
2130
2139
|
: { permissions: livePermissions(fixed.permissions) }),
|
|
2140
|
+
...(agent.browser === undefined ? {} : { browser: snapshotFastMode(agent.browser, `${path}.browser`) }),
|
|
2131
2141
|
model: snapshotTuningSelection(agent.model, `${path}.model`),
|
|
2132
2142
|
effort: snapshotEffortSelection(agent.effort, `${path}.effort`),
|
|
2133
2143
|
...(agent.fastMode === undefined
|
|
@@ -2136,7 +2146,12 @@ function snapshotSessionAgent(value, path) {
|
|
|
2136
2146
|
...(agent.subagentModel === undefined
|
|
2137
2147
|
? {}
|
|
2138
2148
|
: {
|
|
2139
|
-
subagentModel:
|
|
2149
|
+
subagentModel: snapshotNonblankTuning(agent.subagentModel, `${path}.subagentModel`),
|
|
2150
|
+
}),
|
|
2151
|
+
...(agent.subagentEffort === undefined
|
|
2152
|
+
? {}
|
|
2153
|
+
: {
|
|
2154
|
+
subagentEffort: snapshotNonblankTuning(agent.subagentEffort, `${path}.subagentEffort`),
|
|
2140
2155
|
}),
|
|
2141
2156
|
};
|
|
2142
2157
|
}
|
|
@@ -2150,18 +2165,27 @@ function fixedAgent(agent) {
|
|
|
2150
2165
|
function callSettings(agent, tuning = agent) {
|
|
2151
2166
|
// cligent treats supplied call settings as a complete replacement, so an
|
|
2152
2167
|
// omitted fastMode or subagentModel here is a request for the provider
|
|
2153
|
-
// default,
|
|
2168
|
+
// default, and an omitted subagentEffort leaves each subagent's effort to
|
|
2169
|
+
// the agent — never an inheritance of whatever the previous call left
|
|
2170
|
+
// behind.
|
|
2154
2171
|
return {
|
|
2155
2172
|
model: tuning.model,
|
|
2156
2173
|
effort: tuning.effort,
|
|
2174
|
+
browser: agent.browser ?? false,
|
|
2157
2175
|
...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
|
|
2158
2176
|
...(tuning.subagentModel === undefined
|
|
2159
2177
|
? {}
|
|
2160
2178
|
: { subagentModel: tuning.subagentModel }),
|
|
2179
|
+
...(tuning.subagentEffort === undefined
|
|
2180
|
+
? {}
|
|
2181
|
+
: { subagentEffort: tuning.subagentEffort }),
|
|
2161
2182
|
...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
|
|
2162
2183
|
...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
|
|
2163
2184
|
};
|
|
2164
2185
|
}
|
|
2186
|
+
function controlCallSettings(agent) {
|
|
2187
|
+
return { ...callSettings(agent), browser: false, mcpServers: {} };
|
|
2188
|
+
}
|
|
2165
2189
|
function promptIdentity(binding) {
|
|
2166
2190
|
return binding.model.kind === 'value'
|
|
2167
2191
|
? binding.model.value
|
|
@@ -2285,7 +2309,14 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
|
|
|
2285
2309
|
for (const role of entry.requiredRoleIds) {
|
|
2286
2310
|
const path = `captain.options.playbooks.${id}.roles.${role}`;
|
|
2287
2311
|
const rawBinding = snapshotRecord(roleRecord[role], path);
|
|
2288
|
-
rejectSnapshotKeys(rawBinding, [
|
|
2312
|
+
rejectSnapshotKeys(rawBinding, [
|
|
2313
|
+
'playerId',
|
|
2314
|
+
'model',
|
|
2315
|
+
'effort',
|
|
2316
|
+
'fastMode',
|
|
2317
|
+
'subagentModel',
|
|
2318
|
+
'subagentEffort',
|
|
2319
|
+
], path);
|
|
2289
2320
|
const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
|
|
2290
2321
|
if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
|
|
2291
2322
|
throw new Error(`${path}.playerId is not a canonical player id`);
|
|
@@ -2306,7 +2337,12 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
|
|
|
2306
2337
|
...(rawBinding.subagentModel === undefined
|
|
2307
2338
|
? {}
|
|
2308
2339
|
: {
|
|
2309
|
-
subagentModel:
|
|
2340
|
+
subagentModel: snapshotNonblankTuning(rawBinding.subagentModel, `${path}.subagentModel`),
|
|
2341
|
+
}),
|
|
2342
|
+
...(rawBinding.subagentEffort === undefined
|
|
2343
|
+
? {}
|
|
2344
|
+
: {
|
|
2345
|
+
subagentEffort: snapshotNonblankTuning(rawBinding.subagentEffort, `${path}.subagentEffort`),
|
|
2310
2346
|
}),
|
|
2311
2347
|
agent,
|
|
2312
2348
|
});
|
|
@@ -2521,6 +2557,19 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2521
2557
|
const createCaptainRuntime = deps.createCaptainRuntime ?? createDefaultCaptainRuntime;
|
|
2522
2558
|
const unresolvedEffectSettlement = deps.unresolvedEffectSettlement;
|
|
2523
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
|
+
};
|
|
2524
2573
|
const abortPreparation = deps.abortPreparation;
|
|
2525
2574
|
let pendingHostCapabilities = deps.hostCapabilities;
|
|
2526
2575
|
let currentEffectLedger = () => emptyPlaybookEffectLedger();
|
|
@@ -2842,6 +2891,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2842
2891
|
'parentCallId',
|
|
2843
2892
|
'request',
|
|
2844
2893
|
'inputs',
|
|
2894
|
+
'attachments',
|
|
2845
2895
|
'options',
|
|
2846
2896
|
'roleBindings',
|
|
2847
2897
|
'runtime',
|
|
@@ -2927,6 +2977,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2927
2977
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
2928
2978
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
2929
2979
|
...(frame.inputs === undefined ? {} : { inputs: frame.inputs }),
|
|
2980
|
+
...(frame.attachments === undefined ? {} : { attachments: validateAssetRefs(frame.attachments) }),
|
|
2930
2981
|
options,
|
|
2931
2982
|
roleBindings,
|
|
2932
2983
|
runtime,
|
|
@@ -3300,12 +3351,20 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3300
3351
|
turnCalls?.add(tracked);
|
|
3301
3352
|
return tracked;
|
|
3302
3353
|
};
|
|
3303
|
-
const callCaptainQueued = (frame, context, prompt, options, signal) => {
|
|
3354
|
+
const callCaptainQueued = (frame, context, prompt, options, signal, media) => {
|
|
3304
3355
|
const queued = captainQueue.add(async () => {
|
|
3305
3356
|
signal.throwIfAborted();
|
|
3306
|
-
const
|
|
3357
|
+
const attachments = media?.attachments?.length ? await attachmentPaths(media.attachments, signal) : undefined;
|
|
3307
3358
|
signal.throwIfAborted();
|
|
3308
|
-
|
|
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
|
+
}
|
|
3309
3368
|
});
|
|
3310
3369
|
return trackHostCall(frame, queued);
|
|
3311
3370
|
};
|
|
@@ -3374,9 +3433,17 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3374
3433
|
!Number.isSafeInteger(trace.turnId) ||
|
|
3375
3434
|
trace.turnId <= 0 ||
|
|
3376
3435
|
!Number.isSafeInteger(trace.sequence) ||
|
|
3377
|
-
trace.sequence <= 0
|
|
3378
|
-
|
|
3379
|
-
|
|
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
|
+
}
|
|
3380
3447
|
return;
|
|
3381
3448
|
}
|
|
3382
3449
|
if (trace.type !== 'outcome.accepted')
|
|
@@ -3395,6 +3462,24 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3395
3462
|
receipt.acceptedOutcome.trim().length === 0) {
|
|
3396
3463
|
return;
|
|
3397
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;
|
|
3398
3483
|
const traceKey = `${frame.sessionId}:${trace.sequence}`;
|
|
3399
3484
|
if (summary.acceptedOutcomeTraceKeys.has(traceKey))
|
|
3400
3485
|
return;
|
|
@@ -3414,7 +3499,27 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3414
3499
|
const createPorts = (frame) => ({
|
|
3415
3500
|
...(recordProgress === undefined ? {} : { recordStep: async (step, position) => {
|
|
3416
3501
|
journalledFrames.add(frame);
|
|
3417
|
-
|
|
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 }) });
|
|
3418
3523
|
} }),
|
|
3419
3524
|
callPlayer: async (roleId, prompt, signal, options) => {
|
|
3420
3525
|
admitHostBoundary();
|
|
@@ -3441,6 +3546,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3441
3546
|
throw new Error(`session player ${JSON.stringify(binding.playerId)} already has a call in flight`);
|
|
3442
3547
|
}
|
|
3443
3548
|
const settings = callSettings(binding.agent, binding);
|
|
3549
|
+
const attachments = await attachmentPaths(frame.attachments, signal);
|
|
3444
3550
|
const calling = {
|
|
3445
3551
|
phase: 'calling',
|
|
3446
3552
|
frame,
|
|
@@ -3465,7 +3571,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3465
3571
|
const call = async (resume) => {
|
|
3466
3572
|
await continuity?.beforeCall(binding.playerId);
|
|
3467
3573
|
signal.throwIfAborted();
|
|
3468
|
-
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 }) })));
|
|
3469
3575
|
hostResolved = true;
|
|
3470
3576
|
return raw;
|
|
3471
3577
|
};
|
|
@@ -3587,8 +3693,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3587
3693
|
const result = await callCaptainQueued(frame, activeContext, prompt, {
|
|
3588
3694
|
visibility: options.visibility,
|
|
3589
3695
|
resume: options.resume,
|
|
3696
|
+
settings: options.allowedTools?.length === 0 ? controlCallSettings(captainAgent) : callSettings(captainAgent),
|
|
3590
3697
|
...forwardedToolOptions(options.allowedTools, captainAdapter),
|
|
3591
|
-
}, signal);
|
|
3698
|
+
}, signal, { attachments: options.allowedTools?.length === 0 ? undefined : frame.attachments });
|
|
3592
3699
|
const output = {
|
|
3593
3700
|
status: result.status,
|
|
3594
3701
|
...(result.finalText !== undefined
|
|
@@ -3606,6 +3713,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3606
3713
|
throw new Error('callJudge invoked outside a Boss turn');
|
|
3607
3714
|
}
|
|
3608
3715
|
const result = await callCaptainQueued(frame, activeContext, hiddenJudgeEnvelope(prompt), {
|
|
3716
|
+
settings: controlCallSettings(captainAgent),
|
|
3609
3717
|
visibility: 'hidden',
|
|
3610
3718
|
resume: false,
|
|
3611
3719
|
...controlCallToolOptions(captainAdapter),
|
|
@@ -3740,6 +3848,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3740
3848
|
runtime,
|
|
3741
3849
|
sessionId,
|
|
3742
3850
|
rootSessionId: parent?.frame.rootSessionId ?? sessionId,
|
|
3851
|
+
...((parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn?.attachments)?.length ? { attachments: [...(parent?.frame.attachments ?? activeTurn?.selectedAttachments ?? activeTurn.attachments)] } : {}),
|
|
3743
3852
|
depth: parent ? parent.frame.depth + 1 : 0,
|
|
3744
3853
|
playerBindings,
|
|
3745
3854
|
...(parent ? { parent } : {}),
|
|
@@ -3755,12 +3864,14 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
3755
3864
|
enablement,
|
|
3756
3865
|
runtime,
|
|
3757
3866
|
sessionId: snapshot.sessionId,
|
|
3867
|
+
effectOwnerSessionId: snapshot.runtime.retainedEffectSourceSessionId ?? snapshot.sessionId,
|
|
3758
3868
|
rootSessionId: snapshot.rootSessionId,
|
|
3759
3869
|
depth: snapshot.depth,
|
|
3760
3870
|
playerBindings,
|
|
3761
3871
|
...(parent ? { parent } : {}),
|
|
3762
3872
|
...(snapshot.request === undefined ? {} : { request: snapshot.request }),
|
|
3763
3873
|
...(snapshot.inputs === undefined ? {} : { inputs: [...snapshot.inputs] }),
|
|
3874
|
+
...(snapshot.attachments === undefined ? {} : { attachments: [...snapshot.attachments] }),
|
|
3764
3875
|
state: snapshot.runtime.state,
|
|
3765
3876
|
inFlightHostCalls: new Set(),
|
|
3766
3877
|
};
|
|
@@ -4041,6 +4152,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
4041
4152
|
finally {
|
|
4042
4153
|
if (leafFrame() === frame) {
|
|
4043
4154
|
frames.pop();
|
|
4155
|
+
if (!frame.parent)
|
|
4156
|
+
pendingAttachments = [];
|
|
4044
4157
|
if (frame.parent) {
|
|
4045
4158
|
pendingChildParents.delete(frame.parent.frame);
|
|
4046
4159
|
}
|
|
@@ -5304,11 +5417,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5304
5417
|
await continuity?.beforeCall('captain');
|
|
5305
5418
|
context.signal.throwIfAborted();
|
|
5306
5419
|
attempt.providerBoundaryEntered = true;
|
|
5307
|
-
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, {
|
|
5308
5421
|
visibility: 'hidden',
|
|
5309
5422
|
resume,
|
|
5310
5423
|
...controlCallToolOptions(captainAdapter),
|
|
5311
|
-
settings:
|
|
5424
|
+
settings: controlCallSettings(captainAgent),
|
|
5312
5425
|
}));
|
|
5313
5426
|
context.signal.throwIfAborted();
|
|
5314
5427
|
return result;
|
|
@@ -5473,6 +5586,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5473
5586
|
...(kind === 'decision'
|
|
5474
5587
|
? [labeledBlock('Catalog digest', catalogDigest())]
|
|
5475
5588
|
: []),
|
|
5589
|
+
...(kind === 'closingReply' && turn?.workerReports.size
|
|
5590
|
+
? [workerReportBlock([...turn.workerReports.values()])]
|
|
5591
|
+
: []),
|
|
5476
5592
|
...(kind === 'closingReply' && turn?.report
|
|
5477
5593
|
? [
|
|
5478
5594
|
labeledBlock('ControlView digest', controlViewDigest()),
|
|
@@ -5797,6 +5913,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5797
5913
|
enablement,
|
|
5798
5914
|
runtime: offer.runtimes[index],
|
|
5799
5915
|
sessionId: targetSessionIds[index],
|
|
5916
|
+
effectOwnerSessionId: sourceFrame.runtime.retainedEffectSourceSessionId ?? sourceFrame.sessionId,
|
|
5800
5917
|
rootSessionId: targetRootSessionId,
|
|
5801
5918
|
depth: index,
|
|
5802
5919
|
playerBindings: makePlayerBindings(enablement),
|
|
@@ -5805,6 +5922,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
5805
5922
|
: {}),
|
|
5806
5923
|
...(sourceFrame.request === undefined ? {} : { request: sourceFrame.request }),
|
|
5807
5924
|
...(sourceFrame.inputs === undefined ? {} : { inputs: [...sourceFrame.inputs] }),
|
|
5925
|
+
...(sourceFrame.attachments === undefined ? {} : { attachments: [...sourceFrame.attachments] }),
|
|
5808
5926
|
state: sourceFrame.runtime.state,
|
|
5809
5927
|
inFlightHostCalls: new Set(),
|
|
5810
5928
|
});
|
|
@@ -6359,6 +6477,12 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6359
6477
|
return rejectSelection(selection, `/${enablementById.get(entry.id).command} is already on the active path`);
|
|
6360
6478
|
}
|
|
6361
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 = [];
|
|
6362
6486
|
turn.settled = true;
|
|
6363
6487
|
journalAction({
|
|
6364
6488
|
action: selection.action,
|
|
@@ -6491,7 +6615,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6491
6615
|
`Later Boss input (context only): ${JSON.stringify(laterInputs)}`,
|
|
6492
6616
|
`Existing instructions: ${JSON.stringify(instructions)}`,
|
|
6493
6617
|
`Pending questions (quoted evidence): ${JSON.stringify(before.pendingQuestions.map(({ question }) => question))}`,
|
|
6494
|
-
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings:
|
|
6618
|
+
].join('\n')), { visibility: 'hidden', resume: false, ...controlCallToolOptions(captainAdapter), settings: controlCallSettings(captainAgent) }, signal);
|
|
6495
6619
|
const answer = result.status === 'ok' ? controlReplyJson(result.finalText) : undefined;
|
|
6496
6620
|
const index = answer && typeof answer === 'object' && !Array.isArray(answer) && Object.keys(answer).join(',') === 'instructionIndex'
|
|
6497
6621
|
? answer.instructionIndex : undefined;
|
|
@@ -6549,7 +6673,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6549
6673
|
try {
|
|
6550
6674
|
claim = await capability.acquire({ signal: preparationSignal });
|
|
6551
6675
|
await claim.assertOwner();
|
|
6552
|
-
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 }));
|
|
6553
6677
|
preparationSignal.throwIfAborted();
|
|
6554
6678
|
await claim.assertOwner();
|
|
6555
6679
|
if (result.status !== 'ok') {
|
|
@@ -6592,6 +6716,13 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6592
6716
|
: { action: 'runtime', actionId: offer.continuation.actionId };
|
|
6593
6717
|
}
|
|
6594
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;
|
|
6595
6726
|
// CAPTAIN-8: delivery carries text only, and the shell is authoritative
|
|
6596
6727
|
// for that text — any text carried on the selection is ignored.
|
|
6597
6728
|
turn.settled = true;
|
|
@@ -7130,6 +7261,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7130
7261
|
: {}),
|
|
7131
7262
|
...(frame.request === undefined ? {} : { request: frame.request }),
|
|
7132
7263
|
...(frame.inputs === undefined ? {} : { inputs: [...frame.inputs] }),
|
|
7264
|
+
...(frame.attachments === undefined ? {} : { attachments: [...frame.attachments] }),
|
|
7133
7265
|
options: frame.enablement.options,
|
|
7134
7266
|
roleBindings: Object.fromEntries([...frame.playerBindings].map(([role, binding]) => [
|
|
7135
7267
|
role,
|
|
@@ -7341,6 +7473,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7341
7473
|
issuedSessionIds: [...issuedSessionIds],
|
|
7342
7474
|
sequences: { turn: turnSequence, journal: journalSeq },
|
|
7343
7475
|
journal,
|
|
7476
|
+
...(pendingAttachments.length ? { pendingAttachments } : {}),
|
|
7344
7477
|
...(lastAction === undefined ? {} : { lastAction }),
|
|
7345
7478
|
...(lastSettlementStatus === undefined
|
|
7346
7479
|
? {}
|
|
@@ -7635,6 +7768,7 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7635
7768
|
for (const id of snapshot.issuedSessionIds)
|
|
7636
7769
|
issuedSessionIds.add(id);
|
|
7637
7770
|
journal.push(...snapshot.journal);
|
|
7771
|
+
pendingAttachments = [...(snapshot.pendingAttachments ?? [])];
|
|
7638
7772
|
journalSeq = snapshot.sequences.journal;
|
|
7639
7773
|
turnSequence = snapshot.sequences.turn;
|
|
7640
7774
|
conversation = snapshot.captain.conversation;
|
|
@@ -7766,10 +7900,29 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7766
7900
|
selectInterruptedReport(input, report) {
|
|
7767
7901
|
if (activeTurn)
|
|
7768
7902
|
throw new Error('A Captain turn is already active');
|
|
7769
|
-
if (!input.trim() || !report.text.trim())
|
|
7903
|
+
if ((!input.trim() && nextTurnAttachments.length === 0) || !report.text.trim())
|
|
7770
7904
|
throw new Error('Interruption report requires its recorded input and explanation');
|
|
7771
7905
|
pendingHostSelection = { kind: 'report', text: input, report };
|
|
7772
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
|
+
},
|
|
7773
7926
|
async handleBossTurn(turn, context) {
|
|
7774
7927
|
if (lifecycle !== 'ready' || terminallyDisposed) {
|
|
7775
7928
|
throw new Error('init must be called first, or restore must complete before handling a Boss turn');
|
|
@@ -7788,7 +7941,9 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7788
7941
|
// telemetry (CAPTAIN-7).
|
|
7789
7942
|
retentionSettlementReady = false;
|
|
7790
7943
|
abandonmentSettlementUnsafe = false;
|
|
7791
|
-
|
|
7944
|
+
const attachments = nextTurnAttachments;
|
|
7945
|
+
nextTurnAttachments = [];
|
|
7946
|
+
if (turn.prompt.trim().length === 0 && attachments.length === 0)
|
|
7792
7947
|
return;
|
|
7793
7948
|
const progressBase = exportShellSnapshot();
|
|
7794
7949
|
settledTurnUnresolvedEffects = undefined;
|
|
@@ -7816,6 +7971,10 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7816
7971
|
const hostGiveUp = decided?.kind === 'give-up' ? true : undefined;
|
|
7817
7972
|
const parsed = decided === undefined ? resolveCommandTurn(turn.prompt) : undefined;
|
|
7818
7973
|
activeTurn = {
|
|
7974
|
+
attachments,
|
|
7975
|
+
workerReports: new Map(),
|
|
7976
|
+
workerStepStarts: new Map(),
|
|
7977
|
+
workerCalls: new Map(),
|
|
7819
7978
|
appliedActionCount: 0,
|
|
7820
7979
|
stoppedFrames: new Set(),
|
|
7821
7980
|
...(progressBase === undefined ? {} : { progressBase }),
|
|
@@ -7849,7 +8008,8 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7849
8008
|
}
|
|
7850
8009
|
}
|
|
7851
8010
|
decisionCall = undefined;
|
|
7852
|
-
appendJournal('boss', turn.prompt);
|
|
8011
|
+
appendJournal('boss', attachments.length ? { text: turn.prompt, attachments } : turn.prompt);
|
|
8012
|
+
pendingAttachments = mergeAttachments(pendingAttachments, attachments);
|
|
7853
8013
|
try {
|
|
7854
8014
|
await drainRetiredRetainedRuntimes();
|
|
7855
8015
|
await prepareRetainedGenerationOffers();
|
|
@@ -7858,6 +8018,11 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
7858
8018
|
await settleSelection({ action: 'respond', text: activeTurn.interruptedReport.text }, context.signal);
|
|
7859
8019
|
return;
|
|
7860
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
|
+
}
|
|
7861
8026
|
const result = await captainRuntime.handleBossInput({
|
|
7862
8027
|
text: turn.prompt,
|
|
7863
8028
|
signal: context.signal,
|