berry-agent 0.1.0-alpha.1

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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * browser 编排件(03 §10.3 生命周期条款的执法位)。
3
+ *
4
+ * - 惰性首用:apply 零 spawn(「装载在场」≠「引擎在场」——零工具调用 =
5
+ * 零进程零连接);首个工具调用才经 launchBrowserEngine 起引擎。
6
+ * - 会话路由:contexts Map by toolCtx.sessionId(?? 'default')——每键一
7
+ * BrowserPage(专属 BrowserContext,cookies/storage/缓存会话间隔离)。
8
+ * - 两级闲置回收(context 级 + 引擎级各 idleMs 缺省 300s):自续 setTimeout
9
+ * 链扫掠(unref 不阻进程退出)——context 闲置即 dispose;全 context 空
10
+ * 且引擎闲置即协议化关停。非钩子驱动(页面态不跨进程恢复——durable 只落
11
+ * 工具事件)。
12
+ * - 引擎降级:spawn/运行期死 → enginePromise 清空 + 全 context 收口,下用
13
+ * 再试(BROWSER_CONNECT_FAILED 不炸宿主);失败路径 launchPromise 同清
14
+ * (启动失败不粘住)。
15
+ * - scope.effect 回卷:作用域 dispose 即 shutdown(页面全关 + 引擎关 +
16
+ * 工具面注销)。
17
+ * - 云端占位:config.providers 在场即 notify「引擎恒本地」(v1 执行面零接)。
18
+ */
19
+ import { launchBrowserEngine } from './engine.js';
20
+ import { createBrowserPage } from './page.js';
21
+ import { buildBrowserTools } from './tools.js';
22
+ import { BROWSER_IDLE_MS, BROWSER_SWEEP_INTERVAL_MS } from './types.js';
23
+ /**
24
+ * 组 browser 编排(装载批装配根消费)。
25
+ * 生命周期:apply 起扫掠;scope 回卷或显式 shutdown 收口。
26
+ */
27
+ export function createBrowserService(deps) {
28
+ const idleMs = deps.idleMs ?? BROWSER_IDLE_MS;
29
+ const sweepIntervalMs = deps.sweepIntervalMs ?? BROWSER_SWEEP_INTERVAL_MS;
30
+ const now = () => deps.clock?.() ?? Date.now();
31
+ let enginePromise;
32
+ /** 会话页面账(sessionKey → 页面 + 末用时刻——闲置回收判据) */
33
+ const contexts = new Map();
34
+ /** 引擎末用时刻(全 context 空时的引擎级闲置判据) */
35
+ let engineLastUsed = 0;
36
+ let sweepTimer;
37
+ let disposed = false;
38
+ const unregisters = [];
39
+ /** 惰性引擎(失败/降级即清——下用再试) */
40
+ const ensureEngine = () => {
41
+ if (enginePromise === undefined) {
42
+ engineLastUsed = now();
43
+ const launcher = deps.launchEngine ?? launchBrowserEngine;
44
+ enginePromise = launcher({
45
+ spawn: deps.spawn,
46
+ ws: deps.ws,
47
+ fs: deps.fs,
48
+ readEnv: deps.readEnv,
49
+ platform: deps.platform,
50
+ homeDir: deps.homeDir,
51
+ dataDir: deps.dataDir,
52
+ config: deps.config,
53
+ ...(deps.logger !== undefined ? { logger: deps.logger } : {}),
54
+ })
55
+ .then((handle) => {
56
+ // 引擎级降级双源(进程退/ws 关):清粘滞 + 全 context 收口——
57
+ // 页面 send 已被 cdp 件快拒,close 幂等吞错
58
+ handle.onDown((reason) => {
59
+ enginePromise = undefined;
60
+ deps.notify?.(`browser:引擎已降级(${reason})——下次使用时重试`);
61
+ for (const entry of contexts.values())
62
+ void entry.page.close().catch(() => { });
63
+ contexts.clear();
64
+ });
65
+ return handle;
66
+ })
67
+ .catch((err) => {
68
+ enginePromise = undefined; // 启动失败不粘住(下用再试)
69
+ throw err;
70
+ });
71
+ }
72
+ return enginePromise;
73
+ };
74
+ /** 扫掠一步:context 级 → 引擎级(自续链 unref) */
75
+ const sweep = () => {
76
+ if (disposed)
77
+ return;
78
+ const t = now();
79
+ for (const [key, entry] of contexts) {
80
+ if (t - entry.lastUsed >= idleMs) {
81
+ contexts.delete(key);
82
+ void entry.page.close().catch(() => { });
83
+ deps.logger?.debug?.(`browser 闲置回收:context ${key}(${idleMs}ms 未用)`);
84
+ }
85
+ }
86
+ // 引擎级:全 context 空 + 引擎闲置 → 协议化关停(页面态不跨进程恢复)
87
+ if (contexts.size === 0 && enginePromise !== undefined && t - engineLastUsed >= idleMs) {
88
+ const closing = enginePromise;
89
+ enginePromise = undefined;
90
+ void closing.then((handle) => handle.close()).catch(() => { });
91
+ deps.logger?.debug?.('browser 闲置回收:引擎协议化关停');
92
+ }
93
+ sweepTimer = setTimeout(sweep, sweepIntervalMs);
94
+ sweepTimer.unref();
95
+ };
96
+ const service = {
97
+ apply() {
98
+ // 云端占位(03 §10.3:providers 在场即通知——v1 执行面零接)
99
+ if (deps.config.providers !== undefined && deps.config.providers.length > 0) {
100
+ deps.notify?.('browser:云端 providers 已配置——v1 未接云端执行面,引擎恒本地');
101
+ }
102
+ if (deps.register !== undefined) {
103
+ for (const def of buildBrowserTools({ pageFor: (key) => service.pageFor(key) })) {
104
+ unregisters.push(deps.register.register(def));
105
+ }
106
+ }
107
+ if (deps.scope !== undefined) {
108
+ deps.scope.effect(() => () => {
109
+ void service.shutdown();
110
+ });
111
+ }
112
+ sweepTimer = setTimeout(sweep, sweepIntervalMs);
113
+ sweepTimer.unref();
114
+ },
115
+ async pageFor(sessionKey) {
116
+ const existing = contexts.get(sessionKey);
117
+ if (existing !== undefined) {
118
+ existing.lastUsed = now();
119
+ return existing.page;
120
+ }
121
+ engineLastUsed = now();
122
+ const engine = await ensureEngine();
123
+ const page = await createBrowserPage({
124
+ conn: engine.conn,
125
+ fs: deps.fs,
126
+ dataDir: deps.dataDir,
127
+ web: deps.web,
128
+ ...(deps.logger !== undefined ? { logger: deps.logger } : {}),
129
+ });
130
+ // 并发竞态:建页期间(引擎首启慢)同键可能已入账——后建让先建
131
+ const raced = contexts.get(sessionKey);
132
+ if (raced !== undefined) {
133
+ void page.close().catch(() => { });
134
+ raced.lastUsed = now();
135
+ return raced.page;
136
+ }
137
+ contexts.set(sessionKey, { page, lastUsed: now() });
138
+ return page;
139
+ },
140
+ async shutdown() {
141
+ disposed = true;
142
+ if (sweepTimer !== undefined)
143
+ clearTimeout(sweepTimer);
144
+ for (const unregister of unregisters.splice(0))
145
+ unregister();
146
+ const entries = [...contexts.values()];
147
+ contexts.clear();
148
+ const closingEngine = enginePromise;
149
+ enginePromise = undefined;
150
+ // 全收口 best-effort(引擎死态页面 close 幂等吞错)
151
+ await Promise.allSettled([
152
+ ...entries.map((e) => e.page.close()),
153
+ ...(closingEngine !== undefined ? [closingEngine.then((h) => h.close())] : []),
154
+ ]);
155
+ },
156
+ };
157
+ return service;
158
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * browser 工具面十件(03 §10.3 工具面条款——navigate/back/forward/snapshot/
3
+ * click/type/press/scroll/screenshot/console)。
4
+ *
5
+ * effect 分账:交互族七件 'write'(navigate/back/forward/click/type/press/
6
+ * scroll——改变页面状态的世界动作,走三段管道审批 fail-closed);读族三件
7
+ * 'read'(snapshot/screenshot/console——只读观测)。
8
+ *
9
+ * 会话路由:toolCtx.sessionId 缺席折 'default'(无会话语境的调用方共一页
10
+ * 面——诚实简化非隔离缺失)。
11
+ *
12
+ * 失败编码为 isError 数据面(03 §2.3)——BaseError 携码前置披露(判别收紧:
13
+ * Node 系统错误也带 code 属性,只认注册码体系成员)。
14
+ */
15
+ import { BaseError } from '../contracts/index.js';
16
+ import { Type } from 'typebox';
17
+ /** 命名键词表(press 工具 schema 收词面——运行时词表在 page 件单源) */
18
+ const PRESS_KEYS = [
19
+ 'Enter',
20
+ 'Tab',
21
+ 'Escape',
22
+ 'Backspace',
23
+ 'Delete',
24
+ 'ArrowUp',
25
+ 'ArrowDown',
26
+ 'ArrowLeft',
27
+ 'ArrowRight',
28
+ 'Home',
29
+ 'End',
30
+ 'PageUp',
31
+ 'PageDown',
32
+ 'Space',
33
+ ];
34
+ /** toolCtx → 会话键(sessionId 缺席折 'default') */
35
+ function sessionKeyOf(toolCtx) {
36
+ const sid = toolCtx?.sessionId;
37
+ return typeof sid === 'string' && sid !== '' ? sid : 'default';
38
+ }
39
+ /** 结果编码两分:成功面拼模型可见文本;失败面 isError(携码前置) */
40
+ function ok(text) {
41
+ return { content: [{ type: 'text', text }] };
42
+ }
43
+ function fail(error) {
44
+ const code = error instanceof BaseError ? error.code : undefined;
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ return { content: [{ type: 'text', text: code ? `[${code}] ${message}` : message }], isError: true };
47
+ }
48
+ /** 参数窄化取串(schema 已校验——防御位) */
49
+ const str = (args, key) => String(args[key] ?? '');
50
+ /**
51
+ * 组工具面十件(装载批由装配根经注册窄面挂入工具注册表)。
52
+ * @param source 会话路由后的页面取用面(service 注入)
53
+ */
54
+ export function buildBrowserTools(source) {
55
+ /** 执行包装:会话路由 + 失败编码统一(fn 收页面与已校验参数) */
56
+ const run = (fn) => {
57
+ return async (args, toolCtx) => {
58
+ try {
59
+ const page = await source.pageFor(sessionKeyOf(toolCtx));
60
+ return ok(await fn(page, args));
61
+ }
62
+ catch (error) {
63
+ return fail(error);
64
+ }
65
+ };
66
+ };
67
+ return [
68
+ {
69
+ name: 'navigate',
70
+ description: '导航当前页面到指定 URL(http/https)。入口 URL 先过安全卫生件(私网/环回' +
71
+ '地址拒绝);自动跟随服务端重定向,回执给最终 URL。页面加载完成(load 事件)后才返回。',
72
+ parameters: Type.Object({ url: Type.String({ description: '完整 URL(须含 http:// 或 https://)' }) }, { additionalProperties: false }),
73
+ effect: 'write',
74
+ timeoutMs: 45_000,
75
+ execute: run(async (page, args) => {
76
+ const r = await page.navigate(str(args, 'url'));
77
+ return `已导航到 ${r.url}`;
78
+ }),
79
+ },
80
+ {
81
+ name: 'back',
82
+ description: '浏览器后退一步(导航史前一步)。无前史时回执「无前一步」不算错误。',
83
+ parameters: Type.Object({}, { additionalProperties: false }),
84
+ effect: 'write',
85
+ timeoutMs: 45_000,
86
+ execute: run(async (page) => {
87
+ const r = await page.back();
88
+ return r.moved ? `已后退到 ${r.url}` : '已在最早一页(无前一步可退)';
89
+ }),
90
+ },
91
+ {
92
+ name: 'forward',
93
+ description: '浏览器前进一步(导航史后一步)。无后史时回执「无后一步」不算错误。',
94
+ parameters: Type.Object({}, { additionalProperties: false }),
95
+ effect: 'write',
96
+ timeoutMs: 45_000,
97
+ execute: run(async (page) => {
98
+ const r = await page.forward();
99
+ return r.moved ? `已前进到 ${r.url}` : '已在最新一页(无后一步可进)';
100
+ }),
101
+ },
102
+ {
103
+ name: 'snapshot',
104
+ description: '取当前页面可交互元素清单(a11y 快照)——每个元素一行(角色 + 可见名 + ' +
105
+ '可选 href/name),锚形如 [ref=@e1]。click/type 用回执中的 ref 锚定位元素;' +
106
+ '每次快照重建锚表(旧锚作废)。页面结构变化后应重新快照。',
107
+ parameters: Type.Object({}, { additionalProperties: false }),
108
+ effect: 'read',
109
+ timeoutMs: 30_000,
110
+ execute: run(async (page) => page.snapshot()),
111
+ },
112
+ {
113
+ name: 'click',
114
+ description: '点击快照中的元素(ref 锚定位)。元素若在折叠线外会先自动滚入视口再点击。' +
115
+ 'ref 不在当前快照(过期/未取)即报错——先 snapshot。',
116
+ parameters: Type.Object({ ref: Type.String({ description: 'snapshot 回执中的锚(形如 @e1)' }) }, { additionalProperties: false }),
117
+ effect: 'write',
118
+ timeoutMs: 30_000,
119
+ execute: run(async (page, args) => {
120
+ await page.click(str(args, 'ref'));
121
+ return `已点击 ${str(args, 'ref')}`;
122
+ }),
123
+ },
124
+ {
125
+ name: 'type',
126
+ description: '向快照中的输入框键入文本(ref 锚定位;整体插入不改写法——适合表单填充)。' + '目标会先自动滚入视口并获得焦点。',
127
+ parameters: Type.Object({
128
+ ref: Type.String({ description: 'snapshot 回执中的锚(形如 @e1)' }),
129
+ text: Type.String({ description: '要键入的文本' }),
130
+ }, { additionalProperties: false }),
131
+ effect: 'write',
132
+ timeoutMs: 30_000,
133
+ execute: run(async (page, args) => {
134
+ await page.type(str(args, 'ref'), str(args, 'text'));
135
+ return `已键入 ${str(args, 'text').length} 字符到 ${str(args, 'ref')}`;
136
+ }),
137
+ },
138
+ {
139
+ name: 'press',
140
+ description: '按键(命名键词表:Enter/Tab/Escape/Backspace/Delete/方向键/Home/End/PageUp/PageDown/Space;或任意单字符)。',
141
+ parameters: Type.Object({
142
+ key: Type.Union([
143
+ ...PRESS_KEYS.map((k) => Type.Literal(k)),
144
+ Type.String({ pattern: '^[\\s\\S]$' }), // 单字符(含空格)
145
+ ], { description: '命名键或单字符' }),
146
+ }, { additionalProperties: false }),
147
+ effect: 'write',
148
+ timeoutMs: 30_000,
149
+ execute: run(async (page, args) => {
150
+ await page.press(str(args, 'key'));
151
+ return `已按键 ${str(args, 'key')}`;
152
+ }),
153
+ },
154
+ {
155
+ name: 'scroll',
156
+ description: '滚动页面(缺省向下 600 像素)。direction 二值 up/down;amount 为像素量(1~10000)。',
157
+ parameters: Type.Object({
158
+ direction: Type.Optional(Type.Union([Type.Literal('up'), Type.Literal('down')], { description: '方向(缺省 down)' })),
159
+ amount: Type.Optional(Type.Integer({ minimum: 1, maximum: 10_000, description: '像素量(缺省 600)' })),
160
+ }, { additionalProperties: false }),
161
+ effect: 'write',
162
+ timeoutMs: 30_000,
163
+ execute: run(async (page, args) => {
164
+ const raw = args;
165
+ const direction = raw.direction === 'up' ? 'up' : 'down';
166
+ const amount = typeof raw.amount === 'number' ? raw.amount : 600;
167
+ await page.scroll(direction, amount);
168
+ return `已${direction === 'down' ? '向下' : '向上'}滚动 ${amount} 像素`;
169
+ }),
170
+ },
171
+ {
172
+ name: 'screenshot',
173
+ description: '截取当前视口 PNG,落数据目录可取阅位(滚动保留最近 20 张),回执给文件路径与字节数。',
174
+ parameters: Type.Object({}, { additionalProperties: false }),
175
+ effect: 'read',
176
+ timeoutMs: 30_000,
177
+ execute: run(async (page) => {
178
+ const r = await page.screenshot();
179
+ return `截图已保存:${r.path}(${r.bytes} 字节)`;
180
+ }),
181
+ },
182
+ {
183
+ name: 'console',
184
+ description: '读取页面 console 回流(log/warn/error 与未捕获异常,本页面上下文创建以来累积,环形上限 200 条)。',
185
+ parameters: Type.Object({ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200, description: '取最近 N 条(缺省 50)' })) }, { additionalProperties: false }),
186
+ effect: 'read',
187
+ timeoutMs: 15_000,
188
+ execute: run(async (page, args) => {
189
+ const raw = args;
190
+ const limit = typeof raw.limit === 'number' ? raw.limit : 50;
191
+ const entries = await page.consoleLog(limit);
192
+ if (entries.length === 0)
193
+ return '(本页面上下文尚无 console 输出)';
194
+ return entries.map((e) => `[${e.kind === 'exception' ? 'exception' : e.level}] ${e.text}`).join('\n');
195
+ }),
196
+ },
197
+ ];
198
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * core:browser 类型面(03 §10.3——CDP 手写最小桥 + 引擎发现序 + 十工具;
3
+ * 批 17b-2 传输层腿)。
4
+ *
5
+ * 词面独立律(02 §4.1 席 19 deps = contracts + web + persist):spawn 与
6
+ * 工具注册全经**窄面注入**消费(mcp/lsp 先例同律——本件与 exec/tools 零 DAG
7
+ * 边):
8
+ * - `BrowserSpawnFace` 与 exec SpawnPipeline.spawnInteractive 结构兼容(组合
9
+ * 根直接传管道真身;兼容性互证测试在 compat.test.ts);
10
+ * - `BrowserRegisterToolsFace` 与 tools ToolRegistry.register 子面结构兼容
11
+ * (返回注销器 = 撤工具面);
12
+ * - `BrowserScopeFace` 与 context Scope 的 effect/isDisposed 子面结构兼容
13
+ * (ctx.effect 回卷 + 续段 scope 活查);
14
+ * - logger 窄面(本件无 context 边——createLogger 不可 import,装配根传真
15
+ * 身、缺省 no-op 测试静默)。
16
+ *
17
+ * web 席边为真消费(卫生单源复用——navigate 入口 URL 经 WebFetchService
18
+ * 同一 execute 跑五卫生件 + 同一在飞门实例,「两消费面同一 execute 同一限
19
+ * 流」的第三消费位)。persist 席边留白(v1 账本/截图走数据目录文件面——
20
+ * 页面态永不跨进程恢复,durable 只落工具事件不落页面态)。
21
+ */
22
+ import { BaseError } from '../contracts/index.js';
23
+ /* ---------------- 常量(缺省值单源——03 §10.3 生命周期与回收条款) ---------------- */
24
+ /** 引擎启动预算 ms(spawn → DevTools 侦听行 → ws 建立 → Browser.getVersion 同罩) */
25
+ export const BROWSER_STARTUP_TIMEOUT_MS = 20_000;
26
+ /** 单条 CDP 命令缺省钟 ms(命令级超时——连接级失败分账 BROWSER_CONNECT_FAILED) */
27
+ export const BROWSER_CDP_COMMAND_TIMEOUT_MS = 15_000;
28
+ /** 两级闲置回收缺省(03 §10.3:context 级与引擎级各 300s) */
29
+ export const BROWSER_IDLE_MS = 300_000;
30
+ /** 闲置扫掠间隔 ms(自续 setTimeout 链——unref 不阻进程退出) */
31
+ export const BROWSER_SWEEP_INTERVAL_MS = 5_000;
32
+ /** 引擎协议化关停宽限 ms(Browser.close → 宽限等退 → kill 兜底) */
33
+ export const BROWSER_CLOSE_GRACE_MS = 3_000;
34
+ /** stderr DevTools 侦听行解析(Chrome 系引擎 --remote-debugging-port=0 唯一出口) */
35
+ export const DEVTOOLS_LISTENING_RE = /DevTools listening on (ws:\/\/\S+)/;
36
+ /** 导航预算 ms(Page.navigate 发出 → loadEventFired 等待同罩——慢页面不误杀) */
37
+ export const BROWSER_NAV_TIMEOUT_MS = 30_000;
38
+ /** console 环形缓冲帽(Runtime.consoleAPICalled/exceptionThrown 累积——超帽弃最旧) */
39
+ export const BROWSER_CONSOLE_RING_CAP = 200;
40
+ /** 截图滚动清理保留数(落数据目录可取阅位——超量弃最旧,图像字节永不进 durable) */
41
+ export const BROWSER_SCREENSHOT_KEEP = 20;
42
+ /** 截图子目录名(`${dataDir}/browser/` 下——与 engine/profile 同级) */
43
+ export const BROWSER_SCREENSHOT_DIRNAME = 'screenshots';
44
+ /** 引擎 spawn 归属名(登记簿同册——三桥同律) */
45
+ export const BROWSER_ENGINE_OWNER = 'browser:engine';
46
+ /**
47
+ * 引擎最小 env 白名单(deny-by-default 同律 exec):Chrome headless 实测
48
+ * 需 HOME(用户数据目录解析)与 PATH( crashpad 报告器自寻);TMPDIR/locale
49
+ * 为崩溃报告与字体定位保底。零继承宿主其余变量(凭证面封死)。
50
+ */
51
+ export const BROWSER_ENGINE_ENV_ALLOW = ['HOME', 'PATH', 'TMPDIR', 'LANG', 'LC_ALL'];
52
+ /**
53
+ * 归一 config.browser(坏形响亮拒 BROWSER_CONFIG_INVALID)。
54
+ * undefined/null → 空配置(缺省行惰性无害零 spawn——零工具调用 = 零进程零连接)。
55
+ */
56
+ export function normalizeBrowserConfig(raw) {
57
+ if (raw === undefined || raw === null)
58
+ return {};
59
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
60
+ throw new BaseError('BROWSER_CONFIG_INVALID', 'config.browser 须为对象');
61
+ }
62
+ const v = raw;
63
+ let executablePath;
64
+ if (v.executablePath !== undefined) {
65
+ if (typeof v.executablePath !== 'string' || v.executablePath === '') {
66
+ throw new BaseError('BROWSER_CONFIG_INVALID', `browser.executablePath 须为非空字符串(引擎可执行绝对路径):${JSON.stringify(v.executablePath)}`);
67
+ }
68
+ executablePath = v.executablePath;
69
+ }
70
+ let providers;
71
+ if (v.providers !== undefined) {
72
+ if (!Array.isArray(v.providers)) {
73
+ throw new BaseError('BROWSER_CONFIG_INVALID', 'browser.providers 须为数组(云端占位——v1 执行面零接)');
74
+ }
75
+ providers = v.providers;
76
+ }
77
+ return {
78
+ ...(executablePath !== undefined ? { executablePath } : {}),
79
+ ...(providers !== undefined ? { providers } : {}),
80
+ };
81
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 提问队列(07 §4.3 提问队列机制正文——2026-09-06 channels 纵切批补写):
3
+ * 阻塞原语(confirm/select/input)与审批 ask 统一入 per-session FIFO——
4
+ * 队首才呈现、应答即出队、后继顶上(「无并存态」即此律)。
5
+ *
6
+ * 收口三则(规范同条):会话关闭按 sessionId 清队(余项按取消语义收场——
7
+ * 保守值同撤销面);run 打断信号穿透队内该 run 发起的 ask(signal 由调用方
8
+ * ui-core 携带,队列不感知 run);队列是内存态、崩溃即丢(durable 真相在
9
+ * 审批日志不在队列——与 04 §4 PendingMessageQueue 同纪)。
10
+ *
11
+ * 队列持在通道核、呈现形态归后端(TUI overlay 是队首呈现之一)——本类只管
12
+ * 排序与收口,backend 编舞(竞速/撤销说明行)归 ui-core。
13
+ */
14
+ /**
15
+ * 提问队列(per-session FIFO)。同会话严格串行(队首在飞时后继排队);
16
+ * 异会话队列独立(各自队首并行——呈现并存的裁决归后端 overlay 层)。
17
+ */
18
+ export class AskQueue {
19
+ sessions = new Map();
20
+ ensure(sessionId) {
21
+ let q = this.sessions.get(sessionId);
22
+ if (q === undefined) {
23
+ q = { pending: [], active: null };
24
+ this.sessions.set(sessionId, q);
25
+ }
26
+ return q;
27
+ }
28
+ /** 入队:会话队列空闲即直晋队首(start 同步调用——FIFO 无空转窗);否则排队 */
29
+ enqueue(sessionId, kind, hooks) {
30
+ const q = this.ensure(sessionId);
31
+ if (q.active === null) {
32
+ q.active = { kind, hooks };
33
+ hooks.start();
34
+ }
35
+ else {
36
+ q.pending.push({ kind, hooks });
37
+ }
38
+ }
39
+ /**
40
+ * 队首落定(应答/保守值收场都算——由 ui-core 在 promise 链尾调用):
41
+ * 出队、后继顶上。未知会话/已清队 no-op(clearSession 后迟到落定不炸)。
42
+ */
43
+ settled(sessionId) {
44
+ const q = this.sessions.get(sessionId);
45
+ if (q === undefined)
46
+ return;
47
+ if (q.active === null)
48
+ return; // 空转(迟到落定)——幂等
49
+ const next = q.pending.shift();
50
+ if (next === undefined) {
51
+ q.active = null; // 队列清空(会话条目留存——防高频重建;clearSession 才真清)
52
+ }
53
+ else {
54
+ q.active = next;
55
+ next.hooks.start();
56
+ }
57
+ }
58
+ /**
59
+ * 会话收口(04 §9 归属围栏通道侧执法):清排队项 + 取消在飞队首——
60
+ * 全部走 cancel(保守值同撤销面:confirm→false / select·input→''、
61
+ * approval→cancelled 语义由 ui-core 落)。
62
+ */
63
+ clearSession(sessionId) {
64
+ const q = this.sessions.get(sessionId);
65
+ if (q === undefined)
66
+ return;
67
+ const all = q.active === null ? [...q.pending] : [q.active, ...q.pending];
68
+ q.pending.length = 0;
69
+ q.active = null;
70
+ this.sessions.delete(sessionId);
71
+ // 逐项 cancel:在飞与排队同语义收场(cancel 实现须幂等——settled 迟到不双结)
72
+ for (const item of all)
73
+ item.hooks.cancel();
74
+ }
75
+ /** 观察面:会话在飞+排队项 kind 序列(测试与 /help 类诊断面消费) */
76
+ pending(sessionId) {
77
+ const q = this.sessions.get(sessionId);
78
+ if (q === undefined)
79
+ return [];
80
+ return q.active === null ? q.pending.map((p) => p.kind) : [q.active.kind, ...q.pending.map((p) => p.kind)];
81
+ }
82
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * 命令面注册表与分发(03 §2.2 registerCommand 签名定形 + §2.7 冲突律;
3
+ * 2026-09-06 channels 纵切批)。
4
+ *
5
+ * 两律:①后写胜出——命令名是可换实现位(用户插件覆盖 core: 同名命令 =
6
+ * 显式换装正道),撞名不拒、胜出即生效;②词法违例拒——命令名主段连字符式
7
+ * + 可选冒号子段,非法名拒绝注册(CHANNEL_COMMAND_INVALID——与
8
+ * AGENT_ROLE_INVALID 同构:撞名不拒、格式拒)。
9
+ */
10
+ import { BaseError } from '../contracts/index.js';
11
+ /** 命令名词法(03 §2.2 定形:^[a-z][a-z0-9-]*(:[a-z0-9-]+)?$——/memory-export 与 /skill:name 实况同形) */
12
+ const COMMAND_NAME_RE = /^[a-z][a-z0-9-]*(?::[a-z0-9-]+)?$/;
13
+ /**
14
+ * 命令面注册表。注册即生效(03 §2.1——下一次消费点重取,不待重启);
15
+ * 全部方法同步(注册/查询/解析零 I/O,分发是唯一异步面)。
16
+ */
17
+ export class CommandRegistry {
18
+ commands = new Map();
19
+ /** 注册代币发生器(每注册一枚新 token——disposer 校验「自己仍是现任」用) */
20
+ tokenSeed = 0;
21
+ /**
22
+ * 注册命令(后写胜出)。词法违例抛 CHANNEL_COMMAND_INVALID;撞名静默覆盖
23
+ * (03 §2.7:胜出即生效无拒码)。
24
+ *
25
+ * @returns disposer——注销该命令;若已被后写覆盖则 no-op(不误注接任者)
26
+ */
27
+ register(name, handler, description) {
28
+ if (!COMMAND_NAME_RE.test(name)) {
29
+ throw new BaseError('CHANNEL_COMMAND_INVALID', `命令名词法违例:${JSON.stringify(name)}(主段连字符式 + 可选冒号子段,小写)`);
30
+ }
31
+ const token = { seed: this.tokenSeed++ };
32
+ this.commands.set(name, { name, handler, description, token });
33
+ return () => {
34
+ // 只有现任可注(token 相等 = 未被后写覆盖)——后写的接任者不因旧 disposer 被误注
35
+ const current = this.commands.get(name);
36
+ if (current !== undefined && current.token === token) {
37
+ this.commands.delete(name);
38
+ }
39
+ };
40
+ }
41
+ /** 命令清单(/help 与 @-mention 补全面的消费面——注册序) */
42
+ list() {
43
+ return [...this.commands.values()].map(({ token: _token, ...spec }) => spec);
44
+ }
45
+ /** 按名查询(分发前置——后写胜出者即现任) */
46
+ get(name) {
47
+ const entry = this.commands.get(name);
48
+ return entry === undefined
49
+ ? undefined
50
+ : { name: entry.name, handler: entry.handler, description: entry.description };
51
+ }
52
+ /**
53
+ * 解析输入行(同步纯函数面):`/name rest...` → { name, args };
54
+ * 非 `/` 开头返回 null(普通消息路——去哪归驱动侧/conversation 裁决,
55
+ * 命令面只报「不是命令」)。
56
+ */
57
+ parse(input) {
58
+ if (!input.startsWith('/'))
59
+ return null;
60
+ // 首空白分割命令名与实参原文(命令名自身无空白——词法保证)
61
+ const rest = input.slice(1);
62
+ const sep = rest.search(/\s/);
63
+ const name = sep === -1 ? rest : rest.slice(0, sep);
64
+ const raw = sep === -1 ? '' : rest.slice(sep + 1).trim();
65
+ if (!COMMAND_NAME_RE.test(name))
66
+ return null;
67
+ return { name, args: { raw, argv: tokenize(raw) } };
68
+ }
69
+ /**
70
+ * 分发输入行:命中注册命令 → 执行 handler 返回 true;未命中(非命令形或
71
+ * 名不在册)返回 false——「未注册的 /词 怎么处置(提示未知命令 or 当普通
72
+ * 消息进 run)」是驱动侧语义,归 conversation 件。
73
+ *
74
+ * sessionId 位(ix-2 新建):发起会话透传进 CommandArgs(CLI 面缺席)——
75
+ * 插件命令 handler 可显式携带(ctx.ui opts.sessionId 透传),ambient 自动
76
+ * 锚另由 host 侧 ALS 承载(两轨并行——显式位是插件可见面、ALS 是零自觉面)。
77
+ */
78
+ async dispatch(input, sessionId) {
79
+ const parsed = this.parse(input);
80
+ if (parsed === null)
81
+ return false;
82
+ const spec = this.commands.get(parsed.name);
83
+ if (spec === undefined)
84
+ return false;
85
+ await spec.handler(sessionId !== undefined ? { ...parsed.args, sessionId } : parsed.args);
86
+ return true;
87
+ }
88
+ }
89
+ /**
90
+ * 引号感知词切分(03 §2.2 定形:shell 词法的最小实现——不进 glob/变量展开)。
91
+ * 规则:裸词按空白切;'单引号' 保字面(内部再无转义);"双引号" 保内部空格;
92
+ * 引号未闭合按行尾收口(宽容收场——终端输入无第二行可续)。
93
+ */
94
+ export function tokenize(raw) {
95
+ const out = [];
96
+ let current = '';
97
+ let hasCurrent = false;
98
+ let quote = null;
99
+ for (const ch of raw) {
100
+ if (quote !== null) {
101
+ if (ch === quote) {
102
+ quote = null; // 引号闭合——段继续累积(拼接态 "ab'cd ef'" 保空格)
103
+ }
104
+ else {
105
+ current += ch;
106
+ }
107
+ continue;
108
+ }
109
+ if (ch === '"' || ch === "'") {
110
+ quote = ch;
111
+ hasCurrent = true; // 空引号段 '' 也是一段(与裸词不同——显式空串)
112
+ continue;
113
+ }
114
+ if (ch === ' ' || ch === '\t') {
115
+ if (hasCurrent) {
116
+ out.push(current);
117
+ current = '';
118
+ hasCurrent = false;
119
+ }
120
+ continue;
121
+ }
122
+ current += ch;
123
+ hasCurrent = true;
124
+ }
125
+ // 行尾:未闭合引号宽容收口(current 原样成段);尾段在
126
+ if (hasCurrent)
127
+ out.push(current);
128
+ return out;
129
+ }