cortico 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 +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型上下文中的结构标记,用于缺失、截断、不可用或中断的内容。
|
|
3
|
+
* 标记使用英文,不受控制台语言设置影响;语义说明由 Persona 提供。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 工具回执被折叠(见 truncate.ts 的 FOLD_THRESHOLD) */
|
|
7
|
+
export const FOLD_PLACEHOLDER = '[result folded]';
|
|
8
|
+
/** 调用入参被折叠 */
|
|
9
|
+
export const FOLD_ARGS_PLACEHOLDER = '[arguments folded]';
|
|
10
|
+
/** 单条越过整份预算,整条换成记号 */
|
|
11
|
+
export const OVERSIZE_PLACEHOLDER = '[turn too long, omitted]';
|
|
12
|
+
|
|
13
|
+
/** 配对修复:重建后调用还开着,补一条回执 */
|
|
14
|
+
export const MISSING_RESULT = '[result missing]';
|
|
15
|
+
/** 进程重启后,为未完成的调用补充缺失回执。 */
|
|
16
|
+
export const MISSING_RESULT_RESTART = '[result missing (process restart)]';
|
|
17
|
+
/** 主线程调用仍在执行时,fork 快照中的临时回执。 */
|
|
18
|
+
export const PENDING_IN_MAIN_THREAD = '[result pending in the main thread]';
|
|
19
|
+
|
|
20
|
+
/** 工具名不在本 session 的工具表里 */
|
|
21
|
+
export const UNKNOWN_TOOL = '[unknown tool]';
|
|
22
|
+
/** fork 调用了当前工具表中不存在的名称。 */
|
|
23
|
+
export const forkUnknownTool = (name: string): string =>
|
|
24
|
+
`[unknown tool ${name}] This thread wires a subset of the tools. Use the ones listed above.`;
|
|
25
|
+
|
|
26
|
+
/** 上游把这次调用标成未完成(流被截断),参数不可信,不执行 */
|
|
27
|
+
export const NOT_EXECUTED_INCOMPLETE = '[not executed: function call incomplete]';
|
|
28
|
+
/** 屏障工具(barrierAfter)之后同一条消息里的调用 */
|
|
29
|
+
export const NOT_EXECUTED_BARRIER = '[not executed: review the preceding tool result first]';
|
|
30
|
+
/** fork 已结束,同一条消息中后续的调用不执行。 */
|
|
31
|
+
export const NOT_EXECUTED_THREAD_ENDED = '[not executed: this thread already ended]';
|
|
32
|
+
/** 流在响应中途断了,提前派发的调用没跑完 */
|
|
33
|
+
export const NOT_EXECUTED_STREAM_ABORTED = '[not executed: stream aborted mid-response]';
|
|
34
|
+
/** 主循环已停 */
|
|
35
|
+
export const NOT_EXECUTED_LOOP_STOPPED = '[not executed: main loop stopped]';
|
|
36
|
+
/** 关机打断了这一轮,回执取不回来了 */
|
|
37
|
+
export const SHUTDOWN_INTERRUPTED = '[tool result unavailable: shutdown interrupted the round]';
|
|
38
|
+
|
|
39
|
+
/** 工具失败的模型可见标记;结构化失败状态使用 ToolOutcome.failed。 */
|
|
40
|
+
export const toolFailed = (detail: string): string => `[tool failed] ${detail}`;
|
|
41
|
+
/** 参数不是合法 JSON:不进 handler,就地失败 */
|
|
42
|
+
export const TOOL_FAILED_BAD_ARGS = toolFailed('arguments are not valid JSON');
|
|
43
|
+
|
|
44
|
+
/** 合成投递帧的表头(见 loop.ts 的 EXTERNAL_EVENT_FRAME) */
|
|
45
|
+
export const eventFrameHeader = (count: number): string =>
|
|
46
|
+
`[${count} new event${count === 1 ? '' : 's'}]`;
|
|
47
|
+
/** 读取投递帧正文时使用,需与写入的表头一致。 */
|
|
48
|
+
export const EVENT_FRAME_HEADER_RE = /^\[\d+ new events?\]\n?/;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles the main-session system prefix from Persona.systemSegments().
|
|
3
|
+
* World environment text is rendered from templates and supplied to that hook.
|
|
4
|
+
* Core joins the returned segments in order; their text remains opaque to Core.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { renderTemplate } from './template.ts';
|
|
9
|
+
import type {
|
|
10
|
+
World,
|
|
11
|
+
WorldPrefixContext,
|
|
12
|
+
Persona,
|
|
13
|
+
PrefixSegment,
|
|
14
|
+
PromptDocDecl,
|
|
15
|
+
} from './types.ts';
|
|
16
|
+
|
|
17
|
+
export interface AssembleSystemDeps {
|
|
18
|
+
persona: Persona;
|
|
19
|
+
worlds: World[];
|
|
20
|
+
now: Date;
|
|
21
|
+
timezone: string;
|
|
22
|
+
/** 找环境提示词覆盖的两个目录。缺席(测试、预建实例)= 只用 World 自带的模板。 */
|
|
23
|
+
dirs?: EnvPromptDirs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* World 环境模板按以下顺序覆盖,同名文件整份替换:
|
|
28
|
+
* src/worlds/<id>/ENV_PROMPT.md → <代码包>/worlds/<id>/ENV_PROMPT.md
|
|
29
|
+
* → <部署>/worlds/<id>/ENV_PROMPT.md。
|
|
30
|
+
* 部署文件不进版本控制;控制台仅写入部署覆盖,删除后回退到代码包或 World 模板。
|
|
31
|
+
*/
|
|
32
|
+
export interface EnvPromptDirs {
|
|
33
|
+
/** bot 代码包目录。 */
|
|
34
|
+
packageDir?: string;
|
|
35
|
+
/** 部署目录。 */
|
|
36
|
+
deploymentDir?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 某一层里这个 World 的覆盖文件路径:`<dir>/worlds/<Worldid>/ENV_PROMPT.md`。 */
|
|
40
|
+
export function envPromptOverridePath(dir: string, worldId: string): string {
|
|
41
|
+
return join(dir, 'worlds', worldId, 'ENV_PROMPT.md');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** World 环境提示词模板此刻读哪份文件。自后向前找第一份存在的。 */
|
|
45
|
+
export function envPromptTemplateSource(
|
|
46
|
+
doc: PromptDocDecl,
|
|
47
|
+
worldId: string,
|
|
48
|
+
dirs: EnvPromptDirs | undefined,
|
|
49
|
+
): { path: string; origin: EnvPromptOrigin } {
|
|
50
|
+
for (const [dir, origin] of [
|
|
51
|
+
[dirs?.deploymentDir, 'deployment'],
|
|
52
|
+
[dirs?.packageDir, 'package'],
|
|
53
|
+
] as const) {
|
|
54
|
+
if (!dir) continue;
|
|
55
|
+
const override = envPromptOverridePath(dir, worldId);
|
|
56
|
+
if (existsSync(override)) return { path: override, origin };
|
|
57
|
+
}
|
|
58
|
+
return { path: doc.path, origin: 'module' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type EnvPromptOrigin = 'deployment' | 'package' | 'module';
|
|
62
|
+
|
|
63
|
+
/** 按 role=envPrompt 查找模板声明;未声明时不提供环境前缀。 */
|
|
64
|
+
export function envPromptDocOf(mod: World): PromptDocDecl | undefined {
|
|
65
|
+
let decl;
|
|
66
|
+
try {
|
|
67
|
+
decl = mod.console?.();
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined; // 控制台声明异常时省略该 World 的模板。
|
|
70
|
+
}
|
|
71
|
+
return decl?.promptDocs?.find((doc) => doc.role === 'envPrompt');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 读取模板并代入 World 当前值;前缀组装与控制台预览共用。 */
|
|
75
|
+
export async function renderWorldEnvPrompt(
|
|
76
|
+
mod: World,
|
|
77
|
+
dirs?: EnvPromptDirs,
|
|
78
|
+
): Promise<{ text: string; sourceKey?: string }> {
|
|
79
|
+
// null 表示省略整段,不读取模板。
|
|
80
|
+
const vars = await mod.envPromptVars();
|
|
81
|
+
const doc = vars === null ? undefined : envPromptDocOf(mod);
|
|
82
|
+
if (!doc) return { text: '' };
|
|
83
|
+
const { path } = envPromptTemplateSource(doc, mod.id, dirs);
|
|
84
|
+
const text = renderTemplate(readFileSync(path, 'utf8'), vars ?? {}).trim();
|
|
85
|
+
// 空段不提供模板编辑来源。
|
|
86
|
+
return text ? { text, sourceKey: doc.key } : { text: '' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 环境模板提供 World 的前缀文本;工具 description 随工具定义发送。 */
|
|
90
|
+
export async function collectWorldContexts(
|
|
91
|
+
worlds: World[],
|
|
92
|
+
dirs?: EnvPromptDirs,
|
|
93
|
+
): Promise<WorldPrefixContext[]> {
|
|
94
|
+
const sorted = [...worlds].sort((a, b) => a.id.localeCompare(b.id));
|
|
95
|
+
return Promise.all(
|
|
96
|
+
sorted.map(async (mod) => {
|
|
97
|
+
const { text, sourceKey } = await renderWorldEnvPrompt(mod, dirs);
|
|
98
|
+
return {
|
|
99
|
+
id: mod.id,
|
|
100
|
+
envPrompt: text,
|
|
101
|
+
...(sourceKey ? { sourceKey } : {}),
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 供控制台观察:前缀的分段视图(与实际发出的前缀同一来源) */
|
|
108
|
+
export async function assembleSystemSegments(deps: AssembleSystemDeps): Promise<PrefixSegment[]> {
|
|
109
|
+
const { persona, worlds, now, timezone, dirs } = deps;
|
|
110
|
+
return persona.systemSegments({
|
|
111
|
+
now,
|
|
112
|
+
timezone,
|
|
113
|
+
worlds: await collectWorldContexts(worlds, dirs),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 按顺序直接拼接各段,分隔符由 Persona 的段文本提供。 */
|
|
118
|
+
export async function assembleSystem(deps: AssembleSystemDeps): Promise<string> {
|
|
119
|
+
const segments = await assembleSystemSegments(deps);
|
|
120
|
+
return segments.map((segment) => segment.text).join('');
|
|
121
|
+
}
|
package/src/core/run.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一次进程运行对应一个 run,运行日志保存在 data/runs/<run>/。
|
|
3
|
+
* index.jsonl 在启动与正常关机时各追加一行;暂停沿用 run,重启创建新 run。
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { nowIso } from './util.ts';
|
|
9
|
+
|
|
10
|
+
export interface RunInfo {
|
|
11
|
+
id: string;
|
|
12
|
+
dir: string;
|
|
13
|
+
runsDir: string;
|
|
14
|
+
startedAt: string;
|
|
15
|
+
previousRun: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RunOpenMeta {
|
|
19
|
+
timezone: string;
|
|
20
|
+
bot: string;
|
|
21
|
+
repoRoot?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunCloseSummary {
|
|
25
|
+
endedAt: string;
|
|
26
|
+
lastCursor: number;
|
|
27
|
+
/** 关机步骤是否全部完成。 */
|
|
28
|
+
complete: boolean | null;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const RUN_ID = /^r-\d{8}-\d{6}-[0-9a-f]{4}$/;
|
|
33
|
+
|
|
34
|
+
export function runsDirOf(dataDir: string): string {
|
|
35
|
+
return join(dataDir, 'runs');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 已存在的 run id,按时间升序(id 自带时间戳,字典序即时间序)。 */
|
|
39
|
+
export function listRuns(dataDir: string): string[] {
|
|
40
|
+
const dir = runsDirOf(dataDir);
|
|
41
|
+
if (!existsSync(dir)) return [];
|
|
42
|
+
return readdirSync(dir).filter((name) => RUN_ID.test(name)).sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function gitSha(repoRoot: string | undefined): string | null {
|
|
46
|
+
if (!repoRoot) return null;
|
|
47
|
+
try {
|
|
48
|
+
const head = readFileSync(join(repoRoot, '.git', 'HEAD'), 'utf8').trim();
|
|
49
|
+
if (!head.startsWith('ref:')) return head.slice(0, 12);
|
|
50
|
+
const ref = head.slice(4).trim();
|
|
51
|
+
const refFile = resolve(repoRoot, '.git', ref);
|
|
52
|
+
if (existsSync(refFile)) return readFileSync(refFile, 'utf8').trim().slice(0, 12);
|
|
53
|
+
const packed = join(repoRoot, '.git', 'packed-refs');
|
|
54
|
+
if (!existsSync(packed)) return null;
|
|
55
|
+
for (const line of readFileSync(packed, 'utf8').split('\n')) {
|
|
56
|
+
const [sha, name] = line.trim().split(' ');
|
|
57
|
+
if (name === ref) return sha.slice(0, 12);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// 不在 git 仓库里就不记
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 生成 run id、建目录、在 index.jsonl 记一行开机。 */
|
|
66
|
+
export function openRun(dataDir: string, meta: RunOpenMeta): RunInfo {
|
|
67
|
+
const runsDir = runsDirOf(dataDir);
|
|
68
|
+
mkdirSync(runsDir, { recursive: true });
|
|
69
|
+
const previous = listRuns(dataDir);
|
|
70
|
+
const startedAt = nowIso(meta.timezone);
|
|
71
|
+
const stamp = startedAt.slice(0, 19).replace(/[-:T]/g, '');
|
|
72
|
+
const id = `r-${stamp.slice(0, 8)}-${stamp.slice(8, 14)}-${randomBytes(2).toString('hex')}`;
|
|
73
|
+
const dir = join(runsDir, id);
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
const previousRun = previous.length ? previous[previous.length - 1] : null;
|
|
76
|
+
appendFileSync(join(runsDir, 'index.jsonl'), JSON.stringify({
|
|
77
|
+
run: id,
|
|
78
|
+
startedAt,
|
|
79
|
+
bot: meta.bot,
|
|
80
|
+
pid: process.pid,
|
|
81
|
+
gitSha: gitSha(meta.repoRoot),
|
|
82
|
+
previousRun,
|
|
83
|
+
}) + '\n', 'utf8');
|
|
84
|
+
return { id, dir, runsDir, startedAt, previousRun };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 追加关机记录;进程异常结束时可能没有该行,查询方需从已有记录推算结束时间。 */
|
|
88
|
+
export function closeRun(run: RunInfo, summary: RunCloseSummary): void {
|
|
89
|
+
appendFileSync(join(run.runsDir, 'index.jsonl'), JSON.stringify({ run: run.id, ...summary }) + '\n', 'utf8');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** run.json 保存本次运行的 World 清单与配置指纹。 */
|
|
93
|
+
export function writeRunJson(run: RunInfo, data: Record<string, unknown>): void {
|
|
94
|
+
writeFileSync(join(run.dir, 'run.json'), JSON.stringify({ run: run.id, startedAt: run.startedAt, previousRun: run.previousRun, ...data }, null, 2), 'utf8');
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 按名称同步读取密钥;可轮换凭据的生命周期由 provider 管理。 */
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { readTextFile } from './util.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 每次优先读取非空进程环境变量;否则使用首次读取后缓存的文件内容,缺失返回空串。
|
|
7
|
+
* 文件值读取到首个空白字符,不解析引号。
|
|
8
|
+
*/
|
|
9
|
+
export function secretReader(file: string): (name: string) => string {
|
|
10
|
+
let text: string | null = null;
|
|
11
|
+
return (name: string): string => {
|
|
12
|
+
const fromEnv = process.env[name];
|
|
13
|
+
if (fromEnv) return fromEnv;
|
|
14
|
+
if (text === null) text = existsSync(file) ? readTextFile(file) : '';
|
|
15
|
+
const m = new RegExp(`^\\s*${name}\\s*=\\s*(\\S+)`, 'm').exec(text);
|
|
16
|
+
return m ? m[1] : '';
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Session context is append-only between lifecycle resets. */
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { ContextRecord } from '../protocol/open-responses/context.ts';
|
|
4
|
+
import { ContextLog } from '../protocol/open-responses/context-log.ts';
|
|
5
|
+
import { estimateMessagesTokens } from './util.ts';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export class SessionLog {
|
|
9
|
+
private readonly context: ContextLog;
|
|
10
|
+
get records(): readonly ContextRecord[] { return this.context.records; }
|
|
11
|
+
private appendListeners: Array<(msg: ContextRecord, index: number) => void> = [];
|
|
12
|
+
private resetListeners: Array<(messages: ContextRecord[]) => void> = [];
|
|
13
|
+
|
|
14
|
+
/** stamp:入库时刻的来源(config 时区的 ISO);不给则消息不带 ts */
|
|
15
|
+
constructor(
|
|
16
|
+
dataDir: string,
|
|
17
|
+
fileName = 'session-main.jsonl',
|
|
18
|
+
stamp?: () => string,
|
|
19
|
+
) {
|
|
20
|
+
this.context = new ContextLog(join(dataDir, fileName), stamp);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 每次 append 后通知 Web 调试监听器;监听器异常与会话写入隔离。 */
|
|
24
|
+
onAppend(cb: (msg: ContextRecord, index: number) => void): void {
|
|
25
|
+
this.appendListeners.push(cb);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 截断/重置后回调(整个数组被替换) */
|
|
29
|
+
onReset(cb: (messages: ContextRecord[]) => void): void {
|
|
30
|
+
this.resetListeners.push(cb);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
append(msg: ContextRecord): void {
|
|
34
|
+
this.context.append(msg);
|
|
35
|
+
const stored = this.records[this.records.length - 1];
|
|
36
|
+
for (const cb of this.appendListeners) {
|
|
37
|
+
try { cb(stored, this.records.length - 1); } catch { /* 观察者异常不影响主流程 */ }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
load(): void { this.context.load(); }
|
|
42
|
+
|
|
43
|
+
/** 整体替换:先写临时文件,再 rename 覆盖目标文件。 */
|
|
44
|
+
reset(messages: readonly ContextRecord[]): void {
|
|
45
|
+
this.context.reset(messages);
|
|
46
|
+
for (const cb of this.resetListeners) {
|
|
47
|
+
try { cb([...this.records]); } catch { /* 观察者异常不影响主流程 */ }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
estTokens(): number {
|
|
52
|
+
return estimateMessagesTokens(this.records);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ContextRecord } from '../protocol/open-responses/context.ts';
|
|
2
|
+
import type { ProviderAttempt } from './generation.ts';
|
|
3
|
+
import { usageCounters } from '../protocol/open-responses/context-helpers.ts';
|
|
4
|
+
/**
|
|
5
|
+
* 控制台使用的 session 统计与消息引用,注册表仅驻留内存。
|
|
6
|
+
* role 使用 Persona 声明的 session id;open 返回的实例 id 也传给模型调用。
|
|
7
|
+
* 消息通过保存的引用在读取时序列化;重启清空统计。
|
|
8
|
+
*/
|
|
9
|
+
import type { LLMUsage, UsageRecord } from './types.ts';
|
|
10
|
+
import { nowIso } from './util.ts';
|
|
11
|
+
|
|
12
|
+
export interface SessionStats {
|
|
13
|
+
id: string;
|
|
14
|
+
/** session声明id(Persona定义的不透明字符串) */
|
|
15
|
+
role: string;
|
|
16
|
+
label: string;
|
|
17
|
+
startedAt: string;
|
|
18
|
+
/** null=进行中 */
|
|
19
|
+
endedAt: string | null;
|
|
20
|
+
/** LLM调用次数 */
|
|
21
|
+
calls: number;
|
|
22
|
+
promptTokens: number;
|
|
23
|
+
completionTokens: number;
|
|
24
|
+
cacheHitTokens: number;
|
|
25
|
+
cacheMissTokens: number;
|
|
26
|
+
reasoningTokens: number;
|
|
27
|
+
/** 缓存命中率估算 = hit/(hit+miss);无输入数据为null */
|
|
28
|
+
cacheHitRate: number | null;
|
|
29
|
+
/** 当前消息条数(引用实时读) */
|
|
30
|
+
messageCount: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SessionHandle {
|
|
34
|
+
recordAttempts(attempts: readonly ProviderAttempt[], messagesRef?: ContextRecord[], extra?: { prefixHash?: string; outcome?: 'discarded' }): void;
|
|
35
|
+
/** 本次 session 实例的 id;常驻 session 使用声明 id,临时 fork 使用运行期序号。 */
|
|
36
|
+
id: string;
|
|
37
|
+
/** 累计用量,可更新消息数组引用。model 用于持久化归因;extra 提供前缀指纹和费用记录。 */
|
|
38
|
+
record(
|
|
39
|
+
usage: LLMUsage,
|
|
40
|
+
messagesRef?: ContextRecord[],
|
|
41
|
+
model?: string,
|
|
42
|
+
extra?: { prefixHash?: string; charges?: import('./generation.ts').Charge[] },
|
|
43
|
+
): void;
|
|
44
|
+
/** 关闭 session;重复调用无效。 */
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Entry {
|
|
49
|
+
stats: Omit<SessionStats, 'cacheHitRate' | 'messageCount'>;
|
|
50
|
+
messagesRef: (() => readonly ContextRecord[]) | null;
|
|
51
|
+
/** 用固定id注册的常驻session:结束后也不从仪表里清掉 */
|
|
52
|
+
pinned: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 已结束session最多保留条数(临时session的近期历史,防内存膨胀) */
|
|
56
|
+
const CLOSED_KEEP = 8;
|
|
57
|
+
|
|
58
|
+
export class SessionTracker {
|
|
59
|
+
private readonly timezone: string;
|
|
60
|
+
private readonly onRecord?: (rec: UsageRecord) => void;
|
|
61
|
+
private readonly entries = new Map<string, Entry>();
|
|
62
|
+
private readonly listeners: Array<() => void> = [];
|
|
63
|
+
private seq = 0;
|
|
64
|
+
|
|
65
|
+
/** onRecord=每次record时把一条持久化流水交出去(UsageLog.append),可选 */
|
|
66
|
+
constructor(timezone: string, onRecord?: (rec: UsageRecord) => void) {
|
|
67
|
+
this.timezone = timezone;
|
|
68
|
+
this.onRecord = onRecord;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 变化通知(open/record/close都触发;web侧接这里做推送) */
|
|
72
|
+
onChange(cb: () => void): void {
|
|
73
|
+
this.listeners.push(cb);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 注册一个session。opts.id指定固定id(常驻session用它的声明id);
|
|
78
|
+
* opts.messagesRef=消息数组的惰性引用(查看消息流/条数用)。
|
|
79
|
+
*/
|
|
80
|
+
open(
|
|
81
|
+
role: string,
|
|
82
|
+
label: string,
|
|
83
|
+
opts?: { id?: string; messagesRef?: () => readonly ContextRecord[] },
|
|
84
|
+
): SessionHandle {
|
|
85
|
+
const id = opts?.id ?? `${role}-${++this.seq}`;
|
|
86
|
+
const entry: Entry = {
|
|
87
|
+
stats: {
|
|
88
|
+
id,
|
|
89
|
+
role,
|
|
90
|
+
label,
|
|
91
|
+
startedAt: nowIso(this.timezone),
|
|
92
|
+
endedAt: null,
|
|
93
|
+
calls: 0,
|
|
94
|
+
promptTokens: 0,
|
|
95
|
+
completionTokens: 0,
|
|
96
|
+
cacheHitTokens: 0,
|
|
97
|
+
cacheMissTokens: 0,
|
|
98
|
+
reasoningTokens: 0,
|
|
99
|
+
},
|
|
100
|
+
messagesRef: opts?.messagesRef ?? null,
|
|
101
|
+
pinned: opts?.id !== undefined,
|
|
102
|
+
};
|
|
103
|
+
this.entries.set(id, entry);
|
|
104
|
+
this.pruneClosed();
|
|
105
|
+
this.emit();
|
|
106
|
+
|
|
107
|
+
let closed = false;
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
recordAttempts: (attempts, messagesRef, extra) => {
|
|
111
|
+
if (messagesRef) entry.messagesRef = () => messagesRef;
|
|
112
|
+
for (const source of attempts) {
|
|
113
|
+
const attempt = structuredClone(source);
|
|
114
|
+
if (extra?.outcome === 'discarded' && (attempt.outcome === 'completed' || attempt.outcome === 'incomplete')) attempt.outcome = 'discarded';
|
|
115
|
+
const usage = usageCounters(attempt.meters);
|
|
116
|
+
const stats = entry.stats;
|
|
117
|
+
stats.calls++;
|
|
118
|
+
stats.promptTokens += usage.promptTokens;
|
|
119
|
+
stats.completionTokens += usage.completionTokens;
|
|
120
|
+
stats.cacheHitTokens += usage.cacheHitTokens;
|
|
121
|
+
stats.cacheMissTokens += usage.cacheMissTokens;
|
|
122
|
+
stats.reasoningTokens += usage.reasoningTokens ?? 0;
|
|
123
|
+
this.onRecord?.({ version: 2, attempt, ts: nowIso(this.timezone, new Date(attempt.startedAt)), sessionId: id,
|
|
124
|
+
role: stats.role, label: stats.label, model: attempt.origin.model, ...usage, reasoningTokens: usage.reasoningTokens ?? 0,
|
|
125
|
+
failedAfterMs: attempt.elapsedMs, ...(attempt.requestId ? { requestId: attempt.requestId } : {}),
|
|
126
|
+
...(attempt.status !== null ? { status: attempt.status } : {}),
|
|
127
|
+
...(attempt.outcome === 'completed' || attempt.outcome === 'incomplete' ? {} : { outcome: attempt.outcome === 'discarded' ? 'discarded' : 'failed' }),
|
|
128
|
+
...(extra?.prefixHash ? { prefixHash: extra.prefixHash } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
this.emit();
|
|
132
|
+
},
|
|
133
|
+
record: (
|
|
134
|
+
usage: LLMUsage,
|
|
135
|
+
messagesRef?: ContextRecord[],
|
|
136
|
+
model?: string,
|
|
137
|
+
extra?: { prefixHash?: string; charges?: import('./generation.ts').Charge[] },
|
|
138
|
+
) => {
|
|
139
|
+
if (closed) return;
|
|
140
|
+
const s = entry.stats;
|
|
141
|
+
s.calls++;
|
|
142
|
+
s.promptTokens += usage.promptTokens;
|
|
143
|
+
s.completionTokens += usage.completionTokens;
|
|
144
|
+
s.cacheHitTokens += usage.cacheHitTokens;
|
|
145
|
+
s.cacheMissTokens += usage.cacheMissTokens;
|
|
146
|
+
s.reasoningTokens += usage.reasoningTokens ?? 0;
|
|
147
|
+
if (messagesRef) entry.messagesRef = () => messagesRef;
|
|
148
|
+
if (this.onRecord) {
|
|
149
|
+
try {
|
|
150
|
+
this.onRecord({
|
|
151
|
+
ts: nowIso(this.timezone),
|
|
152
|
+
sessionId: id,
|
|
153
|
+
role: s.role,
|
|
154
|
+
label: s.label,
|
|
155
|
+
model: model ?? '',
|
|
156
|
+
promptTokens: usage.promptTokens,
|
|
157
|
+
completionTokens: usage.completionTokens,
|
|
158
|
+
cacheHitTokens: usage.cacheHitTokens,
|
|
159
|
+
cacheMissTokens: usage.cacheMissTokens,
|
|
160
|
+
reasoningTokens: usage.reasoningTokens ?? 0,
|
|
161
|
+
...(extra?.prefixHash ? { prefixHash: extra.prefixHash } : {}),
|
|
162
|
+
...(extra?.charges ? { charges: structuredClone(extra.charges) } : {}),
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// 持久化观察失败不改变session本身的运行状态。
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
this.emit();
|
|
169
|
+
},
|
|
170
|
+
close: () => {
|
|
171
|
+
if (closed) return;
|
|
172
|
+
closed = true;
|
|
173
|
+
entry.stats.endedAt = nowIso(this.timezone);
|
|
174
|
+
this.pruneClosed();
|
|
175
|
+
this.emit();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 全部session统计(进行中在前,新的在前) */
|
|
181
|
+
list(): SessionStats[] {
|
|
182
|
+
const out: SessionStats[] = [];
|
|
183
|
+
for (const e of this.entries.values()) {
|
|
184
|
+
const s = e.stats;
|
|
185
|
+
const denom = s.cacheHitTokens + s.cacheMissTokens;
|
|
186
|
+
out.push({
|
|
187
|
+
...s,
|
|
188
|
+
cacheHitRate: denom > 0 ? s.cacheHitTokens / denom : null,
|
|
189
|
+
messageCount: this.countMessages(e),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return out.sort((a, b) => {
|
|
193
|
+
const ra = a.endedAt === null ? 0 : 1;
|
|
194
|
+
const rb = b.endedAt === null ? 0 : 1;
|
|
195
|
+
if (ra !== rb) return ra - rb;
|
|
196
|
+
return b.startedAt.localeCompare(a.startedAt);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 统计清零(web运维动作):进行中的session保留条目但usage归零、
|
|
202
|
+
* 计时重置(句柄继续有效);已结束的条目移除。
|
|
203
|
+
*/
|
|
204
|
+
reset(): void {
|
|
205
|
+
for (const [id, e] of [...this.entries]) {
|
|
206
|
+
if (e.stats.endedAt !== null) {
|
|
207
|
+
this.entries.delete(id);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
e.stats.calls = 0;
|
|
211
|
+
e.stats.promptTokens = 0;
|
|
212
|
+
e.stats.completionTokens = 0;
|
|
213
|
+
e.stats.cacheHitTokens = 0;
|
|
214
|
+
e.stats.cacheMissTokens = 0;
|
|
215
|
+
e.stats.reasoningTokens = 0;
|
|
216
|
+
e.stats.startedAt = nowIso(this.timezone);
|
|
217
|
+
}
|
|
218
|
+
this.emit();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 某session当前消息流(引用实时序列化);未知id或无引用→null */
|
|
222
|
+
messages(id: string): readonly ContextRecord[] | null {
|
|
223
|
+
const e = this.entries.get(id);
|
|
224
|
+
if (!e?.messagesRef) return null;
|
|
225
|
+
try {
|
|
226
|
+
return e.messagesRef();
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
private countMessages(e: Entry): number {
|
|
234
|
+
if (!e.messagesRef) return 0;
|
|
235
|
+
try {
|
|
236
|
+
return e.messagesRef().length;
|
|
237
|
+
} catch {
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 已结束session只留最近CLOSED_KEEP个(按结束时间;常驻条目永不清) */
|
|
243
|
+
private pruneClosed(): void {
|
|
244
|
+
const closed = [...this.entries.values()]
|
|
245
|
+
.filter((e) => e.stats.endedAt !== null && !e.pinned)
|
|
246
|
+
.sort((a, b) => (b.stats.endedAt ?? '').localeCompare(a.stats.endedAt ?? ''));
|
|
247
|
+
for (const e of closed.slice(CLOSED_KEEP)) {
|
|
248
|
+
this.entries.delete(e.stats.id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private emit(): void {
|
|
253
|
+
for (const cb of this.listeners) {
|
|
254
|
+
try {
|
|
255
|
+
cb();
|
|
256
|
+
} catch {
|
|
257
|
+
/* 观察者异常不影响主流程 */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|