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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
@@ -0,0 +1,699 @@
1
+ /** Composition root for framework services and the `Persona`/`CoreApi` boundary. */
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import type {
5
+ BlobInput,
6
+ BlobRef,
7
+ CognitionHost,
8
+ CognitionRequest,
9
+ CognitionResult,
10
+ EventEnvelope,
11
+ EventOrigin,
12
+ CoreConfig,
13
+ ForkOptions,
14
+ CoreApi,
15
+ World,
16
+ WorldHost,
17
+ LLMProviderEntry,
18
+ LLMUsage,
19
+ Logger,
20
+ ModelFacts,
21
+ ModelSpec,
22
+ Persona,
23
+ PushOptions,
24
+ SessionDecl,
25
+ SessionInfo,
26
+ } from './types.ts';
27
+ import type { LoadedConfig } from './config.ts';
28
+ import { Runlog, estimateMessagesTokens, nowIso, withDeadline } from './util.ts';
29
+ import { JsonlEventStore } from './event-store.ts';
30
+ import { WakeBus } from './bus.ts';
31
+ import { SessionLog } from './session.ts';
32
+ import { CoreState } from './state.ts';
33
+ import { ProviderRegistry } from '../providers/registry.ts';
34
+ import { runForkLoop } from './fork.ts';
35
+ import { providerModule } from '../providers/registry.ts';
36
+ import { LogBlobStore, blobScheme, mimeOfHandle, withBlobLines } from './blobs.ts';
37
+ import { TimerStore } from './timers.ts';
38
+ import { SessionTracker, type SessionHandle } from './sessions.ts';
39
+ import { UsageLog } from './usage-log.ts';
40
+ import { ToolCallLog } from './tool-log.ts';
41
+ import { Transcript } from './transcript.ts';
42
+ import { openRun, writeRunJson, type RunInfo } from './run.ts';
43
+ import { currentAnchors } from './log-context.ts';
44
+ import { MainLoop, type ContextFacts } from './loop.ts';
45
+ import type { ResponseClient } from './generation.ts';
46
+
47
+ /** 单个 World 的停止期限;失败或超时写入结果,其他停止操作继续。 */
48
+ const MODULE_STOP_MS = 20_000;
49
+ /** 主循环收到 abort 后用于落完已外化片段的期限。 */
50
+ const LOOP_DRAIN_MS = 1_000;
51
+ /**
52
+ * 隐藏 World 仍推送事件时,首条及每隔此周期报告一次。可见性开关跨重启保留。
53
+ */
54
+ const HIDDEN_PUSH_NOTE_GAP_MS = 10 * 60_000;
55
+
56
+ export interface CoreDeps {
57
+ persona: Persona;
58
+ worlds: World[];
59
+ llm?: ResponseClient;
60
+ }
61
+
62
+ export interface WorldStopFailure {
63
+ worldId: string;
64
+ detail: string;
65
+ }
66
+
67
+ export class Core<C extends CoreConfig = CoreConfig> {
68
+ readonly loaded: LoadedConfig<C>;
69
+ /** 本次进程运行的标识与日志目录。 */
70
+ readonly run: RunInfo;
71
+ readonly runlog: Runlog;
72
+ readonly transcript: Transcript;
73
+ readonly store: JsonlEventStore;
74
+ readonly bus: WakeBus;
75
+ readonly session: SessionLog;
76
+ readonly state: CoreState;
77
+ readonly timers: TimerStore;
78
+ readonly loop: MainLoop;
79
+ readonly llm: ResponseClient;
80
+ /** 媒体库:字节保存在 data/media/,回执与事件只存引用。 */
81
+ readonly logBlobs: LogBlobStore;
82
+ /** 各agent session的观察注册表(web面板数据源) */
83
+ readonly sessions: SessionTracker;
84
+ /** 每次LLM调用的持久化流水(跨重启;用量·成本页数据源) */
85
+ readonly usageLog: UsageLog;
86
+ /** 主循环每次模型工具调用的持久化流水(工具名/原始参数/耗时/回执) */
87
+ readonly toolLog: ToolCallLog;
88
+ /** Persona注册的 session 声明;core 只按 id 查表,不对值分支 */
89
+ readonly sessionDecls = new Map<string, SessionDecl>();
90
+ /** 挂载表。与装配层共用同一个数组:运行中挂载/卸载就地增删。 */
91
+ private worlds: World[];
92
+ private persona: Persona;
93
+ private log: Logger;
94
+ private runPromise: Promise<void> | null = null;
95
+ /** start() 已跑过 World 启动循环;之后挂载的 World 立即 start,之前的等 start() 统一起。 */
96
+ private started = false;
97
+ /** 每个声明当前运行的 fork 实例数,用于并发记账。 */
98
+ private readonly forkRunning = new Map<string, number>();
99
+ /** 各 World 在途的认知请求数;并发限制由 Persona 决定。 */
100
+ private readonly cognitionRunning = new Map<string, number>();
101
+ /** 接收事件的主 session 声明。 */
102
+ private readonly mainDecl: SessionDecl;
103
+ /** World 自愿上报用量的常驻仪表条目(每 World 一条) */
104
+ private readonly moduleUsageTracks = new Map<string, SessionHandle>();
105
+ /** 隐藏 World 照常落库的事件计数与上次回报时刻(每 World 一条) */
106
+ private readonly hiddenPushes = new Map<string, { count: number; notedAtMs: number }>();
107
+ readonly providers: ProviderRegistry;
108
+ private readonly moduleHostLeases = new Map<World, { active: boolean }>();
109
+
110
+ constructor(loaded: LoadedConfig<C>, deps: CoreDeps) {
111
+ this.loaded = loaded;
112
+ const cfg = loaded.config;
113
+ const dataDir = loaded.dataDir;
114
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
115
+
116
+ this.run = openRun(dataDir, { timezone: cfg.timezone, bot: cfg.displayName, repoRoot: loaded.repoRoot });
117
+ this.runlog = new Runlog(join(this.run.dir, 'log.jsonl'), {
118
+ run: this.run.id,
119
+ timezone: cfg.timezone,
120
+ levels: () => cfg.logging,
121
+ incidentsDir: join(this.run.dir, 'incidents'),
122
+ });
123
+ this.log = this.runlog.logger('core');
124
+ this.store = new JsonlEventStore({ dataDir, run: this.run.id, log: this.log.child('store') });
125
+ // WakeBus 持有只读配置引用,使 batching 热更新对后续 push 生效。
126
+ this.bus = new WakeBus(cfg.batching, this.log.child('bus'));
127
+ this.session = new SessionLog(dataDir, 'session-main.jsonl', () => nowIso(cfg.timezone));
128
+ this.transcript = new Transcript(join(this.run.dir, 'transcript.jsonl'), { run: this.run.id, timezone: cfg.timezone });
129
+ this.session.onAppend((record, index) => this.transcript.item(record, index));
130
+ this.state = new CoreState(dataDir);
131
+ // 先 load 再 attach:load 会整体替换状态对象,Persona首次调用 personaState() 时必须拿到已加载的状态。
132
+ this.state.load();
133
+ this.worlds = deps.worlds;
134
+ this.persona = deps.persona;
135
+ this.logBlobs = new LogBlobStore(dataDir);
136
+ this.providers = new ProviderRegistry(() => cfg.providers, {
137
+ // 同一部署根下的部署共享端点配置和状态目录。
138
+ stateRoot: loaded.providersDir ?? join(loaded.rootDir, 'providers'),
139
+ repoRoot: loaded.repoRoot ?? process.cwd(),
140
+ readBlob: (handle) => {
141
+ const bytes = this.resolveBlob(handle)?.bytes;
142
+ return bytes ? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength) : null;
143
+ },
144
+ keepThinking: () => cfg.context.keepPastThinking,
145
+ log: this.log.child('llm'),
146
+ });
147
+ this.llm = deps.llm ?? this.makeProviderRouter();
148
+
149
+ // TimerStore 仅提供通用持久定时器;Persona定义心跳与闹钟语义。
150
+ this.timers = new TimerStore(dataDir, this.log.child('timer'));
151
+
152
+ this.usageLog = new UsageLog(join(dataDir, 'usage.jsonl'), message => this.log.error(message));
153
+ this.toolLog = new ToolCallLog(join(this.run.dir, 'toolcalls.jsonl'), { timezone: cfg.timezone, run: this.run.id });
154
+ this.sessions = new SessionTracker(cfg.timezone, (rec) => {
155
+ const { round } = currentAnchors();
156
+ this.usageLog.append({ ...rec, run: this.run.id, ...(round !== undefined ? { round } : {}) });
157
+ });
158
+
159
+ // attach 必须先于 declareSessions,后者会将工具 handler 绑定到 CoreApi。
160
+ this.persona.attach(this.makeApi());
161
+ const decls = this.persona.declareSessions();
162
+ for (const decl of decls) this.sessionDecls.set(decl.id, decl);
163
+ const main = decls.filter((d) => d.receivesEvents && d.persistent);
164
+ if (main.length !== 1) {
165
+ throw new Error(
166
+ `Persona必须恰好声明一个接收事件投递的常驻session,当前${main.length}个`,
167
+ );
168
+ }
169
+
170
+ this.mainDecl = main[0];
171
+ this.loop = new MainLoop({
172
+ cfg,
173
+
174
+ llm: this.llm,
175
+ persona: deps.persona,
176
+ decl: main[0],
177
+ spec: () => this.activeSpec(),
178
+ context: this.contextFacts(),
179
+ blobs: { intern: (inputs) => this.internBlobs(inputs) },
180
+ worlds: {
181
+ all: () => this.worlds,
182
+ visible: () => this.worlds.filter((m) => this.isWorldVisible(m.id)),
183
+ },
184
+ dirs: { packageDir: loaded.packageDir ?? loaded.rootDir, deploymentDir: loaded.rootDir },
185
+ bus: this.bus,
186
+ session: this.session,
187
+ store: this.store,
188
+ state: this.state,
189
+ log: this.log.child('loop'),
190
+ tracker: this.sessions,
191
+ toolLog: this.toolLog,
192
+ transcript: this.transcript,
193
+ toolOwner: (name) => this.worlds.find((m) => m.tools().some((t) => t.name === name))?.id,
194
+ });
195
+ this.bus.setPreemptHandler(() => {
196
+ this.loop.abortCurrentRound();
197
+ });
198
+ }
199
+
200
+ private makeApi(): CoreApi {
201
+ return {
202
+ injectInternal: (text, kind) => this.loop.injectInternal(text, kind),
203
+ injectDeferred: (kind, render) => this.loop.injectDeferred(kind, render),
204
+ injectExternal: (text, kind) => this.loop.injectExternal(text, kind),
205
+ requestContextHandoff: () => this.loop.requestContextHandoff(),
206
+ spawnFork: (opts) => this.spawnFork(opts),
207
+ sessionInfo: (id) => this.sessionInfo(id),
208
+ llm: this.llm,
209
+ timers: this.timers,
210
+ deliveryGate: {
211
+ set: (gate) => this.bus.setDeliveryGate(gate),
212
+ clear: (id, deliverQueued) => this.bus.clearDeliveryGate(id, deliverQueued),
213
+ isBlocked: () => this.bus.isDeliveryBlocked(),
214
+ },
215
+ personaState: () => this.state.data.persona,
216
+ savePersonaState: () => this.state.save(),
217
+ toolsTagged: (tag) =>
218
+ new Set(this.loop.getToolSchemas().filter((t) => t.tags.includes(tag)).map((t) => t.name)),
219
+ blob: (handle) => this.resolveBlob(handle),
220
+ log: this.log.child('persona'),
221
+ };
222
+ }
223
+
224
+ /**
225
+ * 按 scheme 解析句柄:`log:` 查日志附件库,`mem:` 问Persona的记忆。
226
+ * 认不出或不在 → null。
227
+ */
228
+ resolveBlob(handle: string): { bytes: Uint8Array; mime: string } | null {
229
+ const scheme = blobScheme(handle);
230
+ if (scheme === 'log') return this.logBlobs.read(handle);
231
+ if (scheme === 'mem') return this.persona.blobs.get(handle);
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * 新附件字节写入日志附件库并转换为 log: 句柄;已有句柄补充 mime 和名称。
237
+ * 无附件时返回 undefined,避免保存空数组。
238
+ */
239
+ internBlobs(inputs: readonly (BlobInput | BlobRef)[] | undefined): BlobRef[] | undefined {
240
+ if (!inputs || inputs.length === 0) return undefined;
241
+ return inputs.map((input) => {
242
+ if ('bytes' in input) {
243
+ const handle = this.logBlobs.put(input.bytes, input.mime);
244
+ return { handle, mime: input.mime, ...(input.name ? { name: input.name } : {}), fallbackText: input.fallbackText };
245
+ }
246
+ const known = 'mime' in input ? input : null;
247
+ const resolved = known ? null : this.resolveBlob(input.handle);
248
+ const mime = known?.mime ?? resolved?.mime ?? mimeOfHandle(input.handle);
249
+ const name = known?.name ?? input.handle.slice(input.handle.lastIndexOf('/') + 1).replace(/^[a-z]+:/, '');
250
+ return { handle: input.handle, mime, name, fallbackText: input.fallbackText };
251
+ });
252
+ }
253
+
254
+ /** 丢弃已发生的事件与候选票据;历史保留,重启不再补投。 */
255
+ discardPendingEvents(): number {
256
+ const dropped = this.bus.drainPending((item) =>
257
+ item.event !== undefined || item.candidate !== undefined);
258
+ this.loop.acknowledgeDiscarded(
259
+ dropped.flatMap((item) => item.event ? [item.event] : []),
260
+ );
261
+ return dropped.length;
262
+ }
263
+
264
+ private sessionInfo(id: string): SessionInfo {
265
+ const decl = this.sessionDecls.get(id);
266
+ const isMainLoop = decl?.persistent === true && decl.receivesEvents;
267
+ const gauge = isMainLoop ? this.loop.contextGauge() : null;
268
+ return {
269
+ id,
270
+ running: this.forkRunning.get(id) ?? 0,
271
+ // 包含合成首轮对话,使继承该快照的 fork 使用相同的请求前缀。
272
+ snapshot: isMainLoop ? this.loop.outboundMessages() : null,
273
+ estTokens: gauge?.estTokens ?? null,
274
+ hardTokens: gauge?.hardTokens ?? null,
275
+ };
276
+ }
277
+
278
+ /**
279
+ * 生效上下文窗口:Provider 实例探到的上游自报值与档位手填值取小。两者都缺席时
280
+ * undefined——core 不猜窗口。
281
+ */
282
+ private contextWindowOf(spec: ModelSpec): number | undefined {
283
+ const { name } = this.activeProviderEntry();
284
+ const detected = this.providers.resolve(name).contextWindow?.(spec.model);
285
+ const manual = spec.contextWindow;
286
+ if (detected === undefined) return manual;
287
+ return manual === undefined ? detected : Math.min(detected, manual);
288
+ }
289
+
290
+ /** 按当前活跃端点读取模型上下文事实。 */
291
+ private contextFacts(): ContextFacts {
292
+ const module = () => providerModule(this.activeProviderEntry().entry.kind);
293
+ return {
294
+ hardTokens: () => {
295
+ const spec = this.activeSpec();
296
+ const window = this.contextWindowOf(spec);
297
+ return window === undefined ? null : Math.max(0, window - (spec.maxTokens ?? 0));
298
+ },
299
+ estimateTokens: (records) => module().estimateTokens?.(records, this.activeSpec()) ?? estimateMessagesTokens(records),
300
+ contextOverflow: (error) => module().contextOverflow?.(error) ?? false,
301
+ };
302
+ }
303
+
304
+ /**
305
+ * 创建临时 session;在创建时绑定当前活跃端点与模型,工具和轮数来自声明或调用参数。
306
+ * 记录并发数与用量;并发限制由 Persona 决定。
307
+ */
308
+ async spawnFork(opts: ForkOptions): Promise<string> {
309
+ const decl = this.sessionDecls.get(opts.id);
310
+ if (!decl) throw new Error(`未声明的session: ${opts.id}`);
311
+ let observedMessages = opts.messages;
312
+ const track = this.sessions.open(decl.id, decl.label, {
313
+ messagesRef: () => observedMessages,
314
+ });
315
+ this.forkRunning.set(decl.id, (this.forkRunning.get(decl.id) ?? 0) + 1);
316
+ try {
317
+ return await runForkLoop({
318
+ id: decl.id,
319
+ llm: this.llm.bind?.() ?? this.llm,
320
+ spec: this.activeSpec(),
321
+ messages: opts.messages,
322
+ tools: opts.tools ?? decl.tools(),
323
+ maxRounds: decl.rounds().hard,
324
+ softRounds: decl.rounds().soft,
325
+ log: this.log.child(`fork.${decl.id}`),
326
+ stopWhen: opts.stopWhen,
327
+ wrapUpHint: opts.wrapUpHint,
328
+ capNote: opts.capNote,
329
+ nudge: opts.nudge,
330
+ track,
331
+ observeMessages: (messages) => {
332
+ observedMessages = messages;
333
+ },
334
+ });
335
+ } finally {
336
+ this.forkRunning.set(decl.id, Math.max(0, (this.forkRunning.get(decl.id) ?? 1) - 1));
337
+ track.close();
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Persona 提供且启用 cognition 时,WorldHost 才提供该接口。
343
+ * 请求只能列出该 World 的工具;不合法的请求返回错误,不调用 Persona。
344
+ * Core 记录在途数供 Persona 决定并发策略。经 spawnFork 产生的用量在 fork 中记录,此处不重复计量。
345
+ */
346
+ private makeCognition(mod: World, active: () => boolean): CognitionHost | undefined {
347
+ const impl = this.persona.cognition;
348
+ if (!impl) return undefined;
349
+ if (impl.enabled && !impl.enabled()) return undefined;
350
+ const log = this.runlog.logger(`worlds.${mod.id}`);
351
+ return {
352
+ request: async (req: CognitionRequest): Promise<CognitionResult> => {
353
+ if (!active()) return { error: '宿主生命周期已结束' };
354
+ const brief = typeof req?.brief === 'string' ? req.brief.trim() : '';
355
+ if (!brief) return { error: '认知请求没有 brief:要想的是什么,得由 World 自己说清楚' };
356
+ const own = new Set(mod.tools().map((t) => t.name));
357
+ const named = req.tools ?? [];
358
+ const outsiders = named.filter((name) => !own.has(name));
359
+ if (outsiders.length > 0) {
360
+ log.warn('认知请求越权点名工具,已驳回', { tools: outsiders });
361
+ return {
362
+ error:
363
+ `认知请求只能点名本 World 自己的工具,这些不是: ${outsiders.join(' / ')}` +
364
+ `(本 World 现有: ${[...own].join(' / ') || '(无)'})`,
365
+ };
366
+ }
367
+ const tools = named.map((name) => mod.tools().find((t) => t.name === name)!);
368
+ const running = (this.cognitionRunning.get(mod.id) ?? 0) + 1;
369
+ this.cognitionRunning.set(mod.id, running);
370
+ try {
371
+ return await impl.request({ ...req, brief }, { worldId: mod.id, tools, running });
372
+ } catch (e) {
373
+ // Persona 异常转换为请求错误,返回 World。
374
+ log.warn('认知请求受理失败', { err: e });
375
+ return { error: e instanceof Error ? e.message : String(e) };
376
+ } finally {
377
+ this.cognitionRunning.set(mod.id, Math.max(0, (this.cognitionRunning.get(mod.id) ?? 1) - 1));
378
+ }
379
+ },
380
+ };
381
+ }
382
+
383
+ /** 各 World 当下在途的认知外包请求数(控制台/诊断用;没有在途的 World 不出现) */
384
+ cognitionInFlight(): Record<string, number> {
385
+ const out: Record<string, number> = {};
386
+ for (const [id, n] of this.cognitionRunning) if (n > 0) out[id] = n;
387
+ return out;
388
+ }
389
+
390
+ get config() {
391
+ return this.loaded.config;
392
+ }
393
+
394
+
395
+ /**
396
+ * World 隐藏后继续运行,事件仅归档且重新显示时不补投。
397
+ * 事件投递立即停止;环境前缀与工具表在下一次前缀重建时一同更新。
398
+ */
399
+ setWorldVisible(id: string, visible: boolean): void {
400
+ if (!this.worlds.some((m) => m.id === id)) throw new Error(`未挂载的 World: ${id}`);
401
+ this.state.data.worldVisibility[id] = visible;
402
+ this.state.save();
403
+ this.log.warn(`World 对 agent ${visible ? '可见' : '隐藏'}: ${id}`);
404
+ }
405
+
406
+ isWorldVisible(id: string): boolean {
407
+ return this.state.data.worldVisibility[id] !== false;
408
+ }
409
+
410
+ /** 隐藏 World 的推送按 HIDDEN_PUSH_NOTE_GAP_MS 限频报告,计数保留在日志中。 */
411
+ private noteHiddenPush(id: string): void {
412
+ const note = this.hiddenPushes.get(id) ?? { count: 0, notedAtMs: 0 };
413
+ note.count += 1;
414
+ this.hiddenPushes.set(id, note);
415
+ const now = Date.now();
416
+ if (note.notedAtMs > 0 && now - note.notedAtMs < HIDDEN_PUSH_NOTE_GAP_MS) return;
417
+ note.notedAtMs = now;
418
+ this.log.warn(
419
+ `隐藏 World 的事件仅归档: ${id}(本轮第 ${note.count} 条)。需要投递时请开启该 World 的可见性`,
420
+ );
421
+ }
422
+
423
+ /** 全部已挂载 World 的可见性 + 前缀是否已经跟上 */
424
+ worldVisibility(): { visibility: Record<string, boolean>; driftedWorlds: string[] } {
425
+ const visibility: Record<string, boolean> = {};
426
+ for (const mod of this.worlds) visibility[mod.id] = this.isWorldVisible(mod.id);
427
+ return { visibility, driftedWorlds: this.loop.modulePrefixDrift() };
428
+ }
429
+
430
+ /** 用于控制台状态和启动信息的当前模型配置。 */
431
+ mainSessionSpec(): ModelSpec {
432
+ return this.activeSpec();
433
+ }
434
+
435
+ /** 按 activeProvider 读取模型配置;端点未配置模型时抛错。 */
436
+ activeSpec(): ModelSpec {
437
+ const { name, entry } = this.activeProviderEntry();
438
+ if (!entry.spec) {
439
+ throw new Error(`LLM provider ${name} 还没选模型(去控制台该 Provider 的「实例与模型」面板填模型名并保存)`);
440
+ }
441
+ return entry.spec;
442
+ }
443
+
444
+ activeProviderEntry(): { name: string; entry: LLMProviderEntry } {
445
+ const cfg = this.loaded.config;
446
+ const name = cfg.activeProvider;
447
+ const entry = cfg.providers?.[name];
448
+ if (!entry) {
449
+ const known = Object.keys(cfg.providers ?? {}).join(' / ') || '(空)';
450
+ throw new Error(`没有这个 LLM provider: ${name}(config.json providers 段现有: ${known})`);
451
+ }
452
+ return { name, entry };
453
+ }
454
+
455
+ /** The selected instance is resolved once at the start of each request. */
456
+ private makeProviderRouter(): ResponseClient {
457
+ const bind = (): ResponseClient => this.providers.bind(this.activeProviderEntry().name);
458
+ return { bind, respond: async (request, options) => bind().respond(request, options) };
459
+ }
460
+
461
+ /** 模型事实(按当前活跃端点实时读,不做启动期快照) */
462
+ private modelFacts(): ModelFacts {
463
+ const spec = (): ModelSpec => this.activeSpec();
464
+ return {
465
+ model: () => spec().model,
466
+ accepts: (mime) => {
467
+ const {entry} = this.activeProviderEntry();
468
+ return providerModule(entry.kind).accepts?.(entry,spec(),mime) ?? (entry.multimodal === true && mime.startsWith('image/'));
469
+ },
470
+ contextWindow: () => this.contextWindowOf(spec()),
471
+ };
472
+ }
473
+
474
+ /** World 宿主接口;每个 World 使用以自身 id 命名的子 logger。 */
475
+ private makeHost(mod: World): WorldHost {
476
+ const previous = this.moduleHostLeases.get(mod);
477
+ if (previous) previous.active = false;
478
+ const lease = { active: true };
479
+ this.moduleHostLeases.set(mod, lease);
480
+ // getter 的 this 是 WorldHost;通过 self 访问 Core。
481
+ const self = this;
482
+ return {
483
+ // origin 由 World 指定,默认 external;决定后续投递方式。
484
+ pushEvent: async (
485
+ e: Omit<EventEnvelope, 'cursor' | 'origin' | 'contextDelivery'> & { origin?: EventOrigin },
486
+ opts?: PushOptions,
487
+ ): Promise<EventEnvelope> => {
488
+ if (!lease.active) throw new Error(`World ${mod.id} 的宿主生命周期已结束`);
489
+ const deliver = opts?.deliver !== false && this.isWorldVisible(mod.id);
490
+ const blobs = this.internBlobs(e.blobs);
491
+ const envelope = this.store.append({
492
+ ...e,
493
+ text: withBlobLines(e.text, blobs),
494
+ ...(blobs ? { blobs } : {}),
495
+ origin: e.origin ?? 'external',
496
+ contextDelivery: deliver ? 'deliver' : 'archive-only',
497
+ });
498
+ // 隐藏 World 的事件仍归档,不投递。
499
+ if (deliver) {
500
+ this.bus.push({ event: envelope }, { trigger: opts?.trigger });
501
+ } else {
502
+ // 仅归档事件不会经过候选处理,必须在此标记已处理以推进投递水位。
503
+ this.loop.acknowledgeDiscarded([envelope]);
504
+ if (opts?.deliver !== false) this.noteHiddenPush(mod.id);
505
+ }
506
+ return envelope;
507
+ },
508
+ pushDeferred: (e, opts) => {
509
+ if (!lease.active) return;
510
+ // 隐藏 World 的延迟渲染项没有正文,直接丢弃。
511
+ if (!this.isWorldVisible(mod.id)) return;
512
+ this.bus.push(
513
+ { deferred: { ...e, source: mod.id, origin: e.origin ?? 'external' } },
514
+ { trigger: opts?.trigger },
515
+ );
516
+ },
517
+ pushCandidate: async (spec, opts) => {
518
+ if (!lease.active) throw new Error(`World ${mod.id} 的宿主生命周期已结束`);
519
+ if (spec.sourceEvents.length === 0) throw new Error('候选票据至少需要一条原始事件');
520
+ const origin = spec.origin ?? 'external';
521
+ const sourceEvents = spec.sourceEvents.map((event) => this.store.append({
522
+ ...event,
523
+ source: mod.id,
524
+ origin,
525
+ contextDelivery: 'archive-only',
526
+ }));
527
+ if (this.isWorldVisible(mod.id)) {
528
+ this.bus.push({
529
+ candidate: {
530
+ source: mod.id,
531
+ origin,
532
+ sourceEvents,
533
+ gateText: spec.gateText,
534
+ value: spec.value,
535
+ project: spec.project,
536
+ },
537
+ }, { trigger: opts?.trigger });
538
+ } else {
539
+ // 隐藏的候选不再生成投递内容,立即标记归档事件已处理。
540
+ this.loop.acknowledgeDiscarded(sourceEvents);
541
+ }
542
+ return sourceEvents;
543
+ },
544
+ store: this.store,
545
+ drainPendingEvents: async (filter) => {
546
+ if (!lease.active) return [];
547
+ const taken = this.bus
548
+ .drainPending((it) => it.event?.origin === 'external' && filter(it.event))
549
+ .map((it) => it.event as EventEnvelope);
550
+ // 被消费的事件须标记已处理,避免投递水位停留在此并在重启后补投。
551
+ if (taken.length > 0) this.loop.acknowledgeDiscarded(taken);
552
+ return taken;
553
+ },
554
+ modelFacts: this.modelFacts(),
555
+ blob: (handle) => this.resolveBlob(handle),
556
+ reportUsage: (usage, opts) => {
557
+ if (lease.active) this.reportWorldUsage(mod.id, usage, opts);
558
+ },
559
+ llmStalls: async (withinMs) => this.loop.llmStalls(withinMs),
560
+ // 每次访问重新检查 Persona 开关,关闭后 getter 立即返回 undefined。
561
+ get cognition(): CognitionHost | undefined {
562
+ return lease.active ? self.makeCognition(mod, () => lease.active) : undefined;
563
+ },
564
+ log: this.runlog.logger(`worlds.${mod.id}`),
565
+ };
566
+ }
567
+
568
+ /**
569
+ * World 上报的用量按 `worlds.<World>` 仪表项计入 session 统计和 usage.jsonl。
570
+ * 用量标签不携带模型用途语义。
571
+ */
572
+ private reportWorldUsage(
573
+ worldId: string,
574
+ usage: LLMUsage,
575
+ opts?: { model?: string; label?: string; charges?: import('./generation.ts').Charge[] },
576
+ ): void {
577
+ const id = `worlds.${worldId}`;
578
+ let track = this.moduleUsageTracks.get(id);
579
+ if (!track) {
580
+ track = this.sessions.open(id, opts?.label ?? `${worldId}World 自带模型`, { id });
581
+ this.moduleUsageTracks.set(id, track);
582
+ }
583
+ track.record(usage, undefined, opts?.model, { charges: opts?.charges });
584
+ }
585
+
586
+ async start(): Promise<void> {
587
+ const dataDir = this.loaded.dataDir;
588
+ if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
589
+ // 构造期已加载状态;此处再次 load 会覆盖装配后、启动前的修改。
590
+ this.session.load();
591
+ for (const mod of this.worlds) {
592
+ try {
593
+ await mod.start(this.makeHost(mod));
594
+ } catch (error) {
595
+ const lease = this.moduleHostLeases.get(mod);
596
+ if (lease) lease.active = false;
597
+ throw error;
598
+ }
599
+ this.log.info(`World 已启动: ${mod.id}`);
600
+ }
601
+ this.started = true;
602
+ if (this.worlds.length === 0) this.log.warn('没有挂载任何 World:agent 收不到外部事件');
603
+ this.timers.start();
604
+ this.runPromise = this.loop.run().catch((e) => {
605
+ this.timers.stop();
606
+ this.log.error('主循环异常退出', { err: e });
607
+ });
608
+ writeRunJson(this.run, {
609
+ bot: this.loaded.config.displayName,
610
+ worlds: this.worlds.map((m) => m.id),
611
+ activeProvider: this.loaded.config.activeProvider,
612
+ });
613
+ this.log.emit('info', 'core已启动', { event: 'started', data: { run: this.run.id } });
614
+ }
615
+
616
+ /** 先终止主循环,再并发停止 World;各步骤有独立期限,失败写入返回结果。 */
617
+ /**
618
+ * 运行中挂载一个 World:入表,core 已启动则立即 start,并重建 system 前缀
619
+ * (环境提示词段与工具表同属缓存前缀,挂载必须连带换掉)。start 抛错时不入表。
620
+ */
621
+ async mountWorld(mod: World): Promise<void> {
622
+ if (this.worlds.some((m) => m.id === mod.id)) throw new Error(`World 已挂载: ${mod.id}`);
623
+ if (this.started) {
624
+ try {
625
+ await mod.start(this.makeHost(mod));
626
+ } catch (error) {
627
+ const lease = this.moduleHostLeases.get(mod);
628
+ if (lease) lease.active = false;
629
+ throw error;
630
+ }
631
+ this.log.info(`World 已启动: ${mod.id}`);
632
+ }
633
+ this.worlds.push(mod);
634
+ if (this.started) await this.loop.reloadSystemPrefix();
635
+ }
636
+
637
+ /** 停止 World 后使租约失效、移出挂载表并重建前缀;停止失败或超时仍继续卸载。 */
638
+ async unmountWorld(id: string): Promise<WorldStopFailure | null> {
639
+ const index = this.worlds.findIndex((m) => m.id === id);
640
+ if (index < 0) throw new Error(`未挂载的 World: ${id}`);
641
+ const mod = this.worlds[index];
642
+ let failure: WorldStopFailure | null = null;
643
+ if (this.started) {
644
+ try {
645
+ await withDeadline(mod.stop(), MODULE_STOP_MS);
646
+ } catch (e) {
647
+ const detail = e instanceof Error ? e.message : String(e);
648
+ this.log.warn(`World 停止失败: ${mod.id}`, { err: detail });
649
+ failure = { worldId: mod.id, detail };
650
+ }
651
+ }
652
+ const lease = this.moduleHostLeases.get(mod);
653
+ if (lease) lease.active = false;
654
+ this.worlds.splice(this.worlds.indexOf(mod), 1);
655
+ this.log.info(`World 已卸载: ${mod.id}`);
656
+ if (this.started) await this.loop.reloadSystemPrefix();
657
+ return failure;
658
+ }
659
+
660
+ async stop(): Promise<WorldStopFailure[]> {
661
+ this.started = false;
662
+ this.loop.stop();
663
+ this.timers.stop();
664
+ const failures: WorldStopFailure[] = [];
665
+ if (this.runPromise) {
666
+ try {
667
+ await withDeadline(this.runPromise, LOOP_DRAIN_MS, '主循环终止');
668
+ } catch (e) {
669
+ const detail = e instanceof Error ? e.message : String(e);
670
+ this.log.warn('主循环未在关机期限内结束', { err: detail });
671
+ failures.push({ worldId: 'core.loop', detail });
672
+ } finally {
673
+ this.loop.seal();
674
+ this.runPromise = null;
675
+ }
676
+ } else {
677
+ this.loop.seal();
678
+ }
679
+ const moduleFailures = (await Promise.all(this.worlds.map(async (mod): Promise<WorldStopFailure | null> => {
680
+ try {
681
+ await withDeadline(mod.stop(), MODULE_STOP_MS);
682
+ return null;
683
+ } catch (e) {
684
+ const detail = e instanceof Error ? e.message : String(e);
685
+ this.log.warn(`World 停止失败: ${mod.id}`, { err: detail });
686
+ return { worldId: mod.id, detail };
687
+ } finally {
688
+ const lease = this.moduleHostLeases.get(mod);
689
+ if (lease) lease.active = false;
690
+ }
691
+ }))).filter((failure): failure is WorldStopFailure => failure !== null);
692
+ failures.push(...moduleFailures);
693
+ this.usageLog.flush();
694
+ const ledger = this.usageLog.status();
695
+ if (ledger.pending) failures.push({ worldId: 'core.usage', detail: `${ledger.pending} usage records remain unwritten: ${ledger.error}` });
696
+ this.log.info('core已停止');
697
+ return failures;
698
+ }
699
+ }