narraeon 0.3.1 → 0.5.0
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/appPreferences.js +25 -0
- package/dist/node/protocol/conversationObservation.js +67 -0
- package/dist/node/protocol/v1.js +188 -34
- package/dist/node/protocol/worldExtensions.js +16 -0
- package/dist/node/protocol/worldMaintenance.js +77 -0
- package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
- package/dist/node/runtime/ConversationChanges.js +24 -0
- package/dist/node/runtime/V1Runtime.js +447 -121
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
- package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
- package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
- package/dist/node/runtime/content/ContentWorkspace.js +43 -13
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
- package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
- package/dist/node/runtime/content/PackageFollowups.js +80 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
- package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
- package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
- package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
- package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
- package/dist/node/runtime/model/ModelHost.js +31 -1
- package/dist/node/runtime/model/ModelHostStream.js +51 -5
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
- package/dist/node/runtime/play/OrderedFollowups.js +71 -0
- package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
- package/dist/node/runtime/play/PlayCallChain.js +814 -61
- package/dist/node/runtime/play/PlayContinuity.js +88 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
- package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
- package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
- package/dist/node/runtime/play/PlayPromptRun.js +86 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
- package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
- package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
- package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
- package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
- package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
- package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
- package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
- package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
- package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
- package/dist/node/server/ConversationStream.js +109 -0
- package/dist/node/server/createServer.js +5 -1
- package/dist/node/shared/artifact-payload-contract.js +123 -0
- package/dist/node/shared/default-play-prompts.js +25 -45
- package/dist/node/shared/default-preset-host.js +114 -138
- package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
- package/dist/node/shared/display-regex.js +114 -0
- package/dist/node/shared/first-party-action-choices.js +1 -6
- package/dist/node/shared/first-party-player-view.js +1 -6
- package/dist/node/shared/ordered-author-prompts.js +64 -0
- package/dist/node/shared/ordered-followups.js +42 -0
- package/dist/node/shared/ordered-play-prompts.js +73 -0
- package/dist/web/assets/index-BXfelCi9.js +1207 -0
- package/dist/web/assets/index-BYnhUxlk.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
- package/dist/web/assets/index-D5VqDvjY.js +0 -1192
- package/dist/web/assets/index-Xa0s35CJ.css +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -52,6 +53,10 @@ export class FileNativePlayTimelineStore {
|
|
|
52
53
|
async persist(value, cursor) {
|
|
53
54
|
return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor);
|
|
54
55
|
}
|
|
56
|
+
/** Write a sealed context segment without making it the active timeline. */
|
|
57
|
+
async persistDetached(value, cursor) {
|
|
58
|
+
return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor, false);
|
|
59
|
+
}
|
|
55
60
|
async persistStaged(targetWorldRoot, value, cursor) {
|
|
56
61
|
return this.#persistAtRuntimeRoot(join(resolve(targetWorldRoot), "runtime"), value, cursor);
|
|
57
62
|
}
|
|
@@ -65,6 +70,16 @@ export class FileNativePlayTimelineStore {
|
|
|
65
70
|
const eventCount = prefixLength(input.source.events, input.target.events);
|
|
66
71
|
const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
|
|
67
72
|
const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
|
|
73
|
+
if (input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
|
|
74
|
+
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
|
|
75
|
+
if (encoding !== null)
|
|
76
|
+
await publishImmutableJson(join(targetRoot, "initial-request-encoding.json"), encoding);
|
|
77
|
+
}
|
|
78
|
+
for (const run of input.target.promptRuns ?? []) {
|
|
79
|
+
const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId, run.exchange).catch(() => null);
|
|
80
|
+
if (encoding !== null)
|
|
81
|
+
await publishImmutableJson(join(targetRoot, `request-encoding-${run.exchange}.json`), encoding);
|
|
82
|
+
}
|
|
68
83
|
await Promise.all([
|
|
69
84
|
cloneNumberedPrefix({
|
|
70
85
|
sourceRoot,
|
|
@@ -109,7 +124,7 @@ export class FileNativePlayTimelineStore {
|
|
|
109
124
|
authorizationCheckpointCount,
|
|
110
125
|
});
|
|
111
126
|
}
|
|
112
|
-
async #persistAtRuntimeRoot(runtimeRoot, value, cursor) {
|
|
127
|
+
async #persistAtRuntimeRoot(runtimeRoot, value, cursor, publishHead = true) {
|
|
113
128
|
assertIdentity(value.worldId, "World ID");
|
|
114
129
|
assertIdentity(value.chainId, "Call-chain ID");
|
|
115
130
|
const root = join(runtimeRoot, "play-contexts", createHash("sha256").update(value.chainId).digest("hex"));
|
|
@@ -125,6 +140,9 @@ export class FileNativePlayTimelineStore {
|
|
|
125
140
|
await publishImmutableJson(join(root, "base.json"), base);
|
|
126
141
|
else if (!isDeepStrictEqual(existingBase, base))
|
|
127
142
|
throw new Error("A play context identity is bound to different frozen data");
|
|
143
|
+
await publishImmutableJson(join(root, "reply-context.json"), {
|
|
144
|
+
contextId: value.continuityContextId ?? value.chainId,
|
|
145
|
+
});
|
|
128
146
|
const supplied = cursor ?? emptyPersistenceCursor();
|
|
129
147
|
// Recovery may replace the unresolved tail (for example, a generic crash
|
|
130
148
|
// failure becomes the already-returned Provider response). Numbered files
|
|
@@ -164,6 +182,10 @@ export class FileNativePlayTimelineStore {
|
|
|
164
182
|
const checkpoints = value.documentAuthorizationCheckpoints ?? [];
|
|
165
183
|
for (let index = previous.authorizationCheckpointCount; index < checkpoints.length; index += 1)
|
|
166
184
|
await publishImmutableJson(join(root, "authorization", numbered(index + 1)), checkpoints[index]);
|
|
185
|
+
const promptRuns = value.promptRuns ?? [];
|
|
186
|
+
for (let index = supplied.promptRunCount ?? 0; index < promptRuns.length; index += 1) {
|
|
187
|
+
await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)), encodePlayPromptRun(promptRuns[index]));
|
|
188
|
+
}
|
|
167
189
|
const state = {
|
|
168
190
|
schemaVersion: 3,
|
|
169
191
|
chainId: value.chainId,
|
|
@@ -174,6 +196,7 @@ export class FileNativePlayTimelineStore {
|
|
|
174
196
|
transcriptCount: value.transcript.length,
|
|
175
197
|
completedToolCount: value.completedTools.length,
|
|
176
198
|
authorizationCheckpointCount: checkpoints.length,
|
|
199
|
+
promptRunCount: promptRuns.length,
|
|
177
200
|
changedDocuments: structuredClone(value.changedDocuments),
|
|
178
201
|
nextEventId: value.nextEventId,
|
|
179
202
|
exchange: value.exchange,
|
|
@@ -183,17 +206,19 @@ export class FileNativePlayTimelineStore {
|
|
|
183
206
|
updatedAt: value.updatedAt,
|
|
184
207
|
};
|
|
185
208
|
await publishJson(join(root, "state.json"), state);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
209
|
+
if (publishHead)
|
|
210
|
+
await publishJson(join(runtimeRoot, "play-timeline-head.json"), {
|
|
211
|
+
schemaVersion: 3,
|
|
212
|
+
worldId: value.worldId,
|
|
213
|
+
chainId: value.chainId,
|
|
214
|
+
generation: value.timelineGeneration,
|
|
215
|
+
});
|
|
192
216
|
return {
|
|
193
217
|
eventCount: value.events.length,
|
|
194
218
|
transcriptCount: value.transcript.length,
|
|
195
219
|
completedToolCount: value.completedTools.length,
|
|
196
220
|
authorizationCheckpointCount: checkpoints.length,
|
|
221
|
+
promptRunCount: promptRuns.length,
|
|
197
222
|
};
|
|
198
223
|
}
|
|
199
224
|
async readCurrent(worldId) {
|
|
@@ -213,13 +238,14 @@ export class FileNativePlayTimelineStore {
|
|
|
213
238
|
if (metadata === null)
|
|
214
239
|
return null;
|
|
215
240
|
const { index: indexValue, state: stateValue, root } = metadata;
|
|
216
|
-
const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints,] = await Promise.all([
|
|
241
|
+
const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints, promptRuns,] = await Promise.all([
|
|
217
242
|
readJson(join(root, "base.json")),
|
|
218
243
|
readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
|
|
219
244
|
readNumbered(join(root, "transcript"), stateValue.transcriptCount),
|
|
220
245
|
readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
|
|
221
246
|
readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
|
|
222
247
|
readNumbered(join(root, "authorization"), stateValue.authorizationCheckpointCount),
|
|
248
|
+
readNumbered(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0).then((records) => records.map(decodePlayPromptRun)),
|
|
223
249
|
]);
|
|
224
250
|
const events = rawEvents.map(normalizePlayEvent);
|
|
225
251
|
assertContextBase(baseValue);
|
|
@@ -247,6 +273,12 @@ export class FileNativePlayTimelineStore {
|
|
|
247
273
|
chainId,
|
|
248
274
|
previousChainId: indexValue.previousChainId,
|
|
249
275
|
timelineGeneration: indexValue.timelineGeneration,
|
|
276
|
+
...(baseValue.continuityContextId === undefined
|
|
277
|
+
? {}
|
|
278
|
+
: { continuityContextId: baseValue.continuityContextId }),
|
|
279
|
+
...(baseValue.presetFiles === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { presetFiles: structuredClone(baseValue.presetFiles) }),
|
|
250
282
|
baselineHead: baseValue.baselineHead,
|
|
251
283
|
parentHead: stateValue.parentHead,
|
|
252
284
|
...(baseValue.baselineHistoryLength === undefined
|
|
@@ -278,6 +310,7 @@ export class FileNativePlayTimelineStore {
|
|
|
278
310
|
events,
|
|
279
311
|
completedTools,
|
|
280
312
|
documentAuthorizationCheckpoints: checkpoints,
|
|
313
|
+
promptRuns,
|
|
281
314
|
changedDocuments: structuredClone(stateValue.changedDocuments),
|
|
282
315
|
nextMaterials: structuredClone(continuationValue.nextMaterials),
|
|
283
316
|
nextEventId: stateValue.nextEventId,
|
|
@@ -294,9 +327,33 @@ export class FileNativePlayTimelineStore {
|
|
|
294
327
|
transcriptCount: stateValue.transcriptCount,
|
|
295
328
|
completedToolCount: stateValue.completedToolCount,
|
|
296
329
|
authorizationCheckpointCount: stateValue.authorizationCheckpointCount,
|
|
330
|
+
promptRunCount: stateValue.promptRunCount ?? 0,
|
|
297
331
|
},
|
|
298
332
|
};
|
|
299
333
|
}
|
|
334
|
+
/** The first dispatched request encoding is diagnostic evidence, not Authority. */
|
|
335
|
+
async recordInitialEncoding(worldId, chainId, encoding, exchange = 1) {
|
|
336
|
+
const path = join(this.#contextRoot(worldId, chainId), exchange === 1
|
|
337
|
+
? "initial-request-encoding.json"
|
|
338
|
+
: `request-encoding-${exchange}.json`);
|
|
339
|
+
await publishImmutableJson(path, encoding);
|
|
340
|
+
}
|
|
341
|
+
async readInitialEncoding(worldId, chainId, exchange = 1) {
|
|
342
|
+
const value = await readOptionalJson(join(this.#contextRoot(worldId, chainId), exchange === 1
|
|
343
|
+
? "initial-request-encoding.json"
|
|
344
|
+
: `request-encoding-${exchange}.json`));
|
|
345
|
+
if (value === null)
|
|
346
|
+
return null;
|
|
347
|
+
if (!isRecord(value) ||
|
|
348
|
+
value.method !== "POST" ||
|
|
349
|
+
!["chat_completions", "openai_responses", "anthropic_messages"].includes(String(value.provider)) ||
|
|
350
|
+
!isRecord(value.body) ||
|
|
351
|
+
typeof value.endpointPath !== "string" ||
|
|
352
|
+
!Array.isArray(value.headerNames) ||
|
|
353
|
+
!value.headerNames.every((name) => typeof name === "string"))
|
|
354
|
+
throw new Error("Initial request encoding has an invalid shape");
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
300
357
|
async readAllContexts(worldId) {
|
|
301
358
|
const current = await this.readCurrent(worldId);
|
|
302
359
|
if (current === null)
|
|
@@ -312,6 +369,40 @@ export class FileNativePlayTimelineStore {
|
|
|
312
369
|
}
|
|
313
370
|
return reverse.reverse();
|
|
314
371
|
}
|
|
372
|
+
/** Resolve only requested reply summaries; decoration reads never load Provider transcripts. */
|
|
373
|
+
async resolveReplies(worldId, targets) {
|
|
374
|
+
if (targets.length === 0)
|
|
375
|
+
return [];
|
|
376
|
+
const result = [];
|
|
377
|
+
let chainId = (await this.#readTimelineHead(worldId))?.chainId ?? null;
|
|
378
|
+
while (chainId !== null) {
|
|
379
|
+
const metadata = await this.#readContextMetadata(worldId, chainId);
|
|
380
|
+
if (metadata === null)
|
|
381
|
+
throw new Error("Play timeline points to a missing context");
|
|
382
|
+
const identity = await readOptionalJson(join(metadata.root, "reply-context.json"));
|
|
383
|
+
if (identity !== null &&
|
|
384
|
+
(!isRecord(identity) || typeof identity.contextId !== "string"))
|
|
385
|
+
throw new Error("Play reply context identity is invalid");
|
|
386
|
+
const contextId = identity === null
|
|
387
|
+
? null
|
|
388
|
+
: identity.contextId;
|
|
389
|
+
for (const target of targets.filter((item) => item.contextId === contextId &&
|
|
390
|
+
item.eventId <= metadata.state.eventCount)) {
|
|
391
|
+
const event = await readJson(join(metadata.root, "summaries", numbered(target.eventId)));
|
|
392
|
+
assertTimelineEventSummary(event);
|
|
393
|
+
if (event.id === target.eventId &&
|
|
394
|
+
event.kind === "assistant" &&
|
|
395
|
+
event.committedHead === target.head)
|
|
396
|
+
result.push({ ...target, chainId });
|
|
397
|
+
}
|
|
398
|
+
if (targets.every((target) => result.some((item) => item.contextId === target.contextId &&
|
|
399
|
+
item.eventId === target.eventId &&
|
|
400
|
+
item.head === target.head)))
|
|
401
|
+
break;
|
|
402
|
+
chainId = metadata.index.previousChainId;
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
315
406
|
async readPage(worldId, limit, cursor) {
|
|
316
407
|
assertIdentity(worldId, "World ID");
|
|
317
408
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
@@ -517,6 +608,12 @@ function contextBase(value) {
|
|
|
517
608
|
kind: "play_context_frozen",
|
|
518
609
|
worldId: value.worldId,
|
|
519
610
|
chainId: value.chainId,
|
|
611
|
+
...(value.continuityContextId === undefined
|
|
612
|
+
? {}
|
|
613
|
+
: { continuityContextId: value.continuityContextId }),
|
|
614
|
+
...(value.presetFiles === undefined
|
|
615
|
+
? {}
|
|
616
|
+
: { presetFiles: structuredClone(value.presetFiles) }),
|
|
520
617
|
baselineHead: value.baselineHead,
|
|
521
618
|
...(value.baselineHistoryLength === undefined
|
|
522
619
|
? {}
|
|
@@ -633,6 +730,9 @@ function assertContextBase(value) {
|
|
|
633
730
|
typeof value.worldId !== "string" ||
|
|
634
731
|
typeof value.chainId !== "string" ||
|
|
635
732
|
typeof value.baselineHead !== "string" ||
|
|
733
|
+
(value.continuityContextId !== undefined &&
|
|
734
|
+
(typeof value.continuityContextId !== "string" ||
|
|
735
|
+
value.continuityContextId.length === 0)) ||
|
|
636
736
|
(value.modelBinding !== undefined &&
|
|
637
737
|
!validModelHostBinding(value.modelBinding)) ||
|
|
638
738
|
!isRecord(value.bootstrap) ||
|
|
@@ -652,6 +752,7 @@ function assertContextState(value) {
|
|
|
652
752
|
!validCount(value.transcriptCount) ||
|
|
653
753
|
!validCount(value.completedToolCount) ||
|
|
654
754
|
!validCount(value.authorizationCheckpointCount) ||
|
|
755
|
+
(value.promptRunCount !== undefined && !validCount(value.promptRunCount)) ||
|
|
655
756
|
!Array.isArray(value.changedDocuments) ||
|
|
656
757
|
!validCount(value.nextEventId) ||
|
|
657
758
|
!validCount(value.exchange) ||
|
|
@@ -697,6 +798,9 @@ function isReleasedPlayContext(value) {
|
|
|
697
798
|
return false;
|
|
698
799
|
if (typeof value.chainId !== "string" ||
|
|
699
800
|
typeof value.baselineHead !== "string" ||
|
|
801
|
+
(value.continuityContextId !== undefined &&
|
|
802
|
+
(typeof value.continuityContextId !== "string" ||
|
|
803
|
+
value.continuityContextId.length === 0)) ||
|
|
700
804
|
(value.baselineHistoryLength !== undefined &&
|
|
701
805
|
!validCount(value.baselineHistoryLength)) ||
|
|
702
806
|
typeof value.parentHead !== "string" ||
|
|
@@ -918,7 +1022,12 @@ function assertTimelineEventSummary(value) {
|
|
|
918
1022
|
throw new Error("Follow-up timeline summary has an invalid shape");
|
|
919
1023
|
}
|
|
920
1024
|
function validAssistantEventCore(value) {
|
|
921
|
-
return (
|
|
1025
|
+
return ((value.checkpoint === undefined ||
|
|
1026
|
+
(value.checkpoint === true &&
|
|
1027
|
+
value.status === "completed" &&
|
|
1028
|
+
value.responseKind === "narrative" &&
|
|
1029
|
+
typeof value.committedHead === "string")) &&
|
|
1030
|
+
typeof value.text === "string" &&
|
|
922
1031
|
(value.status === "streaming" ||
|
|
923
1032
|
value.status === "completed" ||
|
|
924
1033
|
value.status === "interrupted") &&
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readPackageFollowups, packageFollowupResources, } from "../content/PackageFollowups.js";
|
|
2
|
+
import { builtinFollowupExample, defaultFollowupItems, } from "../../shared/ordered-followups.js";
|
|
3
|
+
/** Strict portable permissions, shared by import and structured saves. */
|
|
4
|
+
export function parseFollowupItems(value, definitions) {
|
|
5
|
+
if (!Array.isArray(value))
|
|
6
|
+
throw new Error("Follow-up order must be an array");
|
|
7
|
+
const items = value.map((raw) => {
|
|
8
|
+
if (typeof raw !== "object" ||
|
|
9
|
+
raw === null ||
|
|
10
|
+
!("id" in raw) ||
|
|
11
|
+
!("kind" in raw) ||
|
|
12
|
+
!("enabled" in raw) ||
|
|
13
|
+
Object.keys(raw).some((key) => !["id", "kind", "enabled"].includes(key)) ||
|
|
14
|
+
typeof raw.id !== "string" ||
|
|
15
|
+
typeof raw.enabled !== "boolean" ||
|
|
16
|
+
!["user", "builtin", "content-package"].includes(String(raw.kind)))
|
|
17
|
+
throw new Error("Invalid follow-up reference; system content is read-only");
|
|
18
|
+
return {
|
|
19
|
+
id: raw.id,
|
|
20
|
+
kind: raw.kind,
|
|
21
|
+
enabled: raw.enabled,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
if (new Set(items.map((item) => item.id)).size !== items.length ||
|
|
25
|
+
items.filter((item) => item.kind === "builtin").length !== 1 ||
|
|
26
|
+
!items.some((item) => item.kind === "builtin" && item.id === "builtin:summary") ||
|
|
27
|
+
items.filter((item) => item.kind === "content-package").length !== 1 ||
|
|
28
|
+
!items.some((item) => item.kind === "content-package" && item.id === "package:followups") ||
|
|
29
|
+
items.filter((item) => item.kind === "user").length !==
|
|
30
|
+
definitions.length ||
|
|
31
|
+
definitions.some((def) => !items.some((item) => item.kind === "user" && item.id === def.id)))
|
|
32
|
+
throw new Error("Follow-up order must contain every user request, the read-only system example and content-package group exactly once");
|
|
33
|
+
return items;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve each source against its own files, before requests become immutable snapshots. */
|
|
36
|
+
export function effectiveFollowupDefinitions(definition, locale, worldFiles = [], enabled) {
|
|
37
|
+
const packageSource = readPackageFollowups(worldFiles);
|
|
38
|
+
return (definition.followupItems ?? defaultFollowupItems(definition.followups)).flatMap((item) => {
|
|
39
|
+
if (!(item.kind === "content-package"
|
|
40
|
+
? (enabled?.packageGroup ?? item.enabled)
|
|
41
|
+
: (enabled?.requests.get(item.id) ?? item.enabled)))
|
|
42
|
+
return [];
|
|
43
|
+
if (item.kind === "content-package")
|
|
44
|
+
return packageSource.followups
|
|
45
|
+
.filter((item) => enabled?.requests.get(`package:${item.definition.id}`) ??
|
|
46
|
+
item.enabled)
|
|
47
|
+
.map(({ definition: entry, mount }) => {
|
|
48
|
+
return {
|
|
49
|
+
definition: {
|
|
50
|
+
...entry,
|
|
51
|
+
id: `package:${entry.id}`,
|
|
52
|
+
artifacts: entry.artifacts.map((artifact) => ({
|
|
53
|
+
...artifact,
|
|
54
|
+
channel: `package:${artifact.channel}`,
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
body: packageSource.files[entry.prompt.path],
|
|
58
|
+
frozenResources: {
|
|
59
|
+
files: packageFollowupResources(entry, packageSource.files),
|
|
60
|
+
mount,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
if (item.kind === "builtin")
|
|
65
|
+
return [builtinFollowupExample(locale)];
|
|
66
|
+
const entry = definition.followups.find((entry) => entry.id === item.id);
|
|
67
|
+
if (entry === undefined)
|
|
68
|
+
throw new Error(`Missing follow-up definition: ${item.id}`);
|
|
69
|
+
return [{ definition: entry, body: definition.files[entry.prompt.path] }];
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseDocument } from "yaml";
|
|
3
|
+
import { builtinPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
4
|
+
/** The same strict permission gate is used by portable imports and structured saves. */
|
|
5
|
+
export function parseOrderedPlayPrompts(value, catalog = builtinPlayPrompts("en"), worldRequired = true) {
|
|
6
|
+
const fail = () => {
|
|
7
|
+
throw new Error("Invalid ordered play prompts: unique identities, current builtins, enabled mechanics and one world placeholder are required");
|
|
8
|
+
};
|
|
9
|
+
if (!Array.isArray(value) || value.length > 256)
|
|
10
|
+
return fail();
|
|
11
|
+
const ids = new Set();
|
|
12
|
+
const builtins = new Set();
|
|
13
|
+
let worlds = 0;
|
|
14
|
+
const entries = value.map((raw) => {
|
|
15
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
16
|
+
return fail();
|
|
17
|
+
const item = raw;
|
|
18
|
+
if (typeof item.id !== "string" ||
|
|
19
|
+
!/^[a-zA-Z0-9._-]{1,128}$/u.test(item.id) ||
|
|
20
|
+
ids.has(item.id))
|
|
21
|
+
return fail();
|
|
22
|
+
ids.add(item.id);
|
|
23
|
+
const exact = (keys) => Object.keys(item).length === keys.length &&
|
|
24
|
+
keys.every((key) => Object.hasOwn(item, key));
|
|
25
|
+
if (item.kind === "world" && exact(["id", "kind"])) {
|
|
26
|
+
worlds++;
|
|
27
|
+
return { id: item.id, kind: "world" };
|
|
28
|
+
}
|
|
29
|
+
if (item.kind === "user" &&
|
|
30
|
+
exact(["id", "kind", "name", "enabled", "body"]) &&
|
|
31
|
+
typeof item.name === "string" &&
|
|
32
|
+
item.name.trim().length > 0 &&
|
|
33
|
+
/^[^\r\n\0]{1,160}$/u.test(item.name) &&
|
|
34
|
+
typeof item.body === "string" &&
|
|
35
|
+
typeof item.enabled === "boolean")
|
|
36
|
+
return {
|
|
37
|
+
id: item.id,
|
|
38
|
+
kind: "user",
|
|
39
|
+
name: item.name,
|
|
40
|
+
enabled: item.enabled,
|
|
41
|
+
body: item.body,
|
|
42
|
+
};
|
|
43
|
+
const builtin = catalog.find((entry) => entry.id === item.builtin);
|
|
44
|
+
if (item.kind !== "builtin" ||
|
|
45
|
+
!exact(["id", "kind", "builtin", "enabled"]) ||
|
|
46
|
+
!builtin ||
|
|
47
|
+
builtins.has(builtin.id) ||
|
|
48
|
+
typeof item.enabled !== "boolean" ||
|
|
49
|
+
(builtin.required && !item.enabled))
|
|
50
|
+
return fail();
|
|
51
|
+
builtins.add(builtin.id);
|
|
52
|
+
return {
|
|
53
|
+
id: item.id,
|
|
54
|
+
kind: "builtin",
|
|
55
|
+
builtin: builtin.id,
|
|
56
|
+
enabled: item.enabled,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
if (worlds !== (worldRequired ? 1 : 0) ||
|
|
60
|
+
catalog.some((item) => item.required && !builtins.has(item.id)))
|
|
61
|
+
return fail();
|
|
62
|
+
return entries;
|
|
63
|
+
}
|
|
64
|
+
/** Recognized v1 content becomes user text, never a locked builtin inferred from a path. */
|
|
65
|
+
export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
66
|
+
const document = parseDocument(files["frame.yaml"] ?? "", {
|
|
67
|
+
uniqueKeys: true,
|
|
68
|
+
strict: true,
|
|
69
|
+
});
|
|
70
|
+
const frame = document.toJS({ maxAliasCount: 0 });
|
|
71
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
const roles = ["runtime_system", "author_instruction", "world_context"];
|
|
73
|
+
if (document.errors.length ||
|
|
74
|
+
document.warnings.length ||
|
|
75
|
+
!record(frame) ||
|
|
76
|
+
frame.format !== "narraeon.host-frame/v1" ||
|
|
77
|
+
!record(frame.roles) ||
|
|
78
|
+
Object.keys(frame.roles).length !== 3)
|
|
79
|
+
throw new Error("Legacy host frame is invalid; repair it before migrating");
|
|
80
|
+
const items = {};
|
|
81
|
+
const runtime = new Set();
|
|
82
|
+
let worldInstructions = 0;
|
|
83
|
+
let worldContext = 0;
|
|
84
|
+
let coverage = 0;
|
|
85
|
+
for (const role of roles) {
|
|
86
|
+
items[role] = [];
|
|
87
|
+
const roleItems = frame.roles[role];
|
|
88
|
+
if (!Array.isArray(roleItems))
|
|
89
|
+
throw new Error("Legacy host role must be an array");
|
|
90
|
+
for (const item of roleItems) {
|
|
91
|
+
if (!record(item) || Object.keys(item).length !== 1)
|
|
92
|
+
throw new Error("Legacy host entry is invalid");
|
|
93
|
+
if (role === "author_instruction" && typeof item.markdown === "string")
|
|
94
|
+
items[role].push({ markdown: item.markdown });
|
|
95
|
+
else if (role === "author_instruction" &&
|
|
96
|
+
item.include === "world.instructions") {
|
|
97
|
+
worldInstructions++;
|
|
98
|
+
items[role].push({ include: item.include });
|
|
99
|
+
}
|
|
100
|
+
else if (role === "world_context" && item.include === "world.context") {
|
|
101
|
+
worldContext++;
|
|
102
|
+
items[role].push({ include: item.include });
|
|
103
|
+
}
|
|
104
|
+
else if (role === "world_context" &&
|
|
105
|
+
item.builtin === "runtime.coverage")
|
|
106
|
+
coverage++;
|
|
107
|
+
else if (role === "runtime_system" &&
|
|
108
|
+
typeof item.builtin === "string" &&
|
|
109
|
+
[
|
|
110
|
+
"runtime.play-contract",
|
|
111
|
+
"runtime.tool-contract",
|
|
112
|
+
"runtime.operation-contract",
|
|
113
|
+
].includes(item.builtin) &&
|
|
114
|
+
!runtime.has(item.builtin))
|
|
115
|
+
runtime.add(item.builtin);
|
|
116
|
+
else
|
|
117
|
+
throw new Error("Legacy host entry is unknown or misplaced");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (runtime.size !== 3 ||
|
|
121
|
+
coverage !== 1 ||
|
|
122
|
+
worldInstructions !== 1 ||
|
|
123
|
+
worldContext !== 1)
|
|
124
|
+
throw new Error("Legacy host frame is missing or duplicates required includes");
|
|
125
|
+
const entries = [
|
|
126
|
+
{
|
|
127
|
+
id: "play.mechanics",
|
|
128
|
+
kind: "builtin",
|
|
129
|
+
builtin: "play.mechanics",
|
|
130
|
+
enabled: true,
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
const used = new Set(excludedPaths);
|
|
134
|
+
const occurrences = new Map();
|
|
135
|
+
let world = false;
|
|
136
|
+
const add = (path, enabled) => {
|
|
137
|
+
if (!enabled && used.has(path))
|
|
138
|
+
return;
|
|
139
|
+
const occurrence = occurrences.get(path) ?? 0;
|
|
140
|
+
occurrences.set(path, occurrence + 1);
|
|
141
|
+
used.add(path);
|
|
142
|
+
const body = files[path];
|
|
143
|
+
if (body === undefined)
|
|
144
|
+
throw new Error(`Missing legacy prompt: ${path}`);
|
|
145
|
+
const heading = /^#[ \t]+([^\r\n]+)$/mu.exec(body)?.[1]?.trim() ?? "";
|
|
146
|
+
const stem = path.replace(/^.*\//u, "").replace(/\.md$/u, "");
|
|
147
|
+
const name = heading.length > 0 ? heading : stem.length > 0 ? stem : "Prompt";
|
|
148
|
+
entries.push({
|
|
149
|
+
id: `legacy-${createHash("sha256").update(path).digest("hex").slice(0, 32)}-${occurrence}`,
|
|
150
|
+
kind: "user",
|
|
151
|
+
name: name.slice(0, 160),
|
|
152
|
+
enabled,
|
|
153
|
+
body,
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
for (const role of [
|
|
157
|
+
"runtime_system",
|
|
158
|
+
"author_instruction",
|
|
159
|
+
"world_context",
|
|
160
|
+
]) {
|
|
161
|
+
for (const item of items[role] ?? []) {
|
|
162
|
+
if (item.markdown)
|
|
163
|
+
add(item.markdown, true);
|
|
164
|
+
if (item.include?.startsWith("world.") && !world) {
|
|
165
|
+
entries.push({ id: "world", kind: "world" });
|
|
166
|
+
world = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (role === "author_instruction")
|
|
170
|
+
for (const prompt of narrative)
|
|
171
|
+
add(prompt.path, true);
|
|
172
|
+
}
|
|
173
|
+
if (!world)
|
|
174
|
+
entries.push({ id: "world", kind: "world" });
|
|
175
|
+
for (const path of Object.keys(files).sort())
|
|
176
|
+
if ((path.startsWith("blocks/") || path.startsWith("prompts/")) &&
|
|
177
|
+
path.endsWith(".md"))
|
|
178
|
+
add(path, false);
|
|
179
|
+
return entries;
|
|
180
|
+
}
|