narraeon 0.1.1 → 0.2.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 +29 -0
- package/dist/node/runtime/V1Runtime.js +59 -23
- package/dist/node/runtime/model/FileNativeModelAdapters.js +691 -275
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +74 -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 +1039 -0
- package/dist/node/runtime/play/PlayCallChain.js +609 -330
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +1 -4
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -30
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +626 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +449 -191
- package/dist/web/assets/index-DHV_VWl3.js +1192 -0
- package/dist/web/assets/index-DjLCkP99.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, 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,7 @@ export class PlayCallChain {
|
|
|
27
34
|
#failureLog;
|
|
28
35
|
#active = new Map();
|
|
29
36
|
#worldChains = new Map();
|
|
37
|
+
#loadedCursors = new Map();
|
|
30
38
|
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog) {
|
|
31
39
|
this.#worlds = worlds;
|
|
32
40
|
this.#compiler = compiler;
|
|
@@ -37,6 +45,7 @@ export class PlayCallChain {
|
|
|
37
45
|
validateIdentity(input.chainId, "call-chain ID");
|
|
38
46
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
39
47
|
validatePlayerText(input.playerText);
|
|
48
|
+
assertModelHostBinding(input.modelBinding, input.modelHost);
|
|
40
49
|
const existing = await this.#readPersisted(input.worldId);
|
|
41
50
|
if (existing?.chainId === input.chainId) {
|
|
42
51
|
const duplicate = duplicatePlayerExchange(existing, input.exchangeId, input.playerText, "fresh");
|
|
@@ -70,16 +79,14 @@ export class PlayCallChain {
|
|
|
70
79
|
documents.bindBootstrap(bootstrap);
|
|
71
80
|
const now = Date.now();
|
|
72
81
|
const session = {
|
|
73
|
-
schemaVersion:
|
|
82
|
+
schemaVersion: 3,
|
|
74
83
|
kind: "play_call_chain",
|
|
75
84
|
chainId: input.chainId,
|
|
76
85
|
worldId: input.worldId,
|
|
77
|
-
previousContexts:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
snapshotCurrentContext(existing),
|
|
82
|
-
],
|
|
86
|
+
previousContexts: [],
|
|
87
|
+
previousChainId: existing?.chainId ?? null,
|
|
88
|
+
timelineGeneration: existing?.timelineGeneration ??
|
|
89
|
+
this.#worlds.playTimeline.newGeneration(),
|
|
83
90
|
baselineHead: binding.parentHead,
|
|
84
91
|
baselineHistoryLength: Object.keys(binding.history).length,
|
|
85
92
|
parentHead: binding.parentHead,
|
|
@@ -90,6 +97,7 @@ export class PlayCallChain {
|
|
|
90
97
|
},
|
|
91
98
|
followups: structuredClone(compilation.followups ?? []),
|
|
92
99
|
playPresetScriptsEnabled: input.playPreset.scriptsEnabled,
|
|
100
|
+
modelBinding: structuredClone(input.modelBinding),
|
|
93
101
|
status: "ready",
|
|
94
102
|
canRetry: false,
|
|
95
103
|
bootstrap,
|
|
@@ -114,6 +122,12 @@ export class PlayCallChain {
|
|
|
114
122
|
documents,
|
|
115
123
|
history: historyEntries(binding.history),
|
|
116
124
|
completedToolMap: new Map(),
|
|
125
|
+
persistenceCursor: {
|
|
126
|
+
eventCount: 0,
|
|
127
|
+
transcriptCount: 0,
|
|
128
|
+
completedToolCount: 0,
|
|
129
|
+
authorizationCheckpointCount: 0,
|
|
130
|
+
},
|
|
117
131
|
};
|
|
118
132
|
if (activeId !== undefined)
|
|
119
133
|
this.#active.delete(activeId);
|
|
@@ -126,6 +140,7 @@ export class PlayCallChain {
|
|
|
126
140
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
127
141
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
128
142
|
await this.#assertCurrentHead(session);
|
|
143
|
+
assertSessionModelHostBinding(session, input.modelHost);
|
|
129
144
|
if (input.playerText.trim().length === 0) {
|
|
130
145
|
if (session.status === "running")
|
|
131
146
|
throw new PlayCallChainError("A model request is still running.");
|
|
@@ -148,31 +163,21 @@ export class PlayCallChain {
|
|
|
148
163
|
async inspectWorld(worldId) {
|
|
149
164
|
const activeId = this.#worldChains.get(worldId);
|
|
150
165
|
const active = activeId === undefined ? undefined : this.#active.get(activeId);
|
|
151
|
-
if (active !== undefined)
|
|
166
|
+
if (active !== undefined) {
|
|
167
|
+
await this.#reconcileSessionAdvance(active);
|
|
152
168
|
return projectView(active);
|
|
169
|
+
}
|
|
153
170
|
const persisted = await this.#readPersisted(worldId);
|
|
154
171
|
if (persisted === null)
|
|
155
172
|
return null;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
persisted.events.push({
|
|
165
|
-
id: persisted.nextEventId++,
|
|
166
|
-
kind: "failure",
|
|
167
|
-
message,
|
|
168
|
-
});
|
|
169
|
-
persisted.status = "interrupted";
|
|
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);
|
|
173
|
+
const advance = await this.#worlds.playAdvances.readCurrent(worldId, persisted.chainId);
|
|
174
|
+
const session = await this.#hydrateSession(persisted, advance);
|
|
175
|
+
this.#active.set(session.chainId, session);
|
|
176
|
+
this.#worldChains.set(worldId, session.chainId);
|
|
177
|
+
await this.#reconcileSessionAdvance(session, advance);
|
|
178
|
+
if (session.status === "running")
|
|
179
|
+
await this.#interruptAbandonedDispatch(session);
|
|
180
|
+
return projectView(session);
|
|
176
181
|
}
|
|
177
182
|
async forkToDerivedWorld(input) {
|
|
178
183
|
const existing = await this.#readPersisted(input.targetWorldId);
|
|
@@ -181,7 +186,7 @@ export class PlayCallChain {
|
|
|
181
186
|
const source = await this.#readPersisted(input.sourceWorldId);
|
|
182
187
|
if (source === null)
|
|
183
188
|
return null;
|
|
184
|
-
const sourceContexts =
|
|
189
|
+
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.sourceWorldId);
|
|
185
190
|
const selectedContextIndex = sourceContexts.findIndex(({ events }) => eventsThroughHead(events, input.sourceHead) !== null);
|
|
186
191
|
if (selectedContextIndex < 0)
|
|
187
192
|
return null;
|
|
@@ -216,7 +221,7 @@ export class PlayCallChain {
|
|
|
216
221
|
const source = await this.#readPersisted(input.worldId);
|
|
217
222
|
if (source === null)
|
|
218
223
|
throw new PlayCallChainError("The current world has no model call chain to revise.");
|
|
219
|
-
const sourceContexts =
|
|
224
|
+
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.worldId);
|
|
220
225
|
const appliedEvent = sourceContexts
|
|
221
226
|
.flatMap(({ events }) => events)
|
|
222
227
|
.find((event) => event.kind === "player" &&
|
|
@@ -311,13 +316,15 @@ export class PlayCallChain {
|
|
|
311
316
|
throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
|
|
312
317
|
const documents = restorePlayDocuments(binding.files, sourceContext, prefixEvents);
|
|
313
318
|
const revised = {
|
|
314
|
-
schemaVersion:
|
|
319
|
+
schemaVersion: 3,
|
|
315
320
|
kind: "play_call_chain",
|
|
316
321
|
chainId: derivedChainId(sourceContext.chainId, "timeline-revision:" + request.operationId, request.worldId),
|
|
317
322
|
worldId: request.worldId,
|
|
318
|
-
previousContexts:
|
|
319
|
-
|
|
320
|
-
|
|
323
|
+
previousContexts: [],
|
|
324
|
+
previousChainId: input.selectedContextIndex === 0
|
|
325
|
+
? null
|
|
326
|
+
: input.sourceContexts[input.selectedContextIndex - 1].chainId,
|
|
327
|
+
timelineGeneration: this.#worlds.playTimeline.newGeneration(),
|
|
321
328
|
baselineHead: sourceContext.baselineHead,
|
|
322
329
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
323
330
|
parentHead: outcome.head,
|
|
@@ -330,6 +337,9 @@ export class PlayCallChain {
|
|
|
330
337
|
: {
|
|
331
338
|
playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
|
|
332
339
|
}),
|
|
340
|
+
...(sourceContext.modelBinding === undefined
|
|
341
|
+
? {}
|
|
342
|
+
: { modelBinding: structuredClone(sourceContext.modelBinding) }),
|
|
333
343
|
status: "ready",
|
|
334
344
|
canRetry: false,
|
|
335
345
|
bootstrap: structuredClone(sourceContext.bootstrap),
|
|
@@ -351,7 +361,7 @@ export class PlayCallChain {
|
|
|
351
361
|
lastFailure: null,
|
|
352
362
|
updatedAt: Date.now(),
|
|
353
363
|
};
|
|
354
|
-
await this.#worlds.
|
|
364
|
+
await this.#worlds.playTimeline.persist(structuredClone(revised));
|
|
355
365
|
const activeId = this.#worldChains.get(request.worldId);
|
|
356
366
|
if (activeId !== undefined)
|
|
357
367
|
this.#active.delete(activeId);
|
|
@@ -380,14 +390,32 @@ export class PlayCallChain {
|
|
|
380
390
|
const derivedBinding = await this.#worlds.bindPlayCallChain(input.targetWorldId);
|
|
381
391
|
const derivedDocuments = restorePlayDocuments(derivedBinding.files, sourceContext, input.sourceEvents);
|
|
382
392
|
const now = Date.now();
|
|
393
|
+
const timelineGeneration = this.#worlds.playTimeline.newGeneration();
|
|
394
|
+
let previousChainId = null;
|
|
395
|
+
for (const [index, context] of input.sourceContexts
|
|
396
|
+
.slice(0, input.selectedContextIndex)
|
|
397
|
+
.entries()) {
|
|
398
|
+
const chainId = derivedChainId(context.chainId, `prefix:${input.branchIdentity}:${index}`, input.targetWorldId);
|
|
399
|
+
await this.#worlds.playTimeline.persist({
|
|
400
|
+
...independentContextCopy(context),
|
|
401
|
+
schemaVersion: 3,
|
|
402
|
+
kind: "play_call_chain",
|
|
403
|
+
worldId: input.targetWorldId,
|
|
404
|
+
chainId,
|
|
405
|
+
previousContexts: [],
|
|
406
|
+
previousChainId,
|
|
407
|
+
timelineGeneration,
|
|
408
|
+
});
|
|
409
|
+
previousChainId = chainId;
|
|
410
|
+
}
|
|
383
411
|
const derived = {
|
|
384
|
-
schemaVersion:
|
|
412
|
+
schemaVersion: 3,
|
|
385
413
|
kind: "play_call_chain",
|
|
386
414
|
chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
|
|
387
415
|
worldId: input.targetWorldId,
|
|
388
|
-
previousContexts:
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
previousContexts: [],
|
|
417
|
+
previousChainId,
|
|
418
|
+
timelineGeneration,
|
|
391
419
|
baselineHead: sourceContext.baselineHead,
|
|
392
420
|
baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
|
|
393
421
|
parentHead: input.sourceHead,
|
|
@@ -400,6 +428,9 @@ export class PlayCallChain {
|
|
|
400
428
|
: {
|
|
401
429
|
playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
|
|
402
430
|
}),
|
|
431
|
+
...(sourceContext.modelBinding === undefined
|
|
432
|
+
? {}
|
|
433
|
+
: { modelBinding: structuredClone(sourceContext.modelBinding) }),
|
|
403
434
|
status: "ready",
|
|
404
435
|
canRetry: false,
|
|
405
436
|
bootstrap: structuredClone(sourceContext.bootstrap),
|
|
@@ -421,7 +452,7 @@ export class PlayCallChain {
|
|
|
421
452
|
lastFailure: null,
|
|
422
453
|
updatedAt: now,
|
|
423
454
|
};
|
|
424
|
-
await this.#worlds.
|
|
455
|
+
await this.#worlds.playTimeline.persist(structuredClone(derived));
|
|
425
456
|
return projectView(derived);
|
|
426
457
|
}
|
|
427
458
|
forgetWorld(worldId) {
|
|
@@ -438,17 +469,44 @@ export class PlayCallChain {
|
|
|
438
469
|
return projectView(session);
|
|
439
470
|
if (session.status === "running")
|
|
440
471
|
throw new PlayCallChainError("The call chain is waiting for the model to return.");
|
|
441
|
-
session.
|
|
442
|
-
|
|
443
|
-
|
|
472
|
+
const exchange = session.exchange + 1;
|
|
473
|
+
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
474
|
+
...session.transcript,
|
|
475
|
+
{ kind: "player", text: playerText },
|
|
476
|
+
]);
|
|
477
|
+
const operationKey = `player:${exchangeId}`;
|
|
478
|
+
const playerOperationId = operationId(session.chainId, operationKey);
|
|
479
|
+
const advance = {
|
|
480
|
+
schemaVersion: 1,
|
|
481
|
+
kind: "play_advance",
|
|
482
|
+
advanceKind: "player",
|
|
483
|
+
worldId: session.worldId,
|
|
484
|
+
chainId: session.chainId,
|
|
485
|
+
advanceId: playerOperationId,
|
|
486
|
+
operationId: playerOperationId,
|
|
487
|
+
parentHead: session.parentHead,
|
|
488
|
+
eventId: session.nextEventId,
|
|
489
|
+
exchangeId,
|
|
490
|
+
playerText,
|
|
491
|
+
context,
|
|
492
|
+
transcriptStart: session.transcript.length,
|
|
493
|
+
exchange,
|
|
494
|
+
nextRequest: structuredClone(nextRequest),
|
|
495
|
+
createdAt: Date.now(),
|
|
496
|
+
};
|
|
497
|
+
await this.#worlds.playAdvances.begin(advance);
|
|
444
498
|
try {
|
|
445
|
-
|
|
446
|
-
operationKey: `player:${exchangeId}`,
|
|
447
|
-
historyAppend: [{ role: "player", exactText: playerText }],
|
|
448
|
-
stateChanges: [],
|
|
449
|
-
});
|
|
499
|
+
await this.#settlePlayerAdvance(session, advance);
|
|
450
500
|
}
|
|
451
501
|
catch (error) {
|
|
502
|
+
const accepted = await this.#worlds
|
|
503
|
+
.getOperationOutcome(advance.operationId)
|
|
504
|
+
.catch(() => ({ outcome: "not_started" }));
|
|
505
|
+
if (accepted.outcome === "committed" ||
|
|
506
|
+
accepted.outcome === "committed_materialization_pending") {
|
|
507
|
+
await this.#settlePlayerAdvance(session, advance);
|
|
508
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, observer);
|
|
509
|
+
}
|
|
452
510
|
const message = error instanceof Error
|
|
453
511
|
? error.message
|
|
454
512
|
: "Failed to write the original player text.";
|
|
@@ -461,22 +519,272 @@ export class PlayCallChain {
|
|
|
461
519
|
session.canRetry = false;
|
|
462
520
|
session.lastFailure = message;
|
|
463
521
|
await this.#persist(session);
|
|
522
|
+
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
464
523
|
throw new PlayCallChainError(message);
|
|
465
524
|
}
|
|
466
|
-
session
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
525
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, observer);
|
|
526
|
+
}
|
|
527
|
+
async #settlePlayerAdvance(session, advance) {
|
|
528
|
+
const outcome = await this.#worlds.commitPlayStep({
|
|
529
|
+
operationId: advance.operationId,
|
|
530
|
+
worldId: session.worldId,
|
|
531
|
+
parentHead: advance.parentHead,
|
|
532
|
+
historyAppend: [{ role: "player", exactText: advance.playerText }],
|
|
533
|
+
nextMaterials: structuredClone(session.nextMaterials),
|
|
534
|
+
stateChanges: [],
|
|
474
535
|
});
|
|
536
|
+
if (outcome.worldId !== session.worldId ||
|
|
537
|
+
outcome.parentHead !== advance.parentHead)
|
|
538
|
+
throw new PlayCallChainError("The recovered player advance belongs to a different world endpoint.");
|
|
539
|
+
const playerEvent = {
|
|
540
|
+
id: advance.eventId,
|
|
541
|
+
kind: "player",
|
|
542
|
+
exchangeId: advance.exchangeId,
|
|
543
|
+
text: advance.playerText,
|
|
544
|
+
context: advance.context,
|
|
545
|
+
committedHead: outcome.head,
|
|
546
|
+
};
|
|
547
|
+
session.events = [
|
|
548
|
+
...session.events.filter(({ id }) => id < advance.eventId),
|
|
549
|
+
playerEvent,
|
|
550
|
+
];
|
|
551
|
+
if (session.transcript.length < advance.transcriptStart)
|
|
552
|
+
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its player prefix.");
|
|
553
|
+
session.transcript = [
|
|
554
|
+
...session.transcript.slice(0, advance.transcriptStart),
|
|
555
|
+
{ kind: "player", text: advance.playerText },
|
|
556
|
+
];
|
|
557
|
+
session.nextEventId = advance.eventId + 1;
|
|
558
|
+
session.parentHead = outcome.head;
|
|
559
|
+
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
560
|
+
session.exchange = advance.exchange;
|
|
561
|
+
session.lastRequest = structuredClone(advance.nextRequest);
|
|
562
|
+
session.lastRequestAttempt = 0;
|
|
563
|
+
session.status = "interrupted";
|
|
564
|
+
session.canRetry = true;
|
|
565
|
+
session.lastFailure =
|
|
566
|
+
"The player message is committed; the frozen model request is ready to continue.";
|
|
475
567
|
await this.#persist(session);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
568
|
+
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
569
|
+
await this.#worlds.playAdvances.markSettled(advance, outcome.head);
|
|
570
|
+
}
|
|
571
|
+
#prepareResponseSettlement(session, advance, response) {
|
|
572
|
+
const existing = session.events.find(({ id }) => id === advance.eventId);
|
|
573
|
+
if (existing?.kind !== "assistant")
|
|
574
|
+
throw new PlayCallChainError("The durable Provider result no longer matches its assistant event.");
|
|
575
|
+
const calls = response.toolCalls ?? [];
|
|
576
|
+
const text = response.text ?? existing.text;
|
|
577
|
+
const hasText = text.trim().length > 0;
|
|
578
|
+
const responseKind = calls.length > 0 ? "tool_step" : hasText ? "narrative" : "empty";
|
|
579
|
+
const assistantEvent = {
|
|
580
|
+
id: advance.eventId,
|
|
581
|
+
kind: "assistant",
|
|
582
|
+
text,
|
|
583
|
+
status: "completed",
|
|
584
|
+
responseKind,
|
|
585
|
+
exchange: advance.exchange,
|
|
586
|
+
attempt: advance.attempt,
|
|
587
|
+
...(response.reasoningContent === undefined
|
|
588
|
+
? existing.reasoning === undefined
|
|
589
|
+
? {}
|
|
590
|
+
: { reasoning: existing.reasoning }
|
|
591
|
+
: { reasoning: response.reasoningContent }),
|
|
592
|
+
...(response.usage === undefined
|
|
593
|
+
? {}
|
|
594
|
+
: { usage: structuredClone(response.usage) }),
|
|
595
|
+
...(response.stopReason === undefined
|
|
596
|
+
? {}
|
|
597
|
+
: { stopReason: response.stopReason }),
|
|
598
|
+
continuation: response.providerState === undefined ? "unavailable" : "available",
|
|
599
|
+
};
|
|
600
|
+
const workingTools = new Map(session.completedToolMap);
|
|
601
|
+
const prepared = [];
|
|
602
|
+
const trailingEvents = [];
|
|
603
|
+
let nextEventId = advance.eventId + 1;
|
|
604
|
+
for (const call of calls) {
|
|
605
|
+
const item = prepareTool(session, workingTools, advance.exchange, call);
|
|
606
|
+
prepared.push(item);
|
|
607
|
+
trailingEvents.push({
|
|
608
|
+
id: nextEventId++,
|
|
609
|
+
kind: "tool_call",
|
|
610
|
+
callId: call.id,
|
|
611
|
+
name: call.name,
|
|
612
|
+
arguments: structuredClone(call.arguments),
|
|
613
|
+
replayed: item.replayed,
|
|
614
|
+
});
|
|
615
|
+
item.event.id = nextEventId++;
|
|
616
|
+
trailingEvents.push(structuredClone(item.event));
|
|
617
|
+
}
|
|
618
|
+
const stateChanges = session.documents.stateChanges();
|
|
619
|
+
const visibleText = responseKind === "narrative";
|
|
620
|
+
const assistantItem = {
|
|
621
|
+
kind: "assistant",
|
|
622
|
+
text,
|
|
623
|
+
...(response.reasoningContent === undefined
|
|
624
|
+
? {}
|
|
625
|
+
: { reasoningContent: response.reasoningContent }),
|
|
626
|
+
...(response.providerState === undefined
|
|
627
|
+
? {}
|
|
628
|
+
: { providerState: response.providerState }),
|
|
629
|
+
toolCalls: structuredClone(calls),
|
|
630
|
+
};
|
|
631
|
+
return {
|
|
632
|
+
assistantEvent,
|
|
633
|
+
trailingEvents,
|
|
634
|
+
transcriptStart: session.transcript.length,
|
|
635
|
+
transcriptAppend: [
|
|
636
|
+
...(calls.length > 0 ||
|
|
637
|
+
visibleText ||
|
|
638
|
+
response.providerState !== undefined
|
|
639
|
+
? [assistantItem]
|
|
640
|
+
: []),
|
|
641
|
+
...prepared.map(({ transcript }) => structuredClone(transcript)),
|
|
642
|
+
],
|
|
643
|
+
completedTools: [...workingTools.values()].map((item) => structuredClone(item)),
|
|
644
|
+
authorizationCheckpoint: session.documents.authorizationCheckpoint(),
|
|
645
|
+
stateChanges: structuredClone(stateChanges),
|
|
646
|
+
visibleText,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
async #settleResponseAdvance(session, advance, prepared) {
|
|
650
|
+
const settlement = structuredClone(prepared);
|
|
651
|
+
const hasToolCalls = settlement.trailingEvents.some(({ kind }) => kind === "tool_call");
|
|
652
|
+
// A legacy prepared fact has no responseKind. Its visibleText decision is
|
|
653
|
+
// already part of an immutable recovery operation and must be replayed as
|
|
654
|
+
// recorded; every newly prepared fact is checked against the current rule.
|
|
655
|
+
if (settlement.assistantEvent.responseKind !== undefined) {
|
|
656
|
+
const expectedResponseKind = hasToolCalls
|
|
657
|
+
? "tool_step"
|
|
658
|
+
: settlement.assistantEvent.text.trim().length > 0
|
|
659
|
+
? "narrative"
|
|
660
|
+
: "empty";
|
|
661
|
+
if (settlement.assistantEvent.responseKind !== expectedResponseKind ||
|
|
662
|
+
settlement.visibleText !== (expectedResponseKind === "narrative"))
|
|
663
|
+
throw new PlayCallChainError("The prepared response settlement has an inconsistent narrative classification.");
|
|
664
|
+
}
|
|
665
|
+
let committedHead;
|
|
666
|
+
if (settlement.visibleText || settlement.stateChanges.length > 0) {
|
|
667
|
+
const outcome = await this.#worlds.commitPlayStep({
|
|
668
|
+
operationId: advance.operationId,
|
|
669
|
+
worldId: session.worldId,
|
|
670
|
+
parentHead: advance.parentHead,
|
|
671
|
+
historyAppend: settlement.visibleText
|
|
672
|
+
? [
|
|
673
|
+
{
|
|
674
|
+
role: "narrator",
|
|
675
|
+
exactText: settlement.assistantEvent.text,
|
|
676
|
+
},
|
|
677
|
+
]
|
|
678
|
+
: [],
|
|
679
|
+
nextMaterials: structuredClone(session.nextMaterials),
|
|
680
|
+
stateChanges: structuredClone(settlement.stateChanges),
|
|
681
|
+
});
|
|
682
|
+
committedHead = outcome.head;
|
|
683
|
+
settlement.assistantEvent.committedHead = outcome.head;
|
|
684
|
+
crashAtPlayAdvanceEdge("after_authority_accepted");
|
|
685
|
+
appendCommittedHistory(session, outcome.head, outcome.historyAppend);
|
|
686
|
+
}
|
|
687
|
+
finalizePreparedReceipts(settlement);
|
|
688
|
+
session.events = [
|
|
689
|
+
...session.events.filter(({ id }) => id < advance.eventId),
|
|
690
|
+
structuredClone(settlement.assistantEvent),
|
|
691
|
+
...structuredClone(settlement.trailingEvents),
|
|
692
|
+
];
|
|
693
|
+
session.nextEventId =
|
|
694
|
+
Math.max(advance.eventId, ...settlement.trailingEvents.map(({ id }) => id)) + 1;
|
|
695
|
+
if (session.transcript.length < settlement.transcriptStart)
|
|
696
|
+
throw new PlayCallChainError("The recovered call-chain transcript is shorter than its settlement prefix.");
|
|
697
|
+
session.transcript = [
|
|
698
|
+
...session.transcript.slice(0, settlement.transcriptStart),
|
|
699
|
+
...structuredClone(settlement.transcriptAppend),
|
|
700
|
+
];
|
|
701
|
+
session.completedTools = structuredClone(settlement.completedTools);
|
|
702
|
+
session.completedToolMap = new Map(settlement.completedTools.map((item) => [
|
|
703
|
+
item.key,
|
|
704
|
+
structuredClone(item),
|
|
705
|
+
]));
|
|
706
|
+
const lastToolEventId = settlement.trailingEvents.findLast(({ kind }) => kind === "tool_result")?.id;
|
|
707
|
+
if (lastToolEventId !== undefined) {
|
|
708
|
+
session.documentAuthorizationCheckpoints =
|
|
709
|
+
session.documentAuthorizationCheckpoints.filter(({ afterEventId }) => afterEventId < advance.eventId);
|
|
710
|
+
session.documentAuthorizationCheckpoints.push({
|
|
711
|
+
afterEventId: lastToolEventId,
|
|
712
|
+
authorization: structuredClone(settlement.authorizationCheckpoint),
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (committedHead !== undefined)
|
|
716
|
+
session.parentHead = committedHead;
|
|
717
|
+
if (settlement.stateChanges.length > 0) {
|
|
718
|
+
const currentAuthorization = session.documents.authorizationCheckpoint();
|
|
719
|
+
if (currentAuthorization.stateFingerprint ===
|
|
720
|
+
settlement.authorizationCheckpoint.stateFingerprint)
|
|
721
|
+
session.documents.acceptCommittedState();
|
|
722
|
+
else {
|
|
723
|
+
const binding = await this.#worlds.bindPlayCallChain(session.worldId);
|
|
724
|
+
session.documents = new FileNativePlayDocuments(binding.files);
|
|
725
|
+
session.documents.restoreAuthorizationCheckpoint(settlement.authorizationCheckpoint);
|
|
726
|
+
session.history = historyEntries(binding.history);
|
|
727
|
+
session.nextMaterials = structuredClone(binding.additionalMaterials);
|
|
728
|
+
}
|
|
729
|
+
mergeChangedDocuments(session, settlement.stateChanges);
|
|
730
|
+
}
|
|
731
|
+
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
732
|
+
session.canRetry = false;
|
|
733
|
+
session.lastFailure = null;
|
|
734
|
+
let result;
|
|
735
|
+
if (settlement.assistantEvent.continuation !== "available") {
|
|
736
|
+
const message = "The complete model response has no provider-native continuation payload; use a fresh context.";
|
|
737
|
+
session.events.push({
|
|
738
|
+
id: session.nextEventId++,
|
|
739
|
+
kind: "failure",
|
|
740
|
+
message,
|
|
741
|
+
});
|
|
742
|
+
session.status = "interrupted";
|
|
743
|
+
session.lastFailure = message;
|
|
744
|
+
result = { kind: "terminal", status: "interrupted" };
|
|
745
|
+
}
|
|
746
|
+
else if (hasToolCalls) {
|
|
747
|
+
session.exchange += 1;
|
|
748
|
+
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
749
|
+
session.lastRequest = structuredClone(nextRequest);
|
|
750
|
+
session.lastRequestAttempt = 0;
|
|
751
|
+
session.status = "interrupted";
|
|
752
|
+
session.canRetry = true;
|
|
753
|
+
session.lastFailure =
|
|
754
|
+
"The completed tool response is settled; its frozen continuation request is ready.";
|
|
755
|
+
result = { kind: "continue", nextRequest };
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
session.status = settlement.visibleText ? "ready" : "interrupted";
|
|
759
|
+
if (!settlement.visibleText) {
|
|
760
|
+
const message = "The model returned neither text nor a complete tool call.";
|
|
761
|
+
session.events.push({
|
|
762
|
+
id: session.nextEventId++,
|
|
763
|
+
kind: "failure",
|
|
764
|
+
message,
|
|
765
|
+
});
|
|
766
|
+
session.lastFailure = message;
|
|
767
|
+
}
|
|
768
|
+
result = { kind: "terminal", status: session.status };
|
|
769
|
+
}
|
|
770
|
+
await this.#persist(session);
|
|
771
|
+
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
772
|
+
await this.#worlds.playAdvances.markSettled(advance, committedHead ?? session.parentHead);
|
|
773
|
+
return result;
|
|
774
|
+
}
|
|
775
|
+
async #recoverAcceptedResponseAdvance(session, advance) {
|
|
776
|
+
const outcome = await this.#worlds
|
|
777
|
+
.getOperationOutcome(advance.operationId)
|
|
778
|
+
.catch(() => ({ outcome: "not_started" }));
|
|
779
|
+
if (outcome.outcome !== "committed" &&
|
|
780
|
+
outcome.outcome !== "committed_materialization_pending")
|
|
781
|
+
return null;
|
|
782
|
+
const current = await this.#worlds.playAdvances.readCurrent(advance.worldId, advance.chainId);
|
|
783
|
+
if (current?.base.advanceKind !== "response" ||
|
|
784
|
+
current.base.advanceId !== advance.advanceId ||
|
|
785
|
+
current.settlementPrepared === null)
|
|
786
|
+
throw new PlayCallChainError("Authority accepted a response whose durable settlement is missing.");
|
|
787
|
+
return this.#settleResponseAdvance(session, advance, current.settlementPrepared.settlement);
|
|
480
788
|
}
|
|
481
789
|
async #dispatch(session, modelHost, firstRequest, firstAttempt, observer) {
|
|
482
790
|
let request = structuredClone(firstRequest);
|
|
@@ -493,11 +801,29 @@ export class PlayCallChain {
|
|
|
493
801
|
kind: "assistant",
|
|
494
802
|
text: "",
|
|
495
803
|
status: "streaming",
|
|
804
|
+
responseKind: "pending",
|
|
496
805
|
exchange: responseExchange,
|
|
497
806
|
attempt,
|
|
498
807
|
};
|
|
499
808
|
session.events.push(event);
|
|
500
809
|
await this.#persist(session);
|
|
810
|
+
const responseOperationId = operationId(session.chainId, `response:${responseExchange}:attempt:${attempt}`);
|
|
811
|
+
const advance = {
|
|
812
|
+
schemaVersion: 1,
|
|
813
|
+
kind: "play_advance",
|
|
814
|
+
advanceKind: "response",
|
|
815
|
+
worldId: session.worldId,
|
|
816
|
+
chainId: session.chainId,
|
|
817
|
+
advanceId: responseOperationId,
|
|
818
|
+
operationId: responseOperationId,
|
|
819
|
+
parentHead: session.parentHead,
|
|
820
|
+
eventId: event.id,
|
|
821
|
+
exchange: responseExchange,
|
|
822
|
+
attempt,
|
|
823
|
+
createdAt: Date.now(),
|
|
824
|
+
};
|
|
825
|
+
await this.#worlds.playAdvances.begin(advance);
|
|
826
|
+
crashAtPlayAdvanceEdge("after_response_advance_began");
|
|
501
827
|
notifySnapshot(observer, session);
|
|
502
828
|
let response;
|
|
503
829
|
try {
|
|
@@ -532,53 +858,24 @@ export class PlayCallChain {
|
|
|
532
858
|
message,
|
|
533
859
|
});
|
|
534
860
|
session.status = "interrupted";
|
|
535
|
-
|
|
861
|
+
const requiresFreshContext = error instanceof ModelHostContinuationError ||
|
|
862
|
+
error instanceof ModelHostOutcomeUnknownError;
|
|
863
|
+
session.canRetry =
|
|
864
|
+
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
536
865
|
session.lastFailure = message;
|
|
537
866
|
await this.#persist(session);
|
|
867
|
+
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
538
868
|
notifySnapshot(observer, session);
|
|
539
869
|
return projectView(session);
|
|
540
870
|
}
|
|
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
|
-
};
|
|
871
|
+
const durableResponse = durableModelResponse(response);
|
|
564
872
|
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);
|
|
873
|
+
await this.#worlds.playAdvances.recordProviderCompleted(advance, durableResponse);
|
|
874
|
+
crashAtPlayAdvanceEdge("after_provider_completed");
|
|
875
|
+
const settlement = this.#prepareResponseSettlement(session, advance, durableResponse);
|
|
876
|
+
await this.#worlds.playAdvances.recordSettlementPrepared(advance, settlement);
|
|
877
|
+
crashAtPlayAdvanceEdge("after_settlement_prepared");
|
|
878
|
+
const failedTools = settlement.trailingEvents.filter((candidate) => candidate.kind === "tool_result" && !candidate.ok);
|
|
582
879
|
if (failedTools.length > 0 && response.diagnostics !== undefined)
|
|
583
880
|
await this.#failureLog?.recordFailure({
|
|
584
881
|
exchange: response.diagnostics,
|
|
@@ -587,66 +884,20 @@ export class PlayCallChain {
|
|
|
587
884
|
kind: "tool_execution",
|
|
588
885
|
message: "A Runtime tool call from the model was rejected.",
|
|
589
886
|
details: {
|
|
590
|
-
calls: failedTools.map((
|
|
591
|
-
id:
|
|
592
|
-
name:
|
|
593
|
-
arguments: structuredClone(call.arguments),
|
|
887
|
+
calls: failedTools.map((result) => ({
|
|
888
|
+
id: result.callId,
|
|
889
|
+
name: result.name,
|
|
594
890
|
ok: result.ok,
|
|
595
|
-
failureKind: result.failureKind,
|
|
596
891
|
markdown: result.markdown,
|
|
597
|
-
replayed,
|
|
892
|
+
replayed: result.replayed,
|
|
598
893
|
})),
|
|
599
894
|
},
|
|
600
895
|
},
|
|
601
896
|
],
|
|
602
897
|
});
|
|
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) {
|
|
898
|
+
const settled = await this.#settleResponseAdvance(session, advance, settlement);
|
|
899
|
+
if (settled.kind === "terminal") {
|
|
900
|
+
if (!settlement.visibleText) {
|
|
650
901
|
const message = "The model returned neither text nor a complete tool call.";
|
|
651
902
|
if (response.diagnostics !== undefined)
|
|
652
903
|
await this.#failureLog?.recordFailure({
|
|
@@ -658,15 +909,10 @@ export class PlayCallChain {
|
|
|
658
909
|
},
|
|
659
910
|
],
|
|
660
911
|
});
|
|
661
|
-
session
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
});
|
|
666
|
-
session.status = "interrupted";
|
|
667
|
-
session.canRetry = false;
|
|
668
|
-
session.lastFailure = message;
|
|
669
|
-
await this.#persist(session);
|
|
912
|
+
notifySnapshot(observer, session);
|
|
913
|
+
return projectView(session);
|
|
914
|
+
}
|
|
915
|
+
if (settled.status !== "ready") {
|
|
670
916
|
notifySnapshot(observer, session);
|
|
671
917
|
return projectView(session);
|
|
672
918
|
}
|
|
@@ -686,13 +932,22 @@ export class PlayCallChain {
|
|
|
686
932
|
notifySnapshot(observer, session);
|
|
687
933
|
return projectView(session);
|
|
688
934
|
}
|
|
689
|
-
await this.#persist(session);
|
|
690
935
|
notifySnapshot(observer, session);
|
|
691
|
-
|
|
692
|
-
request = createRequest(session, modelHost, session.exchange);
|
|
936
|
+
request = settled.nextRequest;
|
|
693
937
|
attempt = 1;
|
|
694
938
|
}
|
|
695
939
|
catch (error) {
|
|
940
|
+
const recovered = await this.#recoverAcceptedResponseAdvance(session, advance);
|
|
941
|
+
if (recovered !== null) {
|
|
942
|
+
if (recovered.kind === "continue") {
|
|
943
|
+
request = recovered.nextRequest;
|
|
944
|
+
attempt = 1;
|
|
945
|
+
notifySnapshot(observer, session);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
notifySnapshot(observer, session);
|
|
949
|
+
return projectView(session);
|
|
950
|
+
}
|
|
696
951
|
const message = error instanceof Error
|
|
697
952
|
? error.message
|
|
698
953
|
: "Call-chain processing failed.";
|
|
@@ -789,56 +1044,44 @@ export class PlayCallChain {
|
|
|
789
1044
|
touch(session);
|
|
790
1045
|
}
|
|
791
1046
|
}
|
|
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
1047
|
async #requireChain(worldId, chainId) {
|
|
812
1048
|
validateIdentity(chainId, "call-chain ID");
|
|
813
1049
|
const active = this.#active.get(chainId);
|
|
814
1050
|
if (active !== undefined) {
|
|
815
1051
|
if (active.worldId !== worldId)
|
|
816
1052
|
throw new PlayCallChainError("The call chain does not belong to this world.");
|
|
1053
|
+
await this.#reconcileSessionAdvance(active);
|
|
817
1054
|
return active;
|
|
818
1055
|
}
|
|
819
1056
|
const persisted = await this.#readPersisted(worldId);
|
|
820
1057
|
if (persisted?.chainId !== chainId)
|
|
821
1058
|
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)
|
|
1059
|
+
const advance = await this.#worlds.playAdvances.readCurrent(worldId, chainId);
|
|
1060
|
+
const session = await this.#hydrateSession(persisted, advance);
|
|
1061
|
+
this.#active.set(chainId, session);
|
|
1062
|
+
this.#worldChains.set(worldId, chainId);
|
|
1063
|
+
await this.#reconcileSessionAdvance(session, advance);
|
|
1064
|
+
if (session.status === "running")
|
|
1065
|
+
await this.#interruptAbandonedDispatch(session);
|
|
1066
|
+
const currentHead = await this.#worlds.currentHead(worldId);
|
|
1067
|
+
if (currentHead !== session.parentHead)
|
|
839
1068
|
throw new PlayCallChainError("The call-chain record differs from the current world endpoint; use a fresh context.");
|
|
840
|
-
|
|
841
|
-
|
|
1069
|
+
return session;
|
|
1070
|
+
}
|
|
1071
|
+
async #hydrateSession(persisted, advance) {
|
|
1072
|
+
const binding = await this.#worlds.bindPlayCallChain(persisted.worldId);
|
|
1073
|
+
let documents;
|
|
1074
|
+
try {
|
|
1075
|
+
documents = restorePlayDocuments(binding.files, persisted, persisted.events);
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
const prepared = advance?.settlementPrepared?.settlement;
|
|
1079
|
+
if (prepared === undefined)
|
|
1080
|
+
throw error;
|
|
1081
|
+
documents = new FileNativePlayDocuments(binding.files);
|
|
1082
|
+
documents.restoreAuthorizationCheckpoint(prepared.authorizationCheckpoint);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
842
1085
|
...structuredClone(persisted),
|
|
843
1086
|
documentAuthorizationCheckpoints: persisted.documentAuthorizationCheckpoints?.map((checkpoint) => structuredClone(checkpoint)) ?? [],
|
|
844
1087
|
nextMaterials: structuredClone(binding.additionalMaterials),
|
|
@@ -848,32 +1091,79 @@ export class PlayCallChain {
|
|
|
848
1091
|
item.key,
|
|
849
1092
|
structuredClone(item),
|
|
850
1093
|
])),
|
|
1094
|
+
persistenceCursor: this.#loadedCursors.get(persisted.chainId) ?? {
|
|
1095
|
+
eventCount: 0,
|
|
1096
|
+
transcriptCount: 0,
|
|
1097
|
+
completedToolCount: 0,
|
|
1098
|
+
authorizationCheckpointCount: 0,
|
|
1099
|
+
},
|
|
851
1100
|
};
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
await this.#
|
|
856
|
-
|
|
1101
|
+
}
|
|
1102
|
+
async #reconcileSessionAdvance(session, loaded) {
|
|
1103
|
+
const current = loaded ??
|
|
1104
|
+
(await this.#worlds.playAdvances.readCurrent(session.worldId, session.chainId));
|
|
1105
|
+
if (current?.settled !== null)
|
|
1106
|
+
return;
|
|
1107
|
+
if (current.base.advanceKind === "player") {
|
|
1108
|
+
await this.#settlePlayerAdvance(session, current.base);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (current.providerCompleted === null) {
|
|
1112
|
+
await this.#interruptAbandonedDispatch(session, current.base);
|
|
1113
|
+
await this.#worlds.playAdvances.markSettled(current.base, session.parentHead);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
let settlement = current.settlementPrepared?.settlement;
|
|
1117
|
+
if (settlement === undefined) {
|
|
1118
|
+
settlement = this.#prepareResponseSettlement(session, current.base, current.providerCompleted.response);
|
|
1119
|
+
await this.#worlds.playAdvances.recordSettlementPrepared(current.base, settlement);
|
|
1120
|
+
}
|
|
1121
|
+
await this.#settleResponseAdvance(session, current.base, settlement);
|
|
1122
|
+
}
|
|
1123
|
+
async #interruptAbandonedDispatch(session, advance) {
|
|
1124
|
+
const streaming = session.events.findLast((event) => event.kind === "assistant" &&
|
|
1125
|
+
event.status === "streaming" &&
|
|
1126
|
+
(advance === undefined || event.id === advance.eventId));
|
|
1127
|
+
if (streaming !== undefined)
|
|
1128
|
+
streaming.status = "interrupted";
|
|
1129
|
+
const dispatchMayHaveStarted = advance !== undefined;
|
|
1130
|
+
const canRetry = !dispatchMayHaveStarted && session.lastRequest !== null;
|
|
1131
|
+
const message = dispatchMayHaveStarted
|
|
1132
|
+
? "The service stopped after model dispatch may have started; the outcome is unknown and the request cannot be replayed. Use a fresh context."
|
|
1133
|
+
: canRetry
|
|
1134
|
+
? "The service stopped before model dispatch; the frozen request can be retried."
|
|
1135
|
+
: "The service stopped before preparing a model request; use a fresh context.";
|
|
1136
|
+
session.events.push({
|
|
1137
|
+
id: session.nextEventId++,
|
|
1138
|
+
kind: "failure",
|
|
1139
|
+
message,
|
|
1140
|
+
});
|
|
1141
|
+
session.status = "interrupted";
|
|
1142
|
+
session.canRetry = canRetry;
|
|
1143
|
+
session.lastFailure = message;
|
|
1144
|
+
await this.#persist(session);
|
|
857
1145
|
}
|
|
858
1146
|
async #assertCurrentHead(session) {
|
|
859
1147
|
if ((await this.#worlds.currentHead(session.worldId)) !== session.parentHead)
|
|
860
1148
|
throw new PlayCallChainError("Another operation has advanced the current world; use a fresh context.");
|
|
861
1149
|
}
|
|
862
1150
|
async #readPersisted(worldId) {
|
|
863
|
-
|
|
864
|
-
|
|
1151
|
+
await this.#worlds.ensureCurrentStorage(worldId);
|
|
1152
|
+
const loaded = await this.#worlds.playTimeline.readCurrent(worldId);
|
|
1153
|
+
if (loaded === null)
|
|
865
1154
|
return null;
|
|
866
|
-
|
|
867
|
-
return
|
|
1155
|
+
this.#loadedCursors.set(loaded.value.chainId, loaded.cursor);
|
|
1156
|
+
return structuredClone(loaded.value);
|
|
868
1157
|
}
|
|
869
1158
|
async #persist(session) {
|
|
870
1159
|
touch(session);
|
|
871
1160
|
session.completedTools = [...session.completedToolMap.values()].map((item) => structuredClone(item));
|
|
872
|
-
const { documents: _documents, history: _history, completedToolMap: _completedToolMap, ...persisted } = session;
|
|
1161
|
+
const { documents: _documents, history: _history, completedToolMap: _completedToolMap, persistenceCursor: _persistenceCursor, ...persisted } = session;
|
|
873
1162
|
void _documents;
|
|
874
1163
|
void _history;
|
|
875
1164
|
void _completedToolMap;
|
|
876
|
-
|
|
1165
|
+
void _persistenceCursor;
|
|
1166
|
+
session.persistenceCursor = await this.#worlds.playTimeline.persist(structuredClone(persisted), session.persistenceCursor);
|
|
877
1167
|
}
|
|
878
1168
|
}
|
|
879
1169
|
/**
|
|
@@ -884,21 +1174,72 @@ export class PlayCallChain {
|
|
|
884
1174
|
function followupOperationId(session) {
|
|
885
1175
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
886
1176
|
}
|
|
887
|
-
function createRequest(session, modelHost, exchange) {
|
|
1177
|
+
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1178
|
+
return createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1179
|
+
}
|
|
1180
|
+
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
888
1181
|
return {
|
|
889
1182
|
bootstrap: structuredClone(session.bootstrap),
|
|
890
1183
|
tools: structuredClone(session.tools),
|
|
891
1184
|
toolUniverse: structuredClone(session.tools),
|
|
892
1185
|
allowedTools: session.tools.map(({ name }) => name),
|
|
893
1186
|
toolStrategy: session.bootstrap.toolStrategy,
|
|
894
|
-
appended: structuredClone(
|
|
1187
|
+
appended: structuredClone([...appended]),
|
|
895
1188
|
requestId: "play_call_chain",
|
|
896
1189
|
operationId: session.chainId,
|
|
897
1190
|
requestAttempt: 1,
|
|
898
1191
|
exchange,
|
|
899
|
-
maxOutputTokens
|
|
1192
|
+
maxOutputTokens,
|
|
900
1193
|
};
|
|
901
1194
|
}
|
|
1195
|
+
function durableModelResponse(response) {
|
|
1196
|
+
const { diagnostics: _diagnostics, ...durable } = response;
|
|
1197
|
+
void _diagnostics;
|
|
1198
|
+
return structuredClone(durable);
|
|
1199
|
+
}
|
|
1200
|
+
function finalizePreparedReceipts(settlement) {
|
|
1201
|
+
const committedWriteRefs = new Set(settlement.stateChanges.map(({ stableShortRef }) => stableShortRef));
|
|
1202
|
+
const markdownByCall = new Map();
|
|
1203
|
+
for (const completed of settlement.completedTools) {
|
|
1204
|
+
const candidateWrite = completed.result.candidateWrite;
|
|
1205
|
+
if (!completed.result.ok || candidateWrite === undefined)
|
|
1206
|
+
continue;
|
|
1207
|
+
const markdown = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
|
|
1208
|
+
? `@${candidateWrite.shortRef} write succeeded`
|
|
1209
|
+
: `@${candidateWrite.shortRef} unchanged`;
|
|
1210
|
+
completed.result.markdown = markdown;
|
|
1211
|
+
delete completed.result.candidateWrite;
|
|
1212
|
+
const callId = completed.key.slice(completed.key.indexOf(":") + 1);
|
|
1213
|
+
markdownByCall.set(callId, markdown);
|
|
1214
|
+
}
|
|
1215
|
+
for (const event of settlement.trailingEvents) {
|
|
1216
|
+
if (event.kind !== "tool_result")
|
|
1217
|
+
continue;
|
|
1218
|
+
const markdown = markdownByCall.get(event.callId);
|
|
1219
|
+
if (markdown !== undefined)
|
|
1220
|
+
event.markdown = markdown;
|
|
1221
|
+
}
|
|
1222
|
+
for (const item of settlement.transcriptAppend) {
|
|
1223
|
+
if (item.kind !== "tool")
|
|
1224
|
+
continue;
|
|
1225
|
+
const markdown = markdownByCall.get(item.toolCallId);
|
|
1226
|
+
if (markdown !== undefined)
|
|
1227
|
+
item.markdown = markdown;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function appendCommittedHistory(session, head, messages) {
|
|
1231
|
+
const sequence = Number(head.slice("commit:".length));
|
|
1232
|
+
for (const [index, message] of messages.entries()) {
|
|
1233
|
+
const path = `${session.worldId}.message.${sequence}.${index + 1}.${message.role}`;
|
|
1234
|
+
const existing = session.history.find((item) => item.path === path);
|
|
1235
|
+
if (existing !== undefined) {
|
|
1236
|
+
if (existing.contents !== message.exactText)
|
|
1237
|
+
throw new PlayCallChainError("Recovered Authority history conflicts with the call-chain history.");
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
session.history.push({ path, contents: message.exactText });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
902
1243
|
function prepareTool(session, working, exchange, call) {
|
|
903
1244
|
const key = `${exchange}:${call.id}`;
|
|
904
1245
|
const signature = JSON.stringify(call.arguments);
|
|
@@ -916,7 +1257,8 @@ function prepareTool(session, working, exchange, call) {
|
|
|
916
1257
|
markdown: "# Runtime tool rejected\n\nThe same tool-call ID is already bound to another tool name or argument set.",
|
|
917
1258
|
};
|
|
918
1259
|
}
|
|
919
|
-
else if (!callChainToolNames.has(call.name)
|
|
1260
|
+
else if (!callChainToolNames.has(call.name) ||
|
|
1261
|
+
!session.tools.some(({ name }) => name === call.name)) {
|
|
920
1262
|
result = {
|
|
921
1263
|
ok: false,
|
|
922
1264
|
failureKind: "protocol",
|
|
@@ -960,6 +1302,7 @@ function prepareTool(session, working, exchange, call) {
|
|
|
960
1302
|
kind: "tool",
|
|
961
1303
|
toolCallId: call.id,
|
|
962
1304
|
markdown: result.markdown,
|
|
1305
|
+
...(result.ok ? {} : { isError: true }),
|
|
963
1306
|
},
|
|
964
1307
|
};
|
|
965
1308
|
}
|
|
@@ -999,19 +1342,6 @@ function restorePlayDocuments(files, context, events) {
|
|
|
999
1342
|
}
|
|
1000
1343
|
return documents;
|
|
1001
1344
|
}
|
|
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
1345
|
function documentAuthorizationThroughEvents(context, events) {
|
|
1016
1346
|
const checkpoints = context.documentAuthorizationCheckpoints;
|
|
1017
1347
|
if (checkpoints === undefined || checkpoints.length === 0)
|
|
@@ -1023,20 +1353,6 @@ function documentAuthorizationThroughEvents(context, events) {
|
|
|
1023
1353
|
// old recovery behavior and rebuild only bootstrap authorization.
|
|
1024
1354
|
return selected === undefined ? undefined : structuredClone(selected);
|
|
1025
1355
|
}
|
|
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
1356
|
function independentContextCopy(source) {
|
|
1041
1357
|
const context = structuredClone(source);
|
|
1042
1358
|
delete context.derivedFrom;
|
|
@@ -1068,12 +1384,41 @@ function projectContext(context) {
|
|
|
1068
1384
|
playPreset: structuredClone(context.playPreset),
|
|
1069
1385
|
status: context.status,
|
|
1070
1386
|
canRetry: context.canRetry,
|
|
1071
|
-
events:
|
|
1387
|
+
events: context.events
|
|
1388
|
+
.slice(-projectedEventTail)
|
|
1389
|
+
.map((event) => projectActivityEvent(event)),
|
|
1072
1390
|
changedDocuments: structuredClone(context.changedDocuments),
|
|
1073
1391
|
lastFailure: context.lastFailure,
|
|
1074
1392
|
updatedAt: context.updatedAt,
|
|
1075
1393
|
};
|
|
1076
1394
|
}
|
|
1395
|
+
function projectActivityEvent(event) {
|
|
1396
|
+
if (event.kind === "player" || event.kind === "failure")
|
|
1397
|
+
return structuredClone(event);
|
|
1398
|
+
if (event.kind === "assistant") {
|
|
1399
|
+
const { reasoning, toolFragment, usage, ...summary } = event;
|
|
1400
|
+
void reasoning;
|
|
1401
|
+
void toolFragment;
|
|
1402
|
+
void usage;
|
|
1403
|
+
return structuredClone(summary);
|
|
1404
|
+
}
|
|
1405
|
+
if (event.kind === "tool_call")
|
|
1406
|
+
return { ...structuredClone(event), arguments: null };
|
|
1407
|
+
if (event.kind === "tool_result")
|
|
1408
|
+
return { ...structuredClone(event), markdown: "" };
|
|
1409
|
+
const { reasoning, usage, ...summary } = event;
|
|
1410
|
+
void reasoning;
|
|
1411
|
+
void usage;
|
|
1412
|
+
return {
|
|
1413
|
+
...structuredClone(summary),
|
|
1414
|
+
text: "",
|
|
1415
|
+
toolCalls: event.toolCalls.map((call) => ({
|
|
1416
|
+
...structuredClone(call),
|
|
1417
|
+
arguments: null,
|
|
1418
|
+
markdown: "",
|
|
1419
|
+
})),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1077
1422
|
function projectView(session) {
|
|
1078
1423
|
return {
|
|
1079
1424
|
...projectContext(session),
|
|
@@ -1111,7 +1456,9 @@ function transcriptThroughEvents(transcript, events) {
|
|
|
1111
1456
|
.find((candidate) => candidate.kind === "player" ||
|
|
1112
1457
|
candidate.kind === "assistant" ||
|
|
1113
1458
|
candidate.kind === "tool_call");
|
|
1114
|
-
const recorded = event.text.trim().length > 0 ||
|
|
1459
|
+
const recorded = event.text.trim().length > 0 ||
|
|
1460
|
+
hasToolCall?.kind === "tool_call" ||
|
|
1461
|
+
transcript[cursor]?.kind === "assistant";
|
|
1115
1462
|
if (!recorded)
|
|
1116
1463
|
continue;
|
|
1117
1464
|
const item = transcript[cursor];
|
|
@@ -1168,84 +1515,14 @@ function operationId(chainId, key) {
|
|
|
1168
1515
|
.update(`${chainId}\0${key}`)
|
|
1169
1516
|
.digest("hex")}`;
|
|
1170
1517
|
}
|
|
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.");
|
|
1518
|
+
function assertModelHostBinding(frozen, modelHost) {
|
|
1519
|
+
if (!equalModelHostBinding(frozen, modelHost.binding()))
|
|
1520
|
+
throw new PlayCallChainError("The selected model connection does not match the frozen model binding; start a fresh context.");
|
|
1181
1521
|
}
|
|
1182
|
-
function
|
|
1183
|
-
if (
|
|
1184
|
-
|
|
1185
|
-
|
|
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");
|
|
1226
|
-
}
|
|
1227
|
-
function isPersistedDocumentAuthorizationHistory(value, events) {
|
|
1228
|
-
if (!Array.isArray(value) || value.length > 100_000)
|
|
1229
|
-
return false;
|
|
1230
|
-
const eventIds = new Set(events.flatMap((event) => isRecord(event) && Number.isSafeInteger(event.id) && Number(event.id) > 0
|
|
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;
|
|
1522
|
+
function assertSessionModelHostBinding(session, modelHost) {
|
|
1523
|
+
if (session.modelBinding === undefined)
|
|
1524
|
+
throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
|
|
1525
|
+
assertModelHostBinding(session.modelBinding, modelHost);
|
|
1249
1526
|
}
|
|
1250
1527
|
function validateIdentity(value, label) {
|
|
1251
1528
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
@@ -1275,6 +1552,8 @@ function notifyAssistantDelta(observer, delta) {
|
|
|
1275
1552
|
// See notifySnapshot: the Provider exchange outlives its browser stream.
|
|
1276
1553
|
}
|
|
1277
1554
|
}
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1555
|
+
function crashAtPlayAdvanceEdge(edge) {
|
|
1556
|
+
if (process.env.NODE_ENV === "test" &&
|
|
1557
|
+
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_PLAY_ADVANCE_EDGE === edge)
|
|
1558
|
+
throw new Error(`Simulated process exit at play advance edge: ${edge}`);
|
|
1280
1559
|
}
|