narraeon 0.7.5 → 0.7.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.
- package/dist/node/protocol/v1.js +4 -0
- package/dist/node/runtime/V1Runtime.js +12 -0
- package/dist/node/runtime/model/ToolArgumentCoercion.js +138 -0
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +12 -4
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayCallChain.js +82 -36
- package/dist/node/runtime/play/PlayDocumentTools.js +56 -6
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +5 -5
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +25 -3
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +0 -67
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +62 -6
- package/dist/node/runtime/world/WorldDocumentStore.js +441 -389
- package/dist/node/runtime/world/WorldStateRevision.js +6 -3
- package/dist/node/shared/ordered-play-prompts.js +7 -2
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Da0tdDij.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-9N9kO8kY.js +0 -1408
|
@@ -5,8 +5,10 @@ import { stateDirectoryHandle, parseStateDirectoryHandle, } from "../world/State
|
|
|
5
5
|
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
6
6
|
import { stringify } from "yaml";
|
|
7
7
|
import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
8
|
+
import { coerceToolArguments, renderCoercionNote, } from "../model/ToolArgumentCoercion.js";
|
|
9
|
+
import { runtimeToolInputSchema } from "../prompt/FileNativeToolRegistry.js";
|
|
8
10
|
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
9
|
-
import {} from "../world/WorldDocumentStore.js";
|
|
11
|
+
import { formatWorldDocumentLocator, } from "../world/WorldDocumentStore.js";
|
|
10
12
|
import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
|
|
11
13
|
class PlayDocumentToolFailure extends Error {
|
|
12
14
|
failureKind;
|
|
@@ -76,6 +78,19 @@ export class FileNativePlayDocuments {
|
|
|
76
78
|
return authorizeResolvedRead(this.#reads, this.#candidate.snapshot, shortRef, locator);
|
|
77
79
|
}
|
|
78
80
|
execute(call, history, options = {}) {
|
|
81
|
+
const schema = runtimeToolInputSchema(call.name);
|
|
82
|
+
if (schema === null)
|
|
83
|
+
return this.#dispatch(call, history, options);
|
|
84
|
+
const repaired = coerceToolArguments(schema, call.arguments);
|
|
85
|
+
const result = this.#dispatch({ ...call, arguments: repaired.arguments }, history, options);
|
|
86
|
+
if (repaired.coerced.length === 0 || result.markdown === undefined)
|
|
87
|
+
return result;
|
|
88
|
+
return {
|
|
89
|
+
...result,
|
|
90
|
+
markdown: `${result.markdown}\n\n${renderCoercionNote(repaired.coerced, options.locale ?? "en")}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
#dispatch(call, history, options) {
|
|
79
94
|
let historyQuery;
|
|
80
95
|
const query = () => (historyQuery ??= new HistoryQuery(options.historyScope ?? "local-document-context", historyInputs(history)));
|
|
81
96
|
const render = (result) => renderHistoryResult(result, options.locale ?? "en");
|
|
@@ -232,6 +247,8 @@ function documentDescriptorByRef(snapshot, ref) {
|
|
|
232
247
|
return result.kind === "read_document" && result.ok ? result.document : null;
|
|
233
248
|
}
|
|
234
249
|
function applyPlayCandidatePatch(candidate, reads, args) {
|
|
250
|
+
if (record(args) && typeof args.edits === "string")
|
|
251
|
+
return toolFailure("world_patch.edits must be a JSON array of 1 to 32 edits, not a string.");
|
|
235
252
|
if (!record(args) ||
|
|
236
253
|
typeof args.target !== "string" ||
|
|
237
254
|
!Array.isArray(args.edits) ||
|
|
@@ -257,10 +274,12 @@ function applyPlayCandidatePatch(candidate, reads, args) {
|
|
|
257
274
|
},
|
|
258
275
|
],
|
|
259
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");
|
|
260
279
|
if (!revised.ok || revised.snapshotStatus !== "usable")
|
|
261
280
|
throw new PlayDocumentToolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
|
|
262
281
|
const changed = revised.changes.length > 0;
|
|
263
|
-
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
|
|
282
|
+
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes, revised.editOutcomes);
|
|
264
283
|
const previousSnapshot = candidate.snapshot;
|
|
265
284
|
acceptWorldStateRevision(candidate, revised);
|
|
266
285
|
carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
|
|
@@ -336,12 +355,41 @@ function authorizationsAfterEdits(authorization, edits) {
|
|
|
336
355
|
}
|
|
337
356
|
return authorization;
|
|
338
357
|
}
|
|
339
|
-
function formatToolRevisionReceipt(shortRef, changes) {
|
|
358
|
+
function formatToolRevisionReceipt(shortRef, changes, outcomes) {
|
|
340
359
|
if (changes.length > 1)
|
|
341
360
|
throw new Error("A world_patch revision must affect only its target document");
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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(" · ");
|
|
345
393
|
}
|
|
346
394
|
function applyPlayCandidateRetirement(candidate, reads, args) {
|
|
347
395
|
if (!record(args) ||
|
|
@@ -419,6 +467,8 @@ function applyPlayCandidateCreate(candidate, authorizations, args, declaredDirec
|
|
|
419
467
|
const parentDirectory = record(args) && typeof args.parent === "string"
|
|
420
468
|
? parseStateDirectoryHandle(args.parent)
|
|
421
469
|
: null;
|
|
470
|
+
if (record(args) && typeof args.aliases === "string")
|
|
471
|
+
throw new PlayDocumentToolFailure("world_create.aliases must be a JSON array of strings, not a string");
|
|
422
472
|
if (!record(args) ||
|
|
423
473
|
typeof args.parent !== "string" ||
|
|
424
474
|
parentDirectory === null ||
|
|
@@ -6,7 +6,7 @@ import { effectiveFollowupDefinitions } from "../play/OrderedFollowups.js";
|
|
|
6
6
|
import { legacyAuthorPrompts } from "../play/FileNativePlayPresetStore.js";
|
|
7
7
|
import { builtinAuthorPrompts, authoringMechanics, } from "../../shared/ordered-author-prompts.js";
|
|
8
8
|
import { builtinPlayPrompts, orderedPromptScope, orderedPromptTurnGuide, promptScopeApplies, } from "../../shared/ordered-play-prompts.js";
|
|
9
|
-
import { turnGuideHeader, turnGuideText, } from "../../shared/turn-guide.js";
|
|
9
|
+
import { turnGuideHeader, turnGuideText, turnGuideWithTurnNote, } from "../../shared/turn-guide.js";
|
|
10
10
|
import { parseOrderedPlayPrompts } from "../play/OrderedPlayPrompts.js";
|
|
11
11
|
import { renderDocumentWritePosition } from "./WorldMaintenanceReport.js";
|
|
12
12
|
import { parseDocument } from "yaml";
|
|
@@ -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
|
|
|
@@ -568,12 +568,12 @@ export class FileNativePromptCompiler {
|
|
|
568
568
|
append("player_input", [
|
|
569
569
|
{ source: "player:input", markdown: effective.playerInput },
|
|
570
570
|
]);
|
|
571
|
-
const turnGuide = turnGuideLines.length === 0
|
|
571
|
+
const turnGuide = turnGuideWithTurnNote(this.#locale, turnGuideLines.length === 0
|
|
572
572
|
? undefined
|
|
573
573
|
: {
|
|
574
574
|
text: turnGuideText(this.#locale, turnGuideLines),
|
|
575
575
|
lines: turnGuideLines,
|
|
576
|
-
};
|
|
576
|
+
}, effective.turnNote);
|
|
577
577
|
scanRuntimeLeakage([
|
|
578
578
|
...logicalMessages,
|
|
579
579
|
...(turnGuide === undefined
|
|
@@ -11,6 +11,11 @@ import { resolutionCheckToolForScheme, resolutionToolDefinition, resolutionToolN
|
|
|
11
11
|
* Tools that every current play and authoring surface may expose. Resolution
|
|
12
12
|
* tools are registered below but never enter a surface until a per-world
|
|
13
13
|
* selection (Phase 2) includes them.
|
|
14
|
+
*
|
|
15
|
+
* No declared name may end in `_<another declared name>`: a provider-mangled
|
|
16
|
+
* `mcp__<server>__<word>_<name>` is restored by that suffix (see
|
|
17
|
+
* ToolNameAlias.ts), so such a pair would make the restoration ambiguous.
|
|
18
|
+
* tests/runtime/tool-name-alias.test.ts enforces this across all surfaces.
|
|
14
19
|
*/
|
|
15
20
|
export const baseRuntimeToolNames = [
|
|
16
21
|
"state_list",
|
|
@@ -70,6 +75,16 @@ export function runtimeToolsForNames(names, locale = defaultAppLocale) {
|
|
|
70
75
|
inputSchema: toolDefinitions[name].inputSchema,
|
|
71
76
|
}));
|
|
72
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The original Runtime input schema of a registered tool. Argument repair reads
|
|
80
|
+
* this, not the portable wire projection, so every array or object position is
|
|
81
|
+
* exactly what Runtime validates.
|
|
82
|
+
*/
|
|
83
|
+
export function runtimeToolInputSchema(name, locale = defaultAppLocale) {
|
|
84
|
+
if (!isRegisteredRuntimeToolName(name))
|
|
85
|
+
return null;
|
|
86
|
+
return createToolDefinitions(locale)[name].inputSchema;
|
|
87
|
+
}
|
|
73
88
|
/**
|
|
74
89
|
* Return a Provider-portable root-object schema for a Runtime-owned tool.
|
|
75
90
|
*
|
|
@@ -113,6 +128,8 @@ function createToolDefinitions(locale) {
|
|
|
113
128
|
},
|
|
114
129
|
}, ["yaml"]);
|
|
115
130
|
const markdownLocator = object({ markdown: { type: "array", items: { type: "string" }, minItems: 1 } }, ["markdown"]);
|
|
131
|
+
// add_section takes [] to add a new top-level section at the end of the body.
|
|
132
|
+
const markdownParentLocator = object({ markdown: { type: "array", items: { type: "string" } } }, ["markdown"]);
|
|
116
133
|
const definitions = {
|
|
117
134
|
...resolutionDefinitions(locale),
|
|
118
135
|
state_list: {
|
|
@@ -194,10 +211,15 @@ function createToolDefinitions(locale) {
|
|
|
194
211
|
"locator",
|
|
195
212
|
]),
|
|
196
213
|
object({
|
|
197
|
-
op: {
|
|
214
|
+
op: { const: "replace_section" },
|
|
198
215
|
locator: markdownLocator,
|
|
199
216
|
markdown: { type: "string" },
|
|
200
217
|
}, ["op", "locator", "markdown"]),
|
|
218
|
+
object({
|
|
219
|
+
op: { const: "add_section" },
|
|
220
|
+
locator: markdownParentLocator,
|
|
221
|
+
markdown: { type: "string" },
|
|
222
|
+
}, ["op", "locator", "markdown"]),
|
|
201
223
|
object({
|
|
202
224
|
op: { const: "rename_section" },
|
|
203
225
|
locator: markdownLocator,
|
|
@@ -278,7 +300,7 @@ const toolDescriptions = {
|
|
|
278
300
|
history_read: "Read complete committed originals on the current timeline, never summarized, truncated or body-paginated. Use {latest:N} alone (1–21 messages), or {ref,before,after} with defaults before=after=0 and sum at most 20; do not mix modes. Ref may come directly from Runtime prompt injections or tools without listing first. Windows are always oldest to newest, including the anchor. earlier/later are immediately outside this window and can be the next ref with before/after to avoid rereading. Boundaries and empty history are normal. Counts are messages, not turns; 21 long messages can produce large output.",
|
|
279
301
|
context_search: "Search literal source text in state documents. Use source=state; committed history uses history_search. Zero matches means only that the supplied text had no literal match; it does not prove that a world fact does not exist. Player-visible narrative must not recount the search process. within accepts only an @document or @dir-* handle returned by Runtime.",
|
|
280
302
|
context_read: "Read the complete content of an @document or @node handle previously returned by Runtime. The read heading, metadata boundaries, body boundaries, scope, and completion marker in the result are not source text. set_metadata may include only the fields that must change; Runtime preserves omitted metadata fields. Update the writable body between the boundaries with locators. YAML bodies omit the $document technical header; Markdown bodies begin at the document's level-one heading. Results are never truncated or paginated. Never pass file paths, natural-language names, or an invented world/... value.",
|
|
281
|
-
world_patch: 'Update a document that has been read precisely or whose complete body was injected with write authorization. target must be an @short-ref. When title, summary, or aliases are stale, use set_metadata and include only the title, summary, or aliases that must change. At least one is required; Runtime preserves omitted fields from the current candidate document. A YAML edit uses locator:{yaml:["field","child"]}, for example {op:"replace",locator:{yaml:["status"]},value:"new value"}. Locator segments address map keys or a zero-based array index
|
|
303
|
+
world_patch: 'Update a document that has been read precisely or whose complete body was injected with write authorization. target must be an @short-ref. When title, summary, or aliases are stale, use set_metadata and include only the title, summary, or aliases that must change. At least one is required; Runtime preserves omitted fields from the current candidate document. A YAML edit uses locator:{yaml:["field","child"]}, for example {op:"replace",locator:{yaml:["status"]},value:"new value"}. Locator segments address map keys or a zero-based array index. add and replace are upserts: each writes its value whether or not the target exists, creating missing parent maps, and an array index at or past the end appends. remove deletes the existing key or array item at its locator; a missing target is a no-op that the receipt reports. A YAML value may reference a whole document only as {$ref:"@short-ref"}; the short reference must come from a Runtime list, read, or create result. Never invent a document id. If no handle is available, use ordinary text when semantically correct or call world_create first and use its returned @short-ref. To add an item to the end of an existing sequence, use append with the locator pointing to that sequence. A Markdown locator excludes the document level-one heading: {markdown:["Responsibilities"]} points exactly to ## Responsibilities, and replace_section.markdown must begin with the same heading at the same level; when that heading does not exist yet, replace_section adds it under its parent. add_section adds a child under the locator heading, and locator [] with a level-two markdown heading adds a new top-level section at the end of the body. Use replace_preamble for text after the level-one heading and before the first level-two heading; send only that text. Use replace_body to replace the whole Markdown body and retain the original level-one heading. Each edit settles on its own: a failed edit is skipped and the others still apply. Success reports whether the document changed; when the receipt lists failed edits, the applied ones are already written, so send again only the listed edits. It does not echo the body. Call context_read again only when a later decision depends on Runtime\'s exact serialized body or current metadata. Do not use path, JSON Pointer, set, or a file name.',
|
|
282
304
|
world_create: 'Create a document inside a Runtime-known state directory. parent must be an @dir-* handle supplied by the initial directory index or state_list, such as @dir-/characters; catalog directories declared by the frozen world frame remain available when empty, and @dir-/ is the root. A syntactically valid but unknown directory is rejected. refHint is a lowercase ASCII short-reference hint, not a world/ path. A YAML body may reference a whole document only as {$ref:"@short-ref"}, and the short reference must come from a Runtime list, read, or create result. Never invent a document id.',
|
|
283
305
|
world_retire: "Retire or restore an exact state document without deleting its narrative record. target must be an @short-ref that was read precisely or injected completely. retired:true removes the document from future frame catalog injection but keeps it listed, searchable, readable, referenceable, and restorable; retired:false restores catalog participation. This changes catalog participation, not explicit full-document injection or character activity; use world_patch for ordinary location or field changes.",
|
|
284
306
|
artifact_emit: "Submit an artifact declared in advance for this follow-up request. Supply only the output name and payload; Runtime fixes the channel, key, content type, renderer, retention policy, and authority meaning in the contract.",
|
|
@@ -292,7 +314,7 @@ const toolDescriptions = {
|
|
|
292
314
|
history_read: "完整读取当前时间线已提交原文,无摘要、无截断、无正文分页。{latest:N}单独读取最近1–21条;或{ref,before,after},before/after默认0且合计最多20;两模式互斥。ref可直接使用Runtime提示词注入或工具结果的历史句柄,不必先列举。窗口始终由旧到新且包含锚点。earlier/later是窗口外紧邻句柄,可作为下一次ref并配before/after避免重复读取;边界与空历史正常。数量单位是消息而非回合;21条长消息仍可能产生大量输出。",
|
|
293
315
|
context_search: "在状态文档原文字面中搜索,source只能为state;已提交历史使用history_search。0 命中只表示给定文本没有字面命中,不证明世界事实不存在;玩家可见叙事不得复述搜索过程。within 只能使用 Runtime 已返回的 @文档或 @dir-* 句柄。",
|
|
294
316
|
context_read: "完整读取 Runtime 先前返回的 @文档或@节点句柄。结果中的读取标题、元信息边界、正文边界、范围和完整标记不属于源文;set_metadata 可以只提交需要改变的字段,Runtime 会保留未提供的元数据字段。中间的可写正文用 locator 更新。YAML 正文不含 $document 技术头,Markdown 正文从文档 # 一级标题开始。结果不会截断或分页。不要传文件路径、自然语言名称或自行拼造的 world/...。",
|
|
295
|
-
world_patch: '更新已精确读取,或已随写入资格注入完整正文的文档。target 必须使用 @短引用;文档 title、summary 或 aliases 过时时使用 set_metadata,只提供需要改变的 title、summary 或 aliases。至少提供一项;未提供的字段由 Runtime 从当前候选文档保留。YAML edit 使用 locator:{yaml:["字段","子字段"]},例如 {op:"replace",locator:{yaml:["情况"]},value:"新值"}。locator 的每一段可以定位 map key 或从 0
|
|
317
|
+
world_patch: '更新已精确读取,或已随写入资格注入完整正文的文档。target 必须使用 @短引用;文档 title、summary 或 aliases 过时时使用 set_metadata,只提供需要改变的 title、summary 或 aliases。至少提供一项;未提供的字段由 Runtime 从当前候选文档保留。YAML edit 使用 locator:{yaml:["字段","子字段"]},例如 {op:"replace",locator:{yaml:["情况"]},value:"新值"}。locator 的每一段可以定位 map key 或从 0 开始的数组下标。add 与 replace 都是 upsert:无论目标是否已存在都写入,缺失的父级 map 会被创建,数组下标达到或超过长度时追加到末尾。remove 只删除 locator 精确指向的已有键或数组项;目标不存在时视为无操作,回执会注明。YAML value 需要引用整份文档时只能写 {$ref:"@短引用"},短引用必须来自 Runtime 的 list、read 或 create 结果;不得自行编造文档 id。没有可用句柄时,根据语义使用普通文本,或先 world_create 后使用其返回的 @短引用。向已存在的 sequence 末尾加一项必须使用 append 并把 locator 指向该 sequence。Markdown locator 不包含文档 # 一级标题:{markdown:["职责"]} 精确指向 ## 职责,replace_section.markdown 必须从同级同名标题开始;该标题尚不存在时,replace_section 会把它新增到父级之下。add_section 在 locator 标题之下新增子小节;locator 为 [] 且 markdown 以二级标题开始时,则在正文末尾新增顶层小节。修改 # 标题下、第一个 ## 前的文字用 replace_preamble(只传该段文字);替换整个 Markdown 正文用 replace_body,且 markdown 必须保留原 # 一级标题。每条 edit 独立结算:失败的 edit 被跳过,其余照常生效。成功结果只报告文档是否发生变化;回执列出失败的 edit 时,已生效的 edit 不要重发,只补上列出的 edit。不回显正文;只有后续决策依赖 Runtime 序列化后的精确正文或当前元数据时才重新 context_read。不要使用 path、JSON Pointer、set 或文件名。',
|
|
296
318
|
world_create: '在 Runtime 已知的状态目录中创建文档。parent 必须是初始目录索引或 state_list 列出的 @dir-*,例如 @dir-/characters;冻结世界 frame 声明的 catalog 目录为空时仍然可用,根目录使用 @dir-/。只有句柄形式正确但 Runtime 并不知道的目录会被拒绝。refHint 是小写 ASCII 短引用,不要传 world/ 路径。YAML body 需要引用整份文档时只能写 {$ref:"@短引用"},且短引用必须来自 Runtime 的 list、read 或 create 结果;不得自行编造文档 id。',
|
|
297
319
|
world_retire: "让一份精确状态文档退役或恢复,而不销毁叙事记录。target 必须是已精确读取或已完整注入的 @短引用。retired:true 会让文档不再进入后续 frame catalog 注入,但仍可列出、搜索、读取、引用和恢复;retired:false 恢复 catalog 参与。它只改变 catalog 参与,不取消显式全文注入、不冻结人物活动;普通位置或字段变化仍用 world_patch。",
|
|
298
320
|
artifact_emit: "提交本次后置请求预先声明的产物。只能传 output name 与 payload;频道、key、内容类型、renderer、保存策略和权威含义由 Runtime contract 固定。",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function resolveToolName(called, provided) {
|
|
2
|
+
if (provided.includes(called))
|
|
3
|
+
return { kind: "exact", name: called };
|
|
4
|
+
const candidates = provided.filter((name) => called.length > name.length && called.endsWith(`_${name}`));
|
|
5
|
+
if (candidates.length === 1)
|
|
6
|
+
return { kind: "alias", name: candidates[0], declared: called };
|
|
7
|
+
return { kind: "rejected", candidates };
|
|
8
|
+
}
|
|
9
|
+
/** The Runtime note appended to a result whose call was restored by alias. */
|
|
10
|
+
export function renderToolAliasNote(declared, name, locale) {
|
|
11
|
+
return locale === "zh-CN"
|
|
12
|
+
? `Runtime 说明:"${declared}" 不是本次提供的工具,已按 "${name}" 执行。请使用声明的工具名。`
|
|
13
|
+
: `Runtime note: "${declared}" is not a declared tool; it was executed as "${name}". Call tools by their declared names.`;
|
|
14
|
+
}
|
|
@@ -14,70 +14,3 @@ export function worldMaterialCoverage(coverage) {
|
|
|
14
14
|
});
|
|
15
15
|
return { fullInjected, nodeInjected, catalogSummary, injectedSelections };
|
|
16
16
|
}
|
|
17
|
-
export function renderFreshContextCoverage(snapshot, coverage, shortRef, locale = "en") {
|
|
18
|
-
const exposure = worldMaterialCoverage(coverage);
|
|
19
|
-
const full = exposure.fullInjected.has(shortRef);
|
|
20
|
-
const nodes = exposure.nodeInjected.has(shortRef);
|
|
21
|
-
const summary = exposure.catalogSummary.has(shortRef);
|
|
22
|
-
const zh = locale === "zh-CN";
|
|
23
|
-
const scope = full
|
|
24
|
-
? zh
|
|
25
|
-
? "完整正文"
|
|
26
|
-
: "full body"
|
|
27
|
-
: nodes
|
|
28
|
-
? zh
|
|
29
|
-
? "指定节点"
|
|
30
|
-
: "selected nodes"
|
|
31
|
-
: summary
|
|
32
|
-
? zh
|
|
33
|
-
? "仅目录摘要"
|
|
34
|
-
: "catalog summary only"
|
|
35
|
-
: zh
|
|
36
|
-
? "不直接注入,需要按需读取"
|
|
37
|
-
: "not directly injected; read on demand";
|
|
38
|
-
const lines = [
|
|
39
|
-
`${zh ? "如果现在新开上下文" : "If a fresh context started now"}: ${scope}${nodes && !full && summary ? (zh ? ",以及目录摘要" : " and catalog summary") : ""}`,
|
|
40
|
-
];
|
|
41
|
-
if (!full && nodes) {
|
|
42
|
-
const scopes = exposure.injectedSelections.filter((entry) => entry.shortRef === shortRef && entry.locator !== null);
|
|
43
|
-
lines.push(`${zh ? "节点范围" : "Node scopes"}: ${scopes.map(({ locator }) => JSON.stringify(locator)).join(", ")}`);
|
|
44
|
-
}
|
|
45
|
-
if (!full && summary) {
|
|
46
|
-
const read = snapshot.query({
|
|
47
|
-
kind: "read_document",
|
|
48
|
-
document: { shortRef },
|
|
49
|
-
});
|
|
50
|
-
if (read.kind === "read_document")
|
|
51
|
-
lines.push(`${zh ? "摘要" : "Summary"}: ${read.document.summary}`);
|
|
52
|
-
}
|
|
53
|
-
if (!full && !nodes && !summary) {
|
|
54
|
-
const references = exposure.injectedSelections.flatMap((selection) => {
|
|
55
|
-
const read = snapshot.query({
|
|
56
|
-
kind: "read_document",
|
|
57
|
-
document: { shortRef: selection.shortRef },
|
|
58
|
-
});
|
|
59
|
-
const locator = selection.locator ??
|
|
60
|
-
(read.kind === "read_document" && read.codec === "yaml"
|
|
61
|
-
? { yaml: [] }
|
|
62
|
-
: null);
|
|
63
|
-
if (locator === null)
|
|
64
|
-
return [];
|
|
65
|
-
const node = snapshot.query({
|
|
66
|
-
kind: "select_node",
|
|
67
|
-
document: { shortRef: selection.shortRef },
|
|
68
|
-
locator,
|
|
69
|
-
});
|
|
70
|
-
return node.kind === "select_node" &&
|
|
71
|
-
node.references.some(({ target }) => target.shortRef === shortRef)
|
|
72
|
-
? [`@${selection.shortRef}`]
|
|
73
|
-
: [];
|
|
74
|
-
});
|
|
75
|
-
if (references.length > 0)
|
|
76
|
-
lines.push(`${zh ? "已注入材料中的直接引用来自" : "Direct reference in injected material"}: ${[...new Set(references)].join(", ")}`);
|
|
77
|
-
else if (coverage.some(({ status }) => status === "paged_catalog"))
|
|
78
|
-
lines.push(zh
|
|
79
|
-
? "目录仍有后续页,可使用 state_list 继续查找。"
|
|
80
|
-
: "Catalogs have further pages; use state_list to continue discovery.");
|
|
81
|
-
}
|
|
82
|
-
return lines.join("\n");
|
|
83
|
-
}
|
|
@@ -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
|
|
765
|
-
operation: authoringOperation(surface,
|
|
766
|
-
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) ||
|