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
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
|
+
}
|
|
@@ -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) {
|
|
@@ -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,13 +4,15 @@ 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
|
-
import { renderFreshContextCoverage } from "../prompt/WorldMaterialCoverage.js";
|
|
14
16
|
import { renderWriteSizeAdvice, readDeclaredWorldClock, } from "../prompt/WorldMaintenanceReport.js";
|
|
15
17
|
import { WorldDocumentStore } from "../world/WorldDocumentStore.js";
|
|
16
18
|
import { equalModelHostBinding, ModelHostCancelledError, } from "../model/ModelHost.js";
|
|
@@ -103,6 +105,7 @@ export class PlayCallChain {
|
|
|
103
105
|
validateIdentity(input.chainId, "call-chain ID");
|
|
104
106
|
validateIdentity(input.exchangeId, "exchange ID");
|
|
105
107
|
validatePlayerText(input.playerText);
|
|
108
|
+
validTurnNote(input.turnNote);
|
|
106
109
|
assertModelHostBinding(input.modelBinding, input.modelHost);
|
|
107
110
|
// Only a resubmission to the current context needs that context loaded.
|
|
108
111
|
await this.#worlds.ensureCurrentStorage(input.worldId);
|
|
@@ -152,6 +155,7 @@ export class PlayCallChain {
|
|
|
152
155
|
canRetry: false,
|
|
153
156
|
bootstrap: fresh.bootstrap,
|
|
154
157
|
tools: fresh.tools,
|
|
158
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
155
159
|
transcript: [],
|
|
156
160
|
events: [],
|
|
157
161
|
completedTools: [],
|
|
@@ -243,6 +247,9 @@ export class PlayCallChain {
|
|
|
243
247
|
return this.#dispatch(session, input.modelHost, structuredClone(session.lastRequest), session.lastRequestAttempt + 1, signal, input.observer);
|
|
244
248
|
}
|
|
245
249
|
await this.#refreshPrompt(session, input);
|
|
250
|
+
// Each dispatch carries exactly the request the client sent with it; a
|
|
251
|
+
// continuation without one must not inherit the previous turn's line.
|
|
252
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
246
253
|
session.exchange += 1;
|
|
247
254
|
const last = session.transcript.at(-1);
|
|
248
255
|
if (last?.kind === "assistant" &&
|
|
@@ -251,7 +258,7 @@ export class PlayCallChain {
|
|
|
251
258
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
252
259
|
session.transcript.push(continuationNotice(this.#compiler.locale));
|
|
253
260
|
}
|
|
254
|
-
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange), 1, signal, input.observer);
|
|
261
|
+
return this.#dispatch(session, input.modelHost, createRequest(session, input.modelHost, session.exchange, this.#compiler.locale), 1, signal, input.observer);
|
|
255
262
|
}
|
|
256
263
|
if (duplicatePlayerExchange(session, input.exchangeId, input.playerText, "append"))
|
|
257
264
|
return projectView(session);
|
|
@@ -260,6 +267,7 @@ export class PlayCallChain {
|
|
|
260
267
|
? "The call chain has an incomplete model response; clear the input and append a continuation, or use a fresh context."
|
|
261
268
|
: "The call chain was interrupted; use a fresh context.");
|
|
262
269
|
await this.#refreshPrompt(session, input);
|
|
270
|
+
setTurnNote(session, validTurnNote(input.turnNote));
|
|
263
271
|
return this.#submitPlayer(session, input.exchangeId, input.playerText, "append", input.modelHost, signal, input.observer);
|
|
264
272
|
}
|
|
265
273
|
async #refreshPrompt(session, input) {
|
|
@@ -746,6 +754,7 @@ export class PlayCallChain {
|
|
|
746
754
|
eventId: input.eventId,
|
|
747
755
|
replacementExchangeId: input.replacementExchangeId,
|
|
748
756
|
replacementText: input.replacementText,
|
|
757
|
+
...turnNoteField(validTurnNote(input.turnNote)),
|
|
749
758
|
continuation: input.continuation,
|
|
750
759
|
restoresHead: playerRevisionRestoresHead(sourceContext, eventIndex),
|
|
751
760
|
discardChainIds: contexts
|
|
@@ -854,6 +863,7 @@ export class PlayCallChain {
|
|
|
854
863
|
previousContexts: [],
|
|
855
864
|
previousChainId,
|
|
856
865
|
timelineGeneration,
|
|
866
|
+
...turnNoteField(intent.turnNote),
|
|
857
867
|
parentHead: head,
|
|
858
868
|
updatedAt: Date.now(),
|
|
859
869
|
};
|
|
@@ -974,6 +984,7 @@ export class PlayCallChain {
|
|
|
974
984
|
revision: input.freshContext.playPreset.revision,
|
|
975
985
|
},
|
|
976
986
|
followups: structuredClone(fresh.followups),
|
|
987
|
+
...turnNoteField(input.intent.turnNote),
|
|
977
988
|
playPresetScriptsEnabled: input.freshContext.playPreset.scriptsEnabled,
|
|
978
989
|
presetFiles: structuredClone(input.freshContext.playPreset.files),
|
|
979
990
|
modelBinding: structuredClone(input.freshContext.modelBinding),
|
|
@@ -1141,10 +1152,7 @@ export class PlayCallChain {
|
|
|
1141
1152
|
throw new PlayCallChainError("The call chain is waiting for the model to return.");
|
|
1142
1153
|
session.narrativeOrigin = await this.#captureOrigin(session);
|
|
1143
1154
|
const exchange = session.exchange + 1;
|
|
1144
|
-
const nextRequest = createRequest(session, modelHost, exchange, [
|
|
1145
|
-
...session.transcript,
|
|
1146
|
-
...playerInputAppend(playerText),
|
|
1147
|
-
]);
|
|
1155
|
+
const nextRequest = createRequest(session, modelHost, exchange, this.#compiler.locale, [...session.transcript, ...playerInputAppend(playerText)]);
|
|
1148
1156
|
const operationKey = `player:${exchangeId}`;
|
|
1149
1157
|
const playerOperationId = operationId(session.chainId, operationKey);
|
|
1150
1158
|
const advance = {
|
|
@@ -1276,7 +1284,11 @@ export class PlayCallChain {
|
|
|
1276
1284
|
let nextEventId = advance.eventId + 1;
|
|
1277
1285
|
// Fresh in-memory seed per settlement. It is never persisted: a crashed
|
|
1278
1286
|
// exchange is retried by the player and re-rolls (ADR-0044).
|
|
1279
|
-
const
|
|
1287
|
+
const providedToolNames = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
1288
|
+
const resolutionSeed = calls.some(({ name }) => {
|
|
1289
|
+
const resolved = resolveToolName(name, providedToolNames);
|
|
1290
|
+
return resolved.kind !== "rejected" && isResolutionTool(resolved.name);
|
|
1291
|
+
})
|
|
1280
1292
|
? randomBytes(32).toString("hex")
|
|
1281
1293
|
: undefined;
|
|
1282
1294
|
for (const call of calls) {
|
|
@@ -1287,6 +1299,9 @@ export class PlayCallChain {
|
|
|
1287
1299
|
kind: "tool_call",
|
|
1288
1300
|
callId: call.id,
|
|
1289
1301
|
name: call.name,
|
|
1302
|
+
...(item.event.executedName === undefined
|
|
1303
|
+
? {}
|
|
1304
|
+
: { executedName: item.event.executedName }),
|
|
1290
1305
|
arguments: structuredClone(call.arguments),
|
|
1291
1306
|
replayed: item.replayed,
|
|
1292
1307
|
});
|
|
@@ -1313,7 +1328,7 @@ export class PlayCallChain {
|
|
|
1313
1328
|
],
|
|
1314
1329
|
});
|
|
1315
1330
|
worldClock = readDeclaredWorldClock(snapshot);
|
|
1316
|
-
const {
|
|
1331
|
+
const { maintenance } = this.#compiler.inspectWorldMaterials({
|
|
1317
1332
|
controlFingerprint: "receipt",
|
|
1318
1333
|
documentSnapshot: snapshot,
|
|
1319
1334
|
history: Object.fromEntries(session.history.map(({ path, contents }) => [path, contents])),
|
|
@@ -1321,23 +1336,18 @@ export class PlayCallChain {
|
|
|
1321
1336
|
});
|
|
1322
1337
|
for (const { result } of writes) {
|
|
1323
1338
|
const write = result.candidateWrite;
|
|
1324
|
-
write.freshContextCoverage = renderFreshContextCoverage(snapshot, coverage, write.shortRef, this.#compiler.locale);
|
|
1325
1339
|
const previous = session.documents.committedSnapshot.query({
|
|
1326
1340
|
kind: "read_document",
|
|
1327
1341
|
document: { shortRef: write.shortRef },
|
|
1328
1342
|
});
|
|
1329
|
-
|
|
1343
|
+
write.sizeAdvice = renderWriteSizeAdvice(maintenance, write.shortRef, previous.kind === "read_document"
|
|
1330
1344
|
? Buffer.byteLength(previous.body, "utf8")
|
|
1331
1345
|
: null, this.#compiler.locale);
|
|
1332
|
-
if (advice.length > 0)
|
|
1333
|
-
write.freshContextCoverage += `\n${advice}`;
|
|
1334
1346
|
}
|
|
1335
1347
|
}
|
|
1336
|
-
catch
|
|
1337
|
-
//
|
|
1338
|
-
|
|
1339
|
-
for (const { result } of writes)
|
|
1340
|
-
result.candidateWrite.freshContextCoverage = `${this.#compiler.locale === "zh-CN" ? "暂时无法判定下次注入;写入结果不受影响。原因" : "Fresh-context coverage unavailable; the write result is unaffected. Reason"}: ${reason}`;
|
|
1348
|
+
catch {
|
|
1349
|
+
// Maintenance advice is diagnostic: a failure omits it and never
|
|
1350
|
+
// rejects or undoes an accepted write.
|
|
1341
1351
|
}
|
|
1342
1352
|
}
|
|
1343
1353
|
const previousNarrative = session.events.findLast((event) => event.kind === "assistant" &&
|
|
@@ -1348,7 +1358,7 @@ export class PlayCallChain {
|
|
|
1348
1358
|
session.events.some((event) => event.id >
|
|
1349
1359
|
Math.max(previousNarrative?.id ?? 0, previousPlayer?.id ?? 0) &&
|
|
1350
1360
|
event.kind === "tool_result" &&
|
|
1351
|
-
event.name === "world_checkpoint" &&
|
|
1361
|
+
(event.executedName ?? event.name) === "world_checkpoint" &&
|
|
1352
1362
|
event.ok);
|
|
1353
1363
|
const narrativeCheckpoint = checkpointRequested
|
|
1354
1364
|
? {
|
|
@@ -1475,7 +1485,7 @@ export class PlayCallChain {
|
|
|
1475
1485
|
}
|
|
1476
1486
|
if (hasToolCalls) {
|
|
1477
1487
|
session.exchange += 1;
|
|
1478
|
-
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange);
|
|
1488
|
+
const nextRequest = createRequestWithMaxOutputTokens(session, session.lastRequest?.maxOutputTokens ?? 1, session.exchange, this.#compiler.locale);
|
|
1479
1489
|
if (session.lastRequest?.modelConfig !== undefined)
|
|
1480
1490
|
nextRequest.modelConfig = structuredClone(session.lastRequest.modelConfig);
|
|
1481
1491
|
await this.#advances.end(session, {
|
|
@@ -1942,14 +1952,16 @@ export class PlayCallChain {
|
|
|
1942
1952
|
function followupOperationId(session) {
|
|
1943
1953
|
return `${session.chainId}:followup:${session.parentHead}`;
|
|
1944
1954
|
}
|
|
1945
|
-
function createRequest(session, modelHost, exchange, appended = session.transcript) {
|
|
1946
|
-
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, appended);
|
|
1955
|
+
function createRequest(session, modelHost, exchange, locale, appended = session.transcript) {
|
|
1956
|
+
const request = createRequestWithMaxOutputTokens(session, modelHost.binding().maxOutputTokens, exchange, locale, appended);
|
|
1947
1957
|
return modelHost.freezeRequest?.(request) ?? request;
|
|
1948
1958
|
}
|
|
1949
|
-
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, appended = session.transcript) {
|
|
1959
|
+
function createRequestWithMaxOutputTokens(session, maxOutputTokens, exchange, locale, appended = session.transcript) {
|
|
1950
1960
|
// 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
|
-
|
|
1961
|
+
// dispatch appends and is never part of the transcript it is built on. The
|
|
1962
|
+
// player's one-turn request joins the suffix here, not in the bootstrap, so
|
|
1963
|
+
// the bootstrap snapshot stays shared by every dispatch of the turn.
|
|
1964
|
+
const turnGuide = turnGuideWithTurnNote(locale, currentPlayPrompt(session).bootstrap.turnGuide, session.turnNote);
|
|
1953
1965
|
return {
|
|
1954
1966
|
bootstrap: structuredClone(currentPlayPrompt(session).bootstrap),
|
|
1955
1967
|
tools: structuredClone(currentPlayPrompt(session).tools),
|
|
@@ -1997,7 +2009,7 @@ function finalizeWriteReceipts(settlement) {
|
|
|
1997
2009
|
const status = candidateWrite.changed && committedWriteRefs.has(candidateWrite.shortRef)
|
|
1998
2010
|
? `@${candidateWrite.shortRef} write succeeded`
|
|
1999
2011
|
: `@${candidateWrite.shortRef} unchanged`;
|
|
2000
|
-
const markdown = [status, candidateWrite.
|
|
2012
|
+
const markdown = [status, candidateWrite.sizeAdvice]
|
|
2001
2013
|
.filter(Boolean)
|
|
2002
2014
|
.join("\n");
|
|
2003
2015
|
completed.result.markdown = markdown;
|
|
@@ -2043,13 +2055,17 @@ function appendCommittedHistory(session, head, messages) {
|
|
|
2043
2055
|
function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
2044
2056
|
const key = `${exchange}:${call.id}`;
|
|
2045
2057
|
const signature = JSON.stringify(call.arguments);
|
|
2058
|
+
const provided = currentPlayPrompt(session).tools.map(({ name }) => name);
|
|
2059
|
+
const resolved = resolveToolName(call.name, provided);
|
|
2060
|
+
// The executed name, not the model's spelling, decides replay identity.
|
|
2061
|
+
const executedName = resolved.kind === "rejected" ? call.name : resolved.name;
|
|
2046
2062
|
const completed = working.get(key);
|
|
2047
2063
|
let result;
|
|
2048
2064
|
let replayed = false;
|
|
2049
2065
|
if (completed !== undefined) {
|
|
2050
2066
|
replayed = true;
|
|
2051
2067
|
result =
|
|
2052
|
-
completed.name ===
|
|
2068
|
+
completed.name === executedName && completed.signature === signature
|
|
2053
2069
|
? structuredClone(completed.result)
|
|
2054
2070
|
: {
|
|
2055
2071
|
ok: false,
|
|
@@ -2057,19 +2073,19 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2057
2073
|
markdown: "# Runtime tool rejected\n\nThe same tool-call ID is already bound to another tool name or argument set.",
|
|
2058
2074
|
};
|
|
2059
2075
|
}
|
|
2060
|
-
else if (
|
|
2061
|
-
!
|
|
2076
|
+
else if (resolved.kind === "rejected" ||
|
|
2077
|
+
!callChainToolNames.has(executedName)) {
|
|
2062
2078
|
result = {
|
|
2063
2079
|
ok: false,
|
|
2064
2080
|
failureKind: "protocol",
|
|
2065
|
-
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}.`,
|
|
2081
|
+
markdown: `# Runtime tool rejected\n\nThe current call chain does not provide ${call.name}. Available tools: ${provided.join(", ")}.`,
|
|
2066
2082
|
};
|
|
2067
2083
|
}
|
|
2068
2084
|
else {
|
|
2069
2085
|
try {
|
|
2070
|
-
result = isResolutionTool(
|
|
2071
|
-
? resolutionToolResult(
|
|
2072
|
-
:
|
|
2086
|
+
result = isResolutionTool(executedName)
|
|
2087
|
+
? resolutionToolResult(executedName, call.arguments, resolutionSeed, key, locale)
|
|
2088
|
+
: executedName === "world_checkpoint"
|
|
2073
2089
|
? typeof call.arguments === "object" &&
|
|
2074
2090
|
call.arguments !== null &&
|
|
2075
2091
|
!Array.isArray(call.arguments) &&
|
|
@@ -2085,7 +2101,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2085
2101
|
failureKind: "protocol",
|
|
2086
2102
|
markdown: "world_checkpoint requires an empty argument object.",
|
|
2087
2103
|
}
|
|
2088
|
-
: session.documents.execute(call, session.history, {
|
|
2104
|
+
: session.documents.execute({ ...call, name: executedName }, session.history, {
|
|
2089
2105
|
historyScope: session.worldId,
|
|
2090
2106
|
locale,
|
|
2091
2107
|
legacyHistoryTools: hasLegacyHistoryTools(currentPlayPrompt(session).tools),
|
|
@@ -2098,9 +2114,14 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2098
2114
|
markdown: `# Runtime tool failed\n\n${error instanceof Error ? error.message : "Tool execution failed."}`,
|
|
2099
2115
|
};
|
|
2100
2116
|
}
|
|
2117
|
+
if (resolved.kind === "alias")
|
|
2118
|
+
result = {
|
|
2119
|
+
...result,
|
|
2120
|
+
markdown: `${result.markdown}\n\n${renderToolAliasNote(call.name, executedName, locale)}`,
|
|
2121
|
+
};
|
|
2101
2122
|
working.set(key, {
|
|
2102
2123
|
key,
|
|
2103
|
-
name:
|
|
2124
|
+
name: executedName,
|
|
2104
2125
|
signature,
|
|
2105
2126
|
result: structuredClone(result),
|
|
2106
2127
|
});
|
|
@@ -2110,6 +2131,7 @@ function prepareTool(session, working, exchange, call, locale, resolutionSeed) {
|
|
|
2110
2131
|
kind: "tool_result",
|
|
2111
2132
|
callId: call.id,
|
|
2112
2133
|
name: call.name,
|
|
2134
|
+
...(executedName === call.name ? {} : { executedName }),
|
|
2113
2135
|
ok: result.ok,
|
|
2114
2136
|
markdown: result.markdown,
|
|
2115
2137
|
...(result.resolution === undefined
|
|
@@ -2240,7 +2262,8 @@ function projectContextReads(session) {
|
|
|
2240
2262
|
exchange = event.exchange;
|
|
2241
2263
|
continue;
|
|
2242
2264
|
}
|
|
2243
|
-
if (event.kind === "tool_call" &&
|
|
2265
|
+
if (event.kind === "tool_call" &&
|
|
2266
|
+
(event.executedName ?? event.name) === "context_read") {
|
|
2244
2267
|
const ref = isRecord(event.arguments) && typeof event.arguments.ref === "string"
|
|
2245
2268
|
? event.arguments.ref
|
|
2246
2269
|
: "(invalid ref)";
|
|
@@ -2250,7 +2273,8 @@ function projectContextReads(session) {
|
|
|
2250
2273
|
});
|
|
2251
2274
|
continue;
|
|
2252
2275
|
}
|
|
2253
|
-
if (event.kind !== "tool_result" ||
|
|
2276
|
+
if (event.kind !== "tool_result" ||
|
|
2277
|
+
(event.executedName ?? event.name) !== "context_read")
|
|
2254
2278
|
continue;
|
|
2255
2279
|
const call = calls.get(event.callId);
|
|
2256
2280
|
if (call === undefined)
|
|
@@ -2354,6 +2378,28 @@ function validatePlayerText(value) {
|
|
|
2354
2378
|
if (value.trim().length === 0)
|
|
2355
2379
|
throw new PlayCallChainError("Player text must not be empty.");
|
|
2356
2380
|
}
|
|
2381
|
+
/**
|
|
2382
|
+
* The player's one-turn request: one line, bounded like a turn-guide line.
|
|
2383
|
+
* Blank input means no request. Length beyond the soft limit is only warned
|
|
2384
|
+
* about by the client and Prompt Preview, never rejected here.
|
|
2385
|
+
*/
|
|
2386
|
+
function validTurnNote(value) {
|
|
2387
|
+
if (value === undefined)
|
|
2388
|
+
return undefined;
|
|
2389
|
+
if (!isTurnGuideLine(value))
|
|
2390
|
+
throw new PlayCallChainError("The turn request must be a single line of at most 1000 characters.");
|
|
2391
|
+
const trimmed = value.trim();
|
|
2392
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
2393
|
+
}
|
|
2394
|
+
function setTurnNote(session, turnNote) {
|
|
2395
|
+
if (turnNote === undefined)
|
|
2396
|
+
delete session.turnNote;
|
|
2397
|
+
else
|
|
2398
|
+
session.turnNote = turnNote;
|
|
2399
|
+
}
|
|
2400
|
+
function turnNoteField(turnNote) {
|
|
2401
|
+
return turnNote === undefined ? {} : { turnNote };
|
|
2402
|
+
}
|
|
2357
2403
|
function touch(session) {
|
|
2358
2404
|
session.updatedAt = Date.now();
|
|
2359
2405
|
}
|