narraeon 0.5.2 → 0.5.4

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.
@@ -1,7 +1,10 @@
1
+ import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
2
+ import { PlayCallChainError } from "./PlayCallChainError.js";
3
+ import { preparePlayerRevisionTrace, prepareForkTrace, restorePlayDocuments, independentContextCopy, } from "./PlayTraceRetention.js";
1
4
  import { currentPlayPrompt, recordedPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
2
5
  import { comparePromptPrefixes } from "../prompt/WorldPromptDiagnostics.js";
3
6
  import { createHash } from "node:crypto";
4
- import { completedPlayerRounds, continuationNotice, isPlayerRoundMarker, playerInputAppend, toolStepNotice, } from "./PlayContinuity.js";
7
+ import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, } from "./PlayContinuity.js";
5
8
  import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
6
9
  import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
7
10
  import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
@@ -13,6 +16,8 @@ import { FileNativePlayDocuments, fingerprintControl, } from "./PlayDocumentTool
13
16
  const callChainToolNames = new Set([
14
17
  "state_list",
15
18
  "history_list",
19
+ "history_search",
20
+ "history_read",
16
21
  // Kept executable only for contexts whose frozen tool universe contains it.
17
22
  "context_list",
18
23
  "context_search",
@@ -23,12 +28,7 @@ const callChainToolNames = new Set([
23
28
  "world_checkpoint",
24
29
  ]);
25
30
  const projectedEventTail = 40;
26
- export class PlayCallChainError extends Error {
27
- constructor(message) {
28
- super(message);
29
- this.name = "PlayCallChainError";
30
- }
31
- }
31
+ export { PlayCallChainError } from "./PlayCallChainError.js";
32
32
  /**
33
33
  * The production play loop: one durable logical context, no Runtime-authored
34
34
  * adjudication/narration phases. Responses with tool calls are intermediate
@@ -625,8 +625,7 @@ export class PlayCallChain {
625
625
  : null;
626
626
  const timelineGeneration = playerRevisionTimelineGeneration(input.source.timelineGeneration, request.operationId, request.worldId, request.continuation);
627
627
  const requestFingerprint = playerRevisionRequestFingerprint(request, input.selectedEvent.committedHead, restoresHead);
628
- const prefixEvents = structuredClone(sourceContext.events.slice(0, input.selectedEventIndex));
629
- const transcript = transcriptThroughEvents(sourceContext.transcript, prefixEvents);
628
+ const traceRetention = preparePlayerRevisionTrace(sourceContext, input.selectedEventIndex);
630
629
  const outcome = await this.#worlds.reviseTimeline({
631
630
  operationId: request.operationId,
632
631
  worldId: request.worldId,
@@ -636,33 +635,17 @@ export class PlayCallChain {
636
635
  replacementText: request.replacementText,
637
636
  requestFingerprint,
638
637
  });
639
- const events = [
640
- ...prefixEvents,
641
- {
642
- id: input.selectedEvent.id,
643
- kind: "player",
644
- exchangeId: request.replacementExchangeId,
645
- text: request.replacementText,
646
- context: input.selectedEvent.context,
647
- committedHead: outcome.head,
648
- },
649
- ];
650
- const completedKeys = completedToolKeys(prefixEvents);
651
638
  const [baseline, selected, binding] = await Promise.all([
652
639
  this.#worlds.recoverEndpoint(request.worldId, sourceContext.baselineHead),
653
640
  this.#worlds.recoverEndpoint(request.worldId, restoresHead),
654
641
  this.#worlds.bindPlayCallChain(request.worldId),
655
642
  ]);
656
- transcript.push(...playerInputAppend({
657
- history: Object.fromEntries(selected.history.map(({ messageId, exactText }) => [
658
- messageId,
659
- exactText,
660
- ])),
661
- checkpoint: selected.narrativeCheckpoint,
643
+ const continuedTrace = traceRetention.withReplacement(selected, {
644
+ head: outcome.head,
645
+ exchangeId: request.replacementExchangeId,
662
646
  text: request.replacementText,
663
647
  locale: this.#compiler.locale,
664
- checkpointAvailable: sourceContext.tools.some(({ name }) => name === "world_checkpoint"),
665
- }));
648
+ });
666
649
  if (binding.parentHead !== outcome.head)
667
650
  throw new PlayCallChainError("The timeline revision was committed, but current-world materialization has not reached the new endpoint.");
668
651
  if (request.continuation === "fresh_context") {
@@ -673,7 +656,7 @@ export class PlayCallChain {
673
656
  sourceContext,
674
657
  sourceContexts: input.sourceContexts,
675
658
  selectedContextIndex: input.selectedContextIndex,
676
- prefixEvents,
659
+ traceRetention,
677
660
  baseline,
678
661
  selected,
679
662
  binding,
@@ -694,9 +677,8 @@ export class PlayCallChain {
694
677
  playCallChain: projectView(revised),
695
678
  };
696
679
  }
697
- const documents = restorePlayDocuments(binding.files, sourceContext, prefixEvents);
698
680
  const revised = {
699
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
681
+ ...continuedTrace.atEndpoint({ baseline, selected, binding }),
700
682
  schemaVersion: 3,
701
683
  kind: "play_call_chain",
702
684
  chainId: playerRevisionChainId(sourceContext.chainId, request.operationId, request.worldId, request.continuation),
@@ -706,44 +688,7 @@ export class PlayCallChain {
706
688
  ? null
707
689
  : input.sourceContexts[input.selectedContextIndex - 1].chainId,
708
690
  timelineGeneration,
709
- baselineHead: sourceContext.baselineHead,
710
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
711
691
  parentHead: outcome.head,
712
- promptRuns: playPromptRunsThroughEvents(sourceContext, events),
713
- ...(sourceContext.presetFiles === undefined
714
- ? {}
715
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
716
- playPreset: structuredClone(sourceContext.playPreset),
717
- ...(sourceContext.followups === undefined
718
- ? {}
719
- : { followups: structuredClone(sourceContext.followups) }),
720
- ...(sourceContext.playPresetScriptsEnabled === undefined
721
- ? {}
722
- : {
723
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
724
- }),
725
- ...(sourceContext.modelBinding === undefined
726
- ? {}
727
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
728
- status: "ready",
729
- canRetry: false,
730
- bootstrap: structuredClone(sourceContext.bootstrap),
731
- tools: structuredClone(sourceContext.tools),
732
- transcript,
733
- events,
734
- completedTools: sourceContext.completedTools
735
- .filter(({ key }) => completedKeys.has(key))
736
- .map((item) => structuredClone(item)),
737
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, documents.authorizationCheckpoint()),
738
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
739
- nextMaterials: structuredClone(binding.additionalMaterials),
740
- nextEventId: Math.max(0, ...events.map(({ id }) => id)) + 1,
741
- exchange: Math.max(0, ...prefixEvents
742
- .filter((event) => event.kind === "assistant" && event.status === "completed")
743
- .map(({ exchange }) => exchange)),
744
- lastRequest: null,
745
- lastRequestAttempt: 0,
746
- lastFailure: null,
747
692
  updatedAt: Date.now(),
748
693
  };
749
694
  await this.#worlds.playTimeline.persist(structuredClone(revised));
@@ -797,12 +742,14 @@ export class PlayCallChain {
797
742
  let previousChainId = input.selectedContextIndex === 0
798
743
  ? null
799
744
  : input.sourceContexts[input.selectedContextIndex - 1].chainId;
800
- if (input.prefixEvents.length > 0) {
801
- const prefixTranscript = transcriptThroughEvents(sourceContext.transcript, input.prefixEvents);
802
- const completedKeys = completedToolKeys(input.prefixEvents);
803
- const documents = restorePlayDocuments(input.binding.files, sourceContext, input.prefixEvents);
745
+ const retainedPrefix = input.traceRetention.pagePrefixAtEndpoint({
746
+ baseline: input.baseline,
747
+ selected: input.selected,
748
+ binding: input.binding,
749
+ });
750
+ if (retainedPrefix !== null) {
804
751
  const prefix = {
805
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
752
+ ...retainedPrefix,
806
753
  schemaVersion: 3,
807
754
  kind: "play_call_chain",
808
755
  chainId: derivedChainId(sourceContext.chainId, "timeline-revision-prefix:" + request.operationId, request.worldId),
@@ -810,42 +757,7 @@ export class PlayCallChain {
810
757
  previousContexts: [],
811
758
  previousChainId,
812
759
  timelineGeneration: input.timelineGeneration,
813
- baselineHead: sourceContext.baselineHead,
814
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? input.baseline.history.length,
815
760
  parentHead: input.restoresHead,
816
- promptRuns: playPromptRunsThroughEvents(sourceContext, input.prefixEvents),
817
- ...(sourceContext.presetFiles === undefined
818
- ? {}
819
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
820
- playPreset: structuredClone(sourceContext.playPreset),
821
- ...(sourceContext.followups === undefined
822
- ? {}
823
- : { followups: structuredClone(sourceContext.followups) }),
824
- ...(sourceContext.playPresetScriptsEnabled === undefined
825
- ? {}
826
- : {
827
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
828
- }),
829
- ...(sourceContext.modelBinding === undefined
830
- ? {}
831
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
832
- status: "ready",
833
- canRetry: false,
834
- bootstrap: structuredClone(sourceContext.bootstrap),
835
- tools: structuredClone(sourceContext.tools),
836
- transcript: prefixTranscript,
837
- events: structuredClone(input.prefixEvents),
838
- completedTools: sourceContext.completedTools
839
- .filter(({ key }) => completedKeys.has(key))
840
- .map((item) => structuredClone(item)),
841
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, input.prefixEvents, documents.authorizationCheckpoint()),
842
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, input.baseline.state, input.selected.state),
843
- nextMaterials: structuredClone(input.selected.additionalMaterials),
844
- nextEventId: Math.max(0, ...input.prefixEvents.map(({ id }) => id)) + 1,
845
- exchange: completedAssistantExchange(input.prefixEvents),
846
- lastRequest: null,
847
- lastRequestAttempt: 0,
848
- lastFailure: null,
849
761
  updatedAt: Date.now(),
850
762
  };
851
763
  await this.#worlds.playTimeline.persistDetached(structuredClone(prefix));
@@ -928,20 +840,14 @@ export class PlayCallChain {
928
840
  ? input.targetWorldId
929
841
  : input.sourceWorldId, input.sourceHead),
930
842
  ]);
931
- const selectsCompleteContext = input.sourceEvents.length === sourceContext.events.length;
932
- // A complete context already defines the selected trace closure. Preserve
933
- // its model transcript verbatim instead of reconstructing it from the
934
- // independently persisted page-event projection.
935
- const transcript = selectsCompleteContext
936
- ? structuredClone(sourceContext.transcript)
937
- : transcriptThroughEvents(sourceContext.transcript, input.sourceEvents);
938
- const completedKeys = selectsCompleteContext
939
- ? null
940
- : completedToolKeys(input.sourceEvents);
941
- const events = structuredClone(input.sourceEvents);
843
+ const traceRetention = prepareForkTrace(sourceContext, input.sourceEvents);
942
844
  const derivedBinding = input.targetBinding ??
943
845
  (await this.#worlds.bindPlayCallChain(input.targetWorldId));
944
- const derivedDocuments = restorePlayDocuments(derivedBinding.files, sourceContext, input.sourceEvents);
846
+ const retainedTrace = traceRetention.atEndpoint({
847
+ baseline,
848
+ selected,
849
+ binding: derivedBinding,
850
+ });
945
851
  const now = Date.now();
946
852
  const timelineGeneration = this.#worlds.playTimeline.newGeneration();
947
853
  let previousChainId = null;
@@ -971,7 +877,7 @@ export class PlayCallChain {
971
877
  previousChainId = chainId;
972
878
  }
973
879
  const derived = {
974
- continuityContextId: sourceContext.continuityContextId ?? sourceContext.chainId,
880
+ ...retainedTrace,
975
881
  schemaVersion: 3,
976
882
  kind: "play_call_chain",
977
883
  chainId: derivedChainId(sourceContext.chainId, input.branchIdentity, input.targetWorldId),
@@ -979,44 +885,7 @@ export class PlayCallChain {
979
885
  previousContexts: [],
980
886
  previousChainId,
981
887
  timelineGeneration,
982
- baselineHead: sourceContext.baselineHead,
983
- baselineHistoryLength: sourceContext.baselineHistoryLength ?? baseline.history.length,
984
888
  parentHead: input.sourceHead,
985
- promptRuns: playPromptRunsThroughEvents(sourceContext, events),
986
- ...(sourceContext.presetFiles === undefined
987
- ? {}
988
- : { presetFiles: structuredClone(sourceContext.presetFiles) }),
989
- playPreset: structuredClone(sourceContext.playPreset),
990
- ...(sourceContext.followups === undefined
991
- ? {}
992
- : { followups: structuredClone(sourceContext.followups) }),
993
- ...(sourceContext.playPresetScriptsEnabled === undefined
994
- ? {}
995
- : {
996
- playPresetScriptsEnabled: sourceContext.playPresetScriptsEnabled,
997
- }),
998
- ...(sourceContext.modelBinding === undefined
999
- ? {}
1000
- : { modelBinding: structuredClone(sourceContext.modelBinding) }),
1001
- status: "ready",
1002
- canRetry: false,
1003
- bootstrap: structuredClone(sourceContext.bootstrap),
1004
- tools: structuredClone(sourceContext.tools),
1005
- transcript,
1006
- events,
1007
- completedTools: sourceContext.completedTools
1008
- .filter(({ key }) => completedKeys === null || completedKeys.has(key))
1009
- .map((item) => structuredClone(item)),
1010
- documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(sourceContext, events, derivedDocuments.authorizationCheckpoint()),
1011
- changedDocuments: changedDocumentsAtHead(sourceContext.changedDocuments, baseline.state, selected.state),
1012
- nextMaterials: structuredClone(derivedBinding.additionalMaterials),
1013
- nextEventId: Math.max(0, ...events.map(({ id }) => id)) + 1,
1014
- exchange: Math.max(0, ...events
1015
- .filter((event) => event.kind === "assistant" && event.status === "completed")
1016
- .map(({ exchange }) => exchange)),
1017
- lastRequest: null,
1018
- lastRequestAttempt: 0,
1019
- lastFailure: null,
1020
889
  updatedAt: now,
1021
890
  };
1022
891
  if (input.targetWorldRoot === undefined)
@@ -2109,15 +1978,19 @@ function prepareTool(session, working, exchange, call, locale) {
2109
1978
  ? {
2110
1979
  ok: true,
2111
1980
  markdown: locale === "zh-CN"
2112
- ? "检查点已登记,将在本轮最终叙事提交后生效。玩家随后可以选择开启全新上下文。"
2113
- : "Checkpoint registered. It becomes effective when this turn’s final narrative commits. The player can then choose a fresh context.",
1981
+ ? "检查点已登记;随后非空且无工具调用的最终正文成功提交后生效,边界包含该正文。它仅标记历史重放边界,不结束世界内事件;登记后仍可继续调用工具。玩家随后可自行选择全新上下文。"
1982
+ : "Checkpoint registered. It takes effect after the subsequent nonempty tool-free final prose successfully commits, including that prose in its boundary. It marks a history-replay boundary without ending an in-world event; further tool calls may follow registration. The player may then choose a fresh context.",
2114
1983
  }
2115
1984
  : {
2116
1985
  ok: false,
2117
1986
  failureKind: "protocol",
2118
1987
  markdown: "world_checkpoint requires an empty argument object.",
2119
1988
  }
2120
- : session.documents.execute(call, session.history);
1989
+ : session.documents.execute(call, session.history, {
1990
+ historyScope: session.worldId,
1991
+ locale,
1992
+ legacyHistoryTools: hasLegacyHistoryTools(currentPlayPrompt(session).tools),
1993
+ });
2121
1994
  }
2122
1995
  catch (error) {
2123
1996
  result = {
@@ -2178,53 +2051,6 @@ function mergeChangedDocuments(session, changes) {
2178
2051
  session.changedDocuments[index] = projected;
2179
2052
  }
2180
2053
  }
2181
- function restorePlayDocuments(files, context, events) {
2182
- const documents = new FileNativePlayDocuments(files);
2183
- try {
2184
- const checkpoint = documentAuthorizationThroughEvents(context, events);
2185
- const runs = playPromptRunsThroughEvents(context, events);
2186
- if (checkpoint === undefined || runs.length > 0)
2187
- documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
2188
- if (checkpoint !== undefined)
2189
- documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
2190
- }
2191
- catch (error) {
2192
- throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
2193
- }
2194
- return documents;
2195
- }
2196
- function documentAuthorizationThroughEvents(context, events) {
2197
- const checkpoints = context.documentAuthorizationCheckpoints;
2198
- if (checkpoints === undefined || checkpoints.length === 0)
2199
- return undefined;
2200
- const selectedEventIds = new Set(events.map(({ id }) => id));
2201
- const selected = checkpoints.findLast(({ afterEventId }) => afterEventId === 0 || selectedEventIds.has(afterEventId));
2202
- // Legacy V1 records had no durable dynamic authorization. If the selected
2203
- // prefix predates their first lazily-written checkpoint, retain the exact
2204
- // old recovery behavior and rebuild only bootstrap authorization.
2205
- return selected === undefined ? undefined : structuredClone(selected);
2206
- }
2207
- function independentContextCopy(source) {
2208
- const context = structuredClone(source);
2209
- context.continuityContextId ??= source.chainId;
2210
- delete context.derivedFrom;
2211
- delete context.branchedBeforePlayer;
2212
- return context;
2213
- }
2214
- function authorizationCheckpointsThroughEvents(context, events, fallback) {
2215
- const eventIds = new Set(events.map(({ id }) => id));
2216
- const selected = (context.documentAuthorizationCheckpoints ?? [])
2217
- .filter(({ afterEventId }) => afterEventId === 0 || eventIds.has(afterEventId))
2218
- .map((checkpoint) => structuredClone(checkpoint));
2219
- if (selected.length > 0)
2220
- return selected;
2221
- return [
2222
- {
2223
- afterEventId: Math.max(0, ...events.map(({ id }) => id)),
2224
- authorization: structuredClone(fallback),
2225
- },
2226
- ];
2227
- }
2228
2054
  function projectContext(context) {
2229
2055
  return {
2230
2056
  chainId: context.chainId,
@@ -2343,86 +2169,6 @@ function eventsThroughHead(events, head) {
2343
2169
  end += 1;
2344
2170
  return structuredClone(events.slice(0, end));
2345
2171
  }
2346
- function transcriptThroughEvents(transcript, events) {
2347
- const result = [];
2348
- let cursor = 0;
2349
- for (const [index, event] of events.entries()) {
2350
- if (event.kind === "player") {
2351
- const marker = transcript[cursor];
2352
- if (isPlayerRoundMarker(marker)) {
2353
- result.push(structuredClone(marker));
2354
- cursor += 1;
2355
- }
2356
- const item = transcript[cursor];
2357
- if (item?.kind !== "player" || item.text !== event.text)
2358
- throw new PlayCallChainError("Source call-chain events do not match the model transcript, so a fork cannot be created safely.");
2359
- result.push(structuredClone(item));
2360
- cursor += 1;
2361
- continue;
2362
- }
2363
- if (event.kind === "assistant" && event.status === "completed") {
2364
- const notice = transcript[cursor];
2365
- const hasContinuationNotice = notice?.kind === "runtime_notice" && notice.notice === "continuation";
2366
- const item = transcript[cursor + (hasContinuationNotice ? 1 : 0)];
2367
- const hasToolCall = events
2368
- .slice(index + 1)
2369
- .find((candidate) => candidate.kind === "player" ||
2370
- candidate.kind === "assistant" ||
2371
- candidate.kind === "tool_call");
2372
- const recorded = event.text.trim().length > 0 ||
2373
- hasToolCall?.kind === "tool_call" ||
2374
- item?.kind === "assistant";
2375
- if (!recorded)
2376
- continue;
2377
- if (item?.kind !== "assistant" || item.text !== event.text)
2378
- throw new PlayCallChainError("Source call-chain responses do not match the model transcript, so a fork cannot be created safely.");
2379
- // A continuation notice belongs to the generation it precedes, never
2380
- // to a fork ending at the previously completed narrative.
2381
- if (hasContinuationNotice) {
2382
- result.push(structuredClone(notice));
2383
- cursor += 1;
2384
- }
2385
- result.push(structuredClone(item));
2386
- cursor += 1;
2387
- continue;
2388
- }
2389
- if (event.kind === "tool_result") {
2390
- const item = transcript[cursor];
2391
- if (item?.kind !== "tool" || item.toolCallId !== event.callId)
2392
- throw new PlayCallChainError("Source call-chain tool results do not match the model transcript, so a fork cannot be created safely.");
2393
- result.push(structuredClone(item));
2394
- cursor += 1;
2395
- // A tool-step notice follows the entire settled batch and remains part
2396
- // of its closure even when the selected endpoint stops at that batch.
2397
- const notice = transcript[cursor];
2398
- if (notice?.kind === "runtime_notice" && notice.notice === "tool_step") {
2399
- result.push(structuredClone(notice));
2400
- cursor += 1;
2401
- }
2402
- }
2403
- }
2404
- return result;
2405
- }
2406
- function completedToolKeys(events) {
2407
- const keys = new Set();
2408
- let exchange = null;
2409
- for (const event of events) {
2410
- if (event.kind === "player")
2411
- exchange = null;
2412
- else if (event.kind === "assistant")
2413
- exchange = event.exchange;
2414
- else if (event.kind === "tool_call" && exchange !== null)
2415
- keys.add(`${exchange}:${event.callId}`);
2416
- }
2417
- return keys;
2418
- }
2419
- function changedDocumentsAtHead(changes, baseline, selected) {
2420
- const before = new Map(baseline.map((file) => [file.path, file.contents]));
2421
- const after = new Map(selected.map((file) => [file.path, file.contents]));
2422
- return changes
2423
- .filter(({ path }) => after.has(path) && before.get(path) !== after.get(path))
2424
- .map((change) => structuredClone(change));
2425
- }
2426
2172
  function derivedChainId(sourceChainId, branchIdentity, targetWorldId) {
2427
2173
  return `play-branch-${createHash("sha256")
2428
2174
  .update(`${sourceChainId}\0${branchIdentity}\0${targetWorldId}`)
@@ -2473,11 +2219,6 @@ function playerRevisionTimelineGeneration(sourceGeneration, operationId, worldId
2473
2219
  .digest("hex")
2474
2220
  .slice(0, 40)}`;
2475
2221
  }
2476
- function completedAssistantExchange(events) {
2477
- return Math.max(0, ...events
2478
- .filter((event) => event.kind === "assistant" && event.status === "completed")
2479
- .map(({ exchange }) => exchange));
2480
- }
2481
2222
  function historyEntries(history) {
2482
2223
  return Object.entries(history).map(([path, contents]) => ({
2483
2224
  path,
@@ -0,0 +1,6 @@
1
+ export class PlayCallChainError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "PlayCallChainError";
5
+ }
6
+ }
@@ -1,3 +1,5 @@
1
+ import { HistoryQuery, historyInputs } from "../history/HistoryQuery.js";
2
+ import { renderHistoryMessage, renderHistoryInjectionNotice, } from "../history/HistoryRendering.js";
1
3
  /** Narrative history contains only committed original inputs and final prose. */
2
4
  export function completedPlayerRounds(history) {
3
5
  let pending = false;
@@ -28,8 +30,8 @@ export function checkpointHistory(history, checkpoint) {
28
30
  }
29
31
  export function playerRoundMarker(rounds, hasCheckpoint, locale) {
30
32
  const markdown = locale === "zh-CN"
31
- ? `距上次检查点已完成 ${rounds} 回合。${hasCheckpoint ? "" : "当前尚无检查点,以世界起点计数。"}必要状态整理完成时,可调用 world_checkpoint 建议在此开启全新上下文。`
32
- : `Completed player rounds since the last checkpoint: ${rounds}. ${hasCheckpoint ? "" : "No checkpoint yet; counting from the world origin. "}When necessary state maintenance is complete, world_checkpoint can suggest starting a fresh context here.`;
33
+ ? `距上次检查点已完成 ${rounds} 回合。${hasCheckpoint ? "" : "当前尚无检查点,以世界起点计数。"}`
34
+ : `Completed player rounds since the last checkpoint: ${rounds}.${hasCheckpoint ? "" : " No checkpoint yet; counting from the world origin."}`;
33
35
  return {
34
36
  kind: "runtime_notice",
35
37
  notice: "checkpoint_rounds",
@@ -44,8 +46,8 @@ export function continuationNotice(locale) {
44
46
  kind: "runtime_notice",
45
47
  notice: "continuation",
46
48
  text: locale === "zh-CN"
47
- ? "[Runtime 续写提示]\n本次发送没有追加新的玩家原文。请从当前对话继续生成;已有的最终主持正文是已经完成的历史。"
48
- : "[Runtime continuation]\nThis send adds no new player input. Continue generation from the current conversation; existing final narrator messages are already completed history.",
49
+ ? "[Runtime 续写提示]\n本次发送没有追加新的玩家原文。已有的最终主持正文已展示并提交为历史;本次响应接在其后,不重新提交旧正文。回复提交不表示世界内活动已经结束。"
50
+ : "[Runtime continuation]\nThis send adds no new player input. Existing final narrator prose has been displayed and committed as history; this response follows it without resubmitting it. A committed reply does not mean an in-world activity has ended.",
49
51
  };
50
52
  }
51
53
  export function toolStepNotice(locale) {
@@ -53,8 +55,8 @@ export function toolStepNotice(locale) {
53
55
  kind: "runtime_notice",
54
56
  notice: "tool_step",
55
57
  text: locale === "zh-CN"
56
- ? "[Runtime 结算提示]\n上一响应同时调用了工具,其中的文字没有作为最终故事展示或提交。工具已按回执结算;请从已提交结果继续,最终用无工具调用的非空叙事完成本次发送。"
57
- : "[Runtime settlement]\nThe previous response called tools, so its text was not displayed or committed as final story. The tools have settled as their receipts state. Continue from committed results, then finish this send with nonempty narrative and no tool calls.",
58
+ ? "[Runtime 结算提示]\n上一响应调用了工具,其中的文字尚未作为最终故事展示或提交。工具已按回执结算;请根据回执,用无工具调用的非空最终正文呈现本轮尚未展示的内容,并与已提交状态一致。工具结算不要求在这些内容之后再推进剧情。"
59
+ : "[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.",
58
60
  };
59
61
  }
60
62
  export function playerInputAppend(input) {
@@ -68,21 +70,24 @@ export function playerInputAppend(input) {
68
70
  { kind: "player", text: input.text },
69
71
  ];
70
72
  }
71
- export function checkpointReplayBlocks(history, checkpoint, locale) {
72
- const entries = checkpointHistory(history, checkpoint);
73
- if (entries.length === 0)
73
+ export function checkpointReplayBlocks(history, checkpoint, locale, excluded = new Set(), snapshot = new HistoryQuery("checkpoint-replay", historyInputs(Object.entries(history).map(([path, contents]) => ({ path, contents }))))) {
74
+ const eligible = checkpointHistory(history, checkpoint);
75
+ if (eligible.length === 0)
74
76
  return [];
77
+ const ids = new Set(eligible.filter(([id]) => !excluded.has(id)).map(([id]) => id));
78
+ const entries = snapshot.messages.filter(({ id }) => ids.has(id));
75
79
  const zh = locale === "zh-CN";
76
80
  return [
77
81
  {
78
82
  source: "runtime:checkpoint-history:notice",
79
- markdown: zh
83
+ markdown: (zh
80
84
  ? "# 检查点后的已提交原文\n\n以下是当前时间线上最近一次已生效检查点之后的玩家原文与最终主持叙事;尚无检查点时从世界起点选取,开场白除外。部分结果可能已写入当前文档;这些记录用于核对连续性,不代表需要再次执行其中的事件。当前明确的世界修订优先,不能用旧叙事推翻修订。"
81
- : "# Committed history after the checkpoint\n\nThese are the current timeline’s original player inputs and final narratives after its last effective checkpoint (or the world origin if none, excluding the opening). Some results may already be in current documents. These records support continuity, not repeated execution. Explicit current world corrections take precedence over old narrative.",
85
+ : "# Committed history after the checkpoint\n\nThese are the current timeline’s original player inputs and final narratives after its last effective checkpoint (or the world origin if none, excluding the opening). Some results may already be in current documents. These records support continuity, not repeated execution. Explicit current world corrections take precedence over old narrative.") +
86
+ `\n\n${renderHistoryInjectionNotice(snapshot.messages.length, entries, "checkpoint", locale)}`,
82
87
  },
83
- ...entries.map(([id, text]) => ({
84
- source: `runtime:checkpoint-history:${id}`,
85
- markdown: `## ${id.endsWith(".player") ? (zh ? "玩家原文" : "Player input") : zh ? "主持叙事" : "Host narrative"}\n\n${text}`,
88
+ ...entries.map((message) => ({
89
+ source: `runtime:checkpoint-history:${message.id}`,
90
+ markdown: renderHistoryMessage(message, locale),
86
91
  })),
87
92
  ];
88
93
  }