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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/node/protocol/appPreferences.js +4 -2
  4. package/dist/node/protocol/playFailure.js +13 -0
  5. package/dist/node/protocol/protagonistTemplates.js +13 -0
  6. package/dist/node/protocol/v1.js +25 -19
  7. package/dist/node/protocol/worldArchives.js +7 -0
  8. package/dist/node/runtime/V1Runtime.js +46 -6
  9. package/dist/node/runtime/archive/WorldArchiveZip.js +202 -0
  10. package/dist/node/runtime/archive/WorldArchives.js +389 -0
  11. package/dist/node/runtime/config/AppPreferencesStore.js +23 -10
  12. package/dist/node/runtime/config/ProtagonistTemplateStore.js +134 -0
  13. package/dist/node/runtime/content/ContentWorkspace.js +5 -0
  14. package/dist/node/runtime/history/StoryExport.js +33 -0
  15. package/dist/node/runtime/model/FileNativeModelAdapters.js +34 -2
  16. package/dist/node/runtime/model/ModelHost.js +12 -1
  17. package/dist/node/runtime/model/ModelHostStream.js +11 -3
  18. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +80 -4
  19. package/dist/node/runtime/play/LegacyModelBindingFailure.js +10 -0
  20. package/dist/node/runtime/play/PlayCallChain.js +144 -52
  21. package/dist/node/runtime/play/PlayCallChainError.js +3 -1
  22. package/dist/node/runtime/play/PlayContinuity.js +3 -20
  23. package/dist/node/runtime/play/PlayFailure.js +20 -0
  24. package/dist/node/runtime/play/PlayTraceRetention.js +3 -11
  25. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +4 -22
  26. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +2 -2
  27. package/dist/node/runtime/world/FileNativeWorldStore.js +172 -39
  28. package/dist/node/runtime/world/WorldActivity.js +143 -0
  29. package/dist/node/server/createServer.js +48 -0
  30. package/dist/node/shared/default-preset-host.js +10 -6
  31. package/dist/node/shared/ordered-play-prompts.js +2 -2
  32. package/dist/node/shared/sample-packages/index.js +10 -0
  33. package/dist/node/shared/sample-packages/rainy-inn-en.js +194 -0
  34. package/dist/node/shared/sample-packages/rainy-inn-zh.js +194 -0
  35. package/dist/web/assets/index-DxNtxzLR.js +1255 -0
  36. package/dist/web/assets/{index-CVN48xrw.css → index-DzOnu9uv.css} +1 -1
  37. package/dist/web/index.html +2 -2
  38. package/package.json +6 -2
  39. package/dist/web/assets/index-BH8NyuE4.js +0 -1233
@@ -1,8 +1,9 @@
1
+ var _a;
1
2
  import { createHash } from "node:crypto";
2
3
  import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
3
4
  import { dirname } from "node:path";
4
5
  import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
5
- import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
6
+ import { assertModelOutputComplete, ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
6
7
  import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
7
8
  import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
8
9
  import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
@@ -77,7 +78,31 @@ export class FileNativeModelHost {
77
78
  .digest("hex"),
78
79
  };
79
80
  }
80
- async exchange(request, observer) {
81
+ freezeRequest(request) {
82
+ const frozen = structuredClone(request);
83
+ if (frozen.modelConfig === undefined) {
84
+ const { baseUrl: _baseUrl, apiKey: _apiKey, ...config } = this.#connection;
85
+ void _baseUrl;
86
+ void _apiKey;
87
+ frozen.modelConfig = config;
88
+ }
89
+ return frozen;
90
+ }
91
+ #requestHost(request) {
92
+ if (request.modelConfig === undefined)
93
+ return this;
94
+ if (request.modelConfig.provider !== this.#connection.provider)
95
+ throw new ModelHostBindingMismatchError("The request protocol does not match the selected model host");
96
+ return new _a({
97
+ ...request.modelConfig,
98
+ baseUrl: this.#connection.baseUrl,
99
+ apiKey: this.#connection.apiKey,
100
+ }, this.#fetch, this.#failureLog);
101
+ }
102
+ exchange(request, observer) {
103
+ return this.#requestHost(request).#exchange(request, observer);
104
+ }
105
+ async #exchange(request, observer) {
81
106
  assertExchangeCompatibility(this.#connection, request);
82
107
  await trace("model_host_exchange", modelHostTrace(request));
83
108
  const signal = observer?.signal;
@@ -99,6 +124,9 @@ export class FileNativeModelHost {
99
124
  }
100
125
  /** Uses the production encoders and deliberately omits credentials. */
101
126
  previewRequest(request) {
127
+ return this.#requestHost(request).#previewRequest(request);
128
+ }
129
+ #previewRequest(request) {
102
130
  assertExchangeCompatibility(this.#connection, request);
103
131
  const provider = this.#connection.provider;
104
132
  const body = provider === "chat_completions"
@@ -183,6 +211,7 @@ export class FileNativeModelHost {
183
211
  await trace("chat_response", payload);
184
212
  const message = payload.choices?.[0]?.message;
185
213
  const finishReason = payload.choices?.[0]?.finish_reason;
214
+ assertModelOutputComplete(finishReason);
186
215
  if (message === undefined)
187
216
  throw unknownProviderResponse("Chat Completions");
188
217
  if (finishReason !== undefined &&
@@ -271,6 +300,7 @@ export class FileNativeModelHost {
271
300
  if (!isRecord(rawPayload) || !Array.isArray(rawPayload.content))
272
301
  throw unknownProviderResponse("Anthropic Messages");
273
302
  const payload = rawPayload;
303
+ assertModelOutputComplete(payload.stop_reason);
274
304
  const content = payload.content;
275
305
  if (!validAnthropicContent(content))
276
306
  throw unknownProviderResponse("Anthropic Messages");
@@ -310,6 +340,7 @@ export class FileNativeModelHost {
310
340
  });
311
341
  }
312
342
  }
343
+ _a = FileNativeModelHost;
313
344
  function assertExchangeCompatibility(connection, request) {
314
345
  if (request.bootstrap.provider.protocol !== connection.provider)
315
346
  throw new ModelHostBindingMismatchError("The compiled prompt protocol does not match the selected model host");
@@ -1048,6 +1079,7 @@ async function openAIResponsesRequest(input) {
1048
1079
  await trace(`${input.tracePrefix}_response`, payload);
1049
1080
  if (!isRecord(payload) || !Array.isArray(payload.output))
1050
1081
  throw unknownProviderResponse("Responses API");
1082
+ assertModelOutputComplete(responsesStopReason(payload));
1051
1083
  const output = cloneResponseOutput(payload.output);
1052
1084
  if (!validResponsesOutput(output))
1053
1085
  throw unknownProviderResponse("Responses API");
@@ -33,10 +33,21 @@ export class ModelHostFailureError extends Error {
33
33
  this.details = options.details;
34
34
  }
35
35
  }
36
- /** A Provider has rejected the complete request because its context is full. */
36
+ /** Reject provider-confirmed truncation before accepting text or tool calls. */
37
+ export function assertModelOutputComplete(stopReason) {
38
+ if (stopReason === "length" ||
39
+ stopReason === "max_tokens" ||
40
+ stopReason === "max_output_tokens" ||
41
+ stopReason === "incomplete:max_output_tokens") {
42
+ throw new ModelHostFailureError("The model response reached the output token limit before completion. Increase maxOutputTokens and start a fresh context; reasoning may also consume the output budget.", { details: { code: "model_output_limit", stopReason } });
43
+ }
44
+ }
45
+ /** Replaying the frozen request cannot recover a context or output limit. */
37
46
  export function modelHostFailureRequiresFreshContext(error) {
38
47
  if (!(error instanceof ModelHostFailureError))
39
48
  return false;
49
+ if (failureDetailStrings(error.details).includes("model_output_limit"))
50
+ return true;
40
51
  const evidence = [error.message, ...failureDetailStrings(error.details)]
41
52
  .join("\n")
42
53
  .toLocaleLowerCase("und");
@@ -1,4 +1,4 @@
1
- import { ModelHostFailureError } from "./ModelHost.js";
1
+ import { assertModelOutputComplete, ModelHostFailureError, } from "./ModelHost.js";
2
2
  import { providerStreamEvents } from "./ProviderStream.js";
3
3
  export async function aggregateChatModelStream(body, onDelta) {
4
4
  let content = "";
@@ -110,6 +110,7 @@ export async function aggregateChatModelStream(body, onDelta) {
110
110
  }
111
111
  if (!completed || !sawPayload)
112
112
  throw new Error("Chat SSE ended before a complete response");
113
+ assertModelOutputComplete(stopReason);
113
114
  const toolCalls = [...calls.entries()]
114
115
  .sort(([left], [right]) => left - right)
115
116
  .map(([, call]) => {
@@ -259,8 +260,6 @@ export async function aggregateAnthropicModelStream(body, onDelta) {
259
260
  if (block === undefined || block.stopped)
260
261
  throw new Error("Anthropic SSE content block stop is invalid");
261
262
  block.stopped = true;
262
- if (block.value.type === "tool_use" && block.partialJson !== "")
263
- block.value.input = parseStreamJson(block.partialJson);
264
263
  break;
265
264
  }
266
265
  case "message_delta": {
@@ -289,11 +288,14 @@ export async function aggregateAnthropicModelStream(body, onDelta) {
289
288
  }
290
289
  if (!started || !stopped)
291
290
  throw new Error("Anthropic SSE ended before a complete response");
291
+ assertModelOutputComplete(stopReason);
292
292
  const content = [...blocks.entries()]
293
293
  .sort(([left], [right]) => left - right)
294
294
  .map(([, block]) => {
295
295
  if (!block.stopped)
296
296
  throw new Error("Anthropic SSE content block did not end");
297
+ if (block.value.type === "tool_use" && block.partialJson !== "")
298
+ block.value.input = parseStreamJson(block.partialJson);
297
299
  return block.value;
298
300
  });
299
301
  const text = content
@@ -364,6 +366,12 @@ export async function aggregateResponsesModelStream(body, onDelta) {
364
366
  payload.type === "response.incomplete" ||
365
367
  payload.type === "response.cancelled" ||
366
368
  payload.type === "error") {
369
+ if (payload.type === "response.incomplete" &&
370
+ isRecord(payload.response)) {
371
+ const details = payload.response.incomplete_details;
372
+ if (isRecord(details))
373
+ assertModelOutputComplete(details.reason);
374
+ }
367
375
  throw new ModelHostFailureError("Responses SSE returned an explicit failure");
368
376
  }
369
377
  }
@@ -1,3 +1,5 @@
1
+ import { legacyModelBindingFailure, projectLegacyModelBindingFailure, } from "./LegacyModelBindingFailure.js";
2
+ import { isPlayFailureKind, } from "../../protocol/playFailure.js";
1
3
  import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
2
4
  import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
3
5
  import { createHash, randomUUID } from "node:crypto";
@@ -71,7 +73,8 @@ export class FileNativePlayTimelineStore {
71
73
  const eventCount = prefixLength(input.source.events, input.target.events);
72
74
  const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
73
75
  const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
74
- if (input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
76
+ if (input.retainCompleteContext ||
77
+ input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
75
78
  const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
76
79
  if (encoding !== null)
77
80
  await publishImmutableJson(join(targetRoot, "initial-request-encoding.json"), encoding);
@@ -207,6 +210,9 @@ export class FileNativePlayTimelineStore {
207
210
  continuationDigest,
208
211
  lastRequestAttempt: value.lastRequestAttempt,
209
212
  lastFailure: value.lastFailure,
213
+ ...(value.lastFailureKind === undefined
214
+ ? {}
215
+ : { lastFailureKind: value.lastFailureKind }),
210
216
  updatedAt: value.updatedAt,
211
217
  };
212
218
  await publishJson(join(root, "state.json"), state);
@@ -235,6 +241,20 @@ export class FileNativePlayTimelineStore {
235
241
  throw new Error("Play timeline head generation is inconsistent");
236
242
  return loaded;
237
243
  }
244
+ /** Read only the small active context index, never its transcript or bootstrap. */
245
+ async currentContextRequiresFreshContext(worldId) {
246
+ assertIdentity(worldId, "World ID");
247
+ const head = await this.#readTimelineHead(worldId);
248
+ if (head === null)
249
+ return false;
250
+ const index = await readJson(join(this.#contextRoot(worldId, head.chainId), "index.json"));
251
+ assertContextIndex(index);
252
+ if (index.worldId !== worldId ||
253
+ index.chainId !== head.chainId ||
254
+ index.timelineGeneration !== head.generation)
255
+ throw new Error("Play context durable identity is inconsistent");
256
+ return index.importedFromArchive === true;
257
+ }
238
258
  async readContext(worldId, chainId) {
239
259
  assertIdentity(worldId, "World ID");
240
260
  assertIdentity(chainId, "Call-chain ID");
@@ -277,6 +297,7 @@ export class FileNativePlayTimelineStore {
277
297
  chainId,
278
298
  previousChainId: indexValue.previousChainId,
279
299
  timelineGeneration: indexValue.timelineGeneration,
300
+ ...(indexValue.importedFromArchive ? { importedFromArchive: true } : {}),
280
301
  ...(baseValue.continuityContextId === undefined
281
302
  ? {}
282
303
  : { continuityContextId: baseValue.continuityContextId }),
@@ -327,6 +348,9 @@ export class FileNativePlayTimelineStore {
327
348
  lastRequest,
328
349
  lastRequestAttempt: stateValue.lastRequestAttempt,
329
350
  lastFailure: stateValue.lastFailure,
351
+ ...(stateValue.lastFailureKind === undefined
352
+ ? {}
353
+ : { lastFailureKind: stateValue.lastFailureKind }),
330
354
  updatedAt: stateValue.updatedAt,
331
355
  };
332
356
  return {
@@ -445,7 +469,10 @@ export class FileNativePlayTimelineStore {
445
469
  kind: "event",
446
470
  chainId: pointer.chainId,
447
471
  current: pointer.chainId === head?.chainId,
448
- event: summary,
472
+ ...(context.index.importedFromArchive
473
+ ? { requiresFreshContext: true }
474
+ : {}),
475
+ event: projectLegacyModelBindingFailure(summary),
449
476
  });
450
477
  pointer = { ...pointer, eventId: pointer.eventId - 1 };
451
478
  continue;
@@ -498,6 +525,9 @@ export class FileNativePlayTimelineStore {
498
525
  activeStatus: active?.state.status ?? null,
499
526
  activeCanRetry: active?.state.canRetry ?? false,
500
527
  activeLastFailure: active?.state.lastFailure ?? null,
528
+ ...(active?.state.lastFailureKind === undefined
529
+ ? {}
530
+ : { activeLastFailureKind: active.state.lastFailureKind }),
501
531
  items: reverse.reverse(),
502
532
  nextCursor: pointer.chainId === null && pointer.genesisIndex < 0
503
533
  ? null
@@ -516,7 +546,7 @@ export class FileNativePlayTimelineStore {
516
546
  assertPlayEvent(event);
517
547
  if (event.id !== eventId)
518
548
  throw new Error("Play timeline event identity is inconsistent");
519
- return normalizePlayEvent(event);
549
+ return projectLegacyModelBindingFailure(normalizePlayEvent(event));
520
550
  }
521
551
  newGeneration() {
522
552
  return `timeline:${randomUUID()}`;
@@ -597,11 +627,48 @@ export class FileNativePlayTimelineStore {
597
627
  indexValue.chainId !== chainId ||
598
628
  stateValue.chainId !== chainId)
599
629
  throw new Error("Play context durable identity is inconsistent");
630
+ // Old versions persisted a configuration preflight failure as a terminal
631
+ // context failure. Recover only the disposition proven by its event prefix.
632
+ // Archive contexts and genuine provider failures retain their restrictions.
633
+ if (!indexValue.importedFromArchive &&
634
+ stateValue.status === "interrupted" &&
635
+ stateValue.lastFailure === legacyModelBindingFailure &&
636
+ (stateValue.lastFailureKind === undefined ||
637
+ stateValue.lastFailureKind === "context_unavailable")) {
638
+ for (let id = stateValue.eventCount; id > 0; id -= 1) {
639
+ const event = await readJson(join(root, "events", numbered(id)));
640
+ assertPlayEvent(event);
641
+ if (event.kind === "followup" ||
642
+ (event.kind === "failure" &&
643
+ event.message === legacyModelBindingFailure))
644
+ continue;
645
+ if (event.kind === "assistant" &&
646
+ event.status === "completed" &&
647
+ event.responseKind === "narrative" &&
648
+ event.continuation === "available" &&
649
+ event.committedHead === stateValue.parentHead) {
650
+ stateValue.status = "ready";
651
+ stateValue.canRetry = false;
652
+ stateValue.lastFailure = null;
653
+ delete stateValue.lastFailureKind;
654
+ }
655
+ else if (event.kind === "failure") {
656
+ stateValue.canRetry = event.failureKind === "retryable";
657
+ stateValue.lastFailure = event.message;
658
+ if (event.failureKind === undefined)
659
+ delete stateValue.lastFailureKind;
660
+ else
661
+ stateValue.lastFailureKind = event.failureKind;
662
+ }
663
+ break;
664
+ }
665
+ }
600
666
  return { root, index: indexValue, state: stateValue };
601
667
  }
602
668
  }
603
669
  function contextIndex(value) {
604
670
  return {
671
+ ...(value.importedFromArchive ? { importedFromArchive: true } : {}),
605
672
  schemaVersion: 3,
606
673
  kind: "play_context_index",
607
674
  worldId: value.worldId,
@@ -724,6 +791,8 @@ function assertContextIndex(value) {
724
791
  if (!isRecord(value) ||
725
792
  value.schemaVersion !== 3 ||
726
793
  value.kind !== "play_context_index" ||
794
+ (value.importedFromArchive !== undefined &&
795
+ value.importedFromArchive !== true) ||
727
796
  typeof value.worldId !== "string" ||
728
797
  typeof value.chainId !== "string" ||
729
798
  (value.previousChainId !== null &&
@@ -769,6 +838,8 @@ function assertContextState(value) {
769
838
  !/^[a-f0-9]{64}$/u.test(value.continuationDigest) ||
770
839
  !validCount(value.lastRequestAttempt) ||
771
840
  (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
841
+ (value.lastFailureKind !== undefined &&
842
+ !isPlayFailureKind(value.lastFailureKind)) ||
772
843
  typeof value.updatedAt !== "number")
773
844
  throw new Error("Play context state has an invalid shape");
774
845
  }
@@ -839,6 +910,8 @@ function isReleasedPlayContext(value) {
839
910
  (value.lastRequest !== null && !isRecord(value.lastRequest)) ||
840
911
  !validCount(value.lastRequestAttempt) ||
841
912
  (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
913
+ (value.lastFailureKind !== undefined &&
914
+ !isPlayFailureKind(value.lastFailureKind)) ||
842
915
  typeof value.updatedAt !== "number")
843
916
  return false;
844
917
  try {
@@ -976,7 +1049,10 @@ function assertPlayEvent(value) {
976
1049
  return;
977
1050
  }
978
1051
  if ((value.kind !== "failure" && value.kind !== "cancellation") ||
979
- typeof value.message !== "string")
1052
+ typeof value.message !== "string" ||
1053
+ (value.kind === "failure" &&
1054
+ value.failureKind !== undefined &&
1055
+ !isPlayFailureKind(value.failureKind)))
980
1056
  throw new Error("Play timeline event has an invalid kind");
981
1057
  }
982
1058
  function assertTimelineEventSummary(value) {
@@ -0,0 +1,10 @@
1
+ export const legacyModelBindingFailure = "The selected model connection does not match the frozen model binding; start a fresh context.";
2
+ /** Presentation only: immutable historical events remain unchanged on disk. */
3
+ export function projectLegacyModelBindingFailure(event) {
4
+ if (event.kind === "failure" &&
5
+ event.message === legacyModelBindingFailure &&
6
+ (event.failureKind === undefined ||
7
+ event.failureKind === "context_unavailable"))
8
+ return { ...event, failureKind: "configuration_mismatch" };
9
+ return event;
10
+ }