narraeon 0.1.1 → 0.2.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.
@@ -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
+ }
@@ -107,6 +107,14 @@ const requiredFields = {
107
107
  playerText: "string",
108
108
  },
109
109
  "play.chain.inspect": { worldId: "string" },
110
+ "play.timeline.page": { worldId: "string", limit: "number" },
111
+ "play.timeline.detail": {
112
+ worldId: "string",
113
+ chainId: "string",
114
+ eventId: "number",
115
+ },
116
+ "world.surface.read": { worldId: "string", surface: "string" },
117
+ "world.play-decorations.read": { worldId: "string" },
110
118
  "correction.begin": { worldId: "string", operationId: "string" },
111
119
  "correction.read": { candidateId: "string", document: "string" },
112
120
  "correction.patch": {
@@ -205,6 +213,23 @@ function validateRequestFields(request) {
205
213
  request.operationId !== undefined &&
206
214
  (typeof request.operationId !== "string" || request.operationId === ""))
207
215
  throw new V1ProtocolError("invalid_request", "artifacts.debug.operationId is invalid");
216
+ if (request.type === "play.timeline.page") {
217
+ if (!Number.isSafeInteger(request.limit) ||
218
+ Number(request.limit) < 1 ||
219
+ Number(request.limit) > 100 ||
220
+ (request.cursor !== undefined &&
221
+ (typeof request.cursor !== "string" || request.cursor === "")))
222
+ throw new V1ProtocolError("invalid_request", "play.timeline.page pagination is invalid");
223
+ }
224
+ if (request.type === "play.timeline.detail" &&
225
+ (!Number.isSafeInteger(request.eventId) || Number(request.eventId) < 1))
226
+ throw new V1ProtocolError("invalid_request", "play.timeline.detail.eventId is invalid");
227
+ if (request.type === "world.surface.read" &&
228
+ request.surface !== "state" &&
229
+ request.surface !== "control" &&
230
+ request.surface !== "history" &&
231
+ request.surface !== "runtime")
232
+ throw new V1ProtocolError("invalid_request", "world.surface.read.surface is invalid");
208
233
  if (request.type !== "setting-improvement.start")
209
234
  return;
210
235
  if (request.mode !== "plan_first" && request.mode !== "direct_candidate")
@@ -265,6 +290,10 @@ const requestTypes = new Set([
265
290
  "play.chain.start",
266
291
  "play.chain.append",
267
292
  "play.chain.inspect",
293
+ "play.timeline.page",
294
+ "play.timeline.detail",
295
+ "world.surface.read",
296
+ "world.play-decorations.read",
268
297
  "correction.begin",
269
298
  "correction.read",
270
299
  "correction.patch",
@@ -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);
@@ -400,6 +429,12 @@ export class V1Runtime {
400
429
  }));
401
430
  case "play.chain.inspect":
402
431
  return this.#playCallChains.inspectWorld(request.worldId);
432
+ case "play.timeline.page":
433
+ await this.#worlds.ensureCurrentStorage(request.worldId);
434
+ return this.#worlds.playTimeline.readPage(request.worldId, request.limit, request.cursor);
435
+ case "play.timeline.detail":
436
+ await this.#worlds.ensureCurrentStorage(request.worldId);
437
+ return this.#worlds.playTimeline.readDetail(request.worldId, request.chainId, request.eventId);
403
438
  case "correction.begin":
404
439
  return this.#corrections.begin({
405
440
  worldId: request.worldId,
@@ -437,6 +472,7 @@ export class V1Runtime {
437
472
  this.#worlds.currentHeadOperationId(worldId),
438
473
  ]);
439
474
  await this.#artifacts.reconcileHead(worldId, head, currentOperationId ?? undefined);
475
+ return head;
440
476
  }
441
477
  async #frontendProjection(worldId, channel) {
442
478
  const artifacts = await this.#artifacts.readActiveProjection(worldId, channel);