@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,7 @@
|
|
|
7
7
|
// complete retained-generation map under guarded source-first publication.
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from 'node:crypto';
|
|
10
|
+
import { createAssetStore, parseAssetUri, validateAssetRef, validateAssetRefs } from '../session-assets.js';
|
|
10
11
|
import {
|
|
11
12
|
SESSION_MANIFEST_VERSION, EMPTY_REPLAY_SHA256, sha256,
|
|
12
13
|
validateSessionManifest, recoveryFromManifest, manifestFromRecovery,
|
|
@@ -637,6 +638,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
637
638
|
const replayReadCursors = new Map();
|
|
638
639
|
const replayReadQueues = new Map();
|
|
639
640
|
const portableWriters = new Map();
|
|
641
|
+
const assetStores = new Map();
|
|
642
|
+
const assetStoreFor = (sessionId) => {
|
|
643
|
+
assertSessionId(sessionId);
|
|
644
|
+
if (!assetStores.has(sessionId)) assetStores.set(sessionId, createAssetStore({ directory: join(sessionsDir, `${sessionId}.assets`), ...(options.maxAssetBytes === undefined ? {} : { maxAssetBytes: options.maxAssetBytes }) }));
|
|
645
|
+
return assetStores.get(sessionId);
|
|
646
|
+
};
|
|
640
647
|
|
|
641
648
|
if (!isAbsolute(sessionsDir)) {
|
|
642
649
|
throw new Error('Captain session store path must be absolute');
|
|
@@ -758,7 +765,11 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
758
765
|
if (value.sessionId !== sessionId) throw new Error('session manifest identity does not match its filename');
|
|
759
766
|
return value.schemaVersion === 7 ? validateSessionManifest(value) : value;
|
|
760
767
|
};
|
|
761
|
-
const prepare = () =>
|
|
768
|
+
const prepare = async () => {
|
|
769
|
+
await prepareSessionPermissions(sessionsDir, fs);
|
|
770
|
+
let names; try { names = await fs.readdir(sessionsDir); } catch (cause) { if (cause?.code === 'ENOENT') return; throw cause; }
|
|
771
|
+
for (const name of names) if (name.endsWith('.assets') && SESSION_ID_PATTERN.test(name.slice(0, -7))) await assetStoreFor(name.slice(0, -7)).prepare();
|
|
772
|
+
};
|
|
762
773
|
const readHistory = async (sessionId, options = {}) => {
|
|
763
774
|
assertSessionId(sessionId);
|
|
764
775
|
const history = await readSessionHistory({ sessionsDir, path: recordsPathFor(sessionId), fs, afterSeq: options.afterSeq ?? 0 });
|
|
@@ -771,6 +782,35 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
771
782
|
} catch { return history; }
|
|
772
783
|
return legacyJournalHistory(legacy, options.afterSeq ?? 0, history);
|
|
773
784
|
};
|
|
785
|
+
// Only our typed reference locations participate; opaque tool/text data is history.
|
|
786
|
+
const assertAssetInventory = (manifest, entries) => {
|
|
787
|
+
const declared = new Map((manifest.assets?.entries ?? []).map((reference) => [reference.assetId, reference]));
|
|
788
|
+
const check = (reference) => {
|
|
789
|
+
const valid = validateAssetRef(reference);
|
|
790
|
+
if (declared.get(valid.assetId)?.byteLength !== valid.byteLength) throw new Error(`session asset reference is absent from its checkpoint inventory: ${valid.assetId}`);
|
|
791
|
+
};
|
|
792
|
+
const checkRefs = (references) => { for (const reference of references ?? []) check(reference); };
|
|
793
|
+
const snapshot = (value) => {
|
|
794
|
+
if (!value) return;
|
|
795
|
+
checkRefs(value.pendingAttachments);
|
|
796
|
+
for (const frame of value.frames ?? []) checkRefs(frame.attachments);
|
|
797
|
+
for (const item of value.journal ?? []) if (item.kind === 'boss' && typeof item.payload === 'object' && item.payload !== null) checkRefs(item.payload.attachments);
|
|
798
|
+
};
|
|
799
|
+
snapshot(manifest.snapshot); snapshot(manifest.uncertain?.progress?.snapshot);
|
|
800
|
+
checkRefs(manifest.uncertain?.attachments);
|
|
801
|
+
for (const generation of Object.values(manifest.retainedGenerations ?? {})) for (const frame of generation.frames ?? []) checkRefs(frame.attachments);
|
|
802
|
+
for (const { record } of entries) {
|
|
803
|
+
if (record.type === 'turn_started') checkRefs(record.turn?.attachments);
|
|
804
|
+
else if (record.type === 'playbook_evidence') check(record.asset);
|
|
805
|
+
else if (record.type === 'player_event' || record.type === 'captain_event') {
|
|
806
|
+
const event = record.event;
|
|
807
|
+
if (event?.type === 'media' && event.payload?.source?.type === 'uri') {
|
|
808
|
+
const id = parseAssetUri(event.payload.source.uri);
|
|
809
|
+
if (id && !declared.has(id)) throw new Error(`session media asset is absent from its checkpoint inventory: ${id}`);
|
|
810
|
+
} else if (event?.type === 'tool_result' && event.payload?.output?.type === 'asset_reference') check(event.payload.output.asset);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
774
814
|
const validate = async (sessionId, context = {}) => {
|
|
775
815
|
const manifest = await readManifest(sessionId);
|
|
776
816
|
const history = await readHistory(sessionId);
|
|
@@ -790,6 +830,12 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
790
830
|
integrityValid = false;
|
|
791
831
|
reasons.push('session replay contains provider continuation fields');
|
|
792
832
|
}
|
|
833
|
+
try { assertAssetInventory(manifest, history.entries.filter(({ seq }) => seq <= manifest.replay.seq)); }
|
|
834
|
+
catch (cause) { integrityValid = false; reasons.push(errorMessage(cause)); }
|
|
835
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
836
|
+
try { const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close(); }
|
|
837
|
+
catch (cause) { integrityValid = false; reasons.push(`session asset is unavailable: ${errorMessage(cause)}`); }
|
|
838
|
+
}
|
|
793
839
|
if (manifest.state === 'history-only') reasons.push(manifest.reason);
|
|
794
840
|
if (manifest.replay.incomplete || history.incomplete) reasons.push('session replay is incomplete');
|
|
795
841
|
if (history.digests[manifest.replay.seq] !== manifest.replay.sha256) { integrityValid = false; reasons.push('session replay checkpoint digest does not match'); }
|
|
@@ -1111,6 +1157,18 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1111
1157
|
};
|
|
1112
1158
|
|
|
1113
1159
|
const deleteRecord = async (sessionId) => {
|
|
1160
|
+
const assetsDirectory = join(sessionsDir, `${sessionId}.assets`);
|
|
1161
|
+
try {
|
|
1162
|
+
await assertPrivateDirectory(assetsDirectory, fs);
|
|
1163
|
+
const names = await fs.readdir(assetsDirectory);
|
|
1164
|
+
for (const name of names) {
|
|
1165
|
+
if (!/^(?:[0-9a-f]{64}(?:\.json)?|\.import-[0-9a-f-]+\.tmp)$/.test(name)) throw new Error('unknown session asset entry prevents deletion');
|
|
1166
|
+
await assertPrivateRegularPath(join(assetsDirectory, name), 0o600, fs, 'session asset');
|
|
1167
|
+
}
|
|
1168
|
+
for (const name of names) await fs.unlink(join(assetsDirectory, name));
|
|
1169
|
+
await fs.rmdir(assetsDirectory);
|
|
1170
|
+
await syncDirectory(sessionsDir, fs);
|
|
1171
|
+
} catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
|
|
1114
1172
|
for (const suffix of ['.records.jsonl', '.hints.json', '.spex.json', '.json']) {
|
|
1115
1173
|
const path = join(sessionsDir, `${sessionId}${suffix}`);
|
|
1116
1174
|
try { await assertPrivateRegularPath(path, 0o600, fs, 'session file'); await fs.unlink(path); await syncDirectory(sessionsDir, fs); }
|
|
@@ -1396,6 +1454,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1396
1454
|
const acquire = async (sessionId, management = false) => {
|
|
1397
1455
|
assertSessionId(sessionId);
|
|
1398
1456
|
await prepareSessionPermissions(sessionsDir, fs, sessionId);
|
|
1457
|
+
await assetStoreFor(sessionId).prepare();
|
|
1399
1458
|
let stage;
|
|
1400
1459
|
let stagePublished = false;
|
|
1401
1460
|
try {
|
|
@@ -1451,6 +1510,7 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1451
1510
|
readHistory,
|
|
1452
1511
|
validateSession: validate,
|
|
1453
1512
|
portableWriters,
|
|
1513
|
+
assetStore: assetStoreFor(sessionId),
|
|
1454
1514
|
writeRecord,
|
|
1455
1515
|
syncRecordDirectory: () => syncDirectory(sessionsDir, fs),
|
|
1456
1516
|
deleteRecord,
|
|
@@ -1684,8 +1744,39 @@ export function createCaptainSessionStore(options = {}) {
|
|
|
1684
1744
|
finally { await lease.release(); }
|
|
1685
1745
|
};
|
|
1686
1746
|
|
|
1747
|
+
const exportBundle = async (sessionId) => {
|
|
1748
|
+
assertSessionId(sessionId);
|
|
1749
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1750
|
+
const manifestText = await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record');
|
|
1751
|
+
const manifest = validateSessionManifest(JSON.parse(manifestText));
|
|
1752
|
+
const replayBytes = Buffer.from(await readPrivateRegularFile(recordsPathFor(sessionId), 0o600, fs, 'replay stream'), 'utf8');
|
|
1753
|
+
let end = 0;
|
|
1754
|
+
for (let seq = 0; seq < manifest.replay.seq; seq++) {
|
|
1755
|
+
const next = replayBytes.indexOf(10, end);
|
|
1756
|
+
if (next === -1) throw new Error('session bundle is missing its checkpoint replay prefix');
|
|
1757
|
+
end = next + 1;
|
|
1758
|
+
}
|
|
1759
|
+
const replay = replayBytes.subarray(0, end);
|
|
1760
|
+
if (sha256(replay) !== manifest.replay.sha256 || manifest.replay.incomplete) throw new Error('session bundle replay checkpoint is incomplete or corrupt');
|
|
1761
|
+
assertAssetInventory(manifest, parseSessionHistory(replay).entries);
|
|
1762
|
+
const assets = [];
|
|
1763
|
+
for (const reference of manifest.assets?.entries ?? []) {
|
|
1764
|
+
const reader = await assetStoreFor(sessionId).openAsset(reference); await reader.close();
|
|
1765
|
+
const hash = reference.assetId.slice(7);
|
|
1766
|
+
assets.push(Object.freeze({ assetId: reference.assetId, path: hash, metadataPath: `${hash}.json`, byteLength: reference.byteLength }));
|
|
1767
|
+
}
|
|
1768
|
+
if (manifestText !== await readPrivateRegularFile(recordPathFor(sessionId), 0o600, fs, 'record')) continue;
|
|
1769
|
+
return Object.freeze({ sessionId, manifest: Buffer.from(manifestText, 'utf8'), replay, assets: Object.freeze(assets) });
|
|
1770
|
+
}
|
|
1771
|
+
throw new Error('session changed while exporting its bundle; retry');
|
|
1772
|
+
};
|
|
1773
|
+
|
|
1687
1774
|
return Object.freeze({
|
|
1688
1775
|
sessionsDir,
|
|
1776
|
+
readAsset: (sessionId, reference, options) => assetStoreFor(sessionId).readAsset(reference, options),
|
|
1777
|
+
openAsset: (sessionId, reference, options) => assetStoreFor(sessionId).openAsset(reference, options),
|
|
1778
|
+
describeAsset: (sessionId, assetId) => assetStoreFor(sessionId).describeAsset(assetId),
|
|
1779
|
+
exportBundle,
|
|
1689
1780
|
prepare,
|
|
1690
1781
|
migrate,
|
|
1691
1782
|
migrateLegacyDefault,
|
|
@@ -2540,6 +2631,7 @@ async function createLease({
|
|
|
2540
2631
|
readHistory,
|
|
2541
2632
|
validateSession,
|
|
2542
2633
|
portableWriters,
|
|
2634
|
+
assetStore,
|
|
2543
2635
|
writeRecord,
|
|
2544
2636
|
syncRecordDirectory,
|
|
2545
2637
|
deleteRecord,
|
|
@@ -2636,7 +2728,9 @@ async function createLease({
|
|
|
2636
2728
|
const seq = history.digests[durableSeq] === undefined ? previousManifest?.replay?.seq ?? 0 : durableSeq;
|
|
2637
2729
|
const digest = history.digests[seq] ?? previousManifest?.replay?.sha256 ?? EMPTY_REPLAY_SHA256;
|
|
2638
2730
|
const replay = { seq, sha256: digest, incomplete: previousManifest?.replay?.incomplete === true || status.incomplete || history.incomplete || history.digests[seq] === undefined };
|
|
2639
|
-
const
|
|
2731
|
+
const entries = await assetStore.listAssets();
|
|
2732
|
+
const assets = entries.length === 0 ? undefined : { version: 1, entries: await Promise.all(entries.map(({ assetId }) => assetStore.describeAsset(assetId))) };
|
|
2733
|
+
const manifest = manifestFromRecovery(recovery, replay, contextSeq, assets);
|
|
2640
2734
|
return manifest;
|
|
2641
2735
|
};
|
|
2642
2736
|
portableWriters.set(sessionId, { checkpoint, sync: replayWriter.checkpoint, published: (manifest) => { previousManifest = manifest; } });
|
|
@@ -2928,6 +3022,8 @@ async function createLease({
|
|
|
2928
3022
|
source.effectLedger,
|
|
2929
3023
|
);
|
|
2930
3024
|
|
|
3025
|
+
const inheritedAssets = new Map(Object.values(retainedGenerations).flatMap((generation) => generation.frames.flatMap((frame) => frame.attachments ?? [])).map((reference) => [reference.assetId, reference]));
|
|
3026
|
+
for (const reference of inheritedAssets.values()) await assetStore.copyAsset(sourceLease, reference);
|
|
2931
3027
|
await sourceLease.assertOwner();
|
|
2932
3028
|
await assertOwnerUnchecked();
|
|
2933
3029
|
let sourcePublished = false;
|
|
@@ -3026,11 +3122,12 @@ async function createLease({
|
|
|
3026
3122
|
|
|
3027
3123
|
const beginTurn = ({
|
|
3028
3124
|
input,
|
|
3125
|
+
attachments,
|
|
3029
3126
|
attemptId,
|
|
3030
3127
|
attemptedExecutionProjection,
|
|
3031
3128
|
} = {}) =>
|
|
3032
3129
|
runExclusive(async () => {
|
|
3033
|
-
assertAcceptedInput(input);
|
|
3130
|
+
assertAcceptedInput(input, attachments);
|
|
3034
3131
|
assertUuid(attemptId, 'Captain session attempt id');
|
|
3035
3132
|
const attempted = validateCaptainSessionExecutionProjection(
|
|
3036
3133
|
attemptedExecutionProjection,
|
|
@@ -3065,6 +3162,7 @@ async function createLease({
|
|
|
3065
3162
|
uncertain: {
|
|
3066
3163
|
baseUpdatedAt: prior.updatedAt,
|
|
3067
3164
|
input,
|
|
3165
|
+
...(attachments?.length ? { attachments: validateAssetRefs(attachments) } : {}),
|
|
3068
3166
|
attemptId,
|
|
3069
3167
|
attemptNumber: 1,
|
|
3070
3168
|
markedAt: timestamp,
|
|
@@ -3119,6 +3217,7 @@ async function createLease({
|
|
|
3119
3217
|
baseUpdatedAt: prior.uncertain.baseUpdatedAt,
|
|
3120
3218
|
...(prior.uncertain.progress === undefined ? {} : { progress: prior.uncertain.progress }),
|
|
3121
3219
|
input: prior.uncertain.input,
|
|
3220
|
+
...(prior.uncertain.attachments === undefined ? {} : { attachments: prior.uncertain.attachments }),
|
|
3122
3221
|
attemptId: nextAttemptId,
|
|
3123
3222
|
attemptNumber: prior.uncertain.attemptNumber + 1,
|
|
3124
3223
|
markedAt: timestamp,
|
|
@@ -3637,11 +3736,11 @@ async function createLease({
|
|
|
3637
3736
|
if (Object.hasOwn(step, 'result') && !['completion', 'answer'].includes(step.kind)) throw new Error('A step result requires its saved start');
|
|
3638
3737
|
progress.steps.push(step);
|
|
3639
3738
|
} else {
|
|
3640
|
-
const { result, ...start } = step;
|
|
3739
|
+
const { result, workerEvidence, ...start } = step;
|
|
3641
3740
|
const saved = progress.steps[index];
|
|
3642
|
-
const { result: priorResult, ...priorStart } = saved;
|
|
3741
|
+
const { result: priorResult, workerEvidence: priorEvidence, ...priorStart } = saved;
|
|
3643
3742
|
if (!isDeepStrictEqual(start, priorStart) ||
|
|
3644
|
-
(priorResult !== undefined && !isDeepStrictEqual(priorResult, result))) {
|
|
3743
|
+
(priorResult !== undefined && (!isDeepStrictEqual(priorResult, result) || !isDeepStrictEqual(priorEvidence, workerEvidence)))) {
|
|
3645
3744
|
throw new Error('A saved step cannot be replaced');
|
|
3646
3745
|
}
|
|
3647
3746
|
progress.steps[index] = step;
|
|
@@ -3728,6 +3827,11 @@ async function createLease({
|
|
|
3728
3827
|
sessionId,
|
|
3729
3828
|
ownerToken: owner.ownerToken,
|
|
3730
3829
|
append,
|
|
3830
|
+
importAsset: (input) => runExclusive(async () => { await assertOwnerUnchecked(); return assetStore.importAsset(input); }),
|
|
3831
|
+
resolveAttachments: (references, options) => runExclusive(async () => { await assertOwnerUnchecked(); if (!Array.isArray(references)) throw new TypeError('attachments must be an array'); return Promise.all(references.map((reference) => assetStore.resolveAttachment(reference, options))); }),
|
|
3832
|
+
readAsset: (reference, options) => assetStore.readAsset(reference, options),
|
|
3833
|
+
openAsset: (reference, options) => assetStore.openAsset(reference, options),
|
|
3834
|
+
describeAsset: (assetId) => assetStore.describeAsset(assetId),
|
|
3731
3835
|
recordContext,
|
|
3732
3836
|
consumeHints,
|
|
3733
3837
|
acknowledgeHint,
|
|
@@ -4089,7 +4193,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4089
4193
|
exactOptionalKeys(
|
|
4090
4194
|
uncertain,
|
|
4091
4195
|
UNCERTAIN_KEYS,
|
|
4092
|
-
['abandonment', 'progress'],
|
|
4196
|
+
['abandonment', 'progress', 'attachments'],
|
|
4093
4197
|
'Captain session record uncertain',
|
|
4094
4198
|
);
|
|
4095
4199
|
if (uncertain.baseUpdatedAt !== null) {
|
|
@@ -4110,7 +4214,7 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4110
4214
|
);
|
|
4111
4215
|
}
|
|
4112
4216
|
}
|
|
4113
|
-
assertAcceptedInput(uncertain.input);
|
|
4217
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
4114
4218
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
4115
4219
|
if (
|
|
4116
4220
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -4172,8 +4276,9 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4172
4276
|
exactOptionalKeys(progress, ['snapshot', 'steps'], ['positionStepId'], 'Captain progress');
|
|
4173
4277
|
if (!Array.isArray(progress.steps)) throw new Error('Captain steps must be an array');
|
|
4174
4278
|
const ids = new Set();
|
|
4279
|
+
const workerBoundaryIds = new Set();
|
|
4175
4280
|
for (const step of progress.steps) {
|
|
4176
|
-
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result'], 'Captain step');
|
|
4281
|
+
exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result', 'workerEvidence'], 'Captain step');
|
|
4177
4282
|
assertUuid(step.id, 'Captain step id');
|
|
4178
4283
|
assertUuid(step.runtimeSessionId, 'Captain step runtime');
|
|
4179
4284
|
if (ids.has(step.id)) throw new Error('Captain step ids must be unique');
|
|
@@ -4209,6 +4314,22 @@ function validateCanonicalCaptainSessionRecord(
|
|
|
4209
4314
|
}
|
|
4210
4315
|
requireCanonicalNonblank(step.stateId, 'Captain step state');
|
|
4211
4316
|
if (!Object.hasOwn(structural.catalog, step.playbookId)) throw new Error('Captain step names an unknown playbook');
|
|
4317
|
+
if (step.workerEvidence !== undefined) {
|
|
4318
|
+
const evidence = requireRecord(step.workerEvidence, 'Worker evidence');
|
|
4319
|
+
exactOptionalKeys(evidence, ['boundaryId', 'playerId'], [], 'Worker evidence');
|
|
4320
|
+
assertUuid(evidence.boundaryId, 'Worker evidence boundary');
|
|
4321
|
+
requireCanonicalNonblank(evidence.playerId, 'Worker evidence player');
|
|
4322
|
+
const boundary = record.effectLedger.boundaries.find((entry) => entry.boundaryId === evidence.boundaryId);
|
|
4323
|
+
if (step.kind !== 'player' || !step.result || typeof step.result !== 'object' || Array.isArray(step.result) ||
|
|
4324
|
+
!boundary || boundary.playbookId !== step.playbookId || boundary.sourceStateId !== step.stateId ||
|
|
4325
|
+
workerBoundaryIds.has(evidence.boundaryId) || isDeepStrictEqual(boundary, record.snapshot.effectLedger.boundaries[boundary.sequence - 1]) ||
|
|
4326
|
+
!boundary.physicalReceipt || !boundary.finalText?.trim() ||
|
|
4327
|
+
boundary.semanticCandidate?.guard !== step.result.guard || typeof step.result.guard !== 'string' || step.result.guard === 'needsBossReply' ||
|
|
4328
|
+
structural.catalog[step.playbookId]?.roles?.[boundary.roleId]?.playerId !== evidence.playerId) {
|
|
4329
|
+
throw new Error('Worker evidence must name its accepted player boundary');
|
|
4330
|
+
}
|
|
4331
|
+
workerBoundaryIds.add(evidence.boundaryId);
|
|
4332
|
+
}
|
|
4212
4333
|
}
|
|
4213
4334
|
if (progress.positionStepId !== undefined && progress.positionStepId !== null && !ids.has(progress.positionStepId)) throw new Error('Captain progress positionStepId must name a recorded step');
|
|
4214
4335
|
if (progress.snapshot === null && progress.positionStepId != null) throw new Error('Captain progress positionStepId requires a saved position');
|
|
@@ -5312,7 +5433,7 @@ function assertReleasedSchema2CaptainSessionRecord(record) {
|
|
|
5312
5433
|
);
|
|
5313
5434
|
}
|
|
5314
5435
|
}
|
|
5315
|
-
assertAcceptedInput(uncertain.input);
|
|
5436
|
+
assertAcceptedInput(uncertain.input, uncertain.attachments);
|
|
5316
5437
|
assertUuid(uncertain.attemptId, 'Captain session attempt id');
|
|
5317
5438
|
if (
|
|
5318
5439
|
!Number.isSafeInteger(uncertain.attemptNumber) ||
|
|
@@ -5408,6 +5529,7 @@ function validateCaptainSessionProjection(
|
|
|
5408
5529
|
'fastMode',
|
|
5409
5530
|
'subagentModel',
|
|
5410
5531
|
'subagentEffort',
|
|
5532
|
+
'browser',
|
|
5411
5533
|
],
|
|
5412
5534
|
playerPath,
|
|
5413
5535
|
);
|
|
@@ -5578,6 +5700,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5578
5700
|
const agent = requireRecord(value, path);
|
|
5579
5701
|
if (!structural) {
|
|
5580
5702
|
validateProjectedFastMode(agent, `${path}.fastMode`);
|
|
5703
|
+
if (agent.browser !== undefined && typeof agent.browser !== 'boolean') throw new Error(`${path}.browser must be a boolean`);
|
|
5581
5704
|
validateProjectedSubagentTuning(agent, path);
|
|
5582
5705
|
}
|
|
5583
5706
|
if (!hasId) {
|
|
@@ -5592,6 +5715,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
|
|
|
5592
5715
|
'fastMode',
|
|
5593
5716
|
'subagentModel',
|
|
5594
5717
|
'subagentEffort',
|
|
5718
|
+
'browser',
|
|
5595
5719
|
],
|
|
5596
5720
|
path,
|
|
5597
5721
|
);
|
|
@@ -6144,9 +6268,10 @@ function validateRetainedGenerationFrame(
|
|
|
6144
6268
|
'roleBindings',
|
|
6145
6269
|
'runtime',
|
|
6146
6270
|
],
|
|
6147
|
-
['parentSessionId', 'parentCallId', 'request', 'inputs'],
|
|
6271
|
+
['parentSessionId', 'parentCallId', 'request', 'inputs', 'attachments'],
|
|
6148
6272
|
path,
|
|
6149
6273
|
);
|
|
6274
|
+
if (frame.attachments !== undefined) validateAssetRefs(frame.attachments);
|
|
6150
6275
|
if (frame.request !== undefined) {
|
|
6151
6276
|
if (index !== 0) throw new Error(`${path}.request belongs only to the root`);
|
|
6152
6277
|
requireNonblank(frame.request, `${path}.request`);
|
|
@@ -6369,8 +6494,9 @@ async function requireUncertainRecord(record, attemptId) {
|
|
|
6369
6494
|
return record;
|
|
6370
6495
|
}
|
|
6371
6496
|
|
|
6372
|
-
function assertAcceptedInput(value) {
|
|
6373
|
-
if (
|
|
6497
|
+
function assertAcceptedInput(value, attachments) {
|
|
6498
|
+
if (attachments !== undefined) validateAssetRefs(attachments);
|
|
6499
|
+
if (typeof value !== 'string' || (value.trim().length === 0 && !attachments?.length)) {
|
|
6374
6500
|
throw new Error('Captain session input must be a non-empty string');
|
|
6375
6501
|
}
|
|
6376
6502
|
}
|
|
@@ -3,7 +3,11 @@ import type { Effort, PermissionPolicy } from '@sublang/cligent';
|
|
|
3
3
|
import type { JsonValue, PlaybookEffectLedger, PlaybookEffectLedgerCommandBatch, PlaybookControlAction, PlaybookFailureCause, PlaybookRuntime, PlaybookRuntimeSnapshot, PlaybookStepRecord } from '@sublang/playbook/runtime';
|
|
4
4
|
import { type CaptainControllerPort } from '../captain.playbook/captain.playbook.js';
|
|
5
5
|
import type { PlaybookSummaryPolicy } from './code.registry.js';
|
|
6
|
+
import type { Attachment } from '@sublang/cligent';
|
|
7
|
+
import type { SessionAssetRef } from './session-assets.js';
|
|
6
8
|
interface SessionAgent {
|
|
9
|
+
/** Mutable next-turn execution capability; never structural identity. */
|
|
10
|
+
readonly browser?: boolean;
|
|
7
11
|
readonly adapter: string;
|
|
8
12
|
readonly model: TuningSelection;
|
|
9
13
|
readonly effort: TuningSelection<Effort>;
|
|
@@ -40,7 +44,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
|
|
|
40
44
|
readonly rootPlaybookId: string;
|
|
41
45
|
readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
|
|
42
46
|
}
|
|
43
|
-
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey>>;
|
|
47
|
+
type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey | 'browser'>>;
|
|
44
48
|
type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
|
|
45
49
|
export interface ProgressChange {
|
|
46
50
|
snapshot?: PlaybookCaptainShellSnapshot | null;
|
|
@@ -48,6 +52,10 @@ export interface ProgressChange {
|
|
|
48
52
|
kind: PlaybookStepRecord['kind'] | 'preparation' | 'completion' | 'answer';
|
|
49
53
|
runtimeSessionId: string;
|
|
50
54
|
playbookId: string;
|
|
55
|
+
workerEvidence?: {
|
|
56
|
+
boundaryId: string;
|
|
57
|
+
playerId: string;
|
|
58
|
+
};
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
export interface InterruptedReport {
|
|
@@ -57,6 +65,9 @@ export interface InterruptedReport {
|
|
|
57
65
|
unresolvedEffects?: readonly PlaybookCaptainUnresolvedEffect[];
|
|
58
66
|
}
|
|
59
67
|
export interface PlaybookCaptainDeps {
|
|
68
|
+
onTurnAttachments?: (references: readonly SessionAssetRef[]) => void;
|
|
69
|
+
beginPreparationMedia?: (runtimeSessionId: string) => () => void;
|
|
70
|
+
resolveAttachments?: (references: readonly SessionAssetRef[], signal: AbortSignal) => Promise<readonly Attachment[]>;
|
|
60
71
|
/** Stop the host's active turn, including admitted tool calls, on preparation expiry or a required save failure. */
|
|
61
72
|
abortPreparation?: (reason?: string) => void;
|
|
62
73
|
recordProgress?: (change: ProgressChange) => Promise<void>;
|
|
@@ -164,6 +175,7 @@ export interface PlaybookCaptainFrameSnapshot {
|
|
|
164
175
|
readonly parentCallId?: string;
|
|
165
176
|
readonly request?: string;
|
|
166
177
|
readonly inputs?: readonly string[];
|
|
178
|
+
readonly attachments?: readonly SessionAssetRef[];
|
|
167
179
|
readonly options: JsonValue;
|
|
168
180
|
readonly roleBindings: Readonly<Record<string, string>>;
|
|
169
181
|
readonly runtime: DeepReadonly<PlaybookRuntimeSnapshot>;
|
|
@@ -186,6 +198,7 @@ interface PlaybookCaptainShellSnapshotFields {
|
|
|
186
198
|
readonly journal: number;
|
|
187
199
|
};
|
|
188
200
|
readonly journal: readonly PlaybookCaptainJournalRecord[];
|
|
201
|
+
readonly pendingAttachments?: readonly SessionAssetRef[];
|
|
189
202
|
readonly lastAction?: 'respond' | 'start' | 'switch' | 'resume' | 'dismiss' | 'deliver' | 'runtime' | 'recover';
|
|
190
203
|
readonly lastSettlementStatus?: 'ok' | 'rejected' | 'failed';
|
|
191
204
|
}
|
|
@@ -241,6 +254,9 @@ export interface PlaybookCaptainSettlement {
|
|
|
241
254
|
}
|
|
242
255
|
/** tmux and headless front ends share this one durable Captain shell API. */
|
|
243
256
|
export interface PlaybookCaptainShell extends Captain {
|
|
257
|
+
/** Bind already-owned evidence to the next visible Boss turn without changing its text. */
|
|
258
|
+
setTurnAttachments(references: readonly SessionAssetRef[]): void;
|
|
259
|
+
recordMediaEvidence(reference: SessionAssetRef, runtimeSessionId?: string): void;
|
|
244
260
|
installRetainedGenerations(generations: Readonly<Record<string, PlaybookCaptainRetainedGeneration>>): Promise<void>;
|
|
245
261
|
exportSnapshot(): PlaybookCaptainShellSnapshot | undefined;
|
|
246
262
|
exportSettlement(): PlaybookCaptainSettlement | undefined;
|