narraeon 0.5.6 → 0.6.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/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/gameplay.js +69 -0
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/resolution.js +274 -0
- package/dist/node/protocol/v1.js +40 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/RuntimeErrorLog.js +95 -0
- package/dist/node/runtime/V1Runtime.js +103 -8
- package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
- package/dist/node/runtime/archive/WorldArchives.js +389 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
- package/dist/node/runtime/content/ContentWorkspace.js +15 -2
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +43 -2
- package/dist/node/runtime/content/FileNativeContentTree.js +8 -1
- package/dist/node/runtime/history/StoryExport.js +33 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
- package/dist/node/runtime/model/ModelHost.js +12 -1
- package/dist/node/runtime/model/ModelHostStream.js +11 -3
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +37 -1
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +48 -3
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +87 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +68 -6
- package/dist/node/runtime/play/PlayCallChain.js +177 -57
- package/dist/node/runtime/play/PlayCallChainError.js +3 -1
- package/dist/node/runtime/play/PlayContinuity.js +3 -20
- package/dist/node/runtime/play/PlayFailure.js +20 -0
- package/dist/node/runtime/play/PlayTraceRetention.js +3 -11
- package/dist/node/runtime/play/TabletopResolution.js +372 -0
- package/dist/node/runtime/play/TabletopRules.js +141 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +49 -32
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -4
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +4 -1
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +56 -13
- package/dist/node/runtime/setting/SettingImprovementSession.js +6 -4
- package/dist/node/runtime/world/FileNativeWorldStore.js +207 -40
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +2 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +35 -6
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +9 -5
- package/dist/node/server/ConversationStream.js +2 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +10 -6
- package/dist/node/shared/ordered-author-prompts.js +8 -0
- package/dist/node/shared/ordered-play-prompts.js +25 -2
- package/dist/node/shared/sample-packages/index.js +10 -0
- package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
- package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
- package/dist/node/shared/tabletop-guidance.js +100 -0
- package/dist/web/assets/index-BJ-1Mb0e.css +1 -0
- package/dist/web/assets/index-BgBL8G-A.js +1323 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/web/assets/index-BH8NyuE4.js +0 -1233
- package/dist/web/assets/index-CVN48xrw.css +0 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js";
|
|
2
|
+
import { classifyPlayFailure } from "./PlayFailure.js";
|
|
3
|
+
import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
|
|
1
4
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
2
5
|
import { isDeepStrictEqual } from "node:util";
|
|
3
6
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
@@ -10,12 +13,13 @@ import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepN
|
|
|
10
13
|
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
11
14
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
12
15
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
13
|
-
import { equalModelHostBinding, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
16
|
+
import { equalModelHostBinding, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
14
17
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
15
18
|
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
16
19
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
17
20
|
import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTools.js";
|
|
18
21
|
const callChainToolNames = new Set([
|
|
22
|
+
...resolutionToolNames,
|
|
19
23
|
"state_list",
|
|
20
24
|
"history_list",
|
|
21
25
|
"history_search",
|
|
@@ -162,6 +166,7 @@ export class PlayCallChain {
|
|
|
162
166
|
this.#active.set(session.chainId, session);
|
|
163
167
|
this.#worldChains.set(session.worldId, session.chainId);
|
|
164
168
|
await this.#persist(session);
|
|
169
|
+
await this.#worlds.completeArchiveFreshContext(session.worldId);
|
|
165
170
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, signal, input.observer);
|
|
166
171
|
}
|
|
167
172
|
async append(input) {
|
|
@@ -169,6 +174,8 @@ export class PlayCallChain {
|
|
|
169
174
|
}
|
|
170
175
|
async #append(input, signal) {
|
|
171
176
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
177
|
+
if (await this.#worlds.archiveNeedsFreshContext(input.worldId))
|
|
178
|
+
throw new PlayCallChainError("This world was imported from a backup; start a fresh context before continuing.");
|
|
172
179
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
173
180
|
await this.#assertCurrentHead(session);
|
|
174
181
|
assertSessionModelHostBinding(session, input.modelHost);
|
|
@@ -178,6 +185,11 @@ export class PlayCallChain {
|
|
|
178
185
|
if (session.status === "interrupted") {
|
|
179
186
|
if (!session.canRetry || session.lastRequest === null)
|
|
180
187
|
throw new PlayCallChainError("The current model context cannot continue; use a fresh context.");
|
|
188
|
+
// Pre-snapshot requests have only a binding fingerprint. Do not invent
|
|
189
|
+
// their original encoding settings from a newly selected connection.
|
|
190
|
+
if (session.lastRequest.modelConfig === undefined &&
|
|
191
|
+
!equalModelHostBinding(session.modelBinding, input.modelHost.binding()))
|
|
192
|
+
throw new PlayCallChainError("This older saved request has no model configuration snapshot. Restore its original settings to retry it, or start a fresh context.");
|
|
181
193
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
182
194
|
}
|
|
183
195
|
await this.#refreshPrompt(session, input);
|
|
@@ -464,7 +476,8 @@ export class PlayCallChain {
|
|
|
464
476
|
chainId: session.chainId,
|
|
465
477
|
baselineHead: session.baselineHead,
|
|
466
478
|
parentHead: session.parentHead,
|
|
467
|
-
stale: session.parentHead !== worldHead
|
|
479
|
+
stale: session.parentHead !== worldHead ||
|
|
480
|
+
(await this.#worlds.archiveNeedsFreshContext(worldId)),
|
|
468
481
|
playPreset: structuredClone(recordedPlayPrompt(session).playPreset),
|
|
469
482
|
updatedAt: session.updatedAt,
|
|
470
483
|
bootstrap: {
|
|
@@ -477,17 +490,25 @@ export class PlayCallChain {
|
|
|
477
490
|
async deriveWorld(input) {
|
|
478
491
|
const result = await this.#worlds.deriveWorld({
|
|
479
492
|
...input,
|
|
480
|
-
stageTarget: async ({ targetWorldRoot, targetWorldId, binding }) => {
|
|
493
|
+
stageTarget: async ({ targetWorldRoot, targetWorldId, binding, sourceHead, }) => {
|
|
481
494
|
await this.stageForkToDerivedWorld({
|
|
482
495
|
sourceWorldId: input.sourceWorldId,
|
|
483
|
-
sourceHead
|
|
496
|
+
sourceHead,
|
|
484
497
|
targetWorldId,
|
|
485
498
|
targetWorldRoot,
|
|
486
499
|
targetBinding: binding,
|
|
500
|
+
...(input.retainCompleteTimeline
|
|
501
|
+
? { retainCompleteTimeline: true }
|
|
502
|
+
: {}),
|
|
503
|
+
...(input.identityPrefix === "import"
|
|
504
|
+
? { markImportedContexts: true }
|
|
505
|
+
: {}),
|
|
487
506
|
});
|
|
488
507
|
},
|
|
489
508
|
});
|
|
490
|
-
if (
|
|
509
|
+
if (input.targetDataRoot === undefined &&
|
|
510
|
+
input.publish !== false &&
|
|
511
|
+
(await this.inspectWorld(result.world.worldId)) === null)
|
|
491
512
|
await this.forkToDerivedWorld({
|
|
492
513
|
sourceWorldId: input.sourceWorldId,
|
|
493
514
|
sourceHead: input.sourceHead,
|
|
@@ -523,10 +544,14 @@ export class PlayCallChain {
|
|
|
523
544
|
if (source === null)
|
|
524
545
|
return null;
|
|
525
546
|
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.sourceWorldId);
|
|
526
|
-
const selectedContextIndex =
|
|
547
|
+
const selectedContextIndex = input.retainCompleteTimeline
|
|
548
|
+
? sourceContexts.length - 1
|
|
549
|
+
: sourceContexts.findIndex(({ events }) => eventsThroughHead(events, input.sourceHead) !== null);
|
|
527
550
|
if (selectedContextIndex < 0)
|
|
528
551
|
return null;
|
|
529
|
-
const sourceEvents =
|
|
552
|
+
const sourceEvents = input.retainCompleteTimeline
|
|
553
|
+
? sourceContexts[selectedContextIndex].events
|
|
554
|
+
: eventsThroughHead(sourceContexts[selectedContextIndex].events, input.sourceHead);
|
|
530
555
|
return this.#forkSelectionToDerivedWorld({
|
|
531
556
|
sourceWorldId: input.sourceWorldId,
|
|
532
557
|
sourceContexts,
|
|
@@ -537,6 +562,8 @@ export class PlayCallChain {
|
|
|
537
562
|
branchIdentity: input.sourceHead,
|
|
538
563
|
targetWorldRoot: input.targetWorldRoot,
|
|
539
564
|
targetBinding: input.targetBinding,
|
|
565
|
+
...(input.retainCompleteTimeline ? { retainCompleteTimeline: true } : {}),
|
|
566
|
+
...(input.markImportedContexts ? { markImportedContexts: true } : {}),
|
|
540
567
|
});
|
|
541
568
|
}
|
|
542
569
|
/**
|
|
@@ -604,6 +631,9 @@ export class PlayCallChain {
|
|
|
604
631
|
if (selectedContextIndex < 0)
|
|
605
632
|
throw new PlayCallChainError("The model context to revise does not exist.");
|
|
606
633
|
const sourceContext = sourceContexts[selectedContextIndex];
|
|
634
|
+
if (input.continuation === "continue_context" &&
|
|
635
|
+
sourceContext.importedFromArchive)
|
|
636
|
+
throw new PlayCallChainError("This model context was imported from a backup; start a fresh context before continuing.");
|
|
607
637
|
const selectedEventIndex = sourceContext.events.findIndex((event) => event.id === input.eventId);
|
|
608
638
|
const selectedEvent = sourceContext.events[selectedEventIndex];
|
|
609
639
|
if (selectedEventIndex < 0 ||
|
|
@@ -632,10 +662,15 @@ export class PlayCallChain {
|
|
|
632
662
|
const timelineGeneration = playerRevisionTimelineGeneration(input.source.timelineGeneration, request.operationId, request.worldId, request.continuation);
|
|
633
663
|
const requestFingerprint = playerRevisionRequestFingerprint(request, input.selectedEvent.committedHead, restoresHead);
|
|
634
664
|
const traceRetention = preparePlayerRevisionTrace(sourceContext, input.selectedEventIndex);
|
|
665
|
+
// A timeline revision appends on the current audit head and restores the
|
|
666
|
+
// selected message's logical parent, so a chain that lags the world (an
|
|
667
|
+
// out-of-story world revision, or an earlier revision whose chain was not
|
|
668
|
+
// published) must not block it. The commit still rechecks this head under
|
|
669
|
+
// the world lock.
|
|
635
670
|
const outcome = await this.#worlds.reviseTimeline({
|
|
636
671
|
operationId: request.operationId,
|
|
637
672
|
worldId: request.worldId,
|
|
638
|
-
expectedCurrentHead:
|
|
673
|
+
expectedCurrentHead: await this.#worlds.currentHead(request.worldId),
|
|
639
674
|
restoresHead,
|
|
640
675
|
replacesHead: input.selectedEvent.committedHead,
|
|
641
676
|
replacementText: request.replacementText,
|
|
@@ -646,11 +681,10 @@ export class PlayCallChain {
|
|
|
646
681
|
this.#worlds.recoverEndpoint(request.worldId, restoresHead),
|
|
647
682
|
this.#worlds.bindPlayCallChain(request.worldId),
|
|
648
683
|
]);
|
|
649
|
-
const continuedTrace = traceRetention.withReplacement(
|
|
684
|
+
const continuedTrace = traceRetention.withReplacement({
|
|
650
685
|
head: outcome.head,
|
|
651
686
|
exchangeId: request.replacementExchangeId,
|
|
652
687
|
text: request.replacementText,
|
|
653
|
-
locale: this.#compiler.locale,
|
|
654
688
|
});
|
|
655
689
|
if (binding.parentHead !== outcome.head)
|
|
656
690
|
throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
|
|
@@ -672,6 +706,7 @@ export class PlayCallChain {
|
|
|
672
706
|
freshPreparation,
|
|
673
707
|
});
|
|
674
708
|
await this.#worlds.playTimeline.persist(structuredClone(revised));
|
|
709
|
+
await this.#worlds.completeArchiveFreshContext(request.worldId);
|
|
675
710
|
const activeId = this.#worldChains.get(request.worldId);
|
|
676
711
|
if (activeId !== undefined)
|
|
677
712
|
this.#active.delete(activeId);
|
|
@@ -799,13 +834,7 @@ export class PlayCallChain {
|
|
|
799
834
|
layout: "world_state",
|
|
800
835
|
files: Object.entries(input.binding.files).map(([path, contents]) => ({ path, contents })),
|
|
801
836
|
}), 1),
|
|
802
|
-
transcript: playerInputAppend(
|
|
803
|
-
history: fresh.binding.history,
|
|
804
|
-
checkpoint: fresh.binding.narrativeCheckpoint,
|
|
805
|
-
text: request.replacementText,
|
|
806
|
-
locale: this.#compiler.locale,
|
|
807
|
-
checkpointAvailable: fresh.tools.some(({ name }) => name === "world_checkpoint"),
|
|
808
|
-
}),
|
|
837
|
+
transcript: playerInputAppend(request.replacementText),
|
|
809
838
|
events: [
|
|
810
839
|
{
|
|
811
840
|
id: 1,
|
|
@@ -843,22 +872,30 @@ export class PlayCallChain {
|
|
|
843
872
|
if (input.targetWorldRoot === undefined &&
|
|
844
873
|
(await this.#worlds.currentHead(input.targetWorldId)) !== input.sourceHead)
|
|
845
874
|
throw new PlayCallChainError("The forked world's Authority does not match the selected history endpoint, so the call chain cannot be written.");
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
875
|
+
let retainedTrace;
|
|
876
|
+
if (input.retainCompleteTimeline) {
|
|
877
|
+
// A backup retains the current page closure, even when its last context
|
|
878
|
+
// precedes a correction or contains an uncommitted response diagnostic.
|
|
879
|
+
retainedTrace = independentContextCopy(sourceContext);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
const [baseline, selected] = await Promise.all([
|
|
883
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
884
|
+
? input.targetWorldId
|
|
885
|
+
: input.sourceWorldId, sourceContext.baselineHead),
|
|
886
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
887
|
+
? input.targetWorldId
|
|
888
|
+
: input.sourceWorldId, input.sourceHead),
|
|
889
|
+
]);
|
|
890
|
+
const traceRetention = prepareForkTrace(sourceContext, input.sourceEvents);
|
|
891
|
+
const derivedBinding = input.targetBinding ??
|
|
892
|
+
(await this.#worlds.bindPlayCallChain(input.targetWorldId));
|
|
893
|
+
retainedTrace = traceRetention.atEndpoint({
|
|
894
|
+
baseline,
|
|
895
|
+
selected,
|
|
896
|
+
binding: derivedBinding,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
862
899
|
const now = Date.now();
|
|
863
900
|
const timelineGeneration = this.#worlds.playTimeline.newGeneration();
|
|
864
901
|
let previousChainId = null;
|
|
@@ -868,6 +905,7 @@ export class PlayCallChain {
|
|
|
868
905
|
const chainId = derivedChainId(context.chainId, `prefix:${input.branchIdentity}:${index}`, input.targetWorldId);
|
|
869
906
|
const previousContext = {
|
|
870
907
|
...independentContextCopy(context),
|
|
908
|
+
...(input.markImportedContexts ? { importedFromArchive: true } : {}),
|
|
871
909
|
schemaVersion: 3,
|
|
872
910
|
kind: "play_call_chain",
|
|
873
911
|
worldId: input.targetWorldId,
|
|
@@ -884,11 +922,15 @@ export class PlayCallChain {
|
|
|
884
922
|
targetWorldRoot: input.targetWorldRoot,
|
|
885
923
|
source: context,
|
|
886
924
|
target: previousContext,
|
|
925
|
+
...(input.retainCompleteTimeline
|
|
926
|
+
? { retainCompleteContext: true }
|
|
927
|
+
: {}),
|
|
887
928
|
});
|
|
888
929
|
previousChainId = chainId;
|
|
889
930
|
}
|
|
890
931
|
const derived = {
|
|
891
932
|
...retainedTrace,
|
|
933
|
+
...(input.markImportedContexts ? { importedFromArchive: true } : {}),
|
|
892
934
|
schemaVersion: 3,
|
|
893
935
|
kind: "play_call_chain",
|
|
894
936
|
chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
|
|
@@ -896,7 +938,9 @@ export class PlayCallChain {
|
|
|
896
938
|
previousContexts: [],
|
|
897
939
|
previousChainId,
|
|
898
940
|
timelineGeneration,
|
|
899
|
-
parentHead: input.
|
|
941
|
+
parentHead: input.retainCompleteTimeline
|
|
942
|
+
? sourceContext.parentHead
|
|
943
|
+
: input.sourceHead,
|
|
900
944
|
updatedAt: now,
|
|
901
945
|
};
|
|
902
946
|
if (input.targetWorldRoot === undefined)
|
|
@@ -907,6 +951,9 @@ export class PlayCallChain {
|
|
|
907
951
|
targetWorldRoot: input.targetWorldRoot,
|
|
908
952
|
source: sourceContext,
|
|
909
953
|
target: structuredClone(derived),
|
|
954
|
+
...(input.retainCompleteTimeline
|
|
955
|
+
? { retainCompleteContext: true }
|
|
956
|
+
: {}),
|
|
910
957
|
});
|
|
911
958
|
if (input.targetWorldRoot !== undefined) {
|
|
912
959
|
const retainedContexts = [
|
|
@@ -960,13 +1007,7 @@ export class PlayCallChain {
|
|
|
960
1007
|
const exchange = session.exchange + 1;
|
|
961
1008
|
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
962
1009
|
...session.transcript,
|
|
963
|
-
...playerInputAppend(
|
|
964
|
-
history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
965
|
-
checkpoint: session.narrativeCheckpoint,
|
|
966
|
-
text: playerText,
|
|
967
|
-
locale: this.#compiler.locale,
|
|
968
|
-
checkpointAvailable: currentPlayPrompt(session).tools.some(({ name }) => name === "world_checkpoint"),
|
|
969
|
-
}),
|
|
1010
|
+
...playerInputAppend(playerText),
|
|
970
1011
|
]);
|
|
971
1012
|
const operationKey = `player:${exchangeId}`;
|
|
972
1013
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
@@ -1006,14 +1047,17 @@ export class PlayCallChain {
|
|
|
1006
1047
|
const message = error instanceof Error
|
|
1007
1048
|
? error.message
|
|
1008
1049
|
: "Failed to write the original player text.";
|
|
1050
|
+
const failureKind = classifyPlayFailure(error, false);
|
|
1009
1051
|
session.events.push({
|
|
1010
1052
|
id: session.nextEventId++,
|
|
1011
1053
|
kind: "failure",
|
|
1012
1054
|
message,
|
|
1055
|
+
failureKind,
|
|
1013
1056
|
});
|
|
1014
1057
|
session.status = "interrupted";
|
|
1015
1058
|
session.canRetry = false;
|
|
1016
1059
|
session.lastFailure = message;
|
|
1060
|
+
session.lastFailureKind = failureKind;
|
|
1017
1061
|
await this.#persist(session);
|
|
1018
1062
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1019
1063
|
throw new PlayCallChainError(message);
|
|
@@ -1077,6 +1121,7 @@ export class PlayCallChain {
|
|
|
1077
1121
|
session.canRetry = true;
|
|
1078
1122
|
session.lastFailure =
|
|
1079
1123
|
"The player message is committed; the frozen model request is ready to continue.";
|
|
1124
|
+
session.lastFailureKind = "retryable";
|
|
1080
1125
|
await this.#persist(session);
|
|
1081
1126
|
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
1082
1127
|
await this.#worlds.playAdvances.markSettled(advance, outcome.head);
|
|
@@ -1114,8 +1159,15 @@ export class PlayCallChain {
|
|
|
1114
1159
|
const prepared = [];
|
|
1115
1160
|
const trailingEvents = [];
|
|
1116
1161
|
let nextEventId = advance.eventId + 1;
|
|
1162
|
+
// One persisted seed per advance: every resolution call draws from it by
|
|
1163
|
+
// call identity, so recovery reproduces the same receipts.
|
|
1164
|
+
const resolutionSeed = calls.some(({ name }) => isResolutionTool(name))
|
|
1165
|
+
? await this.#worlds.playAdvances.resolutionSeed(advance)
|
|
1166
|
+
: undefined;
|
|
1117
1167
|
for (const call of calls) {
|
|
1118
|
-
const item = prepareTool(session, workingTools, advance.exchange, call, this.#compiler.locale);
|
|
1168
|
+
const item = prepareTool(session, workingTools, advance.exchange, call, this.#compiler.locale, resolutionSeed);
|
|
1169
|
+
if (item.result.resolution !== undefined)
|
|
1170
|
+
crashAtPlayAdvanceEdge("after_resolution");
|
|
1119
1171
|
prepared.push(item);
|
|
1120
1172
|
trailingEvents.push({
|
|
1121
1173
|
id: nextEventId++,
|
|
@@ -1343,6 +1395,7 @@ export class PlayCallChain {
|
|
|
1343
1395
|
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
1344
1396
|
session.canRetry = false;
|
|
1345
1397
|
session.lastFailure = null;
|
|
1398
|
+
delete session.lastFailureKind;
|
|
1346
1399
|
let result;
|
|
1347
1400
|
if (settlement.assistantEvent.continuation !== "available") {
|
|
1348
1401
|
const message = "The complete model response has no provider-native continuation payload; use a fresh context.";
|
|
@@ -1350,20 +1403,25 @@ export class PlayCallChain {
|
|
|
1350
1403
|
id: session.nextEventId++,
|
|
1351
1404
|
kind: "failure",
|
|
1352
1405
|
message,
|
|
1406
|
+
failureKind: "context_unavailable",
|
|
1353
1407
|
});
|
|
1354
1408
|
session.status = "interrupted";
|
|
1355
1409
|
session.lastFailure = message;
|
|
1410
|
+
session.lastFailureKind = "context_unavailable";
|
|
1356
1411
|
result = { kind: "terminal", status: "interrupted" };
|
|
1357
1412
|
}
|
|
1358
1413
|
else if (hasToolCalls) {
|
|
1359
1414
|
session.exchange += 1;
|
|
1360
1415
|
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1416
|
+
if (session.lastRequest?.modelConfig !== undefined)
|
|
1417
|
+
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1361
1418
|
session.lastRequest = structuredClone(nextRequest);
|
|
1362
1419
|
session.lastRequestAttempt = 0;
|
|
1363
1420
|
session.status = "interrupted";
|
|
1364
1421
|
session.canRetry = true;
|
|
1365
1422
|
session.lastFailure =
|
|
1366
1423
|
"The completed tool response is settled; its frozen continuation request is ready.";
|
|
1424
|
+
session.lastFailureKind = "retryable";
|
|
1367
1425
|
result = { kind: "continue", nextRequest };
|
|
1368
1426
|
}
|
|
1369
1427
|
else {
|
|
@@ -1374,8 +1432,10 @@ export class PlayCallChain {
|
|
|
1374
1432
|
id: session.nextEventId++,
|
|
1375
1433
|
kind: "failure",
|
|
1376
1434
|
message,
|
|
1435
|
+
failureKind: "other",
|
|
1377
1436
|
});
|
|
1378
1437
|
session.lastFailure = message;
|
|
1438
|
+
session.lastFailureKind = "other";
|
|
1379
1439
|
}
|
|
1380
1440
|
result = { kind: "terminal", status: session.status };
|
|
1381
1441
|
}
|
|
@@ -1399,7 +1459,7 @@ export class PlayCallChain {
|
|
|
1399
1459
|
return this.#settleResponseAdvance(session, advance, current.settlementPrepared.settlement);
|
|
1400
1460
|
}
|
|
1401
1461
|
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
1402
|
-
let request = structuredClone(firstRequest);
|
|
1462
|
+
let request = modelHost.freezeRequest?.(firstRequest) ?? structuredClone(firstRequest);
|
|
1403
1463
|
let attempt = firstAttempt;
|
|
1404
1464
|
for (;;) {
|
|
1405
1465
|
this.#setInvocationAbortable(session.chainId, true);
|
|
@@ -1409,6 +1469,7 @@ export class PlayCallChain {
|
|
|
1409
1469
|
session.status = "running";
|
|
1410
1470
|
session.canRetry = false;
|
|
1411
1471
|
session.lastFailure = null;
|
|
1472
|
+
delete session.lastFailureKind;
|
|
1412
1473
|
session.lastRequest = structuredClone(request);
|
|
1413
1474
|
session.lastRequestAttempt = attempt;
|
|
1414
1475
|
const responseExchange = request.exchange ?? session.exchange;
|
|
@@ -1500,20 +1561,31 @@ export class PlayCallChain {
|
|
|
1500
1561
|
const message = error instanceof Error
|
|
1501
1562
|
? error.message
|
|
1502
1563
|
: "The model request was interrupted.";
|
|
1503
|
-
session.events.push({
|
|
1504
|
-
id: session.nextEventId++,
|
|
1505
|
-
kind: error instanceof ModelHostCancelledError
|
|
1506
|
-
? "cancellation"
|
|
1507
|
-
: "failure",
|
|
1508
|
-
message,
|
|
1509
|
-
});
|
|
1510
1564
|
session.status = "interrupted";
|
|
1511
|
-
const requiresFreshContext = error instanceof
|
|
1565
|
+
const requiresFreshContext = error instanceof ModelHostBindingMismatchError ||
|
|
1566
|
+
error instanceof ModelHostContinuationError ||
|
|
1512
1567
|
error instanceof ModelHostOutcomeUnknownError ||
|
|
1513
1568
|
modelHostFailureRequiresFreshContext(error);
|
|
1514
1569
|
session.canRetry =
|
|
1515
1570
|
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
1516
1571
|
session.lastFailure = message;
|
|
1572
|
+
if (error instanceof ModelHostCancelledError) {
|
|
1573
|
+
delete session.lastFailureKind;
|
|
1574
|
+
session.events.push({
|
|
1575
|
+
id: session.nextEventId++,
|
|
1576
|
+
kind: "cancellation",
|
|
1577
|
+
message,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
else {
|
|
1581
|
+
session.lastFailureKind = classifyPlayFailure(error, session.canRetry);
|
|
1582
|
+
session.events.push({
|
|
1583
|
+
id: session.nextEventId++,
|
|
1584
|
+
kind: "failure",
|
|
1585
|
+
message,
|
|
1586
|
+
failureKind: session.lastFailureKind,
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1517
1589
|
await this.#persist(session);
|
|
1518
1590
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1519
1591
|
this.#notifySnapshot(observer, session);
|
|
@@ -1631,14 +1703,17 @@ export class PlayCallChain {
|
|
|
1631
1703
|
},
|
|
1632
1704
|
],
|
|
1633
1705
|
});
|
|
1706
|
+
const failureKind = classifyPlayFailure(error, false);
|
|
1634
1707
|
session.events.push({
|
|
1635
1708
|
id: session.nextEventId++,
|
|
1636
1709
|
kind: "failure",
|
|
1637
1710
|
message,
|
|
1711
|
+
failureKind,
|
|
1638
1712
|
});
|
|
1639
1713
|
session.status = "interrupted";
|
|
1640
1714
|
session.canRetry = false;
|
|
1641
1715
|
session.lastFailure = message;
|
|
1716
|
+
session.lastFailureKind = failureKind;
|
|
1642
1717
|
await this.#persist(session);
|
|
1643
1718
|
this.#notifySnapshot(observer, session);
|
|
1644
1719
|
return projectView(session);
|
|
@@ -1662,6 +1737,7 @@ export class PlayCallChain {
|
|
|
1662
1737
|
session.lastRequest = structuredClone(request);
|
|
1663
1738
|
session.lastRequestAttempt = Math.max(0, attempt - 1);
|
|
1664
1739
|
session.lastFailure = message;
|
|
1740
|
+
delete session.lastFailureKind;
|
|
1665
1741
|
await this.#persist(session);
|
|
1666
1742
|
if (advance !== undefined)
|
|
1667
1743
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
@@ -1881,14 +1957,21 @@ export class PlayCallChain {
|
|
|
1881
1957
|
: canRetry
|
|
1882
1958
|
? "The service stopped before model dispatch; the frozen request can be retried."
|
|
1883
1959
|
: "The service stopped before preparing a model request; use a fresh context.";
|
|
1960
|
+
const failureKind = dispatchMayHaveStarted
|
|
1961
|
+
? "outcome_unknown"
|
|
1962
|
+
: canRetry
|
|
1963
|
+
? "retryable"
|
|
1964
|
+
: "other";
|
|
1884
1965
|
session.events.push({
|
|
1885
1966
|
id: session.nextEventId++,
|
|
1886
1967
|
kind: "failure",
|
|
1887
1968
|
message,
|
|
1969
|
+
failureKind,
|
|
1888
1970
|
});
|
|
1889
1971
|
session.status = "interrupted";
|
|
1890
1972
|
session.canRetry = canRetry;
|
|
1891
1973
|
session.lastFailure = message;
|
|
1974
|
+
session.lastFailureKind = failureKind;
|
|
1892
1975
|
await this.#persist(session);
|
|
1893
1976
|
}
|
|
1894
1977
|
async #assertCurrentHead(session) {
|
|
@@ -1923,7 +2006,8 @@ function followupOperationId(session) {
|
|
|
1923
2006
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
1924
2007
|
}
|
|
1925
2008
|
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1926
|
-
|
|
2009
|
+
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
2010
|
+
return modelHost.freezeRequest?.(request) ?? request;
|
|
1927
2011
|
}
|
|
1928
2012
|
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1929
2013
|
return {
|
|
@@ -1998,7 +2082,7 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
1998
2082
|
});
|
|
1999
2083
|
}
|
|
2000
2084
|
}
|
|
2001
|
-
function prepareTool(session, working, exchange, call, locale) {
|
|
2085
|
+
function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
2002
2086
|
const key = `${exchange}:${call.id}`;
|
|
2003
2087
|
const signature = JSON.stringify(call.arguments);
|
|
2004
2088
|
const completed = working.get(key);
|
|
@@ -2025,8 +2109,9 @@ function prepareTool(session, working, exchange, call, locale) {
|
|
|
2025
2109
|
}
|
|
2026
2110
|
else {
|
|
2027
2111
|
try {
|
|
2028
|
-
result =
|
|
2029
|
-
call.name
|
|
2112
|
+
result = isResolutionTool(call.name)
|
|
2113
|
+
? resolutionToolResult(call.name, call.arguments, resolutionSeed, key, locale)
|
|
2114
|
+
: call.name === "world_checkpoint"
|
|
2030
2115
|
? typeof call.arguments === "object" &&
|
|
2031
2116
|
call.arguments !== null &&
|
|
2032
2117
|
!Array.isArray(call.arguments) &&
|
|
@@ -2069,6 +2154,9 @@ function prepareTool(session, working, exchange, call, locale) {
|
|
|
2069
2154
|
name: call.name,
|
|
2070
2155
|
ok: result.ok,
|
|
2071
2156
|
markdown: result.markdown,
|
|
2157
|
+
...(result.resolution === undefined
|
|
2158
|
+
? {}
|
|
2159
|
+
: { resolution: structuredClone(result.resolution) }),
|
|
2072
2160
|
replayed,
|
|
2073
2161
|
};
|
|
2074
2162
|
return {
|
|
@@ -2108,7 +2196,9 @@ function mergeChangedDocuments(session, changes) {
|
|
|
2108
2196
|
}
|
|
2109
2197
|
}
|
|
2110
2198
|
function projectContext(context) {
|
|
2199
|
+
const response = context.events.findLast((event) => event.kind === "assistant" && event.status === "completed");
|
|
2111
2200
|
return {
|
|
2201
|
+
...(context.importedFromArchive ? { requiresFreshContext: true } : {}),
|
|
2112
2202
|
chainId: context.chainId,
|
|
2113
2203
|
baselineHead: context.baselineHead,
|
|
2114
2204
|
...(context.baselineHistoryLength === undefined
|
|
@@ -2116,6 +2206,22 @@ function projectContext(context) {
|
|
|
2116
2206
|
: { baselineHistoryLength: context.baselineHistoryLength }),
|
|
2117
2207
|
parentHead: context.parentHead,
|
|
2118
2208
|
playPreset: structuredClone(recordedPlayPrompt(context).playPreset),
|
|
2209
|
+
...(context.modelBinding === undefined
|
|
2210
|
+
? {}
|
|
2211
|
+
: {
|
|
2212
|
+
contextWindowTokens: context.lastRequest?.modelConfig?.contextWindowTokens ??
|
|
2213
|
+
context.modelBinding.contextWindowTokens,
|
|
2214
|
+
}),
|
|
2215
|
+
...(response?.kind !== "assistant"
|
|
2216
|
+
? {}
|
|
2217
|
+
: {
|
|
2218
|
+
latestCompletedResponse: {
|
|
2219
|
+
id: response.id,
|
|
2220
|
+
...(response.usage === undefined
|
|
2221
|
+
? {}
|
|
2222
|
+
: { usage: structuredClone(response.usage) }),
|
|
2223
|
+
},
|
|
2224
|
+
}),
|
|
2119
2225
|
status: context.status,
|
|
2120
2226
|
canRetry: context.canRetry,
|
|
2121
2227
|
events: context.events
|
|
@@ -2123,6 +2229,9 @@ function projectContext(context) {
|
|
|
2123
2229
|
.map((event) => projectActivityEvent(event)),
|
|
2124
2230
|
changedDocuments: structuredClone(context.changedDocuments),
|
|
2125
2231
|
lastFailure: context.lastFailure,
|
|
2232
|
+
...(context.lastFailureKind === undefined
|
|
2233
|
+
? {}
|
|
2234
|
+
: { lastFailureKind: context.lastFailureKind }),
|
|
2126
2235
|
updatedAt: context.updatedAt,
|
|
2127
2236
|
};
|
|
2128
2237
|
}
|
|
@@ -2130,7 +2239,7 @@ function projectActivityEvent(event) {
|
|
|
2130
2239
|
if (event.kind === "player" ||
|
|
2131
2240
|
event.kind === "failure" ||
|
|
2132
2241
|
event.kind === "cancellation")
|
|
2133
|
-
return structuredClone(event);
|
|
2242
|
+
return structuredClone(projectLegacyModelBindingFailure(event));
|
|
2134
2243
|
if (event.kind === "assistant") {
|
|
2135
2244
|
const { reasoning, toolFragment, usage, ...summary } = event;
|
|
2136
2245
|
void reasoning;
|
|
@@ -2296,7 +2405,8 @@ function assertModelHostBinding(frozen, modelHost) {
|
|
|
2296
2405
|
function assertSessionModelHostBinding(session, modelHost) {
|
|
2297
2406
|
if (session.modelBinding === undefined)
|
|
2298
2407
|
throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
|
|
2299
|
-
|
|
2408
|
+
if (session.modelBinding.provider !== modelHost.binding().provider)
|
|
2409
|
+
throw new PlayCallChainError("The selected model protocol differs from this context. Switch back to its original protocol to continue, or start a fresh context.", "model_protocol_mismatch");
|
|
2300
2410
|
}
|
|
2301
2411
|
function validateIdentity(value, label) {
|
|
2302
2412
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
@@ -2317,6 +2427,16 @@ function notifyAssistantDelta(observer, delta) {
|
|
|
2317
2427
|
// Like snapshot projection, the Provider exchange outlives its browser stream.
|
|
2318
2428
|
}
|
|
2319
2429
|
}
|
|
2430
|
+
function resolutionToolResult(name, args, seed, identity, locale) {
|
|
2431
|
+
if (seed === undefined)
|
|
2432
|
+
throw new Error("The resolution seed for this advance is unavailable.");
|
|
2433
|
+
const resolution = resolveTabletop(name, args, seededRandom(seed, identity));
|
|
2434
|
+
return {
|
|
2435
|
+
ok: true,
|
|
2436
|
+
markdown: resolutionMarkdown(resolution, locale),
|
|
2437
|
+
resolution,
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2320
2440
|
function crashAtPlayAdvanceEdge(edge) {
|
|
2321
2441
|
if (process.env.NODE_ENV === "test" &&
|
|
2322
2442
|
process.env.NARRAEON_INTERNAL_TEST_CRASH_AT_PLAY_ADVANCE_EDGE === edge)
|
|
@@ -28,16 +28,7 @@ export function checkpointHistory(history, checkpoint) {
|
|
|
28
28
|
.slice(cutoff + 1)
|
|
29
29
|
.filter(([id]) => !id.includes("message.genesis"));
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
const markdown = locale === "zh-CN"
|
|
33
|
-
? `距上次检查点已完成 ${rounds} 回合。${hasCheckpoint ? "" : "当前尚无检查点,以世界起点计数。"}`
|
|
34
|
-
: `Completed player rounds since the last checkpoint: ${rounds}.${hasCheckpoint ? "" : " No checkpoint yet; counting from the world origin."}`;
|
|
35
|
-
return {
|
|
36
|
-
kind: "runtime_notice",
|
|
37
|
-
notice: "checkpoint_rounds",
|
|
38
|
-
text: `${locale === "zh-CN" ? "[Runtime 回合提示]" : "[Runtime round marker]"}\n${markdown}`,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
31
|
+
/** Retain released markers when reconstructing frozen transcript prefixes. */
|
|
41
32
|
export function isPlayerRoundMarker(item) {
|
|
42
33
|
return item?.kind === "runtime_notice" && item.notice === "checkpoint_rounds";
|
|
43
34
|
}
|
|
@@ -59,16 +50,8 @@ export function toolStepNotice(locale) {
|
|
|
59
50
|
: "[Runtime settlement]\nThe previous response called tools, so its text has not yet been displayed or committed as final story. The tools have settled as their receipts state. Use those receipts to present this turn’s still-unshown content in nonempty final prose with no tool calls, consistent with committed state. Tool settlement does not require advancing the story beyond that content.",
|
|
60
51
|
};
|
|
61
52
|
}
|
|
62
|
-
export function playerInputAppend(
|
|
63
|
-
return [
|
|
64
|
-
...(input.text.trim().length > 0 && input.checkpointAvailable
|
|
65
|
-
? [
|
|
66
|
-
playerRoundMarker(completedPlayerRounds(input.history) -
|
|
67
|
-
(input.checkpoint?.completedPlayerRounds ?? 0), input.checkpoint !== undefined, input.locale),
|
|
68
|
-
]
|
|
69
|
-
: []),
|
|
70
|
-
{ kind: "player", text: input.text },
|
|
71
|
-
];
|
|
53
|
+
export function playerInputAppend(text) {
|
|
54
|
+
return [{ kind: "player", text }];
|
|
72
55
|
}
|
|
73
56
|
export function checkpointReplayBlocks(history, checkpoint, locale, excluded = new Set(), snapshot = new HistoryQuery("checkpoint-replay", historyInputs(Object.entries(history).map(([path, contents]) => ({ path, contents }))))) {
|
|
74
57
|
const eligible = checkpointHistory(history, checkpoint);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ModelHostBindingMismatchError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
2
|
+
/** Classification does not grant permission to replay a request. */
|
|
3
|
+
export function classifyPlayFailure(error, canRetry) {
|
|
4
|
+
if (error instanceof ModelHostOutcomeUnknownError)
|
|
5
|
+
return "outcome_unknown";
|
|
6
|
+
if (error instanceof ModelHostContinuationError ||
|
|
7
|
+
error instanceof ModelHostBindingMismatchError)
|
|
8
|
+
return "context_unavailable";
|
|
9
|
+
if (error instanceof ModelHostFailureError) {
|
|
10
|
+
if (typeof error.details === "object" &&
|
|
11
|
+
error.details !== null &&
|
|
12
|
+
"code" in error.details &&
|
|
13
|
+
error.details.code === "model_output_limit")
|
|
14
|
+
return "output_limit";
|
|
15
|
+
if (modelHostFailureRequiresFreshContext(error))
|
|
16
|
+
return "context_limit";
|
|
17
|
+
return canRetry ? "retryable" : "provider_error";
|
|
18
|
+
}
|
|
19
|
+
return canRetry ? "retryable" : "other";
|
|
20
|
+
}
|