narraeon 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +35 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +66 -29
- package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +81 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
- package/dist/node/runtime/play/PlayCallChain.js +904 -369
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
- package/dist/node/server/createServer.js +10 -2
- package/dist/web/assets/index-D5VqDvjY.js +1192 -0
- package/dist/web/assets/index-Xa0s35CJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { equalModelHostBinding, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "../model/ModelHost.js";
|
|
2
3
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
3
4
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
4
|
-
import { FileNativePlayDocuments, fingerprintControl,
|
|
5
|
+
import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTools.js";
|
|
5
6
|
const callChainToolNames = new Set([
|
|
7
|
+
"state_list",
|
|
8
|
+
"history_list",
|
|
9
|
+
// Kept executable only for contexts whose frozen tool universe contains it.
|
|
6
10
|
"context_list",
|
|
7
11
|
"context_search",
|
|
8
12
|
"context_read",
|
|
9
13
|
"world_patch",
|
|
10
14
|
"world_create",
|
|
11
15
|
]);
|
|
16
|
+
const projectedEventTail = 40;
|
|
12
17
|
export class PlayCallChainError extends Error {
|
|
13
18
|
constructor(message) {
|
|
14
19
|
super(message);
|
|
@@ -17,8 +22,10 @@ export class PlayCallChainError extends Error {
|
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
19
24
|
* The production play loop: one durable logical context, no Runtime-authored
|
|
20
|
-
* adjudication/narration phases
|
|
21
|
-
*
|
|
25
|
+
* adjudication/narration phases. Responses with tool calls are intermediate
|
|
26
|
+
* tool steps: their Provider-native payload remains appendable, while only
|
|
27
|
+
* their world changes may advance Authority. A tool-free response is the sole
|
|
28
|
+
* response shape whose non-empty text can become committed narrative.
|
|
22
29
|
*/
|
|
23
30
|
export class PlayCallChain {
|
|
24
31
|
#worlds;
|
|
@@ -27,6 +34,8 @@ export class PlayCallChain {
|
|
|
27
34
|
#failureLog;
|
|
28
35
|
#active = new Map();
|
|
29
36
|
#worldChains = new Map();
|
|
37
|
+
#loadedCursors = new Map();
|
|
38
|
+
#activeInvocations = new Map();
|
|
30
39
|
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog) {
|
|
31
40
|
this.#worlds = worlds;
|
|
32
41
|
this.#compiler = compiler;
|
|
@@ -34,9 +43,13 @@ export class PlayCallChain {
|
|
|
34
43
|
this.#failureLog = failureLog;
|
|
35
44
|
}
|
|
36
45
|
async start(input) {
|
|
46
|
+
return this.#runInvocation(input, (signal) => this.#start(input, signal));
|
|
47
|
+
}
|
|
48
|
+
async #start(input, signal) {
|
|
37
49
|
validateIdentity(input.chainId, "call-chain ID");
|
|
38
50
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
39
51
|
validatePlayerText(input.playerText);
|
|
52
|
+
assertModelHostBinding(input.modelBinding, input.modelHost);
|
|
40
53
|
const existing = await this.#readPersisted(input.worldId);
|
|
41
54
|
if (existing?.chainId === input.chainId) {
|
|
42
55
|
const duplicate = duplicatePlayerExchange(existing, input.exchangeId, input.playerText, "fresh");
|
|
@@ -70,16 +83,14 @@ export class PlayCallChain {
|
|
|
70
83
|
documents.bindBootstrap(bootstrap);
|
|
71
84
|
const now = Date.now();
|
|
72
85
|
const session = {
|
|
73
|
-
schemaVersion:
|
|
86
|
+
schemaVersion: 3,
|
|
74
87
|
kind: "play_call_chain",
|
|
75
88
|
chainId: input.chainId,
|
|
76
89
|
worldId: input.worldId,
|
|
77
|
-
previousContexts:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
snapshotCurrentContext(existing),
|
|
82
|
-
],
|
|
90
|
+
previousContexts: [],
|
|
91
|
+
previousChainId: existing?.chainId ?? null,
|
|
92
|
+
timelineGeneration: existing?.timelineGeneration ??
|
|
93
|
+
this.#worlds.playTimeline.newGeneration(),
|
|
83
94
|
baselineHead: binding.parentHead,
|
|
84
95
|
baselineHistoryLength: Object.keys(binding.history).length,
|
|
85
96
|
parentHead: binding.parentHead,
|
|
@@ -90,6 +101,7 @@ export class PlayCallChain {
|
|
|
90
101
|
},
|
|
91
102
|
followups: structuredClone(compilation.followups ?? []),
|
|
92
103
|
playPresetScriptsEnabled: input.playPreset.scriptsEnabled,
|
|
104
|
+
modelBinding: structuredClone(input.modelBinding),
|
|
93
105
|
status: "ready",
|
|
94
106
|
canRetry: false,
|
|
95
107
|
bootstrap,
|
|
@@ -114,28 +126,38 @@ export class PlayCallChain {
|
|
|
114
126
|
documents,
|
|
115
127
|
history: historyEntries(binding.history),
|
|
116
128
|
completedToolMap: new Map(),
|
|
129
|
+
persistenceCursor: {
|
|
130
|
+
eventCount: 0,
|
|
131
|
+
transcriptCount: 0,
|
|
132
|
+
completedToolCount: 0,
|
|
133
|
+
authorizationCheckpointCount: 0,
|
|
134
|
+
},
|
|
117
135
|
};
|
|
118
136
|
if (activeId !== undefined)
|
|
119
137
|
this.#active.delete(activeId);
|
|
120
138
|
this.#active.set(session.chainId, session);
|
|
121
139
|
this.#worldChains.set(session.worldId, session.chainId);
|
|
122
140
|
await this.#persist(session);
|
|
123
|
-
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, input.observer);
|
|
141
|
+
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, signal, input.observer);
|
|
124
142
|
}
|
|
125
143
|
async append(input) {
|
|
144
|
+
return this.#runInvocation(input, (signal) => this.#append(input, signal));
|
|
145
|
+
}
|
|
146
|
+
async #append(input, signal) {
|
|
126
147
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
127
148
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
128
149
|
await this.#assertCurrentHead(session);
|
|
150
|
+
assertSessionModelHostBinding(session, input.modelHost);
|
|
129
151
|
if (input.playerText.trim().length === 0) {
|
|
130
152
|
if (session.status === "running")
|
|
131
153
|
throw new PlayCallChainError("A model request is still running.");
|
|
132
154
|
if (session.status === "interrupted") {
|
|
133
155
|
if (!session.canRetry || session.lastRequest === null)
|
|
134
156
|
throw new PlayCallChainError("Call-chain processing failed, and there is no model context to continue.");
|
|
135
|
-
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, input.observer);
|
|
157
|
+
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
136
158
|
}
|
|
137
159
|
session.exchange += 1;
|
|
138
|
-
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, input.observer);
|
|
160
|
+
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, signal, input.observer);
|
|
139
161
|
}
|
|
140
162
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
141
163
|
return projectView(session);
|
|
@@ -143,36 +165,133 @@ export class PlayCallChain {
|
|
|
143
165
|
throw new PlayCallChainError(session.canRetry
|
|
144
166
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
145
167
|
: "The call chain was interrupted; use a fresh context.");
|
|
146
|
-
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, input.observer);
|
|
168
|
+
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, signal, input.observer);
|
|
169
|
+
}
|
|
170
|
+
cancel(input) {
|
|
171
|
+
validateIdentity(input.worldId, "world ID");
|
|
172
|
+
validateIdentity(input.chainId, "call-chain ID");
|
|
173
|
+
validateIdentity(input.exchangeId, "exchange ID");
|
|
174
|
+
const active = this.#activeInvocations.get(input.chainId);
|
|
175
|
+
if (active?.worldId !== input.worldId ||
|
|
176
|
+
active?.exchangeId !== input.exchangeId ||
|
|
177
|
+
active?.abortable !== true)
|
|
178
|
+
return { outcome: "not_running" };
|
|
179
|
+
if (!active.controller.signal.aborted) {
|
|
180
|
+
active.phase = "cancelling";
|
|
181
|
+
active.lastActivityAt = Date.now();
|
|
182
|
+
active.controller.abort();
|
|
183
|
+
}
|
|
184
|
+
return { outcome: "cancellation_requested" };
|
|
185
|
+
}
|
|
186
|
+
async #runInvocation(input, run) {
|
|
187
|
+
validateIdentity(input.worldId, "world ID");
|
|
188
|
+
validateIdentity(input.chainId, "call-chain ID");
|
|
189
|
+
validateIdentity(input.exchangeId, "exchange ID");
|
|
190
|
+
if (this.#activeInvocations.has(input.chainId) ||
|
|
191
|
+
[...this.#activeInvocations.values()].some(({ worldId }) => worldId === input.worldId))
|
|
192
|
+
throw new PlayCallChainError("A model request is already running for this world or call chain.");
|
|
193
|
+
const startedAt = Date.now();
|
|
194
|
+
const invocation = {
|
|
195
|
+
worldId: input.worldId,
|
|
196
|
+
chainId: input.chainId,
|
|
197
|
+
exchangeId: input.exchangeId,
|
|
198
|
+
phase: "preparing",
|
|
199
|
+
startedAt,
|
|
200
|
+
lastActivityAt: startedAt,
|
|
201
|
+
reasoningChars: 0,
|
|
202
|
+
textChars: 0,
|
|
203
|
+
toolChars: 0,
|
|
204
|
+
toolCalls: 0,
|
|
205
|
+
dispatches: 0,
|
|
206
|
+
controller: new AbortController(),
|
|
207
|
+
abortable: true,
|
|
208
|
+
};
|
|
209
|
+
this.#activeInvocations.set(input.chainId, invocation);
|
|
210
|
+
try {
|
|
211
|
+
return await run(invocation.controller.signal);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (this.#activeInvocations.get(input.chainId) === invocation)
|
|
215
|
+
this.#activeInvocations.delete(input.chainId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
#observeInvocation(chainId, phase, increments = {}) {
|
|
219
|
+
const active = this.#activeInvocations.get(chainId);
|
|
220
|
+
if (active === undefined || active.phase === "cancelling")
|
|
221
|
+
return;
|
|
222
|
+
active.phase = phase;
|
|
223
|
+
active.lastActivityAt = Date.now();
|
|
224
|
+
active.reasoningChars += increments.reasoningChars ?? 0;
|
|
225
|
+
active.textChars += increments.textChars ?? 0;
|
|
226
|
+
active.toolChars += increments.toolChars ?? 0;
|
|
227
|
+
active.toolCalls += increments.toolCalls ?? 0;
|
|
228
|
+
active.dispatches += increments.dispatches ?? 0;
|
|
229
|
+
}
|
|
230
|
+
#setInvocationAbortable(chainId, abortable) {
|
|
231
|
+
const active = this.#activeInvocations.get(chainId);
|
|
232
|
+
if (active !== undefined)
|
|
233
|
+
active.abortable = abortable;
|
|
234
|
+
}
|
|
235
|
+
#projectActiveView(session) {
|
|
236
|
+
const view = projectView(session);
|
|
237
|
+
const active = this.#activeInvocations.get(session.chainId);
|
|
238
|
+
if (active?.worldId !== session.worldId)
|
|
239
|
+
return view;
|
|
240
|
+
const { controller: _controller, worldId: _worldId, abortable: _abortable, ...progress } = active;
|
|
241
|
+
void _controller;
|
|
242
|
+
void _worldId;
|
|
243
|
+
void _abortable;
|
|
244
|
+
return { ...view, activeInvocation: structuredClone(progress) };
|
|
245
|
+
}
|
|
246
|
+
#notifySnapshot(observer, session) {
|
|
247
|
+
try {
|
|
248
|
+
observer?.onSnapshot?.(this.#projectActiveView(session));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// A browser stream may disconnect while the durable model request keeps
|
|
252
|
+
// running. Presentation failure never cancels or changes play Authority.
|
|
253
|
+
}
|
|
147
254
|
}
|
|
148
255
|
async inspectWorld(worldId) {
|
|
149
256
|
const activeId = this.#worldChains.get(worldId);
|
|
150
257
|
const active = activeId === undefined ? undefined : this.#active.get(activeId);
|
|
151
|
-
if (active !== undefined)
|
|
152
|
-
|
|
258
|
+
if (active !== undefined) {
|
|
259
|
+
if (this.#activeInvocations.get(active.chainId)?.worldId !== active.worldId)
|
|
260
|
+
await this.#reconcileSessionAdvance(active);
|
|
261
|
+
return this.#projectActiveView(active);
|
|
262
|
+
}
|
|
153
263
|
const persisted = await this.#readPersisted(worldId);
|
|
154
264
|
if (persisted === null)
|
|
155
265
|
return null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
266
|
+
const advance = await this.#worlds.playAdvances.readCurrent(worldId, persisted.chainId);
|
|
267
|
+
const session = await this.#hydrateSession(persisted, advance);
|
|
268
|
+
this.#active.set(session.chainId, session);
|
|
269
|
+
this.#worldChains.set(worldId, session.chainId);
|
|
270
|
+
await this.#reconcileSessionAdvance(session, advance);
|
|
271
|
+
if (session.status === "running")
|
|
272
|
+
await this.#interruptAbandonedDispatch(session);
|
|
273
|
+
return this.#projectActiveView(session);
|
|
274
|
+
}
|
|
275
|
+
async deriveWorld(input) {
|
|
276
|
+
const result = await this.#worlds.deriveWorld({
|
|
277
|
+
...input,
|
|
278
|
+
stageTarget: async ({ targetWorldRoot, targetWorldId, binding }) => {
|
|
279
|
+
await this.stageForkToDerivedWorld({
|
|
280
|
+
sourceWorldId: input.sourceWorldId,
|
|
281
|
+
sourceHead: input.sourceHead,
|
|
282
|
+
targetWorldId,
|
|
283
|
+
targetWorldRoot,
|
|
284
|
+
targetBinding: binding,
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
if ((await this.inspectWorld(result.world.worldId)) === null)
|
|
289
|
+
await this.forkToDerivedWorld({
|
|
290
|
+
sourceWorldId: input.sourceWorldId,
|
|
291
|
+
sourceHead: input.sourceHead,
|
|
292
|
+
targetWorldId: result.world.worldId,
|
|
168
293
|
});
|
|
169
|
-
|
|
170
|
-
persisted.canRetry = canRetry;
|
|
171
|
-
persisted.lastFailure = message;
|
|
172
|
-
persisted.updatedAt = Date.now();
|
|
173
|
-
await this.#worlds.writePlayCallChain(worldId, persisted);
|
|
174
|
-
}
|
|
175
|
-
return projectView(persisted);
|
|
294
|
+
return result;
|
|
176
295
|
}
|
|
177
296
|
async forkToDerivedWorld(input) {
|
|
178
297
|
const existing = await this.#readPersisted(input.targetWorldId);
|
|
@@ -181,13 +300,14 @@ export class PlayCallChain {
|
|
|
181
300
|
const source = await this.#readPersisted(input.sourceWorldId);
|
|
182
301
|
if (source === null)
|
|
183
302
|
return null;
|
|
184
|
-
const sourceContexts =
|
|
303
|
+
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.sourceWorldId);
|
|
185
304
|
const selectedContextIndex = sourceContexts.findIndex(({ events }) => eventsThroughHead(events, input.sourceHead) !== null);
|
|
186
305
|
if (selectedContextIndex < 0)
|
|
187
306
|
return null;
|
|
188
307
|
const sourceContext = sourceContexts[selectedContextIndex];
|
|
189
308
|
const sourceEvents = eventsThroughHead(sourceContext.events, input.sourceHead);
|
|
190
309
|
return this.#forkSelectionToDerivedWorld({
|
|
310
|
+
sourceWorldId: input.sourceWorldId,
|
|
191
311
|
sourceContexts,
|
|
192
312
|
selectedContextIndex,
|
|
193
313
|
sourceEvents,
|
|
@@ -196,6 +316,27 @@ export class PlayCallChain {
|
|
|
196
316
|
branchIdentity: input.sourceHead,
|
|
197
317
|
});
|
|
198
318
|
}
|
|
319
|
+
async stageForkToDerivedWorld(input) {
|
|
320
|
+
const source = await this.#readPersisted(input.sourceWorldId);
|
|
321
|
+
if (source === null)
|
|
322
|
+
return null;
|
|
323
|
+
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.sourceWorldId);
|
|
324
|
+
const selectedContextIndex = sourceContexts.findIndex(({ events }) => eventsThroughHead(events, input.sourceHead) !== null);
|
|
325
|
+
if (selectedContextIndex < 0)
|
|
326
|
+
return null;
|
|
327
|
+
const sourceEvents = eventsThroughHead(sourceContexts[selectedContextIndex].events, input.sourceHead);
|
|
328
|
+
return this.#forkSelectionToDerivedWorld({
|
|
329
|
+
sourceWorldId: input.sourceWorldId,
|
|
330
|
+
sourceContexts,
|
|
331
|
+
selectedContextIndex,
|
|
332
|
+
sourceEvents,
|
|
333
|
+
sourceHead: input.sourceHead,
|
|
334
|
+
targetWorldId: input.targetWorldId,
|
|
335
|
+
branchIdentity: input.sourceHead,
|
|
336
|
+
targetWorldRoot: input.targetWorldRoot,
|
|
337
|
+
targetBinding: input.targetBinding,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
199
340
|
/**
|
|
200
341
|
* Revise one committed player message in place. Authority remains
|
|
201
342
|
* append-only: one timeline-revision commit restores the player's logical
|
|
@@ -216,7 +357,7 @@ export class PlayCallChain {
|
|
|
216
357
|
const source = await this.#readPersisted(input.worldId);
|
|
217
358
|
if (source === null)
|
|
218
359
|
throw new PlayCallChainError("The current world has no model call chain to revise.");
|
|
219
|
-
const sourceContexts =
|
|
360
|
+
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.worldId);
|
|
220
361
|
const appliedEvent = sourceContexts
|
|
221
362
|
.flatMap(({ events }) => events)
|
|
222
363
|
.find((event) => event.kind === "player" &&
|
|
@@ -227,8 +368,12 @@ export class PlayCallChain {
|
|
|
227
368
|
throw new PlayCallChainError("The same replacement exchange ID is already bound to different player text.");
|
|
228
369
|
const authority = await this.#worlds.readAuthorityHistory(input.worldId);
|
|
229
370
|
const commit = authority.commits.find(({ head }) => head === appliedEvent.committedHead);
|
|
230
|
-
|
|
231
|
-
|
|
371
|
+
const operation = await this.#worlds.getOperationOutcome(input.operationId);
|
|
372
|
+
if (commit?.mode !== "timeline_revision" ||
|
|
373
|
+
(operation.outcome !== "committed" &&
|
|
374
|
+
operation.outcome !== "committed_materialization_pending") ||
|
|
375
|
+
operation.worldId !== input.worldId ||
|
|
376
|
+
operation.head !== appliedEvent.committedHead)
|
|
232
377
|
throw new PlayCallChainError("The replacement exchange ID is already used by another commit.");
|
|
233
378
|
return {
|
|
234
379
|
outcome: "revised",
|
|
@@ -311,13 +456,15 @@ export class PlayCallChain {
|
|
|
311
456
|
throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
|
|
312
457
|
const documents = restorePlayDocuments(binding.files, sourceContext, prefixEvents);
|
|
313
458
|
const revised = {
|
|
314
|
-
schemaVersion:
|
|
459
|
+
schemaVersion: 3,
|
|
315
460
|
kind: "play_call_chain",
|
|
316
461
|
chainId: derivedChainId(sourceContext.chainId, "timeline-revision:" + request.operationId, request.worldId),
|
|
317
462
|
worldId: request.worldId,
|
|
318
|
-
previousContexts:
|
|
319
|
-
|
|
320
|
-
|
|
463
|
+
previousContexts: [],
|
|
464
|
+
previousChainId: input.selectedContextIndex === 0
|
|
465
|
+
? null
|
|
466
|
+
: input.sourceContexts[input.selectedContextIndex - 1].chainId,
|
|
467
|
+
timelineGeneration: this.#worlds.playTimeline.newGeneration(),
|
|
321
468
|
baselineHead: sourceContext.baselineHead,
|
|
322
469
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
323
470
|
parentHead: outcome.head,
|
|
@@ -330,6 +477,9 @@ export class PlayCallChain {
|
|
|
330
477
|
: {
|
|
331
478
|
playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
|
|
332
479
|
}),
|
|
480
|
+
...(sourceContext.modelBinding === undefined
|
|
481
|
+
? {}
|
|
482
|
+
: { modelBinding: structuredClone(sourceContext.modelBinding) }),
|
|
333
483
|
status: "ready",
|
|
334
484
|
canRetry: false,
|
|
335
485
|
bootstrap: structuredClone(sourceContext.bootstrap),
|
|
@@ -351,7 +501,7 @@ export class PlayCallChain {
|
|
|
351
501
|
lastFailure: null,
|
|
352
502
|
updatedAt: Date.now(),
|
|
353
503
|
};
|
|
354
|
-
await this.#worlds.
|
|
504
|
+
await this.#worlds.playTimeline.persist(structuredClone(revised));
|
|
355
505
|
const activeId = this.#worldChains.get(request.worldId);
|
|
356
506
|
if (activeId !== undefined)
|
|
357
507
|
this.#active.delete(activeId);
|
|
@@ -364,30 +514,73 @@ export class PlayCallChain {
|
|
|
364
514
|
};
|
|
365
515
|
}
|
|
366
516
|
async #forkSelectionToDerivedWorld(input) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
517
|
+
if (input.targetWorldRoot === undefined) {
|
|
518
|
+
const existing = await this.#readPersisted(input.targetWorldId);
|
|
519
|
+
if (existing !== null)
|
|
520
|
+
return projectView(existing);
|
|
521
|
+
}
|
|
370
522
|
const sourceContext = input.sourceContexts[input.selectedContextIndex];
|
|
371
|
-
if (
|
|
523
|
+
if (input.targetWorldRoot === undefined &&
|
|
524
|
+
(await this.#worlds.currentHead(input.targetWorldId)) !== input.sourceHead)
|
|
372
525
|
throw new PlayCallChainError("The forked world's Authority does not match the selected history endpoint, so the call chain cannot be written.");
|
|
373
526
|
const [baseline, selected] = await Promise.all([
|
|
374
|
-
this.#worlds.recoverEndpoint(input.
|
|
375
|
-
|
|
527
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
528
|
+
? input.targetWorldId
|
|
529
|
+
: input.sourceWorldId, sourceContext.baselineHead),
|
|
530
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
531
|
+
? input.targetWorldId
|
|
532
|
+
: input.sourceWorldId, input.sourceHead),
|
|
376
533
|
]);
|
|
377
|
-
const
|
|
378
|
-
|
|
534
|
+
const selectsCompleteContext = input.sourceEvents.length === sourceContext.events.length;
|
|
535
|
+
// A complete context already defines the selected trace closure. Preserve
|
|
536
|
+
// its model transcript verbatim instead of reconstructing it from the
|
|
537
|
+
// independently persisted page-event projection.
|
|
538
|
+
const transcript = selectsCompleteContext
|
|
539
|
+
? structuredClone(sourceContext.transcript)
|
|
540
|
+
: transcriptThroughEvents(sourceContext.transcript, input.sourceEvents);
|
|
541
|
+
const completedKeys = selectsCompleteContext
|
|
542
|
+
? null
|
|
543
|
+
: completedToolKeys(input.sourceEvents);
|
|
379
544
|
const events = structuredClone(input.sourceEvents);
|
|
380
|
-
const derivedBinding =
|
|
545
|
+
const derivedBinding = input.targetBinding ??
|
|
546
|
+
(await this.#worlds.bindPlayCallChain(input.targetWorldId));
|
|
381
547
|
const derivedDocuments = restorePlayDocuments(derivedBinding.files, sourceContext, input.sourceEvents);
|
|
382
548
|
const now = Date.now();
|
|
549
|
+
const timelineGeneration = this.#worlds.playTimeline.newGeneration();
|
|
550
|
+
let previousChainId = null;
|
|
551
|
+
for (const [index, context] of input.sourceContexts
|
|
552
|
+
.slice(0, input.selectedContextIndex)
|
|
553
|
+
.entries()) {
|
|
554
|
+
const chainId = derivedChainId(context.chainId, `prefix:${input.branchIdentity}:${index}`, input.targetWorldId);
|
|
555
|
+
const previousContext = {
|
|
556
|
+
...independentContextCopy(context),
|
|
557
|
+
schemaVersion: 3,
|
|
558
|
+
kind: "play_call_chain",
|
|
559
|
+
worldId: input.targetWorldId,
|
|
560
|
+
chainId,
|
|
561
|
+
previousContexts: [],
|
|
562
|
+
previousChainId,
|
|
563
|
+
timelineGeneration,
|
|
564
|
+
};
|
|
565
|
+
if (input.targetWorldRoot === undefined)
|
|
566
|
+
await this.#worlds.playTimeline.persist(previousContext);
|
|
567
|
+
else
|
|
568
|
+
await this.#worlds.playTimeline.cloneContextPrefixToStaging({
|
|
569
|
+
sourceWorldId: input.sourceWorldId,
|
|
570
|
+
targetWorldRoot: input.targetWorldRoot,
|
|
571
|
+
source: context,
|
|
572
|
+
target: previousContext,
|
|
573
|
+
});
|
|
574
|
+
previousChainId = chainId;
|
|
575
|
+
}
|
|
383
576
|
const derived = {
|
|
384
|
-
schemaVersion:
|
|
577
|
+
schemaVersion: 3,
|
|
385
578
|
kind: "play_call_chain",
|
|
386
579
|
chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
|
|
387
580
|
worldId: input.targetWorldId,
|
|
388
|
-
previousContexts:
|
|
389
|
-
|
|
390
|
-
|
|
581
|
+
previousContexts: [],
|
|
582
|
+
previousChainId,
|
|
583
|
+
timelineGeneration,
|
|
391
584
|
baselineHead: sourceContext.baselineHead,
|
|
392
585
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
393
586
|
parentHead: input.sourceHead,
|
|
@@ -400,6 +593,9 @@ export class PlayCallChain {
|
|
|
400
593
|
: {
|
|
401
594
|
playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
|
|
402
595
|
}),
|
|
596
|
+
...(sourceContext.modelBinding === undefined
|
|
597
|
+
? {}
|
|
598
|
+
: { modelBinding: structuredClone(sourceContext.modelBinding) }),
|
|
403
599
|
status: "ready",
|
|
404
600
|
canRetry: false,
|
|
405
601
|
bootstrap: structuredClone(sourceContext.bootstrap),
|
|
@@ -407,7 +603,7 @@ export class PlayCallChain {
|
|
|
407
603
|
transcript,
|
|
408
604
|
events,
|
|
409
605
|
completedTools: sourceContext.completedTools
|
|
410
|
-
.filter(({ key }) => completedKeys.has(key))
|
|
606
|
+
.filter(({ key }) => completedKeys === null || completedKeys.has(key))
|
|
411
607
|
.map((item) => structuredClone(item)),
|
|
412
608
|
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, derivedDocuments.authorizationCheckpoint()),
|
|
413
609
|
changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
|
|
@@ -421,7 +617,15 @@ export class PlayCallChain {
|
|
|
421
617
|
lastFailure: null,
|
|
422
618
|
updatedAt: now,
|
|
423
619
|
};
|
|
424
|
-
|
|
620
|
+
if (input.targetWorldRoot === undefined)
|
|
621
|
+
await this.#worlds.playTimeline.persist(structuredClone(derived));
|
|
622
|
+
else
|
|
623
|
+
await this.#worlds.playTimeline.cloneContextPrefixToStaging({
|
|
624
|
+
sourceWorldId: input.sourceWorldId,
|
|
625
|
+
targetWorldRoot: input.targetWorldRoot,
|
|
626
|
+
source: sourceContext,
|
|
627
|
+
target: structuredClone(derived),
|
|
628
|
+
});
|
|
425
629
|
return projectView(derived);
|
|
426
630
|
}
|
|
427
631
|
forgetWorld(worldId) {
|
|
@@ -433,22 +637,49 @@ export class PlayCallChain {
|
|
|
433
637
|
this.#active.delete(activeId);
|
|
434
638
|
this.#worldChains.delete(worldId);
|
|
435
639
|
}
|
|
436
|
-
async #submitPlayer(session, exchangeId, playerText, context, modelHost, observer) {
|
|
640
|
+
async #submitPlayer(session, exchangeId, playerText, context, modelHost, signal, observer) {
|
|
437
641
|
if (duplicatePlayerExchange(session, exchangeId, playerText, context))
|
|
438
642
|
return projectView(session);
|
|
439
643
|
if (session.status === "running")
|
|
440
644
|
throw new PlayCallChainError("The call chain is waiting for the model to return.");
|
|
441
|
-
session.
|
|
442
|
-
|
|
443
|
-
|
|
645
|
+
const exchange = session.exchange + 1;
|
|
646
|
+
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
647
|
+
...session.transcript,
|
|
648
|
+
{ kind: "player", text: playerText },
|
|
649
|
+
]);
|
|
650
|
+
const operationKey = `player:${exchangeId}`;
|
|
651
|
+
const playerOperationId = operationId(session.chainId, operationKey);
|
|
652
|
+
const advance = {
|
|
653
|
+
schemaVersion: 1,
|
|
654
|
+
kind: "play_advance",
|
|
655
|
+
advanceKind: "player",
|
|
656
|
+
worldId: session.worldId,
|
|
657
|
+
chainId: session.chainId,
|
|
658
|
+
advanceId: playerOperationId,
|
|
659
|
+
operationId: playerOperationId,
|
|
660
|
+
parentHead: session.parentHead,
|
|
661
|
+
eventId: session.nextEventId,
|
|
662
|
+
exchangeId,
|
|
663
|
+
playerText,
|
|
664
|
+
context,
|
|
665
|
+
transcriptStart: session.transcript.length,
|
|
666
|
+
exchange,
|
|
667
|
+
nextRequest: structuredClone(nextRequest),
|
|
668
|
+
createdAt: Date.now(),
|
|
669
|
+
};
|
|
670
|
+
await this.#worlds.playAdvances.begin(advance);
|
|
444
671
|
try {
|
|
445
|
-
|
|
446
|
-
operationKey: `player:${exchangeId}`,
|
|
447
|
-
historyAppend: [{ role: "player", exactText: playerText }],
|
|
448
|
-
stateChanges: [],
|
|
449
|
-
});
|
|
672
|
+
await this.#settlePlayerAdvance(session, advance);
|
|
450
673
|
}
|
|
451
674
|
catch (error) {
|
|
675
|
+
const accepted = await this.#worlds
|
|
676
|
+
.getOperationOutcome(advance.operationId)
|
|
677
|
+
.catch(() => ({ outcome: "not_started" }));
|
|
678
|
+
if (accepted.outcome === "committed" ||
|
|
679
|
+
accepted.outcome === "committed_materialization_pending") {
|
|
680
|
+
await this.#settlePlayerAdvance(session, advance);
|
|
681
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
682
|
+
}
|
|
452
683
|
const message = error instanceof Error
|
|
453
684
|
? error.message
|
|
454
685
|
: "Failed to write the original player text.";
|
|
@@ -461,27 +692,281 @@ export class PlayCallChain {
|
|
|
461
692
|
session.canRetry = false;
|
|
462
693
|
session.lastFailure = message;
|
|
463
694
|
await this.#persist(session);
|
|
695
|
+
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
464
696
|
throw new PlayCallChainError(message);
|
|
465
697
|
}
|
|
466
|
-
session
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
698
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
699
|
+
}
|
|
700
|
+
async #settlePlayerAdvance(session, advance) {
|
|
701
|
+
const outcome = await this.#worlds.commitPlayStep({
|
|
702
|
+
operationId: advance.operationId,
|
|
703
|
+
worldId: session.worldId,
|
|
704
|
+
parentHead: advance.parentHead,
|
|
705
|
+
historyAppend: [{ role: "player", exactText: advance.playerText }],
|
|
706
|
+
nextMaterials: structuredClone(session.nextMaterials),
|
|
707
|
+
stateChanges: [],
|
|
474
708
|
});
|
|
709
|
+
if (outcome.worldId !== session.worldId ||
|
|
710
|
+
outcome.parentHead !== advance.parentHead)
|
|
711
|
+
throw new PlayCallChainError("The recovered player advance belongs to a different world endpoint.");
|
|
712
|
+
const playerEvent = {
|
|
713
|
+
id: advance.eventId,
|
|
714
|
+
kind: "player",
|
|
715
|
+
exchangeId: advance.exchangeId,
|
|
716
|
+
text: advance.playerText,
|
|
717
|
+
context: advance.context,
|
|
718
|
+
committedHead: outcome.head,
|
|
719
|
+
};
|
|
720
|
+
session.events = [
|
|
721
|
+
...session.events.filter(({ id }) => id < advance.eventId),
|
|
722
|
+
playerEvent,
|
|
723
|
+
];
|
|
724
|
+
if (session.transcript.length < advance.transcriptStart)
|
|
725
|
+
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its player prefix.");
|
|
726
|
+
session.transcript = [
|
|
727
|
+
...session.transcript.slice(0, advance.transcriptStart),
|
|
728
|
+
{ kind: "player", text: advance.playerText },
|
|
729
|
+
];
|
|
730
|
+
session.nextEventId = advance.eventId + 1;
|
|
731
|
+
session.parentHead = outcome.head;
|
|
732
|
+
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
733
|
+
session.exchange = advance.exchange;
|
|
734
|
+
session.lastRequest = structuredClone(advance.nextRequest);
|
|
735
|
+
session.lastRequestAttempt = 0;
|
|
736
|
+
session.status = "interrupted";
|
|
737
|
+
session.canRetry = true;
|
|
738
|
+
session.lastFailure =
|
|
739
|
+
"The player message is committed; the frozen model request is ready to continue.";
|
|
475
740
|
await this.#persist(session);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
741
|
+
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
742
|
+
await this.#worlds.playAdvances.markSettled(advance, outcome.head);
|
|
743
|
+
}
|
|
744
|
+
#prepareResponseSettlement(session, advance, response) {
|
|
745
|
+
const existing = session.events.find(({ id }) => id === advance.eventId);
|
|
746
|
+
if (existing?.kind !== "assistant")
|
|
747
|
+
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
748
|
+
const calls = response.toolCalls ?? [];
|
|
749
|
+
const text = response.text ?? existing.text;
|
|
750
|
+
const hasText = text.trim().length > 0;
|
|
751
|
+
const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
|
|
752
|
+
const assistantEvent = {
|
|
753
|
+
id: advance.eventId,
|
|
754
|
+
kind: "assistant",
|
|
755
|
+
text,
|
|
756
|
+
status: "completed",
|
|
757
|
+
responseKind,
|
|
758
|
+
exchange: advance.exchange,
|
|
759
|
+
attempt: advance.attempt,
|
|
760
|
+
...(response.reasoningContent === undefined
|
|
761
|
+
? existing.reasoning === undefined
|
|
762
|
+
? {}
|
|
763
|
+
: { reasoning: existing.reasoning }
|
|
764
|
+
: { reasoning: response.reasoningContent }),
|
|
765
|
+
...(response.usage === undefined
|
|
766
|
+
? {}
|
|
767
|
+
: { usage: structuredClone(response.usage) }),
|
|
768
|
+
...(response.stopReason === undefined
|
|
769
|
+
? {}
|
|
770
|
+
: { stopReason: response.stopReason }),
|
|
771
|
+
continuation: response.providerState === undefined ? "unavailable" : "available",
|
|
772
|
+
};
|
|
773
|
+
const workingTools = new Map(session.completedToolMap);
|
|
774
|
+
const prepared = [];
|
|
775
|
+
const trailingEvents = [];
|
|
776
|
+
let nextEventId = advance.eventId + 1;
|
|
777
|
+
for (const call of calls) {
|
|
778
|
+
const item = prepareTool(session, workingTools, advance.exchange, call);
|
|
779
|
+
prepared.push(item);
|
|
780
|
+
trailingEvents.push({
|
|
781
|
+
id: nextEventId++,
|
|
782
|
+
kind: "tool_call",
|
|
783
|
+
callId: call.id,
|
|
784
|
+
name: call.name,
|
|
785
|
+
arguments: structuredClone(call.arguments),
|
|
786
|
+
replayed: item.replayed,
|
|
787
|
+
});
|
|
788
|
+
item.event.id = nextEventId++;
|
|
789
|
+
trailingEvents.push(structuredClone(item.event));
|
|
790
|
+
}
|
|
791
|
+
const stateChanges = session.documents.stateChanges();
|
|
792
|
+
const visibleText = responseKind === "narrative";
|
|
793
|
+
const assistantItem = {
|
|
794
|
+
kind: "assistant",
|
|
795
|
+
text,
|
|
796
|
+
...(response.reasoningContent === undefined
|
|
797
|
+
? {}
|
|
798
|
+
: { reasoningContent: response.reasoningContent }),
|
|
799
|
+
...(response.providerState === undefined
|
|
800
|
+
? {}
|
|
801
|
+
: { providerState: response.providerState }),
|
|
802
|
+
toolCalls: structuredClone(calls),
|
|
803
|
+
};
|
|
804
|
+
return {
|
|
805
|
+
assistantEvent,
|
|
806
|
+
trailingEvents,
|
|
807
|
+
transcriptStart: session.transcript.length,
|
|
808
|
+
transcriptAppend: [
|
|
809
|
+
...(calls.length > 0 ||
|
|
810
|
+
visibleText ||
|
|
811
|
+
response.providerState !== undefined
|
|
812
|
+
? [assistantItem]
|
|
813
|
+
: []),
|
|
814
|
+
...prepared.map(({ transcript }) => structuredClone(transcript)),
|
|
815
|
+
],
|
|
816
|
+
completedTools: [...workingTools.values()].map((item) => structuredClone(item)),
|
|
817
|
+
authorizationCheckpoint: session.documents.authorizationCheckpoint(),
|
|
818
|
+
stateChanges: structuredClone(stateChanges),
|
|
819
|
+
visibleText,
|
|
820
|
+
};
|
|
480
821
|
}
|
|
481
|
-
async #
|
|
822
|
+
async #settleResponseAdvance(session, advance, prepared) {
|
|
823
|
+
const settlement = structuredClone(prepared);
|
|
824
|
+
const hasToolCalls = settlement.trailingEvents.some(({ kind }) => kind === "tool_call");
|
|
825
|
+
// A legacy prepared fact has no responseKind. Its visibleText decision is
|
|
826
|
+
// already part of an immutable recovery operation and must be replayed as
|
|
827
|
+
// recorded; every newly prepared fact is checked against the current rule.
|
|
828
|
+
if (settlement.assistantEvent.responseKind !== undefined) {
|
|
829
|
+
const expectedResponseKind = hasToolCalls
|
|
830
|
+
? "tool_step"
|
|
831
|
+
: settlement.assistantEvent.text.trim().length > 0
|
|
832
|
+
? "narrative"
|
|
833
|
+
: "empty";
|
|
834
|
+
if (settlement.assistantEvent.responseKind !== expectedResponseKind ||
|
|
835
|
+
settlement.visibleText !== (expectedResponseKind === "narrative"))
|
|
836
|
+
throw new PlayCallChainError("The prepared response settlement has an inconsistent narrative classification.");
|
|
837
|
+
}
|
|
838
|
+
let committedHead;
|
|
839
|
+
if (settlement.visibleText || settlement.stateChanges.length > 0) {
|
|
840
|
+
const outcome = await this.#worlds.commitPlayStep({
|
|
841
|
+
operationId: advance.operationId,
|
|
842
|
+
worldId: session.worldId,
|
|
843
|
+
parentHead: advance.parentHead,
|
|
844
|
+
historyAppend: settlement.visibleText
|
|
845
|
+
? [
|
|
846
|
+
{
|
|
847
|
+
role: "narrator",
|
|
848
|
+
exactText: settlement.assistantEvent.text,
|
|
849
|
+
},
|
|
850
|
+
]
|
|
851
|
+
: [],
|
|
852
|
+
nextMaterials: structuredClone(session.nextMaterials),
|
|
853
|
+
stateChanges: structuredClone(settlement.stateChanges),
|
|
854
|
+
});
|
|
855
|
+
committedHead = outcome.head;
|
|
856
|
+
settlement.assistantEvent.committedHead = outcome.head;
|
|
857
|
+
crashAtPlayAdvanceEdge("after_authority_accepted");
|
|
858
|
+
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
859
|
+
}
|
|
860
|
+
finalizePreparedReceipts(settlement);
|
|
861
|
+
session.events = [
|
|
862
|
+
...session.events.filter(({ id }) => id < advance.eventId),
|
|
863
|
+
structuredClone(settlement.assistantEvent),
|
|
864
|
+
...structuredClone(settlement.trailingEvents),
|
|
865
|
+
];
|
|
866
|
+
session.nextEventId =
|
|
867
|
+
Math.max(advance.eventId, ...settlement.trailingEvents.map(({ id }) => id)) + 1;
|
|
868
|
+
if (session.transcript.length < settlement.transcriptStart)
|
|
869
|
+
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its settlement prefix.");
|
|
870
|
+
session.transcript = [
|
|
871
|
+
...session.transcript.slice(0, settlement.transcriptStart),
|
|
872
|
+
...structuredClone(settlement.transcriptAppend),
|
|
873
|
+
];
|
|
874
|
+
session.completedTools = structuredClone(settlement.completedTools);
|
|
875
|
+
session.completedToolMap = new Map(settlement.completedTools.map((item) => [
|
|
876
|
+
item.key,
|
|
877
|
+
structuredClone(item),
|
|
878
|
+
]));
|
|
879
|
+
const lastToolEventId = settlement.trailingEvents.findLast(({ kind }) => kind === "tool_result")?.id;
|
|
880
|
+
if (lastToolEventId !== undefined) {
|
|
881
|
+
session.documentAuthorizationCheckpoints =
|
|
882
|
+
session.documentAuthorizationCheckpoints.filter(({ afterEventId }) => afterEventId < advance.eventId);
|
|
883
|
+
session.documentAuthorizationCheckpoints.push({
|
|
884
|
+
afterEventId: lastToolEventId,
|
|
885
|
+
authorization: structuredClone(settlement.authorizationCheckpoint),
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (committedHead !== undefined)
|
|
889
|
+
session.parentHead = committedHead;
|
|
890
|
+
if (settlement.stateChanges.length > 0) {
|
|
891
|
+
const currentAuthorization = session.documents.authorizationCheckpoint();
|
|
892
|
+
if (currentAuthorization.stateFingerprint ===
|
|
893
|
+
settlement.authorizationCheckpoint.stateFingerprint)
|
|
894
|
+
session.documents.acceptCommittedState();
|
|
895
|
+
else {
|
|
896
|
+
const binding = await this.#worlds.bindPlayCallChain(session.worldId);
|
|
897
|
+
session.documents = new FileNativePlayDocuments(binding.files);
|
|
898
|
+
session.documents.restoreAuthorizationCheckpoint(settlement.authorizationCheckpoint);
|
|
899
|
+
session.history = historyEntries(binding.history);
|
|
900
|
+
session.nextMaterials = structuredClone(binding.additionalMaterials);
|
|
901
|
+
}
|
|
902
|
+
mergeChangedDocuments(session, settlement.stateChanges);
|
|
903
|
+
}
|
|
904
|
+
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
905
|
+
session.canRetry = false;
|
|
906
|
+
session.lastFailure = null;
|
|
907
|
+
let result;
|
|
908
|
+
if (settlement.assistantEvent.continuation !== "available") {
|
|
909
|
+
const message = "The complete model response has no provider-native continuation payload; use a fresh context.";
|
|
910
|
+
session.events.push({
|
|
911
|
+
id: session.nextEventId++,
|
|
912
|
+
kind: "failure",
|
|
913
|
+
message,
|
|
914
|
+
});
|
|
915
|
+
session.status = "interrupted";
|
|
916
|
+
session.lastFailure = message;
|
|
917
|
+
result = { kind: "terminal", status: "interrupted" };
|
|
918
|
+
}
|
|
919
|
+
else if (hasToolCalls) {
|
|
920
|
+
session.exchange += 1;
|
|
921
|
+
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
922
|
+
session.lastRequest = structuredClone(nextRequest);
|
|
923
|
+
session.lastRequestAttempt = 0;
|
|
924
|
+
session.status = "interrupted";
|
|
925
|
+
session.canRetry = true;
|
|
926
|
+
session.lastFailure =
|
|
927
|
+
"The completed tool response is settled; its frozen continuation request is ready.";
|
|
928
|
+
result = { kind: "continue", nextRequest };
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
session.status = settlement.visibleText ? "ready" : "interrupted";
|
|
932
|
+
if (!settlement.visibleText) {
|
|
933
|
+
const message = "The model returned neither text nor a complete tool call.";
|
|
934
|
+
session.events.push({
|
|
935
|
+
id: session.nextEventId++,
|
|
936
|
+
kind: "failure",
|
|
937
|
+
message,
|
|
938
|
+
});
|
|
939
|
+
session.lastFailure = message;
|
|
940
|
+
}
|
|
941
|
+
result = { kind: "terminal", status: session.status };
|
|
942
|
+
}
|
|
943
|
+
await this.#persist(session);
|
|
944
|
+
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
945
|
+
await this.#worlds.playAdvances.markSettled(advance, committedHead ?? session.parentHead);
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
async #recoverAcceptedResponseAdvance(session, advance) {
|
|
949
|
+
const outcome = await this.#worlds
|
|
950
|
+
.getOperationOutcome(advance.operationId)
|
|
951
|
+
.catch(() => ({ outcome: "not_started" }));
|
|
952
|
+
if (outcome.outcome !== "committed" &&
|
|
953
|
+
outcome.outcome !== "committed_materialization_pending")
|
|
954
|
+
return null;
|
|
955
|
+
const current = await this.#worlds.playAdvances.readCurrent(advance.worldId, advance.chainId);
|
|
956
|
+
if (current?.base.advanceKind !== "response" ||
|
|
957
|
+
current.base.advanceId !== advance.advanceId ||
|
|
958
|
+
current.settlementPrepared === null)
|
|
959
|
+
throw new PlayCallChainError("Authority accepted a response whose durable settlement is missing.");
|
|
960
|
+
return this.#settleResponseAdvance(session, advance, current.settlementPrepared.settlement);
|
|
961
|
+
}
|
|
962
|
+
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
482
963
|
let request = structuredClone(firstRequest);
|
|
483
964
|
let attempt = firstAttempt;
|
|
484
965
|
for (;;) {
|
|
966
|
+
this.#setInvocationAbortable(session.chainId, true);
|
|
967
|
+
if (signal.aborted)
|
|
968
|
+
return this.#cancelBeforeProviderDispatch(session, request, attempt, observer);
|
|
969
|
+
this.#observeInvocation(session.chainId, "waiting");
|
|
485
970
|
session.status = "running";
|
|
486
971
|
session.canRetry = false;
|
|
487
972
|
session.lastFailure = null;
|
|
@@ -493,16 +978,45 @@ export class PlayCallChain {
|
|
|
493
978
|
kind: "assistant",
|
|
494
979
|
text: "",
|
|
495
980
|
status: "streaming",
|
|
981
|
+
responseKind: "pending",
|
|
496
982
|
exchange: responseExchange,
|
|
497
983
|
attempt,
|
|
498
984
|
};
|
|
499
985
|
session.events.push(event);
|
|
500
986
|
await this.#persist(session);
|
|
501
|
-
|
|
987
|
+
const responseOperationId = operationId(session.chainId, `response:${responseExchange}:attempt:${attempt}`);
|
|
988
|
+
const advance = {
|
|
989
|
+
schemaVersion: 1,
|
|
990
|
+
kind: "play_advance",
|
|
991
|
+
advanceKind: "response",
|
|
992
|
+
worldId: session.worldId,
|
|
993
|
+
chainId: session.chainId,
|
|
994
|
+
advanceId: responseOperationId,
|
|
995
|
+
operationId: responseOperationId,
|
|
996
|
+
parentHead: session.parentHead,
|
|
997
|
+
eventId: event.id,
|
|
998
|
+
exchange: responseExchange,
|
|
999
|
+
attempt,
|
|
1000
|
+
createdAt: Date.now(),
|
|
1001
|
+
};
|
|
1002
|
+
await this.#worlds.playAdvances.begin(advance);
|
|
1003
|
+
crashAtPlayAdvanceEdge("after_response_advance_began");
|
|
1004
|
+
this.#notifySnapshot(observer, session);
|
|
1005
|
+
if (signal.aborted)
|
|
1006
|
+
return this.#cancelBeforeProviderDispatch(session, request, attempt, observer, advance);
|
|
502
1007
|
let response;
|
|
503
1008
|
try {
|
|
1009
|
+
this.#observeInvocation(session.chainId, "waiting", { dispatches: 1 });
|
|
504
1010
|
response = await modelHost.exchange(request, {
|
|
1011
|
+
signal,
|
|
505
1012
|
onDelta: (delta) => {
|
|
1013
|
+
this.#observeInvocation(session.chainId, delta.kind, {
|
|
1014
|
+
...(delta.kind === "reasoning"
|
|
1015
|
+
? { reasoningChars: delta.text.length }
|
|
1016
|
+
: delta.kind === "text"
|
|
1017
|
+
? { textChars: delta.text.length }
|
|
1018
|
+
: { toolChars: delta.text.length }),
|
|
1019
|
+
});
|
|
506
1020
|
if (delta.kind === "text")
|
|
507
1021
|
event.text += delta.text;
|
|
508
1022
|
else if (delta.kind === "reasoning")
|
|
@@ -518,6 +1032,13 @@ export class PlayCallChain {
|
|
|
518
1032
|
});
|
|
519
1033
|
},
|
|
520
1034
|
});
|
|
1035
|
+
if (signal.aborted)
|
|
1036
|
+
throw new ModelHostCancelledError("The player cancelled the dispatched model request; its response was not committed and cannot be replayed.");
|
|
1037
|
+
this.#setInvocationAbortable(session.chainId, false);
|
|
1038
|
+
if ((response.toolCalls?.length ?? 0) > 0)
|
|
1039
|
+
this.#observeInvocation(session.chainId, "tool", {
|
|
1040
|
+
toolCalls: response.toolCalls?.length ?? 0,
|
|
1041
|
+
});
|
|
521
1042
|
if (response.diagnostics !== undefined)
|
|
522
1043
|
await this.#failureLog?.recordExchangeIfActive(response.diagnostics);
|
|
523
1044
|
}
|
|
@@ -528,57 +1049,30 @@ export class PlayCallChain {
|
|
|
528
1049
|
: "The model request was interrupted.";
|
|
529
1050
|
session.events.push({
|
|
530
1051
|
id: session.nextEventId++,
|
|
531
|
-
kind:
|
|
1052
|
+
kind: error instanceof ModelHostCancelledError
|
|
1053
|
+
? "cancellation"
|
|
1054
|
+
: "failure",
|
|
532
1055
|
message,
|
|
533
1056
|
});
|
|
534
1057
|
session.status = "interrupted";
|
|
535
|
-
|
|
1058
|
+
const requiresFreshContext = error instanceof ModelHostContinuationError ||
|
|
1059
|
+
error instanceof ModelHostOutcomeUnknownError;
|
|
1060
|
+
session.canRetry =
|
|
1061
|
+
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
536
1062
|
session.lastFailure = message;
|
|
537
1063
|
await this.#persist(session);
|
|
538
|
-
|
|
1064
|
+
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1065
|
+
this.#notifySnapshot(observer, session);
|
|
539
1066
|
return projectView(session);
|
|
540
1067
|
}
|
|
541
|
-
|
|
542
|
-
session.canRetry = false;
|
|
543
|
-
event.text = response.text ?? event.text;
|
|
544
|
-
if (response.reasoningContent !== undefined)
|
|
545
|
-
event.reasoning = response.reasoningContent;
|
|
546
|
-
delete event.toolFragment;
|
|
547
|
-
if (response.usage !== undefined)
|
|
548
|
-
event.usage = {
|
|
549
|
-
inputTokens: response.usage.inputTokens,
|
|
550
|
-
outputTokens: response.usage.outputTokens,
|
|
551
|
-
};
|
|
552
|
-
const calls = response.toolCalls ?? [];
|
|
553
|
-
const assistantItem = {
|
|
554
|
-
kind: "assistant",
|
|
555
|
-
text: response.text ?? "",
|
|
556
|
-
...(response.reasoningContent === undefined
|
|
557
|
-
? {}
|
|
558
|
-
: { reasoningContent: response.reasoningContent }),
|
|
559
|
-
...(response.providerState === undefined
|
|
560
|
-
? {}
|
|
561
|
-
: { providerState: response.providerState }),
|
|
562
|
-
toolCalls: structuredClone(calls),
|
|
563
|
-
};
|
|
1068
|
+
const durableResponse = durableModelResponse(response);
|
|
564
1069
|
try {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
id: session.nextEventId++,
|
|
572
|
-
kind: "tool_call",
|
|
573
|
-
callId: call.id,
|
|
574
|
-
name: call.name,
|
|
575
|
-
arguments: structuredClone(call.arguments),
|
|
576
|
-
replayed: item.replayed,
|
|
577
|
-
});
|
|
578
|
-
item.event.id = session.nextEventId++;
|
|
579
|
-
session.events.push(item.event);
|
|
580
|
-
}
|
|
581
|
-
const failedTools = prepared.filter(({ result }) => !result.ok);
|
|
1070
|
+
await this.#worlds.playAdvances.recordProviderCompleted(advance, durableResponse);
|
|
1071
|
+
crashAtPlayAdvanceEdge("after_provider_completed");
|
|
1072
|
+
const settlement = this.#prepareResponseSettlement(session, advance, durableResponse);
|
|
1073
|
+
await this.#worlds.playAdvances.recordSettlementPrepared(advance, settlement);
|
|
1074
|
+
crashAtPlayAdvanceEdge("after_settlement_prepared");
|
|
1075
|
+
const failedTools = settlement.trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
582
1076
|
if (failedTools.length > 0 && response.diagnostics !== undefined)
|
|
583
1077
|
await this.#failureLog?.recordFailure({
|
|
584
1078
|
exchange: response.diagnostics,
|
|
@@ -587,66 +1081,20 @@ export class PlayCallChain {
|
|
|
587
1081
|
kind: "tool_execution",
|
|
588
1082
|
message: "A Runtime tool call from the model was rejected.",
|
|
589
1083
|
details: {
|
|
590
|
-
calls: failedTools.map((
|
|
591
|
-
id:
|
|
592
|
-
name:
|
|
593
|
-
arguments: structuredClone(call.arguments),
|
|
1084
|
+
calls: failedTools.map((result) => ({
|
|
1085
|
+
id: result.callId,
|
|
1086
|
+
name: result.name,
|
|
594
1087
|
ok: result.ok,
|
|
595
|
-
failureKind: result.failureKind,
|
|
596
1088
|
markdown: result.markdown,
|
|
597
|
-
replayed,
|
|
1089
|
+
replayed: result.replayed,
|
|
598
1090
|
})),
|
|
599
1091
|
},
|
|
600
1092
|
},
|
|
601
1093
|
],
|
|
602
1094
|
});
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (visibleText || stateChanges.length > 0) {
|
|
607
|
-
const committed = await this.#commitStep(session, {
|
|
608
|
-
operationKey: `response:${responseExchange}:attempt:${attempt}`,
|
|
609
|
-
historyAppend: visibleText
|
|
610
|
-
? [{ role: "narrator", exactText: response.text ?? "" }]
|
|
611
|
-
: [],
|
|
612
|
-
stateChanges,
|
|
613
|
-
});
|
|
614
|
-
committedHead = committed.head;
|
|
615
|
-
event.committedHead = committed.head;
|
|
616
|
-
if (stateChanges.length > 0) {
|
|
617
|
-
session.documents.acceptCommittedState();
|
|
618
|
-
mergeChangedDocuments(session, stateChanges);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const committedWriteRefs = new Set(stateChanges.map(({ stableShortRef }) => stableShortRef));
|
|
622
|
-
for (const item of prepared) {
|
|
623
|
-
const candidateWrite = item.result.candidateWrite;
|
|
624
|
-
if (!item.result.ok || candidateWrite === undefined)
|
|
625
|
-
continue;
|
|
626
|
-
const markdown = candidateWrite.changed &&
|
|
627
|
-
committedWriteRefs.has(candidateWrite.shortRef)
|
|
628
|
-
? `@${candidateWrite.shortRef} write succeeded`
|
|
629
|
-
: `@${candidateWrite.shortRef} unchanged`;
|
|
630
|
-
item.result.markdown = markdown;
|
|
631
|
-
delete item.result.candidateWrite;
|
|
632
|
-
item.event.markdown = markdown;
|
|
633
|
-
item.transcript.markdown = markdown;
|
|
634
|
-
const completed = workingTools.get(item.key);
|
|
635
|
-
if (completed !== undefined)
|
|
636
|
-
completed.result = structuredClone(item.result);
|
|
637
|
-
}
|
|
638
|
-
if (calls.length > 0 || visibleText)
|
|
639
|
-
session.transcript.push(assistantItem);
|
|
640
|
-
session.transcript.push(...prepared.map(({ transcript }) => transcript));
|
|
641
|
-
session.completedToolMap = workingTools;
|
|
642
|
-
session.completedTools = [...workingTools.values()].map((item) => structuredClone(item));
|
|
643
|
-
const lastToolEventId = prepared.at(-1)?.event.id;
|
|
644
|
-
if (lastToolEventId !== undefined)
|
|
645
|
-
recordDocumentAuthorizationCheckpoint(session, lastToolEventId);
|
|
646
|
-
if (committedHead !== undefined)
|
|
647
|
-
session.parentHead = committedHead;
|
|
648
|
-
if (calls.length === 0) {
|
|
649
|
-
if (!visibleText) {
|
|
1095
|
+
const settled = await this.#settleResponseAdvance(session, advance, settlement);
|
|
1096
|
+
if (settled.kind === "terminal") {
|
|
1097
|
+
if (!settlement.visibleText) {
|
|
650
1098
|
const message = "The model returned neither text nor a complete tool call.";
|
|
651
1099
|
if (response.diagnostics !== undefined)
|
|
652
1100
|
await this.#failureLog?.recordFailure({
|
|
@@ -658,16 +1106,11 @@ export class PlayCallChain {
|
|
|
658
1106
|
},
|
|
659
1107
|
],
|
|
660
1108
|
});
|
|
661
|
-
session
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
session.status = "interrupted";
|
|
667
|
-
session.canRetry = false;
|
|
668
|
-
session.lastFailure = message;
|
|
669
|
-
await this.#persist(session);
|
|
670
|
-
notifySnapshot(observer, session);
|
|
1109
|
+
this.#notifySnapshot(observer, session);
|
|
1110
|
+
return projectView(session);
|
|
1111
|
+
}
|
|
1112
|
+
if (settled.status !== "ready") {
|
|
1113
|
+
this.#notifySnapshot(observer, session);
|
|
671
1114
|
return projectView(session);
|
|
672
1115
|
}
|
|
673
1116
|
// The exchange has settled: no tool call is pending and the narrative
|
|
@@ -680,19 +1123,46 @@ export class PlayCallChain {
|
|
|
680
1123
|
message: "The play call chain recovered during a later model exchange and completed.",
|
|
681
1124
|
details: { parentHead: session.parentHead },
|
|
682
1125
|
});
|
|
683
|
-
|
|
1126
|
+
if ((session.followups?.length ?? 0) > 0) {
|
|
1127
|
+
// Follow-up artifacts are part of this player-visible invocation.
|
|
1128
|
+
// Keep `running` ephemeral: the settled main response remains the
|
|
1129
|
+
// durable recovery point if this Runtime process stops here.
|
|
1130
|
+
session.status = "running";
|
|
1131
|
+
this.#observeInvocation(session.chainId, "followup");
|
|
1132
|
+
this.#setInvocationAbortable(session.chainId, true);
|
|
1133
|
+
this.#notifySnapshot(observer, session);
|
|
1134
|
+
}
|
|
1135
|
+
await this.#runFollowups(session, modelHost, signal, observer);
|
|
1136
|
+
this.#setInvocationAbortable(session.chainId, false);
|
|
1137
|
+
if (signal.aborted) {
|
|
1138
|
+
session.events.push({
|
|
1139
|
+
id: session.nextEventId++,
|
|
1140
|
+
kind: "cancellation",
|
|
1141
|
+
message: "The player cancelled the remaining follow-up generation after the narrative was committed.",
|
|
1142
|
+
});
|
|
1143
|
+
touch(session);
|
|
1144
|
+
}
|
|
684
1145
|
session.status = "ready";
|
|
685
1146
|
await this.#persist(session);
|
|
686
|
-
notifySnapshot(observer, session);
|
|
1147
|
+
this.#notifySnapshot(observer, session);
|
|
687
1148
|
return projectView(session);
|
|
688
1149
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
session.exchange = Math.max(session.exchange, responseExchange) + 1;
|
|
692
|
-
request = createRequest(session, modelHost, session.exchange);
|
|
1150
|
+
this.#notifySnapshot(observer, session);
|
|
1151
|
+
request = settled.nextRequest;
|
|
693
1152
|
attempt = 1;
|
|
694
1153
|
}
|
|
695
1154
|
catch (error) {
|
|
1155
|
+
const recovered = await this.#recoverAcceptedResponseAdvance(session, advance);
|
|
1156
|
+
if (recovered !== null) {
|
|
1157
|
+
if (recovered.kind === "continue") {
|
|
1158
|
+
request = recovered.nextRequest;
|
|
1159
|
+
attempt = 1;
|
|
1160
|
+
this.#notifySnapshot(observer, session);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
this.#notifySnapshot(observer, session);
|
|
1164
|
+
return projectView(session);
|
|
1165
|
+
}
|
|
696
1166
|
const message = error instanceof Error
|
|
697
1167
|
? error.message
|
|
698
1168
|
: "Call-chain processing failed.";
|
|
@@ -715,11 +1185,34 @@ export class PlayCallChain {
|
|
|
715
1185
|
session.canRetry = false;
|
|
716
1186
|
session.lastFailure = message;
|
|
717
1187
|
await this.#persist(session);
|
|
718
|
-
notifySnapshot(observer, session);
|
|
1188
|
+
this.#notifySnapshot(observer, session);
|
|
719
1189
|
return projectView(session);
|
|
720
1190
|
}
|
|
721
1191
|
}
|
|
722
1192
|
}
|
|
1193
|
+
async #cancelBeforeProviderDispatch(session, request, attempt, observer, advance) {
|
|
1194
|
+
if (advance !== undefined) {
|
|
1195
|
+
const streaming = session.events.find((event) => event.kind === "assistant" && event.id === advance.eventId);
|
|
1196
|
+
if (streaming !== undefined)
|
|
1197
|
+
streaming.status = "interrupted";
|
|
1198
|
+
}
|
|
1199
|
+
const message = "The player cancelled before the next Provider dispatch; the frozen request can be resumed without replaying a completed model result.";
|
|
1200
|
+
session.events.push({
|
|
1201
|
+
id: session.nextEventId++,
|
|
1202
|
+
kind: "cancellation",
|
|
1203
|
+
message,
|
|
1204
|
+
});
|
|
1205
|
+
session.status = "interrupted";
|
|
1206
|
+
session.canRetry = true;
|
|
1207
|
+
session.lastRequest = structuredClone(request);
|
|
1208
|
+
session.lastRequestAttempt = Math.max(0, attempt - 1);
|
|
1209
|
+
session.lastFailure = message;
|
|
1210
|
+
await this.#persist(session);
|
|
1211
|
+
if (advance !== undefined)
|
|
1212
|
+
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1213
|
+
this.#notifySnapshot(observer, session);
|
|
1214
|
+
return projectView(session);
|
|
1215
|
+
}
|
|
723
1216
|
/**
|
|
724
1217
|
* Dispatch the frozen follow-ups once against the settled exchange.
|
|
725
1218
|
*
|
|
@@ -728,11 +1221,24 @@ export class PlayCallChain {
|
|
|
728
1221
|
* player nothing more than a missing panel. Failures are recorded in the
|
|
729
1222
|
* trace and the chain still returns to `ready`.
|
|
730
1223
|
*/
|
|
731
|
-
async #runFollowups(session, modelHost, observer) {
|
|
1224
|
+
async #runFollowups(session, modelHost, signal, observer) {
|
|
732
1225
|
const followups = session.followups ?? [];
|
|
733
1226
|
if (this.#artifacts === undefined || followups.length === 0)
|
|
734
1227
|
return;
|
|
1228
|
+
const observeFollowupDelta = (delta) => {
|
|
1229
|
+
this.#observeInvocation(session.chainId, "followup", {
|
|
1230
|
+
...(delta.kind === "reasoning"
|
|
1231
|
+
? { reasoningChars: delta.text.length }
|
|
1232
|
+
: delta.kind === "text"
|
|
1233
|
+
? { textChars: delta.text.length }
|
|
1234
|
+
: { toolChars: delta.text.length }),
|
|
1235
|
+
});
|
|
1236
|
+
this.#notifySnapshot(observer, session);
|
|
1237
|
+
};
|
|
735
1238
|
const record = (outcome) => {
|
|
1239
|
+
this.#observeInvocation(session.chainId, "followup", {
|
|
1240
|
+
toolCalls: outcome.toolCalls.length,
|
|
1241
|
+
});
|
|
736
1242
|
session.events.push({
|
|
737
1243
|
id: session.nextEventId++,
|
|
738
1244
|
kind: "followup",
|
|
@@ -747,7 +1253,7 @@ export class PlayCallChain {
|
|
|
747
1253
|
...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
|
|
748
1254
|
});
|
|
749
1255
|
touch(session);
|
|
750
|
-
notifySnapshot(observer, session);
|
|
1256
|
+
this.#notifySnapshot(observer, session);
|
|
751
1257
|
};
|
|
752
1258
|
try {
|
|
753
1259
|
await runPlayFollowupRequests({
|
|
@@ -772,10 +1278,22 @@ export class PlayCallChain {
|
|
|
772
1278
|
},
|
|
773
1279
|
head: session.parentHead,
|
|
774
1280
|
maxOutputTokens: modelHost.binding().maxOutputTokens,
|
|
1281
|
+
signal,
|
|
775
1282
|
...(this.#failureLog === undefined
|
|
776
1283
|
? {}
|
|
777
1284
|
: { failureLog: this.#failureLog }),
|
|
778
|
-
observer: {
|
|
1285
|
+
observer: {
|
|
1286
|
+
onProviderDispatch: () => {
|
|
1287
|
+
this.#setInvocationAbortable(session.chainId, true);
|
|
1288
|
+
this.#observeInvocation(session.chainId, "followup", {
|
|
1289
|
+
dispatches: 1,
|
|
1290
|
+
});
|
|
1291
|
+
this.#notifySnapshot(observer, session);
|
|
1292
|
+
},
|
|
1293
|
+
onProviderDelta: observeFollowupDelta,
|
|
1294
|
+
onProviderSettled: () => this.#setInvocationAbortable(session.chainId, false),
|
|
1295
|
+
onOutcome: record,
|
|
1296
|
+
},
|
|
779
1297
|
});
|
|
780
1298
|
}
|
|
781
1299
|
catch (error) {
|
|
@@ -789,56 +1307,44 @@ export class PlayCallChain {
|
|
|
789
1307
|
touch(session);
|
|
790
1308
|
}
|
|
791
1309
|
}
|
|
792
|
-
async #commitStep(session, input) {
|
|
793
|
-
const parentHead = session.parentHead;
|
|
794
|
-
const outcome = await this.#worlds.commitPlayStep({
|
|
795
|
-
operationId: operationId(session.chainId, input.operationKey),
|
|
796
|
-
worldId: session.worldId,
|
|
797
|
-
parentHead,
|
|
798
|
-
historyAppend: structuredClone(input.historyAppend),
|
|
799
|
-
nextMaterials: structuredClone(session.nextMaterials),
|
|
800
|
-
stateChanges: structuredClone(input.stateChanges),
|
|
801
|
-
});
|
|
802
|
-
session.parentHead = outcome.head;
|
|
803
|
-
const sequence = Number(outcome.head.slice("commit:".length));
|
|
804
|
-
for (const [index, message] of input.historyAppend.entries())
|
|
805
|
-
session.history.push({
|
|
806
|
-
path: `${session.worldId}.message.${sequence}.${index + 1}.${message.role}`,
|
|
807
|
-
contents: message.exactText,
|
|
808
|
-
});
|
|
809
|
-
return outcome;
|
|
810
|
-
}
|
|
811
1310
|
async #requireChain(worldId, chainId) {
|
|
812
1311
|
validateIdentity(chainId, "call-chain ID");
|
|
813
1312
|
const active = this.#active.get(chainId);
|
|
814
1313
|
if (active !== undefined) {
|
|
815
1314
|
if (active.worldId !== worldId)
|
|
816
1315
|
throw new PlayCallChainError("The call chain does not belong to this world.");
|
|
1316
|
+
await this.#reconcileSessionAdvance(active);
|
|
817
1317
|
return active;
|
|
818
1318
|
}
|
|
819
1319
|
const persisted = await this.#readPersisted(worldId);
|
|
820
1320
|
if (persisted?.chainId !== chainId)
|
|
821
1321
|
throw new PlayCallChainError("The call chain does not exist; start from the latest world state.");
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
persisted.events.push({
|
|
832
|
-
id: persisted.nextEventId++,
|
|
833
|
-
kind: "failure",
|
|
834
|
-
message: persisted.lastFailure,
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
const binding = await this.#worlds.bindPlayCallChain(worldId);
|
|
838
|
-
if (binding.parentHead !== persisted.parentHead)
|
|
1322
|
+
const advance = await this.#worlds.playAdvances.readCurrent(worldId, chainId);
|
|
1323
|
+
const session = await this.#hydrateSession(persisted, advance);
|
|
1324
|
+
this.#active.set(chainId, session);
|
|
1325
|
+
this.#worldChains.set(worldId, chainId);
|
|
1326
|
+
await this.#reconcileSessionAdvance(session, advance);
|
|
1327
|
+
if (session.status === "running")
|
|
1328
|
+
await this.#interruptAbandonedDispatch(session);
|
|
1329
|
+
const currentHead = await this.#worlds.currentHead(worldId);
|
|
1330
|
+
if (currentHead !== session.parentHead)
|
|
839
1331
|
throw new PlayCallChainError("The call-chain record differs from the current world endpoint; use a fresh context.");
|
|
840
|
-
|
|
841
|
-
|
|
1332
|
+
return session;
|
|
1333
|
+
}
|
|
1334
|
+
async #hydrateSession(persisted, advance) {
|
|
1335
|
+
const binding = await this.#worlds.bindPlayCallChain(persisted.worldId);
|
|
1336
|
+
let documents;
|
|
1337
|
+
try {
|
|
1338
|
+
documents = restorePlayDocuments(binding.files, persisted, persisted.events);
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
const prepared = advance?.settlementPrepared?.settlement;
|
|
1342
|
+
if (prepared === undefined)
|
|
1343
|
+
throw error;
|
|
1344
|
+
documents = new FileNativePlayDocuments(binding.files);
|
|
1345
|
+
documents.restoreAuthorizationCheckpoint(prepared.authorizationCheckpoint);
|
|
1346
|
+
}
|
|
1347
|
+
return {
|
|
842
1348
|
...structuredClone(persisted),
|
|
843
1349
|
documentAuthorizationCheckpoints: persisted.documentAuthorizationCheckpoints?.map((checkpoint) => structuredClone(checkpoint)) ?? [],
|
|
844
1350
|
nextMaterials: structuredClone(binding.additionalMaterials),
|
|
@@ -848,32 +1354,79 @@ export class PlayCallChain {
|
|
|
848
1354
|
item.key,
|
|
849
1355
|
structuredClone(item),
|
|
850
1356
|
])),
|
|
1357
|
+
persistenceCursor: this.#loadedCursors.get(persisted.chainId) ?? {
|
|
1358
|
+
eventCount: 0,
|
|
1359
|
+
transcriptCount: 0,
|
|
1360
|
+
completedToolCount: 0,
|
|
1361
|
+
authorizationCheckpointCount: 0,
|
|
1362
|
+
},
|
|
851
1363
|
};
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
await this.#
|
|
856
|
-
|
|
1364
|
+
}
|
|
1365
|
+
async #reconcileSessionAdvance(session, loaded) {
|
|
1366
|
+
const current = loaded ??
|
|
1367
|
+
(await this.#worlds.playAdvances.readCurrent(session.worldId, session.chainId));
|
|
1368
|
+
if (current?.settled !== null)
|
|
1369
|
+
return;
|
|
1370
|
+
if (current.base.advanceKind === "player") {
|
|
1371
|
+
await this.#settlePlayerAdvance(session, current.base);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (current.providerCompleted === null) {
|
|
1375
|
+
await this.#interruptAbandonedDispatch(session, current.base);
|
|
1376
|
+
await this.#worlds.playAdvances.markSettled(current.base, session.parentHead);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
let settlement = current.settlementPrepared?.settlement;
|
|
1380
|
+
if (settlement === undefined) {
|
|
1381
|
+
settlement = this.#prepareResponseSettlement(session, current.base, current.providerCompleted.response);
|
|
1382
|
+
await this.#worlds.playAdvances.recordSettlementPrepared(current.base, settlement);
|
|
1383
|
+
}
|
|
1384
|
+
await this.#settleResponseAdvance(session, current.base, settlement);
|
|
1385
|
+
}
|
|
1386
|
+
async #interruptAbandonedDispatch(session, advance) {
|
|
1387
|
+
const streaming = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1388
|
+
event.status === "streaming" &&
|
|
1389
|
+
(advance === undefined || event.id === advance.eventId));
|
|
1390
|
+
if (streaming !== undefined)
|
|
1391
|
+
streaming.status = "interrupted";
|
|
1392
|
+
const dispatchMayHaveStarted = advance !== undefined;
|
|
1393
|
+
const canRetry = !dispatchMayHaveStarted && session.lastRequest !== null;
|
|
1394
|
+
const message = dispatchMayHaveStarted
|
|
1395
|
+
? "The service stopped after model dispatch may have started; the outcome is unknown and the request cannot be replayed. Use a fresh context."
|
|
1396
|
+
: canRetry
|
|
1397
|
+
? "The service stopped before model dispatch; the frozen request can be retried."
|
|
1398
|
+
: "The service stopped before preparing a model request; use a fresh context.";
|
|
1399
|
+
session.events.push({
|
|
1400
|
+
id: session.nextEventId++,
|
|
1401
|
+
kind: "failure",
|
|
1402
|
+
message,
|
|
1403
|
+
});
|
|
1404
|
+
session.status = "interrupted";
|
|
1405
|
+
session.canRetry = canRetry;
|
|
1406
|
+
session.lastFailure = message;
|
|
1407
|
+
await this.#persist(session);
|
|
857
1408
|
}
|
|
858
1409
|
async #assertCurrentHead(session) {
|
|
859
1410
|
if ((await this.#worlds.currentHead(session.worldId)) !== session.parentHead)
|
|
860
1411
|
throw new PlayCallChainError("Another operation has advanced the current world; use a fresh context.");
|
|
861
1412
|
}
|
|
862
1413
|
async #readPersisted(worldId) {
|
|
863
|
-
|
|
864
|
-
|
|
1414
|
+
await this.#worlds.ensureCurrentStorage(worldId);
|
|
1415
|
+
const loaded = await this.#worlds.playTimeline.readCurrent(worldId);
|
|
1416
|
+
if (loaded === null)
|
|
865
1417
|
return null;
|
|
866
|
-
|
|
867
|
-
return
|
|
1418
|
+
this.#loadedCursors.set(loaded.value.chainId, loaded.cursor);
|
|
1419
|
+
return structuredClone(loaded.value);
|
|
868
1420
|
}
|
|
869
1421
|
async #persist(session) {
|
|
870
1422
|
touch(session);
|
|
871
1423
|
session.completedTools = [...session.completedToolMap.values()].map((item) => structuredClone(item));
|
|
872
|
-
const { documents: _documents, history: _history, completedToolMap: _completedToolMap, ...persisted } = session;
|
|
1424
|
+
const { documents: _documents, history: _history, completedToolMap: _completedToolMap, persistenceCursor: _persistenceCursor, ...persisted } = session;
|
|
873
1425
|
void _documents;
|
|
874
1426
|
void _history;
|
|
875
1427
|
void _completedToolMap;
|
|
876
|
-
|
|
1428
|
+
void _persistenceCursor;
|
|
1429
|
+
session.persistenceCursor = await this.#worlds.playTimeline.persist(structuredClone(persisted), session.persistenceCursor);
|
|
877
1430
|
}
|
|
878
1431
|
}
|
|
879
1432
|
/**
|
|
@@ -884,21 +1437,72 @@ export class PlayCallChain {
|
|
|
884
1437
|
function followupOperationId(session) {
|
|
885
1438
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
886
1439
|
}
|
|
887
|
-
function createRequest(session, modelHost, exchange) {
|
|
1440
|
+
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1441
|
+
return createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1442
|
+
}
|
|
1443
|
+
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
888
1444
|
return {
|
|
889
1445
|
bootstrap: structuredClone(session.bootstrap),
|
|
890
1446
|
tools: structuredClone(session.tools),
|
|
891
1447
|
toolUniverse: structuredClone(session.tools),
|
|
892
1448
|
allowedTools: session.tools.map(({ name }) => name),
|
|
893
1449
|
toolStrategy: session.bootstrap.toolStrategy,
|
|
894
|
-
appended: structuredClone(
|
|
1450
|
+
appended: structuredClone([...appended]),
|
|
895
1451
|
requestId: "play_call_chain",
|
|
896
1452
|
operationId: session.chainId,
|
|
897
1453
|
requestAttempt: 1,
|
|
898
1454
|
exchange,
|
|
899
|
-
maxOutputTokens
|
|
1455
|
+
maxOutputTokens,
|
|
900
1456
|
};
|
|
901
1457
|
}
|
|
1458
|
+
function durableModelResponse(response) {
|
|
1459
|
+
const { diagnostics: _diagnostics, ...durable } = response;
|
|
1460
|
+
void _diagnostics;
|
|
1461
|
+
return structuredClone(durable);
|
|
1462
|
+
}
|
|
1463
|
+
function finalizePreparedReceipts(settlement) {
|
|
1464
|
+
const committedWriteRefs = new Set(settlement.stateChanges.map(({ stableShortRef }) => stableShortRef));
|
|
1465
|
+
const markdownByCall = new Map();
|
|
1466
|
+
for (const completed of settlement.completedTools) {
|
|
1467
|
+
const candidateWrite = completed.result.candidateWrite;
|
|
1468
|
+
if (!completed.result.ok || candidateWrite === undefined)
|
|
1469
|
+
continue;
|
|
1470
|
+
const markdown = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
|
|
1471
|
+
? `@${candidateWrite.shortRef} write succeeded`
|
|
1472
|
+
: `@${candidateWrite.shortRef} unchanged`;
|
|
1473
|
+
completed.result.markdown = markdown;
|
|
1474
|
+
delete completed.result.candidateWrite;
|
|
1475
|
+
const callId = completed.key.slice(completed.key.indexOf(":") + 1);
|
|
1476
|
+
markdownByCall.set(callId, markdown);
|
|
1477
|
+
}
|
|
1478
|
+
for (const event of settlement.trailingEvents) {
|
|
1479
|
+
if (event.kind !== "tool_result")
|
|
1480
|
+
continue;
|
|
1481
|
+
const markdown = markdownByCall.get(event.callId);
|
|
1482
|
+
if (markdown !== undefined)
|
|
1483
|
+
event.markdown = markdown;
|
|
1484
|
+
}
|
|
1485
|
+
for (const item of settlement.transcriptAppend) {
|
|
1486
|
+
if (item.kind !== "tool")
|
|
1487
|
+
continue;
|
|
1488
|
+
const markdown = markdownByCall.get(item.toolCallId);
|
|
1489
|
+
if (markdown !== undefined)
|
|
1490
|
+
item.markdown = markdown;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function appendCommittedHistory(session, head, messages) {
|
|
1494
|
+
const sequence = Number(head.slice("commit:".length));
|
|
1495
|
+
for (const [index, message] of messages.entries()) {
|
|
1496
|
+
const path = `message.${sequence}.${index + 1}.${message.role}`;
|
|
1497
|
+
const existing = session.history.find((item) => item.path === path);
|
|
1498
|
+
if (existing !== undefined) {
|
|
1499
|
+
if (existing.contents !== message.exactText)
|
|
1500
|
+
throw new PlayCallChainError("Recovered Authority history conflicts with the call-chain history.");
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
session.history.push({ path, contents: message.exactText });
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
902
1506
|
function prepareTool(session, working, exchange, call) {
|
|
903
1507
|
const key = `${exchange}:${call.id}`;
|
|
904
1508
|
const signature = JSON.stringify(call.arguments);
|
|
@@ -916,7 +1520,8 @@ function prepareTool(session, working, exchange, call) {
|
|
|
916
1520
|
markdown: "# Runtime tool rejected\n\nThe same tool-call ID is already bound to another tool name or argument set.",
|
|
917
1521
|
};
|
|
918
1522
|
}
|
|
919
|
-
else if (!callChainToolNames.has(call.name)
|
|
1523
|
+
else if (!callChainToolNames.has(call.name) ||
|
|
1524
|
+
!session.tools.some(({ name }) => name === call.name)) {
|
|
920
1525
|
result = {
|
|
921
1526
|
ok: false,
|
|
922
1527
|
failureKind: "protocol",
|
|
@@ -960,6 +1565,7 @@ function prepareTool(session, working, exchange, call) {
|
|
|
960
1565
|
kind: "tool",
|
|
961
1566
|
toolCallId: call.id,
|
|
962
1567
|
markdown: result.markdown,
|
|
1568
|
+
...(result.ok ? {} : { isError: true }),
|
|
963
1569
|
},
|
|
964
1570
|
};
|
|
965
1571
|
}
|
|
@@ -999,19 +1605,6 @@ function restorePlayDocuments(files, context, events) {
|
|
|
999
1605
|
}
|
|
1000
1606
|
return documents;
|
|
1001
1607
|
}
|
|
1002
|
-
function recordDocumentAuthorizationCheckpoint(session, afterEventId) {
|
|
1003
|
-
const authorization = session.documents.authorizationCheckpoint();
|
|
1004
|
-
const previous = session.documentAuthorizationCheckpoints.at(-1);
|
|
1005
|
-
if (previous !== undefined &&
|
|
1006
|
-
JSON.stringify(previous.authorization) === JSON.stringify(authorization))
|
|
1007
|
-
return;
|
|
1008
|
-
if (previous !== undefined && afterEventId <= previous.afterEventId)
|
|
1009
|
-
throw new PlayCallChainError("Call-chain document authorization checkpoints are out of order.");
|
|
1010
|
-
session.documentAuthorizationCheckpoints.push({
|
|
1011
|
-
afterEventId,
|
|
1012
|
-
authorization,
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
1608
|
function documentAuthorizationThroughEvents(context, events) {
|
|
1016
1609
|
const checkpoints = context.documentAuthorizationCheckpoints;
|
|
1017
1610
|
if (checkpoints === undefined || checkpoints.length === 0)
|
|
@@ -1023,20 +1616,6 @@ function documentAuthorizationThroughEvents(context, events) {
|
|
|
1023
1616
|
// old recovery behavior and rebuild only bootstrap authorization.
|
|
1024
1617
|
return selected === undefined ? undefined : structuredClone(selected);
|
|
1025
1618
|
}
|
|
1026
|
-
function snapshotCurrentContext(session) {
|
|
1027
|
-
const { schemaVersion: _schemaVersion, kind: _kind, worldId: _worldId, previousContexts: _previousContexts, ...context } = session;
|
|
1028
|
-
void _schemaVersion;
|
|
1029
|
-
void _kind;
|
|
1030
|
-
void _worldId;
|
|
1031
|
-
void _previousContexts;
|
|
1032
|
-
return structuredClone(context);
|
|
1033
|
-
}
|
|
1034
|
-
function persistedContexts(session) {
|
|
1035
|
-
return [
|
|
1036
|
-
...(session.previousContexts ?? []).map((context) => structuredClone(context)),
|
|
1037
|
-
snapshotCurrentContext(session),
|
|
1038
|
-
];
|
|
1039
|
-
}
|
|
1040
1619
|
function independentContextCopy(source) {
|
|
1041
1620
|
const context = structuredClone(source);
|
|
1042
1621
|
delete context.derivedFrom;
|
|
@@ -1068,12 +1647,43 @@ function projectContext(context) {
|
|
|
1068
1647
|
playPreset: structuredClone(context.playPreset),
|
|
1069
1648
|
status: context.status,
|
|
1070
1649
|
canRetry: context.canRetry,
|
|
1071
|
-
events:
|
|
1650
|
+
events: context.events
|
|
1651
|
+
.slice(-projectedEventTail)
|
|
1652
|
+
.map((event) => projectActivityEvent(event)),
|
|
1072
1653
|
changedDocuments: structuredClone(context.changedDocuments),
|
|
1073
1654
|
lastFailure: context.lastFailure,
|
|
1074
1655
|
updatedAt: context.updatedAt,
|
|
1075
1656
|
};
|
|
1076
1657
|
}
|
|
1658
|
+
function projectActivityEvent(event) {
|
|
1659
|
+
if (event.kind === "player" ||
|
|
1660
|
+
event.kind === "failure" ||
|
|
1661
|
+
event.kind === "cancellation")
|
|
1662
|
+
return structuredClone(event);
|
|
1663
|
+
if (event.kind === "assistant") {
|
|
1664
|
+
const { reasoning, toolFragment, usage, ...summary } = event;
|
|
1665
|
+
void reasoning;
|
|
1666
|
+
void toolFragment;
|
|
1667
|
+
void usage;
|
|
1668
|
+
return structuredClone(summary);
|
|
1669
|
+
}
|
|
1670
|
+
if (event.kind === "tool_call")
|
|
1671
|
+
return { ...structuredClone(event), arguments: null };
|
|
1672
|
+
if (event.kind === "tool_result")
|
|
1673
|
+
return { ...structuredClone(event), markdown: "" };
|
|
1674
|
+
const { reasoning, usage, ...summary } = event;
|
|
1675
|
+
void reasoning;
|
|
1676
|
+
void usage;
|
|
1677
|
+
return {
|
|
1678
|
+
...structuredClone(summary),
|
|
1679
|
+
text: "",
|
|
1680
|
+
toolCalls: event.toolCalls.map((call) => ({
|
|
1681
|
+
...structuredClone(call),
|
|
1682
|
+
arguments: null,
|
|
1683
|
+
markdown: "",
|
|
1684
|
+
})),
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1077
1687
|
function projectView(session) {
|
|
1078
1688
|
return {
|
|
1079
1689
|
...projectContext(session),
|
|
@@ -1111,7 +1721,9 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1111
1721
|
.find((candidate) => candidate.kind === "player" ||
|
|
1112
1722
|
candidate.kind === "assistant" ||
|
|
1113
1723
|
candidate.kind === "tool_call");
|
|
1114
|
-
const recorded = event.text.trim().length > 0 ||
|
|
1724
|
+
const recorded = event.text.trim().length > 0 ||
|
|
1725
|
+
hasToolCall?.kind === "tool_call" ||
|
|
1726
|
+
transcript[cursor]?.kind === "assistant";
|
|
1115
1727
|
if (!recorded)
|
|
1116
1728
|
continue;
|
|
1117
1729
|
const item = transcript[cursor];
|
|
@@ -1168,84 +1780,14 @@ function operationId(chainId, key) {
|
|
|
1168
1780
|
.update(`${chainId}\0${key}`)
|
|
1169
1781
|
.digest("hex")}`;
|
|
1170
1782
|
}
|
|
1171
|
-
function
|
|
1172
|
-
if (!
|
|
1173
|
-
(
|
|
1174
|
-
value.kind !== "play_call_chain" ||
|
|
1175
|
-
value.worldId !== worldId ||
|
|
1176
|
-
!isPersistedPlayCallChainContext(value) ||
|
|
1177
|
-
(value.previousContexts !== undefined &&
|
|
1178
|
-
(!Array.isArray(value.previousContexts) ||
|
|
1179
|
-
value.previousContexts.some((context) => !isPersistedPlayCallChainContext(context)))))
|
|
1180
|
-
throw new PlayCallChainError("The persisted call-chain record does not match the current format and cannot be read.");
|
|
1181
|
-
}
|
|
1182
|
-
function isPersistedPlayCallChainContext(value) {
|
|
1183
|
-
if (!isRecord(value))
|
|
1184
|
-
return false;
|
|
1185
|
-
return (typeof value.chainId === "string" &&
|
|
1186
|
-
typeof value.baselineHead === "string" &&
|
|
1187
|
-
(value.baselineHistoryLength === undefined ||
|
|
1188
|
-
(typeof value.baselineHistoryLength === "number" &&
|
|
1189
|
-
Number.isSafeInteger(value.baselineHistoryLength) &&
|
|
1190
|
-
value.baselineHistoryLength >= 0)) &&
|
|
1191
|
-
typeof value.parentHead === "string" &&
|
|
1192
|
-
(value.derivedFrom === undefined ||
|
|
1193
|
-
(isRecord(value.derivedFrom) &&
|
|
1194
|
-
typeof value.derivedFrom.worldId === "string" &&
|
|
1195
|
-
typeof value.derivedFrom.chainId === "string" &&
|
|
1196
|
-
typeof value.derivedFrom.head === "string")) &&
|
|
1197
|
-
(value.branchedBeforePlayer === undefined ||
|
|
1198
|
-
(isRecord(value.branchedBeforePlayer) &&
|
|
1199
|
-
typeof value.branchedBeforePlayer.worldId === "string" &&
|
|
1200
|
-
typeof value.branchedBeforePlayer.chainId === "string" &&
|
|
1201
|
-
Number.isSafeInteger(value.branchedBeforePlayer.eventId) &&
|
|
1202
|
-
value.branchedBeforePlayer.eventId > 0)) &&
|
|
1203
|
-
isRecord(value.playPreset) &&
|
|
1204
|
-
(value.followups === undefined || Array.isArray(value.followups)) &&
|
|
1205
|
-
(value.playPresetScriptsEnabled === undefined ||
|
|
1206
|
-
typeof value.playPresetScriptsEnabled === "boolean") &&
|
|
1207
|
-
(value.status === "ready" ||
|
|
1208
|
-
value.status === "running" ||
|
|
1209
|
-
value.status === "interrupted") &&
|
|
1210
|
-
typeof value.canRetry === "boolean" &&
|
|
1211
|
-
isRecord(value.bootstrap) &&
|
|
1212
|
-
Array.isArray(value.tools) &&
|
|
1213
|
-
Array.isArray(value.transcript) &&
|
|
1214
|
-
Array.isArray(value.events) &&
|
|
1215
|
-
Array.isArray(value.completedTools) &&
|
|
1216
|
-
(value.documentAuthorizationCheckpoints === undefined ||
|
|
1217
|
-
isPersistedDocumentAuthorizationHistory(value.documentAuthorizationCheckpoints, value.events)) &&
|
|
1218
|
-
Array.isArray(value.changedDocuments) &&
|
|
1219
|
-
Array.isArray(value.nextMaterials) &&
|
|
1220
|
-
Number.isSafeInteger(value.nextEventId) &&
|
|
1221
|
-
Number.isSafeInteger(value.exchange) &&
|
|
1222
|
-
(value.lastRequest === null || isRecord(value.lastRequest)) &&
|
|
1223
|
-
Number.isSafeInteger(value.lastRequestAttempt) &&
|
|
1224
|
-
(value.lastFailure === null || typeof value.lastFailure === "string") &&
|
|
1225
|
-
typeof value.updatedAt === "number");
|
|
1783
|
+
function assertModelHostBinding(frozen, modelHost) {
|
|
1784
|
+
if (!equalModelHostBinding(frozen, modelHost.binding()))
|
|
1785
|
+
throw new PlayCallChainError("The selected model connection does not match the frozen model binding; start a fresh context.");
|
|
1226
1786
|
}
|
|
1227
|
-
function
|
|
1228
|
-
if (
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
? [Number(event.id)]
|
|
1232
|
-
: []));
|
|
1233
|
-
let previousEventId = -1;
|
|
1234
|
-
for (const checkpoint of value) {
|
|
1235
|
-
if (!isRecord(checkpoint) ||
|
|
1236
|
-
Object.keys(checkpoint).length !== 2 ||
|
|
1237
|
-
!Object.hasOwn(checkpoint, "afterEventId") ||
|
|
1238
|
-
!Object.hasOwn(checkpoint, "authorization") ||
|
|
1239
|
-
!Number.isSafeInteger(checkpoint.afterEventId) ||
|
|
1240
|
-
Number(checkpoint.afterEventId) < 0 ||
|
|
1241
|
-
Number(checkpoint.afterEventId) <= previousEventId ||
|
|
1242
|
-
(Number(checkpoint.afterEventId) !== 0 &&
|
|
1243
|
-
!eventIds.has(Number(checkpoint.afterEventId))) ||
|
|
1244
|
-
!isPlayDocumentAuthorizationCheckpoint(checkpoint.authorization))
|
|
1245
|
-
return false;
|
|
1246
|
-
previousEventId = Number(checkpoint.afterEventId);
|
|
1247
|
-
}
|
|
1248
|
-
return true;
|
|
1787
|
+
function assertSessionModelHostBinding(session, modelHost) {
|
|
1788
|
+
if (session.modelBinding === undefined)
|
|
1789
|
+
throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
|
|
1790
|
+
assertModelHostBinding(session.modelBinding, modelHost);
|
|
1249
1791
|
}
|
|
1250
1792
|
function validateIdentity(value, label) {
|
|
1251
1793
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
@@ -1258,23 +1800,16 @@ function validatePlayerText(value) {
|
|
|
1258
1800
|
function touch(session) {
|
|
1259
1801
|
session.updatedAt = Date.now();
|
|
1260
1802
|
}
|
|
1261
|
-
function notifySnapshot(observer, session) {
|
|
1262
|
-
try {
|
|
1263
|
-
observer?.onSnapshot?.(projectView(session));
|
|
1264
|
-
}
|
|
1265
|
-
catch {
|
|
1266
|
-
// A browser stream may disconnect while the durable model request keeps
|
|
1267
|
-
// running. Presentation failure never cancels or changes play Authority.
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
1803
|
function notifyAssistantDelta(observer, delta) {
|
|
1271
1804
|
try {
|
|
1272
1805
|
observer?.onAssistantDelta?.(structuredClone(delta));
|
|
1273
1806
|
}
|
|
1274
1807
|
catch {
|
|
1275
|
-
//
|
|
1808
|
+
// Like snapshot projection, the Provider exchange outlives its browser stream.
|
|
1276
1809
|
}
|
|
1277
1810
|
}
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1811
|
+
function crashAtPlayAdvanceEdge(edge) {
|
|
1812
|
+
if (process.env.NODE_ENV === "test" &&
|
|
1813
|
+
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_PLAY_ADVANCE_EDGE === edge)
|
|
1814
|
+
throw new Error(`Simulated process exit at play advance edge: ${edge}`);
|
|
1280
1815
|
}
|