narraeon 0.4.0 → 0.5.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/appPreferences.js +25 -0
- package/dist/node/protocol/conversationObservation.js +67 -0
- package/dist/node/protocol/v1.js +186 -34
- package/dist/node/protocol/worldExtensions.js +16 -0
- package/dist/node/protocol/worldMaintenance.js +77 -0
- package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
- package/dist/node/runtime/ConversationChanges.js +24 -0
- package/dist/node/runtime/V1Runtime.js +445 -121
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
- package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
- package/dist/node/runtime/content/ContentWorkspace.js +43 -13
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
- package/dist/node/runtime/content/PackageFollowups.js +80 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
- package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
- package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
- package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
- package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
- package/dist/node/runtime/model/ModelHost.js +31 -1
- package/dist/node/runtime/model/ModelHostStream.js +51 -5
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
- package/dist/node/runtime/play/OrderedFollowups.js +71 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
- package/dist/node/runtime/play/PlayCallChain.js +814 -61
- package/dist/node/runtime/play/PlayContinuity.js +88 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
- package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
- package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
- package/dist/node/runtime/play/PlayPromptRun.js +86 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
- package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
- package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
- package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
- package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
- package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
- package/dist/node/server/ConversationStream.js +109 -0
- package/dist/node/server/createServer.js +5 -1
- package/dist/node/shared/artifact-payload-contract.js +123 -0
- package/dist/node/shared/default-play-prompts.js +25 -45
- package/dist/node/shared/default-preset-host.js +114 -138
- package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
- package/dist/node/shared/display-regex.js +114 -0
- package/dist/node/shared/first-party-action-choices.js +1 -6
- package/dist/node/shared/first-party-player-view.js +1 -6
- package/dist/node/shared/ordered-author-prompts.js +64 -0
- package/dist/node/shared/ordered-followups.js +42 -0
- package/dist/node/shared/ordered-play-prompts.js +73 -0
- package/dist/web/assets/index-BXfelCi9.js +1207 -0
- package/dist/web/assets/index-BYnhUxlk.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
- package/dist/web/assets/index-D8YKfmNG.js +0 -1192
- package/dist/web/assets/index-Xa0s35CJ.css +0 -1
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import { currentPlayPrompt, recordedPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
|
|
2
|
+
import { comparePromptPrefixes } from "../prompt/WorldPromptDiagnostics.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
4
|
+
import { completedPlayerRounds, continuationNotice, isPlayerRoundMarker, playerInputAppend, toolStepNotice, } from "./PlayContinuity.js";
|
|
5
|
+
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
6
|
+
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
7
|
+
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
8
|
+
import { equalModelHostBinding, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
3
9
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
10
|
+
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
4
11
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
5
12
|
import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTools.js";
|
|
6
13
|
const callChainToolNames = new Set([
|
|
@@ -12,6 +19,8 @@ const callChainToolNames = new Set([
|
|
|
12
19
|
"context_read",
|
|
13
20
|
"world_patch",
|
|
14
21
|
"world_create",
|
|
22
|
+
"world_retire",
|
|
23
|
+
"world_checkpoint",
|
|
15
24
|
]);
|
|
16
25
|
const projectedEventTail = 40;
|
|
17
26
|
export class PlayCallChainError extends Error {
|
|
@@ -28,6 +37,7 @@ export class PlayCallChainError extends Error {
|
|
|
28
37
|
* response shape whose non-empty text can become committed narrative.
|
|
29
38
|
*/
|
|
30
39
|
export class PlayCallChain {
|
|
40
|
+
changed;
|
|
31
41
|
#worlds;
|
|
32
42
|
#compiler;
|
|
33
43
|
#artifacts;
|
|
@@ -36,7 +46,10 @@ export class PlayCallChain {
|
|
|
36
46
|
#worldChains = new Map();
|
|
37
47
|
#loadedCursors = new Map();
|
|
38
48
|
#activeInvocations = new Map();
|
|
39
|
-
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog) {
|
|
49
|
+
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog, changed = () => {
|
|
50
|
+
/* No observers attached. */
|
|
51
|
+
}) {
|
|
52
|
+
this.changed = changed;
|
|
40
53
|
this.#worlds = worlds;
|
|
41
54
|
this.#compiler = compiler;
|
|
42
55
|
this.#artifacts = artifacts;
|
|
@@ -62,7 +75,9 @@ export class PlayCallChain {
|
|
|
62
75
|
throw new PlayCallChainError("A model request is still running for this world.");
|
|
63
76
|
const binding = await this.#worlds.bindPlayCallChain(input.worldId);
|
|
64
77
|
const documents = new FileNativePlayDocuments(binding.files);
|
|
78
|
+
const documentMaintenance = await this.#bindDocumentMaintenance(documents, input.worldId, binding.parentHead);
|
|
65
79
|
const compilation = this.#compiler.compilePlayCallChain({
|
|
80
|
+
extensionControls: await this.#worlds.extensionControls(input.worldId, input.playPreset, this.#compiler.locale),
|
|
66
81
|
endpoint: {
|
|
67
82
|
id: `${input.worldId}:${binding.parentHead}`,
|
|
68
83
|
commit: binding.parentHead,
|
|
@@ -73,6 +88,9 @@ export class PlayCallChain {
|
|
|
73
88
|
documentSnapshot: documents.snapshot,
|
|
74
89
|
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
75
90
|
history: structuredClone(binding.history),
|
|
91
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
92
|
+
documentMaintenance,
|
|
93
|
+
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
76
94
|
},
|
|
77
95
|
playerInputPlacement: "append",
|
|
78
96
|
playerInput: input.playerText,
|
|
@@ -86,6 +104,7 @@ export class PlayCallChain {
|
|
|
86
104
|
schemaVersion: 3,
|
|
87
105
|
kind: "play_call_chain",
|
|
88
106
|
chainId: input.chainId,
|
|
107
|
+
continuityContextId: input.chainId,
|
|
89
108
|
worldId: input.worldId,
|
|
90
109
|
previousContexts: [],
|
|
91
110
|
previousChainId: existing?.chainId ?? null,
|
|
@@ -101,6 +120,7 @@ export class PlayCallChain {
|
|
|
101
120
|
},
|
|
102
121
|
followups: structuredClone(compilation.followups ?? []),
|
|
103
122
|
playPresetScriptsEnabled: input.playPreset.scriptsEnabled,
|
|
123
|
+
presetFiles: structuredClone(input.playPreset.files),
|
|
104
124
|
modelBinding: structuredClone(input.modelBinding),
|
|
105
125
|
status: "ready",
|
|
106
126
|
canRetry: false,
|
|
@@ -125,6 +145,7 @@ export class PlayCallChain {
|
|
|
125
145
|
updatedAt: now,
|
|
126
146
|
documents,
|
|
127
147
|
history: historyEntries(binding.history),
|
|
148
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
128
149
|
completedToolMap: new Map(),
|
|
129
150
|
persistenceCursor: {
|
|
130
151
|
eventCount: 0,
|
|
@@ -153,10 +174,16 @@ export class PlayCallChain {
|
|
|
153
174
|
throw new PlayCallChainError("A model request is still running.");
|
|
154
175
|
if (session.status === "interrupted") {
|
|
155
176
|
if (!session.canRetry || session.lastRequest === null)
|
|
156
|
-
throw new PlayCallChainError("
|
|
177
|
+
throw new PlayCallChainError("The current model context cannot continue; use a fresh context.");
|
|
157
178
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
158
179
|
}
|
|
180
|
+
await this.#refreshPrompt(session, input);
|
|
159
181
|
session.exchange += 1;
|
|
182
|
+
const last = session.transcript.at(-1);
|
|
183
|
+
if (last?.kind === "assistant" &&
|
|
184
|
+
last.toolCalls.length === 0 &&
|
|
185
|
+
last.text.trim().length > 0)
|
|
186
|
+
session.transcript.push(continuationNotice(this.#compiler.locale));
|
|
160
187
|
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, signal, input.observer);
|
|
161
188
|
}
|
|
162
189
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
@@ -165,8 +192,67 @@ export class PlayCallChain {
|
|
|
165
192
|
throw new PlayCallChainError(session.canRetry
|
|
166
193
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
167
194
|
: "The call chain was interrupted; use a fresh context.");
|
|
195
|
+
await this.#refreshPrompt(session, input);
|
|
168
196
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, signal, input.observer);
|
|
169
197
|
}
|
|
198
|
+
async #refreshPrompt(session, input) {
|
|
199
|
+
if (input.resolvePrompt === undefined)
|
|
200
|
+
return;
|
|
201
|
+
const { hostBinding, playPreset } = await input.resolvePrompt();
|
|
202
|
+
const binding = await this.#worlds.bindPlayCallChain(input.worldId);
|
|
203
|
+
const baseline = await this.#worlds.bindPlayCallChainAt(input.worldId, session.baselineHead);
|
|
204
|
+
const documents = new FileNativePlayDocuments(binding.files);
|
|
205
|
+
const documentMaintenance = await this.#bindDocumentMaintenance(documents, input.worldId, binding.parentHead);
|
|
206
|
+
const compilation = this.#compiler.compilePlayCallChain({
|
|
207
|
+
extensionControls: await this.#worlds.extensionControls(input.worldId, playPreset, this.#compiler.locale),
|
|
208
|
+
endpoint: {
|
|
209
|
+
id: `${input.worldId}:${binding.parentHead}`,
|
|
210
|
+
commit: binding.parentHead,
|
|
211
|
+
},
|
|
212
|
+
hostBinding,
|
|
213
|
+
world: {
|
|
214
|
+
controlFingerprint: fingerprintControl(binding.files),
|
|
215
|
+
documentSnapshot: documents.snapshot,
|
|
216
|
+
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
217
|
+
// Only pre-context history belongs in the prefix. Native conversation stays appended.
|
|
218
|
+
history: structuredClone(binding.history),
|
|
219
|
+
historyAlreadyAppended: Object.keys(binding.history).filter((key) => !(key in baseline.history)),
|
|
220
|
+
narrativeCheckpoint: baseline.narrativeCheckpoint,
|
|
221
|
+
documentMaintenance,
|
|
222
|
+
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
223
|
+
},
|
|
224
|
+
playerInputPlacement: "append",
|
|
225
|
+
playerInput: input.playerText,
|
|
226
|
+
modelBinding: input.modelHost.binding(),
|
|
227
|
+
}, playPreset);
|
|
228
|
+
documents.bindBootstrap(compilation.bootstrap, session.documents.authorizationCheckpoint());
|
|
229
|
+
session.documents = documents;
|
|
230
|
+
session.history = historyEntries(binding.history);
|
|
231
|
+
session.narrativeCheckpoint = binding.narrativeCheckpoint;
|
|
232
|
+
session.nextMaterials = structuredClone(binding.additionalMaterials);
|
|
233
|
+
(session.promptRuns ??= []).push({
|
|
234
|
+
schemaVersion: 1,
|
|
235
|
+
firstEventId: session.nextEventId,
|
|
236
|
+
head: binding.parentHead,
|
|
237
|
+
exchange: session.exchange + 1,
|
|
238
|
+
playPreset: {
|
|
239
|
+
id: playPreset.id,
|
|
240
|
+
name: playPreset.name,
|
|
241
|
+
revision: playPreset.revision,
|
|
242
|
+
},
|
|
243
|
+
bootstrap: structuredClone(compilation.bootstrap),
|
|
244
|
+
tools: structuredClone(compilation.toolUniverse),
|
|
245
|
+
followups: structuredClone(compilation.followups ?? []),
|
|
246
|
+
playPresetScriptsEnabled: playPreset.scriptsEnabled,
|
|
247
|
+
presetFiles: structuredClone(playPreset.files),
|
|
248
|
+
});
|
|
249
|
+
session.documentAuthorizationCheckpoints.push({
|
|
250
|
+
afterEventId: session.nextEventId,
|
|
251
|
+
authorization: documents.authorizationCheckpoint(),
|
|
252
|
+
});
|
|
253
|
+
await this.#persist(session);
|
|
254
|
+
crashAtPlayAdvanceEdge("after_prompt_run_prepared");
|
|
255
|
+
}
|
|
170
256
|
cancel(input) {
|
|
171
257
|
validateIdentity(input.worldId, "world ID");
|
|
172
258
|
validateIdentity(input.chainId, "call-chain ID");
|
|
@@ -206,13 +292,19 @@ export class PlayCallChain {
|
|
|
206
292
|
controller: new AbortController(),
|
|
207
293
|
abortable: true,
|
|
208
294
|
};
|
|
295
|
+
const controlUseId = operationId(input.chainId, `control-use:${input.exchangeId}`);
|
|
296
|
+
this.#worlds.freezeControl(input.worldId, controlUseId);
|
|
209
297
|
this.#activeInvocations.set(input.chainId, invocation);
|
|
298
|
+
this.changed(input.worldId);
|
|
210
299
|
try {
|
|
300
|
+
await this.#worlds.assertWorldRevisionUnlocked(input.worldId);
|
|
211
301
|
return await run(invocation.controller.signal);
|
|
212
302
|
}
|
|
213
303
|
finally {
|
|
214
304
|
if (this.#activeInvocations.get(input.chainId) === invocation)
|
|
215
305
|
this.#activeInvocations.delete(input.chainId);
|
|
306
|
+
this.#worlds.releaseControl(input.worldId, controlUseId);
|
|
307
|
+
this.changed(input.worldId);
|
|
216
308
|
}
|
|
217
309
|
}
|
|
218
310
|
#observeInvocation(chainId, phase, increments = {}) {
|
|
@@ -226,6 +318,7 @@ export class PlayCallChain {
|
|
|
226
318
|
active.toolChars += increments.toolChars ?? 0;
|
|
227
319
|
active.toolCalls += increments.toolCalls ?? 0;
|
|
228
320
|
active.dispatches += increments.dispatches ?? 0;
|
|
321
|
+
this.changed(active.worldId);
|
|
229
322
|
}
|
|
230
323
|
#setInvocationAbortable(chainId, abortable) {
|
|
231
324
|
const active = this.#activeInvocations.get(chainId);
|
|
@@ -245,6 +338,7 @@ export class PlayCallChain {
|
|
|
245
338
|
}
|
|
246
339
|
#notifySnapshot(observer, session) {
|
|
247
340
|
try {
|
|
341
|
+
this.changed(session.worldId);
|
|
248
342
|
observer?.onSnapshot?.(this.#projectActiveView(session));
|
|
249
343
|
}
|
|
250
344
|
catch {
|
|
@@ -253,12 +347,31 @@ export class PlayCallChain {
|
|
|
253
347
|
}
|
|
254
348
|
}
|
|
255
349
|
async inspectWorld(worldId) {
|
|
350
|
+
const session = await this.#worldSession(worldId);
|
|
351
|
+
return session === null ? null : this.#projectActiveView(session);
|
|
352
|
+
}
|
|
353
|
+
/** Browser observation never traverses old contexts. History and completed
|
|
354
|
+
* diagnostics remain behind the paged timeline/detail Interface. */
|
|
355
|
+
async observeWorld(worldId) {
|
|
356
|
+
const session = await this.#worldSession(worldId);
|
|
357
|
+
if (session === null)
|
|
358
|
+
return null;
|
|
359
|
+
const view = this.#projectActiveView({ ...session, previousContexts: [] });
|
|
360
|
+
const live = new Map(session.events
|
|
361
|
+
.slice(-projectedEventTail)
|
|
362
|
+
.map((event) => [event.id, event]));
|
|
363
|
+
view.events = view.events.map((event) => event.kind === "assistant" && event.status === "streaming"
|
|
364
|
+
? structuredClone(live.get(event.id))
|
|
365
|
+
: event);
|
|
366
|
+
return view;
|
|
367
|
+
}
|
|
368
|
+
async #worldSession(worldId) {
|
|
256
369
|
const activeId = this.#worldChains.get(worldId);
|
|
257
370
|
const active = activeId === undefined ? undefined : this.#active.get(activeId);
|
|
258
371
|
if (active !== undefined) {
|
|
259
372
|
if (this.#activeInvocations.get(active.chainId)?.worldId !== active.worldId)
|
|
260
373
|
await this.#reconcileSessionAdvance(active);
|
|
261
|
-
return
|
|
374
|
+
return active;
|
|
262
375
|
}
|
|
263
376
|
const persisted = await this.#readPersisted(worldId);
|
|
264
377
|
if (persisted === null)
|
|
@@ -270,7 +383,96 @@ export class PlayCallChain {
|
|
|
270
383
|
await this.#reconcileSessionAdvance(session, advance);
|
|
271
384
|
if (session.status === "running")
|
|
272
385
|
await this.#interruptAbandonedDispatch(session);
|
|
273
|
-
return
|
|
386
|
+
return session;
|
|
387
|
+
}
|
|
388
|
+
async inspectReading(worldId, worldHead) {
|
|
389
|
+
const view = await this.inspectWorld(worldId);
|
|
390
|
+
if (view === null)
|
|
391
|
+
return null;
|
|
392
|
+
const session = this.#active.get(view.chainId);
|
|
393
|
+
if (session === undefined)
|
|
394
|
+
throw new PlayCallChainError("The current play context could not be inspected.");
|
|
395
|
+
const runs = playPromptRunsThroughEvents(session, session.events);
|
|
396
|
+
const latestRun = runs.at(-1);
|
|
397
|
+
const priorRun = runs.at(-2);
|
|
398
|
+
const previousContext = latestRun !== undefined || session.previousChainId === null
|
|
399
|
+
? null
|
|
400
|
+
: await this.#worlds.playTimeline.readContext(worldId, session.previousChainId);
|
|
401
|
+
const previousContextRun = previousContext === null
|
|
402
|
+
? undefined
|
|
403
|
+
: playPromptRunsThroughEvents(previousContext.value, previousContext.value.events).at(-1);
|
|
404
|
+
const previous = latestRun !== undefined
|
|
405
|
+
? (priorRun ?? session)
|
|
406
|
+
: (previousContextRun ?? previousContext?.value ?? null);
|
|
407
|
+
const currentExchange = latestRun?.exchange ?? 1;
|
|
408
|
+
const previousExchange = priorRun?.exchange ?? previousContextRun?.exchange ?? 1;
|
|
409
|
+
const [currentEncoding, previousEncoding] = await Promise.all([
|
|
410
|
+
this.#worlds.playTimeline
|
|
411
|
+
.readInitialEncoding(worldId, session.chainId, currentExchange)
|
|
412
|
+
.catch(() => null),
|
|
413
|
+
previous === null
|
|
414
|
+
? null
|
|
415
|
+
: this.#worlds.playTimeline
|
|
416
|
+
.readInitialEncoding(worldId, latestRun !== undefined
|
|
417
|
+
? session.chainId
|
|
418
|
+
: previousContext.value.chainId, previousExchange)
|
|
419
|
+
.catch(() => null),
|
|
420
|
+
]);
|
|
421
|
+
const usageAt = (events, exchange) => events.findLast((event) => event.kind === "assistant" &&
|
|
422
|
+
event.status === "completed" &&
|
|
423
|
+
event.exchange === exchange);
|
|
424
|
+
const currentResponse = usageAt(session.events, currentExchange);
|
|
425
|
+
const previousResponse = usageAt(previousContext?.value.events ?? session.events, previousExchange);
|
|
426
|
+
const prefixDiagnostics = comparePromptPrefixes(previous === null
|
|
427
|
+
? null
|
|
428
|
+
: {
|
|
429
|
+
bootstrap: previous.bootstrap,
|
|
430
|
+
encoding: previousEncoding,
|
|
431
|
+
...(previousResponse?.kind === "assistant" &&
|
|
432
|
+
previousResponse.usage !== undefined
|
|
433
|
+
? { usage: previousResponse.usage }
|
|
434
|
+
: {}),
|
|
435
|
+
}, {
|
|
436
|
+
bootstrap: recordedPlayPrompt(session).bootstrap,
|
|
437
|
+
encoding: currentEncoding,
|
|
438
|
+
...(currentResponse?.kind === "assistant" &&
|
|
439
|
+
currentResponse.usage !== undefined
|
|
440
|
+
? { usage: currentResponse.usage }
|
|
441
|
+
: {}),
|
|
442
|
+
});
|
|
443
|
+
return {
|
|
444
|
+
prefixDiagnostics,
|
|
445
|
+
promptHistory: [
|
|
446
|
+
{
|
|
447
|
+
exchange: 1,
|
|
448
|
+
head: session.baselineHead,
|
|
449
|
+
playPreset: session.playPreset,
|
|
450
|
+
bootstrap: session.bootstrap,
|
|
451
|
+
},
|
|
452
|
+
...runs,
|
|
453
|
+
].map(({ exchange, head, playPreset, bootstrap }) => ({
|
|
454
|
+
exchange,
|
|
455
|
+
head,
|
|
456
|
+
playPreset: structuredClone(playPreset),
|
|
457
|
+
bootstrap: {
|
|
458
|
+
logicalMessages: structuredClone(bootstrap.logicalMessages),
|
|
459
|
+
coverage: structuredClone(bootstrap.coverage),
|
|
460
|
+
},
|
|
461
|
+
})),
|
|
462
|
+
requestExchange: currentExchange,
|
|
463
|
+
requestHead: latestRun?.head ?? session.baselineHead,
|
|
464
|
+
chainId: session.chainId,
|
|
465
|
+
baselineHead: session.baselineHead,
|
|
466
|
+
parentHead: session.parentHead,
|
|
467
|
+
stale: session.parentHead !== worldHead,
|
|
468
|
+
playPreset: structuredClone(recordedPlayPrompt(session).playPreset),
|
|
469
|
+
updatedAt: session.updatedAt,
|
|
470
|
+
bootstrap: {
|
|
471
|
+
logicalMessages: structuredClone(recordedPlayPrompt(session).bootstrap.logicalMessages),
|
|
472
|
+
coverage: structuredClone(recordedPlayPrompt(session).bootstrap.coverage),
|
|
473
|
+
},
|
|
474
|
+
reads: projectContextReads(session),
|
|
475
|
+
};
|
|
274
476
|
}
|
|
275
477
|
async deriveWorld(input) {
|
|
276
478
|
const result = await this.#worlds.deriveWorld({
|
|
@@ -358,14 +560,27 @@ export class PlayCallChain {
|
|
|
358
560
|
if (source === null)
|
|
359
561
|
throw new PlayCallChainError("The current world has no model call chain to revise.");
|
|
360
562
|
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.worldId);
|
|
361
|
-
const
|
|
362
|
-
.
|
|
363
|
-
|
|
563
|
+
const appliedContext = sourceContexts.find(({ events }) => events.some((event) => event.kind === "player" &&
|
|
564
|
+
event.exchangeId === input.replacementExchangeId &&
|
|
565
|
+
event.committedHead !== undefined));
|
|
566
|
+
const appliedEvent = appliedContext?.events.find((event) => event.kind === "player" &&
|
|
364
567
|
event.exchangeId === input.replacementExchangeId &&
|
|
365
568
|
event.committedHead !== undefined);
|
|
366
569
|
if (appliedEvent !== undefined) {
|
|
367
570
|
if (appliedEvent.text !== input.replacementText)
|
|
368
571
|
throw new PlayCallChainError("The same replacement exchange ID is already bound to different player text.");
|
|
572
|
+
const requestedSource = await this.#worlds.playTimeline.readContext(input.worldId, input.chainId);
|
|
573
|
+
const requestedEventIndex = requestedSource?.value.events.findIndex(({ id }) => id === input.eventId);
|
|
574
|
+
const requestedEvent = requestedEventIndex === undefined || requestedEventIndex < 0
|
|
575
|
+
? undefined
|
|
576
|
+
: requestedSource?.value.events[requestedEventIndex];
|
|
577
|
+
if (requestedSource === null ||
|
|
578
|
+
requestedEventIndex === undefined ||
|
|
579
|
+
requestedEventIndex < 0 ||
|
|
580
|
+
requestedEvent?.kind !== "player" ||
|
|
581
|
+
requestedEvent.committedHead === undefined)
|
|
582
|
+
throw new PlayCallChainError("The replacement exchange ID is already used by another commit.");
|
|
583
|
+
const requestedRestoresHead = playerRevisionRestoresHead(requestedSource.value, requestedEventIndex);
|
|
369
584
|
const authority = await this.#worlds.readAuthorityHistory(input.worldId);
|
|
370
585
|
const commit = authority.commits.find(({ head }) => head === appliedEvent.committedHead);
|
|
371
586
|
const operation = await this.#worlds.getOperationOutcome(input.operationId);
|
|
@@ -373,7 +588,11 @@ export class PlayCallChain {
|
|
|
373
588
|
(operation.outcome !== "committed" &&
|
|
374
589
|
operation.outcome !== "committed_materialization_pending") ||
|
|
375
590
|
operation.worldId !== input.worldId ||
|
|
376
|
-
operation.head !== appliedEvent.committedHead
|
|
591
|
+
operation.head !== appliedEvent.committedHead ||
|
|
592
|
+
commit.timelineRevision?.restoresHead !== requestedRestoresHead ||
|
|
593
|
+
commit.timelineRevision.replacesHead !== requestedEvent.committedHead ||
|
|
594
|
+
commit.timelineRevision.requestFingerprint !==
|
|
595
|
+
playerRevisionRequestFingerprint(input, requestedEvent.committedHead, requestedRestoresHead))
|
|
377
596
|
throw new PlayCallChainError("The replacement exchange ID is already used by another commit.");
|
|
378
597
|
return {
|
|
379
598
|
outcome: "revised",
|
|
@@ -406,23 +625,14 @@ export class PlayCallChain {
|
|
|
406
625
|
async #applyPlayerRevision(input) {
|
|
407
626
|
const request = input.request;
|
|
408
627
|
const sourceContext = input.sourceContexts[input.selectedContextIndex];
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const requestFingerprint =
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
schema: "narraeon.timeline-revision-request/v1",
|
|
418
|
-
chainId: request.chainId,
|
|
419
|
-
eventId: request.eventId,
|
|
420
|
-
selectedHead: input.selectedEvent.committedHead,
|
|
421
|
-
restoresHead,
|
|
422
|
-
replacementExchangeId: request.replacementExchangeId,
|
|
423
|
-
replacementText: request.replacementText,
|
|
424
|
-
}))
|
|
425
|
-
.digest("hex");
|
|
628
|
+
const restoresHead = playerRevisionRestoresHead(sourceContext, input.selectedEventIndex);
|
|
629
|
+
const freshPreparation = request.continuation === "fresh_context"
|
|
630
|
+
? await this.#prepareFreshPlayerRevision(request, restoresHead)
|
|
631
|
+
: null;
|
|
632
|
+
const timelineGeneration = playerRevisionTimelineGeneration(input.source.timelineGeneration, request.operationId, request.worldId, request.continuation);
|
|
633
|
+
const requestFingerprint = playerRevisionRequestFingerprint(request, input.selectedEvent.committedHead, restoresHead);
|
|
634
|
+
const prefixEvents = structuredClone(sourceContext.events.slice(0, input.selectedEventIndex));
|
|
635
|
+
const transcript = transcriptThroughEvents(sourceContext.transcript, prefixEvents);
|
|
426
636
|
const outcome = await this.#worlds.reviseTimeline({
|
|
427
637
|
operationId: request.operationId,
|
|
428
638
|
worldId: request.worldId,
|
|
@@ -432,9 +642,6 @@ export class PlayCallChain {
|
|
|
432
642
|
replacementText: request.replacementText,
|
|
433
643
|
requestFingerprint,
|
|
434
644
|
});
|
|
435
|
-
const prefixEvents = structuredClone(sourceContext.events.slice(0, input.selectedEventIndex));
|
|
436
|
-
const transcript = transcriptThroughEvents(sourceContext.transcript, prefixEvents);
|
|
437
|
-
transcript.push({ kind: "player", text: request.replacementText });
|
|
438
645
|
const events = [
|
|
439
646
|
...prefixEvents,
|
|
440
647
|
{
|
|
@@ -452,22 +659,66 @@ export class PlayCallChain {
|
|
|
452
659
|
this.#worlds.recoverEndpoint(request.worldId, restoresHead),
|
|
453
660
|
this.#worlds.bindPlayCallChain(request.worldId),
|
|
454
661
|
]);
|
|
662
|
+
transcript.push(...playerInputAppend({
|
|
663
|
+
history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
|
|
664
|
+
messageId,
|
|
665
|
+
exactText,
|
|
666
|
+
])),
|
|
667
|
+
checkpoint: selected.narrativeCheckpoint,
|
|
668
|
+
text: request.replacementText,
|
|
669
|
+
locale: this.#compiler.locale,
|
|
670
|
+
checkpointAvailable: sourceContext.tools.some(({ name }) => name === "world_checkpoint"),
|
|
671
|
+
}));
|
|
455
672
|
if (binding.parentHead !== outcome.head)
|
|
456
673
|
throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
|
|
674
|
+
if (request.continuation === "fresh_context") {
|
|
675
|
+
if (freshPreparation === null)
|
|
676
|
+
throw new PlayCallChainError("The fresh model context was not prepared before committing the timeline revision.");
|
|
677
|
+
const revised = await this.#buildFreshPlayerRevision({
|
|
678
|
+
request,
|
|
679
|
+
sourceContext,
|
|
680
|
+
sourceContexts: input.sourceContexts,
|
|
681
|
+
selectedContextIndex: input.selectedContextIndex,
|
|
682
|
+
prefixEvents,
|
|
683
|
+
baseline,
|
|
684
|
+
selected,
|
|
685
|
+
binding,
|
|
686
|
+
restoresHead,
|
|
687
|
+
outcomeHead: outcome.head,
|
|
688
|
+
timelineGeneration,
|
|
689
|
+
freshPreparation,
|
|
690
|
+
});
|
|
691
|
+
await this.#worlds.playTimeline.persist(structuredClone(revised));
|
|
692
|
+
const activeId = this.#worldChains.get(request.worldId);
|
|
693
|
+
if (activeId !== undefined)
|
|
694
|
+
this.#active.delete(activeId);
|
|
695
|
+
this.#active.delete(sourceContext.chainId);
|
|
696
|
+
this.#worldChains.delete(request.worldId);
|
|
697
|
+
return {
|
|
698
|
+
outcome: "revised",
|
|
699
|
+
worldId: request.worldId,
|
|
700
|
+
playCallChain: projectView(revised),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
457
703
|
const documents = restorePlayDocuments(binding.files, sourceContext, prefixEvents);
|
|
458
704
|
const revised = {
|
|
705
|
+
continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
|
|
459
706
|
schemaVersion: 3,
|
|
460
707
|
kind: "play_call_chain",
|
|
461
|
-
chainId:
|
|
708
|
+
chainId: playerRevisionChainId(sourceContext.chainId, request.operationId, request.worldId, request.continuation),
|
|
462
709
|
worldId: request.worldId,
|
|
463
710
|
previousContexts: [],
|
|
464
711
|
previousChainId: input.selectedContextIndex === 0
|
|
465
712
|
? null
|
|
466
713
|
: input.sourceContexts[input.selectedContextIndex - 1].chainId,
|
|
467
|
-
timelineGeneration
|
|
714
|
+
timelineGeneration,
|
|
468
715
|
baselineHead: sourceContext.baselineHead,
|
|
469
716
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
470
717
|
parentHead: outcome.head,
|
|
718
|
+
promptRuns: playPromptRunsThroughEvents(sourceContext, events),
|
|
719
|
+
...(sourceContext.presetFiles === undefined
|
|
720
|
+
? {}
|
|
721
|
+
: { presetFiles: structuredClone(sourceContext.presetFiles) }),
|
|
471
722
|
playPreset: structuredClone(sourceContext.playPreset),
|
|
472
723
|
...(sourceContext.followups === undefined
|
|
473
724
|
? {}
|
|
@@ -513,6 +764,158 @@ export class PlayCallChain {
|
|
|
513
764
|
playCallChain: projectView(revised),
|
|
514
765
|
};
|
|
515
766
|
}
|
|
767
|
+
async #prepareFreshPlayerRevision(request, restoresHead) {
|
|
768
|
+
const binding = await this.#worlds.bindPlayCallChainAt(request.worldId, restoresHead);
|
|
769
|
+
const documents = new FileNativePlayDocuments(binding.files);
|
|
770
|
+
const documentMaintenance = await this.#bindDocumentMaintenance(documents, request.worldId, binding.parentHead);
|
|
771
|
+
const compilation = this.#compiler.compilePlayCallChain({
|
|
772
|
+
extensionControls: await this.#worlds.extensionControls(request.worldId, request.freshContext.playPreset, this.#compiler.locale),
|
|
773
|
+
endpoint: {
|
|
774
|
+
id: `${request.worldId}:${binding.parentHead}`,
|
|
775
|
+
commit: binding.parentHead,
|
|
776
|
+
},
|
|
777
|
+
hostBinding: structuredClone(request.freshContext.hostBinding),
|
|
778
|
+
world: {
|
|
779
|
+
controlFingerprint: fingerprintControl(binding.files),
|
|
780
|
+
documentSnapshot: documents.snapshot,
|
|
781
|
+
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
782
|
+
history: structuredClone(binding.history),
|
|
783
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
784
|
+
documentMaintenance,
|
|
785
|
+
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
786
|
+
},
|
|
787
|
+
playerInputPlacement: "append",
|
|
788
|
+
playerInput: request.replacementText,
|
|
789
|
+
modelBinding: structuredClone(request.freshContext.modelBinding),
|
|
790
|
+
}, request.freshContext.playPreset);
|
|
791
|
+
const bootstrap = structuredClone(compilation.bootstrap);
|
|
792
|
+
documents.bindBootstrap(bootstrap);
|
|
793
|
+
return {
|
|
794
|
+
binding,
|
|
795
|
+
bootstrap,
|
|
796
|
+
tools: structuredClone(compilation.toolUniverse),
|
|
797
|
+
followups: structuredClone(compilation.followups ?? []),
|
|
798
|
+
authorization: documents.authorizationCheckpoint(),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
async #buildFreshPlayerRevision(input) {
|
|
802
|
+
const { request, sourceContext } = input;
|
|
803
|
+
let previousChainId = input.selectedContextIndex === 0
|
|
804
|
+
? null
|
|
805
|
+
: input.sourceContexts[input.selectedContextIndex - 1].chainId;
|
|
806
|
+
if (input.prefixEvents.length > 0) {
|
|
807
|
+
const prefixTranscript = transcriptThroughEvents(sourceContext.transcript, input.prefixEvents);
|
|
808
|
+
const completedKeys = completedToolKeys(input.prefixEvents);
|
|
809
|
+
const documents = restorePlayDocuments(input.binding.files, sourceContext, input.prefixEvents);
|
|
810
|
+
const prefix = {
|
|
811
|
+
continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
|
|
812
|
+
schemaVersion: 3,
|
|
813
|
+
kind: "play_call_chain",
|
|
814
|
+
chainId: derivedChainId(sourceContext.chainId, "timeline-revision-prefix:" + request.operationId, request.worldId),
|
|
815
|
+
worldId: request.worldId,
|
|
816
|
+
previousContexts: [],
|
|
817
|
+
previousChainId,
|
|
818
|
+
timelineGeneration: input.timelineGeneration,
|
|
819
|
+
baselineHead: sourceContext.baselineHead,
|
|
820
|
+
baselineHistoryLength: sourceContext.baselineHistoryLength ?? input.baseline.history.length,
|
|
821
|
+
parentHead: input.restoresHead,
|
|
822
|
+
promptRuns: playPromptRunsThroughEvents(sourceContext, input.prefixEvents),
|
|
823
|
+
...(sourceContext.presetFiles === undefined
|
|
824
|
+
? {}
|
|
825
|
+
: { presetFiles: structuredClone(sourceContext.presetFiles) }),
|
|
826
|
+
playPreset: structuredClone(sourceContext.playPreset),
|
|
827
|
+
...(sourceContext.followups === undefined
|
|
828
|
+
? {}
|
|
829
|
+
: { followups: structuredClone(sourceContext.followups) }),
|
|
830
|
+
...(sourceContext.playPresetScriptsEnabled === undefined
|
|
831
|
+
? {}
|
|
832
|
+
: {
|
|
833
|
+
playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
|
|
834
|
+
}),
|
|
835
|
+
...(sourceContext.modelBinding === undefined
|
|
836
|
+
? {}
|
|
837
|
+
: { modelBinding: structuredClone(sourceContext.modelBinding) }),
|
|
838
|
+
status: "ready",
|
|
839
|
+
canRetry: false,
|
|
840
|
+
bootstrap: structuredClone(sourceContext.bootstrap),
|
|
841
|
+
tools: structuredClone(sourceContext.tools),
|
|
842
|
+
transcript: prefixTranscript,
|
|
843
|
+
events: structuredClone(input.prefixEvents),
|
|
844
|
+
completedTools: sourceContext.completedTools
|
|
845
|
+
.filter(({ key }) => completedKeys.has(key))
|
|
846
|
+
.map((item) => structuredClone(item)),
|
|
847
|
+
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, input.prefixEvents, documents.authorizationCheckpoint()),
|
|
848
|
+
changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, input.baseline.state, input.selected.state),
|
|
849
|
+
nextMaterials: structuredClone(input.selected.additionalMaterials),
|
|
850
|
+
nextEventId: Math.max(0, ...input.prefixEvents.map(({ id }) => id)) + 1,
|
|
851
|
+
exchange: completedAssistantExchange(input.prefixEvents),
|
|
852
|
+
lastRequest: null,
|
|
853
|
+
lastRequestAttempt: 0,
|
|
854
|
+
lastFailure: null,
|
|
855
|
+
updatedAt: Date.now(),
|
|
856
|
+
};
|
|
857
|
+
await this.#worlds.playTimeline.persistDetached(structuredClone(prefix));
|
|
858
|
+
previousChainId = prefix.chainId;
|
|
859
|
+
}
|
|
860
|
+
const fresh = input.freshPreparation;
|
|
861
|
+
return {
|
|
862
|
+
schemaVersion: 3,
|
|
863
|
+
kind: "play_call_chain",
|
|
864
|
+
chainId: playerRevisionChainId(sourceContext.chainId, request.operationId, request.worldId, request.continuation),
|
|
865
|
+
worldId: request.worldId,
|
|
866
|
+
previousContexts: [],
|
|
867
|
+
previousChainId,
|
|
868
|
+
timelineGeneration: input.timelineGeneration,
|
|
869
|
+
baselineHead: input.restoresHead,
|
|
870
|
+
baselineHistoryLength: Object.keys(fresh.binding.history).length,
|
|
871
|
+
parentHead: input.outcomeHead,
|
|
872
|
+
playPreset: {
|
|
873
|
+
id: request.freshContext.playPreset.id,
|
|
874
|
+
name: request.freshContext.playPreset.name,
|
|
875
|
+
revision: request.freshContext.playPreset.revision,
|
|
876
|
+
},
|
|
877
|
+
followups: structuredClone(fresh.followups),
|
|
878
|
+
playPresetScriptsEnabled: request.freshContext.playPreset.scriptsEnabled,
|
|
879
|
+
presetFiles: structuredClone(request.freshContext.playPreset.files),
|
|
880
|
+
modelBinding: structuredClone(request.freshContext.modelBinding),
|
|
881
|
+
status: "ready",
|
|
882
|
+
canRetry: false,
|
|
883
|
+
bootstrap: structuredClone(fresh.bootstrap),
|
|
884
|
+
tools: structuredClone(fresh.tools),
|
|
885
|
+
transcript: playerInputAppend({
|
|
886
|
+
history: fresh.binding.history,
|
|
887
|
+
checkpoint: fresh.binding.narrativeCheckpoint,
|
|
888
|
+
text: request.replacementText,
|
|
889
|
+
locale: this.#compiler.locale,
|
|
890
|
+
checkpointAvailable: fresh.tools.some(({ name }) => name === "world_checkpoint"),
|
|
891
|
+
}),
|
|
892
|
+
events: [
|
|
893
|
+
{
|
|
894
|
+
id: 1,
|
|
895
|
+
kind: "player",
|
|
896
|
+
exchangeId: request.replacementExchangeId,
|
|
897
|
+
text: request.replacementText,
|
|
898
|
+
context: "fresh",
|
|
899
|
+
committedHead: input.outcomeHead,
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
completedTools: [],
|
|
903
|
+
documentAuthorizationCheckpoints: [
|
|
904
|
+
{
|
|
905
|
+
afterEventId: 0,
|
|
906
|
+
authorization: structuredClone(fresh.authorization),
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
changedDocuments: [],
|
|
910
|
+
nextMaterials: structuredClone(input.binding.additionalMaterials),
|
|
911
|
+
nextEventId: 2,
|
|
912
|
+
exchange: 0,
|
|
913
|
+
lastRequest: null,
|
|
914
|
+
lastRequestAttempt: 0,
|
|
915
|
+
lastFailure: null,
|
|
916
|
+
updatedAt: Date.now(),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
516
919
|
async #forkSelectionToDerivedWorld(input) {
|
|
517
920
|
if (input.targetWorldRoot === undefined) {
|
|
518
921
|
const existing = await this.#readPersisted(input.targetWorldId);
|
|
@@ -574,6 +977,7 @@ export class PlayCallChain {
|
|
|
574
977
|
previousChainId = chainId;
|
|
575
978
|
}
|
|
576
979
|
const derived = {
|
|
980
|
+
continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
|
|
577
981
|
schemaVersion: 3,
|
|
578
982
|
kind: "play_call_chain",
|
|
579
983
|
chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
|
|
@@ -584,6 +988,10 @@ export class PlayCallChain {
|
|
|
584
988
|
baselineHead: sourceContext.baselineHead,
|
|
585
989
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
586
990
|
parentHead: input.sourceHead,
|
|
991
|
+
promptRuns: playPromptRunsThroughEvents(sourceContext, events),
|
|
992
|
+
...(sourceContext.presetFiles === undefined
|
|
993
|
+
? {}
|
|
994
|
+
: { presetFiles: structuredClone(sourceContext.presetFiles) }),
|
|
587
995
|
playPreset: structuredClone(sourceContext.playPreset),
|
|
588
996
|
...(sourceContext.followups === undefined
|
|
589
997
|
? {}
|
|
@@ -626,6 +1034,26 @@ export class PlayCallChain {
|
|
|
626
1034
|
source: sourceContext,
|
|
627
1035
|
target: structuredClone(derived),
|
|
628
1036
|
});
|
|
1037
|
+
if (input.targetWorldRoot !== undefined) {
|
|
1038
|
+
const retainedContexts = [
|
|
1039
|
+
...input.sourceContexts.slice(0, input.selectedContextIndex),
|
|
1040
|
+
{ ...sourceContext, events: input.sourceEvents },
|
|
1041
|
+
];
|
|
1042
|
+
await this.#artifacts?.stageFork?.({
|
|
1043
|
+
sourceWorldId: input.sourceWorldId,
|
|
1044
|
+
targetWorldId: input.targetWorldId,
|
|
1045
|
+
targetWorldRoot: input.targetWorldRoot,
|
|
1046
|
+
timeline: retainedContexts.flatMap((context) => context.events.flatMap((event) => event.kind === "assistant" && event.committedHead !== undefined
|
|
1047
|
+
? [
|
|
1048
|
+
{
|
|
1049
|
+
contextId: context.continuityContextId ?? context.chainId,
|
|
1050
|
+
eventId: event.id,
|
|
1051
|
+
head: event.committedHead,
|
|
1052
|
+
},
|
|
1053
|
+
]
|
|
1054
|
+
: [])),
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
629
1057
|
return projectView(derived);
|
|
630
1058
|
}
|
|
631
1059
|
forgetWorld(worldId) {
|
|
@@ -645,7 +1073,13 @@ export class PlayCallChain {
|
|
|
645
1073
|
const exchange = session.exchange + 1;
|
|
646
1074
|
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
647
1075
|
...session.transcript,
|
|
648
|
-
{
|
|
1076
|
+
...playerInputAppend({
|
|
1077
|
+
history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
1078
|
+
checkpoint: session.narrativeCheckpoint,
|
|
1079
|
+
text: playerText,
|
|
1080
|
+
locale: this.#compiler.locale,
|
|
1081
|
+
checkpointAvailable: currentPlayPrompt(session).tools.some(({ name }) => name === "world_checkpoint"),
|
|
1082
|
+
}),
|
|
649
1083
|
]);
|
|
650
1084
|
const operationKey = `player:${exchangeId}`;
|
|
651
1085
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
@@ -653,6 +1087,7 @@ export class PlayCallChain {
|
|
|
653
1087
|
schemaVersion: 1,
|
|
654
1088
|
kind: "play_advance",
|
|
655
1089
|
advanceKind: "player",
|
|
1090
|
+
playContext: session.continuityContextId ?? session.chainId,
|
|
656
1091
|
worldId: session.worldId,
|
|
657
1092
|
chainId: session.chainId,
|
|
658
1093
|
advanceId: playerOperationId,
|
|
@@ -699,6 +1134,9 @@ export class PlayCallChain {
|
|
|
699
1134
|
}
|
|
700
1135
|
async #settlePlayerAdvance(session, advance) {
|
|
701
1136
|
const outcome = await this.#worlds.commitPlayStep({
|
|
1137
|
+
...(advance.playContext === undefined
|
|
1138
|
+
? {}
|
|
1139
|
+
: { playContext: advance.playContext }),
|
|
702
1140
|
operationId: advance.operationId,
|
|
703
1141
|
worldId: session.worldId,
|
|
704
1142
|
parentHead: advance.parentHead,
|
|
@@ -725,7 +1163,7 @@ export class PlayCallChain {
|
|
|
725
1163
|
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its player prefix.");
|
|
726
1164
|
session.transcript = [
|
|
727
1165
|
...session.transcript.slice(0, advance.transcriptStart),
|
|
728
|
-
|
|
1166
|
+
...structuredClone(advance.nextRequest.appended.slice(advance.transcriptStart)),
|
|
729
1167
|
];
|
|
730
1168
|
session.nextEventId = advance.eventId + 1;
|
|
731
1169
|
session.parentHead = outcome.head;
|
|
@@ -741,7 +1179,7 @@ export class PlayCallChain {
|
|
|
741
1179
|
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
742
1180
|
await this.#worlds.playAdvances.markSettled(advance, outcome.head);
|
|
743
1181
|
}
|
|
744
|
-
#prepareResponseSettlement(session, advance, response) {
|
|
1182
|
+
async #prepareResponseSettlement(session, advance, response) {
|
|
745
1183
|
const existing = session.events.find(({ id }) => id === advance.eventId);
|
|
746
1184
|
if (existing?.kind !== "assistant")
|
|
747
1185
|
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
@@ -775,7 +1213,7 @@ export class PlayCallChain {
|
|
|
775
1213
|
const trailingEvents = [];
|
|
776
1214
|
let nextEventId = advance.eventId + 1;
|
|
777
1215
|
for (const call of calls) {
|
|
778
|
-
const item = prepareTool(session, workingTools, advance.exchange, call);
|
|
1216
|
+
const item = prepareTool(session, workingTools, advance.exchange, call, this.#compiler.locale);
|
|
779
1217
|
prepared.push(item);
|
|
780
1218
|
trailingEvents.push({
|
|
781
1219
|
id: nextEventId++,
|
|
@@ -789,7 +1227,72 @@ export class PlayCallChain {
|
|
|
789
1227
|
trailingEvents.push(structuredClone(item.event));
|
|
790
1228
|
}
|
|
791
1229
|
const stateChanges = session.documents.stateChanges();
|
|
1230
|
+
// Resolve once after the entire tool batch. Store the prediction with the
|
|
1231
|
+
// prepared fact so crash recovery publishes exactly the same receipt.
|
|
1232
|
+
const writes = [...workingTools.values()].filter(({ result }) => result.candidateWrite !== undefined);
|
|
1233
|
+
let worldClock;
|
|
1234
|
+
if (writes.length > 0) {
|
|
1235
|
+
try {
|
|
1236
|
+
// Author controls may change while an existing Provider context stays
|
|
1237
|
+
// frozen. Predictions and clock capture use the controls active now.
|
|
1238
|
+
const binding = await this.#worlds.bindPlayCallChain(session.worldId);
|
|
1239
|
+
const snapshot = WorldDocumentStore.open({
|
|
1240
|
+
layout: "world_state",
|
|
1241
|
+
files: [
|
|
1242
|
+
...session.documents.snapshot.files.filter(({ path }) => path.startsWith("state/")),
|
|
1243
|
+
...Object.entries(binding.files)
|
|
1244
|
+
.filter(([path]) => path.startsWith("control/"))
|
|
1245
|
+
.map(([path, contents]) => ({ path, contents })),
|
|
1246
|
+
],
|
|
1247
|
+
});
|
|
1248
|
+
worldClock = readDeclaredWorldClock(snapshot);
|
|
1249
|
+
const { coverage, maintenance } = this.#compiler.inspectWorldMaterials({
|
|
1250
|
+
controlFingerprint: "receipt",
|
|
1251
|
+
documentSnapshot: snapshot,
|
|
1252
|
+
history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
1253
|
+
additionalMaterials: session.nextMaterials,
|
|
1254
|
+
});
|
|
1255
|
+
for (const { result } of writes) {
|
|
1256
|
+
const write = result.candidateWrite;
|
|
1257
|
+
write.freshContextCoverage = renderFreshContextCoverage(snapshot, coverage, write.shortRef, this.#compiler.locale);
|
|
1258
|
+
const previous = session.documents.committedSnapshot.query({
|
|
1259
|
+
kind: "read_document",
|
|
1260
|
+
document: { shortRef: write.shortRef },
|
|
1261
|
+
});
|
|
1262
|
+
const advice = renderWriteSizeAdvice(maintenance, write.shortRef, previous.kind === "read_document"
|
|
1263
|
+
? Buffer.byteLength(previous.body, "utf8")
|
|
1264
|
+
: null, this.#compiler.locale);
|
|
1265
|
+
if (advice.length > 0)
|
|
1266
|
+
write.freshContextCoverage += `\n${advice}`;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
// A coverage prediction must never reject an otherwise valid write.
|
|
1271
|
+
const reason = error instanceof Error ? error.message : "Material selection failed";
|
|
1272
|
+
for (const { result } of writes)
|
|
1273
|
+
result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
792
1276
|
const visibleText = responseKind === "narrative";
|
|
1277
|
+
const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1278
|
+
event.committedHead !== undefined &&
|
|
1279
|
+
event.responseKind === "narrative");
|
|
1280
|
+
const previousPlayer = session.events.findLast((event) => event.kind === "player");
|
|
1281
|
+
const checkpointRequested = visibleText &&
|
|
1282
|
+
session.events.some((event) => event.id >
|
|
1283
|
+
Math.max(previousNarrative?.id ?? 0, previousPlayer?.id ?? 0) &&
|
|
1284
|
+
event.kind === "tool_result" &&
|
|
1285
|
+
event.name === "world_checkpoint" &&
|
|
1286
|
+
event.ok);
|
|
1287
|
+
const narrativeCheckpoint = checkpointRequested
|
|
1288
|
+
? {
|
|
1289
|
+
contextId: session.chainId,
|
|
1290
|
+
completedPlayerRounds: completedPlayerRounds({
|
|
1291
|
+
...Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
1292
|
+
"message.pending.narrator": text,
|
|
1293
|
+
}),
|
|
1294
|
+
}
|
|
1295
|
+
: undefined;
|
|
793
1296
|
const assistantItem = {
|
|
794
1297
|
kind: "assistant",
|
|
795
1298
|
text,
|
|
@@ -803,6 +1306,10 @@ export class PlayCallChain {
|
|
|
803
1306
|
};
|
|
804
1307
|
return {
|
|
805
1308
|
assistantEvent,
|
|
1309
|
+
playContext: session.continuityContextId ?? session.chainId,
|
|
1310
|
+
...(worldClock === undefined || stateChanges.length === 0
|
|
1311
|
+
? {}
|
|
1312
|
+
: { worldClock }),
|
|
806
1313
|
trailingEvents,
|
|
807
1314
|
transcriptStart: session.transcript.length,
|
|
808
1315
|
transcriptAppend: [
|
|
@@ -812,11 +1319,15 @@ export class PlayCallChain {
|
|
|
812
1319
|
? [assistantItem]
|
|
813
1320
|
: []),
|
|
814
1321
|
...prepared.map(({ transcript }) => structuredClone(transcript)),
|
|
1322
|
+
...(calls.length > 0 && hasText
|
|
1323
|
+
? [toolStepNotice(this.#compiler.locale)]
|
|
1324
|
+
: []),
|
|
815
1325
|
],
|
|
816
1326
|
completedTools: [...workingTools.values()].map((item) => structuredClone(item)),
|
|
817
1327
|
authorizationCheckpoint: session.documents.authorizationCheckpoint(),
|
|
818
1328
|
stateChanges: structuredClone(stateChanges),
|
|
819
1329
|
visibleText,
|
|
1330
|
+
...(narrativeCheckpoint === undefined ? {} : { narrativeCheckpoint }),
|
|
820
1331
|
};
|
|
821
1332
|
}
|
|
822
1333
|
async #settleResponseAdvance(session, advance, prepared) {
|
|
@@ -838,6 +1349,12 @@ export class PlayCallChain {
|
|
|
838
1349
|
let committedHead;
|
|
839
1350
|
if (settlement.visibleText || settlement.stateChanges.length > 0) {
|
|
840
1351
|
const outcome = await this.#worlds.commitPlayStep({
|
|
1352
|
+
...(settlement.playContext === undefined
|
|
1353
|
+
? {}
|
|
1354
|
+
: { playContext: settlement.playContext }),
|
|
1355
|
+
...(settlement.worldClock === undefined
|
|
1356
|
+
? {}
|
|
1357
|
+
: { worldClock: settlement.worldClock }),
|
|
841
1358
|
operationId: advance.operationId,
|
|
842
1359
|
worldId: session.worldId,
|
|
843
1360
|
parentHead: advance.parentHead,
|
|
@@ -851,9 +1368,20 @@ export class PlayCallChain {
|
|
|
851
1368
|
: [],
|
|
852
1369
|
nextMaterials: structuredClone(session.nextMaterials),
|
|
853
1370
|
stateChanges: structuredClone(settlement.stateChanges),
|
|
1371
|
+
...(settlement.narrativeCheckpoint === undefined
|
|
1372
|
+
? {}
|
|
1373
|
+
: { narrativeCheckpoint: settlement.narrativeCheckpoint }),
|
|
854
1374
|
});
|
|
855
1375
|
committedHead = outcome.head;
|
|
856
1376
|
settlement.assistantEvent.committedHead = outcome.head;
|
|
1377
|
+
if (settlement.narrativeCheckpoint !== undefined) {
|
|
1378
|
+
settlement.assistantEvent.checkpoint = true;
|
|
1379
|
+
session.narrativeCheckpoint = {
|
|
1380
|
+
...settlement.narrativeCheckpoint,
|
|
1381
|
+
head: outcome.head,
|
|
1382
|
+
historyMessageId: `message.${outcome.head.slice("commit:".length)}.1.narrator`,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
857
1385
|
crashAtPlayAdvanceEdge("after_authority_accepted");
|
|
858
1386
|
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
859
1387
|
}
|
|
@@ -901,6 +1429,8 @@ export class PlayCallChain {
|
|
|
901
1429
|
}
|
|
902
1430
|
mergeChangedDocuments(session, settlement.stateChanges);
|
|
903
1431
|
}
|
|
1432
|
+
if (settlement.stateChanges.length > 0)
|
|
1433
|
+
await this.#bindDocumentMaintenance(session.documents, session.worldId, session.parentHead);
|
|
904
1434
|
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
905
1435
|
session.canRetry = false;
|
|
906
1436
|
session.lastFailure = null;
|
|
@@ -1007,6 +1537,16 @@ export class PlayCallChain {
|
|
|
1007
1537
|
let response;
|
|
1008
1538
|
try {
|
|
1009
1539
|
this.#observeInvocation(session.chainId, "waiting", { dispatches: 1 });
|
|
1540
|
+
if (modelHost.previewRequest !== undefined &&
|
|
1541
|
+
(request.exchange === 1 ||
|
|
1542
|
+
request.exchange === session.promptRuns?.at(-1)?.exchange)) {
|
|
1543
|
+
try {
|
|
1544
|
+
await this.#worlds.playTimeline.recordInitialEncoding(session.worldId, session.chainId, modelHost.previewRequest(request), request.exchange);
|
|
1545
|
+
}
|
|
1546
|
+
catch {
|
|
1547
|
+
// Optional diagnostics never interrupt a model request or alter its cache key.
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1010
1550
|
response = await modelHost.exchange(request, {
|
|
1011
1551
|
signal,
|
|
1012
1552
|
onDelta: (delta) => {
|
|
@@ -1024,6 +1564,7 @@ export class PlayCallChain {
|
|
|
1024
1564
|
else if (delta.kind === "tool")
|
|
1025
1565
|
event.toolFragment = `${event.toolFragment ?? ""}${delta.text}`;
|
|
1026
1566
|
touch(session);
|
|
1567
|
+
this.changed(session.worldId);
|
|
1027
1568
|
notifyAssistantDelta(observer, {
|
|
1028
1569
|
eventId: event.id,
|
|
1029
1570
|
kind: delta.kind,
|
|
@@ -1056,7 +1597,8 @@ export class PlayCallChain {
|
|
|
1056
1597
|
});
|
|
1057
1598
|
session.status = "interrupted";
|
|
1058
1599
|
const requiresFreshContext = error instanceof ModelHostContinuationError ||
|
|
1059
|
-
error instanceof ModelHostOutcomeUnknownError
|
|
1600
|
+
error instanceof ModelHostOutcomeUnknownError ||
|
|
1601
|
+
modelHostFailureRequiresFreshContext(error);
|
|
1060
1602
|
session.canRetry =
|
|
1061
1603
|
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
1062
1604
|
session.lastFailure = message;
|
|
@@ -1069,7 +1611,7 @@ export class PlayCallChain {
|
|
|
1069
1611
|
try {
|
|
1070
1612
|
await this.#worlds.playAdvances.recordProviderCompleted(advance, durableResponse);
|
|
1071
1613
|
crashAtPlayAdvanceEdge("after_provider_completed");
|
|
1072
|
-
const settlement = this.#prepareResponseSettlement(session, advance, durableResponse);
|
|
1614
|
+
const settlement = await this.#prepareResponseSettlement(session, advance, durableResponse);
|
|
1073
1615
|
await this.#worlds.playAdvances.recordSettlementPrepared(advance, settlement);
|
|
1074
1616
|
crashAtPlayAdvanceEdge("after_settlement_prepared");
|
|
1075
1617
|
const failedTools = settlement.trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
@@ -1123,7 +1665,7 @@ export class PlayCallChain {
|
|
|
1123
1665
|
message: "The play call chain recovered during a later model exchange and completed.",
|
|
1124
1666
|
details: { parentHead: session.parentHead },
|
|
1125
1667
|
});
|
|
1126
|
-
if ((session.followups?.length ?? 0) > 0) {
|
|
1668
|
+
if ((currentPlayPrompt(session).followups?.length ?? 0) > 0) {
|
|
1127
1669
|
// Follow-up artifacts are part of this player-visible invocation.
|
|
1128
1670
|
// Keep `running` ephemeral: the settled main response remains the
|
|
1129
1671
|
// durable recovery point if this Runtime process stops here.
|
|
@@ -1141,6 +1683,7 @@ export class PlayCallChain {
|
|
|
1141
1683
|
message: "The player cancelled the remaining follow-up generation after the narrative was committed.",
|
|
1142
1684
|
});
|
|
1143
1685
|
touch(session);
|
|
1686
|
+
this.changed(session.worldId);
|
|
1144
1687
|
}
|
|
1145
1688
|
session.status = "ready";
|
|
1146
1689
|
await this.#persist(session);
|
|
@@ -1222,9 +1765,13 @@ export class PlayCallChain {
|
|
|
1222
1765
|
* trace and the chain still returns to `ready`.
|
|
1223
1766
|
*/
|
|
1224
1767
|
async #runFollowups(session, modelHost, signal, observer) {
|
|
1225
|
-
const followups = session.followups ?? [];
|
|
1768
|
+
const followups = currentPlayPrompt(session).followups ?? [];
|
|
1226
1769
|
if (this.#artifacts === undefined || followups.length === 0)
|
|
1227
1770
|
return;
|
|
1771
|
+
const reply = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1772
|
+
event.committedHead === session.parentHead);
|
|
1773
|
+
if (reply === undefined)
|
|
1774
|
+
throw new PlayCallChainError("Follow-up reply is not committed");
|
|
1228
1775
|
const observeFollowupDelta = (delta) => {
|
|
1229
1776
|
this.#observeInvocation(session.chainId, "followup", {
|
|
1230
1777
|
...(delta.kind === "reasoning"
|
|
@@ -1258,13 +1805,15 @@ export class PlayCallChain {
|
|
|
1258
1805
|
try {
|
|
1259
1806
|
await runPlayFollowupRequests({
|
|
1260
1807
|
artifacts: this.#artifacts,
|
|
1808
|
+
controls: this.#worlds.extensionRequests,
|
|
1809
|
+
presentations: frozenFollowupPresentations(session),
|
|
1261
1810
|
modelHost,
|
|
1262
1811
|
followups,
|
|
1263
|
-
bootstrap: session.bootstrap,
|
|
1812
|
+
bootstrap: currentPlayPrompt(session).bootstrap,
|
|
1264
1813
|
// Captured once: every followup is dispatched against this exact
|
|
1265
1814
|
// prefix, so none of them observes another one's prompt or output.
|
|
1266
1815
|
prefix: structuredClone(session.transcript),
|
|
1267
|
-
toolStrategy: session.bootstrap.toolStrategy,
|
|
1816
|
+
toolStrategy: currentPlayPrompt(session).bootstrap.toolStrategy,
|
|
1268
1817
|
context: {
|
|
1269
1818
|
// A follow-up produces no world endpoint of its own, so it starts and
|
|
1270
1819
|
// ends on the settled exchange's head. Reconciliation keeps panels
|
|
@@ -1272,9 +1821,15 @@ export class PlayCallChain {
|
|
|
1272
1821
|
worldId: session.worldId,
|
|
1273
1822
|
parentHead: session.parentHead,
|
|
1274
1823
|
operationId: followupOperationId(session),
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1824
|
+
attachment: {
|
|
1825
|
+
contextId: session.continuityContextId ?? session.chainId,
|
|
1826
|
+
eventId: reply.id,
|
|
1827
|
+
head: session.parentHead,
|
|
1828
|
+
runId: `${session.chainId}:send:${session.promptRuns?.at(-1)?.firstEventId ?? reply.id}`,
|
|
1829
|
+
},
|
|
1830
|
+
playPresetId: currentPlayPrompt(session).playPreset.id,
|
|
1831
|
+
playPresetRevision: currentPlayPrompt(session).playPreset.revision,
|
|
1832
|
+
playPresetScriptsEnabled: currentPlayPrompt(session).playPresetScriptsEnabled ?? true,
|
|
1278
1833
|
},
|
|
1279
1834
|
head: session.parentHead,
|
|
1280
1835
|
maxOutputTokens: modelHost.binding().maxOutputTokens,
|
|
@@ -1331,8 +1886,24 @@ export class PlayCallChain {
|
|
|
1331
1886
|
throw new PlayCallChainError("The call-chain record differs from the current world endpoint; use a fresh context.");
|
|
1332
1887
|
return session;
|
|
1333
1888
|
}
|
|
1889
|
+
async #bindDocumentMaintenance(documents, worldId, head) {
|
|
1890
|
+
const inspection = await this.#worlds.inspectDocumentMaintenance(worldId, head);
|
|
1891
|
+
if (inspection.documentMaintenanceUnavailableReason === undefined)
|
|
1892
|
+
documents.bindMaintenance(inspection.documentMaintenance, this.#compiler.locale);
|
|
1893
|
+
else
|
|
1894
|
+
documents.bindMaintenanceUnavailable(inspection.documentMaintenanceUnavailableReason, this.#compiler.locale);
|
|
1895
|
+
return inspection.documentMaintenance;
|
|
1896
|
+
}
|
|
1334
1897
|
async #hydrateSession(persisted, advance) {
|
|
1335
|
-
const
|
|
1898
|
+
const currentBinding = await this.#worlds.bindPlayCallChain(persisted.worldId);
|
|
1899
|
+
// A completed chain still describes its own immutable endpoint after a
|
|
1900
|
+
// world revision. Restore its reads against that state, not the revised
|
|
1901
|
+
// current tree. An unsettled advance keeps the existing crash-recovery
|
|
1902
|
+
// path because Authority may already contain its prepared result.
|
|
1903
|
+
const binding = currentBinding.parentHead !== persisted.parentHead &&
|
|
1904
|
+
advance?.settled !== null
|
|
1905
|
+
? await this.#worlds.bindPlayCallChainAt(persisted.worldId, persisted.parentHead)
|
|
1906
|
+
: currentBinding;
|
|
1336
1907
|
let documents;
|
|
1337
1908
|
try {
|
|
1338
1909
|
documents = restorePlayDocuments(binding.files, persisted, persisted.events);
|
|
@@ -1344,12 +1915,14 @@ export class PlayCallChain {
|
|
|
1344
1915
|
documents = new FileNativePlayDocuments(binding.files);
|
|
1345
1916
|
documents.restoreAuthorizationCheckpoint(prepared.authorizationCheckpoint);
|
|
1346
1917
|
}
|
|
1918
|
+
await this.#bindDocumentMaintenance(documents, persisted.worldId, binding.parentHead);
|
|
1347
1919
|
return {
|
|
1348
1920
|
...structuredClone(persisted),
|
|
1349
1921
|
documentAuthorizationCheckpoints: persisted.documentAuthorizationCheckpoints?.map((checkpoint) => structuredClone(checkpoint)) ?? [],
|
|
1350
1922
|
nextMaterials: structuredClone(binding.additionalMaterials),
|
|
1351
1923
|
documents,
|
|
1352
1924
|
history: historyEntries(binding.history),
|
|
1925
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
1353
1926
|
completedToolMap: new Map(persisted.completedTools.map((item) => [
|
|
1354
1927
|
item.key,
|
|
1355
1928
|
structuredClone(item),
|
|
@@ -1378,7 +1951,7 @@ export class PlayCallChain {
|
|
|
1378
1951
|
}
|
|
1379
1952
|
let settlement = current.settlementPrepared?.settlement;
|
|
1380
1953
|
if (settlement === undefined) {
|
|
1381
|
-
settlement = this.#prepareResponseSettlement(session, current.base, current.providerCompleted.response);
|
|
1954
|
+
settlement = await this.#prepareResponseSettlement(session, current.base, current.providerCompleted.response);
|
|
1382
1955
|
await this.#worlds.playAdvances.recordSettlementPrepared(current.base, settlement);
|
|
1383
1956
|
}
|
|
1384
1957
|
await this.#settleResponseAdvance(session, current.base, settlement);
|
|
@@ -1442,11 +2015,11 @@ function createRequest(session, modelHost, exchange, appended = session.transcri
|
|
|
1442
2015
|
}
|
|
1443
2016
|
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1444
2017
|
return {
|
|
1445
|
-
bootstrap: structuredClone(session.bootstrap),
|
|
1446
|
-
tools: structuredClone(session.tools),
|
|
1447
|
-
toolUniverse: structuredClone(session.tools),
|
|
1448
|
-
allowedTools: session.tools.map(({ name }) => name),
|
|
1449
|
-
toolStrategy: session.bootstrap.toolStrategy,
|
|
2018
|
+
bootstrap: structuredClone(currentPlayPrompt(session).bootstrap),
|
|
2019
|
+
tools: structuredClone(currentPlayPrompt(session).tools),
|
|
2020
|
+
toolUniverse: structuredClone(currentPlayPrompt(session).tools),
|
|
2021
|
+
allowedTools: currentPlayPrompt(session).tools.map(({ name }) => name),
|
|
2022
|
+
toolStrategy: currentPlayPrompt(session).bootstrap.toolStrategy,
|
|
1450
2023
|
appended: structuredClone([...appended]),
|
|
1451
2024
|
requestId: "play_call_chain",
|
|
1452
2025
|
operationId: session.chainId,
|
|
@@ -1467,9 +2040,12 @@ function finalizePreparedReceipts(settlement) {
|
|
|
1467
2040
|
const candidateWrite = completed.result.candidateWrite;
|
|
1468
2041
|
if (!completed.result.ok || candidateWrite === undefined)
|
|
1469
2042
|
continue;
|
|
1470
|
-
const
|
|
2043
|
+
const status = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
|
|
1471
2044
|
? `@${candidateWrite.shortRef} write succeeded`
|
|
1472
2045
|
: `@${candidateWrite.shortRef} unchanged`;
|
|
2046
|
+
const markdown = [status, candidateWrite.freshContextCoverage]
|
|
2047
|
+
.filter(Boolean)
|
|
2048
|
+
.join("\n");
|
|
1473
2049
|
completed.result.markdown = markdown;
|
|
1474
2050
|
delete completed.result.candidateWrite;
|
|
1475
2051
|
const callId = completed.key.slice(completed.key.indexOf(":") + 1);
|
|
@@ -1503,7 +2079,7 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
1503
2079
|
session.history.push({ path, contents: message.exactText });
|
|
1504
2080
|
}
|
|
1505
2081
|
}
|
|
1506
|
-
function prepareTool(session, working, exchange, call) {
|
|
2082
|
+
function prepareTool(session, working, exchange, call, locale) {
|
|
1507
2083
|
const key = `${exchange}:${call.id}`;
|
|
1508
2084
|
const signature = JSON.stringify(call.arguments);
|
|
1509
2085
|
const completed = working.get(key);
|
|
@@ -1521,7 +2097,7 @@ function prepareTool(session, working, exchange, call) {
|
|
|
1521
2097
|
};
|
|
1522
2098
|
}
|
|
1523
2099
|
else if (!callChainToolNames.has(call.name) ||
|
|
1524
|
-
!session.tools.some(({ name }) => name === call.name)) {
|
|
2100
|
+
!currentPlayPrompt(session).tools.some(({ name }) => name === call.name)) {
|
|
1525
2101
|
result = {
|
|
1526
2102
|
ok: false,
|
|
1527
2103
|
failureKind: "protocol",
|
|
@@ -1530,7 +2106,24 @@ function prepareTool(session, working, exchange, call) {
|
|
|
1530
2106
|
}
|
|
1531
2107
|
else {
|
|
1532
2108
|
try {
|
|
1533
|
-
result =
|
|
2109
|
+
result =
|
|
2110
|
+
call.name === "world_checkpoint"
|
|
2111
|
+
? typeof call.arguments === "object" &&
|
|
2112
|
+
call.arguments !== null &&
|
|
2113
|
+
!Array.isArray(call.arguments) &&
|
|
2114
|
+
Object.keys(call.arguments).length === 0
|
|
2115
|
+
? {
|
|
2116
|
+
ok: true,
|
|
2117
|
+
markdown: locale === "zh-CN"
|
|
2118
|
+
? "检查点已登记,将在本轮最终叙事提交后生效。玩家随后可以选择开启全新上下文。"
|
|
2119
|
+
: "Checkpoint registered. It becomes effective when this turn’s final narrative commits. The player can then choose a fresh context.",
|
|
2120
|
+
}
|
|
2121
|
+
: {
|
|
2122
|
+
ok: false,
|
|
2123
|
+
failureKind: "protocol",
|
|
2124
|
+
markdown: "world_checkpoint requires an empty argument object.",
|
|
2125
|
+
}
|
|
2126
|
+
: session.documents.execute(call, session.history);
|
|
1534
2127
|
}
|
|
1535
2128
|
catch (error) {
|
|
1536
2129
|
result = {
|
|
@@ -1595,9 +2188,10 @@ function restorePlayDocuments(files, context, events) {
|
|
|
1595
2188
|
const documents = new FileNativePlayDocuments(files);
|
|
1596
2189
|
try {
|
|
1597
2190
|
const checkpoint = documentAuthorizationThroughEvents(context, events);
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
2191
|
+
const runs = playPromptRunsThroughEvents(context, events);
|
|
2192
|
+
if (checkpoint === undefined || runs.length > 0)
|
|
2193
|
+
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
2194
|
+
if (checkpoint !== undefined)
|
|
1601
2195
|
documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
|
|
1602
2196
|
}
|
|
1603
2197
|
catch (error) {
|
|
@@ -1618,6 +2212,7 @@ function documentAuthorizationThroughEvents(context, events) {
|
|
|
1618
2212
|
}
|
|
1619
2213
|
function independentContextCopy(source) {
|
|
1620
2214
|
const context = structuredClone(source);
|
|
2215
|
+
context.continuityContextId ??= source.chainId;
|
|
1621
2216
|
delete context.derivedFrom;
|
|
1622
2217
|
delete context.branchedBeforePlayer;
|
|
1623
2218
|
return context;
|
|
@@ -1644,7 +2239,7 @@ function projectContext(context) {
|
|
|
1644
2239
|
? {}
|
|
1645
2240
|
: { baselineHistoryLength: context.baselineHistoryLength }),
|
|
1646
2241
|
parentHead: context.parentHead,
|
|
1647
|
-
playPreset: structuredClone(context.playPreset),
|
|
2242
|
+
playPreset: structuredClone(recordedPlayPrompt(context).playPreset),
|
|
1648
2243
|
status: context.status,
|
|
1649
2244
|
canRetry: context.canRetry,
|
|
1650
2245
|
events: context.events
|
|
@@ -1691,6 +2286,57 @@ function projectView(session) {
|
|
|
1691
2286
|
previousContexts: (session.previousContexts ?? []).map((context) => projectContext(context)),
|
|
1692
2287
|
};
|
|
1693
2288
|
}
|
|
2289
|
+
function projectContextReads(session) {
|
|
2290
|
+
const completed = new Map(session.completedTools.map((tool) => [tool.key, tool.result]));
|
|
2291
|
+
let exchange = 0;
|
|
2292
|
+
const calls = new Map();
|
|
2293
|
+
const reads = [];
|
|
2294
|
+
for (const event of session.events) {
|
|
2295
|
+
if (event.kind === "assistant") {
|
|
2296
|
+
exchange = event.exchange;
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (event.kind === "tool_call" && event.name === "context_read") {
|
|
2300
|
+
const ref = isRecord(event.arguments) && typeof event.arguments.ref === "string"
|
|
2301
|
+
? event.arguments.ref
|
|
2302
|
+
: "(invalid ref)";
|
|
2303
|
+
calls.set(event.callId, {
|
|
2304
|
+
key: `${exchange}:${event.callId}`,
|
|
2305
|
+
ref,
|
|
2306
|
+
});
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
if (event.kind !== "tool_result" || event.name !== "context_read")
|
|
2310
|
+
continue;
|
|
2311
|
+
const call = calls.get(event.callId);
|
|
2312
|
+
if (call === undefined)
|
|
2313
|
+
continue;
|
|
2314
|
+
const result = completed.get(call.key);
|
|
2315
|
+
reads.push({
|
|
2316
|
+
eventId: event.id,
|
|
2317
|
+
callId: event.callId,
|
|
2318
|
+
ref: call.ref,
|
|
2319
|
+
ok: event.ok,
|
|
2320
|
+
complete: result === undefined ? null : completedContextReadIsComplete(result),
|
|
2321
|
+
markdown: result?.markdown ?? null,
|
|
2322
|
+
locator: result?.readAuthorization?.locator ?? null,
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
return reads;
|
|
2326
|
+
}
|
|
2327
|
+
function completedContextReadIsComplete(result) {
|
|
2328
|
+
if (result.readAuthorization === undefined)
|
|
2329
|
+
return /(?:^|\n)Complete: yes(?:\n|$)/u.test(result.markdown);
|
|
2330
|
+
const legacyAuthorization = result.readAuthorization;
|
|
2331
|
+
if (!isRecord(legacyAuthorization) || !isRecord(legacyAuthorization.page))
|
|
2332
|
+
return true;
|
|
2333
|
+
return (typeof legacyAuthorization.page.end === "number" &&
|
|
2334
|
+
typeof legacyAuthorization.page.total === "number" &&
|
|
2335
|
+
legacyAuthorization.page.end === legacyAuthorization.page.total);
|
|
2336
|
+
}
|
|
2337
|
+
function isRecord(value) {
|
|
2338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2339
|
+
}
|
|
1694
2340
|
function eventsThroughHead(events, head) {
|
|
1695
2341
|
const selectedIndex = events.findIndex((event) => (event.kind === "player" || event.kind === "assistant") &&
|
|
1696
2342
|
event.committedHead === head);
|
|
@@ -1708,6 +2354,11 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1708
2354
|
let cursor = 0;
|
|
1709
2355
|
for (const [index, event] of events.entries()) {
|
|
1710
2356
|
if (event.kind === "player") {
|
|
2357
|
+
const marker = transcript[cursor];
|
|
2358
|
+
if (isPlayerRoundMarker(marker)) {
|
|
2359
|
+
result.push(structuredClone(marker));
|
|
2360
|
+
cursor += 1;
|
|
2361
|
+
}
|
|
1711
2362
|
const item = transcript[cursor];
|
|
1712
2363
|
if (item?.kind !== "player" || item.text !== event.text)
|
|
1713
2364
|
throw new PlayCallChainError("Source call-chain events do not match the model transcript, so a fork cannot be created safely.");
|
|
@@ -1716,6 +2367,9 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1716
2367
|
continue;
|
|
1717
2368
|
}
|
|
1718
2369
|
if (event.kind === "assistant" && event.status === "completed") {
|
|
2370
|
+
const notice = transcript[cursor];
|
|
2371
|
+
const hasContinuationNotice = notice?.kind === "runtime_notice" && notice.notice === "continuation";
|
|
2372
|
+
const item = transcript[cursor + (hasContinuationNotice ? 1 : 0)];
|
|
1719
2373
|
const hasToolCall = events
|
|
1720
2374
|
.slice(index + 1)
|
|
1721
2375
|
.find((candidate) => candidate.kind === "player" ||
|
|
@@ -1723,12 +2377,17 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1723
2377
|
candidate.kind === "tool_call");
|
|
1724
2378
|
const recorded = event.text.trim().length > 0 ||
|
|
1725
2379
|
hasToolCall?.kind === "tool_call" ||
|
|
1726
|
-
|
|
2380
|
+
item?.kind === "assistant";
|
|
1727
2381
|
if (!recorded)
|
|
1728
2382
|
continue;
|
|
1729
|
-
const item = transcript[cursor];
|
|
1730
2383
|
if (item?.kind !== "assistant" || item.text !== event.text)
|
|
1731
2384
|
throw new PlayCallChainError("Source call-chain responses do not match the model transcript, so a fork cannot be created safely.");
|
|
2385
|
+
// A continuation notice belongs to the generation it precedes, never
|
|
2386
|
+
// to a fork ending at the previously completed narrative.
|
|
2387
|
+
if (hasContinuationNotice) {
|
|
2388
|
+
result.push(structuredClone(notice));
|
|
2389
|
+
cursor += 1;
|
|
2390
|
+
}
|
|
1732
2391
|
result.push(structuredClone(item));
|
|
1733
2392
|
cursor += 1;
|
|
1734
2393
|
continue;
|
|
@@ -1739,6 +2398,13 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1739
2398
|
throw new PlayCallChainError("Source call-chain tool results do not match the model transcript, so a fork cannot be created safely.");
|
|
1740
2399
|
result.push(structuredClone(item));
|
|
1741
2400
|
cursor += 1;
|
|
2401
|
+
// A tool-step notice follows the entire settled batch and remains part
|
|
2402
|
+
// of its closure even when the selected endpoint stops at that batch.
|
|
2403
|
+
const notice = transcript[cursor];
|
|
2404
|
+
if (notice?.kind === "runtime_notice" && notice.notice === "tool_step") {
|
|
2405
|
+
result.push(structuredClone(notice));
|
|
2406
|
+
cursor += 1;
|
|
2407
|
+
}
|
|
1742
2408
|
}
|
|
1743
2409
|
}
|
|
1744
2410
|
return result;
|
|
@@ -1769,6 +2435,55 @@ function derivedChainId(sourceChainId, branchIdentity, targetWorldId) {
|
|
|
1769
2435
|
.digest("hex")
|
|
1770
2436
|
.slice(0, 40)}`;
|
|
1771
2437
|
}
|
|
2438
|
+
function playerRevisionRestoresHead(source, selectedEventIndex) {
|
|
2439
|
+
let restoresHead = source.baselineHead;
|
|
2440
|
+
for (const event of source.events.slice(0, selectedEventIndex))
|
|
2441
|
+
if ((event.kind === "player" || event.kind === "assistant") &&
|
|
2442
|
+
event.committedHead !== undefined)
|
|
2443
|
+
restoresHead = event.committedHead;
|
|
2444
|
+
return restoresHead;
|
|
2445
|
+
}
|
|
2446
|
+
function playerRevisionRequestFingerprint(request, selectedHead, restoresHead) {
|
|
2447
|
+
const base = {
|
|
2448
|
+
schema: "narraeon.timeline-revision-request/v1",
|
|
2449
|
+
chainId: request.chainId,
|
|
2450
|
+
eventId: request.eventId,
|
|
2451
|
+
selectedHead,
|
|
2452
|
+
restoresHead,
|
|
2453
|
+
replacementExchangeId: request.replacementExchangeId,
|
|
2454
|
+
replacementText: request.replacementText,
|
|
2455
|
+
};
|
|
2456
|
+
// Keep the released continuation fingerprint byte-for-byte recoverable;
|
|
2457
|
+
// the fresh strategy owns a new request shape with an explicit mode.
|
|
2458
|
+
const value = request.continuation === "continue_context"
|
|
2459
|
+
? base
|
|
2460
|
+
: {
|
|
2461
|
+
...base,
|
|
2462
|
+
schema: "narraeon.timeline-revision-request/v2",
|
|
2463
|
+
continuation: request.continuation,
|
|
2464
|
+
};
|
|
2465
|
+
return ("sha256:" + createHash("sha256").update(JSON.stringify(value)).digest("hex"));
|
|
2466
|
+
}
|
|
2467
|
+
function playerRevisionChainId(sourceChainId, operationId, worldId, continuation) {
|
|
2468
|
+
return derivedChainId(sourceChainId, `${continuation === "fresh_context" ? "timeline-revision-fresh" : "timeline-revision"}:${operationId}`, worldId);
|
|
2469
|
+
}
|
|
2470
|
+
function playerRevisionTimelineGeneration(sourceGeneration, operationId, worldId, continuation) {
|
|
2471
|
+
return `timeline:${createHash("sha256")
|
|
2472
|
+
.update(JSON.stringify({
|
|
2473
|
+
schema: "narraeon.player-revision-timeline-generation/v1",
|
|
2474
|
+
sourceGeneration,
|
|
2475
|
+
operationId,
|
|
2476
|
+
worldId,
|
|
2477
|
+
continuation,
|
|
2478
|
+
}))
|
|
2479
|
+
.digest("hex")
|
|
2480
|
+
.slice(0, 40)}`;
|
|
2481
|
+
}
|
|
2482
|
+
function completedAssistantExchange(events) {
|
|
2483
|
+
return Math.max(0, ...events
|
|
2484
|
+
.filter((event) => event.kind === "assistant" && event.status === "completed")
|
|
2485
|
+
.map(({ exchange }) => exchange));
|
|
2486
|
+
}
|
|
1772
2487
|
function historyEntries(history) {
|
|
1773
2488
|
return Object.entries(history).map(([path, contents]) => ({
|
|
1774
2489
|
path,
|
|
@@ -1813,3 +2528,41 @@ function crashAtPlayAdvanceEdge(edge) {
|
|
|
1813
2528
|
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_PLAY_ADVANCE_EDGE === edge)
|
|
1814
2529
|
throw new Error(`Simulated process exit at play advance edge: ${edge}`);
|
|
1815
2530
|
}
|
|
2531
|
+
function frozenFollowupPresentations(session) {
|
|
2532
|
+
const files = session.promptRuns?.at(-1)?.presetFiles ?? session.presetFiles;
|
|
2533
|
+
if (files === undefined)
|
|
2534
|
+
return {};
|
|
2535
|
+
const parsed = parsePlayPresetFiles(files);
|
|
2536
|
+
if (parsed.kind === "invalid")
|
|
2537
|
+
throw parsed.error;
|
|
2538
|
+
const definition = parsed.definition;
|
|
2539
|
+
return Object.fromEntries((currentPlayPrompt(session).followups ?? []).map((followup) => [
|
|
2540
|
+
followup.id,
|
|
2541
|
+
Object.fromEntries(followup.artifacts.flatMap((declaration) => {
|
|
2542
|
+
const resources = followup.frozenResources;
|
|
2543
|
+
const mount = resources?.mount ??
|
|
2544
|
+
definition.mounts.find((item) => item.channel === declaration.channel)?.mount;
|
|
2545
|
+
return mount === undefined
|
|
2546
|
+
? []
|
|
2547
|
+
: [
|
|
2548
|
+
[
|
|
2549
|
+
declaration.name,
|
|
2550
|
+
{
|
|
2551
|
+
...resources,
|
|
2552
|
+
declaration: structuredClone(declaration),
|
|
2553
|
+
files: resources?.files ??
|
|
2554
|
+
Object.fromEntries([
|
|
2555
|
+
declaration.renderer,
|
|
2556
|
+
declaration.regex,
|
|
2557
|
+
...(declaration.scripts ?? []),
|
|
2558
|
+
...(declaration.assets ?? []),
|
|
2559
|
+
].flatMap((path) => path === undefined
|
|
2560
|
+
? []
|
|
2561
|
+
: [[path, (resources?.files ?? files)[path]]])),
|
|
2562
|
+
mount,
|
|
2563
|
+
},
|
|
2564
|
+
],
|
|
2565
|
+
];
|
|
2566
|
+
})),
|
|
2567
|
+
]));
|
|
2568
|
+
}
|