narraeon 0.1.1-oidc.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.
- package/README.md +4 -1
- package/README.zh-CN.md +2 -2
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +29 -0
- package/dist/node/runtime/V1Runtime.js +59 -23
- package/dist/node/runtime/model/FileNativeModelAdapters.js +691 -275
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +74 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1039 -0
- package/dist/node/runtime/play/PlayCallChain.js +609 -330
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +1 -4
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +45 -30
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +626 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +449 -191
- package/dist/node/server/createServer.js +0 -27
- package/dist/web/assets/index-DHV_VWl3.js +1192 -0
- package/dist/web/assets/index-DjLCkP99.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
package/README.md
CHANGED
|
@@ -59,6 +59,9 @@ Then open <http://127.0.0.1:4317>. The named volume preserves worlds,
|
|
|
59
59
|
configuration, credentials, logs, and recovery state across container
|
|
60
60
|
recreation. Keep the host-side address as `127.0.0.1`; publishing the port on
|
|
61
61
|
all interfaces would unnecessarily expose a local, single-user Runtime.
|
|
62
|
+
Narraeon does not currently provide built-in web authentication or access
|
|
63
|
+
control. Run it only on loopback or a trusted private network; public exposure
|
|
64
|
+
is unsupported until a unified security model is designed.
|
|
62
65
|
Stable releases move `latest`, prereleases move `next`, and every release also
|
|
63
66
|
has an exact version tag.
|
|
64
67
|
|
|
@@ -156,7 +159,7 @@ The container image maps all three storage roots under
|
|
|
156
159
|
`/var/lib/narraeon`, which is why the Docker example persists that single
|
|
157
160
|
directory as a volume.
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
By default, the browser talks only to the same-machine Runtime. Local-first does not mean that remote model inference is offline: prompts, selected world material, tool exchanges, and generated text are sent to the provider you configure. API keys are stored in the local configuration and are not returned to the browser after saving.
|
|
160
163
|
|
|
161
164
|
When a provider, response-format, Runtime-tool, or candidate-check error occurs, Narraeon creates a JSONL incident under `NARRAEON_LOG_ROOT/ai-failures`. It preserves the raw provider exchange and later recovery attempts, including reasoning or thinking actually returned by the provider. It does not invent hidden reasoning or record API keys and request headers. The files can still contain private prompts, world content, tool arguments, and provider-returned reasoning, so treat the log directory as sensitive.
|
|
162
165
|
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ docker run --detach --name narraeon \
|
|
|
55
55
|
ghcr.io/narraeon/narraeon:latest
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
然后打开 <http://127.0.0.1:4317>。命名卷会在重建容器后继续保留世界、配置、凭据、日志和恢复数据。宿主机一侧应继续绑定 `127.0.0.1`;绑定所有网卡会把本地单用户 Runtime
|
|
58
|
+
然后打开 <http://127.0.0.1:4317>。命名卷会在重建容器后继续保留世界、配置、凭据、日志和恢复数据。宿主机一侧应继续绑定 `127.0.0.1`;绑定所有网卡会把本地单用户 Runtime 不必要地暴露出去。Narraeon 当前不内置 Web 认证或访问控制,只应运行在回环地址或受信任私网中;在统一安全模型设计完成前,不支持暴露到公网。稳定版会移动 `latest`,预发布版会移动 `next`,每次发布还会保留精确版本标签。
|
|
59
59
|
|
|
60
60
|
## 从源码运行
|
|
61
61
|
|
|
@@ -149,7 +149,7 @@ Narraeon 默认使用当前操作系统的标准用户应用目录。如果要
|
|
|
149
149
|
|
|
150
150
|
容器镜像把前三类存储目录统一放在 `/var/lib/narraeon` 下,因此 Docker 示例只需要持久化这一个目录。
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
默认情况下,浏览器只与同机 Runtime 通信。“本地优先”不等于远程模型推理也离线:提示词、选中的世界材料、工具交换和生成文本会发送到你配置的 Provider。API Key 保存在本地配置中,保存后不会再返回给浏览器。
|
|
153
153
|
|
|
154
154
|
发生 Provider、响应格式、Runtime 工具或候选检查错误时,Narraeon 会在 `NARRAEON_LOG_ROOT/ai-failures` 下创建 JSONL 事故记录。它会保存原始 Provider 交换和后续恢复尝试,包括 Provider 真实返回的 reasoning/thinking。它不会伪造隐藏思考,也不会记录 API Key 和请求 header。不过日志仍可能包含私密提示词、世界内容、工具参数和 Provider 返回的思考,因此应当把该目录视为敏感数据。
|
|
155
155
|
|
|
@@ -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
|
+
}
|
package/dist/node/protocol/v1.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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 [
|
|
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:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
})),
|
|
309
|
-
artifacts,
|
|
310
|
-
extensions,
|
|
322
|
+
committedMessages: [],
|
|
323
|
+
artifacts: [],
|
|
324
|
+
extensions: [],
|
|
311
325
|
playerViewPanels,
|
|
312
|
-
|
|
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);
|