narraeon 0.2.0 → 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/v1.js +6 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +7 -6
- package/dist/node/runtime/model/FileNativeModelAdapters.js +40 -13
- package/dist/node/runtime/model/ModelHost.js +7 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +96 -7
- package/dist/node/runtime/play/PlayCallChain.js +309 -53
- package/dist/node/runtime/play/PlayFollowupRequests.js +42 -22
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +5 -5
- 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 +447 -6
- package/dist/node/runtime/world/FileNativeWorldStore.js +423 -850
- 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-DHV_VWl3.js +0 -1192
- package/dist/web/assets/index-DjLCkP99.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { equalModelHostBinding, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "../model/ModelHost.js";
|
|
2
|
+
import { equalModelHostBinding, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "../model/ModelHost.js";
|
|
3
3
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
4
4
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
5
5
|
import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTools.js";
|
|
@@ -35,6 +35,7 @@ export class PlayCallChain {
|
|
|
35
35
|
#active = new Map();
|
|
36
36
|
#worldChains = new Map();
|
|
37
37
|
#loadedCursors = new Map();
|
|
38
|
+
#activeInvocations = new Map();
|
|
38
39
|
constructor(worlds, compiler = new FileNativePromptCompiler(), artifacts, failureLog) {
|
|
39
40
|
this.#worlds = worlds;
|
|
40
41
|
this.#compiler = compiler;
|
|
@@ -42,6 +43,9 @@ export class PlayCallChain {
|
|
|
42
43
|
this.#failureLog = failureLog;
|
|
43
44
|
}
|
|
44
45
|
async start(input) {
|
|
46
|
+
return this.#runInvocation(input, (signal) => this.#start(input, signal));
|
|
47
|
+
}
|
|
48
|
+
async #start(input, signal) {
|
|
45
49
|
validateIdentity(input.chainId, "call-chain ID");
|
|
46
50
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
47
51
|
validatePlayerText(input.playerText);
|
|
@@ -134,9 +138,12 @@ export class PlayCallChain {
|
|
|
134
138
|
this.#active.set(session.chainId, session);
|
|
135
139
|
this.#worldChains.set(session.worldId, session.chainId);
|
|
136
140
|
await this.#persist(session);
|
|
137
|
-
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);
|
|
138
142
|
}
|
|
139
143
|
async append(input) {
|
|
144
|
+
return this.#runInvocation(input, (signal) => this.#append(input, signal));
|
|
145
|
+
}
|
|
146
|
+
async #append(input, signal) {
|
|
140
147
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
141
148
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
142
149
|
await this.#assertCurrentHead(session);
|
|
@@ -147,10 +154,10 @@ export class PlayCallChain {
|
|
|
147
154
|
if (session.status === "interrupted") {
|
|
148
155
|
if (!session.canRetry || session.lastRequest === null)
|
|
149
156
|
throw new PlayCallChainError("Call-chain processing failed, and there is no model context to continue.");
|
|
150
|
-
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);
|
|
151
158
|
}
|
|
152
159
|
session.exchange += 1;
|
|
153
|
-
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);
|
|
154
161
|
}
|
|
155
162
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
156
163
|
return projectView(session);
|
|
@@ -158,14 +165,100 @@ export class PlayCallChain {
|
|
|
158
165
|
throw new PlayCallChainError(session.canRetry
|
|
159
166
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
160
167
|
: "The call chain was interrupted; use a fresh context.");
|
|
161
|
-
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
|
+
}
|
|
162
254
|
}
|
|
163
255
|
async inspectWorld(worldId) {
|
|
164
256
|
const activeId = this.#worldChains.get(worldId);
|
|
165
257
|
const active = activeId === undefined ? undefined : this.#active.get(activeId);
|
|
166
258
|
if (active !== undefined) {
|
|
167
|
-
|
|
168
|
-
|
|
259
|
+
if (this.#activeInvocations.get(active.chainId)?.worldId !== active.worldId)
|
|
260
|
+
await this.#reconcileSessionAdvance(active);
|
|
261
|
+
return this.#projectActiveView(active);
|
|
169
262
|
}
|
|
170
263
|
const persisted = await this.#readPersisted(worldId);
|
|
171
264
|
if (persisted === null)
|
|
@@ -177,7 +270,28 @@ export class PlayCallChain {
|
|
|
177
270
|
await this.#reconcileSessionAdvance(session, advance);
|
|
178
271
|
if (session.status === "running")
|
|
179
272
|
await this.#interruptAbandonedDispatch(session);
|
|
180
|
-
return
|
|
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,
|
|
293
|
+
});
|
|
294
|
+
return result;
|
|
181
295
|
}
|
|
182
296
|
async forkToDerivedWorld(input) {
|
|
183
297
|
const existing = await this.#readPersisted(input.targetWorldId);
|
|
@@ -193,12 +307,34 @@ export class PlayCallChain {
|
|
|
193
307
|
const sourceContext = sourceContexts[selectedContextIndex];
|
|
194
308
|
const sourceEvents = eventsThroughHead(sourceContext.events, input.sourceHead);
|
|
195
309
|
return this.#forkSelectionToDerivedWorld({
|
|
310
|
+
sourceWorldId: input.sourceWorldId,
|
|
311
|
+
sourceContexts,
|
|
312
|
+
selectedContextIndex,
|
|
313
|
+
sourceEvents,
|
|
314
|
+
sourceHead: input.sourceHead,
|
|
315
|
+
targetWorldId: input.targetWorldId,
|
|
316
|
+
branchIdentity: input.sourceHead,
|
|
317
|
+
});
|
|
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,
|
|
196
330
|
sourceContexts,
|
|
197
331
|
selectedContextIndex,
|
|
198
332
|
sourceEvents,
|
|
199
333
|
sourceHead: input.sourceHead,
|
|
200
334
|
targetWorldId: input.targetWorldId,
|
|
201
335
|
branchIdentity: input.sourceHead,
|
|
336
|
+
targetWorldRoot: input.targetWorldRoot,
|
|
337
|
+
targetBinding: input.targetBinding,
|
|
202
338
|
});
|
|
203
339
|
}
|
|
204
340
|
/**
|
|
@@ -232,8 +368,12 @@ export class PlayCallChain {
|
|
|
232
368
|
throw new PlayCallChainError("The same replacement exchange ID is already bound to different player text.");
|
|
233
369
|
const authority = await this.#worlds.readAuthorityHistory(input.worldId);
|
|
234
370
|
const commit = authority.commits.find(({ head }) => head === appliedEvent.committedHead);
|
|
235
|
-
|
|
236
|
-
|
|
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)
|
|
237
377
|
throw new PlayCallChainError("The replacement exchange ID is already used by another commit.");
|
|
238
378
|
return {
|
|
239
379
|
outcome: "revised",
|
|
@@ -374,20 +514,36 @@ export class PlayCallChain {
|
|
|
374
514
|
};
|
|
375
515
|
}
|
|
376
516
|
async #forkSelectionToDerivedWorld(input) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
517
|
+
if (input.targetWorldRoot === undefined) {
|
|
518
|
+
const existing = await this.#readPersisted(input.targetWorldId);
|
|
519
|
+
if (existing !== null)
|
|
520
|
+
return projectView(existing);
|
|
521
|
+
}
|
|
380
522
|
const sourceContext = input.sourceContexts[input.selectedContextIndex];
|
|
381
|
-
if (
|
|
523
|
+
if (input.targetWorldRoot === undefined &&
|
|
524
|
+
(await this.#worlds.currentHead(input.targetWorldId)) !== input.sourceHead)
|
|
382
525
|
throw new PlayCallChainError("The forked world's Authority does not match the selected history endpoint, so the call chain cannot be written.");
|
|
383
526
|
const [baseline, selected] = await Promise.all([
|
|
384
|
-
this.#worlds.recoverEndpoint(input.
|
|
385
|
-
|
|
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),
|
|
386
533
|
]);
|
|
387
|
-
const
|
|
388
|
-
|
|
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);
|
|
389
544
|
const events = structuredClone(input.sourceEvents);
|
|
390
|
-
const derivedBinding =
|
|
545
|
+
const derivedBinding = input.targetBinding ??
|
|
546
|
+
(await this.#worlds.bindPlayCallChain(input.targetWorldId));
|
|
391
547
|
const derivedDocuments = restorePlayDocuments(derivedBinding.files, sourceContext, input.sourceEvents);
|
|
392
548
|
const now = Date.now();
|
|
393
549
|
const timelineGeneration = this.#worlds.playTimeline.newGeneration();
|
|
@@ -396,7 +552,7 @@ export class PlayCallChain {
|
|
|
396
552
|
.slice(0, input.selectedContextIndex)
|
|
397
553
|
.entries()) {
|
|
398
554
|
const chainId = derivedChainId(context.chainId, `prefix:${input.branchIdentity}:${index}`, input.targetWorldId);
|
|
399
|
-
|
|
555
|
+
const previousContext = {
|
|
400
556
|
...independentContextCopy(context),
|
|
401
557
|
schemaVersion: 3,
|
|
402
558
|
kind: "play_call_chain",
|
|
@@ -405,7 +561,16 @@ export class PlayCallChain {
|
|
|
405
561
|
previousContexts: [],
|
|
406
562
|
previousChainId,
|
|
407
563
|
timelineGeneration,
|
|
408
|
-
}
|
|
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
|
+
});
|
|
409
574
|
previousChainId = chainId;
|
|
410
575
|
}
|
|
411
576
|
const derived = {
|
|
@@ -438,7 +603,7 @@ export class PlayCallChain {
|
|
|
438
603
|
transcript,
|
|
439
604
|
events,
|
|
440
605
|
completedTools: sourceContext.completedTools
|
|
441
|
-
.filter(({ key }) => completedKeys.has(key))
|
|
606
|
+
.filter(({ key }) => completedKeys === null || completedKeys.has(key))
|
|
442
607
|
.map((item) => structuredClone(item)),
|
|
443
608
|
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, derivedDocuments.authorizationCheckpoint()),
|
|
444
609
|
changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
|
|
@@ -452,7 +617,15 @@ export class PlayCallChain {
|
|
|
452
617
|
lastFailure: null,
|
|
453
618
|
updatedAt: now,
|
|
454
619
|
};
|
|
455
|
-
|
|
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
|
+
});
|
|
456
629
|
return projectView(derived);
|
|
457
630
|
}
|
|
458
631
|
forgetWorld(worldId) {
|
|
@@ -464,7 +637,7 @@ export class PlayCallChain {
|
|
|
464
637
|
this.#active.delete(activeId);
|
|
465
638
|
this.#worldChains.delete(worldId);
|
|
466
639
|
}
|
|
467
|
-
async #submitPlayer(session, exchangeId, playerText, context, modelHost, observer) {
|
|
640
|
+
async #submitPlayer(session, exchangeId, playerText, context, modelHost, signal, observer) {
|
|
468
641
|
if (duplicatePlayerExchange(session, exchangeId, playerText, context))
|
|
469
642
|
return projectView(session);
|
|
470
643
|
if (session.status === "running")
|
|
@@ -505,7 +678,7 @@ export class PlayCallChain {
|
|
|
505
678
|
if (accepted.outcome === "committed" ||
|
|
506
679
|
accepted.outcome === "committed_materialization_pending") {
|
|
507
680
|
await this.#settlePlayerAdvance(session, advance);
|
|
508
|
-
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, observer);
|
|
681
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
509
682
|
}
|
|
510
683
|
const message = error instanceof Error
|
|
511
684
|
? error.message
|
|
@@ -522,7 +695,7 @@ export class PlayCallChain {
|
|
|
522
695
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
523
696
|
throw new PlayCallChainError(message);
|
|
524
697
|
}
|
|
525
|
-
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, observer);
|
|
698
|
+
return this.#dispatch(session, modelHost, structuredClone(nextRequest), 1, signal, observer);
|
|
526
699
|
}
|
|
527
700
|
async #settlePlayerAdvance(session, advance) {
|
|
528
701
|
const outcome = await this.#worlds.commitPlayStep({
|
|
@@ -786,10 +959,14 @@ export class PlayCallChain {
|
|
|
786
959
|
throw new PlayCallChainError("Authority accepted a response whose durable settlement is missing.");
|
|
787
960
|
return this.#settleResponseAdvance(session, advance, current.settlementPrepared.settlement);
|
|
788
961
|
}
|
|
789
|
-
async #dispatch(session, modelHost, firstRequest, firstAttempt, observer) {
|
|
962
|
+
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
790
963
|
let request = structuredClone(firstRequest);
|
|
791
964
|
let attempt = firstAttempt;
|
|
792
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");
|
|
793
970
|
session.status = "running";
|
|
794
971
|
session.canRetry = false;
|
|
795
972
|
session.lastFailure = null;
|
|
@@ -824,11 +1001,22 @@ export class PlayCallChain {
|
|
|
824
1001
|
};
|
|
825
1002
|
await this.#worlds.playAdvances.begin(advance);
|
|
826
1003
|
crashAtPlayAdvanceEdge("after_response_advance_began");
|
|
827
|
-
notifySnapshot(observer, session);
|
|
1004
|
+
this.#notifySnapshot(observer, session);
|
|
1005
|
+
if (signal.aborted)
|
|
1006
|
+
return this.#cancelBeforeProviderDispatch(session, request, attempt, observer, advance);
|
|
828
1007
|
let response;
|
|
829
1008
|
try {
|
|
1009
|
+
this.#observeInvocation(session.chainId, "waiting", { dispatches: 1 });
|
|
830
1010
|
response = await modelHost.exchange(request, {
|
|
1011
|
+
signal,
|
|
831
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
|
+
});
|
|
832
1020
|
if (delta.kind === "text")
|
|
833
1021
|
event.text += delta.text;
|
|
834
1022
|
else if (delta.kind === "reasoning")
|
|
@@ -844,6 +1032,13 @@ export class PlayCallChain {
|
|
|
844
1032
|
});
|
|
845
1033
|
},
|
|
846
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
|
+
});
|
|
847
1042
|
if (response.diagnostics !== undefined)
|
|
848
1043
|
await this.#failureLog?.recordExchangeIfActive(response.diagnostics);
|
|
849
1044
|
}
|
|
@@ -854,7 +1049,9 @@ export class PlayCallChain {
|
|
|
854
1049
|
: "The model request was interrupted.";
|
|
855
1050
|
session.events.push({
|
|
856
1051
|
id: session.nextEventId++,
|
|
857
|
-
kind:
|
|
1052
|
+
kind: error instanceof ModelHostCancelledError
|
|
1053
|
+
? "cancellation"
|
|
1054
|
+
: "failure",
|
|
858
1055
|
message,
|
|
859
1056
|
});
|
|
860
1057
|
session.status = "interrupted";
|
|
@@ -865,7 +1062,7 @@ export class PlayCallChain {
|
|
|
865
1062
|
session.lastFailure = message;
|
|
866
1063
|
await this.#persist(session);
|
|
867
1064
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
868
|
-
notifySnapshot(observer, session);
|
|
1065
|
+
this.#notifySnapshot(observer, session);
|
|
869
1066
|
return projectView(session);
|
|
870
1067
|
}
|
|
871
1068
|
const durableResponse = durableModelResponse(response);
|
|
@@ -909,11 +1106,11 @@ export class PlayCallChain {
|
|
|
909
1106
|
},
|
|
910
1107
|
],
|
|
911
1108
|
});
|
|
912
|
-
notifySnapshot(observer, session);
|
|
1109
|
+
this.#notifySnapshot(observer, session);
|
|
913
1110
|
return projectView(session);
|
|
914
1111
|
}
|
|
915
1112
|
if (settled.status !== "ready") {
|
|
916
|
-
notifySnapshot(observer, session);
|
|
1113
|
+
this.#notifySnapshot(observer, session);
|
|
917
1114
|
return projectView(session);
|
|
918
1115
|
}
|
|
919
1116
|
// The exchange has settled: no tool call is pending and the narrative
|
|
@@ -926,13 +1123,31 @@ export class PlayCallChain {
|
|
|
926
1123
|
message: "The play call chain recovered during a later model exchange and completed.",
|
|
927
1124
|
details: { parentHead: session.parentHead },
|
|
928
1125
|
});
|
|
929
|
-
|
|
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
|
+
}
|
|
930
1145
|
session.status = "ready";
|
|
931
1146
|
await this.#persist(session);
|
|
932
|
-
notifySnapshot(observer, session);
|
|
1147
|
+
this.#notifySnapshot(observer, session);
|
|
933
1148
|
return projectView(session);
|
|
934
1149
|
}
|
|
935
|
-
notifySnapshot(observer, session);
|
|
1150
|
+
this.#notifySnapshot(observer, session);
|
|
936
1151
|
request = settled.nextRequest;
|
|
937
1152
|
attempt = 1;
|
|
938
1153
|
}
|
|
@@ -942,10 +1157,10 @@ export class PlayCallChain {
|
|
|
942
1157
|
if (recovered.kind === "continue") {
|
|
943
1158
|
request = recovered.nextRequest;
|
|
944
1159
|
attempt = 1;
|
|
945
|
-
notifySnapshot(observer, session);
|
|
1160
|
+
this.#notifySnapshot(observer, session);
|
|
946
1161
|
continue;
|
|
947
1162
|
}
|
|
948
|
-
notifySnapshot(observer, session);
|
|
1163
|
+
this.#notifySnapshot(observer, session);
|
|
949
1164
|
return projectView(session);
|
|
950
1165
|
}
|
|
951
1166
|
const message = error instanceof Error
|
|
@@ -970,11 +1185,34 @@ export class PlayCallChain {
|
|
|
970
1185
|
session.canRetry = false;
|
|
971
1186
|
session.lastFailure = message;
|
|
972
1187
|
await this.#persist(session);
|
|
973
|
-
notifySnapshot(observer, session);
|
|
1188
|
+
this.#notifySnapshot(observer, session);
|
|
974
1189
|
return projectView(session);
|
|
975
1190
|
}
|
|
976
1191
|
}
|
|
977
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
|
+
}
|
|
978
1216
|
/**
|
|
979
1217
|
* Dispatch the frozen follow-ups once against the settled exchange.
|
|
980
1218
|
*
|
|
@@ -983,11 +1221,24 @@ export class PlayCallChain {
|
|
|
983
1221
|
* player nothing more than a missing panel. Failures are recorded in the
|
|
984
1222
|
* trace and the chain still returns to `ready`.
|
|
985
1223
|
*/
|
|
986
|
-
async #runFollowups(session, modelHost, observer) {
|
|
1224
|
+
async #runFollowups(session, modelHost, signal, observer) {
|
|
987
1225
|
const followups = session.followups ?? [];
|
|
988
1226
|
if (this.#artifacts === undefined || followups.length === 0)
|
|
989
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
|
+
};
|
|
990
1238
|
const record = (outcome) => {
|
|
1239
|
+
this.#observeInvocation(session.chainId, "followup", {
|
|
1240
|
+
toolCalls: outcome.toolCalls.length,
|
|
1241
|
+
});
|
|
991
1242
|
session.events.push({
|
|
992
1243
|
id: session.nextEventId++,
|
|
993
1244
|
kind: "followup",
|
|
@@ -1002,7 +1253,7 @@ export class PlayCallChain {
|
|
|
1002
1253
|
...(outcome.failure === undefined ? {} : { failure: outcome.failure }),
|
|
1003
1254
|
});
|
|
1004
1255
|
touch(session);
|
|
1005
|
-
notifySnapshot(observer, session);
|
|
1256
|
+
this.#notifySnapshot(observer, session);
|
|
1006
1257
|
};
|
|
1007
1258
|
try {
|
|
1008
1259
|
await runPlayFollowupRequests({
|
|
@@ -1027,10 +1278,22 @@ export class PlayCallChain {
|
|
|
1027
1278
|
},
|
|
1028
1279
|
head: session.parentHead,
|
|
1029
1280
|
maxOutputTokens: modelHost.binding().maxOutputTokens,
|
|
1281
|
+
signal,
|
|
1030
1282
|
...(this.#failureLog === undefined
|
|
1031
1283
|
? {}
|
|
1032
1284
|
: { failureLog: this.#failureLog }),
|
|
1033
|
-
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
|
+
},
|
|
1034
1297
|
});
|
|
1035
1298
|
}
|
|
1036
1299
|
catch (error) {
|
|
@@ -1230,7 +1493,7 @@ function finalizePreparedReceipts(settlement) {
|
|
|
1230
1493
|
function appendCommittedHistory(session, head, messages) {
|
|
1231
1494
|
const sequence = Number(head.slice("commit:".length));
|
|
1232
1495
|
for (const [index, message] of messages.entries()) {
|
|
1233
|
-
const path =
|
|
1496
|
+
const path = `message.${sequence}.${index + 1}.${message.role}`;
|
|
1234
1497
|
const existing = session.history.find((item) => item.path === path);
|
|
1235
1498
|
if (existing !== undefined) {
|
|
1236
1499
|
if (existing.contents !== message.exactText)
|
|
@@ -1393,7 +1656,9 @@ function projectContext(context) {
|
|
|
1393
1656
|
};
|
|
1394
1657
|
}
|
|
1395
1658
|
function projectActivityEvent(event) {
|
|
1396
|
-
if (event.kind === "player" ||
|
|
1659
|
+
if (event.kind === "player" ||
|
|
1660
|
+
event.kind === "failure" ||
|
|
1661
|
+
event.kind === "cancellation")
|
|
1397
1662
|
return structuredClone(event);
|
|
1398
1663
|
if (event.kind === "assistant") {
|
|
1399
1664
|
const { reasoning, toolFragment, usage, ...summary } = event;
|
|
@@ -1535,21 +1800,12 @@ function validatePlayerText(value) {
|
|
|
1535
1800
|
function touch(session) {
|
|
1536
1801
|
session.updatedAt = Date.now();
|
|
1537
1802
|
}
|
|
1538
|
-
function notifySnapshot(observer, session) {
|
|
1539
|
-
try {
|
|
1540
|
-
observer?.onSnapshot?.(projectView(session));
|
|
1541
|
-
}
|
|
1542
|
-
catch {
|
|
1543
|
-
// A browser stream may disconnect while the durable model request keeps
|
|
1544
|
-
// running. Presentation failure never cancels or changes play Authority.
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
1803
|
function notifyAssistantDelta(observer, delta) {
|
|
1548
1804
|
try {
|
|
1549
1805
|
observer?.onAssistantDelta?.(structuredClone(delta));
|
|
1550
1806
|
}
|
|
1551
1807
|
catch {
|
|
1552
|
-
//
|
|
1808
|
+
// Like snapshot projection, the Provider exchange outlives its browser stream.
|
|
1553
1809
|
}
|
|
1554
1810
|
}
|
|
1555
1811
|
function crashAtPlayAdvanceEdge(edge) {
|