narraeon 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/runtime/model/FileNativeModelAdapters.js +85 -6
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +23 -1
- package/dist/node/runtime/play/OrderedPlayPrompts.js +13 -4
- package/dist/node/runtime/play/PlayCallChain.js +6 -12
- package/dist/node/runtime/play/PlayDocumentTools.js +1 -31
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +213 -52
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +4 -4
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +13 -3
- package/dist/node/runtime/world/StateDirectoryHandles.js +31 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -19
- package/dist/node/shared/ordered-play-prompts.js +5 -2
- package/dist/web/assets/index-CVN48xrw.css +1 -0
- package/dist/web/assets/index-CVllMgyP.js +1207 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DG85hpWC.css +0 -1
- package/dist/web/assets/index-DfxvCjb6.js +0 -1207
|
@@ -473,7 +473,7 @@ function anthropicRequestBody(connection, request) {
|
|
|
473
473
|
...anthropicReasoningRequest(connection),
|
|
474
474
|
system: request.bootstrap.provider.system,
|
|
475
475
|
messages: [
|
|
476
|
-
...request
|
|
476
|
+
...anthropicBootstrapMessages(request),
|
|
477
477
|
...anthropicAppend(request),
|
|
478
478
|
],
|
|
479
479
|
...(tools.length === 0
|
|
@@ -602,15 +602,94 @@ function providerToolStrategy(request, provider) {
|
|
|
602
602
|
}
|
|
603
603
|
function chatBootstrapMessages(connection, request) {
|
|
604
604
|
const messages = request.bootstrap.provider.messages.map((message) => cloneProviderValue(message));
|
|
605
|
+
const prefixEnd = bootstrapPrefixEnd(request);
|
|
606
|
+
messages.splice(prefixEnd, 0, ...initialWorldContext(request, "chat_completions"));
|
|
605
607
|
return connection.dialect === "cliproxyapi"
|
|
606
|
-
? attachCacheControlToLastRole(messages, "system")
|
|
608
|
+
? attachCacheControlToLastRole(messages, request.bootstrap.provider.preload === undefined ? "system" : "tool")
|
|
607
609
|
: messages;
|
|
608
610
|
}
|
|
609
611
|
function responsesBootstrapMessages(connection, request) {
|
|
610
612
|
const messages = request.bootstrap.provider.messages.map((message) => responsesBootstrapMessage(message, connection.dialect === "cliproxyapi"));
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
613
|
+
const prefixEnd = bootstrapPrefixEnd(request);
|
|
614
|
+
// Legacy material arrays already mark their stable blocks, and may also contain
|
|
615
|
+
// player input. Only mark whole messages for the plain-text instruction prefix.
|
|
616
|
+
const cacheMessageIndex = request.bootstrap.provider.messages
|
|
617
|
+
.slice(0, prefixEnd)
|
|
618
|
+
.findLastIndex((message) => typeof message.content === "string");
|
|
619
|
+
const prefix = messages.map((message, index) => connection.dialect === "cliproxyapi" &&
|
|
620
|
+
index === cacheMessageIndex &&
|
|
621
|
+
isRecord(message)
|
|
622
|
+
? { ...message, cache_control: { type: "ephemeral" } }
|
|
623
|
+
: message);
|
|
624
|
+
prefix.splice(prefixEnd, 0, ...initialWorldContext(request, "openai_responses"));
|
|
625
|
+
return prefix;
|
|
626
|
+
}
|
|
627
|
+
/** Configured User prompts are part of the prefix, not the player's first input. */
|
|
628
|
+
function bootstrapPrefixEnd(request) {
|
|
629
|
+
return (request.bootstrap.provider.messages.length -
|
|
630
|
+
request.bootstrap.logicalMessages.filter(({ role }) => role === "player_input").length);
|
|
631
|
+
}
|
|
632
|
+
function anthropicBootstrapMessages(request) {
|
|
633
|
+
const messages = request.bootstrap.provider.messages.map((message) => cloneProviderValue(message));
|
|
634
|
+
messages.splice(bootstrapPrefixEnd(request), 0, ...initialWorldContext(request, "anthropic_messages"));
|
|
635
|
+
return messages.filter((message) => !isRecord(message) || message.role !== "system");
|
|
636
|
+
}
|
|
637
|
+
/** Synthetic bootstrap only: never manufacture a provider continuation or a story event. */
|
|
638
|
+
function initialWorldContext(request, protocol) {
|
|
639
|
+
const preload = request.bootstrap.provider.preload;
|
|
640
|
+
if (preload === undefined)
|
|
641
|
+
return [];
|
|
642
|
+
const name = "runtime_load_context";
|
|
643
|
+
const notice = {
|
|
644
|
+
role: "user",
|
|
645
|
+
content: "Runtime preload: supply the initial world materials selected by the frame. This bootstrap-only read is not a player action or a callable tool. Later successful tool results update these initial values.",
|
|
646
|
+
};
|
|
647
|
+
if (protocol === "chat_completions")
|
|
648
|
+
return [
|
|
649
|
+
notice,
|
|
650
|
+
{
|
|
651
|
+
role: "assistant",
|
|
652
|
+
content: null,
|
|
653
|
+
tool_calls: [
|
|
654
|
+
{
|
|
655
|
+
id: preload.callId,
|
|
656
|
+
type: "function",
|
|
657
|
+
function: { name, arguments: "{}" },
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
{ role: "tool", tool_call_id: preload.callId, content: preload.markdown },
|
|
662
|
+
];
|
|
663
|
+
if (protocol === "openai_responses")
|
|
664
|
+
return [
|
|
665
|
+
notice,
|
|
666
|
+
{ type: "function_call", call_id: preload.callId, name, arguments: "{}" },
|
|
667
|
+
{
|
|
668
|
+
type: "function_call_output",
|
|
669
|
+
call_id: preload.callId,
|
|
670
|
+
output: preload.markdown,
|
|
671
|
+
},
|
|
672
|
+
];
|
|
673
|
+
return [
|
|
674
|
+
notice,
|
|
675
|
+
{
|
|
676
|
+
role: "assistant",
|
|
677
|
+
content: [{ type: "tool_use", id: preload.callId, name, input: {} }],
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
role: "user",
|
|
681
|
+
content: [
|
|
682
|
+
{
|
|
683
|
+
type: "tool_result",
|
|
684
|
+
tool_use_id: preload.callId,
|
|
685
|
+
content: preload.markdown,
|
|
686
|
+
...(request.bootstrap.cache.strategy === "explicit_anthropic_blocks"
|
|
687
|
+
? { cache_control: { type: "ephemeral" } }
|
|
688
|
+
: {}),
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
},
|
|
692
|
+
];
|
|
614
693
|
}
|
|
615
694
|
function attachCacheControlToLastRole(messages, role) {
|
|
616
695
|
const result = cloneProviderValue(messages);
|
|
@@ -858,7 +937,7 @@ function responsesBootstrapMessage(message, preserveCacheControl = false) {
|
|
|
858
937
|
return [];
|
|
859
938
|
return [
|
|
860
939
|
{
|
|
861
|
-
type: "input_text",
|
|
940
|
+
type: message.role === "assistant" ? "output_text" : "input_text",
|
|
862
941
|
text: item.text,
|
|
863
942
|
...(isRecord(item.cache_control)
|
|
864
943
|
? { cache_control: cloneProviderValue(item.cache_control) }
|
|
@@ -138,6 +138,7 @@ export function defaultPlayPresetFilesForLocale(locale) {
|
|
|
138
138
|
preset.set("format", "narraeon.play-preset/v2");
|
|
139
139
|
preset.set("playPrompts", defaultOrderedPlayPrompts());
|
|
140
140
|
preset.set("authorPrompts", defaultOrderedAuthorPrompts());
|
|
141
|
+
preset.set("mergeConsecutiveMessages", false);
|
|
141
142
|
files["preset.yaml"] = stringify(preset.toJS());
|
|
142
143
|
return files;
|
|
143
144
|
}
|
|
@@ -676,6 +677,7 @@ function editorProjection(files, limits) {
|
|
|
676
677
|
export function toPlayPresetStructuredEditor(definition) {
|
|
677
678
|
return {
|
|
678
679
|
name: definition.name,
|
|
680
|
+
mergeConsecutiveMessages: definition.mergeConsecutiveMessages ?? true,
|
|
679
681
|
authorPrompts: structuredClone(definition.authorPrompts ?? legacyAuthorPrompts(definition)),
|
|
680
682
|
playPrompts: structuredClone(definition.playPrompts ??
|
|
681
683
|
migrateLegacyPlayPrompts(definition.files, definition.narrativePrompts, [
|
|
@@ -726,6 +728,8 @@ export function applyPlayPresetStructuredEditor(files, input) {
|
|
|
726
728
|
if (preset.errors.length > 0 || callChain.errors.length > 0)
|
|
727
729
|
throw new FileNativePlayPresetError("structured_editor_yaml_invalid", "Structured editing can be applied only to parseable preset/call-chain YAML");
|
|
728
730
|
preset.set("name", input.name);
|
|
731
|
+
if (input.mergeConsecutiveMessages !== undefined)
|
|
732
|
+
preset.set("mergeConsecutiveMessages", parseMessageMerging(input.mergeConsecutiveMessages));
|
|
729
733
|
if (input.authorPrompts !== undefined)
|
|
730
734
|
preset.set("authorPrompts", parseOrderedAuthorPrompts(input.authorPrompts));
|
|
731
735
|
if (input.playPrompts !== undefined) {
|
|
@@ -839,6 +843,11 @@ export function parsePlayPresetStructuredEditor(value) {
|
|
|
839
843
|
? {}
|
|
840
844
|
: { followupItems: parseFollowupItems(value.followupItems, followups) }),
|
|
841
845
|
name: value.name,
|
|
846
|
+
...(value.mergeConsecutiveMessages === undefined
|
|
847
|
+
? {}
|
|
848
|
+
: {
|
|
849
|
+
mergeConsecutiveMessages: parseMessageMerging(value.mergeConsecutiveMessages),
|
|
850
|
+
}),
|
|
842
851
|
...(value.authorPrompts === undefined
|
|
843
852
|
? {}
|
|
844
853
|
: { authorPrompts: parseOrderedAuthorPrompts(value.authorPrompts) }),
|
|
@@ -873,6 +882,7 @@ export function parsePlayPresetFiles(files, limits = {}) {
|
|
|
873
882
|
"settingImprovement",
|
|
874
883
|
"playPrompts",
|
|
875
884
|
"authorPrompts",
|
|
885
|
+
"mergeConsecutiveMessages",
|
|
876
886
|
"mounts",
|
|
877
887
|
"playerViewPanels",
|
|
878
888
|
"extensions",
|
|
@@ -884,7 +894,9 @@ export function parsePlayPresetFiles(files, limits = {}) {
|
|
|
884
894
|
preset.format !== "narraeon.play-preset/v2")
|
|
885
895
|
invalid("preset_format_invalid", "preset.yaml format must be narraeon.play-preset/v1", "preset.yaml");
|
|
886
896
|
if (preset.format === "narraeon.play-preset/v1" &&
|
|
887
|
-
(preset.playPrompts !== undefined ||
|
|
897
|
+
(preset.playPrompts !== undefined ||
|
|
898
|
+
preset.authorPrompts !== undefined ||
|
|
899
|
+
preset.mergeConsecutiveMessages !== undefined))
|
|
888
900
|
invalid("preset_format_invalid", "Ordered prompts require v2", "preset.yaml");
|
|
889
901
|
const name = stringValue(preset.name);
|
|
890
902
|
if (!namePattern.test(name.trim()) || name.includes(String.fromCharCode(0)))
|
|
@@ -918,6 +930,11 @@ export function parsePlayPresetFiles(files, limits = {}) {
|
|
|
918
930
|
definition: {
|
|
919
931
|
...(followupItems === undefined ? {} : { followupItems }),
|
|
920
932
|
format: preset.format,
|
|
933
|
+
...(preset.mergeConsecutiveMessages === undefined
|
|
934
|
+
? {}
|
|
935
|
+
: {
|
|
936
|
+
mergeConsecutiveMessages: parseMessageMerging(preset.mergeConsecutiveMessages),
|
|
937
|
+
}),
|
|
921
938
|
...(preset.format === "narraeon.play-preset/v2"
|
|
922
939
|
? { playPrompts: parseOrderedPlayPrompts(preset.playPrompts) }
|
|
923
940
|
: {}),
|
|
@@ -1872,3 +1889,8 @@ export function legacyAuthorPrompts(definition) {
|
|
|
1872
1889
|
};
|
|
1873
1890
|
return entries;
|
|
1874
1891
|
}
|
|
1892
|
+
function parseMessageMerging(value) {
|
|
1893
|
+
if (typeof value !== "boolean")
|
|
1894
|
+
throw new FileNativePlayPresetError("message_merging_invalid", "mergeConsecutiveMessages must be a boolean");
|
|
1895
|
+
return value;
|
|
1896
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { parseDocument } from "yaml";
|
|
3
|
-
import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
3
|
+
import { builtinPlayPrompts, isPromptMessageRole, } from "../../shared/ordered-play-prompts.js";
|
|
4
4
|
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
5
5
|
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
6
6
|
const fail = () => {
|
|
@@ -20,11 +20,16 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
20
20
|
ids.has(item.id))
|
|
21
21
|
return fail();
|
|
22
22
|
ids.add(item.id);
|
|
23
|
-
|
|
23
|
+
if (item.messageRole !== undefined &&
|
|
24
|
+
!isPromptMessageRole(item.messageRole))
|
|
25
|
+
return fail();
|
|
26
|
+
const messageRole = item.messageRole === undefined ? {} : { messageRole: item.messageRole };
|
|
27
|
+
const exact = (keys) => Object.keys(item).length ===
|
|
28
|
+
keys.length + (item.messageRole === undefined ? 0 : 1) &&
|
|
24
29
|
keys.every((key) => Object.hasOwn(item, key));
|
|
25
30
|
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
26
31
|
worlds++;
|
|
27
|
-
return { id: item.id, kind: "world" };
|
|
32
|
+
return { id: item.id, kind: "world", ...messageRole };
|
|
28
33
|
}
|
|
29
34
|
if (item.kind === "user" &&
|
|
30
35
|
exact(["id", "kind", "name", "enabled", "body"]) &&
|
|
@@ -39,6 +44,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
39
44
|
name: item.name,
|
|
40
45
|
enabled: item.enabled,
|
|
41
46
|
body: item.body,
|
|
47
|
+
...messageRole,
|
|
42
48
|
};
|
|
43
49
|
const builtin = catalog.find((entry) => entry.id === item.builtin);
|
|
44
50
|
if (item.kind !== "builtin" ||
|
|
@@ -46,7 +52,9 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
46
52
|
!builtin ||
|
|
47
53
|
builtins.has(builtin.id) ||
|
|
48
54
|
typeof item.enabled !== "boolean" ||
|
|
49
|
-
(builtin.required &&
|
|
55
|
+
(builtin.required &&
|
|
56
|
+
(!item.enabled ||
|
|
57
|
+
(item.messageRole !== undefined && item.messageRole !== "system"))))
|
|
50
58
|
return fail();
|
|
51
59
|
builtins.add(builtin.id);
|
|
52
60
|
return {
|
|
@@ -54,6 +62,7 @@ export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"
|
|
|
54
62
|
kind: "builtin",
|
|
55
63
|
builtin: builtin.id,
|
|
56
64
|
enabled: item.enabled,
|
|
65
|
+
...messageRole,
|
|
57
66
|
};
|
|
58
67
|
});
|
|
59
68
|
if (worlds !== (worldRequired ? 1 : 0) ||
|
|
@@ -200,10 +200,8 @@ export class PlayCallChain {
|
|
|
200
200
|
return;
|
|
201
201
|
const { hostBinding, playPreset } = await input.resolvePrompt();
|
|
202
202
|
const binding = await this.#worlds.bindPlayCallChain(input.worldId);
|
|
203
|
-
const baseline = await this.#worlds.bindPlayCallChainAt(input.worldId, session.baselineHead);
|
|
204
|
-
const documents = new FileNativePlayDocuments(binding.files);
|
|
205
|
-
const documentMaintenance = await this.#bindDocumentMaintenance(documents, input.worldId, binding.parentHead);
|
|
206
203
|
const compilation = this.#compiler.compilePlayCallChain({
|
|
204
|
+
frozenWorldContext: currentPlayPrompt(session).bootstrap,
|
|
207
205
|
extensionControls: await this.#worlds.extensionControls(input.worldId, playPreset, this.#compiler.locale),
|
|
208
206
|
endpoint: {
|
|
209
207
|
id: `${input.worldId}:${binding.parentHead}`,
|
|
@@ -212,21 +210,17 @@ export class PlayCallChain {
|
|
|
212
210
|
hostBinding,
|
|
213
211
|
world: {
|
|
214
212
|
controlFingerprint: fingerprintControl(binding.files),
|
|
215
|
-
documentSnapshot: documents.snapshot,
|
|
213
|
+
documentSnapshot: session.documents.snapshot,
|
|
216
214
|
additionalMaterials: structuredClone(binding.additionalMaterials),
|
|
217
|
-
// Only pre-context history belongs in the prefix. Native conversation stays appended.
|
|
218
215
|
history: structuredClone(binding.history),
|
|
219
|
-
|
|
220
|
-
narrativeCheckpoint: baseline.narrativeCheckpoint,
|
|
221
|
-
documentMaintenance,
|
|
222
|
-
documentMaintenanceUnavailableReason: documents.maintenanceWarning,
|
|
216
|
+
narrativeCheckpoint: binding.narrativeCheckpoint,
|
|
223
217
|
},
|
|
224
218
|
playerInputPlacement: "append",
|
|
225
219
|
playerInput: input.playerText,
|
|
226
220
|
modelBinding: input.modelHost.binding(),
|
|
227
221
|
}, playPreset);
|
|
228
|
-
|
|
229
|
-
|
|
222
|
+
// The native conversation already contains successful writes and exact reads.
|
|
223
|
+
// Rebinding initial coverage here would incorrectly reauthorize stale scopes.
|
|
230
224
|
session.history = historyEntries(binding.history);
|
|
231
225
|
session.narrativeCheckpoint = binding.narrativeCheckpoint;
|
|
232
226
|
session.nextMaterials = structuredClone(binding.additionalMaterials);
|
|
@@ -248,7 +242,7 @@ export class PlayCallChain {
|
|
|
248
242
|
});
|
|
249
243
|
session.documentAuthorizationCheckpoints.push({
|
|
250
244
|
afterEventId: session.nextEventId,
|
|
251
|
-
authorization: documents.authorizationCheckpoint(),
|
|
245
|
+
authorization: session.documents.authorizationCheckpoint(),
|
|
252
246
|
});
|
|
253
247
|
await this.#persist(session);
|
|
254
248
|
crashAtPlayAdvanceEdge("after_prompt_run_prepared");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stateDirectoryHandle, parseStateDirectoryHandle, } from "../world/StateDirectoryHandles.js";
|
|
1
2
|
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { stringify } from "yaml";
|
|
@@ -632,37 +633,6 @@ function parseStateSearchScope(handle) {
|
|
|
632
633
|
? invalidStateHandle
|
|
633
634
|
: { document: { shortRef: document[1] } };
|
|
634
635
|
}
|
|
635
|
-
function stateDirectoryHandle(directory) {
|
|
636
|
-
if (directory === "")
|
|
637
|
-
return "@dir-/";
|
|
638
|
-
const encoded = directory.split("/").map(encodeURIComponent).join("/");
|
|
639
|
-
return `@dir-/${encoded}`;
|
|
640
|
-
}
|
|
641
|
-
function parseStateDirectoryHandle(handle) {
|
|
642
|
-
if (handle === "@dir-/")
|
|
643
|
-
return "";
|
|
644
|
-
if (!handle.startsWith("@dir-/") || handle.length === "@dir-/".length)
|
|
645
|
-
return null;
|
|
646
|
-
try {
|
|
647
|
-
const directory = handle
|
|
648
|
-
.slice("@dir-/".length)
|
|
649
|
-
.split("/")
|
|
650
|
-
.map(decodeURIComponent)
|
|
651
|
-
.join("/");
|
|
652
|
-
if (directory
|
|
653
|
-
.split("/")
|
|
654
|
-
.some((segment) => segment === "" ||
|
|
655
|
-
segment === "." ||
|
|
656
|
-
segment === ".." ||
|
|
657
|
-
segment.includes("\\")) ||
|
|
658
|
-
stateDirectoryHandle(directory) !== handle)
|
|
659
|
-
return null;
|
|
660
|
-
return directory;
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
636
|
function validStateDirectory(directory) {
|
|
667
637
|
if (directory.length === 0)
|
|
668
638
|
return false;
|