narraeon 0.7.5 → 0.7.6

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.
@@ -2,10 +2,12 @@ import { isPackageFollowupControlPath } from "../content/PackageFollowups.js";
2
2
  import { renderWorldMaintenanceReport } from "../prompt/WorldMaintenanceReport.js";
3
3
  import { listWorldDocuments as listQueryableWorldDocuments } from "../prompt/WorldMaintenanceReport.js";
4
4
  import { defaultAppLocale } from "../../protocol/appPreferences.js";
5
- import { gameplayLabel, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
5
+ import { fictionGameplay, gameplayLabel, gameplayPath, readGameplay, } from "../../protocol/gameplay.js";
6
6
  import { isScalar, isSeq, parseDocument, stringify } from "yaml";
7
7
  import { tabletopSchemeReceipt } from "../play/TabletopRules.js";
8
8
  import { inspectContentPackageCurrentTree } from "../content/FileNativeContentTree.js";
9
+ import { coerceToolArguments, renderCoercionNote, } from "../model/ToolArgumentCoercion.js";
10
+ import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
9
11
  import { worldMaterialCoverage } from "../prompt/WorldMaterialCoverage.js";
10
12
  import { WorldDocumentStore, } from "../world/WorldDocumentStore.js";
11
13
  export const settingImprovementToolNames = [
@@ -434,11 +436,20 @@ export class SettingAuthoringTransaction {
434
436
  };
435
437
  }
436
438
  execute(calls) {
437
- const normalized = calls.map((call) => normalizeCall(call, this.#surface));
439
+ const normalized = calls.map((call) => normalizeCall(call, this.#surface, this.#locale));
438
440
  const results = [];
439
441
  const responseBase = this.files();
440
442
  let lastSuccessfulWrite = -1;
441
443
  for (const call of normalized) {
444
+ if (call.rejection !== undefined) {
445
+ results.push({
446
+ toolCallId: call.id,
447
+ markdown: `# Runtime tool rejected\n\n${call.rejection}`,
448
+ isError: true,
449
+ changes: [],
450
+ });
451
+ continue;
452
+ }
442
453
  const before = this.files();
443
454
  const unavailable = unavailableArgument(this.#surface, call.arguments, this.#locale);
444
455
  if (unavailable !== null) {
@@ -466,6 +477,20 @@ export class SettingAuthoringTransaction {
466
477
  // model only needs what this tool response changed.
467
478
  settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#surface, this.#locale)}`;
468
479
  }
480
+ normalized.forEach((call, index) => {
481
+ const notes = [
482
+ ...(call.coerced.length === 0
483
+ ? []
484
+ : [renderCoercionNote(call.coerced, this.#locale)]),
485
+ ...(call.alias === undefined
486
+ ? []
487
+ : [renderToolAliasNote(call.alias, call.name, this.#locale)]),
488
+ ];
489
+ if (notes.length === 0)
490
+ return;
491
+ const result = results[index];
492
+ result.markdown = `${result.markdown}\n\n${notes.join("\n\n")}`;
493
+ });
469
494
  return results;
470
495
  }
471
496
  #executeRead(call) {
@@ -758,14 +783,41 @@ class SettingAuthoringError extends Error {
758
783
  this.name = "SettingAuthoringError";
759
784
  }
760
785
  }
761
- function normalizeCall(call, surface) {
786
+ function normalizeCall(call, surface, locale) {
787
+ const cloned = isRecord(call.arguments)
788
+ ? structuredClone(call.arguments)
789
+ : {};
790
+ const definitions = authoringToolDefinitions(surface, locale, fictionGameplay);
791
+ const provided = definitions.map(({ name }) => name);
792
+ const resolved = resolveToolName(call.name, provided);
793
+ if (resolved.kind === "rejected")
794
+ return {
795
+ id: call.id,
796
+ name: call.name,
797
+ operation: null,
798
+ arguments: cloned,
799
+ coerced: [],
800
+ rejection: unknownToolMessage(call.name, provided, locale),
801
+ };
802
+ const name = resolved.name;
803
+ const schema = definitions.find((definition) => definition.name === name)?.inputSchema;
804
+ const repaired = schema === undefined
805
+ ? { arguments: cloned, coerced: [] }
806
+ : coerceToolArguments(schema, cloned);
762
807
  return {
763
808
  id: call.id,
764
- name: call.name,
765
- operation: authoringOperation(surface, call.name),
766
- arguments: isRecord(call.arguments) ? structuredClone(call.arguments) : {},
809
+ name,
810
+ operation: authoringOperation(surface, name),
811
+ arguments: repaired.arguments,
812
+ coerced: repaired.coerced,
813
+ ...(resolved.kind === "alias" ? { alias: resolved.declared } : {}),
767
814
  };
768
815
  }
816
+ function unknownToolMessage(name, provided, locale) {
817
+ return locale === "zh-CN"
818
+ ? `${name} 不是本界面提供的工具。可用工具:${provided.join(", ")}。`
819
+ : `${name} is not a tool on this surface. Available tools: ${provided.join(", ")}.`;
820
+ }
769
821
  function isReadOperation(operation) {
770
822
  return operation === "list" || operation === "search" || operation === "read";
771
823
  }
@@ -1474,6 +1526,8 @@ function requiredSettingRef(value, locale) {
1474
1526
  return value;
1475
1527
  }
1476
1528
  function requiredAliases(value) {
1529
+ if (typeof value === "string")
1530
+ throw new SettingAuthoringError("aliases must be a JSON array of at most 16 strings, not a string");
1477
1531
  if (!Array.isArray(value) ||
1478
1532
  value.length > 16 ||
1479
1533
  !value.every((item) => typeof item === "string" &&
@@ -1508,6 +1562,8 @@ function assertNoLiteralUnicodeEscapes(value, locale) {
1508
1562
  walk(value);
1509
1563
  }
1510
1564
  function requiredYamlLocatorPath(value, name) {
1565
+ if (typeof value === "string")
1566
+ throw new SettingAuthoringError(`${name} must be a JSON array of map keys or zero-based array indexes, not a string`);
1511
1567
  if (!Array.isArray(value) ||
1512
1568
  value.length === 0 ||
1513
1569
  !value.every((item) => (typeof item === "string" && item.length > 0) ||
@@ -16,6 +16,11 @@ export function isPromptGroup(value) {
16
16
  /** Soft limits for the turn guide; exceeding them warns without rejecting. */
17
17
  export const turnGuideLineSoftLimit = 120;
18
18
  export const turnGuideLinesSoftLimit = 10;
19
+ /** One turn-guide line: no line breaks, bounded length; empty clears a builtin default. */
20
+ const turnGuideLinePattern = /^[^\r\n\0]{0,1000}$/u;
21
+ export function isTurnGuideLine(value) {
22
+ return turnGuideLinePattern.test(value);
23
+ }
19
24
  /**
20
25
  * Unscoped prompts always apply. A scoped prompt applies only when the compiled
21
26
  * world or package is tabletop, so fiction requests never see it.
@@ -1,12 +1,37 @@
1
1
  import { turnGuideLineSoftLimit, turnGuideLinesSoftLimit, } from "./ordered-play-prompts.js";
2
+ /** The source of the player's one-turn request line. */
3
+ export const turnNoteSource = "player:turn";
2
4
  /**
3
5
  * The fixed Runtime header. It is mechanical: it tells the model what the
4
6
  * suffix is and that the prose, not a reply to the suffix, is wanted.
5
7
  */
6
8
  export function turnGuideHeader(locale) {
7
9
  return locale === "zh-CN"
8
- ? "[Runtime 本轮提示]\n以下是本轮写正文时最容易丢的要求,不要回应本提示,直接写正文。"
9
- : "[Runtime turn guide]\nThese are the requirements most easily lost while writing this turn's prose. Do not reply to this notice; write the prose.";
10
+ ? "[Runtime 本轮提示]\n以下是对这一轮正文的提示,来自预设、世界设定和玩家的临时要求。不要回应本提示,直接写正文。"
11
+ : "[Runtime turn guide]\nThese are notes for this turn's prose, from the preset, the world, and the player's requests for this turn. Do not reply to this notice; write the prose.";
12
+ }
13
+ /**
14
+ * The player's one-turn request as a turn-guide line. The prefix only names
15
+ * the source; it adds no constraint of its own.
16
+ */
17
+ export function turnNoteLine(locale, turnNote) {
18
+ return {
19
+ source: turnNoteSource,
20
+ text: locale === "zh-CN"
21
+ ? `玩家本轮要求:${turnNote}`
22
+ : `Player's request for this turn: ${turnNote}`,
23
+ };
24
+ }
25
+ /**
26
+ * Appends the player's request line to a compiled turn guide, creating the
27
+ * suffix when no preset or world line exists. Without a request the guide is
28
+ * returned unchanged.
29
+ */
30
+ export function turnGuideWithTurnNote(locale, guide, turnNote) {
31
+ if (turnNote === undefined)
32
+ return guide;
33
+ const lines = [...(guide?.lines ?? []), turnNoteLine(locale, turnNote)];
34
+ return { text: turnGuideText(locale, lines), lines };
10
35
  }
11
36
  /** The complete suffix text: header, then one line per enabled entry in list order. */
12
37
  export function turnGuideText(locale, lines) {