narraeon 0.6.1 → 0.6.3
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 +12 -0
- package/dist/node/runtime/V1Runtime.js +4 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +7 -7
- package/dist/node/runtime/play/OrderedPlayPrompts.js +80 -2
- package/dist/node/runtime/play/PlayDocumentTools.js +31 -17
- package/dist/node/runtime/play/PlayTraceRetention.js +50 -2
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +4 -1
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
- package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -1
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
- package/dist/node/server/mobile.js +46 -14
- package/dist/node/shared/default-play-prompts.js +2 -2
- package/dist/node/shared/default-preset-host.js +82 -2
- package/dist/node/shared/ordered-play-prompts.js +25 -1
- package/dist/web/assets/index-BZwaOsvk.js +1393 -0
- package/dist/web/assets/index-eUMdHbq2.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-7GzLSUpU.js +0 -1323
- package/dist/web/assets/index-BJ-1Mb0e.css +0 -1
package/dist/node/protocol/v1.js
CHANGED
|
@@ -68,6 +68,11 @@ const requiredFields = {
|
|
|
68
68
|
continuation: "object",
|
|
69
69
|
},
|
|
70
70
|
"setting-improvement.cancel": { sessionId: "string" },
|
|
71
|
+
"setting-improvement.resume": {
|
|
72
|
+
packageId: "string",
|
|
73
|
+
sessionId: "string",
|
|
74
|
+
requestId: "string",
|
|
75
|
+
},
|
|
71
76
|
"play.create": { name: "string" },
|
|
72
77
|
"play.copy": { presetId: "string" },
|
|
73
78
|
"play.save": { presetId: "string", name: "string", files: "object" },
|
|
@@ -120,6 +125,11 @@ const requiredFields = {
|
|
|
120
125
|
continuation: "object",
|
|
121
126
|
},
|
|
122
127
|
"world.revision.cancel": { sessionId: "string" },
|
|
128
|
+
"world.revision.resume": {
|
|
129
|
+
worldId: "string",
|
|
130
|
+
sessionId: "string",
|
|
131
|
+
requestId: "string",
|
|
132
|
+
},
|
|
123
133
|
"world.revision.files.replace": {
|
|
124
134
|
worldId: "string",
|
|
125
135
|
epochId: "string",
|
|
@@ -422,6 +432,7 @@ const requestTypes = new Set([
|
|
|
422
432
|
"setting-improvement.rollback",
|
|
423
433
|
"setting-improvement.message",
|
|
424
434
|
"setting-improvement.cancel",
|
|
435
|
+
"setting-improvement.resume",
|
|
425
436
|
"play.read",
|
|
426
437
|
"play.create",
|
|
427
438
|
"play.copy",
|
|
@@ -456,6 +467,7 @@ const requestTypes = new Set([
|
|
|
456
467
|
"world.revision.session.delete",
|
|
457
468
|
"world.revision.message",
|
|
458
469
|
"world.revision.cancel",
|
|
470
|
+
"world.revision.resume",
|
|
459
471
|
"world.revision.files.replace",
|
|
460
472
|
"world.revision.rollback",
|
|
461
473
|
"world.revision.apply",
|
|
@@ -383,6 +383,8 @@ export class V1Runtime {
|
|
|
383
383
|
return this.#settingImprovements.send(request);
|
|
384
384
|
case "setting-improvement.cancel":
|
|
385
385
|
return this.#settingImprovements.cancel(request.sessionId);
|
|
386
|
+
case "setting-improvement.resume":
|
|
387
|
+
return this.#settingImprovements.resume(request);
|
|
386
388
|
case "play.read":
|
|
387
389
|
return this.#playPresets.list();
|
|
388
390
|
case "play.create":
|
|
@@ -614,6 +616,8 @@ export class V1Runtime {
|
|
|
614
616
|
return this.#worldRevisionCall(() => this.#worldRevisions.send(request));
|
|
615
617
|
case "world.revision.cancel":
|
|
616
618
|
return this.#worldRevisionCall(() => this.#worldRevisions.cancel(request.sessionId));
|
|
619
|
+
case "world.revision.resume":
|
|
620
|
+
return this.#worldRevisionCall(() => this.#worldRevisions.resume(request));
|
|
617
621
|
case "world.revision.files.replace":
|
|
618
622
|
return this.#worldRevisionCall(() => this.#worldRevisions.replaceFiles(request));
|
|
619
623
|
case "world.revision.rollback":
|
|
@@ -119,8 +119,15 @@ export class ModelToolConversation {
|
|
|
119
119
|
await input.settleToolResponse(session, session.modelItems.length - 1, toolCalls);
|
|
120
120
|
input.changed?.(true);
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// The exchange budget only bounds a runaway loop. Every tool response is
|
|
123
|
+
// already settled, so the conversation pauses and can resume unchanged.
|
|
124
|
+
session.runStatus = "paused";
|
|
125
|
+
session.activeRequestId = null;
|
|
126
|
+
session.completedRequestIds = rememberRequest(session.completedRequestIds, live.requestId);
|
|
127
|
+
session.lastFailure = null;
|
|
128
|
+
session.updatedAt = Date.now();
|
|
129
|
+
await input.save(session);
|
|
130
|
+
return input.view(session);
|
|
124
131
|
}
|
|
125
132
|
catch (error) {
|
|
126
133
|
session.runStatus = "interrupted";
|
|
@@ -154,6 +161,22 @@ export function appendAuthoringUserMessage(session, message, requestId) {
|
|
|
154
161
|
session.lastFailure = null;
|
|
155
162
|
session.updatedAt = now;
|
|
156
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Starts another exchange budget for a paused conversation without appending a
|
|
166
|
+
* user message; the next request replays the settled transcript as-is. The run
|
|
167
|
+
* still belongs to the paused user request, so that request stays active; the
|
|
168
|
+
* caller's resume request ID only deduplicates the resume itself.
|
|
169
|
+
*/
|
|
170
|
+
export function resumePausedAuthoringConversation(session) {
|
|
171
|
+
if (session.runStatus !== "paused")
|
|
172
|
+
throw new Error("Only a paused authoring conversation can be continued");
|
|
173
|
+
if (session.modelItems.at(-1)?.kind !== "tool")
|
|
174
|
+
throw new Error("The paused authoring conversation does not end with settled tool results");
|
|
175
|
+
session.runStatus = "running";
|
|
176
|
+
session.activeRequestId = session.requests?.at(-1)?.requestId ?? null;
|
|
177
|
+
session.lastFailure = null;
|
|
178
|
+
session.updatedAt = Date.now();
|
|
179
|
+
}
|
|
157
180
|
export function requiredAuthoringMessage(message) {
|
|
158
181
|
if (message.trim().length === 0)
|
|
159
182
|
throw new Error("An authoring message cannot be empty");
|
|
@@ -3,7 +3,7 @@ export { validatePlayPresetArtifactPayload } from "../../shared/artifact-payload
|
|
|
3
3
|
import { defaultFollowupItems, } from "../../shared/ordered-followups.js";
|
|
4
4
|
import { parseFollowupItems } from "./OrderedFollowups.js";
|
|
5
5
|
import { builtinAuthorPrompts, defaultOrderedAuthorPrompts, } from "../../shared/ordered-author-prompts.js";
|
|
6
|
-
import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts,
|
|
6
|
+
import { parseOrderedPlayPrompts, migrateLegacyPlayPrompts, withCurrentPromptBuiltins, } from "./OrderedPlayPrompts.js";
|
|
7
7
|
import { defaultOrderedPlayPrompts, } from "../../shared/ordered-play-prompts.js";
|
|
8
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
9
9
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
@@ -457,8 +457,8 @@ export class FileNativePlayPresetStore {
|
|
|
457
457
|
.map((path) => ({ path, contents: binding.files[path] }));
|
|
458
458
|
}
|
|
459
459
|
async importPortable(input) {
|
|
460
|
-
// A released preset gains the
|
|
461
|
-
const files =
|
|
460
|
+
// A released preset gains the current builtins at import, as at upgrade.
|
|
461
|
+
const files = withCurrentPromptBuiltins(toFileMap(input.files, this.#limits));
|
|
462
462
|
const parsed = parsePlayPresetFiles(files, this.#limits);
|
|
463
463
|
if (parsed.kind === "invalid")
|
|
464
464
|
throw parsed.error;
|
|
@@ -568,8 +568,8 @@ export class FileNativePlayPresetStore {
|
|
|
568
568
|
return true;
|
|
569
569
|
}
|
|
570
570
|
/**
|
|
571
|
-
* One-time upgrade for ordered presets saved before the tabletop
|
|
572
|
-
* existed. An affected revision is replaced by a new revision that inserts
|
|
571
|
+
* One-time upgrade for ordered presets saved before the tabletop or
|
|
572
|
+
* continuation builtins existed. An affected revision is replaced by a new revision that inserts
|
|
573
573
|
* them; the old revision stays for frozen requests. A pending draft gets the
|
|
574
574
|
* same insertion, because a draft that kept the old list would fail the
|
|
575
575
|
* builtin-retention rule on save. The library is one atomically written file,
|
|
@@ -580,7 +580,7 @@ export class FileNativePlayPresetStore {
|
|
|
580
580
|
for (const stored of document.presets) {
|
|
581
581
|
const current = stored.revisions[stored.currentRevision];
|
|
582
582
|
if (current !== undefined) {
|
|
583
|
-
const migrated =
|
|
583
|
+
const migrated = withCurrentPromptBuiltins(current);
|
|
584
584
|
if (migrated !== current) {
|
|
585
585
|
const revision = revisionForFiles(migrated, this.#limits);
|
|
586
586
|
stored.revisions[revision] = cloneFiles(migrated);
|
|
@@ -592,7 +592,7 @@ export class FileNativePlayPresetStore {
|
|
|
592
592
|
? undefined
|
|
593
593
|
: stored.revisions[stored.draftRevision];
|
|
594
594
|
if (draft !== undefined) {
|
|
595
|
-
const migrated =
|
|
595
|
+
const migrated = withCurrentPromptBuiltins(draft);
|
|
596
596
|
if (migrated !== draft) {
|
|
597
597
|
const revision = revisionForFiles(migrated, this.#limits);
|
|
598
598
|
stored.revisions[revision] = cloneFiles(migrated);
|
|
@@ -118,6 +118,81 @@ export function withTabletopPromptBuiltins(files) {
|
|
|
118
118
|
}
|
|
119
119
|
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
120
120
|
}
|
|
121
|
+
const alternativeGroups = () => [
|
|
122
|
+
{
|
|
123
|
+
anchor: "play.style",
|
|
124
|
+
before: [
|
|
125
|
+
"play.adjudication",
|
|
126
|
+
"play.state",
|
|
127
|
+
"play.narrative",
|
|
128
|
+
"play.continue-stage",
|
|
129
|
+
"play.tabletop",
|
|
130
|
+
],
|
|
131
|
+
entries: personPromptEntries(),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
anchor: "play.narrative",
|
|
135
|
+
before: ["play.tabletop"],
|
|
136
|
+
entries: continuationPromptEntries(),
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
/**
|
|
140
|
+
* Insert the narrative-person and continuation builtins that an ordered preset
|
|
141
|
+
* does not list yet. Same identity contract as the tabletop insertion: files
|
|
142
|
+
* that are not ordered v2 or do not parse come back unchanged, and the same
|
|
143
|
+
* object is returned when nothing needs inserting.
|
|
144
|
+
*/
|
|
145
|
+
export function withAlternativePromptBuiltins(files) {
|
|
146
|
+
const source = files["preset.yaml"];
|
|
147
|
+
if (source === undefined)
|
|
148
|
+
return files;
|
|
149
|
+
const document = parseDocument(source, { uniqueKeys: true });
|
|
150
|
+
if (document.errors.length > 0 ||
|
|
151
|
+
document.warnings.length > 0 ||
|
|
152
|
+
!isMap(document.contents) ||
|
|
153
|
+
document.get("format") !== "narraeon.play-preset/v2")
|
|
154
|
+
return files;
|
|
155
|
+
const playPrompts = document.get("playPrompts", true);
|
|
156
|
+
if (!isSeq(playPrompts))
|
|
157
|
+
return files;
|
|
158
|
+
let changed = false;
|
|
159
|
+
for (const group of alternativeGroups()) {
|
|
160
|
+
if (group.entries.some((entry) => hasEntry(playPrompts, entry.id)))
|
|
161
|
+
continue;
|
|
162
|
+
const anchor = playPrompts.items.findIndex((item) => entryField(item, "id") === group.anchor);
|
|
163
|
+
const fallback = playPrompts.items.findIndex((item) => {
|
|
164
|
+
const id = entryField(item, "id");
|
|
165
|
+
return ((typeof id === "string" && group.before.includes(id)) ||
|
|
166
|
+
entryField(item, "kind") === "world");
|
|
167
|
+
});
|
|
168
|
+
const at = anchor >= 0
|
|
169
|
+
? anchor + 1
|
|
170
|
+
: fallback >= 0
|
|
171
|
+
? fallback
|
|
172
|
+
: playPrompts.items.length;
|
|
173
|
+
playPrompts.items.splice(at, 0, ...group.entries.map((entry) => document.createNode(entry)));
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
return changed ? { ...files, "preset.yaml": document.toString() } : files;
|
|
177
|
+
}
|
|
178
|
+
/** Every builtin insertion a stored or imported ordered preset must receive. */
|
|
179
|
+
export function withCurrentPromptBuiltins(files) {
|
|
180
|
+
return withAlternativePromptBuiltins(withTabletopPromptBuiltins(files));
|
|
181
|
+
}
|
|
182
|
+
function alternativeEntries(prefix, ids, enabledId) {
|
|
183
|
+
return ids.map((id) => ({
|
|
184
|
+
id: `${prefix}${id}`,
|
|
185
|
+
kind: "builtin",
|
|
186
|
+
builtin: `${prefix}${id}`,
|
|
187
|
+
enabled: id === enabledId,
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
function personPromptEntries() {
|
|
191
|
+
return alternativeEntries("play.person-", ["second", "first", "third"], "second");
|
|
192
|
+
}
|
|
193
|
+
function continuationPromptEntries() {
|
|
194
|
+
return alternativeEntries("play.continue-", ["stage", "advance"], "stage");
|
|
195
|
+
}
|
|
121
196
|
function hasEntry(sequence, id) {
|
|
122
197
|
return sequence.items.some((item) => entryField(item, "id") === id);
|
|
123
198
|
}
|
|
@@ -239,9 +314,12 @@ export function migrateLegacyPlayPrompts(files, narrative, excludedPaths = []) {
|
|
|
239
314
|
if ((path.startsWith("blocks/") || path.startsWith("prompts/")) &&
|
|
240
315
|
path.endsWith(".md"))
|
|
241
316
|
add(path, false);
|
|
242
|
-
//
|
|
317
|
+
// Converted presets gain the person and continuation builtins and then the
|
|
318
|
+
// tabletop builtin just before the world placeholder. The legacy style and
|
|
319
|
+
// narrative copies keep their own wording as user text; the current builtins
|
|
320
|
+
// arrive beside them.
|
|
243
321
|
const worldIndex = entries.findIndex((entry) => entry.kind === "world");
|
|
244
|
-
entries.splice(worldIndex, 0, {
|
|
322
|
+
entries.splice(worldIndex, 0, ...personPromptEntries(), ...continuationPromptEntries(), {
|
|
245
323
|
id: "play.tabletop",
|
|
246
324
|
kind: "builtin",
|
|
247
325
|
builtin: "play.tabletop",
|
|
@@ -66,6 +66,15 @@ export class FileNativePlayDocuments {
|
|
|
66
66
|
// Creation capabilities come from this prefix, not past directory slots.
|
|
67
67
|
this.#declaredDirectories = declaredStateDirectories(bootstrap);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Re-grant one exact read the model already completed in a retained
|
|
71
|
+
* transcript. The scope is resolved against the current snapshot again, so a
|
|
72
|
+
* document or node that no longer exists grants nothing. Returns whether the
|
|
73
|
+
* scope resolved.
|
|
74
|
+
*/
|
|
75
|
+
replayRead(shortRef, locator) {
|
|
76
|
+
return authorizeResolvedRead(this.#reads, this.#candidate.snapshot, shortRef, locator);
|
|
77
|
+
}
|
|
69
78
|
execute(call, history, options = {}) {
|
|
70
79
|
let historyQuery;
|
|
71
80
|
const query = () => (historyQuery ??= new HistoryQuery(options.historyScope ?? "local-document-context", historyInputs(history)));
|
|
@@ -156,26 +165,31 @@ function bootstrapAuthorizations(bootstrap, snapshot) {
|
|
|
156
165
|
for (const { status, complete, readAuthorization } of bootstrap.coverage) {
|
|
157
166
|
if (status !== "resolved" || !complete || readAuthorization === undefined)
|
|
158
167
|
continue;
|
|
159
|
-
|
|
160
|
-
const resolved = snapshot.query({
|
|
161
|
-
kind: "read_document",
|
|
162
|
-
document: { shortRef: readAuthorization.shortRef },
|
|
163
|
-
});
|
|
164
|
-
if (resolved.kind === "read_document" && resolved.ok)
|
|
165
|
-
authorizeRead(result, snapshot.id, resolved.document.shortRef, null);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
const selected = snapshot.query({
|
|
169
|
-
kind: "select_node",
|
|
170
|
-
document: { shortRef: readAuthorization.shortRef },
|
|
171
|
-
locator: readAuthorization.locator,
|
|
172
|
-
});
|
|
173
|
-
if (selected.kind === "select_node" && selected.ok)
|
|
174
|
-
authorizeRead(result, snapshot.id, selected.document.shortRef, selected.node.locator);
|
|
175
|
-
}
|
|
168
|
+
authorizeResolvedRead(result, snapshot, readAuthorization.shortRef, readAuthorization.locator);
|
|
176
169
|
}
|
|
177
170
|
return result;
|
|
178
171
|
}
|
|
172
|
+
function authorizeResolvedRead(authorizations, snapshot, shortRef, locator) {
|
|
173
|
+
if (locator === null) {
|
|
174
|
+
const resolved = snapshot.query({
|
|
175
|
+
kind: "read_document",
|
|
176
|
+
document: { shortRef },
|
|
177
|
+
});
|
|
178
|
+
if (resolved.kind !== "read_document" || !resolved.ok)
|
|
179
|
+
return false;
|
|
180
|
+
authorizeRead(authorizations, snapshot.id, resolved.document.shortRef, null);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
const selected = snapshot.query({
|
|
184
|
+
kind: "select_node",
|
|
185
|
+
document: { shortRef },
|
|
186
|
+
locator,
|
|
187
|
+
});
|
|
188
|
+
if (selected.kind !== "select_node" || !selected.ok)
|
|
189
|
+
return false;
|
|
190
|
+
authorizeRead(authorizations, snapshot.id, selected.document.shortRef, selected.node.locator);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
179
193
|
function declaredStateDirectories(bootstrap) {
|
|
180
194
|
// Catalog coverage is the durable compiler projection shared by current
|
|
181
195
|
// state_list contexts and legacy context_list contexts.
|
|
@@ -131,16 +131,64 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
131
131
|
export function restorePlayDocuments(files, context, events) {
|
|
132
132
|
const documents = new FileNativePlayDocuments(files);
|
|
133
133
|
try {
|
|
134
|
-
// Read authorizations are memory-only
|
|
135
|
-
// its bootstrap,
|
|
134
|
+
// Read authorizations are memory-only (ADR-0044). A restored prefix starts
|
|
135
|
+
// from its bootstrap, then replays the exact reads the retained transcript
|
|
136
|
+
// still shows the model, so it is not asked to read again what it has
|
|
137
|
+
// already seen. Stale write authority is caught by the commit's hash.
|
|
136
138
|
const runs = playPromptRunsThroughEvents(context, events);
|
|
137
139
|
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
140
|
+
for (const read of completedReadsThroughEvents(context, events))
|
|
141
|
+
documents.replayRead(read.shortRef, read.locator);
|
|
138
142
|
}
|
|
139
143
|
catch (error) {
|
|
140
144
|
throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
|
|
141
145
|
}
|
|
142
146
|
return documents;
|
|
143
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* The exact document scopes successfully read by context_read within the
|
|
150
|
+
* selected event prefix, in event order. Tool results are keyed by exchange and
|
|
151
|
+
* call ID, and the exchange is the one of the nearest preceding assistant event.
|
|
152
|
+
*/
|
|
153
|
+
function completedReadsThroughEvents(context, events) {
|
|
154
|
+
const completed = new Map(context.completedTools.map((tool) => [tool.key, tool.result]));
|
|
155
|
+
const reads = [];
|
|
156
|
+
let exchange = 0;
|
|
157
|
+
for (const event of events) {
|
|
158
|
+
if (event.kind === "assistant") {
|
|
159
|
+
exchange = event.exchange;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (event.kind !== "tool_result" || !event.ok)
|
|
163
|
+
continue;
|
|
164
|
+
const result = completed.get(`${exchange}:${event.callId}`);
|
|
165
|
+
const authorization = result?.readAuthorization;
|
|
166
|
+
if (!result?.ok || !isReplayableReadAuthorization(authorization))
|
|
167
|
+
continue;
|
|
168
|
+
reads.push({
|
|
169
|
+
shortRef: authorization.shortRef,
|
|
170
|
+
locator: structuredClone(authorization.locator),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return reads;
|
|
174
|
+
}
|
|
175
|
+
function isReplayableReadAuthorization(value) {
|
|
176
|
+
if (typeof value !== "object" || value === null)
|
|
177
|
+
return false;
|
|
178
|
+
const { shortRef, locator } = value;
|
|
179
|
+
if (typeof shortRef !== "string")
|
|
180
|
+
return false;
|
|
181
|
+
if (locator === null)
|
|
182
|
+
return true;
|
|
183
|
+
if (typeof locator !== "object" || locator === null)
|
|
184
|
+
return false;
|
|
185
|
+
const { yaml, markdown } = locator;
|
|
186
|
+
if (Array.isArray(yaml))
|
|
187
|
+
return yaml.every((segment) => typeof segment === "string" ||
|
|
188
|
+
(Number.isSafeInteger(segment) && Number(segment) >= 0));
|
|
189
|
+
return (Array.isArray(markdown) &&
|
|
190
|
+
markdown.every((segment) => typeof segment === "string"));
|
|
191
|
+
}
|
|
144
192
|
export function independentContextCopy(source) {
|
|
145
193
|
const context = structuredClone(source);
|
|
146
194
|
context.continuityContextId ??= source.chainId;
|
|
@@ -236,16 +236,48 @@ export function renderDocumentWritePosition(shortRef, fact, locale) {
|
|
|
236
236
|
? `最近写入位置 @${shortRef}:\n正文:${worldWritePositionText(fact.lastBodyWrite, locale)}\n元信息:${worldWritePositionText(fact.lastMetadataWrite, locale)}`
|
|
237
237
|
: `Last committed write @${shortRef}:\nBody: ${worldWritePositionText(fact.lastBodyWrite, locale)}\nMetadata: ${worldWritePositionText(fact.lastMetadataWrite, locale)}`;
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Lists only documents the current tool response changed or that exceed their
|
|
241
|
+
* advisory limit, so the report stays proportional to the edit rather than to
|
|
242
|
+
* the package size.
|
|
243
|
+
*/
|
|
244
|
+
export function renderWorldMaintenanceReport(report, changedShortRefs, locale) {
|
|
240
245
|
if (report === undefined)
|
|
241
246
|
return "";
|
|
242
247
|
const zh = locale === "zh-CN";
|
|
248
|
+
const heading = zh
|
|
249
|
+
? "# 文档整理报告(UTF-8 字节;超限仅提示)"
|
|
250
|
+
: "# Document maintenance (UTF-8 bytes; advisory only)";
|
|
251
|
+
const documents = report.documents.filter((document) => document.overAdvisory || changedShortRefs.has(document.shortRef));
|
|
252
|
+
const slots = report.slots.filter((slot) => slot.overAdvisory ||
|
|
253
|
+
slot.documents.some((shortRef) => changedShortRefs.has(shortRef)));
|
|
254
|
+
if (documents.length === 0 && slots.length === 0)
|
|
255
|
+
return `${heading}\n${zh ? "本次没有改动世界文档,也没有文档或材料槽超过建议上限。" : "No world document changed in this response, and no document or slot exceeds its advisory limit."}`;
|
|
256
|
+
const allColumns = [
|
|
257
|
+
{ label: zh ? "正文" : "body", value: (d) => d.bodyBytes },
|
|
258
|
+
{
|
|
259
|
+
label: zh ? "注入贡献" : "injection contribution",
|
|
260
|
+
value: (d) => d.injectedBytes,
|
|
261
|
+
},
|
|
262
|
+
{ label: zh ? "建议上限" : "advisory", value: (d) => d.advisoryBytes },
|
|
263
|
+
{ label: zh ? "基准" : "baseline", value: (d) => d.baselineBytes },
|
|
264
|
+
{ label: zh ? "增长" : "growth", value: (d) => d.growthBytes },
|
|
265
|
+
];
|
|
266
|
+
// Columns that are unset for every listed document carry no information.
|
|
267
|
+
const columns = allColumns.filter(({ value }) => documents.some((document) => value(document) !== null));
|
|
268
|
+
const overAdvisory = report.documents.some(({ overAdvisory }) => overAdvisory) ||
|
|
269
|
+
report.slots.some(({ overAdvisory }) => overAdvisory);
|
|
243
270
|
return [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
heading,
|
|
272
|
+
...(overAdvisory
|
|
273
|
+
? [
|
|
274
|
+
`${zh ? "世界材料 / 其中检查点历史 / 作者提示 / Runtime 提示 / 工具定义" : "World materials / checkpoint history subset / author instructions / Runtime instructions / tool definitions"}: ${report.worldMaterialsBytes} / ${report.checkpointHistoryBytes} / ${report.authorInstructionBytes} / ${report.runtimeInstructionBytes} / ${report.toolDefinitionBytes}`,
|
|
275
|
+
]
|
|
276
|
+
: []),
|
|
277
|
+
...(documents.length > 0
|
|
278
|
+
? [columns.map(({ label }) => label).join(" / ")]
|
|
279
|
+
: []),
|
|
280
|
+
...documents.map((document) => `@${document.shortRef}: ${columns.map(({ value }) => value(document) ?? "—").join(" / ")}${document.overAdvisory ? (zh ? "(超过建议)" : " (over advisory)") : ""}`),
|
|
281
|
+
...slots.map((slot) => `${zh ? "材料槽" : "Slot"} ${slot.id}: ${slot.bytes} / ${slot.advisoryBytes ?? "—"}${slot.overAdvisory ? (zh ? "(超过建议)" : " (over advisory)") : ""}`),
|
|
250
282
|
].join("\n");
|
|
251
283
|
}
|
|
@@ -487,7 +487,10 @@ function validOptionalTitle(value) {
|
|
|
487
487
|
!/[\r\n]/u.test(value)));
|
|
488
488
|
}
|
|
489
489
|
function validRunStatus(value) {
|
|
490
|
-
return value === "ready" ||
|
|
490
|
+
return (value === "ready" ||
|
|
491
|
+
value === "running" ||
|
|
492
|
+
value === "paused" ||
|
|
493
|
+
value === "interrupted");
|
|
491
494
|
}
|
|
492
495
|
function validCount(value) {
|
|
493
496
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
@@ -329,6 +329,7 @@ export class SettingAuthoringTransaction {
|
|
|
329
329
|
execute(calls) {
|
|
330
330
|
const normalized = calls.map(normalizeCall);
|
|
331
331
|
const results = [];
|
|
332
|
+
const responseBase = this.files();
|
|
332
333
|
let lastSuccessfulWrite = -1;
|
|
333
334
|
for (const call of normalized) {
|
|
334
335
|
const before = this.files();
|
|
@@ -344,7 +345,9 @@ export class SettingAuthoringTransaction {
|
|
|
344
345
|
if (lastSuccessfulWrite >= 0) {
|
|
345
346
|
this.#review = this.#inspect();
|
|
346
347
|
const settled = results[lastSuccessfulWrite];
|
|
347
|
-
|
|
348
|
+
// The stored review stays cumulative for the whole user request; the
|
|
349
|
+
// model only needs what this tool response changed.
|
|
350
|
+
settled.markdown = `${settled.markdown}\n\n${renderAutomaticReview(this.#review, fileDiff(responseBase, this.#snapshot.files), this.#snapshot, this.#locale)}`;
|
|
348
351
|
}
|
|
349
352
|
return results;
|
|
350
353
|
}
|
|
@@ -450,9 +453,16 @@ export class SettingAuthoringTransaction {
|
|
|
450
453
|
}
|
|
451
454
|
}
|
|
452
455
|
}
|
|
453
|
-
function renderAutomaticReview(review, locale) {
|
|
454
|
-
if (review.status === "usable")
|
|
455
|
-
|
|
456
|
+
function renderAutomaticReview(review, responseDiff, snapshot, locale) {
|
|
457
|
+
if (review.status === "usable" && review.preview !== null) {
|
|
458
|
+
const changed = responseDiff.length;
|
|
459
|
+
const cumulative = review.diff.length;
|
|
460
|
+
const changedPaths = new Set(responseDiff.map(({ path }) => path));
|
|
461
|
+
const changedShortRefs = new Set(listQueryableWorldDocuments(snapshot)
|
|
462
|
+
.filter(({ logicalPath }) => changedPaths.has(logicalPath))
|
|
463
|
+
.map(({ shortRef }) => shortRef));
|
|
464
|
+
return `${localized(locale, `# Current-tree review passed\n\nContent-tree validation and the real Prompt Preview both passed. This tool response changed ${changed} file(s) in the content package${cumulative === changed ? "" : `; ${cumulative} file(s) changed so far in this user request`}. Use this review and the coverage below to decide whether the current user request is satisfied or further edits are needed.`, `# 当前树自动检查通过\n\n内容树校验和真实 Prompt Preview 均已通过;本次工具响应修改内容包中的 ${changed} 个文件${cumulative === changed ? "" : `,本次用户请求累计改动 ${cumulative} 个文件`}。请根据本次检查与下方覆盖报告判断当前用户要求是否已经满足,或是否还需修改。`)}\n\n${renderPlayCoverage(buildSettingAuthoringPlayCoverage(snapshot, review.preview, responseDiff), locale)}\n\n${renderWorldMaintenanceReport(review.preview.compilation.maintenance, changedShortRefs, locale)}`;
|
|
465
|
+
}
|
|
456
466
|
return [
|
|
457
467
|
localized(locale, "# Current-tree review needs repair", "# 当前树自动检查需要修复"),
|
|
458
468
|
"",
|
|
@@ -561,8 +571,6 @@ function playAccessDetail(access) {
|
|
|
561
571
|
return details[access];
|
|
562
572
|
}
|
|
563
573
|
function renderPlayCoverage(coverage, locale) {
|
|
564
|
-
if (coverage === null)
|
|
565
|
-
return localized(locale, "# Play-consumption coverage\n\nCoverage is unavailable until the real Prompt Preview succeeds.", "# 游玩读取覆盖\n\n真实 Prompt Preview 通过后才会提供覆盖报告。");
|
|
566
574
|
const labels = locale === "zh-CN"
|
|
567
575
|
? {
|
|
568
576
|
full_injected: "全文注入",
|
|
@@ -4,7 +4,7 @@ import { InvalidContentTreeError, } from "../content/ContentWorkspace.js";
|
|
|
4
4
|
import { contentTreeFingerprint } from "../content/ContentTreeFingerprint.js";
|
|
5
5
|
import { equalModelHostBinding } from "../model/ModelHost.js";
|
|
6
6
|
import { settingImprovementPromptForBinding, } from "../play/FileNativePlayPresetStore.js";
|
|
7
|
-
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, } from "../authoring/ModelToolConversation.js";
|
|
7
|
+
import { appendAuthoringUserMessage, ModelToolConversation, rememberAuthoringRequest, resumePausedAuthoringConversation, } from "../authoring/ModelToolConversation.js";
|
|
8
8
|
import { summarizeStoredSettingImprovementSession, } from "./FileNativeSettingImprovementStore.js";
|
|
9
9
|
import { SettingAuthoringTransaction, settingImprovementRuntimeContract, settingImprovementToolDefinitions, } from "./SettingAuthoringTransaction.js";
|
|
10
10
|
class RecoveryWriteConflictError extends Error {
|
|
@@ -185,33 +185,48 @@ export class SettingImprovementSession {
|
|
|
185
185
|
session = await this.#store.read(input.continuation.sessionId);
|
|
186
186
|
assertPackage(session, input.packageId);
|
|
187
187
|
}
|
|
188
|
-
session
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
200
|
-
const { playPreset, bootstrap } = await this.#compileCurrent(input.packageId, session.modelBinding);
|
|
201
|
-
session.schemaVersion = 3;
|
|
202
|
-
session.requests ??= [];
|
|
203
|
-
session.requests.push({
|
|
204
|
-
requestId,
|
|
205
|
-
modelItemStart: session.modelItems.length,
|
|
206
|
-
bootstrap,
|
|
207
|
-
playPreset: structuredClone(playPreset),
|
|
188
|
+
return this.#startRequest(session, requestId, async (next) => {
|
|
189
|
+
const { playPreset, bootstrap } = await this.#compileCurrent(input.packageId, next.modelBinding);
|
|
190
|
+
next.schemaVersion = 3;
|
|
191
|
+
next.requests ??= [];
|
|
192
|
+
next.requests.push({
|
|
193
|
+
requestId,
|
|
194
|
+
modelItemStart: next.modelItems.length,
|
|
195
|
+
bootstrap,
|
|
196
|
+
playPreset: structuredClone(playPreset),
|
|
197
|
+
});
|
|
198
|
+
appendAuthoringUserMessage(next, message, requestId);
|
|
208
199
|
});
|
|
209
|
-
appendAuthoringUserMessage(session, message, requestId);
|
|
210
|
-
await this.#store.save(session);
|
|
211
|
-
return { session, run: this.#startRun(session, host, requestId) };
|
|
212
200
|
});
|
|
213
201
|
return prepared.run ?? this.#view(prepared.session);
|
|
214
202
|
}
|
|
203
|
+
/** Continues a paused conversation with the frozen request it paused in. */
|
|
204
|
+
async resume(input) {
|
|
205
|
+
const requestId = requiredRequestId(input.requestId);
|
|
206
|
+
const prepared = await this.#mutate(async () => {
|
|
207
|
+
const session = await this.#store.read(input.sessionId);
|
|
208
|
+
assertPackage(session, input.packageId);
|
|
209
|
+
return this.#startRequest(session, requestId, (next) => resumePausedAuthoringConversation(next));
|
|
210
|
+
});
|
|
211
|
+
return prepared.run ?? this.#view(prepared.session);
|
|
212
|
+
}
|
|
213
|
+
async #startRequest(stored, requestId, prepare) {
|
|
214
|
+
const session = await this.#recoverIfNeeded(stored);
|
|
215
|
+
if (session.completedRequestIds.includes(requestId))
|
|
216
|
+
return { session, run: null };
|
|
217
|
+
const running = this.#conversation.running(session.sessionId);
|
|
218
|
+
if (running !== null) {
|
|
219
|
+
if (running.requestId !== requestId)
|
|
220
|
+
throw new Error("Another message in this setting-improvement conversation is still running");
|
|
221
|
+
return { session, run: running.promise };
|
|
222
|
+
}
|
|
223
|
+
const host = await this.#bindExistingModelHost(session.modelBinding);
|
|
224
|
+
if (!equalModelHostBinding(session.modelBinding, host.binding()))
|
|
225
|
+
throw new Error("No saved model connection matches this setting-improvement conversation");
|
|
226
|
+
await prepare(session);
|
|
227
|
+
await this.#store.save(session);
|
|
228
|
+
return { session, run: this.#startRun(session, host, requestId) };
|
|
229
|
+
}
|
|
215
230
|
async preview(packageId, sessionId) {
|
|
216
231
|
const session = sessionId === undefined ? null : await this.#store.read(sessionId);
|
|
217
232
|
if (session !== null)
|
|
@@ -307,7 +322,6 @@ export class SettingImprovementSession {
|
|
|
307
322
|
view: (next) => this.#view(next),
|
|
308
323
|
failure: {
|
|
309
324
|
emptyResponse: "The model returned an empty tool-free authoring response",
|
|
310
|
-
exchangeLimit: "Setting improvement exceeded 64 model exchanges for one user message",
|
|
311
325
|
cancelled: "The setting-improvement response was cancelled. Complete tool responses already published to the current tree were retained.",
|
|
312
326
|
interrupted: "The setting-improvement response was interrupted",
|
|
313
327
|
},
|
|
@@ -246,7 +246,7 @@ function validateSession(value) {
|
|
|
246
246
|
/[\r\n\0]/u.test(value.worldTitle) ||
|
|
247
247
|
typeof value.epochId !== "string" ||
|
|
248
248
|
(value.locale !== "en" && value.locale !== "zh-CN") ||
|
|
249
|
-
!["ready", "running", "interrupted"].includes(String(value.runStatus)) ||
|
|
249
|
+
!["ready", "running", "paused", "interrupted"].includes(String(value.runStatus)) ||
|
|
250
250
|
!finite(value.createdAt) ||
|
|
251
251
|
!finite(value.updatedAt) ||
|
|
252
252
|
value.updatedAt < value.createdAt ||
|