narraeon 0.1.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/LICENSE +201 -0
- package/README.md +164 -0
- package/README.zh-CN.md +164 -0
- package/dist/node/cli/NarraeonCli.js +163 -0
- package/dist/node/cli/main.js +38 -0
- package/dist/node/protocol/appPreferences.js +5 -0
- package/dist/node/protocol/contentTree.js +14 -0
- package/dist/node/protocol/modelConnections.js +110 -0
- package/dist/node/protocol/playerViews.js +1 -0
- package/dist/node/protocol/v1.js +278 -0
- package/dist/node/runtime/V1Runtime.js +625 -0
- package/dist/node/runtime/artifact/FileNativeArtifactStore.js +1914 -0
- package/dist/node/runtime/config/AppPreferencesStore.js +92 -0
- package/dist/node/runtime/config/appPaths.js +19 -0
- package/dist/node/runtime/content/ContentLimits.js +1 -0
- package/dist/node/runtime/content/ContentTreeFile.js +1 -0
- package/dist/node/runtime/content/ContentWorkspace.js +601 -0
- package/dist/node/runtime/content/CurrentTreeContentLibrary.js +721 -0
- package/dist/node/runtime/content/FileNativeContentTree.js +512 -0
- package/dist/node/runtime/extension/FrontendExtensionBundle.js +229 -0
- package/dist/node/runtime/extension/PlayerViewPanelProjector.js +121 -0
- package/dist/node/runtime/model/AiFailureLog.js +332 -0
- package/dist/node/runtime/model/FileNativeModelAdapters.js +1246 -0
- package/dist/node/runtime/model/ModelConnectionStore.js +378 -0
- package/dist/node/runtime/model/ModelHost.js +89 -0
- package/dist/node/runtime/model/ModelHostStream.js +367 -0
- package/dist/node/runtime/model/ProviderExchangeState.js +1 -0
- package/dist/node/runtime/model/ProviderStream.js +90 -0
- package/dist/node/runtime/model/SettingAuthorStream.js +118 -0
- package/dist/node/runtime/play/FileNativeContinuityCorrection.js +304 -0
- package/dist/node/runtime/play/FileNativePlayPresetStore.js +1856 -0
- package/dist/node/runtime/play/PlayCallChain.js +1280 -0
- package/dist/node/runtime/play/PlayDocumentTools.js +1121 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +183 -0
- package/dist/node/runtime/play/PlayPresetWorkbench.js +185 -0
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +1425 -0
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +192 -0
- package/dist/node/runtime/prompt/MaterialSelection.js +95 -0
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +1726 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +2028 -0
- package/dist/node/runtime/world/PlayerViewRenderer.js +322 -0
- package/dist/node/runtime/world/WorldDocumentStore.js +2808 -0
- package/dist/node/runtime/world/WorldOperationCoordinator.js +490 -0
- package/dist/node/runtime/world/WorldStateRevision.js +75 -0
- package/dist/node/server/createRuntime.js +6 -0
- package/dist/node/server/createServer.js +218 -0
- package/dist/node/server/main.js +25 -0
- package/dist/node/server/startWebServer.js +28 -0
- package/dist/node/shared/default-play-prompts.js +85 -0
- package/dist/node/shared/default-preset-host.js +538 -0
- package/dist/node/shared/default-setting-improvement-prompt.js +77 -0
- package/dist/node/shared/first-party-action-choices.js +164 -0
- package/dist/node/shared/first-party-generic-panels.js +105 -0
- package/dist/node/shared/first-party-play-preset-templates.js +41 -0
- package/dist/node/shared/first-party-player-view.js +179 -0
- package/dist/node/shared/play-preset-prompt-roles.js +4 -0
- package/dist/web/assets/index-CRGTZ9Ak.css +1 -0
- package/dist/web/assets/index-DDx1itzT.js +1192 -0
- package/dist/web/index.html +14 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
|
|
2
|
+
import { stringify } from "yaml";
|
|
3
|
+
import { PromptCompilationError, } from "../prompt/FileNativePromptCompiler.js";
|
|
4
|
+
import {} from "../world/WorldDocumentStore.js";
|
|
5
|
+
import { acceptWorldStateRevision, beginWorldStateRevision, fileNativeStateChanges, worldDocumentRevisionFailureMessage, } from "../world/WorldStateRevision.js";
|
|
6
|
+
class PlayDocumentToolFailure extends Error {
|
|
7
|
+
failureKind;
|
|
8
|
+
constructor(message, failureKind = "protocol") {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "PlayDocumentToolFailure";
|
|
11
|
+
this.failureKind = failureKind;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One operation-bound document context for the play call chain. Product flows
|
|
16
|
+
* submit tool calls here; document parsing, identity, lookup, locator semantics
|
|
17
|
+
* and revision mechanics remain behind WorldDocumentStore.open/query/revise.
|
|
18
|
+
*/
|
|
19
|
+
export class FileNativePlayDocuments {
|
|
20
|
+
#candidate;
|
|
21
|
+
#reads;
|
|
22
|
+
constructor(files) {
|
|
23
|
+
this.#candidate = openPlayCandidate({ ...files });
|
|
24
|
+
this.#reads = {
|
|
25
|
+
snapshotId: this.#candidate.snapshot.id,
|
|
26
|
+
documents: new Map(),
|
|
27
|
+
pendingReads: new Map(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
get snapshot() {
|
|
31
|
+
return this.#candidate.snapshot;
|
|
32
|
+
}
|
|
33
|
+
bindBootstrap(bootstrap) {
|
|
34
|
+
this.#reads = bootstrapAuthorizations(bootstrap, this.#candidate.snapshot);
|
|
35
|
+
}
|
|
36
|
+
authorizationCheckpoint() {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
kind: "play_document_authorizations",
|
|
40
|
+
stateFingerprint: fingerprintStateFiles(this.#candidate.files),
|
|
41
|
+
documents: [...this.#reads.documents.entries()]
|
|
42
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
43
|
+
.map(([shortRef, locators]) => ({
|
|
44
|
+
shortRef,
|
|
45
|
+
locators: locators === null
|
|
46
|
+
? null
|
|
47
|
+
: locators
|
|
48
|
+
.filter((locator, index, all) => all.findIndex((candidate) => sameLocator(candidate, locator)) === index)
|
|
49
|
+
.map((locator) => structuredClone(locator)),
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
restoreAuthorizationCheckpoint(checkpoint) {
|
|
54
|
+
if (!isPlayDocumentAuthorizationCheckpoint(checkpoint))
|
|
55
|
+
throw new TypeError("The play-document authorization checkpoint has an invalid format");
|
|
56
|
+
if (checkpoint.stateFingerprint !==
|
|
57
|
+
fingerprintStateFiles(this.#candidate.files))
|
|
58
|
+
throw new TypeError("The play-document authorization checkpoint does not match the current state");
|
|
59
|
+
const documents = new Map();
|
|
60
|
+
for (const { shortRef, locators } of checkpoint.documents) {
|
|
61
|
+
if (documentDescriptorByRef(this.#candidate.snapshot, shortRef) === null)
|
|
62
|
+
throw new TypeError(`The play-document authorization checkpoint refers to missing @${shortRef}`);
|
|
63
|
+
const restoredLocators = locators?.map((locator) => {
|
|
64
|
+
const selected = this.#candidate.snapshot.query({
|
|
65
|
+
kind: "select_node",
|
|
66
|
+
document: { shortRef },
|
|
67
|
+
locator,
|
|
68
|
+
});
|
|
69
|
+
if (selected.kind !== "select_node" || !selected.ok)
|
|
70
|
+
throw new TypeError(`The play-document authorization checkpoint refers to a missing node in @${shortRef}`);
|
|
71
|
+
return structuredClone(selected.node.locator);
|
|
72
|
+
});
|
|
73
|
+
documents.set(shortRef, restoredLocators ?? null);
|
|
74
|
+
}
|
|
75
|
+
this.#reads = {
|
|
76
|
+
snapshotId: this.#candidate.snapshot.id,
|
|
77
|
+
documents,
|
|
78
|
+
// Provider cursors are deliberately snapshot-local. A partial read has
|
|
79
|
+
// granted no write authority yet, so it restarts after cold recovery.
|
|
80
|
+
pendingReads: new Map(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
contextToolBudgetRequest(call, history) {
|
|
84
|
+
const preview = previewContextToolResult(this.#candidate.snapshot, history, call);
|
|
85
|
+
if (preview === null)
|
|
86
|
+
return null;
|
|
87
|
+
const retryParameter = call.name === "context_read" ? "max_bytes" : "limit";
|
|
88
|
+
return {
|
|
89
|
+
requestedResultBytes: Buffer.byteLength(preview.markdown, "utf8"),
|
|
90
|
+
previewMarkdown: preview.markdown,
|
|
91
|
+
retryParameter,
|
|
92
|
+
retryMinimum: call.name === "context_read" ? 4 : 1,
|
|
93
|
+
retryMaximum: contextToolRetryMaximum(call),
|
|
94
|
+
restartWithoutCursor: record(call.arguments) && typeof call.arguments.cursor === "string",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
contextToolRetryPreview(call, history, retryValue) {
|
|
98
|
+
const argumentsWithoutCursor = record(call.arguments)
|
|
99
|
+
? Object.fromEntries(Object.entries(call.arguments).filter(([key]) => key !== "cursor"))
|
|
100
|
+
: {};
|
|
101
|
+
const retryCall = {
|
|
102
|
+
...structuredClone(call),
|
|
103
|
+
id: `${call.id}-budget-retry`,
|
|
104
|
+
arguments: {
|
|
105
|
+
...argumentsWithoutCursor,
|
|
106
|
+
[call.name === "context_read" ? "maxBytes" : "limit"]: retryValue,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const preview = previewContextToolResult(this.#candidate.snapshot, history, retryCall);
|
|
110
|
+
if (preview === null)
|
|
111
|
+
throw new TypeError("context tool retry preview requires a read tool");
|
|
112
|
+
return { call: retryCall, markdown: preview.markdown };
|
|
113
|
+
}
|
|
114
|
+
execute(call, history) {
|
|
115
|
+
if (call.name === "context_search")
|
|
116
|
+
return executeContextSearch(this.#candidate.snapshot, history, call.arguments);
|
|
117
|
+
if (call.name === "context_list")
|
|
118
|
+
return executeContextList(this.#candidate.snapshot, history, call.arguments);
|
|
119
|
+
if (call.name === "context_read") {
|
|
120
|
+
const result = executeContextRead(this.#candidate.snapshot, history, call.arguments);
|
|
121
|
+
authorizeToolRead(this.#reads, this.#candidate.snapshot, result);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
if (call.name === "world_patch")
|
|
125
|
+
return applyPlayCandidatePatch(this.#candidate, this.#reads, call.arguments);
|
|
126
|
+
if (call.name === "world_create")
|
|
127
|
+
return applyPlayCandidateCreate(this.#candidate, this.#reads, call.arguments);
|
|
128
|
+
return toolFailure(`The current document context does not accept ${call.name}`);
|
|
129
|
+
}
|
|
130
|
+
stateChanges() {
|
|
131
|
+
return playCandidateStateChanges(this.#candidate);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The current revision has been accepted by Authority. Keep using the same
|
|
135
|
+
* snapshot and read authorizations, but make subsequent writes relative to
|
|
136
|
+
* this new committed baseline instead of replaying the old diff.
|
|
137
|
+
*/
|
|
138
|
+
acceptCommittedState() {
|
|
139
|
+
this.#candidate.changes.clear();
|
|
140
|
+
this.#candidate.suppliedBytes = 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function bootstrapAuthorizations(bootstrap, snapshot) {
|
|
144
|
+
const result = {
|
|
145
|
+
snapshotId: snapshot.id,
|
|
146
|
+
documents: new Map(),
|
|
147
|
+
pendingReads: new Map(),
|
|
148
|
+
};
|
|
149
|
+
for (const { status, complete, readAuthorization } of bootstrap.coverage) {
|
|
150
|
+
if (status !== "resolved" || !complete || readAuthorization === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
if (readAuthorization.locator === null) {
|
|
153
|
+
const resolved = snapshot.query({
|
|
154
|
+
kind: "read_document",
|
|
155
|
+
document: { shortRef: readAuthorization.shortRef },
|
|
156
|
+
maxBytes: 4,
|
|
157
|
+
});
|
|
158
|
+
if (resolved.kind === "read_document" && resolved.ok)
|
|
159
|
+
authorizeRead(result, snapshot.id, resolved.document.shortRef, null);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const selected = snapshot.query({
|
|
163
|
+
kind: "select_node",
|
|
164
|
+
document: { shortRef: readAuthorization.shortRef },
|
|
165
|
+
locator: readAuthorization.locator,
|
|
166
|
+
});
|
|
167
|
+
if (selected.kind === "select_node" && selected.ok)
|
|
168
|
+
authorizeRead(result, snapshot.id, selected.document.shortRef, selected.node.locator);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
function authorizeRead(authorizations, snapshotId, ref, locator) {
|
|
174
|
+
if (authorizations.snapshotId !== snapshotId)
|
|
175
|
+
return;
|
|
176
|
+
if (locator === null)
|
|
177
|
+
authorizations.documents.set(ref, null);
|
|
178
|
+
else if (authorizations.documents.get(ref) !== null)
|
|
179
|
+
authorizations.documents.set(ref, [
|
|
180
|
+
...(authorizations.documents.get(ref) ?? []),
|
|
181
|
+
locator,
|
|
182
|
+
]);
|
|
183
|
+
}
|
|
184
|
+
function authorizeToolRead(authorizations, snapshot, result) {
|
|
185
|
+
const authorization = result.readAuthorization;
|
|
186
|
+
if (authorization?.snapshotId !== snapshot.id ||
|
|
187
|
+
authorizations.snapshotId !== snapshot.id)
|
|
188
|
+
return;
|
|
189
|
+
const { page } = authorization;
|
|
190
|
+
let progress = authorizations.pendingReads.get(authorization.readKey);
|
|
191
|
+
if (page.start === 0) {
|
|
192
|
+
progress = {
|
|
193
|
+
shortRef: authorization.shortRef,
|
|
194
|
+
locator: authorization.locator,
|
|
195
|
+
nextOffset: page.end,
|
|
196
|
+
totalBytes: page.total,
|
|
197
|
+
};
|
|
198
|
+
authorizations.pendingReads.set(authorization.readKey, progress);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
if (progress === undefined)
|
|
202
|
+
return;
|
|
203
|
+
if (progress.shortRef !== authorization.shortRef ||
|
|
204
|
+
!sameLocator(progress.locator, authorization.locator) ||
|
|
205
|
+
progress.nextOffset !== page.start ||
|
|
206
|
+
progress.totalBytes !== page.total)
|
|
207
|
+
return;
|
|
208
|
+
progress.nextOffset = page.end;
|
|
209
|
+
}
|
|
210
|
+
if (progress.nextOffset !== progress.totalBytes)
|
|
211
|
+
return;
|
|
212
|
+
authorizeRead(authorizations, snapshot.id, progress.shortRef, progress.locator);
|
|
213
|
+
authorizations.pendingReads.delete(authorization.readKey);
|
|
214
|
+
}
|
|
215
|
+
function sameLocator(left, right) {
|
|
216
|
+
if (left === null || right === null)
|
|
217
|
+
return left === right;
|
|
218
|
+
const leftPath = "yaml" in left ? left.yaml : left.markdown;
|
|
219
|
+
const rightPath = "yaml" in right ? right.yaml : right.markdown;
|
|
220
|
+
return ("yaml" in left === "yaml" in right &&
|
|
221
|
+
leftPath.length === rightPath.length &&
|
|
222
|
+
leftPath.every((segment, index) => segment === rightPath[index]));
|
|
223
|
+
}
|
|
224
|
+
function parseYamlHandleSegment(segment) {
|
|
225
|
+
return /^(?:0|[1-9][0-9]*)$/u.test(segment) ? Number(segment) : segment;
|
|
226
|
+
}
|
|
227
|
+
function openPlayCandidate(files) {
|
|
228
|
+
return {
|
|
229
|
+
...beginWorldStateRevision(files),
|
|
230
|
+
suppliedBytes: 0,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function documentDescriptorByRef(snapshot, ref) {
|
|
234
|
+
const result = snapshot.query({
|
|
235
|
+
kind: "read_document",
|
|
236
|
+
document: { shortRef: ref },
|
|
237
|
+
maxBytes: 4,
|
|
238
|
+
});
|
|
239
|
+
return result.kind === "read_document" && result.ok ? result.document : null;
|
|
240
|
+
}
|
|
241
|
+
function applyPlayCandidatePatch(candidate, reads, args) {
|
|
242
|
+
if (!record(args) ||
|
|
243
|
+
typeof args.target !== "string" ||
|
|
244
|
+
!Array.isArray(args.edits) ||
|
|
245
|
+
args.edits.length < 1 ||
|
|
246
|
+
args.edits.length > 32)
|
|
247
|
+
return toolFailure("world_patch requires a target and 1 to 32 edits.");
|
|
248
|
+
const ref = args.target.replace(/^@/u, "");
|
|
249
|
+
const target = documentDescriptorByRef(candidate.snapshot, ref);
|
|
250
|
+
if (target === null)
|
|
251
|
+
return toolFailure(`Target @${ref} does not exist.`);
|
|
252
|
+
const retainedAuthorization = reads.documents.get(ref);
|
|
253
|
+
if (reads.snapshotId !== candidate.snapshot.id ||
|
|
254
|
+
retainedAuthorization === undefined ||
|
|
255
|
+
!editsAreAuthorized(retainedAuthorization, args.edits))
|
|
256
|
+
return toolFailure(`Read @${ref} exactly, or receive the complete document in bootstrap, before writing it.`);
|
|
257
|
+
try {
|
|
258
|
+
const suppliedBytes = Buffer.byteLength(JSON.stringify(args.edits), "utf8");
|
|
259
|
+
if (suppliedBytes > 64 * 1024)
|
|
260
|
+
throw new PlayDocumentToolFailure("A single patch body exceeds 64 KiB");
|
|
261
|
+
if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
|
|
262
|
+
throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
|
|
263
|
+
assertToolReferenceHandles(args.edits);
|
|
264
|
+
const revised = candidate.snapshot.revise({
|
|
265
|
+
commands: [
|
|
266
|
+
{
|
|
267
|
+
kind: "patch",
|
|
268
|
+
document: { documentId: target.documentId },
|
|
269
|
+
edits: args.edits,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
});
|
|
273
|
+
if (!revised.ok || revised.snapshotStatus !== "usable")
|
|
274
|
+
throw new PlayDocumentToolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
|
|
275
|
+
const changed = revised.changes.length > 0;
|
|
276
|
+
const receipt = formatToolRevisionReceipt(target.shortRef, revised.changes);
|
|
277
|
+
const previousSnapshot = candidate.snapshot;
|
|
278
|
+
acceptWorldStateRevision(candidate, revised);
|
|
279
|
+
candidate.suppliedBytes += suppliedBytes;
|
|
280
|
+
carryWriteAuthorizations(previousSnapshot, candidate, reads, revised.changes);
|
|
281
|
+
if (retainedAuthorization === null)
|
|
282
|
+
authorizeRead(reads, candidate.snapshot.id, ref, null);
|
|
283
|
+
else
|
|
284
|
+
for (const locator of retainedAuthorization)
|
|
285
|
+
authorizeRead(reads, candidate.snapshot.id, ref, locator);
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
markdown: receipt,
|
|
289
|
+
candidateWrite: { shortRef: target.shortRef, changed },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (error instanceof PlayDocumentToolFailure)
|
|
294
|
+
return toolFailure(error.message, error.failureKind);
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function editsAreAuthorized(authorization, edits) {
|
|
299
|
+
if (authorization === null)
|
|
300
|
+
return true;
|
|
301
|
+
if (authorization === undefined)
|
|
302
|
+
return false;
|
|
303
|
+
return edits.every((edit) => {
|
|
304
|
+
if (!record(edit) || edit.op === "set_metadata" || !record(edit.locator))
|
|
305
|
+
return false;
|
|
306
|
+
const requested = Array.isArray(edit.locator.yaml)
|
|
307
|
+
? { codec: "yaml", path: edit.locator.yaml }
|
|
308
|
+
: Array.isArray(edit.locator.markdown)
|
|
309
|
+
? { codec: "markdown", path: edit.locator.markdown }
|
|
310
|
+
: null;
|
|
311
|
+
return (requested !== null &&
|
|
312
|
+
authorization.some((allowed) => {
|
|
313
|
+
const path = "yaml" in allowed ? allowed.yaml : allowed.markdown;
|
|
314
|
+
return (requested.codec in allowed &&
|
|
315
|
+
path.every((segment, index) => requested.path[index] === segment));
|
|
316
|
+
}));
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
function formatToolRevisionReceipt(shortRef, changes) {
|
|
320
|
+
if (changes.length > 1)
|
|
321
|
+
throw new Error("A world_patch revision must affect only its target document");
|
|
322
|
+
return changes.length === 1
|
|
323
|
+
? `@${shortRef} pending write`
|
|
324
|
+
: `@${shortRef} unchanged`;
|
|
325
|
+
}
|
|
326
|
+
function nextAvailableStatePath(candidate, parentDirectory, refHint, codec) {
|
|
327
|
+
const directory = parentDirectory === "" ? "" : `${parentDirectory}/`;
|
|
328
|
+
const extension = codec === "yaml" ? "yaml" : "md";
|
|
329
|
+
for (let suffix = 1;; suffix += 1) {
|
|
330
|
+
const suffixText = suffix === 1 ? "" : `-${suffix}`;
|
|
331
|
+
const fileName = `${refHint.slice(0, 32 - suffixText.length)}${suffixText}`;
|
|
332
|
+
const path = `state/${directory}${fileName}.${extension}`;
|
|
333
|
+
if (candidate.files[path] === undefined)
|
|
334
|
+
return path;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function assertToolReferenceHandles(value, seen = new WeakSet()) {
|
|
338
|
+
if (value === null || typeof value !== "object")
|
|
339
|
+
return;
|
|
340
|
+
if (seen.has(value))
|
|
341
|
+
throw new PlayDocumentToolFailure("Tool arguments must not contain circular references");
|
|
342
|
+
seen.add(value);
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
for (const item of value)
|
|
345
|
+
assertToolReferenceHandles(item, seen);
|
|
346
|
+
seen.delete(value);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!record(value))
|
|
350
|
+
throw new PlayDocumentToolFailure("Tool arguments may contain only plain objects");
|
|
351
|
+
if (Object.hasOwn(value, "$ref") &&
|
|
352
|
+
(Object.keys(value).length !== 1 ||
|
|
353
|
+
typeof value.$ref !== "string" ||
|
|
354
|
+
!value.$ref.startsWith("@")))
|
|
355
|
+
throw new PlayDocumentToolFailure("$ref must use an @short-ref returned by the Runtime, and a reference object cannot contain other fields");
|
|
356
|
+
for (const item of Object.values(value))
|
|
357
|
+
assertToolReferenceHandles(item, seen);
|
|
358
|
+
seen.delete(value);
|
|
359
|
+
}
|
|
360
|
+
function applyPlayCandidateCreate(candidate, authorizations, args) {
|
|
361
|
+
try {
|
|
362
|
+
const parentDirectory = record(args) && typeof args.parent === "string"
|
|
363
|
+
? parseStateDirectoryHandle(args.parent)
|
|
364
|
+
: null;
|
|
365
|
+
if (!record(args) ||
|
|
366
|
+
typeof args.parent !== "string" ||
|
|
367
|
+
parentDirectory === null ||
|
|
368
|
+
(args.codec !== "yaml" && args.codec !== "markdown") ||
|
|
369
|
+
typeof args.refHint !== "string" ||
|
|
370
|
+
!/^[a-z][a-z0-9-]{1,31}$/u.test(args.refHint) ||
|
|
371
|
+
typeof args.title !== "string" ||
|
|
372
|
+
typeof args.summary !== "string" ||
|
|
373
|
+
!Array.isArray(args.aliases) ||
|
|
374
|
+
!args.aliases.every((alias) => typeof alias === "string") ||
|
|
375
|
+
typeof args.body !== "string")
|
|
376
|
+
throw new PlayDocumentToolFailure("world_create arguments are invalid");
|
|
377
|
+
const suppliedBytes = Buffer.byteLength(args.body, "utf8");
|
|
378
|
+
if (suppliedBytes > 64 * 1024)
|
|
379
|
+
throw new PlayDocumentToolFailure("The world_create body exceeds 64 KiB");
|
|
380
|
+
if (candidate.suppliedBytes + suppliedBytes > 256 * 1024)
|
|
381
|
+
throw new PlayDocumentToolFailure("New body content for this model operation exceeds 256 KiB in total");
|
|
382
|
+
const path = nextAvailableStatePath(candidate, parentDirectory, args.refHint, args.codec);
|
|
383
|
+
const command = args.codec === "yaml"
|
|
384
|
+
? {
|
|
385
|
+
kind: "create",
|
|
386
|
+
temporaryName: "created",
|
|
387
|
+
logicalPath: path,
|
|
388
|
+
codec: "yaml",
|
|
389
|
+
refHint: args.refHint,
|
|
390
|
+
title: args.title,
|
|
391
|
+
summary: args.summary,
|
|
392
|
+
aliases: args.aliases,
|
|
393
|
+
body: args.body,
|
|
394
|
+
}
|
|
395
|
+
: {
|
|
396
|
+
kind: "create",
|
|
397
|
+
temporaryName: "created",
|
|
398
|
+
logicalPath: path,
|
|
399
|
+
codec: "markdown",
|
|
400
|
+
refHint: args.refHint,
|
|
401
|
+
title: args.title,
|
|
402
|
+
summary: args.summary,
|
|
403
|
+
aliases: args.aliases,
|
|
404
|
+
body: args.body,
|
|
405
|
+
};
|
|
406
|
+
if (command.codec === "yaml")
|
|
407
|
+
assertToolReferenceHandles(command.body);
|
|
408
|
+
const revised = candidate.snapshot.revise({
|
|
409
|
+
commands: [command],
|
|
410
|
+
});
|
|
411
|
+
if (!revised.ok || revised.snapshotStatus !== "usable")
|
|
412
|
+
throw new PlayDocumentToolFailure(worldDocumentRevisionFailureMessage(revised.diagnostics), "candidate");
|
|
413
|
+
const created = revised.changes.find(({ before }) => before === null);
|
|
414
|
+
if (created === undefined)
|
|
415
|
+
throw new Error("The world_create revision did not return a newly created document change");
|
|
416
|
+
const previousSnapshot = candidate.snapshot;
|
|
417
|
+
acceptWorldStateRevision(candidate, revised);
|
|
418
|
+
carryWriteAuthorizations(previousSnapshot, candidate, authorizations, revised.changes);
|
|
419
|
+
authorizeRead(authorizations, candidate.snapshot.id, created.shortRef, null);
|
|
420
|
+
candidate.suppliedBytes += suppliedBytes;
|
|
421
|
+
return {
|
|
422
|
+
ok: true,
|
|
423
|
+
markdown: `@${created.shortRef} pending write`,
|
|
424
|
+
candidateWrite: { shortRef: created.shortRef, changed: true },
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error instanceof PlayDocumentToolFailure)
|
|
429
|
+
return toolFailure(error.message, error.failureKind);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Move read authorizations onto the revised snapshot. Only the documents this
|
|
435
|
+
* revision actually rewrote go stale — everything else is byte-identical in the
|
|
436
|
+
* new snapshot, so a model that read several documents and then patches one of
|
|
437
|
+
* them keeps the right to write the others in the same model response.
|
|
438
|
+
*/
|
|
439
|
+
function carryWriteAuthorizations(previousSnapshot, candidate, authorizations, changes) {
|
|
440
|
+
const stale = new Set();
|
|
441
|
+
for (const change of changes) {
|
|
442
|
+
stale.add(change.shortRef);
|
|
443
|
+
const previous = documentById(previousSnapshot, change.documentId);
|
|
444
|
+
if (previous !== null)
|
|
445
|
+
stale.add(previous.shortRef);
|
|
446
|
+
}
|
|
447
|
+
authorizations.snapshotId = candidate.snapshot.id;
|
|
448
|
+
for (const ref of stale)
|
|
449
|
+
authorizations.documents.delete(ref);
|
|
450
|
+
for (const [readKey, progress] of authorizations.pendingReads)
|
|
451
|
+
if (stale.has(progress.shortRef))
|
|
452
|
+
authorizations.pendingReads.delete(readKey);
|
|
453
|
+
}
|
|
454
|
+
function playCandidateStateChanges(candidate) {
|
|
455
|
+
return fileNativeStateChanges(candidate);
|
|
456
|
+
}
|
|
457
|
+
function documentById(snapshot, id) {
|
|
458
|
+
const result = snapshot.query({
|
|
459
|
+
kind: "read_document",
|
|
460
|
+
document: { documentId: id },
|
|
461
|
+
maxBytes: 4,
|
|
462
|
+
});
|
|
463
|
+
return result.kind === "read_document" && result.ok ? result.document : null;
|
|
464
|
+
}
|
|
465
|
+
function toolFailure(message, failureKind = "protocol") {
|
|
466
|
+
return {
|
|
467
|
+
ok: false,
|
|
468
|
+
markdown: `# Runtime tool rejected\n\n${message}`,
|
|
469
|
+
failureKind,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function previewContextToolResult(snapshot, history, call) {
|
|
473
|
+
if (call.name === "context_list")
|
|
474
|
+
return executeContextList(snapshot, history, call.arguments);
|
|
475
|
+
if (call.name === "context_search")
|
|
476
|
+
return executeContextSearch(snapshot, history, call.arguments);
|
|
477
|
+
if (call.name === "context_read")
|
|
478
|
+
return executeContextRead(snapshot, history, call.arguments);
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
function contextToolRetryMaximum(call) {
|
|
482
|
+
if (!record(call.arguments))
|
|
483
|
+
return call.name === "context_read" ? 8_192 : 1;
|
|
484
|
+
if (call.name === "context_read")
|
|
485
|
+
return Number.isInteger(call.arguments.maxBytes) &&
|
|
486
|
+
Number(call.arguments.maxBytes) >= 4 &&
|
|
487
|
+
Number(call.arguments.maxBytes) <= 8_192
|
|
488
|
+
? Number(call.arguments.maxBytes)
|
|
489
|
+
: 8_192;
|
|
490
|
+
const defaultLimit = call.name === "context_search" ? 10 : 20;
|
|
491
|
+
const maximumLimit = call.name === "context_search" ? 50 : 100;
|
|
492
|
+
return Number.isInteger(call.arguments.limit) &&
|
|
493
|
+
Number(call.arguments.limit) >= 1 &&
|
|
494
|
+
Number(call.arguments.limit) <= maximumLimit
|
|
495
|
+
? Number(call.arguments.limit)
|
|
496
|
+
: defaultLimit;
|
|
497
|
+
}
|
|
498
|
+
function executeContextSearch(snapshot, history, args) {
|
|
499
|
+
if (!record(args) ||
|
|
500
|
+
!hasOnlyToolKeys(args, [
|
|
501
|
+
"source",
|
|
502
|
+
"query",
|
|
503
|
+
"caseSensitive",
|
|
504
|
+
"within",
|
|
505
|
+
"limit",
|
|
506
|
+
"cursor",
|
|
507
|
+
]) ||
|
|
508
|
+
(args.source !== "state" && args.source !== "history") ||
|
|
509
|
+
typeof args.query !== "string" ||
|
|
510
|
+
args.query.length < 1 ||
|
|
511
|
+
args.query.length > 256 ||
|
|
512
|
+
(args.caseSensitive !== undefined &&
|
|
513
|
+
typeof args.caseSensitive !== "boolean") ||
|
|
514
|
+
(args.within !== undefined && typeof args.within !== "string") ||
|
|
515
|
+
(args.limit !== undefined && typeof args.limit !== "number") ||
|
|
516
|
+
!validOptionalCursor(args.cursor))
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
markdown: "# Runtime argument error\n\ncontext_search accepts only a literal query and strict source, within, caseSensitive, limit, and cursor arguments; semantic filters are not accepted.",
|
|
520
|
+
};
|
|
521
|
+
const caseSensitive = args.caseSensitive === true;
|
|
522
|
+
const query = args.query;
|
|
523
|
+
const limit = typeof args.limit === "number" ? args.limit : 10;
|
|
524
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50)
|
|
525
|
+
return {
|
|
526
|
+
ok: false,
|
|
527
|
+
markdown: "# Runtime argument error\n\nlimit must be between 1 and 50.",
|
|
528
|
+
};
|
|
529
|
+
if (args.source === "state")
|
|
530
|
+
return searchState(snapshot, {
|
|
531
|
+
query,
|
|
532
|
+
caseSensitive,
|
|
533
|
+
...(args.within === undefined ? {} : { within: args.within }),
|
|
534
|
+
limit,
|
|
535
|
+
...(typeof args.cursor === "string" || args.cursor === null
|
|
536
|
+
? { cursor: args.cursor }
|
|
537
|
+
: {}),
|
|
538
|
+
});
|
|
539
|
+
const source = history.map(({ path, contents }) => [
|
|
540
|
+
historyRef(path),
|
|
541
|
+
contents,
|
|
542
|
+
[`history-commit-${path.split("-")[0] ?? ""}`],
|
|
543
|
+
]);
|
|
544
|
+
const normalizedQuery = normalizeSearch(query, caseSensitive);
|
|
545
|
+
const within = typeof args.within === "string" ? args.within.replace(/^@/u, "") : null;
|
|
546
|
+
const allHits = source.filter(([ref, text, parentScopes]) => (within === null || ref === within || parentScopes.includes(within)) &&
|
|
547
|
+
normalizeSearch(text, caseSensitive).includes(normalizedQuery));
|
|
548
|
+
const scope = JSON.stringify({
|
|
549
|
+
kind: "search",
|
|
550
|
+
source: "history",
|
|
551
|
+
query,
|
|
552
|
+
caseSensitive,
|
|
553
|
+
within: args.within ?? null,
|
|
554
|
+
limit,
|
|
555
|
+
});
|
|
556
|
+
const offset = parseCursor(args.cursor, scope);
|
|
557
|
+
if (offset === null)
|
|
558
|
+
return {
|
|
559
|
+
ok: false,
|
|
560
|
+
markdown: "# Runtime argument error\n\nThe cursor does not match the search criteria or endpoint.",
|
|
561
|
+
};
|
|
562
|
+
const hits = allHits.slice(offset, offset + limit);
|
|
563
|
+
const complete = offset + hits.length >= allHits.length;
|
|
564
|
+
const renderedHits = hits
|
|
565
|
+
.map(([ref, text]) => `- @${ref}\n Exact-match excerpt:\n${quoteMarkdown(snippet(text, query), " ")}`)
|
|
566
|
+
.join("\n");
|
|
567
|
+
return {
|
|
568
|
+
ok: true,
|
|
569
|
+
markdown: `# Literal search\n\nScope: history${args.within === undefined ? "" : ` · ${args.within}`}\nNormalization: ${caseSensitive ? "original text" : "NFKC + case folding"}\nTotal matches: ${allHits.length}\n${renderedHits || "Zero literal matches do not prove that the fact is absent from the world."}\n\n---\nThis page: ${offset}..${offset + hits.length} / ${allHits.length} matches\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${cursorFor(scope, offset + hits.length)}`}`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function searchState(snapshot, input) {
|
|
573
|
+
const within = parseStateSearchScope(input.within);
|
|
574
|
+
if (within === invalidStateHandle)
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
markdown: "# Runtime argument error\n\nA state search scope must be an @dir-* or document @short-ref returned by the Runtime.",
|
|
578
|
+
};
|
|
579
|
+
const result = snapshot.query({
|
|
580
|
+
kind: "literal_search",
|
|
581
|
+
query: input.query,
|
|
582
|
+
caseSensitive: input.caseSensitive,
|
|
583
|
+
...(within === undefined ? {} : { within }),
|
|
584
|
+
limit: input.limit,
|
|
585
|
+
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
586
|
+
});
|
|
587
|
+
if (result.kind === "error")
|
|
588
|
+
return renderStateQueryFailure(result);
|
|
589
|
+
if (result.kind !== "literal_search")
|
|
590
|
+
return unexpectedStateQueryResult();
|
|
591
|
+
const renderedHits = result.matches
|
|
592
|
+
.map(({ document, referenceProjection, range }) => `- @${document.shortRef} · ${document.title} · line ${range.start.line}, column ${range.start.column}\n Exact match: ${JSON.stringify(referenceProjection.text)}\n Exact-match excerpt (mechanical references appear as @short-refs):\n${quoteMarkdown(referenceProjection.excerpt, " ")}`)
|
|
593
|
+
.join("\n");
|
|
594
|
+
const scope = input.within === undefined ? "state" : `state · ${input.within}`;
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
markdown: `# Literal search\n\nScope: ${scope}\nNormalization: ${input.caseSensitive ? "original text" : "NFKC + case folding"}\nCoverage: ${result.coverage.status === "complete" ? "complete" : `partial (${result.coverage.excludedDocuments} damaged documents excluded)`}\nTotal matches: ${result.page.total}\n${renderedHits || "Zero literal matches do not prove that the fact is absent from the world."}\n\n---\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} matches\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
const invalidStateHandle = Symbol("invalid-state-handle");
|
|
601
|
+
function parseStateSearchScope(handle) {
|
|
602
|
+
if (handle === undefined)
|
|
603
|
+
return undefined;
|
|
604
|
+
const directory = parseStateDirectoryHandle(handle);
|
|
605
|
+
if (directory !== null)
|
|
606
|
+
return { directory };
|
|
607
|
+
const document = /^@([a-z][a-z0-9-]*)$/u.exec(handle);
|
|
608
|
+
return document === null
|
|
609
|
+
? invalidStateHandle
|
|
610
|
+
: { document: { shortRef: document[1] } };
|
|
611
|
+
}
|
|
612
|
+
function stateDirectoryHandle(directory) {
|
|
613
|
+
if (directory === "")
|
|
614
|
+
return "@dir-/";
|
|
615
|
+
const encoded = directory.split("/").map(encodeURIComponent).join("/");
|
|
616
|
+
return `@dir-/${encoded}`;
|
|
617
|
+
}
|
|
618
|
+
function parseStateDirectoryHandle(handle) {
|
|
619
|
+
if (handle === "@dir-/")
|
|
620
|
+
return "";
|
|
621
|
+
if (!handle.startsWith("@dir-/") || handle.length === "@dir-/".length)
|
|
622
|
+
return null;
|
|
623
|
+
try {
|
|
624
|
+
const directory = handle
|
|
625
|
+
.slice("@dir-/".length)
|
|
626
|
+
.split("/")
|
|
627
|
+
.map(decodeURIComponent)
|
|
628
|
+
.join("/");
|
|
629
|
+
if (directory
|
|
630
|
+
.split("/")
|
|
631
|
+
.some((segment) => segment === "" ||
|
|
632
|
+
segment === "." ||
|
|
633
|
+
segment === ".." ||
|
|
634
|
+
segment.includes("\\")) ||
|
|
635
|
+
stateDirectoryHandle(directory) !== handle)
|
|
636
|
+
return null;
|
|
637
|
+
return directory;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function executeContextList(snapshot, history, args) {
|
|
644
|
+
if (!record(args) || (args.source !== "state" && args.source !== "history"))
|
|
645
|
+
return {
|
|
646
|
+
ok: false,
|
|
647
|
+
markdown: '# Runtime argument error\n\nUse {source:"state", parent:"@dir-/"} for state directories and {source:"history", order:"newest_first"} for history.',
|
|
648
|
+
};
|
|
649
|
+
const allowedKeys = ["source", "parent", "order", "cursor", "limit"];
|
|
650
|
+
const invalidStateShape = args.source === "state" &&
|
|
651
|
+
(typeof args.parent !== "string" ||
|
|
652
|
+
parseStateDirectoryHandle(args.parent) === null ||
|
|
653
|
+
args.order !== undefined);
|
|
654
|
+
const invalidHistoryShape = args.source === "history" &&
|
|
655
|
+
(args.parent !== undefined ||
|
|
656
|
+
(args.order !== "newest_first" && args.order !== "oldest_first"));
|
|
657
|
+
if (!hasOnlyToolKeys(args, allowedKeys) ||
|
|
658
|
+
invalidStateShape ||
|
|
659
|
+
invalidHistoryShape ||
|
|
660
|
+
(args.limit !== undefined && typeof args.limit !== "number") ||
|
|
661
|
+
!validOptionalCursor(args.cursor))
|
|
662
|
+
return {
|
|
663
|
+
ok: false,
|
|
664
|
+
markdown: "# Runtime argument error\n\nstate must use an @dir-* parent returned by the Runtime and omit order; history must use newest_first or oldest_first order and omit parent.",
|
|
665
|
+
};
|
|
666
|
+
const limit = typeof args.limit === "number" ? args.limit : 20;
|
|
667
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
668
|
+
return {
|
|
669
|
+
ok: false,
|
|
670
|
+
markdown: "# Runtime argument error\n\nlimit must be between 1 and 100.",
|
|
671
|
+
};
|
|
672
|
+
if (args.source === "state") {
|
|
673
|
+
const parent = args.parent;
|
|
674
|
+
const directory = parseStateDirectoryHandle(parent);
|
|
675
|
+
if (directory === null)
|
|
676
|
+
return unexpectedStateQueryResult();
|
|
677
|
+
const result = snapshot.query({
|
|
678
|
+
kind: "catalog",
|
|
679
|
+
directory,
|
|
680
|
+
limit,
|
|
681
|
+
...(typeof args.cursor === "string" || args.cursor === null
|
|
682
|
+
? { cursor: args.cursor }
|
|
683
|
+
: {}),
|
|
684
|
+
});
|
|
685
|
+
if (result.kind === "error")
|
|
686
|
+
return renderStateQueryFailure(result);
|
|
687
|
+
if (result.kind !== "catalog")
|
|
688
|
+
return unexpectedStateQueryResult();
|
|
689
|
+
const entries = result.entries.map((entry) => {
|
|
690
|
+
if (entry.kind === "directory") {
|
|
691
|
+
const relative = entry.logicalPath.slice(`${snapshot.logicalRoot}/`.length);
|
|
692
|
+
return `- Directory ${stateDirectoryHandle(relative)}`;
|
|
693
|
+
}
|
|
694
|
+
if (entry.document === undefined)
|
|
695
|
+
return "- Damaged document (currently not addressable)";
|
|
696
|
+
return `- Document @${entry.document.shortRef} · ${entry.document.title} · ${entry.document.codec.toUpperCase()} · ${entry.document.summary}${entry.status === "damaged" ? " · needs repair" : ""}`;
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
ok: true,
|
|
700
|
+
markdown: `# Directory listing\n\nScope: state · ${parent}\nCoverage: ${result.coverage.status === "complete" ? "complete" : "partial"}\n${entries.join("\n") || "(empty)"}\n\n---\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} items\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const entries = [...history]
|
|
704
|
+
.sort((a, b) => (args.order ?? "newest_first") === "oldest_first"
|
|
705
|
+
? a.path.localeCompare(b.path)
|
|
706
|
+
: b.path.localeCompare(a.path))
|
|
707
|
+
.map(({ path, contents }) => `- @${historyRef(path)}, ${Buffer.byteLength(contents, "utf8")} bytes`);
|
|
708
|
+
const scope = JSON.stringify({
|
|
709
|
+
kind: "list",
|
|
710
|
+
source: "history",
|
|
711
|
+
order: args.order,
|
|
712
|
+
limit,
|
|
713
|
+
});
|
|
714
|
+
const offset = parseCursor(args.cursor, scope);
|
|
715
|
+
if (offset === null)
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
markdown: "# Runtime argument error\n\nThe cursor does not match the listing criteria or endpoint.",
|
|
719
|
+
};
|
|
720
|
+
const page = entries.slice(offset, offset + limit);
|
|
721
|
+
const complete = offset + page.length >= entries.length;
|
|
722
|
+
return {
|
|
723
|
+
ok: true,
|
|
724
|
+
markdown: `# Directory listing\n\nScope: history · ${String(args.order)}\n${page.join("\n") || "(empty)"}\n\n---\nThis page: ${offset}..${offset + page.length} / ${entries.length} items\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${cursorFor(scope, offset + page.length)}`}`,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function executeContextRead(snapshot, history, args) {
|
|
728
|
+
if (!record(args) ||
|
|
729
|
+
!hasOnlyToolKeys(args, ["ref", "cursor", "maxBytes"]) ||
|
|
730
|
+
typeof args.ref !== "string" ||
|
|
731
|
+
(args.maxBytes !== undefined && typeof args.maxBytes !== "number") ||
|
|
732
|
+
!validOptionalCursor(args.cursor))
|
|
733
|
+
return {
|
|
734
|
+
ok: false,
|
|
735
|
+
markdown: "# Runtime argument error\n\nA stable ref returned by list or search is required.",
|
|
736
|
+
};
|
|
737
|
+
const maxBytes = typeof args.maxBytes === "number" ? args.maxBytes : 8192;
|
|
738
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > 8192)
|
|
739
|
+
return {
|
|
740
|
+
ok: false,
|
|
741
|
+
markdown: "# Runtime argument error\n\nmaxBytes must be between 4 and 8192.",
|
|
742
|
+
};
|
|
743
|
+
const historyRefValue = args.ref.replace(/^@/u, "");
|
|
744
|
+
const historyEntry = history.find(({ path }) => historyRef(path) === historyRefValue);
|
|
745
|
+
if (historyEntry !== undefined)
|
|
746
|
+
return pagedRead(historyRefValue, historyEntry.contents, maxBytes, args.cursor);
|
|
747
|
+
const handle = parseStateReadHandle(args.ref);
|
|
748
|
+
if (handle === null)
|
|
749
|
+
return {
|
|
750
|
+
ok: false,
|
|
751
|
+
markdown: "# Runtime argument error\n\nAn exact state read requires a document @short-ref or one of its #/logical nodes.",
|
|
752
|
+
};
|
|
753
|
+
const resolved = snapshot.query({
|
|
754
|
+
kind: "read_document",
|
|
755
|
+
document: { shortRef: handle.shortRef },
|
|
756
|
+
maxBytes: 4,
|
|
757
|
+
});
|
|
758
|
+
if (resolved.kind === "error")
|
|
759
|
+
return renderStateQueryFailure(resolved);
|
|
760
|
+
if (resolved.kind !== "read_document")
|
|
761
|
+
return unexpectedStateQueryResult();
|
|
762
|
+
if (handle.path === null)
|
|
763
|
+
return resolved.codec === "markdown"
|
|
764
|
+
? readMarkdownDocument(snapshot, resolved.document, maxBytes, args.cursor)
|
|
765
|
+
: readProjectedNode(snapshot, resolved.document, { yaml: [] }, args.ref, maxBytes, args.cursor, true);
|
|
766
|
+
const locator = resolved.codec === "yaml"
|
|
767
|
+
? { yaml: handle.path.map(parseYamlHandleSegment) }
|
|
768
|
+
: { markdown: handle.path };
|
|
769
|
+
return readProjectedNode(snapshot, resolved.document, locator, args.ref, maxBytes, args.cursor, false);
|
|
770
|
+
}
|
|
771
|
+
function readMarkdownDocument(snapshot, document, maxBytes, cursor) {
|
|
772
|
+
const result = snapshot.query({
|
|
773
|
+
kind: "read_document",
|
|
774
|
+
document: { shortRef: document.shortRef },
|
|
775
|
+
maxBytes,
|
|
776
|
+
...(typeof cursor === "string" || cursor === null ? { cursor } : {}),
|
|
777
|
+
});
|
|
778
|
+
if (result.kind === "error")
|
|
779
|
+
return renderStateQueryFailure(result);
|
|
780
|
+
if (result.kind !== "read_document")
|
|
781
|
+
return unexpectedStateQueryResult();
|
|
782
|
+
return {
|
|
783
|
+
ok: true,
|
|
784
|
+
markdown: `# Exact read @${document.shortRef}\n\n${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${result.body.trimEnd()}\n[Writable body ${result.page.complete ? "ends" : "continues"}]\n\n---\nScope: state · @${document.shortRef}\nThis page: ${result.page.start}..${result.page.end} / ${result.page.total} bytes\nComplete: ${result.page.complete ? "yes" : "no"}${result.page.nextCursor === null ? "" : `\nNext-page cursor: ${result.page.nextCursor}`}`,
|
|
785
|
+
readAuthorization: {
|
|
786
|
+
snapshotId: snapshot.id,
|
|
787
|
+
readKey: readAuthorizationKey(document.shortRef, null, maxBytes),
|
|
788
|
+
shortRef: document.shortRef,
|
|
789
|
+
locator: null,
|
|
790
|
+
page: {
|
|
791
|
+
start: result.page.start,
|
|
792
|
+
end: result.page.end,
|
|
793
|
+
total: result.page.total,
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function readProjectedNode(snapshot, document, locator, requestedHandle, maxBytes, cursor, wholeDocument) {
|
|
799
|
+
const result = snapshot.query({
|
|
800
|
+
kind: "select_node",
|
|
801
|
+
document: { shortRef: document.shortRef },
|
|
802
|
+
locator,
|
|
803
|
+
});
|
|
804
|
+
if (result.kind === "error")
|
|
805
|
+
return renderStateQueryFailure(result);
|
|
806
|
+
if (result.kind !== "select_node")
|
|
807
|
+
return unexpectedStateQueryResult();
|
|
808
|
+
const body = result.node.codec === "yaml"
|
|
809
|
+
? stringify(toolWorldDocumentValue(result.node.value), {
|
|
810
|
+
indent: 2,
|
|
811
|
+
lineWidth: 0,
|
|
812
|
+
}).trimEnd()
|
|
813
|
+
: result.node.markdown.trimEnd();
|
|
814
|
+
const text = wholeDocument
|
|
815
|
+
? `${renderDocumentMetadata(document)}\n[Writable body starts; locators are relative to this point]\n${body}\n[Writable body ends]\n`
|
|
816
|
+
: `# ${document.title} · ${renderLocator(result.node.locator)} [${requestedHandle}]\n\n${body}\n`;
|
|
817
|
+
return pagedStateProjection(snapshot, requestedHandle, text, maxBytes, cursor, document.shortRef, wholeDocument ? null : result.node.locator);
|
|
818
|
+
}
|
|
819
|
+
function renderDocumentMetadata(document) {
|
|
820
|
+
const metadata = stringify({
|
|
821
|
+
title: document.title,
|
|
822
|
+
summary: document.summary,
|
|
823
|
+
aliases: document.aliases,
|
|
824
|
+
}, { indent: 2, lineWidth: 0 }).trimEnd();
|
|
825
|
+
return `[Document metadata: not body content; use set_metadata to replace the entire group]\n${metadata}`;
|
|
826
|
+
}
|
|
827
|
+
function toolWorldDocumentValue(value) {
|
|
828
|
+
if (Array.isArray(value))
|
|
829
|
+
return value.map(toolWorldDocumentValue);
|
|
830
|
+
if (!record(value))
|
|
831
|
+
return value;
|
|
832
|
+
if (typeof value.$ref === "string" &&
|
|
833
|
+
record(value.target) &&
|
|
834
|
+
typeof value.target.shortRef === "string")
|
|
835
|
+
return { $ref: `@${value.target.shortRef}` };
|
|
836
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
837
|
+
key,
|
|
838
|
+
toolWorldDocumentValue(child),
|
|
839
|
+
]));
|
|
840
|
+
}
|
|
841
|
+
function pagedStateProjection(snapshot, handle, text, maxBytes, cursor, shortRef, locator) {
|
|
842
|
+
const scope = JSON.stringify({
|
|
843
|
+
kind: "state_projected_read",
|
|
844
|
+
snapshotId: snapshot.id,
|
|
845
|
+
handle,
|
|
846
|
+
maxBytes,
|
|
847
|
+
});
|
|
848
|
+
const offset = parseStateProjectionCursor(snapshot, cursor, scope);
|
|
849
|
+
const bytes = Buffer.from(text, "utf8");
|
|
850
|
+
if (offset === null || offset > bytes.length)
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
markdown: "# Runtime argument error\n\nThe cursor does not match the current snapshot or complete query criteria.",
|
|
854
|
+
};
|
|
855
|
+
const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
|
|
856
|
+
const complete = end === bytes.length;
|
|
857
|
+
const page = bytes.subarray(offset, end).toString("utf8");
|
|
858
|
+
return {
|
|
859
|
+
ok: true,
|
|
860
|
+
markdown: `# Exact read ${handle}\n\n${page}\n\n---\nScope: state · ${handle}\nThis page: ${offset}..${end} / ${bytes.length} bytes\nComplete: ${complete ? "yes" : "no"}${complete ? "" : `\nNext-page cursor: ${stateProjectionCursorFor(snapshot, scope, end)}`}`,
|
|
861
|
+
readAuthorization: {
|
|
862
|
+
snapshotId: snapshot.id,
|
|
863
|
+
readKey: readAuthorizationKey(shortRef, locator, maxBytes),
|
|
864
|
+
shortRef,
|
|
865
|
+
locator,
|
|
866
|
+
page: { start: offset, end, total: bytes.length },
|
|
867
|
+
},
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function readAuthorizationKey(shortRef, locator, maxBytes) {
|
|
871
|
+
return JSON.stringify({ shortRef, locator, maxBytes });
|
|
872
|
+
}
|
|
873
|
+
const stateProjectionCursorSecrets = new WeakMap();
|
|
874
|
+
function stateProjectionCursorFor(snapshot, scope, offset) {
|
|
875
|
+
const payload = Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
876
|
+
const signature = createHmac("sha256", stateProjectionCursorSecret(snapshot))
|
|
877
|
+
.update(snapshot.id)
|
|
878
|
+
.update("\0")
|
|
879
|
+
.update(scope)
|
|
880
|
+
.update("\0")
|
|
881
|
+
.update(payload)
|
|
882
|
+
.digest("base64url");
|
|
883
|
+
return `${payload}.${signature}`;
|
|
884
|
+
}
|
|
885
|
+
function parseStateProjectionCursor(snapshot, cursor, scope) {
|
|
886
|
+
if (cursor === undefined || cursor === null)
|
|
887
|
+
return 0;
|
|
888
|
+
if (typeof cursor !== "string")
|
|
889
|
+
return null;
|
|
890
|
+
const parts = cursor.split(".");
|
|
891
|
+
if (parts.length !== 2)
|
|
892
|
+
return null;
|
|
893
|
+
const [payload, suppliedSignature] = parts;
|
|
894
|
+
if (payload === undefined || suppliedSignature === undefined)
|
|
895
|
+
return null;
|
|
896
|
+
try {
|
|
897
|
+
const supplied = Buffer.from(suppliedSignature, "base64url");
|
|
898
|
+
if (supplied.toString("base64url") !== suppliedSignature)
|
|
899
|
+
return null;
|
|
900
|
+
const expected = createHmac("sha256", stateProjectionCursorSecret(snapshot))
|
|
901
|
+
.update(snapshot.id)
|
|
902
|
+
.update("\0")
|
|
903
|
+
.update(scope)
|
|
904
|
+
.update("\0")
|
|
905
|
+
.update(payload)
|
|
906
|
+
.digest();
|
|
907
|
+
if (supplied.length !== expected.length ||
|
|
908
|
+
!timingSafeEqual(supplied, expected))
|
|
909
|
+
return null;
|
|
910
|
+
const decoded = Buffer.from(payload, "base64url");
|
|
911
|
+
if (decoded.toString("base64url") !== payload)
|
|
912
|
+
return null;
|
|
913
|
+
const value = JSON.parse(decoded.toString("utf8"));
|
|
914
|
+
return record(value) &&
|
|
915
|
+
Object.keys(value).length === 1 &&
|
|
916
|
+
Number.isSafeInteger(value.offset) &&
|
|
917
|
+
Number(value.offset) >= 0
|
|
918
|
+
? Number(value.offset)
|
|
919
|
+
: null;
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function stateProjectionCursorSecret(snapshot) {
|
|
926
|
+
const existing = stateProjectionCursorSecrets.get(snapshot);
|
|
927
|
+
if (existing !== undefined)
|
|
928
|
+
return existing;
|
|
929
|
+
const created = randomBytes(32);
|
|
930
|
+
stateProjectionCursorSecrets.set(snapshot, created);
|
|
931
|
+
return created;
|
|
932
|
+
}
|
|
933
|
+
function parseStateReadHandle(value) {
|
|
934
|
+
const match = /^@([a-z][a-z0-9-]*)(?:#\/(.+))?$/u.exec(value);
|
|
935
|
+
if (match === null)
|
|
936
|
+
return null;
|
|
937
|
+
try {
|
|
938
|
+
return {
|
|
939
|
+
shortRef: match[1],
|
|
940
|
+
path: match[2] === undefined
|
|
941
|
+
? null
|
|
942
|
+
: match[2].split("/").map(decodeURIComponent),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function renderLocator(locator) {
|
|
950
|
+
return "yaml" in locator
|
|
951
|
+
? `yaml:${locator.yaml.join("/")}`
|
|
952
|
+
: `markdown:${locator.markdown.join("/")}`;
|
|
953
|
+
}
|
|
954
|
+
function renderStateQueryFailure(result) {
|
|
955
|
+
return {
|
|
956
|
+
ok: false,
|
|
957
|
+
markdown: `# Runtime argument error\n\n${result.diagnostics.map(({ code, message }) => `${code}: ${message}`).join("\n")}`,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function unexpectedStateQueryResult() {
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
markdown: "# Runtime tool rejected\n\nThe world-document query returned an incompatible result type.",
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function quoteMarkdown(value, indent = "") {
|
|
967
|
+
return value
|
|
968
|
+
.split("\n")
|
|
969
|
+
.map((line) => `${indent}> ${line}`)
|
|
970
|
+
.join("\n");
|
|
971
|
+
}
|
|
972
|
+
function hasOnlyToolKeys(value, allowed) {
|
|
973
|
+
const keys = new Set(allowed);
|
|
974
|
+
return Object.keys(value).every((key) => keys.has(key));
|
|
975
|
+
}
|
|
976
|
+
function validOptionalCursor(value) {
|
|
977
|
+
return value === undefined || value === null || typeof value === "string";
|
|
978
|
+
}
|
|
979
|
+
function normalizeSearch(value, caseSensitive) {
|
|
980
|
+
const normalized = value.normalize("NFKC");
|
|
981
|
+
return caseSensitive ? normalized : normalized.toLocaleLowerCase("und");
|
|
982
|
+
}
|
|
983
|
+
function snippet(text, query) {
|
|
984
|
+
const position = text
|
|
985
|
+
.normalize("NFKC")
|
|
986
|
+
.toLocaleLowerCase("und")
|
|
987
|
+
.indexOf(query.normalize("NFKC").toLocaleLowerCase("und"));
|
|
988
|
+
const start = Math.max(0, position - 120);
|
|
989
|
+
return text.slice(start, start + 240).replace(/\s+/gu, " ");
|
|
990
|
+
}
|
|
991
|
+
function historyRef(path) {
|
|
992
|
+
return `history-message-${path.replace(/\.md$/u, "")}`;
|
|
993
|
+
}
|
|
994
|
+
function pagedRead(ref, text, maxBytes, cursor) {
|
|
995
|
+
const scope = JSON.stringify({
|
|
996
|
+
kind: "read",
|
|
997
|
+
ref,
|
|
998
|
+
sha256: createHash("sha256").update(text).digest("hex"),
|
|
999
|
+
maxBytes,
|
|
1000
|
+
});
|
|
1001
|
+
const offset = parseCursor(cursor, scope);
|
|
1002
|
+
if (offset === null)
|
|
1003
|
+
return {
|
|
1004
|
+
ok: false,
|
|
1005
|
+
markdown: "# Runtime argument error\n\nThe cursor does not match the current read.",
|
|
1006
|
+
};
|
|
1007
|
+
const bytes = Buffer.from(text, "utf8");
|
|
1008
|
+
const end = safeUtf8ReadEnd(bytes, offset, maxBytes);
|
|
1009
|
+
const page = bytes.subarray(offset, end).toString("utf8");
|
|
1010
|
+
return {
|
|
1011
|
+
ok: true,
|
|
1012
|
+
markdown: `# Exact read @${ref}\n\n${page}\n\n---\nSource: @${ref}\nThis page: ${offset}..${end} bytes\nComplete: ${end === bytes.length ? "yes" : "no"}\n${end === bytes.length ? "" : `Next-page cursor: ${cursorFor(scope, end)}`}`,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function safeUtf8ReadEnd(bytes, offset, maxBytes) {
|
|
1016
|
+
let end = Math.min(bytes.length, offset + maxBytes);
|
|
1017
|
+
while (end > offset && (bytes[end] ?? 0) >= 0x80 && (bytes[end] ?? 0) < 0xc0)
|
|
1018
|
+
end -= 1;
|
|
1019
|
+
return end;
|
|
1020
|
+
}
|
|
1021
|
+
function cursorFor(scope, offset) {
|
|
1022
|
+
return Buffer.from(JSON.stringify({
|
|
1023
|
+
scope: createHash("sha256").update(scope).digest("hex"),
|
|
1024
|
+
offset,
|
|
1025
|
+
}), "utf8").toString("base64url");
|
|
1026
|
+
}
|
|
1027
|
+
function parseCursor(cursor, scope) {
|
|
1028
|
+
if (cursor === undefined || cursor === null)
|
|
1029
|
+
return 0;
|
|
1030
|
+
if (typeof cursor !== "string")
|
|
1031
|
+
return null;
|
|
1032
|
+
try {
|
|
1033
|
+
const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
1034
|
+
if (!record(value) ||
|
|
1035
|
+
value.scope !== createHash("sha256").update(scope).digest("hex") ||
|
|
1036
|
+
!Number.isSafeInteger(value.offset) ||
|
|
1037
|
+
Number(value.offset) < 0)
|
|
1038
|
+
return null;
|
|
1039
|
+
return Number(value.offset);
|
|
1040
|
+
}
|
|
1041
|
+
catch {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
export function parseNarrative(args) {
|
|
1046
|
+
if (!record(args) ||
|
|
1047
|
+
typeof args.text !== "string" ||
|
|
1048
|
+
args.text.length < 1 ||
|
|
1049
|
+
args.text.length > 24_000)
|
|
1050
|
+
throw new PromptCompilationError("narrative_invalid", "Narrative text must contain 1 to 24,000 Unicode characters");
|
|
1051
|
+
return args.text;
|
|
1052
|
+
}
|
|
1053
|
+
export function fingerprintControl(files) {
|
|
1054
|
+
const hash = createHash("sha256");
|
|
1055
|
+
for (const [path, contents] of Object.entries(files)
|
|
1056
|
+
.filter(([path]) => path.startsWith("control/"))
|
|
1057
|
+
.sort(([a], [b]) => a.localeCompare(b)))
|
|
1058
|
+
hash.update(path).update("\0").update(contents).update("\0");
|
|
1059
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1060
|
+
}
|
|
1061
|
+
export function isPlayDocumentAuthorizationCheckpoint(value) {
|
|
1062
|
+
if (!record(value) ||
|
|
1063
|
+
!hasExactKeys(value, [
|
|
1064
|
+
"schemaVersion",
|
|
1065
|
+
"kind",
|
|
1066
|
+
"stateFingerprint",
|
|
1067
|
+
"documents",
|
|
1068
|
+
]) ||
|
|
1069
|
+
value.schemaVersion !== 1 ||
|
|
1070
|
+
value.kind !== "play_document_authorizations" ||
|
|
1071
|
+
typeof value.stateFingerprint !== "string" ||
|
|
1072
|
+
!/^sha256:[0-9a-f]{64}$/u.test(value.stateFingerprint) ||
|
|
1073
|
+
!Array.isArray(value.documents) ||
|
|
1074
|
+
value.documents.length > 100_000)
|
|
1075
|
+
return false;
|
|
1076
|
+
const refs = new Set();
|
|
1077
|
+
for (const document of value.documents) {
|
|
1078
|
+
if (!record(document) ||
|
|
1079
|
+
!hasExactKeys(document, ["shortRef", "locators"]) ||
|
|
1080
|
+
typeof document.shortRef !== "string" ||
|
|
1081
|
+
!/^[a-z][a-z0-9-]*$/u.test(document.shortRef) ||
|
|
1082
|
+
refs.has(document.shortRef) ||
|
|
1083
|
+
(document.locators !== null &&
|
|
1084
|
+
(!Array.isArray(document.locators) ||
|
|
1085
|
+
document.locators.length > 100_000 ||
|
|
1086
|
+
!document.locators.every(isPersistedAuthorizedLocator))))
|
|
1087
|
+
return false;
|
|
1088
|
+
refs.add(document.shortRef);
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
}
|
|
1092
|
+
function fingerprintStateFiles(files) {
|
|
1093
|
+
const hash = createHash("sha256");
|
|
1094
|
+
for (const [path, contents] of Object.entries(files)
|
|
1095
|
+
.filter(([path]) => path.startsWith("state/"))
|
|
1096
|
+
.sort(([left], [right]) => left.localeCompare(right)))
|
|
1097
|
+
hash.update(path).update("\0").update(contents).update("\0");
|
|
1098
|
+
return `sha256:${hash.digest("hex")}`;
|
|
1099
|
+
}
|
|
1100
|
+
function isPersistedAuthorizedLocator(value) {
|
|
1101
|
+
if (!record(value))
|
|
1102
|
+
return false;
|
|
1103
|
+
if (hasExactKeys(value, ["yaml"]))
|
|
1104
|
+
return (Array.isArray(value.yaml) &&
|
|
1105
|
+
value.yaml.length <= 64 &&
|
|
1106
|
+
value.yaml.every((segment) => typeof segment === "string" ||
|
|
1107
|
+
(Number.isSafeInteger(segment) && Number(segment) >= 0)));
|
|
1108
|
+
if (hasExactKeys(value, ["markdown"]))
|
|
1109
|
+
return (Array.isArray(value.markdown) &&
|
|
1110
|
+
value.markdown.length <= 64 &&
|
|
1111
|
+
value.markdown.every((segment) => typeof segment === "string"));
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
function hasExactKeys(value, expected) {
|
|
1115
|
+
const keys = Object.keys(value);
|
|
1116
|
+
return (keys.length === expected.length &&
|
|
1117
|
+
expected.every((key) => Object.hasOwn(value, key)));
|
|
1118
|
+
}
|
|
1119
|
+
function record(value) {
|
|
1120
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1121
|
+
}
|