narraeon 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +35 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +66 -29
- package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +81 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
- package/dist/node/runtime/play/PlayCallChain.js +904 -369
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
- package/dist/node/server/createServer.js +10 -2
- package/dist/web/assets/index-D5VqDvjY.js +1192 -0
- package/dist/web/assets/index-Xa0s35CJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { cloneFilePhysically } from "../FileNativePhysicalClone.js";
|
|
6
|
+
export class FileNativePlayTimelineStore {
|
|
7
|
+
#worldsRoot;
|
|
8
|
+
constructor(dataRoot) {
|
|
9
|
+
this.#worldsRoot = join(resolve(dataRoot), "worlds-file-native");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Convert the cumulative call-chain record written by every released
|
|
13
|
+
* Narraeon version through v0.1.1 into immutable context segments. The
|
|
14
|
+
* source digest makes interrupted retries reproduce the same generation and
|
|
15
|
+
* identities; callers publish the world-wide storage marker only after this
|
|
16
|
+
* method has completed.
|
|
17
|
+
*/
|
|
18
|
+
async migrateReleasedRecord(worldId, value, sourceDigest) {
|
|
19
|
+
assertIdentity(worldId, "World ID");
|
|
20
|
+
if (!/^[a-f0-9]{64}$/u.test(sourceDigest))
|
|
21
|
+
throw new TypeError("Released call-chain source digest is invalid");
|
|
22
|
+
assertReleasedPersistedPlayCallChain(value, worldId);
|
|
23
|
+
const contexts = [
|
|
24
|
+
...(value.previousContexts ?? []).map((context) => structuredClone(context)),
|
|
25
|
+
releasedCurrentContext(value),
|
|
26
|
+
];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const generation = `released:${sourceDigest}`;
|
|
29
|
+
let previousChainId = null;
|
|
30
|
+
for (const context of contexts) {
|
|
31
|
+
assertIdentity(context.chainId, "Call-chain ID");
|
|
32
|
+
if (seen.has(context.chainId))
|
|
33
|
+
throw new Error("Released play-call-chain contexts contain a duplicate identity");
|
|
34
|
+
seen.add(context.chainId);
|
|
35
|
+
await this.persist({
|
|
36
|
+
...structuredClone(context),
|
|
37
|
+
events: context.events.map(normalizePlayEvent),
|
|
38
|
+
schemaVersion: 3,
|
|
39
|
+
kind: "play_call_chain",
|
|
40
|
+
worldId,
|
|
41
|
+
previousChainId,
|
|
42
|
+
timelineGeneration: generation,
|
|
43
|
+
documentAuthorizationCheckpoints: structuredClone(context.documentAuthorizationCheckpoints ?? []),
|
|
44
|
+
});
|
|
45
|
+
previousChainId = context.chainId;
|
|
46
|
+
}
|
|
47
|
+
const migrated = await this.readAllContexts(worldId);
|
|
48
|
+
if (migrated.length !== contexts.length ||
|
|
49
|
+
migrated.some((context, index) => context.chainId !== contexts[index]?.chainId))
|
|
50
|
+
throw new Error("Released play-call-chain migration did not reproduce its context chain");
|
|
51
|
+
}
|
|
52
|
+
async persist(value, cursor) {
|
|
53
|
+
return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor);
|
|
54
|
+
}
|
|
55
|
+
async persistStaged(targetWorldRoot, value, cursor) {
|
|
56
|
+
return this.#persistAtRuntimeRoot(join(resolve(targetWorldRoot), "runtime"), value, cursor);
|
|
57
|
+
}
|
|
58
|
+
async cloneContextPrefixToStaging(input) {
|
|
59
|
+
assertIdentity(input.sourceWorldId, "Source world ID");
|
|
60
|
+
assertIdentity(input.source.chainId, "Source call-chain ID");
|
|
61
|
+
const sourceRoot = this.#contextRoot(input.sourceWorldId, input.source.chainId);
|
|
62
|
+
const targetRuntime = join(resolve(input.targetWorldRoot), "runtime");
|
|
63
|
+
const targetRoot = join(targetRuntime, "play-contexts", createHash("sha256").update(input.target.chainId).digest("hex"));
|
|
64
|
+
const transcriptCount = prefixLength(input.source.transcript, input.target.transcript);
|
|
65
|
+
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
66
|
+
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
67
|
+
const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
|
|
68
|
+
await Promise.all([
|
|
69
|
+
cloneNumberedPrefix({
|
|
70
|
+
sourceRoot,
|
|
71
|
+
targetRoot,
|
|
72
|
+
directory: "transcript",
|
|
73
|
+
count: transcriptCount,
|
|
74
|
+
immutable: true,
|
|
75
|
+
}),
|
|
76
|
+
cloneNumberedPrefix({
|
|
77
|
+
sourceRoot,
|
|
78
|
+
targetRoot,
|
|
79
|
+
directory: "events",
|
|
80
|
+
count: eventCount,
|
|
81
|
+
immutable: false,
|
|
82
|
+
}),
|
|
83
|
+
cloneNumberedPrefix({
|
|
84
|
+
sourceRoot,
|
|
85
|
+
targetRoot,
|
|
86
|
+
directory: "summaries",
|
|
87
|
+
count: eventCount,
|
|
88
|
+
immutable: false,
|
|
89
|
+
}),
|
|
90
|
+
cloneNumberedPrefix({
|
|
91
|
+
sourceRoot,
|
|
92
|
+
targetRoot,
|
|
93
|
+
directory: "completed-tools",
|
|
94
|
+
count: completedToolCount,
|
|
95
|
+
immutable: true,
|
|
96
|
+
}),
|
|
97
|
+
cloneNumberedPrefix({
|
|
98
|
+
sourceRoot,
|
|
99
|
+
targetRoot,
|
|
100
|
+
directory: "authorization",
|
|
101
|
+
count: authorizationCheckpointCount,
|
|
102
|
+
immutable: true,
|
|
103
|
+
}),
|
|
104
|
+
]);
|
|
105
|
+
return this.persistStaged(input.targetWorldRoot, input.target, {
|
|
106
|
+
transcriptCount,
|
|
107
|
+
eventCount,
|
|
108
|
+
completedToolCount,
|
|
109
|
+
authorizationCheckpointCount,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async #persistAtRuntimeRoot(runtimeRoot, value, cursor) {
|
|
113
|
+
assertIdentity(value.worldId, "World ID");
|
|
114
|
+
assertIdentity(value.chainId, "Call-chain ID");
|
|
115
|
+
const root = join(runtimeRoot, "play-contexts", createHash("sha256").update(value.chainId).digest("hex"));
|
|
116
|
+
const index = contextIndex(value);
|
|
117
|
+
const existingIndex = await readOptionalJson(join(root, "index.json"));
|
|
118
|
+
if (existingIndex === null)
|
|
119
|
+
await publishImmutableJson(join(root, "index.json"), index);
|
|
120
|
+
else if (!isDeepStrictEqual(existingIndex, index))
|
|
121
|
+
throw new Error("A play context identity is bound to a different index");
|
|
122
|
+
const base = contextBase(value);
|
|
123
|
+
const existingBase = await readOptionalJson(join(root, "base.json"));
|
|
124
|
+
if (existingBase === null)
|
|
125
|
+
await publishImmutableJson(join(root, "base.json"), base);
|
|
126
|
+
else if (!isDeepStrictEqual(existingBase, base))
|
|
127
|
+
throw new Error("A play context identity is bound to different frozen data");
|
|
128
|
+
const supplied = cursor ?? emptyPersistenceCursor();
|
|
129
|
+
// Recovery may replace the unresolved tail (for example, a generic crash
|
|
130
|
+
// failure becomes the already-returned Provider response). Numbered files
|
|
131
|
+
// beyond the new state counts are unreachable projection residue.
|
|
132
|
+
const previous = {
|
|
133
|
+
eventCount: Math.min(supplied.eventCount, value.events.length),
|
|
134
|
+
transcriptCount: Math.min(supplied.transcriptCount, value.transcript.length),
|
|
135
|
+
completedToolCount: Math.min(supplied.completedToolCount, value.completedTools.length),
|
|
136
|
+
authorizationCheckpointCount: Math.min(supplied.authorizationCheckpointCount, value.documentAuthorizationCheckpoints?.length ?? 0),
|
|
137
|
+
};
|
|
138
|
+
const lastRequestDigest = value.lastRequest === null
|
|
139
|
+
? null
|
|
140
|
+
: await this.#publishRequest(runtimeRoot, value.lastRequest);
|
|
141
|
+
const continuation = {
|
|
142
|
+
schemaVersion: 3,
|
|
143
|
+
kind: "play_context_continuation",
|
|
144
|
+
chainId: value.chainId,
|
|
145
|
+
nextMaterials: structuredClone(value.nextMaterials),
|
|
146
|
+
lastRequestDigest,
|
|
147
|
+
};
|
|
148
|
+
const continuationDigest = createHash("sha256")
|
|
149
|
+
.update(JSON.stringify(continuation))
|
|
150
|
+
.digest("hex");
|
|
151
|
+
await publishImmutableJson(join(root, "continuations", `${continuationDigest}.json`), continuation);
|
|
152
|
+
for (let index = previous.transcriptCount; index < value.transcript.length; index += 1)
|
|
153
|
+
await publishImmutableJson(join(root, "transcript", numbered(index + 1)), value.transcript[index]);
|
|
154
|
+
const eventStart = previous.eventCount === 0 ? 0 : previous.eventCount - 1;
|
|
155
|
+
for (let index = eventStart; index < value.events.length; index += 1) {
|
|
156
|
+
const event = value.events[index];
|
|
157
|
+
if (event.id !== index + 1)
|
|
158
|
+
throw new Error("Play call-chain event IDs must be contiguous");
|
|
159
|
+
await publishJson(join(root, "events", numbered(event.id)), event);
|
|
160
|
+
await publishJson(join(root, "summaries", numbered(event.id)), summarizeEvent(event));
|
|
161
|
+
}
|
|
162
|
+
for (let index = previous.completedToolCount; index < value.completedTools.length; index += 1)
|
|
163
|
+
await publishImmutableJson(join(root, "completed-tools", numbered(index + 1)), value.completedTools[index]);
|
|
164
|
+
const checkpoints = value.documentAuthorizationCheckpoints ?? [];
|
|
165
|
+
for (let index = previous.authorizationCheckpointCount; index < checkpoints.length; index += 1)
|
|
166
|
+
await publishImmutableJson(join(root, "authorization", numbered(index + 1)), checkpoints[index]);
|
|
167
|
+
const state = {
|
|
168
|
+
schemaVersion: 3,
|
|
169
|
+
chainId: value.chainId,
|
|
170
|
+
parentHead: value.parentHead,
|
|
171
|
+
status: value.status,
|
|
172
|
+
canRetry: value.canRetry,
|
|
173
|
+
eventCount: value.events.length,
|
|
174
|
+
transcriptCount: value.transcript.length,
|
|
175
|
+
completedToolCount: value.completedTools.length,
|
|
176
|
+
authorizationCheckpointCount: checkpoints.length,
|
|
177
|
+
changedDocuments: structuredClone(value.changedDocuments),
|
|
178
|
+
nextEventId: value.nextEventId,
|
|
179
|
+
exchange: value.exchange,
|
|
180
|
+
continuationDigest,
|
|
181
|
+
lastRequestAttempt: value.lastRequestAttempt,
|
|
182
|
+
lastFailure: value.lastFailure,
|
|
183
|
+
updatedAt: value.updatedAt,
|
|
184
|
+
};
|
|
185
|
+
await publishJson(join(root, "state.json"), state);
|
|
186
|
+
await publishJson(join(runtimeRoot, "play-timeline-head.json"), {
|
|
187
|
+
schemaVersion: 3,
|
|
188
|
+
worldId: value.worldId,
|
|
189
|
+
chainId: value.chainId,
|
|
190
|
+
generation: value.timelineGeneration,
|
|
191
|
+
});
|
|
192
|
+
return {
|
|
193
|
+
eventCount: value.events.length,
|
|
194
|
+
transcriptCount: value.transcript.length,
|
|
195
|
+
completedToolCount: value.completedTools.length,
|
|
196
|
+
authorizationCheckpointCount: checkpoints.length,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async readCurrent(worldId) {
|
|
200
|
+
assertIdentity(worldId, "World ID");
|
|
201
|
+
const head = await this.#readTimelineHead(worldId);
|
|
202
|
+
if (head === null)
|
|
203
|
+
return null;
|
|
204
|
+
const loaded = await this.readContext(worldId, head.chainId);
|
|
205
|
+
if (loaded !== null && loaded.value.timelineGeneration !== head.generation)
|
|
206
|
+
throw new Error("Play timeline head generation is inconsistent");
|
|
207
|
+
return loaded;
|
|
208
|
+
}
|
|
209
|
+
async readContext(worldId, chainId) {
|
|
210
|
+
assertIdentity(worldId, "World ID");
|
|
211
|
+
assertIdentity(chainId, "Call-chain ID");
|
|
212
|
+
const metadata = await this.#readContextMetadata(worldId, chainId);
|
|
213
|
+
if (metadata === null)
|
|
214
|
+
return null;
|
|
215
|
+
const { index: indexValue, state: stateValue, root } = metadata;
|
|
216
|
+
const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints,] = await Promise.all([
|
|
217
|
+
readJson(join(root, "base.json")),
|
|
218
|
+
readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
|
|
219
|
+
readNumbered(join(root, "transcript"), stateValue.transcriptCount),
|
|
220
|
+
readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
|
|
221
|
+
readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
|
|
222
|
+
readNumbered(join(root, "authorization"), stateValue.authorizationCheckpointCount),
|
|
223
|
+
]);
|
|
224
|
+
const events = rawEvents.map(normalizePlayEvent);
|
|
225
|
+
assertContextBase(baseValue);
|
|
226
|
+
assertContextContinuation(continuationValue);
|
|
227
|
+
if (baseValue.worldId !== worldId ||
|
|
228
|
+
baseValue.chainId !== chainId ||
|
|
229
|
+
continuationValue.chainId !== chainId ||
|
|
230
|
+
createHash("sha256")
|
|
231
|
+
.update(JSON.stringify(continuationValue))
|
|
232
|
+
.digest("hex") !== stateValue.continuationDigest)
|
|
233
|
+
throw new Error("Play context durable identity is inconsistent");
|
|
234
|
+
const lastRequest = continuationValue.lastRequestDigest === null
|
|
235
|
+
? null
|
|
236
|
+
: await readJson(join(this.#worldRuntimeRoot(worldId), "play-requests", `${continuationValue.lastRequestDigest}.json`));
|
|
237
|
+
if (lastRequest !== null &&
|
|
238
|
+
(!isRecord(lastRequest) ||
|
|
239
|
+
createHash("sha256")
|
|
240
|
+
.update(JSON.stringify(lastRequest))
|
|
241
|
+
.digest("hex") !== continuationValue.lastRequestDigest))
|
|
242
|
+
throw new Error("Frozen play request does not match its durable identity");
|
|
243
|
+
const value = {
|
|
244
|
+
schemaVersion: 3,
|
|
245
|
+
kind: "play_call_chain",
|
|
246
|
+
worldId,
|
|
247
|
+
chainId,
|
|
248
|
+
previousChainId: indexValue.previousChainId,
|
|
249
|
+
timelineGeneration: indexValue.timelineGeneration,
|
|
250
|
+
baselineHead: baseValue.baselineHead,
|
|
251
|
+
parentHead: stateValue.parentHead,
|
|
252
|
+
...(baseValue.baselineHistoryLength === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: { baselineHistoryLength: baseValue.baselineHistoryLength }),
|
|
255
|
+
...(baseValue.derivedFrom === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { derivedFrom: structuredClone(baseValue.derivedFrom) }),
|
|
258
|
+
...(baseValue.branchedBeforePlayer === undefined
|
|
259
|
+
? {}
|
|
260
|
+
: {
|
|
261
|
+
branchedBeforePlayer: structuredClone(baseValue.branchedBeforePlayer),
|
|
262
|
+
}),
|
|
263
|
+
playPreset: structuredClone(indexValue.playPreset),
|
|
264
|
+
...(baseValue.followups === undefined
|
|
265
|
+
? {}
|
|
266
|
+
: { followups: structuredClone(baseValue.followups) }),
|
|
267
|
+
...(baseValue.playPresetScriptsEnabled === undefined
|
|
268
|
+
? {}
|
|
269
|
+
: { playPresetScriptsEnabled: baseValue.playPresetScriptsEnabled }),
|
|
270
|
+
...(baseValue.modelBinding === undefined
|
|
271
|
+
? {}
|
|
272
|
+
: { modelBinding: structuredClone(baseValue.modelBinding) }),
|
|
273
|
+
status: stateValue.status,
|
|
274
|
+
canRetry: stateValue.canRetry,
|
|
275
|
+
bootstrap: structuredClone(baseValue.bootstrap),
|
|
276
|
+
tools: structuredClone(baseValue.tools),
|
|
277
|
+
transcript,
|
|
278
|
+
events,
|
|
279
|
+
completedTools,
|
|
280
|
+
documentAuthorizationCheckpoints: checkpoints,
|
|
281
|
+
changedDocuments: structuredClone(stateValue.changedDocuments),
|
|
282
|
+
nextMaterials: structuredClone(continuationValue.nextMaterials),
|
|
283
|
+
nextEventId: stateValue.nextEventId,
|
|
284
|
+
exchange: stateValue.exchange,
|
|
285
|
+
lastRequest,
|
|
286
|
+
lastRequestAttempt: stateValue.lastRequestAttempt,
|
|
287
|
+
lastFailure: stateValue.lastFailure,
|
|
288
|
+
updatedAt: stateValue.updatedAt,
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
value,
|
|
292
|
+
cursor: {
|
|
293
|
+
eventCount: stateValue.eventCount,
|
|
294
|
+
transcriptCount: stateValue.transcriptCount,
|
|
295
|
+
completedToolCount: stateValue.completedToolCount,
|
|
296
|
+
authorizationCheckpointCount: stateValue.authorizationCheckpointCount,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async readAllContexts(worldId) {
|
|
301
|
+
const current = await this.readCurrent(worldId);
|
|
302
|
+
if (current === null)
|
|
303
|
+
return [];
|
|
304
|
+
const reverse = [current.value];
|
|
305
|
+
let previous = current.value.previousChainId;
|
|
306
|
+
while (previous !== null) {
|
|
307
|
+
const loaded = await this.readContext(worldId, previous);
|
|
308
|
+
if (loaded === null)
|
|
309
|
+
throw new Error("Play timeline points to a missing context");
|
|
310
|
+
reverse.push(loaded.value);
|
|
311
|
+
previous = loaded.value.previousChainId;
|
|
312
|
+
}
|
|
313
|
+
return reverse.reverse();
|
|
314
|
+
}
|
|
315
|
+
async readPage(worldId, limit, cursor) {
|
|
316
|
+
assertIdentity(worldId, "World ID");
|
|
317
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
318
|
+
throw new TypeError("Play timeline page limit must be between 1 and 100");
|
|
319
|
+
const head = await this.#readTimelineHead(worldId);
|
|
320
|
+
const generation = head?.generation ?? `genesis:${worldId}`;
|
|
321
|
+
let pointer = cursor === undefined
|
|
322
|
+
? await this.#initialCursor(worldId, generation, head?.chainId ?? null)
|
|
323
|
+
: decodeCursor(cursor);
|
|
324
|
+
if (pointer.generation !== generation)
|
|
325
|
+
throw new Error("Play timeline cursor belongs to a different timeline revision");
|
|
326
|
+
const reverse = [];
|
|
327
|
+
const metadata = new Map();
|
|
328
|
+
const readMetadata = async (chainId) => {
|
|
329
|
+
if (metadata.has(chainId))
|
|
330
|
+
return metadata.get(chainId) ?? null;
|
|
331
|
+
const loaded = await this.#readContextMetadata(worldId, chainId);
|
|
332
|
+
metadata.set(chainId, loaded);
|
|
333
|
+
return loaded;
|
|
334
|
+
};
|
|
335
|
+
while (reverse.length < limit && pointer.chainId !== null) {
|
|
336
|
+
const context = await readMetadata(pointer.chainId);
|
|
337
|
+
if (context === null)
|
|
338
|
+
throw new Error("Play timeline cursor points to a missing context");
|
|
339
|
+
if (pointer.eventId > 0) {
|
|
340
|
+
const summary = await readJson(join(this.#contextRoot(worldId, pointer.chainId), "summaries", numbered(pointer.eventId)));
|
|
341
|
+
assertTimelineEventSummary(summary);
|
|
342
|
+
if (summary.id !== pointer.eventId)
|
|
343
|
+
throw new Error("Play timeline summary identity is inconsistent");
|
|
344
|
+
reverse.push({
|
|
345
|
+
kind: "event",
|
|
346
|
+
chainId: pointer.chainId,
|
|
347
|
+
current: pointer.chainId === head?.chainId,
|
|
348
|
+
event: summary,
|
|
349
|
+
});
|
|
350
|
+
pointer = { ...pointer, eventId: pointer.eventId - 1 };
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (pointer.boundaryPending) {
|
|
354
|
+
reverse.push({
|
|
355
|
+
kind: "context_boundary",
|
|
356
|
+
chainId: pointer.chainId,
|
|
357
|
+
playPreset: structuredClone(context.index.playPreset),
|
|
358
|
+
changedDocuments: structuredClone(context.state.changedDocuments),
|
|
359
|
+
current: pointer.chainId === head?.chainId,
|
|
360
|
+
});
|
|
361
|
+
pointer = { ...pointer, boundaryPending: false };
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (context.index.previousChainId !== null) {
|
|
365
|
+
const previous = await readMetadata(context.index.previousChainId);
|
|
366
|
+
if (previous === null)
|
|
367
|
+
throw new Error("Play timeline points to a missing previous context");
|
|
368
|
+
pointer = {
|
|
369
|
+
...pointer,
|
|
370
|
+
chainId: context.index.previousChainId,
|
|
371
|
+
eventId: previous.state.eventCount,
|
|
372
|
+
boundaryPending: true,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
pointer = { ...pointer, chainId: null };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (reverse.length < limit &&
|
|
380
|
+
pointer.chainId === null &&
|
|
381
|
+
pointer.genesisIndex >= 0) {
|
|
382
|
+
const genesis = await this.#readGenesisMessages(worldId);
|
|
383
|
+
while (reverse.length < limit && pointer.genesisIndex >= 0) {
|
|
384
|
+
const message = genesis[pointer.genesisIndex];
|
|
385
|
+
if (message === undefined)
|
|
386
|
+
throw new Error("Play timeline genesis cursor is invalid");
|
|
387
|
+
reverse.push({ kind: "genesis", ...structuredClone(message) });
|
|
388
|
+
pointer = { ...pointer, genesisIndex: pointer.genesisIndex - 1 };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const active = head === null
|
|
392
|
+
? null
|
|
393
|
+
: await this.#readContextMetadata(worldId, head.chainId);
|
|
394
|
+
return {
|
|
395
|
+
worldId,
|
|
396
|
+
generation,
|
|
397
|
+
activeChainId: head?.chainId ?? null,
|
|
398
|
+
activeStatus: active?.state.status ?? null,
|
|
399
|
+
activeCanRetry: active?.state.canRetry ?? false,
|
|
400
|
+
activeLastFailure: active?.state.lastFailure ?? null,
|
|
401
|
+
items: reverse.reverse(),
|
|
402
|
+
nextCursor: pointer.chainId === null && pointer.genesisIndex < 0
|
|
403
|
+
? null
|
|
404
|
+
: encodeCursor(pointer),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async readDetail(worldId, chainId, eventId) {
|
|
408
|
+
assertIdentity(worldId, "World ID");
|
|
409
|
+
assertIdentity(chainId, "Call-chain ID");
|
|
410
|
+
if (!Number.isSafeInteger(eventId) || eventId < 1)
|
|
411
|
+
throw new TypeError("Play timeline event ID is invalid");
|
|
412
|
+
const metadata = await this.#readContextMetadata(worldId, chainId);
|
|
413
|
+
if (metadata === null || eventId > metadata.state.eventCount)
|
|
414
|
+
throw new Error("Play timeline event is outside the current context");
|
|
415
|
+
const event = await readJson(join(this.#contextRoot(worldId, chainId), "events", numbered(eventId)));
|
|
416
|
+
assertPlayEvent(event);
|
|
417
|
+
if (event.id !== eventId)
|
|
418
|
+
throw new Error("Play timeline event identity is inconsistent");
|
|
419
|
+
return normalizePlayEvent(event);
|
|
420
|
+
}
|
|
421
|
+
newGeneration() {
|
|
422
|
+
return `timeline:${randomUUID()}`;
|
|
423
|
+
}
|
|
424
|
+
#worldRuntimeRoot(worldId) {
|
|
425
|
+
return join(this.#worldsRoot, worldId, "runtime");
|
|
426
|
+
}
|
|
427
|
+
#contextRoot(worldId, chainId) {
|
|
428
|
+
return join(this.#worldRuntimeRoot(worldId), "play-contexts", createHash("sha256").update(chainId).digest("hex"));
|
|
429
|
+
}
|
|
430
|
+
#timelineHeadPath(worldId) {
|
|
431
|
+
return join(this.#worldRuntimeRoot(worldId), "play-timeline-head.json");
|
|
432
|
+
}
|
|
433
|
+
async #readTimelineHead(worldId) {
|
|
434
|
+
const value = await readOptionalJson(this.#timelineHeadPath(worldId));
|
|
435
|
+
if (value === null)
|
|
436
|
+
return null;
|
|
437
|
+
assertTimelineHead(value);
|
|
438
|
+
if (value.worldId !== worldId)
|
|
439
|
+
throw new Error("Play timeline head belongs to a different world");
|
|
440
|
+
assertIdentity(value.chainId, "Call-chain ID");
|
|
441
|
+
return value;
|
|
442
|
+
}
|
|
443
|
+
async #publishRequest(runtimeRoot, request) {
|
|
444
|
+
const digest = createHash("sha256")
|
|
445
|
+
.update(JSON.stringify(request))
|
|
446
|
+
.digest("hex");
|
|
447
|
+
await publishImmutableJson(join(runtimeRoot, "play-requests", `${digest}.json`), request);
|
|
448
|
+
return digest;
|
|
449
|
+
}
|
|
450
|
+
async #initialCursor(worldId, generation, chainId) {
|
|
451
|
+
const genesis = await this.#readGenesisMessages(worldId);
|
|
452
|
+
if (chainId === null)
|
|
453
|
+
return {
|
|
454
|
+
generation,
|
|
455
|
+
chainId: null,
|
|
456
|
+
eventId: 0,
|
|
457
|
+
boundaryPending: false,
|
|
458
|
+
genesisIndex: genesis.length - 1,
|
|
459
|
+
};
|
|
460
|
+
const current = await this.#readContextMetadata(worldId, chainId);
|
|
461
|
+
if (current === null)
|
|
462
|
+
throw new Error("Play timeline head points to a missing context");
|
|
463
|
+
if (current.index.timelineGeneration !== generation)
|
|
464
|
+
throw new Error("Play timeline head generation is inconsistent");
|
|
465
|
+
return {
|
|
466
|
+
generation,
|
|
467
|
+
chainId,
|
|
468
|
+
eventId: current.state.eventCount,
|
|
469
|
+
boundaryPending: true,
|
|
470
|
+
genesisIndex: genesis.length - 1,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async #readGenesisMessages(worldId) {
|
|
474
|
+
const value = await readJson(join(this.#worldRuntimeRoot(worldId), "play-genesis-timeline.json"));
|
|
475
|
+
if (!isRecord(value) ||
|
|
476
|
+
value.schemaVersion !== 1 ||
|
|
477
|
+
value.worldId !== worldId ||
|
|
478
|
+
!Array.isArray(value.history) ||
|
|
479
|
+
!value.history.every((message) => isRecord(message) &&
|
|
480
|
+
typeof message.messageId === "string" &&
|
|
481
|
+
(message.role === "player" || message.role === "narrator") &&
|
|
482
|
+
typeof message.exactText === "string"))
|
|
483
|
+
throw new Error("World genesis timeline projection is invalid");
|
|
484
|
+
return structuredClone(value.history);
|
|
485
|
+
}
|
|
486
|
+
async #readContextMetadata(worldId, chainId) {
|
|
487
|
+
const root = this.#contextRoot(worldId, chainId);
|
|
488
|
+
const [indexValue, stateValue] = await Promise.all([
|
|
489
|
+
readOptionalJson(join(root, "index.json")),
|
|
490
|
+
readOptionalJson(join(root, "state.json")),
|
|
491
|
+
]);
|
|
492
|
+
if (indexValue === null && stateValue === null)
|
|
493
|
+
return null;
|
|
494
|
+
assertContextIndex(indexValue);
|
|
495
|
+
assertContextState(stateValue);
|
|
496
|
+
if (indexValue.worldId !== worldId ||
|
|
497
|
+
indexValue.chainId !== chainId ||
|
|
498
|
+
stateValue.chainId !== chainId)
|
|
499
|
+
throw new Error("Play context durable identity is inconsistent");
|
|
500
|
+
return { root, index: indexValue, state: stateValue };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function contextIndex(value) {
|
|
504
|
+
return {
|
|
505
|
+
schemaVersion: 3,
|
|
506
|
+
kind: "play_context_index",
|
|
507
|
+
worldId: value.worldId,
|
|
508
|
+
chainId: value.chainId,
|
|
509
|
+
previousChainId: value.previousChainId,
|
|
510
|
+
timelineGeneration: value.timelineGeneration,
|
|
511
|
+
playPreset: structuredClone(value.playPreset),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function contextBase(value) {
|
|
515
|
+
return {
|
|
516
|
+
schemaVersion: 3,
|
|
517
|
+
kind: "play_context_frozen",
|
|
518
|
+
worldId: value.worldId,
|
|
519
|
+
chainId: value.chainId,
|
|
520
|
+
baselineHead: value.baselineHead,
|
|
521
|
+
...(value.baselineHistoryLength === undefined
|
|
522
|
+
? {}
|
|
523
|
+
: { baselineHistoryLength: value.baselineHistoryLength }),
|
|
524
|
+
...(value.derivedFrom === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { derivedFrom: structuredClone(value.derivedFrom) }),
|
|
527
|
+
...(value.branchedBeforePlayer === undefined
|
|
528
|
+
? {}
|
|
529
|
+
: { branchedBeforePlayer: structuredClone(value.branchedBeforePlayer) }),
|
|
530
|
+
...(value.followups === undefined
|
|
531
|
+
? {}
|
|
532
|
+
: { followups: structuredClone(value.followups) }),
|
|
533
|
+
...(value.playPresetScriptsEnabled === undefined
|
|
534
|
+
? {}
|
|
535
|
+
: { playPresetScriptsEnabled: value.playPresetScriptsEnabled }),
|
|
536
|
+
...(value.modelBinding === undefined
|
|
537
|
+
? {}
|
|
538
|
+
: { modelBinding: structuredClone(value.modelBinding) }),
|
|
539
|
+
bootstrap: structuredClone(value.bootstrap),
|
|
540
|
+
tools: structuredClone(value.tools),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function summarizeEvent(event) {
|
|
544
|
+
if (event.kind === "player")
|
|
545
|
+
return structuredClone(event);
|
|
546
|
+
if (event.kind === "assistant") {
|
|
547
|
+
const { reasoning, toolFragment, ...summary } = event;
|
|
548
|
+
return {
|
|
549
|
+
...structuredClone(summary),
|
|
550
|
+
hasReasoning: reasoning !== undefined && reasoning.length > 0,
|
|
551
|
+
hasToolFragment: toolFragment !== undefined && toolFragment.length > 0,
|
|
552
|
+
hasUsage: event.usage !== undefined,
|
|
553
|
+
detailsAvailable: (reasoning !== undefined && reasoning.length > 0) ||
|
|
554
|
+
(toolFragment !== undefined && toolFragment.length > 0) ||
|
|
555
|
+
event.usage !== undefined ||
|
|
556
|
+
event.stopReason !== undefined ||
|
|
557
|
+
event.continuation !== undefined,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (event.kind === "tool_call") {
|
|
561
|
+
const { arguments: _arguments, ...summary } = event;
|
|
562
|
+
void _arguments;
|
|
563
|
+
return { ...structuredClone(summary), detailsAvailable: true };
|
|
564
|
+
}
|
|
565
|
+
if (event.kind === "tool_result") {
|
|
566
|
+
const { markdown: _markdown, ...summary } = event;
|
|
567
|
+
void _markdown;
|
|
568
|
+
return { ...structuredClone(summary), detailsAvailable: true };
|
|
569
|
+
}
|
|
570
|
+
if (event.kind === "followup")
|
|
571
|
+
return {
|
|
572
|
+
id: event.id,
|
|
573
|
+
kind: "followup",
|
|
574
|
+
followupId: event.followupId,
|
|
575
|
+
displayName: event.displayName,
|
|
576
|
+
toolCallCount: event.toolCalls.length,
|
|
577
|
+
failed: event.failure !== undefined,
|
|
578
|
+
...(event.usage === undefined
|
|
579
|
+
? {}
|
|
580
|
+
: { usage: structuredClone(event.usage) }),
|
|
581
|
+
detailsAvailable: true,
|
|
582
|
+
};
|
|
583
|
+
return structuredClone(event);
|
|
584
|
+
}
|
|
585
|
+
function emptyPersistenceCursor() {
|
|
586
|
+
return {
|
|
587
|
+
eventCount: 0,
|
|
588
|
+
transcriptCount: 0,
|
|
589
|
+
completedToolCount: 0,
|
|
590
|
+
authorizationCheckpointCount: 0,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function numbered(index) {
|
|
594
|
+
return `${String(index).padStart(10, "0")}.json`;
|
|
595
|
+
}
|
|
596
|
+
function encodeCursor(cursor) {
|
|
597
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
598
|
+
}
|
|
599
|
+
function decodeCursor(value) {
|
|
600
|
+
try {
|
|
601
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
602
|
+
if (!isRecord(parsed) ||
|
|
603
|
+
typeof parsed.generation !== "string" ||
|
|
604
|
+
(parsed.chainId !== null && typeof parsed.chainId !== "string") ||
|
|
605
|
+
!Number.isSafeInteger(parsed.eventId) ||
|
|
606
|
+
Number(parsed.eventId) < 0 ||
|
|
607
|
+
typeof parsed.boundaryPending !== "boolean" ||
|
|
608
|
+
!Number.isSafeInteger(parsed.genesisIndex) ||
|
|
609
|
+
Number(parsed.genesisIndex) < -1)
|
|
610
|
+
throw new Error("invalid cursor");
|
|
611
|
+
return parsed;
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
throw new Error("Play timeline cursor is invalid", { cause: error });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function assertContextIndex(value) {
|
|
618
|
+
if (!isRecord(value) ||
|
|
619
|
+
value.schemaVersion !== 3 ||
|
|
620
|
+
value.kind !== "play_context_index" ||
|
|
621
|
+
typeof value.worldId !== "string" ||
|
|
622
|
+
typeof value.chainId !== "string" ||
|
|
623
|
+
(value.previousChainId !== null &&
|
|
624
|
+
typeof value.previousChainId !== "string") ||
|
|
625
|
+
typeof value.timelineGeneration !== "string" ||
|
|
626
|
+
!isRecord(value.playPreset))
|
|
627
|
+
throw new Error("Play context index has an invalid shape");
|
|
628
|
+
}
|
|
629
|
+
function assertContextBase(value) {
|
|
630
|
+
if (!isRecord(value) ||
|
|
631
|
+
value.schemaVersion !== 3 ||
|
|
632
|
+
value.kind !== "play_context_frozen" ||
|
|
633
|
+
typeof value.worldId !== "string" ||
|
|
634
|
+
typeof value.chainId !== "string" ||
|
|
635
|
+
typeof value.baselineHead !== "string" ||
|
|
636
|
+
(value.modelBinding !== undefined &&
|
|
637
|
+
!validModelHostBinding(value.modelBinding)) ||
|
|
638
|
+
!isRecord(value.bootstrap) ||
|
|
639
|
+
!Array.isArray(value.tools))
|
|
640
|
+
throw new Error("Play context frozen data has an invalid shape");
|
|
641
|
+
}
|
|
642
|
+
function assertContextState(value) {
|
|
643
|
+
if (!isRecord(value) ||
|
|
644
|
+
value.schemaVersion !== 3 ||
|
|
645
|
+
typeof value.chainId !== "string" ||
|
|
646
|
+
typeof value.parentHead !== "string" ||
|
|
647
|
+
(value.status !== "ready" &&
|
|
648
|
+
value.status !== "running" &&
|
|
649
|
+
value.status !== "interrupted") ||
|
|
650
|
+
typeof value.canRetry !== "boolean" ||
|
|
651
|
+
!validCount(value.eventCount) ||
|
|
652
|
+
!validCount(value.transcriptCount) ||
|
|
653
|
+
!validCount(value.completedToolCount) ||
|
|
654
|
+
!validCount(value.authorizationCheckpointCount) ||
|
|
655
|
+
!Array.isArray(value.changedDocuments) ||
|
|
656
|
+
!validCount(value.nextEventId) ||
|
|
657
|
+
!validCount(value.exchange) ||
|
|
658
|
+
typeof value.continuationDigest !== "string" ||
|
|
659
|
+
!/^[a-f0-9]{64}$/u.test(value.continuationDigest) ||
|
|
660
|
+
!validCount(value.lastRequestAttempt) ||
|
|
661
|
+
(value.lastFailure !== null && typeof value.lastFailure !== "string") ||
|
|
662
|
+
typeof value.updatedAt !== "number")
|
|
663
|
+
throw new Error("Play context state has an invalid shape");
|
|
664
|
+
}
|
|
665
|
+
function assertContextContinuation(value) {
|
|
666
|
+
if (!isRecord(value) ||
|
|
667
|
+
value.schemaVersion !== 3 ||
|
|
668
|
+
value.kind !== "play_context_continuation" ||
|
|
669
|
+
typeof value.chainId !== "string" ||
|
|
670
|
+
!Array.isArray(value.nextMaterials) ||
|
|
671
|
+
(value.lastRequestDigest !== null &&
|
|
672
|
+
(typeof value.lastRequestDigest !== "string" ||
|
|
673
|
+
!/^[a-f0-9]{64}$/u.test(value.lastRequestDigest))))
|
|
674
|
+
throw new Error("Play context continuation has an invalid shape");
|
|
675
|
+
}
|
|
676
|
+
function assertTimelineHead(value) {
|
|
677
|
+
if (!isRecord(value) ||
|
|
678
|
+
value.schemaVersion !== 3 ||
|
|
679
|
+
typeof value.worldId !== "string" ||
|
|
680
|
+
typeof value.chainId !== "string" ||
|
|
681
|
+
typeof value.generation !== "string")
|
|
682
|
+
throw new Error("Play timeline head has an invalid shape");
|
|
683
|
+
}
|
|
684
|
+
function assertReleasedPersistedPlayCallChain(value, worldId) {
|
|
685
|
+
if (!isRecord(value) ||
|
|
686
|
+
(value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
|
|
687
|
+
value.kind !== "play_call_chain" ||
|
|
688
|
+
value.worldId !== worldId ||
|
|
689
|
+
!isReleasedPlayContext(value) ||
|
|
690
|
+
(value.previousContexts !== undefined &&
|
|
691
|
+
(!Array.isArray(value.previousContexts) ||
|
|
692
|
+
value.previousContexts.some((context) => !isReleasedPlayContext(context)))))
|
|
693
|
+
throw new Error("Released play-call-chain durable data has an invalid shape");
|
|
694
|
+
}
|
|
695
|
+
function isReleasedPlayContext(value) {
|
|
696
|
+
if (!isRecord(value))
|
|
697
|
+
return false;
|
|
698
|
+
if (typeof value.chainId !== "string" ||
|
|
699
|
+
typeof value.baselineHead !== "string" ||
|
|
700
|
+
(value.baselineHistoryLength !== undefined &&
|
|
701
|
+
!validCount(value.baselineHistoryLength)) ||
|
|
702
|
+
typeof value.parentHead !== "string" ||
|
|
703
|
+
!validReleasedDerivedFrom(value.derivedFrom) ||
|
|
704
|
+
!validReleasedBranchedBeforePlayer(value.branchedBeforePlayer) ||
|
|
705
|
+
!isRecord(value.playPreset) ||
|
|
706
|
+
(value.followups !== undefined && !Array.isArray(value.followups)) ||
|
|
707
|
+
(value.playPresetScriptsEnabled !== undefined &&
|
|
708
|
+
typeof value.playPresetScriptsEnabled !== "boolean") ||
|
|
709
|
+
(value.modelBinding !== undefined &&
|
|
710
|
+
!validModelHostBinding(value.modelBinding)) ||
|
|
711
|
+
(value.status !== "ready" &&
|
|
712
|
+
value.status !== "running" &&
|
|
713
|
+
value.status !== "interrupted") ||
|
|
714
|
+
typeof value.canRetry !== "boolean" ||
|
|
715
|
+
!isRecord(value.bootstrap) ||
|
|
716
|
+
!Array.isArray(value.tools) ||
|
|
717
|
+
!Array.isArray(value.transcript) ||
|
|
718
|
+
!Array.isArray(value.events) ||
|
|
719
|
+
!Array.isArray(value.completedTools) ||
|
|
720
|
+
!Array.isArray(value.changedDocuments) ||
|
|
721
|
+
!Array.isArray(value.nextMaterials) ||
|
|
722
|
+
!validCount(value.nextEventId) ||
|
|
723
|
+
!validCount(value.exchange) ||
|
|
724
|
+
(value.lastRequest !== null && !isRecord(value.lastRequest)) ||
|
|
725
|
+
!validCount(value.lastRequestAttempt) ||
|
|
726
|
+
(value.lastFailure !== null && typeof value.lastFailure !== "string") ||
|
|
727
|
+
typeof value.updatedAt !== "number")
|
|
728
|
+
return false;
|
|
729
|
+
try {
|
|
730
|
+
for (const event of value.events)
|
|
731
|
+
assertPlayEvent(event);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
return validReleasedAuthorizationCheckpoints(value.documentAuthorizationCheckpoints, value.events);
|
|
737
|
+
}
|
|
738
|
+
function validModelHostBinding(value) {
|
|
739
|
+
return (isRecord(value) &&
|
|
740
|
+
(value.provider === "chat_completions" ||
|
|
741
|
+
value.provider === "openai_responses" ||
|
|
742
|
+
value.provider === "anthropic_messages") &&
|
|
743
|
+
typeof value.endpointFingerprint === "string" &&
|
|
744
|
+
value.endpointFingerprint.length > 0 &&
|
|
745
|
+
typeof value.modelId === "string" &&
|
|
746
|
+
value.modelId.length > 0 &&
|
|
747
|
+
typeof value.contextWindowTokens === "number" &&
|
|
748
|
+
validCount(value.contextWindowTokens) &&
|
|
749
|
+
value.contextWindowTokens > 0 &&
|
|
750
|
+
typeof value.maxOutputTokens === "number" &&
|
|
751
|
+
validCount(value.maxOutputTokens) &&
|
|
752
|
+
value.maxOutputTokens > 0 &&
|
|
753
|
+
typeof value.protocolConfigFingerprint === "string" &&
|
|
754
|
+
value.protocolConfigFingerprint.length > 0 &&
|
|
755
|
+
(value.cacheStrategy === undefined ||
|
|
756
|
+
value.cacheStrategy === "explicit_anthropic_blocks" ||
|
|
757
|
+
value.cacheStrategy === "explicit_cliproxyapi_message" ||
|
|
758
|
+
value.cacheStrategy === "provider_managed"));
|
|
759
|
+
}
|
|
760
|
+
function validReleasedDerivedFrom(value) {
|
|
761
|
+
return (value === undefined ||
|
|
762
|
+
(isRecord(value) &&
|
|
763
|
+
typeof value.worldId === "string" &&
|
|
764
|
+
typeof value.chainId === "string" &&
|
|
765
|
+
typeof value.head === "string"));
|
|
766
|
+
}
|
|
767
|
+
function validReleasedBranchedBeforePlayer(value) {
|
|
768
|
+
return (value === undefined ||
|
|
769
|
+
(isRecord(value) &&
|
|
770
|
+
typeof value.worldId === "string" &&
|
|
771
|
+
typeof value.chainId === "string" &&
|
|
772
|
+
Number.isSafeInteger(value.eventId) &&
|
|
773
|
+
Number(value.eventId) > 0));
|
|
774
|
+
}
|
|
775
|
+
function validReleasedAuthorizationCheckpoints(value, events) {
|
|
776
|
+
if (value === undefined)
|
|
777
|
+
return true;
|
|
778
|
+
if (!Array.isArray(value) || value.length > 100_000)
|
|
779
|
+
return false;
|
|
780
|
+
const eventIds = new Set(events.flatMap((event) => isRecord(event) && Number.isSafeInteger(event.id) && Number(event.id) > 0
|
|
781
|
+
? [Number(event.id)]
|
|
782
|
+
: []));
|
|
783
|
+
let previousEventId = -1;
|
|
784
|
+
for (const checkpoint of value) {
|
|
785
|
+
if (!isRecord(checkpoint) ||
|
|
786
|
+
Object.keys(checkpoint).length !== 2 ||
|
|
787
|
+
!Object.hasOwn(checkpoint, "afterEventId") ||
|
|
788
|
+
!Object.hasOwn(checkpoint, "authorization") ||
|
|
789
|
+
!Number.isSafeInteger(checkpoint.afterEventId) ||
|
|
790
|
+
Number(checkpoint.afterEventId) < 0 ||
|
|
791
|
+
Number(checkpoint.afterEventId) <= previousEventId ||
|
|
792
|
+
(Number(checkpoint.afterEventId) !== 0 &&
|
|
793
|
+
!eventIds.has(Number(checkpoint.afterEventId))) ||
|
|
794
|
+
!isRecord(checkpoint.authorization))
|
|
795
|
+
return false;
|
|
796
|
+
previousEventId = Number(checkpoint.afterEventId);
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
function releasedCurrentContext(value) {
|
|
801
|
+
const { schemaVersion: _schemaVersion, kind: _kind, worldId: _worldId, previousContexts: _previousContexts, ...context } = value;
|
|
802
|
+
void _schemaVersion;
|
|
803
|
+
void _kind;
|
|
804
|
+
void _worldId;
|
|
805
|
+
void _previousContexts;
|
|
806
|
+
return structuredClone(context);
|
|
807
|
+
}
|
|
808
|
+
function assertPlayEvent(value) {
|
|
809
|
+
if (!isRecord(value) ||
|
|
810
|
+
!Number.isSafeInteger(value.id) ||
|
|
811
|
+
Number(value.id) < 1)
|
|
812
|
+
throw new Error("Play timeline event has an invalid identity");
|
|
813
|
+
if (value.kind === "player") {
|
|
814
|
+
if (typeof value.exchangeId !== "string" ||
|
|
815
|
+
typeof value.text !== "string" ||
|
|
816
|
+
(value.context !== "fresh" && value.context !== "append") ||
|
|
817
|
+
!validOptionalString(value.committedHead))
|
|
818
|
+
throw new Error("Player timeline event has an invalid shape");
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (value.kind === "assistant") {
|
|
822
|
+
if (!validAssistantEventCore(value) ||
|
|
823
|
+
!validOptionalAssistantResponseKind(value.responseKind) ||
|
|
824
|
+
!validOptionalString(value.reasoning) ||
|
|
825
|
+
!validOptionalString(value.toolFragment) ||
|
|
826
|
+
!validOptionalUsage(value.usage) ||
|
|
827
|
+
!validOptionalString(value.stopReason) ||
|
|
828
|
+
(value.continuation !== undefined &&
|
|
829
|
+
value.continuation !== "available" &&
|
|
830
|
+
value.continuation !== "unavailable") ||
|
|
831
|
+
!validOptionalString(value.committedHead))
|
|
832
|
+
throw new Error("Assistant timeline event has an invalid shape");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (value.kind === "tool_call") {
|
|
836
|
+
if (typeof value.callId !== "string" ||
|
|
837
|
+
typeof value.name !== "string" ||
|
|
838
|
+
typeof value.replayed !== "boolean")
|
|
839
|
+
throw new Error("Tool-call timeline event has an invalid shape");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (value.kind === "tool_result") {
|
|
843
|
+
if (typeof value.callId !== "string" ||
|
|
844
|
+
typeof value.name !== "string" ||
|
|
845
|
+
typeof value.ok !== "boolean" ||
|
|
846
|
+
typeof value.markdown !== "string" ||
|
|
847
|
+
typeof value.replayed !== "boolean")
|
|
848
|
+
throw new Error("Tool-result timeline event has an invalid shape");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (value.kind === "followup") {
|
|
852
|
+
if (typeof value.followupId !== "string" ||
|
|
853
|
+
typeof value.displayName !== "string" ||
|
|
854
|
+
typeof value.text !== "string" ||
|
|
855
|
+
!validOptionalString(value.reasoning) ||
|
|
856
|
+
!validOptionalUsage(value.usage) ||
|
|
857
|
+
!Array.isArray(value.toolCalls) ||
|
|
858
|
+
!value.toolCalls.every(validFollowupToolCall) ||
|
|
859
|
+
!validOptionalString(value.failure))
|
|
860
|
+
throw new Error("Follow-up timeline event has an invalid shape");
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if ((value.kind !== "failure" && value.kind !== "cancellation") ||
|
|
864
|
+
typeof value.message !== "string")
|
|
865
|
+
throw new Error("Play timeline event has an invalid kind");
|
|
866
|
+
}
|
|
867
|
+
function assertTimelineEventSummary(value) {
|
|
868
|
+
if (!isRecord(value) ||
|
|
869
|
+
!Number.isSafeInteger(value.id) ||
|
|
870
|
+
Number(value.id) < 1)
|
|
871
|
+
throw new Error("Play timeline summary has an invalid identity");
|
|
872
|
+
if (value.kind === "player" ||
|
|
873
|
+
value.kind === "failure" ||
|
|
874
|
+
value.kind === "cancellation") {
|
|
875
|
+
assertPlayEvent(value);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (value.kind === "assistant") {
|
|
879
|
+
if (!validAssistantEventCore(value) ||
|
|
880
|
+
!validOptionalAssistantResponseKind(value.responseKind) ||
|
|
881
|
+
!validOptionalString(value.committedHead) ||
|
|
882
|
+
!validOptionalString(value.stopReason) ||
|
|
883
|
+
!validOptionalUsage(value.usage) ||
|
|
884
|
+
(value.continuation !== undefined &&
|
|
885
|
+
value.continuation !== "available" &&
|
|
886
|
+
value.continuation !== "unavailable") ||
|
|
887
|
+
typeof value.hasReasoning !== "boolean" ||
|
|
888
|
+
typeof value.hasToolFragment !== "boolean" ||
|
|
889
|
+
typeof value.hasUsage !== "boolean" ||
|
|
890
|
+
typeof value.detailsAvailable !== "boolean")
|
|
891
|
+
throw new Error("Assistant timeline summary has an invalid shape");
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (value.kind === "tool_call") {
|
|
895
|
+
if (typeof value.callId !== "string" ||
|
|
896
|
+
typeof value.name !== "string" ||
|
|
897
|
+
typeof value.replayed !== "boolean" ||
|
|
898
|
+
value.detailsAvailable !== true)
|
|
899
|
+
throw new Error("Tool-call timeline summary has an invalid shape");
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (value.kind === "tool_result") {
|
|
903
|
+
if (typeof value.callId !== "string" ||
|
|
904
|
+
typeof value.name !== "string" ||
|
|
905
|
+
typeof value.ok !== "boolean" ||
|
|
906
|
+
typeof value.replayed !== "boolean" ||
|
|
907
|
+
value.detailsAvailable !== true)
|
|
908
|
+
throw new Error("Tool-result timeline summary has an invalid shape");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (value.kind !== "followup" ||
|
|
912
|
+
typeof value.followupId !== "string" ||
|
|
913
|
+
typeof value.displayName !== "string" ||
|
|
914
|
+
!validCount(value.toolCallCount) ||
|
|
915
|
+
typeof value.failed !== "boolean" ||
|
|
916
|
+
!validOptionalUsage(value.usage) ||
|
|
917
|
+
value.detailsAvailable !== true)
|
|
918
|
+
throw new Error("Follow-up timeline summary has an invalid shape");
|
|
919
|
+
}
|
|
920
|
+
function validAssistantEventCore(value) {
|
|
921
|
+
return (typeof value.text === "string" &&
|
|
922
|
+
(value.status === "streaming" ||
|
|
923
|
+
value.status === "completed" ||
|
|
924
|
+
value.status === "interrupted") &&
|
|
925
|
+
validCount(value.exchange) &&
|
|
926
|
+
Number.isSafeInteger(value.attempt) &&
|
|
927
|
+
Number(value.attempt) >= 1);
|
|
928
|
+
}
|
|
929
|
+
function validOptionalAssistantResponseKind(value) {
|
|
930
|
+
return (value === undefined ||
|
|
931
|
+
value === "pending" ||
|
|
932
|
+
value === "narrative" ||
|
|
933
|
+
value === "tool_step" ||
|
|
934
|
+
value === "empty");
|
|
935
|
+
}
|
|
936
|
+
function validOptionalString(value) {
|
|
937
|
+
return value === undefined || typeof value === "string";
|
|
938
|
+
}
|
|
939
|
+
function validOptionalUsage(value) {
|
|
940
|
+
return (value === undefined ||
|
|
941
|
+
(isRecord(value) &&
|
|
942
|
+
validUsageToken(value.inputTokens) &&
|
|
943
|
+
validUsageToken(value.outputTokens) &&
|
|
944
|
+
(value.provenance === undefined || validFullUsage(value))));
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* v0.1.1 stored only input/output usage. Keep that released shape readable,
|
|
948
|
+
* but expose one current protocol projection with honest unavailable fields.
|
|
949
|
+
*/
|
|
950
|
+
function normalizePlayEvent(event) {
|
|
951
|
+
if ((event.kind !== "assistant" && event.kind !== "followup") ||
|
|
952
|
+
event.usage === undefined ||
|
|
953
|
+
validFullUsage(event.usage))
|
|
954
|
+
return structuredClone(event);
|
|
955
|
+
const legacy = event.usage;
|
|
956
|
+
const unavailable = "unavailable";
|
|
957
|
+
const provider = (value) => value === null ? unavailable : "provider";
|
|
958
|
+
const usage = {
|
|
959
|
+
inputTokens: legacy.inputTokens,
|
|
960
|
+
uncachedInputTokens: null,
|
|
961
|
+
cacheReadTokens: null,
|
|
962
|
+
cacheWriteTokens: null,
|
|
963
|
+
reasoningTokens: null,
|
|
964
|
+
outputTokens: legacy.outputTokens,
|
|
965
|
+
totalTokens: null,
|
|
966
|
+
provenance: {
|
|
967
|
+
inputTokens: provider(legacy.inputTokens),
|
|
968
|
+
uncachedInputTokens: unavailable,
|
|
969
|
+
cacheReadTokens: unavailable,
|
|
970
|
+
cacheWriteTokens: unavailable,
|
|
971
|
+
reasoningTokens: unavailable,
|
|
972
|
+
outputTokens: provider(legacy.outputTokens),
|
|
973
|
+
totalTokens: unavailable,
|
|
974
|
+
},
|
|
975
|
+
};
|
|
976
|
+
return { ...structuredClone(event), usage };
|
|
977
|
+
}
|
|
978
|
+
function validFullUsage(value) {
|
|
979
|
+
if (!isRecord(value) || !isRecord(value.provenance))
|
|
980
|
+
return false;
|
|
981
|
+
const provenance = value.provenance;
|
|
982
|
+
const fields = [
|
|
983
|
+
"inputTokens",
|
|
984
|
+
"uncachedInputTokens",
|
|
985
|
+
"cacheReadTokens",
|
|
986
|
+
"cacheWriteTokens",
|
|
987
|
+
"reasoningTokens",
|
|
988
|
+
"outputTokens",
|
|
989
|
+
"totalTokens",
|
|
990
|
+
];
|
|
991
|
+
return fields.every((field) => validUsageToken(value[field]) && validUsageField(provenance[field]));
|
|
992
|
+
}
|
|
993
|
+
function validUsageToken(value) {
|
|
994
|
+
return (value === null ||
|
|
995
|
+
(typeof value === "number" && Number.isFinite(value) && value >= 0));
|
|
996
|
+
}
|
|
997
|
+
function validUsageField(value) {
|
|
998
|
+
return (value === "provider" ||
|
|
999
|
+
value === "unavailable" ||
|
|
1000
|
+
value === "derived_provider_fields");
|
|
1001
|
+
}
|
|
1002
|
+
function validFollowupToolCall(value) {
|
|
1003
|
+
return (isRecord(value) &&
|
|
1004
|
+
typeof value.callId === "string" &&
|
|
1005
|
+
typeof value.name === "string" &&
|
|
1006
|
+
typeof value.ok === "boolean" &&
|
|
1007
|
+
typeof value.markdown === "string");
|
|
1008
|
+
}
|
|
1009
|
+
function validCount(value) {
|
|
1010
|
+
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1011
|
+
}
|
|
1012
|
+
function assertIdentity(value, label) {
|
|
1013
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
1014
|
+
throw new TypeError(`${label} is invalid`);
|
|
1015
|
+
}
|
|
1016
|
+
function isRecord(value) {
|
|
1017
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1018
|
+
}
|
|
1019
|
+
function isNodeError(value) {
|
|
1020
|
+
return value instanceof Error && "code" in value;
|
|
1021
|
+
}
|
|
1022
|
+
function prefixLength(source, target) {
|
|
1023
|
+
return isDeepStrictEqual(source.slice(0, target.length), target)
|
|
1024
|
+
? target.length
|
|
1025
|
+
: 0;
|
|
1026
|
+
}
|
|
1027
|
+
async function cloneNumberedPrefix(input) {
|
|
1028
|
+
for (let index = 1; index <= input.count; index += 1)
|
|
1029
|
+
await cloneTimelineFile(join(input.sourceRoot, input.directory, numbered(index)), join(input.targetRoot, input.directory, numbered(index)), input.immutable);
|
|
1030
|
+
}
|
|
1031
|
+
async function cloneTimelineFile(source, target, immutable) {
|
|
1032
|
+
await cloneFilePhysically({
|
|
1033
|
+
source,
|
|
1034
|
+
target,
|
|
1035
|
+
immutable,
|
|
1036
|
+
onTargetExists: assertSameFileBytes,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async function assertSameFileBytes(source, target) {
|
|
1040
|
+
const [sourceBytes, targetBytes] = await Promise.all([
|
|
1041
|
+
readFile(source),
|
|
1042
|
+
readFile(target),
|
|
1043
|
+
]);
|
|
1044
|
+
if (!sourceBytes.equals(targetBytes))
|
|
1045
|
+
throw new Error("Cloned play timeline prefix conflicts in target staging");
|
|
1046
|
+
}
|
|
1047
|
+
async function readNumbered(root, count) {
|
|
1048
|
+
return Promise.all(Array.from({ length: count }, (_, index) => readJson(join(root, numbered(index + 1)))));
|
|
1049
|
+
}
|
|
1050
|
+
async function readNumberedValidated(root, count, assertValue) {
|
|
1051
|
+
const values = await readNumbered(root, count);
|
|
1052
|
+
const validated = [];
|
|
1053
|
+
for (const value of values) {
|
|
1054
|
+
assertValue(value);
|
|
1055
|
+
validated.push(value);
|
|
1056
|
+
}
|
|
1057
|
+
return validated;
|
|
1058
|
+
}
|
|
1059
|
+
async function readJson(path) {
|
|
1060
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
1061
|
+
}
|
|
1062
|
+
async function readOptionalJson(path) {
|
|
1063
|
+
try {
|
|
1064
|
+
return await readJson(path);
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
1068
|
+
return null;
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
async function publishJson(path, value) {
|
|
1073
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1074
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
1075
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
1076
|
+
encoding: "utf8",
|
|
1077
|
+
mode: 0o600,
|
|
1078
|
+
});
|
|
1079
|
+
await syncFile(temporary);
|
|
1080
|
+
await rename(temporary, path);
|
|
1081
|
+
await syncDirectory(dirname(path));
|
|
1082
|
+
}
|
|
1083
|
+
async function publishImmutableJson(path, value) {
|
|
1084
|
+
const contents = `${JSON.stringify(value, null, 2)}\n`;
|
|
1085
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
1086
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
1087
|
+
try {
|
|
1088
|
+
await writeFile(temporary, contents, {
|
|
1089
|
+
encoding: "utf8",
|
|
1090
|
+
flag: "wx",
|
|
1091
|
+
mode: 0o600,
|
|
1092
|
+
});
|
|
1093
|
+
await syncFile(temporary);
|
|
1094
|
+
try {
|
|
1095
|
+
await link(temporary, path);
|
|
1096
|
+
await syncDirectory(dirname(path));
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
1100
|
+
throw error;
|
|
1101
|
+
if ((await readFile(path, "utf8")) !== contents)
|
|
1102
|
+
throw new Error("Immutable play timeline record conflicts", {
|
|
1103
|
+
cause: error,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
finally {
|
|
1108
|
+
await rm(temporary, { force: true });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
async function syncFile(path) {
|
|
1112
|
+
const handle = await open(path, "r");
|
|
1113
|
+
try {
|
|
1114
|
+
await handle.sync();
|
|
1115
|
+
}
|
|
1116
|
+
finally {
|
|
1117
|
+
await handle.close();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async function syncDirectory(path) {
|
|
1121
|
+
const handle = await open(path, "r");
|
|
1122
|
+
try {
|
|
1123
|
+
await handle.sync();
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
await handle.close();
|
|
1127
|
+
}
|
|
1128
|
+
}
|