narraeon 0.4.0 → 0.5.1
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-DG85hpWC.css +1 -0
- package/dist/web/assets/index-DfxvCjb6.js +1207 -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
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { authoringRequestPreviews } from "../authoring/AuthoringRequestSnapshot.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { emptyAggregatedModelUsage } from "../../protocol/modelUsage.js";
|
|
4
|
+
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, } from "../authoring/ModelToolConversation.js";
|
|
5
|
+
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
6
|
+
import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
7
|
+
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
8
|
+
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
9
|
+
import { WorldRevisionAuthoringTransaction, worldRevisionRuntimeContract, worldRevisionToolDefinitions, } from "./WorldRevisionAuthoringTransaction.js";
|
|
10
|
+
import { WorldRevisionWorkspaceError } from "./WorldRevisionWorkspace.js";
|
|
11
|
+
/** Conversation orchestration over the shared locked revision worktree. */
|
|
12
|
+
export class WorldRevisionSession {
|
|
13
|
+
#store;
|
|
14
|
+
#workspace;
|
|
15
|
+
#worlds;
|
|
16
|
+
#compiler;
|
|
17
|
+
#locale;
|
|
18
|
+
#bindModelHost;
|
|
19
|
+
#bindExistingModelHost;
|
|
20
|
+
#bindPlayPreset;
|
|
21
|
+
#preview;
|
|
22
|
+
#conversation = new ModelToolConversation();
|
|
23
|
+
#changed;
|
|
24
|
+
#mutationTail = Promise.resolve();
|
|
25
|
+
constructor(input) {
|
|
26
|
+
this.#changed =
|
|
27
|
+
input.changed ??
|
|
28
|
+
(() => {
|
|
29
|
+
/* No observers attached. */
|
|
30
|
+
});
|
|
31
|
+
this.#store = input.store;
|
|
32
|
+
this.#workspace = input.workspace;
|
|
33
|
+
this.#worlds = input.worlds;
|
|
34
|
+
this.#compiler = input.compiler;
|
|
35
|
+
this.#locale = input.locale;
|
|
36
|
+
this.#bindModelHost = input.bindModelHost;
|
|
37
|
+
this.#bindExistingModelHost = input.bindExistingModelHost;
|
|
38
|
+
this.#bindPlayPreset = input.bindPlayPreset;
|
|
39
|
+
this.#preview = input.preview;
|
|
40
|
+
}
|
|
41
|
+
async open(worldId) {
|
|
42
|
+
await this.#mutate(async () => {
|
|
43
|
+
await this.#recoverWorldSessions(worldId);
|
|
44
|
+
await this.#workspace.open(worldId);
|
|
45
|
+
});
|
|
46
|
+
return this.overview(worldId);
|
|
47
|
+
}
|
|
48
|
+
async activeEpoch(worldId) {
|
|
49
|
+
return this.#mutate(async () => {
|
|
50
|
+
const epoch = await this.#workspace.active(worldId);
|
|
51
|
+
return epoch === null ? null : this.#workspace.view(epoch, true);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async overview(worldId) {
|
|
55
|
+
return this.#mutate(async () => {
|
|
56
|
+
const activeEpoch = await this.#workspace.active(worldId);
|
|
57
|
+
const [sessions, sealedEpochs] = await Promise.all([
|
|
58
|
+
this.#recoverWorldSessions(worldId),
|
|
59
|
+
this.#workspace.sealed(worldId),
|
|
60
|
+
]);
|
|
61
|
+
const latest = sessions[0] ?? null;
|
|
62
|
+
const shownEpoch = activeEpoch ?? (await this.#workspace.latest(worldId));
|
|
63
|
+
return {
|
|
64
|
+
epoch: shownEpoch === null
|
|
65
|
+
? null
|
|
66
|
+
: this.#workspace.view(shownEpoch, activeEpoch?.epochId === shownEpoch.epochId),
|
|
67
|
+
sealedEpochs,
|
|
68
|
+
latest: latest === null ? null : this.#view(latest),
|
|
69
|
+
history: sessions.map(historyItem),
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
observeProgress(status) {
|
|
74
|
+
const selected = status.selected;
|
|
75
|
+
if (selected === null)
|
|
76
|
+
return status;
|
|
77
|
+
return {
|
|
78
|
+
...status,
|
|
79
|
+
selected: {
|
|
80
|
+
...selected,
|
|
81
|
+
progress: {
|
|
82
|
+
...selected.progress,
|
|
83
|
+
streaming: this.#conversation.streaming(selected.sessionId),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async status(worldId, sessionId) {
|
|
89
|
+
return this.#mutate(async () => {
|
|
90
|
+
const sessions = await this.#store.listSessions(worldId);
|
|
91
|
+
const selectedIndex = sessionId === undefined
|
|
92
|
+
? sessions.length === 0
|
|
93
|
+
? -1
|
|
94
|
+
: 0
|
|
95
|
+
: sessions.findIndex((session) => session.sessionId === sessionId);
|
|
96
|
+
let selected = selectedIndex < 0 ? null : sessions[selectedIndex];
|
|
97
|
+
if (selected !== null &&
|
|
98
|
+
!this.#conversation.isRunning(selected.sessionId) &&
|
|
99
|
+
(selected.runStatus === "running" ||
|
|
100
|
+
selected.pendingSettlement !== null)) {
|
|
101
|
+
selected = await this.#recoverIfNeeded(selected);
|
|
102
|
+
sessions[selectedIndex] = selected;
|
|
103
|
+
}
|
|
104
|
+
const active = await this.#workspace.active(worldId);
|
|
105
|
+
const epoch = active ?? (await this.#workspace.latest(worldId));
|
|
106
|
+
return {
|
|
107
|
+
revision: statusFingerprint(epoch, sessions),
|
|
108
|
+
selected: selected === null ? null : this.#statusView(selected),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async readSession(worldId, sessionId) {
|
|
113
|
+
return this.#mutate(async () => {
|
|
114
|
+
const session = await this.#store.readSession(sessionId);
|
|
115
|
+
assertWorld(session, worldId);
|
|
116
|
+
return this.#view(await this.#recoverIfNeeded(session));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async deleteSession(worldId, sessionId) {
|
|
120
|
+
await this.#mutate(async () => {
|
|
121
|
+
if (this.#conversation.isRunning(sessionId))
|
|
122
|
+
throw new WorldRevisionWorkspaceError("A running world-revision conversation cannot be deleted");
|
|
123
|
+
const session = await this.#store.readSession(sessionId);
|
|
124
|
+
assertWorld(session, worldId);
|
|
125
|
+
await this.#recoverIfNeeded(session);
|
|
126
|
+
await this.#store.deleteSession(worldId, sessionId);
|
|
127
|
+
});
|
|
128
|
+
return this.overview(worldId);
|
|
129
|
+
}
|
|
130
|
+
async replaceFiles(input) {
|
|
131
|
+
await this.#mutate(async () => {
|
|
132
|
+
await this.#assertNoRunningConversation(input.worldId);
|
|
133
|
+
await this.#workspace.replace(input);
|
|
134
|
+
});
|
|
135
|
+
return this.overview(input.worldId);
|
|
136
|
+
}
|
|
137
|
+
async rollback(input) {
|
|
138
|
+
const result = await this.#mutate(async () => {
|
|
139
|
+
await this.#assertNoRunningConversation(input.worldId);
|
|
140
|
+
return this.#workspace.rollback(input);
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
status: result.status,
|
|
144
|
+
changeSetId: result.changeSetId,
|
|
145
|
+
path: result.path,
|
|
146
|
+
changes: result.changes,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async apply(input) {
|
|
150
|
+
await this.#mutate(async () => {
|
|
151
|
+
await this.#assertNoRunningConversation(input.worldId);
|
|
152
|
+
const epoch = await this.#workspace.active(input.worldId);
|
|
153
|
+
if (epoch?.epochId !== input.epochId)
|
|
154
|
+
throw new WorldRevisionWorkspaceError("The world-revision epoch is no longer active");
|
|
155
|
+
await this.#validateApply(epoch);
|
|
156
|
+
await this.#workspace.apply(input);
|
|
157
|
+
});
|
|
158
|
+
return this.overview(input.worldId);
|
|
159
|
+
}
|
|
160
|
+
async discard(input) {
|
|
161
|
+
await this.#mutate(async () => {
|
|
162
|
+
await this.#assertNoRunningConversation(input.worldId);
|
|
163
|
+
await this.#workspace.discard(input);
|
|
164
|
+
});
|
|
165
|
+
return this.overview(input.worldId);
|
|
166
|
+
}
|
|
167
|
+
async send(input) {
|
|
168
|
+
const message = requiredMessage(input.message);
|
|
169
|
+
const requestId = requiredRequestId(input.requestId);
|
|
170
|
+
const prepared = await this.#mutate(async () => {
|
|
171
|
+
let session;
|
|
172
|
+
if (input.continuation.kind === "fresh_context") {
|
|
173
|
+
session = await this.#store.findByCreationRequest(input.worldId, requestId);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
session = await this.#store.readSession(input.continuation.sessionId);
|
|
177
|
+
assertWorld(session, input.worldId);
|
|
178
|
+
}
|
|
179
|
+
if (session !== null) {
|
|
180
|
+
session = await this.#recoverIfNeeded(session);
|
|
181
|
+
if (session.completedRequestIds.includes(requestId))
|
|
182
|
+
return { session, run: null };
|
|
183
|
+
const running = this.#conversation.running(session.sessionId);
|
|
184
|
+
if (running !== null) {
|
|
185
|
+
if (running.requestId !== requestId)
|
|
186
|
+
throw new Error("Another message in this world-revision conversation is still running");
|
|
187
|
+
return { session, run: running.promise };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await this.#assertNoRunningConversation(input.worldId, session?.sessionId);
|
|
191
|
+
const epoch = await this.#workspace.open(input.worldId);
|
|
192
|
+
session ??= await this.#create(input.worldId, epoch, requestId);
|
|
193
|
+
if (session.epochId !== epoch.epochId) {
|
|
194
|
+
retargetSession(session, epoch, this.#locale());
|
|
195
|
+
await this.#store.saveSession(session);
|
|
196
|
+
}
|
|
197
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
198
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
199
|
+
throw new Error("No saved model connection matches this world-revision conversation");
|
|
200
|
+
const { playPreset, bootstrap } = await this.#compileCurrent(input.worldId, session.modelBinding);
|
|
201
|
+
session.schemaVersion = 2;
|
|
202
|
+
session.requests ??= [];
|
|
203
|
+
session.requests.push({
|
|
204
|
+
requestId,
|
|
205
|
+
modelItemStart: session.modelItems.length,
|
|
206
|
+
bootstrap,
|
|
207
|
+
playPreset: structuredClone(playPreset),
|
|
208
|
+
});
|
|
209
|
+
appendAuthoringUserMessage(session, message, requestId);
|
|
210
|
+
await this.#store.saveSession(session);
|
|
211
|
+
return {
|
|
212
|
+
session,
|
|
213
|
+
run: this.#startRun(session, host, requestId),
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
return prepared.run ?? this.#view(prepared.session);
|
|
217
|
+
}
|
|
218
|
+
async preview(worldId, sessionId) {
|
|
219
|
+
const session = sessionId === undefined ? null : await this.#store.readSession(sessionId);
|
|
220
|
+
if (session !== null)
|
|
221
|
+
assertWorld(session, worldId);
|
|
222
|
+
const host = session === null
|
|
223
|
+
? await this.#bindModelHost()
|
|
224
|
+
: await this.#bindExistingModelHost(session.modelBinding);
|
|
225
|
+
if (session !== null &&
|
|
226
|
+
!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
227
|
+
throw new Error("No compatible saved model connection matches this authoring conversation");
|
|
228
|
+
const { bootstrap } = await this.#compileCurrent(worldId, host.binding());
|
|
229
|
+
return bootstrap;
|
|
230
|
+
}
|
|
231
|
+
async #compileCurrent(worldId, modelBinding) {
|
|
232
|
+
const epoch = await this.#workspace.active(worldId);
|
|
233
|
+
const worlds = await this.#worlds.listWorlds();
|
|
234
|
+
const playPreset = await this.#bindPlayPreset();
|
|
235
|
+
const locale = this.#locale();
|
|
236
|
+
const bootstrap = this.#compiler.compileWorldRevision({
|
|
237
|
+
...(epoch === null
|
|
238
|
+
? {}
|
|
239
|
+
: { epoch: { id: epoch.epochId, baseHead: epoch.baseHead } }),
|
|
240
|
+
worldTitle: worlds.find((world) => world.worldId === worldId)?.title ?? worldId,
|
|
241
|
+
runtimeContract: worldRevisionRuntimeContract(locale),
|
|
242
|
+
authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
|
|
243
|
+
playPreset,
|
|
244
|
+
modelBinding: modelBinding,
|
|
245
|
+
tools: worldRevisionToolDefinitions(locale),
|
|
246
|
+
});
|
|
247
|
+
return { bootstrap, playPreset };
|
|
248
|
+
}
|
|
249
|
+
async cancel(sessionId) {
|
|
250
|
+
const running = this.#conversation.cancel(sessionId);
|
|
251
|
+
if (running !== null)
|
|
252
|
+
return running;
|
|
253
|
+
return this.#mutate(async () => this.#view(await this.#recoverIfNeeded(await this.#store.readSession(sessionId))));
|
|
254
|
+
}
|
|
255
|
+
async #create(worldId, epoch, creationRequestId) {
|
|
256
|
+
const [host, playPreset, worlds] = await Promise.all([
|
|
257
|
+
this.#bindModelHost(),
|
|
258
|
+
this.#bindPlayPreset(),
|
|
259
|
+
this.#worlds.listWorlds(),
|
|
260
|
+
]);
|
|
261
|
+
const worldTitle = worlds.find((world) => world.worldId === worldId)?.title ?? worldId;
|
|
262
|
+
const locale = this.#locale();
|
|
263
|
+
const modelBinding = host.binding();
|
|
264
|
+
const tools = worldRevisionToolDefinitions(locale);
|
|
265
|
+
const bootstrap = this.#compiler.compileWorldRevision({
|
|
266
|
+
epoch: { id: epoch.epochId, baseHead: epoch.baseHead },
|
|
267
|
+
worldTitle,
|
|
268
|
+
runtimeContract: worldRevisionRuntimeContract(locale),
|
|
269
|
+
authorPrompt: settingImprovementPromptForBinding(playPreset, locale),
|
|
270
|
+
playPreset,
|
|
271
|
+
modelBinding,
|
|
272
|
+
tools,
|
|
273
|
+
});
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const session = {
|
|
276
|
+
schemaVersion: 1,
|
|
277
|
+
sessionId: this.#store.createSessionId(),
|
|
278
|
+
worldId,
|
|
279
|
+
worldTitle,
|
|
280
|
+
epochId: epoch.epochId,
|
|
281
|
+
locale,
|
|
282
|
+
runStatus: "ready",
|
|
283
|
+
createdAt: now,
|
|
284
|
+
updatedAt: now,
|
|
285
|
+
creationRequestId,
|
|
286
|
+
bootstrap,
|
|
287
|
+
modelBinding,
|
|
288
|
+
playPreset: structuredClone(playPreset),
|
|
289
|
+
modelItems: [],
|
|
290
|
+
messages: [],
|
|
291
|
+
usage: emptyAggregatedModelUsage(),
|
|
292
|
+
exchange: 0,
|
|
293
|
+
toolCalls: 0,
|
|
294
|
+
activeRequestId: null,
|
|
295
|
+
completedRequestIds: [],
|
|
296
|
+
lastFailure: null,
|
|
297
|
+
authorization: null,
|
|
298
|
+
pendingSettlement: null,
|
|
299
|
+
};
|
|
300
|
+
await this.#store.saveSession(session);
|
|
301
|
+
return session;
|
|
302
|
+
}
|
|
303
|
+
#startRun(session, host, requestId) {
|
|
304
|
+
let activeTransaction = null;
|
|
305
|
+
return this.#conversation.run({
|
|
306
|
+
changed: (durable) => this.#changed(session.worldId, durable),
|
|
307
|
+
session,
|
|
308
|
+
host,
|
|
309
|
+
requestId,
|
|
310
|
+
save: (next) => this.#store.saveSession(next),
|
|
311
|
+
settleToolResponse: async (next, assistantItemIndex, calls) => {
|
|
312
|
+
activeTransaction = await this.#settleToolResponse(next, assistantItemIndex, calls, activeTransaction);
|
|
313
|
+
},
|
|
314
|
+
view: (next) => this.#view(next),
|
|
315
|
+
failure: {
|
|
316
|
+
exchangeLimit: "World revision exceeded 64 model exchanges for one user message",
|
|
317
|
+
cancelled: "The world-revision response was cancelled. Settled worktree changes were retained.",
|
|
318
|
+
interrupted: "The world-revision response was interrupted",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async #settleToolResponse(session, assistantItemIndex, calls, active) {
|
|
323
|
+
const epoch = await this.#requiredActiveEpoch(session);
|
|
324
|
+
const beforeRevision = epoch.revision;
|
|
325
|
+
session.pendingSettlement = {
|
|
326
|
+
phase: "response_confirmed",
|
|
327
|
+
assistantItemIndex,
|
|
328
|
+
epochId: epoch.epochId,
|
|
329
|
+
beforeRevision,
|
|
330
|
+
};
|
|
331
|
+
session.updatedAt = Date.now();
|
|
332
|
+
await this.#store.saveSession(session);
|
|
333
|
+
const binding = await this.#worlds.bindWorldRevision({
|
|
334
|
+
worldId: epoch.worldId,
|
|
335
|
+
epochId: epoch.epochId,
|
|
336
|
+
});
|
|
337
|
+
const transaction = active?.revision === beforeRevision
|
|
338
|
+
? active.transaction
|
|
339
|
+
: await this.#openTransaction(session, epoch, binding);
|
|
340
|
+
const rawResults = transaction.execute(calls);
|
|
341
|
+
const files = transaction.files();
|
|
342
|
+
const afterRevision = contentTreeFingerprint(files);
|
|
343
|
+
const toolResults = rawResults.map((result) => ({
|
|
344
|
+
...structuredClone(result),
|
|
345
|
+
changeSetId: result.isError || result.changes.length === 0
|
|
346
|
+
? null
|
|
347
|
+
: this.#store.createChangeSetId(),
|
|
348
|
+
}));
|
|
349
|
+
const prepared = {
|
|
350
|
+
phase: "publication_prepared",
|
|
351
|
+
assistantItemIndex,
|
|
352
|
+
epochId: epoch.epochId,
|
|
353
|
+
beforeRevision,
|
|
354
|
+
afterRevision,
|
|
355
|
+
afterFiles: files,
|
|
356
|
+
toolResults,
|
|
357
|
+
authorization: transaction.authorization(afterRevision),
|
|
358
|
+
};
|
|
359
|
+
session.pendingSettlement = prepared;
|
|
360
|
+
session.updatedAt = Date.now();
|
|
361
|
+
await this.#store.saveSession(session);
|
|
362
|
+
await this.#workspace.publishAi({
|
|
363
|
+
worldId: epoch.worldId,
|
|
364
|
+
epochId: epoch.epochId,
|
|
365
|
+
expectedRevision: beforeRevision,
|
|
366
|
+
afterRevision,
|
|
367
|
+
files,
|
|
368
|
+
toolResults,
|
|
369
|
+
});
|
|
370
|
+
finalizeSettlement(session, prepared);
|
|
371
|
+
session.updatedAt = Date.now();
|
|
372
|
+
await this.#store.saveSession(session);
|
|
373
|
+
return { revision: afterRevision, transaction };
|
|
374
|
+
}
|
|
375
|
+
async #openTransaction(session, epoch, binding) {
|
|
376
|
+
const maintenance = await this.#worlds.inspectDocumentMaintenance(epoch.worldId, epoch.baseHead);
|
|
377
|
+
return new WorldRevisionAuthoringTransaction({
|
|
378
|
+
baseFiles: epoch.files,
|
|
379
|
+
immutableBaseFiles: epoch.baseFiles,
|
|
380
|
+
locale: session.locale,
|
|
381
|
+
revision: epoch.revision,
|
|
382
|
+
authorization: session.authorization,
|
|
383
|
+
preview: (snapshot) => this.#preview({
|
|
384
|
+
snapshot,
|
|
385
|
+
modelBinding: session.modelBinding,
|
|
386
|
+
playPreset: session.requests?.at(-1)?.playPreset ?? session.playPreset,
|
|
387
|
+
binding,
|
|
388
|
+
epoch,
|
|
389
|
+
maintenance,
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async #validateApply(epoch) {
|
|
394
|
+
const [host, playPreset, binding, maintenance] = await Promise.all([
|
|
395
|
+
this.#bindModelHost(),
|
|
396
|
+
this.#bindPlayPreset(),
|
|
397
|
+
this.#worlds.bindWorldRevision({
|
|
398
|
+
worldId: epoch.worldId,
|
|
399
|
+
epochId: epoch.epochId,
|
|
400
|
+
}),
|
|
401
|
+
this.#worlds.inspectDocumentMaintenance(epoch.worldId, epoch.baseHead),
|
|
402
|
+
]);
|
|
403
|
+
const snapshot = WorldDocumentStore.open({
|
|
404
|
+
layout: "world_state",
|
|
405
|
+
files: epoch.files,
|
|
406
|
+
});
|
|
407
|
+
if (snapshot.status !== "usable")
|
|
408
|
+
throw new WorldRevisionWorkspaceError("Only a usable world-revision worktree can be applied");
|
|
409
|
+
try {
|
|
410
|
+
this.#preview({
|
|
411
|
+
snapshot,
|
|
412
|
+
modelBinding: host.binding(),
|
|
413
|
+
playPreset,
|
|
414
|
+
binding,
|
|
415
|
+
epoch,
|
|
416
|
+
maintenance,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
throw new WorldRevisionWorkspaceError(`World-revision Prompt Preview failed: ${error instanceof Error ? error.message : "unknown preview failure"}`, error instanceof Error ? { cause: error } : undefined);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async #recoverIfNeeded(session) {
|
|
424
|
+
if (this.#conversation.isRunning(session.sessionId))
|
|
425
|
+
return session;
|
|
426
|
+
if (session.pendingSettlement !== null) {
|
|
427
|
+
const pending = session.pendingSettlement;
|
|
428
|
+
if (pending.phase === "publication_prepared") {
|
|
429
|
+
const active = await this.#workspace.active(session.worldId);
|
|
430
|
+
if (active?.epochId === pending.epochId) {
|
|
431
|
+
try {
|
|
432
|
+
await this.#workspace.publishAi({
|
|
433
|
+
worldId: session.worldId,
|
|
434
|
+
epochId: pending.epochId,
|
|
435
|
+
expectedRevision: pending.beforeRevision,
|
|
436
|
+
afterRevision: pending.afterRevision,
|
|
437
|
+
files: pending.afterFiles,
|
|
438
|
+
toolResults: pending.toolResults,
|
|
439
|
+
});
|
|
440
|
+
finalizeSettlement(session, pending);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
finalizeUnconfirmed(session, pending.assistantItemIndex, error);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else
|
|
447
|
+
finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("The revision epoch ended before tool publication recovered"));
|
|
448
|
+
}
|
|
449
|
+
else
|
|
450
|
+
finalizeUnconfirmed(session, pending.assistantItemIndex, new Error("Runtime restarted before the complete tool response was prepared"));
|
|
451
|
+
}
|
|
452
|
+
if (session.runStatus !== "running" && session.pendingSettlement === null)
|
|
453
|
+
return session;
|
|
454
|
+
const requestId = session.activeRequestId;
|
|
455
|
+
session.runStatus = "interrupted";
|
|
456
|
+
session.activeRequestId = null;
|
|
457
|
+
if (requestId !== null)
|
|
458
|
+
session.completedRequestIds = rememberAuthoringRequest(session.completedRequestIds, requestId);
|
|
459
|
+
session.lastFailure =
|
|
460
|
+
"The previous model request stopped after its last settled worktree change. Continue this conversation when ready.";
|
|
461
|
+
session.updatedAt = Date.now();
|
|
462
|
+
await this.#store.saveSession(session);
|
|
463
|
+
return session;
|
|
464
|
+
}
|
|
465
|
+
async #requiredActiveEpoch(session) {
|
|
466
|
+
const epoch = await this.#workspace.active(session.worldId);
|
|
467
|
+
if (epoch?.epochId !== session.epochId)
|
|
468
|
+
throw new WorldRevisionWorkspaceError("The conversation's revision epoch is no longer active");
|
|
469
|
+
return epoch;
|
|
470
|
+
}
|
|
471
|
+
async #recoverWorldSessions(worldId) {
|
|
472
|
+
const sessions = [];
|
|
473
|
+
for (const stored of await this.#store.listSessions(worldId))
|
|
474
|
+
sessions.push(await this.#recoverIfNeeded(stored));
|
|
475
|
+
return sessions;
|
|
476
|
+
}
|
|
477
|
+
async #assertNoRunningConversation(worldId, exceptSessionId) {
|
|
478
|
+
const sessions = await this.#recoverWorldSessions(worldId);
|
|
479
|
+
if (sessions.some(({ sessionId }) => sessionId !== exceptSessionId &&
|
|
480
|
+
this.#conversation.isRunning(sessionId)))
|
|
481
|
+
throw new WorldRevisionWorkspaceError("Wait for or cancel the running AI response before changing the revision");
|
|
482
|
+
}
|
|
483
|
+
#view(session) {
|
|
484
|
+
const streaming = session.runStatus === "running"
|
|
485
|
+
? this.#conversation.streaming(session.sessionId)
|
|
486
|
+
: null;
|
|
487
|
+
return {
|
|
488
|
+
requestPreviews: authoringRequestPreviews(session),
|
|
489
|
+
sessionId: session.sessionId,
|
|
490
|
+
worldId: session.worldId,
|
|
491
|
+
epochId: session.epochId,
|
|
492
|
+
runStatus: streaming === null ? session.runStatus : "running",
|
|
493
|
+
messages: structuredClone(session.messages),
|
|
494
|
+
turns: conversationTurns(session),
|
|
495
|
+
usage: structuredClone(session.usage),
|
|
496
|
+
progress: {
|
|
497
|
+
exchange: session.exchange,
|
|
498
|
+
toolCalls: session.toolCalls,
|
|
499
|
+
streaming,
|
|
500
|
+
updatedAt: session.updatedAt,
|
|
501
|
+
},
|
|
502
|
+
lastFailure: session.lastFailure,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
#statusView(session) {
|
|
506
|
+
const streaming = session.runStatus === "running"
|
|
507
|
+
? this.#conversation.streaming(session.sessionId)
|
|
508
|
+
: null;
|
|
509
|
+
return {
|
|
510
|
+
sessionId: session.sessionId,
|
|
511
|
+
runStatus: streaming === null ? session.runStatus : "running",
|
|
512
|
+
progress: {
|
|
513
|
+
exchange: session.exchange,
|
|
514
|
+
toolCalls: session.toolCalls,
|
|
515
|
+
streaming,
|
|
516
|
+
updatedAt: session.updatedAt,
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async #mutate(operation) {
|
|
521
|
+
const previous = this.#mutationTail;
|
|
522
|
+
let release = () => undefined;
|
|
523
|
+
this.#mutationTail = new Promise((resolveMutation) => {
|
|
524
|
+
release = resolveMutation;
|
|
525
|
+
});
|
|
526
|
+
await previous;
|
|
527
|
+
try {
|
|
528
|
+
return await operation();
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
release();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function retargetSession(session, epoch, locale) {
|
|
536
|
+
const markdown = locale === "zh-CN"
|
|
537
|
+
? `# 新的世界修订 epoch\n\n上一轮修订已经应用或放弃。当前工作树是 ${epoch.epochId},从世界端点 ${epoch.baseHead} 重新建立。此前全部 list/search cursor 和 read 写授权已失效;修改前必须重新读取。`
|
|
538
|
+
: `# New world-revision epoch\n\nThe previous revision was applied or discarded. Worktree ${epoch.epochId} was rebuilt from world endpoint ${epoch.baseHead}. Every prior list/search cursor and read authorization is invalid; re-read before writing.`;
|
|
539
|
+
session.modelItems.push({
|
|
540
|
+
kind: "prompt_delta",
|
|
541
|
+
logicalMessages: [
|
|
542
|
+
{
|
|
543
|
+
role: "runtime_system",
|
|
544
|
+
markdown,
|
|
545
|
+
blocks: [
|
|
546
|
+
{
|
|
547
|
+
source: `runtime:world-revision-epoch:${epoch.epochId}`,
|
|
548
|
+
markdown,
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
});
|
|
554
|
+
session.epochId = epoch.epochId;
|
|
555
|
+
session.authorization = null;
|
|
556
|
+
session.pendingSettlement = null;
|
|
557
|
+
session.lastFailure = null;
|
|
558
|
+
session.updatedAt = Date.now();
|
|
559
|
+
}
|
|
560
|
+
function finalizeSettlement(session, pending) {
|
|
561
|
+
for (const result of pending.toolResults)
|
|
562
|
+
session.modelItems.push({
|
|
563
|
+
kind: "tool",
|
|
564
|
+
toolCallId: result.toolCallId,
|
|
565
|
+
markdown: result.markdown,
|
|
566
|
+
...(result.isError ? { isError: true } : {}),
|
|
567
|
+
changes: structuredClone(result.changes),
|
|
568
|
+
changeSetId: result.changeSetId,
|
|
569
|
+
});
|
|
570
|
+
session.authorization = structuredClone(pending.authorization);
|
|
571
|
+
session.toolCalls += pending.toolResults.length;
|
|
572
|
+
session.pendingSettlement = null;
|
|
573
|
+
}
|
|
574
|
+
function finalizeUnconfirmed(session, assistantItemIndex, error) {
|
|
575
|
+
const assistant = assistantAt(session, assistantItemIndex);
|
|
576
|
+
const message = error instanceof Error ? error.message : "unknown failure";
|
|
577
|
+
for (const call of assistant.toolCalls)
|
|
578
|
+
session.modelItems.push({
|
|
579
|
+
kind: "tool",
|
|
580
|
+
toolCallId: call.id,
|
|
581
|
+
markdown: `# Revision settlement could not be confirmed\n\n${message}. Re-read the active worktree before editing.`,
|
|
582
|
+
isError: true,
|
|
583
|
+
changes: [],
|
|
584
|
+
changeSetId: null,
|
|
585
|
+
});
|
|
586
|
+
session.authorization = null;
|
|
587
|
+
session.toolCalls += assistant.toolCalls.length;
|
|
588
|
+
session.pendingSettlement = null;
|
|
589
|
+
}
|
|
590
|
+
function assistantAt(session, itemIndex) {
|
|
591
|
+
const item = session.modelItems[itemIndex];
|
|
592
|
+
if (item?.kind !== "assistant" || item.toolCalls.length === 0)
|
|
593
|
+
throw new Error("Pending world-revision settlement is damaged");
|
|
594
|
+
return item;
|
|
595
|
+
}
|
|
596
|
+
function conversationTurns(session) {
|
|
597
|
+
const userMessages = session.messages.filter(({ role }) => role === "user");
|
|
598
|
+
const turns = [];
|
|
599
|
+
let userIndex = 0;
|
|
600
|
+
let exchange = 0;
|
|
601
|
+
let current = null;
|
|
602
|
+
let pending = new Map();
|
|
603
|
+
for (const [itemIndex, item] of session.modelItems.entries()) {
|
|
604
|
+
if (item.kind === "user") {
|
|
605
|
+
const persisted = userMessages[userIndex++];
|
|
606
|
+
current = {
|
|
607
|
+
id: persisted?.id ?? `${session.sessionId}:turn:${itemIndex}`,
|
|
608
|
+
user: persisted ??
|
|
609
|
+
{
|
|
610
|
+
id: `${session.sessionId}:user:${itemIndex}`,
|
|
611
|
+
role: "user",
|
|
612
|
+
text: item.text,
|
|
613
|
+
createdAt: session.createdAt,
|
|
614
|
+
},
|
|
615
|
+
exchanges: [],
|
|
616
|
+
};
|
|
617
|
+
turns.push(current);
|
|
618
|
+
pending = new Map();
|
|
619
|
+
}
|
|
620
|
+
else if (item.kind === "assistant" && current !== null) {
|
|
621
|
+
exchange += 1;
|
|
622
|
+
const toolCalls = item.toolCalls.map((call) => ({
|
|
623
|
+
callId: call.id,
|
|
624
|
+
name: call.name,
|
|
625
|
+
arguments: structuredClone(call.arguments),
|
|
626
|
+
result: null,
|
|
627
|
+
}));
|
|
628
|
+
current.exchanges.push({
|
|
629
|
+
id: `${session.sessionId}:exchange:${itemIndex}`,
|
|
630
|
+
exchange,
|
|
631
|
+
text: item.text,
|
|
632
|
+
...(item.reasoningContent === undefined
|
|
633
|
+
? {}
|
|
634
|
+
: { reasoning: item.reasoningContent }),
|
|
635
|
+
toolCalls,
|
|
636
|
+
});
|
|
637
|
+
pending = new Map(toolCalls.map((call) => [call.callId, call]));
|
|
638
|
+
}
|
|
639
|
+
else if (item.kind === "tool") {
|
|
640
|
+
const call = pending.get(item.toolCallId);
|
|
641
|
+
if (call?.result === null)
|
|
642
|
+
call.result = {
|
|
643
|
+
markdown: item.markdown,
|
|
644
|
+
isError: item.isError === true,
|
|
645
|
+
changes: structuredClone(item.changes),
|
|
646
|
+
changeSetId: item.changeSetId,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return turns;
|
|
651
|
+
}
|
|
652
|
+
function historyItem(session) {
|
|
653
|
+
const assistants = session.modelItems.filter((item) => item.kind === "assistant");
|
|
654
|
+
const changed = new Set(session.modelItems.flatMap((item) => item.kind === "tool" ? item.changes.map(({ path }) => path) : []));
|
|
655
|
+
return {
|
|
656
|
+
sessionId: session.sessionId,
|
|
657
|
+
epochId: session.epochId,
|
|
658
|
+
runStatus: session.runStatus,
|
|
659
|
+
createdAt: session.createdAt,
|
|
660
|
+
updatedAt: session.updatedAt,
|
|
661
|
+
excerpt: Array.from((session.messages.find(({ role }) => role === "user")?.text ?? "")
|
|
662
|
+
.replace(/\s+/gu, " ")
|
|
663
|
+
.trim())
|
|
664
|
+
.slice(0, 120)
|
|
665
|
+
.join(""),
|
|
666
|
+
turnCount: session.modelItems.filter((item) => item.kind === "user").length,
|
|
667
|
+
exchangeCount: assistants.length,
|
|
668
|
+
toolCallCount: assistants.reduce((count, item) => count + item.toolCalls.length, 0),
|
|
669
|
+
changedFileCount: changed.size,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function statusFingerprint(epoch, sessions) {
|
|
673
|
+
return createHash("sha256")
|
|
674
|
+
.update(JSON.stringify({
|
|
675
|
+
epoch: epoch === null
|
|
676
|
+
? null
|
|
677
|
+
: {
|
|
678
|
+
epochId: epoch.epochId,
|
|
679
|
+
lifecycle: epoch.lifecycle,
|
|
680
|
+
revision: epoch.revision,
|
|
681
|
+
updatedAt: epoch.updatedAt,
|
|
682
|
+
},
|
|
683
|
+
sessions: sessions.map((session) => ({
|
|
684
|
+
sessionId: session.sessionId,
|
|
685
|
+
epochId: session.epochId,
|
|
686
|
+
runStatus: session.runStatus,
|
|
687
|
+
updatedAt: session.updatedAt,
|
|
688
|
+
exchange: session.exchange,
|
|
689
|
+
toolCalls: session.toolCalls,
|
|
690
|
+
})),
|
|
691
|
+
}))
|
|
692
|
+
.digest("hex");
|
|
693
|
+
}
|
|
694
|
+
function assertWorld(session, worldId) {
|
|
695
|
+
if (session.worldId !== worldId)
|
|
696
|
+
throw new WorldRevisionWorkspaceError("World-revision conversation belongs to another world");
|
|
697
|
+
}
|
|
698
|
+
function requiredMessage(message) {
|
|
699
|
+
if (message.trim().length === 0)
|
|
700
|
+
throw new WorldRevisionWorkspaceError("A world-revision message cannot be empty");
|
|
701
|
+
if (message.length > 64 * 1024)
|
|
702
|
+
throw new WorldRevisionWorkspaceError("World-revision message is too large");
|
|
703
|
+
return message;
|
|
704
|
+
}
|
|
705
|
+
function requiredRequestId(requestId) {
|
|
706
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(requestId))
|
|
707
|
+
throw new WorldRevisionWorkspaceError("Invalid world-revision request ID");
|
|
708
|
+
return requestId;
|
|
709
|
+
}
|