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.
- 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/FileNativePlayTimelineStore.js +6 -2
- package/dist/node/runtime/play/OrderedPlayPrompts.js +3 -6
- package/dist/node/runtime/play/PlayCallChain.js +76 -24
- package/dist/node/runtime/play/PlayDocumentTools.js +19 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +3 -3
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +15 -0
- package/dist/node/runtime/prompt/ToolNameAlias.js +14 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +62 -6
- package/dist/node/shared/ordered-play-prompts.js +5 -0
- package/dist/node/shared/turn-guide.js +27 -2
- package/dist/web/assets/index-Cj2JYCia.js +1408 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-9N9kO8kY.js +0 -1408
package/dist/node/protocol/v1.js
CHANGED
|
@@ -353,6 +353,10 @@ function validateRequestFields(request) {
|
|
|
353
353
|
if (request.type === "play.timeline.detail" &&
|
|
354
354
|
(!Number.isSafeInteger(request.eventId) || Number(request.eventId) < 1))
|
|
355
355
|
throw new V1ProtocolError("invalid_request", "play.timeline.detail.eventId is invalid");
|
|
356
|
+
if ("turnNote" in request &&
|
|
357
|
+
request.turnNote !== undefined &&
|
|
358
|
+
typeof request.turnNote !== "string")
|
|
359
|
+
throw new V1ProtocolError("invalid_request", "turnNote is invalid");
|
|
356
360
|
if (request.type === "play.chain.revise-player" &&
|
|
357
361
|
request.continuation !== "continue_context" &&
|
|
358
362
|
request.continuation !== "fresh_context")
|
|
@@ -458,6 +458,9 @@ export class V1Runtime {
|
|
|
458
458
|
}), `content:${request.packageId}.message.genesis.narrator`, "content-preview"),
|
|
459
459
|
playerInputPlacement: "append",
|
|
460
460
|
playerInput: request.playerInput,
|
|
461
|
+
...(request.turnNote === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { turnNote: request.turnNote }),
|
|
461
464
|
modelBinding: modelHost.binding(),
|
|
462
465
|
}, playPreset);
|
|
463
466
|
const tools = structuredClone(preview.compilation.toolUniverse ?? preview.compilation.tools);
|
|
@@ -669,6 +672,9 @@ export class V1Runtime {
|
|
|
669
672
|
eventId: request.eventId,
|
|
670
673
|
replacementExchangeId: request.replacementExchangeId,
|
|
671
674
|
replacementText: request.replacementText,
|
|
675
|
+
...(request.turnNote === undefined
|
|
676
|
+
? {}
|
|
677
|
+
: { turnNote: request.turnNote }),
|
|
672
678
|
};
|
|
673
679
|
const result = await playCall(async () => {
|
|
674
680
|
await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
|
|
@@ -695,6 +701,9 @@ export class V1Runtime {
|
|
|
695
701
|
chainId: request.chainId,
|
|
696
702
|
exchangeId: request.exchangeId,
|
|
697
703
|
playerText: request.playerText,
|
|
704
|
+
...(request.turnNote === undefined
|
|
705
|
+
? {}
|
|
706
|
+
: { turnNote: request.turnNote }),
|
|
698
707
|
hostBinding,
|
|
699
708
|
playPreset,
|
|
700
709
|
modelBinding,
|
|
@@ -712,6 +721,9 @@ export class V1Runtime {
|
|
|
712
721
|
chainId: request.chainId,
|
|
713
722
|
exchangeId: request.exchangeId,
|
|
714
723
|
playerText: request.playerText,
|
|
724
|
+
...(request.turnNote === undefined
|
|
725
|
+
? {}
|
|
726
|
+
: { turnNote: request.turnNote }),
|
|
715
727
|
modelHost: await this.#modelHost(),
|
|
716
728
|
resolvePrompt: () => this.#continuousBinding(),
|
|
717
729
|
...(playCallChainObserver === undefined
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** Root arguments are depth 0; parsing and descent stop after two levels. */
|
|
2
|
+
const maxCoercionDepth = 2;
|
|
3
|
+
export function coerceToolArguments(schema, args) {
|
|
4
|
+
const coerced = [];
|
|
5
|
+
const value = coerceValue(schema, args, "", 0, coerced);
|
|
6
|
+
return { arguments: value, coerced };
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renders the Runtime note appended to a tool result whose arguments were
|
|
10
|
+
* parsed. It tells the model its arguments were repaired, so the next call
|
|
11
|
+
* can send native JSON values.
|
|
12
|
+
*/
|
|
13
|
+
export function renderCoercionNote(paths, locale) {
|
|
14
|
+
const names = paths.map((path) => `"${path}"`).join(", ");
|
|
15
|
+
return locale === "zh-CN"
|
|
16
|
+
? `Runtime 说明:以下参数以 JSON 字符串到达,本次已解析后执行:${names}。请以 JSON 数组或对象的原生值发送,不要用字符串。`
|
|
17
|
+
: `Runtime note: these arguments arrived as JSON strings and were parsed for this call: ${names}. Send array and object arguments as native JSON values, not strings.`;
|
|
18
|
+
}
|
|
19
|
+
function coerceValue(schema, value, path, depth, coerced) {
|
|
20
|
+
const type = definiteType(schema);
|
|
21
|
+
let current = value;
|
|
22
|
+
if ((type === "array" || type === "object") &&
|
|
23
|
+
typeof current === "string" &&
|
|
24
|
+
depth > 0 &&
|
|
25
|
+
depth <= maxCoercionDepth) {
|
|
26
|
+
const parsed = parseJson(current);
|
|
27
|
+
if (matchesType(type, parsed)) {
|
|
28
|
+
current = parsed;
|
|
29
|
+
coerced.push(path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (depth >= maxCoercionDepth)
|
|
33
|
+
return current;
|
|
34
|
+
if (type === "array" && isArray(current)) {
|
|
35
|
+
const items = itemsSchema(schema);
|
|
36
|
+
if (items === undefined)
|
|
37
|
+
return current;
|
|
38
|
+
let next = current;
|
|
39
|
+
current.forEach((element, index) => {
|
|
40
|
+
const repaired = coerceValue(items, element, `${path}[${index}]`, depth + 1, coerced);
|
|
41
|
+
if (repaired !== element) {
|
|
42
|
+
if (next === current)
|
|
43
|
+
next = [...current];
|
|
44
|
+
next[index] = repaired;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
if (type === "object" && isRecord(current)) {
|
|
50
|
+
let next = current;
|
|
51
|
+
for (const key of Object.keys(current)) {
|
|
52
|
+
const property = propertySchema(schema, key);
|
|
53
|
+
if (property === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
const repaired = coerceValue(property, current[key], path === "" ? key : `${path}.${key}`, depth + 1, coerced);
|
|
56
|
+
if (repaired !== current[key]) {
|
|
57
|
+
if (next === current)
|
|
58
|
+
next = { ...current };
|
|
59
|
+
next[key] = repaired;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
/** The single type a schema position admits, or undefined when it is not definite. */
|
|
67
|
+
function definiteType(schema) {
|
|
68
|
+
if (!isRecord(schema))
|
|
69
|
+
return undefined;
|
|
70
|
+
if (schema.type !== undefined) {
|
|
71
|
+
if (typeof schema.type === "string")
|
|
72
|
+
return schema.type;
|
|
73
|
+
if (Array.isArray(schema.type) &&
|
|
74
|
+
schema.type.length === 1 &&
|
|
75
|
+
typeof schema.type[0] === "string")
|
|
76
|
+
return schema.type[0];
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const branches = Array.isArray(schema.oneOf)
|
|
80
|
+
? schema.oneOf
|
|
81
|
+
: Array.isArray(schema.anyOf)
|
|
82
|
+
? schema.anyOf
|
|
83
|
+
: undefined;
|
|
84
|
+
if (branches === undefined || branches.length === 0)
|
|
85
|
+
return undefined;
|
|
86
|
+
const types = branches.map(definiteType);
|
|
87
|
+
const first = types[0];
|
|
88
|
+
return first !== undefined && types.every((type) => type === first)
|
|
89
|
+
? first
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
function itemsSchema(schema) {
|
|
93
|
+
return isRecord(schema.items) && !Array.isArray(schema.items)
|
|
94
|
+
? schema.items
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A property schema is taken directly from `properties`, or from oneOf/anyOf
|
|
99
|
+
* branches only when every branch that declares the key declares it the same.
|
|
100
|
+
*/
|
|
101
|
+
function propertySchema(schema, key) {
|
|
102
|
+
if (isRecord(schema.properties) && isRecord(schema.properties[key]))
|
|
103
|
+
return schema.properties[key];
|
|
104
|
+
const branches = Array.isArray(schema.oneOf)
|
|
105
|
+
? schema.oneOf
|
|
106
|
+
: Array.isArray(schema.anyOf)
|
|
107
|
+
? schema.anyOf
|
|
108
|
+
: [];
|
|
109
|
+
const declared = branches
|
|
110
|
+
.map((branch) => isRecord(branch) && isRecord(branch.properties)
|
|
111
|
+
? branch.properties[key]
|
|
112
|
+
: undefined)
|
|
113
|
+
.filter((property) => property !== undefined);
|
|
114
|
+
if (declared.length === 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
const serialized = declared.map((property) => JSON.stringify(property));
|
|
117
|
+
return serialized.every((text) => text === serialized[0]) &&
|
|
118
|
+
isRecord(declared[0])
|
|
119
|
+
? declared[0]
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
|
122
|
+
function parseJson(text) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(text);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function matchesType(type, value) {
|
|
131
|
+
return type === "array" ? Array.isArray(value) : isRecord(value);
|
|
132
|
+
}
|
|
133
|
+
function isArray(value) {
|
|
134
|
+
return Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
138
|
+
}
|
|
@@ -948,7 +948,8 @@ function assertPlayEvent(value) {
|
|
|
948
948
|
if (value.kind === "tool_call") {
|
|
949
949
|
if (typeof value.callId !== "string" ||
|
|
950
950
|
typeof value.name !== "string" ||
|
|
951
|
-
typeof value.replayed !== "boolean"
|
|
951
|
+
typeof value.replayed !== "boolean" ||
|
|
952
|
+
!validOptionalString(value.executedName))
|
|
952
953
|
throw new Error("Tool-call timeline event has an invalid shape");
|
|
953
954
|
return;
|
|
954
955
|
}
|
|
@@ -960,7 +961,8 @@ function assertPlayEvent(value) {
|
|
|
960
961
|
typeof value.name !== "string" ||
|
|
961
962
|
typeof value.ok !== "boolean" ||
|
|
962
963
|
typeof value.markdown !== "string" ||
|
|
963
|
-
typeof value.replayed !== "boolean"
|
|
964
|
+
typeof value.replayed !== "boolean" ||
|
|
965
|
+
!validOptionalString(value.executedName))
|
|
964
966
|
throw new Error("Tool-result timeline event has an invalid shape");
|
|
965
967
|
return;
|
|
966
968
|
}
|
|
@@ -1014,6 +1016,7 @@ function assertTimelineEventSummary(value) {
|
|
|
1014
1016
|
if (typeof value.callId !== "string" ||
|
|
1015
1017
|
typeof value.name !== "string" ||
|
|
1016
1018
|
typeof value.replayed !== "boolean" ||
|
|
1019
|
+
!validOptionalString(value.executedName) ||
|
|
1017
1020
|
value.detailsAvailable !== true)
|
|
1018
1021
|
throw new Error("Tool-call timeline summary has an invalid shape");
|
|
1019
1022
|
return;
|
|
@@ -1026,6 +1029,7 @@ function assertTimelineEventSummary(value) {
|
|
|
1026
1029
|
typeof value.name !== "string" ||
|
|
1027
1030
|
typeof value.ok !== "boolean" ||
|
|
1028
1031
|
typeof value.replayed !== "boolean" ||
|
|
1032
|
+
!validOptionalString(value.executedName) ||
|
|
1029
1033
|
value.detailsAvailable !== true)
|
|
1030
1034
|
throw new Error("Tool-result timeline summary has an invalid shape");
|
|
1031
1035
|
return;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isMap, isSeq, parseDocument } from "yaml";
|
|
3
|
-
import { builtinPlayPrompts, isPromptMessageRole, promptGroups, } from "../../shared/ordered-play-prompts.js";
|
|
4
|
-
/** One turn-guide line: no line breaks, bounded length; empty clears a builtin default. */
|
|
5
|
-
const turnGuideLine = /^[^\r\n\0]{0,1000}$/u;
|
|
3
|
+
import { builtinPlayPrompts, isPromptMessageRole, isTurnGuideLine, promptGroups, } from "../../shared/ordered-play-prompts.js";
|
|
6
4
|
const promptName = /^[^\r\n\0]{1,160}$/u;
|
|
7
5
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
8
6
|
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
@@ -33,8 +31,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
33
31
|
key === "id" ||
|
|
34
32
|
key === "kind") && keys.every((key) => Object.hasOwn(item, key));
|
|
35
33
|
const validTurnGuide = item.turnGuide === undefined ||
|
|
36
|
-
(typeof item.turnGuide === "string" &&
|
|
37
|
-
turnGuideLine.test(item.turnGuide));
|
|
34
|
+
(typeof item.turnGuide === "string" && isTurnGuideLine(item.turnGuide));
|
|
38
35
|
const turnGuide = typeof item.turnGuide === "string" ? { turnGuide: item.turnGuide } : {};
|
|
39
36
|
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
40
37
|
worlds++;
|
|
@@ -47,7 +44,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
47
44
|
item.name.trim().length > 0 &&
|
|
48
45
|
promptName.test(item.name) &&
|
|
49
46
|
typeof item.body === "string" &&
|
|
50
|
-
|
|
47
|
+
isTurnGuideLine(item.body) &&
|
|
51
48
|
typeof item.enabled === "boolean")
|
|
52
49
|
return [
|
|
53
50
|
{
|
|
@@ -4,11 +4,14 @@ import { isResolutionTool, resolutionMarkdown, resolutionToolNames, resolveTable
|
|
|
4
4
|
import { captureNarrativeOrigin, snapshotBackground, narrativeBackground, } from "../history/HistoryBackground.js";
|
|
5
5
|
import { isDeepStrictEqual } from "node:util";
|
|
6
6
|
import { hasLegacyHistoryTools } from "../prompt/FileNativeToolRegistry.js";
|
|
7
|
+
import { renderToolAliasNote, resolveToolName, } from "../prompt/ToolNameAlias.js";
|
|
7
8
|
import { PlayCallChainError } from "./PlayCallChainError.js";
|
|
8
9
|
import { preparePlayerRevisionTrace, prepareForkTrace, restorePlayDocuments, independentContextCopy, } from "./PlayTraceRetention.js";
|
|
9
10
|
import { cloneSharingPromptRuns, currentPlayPrompt, recordedPlayPrompt, playPromptRunsThroughEvents, promptRunStrings, sharePromptRun, } from "./PlayPromptRun.js";
|
|
10
11
|
import { comparePromptPrefixes } from "../prompt/WorldPromptDiagnostics.js";
|
|
11
12
|
import { createHash, randomBytes } from "node:crypto";
|
|
13
|
+
import { isTurnGuideLine } from "../../shared/ordered-play-prompts.js";
|
|
14
|
+
import { turnGuideWithTurnNote } from "../../shared/turn-guide.js";
|
|
12
15
|
import { completedPlayerRounds, continuationNotice, playerInputAppend, toolStepNotice, turnGuideNotice, } from "./PlayContinuity.js";
|
|
13
16
|
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
14
17
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
@@ -103,6 +106,7 @@ export class PlayCallChain {
|
|
|
103
106
|
validateIdentity(input.chainId, "call-chain ID");
|
|
104
107
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
105
108
|
validatePlayerText(input.playerText);
|
|
109
|
+
validTurnNote(input.turnNote);
|
|
106
110
|
assertModelHostBinding(input.modelBinding, input.modelHost);
|
|
107
111
|
// Only a resubmission to the current context needs that context loaded.
|
|
108
112
|
await this.#worlds.ensureCurrentStorage(input.worldId);
|
|
@@ -152,6 +156,7 @@ export class PlayCallChain {
|
|
|
152
156
|
canRetry: false,
|
|
153
157
|
bootstrap: fresh.bootstrap,
|
|
154
158
|
tools: fresh.tools,
|
|
159
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
155
160
|
transcript: [],
|
|
156
161
|
events: [],
|
|
157
162
|
completedTools: [],
|
|
@@ -243,6 +248,9 @@ export class PlayCallChain {
|
|
|
243
248
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
244
249
|
}
|
|
245
250
|
await this.#refreshPrompt(session, input);
|
|
251
|
+
// Each dispatch carries exactly the request the client sent with it; a
|
|
252
|
+
// continuation without one must not inherit the previous turn's line.
|
|
253
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
246
254
|
session.exchange += 1;
|
|
247
255
|
const last = session.transcript.at(-1);
|
|
248
256
|
if (last?.kind === "assistant" &&
|
|
@@ -251,7 +259,7 @@ export class PlayCallChain {
|
|
|
251
259
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
252
260
|
session.transcript.push(continuationNotice(this.#compiler.locale));
|
|
253
261
|
}
|
|
254
|
-
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, signal, input.observer);
|
|
262
|
+
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange, this.#compiler.locale), 1, signal, input.observer);
|
|
255
263
|
}
|
|
256
264
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
257
265
|
return projectView(session);
|
|
@@ -260,6 +268,7 @@ export class PlayCallChain {
|
|
|
260
268
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
261
269
|
: "The call chain was interrupted; use a fresh context.");
|
|
262
270
|
await this.#refreshPrompt(session, input);
|
|
271
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
263
272
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, signal, input.observer);
|
|
264
273
|
}
|
|
265
274
|
async #refreshPrompt(session, input) {
|
|
@@ -746,6 +755,7 @@ export class PlayCallChain {
|
|
|
746
755
|
eventId: input.eventId,
|
|
747
756
|
replacementExchangeId: input.replacementExchangeId,
|
|
748
757
|
replacementText: input.replacementText,
|
|
758
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
749
759
|
continuation: input.continuation,
|
|
750
760
|
restoresHead: playerRevisionRestoresHead(sourceContext, eventIndex),
|
|
751
761
|
discardChainIds: contexts
|
|
@@ -854,6 +864,7 @@ export class PlayCallChain {
|
|
|
854
864
|
previousContexts: [],
|
|
855
865
|
previousChainId,
|
|
856
866
|
timelineGeneration,
|
|
867
|
+
...turnNoteField(intent.turnNote),
|
|
857
868
|
parentHead: head,
|
|
858
869
|
updatedAt: Date.now(),
|
|
859
870
|
};
|
|
@@ -974,6 +985,7 @@ export class PlayCallChain {
|
|
|
974
985
|
revision: input.freshContext.playPreset.revision,
|
|
975
986
|
},
|
|
976
987
|
followups: structuredClone(fresh.followups),
|
|
988
|
+
...turnNoteField(input.intent.turnNote),
|
|
977
989
|
playPresetScriptsEnabled: input.freshContext.playPreset.scriptsEnabled,
|
|
978
990
|
presetFiles: structuredClone(input.freshContext.playPreset.files),
|
|
979
991
|
modelBinding: structuredClone(input.freshContext.modelBinding),
|
|
@@ -1141,10 +1153,7 @@ export class PlayCallChain {
|
|
|
1141
1153
|
throw new PlayCallChainError("The call chain is waiting for the model to return.");
|
|
1142
1154
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
1143
1155
|
const exchange = session.exchange + 1;
|
|
1144
|
-
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
1145
|
-
...session.transcript,
|
|
1146
|
-
...playerInputAppend(playerText),
|
|
1147
|
-
]);
|
|
1156
|
+
const nextRequest = createRequest(session, modelHost, exchange, this.#compiler.locale, [...session.transcript, ...playerInputAppend(playerText)]);
|
|
1148
1157
|
const operationKey = `player:${exchangeId}`;
|
|
1149
1158
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
1150
1159
|
const advance = {
|
|
@@ -1276,7 +1285,11 @@ export class PlayCallChain {
|
|
|
1276
1285
|
let nextEventId = advance.eventId + 1;
|
|
1277
1286
|
// Fresh in-memory seed per settlement. It is never persisted: a crashed
|
|
1278
1287
|
// exchange is retried by the player and re-rolls (ADR-0044).
|
|
1279
|
-
const
|
|
1288
|
+
const providedToolNames = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
1289
|
+
const resolutionSeed = calls.some(({ name }) => {
|
|
1290
|
+
const resolved = resolveToolName(name, providedToolNames);
|
|
1291
|
+
return resolved.kind !== "rejected" && isResolutionTool(resolved.name);
|
|
1292
|
+
})
|
|
1280
1293
|
? randomBytes(32).toString("hex")
|
|
1281
1294
|
: undefined;
|
|
1282
1295
|
for (const call of calls) {
|
|
@@ -1287,6 +1300,9 @@ export class PlayCallChain {
|
|
|
1287
1300
|
kind: "tool_call",
|
|
1288
1301
|
callId: call.id,
|
|
1289
1302
|
name: call.name,
|
|
1303
|
+
...(item.event.executedName === undefined
|
|
1304
|
+
? {}
|
|
1305
|
+
: { executedName: item.event.executedName }),
|
|
1290
1306
|
arguments: structuredClone(call.arguments),
|
|
1291
1307
|
replayed: item.replayed,
|
|
1292
1308
|
});
|
|
@@ -1348,7 +1364,7 @@ export class PlayCallChain {
|
|
|
1348
1364
|
session.events.some((event) => event.id >
|
|
1349
1365
|
Math.max(previousNarrative?.id ?? 0, previousPlayer?.id ?? 0) &&
|
|
1350
1366
|
event.kind === "tool_result" &&
|
|
1351
|
-
event.name === "world_checkpoint" &&
|
|
1367
|
+
(event.executedName ?? event.name) === "world_checkpoint" &&
|
|
1352
1368
|
event.ok);
|
|
1353
1369
|
const narrativeCheckpoint = checkpointRequested
|
|
1354
1370
|
? {
|
|
@@ -1475,7 +1491,7 @@ export class PlayCallChain {
|
|
|
1475
1491
|
}
|
|
1476
1492
|
if (hasToolCalls) {
|
|
1477
1493
|
session.exchange += 1;
|
|
1478
|
-
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1494
|
+
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange, this.#compiler.locale);
|
|
1479
1495
|
if (session.lastRequest?.modelConfig !== undefined)
|
|
1480
1496
|
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1481
1497
|
await this.#advances.end(session, {
|
|
@@ -1942,14 +1958,16 @@ export class PlayCallChain {
|
|
|
1942
1958
|
function followupOperationId(session) {
|
|
1943
1959
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
1944
1960
|
}
|
|
1945
|
-
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1946
|
-
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1961
|
+
function createRequest(session, modelHost, exchange, locale, appended = session.transcript) {
|
|
1962
|
+
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, locale, appended);
|
|
1947
1963
|
return modelHost.freezeRequest?.(request) ?? request;
|
|
1948
1964
|
}
|
|
1949
|
-
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1965
|
+
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, locale, appended = session.transcript) {
|
|
1950
1966
|
// The turn guide is a request-level suffix: it follows whatever this
|
|
1951
|
-
// dispatch appends and is never part of the transcript it is built on.
|
|
1952
|
-
|
|
1967
|
+
// dispatch appends and is never part of the transcript it is built on. The
|
|
1968
|
+
// player's one-turn request joins the suffix here, not in the bootstrap, so
|
|
1969
|
+
// the bootstrap snapshot stays shared by every dispatch of the turn.
|
|
1970
|
+
const turnGuide = turnGuideWithTurnNote(locale, currentPlayPrompt(session).bootstrap.turnGuide, session.turnNote);
|
|
1953
1971
|
return {
|
|
1954
1972
|
bootstrap: structuredClone(currentPlayPrompt(session).bootstrap),
|
|
1955
1973
|
tools: structuredClone(currentPlayPrompt(session).tools),
|
|
@@ -2043,13 +2061,17 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
2043
2061
|
function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
2044
2062
|
const key = `${exchange}:${call.id}`;
|
|
2045
2063
|
const signature = JSON.stringify(call.arguments);
|
|
2064
|
+
const provided = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
2065
|
+
const resolved = resolveToolName(call.name, provided);
|
|
2066
|
+
// The executed name, not the model's spelling, decides replay identity.
|
|
2067
|
+
const executedName = resolved.kind === "rejected" ? call.name : resolved.name;
|
|
2046
2068
|
const completed = working.get(key);
|
|
2047
2069
|
let result;
|
|
2048
2070
|
let replayed = false;
|
|
2049
2071
|
if (completed !== undefined) {
|
|
2050
2072
|
replayed = true;
|
|
2051
2073
|
result =
|
|
2052
|
-
completed.name ===
|
|
2074
|
+
completed.name === executedName && completed.signature === signature
|
|
2053
2075
|
? structuredClone(completed.result)
|
|
2054
2076
|
: {
|
|
2055
2077
|
ok: false,
|
|
@@ -2057,19 +2079,19 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2057
2079
|
markdown: "# Runtime tool rejected\n\nThe same tool-call ID is already bound to another tool name or argument set.",
|
|
2058
2080
|
};
|
|
2059
2081
|
}
|
|
2060
|
-
else if (
|
|
2061
|
-
!
|
|
2082
|
+
else if (resolved.kind === "rejected" ||
|
|
2083
|
+
!callChainToolNames.has(executedName)) {
|
|
2062
2084
|
result = {
|
|
2063
2085
|
ok: false,
|
|
2064
2086
|
failureKind: "protocol",
|
|
2065
|
-
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}.`,
|
|
2087
|
+
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}. Available tools: ${provided.join(", ")}.`,
|
|
2066
2088
|
};
|
|
2067
2089
|
}
|
|
2068
2090
|
else {
|
|
2069
2091
|
try {
|
|
2070
|
-
result = isResolutionTool(
|
|
2071
|
-
? resolutionToolResult(
|
|
2072
|
-
:
|
|
2092
|
+
result = isResolutionTool(executedName)
|
|
2093
|
+
? resolutionToolResult(executedName, call.arguments, resolutionSeed, key, locale)
|
|
2094
|
+
: executedName === "world_checkpoint"
|
|
2073
2095
|
? typeof call.arguments === "object" &&
|
|
2074
2096
|
call.arguments !== null &&
|
|
2075
2097
|
!Array.isArray(call.arguments) &&
|
|
@@ -2085,7 +2107,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2085
2107
|
failureKind: "protocol",
|
|
2086
2108
|
markdown: "world_checkpoint requires an empty argument object.",
|
|
2087
2109
|
}
|
|
2088
|
-
: session.documents.execute(call, session.history, {
|
|
2110
|
+
: session.documents.execute({ ...call, name: executedName }, session.history, {
|
|
2089
2111
|
historyScope: session.worldId,
|
|
2090
2112
|
locale,
|
|
2091
2113
|
legacyHistoryTools: hasLegacyHistoryTools(currentPlayPrompt(session).tools),
|
|
@@ -2098,9 +2120,14 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2098
2120
|
markdown: `# Runtime tool failed\n\n${error instanceof Error ? error.message : "Tool execution failed."}`,
|
|
2099
2121
|
};
|
|
2100
2122
|
}
|
|
2123
|
+
if (resolved.kind === "alias")
|
|
2124
|
+
result = {
|
|
2125
|
+
...result,
|
|
2126
|
+
markdown: `${result.markdown}\n\n${renderToolAliasNote(call.name, executedName, locale)}`,
|
|
2127
|
+
};
|
|
2101
2128
|
working.set(key, {
|
|
2102
2129
|
key,
|
|
2103
|
-
name:
|
|
2130
|
+
name: executedName,
|
|
2104
2131
|
signature,
|
|
2105
2132
|
result: structuredClone(result),
|
|
2106
2133
|
});
|
|
@@ -2110,6 +2137,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2110
2137
|
kind: "tool_result",
|
|
2111
2138
|
callId: call.id,
|
|
2112
2139
|
name: call.name,
|
|
2140
|
+
...(executedName === call.name ? {} : { executedName }),
|
|
2113
2141
|
ok: result.ok,
|
|
2114
2142
|
markdown: result.markdown,
|
|
2115
2143
|
...(result.resolution === undefined
|
|
@@ -2240,7 +2268,8 @@ function projectContextReads(session) {
|
|
|
2240
2268
|
exchange = event.exchange;
|
|
2241
2269
|
continue;
|
|
2242
2270
|
}
|
|
2243
|
-
if (event.kind === "tool_call" &&
|
|
2271
|
+
if (event.kind === "tool_call" &&
|
|
2272
|
+
(event.executedName ?? event.name) === "context_read") {
|
|
2244
2273
|
const ref = isRecord(event.arguments) && typeof event.arguments.ref === "string"
|
|
2245
2274
|
? event.arguments.ref
|
|
2246
2275
|
: "(invalid ref)";
|
|
@@ -2250,7 +2279,8 @@ function projectContextReads(session) {
|
|
|
2250
2279
|
});
|
|
2251
2280
|
continue;
|
|
2252
2281
|
}
|
|
2253
|
-
if (event.kind !== "tool_result" ||
|
|
2282
|
+
if (event.kind !== "tool_result" ||
|
|
2283
|
+
(event.executedName ?? event.name) !== "context_read")
|
|
2254
2284
|
continue;
|
|
2255
2285
|
const call = calls.get(event.callId);
|
|
2256
2286
|
if (call === undefined)
|
|
@@ -2354,6 +2384,28 @@ function validatePlayerText(value) {
|
|
|
2354
2384
|
if (value.trim().length === 0)
|
|
2355
2385
|
throw new PlayCallChainError("Player text must not be empty.");
|
|
2356
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* The player's one-turn request: one line, bounded like a turn-guide line.
|
|
2389
|
+
* Blank input means no request. Length beyond the soft limit is only warned
|
|
2390
|
+
* about by the client and Prompt Preview, never rejected here.
|
|
2391
|
+
*/
|
|
2392
|
+
function validTurnNote(value) {
|
|
2393
|
+
if (value === undefined)
|
|
2394
|
+
return undefined;
|
|
2395
|
+
if (!isTurnGuideLine(value))
|
|
2396
|
+
throw new PlayCallChainError("The turn request must be a single line of at most 1000 characters.");
|
|
2397
|
+
const trimmed = value.trim();
|
|
2398
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
2399
|
+
}
|
|
2400
|
+
function setTurnNote(session, turnNote) {
|
|
2401
|
+
if (turnNote === undefined)
|
|
2402
|
+
delete session.turnNote;
|
|
2403
|
+
else
|
|
2404
|
+
session.turnNote = turnNote;
|
|
2405
|
+
}
|
|
2406
|
+
function turnNoteField(turnNote) {
|
|
2407
|
+
return turnNote === undefined ? {} : { turnNote };
|
|
2408
|
+
}
|
|
2357
2409
|
function touch(session) {
|
|
2358
2410
|
session.updatedAt = Date.now();
|
|
2359
2411
|
}
|
|
@@ -5,6 +5,8 @@ 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
11
|
import {} from "../world/WorldDocumentStore.js";
|
|
10
12
|
import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
|
|
@@ -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) ||
|
|
@@ -419,6 +436,8 @@ function applyPlayCandidateCreate(candidate, authorizations, args, declaredDirec
|
|
|
419
436
|
const parentDirectory = record(args) && typeof args.parent === "string"
|
|
420
437
|
? parseStateDirectoryHandle(args.parent)
|
|
421
438
|
: null;
|
|
439
|
+
if (record(args) && typeof args.aliases === "string")
|
|
440
|
+
throw new PlayDocumentToolFailure("world_create.aliases must be a JSON array of strings, not a string");
|
|
422
441
|
if (!record(args) ||
|
|
423
442
|
typeof args.parent !== "string" ||
|
|
424
443
|
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";
|
|
@@ -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
|
*
|
|
@@ -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
|
+
}
|