@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,249 @@
1
+ /** Durable display observations. Database and all identity come from the Host. */
2
+ import { mkdirSync } from 'node:fs'
3
+ import { randomUUID, createHash } from 'node:crypto'
4
+ import { dirname } from 'node:path'
5
+ import {
6
+ ACTIVITY_LIMITS, ACTIVITY_STATES, activityIdentity, activityTurnKey, activityCallKey,
7
+ toolEventProjection, activityRecordFromEvent, applyToolEvent, closeTurnRecord,
8
+ requestCancelRecord, confirmStopRecord, projectActivityRecord, assertDisplayOnly, compareActivityRecords,
9
+ } from './native-activity-contract.mjs'
10
+
11
+ export const ACTIVITY_STORE_SCHEMA_VERSION = 1
12
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value)
13
+ const fail = code => { throw Object.assign(new Error(code), { code }) }
14
+ const optionsKeys = ['Database', 'file', 'namespace', 'now', 'ownerId', 'ownerPid', 'isProcessAlive', 'redactToolName', 'limits']
15
+ const label = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,128}$/.test(value)
16
+ const liveProcess = pid => {
17
+ try { process.kill(pid, 0); return true } catch (error) { return error?.code === 'ESRCH' ? false : null }
18
+ }
19
+ const schema = `
20
+ CREATE TABLE IF NOT EXISTS native_activity_meta(namespace TEXT PRIMARY KEY,version INTEGER NOT NULL);
21
+ CREATE TABLE IF NOT EXISTS native_activity_hosts(namespace TEXT PRIMARY KEY,owner_id TEXT NOT NULL,owner_pid INTEGER NOT NULL,lease_token TEXT NOT NULL,acquired_at INTEGER NOT NULL);
22
+ CREATE TABLE IF NOT EXISTS native_activity_turns(namespace TEXT NOT NULL,turn_key TEXT NOT NULL,bot_id TEXT NOT NULL,mode TEXT NOT NULL,conversation_id TEXT NOT NULL,context_id TEXT,identity_json TEXT NOT NULL,host_opened INTEGER NOT NULL,open INTEGER NOT NULL,outcome TEXT,cancel_at INTEGER,stop_at INTEGER,opened_at INTEGER NOT NULL,closed_at INTEGER,PRIMARY KEY(namespace,turn_key));
23
+ CREATE TABLE IF NOT EXISTS native_activity_calls(seq INTEGER PRIMARY KEY AUTOINCREMENT,namespace TEXT NOT NULL,call_key TEXT NOT NULL,turn_key TEXT NOT NULL,bot_id TEXT NOT NULL,mode TEXT NOT NULL,conversation_id TEXT NOT NULL,context_id TEXT,started_at INTEGER NOT NULL,record_json TEXT NOT NULL,UNIQUE(namespace,call_key));
24
+ CREATE INDEX IF NOT EXISTS native_activity_turns_scope ON native_activity_turns(namespace,bot_id,mode,conversation_id,context_id,opened_at,turn_key);
25
+ CREATE INDEX IF NOT EXISTS native_activity_calls_scope ON native_activity_calls(namespace,bot_id,mode,conversation_id,context_id,started_at DESC,call_key DESC);
26
+ CREATE INDEX IF NOT EXISTS native_activity_calls_turn ON native_activity_calls(namespace,turn_key);
27
+ `
28
+
29
+ export function createActivityStore(options = {}) {
30
+ if (!plain(options) || Object.keys(options).some(key => !optionsKeys.includes(key))) fail('ACTIVITY_STORE_OPTIONS_INVALID')
31
+ const { Database, file, namespace = 'default', now = Date.now, ownerId = `host-${randomUUID()}`,
32
+ ownerPid = process.pid, isProcessAlive = liveProcess, redactToolName = null, limits = {} } = options
33
+ if (typeof Database !== 'function') fail('ACTIVITY_STORE_DATABASE_REQUIRED')
34
+ if (typeof file !== 'string' || !file) fail('ACTIVITY_STORE_FILE_REQUIRED')
35
+ if (!label(namespace) || !label(ownerId)) fail('ACTIVITY_STORE_OWNER_INVALID')
36
+ if (!Number.isSafeInteger(ownerPid) || ownerPid < 1 || typeof isProcessAlive !== 'function') fail('ACTIVITY_STORE_LIVENESS_INVALID')
37
+ if (typeof now !== 'function' || redactToolName !== null && typeof redactToolName !== 'function') fail('ACTIVITY_STORE_POLICY_INVALID')
38
+ if (!plain(limits)) fail('ACTIVITY_STORE_LIMITS_INVALID')
39
+ const bound = { ...ACTIVITY_LIMITS }
40
+ for (const [key, value] of Object.entries(limits)) {
41
+ if (!['maxRecords', 'maxTurnsPerConversation', 'maxCallsPerTurn', 'defaultPageSize', 'maxPageSize'].includes(key) || !Number.isSafeInteger(value) || value < 1 || value > ACTIVITY_LIMITS[key]) fail('ACTIVITY_STORE_LIMIT_INVALID')
42
+ bound[key] = value
43
+ }
44
+ bound.defaultPageSize = Math.min(bound.defaultPageSize, bound.maxPageSize)
45
+ const time = () => { const value = now(); if (!Number.isSafeInteger(value) || value < 0) fail('ACTIVITY_TIME_REQUIRED'); return value }
46
+ if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true })
47
+ const db = new Database(file), leaseToken = randomUUID()
48
+ let closed = false, failureCount = 0, lastCode = null, lastAt = null
49
+ const note = code => { failureCount++; lastCode = code; lastAt = time(); return { ok: false, code } }
50
+ const tx = work => {
51
+ db.exec('BEGIN IMMEDIATE')
52
+ try { const value = work(); db.exec('COMMIT'); return value }
53
+ catch (error) { try { db.exec('ROLLBACK') } catch {}; throw error }
54
+ }
55
+ const readTurn = key => db.prepare('SELECT * FROM native_activity_turns WHERE namespace=? AND turn_key=?').get(namespace, key)
56
+ const readCall = key => db.prepare('SELECT * FROM native_activity_calls WHERE namespace=? AND call_key=?').get(namespace, key)
57
+ const recordOf = row => {
58
+ let value
59
+ try { value = JSON.parse(row.record_json) } catch { fail('ACTIVITY_RECORD_CORRUPT') }
60
+ assertDisplayOnly(value)
61
+ const identity = activityIdentity({ botId: value.botId, mode: value.mode, conversationId: value.conversationId,
62
+ contextId: value.contextId, processGeneration: value.processGeneration, turnId: value.turnId })
63
+ if (activityCallKey(identity, value.callId) !== row.call_key || activityTurnKey(identity) !== row.turn_key || value.startedAt !== row.started_at
64
+ || value.botId !== row.bot_id || value.mode !== row.mode || value.conversationId !== row.conversation_id || value.contextId !== row.context_id) fail('ACTIVITY_RECORD_CORRUPT')
65
+ return projectActivityRecord(value)
66
+ }
67
+ const saveRecord = record => db.prepare('UPDATE native_activity_calls SET record_json=? WHERE namespace=? AND call_key=?')
68
+ .run(JSON.stringify(projectActivityRecord(record)), namespace, record.callKey)
69
+ const settleRows = (turnKey, outcome, at) => {
70
+ const rows = db.prepare('SELECT * FROM native_activity_calls WHERE namespace=? AND turn_key=?').all(namespace, turnKey)
71
+ for (const row of rows) saveRecord(closeTurnRecord(recordOf(row), { outcome, at }))
72
+ db.prepare('UPDATE native_activity_turns SET open=0,outcome=?,closed_at=? WHERE namespace=? AND turn_key=?').run(outcome, at, namespace, turnKey)
73
+ return rows.length
74
+ }
75
+ let ownership
76
+ try {
77
+ db.pragma('busy_timeout = 5000')
78
+ db.pragma('synchronous = FULL')
79
+ if (file !== ':memory:' && db.pragma('journal_mode = WAL', { simple: true }) !== 'wal') fail('ACTIVITY_STORE_WAL_REQUIRED')
80
+ db.exec(schema)
81
+ ownership = tx(() => {
82
+ const version = db.prepare('SELECT version FROM native_activity_meta WHERE namespace=?').get(namespace)?.version
83
+ if (version !== undefined && version !== ACTIVITY_STORE_SCHEMA_VERSION) fail('ACTIVITY_STORE_SCHEMA_UNSUPPORTED')
84
+ const previous = db.prepare('SELECT * FROM native_activity_hosts WHERE namespace=?').get(namespace)
85
+ if (previous) {
86
+ let alive
87
+ try { alive = isProcessAlive(previous.owner_pid) } catch { alive = null }
88
+ if (alive !== false) fail('ACTIVITY_STORE_HOST_CONFLICT')
89
+ }
90
+ const at = time(), pending = db.prepare('SELECT turn_key FROM native_activity_turns WHERE namespace=? AND open=1').all(namespace)
91
+ for (const row of pending) settleRows(row.turn_key, 'host-lost', at)
92
+ db.prepare('INSERT OR IGNORE INTO native_activity_meta(namespace,version) VALUES(?,?)').run(namespace, ACTIVITY_STORE_SCHEMA_VERSION)
93
+ db.prepare('INSERT INTO native_activity_hosts(namespace,owner_id,owner_pid,lease_token,acquired_at) VALUES(?,?,?,?,?) ON CONFLICT(namespace) DO UPDATE SET owner_id=excluded.owner_id,owner_pid=excluded.owner_pid,lease_token=excluded.lease_token,acquired_at=excluded.acquired_at')
94
+ .run(namespace, ownerId, ownerPid, leaseToken, at)
95
+ return { adopted: Boolean(previous), strandedTurns: pending.length }
96
+ })
97
+ } catch (error) { try { db.close() } catch {}; throw error }
98
+ const assertOpen = () => { if (closed) fail('ACTIVITY_STORE_CLOSED') }
99
+ const assertLease = () => {
100
+ const current = db.prepare('SELECT lease_token FROM native_activity_hosts WHERE namespace=?').get(namespace)
101
+ if (current?.lease_token !== leaseToken) fail('ACTIVITY_STORE_OWNER_LOST')
102
+ }
103
+ const write = (failureCode, operation) => {
104
+ assertOpen()
105
+ try { return tx(() => { assertLease(); return operation(time()) }) }
106
+ catch (error) { return note(typeof error?.code === 'string' && error.code.startsWith('ACTIVITY_') ? error.code : failureCode) }
107
+ }
108
+ const deleteTurn = key => {
109
+ db.prepare('DELETE FROM native_activity_calls WHERE namespace=? AND turn_key=?').run(namespace, key)
110
+ db.prepare('DELETE FROM native_activity_turns WHERE namespace=? AND turn_key=?').run(namespace, key)
111
+ }
112
+ const reserveTurn = identity => {
113
+ const key = activityTurnKey(identity)
114
+ while (db.prepare('SELECT COUNT(*) AS n FROM native_activity_turns WHERE namespace=? AND bot_id=? AND mode=? AND conversation_id=? AND context_id IS ?').get(namespace, identity.botId, identity.mode, identity.conversationId, identity.contextId).n >= bound.maxTurnsPerConversation) {
115
+ const victim = db.prepare('SELECT turn_key FROM native_activity_turns WHERE namespace=? AND bot_id=? AND mode=? AND conversation_id=? AND context_id IS ? AND open=0 AND turn_key<>? ORDER BY opened_at,turn_key LIMIT 1').get(namespace, identity.botId, identity.mode, identity.conversationId, identity.contextId, key)
116
+ if (!victim) fail('ACTIVITY_CONVERSATION_TURN_LIMIT')
117
+ deleteTurn(victim.turn_key)
118
+ }
119
+ // Also bound empty turns across distinct conversations, not only call rows.
120
+ while (db.prepare('SELECT COUNT(*) AS n FROM native_activity_turns WHERE namespace=?').get(namespace).n >= bound.maxRecords) {
121
+ const victim = db.prepare('SELECT turn_key FROM native_activity_turns WHERE namespace=? AND open=0 AND turn_key<>? ORDER BY opened_at,turn_key LIMIT 1').get(namespace, key)
122
+ if (!victim) fail('ACTIVITY_RECORD_LIMIT')
123
+ deleteTurn(victim.turn_key)
124
+ }
125
+ }
126
+ const reserveCall = turnKey => {
127
+ if (db.prepare('SELECT COUNT(*) AS n FROM native_activity_calls WHERE namespace=? AND turn_key=?').get(namespace, turnKey).n >= bound.maxCallsPerTurn) fail('ACTIVITY_TURN_CALL_LIMIT')
128
+ while (db.prepare('SELECT COUNT(*) AS n FROM native_activity_calls WHERE namespace=?').get(namespace).n >= bound.maxRecords) {
129
+ const victim = db.prepare('SELECT c.call_key FROM native_activity_calls c JOIN native_activity_turns t ON t.namespace=c.namespace AND t.turn_key=c.turn_key WHERE c.namespace=? AND t.open=0 ORDER BY c.started_at,c.call_key LIMIT 1').get(namespace)
130
+ if (!victim) fail('ACTIVITY_RECORD_LIMIT')
131
+ db.prepare('DELETE FROM native_activity_calls WHERE namespace=? AND call_key=?').run(namespace, victim.call_key)
132
+ }
133
+ }
134
+ const insertTurn = (identity, at, hostOpened) => {
135
+ reserveTurn(identity)
136
+ db.prepare('INSERT INTO native_activity_turns(namespace,turn_key,bot_id,mode,conversation_id,context_id,identity_json,host_opened,open,outcome,cancel_at,stop_at,opened_at,closed_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)')
137
+ .run(namespace, activityTurnKey(identity), identity.botId, identity.mode, identity.conversationId, identity.contextId, JSON.stringify(identity), hostOpened ? 1 : 0, hostOpened ? 1 : 0, hostOpened ? null : 'unknown', null, null, at, hostOpened ? null : at)
138
+ }
139
+ const openTurn = value => {
140
+ assertOpen(); const identity = activityIdentity(value), key = activityTurnKey(identity)
141
+ return write('ACTIVITY_TURN_OPEN_FAILED', at => {
142
+ const previous = readTurn(key)
143
+ if (previous) return previous.open ? { ok: true, turnKey: key, alreadyOpen: true } : { ok: false, code: 'ACTIVITY_TURN_ALREADY_CLOSED' }
144
+ insertTurn(identity, at, true)
145
+ return { ok: true, turnKey: key, alreadyOpen: false }
146
+ })
147
+ }
148
+ const closeTurn = (value, report = {}) => {
149
+ assertOpen(); const identity = activityIdentity(value), key = activityTurnKey(identity)
150
+ if (!plain(report) || Object.keys(report).some(key => key !== 'outcome') || !['succeeded', 'failed', 'cancelled', 'unknown', 'refused', 'host-closed', 'host-lost'].includes(report.outcome)) fail('ACTIVITY_TURN_OUTCOME_INVALID')
151
+ return write('ACTIVITY_TURN_CLOSE_FAILED', at => {
152
+ const turn = readTurn(key)
153
+ if (!turn) return { ok: false, code: 'ACTIVITY_TURN_NOT_FOUND' }
154
+ if (!turn.open) return turn.outcome === report.outcome ? { ok: true, turnKey: key, alreadyClosed: true } : { ok: false, code: 'ACTIVITY_TURN_OUTCOME_CONFLICT' }
155
+ const settled = settleRows(key, report.outcome, at)
156
+ return { ok: true, turnKey: key, settled }
157
+ })
158
+ }
159
+ const recordToolEvent = (value, event) => {
160
+ assertOpen(); const identity = activityIdentity(value), projection = toolEventProjection(event, redactToolName ? { redactToolName } : null)
161
+ if (projection.callId === null) return note('ACTIVITY_CALL_ID_MISSING')
162
+ const callKey = activityCallKey(identity, projection.callId), turnKey = activityTurnKey(identity)
163
+ return write('ACTIVITY_EVENT_WRITE_FAILED', at => {
164
+ let turn = readTurn(turnKey)
165
+ if (!turn) { insertTurn(identity, at, false); turn = readTurn(turnKey) }
166
+ const previous = readCall(callKey), turnOpen = turn.open === 1 && turn.host_opened === 1
167
+ if (previous) {
168
+ const applied = applyToolEvent(recordOf(previous), projection, { turnOpen, at })
169
+ saveRecord(applied.record)
170
+ return { ok: true, created: false, reason: applied.reason, call: projectActivityRecord(applied.record) }
171
+ }
172
+ reserveCall(turnKey)
173
+ let record = activityRecordFromEvent(identity, projection, at)
174
+ if (turn.cancel_at !== null) record = requestCancelRecord(record, turn.cancel_at).record
175
+ if (turn.stop_at !== null) record = confirmStopRecord(record, turn.stop_at).record
176
+ if (!turnOpen) record = closeTurnRecord(record, { outcome: turn.outcome ?? 'unknown', at })
177
+ if (projection.phase === 'completed') record = applyToolEvent(record, projection, { turnOpen, at }).record
178
+ record = projectActivityRecord(record)
179
+ db.prepare('INSERT INTO native_activity_calls(namespace,call_key,turn_key,bot_id,mode,conversation_id,context_id,started_at,record_json) VALUES(?,?,?,?,?,?,?,?,?)')
180
+ .run(namespace, callKey, turnKey, identity.botId, identity.mode, identity.conversationId, identity.contextId, record.startedAt, JSON.stringify(record))
181
+ return { ok: true, created: true, reason: 'recorded', call: record }
182
+ })
183
+ }
184
+ const fact = (value, callId, kind) => {
185
+ assertOpen(); const identity = activityIdentity(value), key = activityTurnKey(identity)
186
+ const selected = callId === undefined || callId === null ? null : activityCallKey(identity, callId)
187
+ return write(kind === 'cancel' ? 'ACTIVITY_CANCEL_FAILED' : 'ACTIVITY_STOP_FAILED', at => {
188
+ const turn = readTurn(key)
189
+ if (!turn) return { ok: false, code: 'ACTIVITY_TURN_NOT_FOUND' }
190
+ const rows = selected ? [readCall(selected)].filter(Boolean) : db.prepare('SELECT * FROM native_activity_calls WHERE namespace=? AND turn_key=?').all(namespace, key)
191
+ if (selected && !rows.length) return { ok: false, code: 'ACTIVITY_CALL_NOT_FOUND' }
192
+ for (const row of rows) saveRecord((kind === 'cancel' ? requestCancelRecord : confirmStopRecord)(recordOf(row), at).record)
193
+ if (!selected) db.prepare(`UPDATE native_activity_turns SET ${kind === 'cancel' ? 'cancel_at' : 'stop_at'}=COALESCE(${kind === 'cancel' ? 'cancel_at' : 'stop_at'},?) WHERE namespace=? AND turn_key=?`).run(at, namespace, key)
194
+ return { ok: true, updated: rows.length }
195
+ })
196
+ }
197
+ const scopeHash = scope => createHash('sha256').update(JSON.stringify([namespace, scope.botId, scope.mode, scope.conversationId, scope.contextId])).digest('hex')
198
+ const page = (query = {}) => {
199
+ assertOpen(); assertLease()
200
+ if (!plain(query) || Object.keys(query).some(key => !['botId', 'mode', 'conversationId', 'contextId', 'cursor', 'limit'].includes(key))) fail('ACTIVITY_QUERY_INVALID')
201
+ const { botId, mode, conversationId, contextId = null, cursor = null, limit = bound.defaultPageSize } = query
202
+ activityIdentity({ botId, mode, conversationId, contextId, processGeneration: 'scope-validation', turnId: 'scope-validation' })
203
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > bound.maxPageSize) fail('ACTIVITY_QUERY_LIMIT_INVALID')
204
+ const scope = { botId, mode, conversationId, contextId }, hash = scopeHash(scope)
205
+ let boundary = null
206
+ if (cursor !== null) {
207
+ if (typeof cursor !== 'string' || cursor.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(cursor)) fail('ACTIVITY_CURSOR_INVALID')
208
+ try { boundary = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')) } catch { fail('ACTIVITY_CURSOR_INVALID') }
209
+ if (!plain(boundary) || Object.keys(boundary).sort().join(',') !== 'at,f,k,s,v' || boundary.v !== 2 || boundary.s !== hash || !Number.isSafeInteger(boundary.at) || boundary.at < 0 || !Number.isSafeInteger(boundary.f) || boundary.f < 0 || !/^call1-[a-f0-9]{64}$/.test(boundary.k ?? '') || Buffer.from(JSON.stringify(boundary)).toString('base64url') !== cursor) fail('ACTIVITY_CURSOR_INVALID')
210
+ }
211
+ const fence = boundary?.f ?? db.prepare('SELECT COALESCE(MAX(seq),0) AS n FROM native_activity_calls WHERE namespace=?').get(namespace).n
212
+ const args = [namespace, botId, mode, conversationId, contextId, fence]
213
+ let sql = 'SELECT * FROM native_activity_calls WHERE namespace=? AND bot_id=? AND mode=? AND conversation_id=? AND context_id IS ? AND seq<=?'
214
+ if (boundary) { sql += ' AND (started_at<? OR (started_at=? AND call_key<?))'; args.push(boundary.at, boundary.at, boundary.k) }
215
+ sql += ' ORDER BY started_at DESC,call_key DESC LIMIT ?'; args.push(limit + 1)
216
+ const rows = db.prepare(sql).all(...args), hasMore = rows.length > limit, items = rows.slice(0, limit).map(recordOf), last = items.at(-1)
217
+ const nextCursor = hasMore && last ? Buffer.from(JSON.stringify({ v: 2, at: last.startedAt, k: last.callKey, f: fence, s: hash })).toString('base64url') : null
218
+ return Object.freeze({ schemaVersion: 1, items: Object.freeze(items), nextCursor, limit })
219
+ }
220
+ const health = () => Object.freeze({ failures: failureCount, lastCode, lastAt, closed })
221
+ const stats = () => {
222
+ assertOpen(); assertLease()
223
+ const byState = Object.fromEntries(ACTIVITY_STATES.map(state => [state, 0]))
224
+ for (const row of db.prepare('SELECT record_json FROM native_activity_calls WHERE namespace=?').all(namespace)) {
225
+ const value = JSON.parse(row.record_json); if (!Object.hasOwn(byState, value.state)) fail('ACTIVITY_RECORD_CORRUPT'); byState[value.state]++
226
+ }
227
+ return Object.freeze({ namespace, ownerId, ...ownership, calls: Object.values(byState).reduce((a, b) => a + b, 0),
228
+ turns: db.prepare('SELECT COUNT(*) AS n FROM native_activity_turns WHERE namespace=?').get(namespace).n,
229
+ openTurns: db.prepare('SELECT COUNT(*) AS n FROM native_activity_turns WHERE namespace=? AND open=1').get(namespace).n,
230
+ byState: Object.freeze(byState), limits: Object.freeze({ ...bound }), failures: failureCount })
231
+ }
232
+ const close = () => {
233
+ if (closed) return { closed: true, alreadyClosed: true }
234
+ try {
235
+ tx(() => {
236
+ const lease = db.prepare('SELECT lease_token FROM native_activity_hosts WHERE namespace=?').get(namespace)
237
+ if (lease?.lease_token !== leaseToken) return
238
+ const at = time()
239
+ for (const turn of db.prepare('SELECT turn_key FROM native_activity_turns WHERE namespace=? AND open=1').all(namespace)) settleRows(turn.turn_key, 'host-closed', at)
240
+ db.prepare('DELETE FROM native_activity_hosts WHERE namespace=? AND lease_token=?').run(namespace, leaseToken)
241
+ })
242
+ } catch { note('ACTIVITY_STORE_RELEASE_FAILED') }
243
+ finally { closed = true; try { db.close() } catch { note('ACTIVITY_STORE_CLOSE_FAILED') } }
244
+ return { closed: true, failures: failureCount }
245
+ }
246
+ return Object.freeze({ schemaVersion: 1, namespace, ownerId, ...ownership, openTurn, closeTurn, recordToolEvent,
247
+ requestCancel: (identity, id) => fact(identity, id, 'cancel'), confirmStop: (identity, id) => fact(identity, id, 'stop'), page, stats, health, close })
248
+ }
249
+ export { ACTIVITY_LIMITS as ACTIVITY_STORE_LIMITS, compareActivityRecords }
@@ -0,0 +1,78 @@
1
+ import { independentToolEntry } from './independent-tool-catalog.mjs'
2
+
3
+ // This fixed catalog uses nullable type arrays and numeric bounds, which the
4
+ // installed SDK's narrower JSON schema validator does not accept.
5
+ function parseToolArgs(entry, args) {
6
+ const fail = () => { throw Object.assign(new Error('INVALID_TOOL_ARGS'), { code: 'INVALID_TOOL_ARGS' }) }
7
+ const { properties, required } = entry.parameters
8
+ if (!args || Object.getPrototypeOf(args) !== Object.prototype || Object.keys(args).some(key => !Object.hasOwn(properties, key))
9
+ || required.some(key => !Object.hasOwn(args, key)) || Buffer.byteLength(JSON.stringify(args)) > 128_000) fail()
10
+ for (const [key, value] of Object.entries(args)) {
11
+ const schema = properties[key], types = [schema.type].flat()
12
+ const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value
13
+ const integer = types.includes('integer') && Number.isSafeInteger(value)
14
+ if (!types.includes(kind) && !integer) fail()
15
+ if (schema.enum && !schema.enum.includes(value)) fail()
16
+ if (schema.items && value.some(item => typeof item !== schema.items.type)) fail()
17
+ if (schema.minimum !== undefined && value < schema.minimum || schema.maximum !== undefined && value > schema.maximum) fail()
18
+ }
19
+ return args
20
+ }
21
+
22
+ const reference = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,128}$/.test(value) ? value : null
23
+
24
+ /** Native MCP calls use the same Bot tool service and durable Task identities. */
25
+ export function createNativeConversationTools({ view, validate, redact = text => text, entryFor = independentToolEntry, render = null }) {
26
+ const handlers = view.schemas().map(schema => {
27
+ const entry = entryFor(schema.name)
28
+ return {
29
+ definition: { name: schema.name, description: schema.description, inputSchema: schema.parameters },
30
+ capability: entry.capability ?? entry.grants[0],
31
+ parse: args => parseToolArgs(entry, args),
32
+ ...(render ? { render: value => render(value, redact) } : {}),
33
+ async execute(args, context) {
34
+ const turn = context.context
35
+ if (!turn?.turnId || !turn.callId || !context.signal) throw new Error('NATIVE_TOOL_TURN_REQUIRED')
36
+ context.signal.throwIfAborted()
37
+ await validate()
38
+ context.signal.throwIfAborted()
39
+ let value
40
+ try { value = await view.call(schema.name, args, { turnId: turn.turnId, callId: turn.callId, signal: context.signal }) }
41
+ catch (error) {
42
+ if (render && reference(error.operationId)) {
43
+ value = { status: 'unknown', operationId: error.operationId, value: null,
44
+ error: /^[A-Z_]{1,80}$/.test(error.code ?? '') ? error.code : 'NATIVE_REMOTE_OUTCOME_UNKNOWN',
45
+ nextAction: 'Query this original operation with bot_remote_operation_status before taking further action.' }
46
+ } else {
47
+ if (!entry.backend || !reference(error.taskId)) throw error
48
+ // A tracked write keeps its identity even when the result is unknown.
49
+ // The native model can inspect that Task without submitting another.
50
+ value = { outcome: error.code === 'TASK_OUTCOME_UNKNOWN' ? 'needs-reconciliation' : 'failed',
51
+ code: /^[A-Z_]{1,80}$/.test(error.code ?? '') ? error.code : 'TASK_NOT_COMPLETED',
52
+ taskId: error.taskId, attemptId: reference(error.attemptId), operationId: reference(error.operationId),
53
+ nextAction: 'Query this original Task with bot_task_status before taking further action. An unknown write must be reconciled before it is repeated.' }
54
+ }
55
+ }
56
+ context.signal.throwIfAborted()
57
+ return render ? value : redactNativeToolValue(value, redact)
58
+ },
59
+ }
60
+ })
61
+ return { tools: handlers.map(handler => handler.definition.name), handlers }
62
+ }
63
+
64
+ /** Preserve JSON types while redacting string values from Host results. */
65
+ export function redactNativeToolValue(value, redact) {
66
+ if (typeof value === 'string') return redact(value)
67
+ if (Array.isArray(value)) return value.map(item => redactNativeToolValue(item, redact))
68
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactNativeToolValue(item, redact)]))
69
+ return value ?? null
70
+ }
71
+
72
+ export function renderNativeRemoteResult(value, redact) {
73
+ const { image, ...metadata } = value
74
+ return { content: [
75
+ { type: 'text', text: JSON.stringify(redactNativeToolValue(metadata, redact)) },
76
+ ...(image ? [{ type: 'image', mimeType: image.mimeType, data: image.data }] : []),
77
+ ], ...(value.status !== 'succeeded' || value.error ? { isError: true } : {}) }
78
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The native MCP half of the five Bot data tools.
3
+ *
4
+ * This is a bridge to the existing Bot data service (one SQLite database per
5
+ * Bot, in the fixed worker of bot-data-engine.mjs) — not a claim of DuckDB or
6
+ * upstream parity. It offers the model exactly the tools the shared contract
7
+ * (data-tool-contract.mjs) describes, with a real nested chart schema, and it
8
+ * runs every call under the same Host authority the SDK door uses.
9
+ *
10
+ * What this module refuses to do, and why:
11
+ * - It never opens an engine for a Bot that has no grant or no committed
12
+ * configuration: an absent authority yields NO tools, not tools that fail
13
+ * later. That is decided once, at catalog construction, from committed state.
14
+ * - It never accepts a Bot, a path, an endpoint, a credential or an operation
15
+ * identity from the model. The Bot is re-derived from the same Host subject
16
+ * on every execute, before the engine is touched and again after the awaited
17
+ * work, so a revocation or a Bot change that lands mid-call withholds the
18
+ * result instead of returning data the Host no longer stands behind.
19
+ * - It never retries. A timed-out, cancelled or failed engine call is reported
20
+ * as it happened: a write whose outcome is unknown is not repeated, and a
21
+ * cancelled call's result is withheld without claiming the write never ran.
22
+ * - It never invents a second database, a Task authority or a request
23
+ * identity. The data engine owns the child lifecycle and the writes; the
24
+ * turn/call identity comes from the Host turn context, not from the model.
25
+ */
26
+
27
+ import { DataFailure, sanitizeDataMessage } from './bot-data-surface.mjs'
28
+ import { redactNativeToolValue } from './native-conversation-tools.mjs'
29
+ import { DATA_TOOL_ENTRIES, invokeDataTool, parseDataToolArgs } from './data-tool-contract.mjs'
30
+
31
+ /** A structured failure the model can read; never a silent success. */
32
+ const errorOf = error => {
33
+ if (error instanceof DataFailure) {
34
+ // The engine already collapses paths and stack frames; sanitising again is
35
+ // cheap and keeps a library-reader message from leaking a Host path.
36
+ return { ...error.error, message: sanitizeDataMessage(error.error?.message ?? error.message) }
37
+ }
38
+ return { code: 'engine_unavailable', message: sanitizeDataMessage(error?.message ?? error) }
39
+ }
40
+
41
+ /**
42
+ * @param {{ access: object, subject: object, botId: string, validate: () => Promise<void>|void,
43
+ * redact?: (text: string) => string }} options
44
+ * @returns {{ tools: string[], handlers: object[] }} native MCP tool names and handlers
45
+ */
46
+ export function createNativeDataTools({ access, subject, botId, validate, redact = text => text } = {}) {
47
+ if (!access || typeof access.botIdForSubject !== 'function' || typeof access.forSubject !== 'function') {
48
+ throw new TypeError('NATIVE_DATA_TOOLS_REQUIRE_ACCESS')
49
+ }
50
+ if (typeof botId !== 'string' || botId === '') throw new TypeError('NATIVE_DATA_TOOLS_REQUIRE_BOT_ID')
51
+ if (typeof validate !== 'function') throw new TypeError('NATIVE_DATA_TOOLS_REQUIRE_VALIDATE')
52
+
53
+ // The catalog is decided from committed authority, and it opens no engine to
54
+ // find out: a Bot without the grant, without a committed configuration, or a
55
+ // subject that does not own this Bot is simply offered nothing.
56
+ let admitted = false
57
+ try { admitted = access.botIdForSubject(subject) === botId } catch { admitted = false }
58
+ if (!admitted) return { tools: [], handlers: [] }
59
+
60
+ /** Re-derive this call's Bot from the same Host subject; throws when it changed. */
61
+ const liveBotId = () => {
62
+ const current = access.botIdForSubject(subject)
63
+ if (current !== botId) {
64
+ throw Object.assign(new Error('The calling Bot changed while this data call was running, so its result is withheld.'), { code: 'DATA_TOOL_BOT_CHANGED' })
65
+ }
66
+ return current
67
+ }
68
+
69
+ const handlers = DATA_TOOL_ENTRIES.map(entry => ({
70
+ definition: { name: entry.name, description: entry.description, inputSchema: entry.inputSchema },
71
+ capability: entry.capability,
72
+ parse: args => parseDataToolArgs(entry.name, args),
73
+ async execute(args, context) {
74
+ const turn = context?.context
75
+ if (!turn?.turnId || !turn.callId || !context?.signal) throw new Error('NATIVE_TOOL_TURN_REQUIRED')
76
+ const { signal } = context
77
+ // An aborted or un-admitted call reaches no engine at all.
78
+ signal.throwIfAborted()
79
+ await validate()
80
+ signal.throwIfAborted()
81
+ liveBotId()
82
+ const bot = access.forSubject(subject, { signal })
83
+ let value
84
+ try { value = await invokeDataTool(bot, entry.name, args) } catch (error) { value = { error: errorOf(error) } }
85
+ // The work is done, so its outcome may be unknown: withhold the result
86
+ // rather than assert either that it happened or that it never ran.
87
+ signal.throwIfAborted()
88
+ await validate()
89
+ signal.throwIfAborted()
90
+ liveBotId()
91
+ return redactNativeToolValue(value, redact)
92
+ },
93
+ render: value => ({
94
+ content: [{ type: 'text', text: JSON.stringify(redactNativeToolValue(value, redact)) }],
95
+ ...(value && typeof value === 'object' && value.error ? { isError: true } : {}),
96
+ }),
97
+ }))
98
+
99
+ return { tools: handlers.map(handler => handler.definition.name), handlers }
100
+ }