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,104 @@
1
+ /** Core 机械状态,持久化到 `<dataDir>/core-state.json`。 */
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ export interface CoreStateData {
6
+ /** 上次截断时刻ISO */
7
+ lastTruncateAt: string | null;
8
+ /**
9
+ * 投递水位:该游标及之前的事件均已投递或已了结。
10
+ * 总线不持久化;重启仅补投水位之后需要投递的外部事件。
11
+ */
12
+ lastDeliveredCursor: number;
13
+ /**
14
+ * LLM 连续失败状态,时间单位为毫秒。since 为起点,0 表示没有连续失败;
15
+ * at 保存窗口内的失败时刻,重启后首次成功仍可报告。
16
+ */
17
+ llmStall: { since: number; at: number[] };
18
+ /** Persona所有的不透明状态;core 仅负责原子持久化。 */
19
+ persona: Record<string, unknown>;
20
+ /** World 可见性默认开启并跨重启保留;隐藏不改变挂载或运行状态。 */
21
+ worldVisibility: Record<string, boolean>;
22
+ }
23
+
24
+ const DEFAULTS: CoreStateData = {
25
+ lastTruncateAt: null,
26
+ lastDeliveredCursor: 0,
27
+ llmStall: { since: 0, at: [] },
28
+ persona: {},
29
+ worldVisibility: {},
30
+ };
31
+
32
+ function freshDefaults(): CoreStateData {
33
+ return { ...DEFAULTS, llmStall: { since: 0, at: [] }, persona: {}, worldVisibility: {} };
34
+ }
35
+
36
+ function readStall(raw: unknown): { since: number; at: number[] } {
37
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { since: 0, at: [] };
38
+ const r = raw as { since?: unknown; at?: unknown };
39
+ return {
40
+ since: typeof r.since === 'number' && Number.isFinite(r.since) ? r.since : 0,
41
+ at: Array.isArray(r.at)
42
+ ? r.at.filter((t): t is number => typeof t === 'number' && Number.isFinite(t))
43
+ : [],
44
+ };
45
+ }
46
+
47
+ export class CoreState {
48
+ private file: string;
49
+ // 各实例需独立的 Persona 状态对象,不能共享 DEFAULTS 中的引用。
50
+ data: CoreStateData = freshDefaults();
51
+
52
+ constructor(dataDir: string) {
53
+ this.file = join(dataDir, 'core-state.json');
54
+ }
55
+
56
+ load(): void {
57
+ this.data = freshDefaults();
58
+ if (!existsSync(this.file)) return;
59
+ try {
60
+ const raw = JSON.parse(readFileSync(this.file, 'utf8')) as Partial<CoreStateData>;
61
+ this.data = {
62
+ lastTruncateAt: raw.lastTruncateAt ?? DEFAULTS.lastTruncateAt,
63
+ lastDeliveredCursor: Number.isInteger(raw.lastDeliveredCursor)
64
+ ? (raw.lastDeliveredCursor as number)
65
+ : DEFAULTS.lastDeliveredCursor,
66
+ llmStall: readStall(raw.llmStall),
67
+ persona:
68
+ raw.persona && typeof raw.persona === 'object' && !Array.isArray(raw.persona)
69
+ ? (raw.persona as Record<string, unknown>)
70
+ : {},
71
+ worldVisibility:
72
+ raw.worldVisibility && typeof raw.worldVisibility === 'object' && !Array.isArray(raw.worldVisibility)
73
+ ? Object.fromEntries(
74
+ Object.entries(raw.worldVisibility as Record<string, unknown>).map(([k, v]) => [k, v !== false]),
75
+ )
76
+ : {},
77
+ };
78
+ } catch {
79
+ // 损坏则用默认值
80
+ }
81
+ }
82
+
83
+ save(): void {
84
+ const dir = dirname(this.file);
85
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
86
+ const tmp = this.file + '.tmp';
87
+ writeFileSync(tmp, JSON.stringify(this.data, null, 2), 'utf8');
88
+ // 通过 rename 原子覆盖状态文件,不先删除,避免崩溃后状态缺失。Windows 的 renameSync 使用 MoveFileExW 与 MOVEFILE_REPLACE_EXISTING 支持覆盖。
89
+ renameSync(tmp, this.file);
90
+ }
91
+
92
+ /**
93
+ * 清除人格状态与截断标记并落盘。投递水位和运维可见性跨重置保留,避免重放
94
+ * 已投递事件或改变 World 可见性。
95
+ */
96
+ clear(): void {
97
+ this.data = {
98
+ ...freshDefaults(),
99
+ lastDeliveredCursor: this.data.lastDeliveredCursor,
100
+ worldVisibility: this.data.worldVisibility,
101
+ };
102
+ this.save();
103
+ }
104
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 模板语法:
3
+ * {{name}}:已声明变量为空时展开为空串。
4
+ * {{name | 缺省文案}}:已声明变量为空时使用缺省文案。
5
+ * 未在值表声明的占位符原样保留。
6
+ */
7
+
8
+ /** `{{名字}}` 或 `{{名字 | 缺省文案}}`。名字限 `[\w.]`,缺省文案吃到 `}}` 为止。 */
9
+ const PLACEHOLDER = /\{\{\s*([\w.]+)\s*(?:\|([^}]*))?\}\}/g;
10
+
11
+ /** 模板里出现过的占位符名(按出现序,去重)。编辑器用它标"已用/未使用"。 */
12
+ export function templateVarNames(template: string): string[] {
13
+ const names: string[] = [];
14
+ const seen = new Set<string>();
15
+ for (const m of template.matchAll(PLACEHOLDER)) {
16
+ const name = m[1];
17
+ if (!seen.has(name)) { seen.add(name); names.push(name); }
18
+ }
19
+ return names;
20
+ }
21
+
22
+ /**
23
+ * 渲染模板,vars 中缺席的占位符原样保留,默认值与空串规则见文件头。
24
+ */
25
+ export function renderTemplate(template: string, vars: Readonly<Record<string, string>>): string {
26
+ return template.replace(PLACEHOLDER, (whole, name: string, fallback?: string) => {
27
+ if (!Object.prototype.hasOwnProperty.call(vars, name)) return whole;
28
+ const value = vars[name] ?? '';
29
+ if (value !== '') return value;
30
+ return fallback === undefined ? '' : fallback.trim();
31
+ });
32
+ }
33
+
34
+ /** 未在值表声明的占位符;控制台据此提示,仍允许保存。 */
35
+ export function unknownVarNames(
36
+ template: string,
37
+ declared: readonly string[],
38
+ ): string[] {
39
+ const known = new Set(declared);
40
+ return templateVarNames(template).filter((name) => !known.has(name));
41
+ }
42
+
43
+ /** 顶层模板切出来的一段:`name` 是那个占位符,`text` 是它连同前面的字面文本。 */
44
+ export interface RenderedSection {
45
+ name: string;
46
+ text: string;
47
+ }
48
+
49
+ /**
50
+ * 按占位符切分,满足 sections.map(s => s.text).join('') === text。
51
+ * 每段包含占位符前的字面文本,末尾文本归最后一段。
52
+ * 没有占位符时返回一段完整文本,name 为空串。
53
+ */
54
+ export function renderSections(
55
+ template: string,
56
+ vars: Readonly<Record<string, string>>,
57
+ ): { text: string; sections: RenderedSection[] } {
58
+ const sections: RenderedSection[] = [];
59
+ let cursor = 0;
60
+ for (const m of template.matchAll(PLACEHOLDER)) {
61
+ const start = m.index ?? 0;
62
+ sections.push({
63
+ name: m[1],
64
+ text: template.slice(cursor, start) + renderTemplate(m[0], vars),
65
+ });
66
+ cursor = start + m[0].length;
67
+ }
68
+ const tail = template.slice(cursor);
69
+ if (sections.length === 0) return { text: tail, sections: [{ name: '', text: tail }] };
70
+ if (tail) sections[sections.length - 1].text += tail;
71
+ return { text: sections.map((s) => s.text).join(''), sections };
72
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 通用持久定时器:到点回调持有方、跨重启恢复。载荷不透明——
3
+ * 闹钟的备注、阻断、关键词这些语义归持有它的Persona。
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import type { Logger, TimerEntry, TimersApi } from './types.ts';
8
+ import { nullLogger, shortId } from './util.ts';
9
+
10
+ const MAX_TIMEOUT = 2 ** 31 - 1; // setTimeout上限(~24.8天)
11
+
12
+ export class TimerStore implements TimersApi {
13
+ private file: string;
14
+ private log: Logger;
15
+ private entries: TimerEntry[] = [];
16
+ private timers = new Map<string, ReturnType<typeof setTimeout>>();
17
+ private started = false;
18
+ private dueHandler: ((entry: TimerEntry) => void) | null = null;
19
+
20
+ /** 构造时即读盘,attach 阶段就能 list();到期定时在 start() 才布防。 */
21
+ constructor(dataDir: string, log: Logger = nullLogger()) {
22
+ this.file = join(dataDir, 'timers.json');
23
+ this.log = log;
24
+ this.load();
25
+ }
26
+
27
+ onDue(handler: (entry: TimerEntry) => void): void {
28
+ this.dueHandler = handler;
29
+ }
30
+
31
+ start(): void {
32
+ if (this.started) return;
33
+ this.started = true;
34
+ for (const e of [...this.entries]) this.arm(e);
35
+ }
36
+
37
+ stop(): void {
38
+ this.started = false;
39
+ for (const t of this.timers.values()) clearTimeout(t);
40
+ this.timers.clear();
41
+ }
42
+
43
+ set(atIso: string, payload: Record<string, unknown> = {}): { ok: true; id: string } | { ok: false; error: string } {
44
+ if (Number.isNaN(Date.parse(atIso))) {
45
+ return { ok: false, error: `could not parse time "${atIso}"` };
46
+ }
47
+ const entry: TimerEntry = { id: shortId('wake_'), atIso, payload };
48
+ this.entries.push(entry);
49
+ this.save();
50
+ if (this.started) this.arm(entry);
51
+ return { ok: true, id: entry.id };
52
+ }
53
+
54
+ cancel(id: string): boolean {
55
+ const idx = this.entries.findIndex((e) => e.id === id);
56
+ if (idx < 0) return false;
57
+ this.entries.splice(idx, 1);
58
+ const t = this.timers.get(id);
59
+ if (t) clearTimeout(t);
60
+ this.timers.delete(id);
61
+ this.save();
62
+ return true;
63
+ }
64
+
65
+ list(): ReadonlyArray<TimerEntry> {
66
+ return this.entries;
67
+ }
68
+
69
+ clearAll(): number {
70
+ const n = this.entries.length;
71
+ this.entries = [];
72
+ for (const t of this.timers.values()) clearTimeout(t);
73
+ this.timers.clear();
74
+ this.save();
75
+ if (n > 0) this.log.warn('定时器已全部清除', { cleared: n });
76
+ return n;
77
+ }
78
+
79
+ private arm(entry: TimerEntry): void {
80
+ const delay = Date.parse(entry.atIso) - Date.now();
81
+ if (delay <= 0) {
82
+ this.fire(entry.id);
83
+ return;
84
+ }
85
+ const existing = this.timers.get(entry.id);
86
+ if (existing) clearTimeout(existing);
87
+ if (delay > MAX_TIMEOUT) {
88
+ // 超长延迟按 MAX_TIMEOUT 分段重新 arm。
89
+ this.timers.set(entry.id, setTimeout(() => this.arm(entry), MAX_TIMEOUT));
90
+ } else {
91
+ this.timers.set(entry.id, setTimeout(() => this.fire(entry.id), delay));
92
+ }
93
+ }
94
+
95
+ private fire(id: string): void {
96
+ if (!this.started) return;
97
+ const idx = this.entries.findIndex((e) => e.id === id);
98
+ if (idx < 0) return;
99
+ const entry = this.entries[idx];
100
+ this.entries.splice(idx, 1);
101
+ const t = this.timers.get(id);
102
+ if (t) clearTimeout(t);
103
+ this.timers.delete(id);
104
+ this.save();
105
+ if (this.dueHandler) this.dueHandler(entry);
106
+ else this.log.warn('定时器到期但无人认领', { id, atIso: entry.atIso });
107
+ }
108
+
109
+ private load(): void {
110
+ if (existsSync(this.file)) {
111
+ try {
112
+ const raw = JSON.parse(readFileSync(this.file, 'utf8')) as TimerEntry[];
113
+ this.entries = Array.isArray(raw) ? raw : [];
114
+ } catch {
115
+ this.log.warn('timers.json损坏,已重置为空');
116
+ this.entries = [];
117
+ }
118
+ }
119
+ }
120
+
121
+ private save(): void {
122
+ const dir = dirname(this.file);
123
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
124
+ const tmp = this.file + '.tmp';
125
+ writeFileSync(tmp, JSON.stringify(this.entries, null, 2), 'utf8');
126
+ if (existsSync(this.file)) rmSync(this.file);
127
+ renameSync(tmp, this.file);
128
+ }
129
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 模型工具调用写入 data/runs/<run>/toolcalls.jsonl,每次调用一行。
3
+ * args 保留模型原始参数;receipt 保存摘要,chars 记录全文长度。
4
+ * 完整回执可通过 call 在 transcript.jsonl 中查找。
5
+ */
6
+ import { appendFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import { currentAnchors } from './log-context.ts';
9
+ import { nowIso } from './util.ts';
10
+
11
+ /** 回执正文进流水的上限;全长另记在 chars,全文在 transcript 里 */
12
+ const RECEIPT_CHARS = 600;
13
+
14
+ export interface ToolCallInput {
15
+ /** 发起调用的 session 声明 id */
16
+ role: string;
17
+ tool: string;
18
+ /** 工具所属 World id;Persona 工具不填。 */
19
+ mod?: string;
20
+ /** 模型原始参数,未经 World 归一化。 */
21
+ args: Record<string, unknown> | null;
22
+ durMs: number;
23
+ /** 回执全长(字符);receipt 只留前 RECEIPT_CHARS 个 */
24
+ chars: number;
25
+ receipt: string;
26
+ /** handler 抛错或 ToolOutcome.failed 标记的失败。 */
27
+ failed?: true;
28
+ /** 回执带的媒体件数 */
29
+ blobs?: number;
30
+ }
31
+
32
+ export interface ToolCallEntry extends ToolCallInput {
33
+ seq: number;
34
+ ts: string;
35
+ run: string;
36
+ round?: number;
37
+ resp?: string;
38
+ /** session 里的 tool_call id,与 transcript 的 function_call / function_call_output 对上 */
39
+ call?: string;
40
+ }
41
+
42
+ export interface ToolCallLogOptions {
43
+ timezone?: string;
44
+ run?: string;
45
+ }
46
+
47
+ /** JSONL 追加;不给文件名就只计数(测试与未配目录的部署) */
48
+ export class ToolCallLog {
49
+ private seq = 0;
50
+ private dirReady = false;
51
+ private readonly timezone: string;
52
+ private readonly run: string;
53
+
54
+ constructor(private readonly file: string | null, opts: ToolCallLogOptions = {}) {
55
+ this.timezone = opts.timezone ?? 'Asia/Shanghai';
56
+ this.run = opts.run ?? 'r-none';
57
+ }
58
+
59
+ write(input: ToolCallInput): ToolCallEntry {
60
+ const anchors = currentAnchors();
61
+ const entry: ToolCallEntry = {
62
+ seq: ++this.seq,
63
+ ts: nowIso(this.timezone),
64
+ run: this.run,
65
+ ...(anchors.round !== undefined ? { round: anchors.round } : {}),
66
+ ...(anchors.resp !== undefined ? { resp: anchors.resp } : {}),
67
+ ...(anchors.call !== undefined ? { call: anchors.call } : {}),
68
+ ...input,
69
+ receipt: input.receipt.slice(0, RECEIPT_CHARS),
70
+ };
71
+ this.append(entry);
72
+ return entry;
73
+ }
74
+
75
+ /** 控制台存储清单的规模描述 */
76
+ stat(): string {
77
+ const size = this.bytes();
78
+ return `${this.seq}条 / ${size === null ? '(无文件)' : `${(size / 1024).toFixed(1)}KB`}`;
79
+ }
80
+
81
+ clear(): void {
82
+ if (!this.file || !existsSync(this.file)) return;
83
+ try {
84
+ writeFileSync(this.file, '', 'utf8');
85
+ } catch {
86
+ // 清空失败不致命
87
+ }
88
+ }
89
+
90
+ private bytes(): number | null {
91
+ if (!this.file || !existsSync(this.file)) return null;
92
+ try {
93
+ return statSync(this.file).size;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ private append(entry: ToolCallEntry): void {
100
+ if (!this.file) return;
101
+ try {
102
+ if (!this.dirReady) {
103
+ mkdirSync(dirname(this.file), { recursive: true });
104
+ this.dirReady = true;
105
+ }
106
+ appendFileSync(this.file, `${JSON.stringify(entry)}\n`, 'utf8');
107
+ } catch {
108
+ // 日志写不进去不影响运行
109
+ }
110
+ }
111
+ }
112
+
113
+ /** 一次调用的落盘记录(执行方在 handler 前后各取一次时刻) */
114
+ export function recordToolCall(
115
+ log: ToolCallLog | undefined,
116
+ role: string,
117
+ tool: string,
118
+ args: Record<string, unknown> | null,
119
+ startedAt: number,
120
+ out: { text: string; blobs?: unknown[]; failed?: true },
121
+ mod?: string,
122
+ ): void {
123
+ if (!log) return;
124
+ log.write({
125
+ role,
126
+ tool,
127
+ ...(mod ? { mod } : {}),
128
+ args,
129
+ durMs: Date.now() - startedAt,
130
+ chars: out.text.length,
131
+ receipt: out.text,
132
+ ...(out.failed ? { failed: true as const } : {}),
133
+ ...(out.blobs?.length ? { blobs: out.blobs.length } : {}),
134
+ });
135
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 主 session 的追加记录,保存在 data/runs/<run>/transcript.jsonl。
3
+ * session-main.jsonl 会在上下文替换时重写;本文件保留每次追加的正文,
4
+ * 并在交接、清空和前缀重载时写入边界记录。
5
+ */
6
+ import { appendFileSync, mkdirSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import type { ContextRecord } from '../protocol/open-responses/context.ts';
9
+ import { currentAnchors } from './log-context.ts';
10
+ import { nowIso } from './util.ts';
11
+
12
+ export type TranscriptBoundary = 'handoff' | 'clear' | 'prefix-reload' | 'ephemeral-drop';
13
+
14
+ export interface TranscriptItemRecord {
15
+ kind: 'item';
16
+ ts: string;
17
+ run: string;
18
+ sess?: string;
19
+ round?: number;
20
+ /** session 里的下标(append 时刻) */
21
+ index: number;
22
+ item: ContextRecord['item'];
23
+ context: ContextRecord['context'];
24
+ }
25
+
26
+ export interface TranscriptBoundaryRecord {
27
+ kind: 'boundary';
28
+ ts: string;
29
+ run: string;
30
+ sess?: string;
31
+ round?: number;
32
+ event: TranscriptBoundary;
33
+ data: Record<string, unknown>;
34
+ }
35
+
36
+ export type TranscriptRecord = TranscriptItemRecord | TranscriptBoundaryRecord;
37
+
38
+ export interface TranscriptOptions {
39
+ run?: string;
40
+ timezone?: string;
41
+ }
42
+
43
+ export class Transcript {
44
+ private readonly run: string;
45
+ private readonly timezone: string;
46
+ private dirReady = false;
47
+
48
+ constructor(private readonly file: string | null, opts: TranscriptOptions = {}) {
49
+ this.run = opts.run ?? 'r-none';
50
+ this.timezone = opts.timezone ?? 'Asia/Shanghai';
51
+ }
52
+
53
+ item(record: ContextRecord, index: number): void {
54
+ const anchors = currentAnchors();
55
+ this.append({
56
+ kind: 'item',
57
+ ts: record.context.ts ?? nowIso(this.timezone),
58
+ run: this.run,
59
+ ...(anchors.sess !== undefined ? { sess: anchors.sess } : {}),
60
+ ...(anchors.round !== undefined ? { round: anchors.round } : {}),
61
+ index,
62
+ item: record.item,
63
+ context: record.context,
64
+ });
65
+ }
66
+
67
+ boundary(event: TranscriptBoundary, data: Record<string, unknown>): void {
68
+ const anchors = currentAnchors();
69
+ this.append({
70
+ kind: 'boundary',
71
+ ts: nowIso(this.timezone),
72
+ run: this.run,
73
+ ...(anchors.sess !== undefined ? { sess: anchors.sess } : {}),
74
+ ...(anchors.round !== undefined ? { round: anchors.round } : {}),
75
+ event,
76
+ data,
77
+ });
78
+ }
79
+
80
+ private append(record: TranscriptRecord): void {
81
+ if (!this.file) return;
82
+ try {
83
+ if (!this.dirReady) {
84
+ mkdirSync(dirname(this.file), { recursive: true });
85
+ this.dirReady = true;
86
+ }
87
+ appendFileSync(this.file, `${JSON.stringify(record)}\n`, 'utf8');
88
+ } catch {
89
+ // 写不进去不影响运行
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,121 @@
1
+ /** Context compaction retains Item identity and repairs function-call pairing across response boundaries. */
2
+ import { functionResult, withText, type ContextRecord } from '../protocol/open-responses/context.ts';
3
+ import { hasRole, textOf } from '../protocol/open-responses/context-helpers.ts';
4
+ import {
5
+ FOLD_ARGS_PLACEHOLDER,
6
+ FOLD_PLACEHOLDER,
7
+ MISSING_RESULT,
8
+ OVERSIZE_PLACEHOLDER,
9
+ PENDING_IN_MAIN_THREAD,
10
+ } from './markers.ts';
11
+ import { estimateMessagesTokens } from './util.ts';
12
+
13
+ /** 回执与入参超过这么多字符就折叠成记号 */
14
+ export const FOLD_THRESHOLD = 600;
15
+
16
+ function responseKey(entry: ContextRecord): string | null {
17
+ return entry.context.responseId ?? null;
18
+ }
19
+
20
+ export function rebuildTail(
21
+ tail: readonly ContextRecord[],
22
+ budgetTokens: number,
23
+ estimate: (records: readonly ContextRecord[]) => number = estimateMessagesTokens,
24
+ ): ContextRecord[] {
25
+ if (!tail.length) return [];
26
+ const calls = new Set(tail.flatMap(({ item }) => item.type === 'function_call' ? [item.call_id] : []));
27
+ const folded = tail.map(entry => {
28
+ const item = entry.item;
29
+ if (item.type === 'function_call' && item.arguments.length > FOLD_THRESHOLD) {
30
+ return { ...entry, item: { ...item, arguments: FOLD_ARGS_PLACEHOLDER } };
31
+ }
32
+ if (item.type === 'function_call_output' && calls.has(item.call_id) && textOf(entry).length > FOLD_THRESHOLD) {
33
+ return withText(entry, FOLD_PLACEHOLDER);
34
+ }
35
+ return entry;
36
+ });
37
+ const oversized = new Set<number>();
38
+ let total = 0;
39
+ let budgetStart = 0;
40
+ for (let index = folded.length - 1; index >= 0; index--) {
41
+ const tokens = estimate([folded[index]]);
42
+ if (tokens > budgetTokens && index < folded.length - 1) { oversized.add(index); continue; }
43
+ if (total + tokens > budgetTokens && total > 0) { budgetStart = index + 1; break; }
44
+ total += tokens;
45
+ }
46
+ let start = folded.findIndex((entry, index) => index >= budgetStart && hasRole(entry, 'user'));
47
+ if (start < 0) start = budgetStart;
48
+ // A retained response begins at its first Item, including reasoning before its calls.
49
+ const key = responseKey(folded[start]);
50
+ while (start > 0 && key !== null && responseKey(folded[start - 1]) === key) start--;
51
+ const kept = folded.slice(start).map((entry, offset) => {
52
+ if (!oversized.has(start + offset)) return entry;
53
+ const item = entry.item;
54
+ if (item.type === 'message' || item.type === 'function_call_output') return withText(entry, OVERSIZE_PLACEHOLDER);
55
+ if (item.type === 'reasoning') return { ...entry, item: { type: 'reasoning' as const, id: item.id ?? 'rs_folded', summary: [{ type: 'summary_text' as const, text: OVERSIZE_PLACEHOLDER }] } };
56
+ return entry;
57
+ });
58
+ return fixPairing(kept);
59
+ }
60
+
61
+ /** Multiple calls, reasoning Items and commentary within one response share a pairing scope. */
62
+ export function fixPairing(records: readonly ContextRecord[]): ContextRecord[] {
63
+ const out: ContextRecord[] = [];
64
+ const open = new Set<string>();
65
+ let group: string | null = null;
66
+ const close = (): void => {
67
+ for (const id of open) out.push(functionResult(id, MISSING_RESULT));
68
+ open.clear();
69
+ };
70
+ for (const entry of records) {
71
+ const item = entry.item;
72
+ if (item.type === 'function_call_output') {
73
+ if (open.delete(item.call_id)) out.push(entry);
74
+ continue;
75
+ }
76
+ const key = responseKey(entry);
77
+ if (hasRole(entry, 'user') || hasRole(entry, 'system') || hasRole(entry, 'developer') || (key !== null && group !== null && key !== group)) close();
78
+ if (key !== null) group = key;
79
+ out.push(entry);
80
+ if (item.type === 'function_call') open.add(item.call_id);
81
+ }
82
+ close();
83
+ return out;
84
+ }
85
+
86
+ export function validatePairing(records: readonly ContextRecord[]): string[] {
87
+ const problems: string[] = [];
88
+ const open = new Set<string>();
89
+ const seen = new Set<string>();
90
+ let group: string | null = null;
91
+ const close = (): void => {
92
+ if (open.size) problems.push(`Unanswered function calls: ${[...open].join(',')}`);
93
+ open.clear();
94
+ };
95
+ records.forEach((entry, index) => {
96
+ const item = entry.item;
97
+ if (item.type === 'function_call_output') {
98
+ if (!open.delete(item.call_id)) problems.push(`#${index} orphan function output: ${item.call_id}`);
99
+ return;
100
+ }
101
+ const key = responseKey(entry);
102
+ if (hasRole(entry, 'user') || hasRole(entry, 'system') || hasRole(entry, 'developer') || (key !== null && group !== null && key !== group)) close();
103
+ if (key !== null) group = key;
104
+ if (item.type === 'function_call') {
105
+ if (seen.has(item.call_id)) problems.push(`#${index} duplicate call_id: ${item.call_id}`);
106
+ seen.add(item.call_id);
107
+ open.add(item.call_id);
108
+ }
109
+ });
110
+ close();
111
+ return problems;
112
+ }
113
+
114
+ export function closeDanglingCalls(records: readonly ContextRecord[]): ContextRecord[] {
115
+ const open = new Set<string>();
116
+ for (const { item } of records) {
117
+ if (item.type === 'function_call') open.add(item.call_id);
118
+ if (item.type === 'function_call_output') open.delete(item.call_id);
119
+ }
120
+ return [...records, ...[...open].map(id => functionResult(id, PENDING_IN_MAIN_THREAD))];
121
+ }