narraeon 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { ModelHostCancelledError } from "../model/ModelHost.js";
1
2
  import { errorDescription, } from "../model/AiFailureLog.js";
2
3
  /**
3
4
  * Run every follow-up declared by the frozen play preset exactly once after the
@@ -24,11 +25,15 @@ export async function runPlayFollowupRequests(input) {
24
25
  const completed = [];
25
26
  let failure;
26
27
  for (const followup of input.followups) {
27
- if (input.signal?.aborted === true) {
28
+ if (signalWasAborted(input.signal)) {
28
29
  failure ??= "The follow-up request was cancelled.";
29
30
  break;
30
31
  }
31
32
  const outcome = await runOne(input, followup, prefix);
33
+ if (signalWasAborted(input.signal)) {
34
+ failure ??= "The follow-up request was cancelled.";
35
+ break;
36
+ }
32
37
  outcomes.push(outcome);
33
38
  input.observer?.onOutcome?.(outcome);
34
39
  if (outcome.failure === undefined)
@@ -58,27 +63,39 @@ async function runOne(input, followup, prefix) {
58
63
  let responseDiagnostics;
59
64
  try {
60
65
  await input.artifacts.beginRequestAttempt(requestContext);
61
- const response = await input.modelHost.exchange({
62
- bootstrap: structuredClone(input.bootstrap),
63
- tools: structuredClone(followup.tools),
64
- toolUniverse: structuredClone(followup.tools),
65
- allowedTools: [...followup.allowedTools],
66
- toolStrategy: input.toolStrategy,
67
- // The prompt replaces the previous followup's prompt instead of being
68
- // appended after it: every request sees exactly the main-chain prefix.
69
- appended: [
70
- ...structuredClone(prefix),
71
- {
72
- kind: "prompt_delta",
73
- logicalMessages: structuredClone(followup.logicalMessages),
74
- },
75
- ],
76
- requestId: followup.id,
77
- operationId: input.context.operationId,
78
- requestAttempt: 1,
79
- exchange: 1,
80
- maxOutputTokens: input.maxOutputTokens,
81
- });
66
+ input.observer?.onProviderDispatch?.();
67
+ let response;
68
+ try {
69
+ response = await input.modelHost.exchange({
70
+ bootstrap: structuredClone(input.bootstrap),
71
+ tools: structuredClone(followup.tools),
72
+ toolUniverse: structuredClone(followup.tools),
73
+ allowedTools: [...followup.allowedTools],
74
+ toolStrategy: input.toolStrategy,
75
+ // The prompt replaces the previous followup's prompt instead of being
76
+ // appended after it: every request sees exactly the main-chain prefix.
77
+ appended: [
78
+ ...structuredClone(prefix),
79
+ {
80
+ kind: "prompt_delta",
81
+ logicalMessages: structuredClone(followup.logicalMessages),
82
+ },
83
+ ],
84
+ requestId: followup.id,
85
+ operationId: input.context.operationId,
86
+ requestAttempt: 1,
87
+ exchange: 1,
88
+ maxOutputTokens: input.maxOutputTokens,
89
+ }, {
90
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
91
+ onDelta: (delta) => input.observer?.onProviderDelta?.(delta),
92
+ });
93
+ if (input.signal?.aborted === true)
94
+ throw new ModelHostCancelledError("The player cancelled the follow-up model request; its response was not committed.");
95
+ }
96
+ finally {
97
+ input.observer?.onProviderSettled?.();
98
+ }
82
99
  responseDiagnostics = response.diagnostics;
83
100
  if (response.diagnostics !== undefined)
84
101
  await input.failureLog?.recordExchangeIfActive(response.diagnostics);
@@ -153,6 +170,9 @@ async function runOne(input, followup, prefix) {
153
170
  }
154
171
  return outcome;
155
172
  }
173
+ function signalWasAborted(signal) {
174
+ return signal?.aborted === true;
175
+ }
156
176
  async function executeArtifactCall(input, context, call) {
157
177
  if (call.name !== "artifact_emit" && call.name !== "artifact_clear")
158
178
  return {
@@ -333,7 +333,7 @@ function withoutAppendedContextGenesis(input) {
333
333
  if (input.playerInputPlacement !== "append")
334
334
  return input;
335
335
  const additionalMaterials = input.world.additionalMaterials.filter((material) => material.kind !== "history_message" ||
336
- !material.message.endsWith(".message.genesis.narrator"));
336
+ !material.message.endsWith("message.genesis.narrator"));
337
337
  if (additionalMaterials.length === input.world.additionalMaterials.length)
338
338
  return input;
339
339
  return {
@@ -886,7 +886,7 @@ function resolveRecentHistory(slot, history, selected, coverage, locale) {
886
886
  // in commit order. Its keys are semantic message IDs, not sortable file
887
887
  // names: lexical sorting would put `message.genesis` after every numbered
888
888
  // message and `message.9` after `message.15`.
889
- const ordered = Object.entries(history).filter(([ref]) => !ref.endsWith(".message.genesis.narrator"));
889
+ const ordered = Object.entries(history).filter(([ref]) => !ref.endsWith("message.genesis.narrator"));
890
890
  const chosen = ordered.slice(-recent);
891
891
  if (chosen.length === 0) {
892
892
  selected.push({
@@ -975,11 +975,11 @@ function renderHistoryMessage(ref, text, locale) {
975
975
  return `## ${historyMessageLabel(ref, locale)}\n\n${text.trim()}`;
976
976
  }
977
977
  function historyMessageLabel(ref, locale) {
978
- if (ref.endsWith(".message.genesis.narrator"))
978
+ if (ref.endsWith("message.genesis.narrator"))
979
979
  return locale === "zh-CN" ? "开场白" : "Opening";
980
- if (/(?:\.message\.[^.]+(?:\.[0-9]+)?\.|-)(player)(?:-|$)/u.test(ref))
980
+ if (/(?:^|\.)message\.[^.]+(?:\.[0-9]+)?\.player$|(?:^|-)player(?:-|$)/u.test(ref))
981
981
  return locale === "zh-CN" ? "玩家原文" : "Player input";
982
- if (/(?:\.message\.[^.]+(?:\.[0-9]+)?\.|-)(narrator)(?:-|$)/u.test(ref))
982
+ if (/(?:^|\.)message\.[^.]+(?:\.[0-9]+)?\.narrator$|(?:^|-)narrator(?:-|$)/u.test(ref))
983
983
  return locale === "zh-CN" ? "主持叙事" : "Host narrative";
984
984
  return locale === "zh-CN" ? "已提交消息" : "Committed message";
985
985
  }