@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,120 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { mkdirSync } from 'node:fs'
3
+ import { dirname } from 'node:path'
4
+ import { TEAM_CODES } from './team-tool-contract.mjs'
5
+
6
+ const fail = code => { throw Object.assign(new Error(code), { code }) }
7
+ const hash = value => createHash('sha256').update(value).digest('hex')
8
+ const alive = pid => { try { process.kill(pid, 0); return true } catch (error) { return error.code !== 'ESRCH' } }
9
+ const terminal = row => ['settled', 'cancelled', 'unknown'].includes(row.state)
10
+ export const computeBriefHash = (mode, message, doneWhen, consult) => hash(JSON.stringify([mode, message, doneWhen ?? null, consult === true]))
11
+ export const standingThreadKey = (parent, target, origin = '') => `standing-${hash(JSON.stringify([parent, target, origin])).slice(0, 40)}`
12
+ export const workThreadConversationKey = (target, thread) => hash(JSON.stringify([target, thread])).slice(0, 48)
13
+ export const assertNoCycle = (lineage, target) => { if (lineage.includes(target)) fail(TEAM_CODES.CYCLE) }
14
+ // Upstream's depth is the number of delegation edges, not the root Bot itself.
15
+ export const assertDepth = (lineage, maxDepth) => { if (!Number.isSafeInteger(maxDepth) || maxDepth < 1) fail('TEAM_DEPTH_LIMIT_INVALID'); if (lineage.length > maxDepth) fail(TEAM_CODES.TOO_DEEP) }
16
+
17
+ /** Durable outbox, with one live Host owner. State writes never prove execution.
18
+ * The exact target request receipt is the only way to settle a dispatched row.
19
+ * A new schema leaves the original, unreleased Step journal intact for inspection.
20
+ */
21
+ export function createTeamAssignmentStore({ Database, file, now = Date.now, maxRows = 65536, isProcessAlive = alive, ownerId = randomUUID() } = {}) {
22
+ if (typeof Database !== 'function' || typeof file !== 'string' || !file) fail('TEAM_STORE_HOST_REQUIRED')
23
+ if (!Number.isSafeInteger(maxRows) || maxRows < 1) fail('TEAM_STORE_INVALID_LIMIT')
24
+ if (file !== ':memory:') mkdirSync(dirname(file), { recursive: true })
25
+ const db = new Database(file)
26
+ let closed = false
27
+ try {
28
+ db.pragma('busy_timeout=5000'); db.pragma('synchronous=FULL')
29
+ if (file !== ':memory:' && db.pragma('journal_mode=WAL', { simple: true }) !== 'wal') fail('TEAM_STORE_WAL_REQUIRED')
30
+ db.exec(`CREATE TABLE IF NOT EXISTS bot_team_owner_v2 (slot INTEGER PRIMARY KEY CHECK(slot=1), token TEXT, pid INTEGER, active INTEGER NOT NULL);
31
+ CREATE TABLE IF NOT EXISTS bot_team_rows_v2 (id TEXT PRIMARY KEY, parent TEXT NOT NULL, origin TEXT NOT NULL, state TEXT NOT NULL, created INTEGER NOT NULL, data TEXT NOT NULL);
32
+ CREATE INDEX IF NOT EXISTS bot_team_rows_parent_v2 ON bot_team_rows_v2(parent,origin,created);
33
+ CREATE TABLE IF NOT EXISTS bot_team_batches_v2 (parent TEXT NOT NULL, origin TEXT NOT NULL, request TEXT NOT NULL, hash TEXT NOT NULL, ids TEXT NOT NULL, PRIMARY KEY(parent,origin,request));`)
34
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='bot_team_assignments'").get() && db.prepare('SELECT count(*) n FROM bot_team_assignments').get().n) fail('TEAM_LEGACY_JOURNAL_RECONCILIATION_REQUIRED')
35
+ db.transaction(() => {
36
+ const owner = db.prepare('SELECT * FROM bot_team_owner_v2 WHERE slot=1').get()
37
+ if (owner?.active && isProcessAlive(owner.pid)) fail('TEAM_STORE_ALREADY_OWNED')
38
+ db.prepare('INSERT OR REPLACE INTO bot_team_owner_v2(slot,token,pid,active) VALUES(1,?,?,1)').run(ownerId, process.pid)
39
+ }).immediate()
40
+ } catch (error) { db.close(); throw error }
41
+ const assertOpen = () => {
42
+ if (closed) fail('TEAM_STORE_CLOSED')
43
+ if (db.prepare('SELECT token FROM bot_team_owner_v2 WHERE slot=1 AND active=1').get()?.token !== ownerId) fail('TEAM_STORE_OWNER_LOST')
44
+ }
45
+ const read = id => { const row = db.prepare('SELECT data FROM bot_team_rows_v2 WHERE id=?').get(id); return row ? JSON.parse(row.data) : null }
46
+ const all = () => { assertOpen(); return db.prepare('SELECT data FROM bot_team_rows_v2 ORDER BY created,id').all().map(row => JSON.parse(row.data)) }
47
+ const save = row => db.prepare('INSERT OR REPLACE INTO bot_team_rows_v2(id,parent,origin,state,created,data) VALUES(?,?,?,?,?,?)').run(row.assignmentId, row.parentBot, row.originKey, row.state, row.createdAt, JSON.stringify(row))
48
+ const mutate = (id, predicate, change) => {
49
+ assertOpen()
50
+ return db.transaction(() => { assertOpen(); const row = read(id); if (!row || !predicate(row)) return null; change(row); row.updatedAt = now(); save(row); return row }).immediate()
51
+ }
52
+ const createBatch = (specs, { parentBot = specs[0]?.parentBot, originKey = specs[0]?.originKey ?? '', requestKey, payloadHash } = {}) => {
53
+ assertOpen()
54
+ if (!Array.isArray(specs) || !specs.length || specs.length > 4 || !requestKey) fail(TEAM_CODES.INVALID_ARGS)
55
+ const identity = payloadHash ?? hash(JSON.stringify(specs.map(({ workThreadKey, ...spec }) => ({ ...spec, thread: spec.requestedThread ?? workThreadKey }))))
56
+ return db.transaction(() => {
57
+ assertOpen()
58
+ const batch = db.prepare('SELECT * FROM bot_team_batches_v2 WHERE parent=? AND origin=? AND request=?').get(parentBot, originKey, requestKey)
59
+ if (batch) return batch.hash === identity ? { status: 'replayed', assignments: JSON.parse(batch.ids).map(read) } : { status: 'conflict' }
60
+ if (db.prepare('SELECT count(*) n FROM bot_team_rows_v2').get().n + specs.length > maxRows) fail('TEAM_STORE_ROW_LIMIT')
61
+ const rows = all()
62
+ const decisions = specs.map(spec => {
63
+ const match = rows.slice().reverse().find(row => row.parentBot === parentBot && row.originKey === originKey && row.targetBot === spec.targetBot && row.workThreadKey === spec.workThreadKey && row.mode === spec.mode && (spec.rework || row.briefHash === spec.briefHash && (row.parentAssignmentId ?? row.createdByTurn) === (spec.parentAssignmentId ?? spec.createdByTurn)))
64
+ if (match && spec.rework && !terminal(match) && !match.outcome) fail(TEAM_CODES.REWORK_NOT_TERMINAL)
65
+ if (match && !spec.rework) return { row: match, duplicate: true }
66
+ const timestamp = now()
67
+ const assignmentId = `assign_${randomUUID()}`
68
+ return { row: { ...spec, requestKey, originKey, assignmentId, rootAssignmentId: spec.rootAssignmentId ?? assignmentId, reworkOf: match?.assignmentId ?? null, state: 'queued', outcome: null, resultText: null, resultCode: null, targetRef: null, targetRequestId: null, resumeState: spec.reply === 'none' ? 'none' : 'waiting', cancelRequestedAt: null, createdAt: timestamp, updatedAt: timestamp } }
69
+ })
70
+ // All decisions, including rework validity, are complete before the first insert.
71
+ for (const { row, duplicate } of decisions) if (!duplicate) save(row)
72
+ db.prepare('INSERT INTO bot_team_batches_v2(parent,origin,request,hash,ids) VALUES(?,?,?,?,?)').run(parentBot, originKey, requestKey, identity, JSON.stringify(decisions.map(({ row }) => row.assignmentId)))
73
+ return { status: 'created', assignments: decisions.map(({ row, duplicate }) => ({ ...row, ...(duplicate ? { duplicate: true } : {}) })) }
74
+ }).immediate()
75
+ }
76
+ const create = spec => { const result = createBatch([spec], { requestKey: spec.requestKey }); return { status: result.status, assignment: result.assignments?.[0] } }
77
+ return Object.freeze({
78
+ ready: true, create, createBatch,
79
+ get(id) { assertOpen(); return read(id) },
80
+ byRequestKey(parent, request, origin = '') { assertOpen(); const batch = db.prepare('SELECT ids FROM bot_team_batches_v2 WHERE parent=? AND origin=? AND request=?').get(parent, origin, request); return batch ? read(JSON.parse(batch.ids)[0]) : null },
81
+ listByParent: (parent, { includeTerminal = false, originKey } = {}) => all().filter(row => row.parentBot === parent && (originKey === undefined || row.originKey === originKey) && (includeTerminal || !terminal(row))),
82
+ threadsByParent(parent, originKey) { const seen = new Map(); for (const row of all().reverse().filter(row => row.parentBot === parent && (originKey === undefined || row.originKey === originKey))) if (!seen.has(row.workThreadKey)) seen.set(row.workThreadKey, row); return [...seen.values()] },
83
+ activeFor(botId, origin) {
84
+ const rows = all(), matches = ref => origin.sessionId ? ref?.sessionId === origin.sessionId : ref?.conversationId === origin.conversationId
85
+ const running = rows.find(row => row.state === 'running' && row.targetBot === botId && matches(row.targetRef))
86
+ if (running) return running
87
+ const uncertain = rows.find(row => row.targetActive === true && row.targetBot === botId && matches(row.targetRef))
88
+ if (uncertain) return uncertain
89
+ // A child result resumes its caller's old work request, even if that
90
+ // request already settled. This frame is durable across awaits and is
91
+ // independent of delivery state: a stop must not erase the execution gate.
92
+ const resume = rows.find(row => row.parentBot === botId && row.resumeActive === true && matches(row.origin))
93
+ if (!resume) return null
94
+ const parent = resume.parentAssignmentId ? read(resume.parentAssignmentId) : null
95
+ return { ...(parent ?? { targetBot: botId, parentBot: botId, lineage: [botId], rootBot: botId, depth: 0, mode: 'work', origin: resume.origin }),
96
+ resumeAssignmentId: resume.assignmentId, cancelRequestedAt: resume.cancelRequestedAt ?? parent?.cancelRequestedAt ?? null }
97
+ },
98
+ dispatchable: () => all().filter(row => row.state === 'queued'),
99
+ rowsForReconcile: () => all().filter(row => row.state === 'running'),
100
+ resumeDue: () => all().filter(row => row.state === 'waiting' && row.resumeState === 'waiting' && row.cancelRequestedAt === null),
101
+ all,
102
+ admit: id => mutate(id, row => row.state === 'queued', row => { row.state = 'admitting' }),
103
+ beginDispatch: (id, ref, requestId) => mutate(id, row => row.state === 'admitting' && !row.cancelRequestedAt, row => { row.state = 'running'; row.targetRef = ref; row.targetRequestId = requestId; row.targetActive = true }),
104
+ settle: (id, { outcome, resultText = null, resultCode = null }) => mutate(id, row => row.state === 'running', row => { row.state = row.reply === 'none' || row.cancelRequestedAt !== null ? 'settled' : 'waiting'; row.outcome = outcome; row.resultText = resultText; row.resultCode = resultCode; row.targetActive = false; if (row.cancelRequestedAt !== null) row.resumeState = 'withheld' }),
105
+ markResumeSending: id => Boolean(mutate(id, row => row.state === 'waiting' && row.resumeState === 'waiting' && row.cancelRequestedAt === null, row => { row.resumeState = 'sending'; row.resumeActive = true })),
106
+ completeResume: (id, state, { executionFinished = true } = {}) => mutate(id, row => row.state === 'waiting' && row.resumeActive === true, row => {
107
+ row.state = 'settled'; row.resumeState = row.cancelRequestedAt !== null ? 'withheld' : state
108
+ row.resumeActive = !executionFinished
109
+ if (!executionFinished) row.cancelRequestedAt ??= now()
110
+ }),
111
+ rewarmResume: id => Boolean(mutate(id, row => row.state === 'waiting' && row.resumeState === 'sending' && row.cancelRequestedAt === null, row => { row.resumeState = 'waiting'; row.resumeActive = false })),
112
+ rewarm: id => mutate(id, row => row.state === 'running' && row.cancelRequestedAt === null, row => { row.state = 'queued'; row.targetRequestId = null; row.targetRef = null; row.targetActive = false }),
113
+ cancel: (id, { resultCode = null } = {}) => mutate(id, row => ['queued', 'admitting'].includes(row.state), row => { row.state = 'cancelled'; row.outcome = 'cancelled'; row.resultCode = resultCode; row.resumeState = 'withheld' }),
114
+ requestCancel: id => mutate(id, row => !terminal(row), row => { row.cancelRequestedAt ??= now(); if (['queued', 'admitting'].includes(row.state)) { row.state = 'cancelled'; row.outcome = 'cancelled' }; row.resumeState = 'withheld' }),
115
+ fenceUnknown: (id, code, { executionFinished = false } = {}) => mutate(id, row => ['admitting', 'running'].includes(row.state), row => { row.state = 'unknown'; row.outcome = 'unknown'; row.resultCode = code ?? TEAM_CODES.OUTCOME_UNKNOWN; row.resumeState = 'withheld'; row.targetActive = row.targetActive === true && !executionFinished; if (row.targetActive) row.cancelRequestedAt ??= now() }),
116
+ recover() { let fenced = 0; for (const row of all()) { if (row.state === 'admitting') { this.fenceUnknown(row.assignmentId, 'TEAM_ADMISSION_INTERRUPTED'); fenced++ } else if (row.resumeActive === true) { mutate(row.assignmentId, () => true, current => { current.resumeActive = false; if (current.state === 'waiting') { current.state = 'settled'; current.resumeState = current.cancelRequestedAt !== null ? 'withheld' : 'uncertain' } }); fenced++ } }; return { fenced } },
117
+ close() { if (closed) return; try { db.prepare('UPDATE bot_team_owner_v2 SET active=0 WHERE slot=1 AND token=?').run(ownerId) } finally { closed = true; db.close() } },
118
+ })
119
+ }
120
+ export { STATE_UNKNOWN as TEAM_STATE_UNKNOWN } from './team-tool-contract.mjs'
@@ -0,0 +1,282 @@
1
+ import { createHash, randomUUID } from 'node:crypto'
2
+ import { createUserMessage } from '@deepseek-ai/dsh-llm'
3
+ import { TEAM_CODES, parseTeamToolArgs, projectCoordinateArgs } from './team-tool-contract.mjs'
4
+ import { createTeamAssignmentStore, computeBriefHash, standingThreadKey, workThreadConversationKey, assertNoCycle, assertDepth } from './team-assignment-store.mjs'
5
+ import { createTeamScheduler } from './team-scheduler.mjs'
6
+
7
+ const fail = (code, extra = {}) => { throw Object.assign(new Error(code), { code, ...extra }) }
8
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
9
+ const textOf = content => Array.isArray(content) ? content.filter(block => block.type === 'text').map(block => block.text ?? '').join('\n') : ''
10
+ const composeBrief = row => `[Team assignment from ${row.parentBot}; mode ${row.mode}]\n${row.doneWhen ? `Done when: ${row.doneWhen}\n` : ''}${row.brief}`
11
+ const composeResult = row => `[Team result from ${row.targetBot}; assignment ${row.assignmentId}; ${row.outcome}${row.resultCode ? ` (${row.resultCode})` : ''}]\n${row.resultText || '(no result text)'}`
12
+ const unknown = code => ({ outcome: 'unknown', code })
13
+
14
+ /** Fold installed SDK events, requiring the exact identified user message in one
15
+ * balanced turn. A later assignment, an injected extra user, or an unterminated
16
+ * request cannot supply this request's answer.
17
+ */
18
+ export function teamReceiptFromEvents(events, requestId, role = 'assign-task') {
19
+ let turn = null, active = false, mixed = false, text = '', result = null, matches = 0
20
+ for (const event of events) {
21
+ if (event.type === 'turn/start') { turn = event.data?.turn; active = false; mixed = false; text = '' }
22
+ if (event.type === 'user/message') {
23
+ const source = event.data?.source
24
+ if (source?.kind === 'bot' && source.role === role && source.exchangeId === requestId) { matches++; active = true }
25
+ else if (active && !['snapshot', 'recall'].includes(source?.form)) mixed = true
26
+ }
27
+ if (event.type === 'assistant/message' && active) {
28
+ const content = event.data?.message?.content
29
+ if (!content?.some(block => block.type === 'tool-call')) text = textOf(content)
30
+ }
31
+ if (event.type === 'turn/end' && active && event.data?.turn === turn) {
32
+ const reason = event.data.reason
33
+ result = mixed ? unknown('TEAM_TARGET_MIXED_INPUT') : reason?.kind === 'completed' ? { outcome: 'succeeded', text } : reason?.kind === 'aborted' ? { outcome: 'cancelled', code: 'TEAM_TARGET_CANCELLED' } : { outcome: 'failed', code: 'TEAM_TARGET_FAILED' }
34
+ active = false
35
+ }
36
+ }
37
+ return matches === 1 && result ? result : unknown(matches > 1 ? 'TEAM_TARGET_DUPLICATE_REQUEST' : 'TEAM_TARGET_NOT_TERMINAL')
38
+ }
39
+
40
+ export function createIntegratedTeamBridge({ rt, directory, authority, lifecycle, flush, threadHost = () => null, cancelWaitMs = 10000 } = {}) {
41
+ if (!rt || !directory || !authority || !lifecycle || typeof flush !== 'function') throw new TypeError('TEAM_BRIDGE_DEPENDENCIES_REQUIRED')
42
+ if (!Number.isSafeInteger(cancelWaitMs) || cancelWaitMs < 10 || cancelWaitMs > 60000) throw new TypeError('TEAM_CANCEL_WAIT_INVALID')
43
+ const subjectBot = agent => authority.describe(directory.forAgent(agent)).botId
44
+ const eventsFor = async (sessionId, signal) => {
45
+ if (typeof rt.eventsBackwards !== 'function') fail('TEAM_TARGET_HISTORY_UNAVAILABLE')
46
+ const events = []
47
+ for await (const event of rt.eventsBackwards(sessionId, signal)) { events.push(event); if (events.length > 16384) fail('TEAM_TARGET_HISTORY_LIMIT') }
48
+ return events.reverse()
49
+ }
50
+ const owns = (botId, sessionId) => {
51
+ const bot = lifecycle.committedBot(botId)
52
+ if (!bot) return false
53
+ if ((bot.sessionId ?? bot.id) === sessionId) return true
54
+ if (bot.threads?.some(thread => thread.sessionId === sessionId && !thread.archivedAt) !== true) return false
55
+ const host = threadHost()
56
+ if (host) {
57
+ try {
58
+ const found = host.service.threadBySession(sessionId)
59
+ const gate = found?.botId === botId ? host.service.probeSendable({ botId, threadId: found.threadId }) : null
60
+ return gate?.ok === true && gate.sessionId === sessionId
61
+ } catch { return false }
62
+ }
63
+ return true
64
+ }
65
+ const readTarget = async row => {
66
+ if (!row.targetRef?.sessionId || !row.requestId && !row.targetRequestId || !owns(row.targetBot, row.targetRef.sessionId)) return unknown('TEAM_TARGET_REFERENCE_UNCONFIRMED')
67
+ try { return teamReceiptFromEvents(await eventsFor(row.targetRef.sessionId, row.signal), row.requestId ?? row.targetRequestId) }
68
+ catch { return unknown('TEAM_TARGET_HISTORY_UNCONFIRMED') }
69
+ }
70
+ const resolve = async (botId, sessionId) => {
71
+ await rt.load(); await rt.ensureCapabilities?.(sessionId)
72
+ if (!owns(botId, sessionId)) fail('TEAM_SESSION_RETIRED', { started: false, retired: true })
73
+ const agent = await rt.resolveAgent(sessionId)
74
+ if (subjectBot(agent) !== botId) fail('TEAM_TARGET_SUBJECT_MISMATCH', { started: false })
75
+ if (typeof agent.whenIdle !== 'function') fail('TEAM_AGENT_API_UNAVAILABLE', { started: false })
76
+ return agent
77
+ }
78
+ const submit = async (row, sessionId, role, requestId, text) => {
79
+ // Followup is the actual public SDK admission. No owner RPC, fake Agent or
80
+ // arbitrary two-second completion timer. whenIdle is followed by a flush and
81
+ // exact request scan; quiescence alone is not a receipt.
82
+ let off = () => {}, agent, submitted = false, finished = false, receipt = null, failure = null
83
+ try {
84
+ agent = await resolve(role === 'assign-task' ? row.targetBot : row.parentBot, sessionId)
85
+ row.validate?.(); row.signal?.throwIfAborted()
86
+ if (agent.status !== 'idle' || rt.live?.get(sessionId)?.turn !== undefined) return { started: false, code: TEAM_CODES.TARGET_BUSY }
87
+ const prior = await eventsFor(sessionId, row.signal)
88
+ const existing = prior.some(event => event.type === 'user/message' && event.data?.source?.kind === 'bot' && event.data.source.exchangeId === requestId && event.data.source.role === role)
89
+ if (existing) return role === 'assign-task' ? { started: true, ...teamReceiptFromEvents(prior, requestId, role), executionFinished: true } : { resumed: true, replayed: true, executionFinished: true }
90
+ row.validate?.(); row.signal?.throwIfAborted()
91
+ const message = createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'bot', form: 'relay', role, exchangeId: requestId, senderName: 'Team coordinator' } })
92
+ const aborted = new Promise(resolve => {
93
+ const stop = () => { try { agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } catch {}; resolve('aborted') }
94
+ row.signal?.addEventListener('abort', stop, { once: true }); off = () => row.signal?.removeEventListener('abort', stop)
95
+ })
96
+ submitted = true // conservative if public followup throws after acceptance
97
+ agent.followup(message)
98
+ if (await flush(agent.session) === false) fail('TEAM_SESSION_DURABILITY_UNAVAILABLE')
99
+ const state = await Promise.race([agent.whenIdle().then(() => 'idle'), aborted])
100
+ finished = state === 'idle'
101
+ if (await flush(agent.session) === false) fail('TEAM_SESSION_DURABILITY_UNAVAILABLE')
102
+ receipt = role === 'assign-result' ? { resumed: true, executionFinished: finished } : { started: true, ...(finished ? await readTarget({ ...row, requestId }) : unknown('TEAM_TARGET_STOP_UNCONFIRMED')), executionFinished: finished }
103
+ return receipt
104
+ } catch (error) { failure = error; if (!submitted) error.started = false; error.executionFinished = !submitted || finished; throw error }
105
+ finally {
106
+ // A result keeps its durable ancestry frame for the real resumed turn.
107
+ // Cancellation has a bounded convergence wait; a missing idle receipt
108
+ // preserves an uncertain execution fence instead of freeing that frame.
109
+ if (submitted && !finished) {
110
+ try { agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } catch {}
111
+ let timer
112
+ try { finished = await Promise.race([agent.whenIdle().then(() => true, () => false), new Promise(resolve => { timer = setTimeout(() => resolve(false), cancelWaitMs) })]) }
113
+ finally { clearTimeout(timer) }
114
+ }
115
+ if (receipt) receipt.executionFinished = finished
116
+ if (failure) failure.executionFinished = !submitted || finished
117
+ off()
118
+ }
119
+ }
120
+ return Object.freeze({
121
+ hooks: { createSession: typeof rt.createSession === 'function', addThreadSession: typeof rt.addThreadSession === 'function' },
122
+ async resolveTargetRef(row) {
123
+ const bot = lifecycle.committedBot(row.targetBot)
124
+ if (!bot) fail(TEAM_CODES.TARGET_NOT_COMMITTED)
125
+ const host = threadHost()
126
+ if (host?.service?.create && host?.service?.ensureSession) {
127
+ const { thread } = host.service.create({ botId: row.targetBot, name: `Team: ${row.parentBot} → ${row.targetBot}`, requestKey: `team-${digest(row.workThreadKey).slice(0, 64)}` })
128
+ await host.service.ensureSession({ botId: row.targetBot, threadId: thread.threadId })
129
+ const gate = await host.sendGate({ botId: row.targetBot, threadId: thread.threadId })
130
+ if (!gate?.ok || !owns(row.targetBot, gate.sessionId)) fail(gate?.code ?? 'TEAM_SESSION_COMMIT_UNCONFIRMED')
131
+ return { mode: 'integration', sessionId: gate.sessionId, workThreadKey: row.workThreadKey, catalogThreadId: thread.threadId }
132
+ }
133
+ const prior = bot.threads?.find(thread => thread.threadId === row.workThreadKey && !thread.archivedAt)
134
+ if (prior) return { mode: 'integration', sessionId: prior.sessionId, workThreadKey: row.workThreadKey }
135
+ if (!rt.createSession || !rt.addThreadSession) fail('TEAM_SESSION_FACTORY_REQUIRED')
136
+ const title = `Team: ${row.parentBot} → ${row.targetBot}`
137
+ const sessionId = await rt.createSession(title, { dir: bot.workDir })
138
+ await rt.addThreadSession(row.targetBot, row.workThreadKey, sessionId, { name: title })
139
+ if (!owns(row.targetBot, sessionId)) fail('TEAM_SESSION_COMMIT_UNCONFIRMED')
140
+ return { mode: 'integration', sessionId, workThreadKey: row.workThreadKey }
141
+ },
142
+ dispatchTarget: row => submit(row, row.targetRef.sessionId, 'assign-task', row.requestId, composeBrief(row)),
143
+ readTarget,
144
+ captureOrigin({ callerBotId, sessionId }) { if (!owns(callerBotId, sessionId)) fail('TEAM_ORIGIN_UNCONFIRMED'); return { mode: 'integration', sessionId } },
145
+ originCurrent: (botId, origin) => origin.mode === 'integration' && owns(botId, origin.sessionId),
146
+ resumeSender(row) { if (!owns(row.parentBot, row.origin?.sessionId)) return { started: false, retired: true }; return submit(row, row.origin.sessionId, 'assign-result', `team-result-${row.assignmentId}`, composeResult(row)) },
147
+ async cancelTarget(row) { if (row.targetRef?.sessionId) { const agent = await resolve(row.targetBot, row.targetRef.sessionId); agent.cancel({ kind: 'hook', reason: 'team-assignment-cancelled' }) } },
148
+ isBusy: botId => rt.live?.get(rt.chatOf(botId))?.turn !== undefined,
149
+ })
150
+ }
151
+
152
+ export function createIndependentTeamBridge({ runtime } = {}) {
153
+ if (typeof runtime !== 'function') throw new TypeError('TEAM_BRIDGE_REQUIRES_RUNTIME')
154
+ const api = () => { const adapter = runtime(); if (!adapter?.workThreadTurn || !adapter?.resumeTeamTurn) fail('TEAM_WORKTHREAD_UNAVAILABLE'); return adapter }
155
+ const readTarget = row => {
156
+ const receipt = api().workThreadRead(row.targetBot, row.targetRef.workThreadKey, row.requestId ?? row.targetRequestId)
157
+ return receipt.hasConfirmedAnswer ? { outcome: 'succeeded', text: receipt.answer } : unknown(receipt.hasBrief ? 'TEAM_TARGET_ANSWER_UNCONFIRMED' : 'TEAM_TARGET_REQUEST_UNCONFIRMED')
158
+ }
159
+ return Object.freeze({
160
+ resolveTargetRef: row => ({ mode: 'independent', workThreadKey: row.workThreadKey, conversationId: `teamwt-${workThreadConversationKey(row.targetBot, row.workThreadKey)}` }),
161
+ async dispatchTarget(row) {
162
+ row.validate?.()
163
+ try { const result = await api().workThreadTurn({ botId: row.targetBot, workThreadId: row.targetRef.workThreadKey, conversationId: row.targetRef.conversationId, input: composeBrief(row), requestId: row.requestId, signal: row.signal, consult: row.mode === 'consult', validate: row.validate }); return { started: true, outcome: 'succeeded', text: result.answer } }
164
+ catch (error) { if (error.code === 'BOT_BUSY') return { started: false, code: TEAM_CODES.TARGET_BUSY }; return { started: true, ...readTarget(row), executionFinished: true } }
165
+ },
166
+ readTarget,
167
+ captureOrigin: row => api().captureTeamOrigin({ botId: row.callerBotId, conversationId: row.conversationId, workThreadId: row.workThreadId }),
168
+ originCurrent: (botId, origin) => api().teamOriginCurrent(botId, origin),
169
+ async resumeSender(row) {
170
+ try { return { ...await api().resumeTeamTurn({ botId: row.parentBot, origin: row.origin, requestId: `team-result-${row.assignmentId}`, input: composeResult(row), signal: row.signal, validate: row.validate }), executionFinished: true } }
171
+ catch (error) { error.executionFinished = true; throw error }
172
+ },
173
+ cancelTarget() {},
174
+ isBusy: botId => api().busyBots?.().includes(botId) === true,
175
+ })
176
+ }
177
+
178
+ export function createTeamBridge({ integrated, independent, serving }) {
179
+ const forBot = botId => { const bridge = serving.readMode(botId) === 'integration' ? integrated : serving.readMode(botId) === 'independent' ? independent : null; if (!bridge) fail('TEAM_BRIDGE_UNAVAILABLE'); return bridge }
180
+ // Recovery uses the recorded mode: moving a Bot cannot reinterpret an old receipt.
181
+ const forRef = ref => ref?.mode === 'integration' ? integrated : ref?.mode === 'independent' ? independent : null
182
+ return Object.freeze({
183
+ resolveTargetRef: row => forBot(row.targetBot).resolveTargetRef(row),
184
+ dispatchTarget: row => forRef(row.targetRef).dispatchTarget(row),
185
+ readTarget: row => forRef(row.targetRef)?.readTarget(row) ?? unknown('TEAM_BRIDGE_UNAVAILABLE'),
186
+ resumeSender: row => forRef(row.origin).resumeSender(row),
187
+ cancelTarget: row => forRef(row.targetRef)?.cancelTarget(row),
188
+ captureOrigin: row => forBot(row.callerBotId).captureOrigin(row),
189
+ originCurrent: (botId, origin) => serving.readMode(botId) === origin?.mode && forRef(origin)?.originCurrent(botId, origin) === true,
190
+ isBusy: botId => typeof serving.busy === 'function' ? serving.busy(botId) : forBot(botId).isBusy(botId),
191
+ })
192
+ }
193
+
194
+ /** Both doors supply Host-resolved origin/turn/call metadata, never model args. */
195
+ export function createTeamCoordination({ Database, file, rt = null, directory, authority, lifecycle, serving, runtime = () => null, policy = null, sessionEvents = null, flush = null, integrated = null, independent = null, threadHost = () => null, now = Date.now, limits = {} } = {}) {
196
+ const maxDepth = limits.maxDepth ?? 4
197
+ const store = createTeamAssignmentStore({ Database, file, now })
198
+ const bridge = createTeamBridge({ integrated: integrated ?? (rt && flush ? createIntegratedTeamBridge({ rt, directory, authority, lifecycle, sessionEvents, flush, threadHost }) : null), independent: independent ?? createIndependentTeamBridge({ runtime }), serving })
199
+ const isMain = id => lifecycle.committedMainBotIds().includes(id)
200
+ const canUse = id => { try { if (!lifecycle.committedBot(id)) return false; authority.assertTool(directory.forBot(id), 'tool/team'); return true } catch { return false } }
201
+ const requireCaller = id => { if (!lifecycle.committedBot(id)) fail(TEAM_CODES.CALLER_NOT_COMMITTED); if (!canUse(id)) fail(TEAM_CODES.CALLER_NOT_AUTHORIZED) }
202
+ const reachable = (fromId, targetId) => {
203
+ const from = lifecycle.committedBot(fromId), target = lifecycle.committedBot(targetId)
204
+ if (!from || !target || fromId === targetId) return false
205
+ if (isMain(fromId)) return true
206
+ if (target.hidden || target.private || target.archivedAt || target.visibility !== undefined && target.visibility !== null && target.visibility !== 'everyone') return false
207
+ if (from.peers !== undefined && (!Array.isArray(from.peers) || !from.peers.includes(targetId))) return false
208
+ // A restricted sender may not disclose its brief into a broader audience.
209
+ if (from.visibility !== undefined && from.visibility !== null && from.visibility !== 'everyone') return false
210
+ return true
211
+ }
212
+ const accessAllowed = row => {
213
+ if (!canUse(row.parentBot) || !reachable(row.parentBot, row.targetBot)) return false
214
+ if (row.parentAssignmentId) { const parent = store.get(row.parentAssignmentId); if (!parent || parent.cancelRequestedAt !== null || parent.state === 'cancelled') return false }
215
+ try { authority.assertTool(directory.forBot(row.targetBot), 'model/infer'); return true } catch { return false }
216
+ }
217
+ const assertCanAct = (botId, { sessionId = null, conversationId = null } = {}) => {
218
+ const active = store.activeFor(botId, { sessionId, conversationId })
219
+ if (active?.mode === 'consult') fail('TEAM_CONSULT_READ_ONLY')
220
+ const resumed = active?.resumeAssignmentId ? store.get(active.resumeAssignmentId) : null
221
+ if (active && (active.cancelRequestedAt !== null || active.assignmentId && !accessAllowed(active) || resumed && !accessAllowed(resumed))) fail('TEAM_ACCESS_CHANGED')
222
+ }
223
+ const scheduler = createTeamScheduler({ store, bridge, checks: { committedBot: id => lifecycle.committedBot(id), accessAllowed, originAllowed: row => bridge.originCurrent(row.parentBot, row.origin) }, policy })
224
+ const scope = input => {
225
+ requireCaller(input.callerBotId)
226
+ const active = store.activeFor(input.callerBotId, { sessionId: input.sessionId, conversationId: input.conversationId })
227
+ const origin = bridge.captureOrigin({ ...input, workThreadId: active?.targetRef?.workThreadKey })
228
+ return { origin, originKey: digest(origin), active }
229
+ }
230
+ const shape = row => ({ assignmentId: row.assignmentId, requestKey: row.requestKey, targetBot: row.targetBot, workThreadKey: row.workThreadKey, threadId: row.workThreadKey, mode: row.mode, reply: row.reply, state: row.state, outcome: row.outcome, resultCode: row.resultCode, result: row.outcome === 'succeeded' ? row.resultText : null, resumeState: row.resumeState, reworkOf: row.reworkOf, depth: row.depth, cancelRequested: row.cancelRequestedAt !== null, ...(row.duplicate ? { duplicate: true } : {}) })
231
+ const coordinateBots = input => {
232
+ const { callerBotId, turnId, callId } = input
233
+ const { origin, originKey, active } = scope(input)
234
+ assertCanAct(callerBotId, input)
235
+ if (!turnId || !callId) fail('TEAM_HOST_TURN_REQUIRED')
236
+ const args = input.args
237
+ // Re-validate at the service door too; callers cannot bypass the shared parser.
238
+ parseTeamToolArgs('coordinate_bots', { bot_ids: args.targets, message: args.message, thread: args.thread ?? 'standing', reply: args.reply ?? 'resume', consult: args.consult ?? false, rework: args.rework ?? false, ...(args.doneWhen !== undefined && args.doneWhen !== null ? { done_when: args.doneWhen } : {}), ...(args.requestKey !== undefined ? { request_key: args.requestKey } : {}) })
239
+ const requestKey = args.requestKey ?? `call-${digest([turnId, callId]).slice(0, 60)}`
240
+ const mode = args.consult ? 'consult' : args.reply === 'none' ? 'notice' : 'work'
241
+ const lineage = active?.lineage ?? [callerBotId]
242
+ assertDepth(lineage, maxDepth)
243
+ const specs = args.targets.map(targetBot => {
244
+ if (targetBot === callerBotId) fail(TEAM_CODES.SELF_TARGET)
245
+ // A missing/private target produces the same refusal, without a name leak.
246
+ if (!reachable(callerBotId, targetBot)) fail(TEAM_CODES.CALLER_NOT_AUTHORIZED)
247
+ assertNoCycle(lineage, targetBot)
248
+ const requestedThread = args.thread ?? 'standing'
249
+ const workThreadKey = requestedThread === 'standing' ? standingThreadKey(callerBotId, targetBot, originKey) : requestedThread === 'new' ? `wt-${randomUUID()}` : requestedThread
250
+ if (!['new', 'standing'].includes(requestedThread) && !store.listByParent(callerBotId, { includeTerminal: true, originKey }).some(row => row.targetBot === targetBot && row.workThreadKey === workThreadKey)) fail(TEAM_CODES.THREAD_NOT_OWNED)
251
+ const spec = { parentBot: callerBotId, targetBot, requestedThread, workThreadKey, origin, originKey, mode, reply: args.reply ?? 'resume', brief: args.message, doneWhen: args.doneWhen ?? null, briefHash: computeBriefHash(mode, args.message, args.doneWhen, args.consult), rework: args.rework === true, lineage: [...lineage, targetBot], depth: lineage.length, rootBot: active?.rootBot ?? callerBotId, rootAssignmentId: active?.rootAssignmentId ?? active?.assignmentId ?? null, parentAssignmentId: active?.assignmentId ?? null, parentRequestId: active?.targetRequestId ?? null, createdByTurn: turnId, createdByCall: callId }
252
+ if (!accessAllowed(spec)) fail(TEAM_CODES.ACCESS_CHANGED)
253
+ return spec
254
+ })
255
+ const decision = store.createBatch(specs, { parentBot: callerBotId, originKey, requestKey, payloadHash: digest([args.targets, args.message, args.thread ?? 'standing', args.reply ?? 'resume', args.consult ?? false, args.rework ?? false, args.doneWhen ?? null]) })
256
+ if (decision.status === 'conflict') fail(TEAM_CODES.REQUEST_CONFLICT)
257
+ return { assignments: decision.assignments.map(row => ({ ...shape(row), ...(decision.status === 'replayed' ? { replayed: true } : {}) })) }
258
+ }
259
+ const rowsFor = input => { const scoped = scope(input); return store.listByParent(input.callerBotId, { includeTerminal: true, originKey: scoped.originKey }).filter(accessAllowed) }
260
+ return Object.freeze({
261
+ ready: true, canUse, assertCanAct, coordinateBots,
262
+ validateResult(input, value) {
263
+ requireCaller(input.callerBotId)
264
+ if (input.sessionId || input.conversationId) scope(input)
265
+ const targets = [...(value.assignments ?? []), ...(value.assignment ? [value.assignment] : []), ...(value.threads ?? [])].map(row => row.targetBot)
266
+ targets.push(...(value.bots ?? []).map(row => row.id))
267
+ if (targets.some(id => !reachable(input.callerBotId, id))) fail(TEAM_CODES.ACCESS_CHANGED)
268
+ },
269
+ assignmentStatus(input) {
270
+ const rows = rowsFor(input), args = input.args ?? {}
271
+ if (rows.some(row => row.createdByTurn === input.turnId)) fail('TEAM_STATUS_SAME_TURN')
272
+ if (args.id) { const row = rows.find(row => row.assignmentId === args.id); if (!row) fail(TEAM_CODES.ASSIGNMENT_NOT_FOUND); return { assignment: shape(row) } }
273
+ return { assignments: rows.filter(row => !['settled', 'unknown', 'cancelled'].includes(row.state)).map(shape) }
274
+ },
275
+ listBots({ callerBotId }) { requireCaller(callerBotId); return { bots: lifecycle.committedBots().filter(bot => reachable(callerBotId, bot.id)).map(bot => ({ id: bot.id, name: bot.name ?? bot.id, main: isMain(bot.id), model: bot.model ?? null, mode: serving.readMode(bot.id) })).sort((a, b) => a.id.localeCompare(b.id)) } },
276
+ listThreads(input) { const rows = rowsFor(input), seen = new Set(); return { threads: rows.reverse().filter(row => { if (seen.has(row.workThreadKey)) return false; seen.add(row.workThreadKey); return true }).map(row => ({ threadId: row.workThreadKey, targetBot: row.targetBot, title: row.brief.slice(0, 80), state: row.state, assignmentId: row.assignmentId, updatedAt: row.updatedAt })) } },
277
+ stop: botId => scheduler.stop(botId), store, scheduler,
278
+ start(options) { scheduler.start(options); return this }, tick: () => scheduler.tick(), whenIdle: () => scheduler.whenIdle(),
279
+ async drain(rounds = 24) { for (let index = 0; index < rounds; index++) { scheduler.tick(); await scheduler.whenIdle(); if (!store.dispatchable().length && !store.rowsForReconcile().length && !store.resumeDue().length) break }; return this },
280
+ close() { scheduler.close(); store.close() },
281
+ })
282
+ }
@@ -0,0 +1,83 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ const safeCode = error => /^[A-Z][A-Z0-9_:-]{0,95}$/.test(error?.code ?? '') ? error.code : 'TEAM_OUTCOME_UNKNOWN'
4
+ const known = receipt => ['succeeded', 'failed', 'cancelled'].includes(receipt?.outcome)
5
+
6
+ /** Persist intent before effects. Running requests are read, never dispatched again.
7
+ * Sending resumes that lack a durable transport receipt are uncertain, never resent.
8
+ */
9
+ export function createTeamScheduler({ store, bridge, checks, policy = null, pollMs = 200 } = {}) {
10
+ if (!store?.dispatchable || !bridge?.dispatchTarget || !checks?.accessAllowed) throw new TypeError('TEAM_SCHEDULER_DEPENDENCIES_REQUIRED')
11
+ if (!Number.isSafeInteger(pollMs) || pollMs < 5) throw new TypeError('TEAM_SCHEDULER_POLL_INVALID')
12
+ const inflight = new Map(), pending = new Set()
13
+ let closed = false, timer = null
14
+ const detached = operation => policy ? policy.runDetached(operation) : operation()
15
+ const allowed = row => !closed && row.cancelRequestedAt === null && checks.committedBot(row.parentBot) && checks.committedBot(row.targetBot) && checks.accessAllowed(row)
16
+ const launch = (row, kind, operation) => {
17
+ if (inflight.has(row.assignmentId) || closed) return
18
+ const controller = new AbortController()
19
+ inflight.set(row.assignmentId, { controller, kind })
20
+ const task = Promise.resolve().then(() => operation(controller.signal)).catch(() => {}).finally(() => { inflight.delete(row.assignmentId); pending.delete(task) })
21
+ pending.add(task)
22
+ }
23
+ const settle = (row, receipt) => {
24
+ if (closed) return
25
+ if (!known(receipt)) store.fenceUnknown(row.assignmentId, receipt?.code ?? 'TEAM_OUTCOME_UNKNOWN', { executionFinished: receipt?.executionFinished === true })
26
+ else store.settle(row.assignmentId, { outcome: receipt.outcome, resultText: receipt.text ?? null, resultCode: receipt.code ?? null })
27
+ }
28
+ const dispatch = async (row, signal) => {
29
+ if (!allowed(row)) { store.cancel(row.assignmentId, { resultCode: 'TEAM_ACCESS_CHANGED' }); return }
30
+ if (bridge.isBusy(row.targetBot)) return
31
+ if (!store.admit(row.assignmentId)) return
32
+ try {
33
+ const targetRef = await detached(() => bridge.resolveTargetRef(row))
34
+ const current = store.get(row.assignmentId)
35
+ if (signal.aborted || !allowed(current)) { store.cancel(row.assignmentId, { resultCode: 'TEAM_ACCESS_CHANGED' }); return }
36
+ const requestId = `teamreq_${randomUUID()}`
37
+ if (!store.beginDispatch(row.assignmentId, targetRef, requestId)) return
38
+ const receipt = await detached(() => bridge.dispatchTarget({ ...row, targetRef, requestId, signal, validate: () => { const current = store.get(row.assignmentId); if (signal.aborted || !allowed(current)) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED' }) } }))
39
+ if (closed) return
40
+ if (receipt?.started === false) {
41
+ if (store.get(row.assignmentId).cancelRequestedAt === null) store.rewarm(row.assignmentId)
42
+ else store.fenceUnknown(row.assignmentId, 'TEAM_STOP_BEFORE_DISPATCH')
43
+ } else settle(row, receipt)
44
+ } catch (error) { if (!closed) store.fenceUnknown(row.assignmentId, safeCode(error), { executionFinished: error?.executionFinished === true }) }
45
+ }
46
+ const reconcile = async row => {
47
+ try { settle(row, await detached(() => bridge.readTarget({ ...row, requestId: row.targetRequestId }))) }
48
+ catch (error) { if (!closed) store.fenceUnknown(row.assignmentId, safeCode(error)) }
49
+ }
50
+ const resume = async (row, signal) => {
51
+ if (!allowed(row) || checks.originAllowed?.(row) === false) return
52
+ if (bridge.isBusy(row.parentBot) || !store.markResumeSending(row.assignmentId)) return
53
+ try {
54
+ const result = await detached(() => bridge.resumeSender({ ...row, signal, validate: () => { if (signal.aborted || !allowed(store.get(row.assignmentId)) || checks.originAllowed?.(row) === false) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED', started: false, retired: true }) } }))
55
+ if (closed) return
56
+ if (result?.started === false) {
57
+ if (result.retired) store.completeResume(row.assignmentId, 'withheld')
58
+ else store.rewarmResume(row.assignmentId)
59
+ } else store.completeResume(row.assignmentId, result?.executionFinished === false ? 'uncertain' : 'delivered', { executionFinished: result?.executionFinished !== false })
60
+ } catch (error) {
61
+ if (closed) return
62
+ if (error?.started === false && !error.retired) store.rewarmResume(row.assignmentId)
63
+ else store.completeResume(row.assignmentId, error?.retired ? 'withheld' : 'uncertain', { executionFinished: error?.executionFinished === true || error?.started === false })
64
+ }
65
+ }
66
+ const tick = () => {
67
+ if (closed) return
68
+ for (const row of store.rowsForReconcile()) launch(row, 'reconcile', () => reconcile(row))
69
+ for (const row of store.dispatchable()) launch(row, 'dispatch', signal => dispatch(row, signal))
70
+ for (const row of store.resumeDue()) launch(row, 'resume', signal => resume(row, signal))
71
+ }
72
+ return Object.freeze({
73
+ tick,
74
+ async whenIdle() { while (pending.size) await Promise.allSettled([...pending]) },
75
+ stop(botId) {
76
+ const rows = store.all().filter(row => !['settled', 'unknown', 'cancelled'].includes(row.state) && (row.lineage.includes(botId) || row.parentBot === botId))
77
+ for (const row of rows) { store.requestCancel(row.assignmentId); inflight.get(row.assignmentId)?.controller.abort(Object.assign(new Error('TEAM_SUBTREE_STOPPED'), { code: 'TEAM_SUBTREE_STOPPED' })); if (row.targetRef) Promise.resolve(detached(() => bridge.cancelTarget(row))).catch(() => {}) }
78
+ return { stopped: rows.map(row => row.assignmentId) }
79
+ },
80
+ start({ auto = true } = {}) { store.recover(); tick(); if (auto && !closed && !timer) { timer = setInterval(tick, pollMs); timer.unref?.() }; return this },
81
+ close() { if (closed) return; closed = true; clearInterval(timer); for (const { controller } of inflight.values()) controller.abort(Object.assign(new Error('TEAM_SCHEDULER_CLOSED'), { code: 'TEAM_SCHEDULER_CLOSED' })) },
82
+ })
83
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The SDK (integrated Harness) door for the four teammate-coordination tools.
3
+ *
4
+ * Registered through `ctx.tools.register` exactly as the data tools are
5
+ * (bot-data-tools.mjs), reading names, descriptions and the SDK parameter map from the
6
+ * ONE shared contract (team-tool-contract.mjs) so the SDK and native doors cannot drift.
7
+ *
8
+ * Authority: the calling Bot comes from the live session through the injected
9
+ * `botIdFor(exec)` — never from an argument, which parseTeamToolArgs has already refused
10
+ * any `botId`/`owner`/route/session key in. The sender's Harness Session id is read from
11
+ * the live agent so a result returns into THIS conversation and resumes it; the target's
12
+ * own model, tools, permissions and funding are re-derived inside the target's frame, so
13
+ * nothing here lends the caller's authority to the target.
14
+ *
15
+ * Note for the parent: `list_bots` already exists in the vendored roster for integrated
16
+ * Bots. Register the NEW team tools (`coordinate_bots`, `assignment_status`,
17
+ * `list_threads`) here and keep the vendor `list_bots`; register `list_bots` from this
18
+ * family only on the native (independent) door, which has no roster tool of its own.
19
+ */
20
+
21
+ import { defineTool } from '@deepseek-ai/dsh-tools'
22
+ import { TEAM_TOOL_ENTRIES, TEAM_CODES, parseTeamToolArgs, projectCoordinateArgs } from './team-tool-contract.mjs'
23
+ import { redactNativeToolValue } from './native-conversation-tools.mjs'
24
+
25
+ const JSON_OUTPUT = { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] }
26
+ const safeCode = code => /^[A-Z][A-Z0-9_:-]{0,95}$/.test(code ?? '') ? code : 'TEAM_CALL_FAILED'
27
+ // Installed SDK author DSL does not support array cardinality/uniqueness. The
28
+ // shared parser enforces these bounds before any service effect; MCP keeps them.
29
+ const sdkParameters = parameters => Object.fromEntries(Object.entries(parameters).map(([key, spec]) => {
30
+ const { minItems, maxItems, uniqueItems, minLength, maxLength, ...supported } = spec
31
+ return [key, supported]
32
+ }))
33
+
34
+ /**
35
+ * @param {object} ctx Host tool registry ({ tools: { register } }).
36
+ * @param {{ service: object, botIdFor: (exec: object) => string | null, names?: string[] }} source
37
+ * @returns {() => void} release
38
+ */
39
+ export function registerTeamTools(ctx, { service, botIdFor, names = ['coordinate_bots', 'assignment_status', 'list_threads'], redact = text => text } = {}) {
40
+ if (!service || typeof service.coordinateBots !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_SERVICE')
41
+ if (typeof botIdFor !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_BOT_ID_FOR')
42
+ if (!ctx || typeof ctx.tools?.register !== 'function') throw new TypeError('TEAM_TOOLS_REQUIRE_CTX')
43
+ const offered = new Set(Array.isArray(names) ? names : TEAM_TOOL_ENTRIES.map(entry => entry.name))
44
+
45
+ const asBot = async (exec, run) => {
46
+ let botId = null
47
+ try { botId = botIdFor(exec) ?? null } catch (error) {
48
+ return { error: { code: safeCode(error?.code), message: redact(String(error?.message ?? error)) } }
49
+ }
50
+ if (typeof botId !== 'string' || botId === '') return { error: { code: 'unknown_subject', message: 'The calling Bot could not be identified from the live session.' } }
51
+ const sessionId = exec?.agent?.session?.id ?? null
52
+ try {
53
+ exec.signal.throwIfAborted()
54
+ if (!service.canUse(botId)) throw Object.assign(new Error('TEAM_CALLER_NOT_AUTHORIZED'), { code: 'TEAM_CALLER_NOT_AUTHORIZED' })
55
+ const boundary = (exec.agent.ctx?.sessionProjections ?? ctx.sessionProjections)?.stateOf(exec.agent.session, 'turnBoundary')
56
+ if (boundary?.openTurnStartSeq === null || boundary?.openTurnStartSeq === undefined || !exec.callId) throw Object.assign(new Error('TEAM_HOST_TURN_REQUIRED'), { code: 'TEAM_HOST_TURN_REQUIRED' })
57
+ const request = { callerBotId: botId, sessionId, turnId: `sdk-${sessionId}-${boundary.openTurnStartSeq}`, callId: exec.callId, signal: exec.signal }
58
+ const value = await run(request)
59
+ exec.signal.throwIfAborted()
60
+ if (botIdFor(exec) !== botId || !service.canUse(botId)) throw Object.assign(new Error('TEAM_ACCESS_CHANGED'), { code: 'TEAM_ACCESS_CHANGED' })
61
+ service.validateResult?.(request, value)
62
+ return redactNativeToolValue(value, redact)
63
+ } catch (error) { return { error: { code: safeCode(error?.code), message: redact(String(error?.message ?? error)) } } }
64
+ }
65
+
66
+ const tools = TEAM_TOOL_ENTRIES.filter(entry => offered.has(entry.name)).map(entry => defineTool({
67
+ name: entry.name,
68
+ description: entry.description,
69
+ parameters: sdkParameters(entry.parameters),
70
+ output: JSON_OUTPUT,
71
+ execute: (args, exec) => asBot(exec, async request => {
72
+ const parsed = parseTeamToolArgs(entry.name, args)
73
+ if (entry.method === 'coordinateBots') { const value = await service.coordinateBots({ ...request, args: projectCoordinateArgs(parsed) }); exec.concludeTurn?.(); return value }
74
+ if (entry.method === 'assignmentStatus') return service.assignmentStatus({ ...request, args: parsed })
75
+ if (entry.method === 'listBots') return service.listBots(request)
76
+ return service.listThreads(request)
77
+ }),
78
+ }))
79
+
80
+ const releases = tools.map(tool => ctx.tools.register(tool))
81
+ return () => { for (const release of releases.reverse()) release() }
82
+ }
83
+
84
+ export { TEAM_CODES }