@ljwei-stak/dsh-model-route-bot 0.66.4 → 0.67.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 (271) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +18 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,228 @@
1
+ /**
2
+ * The fixed data worker: a plain Node child process that owns one Bot's SQLite
3
+ * database and runs its statements. The engine forks THIS module path — never
4
+ * a path a model supplies — and it is the only place better-sqlite3 loads.
5
+ *
6
+ * Protocol (child_process IPC):
7
+ * - `open { dbPath }`, then requests `{ id, op, ... }`; each replies
8
+ * `{ id, ok:true, value }` or `{ id, ok:false, error }`.
9
+ * - The SQL gate lives on the Host side; here a statement is a single prepared
10
+ * statement (better-sqlite3 refuses multi-statement strings), extension
11
+ * loading is never enabled, and every read is bounded by maxRows/maxBytes.
12
+ * - The execution layer re-checks what it is asked to run: a read must be a
13
+ * statement better-sqlite3 reports as a reader (`stmt.reader`), and a write
14
+ * must still classify as `CREATE … AS`. A `WITH … DELETE` cannot pass either
15
+ * check.
16
+ * - Row and character caps alone do not bound SQLite, so `open` also sets
17
+ * `max_page_count` and the soft/hard native heap limits: an oversized
18
+ * allocation or database is refused by SQLite itself (SQLITE_FULL /
19
+ * SQLITE_NOMEM) inside this child, never by the Host.
20
+ * - A statement that never returns is cancelled by the Host SIGKILLing this
21
+ * process; an unknown outcome is never replayed. SQLite recovers the file
22
+ * from its WAL on the next open.
23
+ *
24
+ * The database path arrives only in the `open` message and is computed by the
25
+ * Host from a validated Bot id — this worker accepts no path from any model.
26
+ * The handler is attached only when this file is the forked entry (MB_DATA_WORKER),
27
+ * so the functions below can be imported and unit-tested in-process.
28
+ */
29
+
30
+ import { createRequire } from 'node:module'
31
+ import { DATA_ERROR_CODES, DATA_LIMITS, DataFailure, dataError, sanitizeDataMessage, quoteIdent } from './bot-data-surface.mjs'
32
+ import { gateSql } from './bot-data-sql.mjs'
33
+
34
+ const require = createRequire(import.meta.url)
35
+ const CELL_CHARS_MAX = 4000
36
+
37
+ /** Import better-sqlite3 lazily so importing this module for tests never loads the native binding. */
38
+ let Database = null
39
+ const loadDatabase = () => { Database ??= require('better-sqlite3'); return Database }
40
+
41
+ export const cellOf = value => {
42
+ if (value === null || value === undefined) return null
43
+ if (typeof value === 'bigint') return value.toString()
44
+ if (Buffer.isBuffer(value)) return `<blob ${value.length} bytes>`
45
+ if (typeof value === 'object') return JSON.stringify(value)
46
+ if (typeof value === 'string' && value.length > CELL_CHARS_MAX) return `${value.slice(0, CELL_CHARS_MAX)}…`
47
+ return value
48
+ }
49
+
50
+ const typeName = (type, sample) => {
51
+ if (type) return String(type).toUpperCase()
52
+ if (typeof sample === 'number') return Number.isInteger(sample) ? 'INTEGER' : 'REAL'
53
+ if (typeof sample === 'boolean') return 'BOOLEAN'
54
+ if (sample === null || sample === undefined) return 'ANY'
55
+ return 'TEXT'
56
+ }
57
+
58
+ const columnsOf = stmt => stmt.columns().map(column => ({ name: column.name, type: String(column.type ?? 'ANY').toUpperCase() }))
59
+
60
+ export const openDb = (dbPath, limits = {}) => {
61
+ const db = loadDatabase()(dbPath)
62
+ // One writer per Bot; WAL so an abrupt kill recovers; bounded busy wait.
63
+ db.pragma('journal_mode = WAL')
64
+ db.pragma('busy_timeout = 5000')
65
+ db.pragma('foreign_keys = OFF')
66
+ // Query sort/group intermediates must share the native heap ceiling instead
67
+ // of spilling into an unbounded temporary database on the Host disk.
68
+ db.pragma('temp_store = MEMORY')
69
+ // Native and file bounds. Row/character caps alone do not bound SQLite: one
70
+ // statement can allocate native memory or grow the file without returning many
71
+ // rows, so SQLite itself refuses (SQLITE_FULL / SQLITE_NOMEM) and the child
72
+ // dies alone instead of the Host.
73
+ const maxPageCount = Math.max(1024, Math.trunc(limits.maxPageCount ?? DATA_LIMITS.dbMaxPageCount))
74
+ const hardHeap = Math.max(8 * 1024 * 1024, Math.trunc(limits.hardHeapLimitBytes ?? DATA_LIMITS.dbHardHeapLimitBytes))
75
+ const softHeap = Math.min(hardHeap, Math.max(1024 * 1024, Math.trunc(limits.softHeapLimitBytes ?? DATA_LIMITS.dbSoftHeapLimitBytes)))
76
+ db.pragma(`max_page_count = ${maxPageCount}`)
77
+ db.pragma(`hard_heap_limit = ${hardHeap}`)
78
+ db.pragma(`soft_heap_limit = ${softHeap}`)
79
+ return db
80
+ }
81
+
82
+ /** Run a read statement, collecting up to maxRows rows and maxBytes of JSON. */
83
+ export const runRead = (db, sql, maxRows, maxBytes) => {
84
+ const stmt = db.prepare(sql)
85
+ // A statement that returns no data is not a read, whatever the caller labelled
86
+ // it: better-sqlite3 exposes this per statement, so the execution layer — not a
87
+ // regex — decides. The gate on the Host side is the first line, this is the second.
88
+ if (stmt.reader !== true) throw new DataFailure(dataError('write_refused', 'Only statements that return data may run as a read.'))
89
+ const columns = stmt.columns().map(column => ({ name: column.name, type: String(column.type ?? 'ANY').toUpperCase() }))
90
+ const rows = []
91
+ let bytes = 2
92
+ let truncated = false
93
+ for (const raw of stmt.raw().iterate()) {
94
+ const row = raw.map(cellOf)
95
+ rows.push(row)
96
+ bytes += Buffer.byteLength(JSON.stringify(row)) + 1
97
+ if (rows.length >= maxRows) { truncated = true; break }
98
+ if (bytes > maxBytes) { truncated = true; break }
99
+ }
100
+ const first = rows[0]
101
+ if (first) columns.forEach((column, index) => { if (column.type === 'ANY') column.type = typeName(undefined, first[index]) })
102
+ return { columns, rows, rowCount: rows.length, truncated }
103
+ }
104
+
105
+ /** Run a non-read statement (CREATE … AS); return the affected-row count. */
106
+ export const runWrite = (db, sql) => {
107
+ // The gate is re-checked where the statement is executed: a WITH … DELETE is
108
+ // not a CREATE, and only CREATE … AS may run here.
109
+ const kind = gateSql(sql, { allowCreate: true })
110
+ if (kind.kind !== 'create') throw new DataFailure(dataError('write_refused', `${kind.verb ?? 'That statement'} is not allowed here.`))
111
+ const info = db.prepare(sql).run()
112
+ return { changes: info.changes ?? 0 }
113
+ }
114
+
115
+ /** Describe a table/view/result relation: exact row count, columns, few samples. */
116
+ export const describeRelation = (db, target) => {
117
+ const columns = columnsOf(db.prepare(`SELECT * FROM ${target} LIMIT 0`))
118
+ const count = db.prepare(`SELECT COUNT(*) AS n FROM ${target}`).get()
119
+ columns.forEach(column => {
120
+ const quoted = quoteIdent(column.name)
121
+ let sample = []
122
+ try { sample = db.prepare(`SELECT DISTINCT ${quoted} AS v FROM ${target} WHERE ${quoted} IS NOT NULL LIMIT 3`).all().map(row => String(row.v)) } catch { /* unorderable column */ }
123
+ column.sample = sample
124
+ })
125
+ return { rowCount: Number(count.n), columns }
126
+ }
127
+
128
+ /** List user tables and views with their columns (no row scan). */
129
+ export const listTables = db => db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map(row => {
130
+ const quoted = quoteIdent(row.name)
131
+ let columns = []
132
+ try { columns = columnsOf(db.prepare(`SELECT * FROM ${quoted} LIMIT 0`)) } catch { /* empty view */ }
133
+ return { name: row.name, sqlName: quoted, columns }
134
+ })
135
+
136
+ /** A bounded page for the grid; filter uses a bound parameter, never concatenation. */
137
+ export const pageRelation = (db, { target, offset, limit, sort, filter, filterColumn }) => {
138
+ const columns = columnsOf(db.prepare(`SELECT * FROM ${target} LIMIT 0`))
139
+ const names = columns.map(column => column.name)
140
+ const where = []
141
+ const binds = []
142
+ if (filter) {
143
+ const pattern = `%${filter.replace(/[\\%_]/g, char => `\\${char}`)}%`
144
+ const hit = name => `CAST(${quoteIdent(name)} AS TEXT) LIKE ? ESCAPE '\\'`
145
+ if (filterColumn && names.includes(filterColumn)) { where.push(hit(filterColumn)); binds.push(pattern) }
146
+ else { where.push(`(${names.map(hit).join(' OR ')})`); for (let i = 0; i < names.length; i++) binds.push(pattern) }
147
+ }
148
+ let orderBy = ''
149
+ if (sort && names.includes(sort.column) && ['asc', 'desc'].includes(sort.direction)) orderBy = ` ORDER BY ${quoteIdent(sort.column)} ${sort.direction === 'desc' ? 'DESC' : 'ASC'}`
150
+ const whereSql = where.length ? ` WHERE ${where.join(' AND ')}` : ''
151
+ const total = db.prepare(`SELECT COUNT(*) AS n FROM ${target}${whereSql}`).get(...binds)
152
+ const rows = db.prepare(`SELECT * FROM ${target}${whereSql}${orderBy} LIMIT ? OFFSET ?`).raw().all(...binds, limit, offset).map(raw => raw.map(cellOf))
153
+ return { columns, rows, rowCount: Number(total.n), offset }
154
+ }
155
+
156
+ /** Materialise a SELECT into a results table; return its columns and row count. */
157
+ export const materialise = (db, { sql, table }) => {
158
+ const body = sql.trim().replace(/;+\s*$/, '')
159
+ // SQLite has no CREATE OR REPLACE TABLE, and DDL is transactional here: the
160
+ // drop, the create and the row count all happen in ONE transaction, so a
161
+ // CREATE … AS that hits a bound (SQLITE_FULL/SQLITE_NOMEM) or a syntax error
162
+ // rolls the whole replacement back and the previous table survives intact.
163
+ // `changes` is 0 for CREATE … AS, so count the materialised rows explicitly.
164
+ const replace = db.transaction(() => {
165
+ db.prepare(`DROP TABLE IF EXISTS ${table}`).run()
166
+ db.prepare(`CREATE TABLE ${table} AS ${body}`).run()
167
+ return Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n)
168
+ })
169
+ const rowCount = replace()
170
+ return { columns: columnsOf(db.prepare(`SELECT * FROM ${table} LIMIT 0`)), rowCount }
171
+ }
172
+
173
+ /** Insert parsed rows into a freshly created table (data_load); replace drops first. */
174
+ export const loadRows = (db, { table, columns, sqlTypes, rows }) => {
175
+ // One transaction for drop + create + every insert: a load that fails halfway
176
+ // (a bound, a type error) leaves the previous table in place instead of a
177
+ // half-written replacement.
178
+ const replace = db.transaction(() => {
179
+ db.prepare(`DROP TABLE IF EXISTS ${table}`).run()
180
+ db.prepare(`CREATE TABLE ${table} (${columns.map((name, index) => `${quoteIdent(name)} ${sqlTypes[index]}`).join(', ')})`).run()
181
+ const insert = db.prepare(`INSERT INTO ${table} (${columns.map(quoteIdent).join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`)
182
+ const insertMany = db.transaction(batch => { for (const row of batch) insert.run(...row) })
183
+ insertMany(rows)
184
+ return rows.length
185
+ })
186
+ return { rowCount: replace() }
187
+ }
188
+
189
+ const buildHandlers = () => {
190
+ let db = null
191
+ let dbPath = ''
192
+ return {
193
+ open: message => { dbPath = String(message?.dbPath ?? ''); db = openDb(dbPath, message?.limits ?? {}); return {} },
194
+ close: () => { if (db) { db.close(); db = null } return {} },
195
+ run: message => (message.mode === 'write' ? runWrite(db, message.sql) : runRead(db, message.sql, message.maxRows, message.maxBytes)),
196
+ describe: message => describeRelation(db, message.target),
197
+ listTables: () => listTables(db),
198
+ page: message => pageRelation(db, message),
199
+ materialise: message => materialise(db, message),
200
+ load: message => loadRows(db, message),
201
+ // Internal bookkeeping drop: the target is always an identifier the Host
202
+ // quoted itself (a card/result table), never a statement from a caller.
203
+ drop: message => { db.prepare(`DROP TABLE IF EXISTS ${message.target}`).run(); return { dropped: true } },
204
+ // The database path never leaves this child except redacted out of a message.
205
+ secrets: () => (dbPath.length >= 3 ? [dbPath] : []),
206
+ }
207
+ }
208
+
209
+ /** Attach the IPC handler only when forked as the fixed worker entry. */
210
+ if (process.env.MB_DATA_WORKER === '1' && typeof process.send === 'function') {
211
+ const handlers = buildHandlers()
212
+ process.on('message', message => {
213
+ const { id, op } = message
214
+ try {
215
+ const handler = handlers[op]
216
+ if (!handler) throw Object.assign(new Error(`Unknown op ${op}`), { code: 'invalid_input' })
217
+ process.send({ id, ok: true, value: handler(message) })
218
+ } catch (error) {
219
+ // The model and the panel see a bounded message: no Host paths, no stack,
220
+ // no credentials — only what the statement itself said.
221
+ const code = error?.error?.code ?? error?.code
222
+ process.send({ id, ok: false, error: { message: sanitizeDataMessage(error instanceof Error ? error.message : String(error), handlers.secrets()), code: DATA_ERROR_CODES.includes(code) ? code : 'sql_error' } })
223
+ }
224
+ })
225
+ process.on('disconnect', () => { try { handlers.close() } catch { /* already gone */ } process.exit(0) })
226
+ }
227
+
228
+ export { buildHandlers }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The trusted frame an independent Bot turn runs in.
3
+ *
4
+ * The independent loop owns a transcript, not an identity. Before it may ask a provider for
5
+ * anything, a Host — and only a Host — has to say which committed Bot this turn is, which model
6
+ * that Bot has saved, and that the Bot is still committed, unrevoked and on the independent
7
+ * runtime. This module is that say: the Bot product composes it from its own directory,
8
+ * lifecycle, policy and serving admission, publishes it once, and the loop asks it per step.
9
+ *
10
+ * Three rules are load-bearing:
11
+ *
12
+ * 1. Nothing here accepts an identity from a request. The Bot comes from the argument the
13
+ * dispatch already validated, the subject from the principal directory, the model from the
14
+ * Bot's own committed record, and the runtime from the committed switch. A caller that names
15
+ * a route the Bot has not saved is refused, so a step that started under one model cannot be
16
+ * finished under another.
17
+ * 2. This module holds no money. The stream it returns is the Host's own `llm/stream` entry, so
18
+ * the product's model listener — the same one an integrated turn goes through — applies the
19
+ * Bot policy, re-decides the route per chunk and records the one receipt. A second ledger, or
20
+ * a token-estimated "actual" bill, would be a second authority; there is none here.
21
+ * 3. An outcome the provider did not state is not an answer. A stream that ends without a
22
+ * finish, or with an error, throws: the loop then writes no assistant message and the
23
+ * transcript keeps the honest state instead of a confident-looking reply.
24
+ */
25
+
26
+ import { collectAnswer } from './bot-independent-stream.mjs'
27
+ import { independentSystemMessage } from './independent-message-wire.mjs'
28
+
29
+ const idOk = value => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,128}$/.test(value)
30
+ const fail = (code, message) => { const error = new Error(message ?? code); error.code = code; throw error }
31
+
32
+ /**
33
+ * `provider/model`, or null when the Bot has not committed to a route at all.
34
+ *
35
+ * The split is at the **first** slash only: a Host catalog legitimately serves refs whose model
36
+ * remainder carries a namespace of its own (`openrouter/anthropic/claude-3`), and dropping that
37
+ * remainder would ask for a model nobody committed to.
38
+ */
39
+ export const routeOf = value => {
40
+ if (typeof value !== 'string') return null
41
+ const at = value.indexOf('/')
42
+ if (at <= 0 || at === value.length - 1) return null
43
+ const provider = value.slice(0, at)
44
+ const model = value.slice(at + 1)
45
+ return provider && model ? { provider, model } : null
46
+ }
47
+
48
+ /** What the operator is told when a stream cannot be turned into an answer. */
49
+ const REPLY_REFUSAL = Object.freeze({
50
+ MODEL_STREAM_UNFINISHED: '模型没有给出结束状态,这一轮不写入回答。',
51
+ MODEL_STREAM_ERROR: '模型调用失败,这一轮不写入回答。',
52
+ MODEL_STREAM_ABORTED: '模型调用被中止,这一轮不写入回答。',
53
+ MODEL_STREAM_TRUNCATED: '模型回答被上界截断,未当作完整回答写入。',
54
+ MODEL_STREAM_REASON_UNKNOWN: '模型结束状态无法识别,这一轮不写入回答。',
55
+ MODEL_TOOL_CALL_ID_MISSING: '模型请求的工具没有调用编号,已拒绝。',
56
+ MODEL_TOOL_CALL_NAME_MISSING: '模型请求了一个没有名称的工具,已拒绝。',
57
+ MODEL_TOOL_CALL_ARGS_INVALID: '模型请求的工具参数不是合法 JSON,已拒绝。',
58
+ })
59
+
60
+ /** The closed request shape: nothing here names a session, a runtime or an identity. `conversationId` is the one trusted Host-supplied work-thread correlation the dispatch boundary injects; it is never a model field. */
61
+ const REQUEST_FIELDS = Object.freeze(['botId', 'provider', 'model', 'messages', 'signal', 'system', 'tools', 'maxTokens', 'reasoningEffort', 'onText', 'conversationId'])
62
+
63
+ /**
64
+ * @param {{
65
+ * policy: { modelContext: Function },
66
+ * directory: { forBot: (botId: string) => object },
67
+ * lifecycle: { committedBot: (botId: string) => object | null },
68
+ * serving: { assertMode: (botId: string, mode: string) => string },
69
+ * authority: { assertModel: Function, assertTool: Function },
70
+ * stream: (options: object, context: { botId: string, subject: object }) => AsyncIterable<object>,
71
+ * readyFor?: (botId: string) => boolean,
72
+ * }} options
73
+ */
74
+ export function createBotIndependentExecution({ policy, directory, lifecycle, serving, authority, stream, readyFor = () => true } = {}) {
75
+ if (!policy || typeof policy.modelContext !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_POLICY')
76
+ if (!directory || typeof directory.forBot !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_DIRECTORY')
77
+ if (!lifecycle || typeof lifecycle.committedBot !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_LIFECYCLE')
78
+ if (!serving || typeof serving.assertMode !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_SERVING')
79
+ if (!authority || typeof authority.assertModel !== 'function' || typeof authority.assertTool !== 'function') {
80
+ throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_AUTHORITY')
81
+ }
82
+ if (typeof stream !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_STREAM')
83
+ if (typeof readyFor !== 'function') throw new TypeError('INDEPENDENT_EXECUTION_REQUIRES_READINESS')
84
+
85
+ /**
86
+ * The committed Bot, or a refusal. A deleted Bot, a suspended lifecycle and an unhealthy Host
87
+ * are all "nobody can say", and none of them is the integration path.
88
+ */
89
+ const committed = botId => {
90
+ if (!idOk(botId)) fail('INVALID_BOT_ID')
91
+ const bot = lifecycle.committedBot(botId)
92
+ if (!bot) fail('BOT_NOT_COMMITTED', `无法确认 ${botId} 的已提交状态,本次请求被拒绝。`)
93
+ return bot
94
+ }
95
+
96
+ return Object.freeze({
97
+ /**
98
+ * The route this turn may ask for, read from the Bot's own committed record. A Bot with no
99
+ * saved model has no route: nothing here falls back to a global provider or a default model,
100
+ * and a Host whose model service cannot be reached refuses rather than guessing.
101
+ */
102
+ routeFor(botId) {
103
+ const bot = committed(botId)
104
+ const route = routeOf(bot.model)
105
+ if (!route) fail('BOT_MODEL_UNCOMMITTED', `无法确认 ${botId} 已保存的模型,本次请求被拒绝。`)
106
+ return route
107
+ },
108
+
109
+ /**
110
+ * Re-decide one independent turn before it writes anything: the committed runtime, the
111
+ * committed Bot, a live subject and the Bot's own grant for the model it saved. The loop calls
112
+ * this before every transcript write, every provider step and every tool call, so a Bot that
113
+ * was moved, deleted, revoked or stripped of its grant stops at the next checkpoint instead of
114
+ * finishing — or even writing a user message — on a state that no longer stands.
115
+ */
116
+ assertTurn(botId) {
117
+ serving.assertMode(botId, 'independent')
118
+ const bot = committed(botId)
119
+ const route = routeOf(bot.model)
120
+ if (!route) fail('BOT_MODEL_UNCOMMITTED', `无法确认 ${botId} 已保存的模型,本次请求被拒绝。`)
121
+ const subject = directory.forBot(botId)
122
+ // The grant is re-asserted here, not only when the model call later enters its own frame: an
123
+ // emptied grant must refuse before anything is written to the transcript.
124
+ authority.assertModel(subject, route.provider, route.model)
125
+ authority.assertTool(subject, 'model/infer')
126
+ if(readyFor(botId, route)!==true)fail('BOT_BINDING_UNAVAILABLE','此 Bot 尚未配置可用的默认执行绑定。')
127
+ return true
128
+ },
129
+
130
+ /**
131
+ * One model call for one independent turn, inside the Bot's own frame.
132
+ *
133
+ * The caller passes the route it resolved; this re-reads the committed record and refuses a
134
+ * mismatch, so the saved route identity of one request is never swapped mid-turn. The request
135
+ * shape is closed: a caller cannot open a new path to a session, a runtime or an identity
136
+ * through this trusted facade.
137
+ *
138
+ * @param {{ botId: string, provider: string, model: string, messages: object[], signal?: AbortSignal,
139
+ * system?: unknown, tools?: unknown, maxTokens?: number, reasoningEffort?: string }} request
140
+ * @returns {Promise<{ text: string, toolCalls: object[], source: object }>}
141
+ */
142
+ async complete(request = {}) {
143
+ const { botId, provider, model, messages, signal, system, tools, maxTokens, reasoningEffort, onText, conversationId } = request
144
+ const extra = Object.keys(request).filter(key => !REQUEST_FIELDS.includes(key))
145
+ if (extra.length) fail('INDEPENDENT_REQUEST_UNKNOWN_FIELD', `本次请求带有不受支持的字段:${extra.join(', ')}`)
146
+ if (onText !== undefined && typeof onText !== 'function') fail('INDEPENDENT_PROGRESS_INVALID')
147
+ this.assertTurn(botId)
148
+ const bot = committed(botId)
149
+ const saved = routeOf(bot.model)
150
+ if (!saved || saved.provider !== provider || saved.model !== model) {
151
+ fail('BOT_MODEL_MISMATCH', `已保存模型与本次请求不一致:${botId} 当前为 ${bot.model ?? '未知'},本次 ${provider}/${model} 被拒绝。`)
152
+ }
153
+ // The thinking level belongs to the same committed Bot choice as its
154
+ // route. Omitting it here used to silently select the adapter default,
155
+ // even when the owner had saved another level in the Bot's settings.
156
+ // The actual prepared SDK generation validates capability support; a
157
+ // stale level is refused before provider I/O, never silently replaced.
158
+ const effort = bot.effort
159
+ if (effort !== undefined && (typeof effort !== 'string' || !effort.trim())) fail('BOT_MODEL_EFFORT_INVALID')
160
+ if (reasoningEffort !== undefined && reasoningEffort !== effort) {
161
+ fail('BOT_MODEL_EFFORT_MISMATCH', '本次思考强度与 Bot 已保存的选择不一致,请先在 Bot 设置中保存。')
162
+ }
163
+ if (!Array.isArray(messages)) fail('BOT_TURN_MESSAGES_REQUIRED')
164
+ for (const message of messages) {
165
+ if (!message || !Array.isArray(message.content)) fail('BOT_TURN_MESSAGE_BLOCKS_REQUIRED')
166
+ if (message.role === 'assistant' && (message.source?.kind !== 'model' ||
167
+ typeof message.source.provider !== 'string' || !message.source.provider ||
168
+ typeof message.source.model !== 'string' || !message.source.model)) fail('BOT_TURN_MESSAGE_PROVENANCE_REQUIRED')
169
+ }
170
+ if (signal?.aborted) fail('BOT_LOOP_CANCELLED')
171
+ const subject = directory.forBot(botId)
172
+ const options = {
173
+ provider, model, messages: [independentSystemMessage(bot), ...messages],
174
+ ...(signal ? { signal } : {}),
175
+ ...(system === undefined ? {} : { system }),
176
+ ...(tools === undefined ? {} : { tools }),
177
+ ...(maxTokens === undefined ? {} : { maxTokens }),
178
+ ...(effort === undefined ? {} : { reasoningEffort: effort }),
179
+ }
180
+ // The whole call runs inside the frame, so the product's own model listener sees the
181
+ // committed Bot subject and the turn's cancel — one policy decision, one receipt. The
182
+ // turn's committed Bot and subject travel with the call so a Host-prepared stream can
183
+ // bind the exact adapter generation and admit against it; a plain stream ignores them.
184
+ return policy.modelContext(subject, signal, async () => {
185
+ const reply = await collectAnswer(stream(options, { botId, subject }), { onText })
186
+ // A stream that never finished, failed or asked for something unusable is not an answer:
187
+ // the loop writes nothing, and the receipt keeps the unknown outcome.
188
+ if (reply.failed) fail(reply.failed, REPLY_REFUSAL[reply.failed] ?? reply.failed)
189
+ // The route that produced this answer travels with it, so the next turn's request is a
190
+ // conversation the Host's own model service recognises and can replay.
191
+ return { text: reply.text, toolCalls: reply.toolCalls, source: { kind: 'model', provider, model } }
192
+ }, { botId, ...(typeof conversationId === 'string' ? { conversationId } : {}) })
193
+ },
194
+ })
195
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Read one Host model stream into the small reply shape the independent loop expects.
3
+ *
4
+ * The chunk vocabulary is the Host's own (`text-delta`, `tool-call-delta`, `usage`, `finish`), so
5
+ * an independent turn sees exactly what an integrated one sees. Two older spellings are accepted
6
+ * as well because a fixture or a narrow adapter may emit them; nothing else is invented.
7
+ *
8
+ * The outcome is read, not assumed: a stream that ends without a finish, or with an error or an
9
+ * abort, is reported as failed so the caller writes no answer. Usage is deliberately not read
10
+ * here — the one money authority observes it on the same stream.
11
+ */
12
+
13
+ /**
14
+ * The Host's finish vocabulary. It is merge-extensible, so a kind this module has not been taught
15
+ * is a refusal rather than an answer: an outcome that cannot be read is not a completed turn.
16
+ */
17
+ const FINISH_KINDS = Object.freeze({
18
+ stop: null,
19
+ 'tool-calls': null,
20
+ 'max-tokens': 'MODEL_STREAM_TRUNCATED',
21
+ aborted: 'MODEL_STREAM_ABORTED',
22
+ error: 'MODEL_STREAM_ERROR',
23
+ })
24
+
25
+ const finishFailed = reason => {
26
+ const kind = reason?.kind
27
+ if (typeof kind !== 'string' || !Object.hasOwn(FINISH_KINDS, kind)) return 'MODEL_STREAM_REASON_UNKNOWN'
28
+ return FINISH_KINDS[kind]
29
+ }
30
+
31
+ /**
32
+ * @param {AsyncIterable<object> | Iterable<object>} stream
33
+ * @returns {Promise<{ text: string, toolCalls: object[], failed: string | null }>}
34
+ */
35
+ export async function collectAnswer(stream, { onText } = {}) {
36
+ let text = ''
37
+ const calls = new Map()
38
+ let finished = false
39
+ let failed = null
40
+ for await (const chunk of stream) {
41
+ if (typeof chunk === 'string') { text += chunk; if (onText) await onText(text); continue }
42
+ if (!chunk || typeof chunk !== 'object') continue
43
+ if (chunk.type === 'text-delta' || chunk.type === 'text' || chunk.type === 'delta') {
44
+ if (typeof chunk.text === 'string') { text += chunk.text; if (onText) await onText(text) }
45
+ continue
46
+ }
47
+ if (chunk.type === 'tool-call-delta') {
48
+ const id = typeof chunk.id === 'string' && chunk.id ? chunk.id : null
49
+ if (!id) { failed ??= 'MODEL_TOOL_CALL_ID_MISSING'; continue }
50
+ const call = calls.get(id) ?? { id, name: '', args: '' }
51
+ if (typeof chunk.name === 'string' && chunk.name) call.name = chunk.name
52
+ if (typeof chunk.argumentsDelta === 'string') call.args += chunk.argumentsDelta
53
+ calls.set(id, call)
54
+ continue
55
+ }
56
+ if (chunk.type === 'usage') continue
57
+ if (chunk.type === 'finish') {
58
+ finished = true
59
+ failed ??= finishFailed(chunk.reason)
60
+ }
61
+ }
62
+ if (!finished && failed === null) failed = 'MODEL_STREAM_UNFINISHED'
63
+ const toolCalls = []
64
+ for (const call of calls.values()) {
65
+ if (failed === null && !call.name) failed = 'MODEL_TOOL_CALL_NAME_MISSING'
66
+ let args = {}
67
+ if (call.args) {
68
+ try { args = JSON.parse(call.args) } catch { if (failed === null) failed = 'MODEL_TOOL_CALL_ARGS_INVALID' }
69
+ }
70
+ if (!args || typeof args !== 'object' || Array.isArray(args)) { if (failed === null) failed = 'MODEL_TOOL_CALL_ARGS_INVALID'; args = {} }
71
+ toolCalls.push({ id: call.id, name: call.name, args })
72
+ }
73
+ return { text, toolCalls, failed }
74
+ }