narraeon 0.1.1 → 0.3.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.
Files changed (29) hide show
  1. package/dist/node/protocol/modelConnections.js +49 -0
  2. package/dist/node/protocol/modelUsage.js +39 -0
  3. package/dist/node/protocol/v1.js +35 -0
  4. package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
  5. package/dist/node/runtime/V1Runtime.js +66 -29
  6. package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
  7. package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
  8. package/dist/node/runtime/model/ModelHost.js +81 -9
  9. package/dist/node/runtime/model/ModelHostStream.js +61 -1
  10. package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
  11. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
  12. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
  13. package/dist/node/runtime/play/PlayCallChain.js +904 -369
  14. package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
  15. package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
  16. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
  17. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
  18. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
  19. package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
  20. package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
  21. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
  22. package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
  23. package/dist/node/server/createServer.js +10 -2
  24. package/dist/web/assets/index-D5VqDvjY.js +1192 -0
  25. package/dist/web/assets/index-Xa0s35CJ.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
  29. package/dist/web/assets/index-DDx1itzT.js +0 -1192
@@ -1,3 +1,52 @@
1
+ /** CLIProxyAPI gives these model-name suffixes priority over body effort. */
2
+ export function hasCLIProxyThinkingSuffix(modelId) {
3
+ return /\((?:-1|none|auto|minimal|low|medium|high|xhigh|max|\d+)\)$/iu.test(modelId);
4
+ }
5
+ /** One validation authority shared by storage and the wire adapter boundary. */
6
+ export function modelReasoningPolicyIssue(input) {
7
+ const thinking = input.thinking ?? "provider_default";
8
+ const thinkingBudgetTokens = input.thinkingBudgetTokens ?? null;
9
+ const thinkingControlledByCLIProxySuffix = input.provider === "anthropic_messages" &&
10
+ input.dialect === "cliproxyapi" &&
11
+ hasCLIProxyThinkingSuffix(input.modelId);
12
+ if (input.provider === "anthropic_messages" &&
13
+ (input.effort === "none" || input.effort === "minimal"))
14
+ return "Anthropic Messages effort supports low, medium, high, xhigh, and max; use Thinking disabled instead of effort none";
15
+ if (input.provider !== "anthropic_messages" &&
16
+ thinking !== "provider_default")
17
+ return "Only Anthropic Messages defines a separate thinking mode; OpenAI-shaped protocols control reasoning through effort";
18
+ if (input.provider !== "anthropic_messages" && thinkingBudgetTokens !== null)
19
+ return "Only Anthropic Messages manual thinking accepts a thinking token budget";
20
+ if (thinking === "enabled") {
21
+ if (thinkingBudgetTokens === null ||
22
+ !Number.isSafeInteger(thinkingBudgetTokens) ||
23
+ thinkingBudgetTokens < 1_024)
24
+ return "Anthropic manual thinking requires an integer budget of at least 1024 tokens";
25
+ if (input.maxOutputTokens !== undefined &&
26
+ thinkingBudgetTokens >= input.maxOutputTokens)
27
+ return "Anthropic thinking budget must be lower than maximum output tokens";
28
+ }
29
+ else if (thinkingBudgetTokens !== null) {
30
+ return "A thinking token budget is valid only when Anthropic Thinking is set to manual enabled";
31
+ }
32
+ if (input.provider === "anthropic_messages" &&
33
+ (thinking === "provider_default" || thinking === "disabled") &&
34
+ input.summary !== "provider_default" &&
35
+ !(thinking === "provider_default" && thinkingControlledByCLIProxySuffix))
36
+ return "Anthropic returned thinking can be configured only when Thinking is adaptive or manual enabled, or when a CLIProxyAPI model suffix controls Thinking";
37
+ if (input.provider !== "openai_responses" &&
38
+ (input.summary === "concise" || input.summary === "detailed"))
39
+ return "Concise and detailed reasoning summaries are defined only by OpenAI Responses";
40
+ if (input.provider === "chat_completions" &&
41
+ input.dialect === "standard" &&
42
+ input.summary !== "provider_default")
43
+ return "Standard Chat Completions has no reasoning-summary parameter; use provider default or select the CLIProxyAPI dialect";
44
+ if (input.dialect === "cliproxyapi" &&
45
+ (input.effort !== "provider_default" || thinking !== "provider_default") &&
46
+ hasCLIProxyThinkingSuffix(input.modelId))
47
+ return "CLIProxyAPI model thinking suffixes override request effort and thinking; use either the model suffix with provider defaults or explicit request fields, not both";
48
+ return null;
49
+ }
1
50
  export const modelProviderPresets = [
2
51
  {
3
52
  id: "custom",
@@ -0,0 +1,39 @@
1
+ const modelUsageKeys = [
2
+ "inputTokens",
3
+ "uncachedInputTokens",
4
+ "cacheReadTokens",
5
+ "cacheWriteTokens",
6
+ "reasoningTokens",
7
+ "outputTokens",
8
+ "totalTokens",
9
+ ];
10
+ /** A zero baseline used only before any Provider exchange has completed. */
11
+ export function emptyAggregatedModelUsage() {
12
+ return usageWithValue(0, "derived_provider_fields");
13
+ }
14
+ /**
15
+ * Sum complete Provider usage records without treating an unreported field as
16
+ * zero. Once any exchange lacks a field, that aggregate remains unavailable.
17
+ */
18
+ export function aggregateModelUsage(current, next) {
19
+ const result = {};
20
+ for (const key of modelUsageKeys) {
21
+ const right = next?.[key] ?? null;
22
+ result[key] =
23
+ current[key] === null || right === null ? null : current[key] + right;
24
+ }
25
+ return {
26
+ ...result,
27
+ provenance: Object.fromEntries(modelUsageKeys.map((key) => [
28
+ key,
29
+ result[key] === null ? "unavailable" : "derived_provider_fields",
30
+ ])),
31
+ };
32
+ }
33
+ function usageWithValue(value, source) {
34
+ const values = Object.fromEntries(modelUsageKeys.map((key) => [key, value]));
35
+ return {
36
+ ...values,
37
+ provenance: Object.fromEntries(modelUsageKeys.map((key) => [key, source])),
38
+ };
39
+ }
@@ -106,7 +106,20 @@ const requiredFields = {
106
106
  exchangeId: "string",
107
107
  playerText: "string",
108
108
  },
109
+ "play.chain.cancel": {
110
+ worldId: "string",
111
+ chainId: "string",
112
+ exchangeId: "string",
113
+ },
109
114
  "play.chain.inspect": { worldId: "string" },
115
+ "play.timeline.page": { worldId: "string", limit: "number" },
116
+ "play.timeline.detail": {
117
+ worldId: "string",
118
+ chainId: "string",
119
+ eventId: "number",
120
+ },
121
+ "world.surface.read": { worldId: "string", surface: "string" },
122
+ "world.play-decorations.read": { worldId: "string" },
110
123
  "correction.begin": { worldId: "string", operationId: "string" },
111
124
  "correction.read": { candidateId: "string", document: "string" },
112
125
  "correction.patch": {
@@ -205,6 +218,23 @@ function validateRequestFields(request) {
205
218
  request.operationId !== undefined &&
206
219
  (typeof request.operationId !== "string" || request.operationId === ""))
207
220
  throw new V1ProtocolError("invalid_request", "artifacts.debug.operationId is invalid");
221
+ if (request.type === "play.timeline.page") {
222
+ if (!Number.isSafeInteger(request.limit) ||
223
+ Number(request.limit) < 1 ||
224
+ Number(request.limit) > 100 ||
225
+ (request.cursor !== undefined &&
226
+ (typeof request.cursor !== "string" || request.cursor === "")))
227
+ throw new V1ProtocolError("invalid_request", "play.timeline.page pagination is invalid");
228
+ }
229
+ if (request.type === "play.timeline.detail" &&
230
+ (!Number.isSafeInteger(request.eventId) || Number(request.eventId) < 1))
231
+ throw new V1ProtocolError("invalid_request", "play.timeline.detail.eventId is invalid");
232
+ if (request.type === "world.surface.read" &&
233
+ request.surface !== "state" &&
234
+ request.surface !== "control" &&
235
+ request.surface !== "history" &&
236
+ request.surface !== "runtime")
237
+ throw new V1ProtocolError("invalid_request", "world.surface.read.surface is invalid");
208
238
  if (request.type !== "setting-improvement.start")
209
239
  return;
210
240
  if (request.mode !== "plan_first" && request.mode !== "direct_candidate")
@@ -264,7 +294,12 @@ const requestTypes = new Set([
264
294
  "play.chain.revise-player",
265
295
  "play.chain.start",
266
296
  "play.chain.append",
297
+ "play.chain.cancel",
267
298
  "play.chain.inspect",
299
+ "play.timeline.page",
300
+ "play.timeline.detail",
301
+ "world.surface.read",
302
+ "world.play-decorations.read",
268
303
  "correction.begin",
269
304
  "correction.read",
270
305
  "correction.patch",
@@ -0,0 +1,86 @@
1
+ import { constants } from "node:fs";
2
+ import { copyFile, link, mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ const hardlinkFallbackCodes = new Set([
5
+ "EXDEV",
6
+ "EPERM",
7
+ "EACCES",
8
+ "EMLINK",
9
+ "ENOTSUP",
10
+ "EOPNOTSUPP",
11
+ "ENOSYS",
12
+ ]);
13
+ const reflinkFallbackCodes = new Set([
14
+ "EXDEV",
15
+ "EPERM",
16
+ "EACCES",
17
+ "ENOTSUP",
18
+ "EOPNOTSUPP",
19
+ "EINVAL",
20
+ "ENOSYS",
21
+ ]);
22
+ /**
23
+ * Retains one file without aliasing mutable bytes. Immutable files prefer a
24
+ * hard link; every other case walks reflink -> exclusive byte copy.
25
+ */
26
+ export async function cloneFilePhysically(input) {
27
+ await mkdir(dirname(input.target), { recursive: true, mode: 0o700 });
28
+ const strategy = process.env.NARRAEON_INTERNAL_TEST_CLONE_STRATEGY;
29
+ if (input.immutable && strategy !== "reflink" && strategy !== "copy") {
30
+ try {
31
+ await link(input.source, input.target);
32
+ return;
33
+ }
34
+ catch (error) {
35
+ if (await acceptExistingTarget(error, input))
36
+ return;
37
+ if (!isFallbackError(error, hardlinkFallbackCodes))
38
+ throw error;
39
+ }
40
+ }
41
+ if (strategy !== "copy") {
42
+ try {
43
+ await copyFileAsReflink(input.source, input.target);
44
+ return;
45
+ }
46
+ catch (error) {
47
+ if (await acceptExistingTarget(error, input))
48
+ return;
49
+ if (!isFallbackError(error, reflinkFallbackCodes))
50
+ throw error;
51
+ }
52
+ }
53
+ try {
54
+ await copyFile(input.source, input.target, constants.COPYFILE_EXCL);
55
+ }
56
+ catch (error) {
57
+ if (await acceptExistingTarget(error, input))
58
+ return;
59
+ throw error;
60
+ }
61
+ }
62
+ async function copyFileAsReflink(source, target) {
63
+ const injectedErrorCode = process.env.NARRAEON_INTERNAL_TEST_REFLINK_ERROR_CODE;
64
+ if (injectedErrorCode !== undefined) {
65
+ const error = new Error(`${injectedErrorCode}: injected copy-on-write clone failure`);
66
+ error.code = injectedErrorCode;
67
+ error.path = source;
68
+ error.syscall = "copyfile";
69
+ throw error;
70
+ }
71
+ await copyFile(source, target, constants.COPYFILE_FICLONE_FORCE);
72
+ }
73
+ async function acceptExistingTarget(error, input) {
74
+ if (!isNodeError(error) ||
75
+ error.code !== "EEXIST" ||
76
+ input.onTargetExists === undefined)
77
+ return false;
78
+ await input.onTargetExists(input.source, input.target);
79
+ return true;
80
+ }
81
+ function isFallbackError(error, acceptedCodes) {
82
+ return isNodeError(error) && acceptedCodes.has(error.code ?? "");
83
+ }
84
+ function isNodeError(error) {
85
+ return error instanceof Error && "code" in error;
86
+ }
@@ -239,7 +239,8 @@ export class V1Runtime {
239
239
  : this.#playPresets.bindRevision(request.playPresetId, request.playPresetRevision),
240
240
  ]);
241
241
  const hostBinding = presetHostBinding(playPreset);
242
- return this.#compiler.preview({
242
+ const modelHost = new FileNativeModelHost(connection);
243
+ const preview = this.#compiler.preview({
243
244
  endpoint: {
244
245
  id: `content:${request.packageId}`,
245
246
  commit: "current",
@@ -251,8 +252,32 @@ export class V1Runtime {
251
252
  }), `content:${request.packageId}.message.genesis.narrator`, "content-preview"),
252
253
  playerInputPlacement: "append",
253
254
  playerInput: request.playerInput,
254
- modelBinding: new FileNativeModelHost(connection).binding(),
255
+ modelBinding: modelHost.binding(),
255
256
  }, playPreset);
257
+ const tools = structuredClone(preview.compilation.toolUniverse ?? preview.compilation.tools);
258
+ return {
259
+ ...preview,
260
+ wireRequest: modelHost.previewRequest({
261
+ bootstrap: structuredClone(preview.compilation),
262
+ tools: structuredClone(tools),
263
+ toolUniverse: structuredClone(tools),
264
+ allowedTools: tools.map(({ name }) => name),
265
+ toolStrategy: preview.compilation.toolStrategy,
266
+ appended: preview.initialAppend === undefined
267
+ ? []
268
+ : [
269
+ {
270
+ kind: "player",
271
+ text: preview.initialAppend.logical.text,
272
+ },
273
+ ],
274
+ requestId: "play_call_chain",
275
+ operationId: "prompt-preview",
276
+ requestAttempt: 1,
277
+ exchange: 1,
278
+ maxOutputTokens: modelHost.binding().maxOutputTokens,
279
+ }),
280
+ };
256
281
  }
257
282
  case "world.create": {
258
283
  const [package_, connection, preset] = await Promise.all([
@@ -280,38 +305,42 @@ export class V1Runtime {
280
305
  return this.#worlds.deleteWorld(request.worldId);
281
306
  });
282
307
  case "world.read": {
283
- await this.#reconcileArtifacts(request.worldId);
284
308
  const head = await this.#worlds.currentHead(request.worldId);
285
309
  const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
286
- const [state, control, history, runtime, endpoint, authority, artifacts, extensions, playerViewPanels,] = await Promise.all([
287
- this.#worlds.readSurface(request.worldId, "state"),
288
- this.#worlds.readSurface(request.worldId, "control"),
289
- this.#worlds.readSurface(request.worldId, "history"),
290
- this.#worlds.readSurface(request.worldId, "runtime"),
291
- this.#worlds.recoverEndpoint(request.worldId, head),
292
- this.#worlds.readAuthorityHistory(request.worldId),
293
- this.#frontendProjection(request.worldId),
294
- this.#artifacts.readExtensionSummaries(request.worldId),
310
+ const [playerViewPanels, playTimeline] = await Promise.all([
295
311
  this.#frontendPlayerViewPanels(request.worldId, head, playerViews),
312
+ this.#worlds.playTimeline.readPage(request.worldId, 40),
296
313
  ]);
297
314
  return {
298
315
  worldId: request.worldId,
299
316
  head,
300
- state,
301
- control,
302
- history,
303
- runtime,
317
+ state: [],
318
+ control: [],
319
+ history: [],
320
+ runtime: { head, surfaces: "lazy" },
304
321
  playerViews,
305
- committedMessages: endpoint.history.map((message) => ({
306
- ...message,
307
- head: authority.commits.find((commit) => commit.historyAppend.some(({ messageId }) => messageId === message.messageId))?.head ?? "genesis",
308
- })),
309
- artifacts,
310
- extensions,
322
+ committedMessages: [],
323
+ artifacts: [],
324
+ extensions: [],
311
325
  playerViewPanels,
312
- playCallChain: await this.#playCallChains.inspectWorld(request.worldId),
326
+ artifactDebug: [],
327
+ playCallChain: null,
328
+ playTimeline,
313
329
  };
314
330
  }
331
+ case "world.surface.read":
332
+ return request.surface === "runtime"
333
+ ? this.#worlds.readSurface(request.worldId, "runtime")
334
+ : this.#worlds.readSurface(request.worldId, request.surface);
335
+ case "world.play-decorations.read": {
336
+ const head = await this.#reconcileArtifacts(request.worldId);
337
+ const [artifacts, extensions, artifactDebug] = await Promise.all([
338
+ this.#frontendProjection(request.worldId),
339
+ this.#artifacts.readExtensionSummaries(request.worldId),
340
+ this.#frontendDebug(request.worldId),
341
+ ]);
342
+ return { head, artifacts, extensions, artifactDebug };
343
+ }
315
344
  case "artifacts.read":
316
345
  await this.#reconcileArtifacts(request.worldId);
317
346
  return this.#frontendProjection(request.worldId, request.channel);
@@ -345,17 +374,12 @@ export class V1Runtime {
345
374
  }
346
375
  case "world.derive": {
347
376
  const preset = await this.#playPresets.bindCurrent();
348
- const result = await this.#worlds.deriveWorld({
377
+ const result = await this.#playCallChains.deriveWorld({
349
378
  operationId: request.operationId,
350
379
  sourceWorldId: request.sourceWorldId,
351
380
  sourceHead: request.sourceHead,
352
381
  hostPresetId: preset.id,
353
382
  });
354
- await this.#playCallChains.forkToDerivedWorld({
355
- sourceWorldId: request.sourceWorldId,
356
- sourceHead: request.sourceHead,
357
- targetWorldId: result.world.worldId,
358
- });
359
383
  await this.#reconcileResultArtifacts(result);
360
384
  return result;
361
385
  }
@@ -398,8 +422,20 @@ export class V1Runtime {
398
422
  ? {}
399
423
  : { observer: playCallChainObserver }),
400
424
  }));
425
+ case "play.chain.cancel":
426
+ return playCall(() => this.#playCallChains.cancel({
427
+ worldId: request.worldId,
428
+ chainId: request.chainId,
429
+ exchangeId: request.exchangeId,
430
+ }));
401
431
  case "play.chain.inspect":
402
432
  return this.#playCallChains.inspectWorld(request.worldId);
433
+ case "play.timeline.page":
434
+ await this.#worlds.ensureCurrentStorage(request.worldId);
435
+ return this.#worlds.playTimeline.readPage(request.worldId, request.limit, request.cursor);
436
+ case "play.timeline.detail":
437
+ await this.#worlds.ensureCurrentStorage(request.worldId);
438
+ return this.#worlds.playTimeline.readDetail(request.worldId, request.chainId, request.eventId);
403
439
  case "correction.begin":
404
440
  return this.#corrections.begin({
405
441
  worldId: request.worldId,
@@ -437,6 +473,7 @@ export class V1Runtime {
437
473
  this.#worlds.currentHeadOperationId(worldId),
438
474
  ]);
439
475
  await this.#artifacts.reconcileHead(worldId, head, currentOperationId ?? undefined);
476
+ return head;
440
477
  }
441
478
  async #frontendProjection(worldId, channel) {
442
479
  const artifacts = await this.#artifacts.readActiveProjection(worldId, channel);