narraeon 0.5.6 → 0.5.7
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/node/protocol/appPreferences.js +4 -2
- package/dist/node/protocol/playFailure.js +13 -0
- package/dist/node/protocol/protagonistTemplates.js +13 -0
- package/dist/node/protocol/v1.js +25 -19
- package/dist/node/protocol/worldArchives.js +7 -0
- package/dist/node/runtime/V1Runtime.js +46 -6
- 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 +5 -0
- 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/FileNativePlayTimelineStore.js +80 -4
- package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
- package/dist/node/runtime/play/PlayCallChain.js +144 -52
- 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/prompt/FileNativePromptCompiler.js +4 -22
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
- package/dist/node/runtime/world/FileNativeWorldStore.js +172 -39
- package/dist/node/runtime/world/WorldActivity.js +143 -0
- package/dist/node/server/createServer.js +48 -0
- package/dist/node/shared/default-preset-host.js +10 -6
- package/dist/node/shared/ordered-play-prompts.js +2 -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/web/assets/index-DxNtxzLR.js +1255 -0
- package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/dist/web/assets/index-BH8NyuE4.js +0 -1233
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js";
|
|
2
|
+
import { classifyPlayFailure } from "./PlayFailure.js";
|
|
1
3
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
2
4
|
import { isDeepStrictEqual } from "node:util";
|
|
3
5
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
@@ -10,7 +12,7 @@ import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepN
|
|
|
10
12
|
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
11
13
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
12
14
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
13
|
-
import { equalModelHostBinding, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
15
|
+
import { equalModelHostBinding, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, modelHostFailureRequiresFreshContext, } from "../model/ModelHost.js";
|
|
14
16
|
import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js";
|
|
15
17
|
import { parsePlayPresetFiles, } from "./FileNativePlayPresetStore.js";
|
|
16
18
|
import { runPlayFollowupRequests, } from "./PlayFollowupRequests.js";
|
|
@@ -162,6 +164,7 @@ export class PlayCallChain {
|
|
|
162
164
|
this.#active.set(session.chainId, session);
|
|
163
165
|
this.#worldChains.set(session.worldId, session.chainId);
|
|
164
166
|
await this.#persist(session);
|
|
167
|
+
await this.#worlds.completeArchiveFreshContext(session.worldId);
|
|
165
168
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "fresh", input.modelHost, signal, input.observer);
|
|
166
169
|
}
|
|
167
170
|
async append(input) {
|
|
@@ -169,6 +172,8 @@ export class PlayCallChain {
|
|
|
169
172
|
}
|
|
170
173
|
async #append(input, signal) {
|
|
171
174
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
175
|
+
if (await this.#worlds.archiveNeedsFreshContext(input.worldId))
|
|
176
|
+
throw new PlayCallChainError("This world was imported from a backup; start a fresh context before continuing.");
|
|
172
177
|
const session = await this.#requireChain(input.worldId, input.chainId);
|
|
173
178
|
await this.#assertCurrentHead(session);
|
|
174
179
|
assertSessionModelHostBinding(session, input.modelHost);
|
|
@@ -178,6 +183,11 @@ export class PlayCallChain {
|
|
|
178
183
|
if (session.status === "interrupted") {
|
|
179
184
|
if (!session.canRetry || session.lastRequest === null)
|
|
180
185
|
throw new PlayCallChainError("The current model context cannot continue; use a fresh context.");
|
|
186
|
+
// Pre-snapshot requests have only a binding fingerprint. Do not invent
|
|
187
|
+
// their original encoding settings from a newly selected connection.
|
|
188
|
+
if (session.lastRequest.modelConfig === undefined &&
|
|
189
|
+
!equalModelHostBinding(session.modelBinding, input.modelHost.binding()))
|
|
190
|
+
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
191
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
182
192
|
}
|
|
183
193
|
await this.#refreshPrompt(session, input);
|
|
@@ -464,7 +474,8 @@ export class PlayCallChain {
|
|
|
464
474
|
chainId: session.chainId,
|
|
465
475
|
baselineHead: session.baselineHead,
|
|
466
476
|
parentHead: session.parentHead,
|
|
467
|
-
stale: session.parentHead !== worldHead
|
|
477
|
+
stale: session.parentHead !== worldHead ||
|
|
478
|
+
(await this.#worlds.archiveNeedsFreshContext(worldId)),
|
|
468
479
|
playPreset: structuredClone(recordedPlayPrompt(session).playPreset),
|
|
469
480
|
updatedAt: session.updatedAt,
|
|
470
481
|
bootstrap: {
|
|
@@ -477,17 +488,25 @@ export class PlayCallChain {
|
|
|
477
488
|
async deriveWorld(input) {
|
|
478
489
|
const result = await this.#worlds.deriveWorld({
|
|
479
490
|
...input,
|
|
480
|
-
stageTarget: async ({ targetWorldRoot, targetWorldId, binding }) => {
|
|
491
|
+
stageTarget: async ({ targetWorldRoot, targetWorldId, binding, sourceHead, }) => {
|
|
481
492
|
await this.stageForkToDerivedWorld({
|
|
482
493
|
sourceWorldId: input.sourceWorldId,
|
|
483
|
-
sourceHead
|
|
494
|
+
sourceHead,
|
|
484
495
|
targetWorldId,
|
|
485
496
|
targetWorldRoot,
|
|
486
497
|
targetBinding: binding,
|
|
498
|
+
...(input.retainCompleteTimeline
|
|
499
|
+
? { retainCompleteTimeline: true }
|
|
500
|
+
: {}),
|
|
501
|
+
...(input.identityPrefix === "import"
|
|
502
|
+
? { markImportedContexts: true }
|
|
503
|
+
: {}),
|
|
487
504
|
});
|
|
488
505
|
},
|
|
489
506
|
});
|
|
490
|
-
if (
|
|
507
|
+
if (input.targetDataRoot === undefined &&
|
|
508
|
+
input.publish !== false &&
|
|
509
|
+
(await this.inspectWorld(result.world.worldId)) === null)
|
|
491
510
|
await this.forkToDerivedWorld({
|
|
492
511
|
sourceWorldId: input.sourceWorldId,
|
|
493
512
|
sourceHead: input.sourceHead,
|
|
@@ -523,10 +542,14 @@ export class PlayCallChain {
|
|
|
523
542
|
if (source === null)
|
|
524
543
|
return null;
|
|
525
544
|
const sourceContexts = await this.#worlds.playTimeline.readAllContexts(input.sourceWorldId);
|
|
526
|
-
const selectedContextIndex =
|
|
545
|
+
const selectedContextIndex = input.retainCompleteTimeline
|
|
546
|
+
? sourceContexts.length - 1
|
|
547
|
+
: sourceContexts.findIndex(({ events }) => eventsThroughHead(events, input.sourceHead) !== null);
|
|
527
548
|
if (selectedContextIndex < 0)
|
|
528
549
|
return null;
|
|
529
|
-
const sourceEvents =
|
|
550
|
+
const sourceEvents = input.retainCompleteTimeline
|
|
551
|
+
? sourceContexts[selectedContextIndex].events
|
|
552
|
+
: eventsThroughHead(sourceContexts[selectedContextIndex].events, input.sourceHead);
|
|
530
553
|
return this.#forkSelectionToDerivedWorld({
|
|
531
554
|
sourceWorldId: input.sourceWorldId,
|
|
532
555
|
sourceContexts,
|
|
@@ -537,6 +560,8 @@ export class PlayCallChain {
|
|
|
537
560
|
branchIdentity: input.sourceHead,
|
|
538
561
|
targetWorldRoot: input.targetWorldRoot,
|
|
539
562
|
targetBinding: input.targetBinding,
|
|
563
|
+
...(input.retainCompleteTimeline ? { retainCompleteTimeline: true } : {}),
|
|
564
|
+
...(input.markImportedContexts ? { markImportedContexts: true } : {}),
|
|
540
565
|
});
|
|
541
566
|
}
|
|
542
567
|
/**
|
|
@@ -604,6 +629,9 @@ export class PlayCallChain {
|
|
|
604
629
|
if (selectedContextIndex < 0)
|
|
605
630
|
throw new PlayCallChainError("The model context to revise does not exist.");
|
|
606
631
|
const sourceContext = sourceContexts[selectedContextIndex];
|
|
632
|
+
if (input.continuation === "continue_context" &&
|
|
633
|
+
sourceContext.importedFromArchive)
|
|
634
|
+
throw new PlayCallChainError("This model context was imported from a backup; start a fresh context before continuing.");
|
|
607
635
|
const selectedEventIndex = sourceContext.events.findIndex((event) => event.id === input.eventId);
|
|
608
636
|
const selectedEvent = sourceContext.events[selectedEventIndex];
|
|
609
637
|
if (selectedEventIndex < 0 ||
|
|
@@ -646,11 +674,10 @@ export class PlayCallChain {
|
|
|
646
674
|
this.#worlds.recoverEndpoint(request.worldId, restoresHead),
|
|
647
675
|
this.#worlds.bindPlayCallChain(request.worldId),
|
|
648
676
|
]);
|
|
649
|
-
const continuedTrace = traceRetention.withReplacement(
|
|
677
|
+
const continuedTrace = traceRetention.withReplacement({
|
|
650
678
|
head: outcome.head,
|
|
651
679
|
exchangeId: request.replacementExchangeId,
|
|
652
680
|
text: request.replacementText,
|
|
653
|
-
locale: this.#compiler.locale,
|
|
654
681
|
});
|
|
655
682
|
if (binding.parentHead !== outcome.head)
|
|
656
683
|
throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
|
|
@@ -672,6 +699,7 @@ export class PlayCallChain {
|
|
|
672
699
|
freshPreparation,
|
|
673
700
|
});
|
|
674
701
|
await this.#worlds.playTimeline.persist(structuredClone(revised));
|
|
702
|
+
await this.#worlds.completeArchiveFreshContext(request.worldId);
|
|
675
703
|
const activeId = this.#worldChains.get(request.worldId);
|
|
676
704
|
if (activeId !== undefined)
|
|
677
705
|
this.#active.delete(activeId);
|
|
@@ -799,13 +827,7 @@ export class PlayCallChain {
|
|
|
799
827
|
layout: "world_state",
|
|
800
828
|
files: Object.entries(input.binding.files).map(([path, contents]) => ({ path, contents })),
|
|
801
829
|
}), 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
|
-
}),
|
|
830
|
+
transcript: playerInputAppend(request.replacementText),
|
|
809
831
|
events: [
|
|
810
832
|
{
|
|
811
833
|
id: 1,
|
|
@@ -843,22 +865,30 @@ export class PlayCallChain {
|
|
|
843
865
|
if (input.targetWorldRoot === undefined &&
|
|
844
866
|
(await this.#worlds.currentHead(input.targetWorldId)) !== input.sourceHead)
|
|
845
867
|
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
|
-
|
|
868
|
+
let retainedTrace;
|
|
869
|
+
if (input.retainCompleteTimeline) {
|
|
870
|
+
// A backup retains the current page closure, even when its last context
|
|
871
|
+
// precedes a correction or contains an uncommitted response diagnostic.
|
|
872
|
+
retainedTrace = independentContextCopy(sourceContext);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
const [baseline, selected] = await Promise.all([
|
|
876
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
877
|
+
? input.targetWorldId
|
|
878
|
+
: input.sourceWorldId, sourceContext.baselineHead),
|
|
879
|
+
this.#worlds.recoverEndpoint(input.targetWorldRoot === undefined
|
|
880
|
+
? input.targetWorldId
|
|
881
|
+
: input.sourceWorldId, input.sourceHead),
|
|
882
|
+
]);
|
|
883
|
+
const traceRetention = prepareForkTrace(sourceContext, input.sourceEvents);
|
|
884
|
+
const derivedBinding = input.targetBinding ??
|
|
885
|
+
(await this.#worlds.bindPlayCallChain(input.targetWorldId));
|
|
886
|
+
retainedTrace = traceRetention.atEndpoint({
|
|
887
|
+
baseline,
|
|
888
|
+
selected,
|
|
889
|
+
binding: derivedBinding,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
862
892
|
const now = Date.now();
|
|
863
893
|
const timelineGeneration = this.#worlds.playTimeline.newGeneration();
|
|
864
894
|
let previousChainId = null;
|
|
@@ -868,6 +898,7 @@ export class PlayCallChain {
|
|
|
868
898
|
const chainId = derivedChainId(context.chainId, `prefix:${input.branchIdentity}:${index}`, input.targetWorldId);
|
|
869
899
|
const previousContext = {
|
|
870
900
|
...independentContextCopy(context),
|
|
901
|
+
...(input.markImportedContexts ? { importedFromArchive: true } : {}),
|
|
871
902
|
schemaVersion: 3,
|
|
872
903
|
kind: "play_call_chain",
|
|
873
904
|
worldId: input.targetWorldId,
|
|
@@ -884,11 +915,15 @@ export class PlayCallChain {
|
|
|
884
915
|
targetWorldRoot: input.targetWorldRoot,
|
|
885
916
|
source: context,
|
|
886
917
|
target: previousContext,
|
|
918
|
+
...(input.retainCompleteTimeline
|
|
919
|
+
? { retainCompleteContext: true }
|
|
920
|
+
: {}),
|
|
887
921
|
});
|
|
888
922
|
previousChainId = chainId;
|
|
889
923
|
}
|
|
890
924
|
const derived = {
|
|
891
925
|
...retainedTrace,
|
|
926
|
+
...(input.markImportedContexts ? { importedFromArchive: true } : {}),
|
|
892
927
|
schemaVersion: 3,
|
|
893
928
|
kind: "play_call_chain",
|
|
894
929
|
chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
|
|
@@ -896,7 +931,9 @@ export class PlayCallChain {
|
|
|
896
931
|
previousContexts: [],
|
|
897
932
|
previousChainId,
|
|
898
933
|
timelineGeneration,
|
|
899
|
-
parentHead: input.
|
|
934
|
+
parentHead: input.retainCompleteTimeline
|
|
935
|
+
? sourceContext.parentHead
|
|
936
|
+
: input.sourceHead,
|
|
900
937
|
updatedAt: now,
|
|
901
938
|
};
|
|
902
939
|
if (input.targetWorldRoot === undefined)
|
|
@@ -907,6 +944,9 @@ export class PlayCallChain {
|
|
|
907
944
|
targetWorldRoot: input.targetWorldRoot,
|
|
908
945
|
source: sourceContext,
|
|
909
946
|
target: structuredClone(derived),
|
|
947
|
+
...(input.retainCompleteTimeline
|
|
948
|
+
? { retainCompleteContext: true }
|
|
949
|
+
: {}),
|
|
910
950
|
});
|
|
911
951
|
if (input.targetWorldRoot !== undefined) {
|
|
912
952
|
const retainedContexts = [
|
|
@@ -960,13 +1000,7 @@ export class PlayCallChain {
|
|
|
960
1000
|
const exchange = session.exchange + 1;
|
|
961
1001
|
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
962
1002
|
...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
|
-
}),
|
|
1003
|
+
...playerInputAppend(playerText),
|
|
970
1004
|
]);
|
|
971
1005
|
const operationKey = `player:${exchangeId}`;
|
|
972
1006
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
@@ -1006,14 +1040,17 @@ export class PlayCallChain {
|
|
|
1006
1040
|
const message = error instanceof Error
|
|
1007
1041
|
? error.message
|
|
1008
1042
|
: "Failed to write the original player text.";
|
|
1043
|
+
const failureKind = classifyPlayFailure(error, false);
|
|
1009
1044
|
session.events.push({
|
|
1010
1045
|
id: session.nextEventId++,
|
|
1011
1046
|
kind: "failure",
|
|
1012
1047
|
message,
|
|
1048
|
+
failureKind,
|
|
1013
1049
|
});
|
|
1014
1050
|
session.status = "interrupted";
|
|
1015
1051
|
session.canRetry = false;
|
|
1016
1052
|
session.lastFailure = message;
|
|
1053
|
+
session.lastFailureKind = failureKind;
|
|
1017
1054
|
await this.#persist(session);
|
|
1018
1055
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1019
1056
|
throw new PlayCallChainError(message);
|
|
@@ -1077,6 +1114,7 @@ export class PlayCallChain {
|
|
|
1077
1114
|
session.canRetry = true;
|
|
1078
1115
|
session.lastFailure =
|
|
1079
1116
|
"The player message is committed; the frozen model request is ready to continue.";
|
|
1117
|
+
session.lastFailureKind = "retryable";
|
|
1080
1118
|
await this.#persist(session);
|
|
1081
1119
|
crashAtPlayAdvanceEdge("after_timeline_settled");
|
|
1082
1120
|
await this.#worlds.playAdvances.markSettled(advance, outcome.head);
|
|
@@ -1343,6 +1381,7 @@ export class PlayCallChain {
|
|
|
1343
1381
|
session.exchange = Math.max(session.exchange, advance.exchange);
|
|
1344
1382
|
session.canRetry = false;
|
|
1345
1383
|
session.lastFailure = null;
|
|
1384
|
+
delete session.lastFailureKind;
|
|
1346
1385
|
let result;
|
|
1347
1386
|
if (settlement.assistantEvent.continuation !== "available") {
|
|
1348
1387
|
const message = "The complete model response has no provider-native continuation payload; use a fresh context.";
|
|
@@ -1350,20 +1389,25 @@ export class PlayCallChain {
|
|
|
1350
1389
|
id: session.nextEventId++,
|
|
1351
1390
|
kind: "failure",
|
|
1352
1391
|
message,
|
|
1392
|
+
failureKind: "context_unavailable",
|
|
1353
1393
|
});
|
|
1354
1394
|
session.status = "interrupted";
|
|
1355
1395
|
session.lastFailure = message;
|
|
1396
|
+
session.lastFailureKind = "context_unavailable";
|
|
1356
1397
|
result = { kind: "terminal", status: "interrupted" };
|
|
1357
1398
|
}
|
|
1358
1399
|
else if (hasToolCalls) {
|
|
1359
1400
|
session.exchange += 1;
|
|
1360
1401
|
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1402
|
+
if (session.lastRequest?.modelConfig !== undefined)
|
|
1403
|
+
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1361
1404
|
session.lastRequest = structuredClone(nextRequest);
|
|
1362
1405
|
session.lastRequestAttempt = 0;
|
|
1363
1406
|
session.status = "interrupted";
|
|
1364
1407
|
session.canRetry = true;
|
|
1365
1408
|
session.lastFailure =
|
|
1366
1409
|
"The completed tool response is settled; its frozen continuation request is ready.";
|
|
1410
|
+
session.lastFailureKind = "retryable";
|
|
1367
1411
|
result = { kind: "continue", nextRequest };
|
|
1368
1412
|
}
|
|
1369
1413
|
else {
|
|
@@ -1374,8 +1418,10 @@ export class PlayCallChain {
|
|
|
1374
1418
|
id: session.nextEventId++,
|
|
1375
1419
|
kind: "failure",
|
|
1376
1420
|
message,
|
|
1421
|
+
failureKind: "other",
|
|
1377
1422
|
});
|
|
1378
1423
|
session.lastFailure = message;
|
|
1424
|
+
session.lastFailureKind = "other";
|
|
1379
1425
|
}
|
|
1380
1426
|
result = { kind: "terminal", status: session.status };
|
|
1381
1427
|
}
|
|
@@ -1399,7 +1445,7 @@ export class PlayCallChain {
|
|
|
1399
1445
|
return this.#settleResponseAdvance(session, advance, current.settlementPrepared.settlement);
|
|
1400
1446
|
}
|
|
1401
1447
|
async #dispatch(session, modelHost, firstRequest, firstAttempt, signal, observer) {
|
|
1402
|
-
let request = structuredClone(firstRequest);
|
|
1448
|
+
let request = modelHost.freezeRequest?.(firstRequest) ?? structuredClone(firstRequest);
|
|
1403
1449
|
let attempt = firstAttempt;
|
|
1404
1450
|
for (;;) {
|
|
1405
1451
|
this.#setInvocationAbortable(session.chainId, true);
|
|
@@ -1409,6 +1455,7 @@ export class PlayCallChain {
|
|
|
1409
1455
|
session.status = "running";
|
|
1410
1456
|
session.canRetry = false;
|
|
1411
1457
|
session.lastFailure = null;
|
|
1458
|
+
delete session.lastFailureKind;
|
|
1412
1459
|
session.lastRequest = structuredClone(request);
|
|
1413
1460
|
session.lastRequestAttempt = attempt;
|
|
1414
1461
|
const responseExchange = request.exchange ?? session.exchange;
|
|
@@ -1500,20 +1547,31 @@ export class PlayCallChain {
|
|
|
1500
1547
|
const message = error instanceof Error
|
|
1501
1548
|
? error.message
|
|
1502
1549
|
: "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
1550
|
session.status = "interrupted";
|
|
1511
|
-
const requiresFreshContext = error instanceof
|
|
1551
|
+
const requiresFreshContext = error instanceof ModelHostBindingMismatchError ||
|
|
1552
|
+
error instanceof ModelHostContinuationError ||
|
|
1512
1553
|
error instanceof ModelHostOutcomeUnknownError ||
|
|
1513
1554
|
modelHostFailureRequiresFreshContext(error);
|
|
1514
1555
|
session.canRetry =
|
|
1515
1556
|
!requiresFreshContext && error instanceof ModelHostFailureError;
|
|
1516
1557
|
session.lastFailure = message;
|
|
1558
|
+
if (error instanceof ModelHostCancelledError) {
|
|
1559
|
+
delete session.lastFailureKind;
|
|
1560
|
+
session.events.push({
|
|
1561
|
+
id: session.nextEventId++,
|
|
1562
|
+
kind: "cancellation",
|
|
1563
|
+
message,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
else {
|
|
1567
|
+
session.lastFailureKind = classifyPlayFailure(error, session.canRetry);
|
|
1568
|
+
session.events.push({
|
|
1569
|
+
id: session.nextEventId++,
|
|
1570
|
+
kind: "failure",
|
|
1571
|
+
message,
|
|
1572
|
+
failureKind: session.lastFailureKind,
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1517
1575
|
await this.#persist(session);
|
|
1518
1576
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
1519
1577
|
this.#notifySnapshot(observer, session);
|
|
@@ -1631,14 +1689,17 @@ export class PlayCallChain {
|
|
|
1631
1689
|
},
|
|
1632
1690
|
],
|
|
1633
1691
|
});
|
|
1692
|
+
const failureKind = classifyPlayFailure(error, false);
|
|
1634
1693
|
session.events.push({
|
|
1635
1694
|
id: session.nextEventId++,
|
|
1636
1695
|
kind: "failure",
|
|
1637
1696
|
message,
|
|
1697
|
+
failureKind,
|
|
1638
1698
|
});
|
|
1639
1699
|
session.status = "interrupted";
|
|
1640
1700
|
session.canRetry = false;
|
|
1641
1701
|
session.lastFailure = message;
|
|
1702
|
+
session.lastFailureKind = failureKind;
|
|
1642
1703
|
await this.#persist(session);
|
|
1643
1704
|
this.#notifySnapshot(observer, session);
|
|
1644
1705
|
return projectView(session);
|
|
@@ -1662,6 +1723,7 @@ export class PlayCallChain {
|
|
|
1662
1723
|
session.lastRequest = structuredClone(request);
|
|
1663
1724
|
session.lastRequestAttempt = Math.max(0, attempt - 1);
|
|
1664
1725
|
session.lastFailure = message;
|
|
1726
|
+
delete session.lastFailureKind;
|
|
1665
1727
|
await this.#persist(session);
|
|
1666
1728
|
if (advance !== undefined)
|
|
1667
1729
|
await this.#worlds.playAdvances.markSettled(advance, session.parentHead);
|
|
@@ -1881,14 +1943,21 @@ export class PlayCallChain {
|
|
|
1881
1943
|
: canRetry
|
|
1882
1944
|
? "The service stopped before model dispatch; the frozen request can be retried."
|
|
1883
1945
|
: "The service stopped before preparing a model request; use a fresh context.";
|
|
1946
|
+
const failureKind = dispatchMayHaveStarted
|
|
1947
|
+
? "outcome_unknown"
|
|
1948
|
+
: canRetry
|
|
1949
|
+
? "retryable"
|
|
1950
|
+
: "other";
|
|
1884
1951
|
session.events.push({
|
|
1885
1952
|
id: session.nextEventId++,
|
|
1886
1953
|
kind: "failure",
|
|
1887
1954
|
message,
|
|
1955
|
+
failureKind,
|
|
1888
1956
|
});
|
|
1889
1957
|
session.status = "interrupted";
|
|
1890
1958
|
session.canRetry = canRetry;
|
|
1891
1959
|
session.lastFailure = message;
|
|
1960
|
+
session.lastFailureKind = failureKind;
|
|
1892
1961
|
await this.#persist(session);
|
|
1893
1962
|
}
|
|
1894
1963
|
async #assertCurrentHead(session) {
|
|
@@ -1923,7 +1992,8 @@ function followupOperationId(session) {
|
|
|
1923
1992
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
1924
1993
|
}
|
|
1925
1994
|
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1926
|
-
|
|
1995
|
+
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1996
|
+
return modelHost.freezeRequest?.(request) ?? request;
|
|
1927
1997
|
}
|
|
1928
1998
|
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1929
1999
|
return {
|
|
@@ -2108,7 +2178,9 @@ function mergeChangedDocuments(session, changes) {
|
|
|
2108
2178
|
}
|
|
2109
2179
|
}
|
|
2110
2180
|
function projectContext(context) {
|
|
2181
|
+
const response = context.events.findLast((event) => event.kind === "assistant" && event.status === "completed");
|
|
2111
2182
|
return {
|
|
2183
|
+
...(context.importedFromArchive ? { requiresFreshContext: true } : {}),
|
|
2112
2184
|
chainId: context.chainId,
|
|
2113
2185
|
baselineHead: context.baselineHead,
|
|
2114
2186
|
...(context.baselineHistoryLength === undefined
|
|
@@ -2116,6 +2188,22 @@ function projectContext(context) {
|
|
|
2116
2188
|
: { baselineHistoryLength: context.baselineHistoryLength }),
|
|
2117
2189
|
parentHead: context.parentHead,
|
|
2118
2190
|
playPreset: structuredClone(recordedPlayPrompt(context).playPreset),
|
|
2191
|
+
...(context.modelBinding === undefined
|
|
2192
|
+
? {}
|
|
2193
|
+
: {
|
|
2194
|
+
contextWindowTokens: context.lastRequest?.modelConfig?.contextWindowTokens ??
|
|
2195
|
+
context.modelBinding.contextWindowTokens,
|
|
2196
|
+
}),
|
|
2197
|
+
...(response?.kind !== "assistant"
|
|
2198
|
+
? {}
|
|
2199
|
+
: {
|
|
2200
|
+
latestCompletedResponse: {
|
|
2201
|
+
id: response.id,
|
|
2202
|
+
...(response.usage === undefined
|
|
2203
|
+
? {}
|
|
2204
|
+
: { usage: structuredClone(response.usage) }),
|
|
2205
|
+
},
|
|
2206
|
+
}),
|
|
2119
2207
|
status: context.status,
|
|
2120
2208
|
canRetry: context.canRetry,
|
|
2121
2209
|
events: context.events
|
|
@@ -2123,6 +2211,9 @@ function projectContext(context) {
|
|
|
2123
2211
|
.map((event) => projectActivityEvent(event)),
|
|
2124
2212
|
changedDocuments: structuredClone(context.changedDocuments),
|
|
2125
2213
|
lastFailure: context.lastFailure,
|
|
2214
|
+
...(context.lastFailureKind === undefined
|
|
2215
|
+
? {}
|
|
2216
|
+
: { lastFailureKind: context.lastFailureKind }),
|
|
2126
2217
|
updatedAt: context.updatedAt,
|
|
2127
2218
|
};
|
|
2128
2219
|
}
|
|
@@ -2130,7 +2221,7 @@ function projectActivityEvent(event) {
|
|
|
2130
2221
|
if (event.kind === "player" ||
|
|
2131
2222
|
event.kind === "failure" ||
|
|
2132
2223
|
event.kind === "cancellation")
|
|
2133
|
-
return structuredClone(event);
|
|
2224
|
+
return structuredClone(projectLegacyModelBindingFailure(event));
|
|
2134
2225
|
if (event.kind === "assistant") {
|
|
2135
2226
|
const { reasoning, toolFragment, usage, ...summary } = event;
|
|
2136
2227
|
void reasoning;
|
|
@@ -2296,7 +2387,8 @@ function assertModelHostBinding(frozen, modelHost) {
|
|
|
2296
2387
|
function assertSessionModelHostBinding(session, modelHost) {
|
|
2297
2388
|
if (session.modelBinding === undefined)
|
|
2298
2389
|
throw new PlayCallChainError("This model context predates durable provider continuation bindings; start a fresh context.");
|
|
2299
|
-
|
|
2390
|
+
if (session.modelBinding.provider !== modelHost.binding().provider)
|
|
2391
|
+
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
2392
|
}
|
|
2301
2393
|
function validateIdentity(value, label) {
|
|
2302
2394
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u.test(value))
|
|
@@ -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
|
+
}
|
|
@@ -13,7 +13,7 @@ export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
|
13
13
|
const player = source.events[eventIndex];
|
|
14
14
|
return {
|
|
15
15
|
// This append precedes the operation's materialization check, as before.
|
|
16
|
-
withReplacement(
|
|
16
|
+
withReplacement(replacement) {
|
|
17
17
|
const events = [
|
|
18
18
|
...prefix,
|
|
19
19
|
{
|
|
@@ -27,16 +27,7 @@ export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
|
27
27
|
];
|
|
28
28
|
const continuedTranscript = [
|
|
29
29
|
...transcript,
|
|
30
|
-
...playerInputAppend(
|
|
31
|
-
history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
|
|
32
|
-
messageId,
|
|
33
|
-
exactText,
|
|
34
|
-
])),
|
|
35
|
-
checkpoint: selected.narrativeCheckpoint,
|
|
36
|
-
text: replacement.text,
|
|
37
|
-
locale: replacement.locale,
|
|
38
|
-
checkpointAvailable: source.tools.some(({ name }) => name === "world_checkpoint"),
|
|
39
|
-
}),
|
|
30
|
+
...playerInputAppend(replacement.text),
|
|
40
31
|
];
|
|
41
32
|
return {
|
|
42
33
|
atEndpoint(endpoint) {
|
|
@@ -96,6 +87,7 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
96
87
|
const documents = restorePlayDocuments(endpoint.binding.files, source, selection.authorizationEvents);
|
|
97
88
|
const lastEvent = selection.events.at(-1);
|
|
98
89
|
return {
|
|
90
|
+
...(source.importedFromArchive ? { importedFromArchive: true } : {}),
|
|
99
91
|
continuityContextId: source.continuityContextId ?? source.chainId,
|
|
100
92
|
baselineHead: source.baselineHead,
|
|
101
93
|
baselineHistoryLength: source.baselineHistoryLength ?? endpoint.baseline.history.length,
|
|
@@ -13,7 +13,7 @@ import { renderWorldYamlSource } from "../world/WorldYamlRendering.js";
|
|
|
13
13
|
export { renderWorldYamlSource } from "../world/WorldYamlRendering.js";
|
|
14
14
|
import { measureWorldMaterials, refreshMaintenanceTotals, } from "./WorldMaintenanceReport.js";
|
|
15
15
|
import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
16
|
-
import { checkpointHistory, checkpointReplayBlocks,
|
|
16
|
+
import { checkpointHistory, checkpointReplayBlocks, } from "../play/PlayContinuity.js";
|
|
17
17
|
import { defaultAppLocale, } from "../../protocol/appPreferences.js";
|
|
18
18
|
import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
|
|
19
19
|
import { defaultRuntimeToolDefinitionStrategy, registeredRuntimeToolNames, runtimeToolsForNames, } from "./FileNativeToolRegistry.js";
|
|
@@ -373,11 +373,11 @@ export class FileNativePromptCompiler {
|
|
|
373
373
|
const playCompilation = playPreset === undefined
|
|
374
374
|
? undefined
|
|
375
375
|
: this.compilePlayCallChain(input, playPreset);
|
|
376
|
-
return promptPreview(input, playPreset, playCompilation?.bootstrap ?? this.compileBootstrap(input), playCompilation
|
|
376
|
+
return promptPreview(input, playPreset, playCompilation?.bootstrap ?? this.compileBootstrap(input), playCompilation);
|
|
377
377
|
}
|
|
378
378
|
previewPlayPreset(input, playPreset) {
|
|
379
379
|
const playCompilation = this.compilePlayPreset(input, playPreset);
|
|
380
|
-
return promptPreview(input, playPreset, playCompilation.bootstrap, playCompilation
|
|
380
|
+
return promptPreview(input, playPreset, playCompilation.bootstrap, playCompilation);
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
383
|
* Compile the production play surface. The selected preset contributes its
|
|
@@ -909,14 +909,7 @@ function withoutAppendedContextGenesis(input) {
|
|
|
909
909
|
},
|
|
910
910
|
};
|
|
911
911
|
}
|
|
912
|
-
function promptPreview(input, playPreset, compilation, playCompilation
|
|
913
|
-
const marker = playerInputAppend({
|
|
914
|
-
history: input.world.history ?? {},
|
|
915
|
-
checkpoint: input.world.narrativeCheckpoint,
|
|
916
|
-
text: input.playerInput,
|
|
917
|
-
locale,
|
|
918
|
-
checkpointAvailable: playCompilation?.toolUniverse.some(({ name }) => name === "world_checkpoint") ?? false,
|
|
919
|
-
})[0];
|
|
912
|
+
function promptPreview(input, playPreset, compilation, playCompilation) {
|
|
920
913
|
const result = {
|
|
921
914
|
diagnosticBinding: {
|
|
922
915
|
endpoint: input.endpoint.id,
|
|
@@ -937,17 +930,6 @@ function promptPreview(input, playPreset, compilation, playCompilation, locale =
|
|
|
937
930
|
initialAppend: {
|
|
938
931
|
logical: { kind: "player", text: input.playerInput },
|
|
939
932
|
provider: { role: "user", content: input.playerInput },
|
|
940
|
-
...(isPlayerRoundMarker(marker)
|
|
941
|
-
? {
|
|
942
|
-
beforePlayer: {
|
|
943
|
-
logical: marker,
|
|
944
|
-
provider: {
|
|
945
|
-
role: "user",
|
|
946
|
-
content: marker.text,
|
|
947
|
-
},
|
|
948
|
-
},
|
|
949
|
-
}
|
|
950
|
-
: {}),
|
|
951
933
|
},
|
|
952
934
|
}
|
|
953
935
|
: {}),
|