narraeon 0.7.6 → 0.7.9
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/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +14 -1
- package/dist/node/runtime/history/HistoryRendering.js +25 -6
- package/dist/node/runtime/history/HistoryTools.js +31 -0
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +12 -4
- package/dist/node/runtime/play/PlayCallChain.js +10 -21
- package/dist/node/runtime/play/PlayDocumentTools.js +72 -43
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +35 -19
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -12
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +0 -67
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +326 -62
- package/dist/node/runtime/setting/SettingImprovementSession.js +40 -3
- package/dist/node/runtime/world/FileNativeWorldStore.js +13 -3
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +9 -2
- package/dist/node/runtime/world/WorldDocumentStore.js +441 -389
- package/dist/node/runtime/world/WorldLedgerAuthority.js +18 -2
- package/dist/node/runtime/world/WorldStateRevision.js +6 -3
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +61 -17
- package/dist/node/runtime/world-revision/LegacyWorldRevisionEpochUpgrade.js +46 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +83 -30
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +64 -4
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +32 -11
- package/dist/node/shared/default-setting-improvement-prompt.js +23 -8
- package/dist/node/shared/ordered-author-prompts.js +9 -7
- package/dist/node/shared/ordered-play-prompts.js +2 -2
- package/dist/web/assets/index-B2kbaOGe.js +1408 -0
- package/dist/web/assets/{index-BjxBYYQi.css → index-DH2BVsYd.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Cj2JYCia.js +0 -1408
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { isPlayPresetBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
2
|
+
export function isAuthoringPreloadRecord(value) {
|
|
3
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
4
|
+
return false;
|
|
5
|
+
const record = value;
|
|
6
|
+
return (Object.keys(record).length === 3 &&
|
|
7
|
+
typeof record.revision === "string" &&
|
|
8
|
+
Array.isArray(record.worldDocumentIds) &&
|
|
9
|
+
record.worldDocumentIds.every((id) => typeof id === "string") &&
|
|
10
|
+
Array.isArray(record.opaquePaths) &&
|
|
11
|
+
record.opaquePaths.every((path) => typeof path === "string"));
|
|
12
|
+
}
|
|
2
13
|
export function validAuthoringRequests(value, validPrompt, session) {
|
|
3
14
|
if (!Array.isArray(value) ||
|
|
4
15
|
value.length === 0 ||
|
|
@@ -14,7 +25,9 @@ export function validAuthoringRequests(value, validPrompt, session) {
|
|
|
14
25
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
15
26
|
return false;
|
|
16
27
|
const item = raw;
|
|
17
|
-
|
|
28
|
+
const keys = Object.keys(item).length;
|
|
29
|
+
if ((keys !== 4 && !(keys === 5 && "preload" in item)) ||
|
|
30
|
+
(item.preload !== undefined && !isAuthoringPreloadRecord(item.preload)) ||
|
|
18
31
|
typeof item.requestId !== "string" ||
|
|
19
32
|
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(item.requestId) ||
|
|
20
33
|
ids.has(item.requestId) ||
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { parseDocument } from "yaml";
|
|
3
|
+
/** Scope and total only; ordering and continuation rules live in tool descriptions. */
|
|
4
|
+
function compactHeader(total, locale) {
|
|
5
|
+
const zh = locale === "zh-CN";
|
|
6
|
+
return `Scope: ${zh ? "当前时间线" : "current timeline"}\n${zh ? "本次读取快照消息总数" : "Snapshot total messages"}: ${total}`;
|
|
7
|
+
}
|
|
8
|
+
/** Position, role and ref only; role=x would repeat the localized role label. */
|
|
9
|
+
function compactMessageLabel(message, locale) {
|
|
10
|
+
const zh = locale === "zh-CN";
|
|
11
|
+
return `${zh ? "位置" : "position"} ${message.position} · ${historyRoleLabel(message, locale)}${message.isOpening ? " · isOpening=true" : ""} · ${message.ref} · ${message.length} Unicode code points`;
|
|
12
|
+
}
|
|
3
13
|
/** Shared metadata keeps injected refs and tool results directly navigable. */
|
|
4
14
|
export function historyMessageLabel(message, locale) {
|
|
5
15
|
const zh = locale === "zh-CN";
|
|
@@ -58,18 +68,26 @@ export function renderHistoryResult(result, locale = "en", legacyRead = false) {
|
|
|
58
68
|
if (result.kind === "list")
|
|
59
69
|
return {
|
|
60
70
|
ok: true,
|
|
61
|
-
markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${
|
|
71
|
+
markdown: `# ${zh ? "已提交历史列表" : "Committed history listing"}\n\n${compactHeader(result.total, locale)}\n${result.messages
|
|
62
72
|
.map((message) => {
|
|
63
73
|
const preview = Array.from(message.text).slice(0, 120).join("");
|
|
64
|
-
|
|
74
|
+
const omitted = message.length > 120
|
|
75
|
+
? `\n${zh ? "…(预览省略)" : "…(preview omitted)"}`
|
|
76
|
+
: "";
|
|
77
|
+
return `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}\n${preview}${omitted}`;
|
|
65
78
|
})
|
|
66
|
-
.join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}
|
|
79
|
+
.join("\n\n") || (zh ? "(空页)" : "(empty page)")}\n\n---\n${zh ? "本页位置/条数" : "Page positions / count"}: ${range(result.messages)} / ${result.messages.length}\n${cursorLine("earlierCursor", result.earlierCursor)}\n${cursorLine("laterCursor", result.laterCursor)}${result.messages.length === 0 && result.anchor !== null ? `\n${zh ? "可从锚点转向浏览" : "Browse from the anchor in the other direction"}: {before:"${result.anchor}"} / {after:"${result.anchor}"}` : ""}`,
|
|
67
80
|
};
|
|
68
81
|
if (result.kind === "read")
|
|
69
82
|
return {
|
|
70
83
|
ok: true,
|
|
71
84
|
markdown: `# ${zh ? "完整历史原文" : "Complete history originals"}\n\n${header}\nMode: ${result.mode}${result.center === null ? "" : `\nCenter: ${result.center}`}\nOrder: oldest_first\n${zh ? "实际位置" : "Actual positions"}: ${range(result.messages)}\n${zh ? "每条正文完整,无摘要、无截断" : "Each body is complete, without summary or truncation"}\n\n${result.messages.map((message) => renderHistoryMessage(message, locale)).join("\n\n") || (zh ? "(历史为空)" : "(empty history)")}\n\n---\n${cursorLine("earlier", result.earlier)}\n${cursorLine("later", result.later)}\n${result.earlier === null ? (zh ? "已到历史开头。" : "At the history beginning.") : legacyRead ? `context_read {ref:"${result.earlier}"}` : zh ? "向前不重复读取:history_read {ref:earlier,before:N}。" : "Read earlier without repeating this window: history_read {ref:earlier,before:N}."}\n${result.later === null ? (zh ? "已到历史结尾。" : "At the history end.") : legacyRead ? `context_read {ref:"${result.later}"}` : zh ? "向后不重复读取:history_read {ref:later,after:N}。" : "Read later without repeating this window: history_read {ref:later,after:N}."}\n${legacyRead ? (zh ? "按需逐条读取相邻原文。" : "Read adjacent originals one at a time as needed.") : zh ? "按需小步扩展;21 条消息不代表固定输出体积。" : "Expand in small windows as needed; 21 messages do not imply a fixed output size."}`,
|
|
72
85
|
};
|
|
86
|
+
if (!result.legacy)
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${compactHeader(result.total, locale)}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${compactMessageLabel(message, locale)}${renderHistoryBackground(message.background, locale, false)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}`).join("\n")}${omittedMatches ? `\n${zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify."}` : ""}`).join("\n\n") || (zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine("nextCursor", result.nextCursor)}`,
|
|
90
|
+
};
|
|
73
91
|
return {
|
|
74
92
|
ok: true,
|
|
75
93
|
markdown: `# ${zh ? "历史字面搜索" : "History literal search"}\n\n${header}${result.within === null ? "" : `\nLegacy selected scope: ${result.within}`}\n${zh ? "搜索位置范围(排除边界)" : "Search position range (boundaries excluded)"}: ${result.range.start === null ? "empty" : `${result.range.start}..${result.range.end}`}\nOrder: ${result.order}\nMatch: ${result.match} (${zh ? "同一消息内" : "within one message"})\nNormalization: NFKC${result.caseSensitive ? " (case sensitive)" : " + case folding (locale und)"}\n${zh ? "查询词" : "Queries"}: ${JSON.stringify(result.queries)}\n${zh ? "命中消息总数" : "Total matching messages"}: ${result.matches}\n\n${result.messages.map(({ message, hits, fragments, omittedMatches }) => `### ${historyMessageLabel(message, locale)}\n${message.length} Unicode code points\n${renderHistoryBackground(message.background, locale)}${zh ? "逐词命中次数" : "Matches per query"}: ${hits.map(({ query, count }) => `${JSON.stringify(query)}: ${count}`).join("; ")}\n${fragments.map((fragment) => `[${zh ? "部分原文片段" : "Partial original fragment"}: ${fragment.start}..${fragment.end}${fragment.omittedBefore ? "; prefix omitted" : ""}${fragment.omittedAfter ? "; suffix omitted" : ""}]\n${fragment.text}\n[${zh ? "片段结束" : "fragment ends"}]`).join("\n")}\n${omittedMatches ? (zh ? "另有命中未展示;可完整读取核实。" : "Additional matches omitted; read the complete original to verify.") : zh ? "全部命中均在所示片段内。" : "All matches are covered by these fragments."}`).join("\n\n") || (result.legacy ? (zh ? "零字面命中不证明事件没有发生;可改用其他 query 或读取相邻原文。" : "Zero literal matches do not prove an event never happened. Try another query or read adjacent originals.") : zh ? "零字面命中不证明事件没有发生;可改词、从 all 放宽为 any 或读取相邻上下文。" : "Zero literal matches do not prove an event never happened. Try other words, relax all to any, or read nearby context.")}\n\n---\n${cursorLine(result.legacy ? "Next-page cursor" : "nextCursor", result.nextCursor)}\n${result.legacy ? (zh ? "续页保留原source=history、query、caseSensitive、within与limit并使用返回cursor。" : "Legacy continuation keeps the original source=history, query, caseSensitive, within and limit with the returned cursor.") : zh ? "续页只传 {cursor};已有原文足以判断时停止检索。all 仅限同一消息;相邻消息可改用 any 后读前后文。" : "Continue with only {cursor}; stop when the originals suffice. all applies to one message; use any then read context for terms in adjacent messages."}`,
|
|
@@ -87,7 +105,7 @@ function localizedRecovery(result, zh) {
|
|
|
87
105
|
return `参数不符合要求,请按工具定义修正。${result.recovery}`;
|
|
88
106
|
}
|
|
89
107
|
/** Every result carries its own annotation; no implicit neighboring inheritance. */
|
|
90
|
-
export function renderHistoryBackground(background, locale) {
|
|
108
|
+
export function renderHistoryBackground(background, locale, withNotice = true) {
|
|
91
109
|
if (background === undefined)
|
|
92
110
|
return "";
|
|
93
111
|
const zh = locale === "zh-CN";
|
|
@@ -99,10 +117,11 @@ export function renderHistoryBackground(background, locale) {
|
|
|
99
117
|
const notice = zh
|
|
100
118
|
? "历史背景仅说明采集位置的状态,不是当前世界状态;回忆、梦境或段内局部时间结合原文解释,不认证事实或授予角色知识。"
|
|
101
119
|
: "Historical background describes the captured state, not current world state; interpret memories, dreams and local times with the original text. It does not certify facts or grant character knowledge.";
|
|
120
|
+
const suffix = withNotice ? `\n${notice}` : "";
|
|
102
121
|
if (background.kind === "snapshot" ||
|
|
103
122
|
equalBackground(background.before, background.after))
|
|
104
|
-
return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}
|
|
105
|
-
return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}
|
|
123
|
+
return `\n${zh ? "历史背景" : "Historical background"}:${node(background.kind === "snapshot" ? background.value : background.before)}${suffix}\n`;
|
|
124
|
+
return `\n${zh ? "推进前背景" : "Background before advancement"}:${node(background.before)}\n${zh ? "推进后背景" : "Background after advancement"}:${node(background.after)}${suffix}\n`;
|
|
106
125
|
}
|
|
107
126
|
function equalBackground(before, after) {
|
|
108
127
|
if (before === after)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { HistoryQuery, historyInputs } from "./HistoryQuery.js";
|
|
2
|
+
import { renderHistoryResult } from "./HistoryRendering.js";
|
|
3
|
+
export const historyToolNames = [
|
|
4
|
+
"history_list",
|
|
5
|
+
"history_search",
|
|
6
|
+
"history_read",
|
|
7
|
+
];
|
|
8
|
+
export function isHistoryToolName(name) {
|
|
9
|
+
return historyToolNames.includes(name);
|
|
10
|
+
}
|
|
11
|
+
export function historyEntries(history, backgrounds = {}) {
|
|
12
|
+
return Object.entries(history).map(([path, contents]) => ({
|
|
13
|
+
path,
|
|
14
|
+
contents,
|
|
15
|
+
...(backgrounds[path] === undefined
|
|
16
|
+
? {}
|
|
17
|
+
: { background: structuredClone(backgrounds[path]) }),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
/** Read-only committed-history navigation shared by play and world revision. */
|
|
21
|
+
export function createHistoryQuery(scope, entries) {
|
|
22
|
+
return new HistoryQuery(scope, historyInputs(entries));
|
|
23
|
+
}
|
|
24
|
+
export function runHistoryTool(query, call, locale) {
|
|
25
|
+
const result = call.name === "history_search"
|
|
26
|
+
? query.search(call.arguments)
|
|
27
|
+
: call.name === "history_read"
|
|
28
|
+
? query.read(call.arguments)
|
|
29
|
+
: query.list(call.arguments);
|
|
30
|
+
return renderHistoryResult(result, locale);
|
|
31
|
+
}
|
|
@@ -4,7 +4,7 @@ import { FileNativePromptCompiler, } from "../prompt/FileNativePromptCompiler.js
|
|
|
4
4
|
import { validateMaterialList } from "../prompt/MaterialSelection.js";
|
|
5
5
|
import { FileNativeWorldCreationError, } from "../world/FileNativeWorldStore.js";
|
|
6
6
|
import { WorldOperationBusyError, } from "../world/WorldOperationCoordinator.js";
|
|
7
|
-
import { beginWorldStateRevision, fileNativeStateChanges,
|
|
7
|
+
import { beginWorldStateRevision, fileNativeStateChanges, acceptWorldStateRevision, worldDocumentRevisionFailureMessage, worldStateRevisionChanges, } from "../world/WorldStateRevision.js";
|
|
8
8
|
export class FileNativeContinuityCorrection {
|
|
9
9
|
#worlds;
|
|
10
10
|
#compiler;
|
|
@@ -260,9 +260,17 @@ export class FileNativeContinuityCorrection {
|
|
|
260
260
|
return document;
|
|
261
261
|
}
|
|
262
262
|
#reviseDocument(candidate, command) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
// A correction applies all of its edits or none of them, so a skipped edit
|
|
264
|
+
// is a failure here even though the revision itself settled.
|
|
265
|
+
const revised = candidate.snapshot.revise({ commands: [command] });
|
|
266
|
+
const failures = revised.ok
|
|
267
|
+
? revised.editOutcomes.flatMap(({ status, diagnostic }) => status === "failed" && diagnostic !== undefined ? [diagnostic] : [])
|
|
268
|
+
: revised.diagnostics;
|
|
269
|
+
if (!revised.ok ||
|
|
270
|
+
revised.snapshotStatus !== "usable" ||
|
|
271
|
+
failures.length > 0)
|
|
272
|
+
throw new Error(worldDocumentRevisionFailureMessage(failures));
|
|
273
|
+
acceptWorldStateRevision(candidate, revised);
|
|
266
274
|
candidate.reads.clear();
|
|
267
275
|
}
|
|
268
276
|
#advance(candidate) {
|
|
@@ -2,6 +2,7 @@ import { projectLegacyModelBindingFailure } from "./LegacyModelBindingFailure.js
|
|
|
2
2
|
import { dispatchFailureEnding, emptyResponseEnding, lastEndingCancelled, noContinuationEnding, PlayAdvanceLifecycle, settlementFailureEnding, } from "./PlayAdvanceLifecycle.js";
|
|
3
3
|
import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTabletop, seededRandom, } from "./TabletopResolution.js";
|
|
4
4
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
5
|
+
import { historyEntries } from "../history/HistoryTools.js";
|
|
5
6
|
import { isDeepStrictEqual } from "node:util";
|
|
6
7
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
7
8
|
import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
|
|
@@ -13,7 +14,6 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
13
14
|
import { isTurnGuideLine } from "../../shared/ordered-play-prompts.js";
|
|
14
15
|
import { turnGuideWithTurnNote } from "../../shared/turn-guide.js";
|
|
15
16
|
import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, turnGuideNotice, } from "./PlayContinuity.js";
|
|
16
|
-
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
17
17
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
18
18
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
19
19
|
import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
|
|
@@ -1329,7 +1329,7 @@ export class PlayCallChain {
|
|
|
1329
1329
|
],
|
|
1330
1330
|
});
|
|
1331
1331
|
worldClock = readDeclaredWorldClock(snapshot);
|
|
1332
|
-
const {
|
|
1332
|
+
const { maintenance } = this.#compiler.inspectWorldMaterials({
|
|
1333
1333
|
controlFingerprint: "receipt",
|
|
1334
1334
|
documentSnapshot: snapshot,
|
|
1335
1335
|
history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
@@ -1337,23 +1337,18 @@ export class PlayCallChain {
|
|
|
1337
1337
|
});
|
|
1338
1338
|
for (const { result } of writes) {
|
|
1339
1339
|
const write = result.candidateWrite;
|
|
1340
|
-
write.freshContextCoverage = renderFreshContextCoverage(snapshot, coverage, write.shortRef, this.#compiler.locale);
|
|
1341
1340
|
const previous = session.documents.committedSnapshot.query({
|
|
1342
1341
|
kind: "read_document",
|
|
1343
1342
|
document: { shortRef: write.shortRef },
|
|
1344
1343
|
});
|
|
1345
|
-
|
|
1344
|
+
write.sizeAdvice = renderWriteSizeAdvice(maintenance, write.shortRef, previous.kind === "read_document"
|
|
1346
1345
|
? Buffer.byteLength(previous.body, "utf8")
|
|
1347
1346
|
: null, this.#compiler.locale);
|
|
1348
|
-
if (advice.length > 0)
|
|
1349
|
-
write.freshContextCoverage += `\n${advice}`;
|
|
1350
1347
|
}
|
|
1351
1348
|
}
|
|
1352
|
-
catch
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
for (const { result } of writes)
|
|
1356
|
-
result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
|
|
1349
|
+
catch {
|
|
1350
|
+
// Maintenance advice is diagnostic: a failure omits it and never
|
|
1351
|
+
// rejects or undoes an accepted write.
|
|
1357
1352
|
}
|
|
1358
1353
|
}
|
|
1359
1354
|
const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
|
|
@@ -2015,7 +2010,7 @@ function finalizeWriteReceipts(settlement) {
|
|
|
2015
2010
|
const status = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
|
|
2016
2011
|
? `@${candidateWrite.shortRef} write succeeded`
|
|
2017
2012
|
: `@${candidateWrite.shortRef} unchanged`;
|
|
2018
|
-
const markdown = [status, candidateWrite.
|
|
2013
|
+
const markdown = [status, candidateWrite.sizeAdvice]
|
|
2019
2014
|
.filter(Boolean)
|
|
2020
2015
|
.join("\n");
|
|
2021
2016
|
completed.result.markdown = markdown;
|
|
@@ -2169,6 +2164,9 @@ function duplicatePlayerExchange(session, exchangeId, playerText, context) {
|
|
|
2169
2164
|
}
|
|
2170
2165
|
function mergeChangedDocuments(session, changes) {
|
|
2171
2166
|
for (const change of changes) {
|
|
2167
|
+
// Play and continuity writes never remove a document; only world-revision apply does.
|
|
2168
|
+
if (change.kind === "delete")
|
|
2169
|
+
continue;
|
|
2172
2170
|
const projected = {
|
|
2173
2171
|
kind: change.kind,
|
|
2174
2172
|
ref: `@${change.stableShortRef}`,
|
|
@@ -2352,15 +2350,6 @@ function revisionTimelineGeneration(sourceGeneration, operationId, worldId, cont
|
|
|
2352
2350
|
.digest("hex")
|
|
2353
2351
|
.slice(0, 40)}`;
|
|
2354
2352
|
}
|
|
2355
|
-
function historyEntries(history, backgrounds = {}) {
|
|
2356
|
-
return Object.entries(history).map(([path, contents]) => ({
|
|
2357
|
-
path,
|
|
2358
|
-
contents,
|
|
2359
|
-
...(backgrounds[path] === undefined
|
|
2360
|
-
? {}
|
|
2361
|
-
: { background: structuredClone(backgrounds[path]) }),
|
|
2362
|
-
}));
|
|
2363
|
-
}
|
|
2364
2353
|
function operationId(chainId, key) {
|
|
2365
2354
|
return `play-${createHash("sha256")
|
|
2366
2355
|
.update(`${chainId}\0${key}`)
|
|
@@ -8,7 +8,7 @@ import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
|
8
8
|
import { coerceToolArguments, renderCoercionNote, } from "../model/ToolArgumentCoercion.js";
|
|
9
9
|
import { runtimeToolInputSchema } from "../prompt/FileNativeToolRegistry.js";
|
|
10
10
|
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
11
|
-
import {} from "../world/WorldDocumentStore.js";
|
|
11
|
+
import { formatWorldDocumentLocator, } from "../world/WorldDocumentStore.js";
|
|
12
12
|
import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
|
|
13
13
|
class PlayDocumentToolFailure extends Error {
|
|
14
14
|
failureKind;
|
|
@@ -274,10 +274,12 @@ function applyPlayCandidatePatch(candidate, reads, args) {
|
|
|
274
274
|
},
|
|
275
275
|
],
|
|
276
276
|
});
|
|
277
|
+
if (!revised.ok && revised.editOutcomes.length > 0)
|
|
278
|
+
throw new PlayDocumentToolFailure(`world_patch applied none of its edits to @${ref}. ${formatEditNotes(revised.editOutcomes)}`, "candidate");
|
|
277
279
|
if (!revised.ok || revised.snapshotStatus !== "usable")
|
|
278
280
|
throw new PlayDocumentToolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
|
|
279
281
|
const changed = revised.changes.length > 0;
|
|
280
|
-
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
|
|
282
|
+
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes, revised.editOutcomes);
|
|
281
283
|
const previousSnapshot = candidate.snapshot;
|
|
282
284
|
acceptWorldStateRevision(candidate, revised);
|
|
283
285
|
carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
|
|
@@ -353,12 +355,41 @@ function authorizationsAfterEdits(authorization, edits) {
|
|
|
353
355
|
}
|
|
354
356
|
return authorization;
|
|
355
357
|
}
|
|
356
|
-
function formatToolRevisionReceipt(shortRef, changes) {
|
|
358
|
+
function formatToolRevisionReceipt(shortRef, changes, outcomes) {
|
|
357
359
|
if (changes.length > 1)
|
|
358
360
|
throw new Error("A world_patch revision must affect only its target document");
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
const failed = outcomes.filter(({ status }) => status === "failed").length;
|
|
362
|
+
const parts = [
|
|
363
|
+
changes.length === 1
|
|
364
|
+
? `@${shortRef} pending write`
|
|
365
|
+
: `@${shortRef} unchanged`,
|
|
366
|
+
];
|
|
367
|
+
if (failed > 0)
|
|
368
|
+
parts.push(`${outcomes.length - failed}/${outcomes.length} edits applied`);
|
|
369
|
+
else if (outcomes.some(({ status }) => status === "no_op"))
|
|
370
|
+
parts.push(`${outcomes.length} edits applied`);
|
|
371
|
+
const notes = formatEditNotes(outcomes);
|
|
372
|
+
if (notes !== "")
|
|
373
|
+
parts.push(notes);
|
|
374
|
+
if (failed > 0)
|
|
375
|
+
parts.push("applied edits are not resent; send again only the edits listed above");
|
|
376
|
+
return parts.join(" · ");
|
|
377
|
+
}
|
|
378
|
+
/** Lists only the edits that did not apply cleanly: failed ones and no-ops. */
|
|
379
|
+
function formatEditNotes(outcomes) {
|
|
380
|
+
return outcomes
|
|
381
|
+
.flatMap(({ editIndex, status, diagnostic }) => {
|
|
382
|
+
if (status === "applied" || diagnostic === undefined)
|
|
383
|
+
return [];
|
|
384
|
+
const label = `edit #${editIndex + 1}`;
|
|
385
|
+
if (status === "no_op")
|
|
386
|
+
return [`${label} no-op: ${diagnostic.message}`];
|
|
387
|
+
const location = diagnostic.locator
|
|
388
|
+
? ` (${formatWorldDocumentLocator(diagnostic.locator)})`
|
|
389
|
+
: "";
|
|
390
|
+
return [`${label}${location}: ${diagnostic.message}`];
|
|
391
|
+
})
|
|
392
|
+
.join(" · ");
|
|
362
393
|
}
|
|
363
394
|
function applyPlayCandidateRetirement(candidate, reads, args) {
|
|
364
395
|
if (!record(args) ||
|
|
@@ -640,15 +671,15 @@ function knownStateDirectory(snapshot, directory, declaredDirectories) {
|
|
|
640
671
|
return snapshot.files.some(({ path }) => path.startsWith(logicalPrefix));
|
|
641
672
|
}
|
|
642
673
|
function executeStateList(snapshot, args, declaredDirectories) {
|
|
674
|
+
// Frozen sessions may still send obsolete cursor/limit; directories are
|
|
675
|
+
// listed in full, so those keys are accepted and ignored.
|
|
643
676
|
if (!record(args) ||
|
|
644
677
|
!hasOnlyToolKeys(args, ["parent", "cursor", "limit"]) ||
|
|
645
678
|
typeof args.parent !== "string" ||
|
|
646
|
-
parseStateDirectoryHandle(args.parent) === null
|
|
647
|
-
(args.limit !== undefined && typeof args.limit !== "number") ||
|
|
648
|
-
!validOptionalCursor(args.cursor))
|
|
679
|
+
parseStateDirectoryHandle(args.parent) === null)
|
|
649
680
|
return {
|
|
650
681
|
ok: false,
|
|
651
|
-
markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime
|
|
682
|
+
markdown: "# Runtime argument error\n\nstate_list requires an @dir-* parent returned by Runtime; use history_list for committed history.",
|
|
652
683
|
};
|
|
653
684
|
return executeContextList(snapshot, { ...args, source: "state" }, declaredDirectories);
|
|
654
685
|
}
|
|
@@ -668,19 +699,11 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
|
|
|
668
699
|
(args.order !== "newest_first" && args.order !== "oldest_first"));
|
|
669
700
|
if (!hasOnlyToolKeys(args, allowedKeys) ||
|
|
670
701
|
invalidStateShape ||
|
|
671
|
-
invalidHistoryShape
|
|
672
|
-
(args.limit !== undefined && typeof args.limit !== "number") ||
|
|
673
|
-
!validOptionalCursor(args.cursor))
|
|
702
|
+
invalidHistoryShape)
|
|
674
703
|
return {
|
|
675
704
|
ok: false,
|
|
676
705
|
markdown: "# Runtime argument error\n\nstate must use an @dir-* parent returned by the Runtime and omit order; history must use newest_first or oldest_first order and omit parent.",
|
|
677
706
|
};
|
|
678
|
-
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
679
|
-
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
680
|
-
return {
|
|
681
|
-
ok: false,
|
|
682
|
-
markdown: "# Runtime argument error\n\nlimit must be between 1 and 100.",
|
|
683
|
-
};
|
|
684
707
|
if (args.source === "state") {
|
|
685
708
|
const parent = args.parent;
|
|
686
709
|
const directory = parseStateDirectoryHandle(parent);
|
|
@@ -691,32 +714,38 @@ function executeContextList(snapshot, args, declaredDirectories = []) {
|
|
|
691
714
|
ok: false,
|
|
692
715
|
markdown: "# Runtime argument error\n\nThe parent is not a Runtime-known state directory; call state_list and descend only through its returned directory handles.",
|
|
693
716
|
};
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
+
const entries = [];
|
|
718
|
+
let complete = true;
|
|
719
|
+
let cursor = null;
|
|
720
|
+
do {
|
|
721
|
+
const result = snapshot.query({
|
|
722
|
+
kind: "catalog",
|
|
723
|
+
directory,
|
|
724
|
+
includeRetired: true,
|
|
725
|
+
declaredDirectories,
|
|
726
|
+
limit: 100,
|
|
727
|
+
...(cursor === null ? {} : { cursor }),
|
|
728
|
+
});
|
|
729
|
+
if (result.kind === "error")
|
|
730
|
+
return renderStateQueryFailure(result);
|
|
731
|
+
if (result.kind !== "catalog")
|
|
732
|
+
return unexpectedStateQueryResult();
|
|
733
|
+
if (result.coverage.status !== "complete")
|
|
734
|
+
complete = false;
|
|
735
|
+
entries.push(...result.entries.map((entry) => {
|
|
736
|
+
if (entry.kind === "directory") {
|
|
737
|
+
const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
|
|
738
|
+
return `- Directory ${stateDirectoryHandle(relative)}`;
|
|
739
|
+
}
|
|
740
|
+
if (entry.document === undefined)
|
|
741
|
+
return "- Damaged document (currently not addressable)";
|
|
742
|
+
return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.document.retired ? " · retired" : ""}${entry.status === "damaged" ? " · needs repair" : ""}`;
|
|
743
|
+
}));
|
|
744
|
+
cursor = result.page.nextCursor;
|
|
745
|
+
} while (cursor !== null);
|
|
717
746
|
return {
|
|
718
747
|
ok: true,
|
|
719
|
-
markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${
|
|
748
|
+
markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${complete ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nComplete: yes`,
|
|
720
749
|
};
|
|
721
750
|
}
|
|
722
751
|
return toolFailure("Use history_list for committed history.");
|
|
@@ -43,7 +43,7 @@ const runtimeContracts = {
|
|
|
43
43
|
|
|
44
44
|
- Use only tools attached to the current request. Read results state their scope, cursor, and completeness. Directories must use @dir-* handles returned by Runtime; documents and history must use @handles returned by Runtime. Never substitute a world/ path or natural-language name for a handle.
|
|
45
45
|
- A document marked \`full body injected\` in the material-coverage report already has its complete writable body in the request and carries write authorization. Do not read it again merely to confirm body structure, check body fields, or “be safe.” context_read additionally exposes the current title, summary, and aliases. world_patch preserves metadata fields omitted from set_metadata, so do not read merely to copy unchanged metadata. Read only when a decision depends on unseen metadata or material the report says is not covered.
|
|
46
|
-
- A YAML world_patch locator may mix map keys and zero-based array indexes
|
|
46
|
+
- A YAML world_patch locator may mix map keys and zero-based array indexes. add and replace upsert: they write whether or not the target exists, and an index past the end appends. remove deletes one existing key or array item; a missing target is a no-op the receipt reports. Each edit settles on its own: a failed edit is skipped and the others still apply, so when a receipt lists failed edits, send again only those. Retire an entity that should leave future frame catalogs with world_retire instead of destroying its document; retired documents remain listed, readable, referenceable, and restorable.
|
|
47
47
|
- Zero literal-search matches do not prove that a fact does not exist. Tools, directories, documents, archives, searches, matches, handles, Runtime, and failure processes are private adjudication details. Player-visible narrative must not mention internal phrases such as “nothing was found.” When information is insufficient, preserve uncertainty inside the world instead of inventing an internal process.
|
|
48
48
|
- World-write tools change only an uncommitted working copy. Their changes are not official world facts until Runtime accepts and completes the commit.
|
|
49
49
|
|
|
@@ -74,7 +74,7 @@ ${historyUseGuidance("en")}`,
|
|
|
74
74
|
|
|
75
75
|
- 只使用请求随附的工具;读取结果会明确范围、cursor 与完整性。目录只能使用 Runtime 返回的 @dir-*,文档和历史只能使用 Runtime 返回的 @句柄,不要把 world/路径或自然语言名称冒充句柄。
|
|
76
76
|
- 材料覆盖报告标为 \`已注入完整正文\` 的文档,其完整可写正文已经在本次请求里,写入资格同样已经具备。不要为确认正文结构、核对正文字段或“保险起见”重读它。context_read 会额外显示当前 title、summary 和 aliases;world_patch 会保留 set_metadata 中未提供的元数据字段,因此不要只为照抄未改变的元数据而读取。只有后续决策确实依赖尚未显示的元数据,或材料覆盖报告未覆盖的内容时才读。
|
|
77
|
-
- YAML world_patch 的 locator 可混合 map key 与从 0
|
|
77
|
+
- YAML world_patch 的 locator 可混合 map key 与从 0 开始的数组下标。add 与 replace 是 upsert:无论目标是否存在都写入,数组下标达到或超过长度时追加。remove 只删除一个精确的已有键或数组项;目标不存在时为无操作,回执会注明。每条 edit 独立结算:失败的 edit 被跳过,其余照常生效;回执列出失败项时,只补发列出的 edit。对象需要退出后续 frame catalog 时用 world_retire 退役,不要销毁文档;退役文档仍可列出、读取、引用和恢复。
|
|
78
78
|
- 字面搜索 0 命中不证明事实不存在。工具、目录、文档、档案、检索、命中、句柄、Runtime 及失败过程只供私下裁决;玩家可见叙事不得出现“没搜到/没找到资料”等内部措辞。信息不足时保持世界内的不确定性,不编造内部过程。
|
|
79
79
|
- 世界写入工具只修改尚未提交的工作副本;Runtime 接受并完成提交前,这些修改都不是正式世界事实。
|
|
80
80
|
|
|
@@ -148,15 +148,13 @@ export class FileNativePromptCompiler {
|
|
|
148
148
|
playPreset: input.playPreset,
|
|
149
149
|
modelBinding: input.modelBinding,
|
|
150
150
|
tools: input.tools,
|
|
151
|
+
...(input.preload === undefined ? {} : { preload: input.preload }),
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
compileWorldRevision(input) {
|
|
154
155
|
const identity = this.#locale === "zh-CN"
|
|
155
156
|
? `# 当前修订世界\n\n${input.worldTitle}`
|
|
156
157
|
: `# World being revised\n\n${input.worldTitle}`;
|
|
157
|
-
const boundary = this.#locale === "zh-CN"
|
|
158
|
-
? "# 世界修订工作树边界\n\n随附工具只读写当前锁定 epoch 的 state/* 与 control/* 工作树。成功工具调用尚未进入世界 Authority;只有玩家点击应用才会整体提交并解锁。"
|
|
159
|
-
: "# World-revision worktree boundary\n\nThe attached tools read and write only state/* and control/* in the current locked epoch. Successful tool calls do not enter world Authority until the player applies the complete revision and unlocks it.";
|
|
160
158
|
return this.#compileAuthoringConversation({
|
|
161
159
|
identity: {
|
|
162
160
|
source: "world-revision:title",
|
|
@@ -165,10 +163,7 @@ export class FileNativePromptCompiler {
|
|
|
165
163
|
? `\n\nEpoch: ${input.epoch.id}\nBase: ${input.epoch.baseHead}`
|
|
166
164
|
: ""),
|
|
167
165
|
},
|
|
168
|
-
boundary:
|
|
169
|
-
source: "runtime:world-revision-worktree-boundary",
|
|
170
|
-
markdown: boundary,
|
|
171
|
-
},
|
|
166
|
+
boundary: null,
|
|
172
167
|
runtimeSource: "runtime:builtin/world-revision",
|
|
173
168
|
target: "world-revision",
|
|
174
169
|
gameplay: input.gameplay,
|
|
@@ -177,6 +172,7 @@ export class FileNativePromptCompiler {
|
|
|
177
172
|
playPreset: input.playPreset,
|
|
178
173
|
modelBinding: input.modelBinding,
|
|
179
174
|
tools: input.tools,
|
|
175
|
+
...(input.preload === undefined ? {} : { preload: input.preload }),
|
|
180
176
|
});
|
|
181
177
|
}
|
|
182
178
|
#compileAuthoringConversation(input) {
|
|
@@ -185,7 +181,7 @@ export class FileNativePromptCompiler {
|
|
|
185
181
|
const logicalMessages = [];
|
|
186
182
|
const ordered = input.playPreset.definition.authorPrompts ??
|
|
187
183
|
legacyAuthorPrompts(input.playPreset.definition);
|
|
188
|
-
const catalog = builtinAuthorPrompts(this.#locale);
|
|
184
|
+
const catalog = builtinAuthorPrompts(this.#locale, input.target);
|
|
189
185
|
for (const entry of ordered) {
|
|
190
186
|
if (entry.kind === "world" || !entry.enabled)
|
|
191
187
|
continue;
|
|
@@ -210,7 +206,7 @@ export class FileNativePromptCompiler {
|
|
|
210
206
|
if (builtin?.id === "author.target")
|
|
211
207
|
blocks = [
|
|
212
208
|
input.identity,
|
|
213
|
-
input.boundary,
|
|
209
|
+
...(input.boundary === null ? [] : [input.boundary]),
|
|
214
210
|
...(input.gameplay.mode === "tabletop"
|
|
215
211
|
? [
|
|
216
212
|
tabletopAuthoringRulesBlock(input.gameplay, input.target, this.#locale),
|
|
@@ -238,6 +234,20 @@ export class FileNativePromptCompiler {
|
|
|
238
234
|
blocks,
|
|
239
235
|
});
|
|
240
236
|
}
|
|
237
|
+
// Preloaded material follows every author prompt so the stable prefix stays byte-identical.
|
|
238
|
+
if (input.preload !== undefined && input.preload.length > 0) {
|
|
239
|
+
const preloadBlocks = input.preload.map(({ source, markdown }) => ({
|
|
240
|
+
source,
|
|
241
|
+
markdown,
|
|
242
|
+
}));
|
|
243
|
+
logicalMessages.push({
|
|
244
|
+
messageRole: "system",
|
|
245
|
+
promptId: "runtime.preload",
|
|
246
|
+
role: "world_context",
|
|
247
|
+
markdown: joinBlocks(preloadBlocks),
|
|
248
|
+
blocks: preloadBlocks,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
241
251
|
const tools = structuredClone(input.tools);
|
|
242
252
|
const toolStrategy = this.#toolStrategyOverride ??
|
|
243
253
|
defaultRuntimeToolDefinitionStrategy(input.modelBinding.provider);
|
|
@@ -870,9 +880,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
|
|
|
870
880
|
return [
|
|
871
881
|
{
|
|
872
882
|
...worldInstructionPlaceholder,
|
|
873
|
-
markdown:
|
|
874
|
-
?
|
|
875
|
-
|
|
883
|
+
markdown: revision
|
|
884
|
+
? locale === "zh-CN"
|
|
885
|
+
? "世界指令在此展开;启用块原文见本请求的 Runtime 预载。"
|
|
886
|
+
: "World instructions expand here; enabled block text appears in this request's Runtime preload."
|
|
887
|
+
: locale === "zh-CN"
|
|
888
|
+
? "世界指令在此展开;frame 选定材料仅在全新游玩上下文中作为初始工具结果提供。请通过本轮创作工具检查实际内容。"
|
|
889
|
+
: "World instructions expand here; frame-selected material is supplied as an initial tool result only for a fresh play context. Inspect actual content through the current authoring tools.",
|
|
876
890
|
},
|
|
877
891
|
];
|
|
878
892
|
if (entry.kind === "tail" || !entry.enabled)
|
|
@@ -949,11 +963,13 @@ function settingImprovementPresetReference(binding, locale, target, gameplay) {
|
|
|
949
963
|
: "Player-visible narrative semantics", locale === "zh-CN"
|
|
950
964
|
? "这些块只在未来游玩生成玩家可见正文时生效;不得把它们当作本轮 YAML 或 Markdown 设定正文的范文。"
|
|
951
965
|
: "These blocks apply only when future play produces player-visible story. Do not use them as prose models for YAML or Markdown setting documents in this conversation.", narrativeBlocks),
|
|
952
|
-
|
|
953
|
-
?
|
|
954
|
-
:
|
|
955
|
-
|
|
956
|
-
|
|
966
|
+
revision
|
|
967
|
+
? null
|
|
968
|
+
: settingImprovementReferenceGroup(locale === "zh-CN"
|
|
969
|
+
? "本轮提示(每次发送的尾部)"
|
|
970
|
+
: "Turn guide (per-send suffix)", locale === "zh-CN"
|
|
971
|
+
? "这些行在每次游玩发送时追加在玩家输入之后,由已启用块的尾部行按同一顺序拼成;它们是提醒,不是世界事实。"
|
|
972
|
+
: "These lines are appended after the player input on every play send, assembled from the enabled blocks' turn-guide lines in the same order. They are reminders, not world facts.", turnGuideBlocks),
|
|
957
973
|
].filter((group) => group !== null);
|
|
958
974
|
return [
|
|
959
975
|
heading,
|