@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,581 @@
1
+ // Multi-turn ACP session over one agent process.
2
+ //
3
+ // Ported and re-grounded from OpenMausBot server/drivers/acp/core.ts,
4
+ // turn-state.ts, session-model-controls.ts and session-reports.ts at pin
5
+ // 1bc4749aa8307d7d6998bf86de4ef6478460dd93 (Apache-2.0); see licenses/NOTICE.md.
6
+ //
7
+ // Upstream invariants kept here: there is no turn/completed notification — the
8
+ // session/prompt result (stopReason plus usage) is the completion; session/load
9
+ // replays history as ordinary session/update notifications, so nothing is
10
+ // emitted before the prompt is sent; a Stop the agent answers with end_turn is
11
+ // the person's stop; and a model is only switched through an option the agent
12
+ // actually advertised.
13
+ //
14
+ // Boundaries this port adds: turns are strictly serial with an explicit busy
15
+ // refusal; a refused or unsupported resume never silently opens a new session
16
+ // and replays the old turn; an unknown stop reason is reported verbatim and is
17
+ // never presented as success; model, config and mode changes run only when the
18
+ // agent advertised them and the Host authorized the selection; the dispatch
19
+ // moment is reported to the caller through `onDispatched`, and every refusal
20
+ // raised before it is tagged `acpNotDispatched` so a caller can tell "nothing
21
+ // was sent" from "the peer may have acted".
22
+
23
+ import {
24
+ ACP_METHODS, ACP_PROTOCOL_VERSION, clientCapabilities, readAgentCapabilities,
25
+ } from './rpc-codec.mjs'
26
+ import { createPermissionPolicy } from './permission-policy.mjs'
27
+
28
+ export const DEFAULT_LIMITS = Object.freeze({
29
+ initTimeoutMs: 300_000,
30
+ sessionTimeoutMs: 300_000,
31
+ loadTimeoutMs: 120_000,
32
+ promptTimeoutMs: 600_000,
33
+ promptIdleMs: 180_000,
34
+ /** A resident process runs a bounded number of turns before it is retired. */
35
+ maxTurns: 256,
36
+ /** An unanswered permission ask denies after this long. */
37
+ permissionTimeoutMs: 15 * 60_000,
38
+ })
39
+
40
+ const MAX_PROMPT_BYTES = 64_000
41
+
42
+ export const STOP_REASONS = Object.freeze({
43
+ endTurn: 'end_turn',
44
+ cancelled: 'cancelled',
45
+ refusal: 'refusal',
46
+ maxTurn: 'max_turn_requests',
47
+ })
48
+
49
+ /** Merge one session/update into the running turn. Unknown updates count, they never invent text. */
50
+ export function mergeSessionUpdate(state, update, sessionId) {
51
+ const kind = update?.sessionUpdate
52
+ const entry = { kind, sessionId: sessionId ?? null }
53
+ switch (kind) {
54
+ case 'agent_message_chunk': {
55
+ const content = update.content
56
+ if (content?.type === 'text' && typeof content.text === 'string' && content.text) {
57
+ if (typeof update.messageId === 'string' && update.messageId) state.textMessageId = update.messageId
58
+ state.text += content.text
59
+ state.repliedText = true
60
+ entry.text = content.text
61
+ } else if (content?.type === 'image' && typeof content.data === 'string' && content.data) {
62
+ state.producedItem = true
63
+ entry.image = true
64
+ } else {
65
+ entry.ignored = true
66
+ }
67
+ break
68
+ }
69
+ case 'agent_thought_chunk': {
70
+ const content = update.content
71
+ if (content?.type === 'text' && typeof content.text === 'string' && content.text) {
72
+ entry.text = content.text
73
+ state.producedItem = true
74
+ } else entry.ignored = true
75
+ break
76
+ }
77
+ case 'tool_call':
78
+ case 'tool_call_update': {
79
+ const id = typeof update.toolCallId === 'string' && update.toolCallId ? update.toolCallId : null
80
+ const status = update.status ?? (kind === 'tool_call' ? 'pending' : undefined)
81
+ if (id) {
82
+ if (status === 'completed' || status === 'failed') state.runningTools.delete(id)
83
+ else if (status === 'pending' || status === 'in_progress') state.runningTools.add(id)
84
+ }
85
+ state.producedItem = true
86
+ entry.toolCallId = id
87
+ entry.status = status ?? null
88
+ break
89
+ }
90
+ case 'usage_update': {
91
+ const used = update.used ?? update.usage?.used
92
+ if (typeof used === 'number' && Number.isFinite(used) && used > 0) {
93
+ if (state.usagePeak !== null && used < state.usagePeak * 0.6) state.usageCompacted = true
94
+ if (state.usagePeak === null || used > state.usagePeak) state.usagePeak = used
95
+ state.usageLast = used
96
+ entry.contextTokens = used
97
+ if (typeof (update.size ?? update.usage?.size) === 'number') entry.contextWindow = update.size ?? update.usage?.size
98
+ } else entry.ignored = true
99
+ break
100
+ }
101
+ default:
102
+ entry.ignored = true
103
+ }
104
+ return entry
105
+ }
106
+
107
+ /** Usage evidence. A figure the agent never reported stays null, never estimated. */
108
+ export function usageEvidence(state) {
109
+ return {
110
+ contextTokens: typeof state.usageLast === 'number' ? state.usageLast : null,
111
+ contextPeakTokens: typeof state.usagePeak === 'number' ? state.usagePeak : null,
112
+ compacted: state.usageCompacted === true,
113
+ inputTokens: null, outputTokens: null, cachedInputTokens: null,
114
+ reported: false,
115
+ status: 'unknown',
116
+ }
117
+ }
118
+
119
+ const emptyTurnState = () => ({
120
+ settled: false, promptSent: false, text: '', textMessageId: null,
121
+ producedItem: false, repliedText: false, stopped: false, cancelling: false,
122
+ usagePeak: null, usageLast: null, usageCompacted: false,
123
+ runningTools: new Set(), updates: [],
124
+ })
125
+
126
+ /**
127
+ * @param {object} options
128
+ * @param {object} options.host result of createAcpHost
129
+ * @param {object} options.link one host link (one session)
130
+ * @param {string} options.cwd Host-owned absolute workspace
131
+ * @param {{name: string, version: string}} options.clientInfo
132
+ * @param {{fileSystem?: boolean, terminal?: boolean, meta?: object}} [options.capabilities]
133
+ * @param {{pickMethod?: Function, onAuthFailure?: 'fail'|'continue'}} [options.auth]
134
+ * @param {(entry: object) => void} [options.onUpdate] merged stream, item by item
135
+ * @param {Function} [options.onPermissionRequest] trusted Host permission handler
136
+ * @param {{describe: Function, assertTool: Function}} [options.authority]
137
+ * @param {object} [options.subject]
138
+ * @param {(identity: object) => string|null} [options.operationTool]
139
+ * @param {(modelId: string) => void} [options.assertModelAuthorized] Host model authorization
140
+ * @param {Function} [options.log]
141
+ */
142
+ export function createAcpSession({
143
+ host,
144
+ link,
145
+ cwd,
146
+ clientInfo,
147
+ capabilities = {},
148
+ auth = {},
149
+ onUpdate,
150
+ onPermissionRequest,
151
+ onServerRequest = null,
152
+ authority = null,
153
+ subject = null,
154
+ operationTool = null,
155
+ assertModelAuthorized = null,
156
+ limits = {},
157
+ signal,
158
+ log,
159
+ } = {}) {
160
+ if (!host || typeof host.once !== 'function') throw new TypeError('ACP_SESSION_HOST_REQUIRED')
161
+ if (!link || typeof link.request !== 'function') throw new TypeError('ACP_SESSION_LINK_REQUIRED')
162
+ if (typeof cwd !== 'string' || !cwd) throw new TypeError('ACP_SESSION_CWD_REQUIRED')
163
+ const bounds = { ...DEFAULT_LIMITS, ...limits }
164
+ if (typeof clientInfo?.name !== 'string' || !clientInfo.name) throw new TypeError('ACP_SESSION_CLIENT_INFO_REQUIRED')
165
+
166
+ const permissions = createPermissionPolicy({
167
+ onPermissionRequest, authority, subject, operationTool, timeoutMs: bounds.permissionTimeoutMs, signal, log,
168
+ })
169
+ let agentCapabilities = null
170
+ let initResult = null
171
+ let authenticated = false
172
+ let sessionId = null
173
+ let sessionConfigResult = null
174
+ let current = null
175
+ let turns = 0
176
+ let closed = false
177
+ let closingReason = null
178
+ let cancelRequestedAt = null
179
+
180
+ const sessionBusy = () => current !== null && !current.state.settled
181
+
182
+ const emitUpdate = entry => {
183
+ current?.state.updates.push(entry)
184
+ if (typeof onUpdate === 'function') {
185
+ try { onUpdate(entry) } catch (error) { log?.({ event: 'acp.update-listener-failed', message: error?.message ?? String(error) }) }
186
+ }
187
+ }
188
+
189
+ // ---- inbound ---------------------------------------------------------------
190
+ link.inbound = {
191
+ request: classified => {
192
+ // A Host-installed guard (fs/terminal) answers first; anything it does not
193
+ // own falls through to the fail-closed paths below.
194
+ if (typeof onServerRequest === 'function') {
195
+ const reply = onServerRequest(classified)
196
+ if (reply !== null && reply !== undefined) {
197
+ if (typeof reply?.then === 'function') {
198
+ void reply.then(value => link.respond(classified.id, value?.result ?? value), error => link.respondError(classified.id, -32602, error?.message ?? String(error)))
199
+ } else {
200
+ link.respond(classified.id, reply?.result ?? reply)
201
+ }
202
+ return
203
+ }
204
+ }
205
+ if (classified.method === ACP_METHODS.sessionRequestPermission) {
206
+ // Only while a turn is running: between turns nothing is brokered.
207
+ if (!sessionBusy()) {
208
+ link.respond(classified.id, { outcome: { outcome: 'cancelled' } })
209
+ return
210
+ }
211
+ void permissions.decide(classified).then(decision => {
212
+ link.respond(classified.id, decision.behavior === 'allow' && decision.optionId
213
+ ? { outcome: { outcome: 'selected', optionId: decision.optionId } }
214
+ : { outcome: { outcome: 'cancelled' } })
215
+ emitUpdate({ kind: 'permission-resolved', behavior: decision.behavior, source: decision.source, reason: decision.reason, toolCallId: classified.params?.toolCall?.toolCallId ?? null })
216
+ })
217
+ return
218
+ }
219
+ // fs/* and terminal/* are refused by the guard upstream of this point; any
220
+ // other server request is answered fail-closed rather than blocking the agent.
221
+ link.respondError(classified.id, -32601, 'method not found')
222
+ },
223
+ notification: classified => {
224
+ if (classified.method !== ACP_METHODS.sessionUpdate) return
225
+ const params = classified.params ?? {}
226
+ if (params._meta?.isReplay === true) return
227
+ if (sessionId !== null && params.sessionId !== sessionId) return
228
+ const update = params.update ?? {}
229
+ if (!current || !current.state.promptSent) return
230
+ const entry = mergeSessionUpdate(current.state, update, sessionId)
231
+ emitUpdate(entry)
232
+ },
233
+ }
234
+
235
+ // ---- handshake -------------------------------------------------------------
236
+ const initialize = () => host.once('initialize', async () => {
237
+ const result = await link.request(ACP_METHODS.initialize, {
238
+ protocolVersion: ACP_PROTOCOL_VERSION,
239
+ clientInfo,
240
+ clientCapabilities: clientCapabilities(capabilities),
241
+ }, { timeoutMs: bounds.initTimeoutMs })
242
+ if (!result || typeof result !== 'object') throw new Error('ACP initialize returned no result')
243
+ agentCapabilities = readAgentCapabilities(result)
244
+ if (agentCapabilities.protocolVersion !== ACP_PROTOCOL_VERSION) {
245
+ throw new Error(`ACP protocol version mismatch: agent reports ${agentCapabilities.protocolVersion ?? 'nothing'}, client speaks ${ACP_PROTOCOL_VERSION}`)
246
+ }
247
+ initResult = result
248
+ log?.({ event: 'acp.initialize', agent: agentCapabilities.agentName, version: agentCapabilities.agentVersion, loadSession: agentCapabilities.loadSession })
249
+ return agentCapabilities
250
+ })
251
+
252
+ const authenticate = () => host.once('authenticate', async () => {
253
+ if (authenticated) return true
254
+ const methods = Array.isArray(initResult?.authMethods) ? initResult.authMethods : []
255
+ const methodId = typeof auth.pickMethod === 'function' ? auth.pickMethod(methods) : null
256
+ if (typeof methodId !== 'string' || !methodId) {
257
+ if (auth.onAuthFailure === 'fail') throw new Error('ACP agent requires authentication')
258
+ return false
259
+ }
260
+ try {
261
+ await link.request(ACP_METHODS.authenticate, { methodId }, { timeoutMs: bounds.initTimeoutMs })
262
+ authenticated = true
263
+ return true
264
+ } catch (error) {
265
+ if (auth.onAuthFailure === 'fail') throw error
266
+ return false
267
+ }
268
+ })
269
+
270
+ const handshake = async () => {
271
+ await initialize()
272
+ await authenticate()
273
+ return agentCapabilities
274
+ }
275
+
276
+ // ---- session lifecycle -----------------------------------------------------
277
+ /** Open a session. `resumeSessionId` is only honoured when the agent advertised loadSession. */
278
+ const openSession = async ({ resumeSessionId = null } = {}) => {
279
+ await handshake()
280
+ if (resumeSessionId !== null) {
281
+ if (!agentCapabilities.loadSession) {
282
+ return { ok: false, stopReason: 'resume-unsupported', reason: 'agent does not advertise loadSession' }
283
+ }
284
+ try {
285
+ const result = await link.request(
286
+ ACP_METHODS.sessionLoad,
287
+ { sessionId: resumeSessionId, cwd, mcpServers: [] },
288
+ { timeoutMs: bounds.loadTimeoutMs, owner: link.threadId },
289
+ )
290
+ const loaded = typeof result?.sessionId === 'string' && result.sessionId ? result.sessionId : resumeSessionId
291
+ sessionId = loaded
292
+ // The host routes by sessionId, so the link learns it too.
293
+ link.sessionId = loaded
294
+ sessionConfigResult = result ?? null
295
+ log?.({ event: 'acp.session-loaded', sessionId })
296
+ return { ok: true, sessionId, resumed: true }
297
+ } catch (error) {
298
+ // A refused resume is never papered over with a new session: the caller
299
+ // decides, and the old turn is not replayed into a fresh one.
300
+ log?.({ event: 'acp.session-load-refused', message: error?.message ?? String(error) })
301
+ return { ok: false, stopReason: 'resume-refused', reason: error?.message ?? String(error), error }
302
+ }
303
+ }
304
+ const result = await link.request(
305
+ ACP_METHODS.sessionNew,
306
+ { cwd, mcpServers: [] },
307
+ { timeoutMs: bounds.sessionTimeoutMs, owner: link.threadId },
308
+ )
309
+ if (typeof result?.sessionId !== 'string' || !result.sessionId) {
310
+ throw new Error('ACP session/new returned no sessionId')
311
+ }
312
+ sessionId = result.sessionId
313
+ link.sessionId = result.sessionId
314
+ sessionConfigResult = result ?? null
315
+ turns = 0
316
+ log?.({ event: 'acp.session-new', sessionId })
317
+ return { ok: true, sessionId, resumed: false }
318
+ }
319
+
320
+ /** Host-explicit fresh session. Replaying an earlier turn is the caller's decision, never ours. */
321
+ const resetSession = () => {
322
+ sessionId = null
323
+ link.sessionId = null
324
+ sessionConfigResult = null
325
+ turns = 0
326
+ }
327
+ // ---- model / config / mode -------------------------------------------------
328
+ const advertisedConfigOption = configId => {
329
+ const options = Array.isArray(sessionConfigResult?.configOptions) ? sessionConfigResult.configOptions : []
330
+ return options.find(option => option && typeof option === 'object' && option.id === configId && option.type === 'select') ?? null
331
+ }
332
+
333
+ const advertisedLegacyModels = () => {
334
+ const models = sessionConfigResult?.models
335
+ return models && Array.isArray(models.availableModels) ? models : null
336
+ }
337
+
338
+ /**
339
+ * Switch the session model. Only an advertised model is ever requested, the
340
+ * switch must be confirmed by the agent, and the Host authorizes the model
341
+ * before anything is sent. No prompt is sent on a refused switch.
342
+ */
343
+ const selectModel = async modelId => {
344
+ if (typeof modelId !== 'string' || !modelId) throw new TypeError('ACP_MODEL_REQUIRED')
345
+ if (sessionId === null) throw new Error('ACP_NO_SESSION')
346
+ if (typeof assertModelAuthorized === 'function') assertModelAuthorized(modelId)
347
+ const option = advertisedConfigOption('model')
348
+ if (option) {
349
+ const offered = (Array.isArray(option.options) ? option.options : []).map(entry => entry?.value).filter(value => typeof value === 'string')
350
+ if (!offered.includes(modelId)) throw new Error(`ACP_MODEL_NOT_ADVERTISED: ${modelId}`)
351
+ const result = await link.request(
352
+ ACP_METHODS.sessionSetConfigOption,
353
+ { sessionId, configId: option.id, value: modelId },
354
+ { timeoutMs: bounds.sessionTimeoutMs },
355
+ )
356
+ const confirmed = Array.isArray(result?.configOptions)
357
+ ? result.configOptions.find(entry => entry?.id === option.id)?.currentValue
358
+ : result?.currentValue
359
+ if (confirmed !== undefined && confirmed !== modelId) {
360
+ throw new Error(`ACP_MODEL_NOT_CONFIRMED: asked ${modelId}, agent kept ${String(confirmed)}`)
361
+ }
362
+ sessionConfigResult = result && typeof result === 'object' ? { ...sessionConfigResult, ...result } : sessionConfigResult
363
+ return { modelId, confirmed: confirmed === modelId || confirmed === undefined }
364
+ }
365
+ const legacy = advertisedLegacyModels()
366
+ if (legacy) {
367
+ const offered = legacy.availableModels.map(entry => entry?.modelId).filter(value => typeof value === 'string')
368
+ if (!offered.includes(modelId)) throw new Error(`ACP_MODEL_NOT_ADVERTISED: ${modelId}`)
369
+ await link.request(ACP_METHODS.sessionSetModel, { sessionId, modelId }, { timeoutMs: bounds.sessionTimeoutMs })
370
+ sessionConfigResult = { ...sessionConfigResult, models: { ...legacy, currentModelId: modelId } }
371
+ return { modelId, confirmed: true }
372
+ }
373
+ throw new Error('ACP_MODEL_SELECTION_UNAVAILABLE: agent advertises no model option')
374
+ }
375
+
376
+ /** Set a session mode only when the agent advertised that exact mode. */
377
+ const setMode = async modeId => {
378
+ if (typeof modeId !== 'string' || !modeId) throw new TypeError('ACP_MODE_REQUIRED')
379
+ if (sessionId === null) throw new Error('ACP_NO_SESSION')
380
+ const modes = Array.isArray(sessionConfigResult?.modes) ? sessionConfigResult.modes : []
381
+ const offered = modes.map(mode => mode?.id).filter(value => typeof value === 'string')
382
+ if (!offered.includes(modeId)) throw new Error(`ACP_MODE_NOT_ADVERTISED: ${modeId}`)
383
+ await link.request(ACP_METHODS.sessionSetMode, { sessionId, modeId }, { timeoutMs: bounds.sessionTimeoutMs })
384
+ return { modeId }
385
+ }
386
+
387
+ // ---- turns -----------------------------------------------------------------
388
+ /** Ask the agent to stop. This is a request, never a stop confirmation. */
389
+ const cancel = () => {
390
+ if (!current || current.state.settled) return false
391
+ current.state.stopped = true
392
+ cancelRequestedAt = Date.now()
393
+ if (sessionId !== null) link.notify(ACP_METHODS.sessionCancel, { sessionId })
394
+ log?.({ event: 'acp.cancel-requested', sessionId })
395
+ return true
396
+ }
397
+
398
+ /**
399
+ * Run one turn. Text only: a turn payload can never carry a subject,
400
+ * executable or credential. The session/prompt result is the completion; an
401
+ * unknown stop reason is reported, never treated as success.
402
+ *
403
+ * `onDispatched` is called at the moment the request is issued — before any
404
+ * answer exists — so a caller can record dispatch evidence instead of counting
405
+ * completions. Every refusal raised before that point is tagged
406
+ * `acpNotDispatched`, which is the only honest proof that nothing was written
407
+ * to the peer.
408
+ */
409
+ const prompt = async (input, { onDispatched } = {}) => {
410
+ const notDispatched = error => { throw Object.assign(error, { acpNotDispatched: true }) }
411
+ if (closed) notDispatched(new Error('ACP_SESSION_CLOSED'))
412
+ if (sessionId === null) notDispatched(new Error('ACP_NO_SESSION'))
413
+ if (sessionBusy()) notDispatched(new Error('ACP_SESSION_BUSY'))
414
+ if (turns >= bounds.maxTurns) notDispatched(new Error('ACP_SESSION_TURN_LIMIT'))
415
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => key !== 'text')) {
416
+ notDispatched(new TypeError('ACP_TURN_INPUT: only { text } is accepted'))
417
+ }
418
+ const promptText = input.text
419
+ if (typeof promptText !== 'string' || !promptText.trim() || promptText.includes('\u0000') || Buffer.byteLength(promptText) > MAX_PROMPT_BYTES) {
420
+ notDispatched(new TypeError('ACP_TURN_TEXT_INVALID'))
421
+ }
422
+ try { signal?.throwIfAborted() } catch (error) { notDispatched(error) }
423
+
424
+ const state = emptyTurnState()
425
+ const turn = { state, startedAt: Date.now() }
426
+ current = turn
427
+ turns++
428
+
429
+ try {
430
+ state.promptSent = true
431
+ // The request is issued here. This is the dispatch moment, recorded before
432
+ // any result exists.
433
+ if (typeof onDispatched === 'function') onDispatched()
434
+ // The abort race guarantees the turn settles even if the transport ignores
435
+ // the signal: a cancelled turn is never left waiting on the agent.
436
+ let onAbortRace = null
437
+ const abortRace = new Promise((resolve, reject) => {
438
+ onAbortRace = () => {
439
+ state.stopped = true
440
+ reject(Object.assign(new Error('ACP turn aborted'), { acpAborted: true }))
441
+ }
442
+ if (signal) signal.addEventListener('abort', onAbortRace, { once: true })
443
+ if (signal?.aborted) onAbortRace()
444
+ })
445
+ try {
446
+ const result = await Promise.race([
447
+ link.request(
448
+ ACP_METHODS.sessionPrompt,
449
+ { sessionId, prompt: [{ type: 'text', text: promptText }] },
450
+ {
451
+ timeoutMs: bounds.promptTimeoutMs,
452
+ idleMs: bounds.promptIdleMs,
453
+ signal,
454
+ onReceive: value => {
455
+ // The agent may stream a final update before this continuation runs.
456
+ if (value && typeof value === 'object') turn.result = value
457
+ },
458
+ },
459
+ ),
460
+ abortRace,
461
+ ])
462
+ return settleTurn(turn, result, null)
463
+ } finally {
464
+ if (onAbortRace) signal?.removeEventListener?.('abort', onAbortRace)
465
+ }
466
+ } catch (error) {
467
+ return settleTurn(turn, turn.result ?? null, error)
468
+ } finally {
469
+ current = null
470
+ }
471
+ }
472
+
473
+ const settleTurn = (turn, result, error) => {
474
+ const state = turn.state
475
+ state.settled = true
476
+ const usage = usageEvidence(state)
477
+ const rawUsage = result?.usage ?? result?._meta ?? null
478
+ if (rawUsage && typeof rawUsage === 'object') {
479
+ const input = typeof rawUsage.inputTokens === 'number' ? rawUsage.inputTokens : null
480
+ const output = typeof rawUsage.outputTokens === 'number' ? rawUsage.outputTokens : null
481
+ const cached = typeof rawUsage.cachedReadTokens === 'number' ? rawUsage.cachedReadTokens : null
482
+ usage.inputTokens = input
483
+ usage.outputTokens = output
484
+ usage.cachedInputTokens = cached
485
+ usage.reported = input !== null || output !== null
486
+ usage.status = usage.reported ? 'reported' : 'unknown'
487
+ }
488
+ const stopReason = typeof result?.stopReason === 'string' ? result.stopReason : null
489
+ const outcome = {
490
+ ok: false,
491
+ stopReason,
492
+ outcome: 'failed',
493
+ sessionId,
494
+ text: state.text,
495
+ producedItem: state.producedItem,
496
+ updates: state.updates.length,
497
+ usage,
498
+ // ACP reports no dollar figure: cost stays null/unknown here and flows to
499
+ // the Host ledger as evidence, never as an estimate made up in this adapter.
500
+ costUsd: null,
501
+ costStatus: 'unknown',
502
+ elapsedMs: Date.now() - turn.startedAt,
503
+ cancelRequestedAt,
504
+ errorText: null,
505
+ }
506
+ if (error) {
507
+ const aborted = error?.acpAborted === true || signal?.aborted === true || state.stopped === true
508
+ outcome.outcome = aborted ? 'cancelled' : 'failed'
509
+ outcome.stopReason = aborted ? 'cancelled' : (outcome.stopReason ?? 'rpc-error')
510
+ outcome.errorText = error?.message ?? String(error)
511
+ if (state.text || state.producedItem) outcome.partial = true
512
+ log?.({ event: 'acp.turn-failed', message: outcome.errorText })
513
+ return outcome
514
+ }
515
+ if (state.stopped || stopReason === STOP_REASONS.cancelled) {
516
+ outcome.ok = true
517
+ outcome.outcome = 'cancelled'
518
+ outcome.stopReason = 'cancelled'
519
+ return outcome
520
+ }
521
+ if (stopReason === STOP_REASONS.endTurn) {
522
+ if (!state.repliedText && !state.producedItem) {
523
+ // end_turn with nothing to show is a lost turn, not a success.
524
+ outcome.stopReason = 'empty_turn'
525
+ outcome.errorText = 'ACP agent ended the turn without producing any output'
526
+ return outcome
527
+ }
528
+ outcome.ok = true
529
+ outcome.outcome = 'succeeded'
530
+ return outcome
531
+ }
532
+ // refusal, max_turn_requests or a reason this client does not know: report it.
533
+ outcome.errorText = `ACP agent stopped with ${stopReason ?? 'no stopReason'}`
534
+ if (stopReason === null) outcome.stopReason = 'unknown'
535
+ log?.({ event: 'acp.turn-unfinished', stopReason })
536
+ return outcome
537
+ }
538
+
539
+ const close = (reason = 'closed') => {
540
+ if (closed) return
541
+ closed = true
542
+ closingReason = reason
543
+ permissions.close(reason)
544
+ if (current && !current.state.settled) {
545
+ current.state.settled = true
546
+ current = null
547
+ }
548
+ log?.({ event: 'acp.session-closed', reason })
549
+ }
550
+
551
+ signal?.addEventListener('abort', () => close('aborted'), { once: true })
552
+ if (signal?.aborted) close('aborted')
553
+
554
+ return Object.freeze({
555
+ initialize,
556
+ handshake,
557
+ authenticate,
558
+ openSession,
559
+ resetSession,
560
+ prompt,
561
+ cancel,
562
+ selectModel,
563
+ setMode,
564
+ close,
565
+ permissions,
566
+ get sessionId() { return sessionId },
567
+ get agentCapabilities() { return agentCapabilities },
568
+ get busy() { return sessionBusy() },
569
+ get closed() { return closed },
570
+ get closingReason() { return closingReason },
571
+ get turns() { return turns },
572
+ get stats() {
573
+ return Object.freeze({
574
+ turns, sessionId, closed, cancelRequestedAt,
575
+ agent: agentCapabilities ? { name: agentCapabilities.agentName, version: agentCapabilities.agentVersion, loadSession: agentCapabilities.loadSession } : null,
576
+ permissions: permissions.stats,
577
+ limits: { ...bounds },
578
+ })
579
+ },
580
+ })
581
+ }