narraeon 0.6.0 → 0.6.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/README.md +11 -0
- package/README.zh-CN.md +8 -0
- package/dist/node/protocol/v1.js +12 -2
- package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
- package/dist/node/runtime/V1Runtime.js +5 -5
- package/dist/node/runtime/archive/WorldArchives.js +14 -46
- package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
- package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
- package/dist/node/runtime/authoring/ModelToolConversation.js +25 -2
- package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
- package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
- package/dist/node/runtime/config/dataRootLock.js +108 -0
- package/dist/node/runtime/content/ContentWorkspace.js +0 -10
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
- package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
- package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
- package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
- package/dist/node/runtime/play/PlayCallChain.js +338 -366
- package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
- package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
- package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
- package/dist/node/runtime/prompt/WorldMaintenanceReport.js +39 -7
- package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +7 -16
- package/dist/node/runtime/setting/SettingAuthoringTransaction.js +14 -6
- package/dist/node/runtime/setting/SettingImprovementSession.js +39 -25
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
- package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
- package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
- package/dist/node/runtime/world/WorldActivity.js +14 -22
- package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
- package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
- package/dist/node/runtime/world/WorldQueue.js +46 -0
- package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
- package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +1 -13
- package/dist/node/runtime/world-revision/WorldRevisionSession.js +29 -2
- package/dist/node/server/mobile.js +52 -0
- package/dist/node/server/startWebServer.js +15 -8
- package/dist/web/assets/index-CXnFaYK9.js +1323 -0
- package/dist/web/assets/{index-BJ-1Mb0e.css → index-LPMV0pl3.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
- package/dist/web/assets/index-BgBL8G-A.js +0 -1323
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { HistoryQuery, historyInputs } from "../history/HistoryQuery.js";
|
|
2
3
|
import { renderHistoryResult } from "../history/HistoryRendering.js";
|
|
3
4
|
import { stateDirectoryHandle, parseStateDirectoryHandle, } from "../world/StateDirectoryHandles.js";
|
|
4
5
|
import { renderDocumentWritePosition } from "../prompt/WorldMaintenanceReport.js";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
6
|
import { stringify } from "yaml";
|
|
7
7
|
import { renderPlayerViewBindings } from "../world/PlayerViewBindings.js";
|
|
8
8
|
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
@@ -56,66 +56,16 @@ export class FileNativePlayDocuments {
|
|
|
56
56
|
get committedSnapshot() {
|
|
57
57
|
return this.#committedSnapshot;
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.restoreAuthorizationCheckpoint(previousAuthorization);
|
|
67
|
-
for (const [ref, locators] of injected.documents) {
|
|
68
|
-
for (const locator of locators ?? [null])
|
|
69
|
-
authorizeRead(this.#reads, this.#candidate.snapshot.id, ref, locator);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
59
|
+
/**
|
|
60
|
+
* Read authorizations live only in memory (ADR-0044). A fresh binding starts
|
|
61
|
+
* from the bootstrap's own reads; writes are guarded at commit time by the
|
|
62
|
+
* previous-content hash of each changed file.
|
|
63
|
+
*/
|
|
64
|
+
bindBootstrap(bootstrap) {
|
|
65
|
+
this.#reads = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
|
|
72
66
|
// Creation capabilities come from this prefix, not past directory slots.
|
|
73
67
|
this.#declaredDirectories = declaredStateDirectories(bootstrap);
|
|
74
68
|
}
|
|
75
|
-
authorizationCheckpoint() {
|
|
76
|
-
return {
|
|
77
|
-
schemaVersion: 1,
|
|
78
|
-
kind: "play_document_authorizations",
|
|
79
|
-
stateFingerprint: fingerprintStateFiles(this.#candidate.files),
|
|
80
|
-
documents: [...this.#reads.documents.entries()]
|
|
81
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
82
|
-
.map(([shortRef, locators]) => ({
|
|
83
|
-
shortRef,
|
|
84
|
-
locators: locators === null
|
|
85
|
-
? null
|
|
86
|
-
: locators
|
|
87
|
-
.filter((locator, index, all) => all.findIndex((candidate) => sameLocator(candidate, locator)) === index)
|
|
88
|
-
.map((locator) => structuredClone(locator)),
|
|
89
|
-
})),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
restoreAuthorizationCheckpoint(checkpoint) {
|
|
93
|
-
if (!isPlayDocumentAuthorizationCheckpoint(checkpoint))
|
|
94
|
-
throw new TypeError("The play-document authorization checkpoint has an invalid format");
|
|
95
|
-
if (checkpoint.stateFingerprint !==
|
|
96
|
-
fingerprintStateFiles(this.#candidate.files))
|
|
97
|
-
throw new TypeError("The play-document authorization checkpoint does not match the current state");
|
|
98
|
-
const documents = new Map();
|
|
99
|
-
for (const { shortRef, locators } of checkpoint.documents) {
|
|
100
|
-
if (documentDescriptorByRef(this.#candidate.snapshot, shortRef) === null)
|
|
101
|
-
throw new TypeError(`The play-document authorization checkpoint refers to missing @${shortRef}`);
|
|
102
|
-
const restoredLocators = locators?.map((locator) => {
|
|
103
|
-
const selected = this.#candidate.snapshot.query({
|
|
104
|
-
kind: "select_node",
|
|
105
|
-
document: { shortRef },
|
|
106
|
-
locator,
|
|
107
|
-
});
|
|
108
|
-
if (selected.kind !== "select_node" || !selected.ok)
|
|
109
|
-
throw new TypeError(`The play-document authorization checkpoint refers to a missing node in @${shortRef}`);
|
|
110
|
-
return structuredClone(selected.node.locator);
|
|
111
|
-
});
|
|
112
|
-
documents.set(shortRef, restoredLocators ?? null);
|
|
113
|
-
}
|
|
114
|
-
this.#reads = {
|
|
115
|
-
snapshotId: this.#candidate.snapshot.id,
|
|
116
|
-
documents,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
69
|
execute(call, history, options = {}) {
|
|
120
70
|
let historyQuery;
|
|
121
71
|
const query = () => (historyQuery ??= new HistoryQuery(options.historyScope ?? "local-document-context", historyInputs(history)));
|
|
@@ -254,15 +204,6 @@ function authorizeToolRead(authorizations, snapshot, result) {
|
|
|
254
204
|
return;
|
|
255
205
|
authorizeRead(authorizations, snapshot.id, authorization.shortRef, authorization.locator);
|
|
256
206
|
}
|
|
257
|
-
function sameLocator(left, right) {
|
|
258
|
-
if (left === null || right === null)
|
|
259
|
-
return left === right;
|
|
260
|
-
const leftPath = "yaml" in left ? left.yaml : left.markdown;
|
|
261
|
-
const rightPath = "yaml" in right ? right.yaml : right.markdown;
|
|
262
|
-
return ("yaml" in left === "yaml" in right &&
|
|
263
|
-
leftPath.length === rightPath.length &&
|
|
264
|
-
leftPath.every((segment, index) => segment === rightPath[index]));
|
|
265
|
-
}
|
|
266
207
|
function parseYamlHandleSegment(segment) {
|
|
267
208
|
return /^(?:0|[1-9][0-9]*)$/u.test(segment) ? Number(segment) : segment;
|
|
268
209
|
}
|
|
@@ -914,64 +855,6 @@ export function fingerprintControl(files) {
|
|
|
914
855
|
hash.update(path).update("\0").update(contents).update("\0");
|
|
915
856
|
return `sha256:${hash.digest("hex")}`;
|
|
916
857
|
}
|
|
917
|
-
export function isPlayDocumentAuthorizationCheckpoint(value) {
|
|
918
|
-
if (!record(value) ||
|
|
919
|
-
!hasExactKeys(value, [
|
|
920
|
-
"schemaVersion",
|
|
921
|
-
"kind",
|
|
922
|
-
"stateFingerprint",
|
|
923
|
-
"documents",
|
|
924
|
-
]) ||
|
|
925
|
-
value.schemaVersion !== 1 ||
|
|
926
|
-
value.kind !== "play_document_authorizations" ||
|
|
927
|
-
typeof value.stateFingerprint !== "string" ||
|
|
928
|
-
!/^sha256:[0-9a-f]{64}$/u.test(value.stateFingerprint) ||
|
|
929
|
-
!Array.isArray(value.documents) ||
|
|
930
|
-
value.documents.length > 100_000)
|
|
931
|
-
return false;
|
|
932
|
-
const refs = new Set();
|
|
933
|
-
for (const document of value.documents) {
|
|
934
|
-
if (!record(document) ||
|
|
935
|
-
!hasExactKeys(document, ["shortRef", "locators"]) ||
|
|
936
|
-
typeof document.shortRef !== "string" ||
|
|
937
|
-
!/^[a-z][a-z0-9-]*$/u.test(document.shortRef) ||
|
|
938
|
-
refs.has(document.shortRef) ||
|
|
939
|
-
(document.locators !== null &&
|
|
940
|
-
(!Array.isArray(document.locators) ||
|
|
941
|
-
document.locators.length > 100_000 ||
|
|
942
|
-
!document.locators.every(isPersistedAuthorizedLocator))))
|
|
943
|
-
return false;
|
|
944
|
-
refs.add(document.shortRef);
|
|
945
|
-
}
|
|
946
|
-
return true;
|
|
947
|
-
}
|
|
948
|
-
function fingerprintStateFiles(files) {
|
|
949
|
-
const hash = createHash("sha256");
|
|
950
|
-
for (const [path, contents] of Object.entries(files)
|
|
951
|
-
.filter(([path]) => path.startsWith("state/"))
|
|
952
|
-
.sort(([left], [right]) => left.localeCompare(right)))
|
|
953
|
-
hash.update(path).update("\0").update(contents).update("\0");
|
|
954
|
-
return `sha256:${hash.digest("hex")}`;
|
|
955
|
-
}
|
|
956
|
-
function isPersistedAuthorizedLocator(value) {
|
|
957
|
-
if (!record(value))
|
|
958
|
-
return false;
|
|
959
|
-
if (hasExactKeys(value, ["yaml"]))
|
|
960
|
-
return (Array.isArray(value.yaml) &&
|
|
961
|
-
value.yaml.length <= 64 &&
|
|
962
|
-
value.yaml.every((segment) => typeof segment === "string" ||
|
|
963
|
-
(Number.isSafeInteger(segment) && Number(segment) >= 0)));
|
|
964
|
-
if (hasExactKeys(value, ["markdown"]))
|
|
965
|
-
return (Array.isArray(value.markdown) &&
|
|
966
|
-
value.markdown.length <= 64 &&
|
|
967
|
-
value.markdown.every((segment) => typeof segment === "string"));
|
|
968
|
-
return false;
|
|
969
|
-
}
|
|
970
|
-
function hasExactKeys(value, expected) {
|
|
971
|
-
const keys = Object.keys(value);
|
|
972
|
-
return (keys.length === expected.length &&
|
|
973
|
-
expected.every((key) => Object.hasOwn(value, key)));
|
|
974
|
-
}
|
|
975
858
|
function record(value) {
|
|
976
859
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
977
860
|
}
|
|
@@ -59,8 +59,8 @@ export async function runPlayFollowupRequests(input) {
|
|
|
59
59
|
failure ??= outcome.failure;
|
|
60
60
|
}
|
|
61
61
|
const extension = failure === undefined
|
|
62
|
-
? await input.artifacts.completeExtension(input.context.operationId, completed)
|
|
63
|
-
: await input.artifacts.failExtension(input.context.operationId, "recovery_required", failure, completed);
|
|
62
|
+
? await input.artifacts.completeExtension(input.context.worldId, input.context.operationId, completed)
|
|
63
|
+
: await input.artifacts.failExtension(input.context.worldId, input.context.operationId, "recovery_required", failure, completed);
|
|
64
64
|
return { outcomes, extension };
|
|
65
65
|
}
|
|
66
66
|
async function runOne(input, followup, prefix, requestSignal) {
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { captureNarrativeOrigin } from "../history/HistoryBackground.js";
|
|
2
|
-
import { FileNativePlayDocuments
|
|
2
|
+
import { FileNativePlayDocuments } from "./PlayDocumentTools.js";
|
|
3
3
|
import { currentPlayPrompt, playPromptRunsThroughEvents, } from "./PlayPromptRun.js";
|
|
4
4
|
import { isPlayerRoundMarker, playerInputAppend } from "./PlayContinuity.js";
|
|
5
5
|
import { PlayCallChainError } from "./PlayCallChainError.js";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Split a context at a selected player message for an edit: the prefix before
|
|
8
|
+
* it is kept, and the replacement is appended once the world has rewound and
|
|
9
|
+
* committed it (PlayCallChain.revisePlayer).
|
|
9
10
|
*/
|
|
10
11
|
export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
11
12
|
const prefix = structuredClone(source.events.slice(0, eventIndex));
|
|
12
13
|
const transcript = transcriptThroughEvents(source.transcript, prefix);
|
|
13
14
|
const player = source.events[eventIndex];
|
|
14
15
|
return {
|
|
15
|
-
// This append precedes the operation's materialization check, as before.
|
|
16
16
|
withReplacement(replacement) {
|
|
17
17
|
const events = [
|
|
18
18
|
...prefix,
|
|
@@ -46,7 +46,7 @@ export function preparePlayerRevisionTrace(source, eventIndex) {
|
|
|
46
46
|
pagePrefixAtEndpoint(endpoint) {
|
|
47
47
|
if (prefix.length === 0)
|
|
48
48
|
return null;
|
|
49
|
-
//
|
|
49
|
+
// A fresh context keeps the page before the edit as a detached prefix.
|
|
50
50
|
const prefixTranscript = transcriptThroughEvents(source.transcript, prefix);
|
|
51
51
|
return assembleRetainedTrace(source, endpoint, {
|
|
52
52
|
events: prefix,
|
|
@@ -119,7 +119,6 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
119
119
|
completedTools: source.completedTools
|
|
120
120
|
.filter(({ key }) => completedKeys === null || completedKeys.has(key))
|
|
121
121
|
.map((item) => structuredClone(item)),
|
|
122
|
-
documentAuthorizationCheckpoints: authorizationCheckpointsThroughEvents(source, selection.events, documents.authorizationCheckpoint()),
|
|
123
122
|
changedDocuments: changedDocumentsAtHead(source.changedDocuments, endpoint.baseline.state, endpoint.selected.state),
|
|
124
123
|
nextMaterials: structuredClone(selection.materials),
|
|
125
124
|
nextEventId: Math.max(0, ...selection.events.map(({ id }) => id)) + 1,
|
|
@@ -132,29 +131,16 @@ function assembleRetainedTrace(source, endpoint, selection) {
|
|
|
132
131
|
export function restorePlayDocuments(files, context, events) {
|
|
133
132
|
const documents = new FileNativePlayDocuments(files);
|
|
134
133
|
try {
|
|
135
|
-
|
|
134
|
+
// Read authorizations are memory-only: a restored prefix always starts from
|
|
135
|
+
// its bootstrap, and stale write authority is caught by the commit's hash.
|
|
136
136
|
const runs = playPromptRunsThroughEvents(context, events);
|
|
137
|
-
|
|
138
|
-
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
139
|
-
if (checkpoint !== undefined)
|
|
140
|
-
documents.restoreAuthorizationCheckpoint(checkpoint.authorization);
|
|
137
|
+
documents.bindBootstrap(currentPlayPrompt({ ...context, promptRuns: runs }).bootstrap);
|
|
141
138
|
}
|
|
142
139
|
catch (error) {
|
|
143
140
|
throw new PlayCallChainError(`Call-chain document authorization could not be restored: ${error instanceof Error ? error.message : "invalid checkpoint"}。`);
|
|
144
141
|
}
|
|
145
142
|
return documents;
|
|
146
143
|
}
|
|
147
|
-
function documentAuthorizationThroughEvents(context, events) {
|
|
148
|
-
const checkpoints = context.documentAuthorizationCheckpoints;
|
|
149
|
-
if (checkpoints === undefined || checkpoints.length === 0)
|
|
150
|
-
return undefined;
|
|
151
|
-
const selectedEventIds = new Set(events.map(({ id }) => id));
|
|
152
|
-
const selected = checkpoints.findLast(({ afterEventId }) => afterEventId === 0 || selectedEventIds.has(afterEventId));
|
|
153
|
-
// Legacy V1 records had no durable dynamic authorization. If the selected
|
|
154
|
-
// prefix predates their first lazily-written checkpoint, retain the exact
|
|
155
|
-
// old recovery behavior and rebuild only bootstrap authorization.
|
|
156
|
-
return selected === undefined ? undefined : structuredClone(selected);
|
|
157
|
-
}
|
|
158
144
|
export function independentContextCopy(source) {
|
|
159
145
|
const context = structuredClone(source);
|
|
160
146
|
context.continuityContextId ??= source.chainId;
|
|
@@ -162,20 +148,6 @@ export function independentContextCopy(source) {
|
|
|
162
148
|
delete context.branchedBeforePlayer;
|
|
163
149
|
return context;
|
|
164
150
|
}
|
|
165
|
-
function authorizationCheckpointsThroughEvents(context, events, fallback) {
|
|
166
|
-
const eventIds = new Set(events.map(({ id }) => id));
|
|
167
|
-
const selected = (context.documentAuthorizationCheckpoints ?? [])
|
|
168
|
-
.filter(({ afterEventId }) => afterEventId === 0 || eventIds.has(afterEventId))
|
|
169
|
-
.map((checkpoint) => structuredClone(checkpoint));
|
|
170
|
-
if (selected.length > 0)
|
|
171
|
-
return selected;
|
|
172
|
-
return [
|
|
173
|
-
{
|
|
174
|
-
afterEventId: Math.max(0, ...events.map(({ id }) => id)),
|
|
175
|
-
authorization: structuredClone(fallback),
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
}
|
|
179
151
|
function transcriptThroughEvents(transcript, events) {
|
|
180
152
|
const result = [];
|
|
181
153
|
let cursor = 0;
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { validPromptCompilation, validLogicalMessages, validPromptTools, validPlayFollowup, validProviderKind, validCacheStrategy, validToolStrategy, } from "../prompt/PromptCompilationCodec.js";
|
|
2
2
|
import { validAuthoringRequests, } from "../authoring/AuthoringRequestSnapshot.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, } from "node:fs/promises";
|
|
5
|
+
import { linkExclusive } from "../FileNativeExclusiveLink.js";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { isDeepStrictEqual } from "node:util";
|
|
7
8
|
import { fictionGameplay } from "../../protocol/gameplay.js";
|
|
@@ -486,7 +487,10 @@ function validOptionalTitle(value) {
|
|
|
486
487
|
!/[\r\n]/u.test(value)));
|
|
487
488
|
}
|
|
488
489
|
function validRunStatus(value) {
|
|
489
|
-
return value === "ready" ||
|
|
490
|
+
return (value === "ready" ||
|
|
491
|
+
value === "running" ||
|
|
492
|
+
value === "paused" ||
|
|
493
|
+
value === "interrupted");
|
|
490
494
|
}
|
|
491
495
|
function validCount(value) {
|
|
492
496
|
return Number.isSafeInteger(value) && Number(value) >= 0;
|
|
@@ -969,13 +973,11 @@ async function publishJson(path, value) {
|
|
|
969
973
|
const handle = await open(temporary, "wx", 0o600);
|
|
970
974
|
try {
|
|
971
975
|
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
972
|
-
await handle.sync();
|
|
973
976
|
}
|
|
974
977
|
finally {
|
|
975
978
|
await handle.close();
|
|
976
979
|
}
|
|
977
980
|
await rename(temporary, path);
|
|
978
|
-
await syncDirectory(dirname(path));
|
|
979
981
|
}
|
|
980
982
|
finally {
|
|
981
983
|
await rm(temporary, { force: true });
|
|
@@ -988,14 +990,12 @@ async function publishImmutableSource(path, contents) {
|
|
|
988
990
|
const handle = await open(temporary, "wx", 0o600);
|
|
989
991
|
try {
|
|
990
992
|
await handle.writeFile(contents, "utf8");
|
|
991
|
-
await handle.sync();
|
|
992
993
|
}
|
|
993
994
|
finally {
|
|
994
995
|
await handle.close();
|
|
995
996
|
}
|
|
996
997
|
try {
|
|
997
|
-
await
|
|
998
|
-
await syncDirectory(dirname(path));
|
|
998
|
+
await linkExclusive(temporary, path);
|
|
999
999
|
}
|
|
1000
1000
|
catch (error) {
|
|
1001
1001
|
if (!isNodeError(error) || error.code !== "EEXIST")
|
|
@@ -1008,15 +1008,6 @@ async function publishImmutableSource(path, contents) {
|
|
|
1008
1008
|
await rm(temporary, { force: true });
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
|
-
async function syncDirectory(path) {
|
|
1012
|
-
const handle = await open(path, "r");
|
|
1013
|
-
try {
|
|
1014
|
-
await handle.sync();
|
|
1015
|
-
}
|
|
1016
|
-
finally {
|
|
1017
|
-
await handle.close();
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
1011
|
function isRecord(value) {
|
|
1021
1012
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1022
1013
|
}
|
|
@@ -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
|
},
|