@xyagent/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/README.md +860 -0
  2. package/bin/agentlink +8585 -0
  3. package/bin/agentlink-agent +7 -0
  4. package/bin/agentlink-mcp-stdio +173 -0
  5. package/docs/LOGGING.md +159 -0
  6. package/package.json +38 -0
  7. package/src/core.mjs +320 -0
  8. package/src/tunnel.mjs +291 -0
  9. package/src/tunnel_proxy.mjs +913 -0
  10. package/src/tunnel_service.mjs +699 -0
  11. package/src/tunnel_state.mjs +301 -0
  12. package/src/unified_producer.mjs +259 -0
  13. package/src-ext/README.md +75 -0
  14. package/src-ext/bin.mjs +128 -0
  15. package/src-ext/commands/agent.mjs +66 -0
  16. package/src-ext/commands/help.mjs +31 -0
  17. package/src-ext/commands/pair.mjs +455 -0
  18. package/src-ext/commands/reload.mjs +78 -0
  19. package/src-ext/commands/service.mjs +64 -0
  20. package/src-ext/core/accountResolver.mjs +137 -0
  21. package/src-ext/core/activeRuns.mjs +76 -0
  22. package/src-ext/core/agentlinkToolsBootstrap.mjs +75 -0
  23. package/src-ext/core/agentlinkToolsRegistry.mjs +323 -0
  24. package/src-ext/core/agentlinkToolsServer.mjs +109 -0
  25. package/src-ext/core/approvalGateway.mjs +132 -0
  26. package/src-ext/core/attachAuditLog.mjs +79 -0
  27. package/src-ext/core/attachDenylist.mjs +128 -0
  28. package/src-ext/core/autoTunnelDetector.mjs +98 -0
  29. package/src-ext/core/autoTunnelGateway.mjs +393 -0
  30. package/src-ext/core/autoTunnelWiring.mjs +184 -0
  31. package/src-ext/core/boundedMap.mjs +98 -0
  32. package/src-ext/core/bridgeAttachAutoScanner.mjs +237 -0
  33. package/src-ext/core/bridgeHooks.mjs +168 -0
  34. package/src-ext/core/bridgeSelfUninstall.mjs +133 -0
  35. package/src-ext/core/claudeTrust.mjs +74 -0
  36. package/src-ext/core/codexTrust.mjs +77 -0
  37. package/src-ext/core/cosUploadClient.mjs +326 -0
  38. package/src-ext/core/createWorkspace.mjs +170 -0
  39. package/src-ext/core/daemonRestart.mjs +49 -0
  40. package/src-ext/core/defaultWorkspace.mjs +65 -0
  41. package/src-ext/core/ensureGitRepo.mjs +57 -0
  42. package/src-ext/core/hermesProfile.mjs +75 -0
  43. package/src-ext/core/interruptGateway.mjs +119 -0
  44. package/src-ext/core/interruptRunListener.mjs +106 -0
  45. package/src-ext/core/mcpRuntimeFanout.mjs +236 -0
  46. package/src-ext/core/mcpStdioClient.mjs +316 -0
  47. package/src-ext/core/openclawGatewayClient.mjs +230 -0
  48. package/src-ext/core/pairCodeClient.mjs +331 -0
  49. package/src-ext/core/pairInventory.mjs +59 -0
  50. package/src-ext/core/pathExpand.mjs +34 -0
  51. package/src-ext/core/preprocessAttachments.mjs +113 -0
  52. package/src-ext/core/relayWorker.mjs +284 -0
  53. package/src-ext/core/relayWorkerHttp.mjs +58 -0
  54. package/src-ext/core/scanWorkspaces.mjs +842 -0
  55. package/src-ext/core/sessionSlot.mjs +70 -0
  56. package/src-ext/core/stateMachine.mjs +85 -0
  57. package/src-ext/core/toolsServerLifecycle.mjs +169 -0
  58. package/src-ext/core/unifiedDispatchHandler.mjs +439 -0
  59. package/src-ext/core/usageReporter.mjs +207 -0
  60. package/src-ext/openclaw-plugin/envelope-builder.cjs +619 -0
  61. package/src-ext/openclaw-plugin/index.cjs +1919 -0
  62. package/src-ext/openclaw-plugin/openclaw.plugin.json +20 -0
  63. package/src-ext/openclaw-plugin/package.json +12 -0
  64. package/src-ext/openclaw-plugin/relay-defaults.json +3 -0
  65. package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +101 -0
  66. package/src-ext/runtime/_shared/cliExec.mjs +138 -0
  67. package/src-ext/runtime/_shared/cosTextRefs.mjs +120 -0
  68. package/src-ext/runtime/_shared/fetchAndBase64Encode.mjs +52 -0
  69. package/src-ext/runtime/_shared/relayObjectToBlock.mjs +307 -0
  70. package/src-ext/runtime/_shared/sessionPicker.mjs +418 -0
  71. package/src-ext/runtime/_shared/slashCommandRouter.mjs +1346 -0
  72. package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +94 -0
  73. package/src-ext/runtime/claude/askUserQuestionTranslator.mjs +241 -0
  74. package/src-ext/runtime/claude/customCommandsCollector.mjs +132 -0
  75. package/src-ext/runtime/claude/handleRequest.mjs +1418 -0
  76. package/src-ext/runtime/claude/index.mjs +160 -0
  77. package/src-ext/runtime/claude/internalCommandNames.mjs +58 -0
  78. package/src-ext/runtime/claude/launcher.mjs +313 -0
  79. package/src-ext/runtime/claude/mcpConfigAdapter.mjs +174 -0
  80. package/src-ext/runtime/claude/mcpConfigWriter.mjs +57 -0
  81. package/src-ext/runtime/claude/modelContextWindow.mjs +30 -0
  82. package/src-ext/runtime/claude/modelScanner.mjs +257 -0
  83. package/src-ext/runtime/claude/permissionInterruptBlock.mjs +86 -0
  84. package/src-ext/runtime/claude/permissionPromptServer.mjs +170 -0
  85. package/src-ext/runtime/claude/permissionPromptServerWorker.mjs +214 -0
  86. package/src-ext/runtime/claude/preflight.mjs +62 -0
  87. package/src-ext/runtime/claude/stdoutParser.mjs +365 -0
  88. package/src-ext/runtime/claude/todoTranslator.mjs +66 -0
  89. package/src-ext/runtime/claude/usageProbe.mjs +576 -0
  90. package/src-ext/runtime/claude/usageProbeStrip.mjs +14 -0
  91. package/src-ext/runtime/codex/codexCustomCommandsCollector.mjs +87 -0
  92. package/src-ext/runtime/codex/elicitationHandler.mjs +91 -0
  93. package/src-ext/runtime/codex/eventMapper.mjs +154 -0
  94. package/src-ext/runtime/codex/handleRequest.mjs +1152 -0
  95. package/src-ext/runtime/codex/index.mjs +142 -0
  96. package/src-ext/runtime/codex/mcpConfigAdapter.mjs +155 -0
  97. package/src-ext/runtime/codex/modelScanner.mjs +305 -0
  98. package/src-ext/runtime/codex/planNotificationHandler.mjs +61 -0
  99. package/src-ext/runtime/codex/preflight.mjs +67 -0
  100. package/src-ext/runtime/hermes/commandBackend.mjs +117 -0
  101. package/src-ext/runtime/hermes/envSetup.mjs +229 -0
  102. package/src-ext/runtime/hermes/gatewayManager.mjs +625 -0
  103. package/src-ext/runtime/hermes/handleRequest.mjs +1257 -0
  104. package/src-ext/runtime/hermes/httpBackend.mjs +219 -0
  105. package/src-ext/runtime/hermes/index.mjs +196 -0
  106. package/src-ext/runtime/hermes/mcpConfigAdapter.mjs +189 -0
  107. package/src-ext/runtime/hermes/preflight.mjs +359 -0
  108. package/src-ext/runtime/hermes/responsesBackend.mjs +276 -0
  109. package/src-ext/runtime/hermes/sessionStore.mjs +78 -0
  110. package/src-ext/runtime/hermes/todoTranslator.mjs +90 -0
  111. package/src-ext/runtime/hermes/workspaceRpc.mjs +124 -0
  112. package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +143 -0
  113. package/src-ext/runtime/openclaw/mcpConfigAdapter.mjs +91 -0
  114. package/src-ext/runtime/openclaw/openclawConfig.mjs +145 -0
  115. package/src-ext/runtime/picoclaw/constants.mjs +39 -0
  116. package/src-ext/runtime/picoclaw/handleRequest.mjs +289 -0
  117. package/src-ext/runtime/picoclaw/index.mjs +314 -0
  118. package/src-ext/runtime/picoclaw/pairFlow.mjs +224 -0
  119. package/src-ext/runtime/picoclaw/state.mjs +78 -0
  120. package/src-ext/runtime/picoclaw/todoTranslator.mjs +67 -0
  121. package/src-ext/runtime/picoclaw/translator.mjs +272 -0
  122. package/src-ext/runtime/picoclaw/wsClient.mjs +129 -0
  123. package/src-ext/service/serviceManager.mjs +521 -0
  124. package/src-shared/README.md +44 -0
  125. package/src-shared/chat_push_normalizer.mjs +113 -0
  126. package/src-shared/correlation.mjs +25 -0
  127. package/src-shared/envelope_builder.mjs +612 -0
  128. package/src-shared/logger.mjs +167 -0
  129. package/src-shared/relay-defaults.json +3 -0
  130. package/src-shared/relay_revoke.mjs +46 -0
  131. package/src-shared/vendor/README.md +60 -0
  132. package/src-shared/vendor/smol-toml/LICENSE +24 -0
  133. package/src-shared/vendor/smol-toml/README.md +236 -0
  134. package/src-shared/vendor/smol-toml/VENDORED.md +37 -0
  135. package/src-shared/vendor/smol-toml/dist/date.d.ts +41 -0
  136. package/src-shared/vendor/smol-toml/dist/date.js +127 -0
  137. package/src-shared/vendor/smol-toml/dist/error.d.ts +38 -0
  138. package/src-shared/vendor/smol-toml/dist/error.js +63 -0
  139. package/src-shared/vendor/smol-toml/dist/extract.d.ts +30 -0
  140. package/src-shared/vendor/smol-toml/dist/extract.js +109 -0
  141. package/src-shared/vendor/smol-toml/dist/index.cjs +900 -0
  142. package/src-shared/vendor/smol-toml/dist/index.d.ts +43 -0
  143. package/src-shared/vendor/smol-toml/dist/index.js +33 -0
  144. package/src-shared/vendor/smol-toml/dist/parse.d.ts +37 -0
  145. package/src-shared/vendor/smol-toml/dist/parse.js +148 -0
  146. package/src-shared/vendor/smol-toml/dist/primitive.d.ts +31 -0
  147. package/src-shared/vendor/smol-toml/dist/primitive.js +178 -0
  148. package/src-shared/vendor/smol-toml/dist/stringify.d.ts +31 -0
  149. package/src-shared/vendor/smol-toml/dist/stringify.js +163 -0
  150. package/src-shared/vendor/smol-toml/dist/struct.d.ts +32 -0
  151. package/src-shared/vendor/smol-toml/dist/struct.js +194 -0
  152. package/src-shared/vendor/smol-toml/dist/util.d.ts +42 -0
  153. package/src-shared/vendor/smol-toml/dist/util.js +100 -0
  154. package/src-shared/vendor/smol-toml/package.json +54 -0
  155. package/src-shared/vendor/yaml/LICENSE +13 -0
  156. package/src-shared/vendor/yaml/README.md +155 -0
  157. package/src-shared/vendor/yaml/VENDORED.md +29 -0
  158. package/src-shared/vendor/yaml/bin.mjs +11 -0
  159. package/src-shared/vendor/yaml/browser/dist/compose/compose-collection.js +88 -0
  160. package/src-shared/vendor/yaml/browser/dist/compose/compose-doc.js +42 -0
  161. package/src-shared/vendor/yaml/browser/dist/compose/compose-node.js +92 -0
  162. package/src-shared/vendor/yaml/browser/dist/compose/compose-scalar.js +80 -0
  163. package/src-shared/vendor/yaml/browser/dist/compose/composer.js +217 -0
  164. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-map.js +113 -0
  165. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-scalar.js +198 -0
  166. package/src-shared/vendor/yaml/browser/dist/compose/resolve-block-seq.js +47 -0
  167. package/src-shared/vendor/yaml/browser/dist/compose/resolve-end.js +37 -0
  168. package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-collection.js +203 -0
  169. package/src-shared/vendor/yaml/browser/dist/compose/resolve-flow-scalar.js +223 -0
  170. package/src-shared/vendor/yaml/browser/dist/compose/resolve-props.js +148 -0
  171. package/src-shared/vendor/yaml/browser/dist/compose/util-contains-newline.js +34 -0
  172. package/src-shared/vendor/yaml/browser/dist/compose/util-empty-scalar-position.js +27 -0
  173. package/src-shared/vendor/yaml/browser/dist/compose/util-flow-indent-check.js +15 -0
  174. package/src-shared/vendor/yaml/browser/dist/compose/util-map-includes.js +17 -0
  175. package/src-shared/vendor/yaml/browser/dist/doc/Document.js +334 -0
  176. package/src-shared/vendor/yaml/browser/dist/doc/anchors.js +72 -0
  177. package/src-shared/vendor/yaml/browser/dist/doc/applyReviver.js +55 -0
  178. package/src-shared/vendor/yaml/browser/dist/doc/createNode.js +89 -0
  179. package/src-shared/vendor/yaml/browser/dist/doc/directives.js +176 -0
  180. package/src-shared/vendor/yaml/browser/dist/errors.js +57 -0
  181. package/src-shared/vendor/yaml/browser/dist/index.js +17 -0
  182. package/src-shared/vendor/yaml/browser/dist/log.js +14 -0
  183. package/src-shared/vendor/yaml/browser/dist/nodes/Alias.js +101 -0
  184. package/src-shared/vendor/yaml/browser/dist/nodes/Collection.js +147 -0
  185. package/src-shared/vendor/yaml/browser/dist/nodes/Node.js +38 -0
  186. package/src-shared/vendor/yaml/browser/dist/nodes/Pair.js +36 -0
  187. package/src-shared/vendor/yaml/browser/dist/nodes/Scalar.js +24 -0
  188. package/src-shared/vendor/yaml/browser/dist/nodes/YAMLMap.js +144 -0
  189. package/src-shared/vendor/yaml/browser/dist/nodes/YAMLSeq.js +113 -0
  190. package/src-shared/vendor/yaml/browser/dist/nodes/addPairToJSMap.js +104 -0
  191. package/src-shared/vendor/yaml/browser/dist/nodes/identity.js +36 -0
  192. package/src-shared/vendor/yaml/browser/dist/nodes/toJS.js +37 -0
  193. package/src-shared/vendor/yaml/browser/dist/parse/cst-scalar.js +214 -0
  194. package/src-shared/vendor/yaml/browser/dist/parse/cst-stringify.js +61 -0
  195. package/src-shared/vendor/yaml/browser/dist/parse/cst-visit.js +97 -0
  196. package/src-shared/vendor/yaml/browser/dist/parse/cst.js +98 -0
  197. package/src-shared/vendor/yaml/browser/dist/parse/lexer.js +717 -0
  198. package/src-shared/vendor/yaml/browser/dist/parse/line-counter.js +39 -0
  199. package/src-shared/vendor/yaml/browser/dist/parse/parser.js +954 -0
  200. package/src-shared/vendor/yaml/browser/dist/public-api.js +99 -0
  201. package/src-shared/vendor/yaml/browser/dist/schema/Schema.js +38 -0
  202. package/src-shared/vendor/yaml/browser/dist/schema/common/map.js +17 -0
  203. package/src-shared/vendor/yaml/browser/dist/schema/common/null.js +15 -0
  204. package/src-shared/vendor/yaml/browser/dist/schema/common/seq.js +17 -0
  205. package/src-shared/vendor/yaml/browser/dist/schema/common/string.js +14 -0
  206. package/src-shared/vendor/yaml/browser/dist/schema/core/bool.js +19 -0
  207. package/src-shared/vendor/yaml/browser/dist/schema/core/float.js +43 -0
  208. package/src-shared/vendor/yaml/browser/dist/schema/core/int.js +38 -0
  209. package/src-shared/vendor/yaml/browser/dist/schema/core/schema.js +23 -0
  210. package/src-shared/vendor/yaml/browser/dist/schema/json/schema.js +62 -0
  211. package/src-shared/vendor/yaml/browser/dist/schema/tags.js +83 -0
  212. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/binary.js +66 -0
  213. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/bool.js +26 -0
  214. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/float.js +46 -0
  215. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/int.js +71 -0
  216. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/omap.js +74 -0
  217. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/pairs.js +78 -0
  218. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/schema.js +37 -0
  219. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/set.js +93 -0
  220. package/src-shared/vendor/yaml/browser/dist/schema/yaml-1.1/timestamp.js +101 -0
  221. package/src-shared/vendor/yaml/browser/dist/stringify/foldFlowLines.js +146 -0
  222. package/src-shared/vendor/yaml/browser/dist/stringify/stringify.js +124 -0
  223. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyCollection.js +143 -0
  224. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyComment.js +20 -0
  225. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyDocument.js +85 -0
  226. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyNumber.js +24 -0
  227. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyPair.js +150 -0
  228. package/src-shared/vendor/yaml/browser/dist/stringify/stringifyString.js +328 -0
  229. package/src-shared/vendor/yaml/browser/dist/util.js +11 -0
  230. package/src-shared/vendor/yaml/browser/dist/visit.js +233 -0
  231. package/src-shared/vendor/yaml/browser/index.js +5 -0
  232. package/src-shared/vendor/yaml/browser/package.json +3 -0
  233. package/src-shared/vendor/yaml/dist/cli.d.ts +8 -0
  234. package/src-shared/vendor/yaml/dist/cli.mjs +199 -0
  235. package/src-shared/vendor/yaml/dist/compose/compose-collection.d.ts +11 -0
  236. package/src-shared/vendor/yaml/dist/compose/compose-collection.js +90 -0
  237. package/src-shared/vendor/yaml/dist/compose/compose-doc.d.ts +7 -0
  238. package/src-shared/vendor/yaml/dist/compose/compose-doc.js +44 -0
  239. package/src-shared/vendor/yaml/dist/compose/compose-node.d.ts +28 -0
  240. package/src-shared/vendor/yaml/dist/compose/compose-node.js +95 -0
  241. package/src-shared/vendor/yaml/dist/compose/compose-scalar.d.ts +5 -0
  242. package/src-shared/vendor/yaml/dist/compose/compose-scalar.js +82 -0
  243. package/src-shared/vendor/yaml/dist/compose/composer.d.ts +62 -0
  244. package/src-shared/vendor/yaml/dist/compose/composer.js +221 -0
  245. package/src-shared/vendor/yaml/dist/compose/resolve-block-map.d.ts +6 -0
  246. package/src-shared/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
  247. package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.d.ts +11 -0
  248. package/src-shared/vendor/yaml/dist/compose/resolve-block-scalar.js +200 -0
  249. package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.d.ts +6 -0
  250. package/src-shared/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
  251. package/src-shared/vendor/yaml/dist/compose/resolve-end.d.ts +6 -0
  252. package/src-shared/vendor/yaml/dist/compose/resolve-end.js +39 -0
  253. package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.d.ts +7 -0
  254. package/src-shared/vendor/yaml/dist/compose/resolve-flow-collection.js +205 -0
  255. package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.d.ts +10 -0
  256. package/src-shared/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  257. package/src-shared/vendor/yaml/dist/compose/resolve-props.d.ts +23 -0
  258. package/src-shared/vendor/yaml/dist/compose/resolve-props.js +150 -0
  259. package/src-shared/vendor/yaml/dist/compose/util-contains-newline.d.ts +2 -0
  260. package/src-shared/vendor/yaml/dist/compose/util-contains-newline.js +36 -0
  261. package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.d.ts +2 -0
  262. package/src-shared/vendor/yaml/dist/compose/util-empty-scalar-position.js +29 -0
  263. package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.d.ts +3 -0
  264. package/src-shared/vendor/yaml/dist/compose/util-flow-indent-check.js +17 -0
  265. package/src-shared/vendor/yaml/dist/compose/util-map-includes.d.ts +4 -0
  266. package/src-shared/vendor/yaml/dist/compose/util-map-includes.js +19 -0
  267. package/src-shared/vendor/yaml/dist/doc/Document.d.ts +141 -0
  268. package/src-shared/vendor/yaml/dist/doc/Document.js +336 -0
  269. package/src-shared/vendor/yaml/dist/doc/anchors.d.ts +24 -0
  270. package/src-shared/vendor/yaml/dist/doc/anchors.js +77 -0
  271. package/src-shared/vendor/yaml/dist/doc/applyReviver.d.ts +9 -0
  272. package/src-shared/vendor/yaml/dist/doc/applyReviver.js +57 -0
  273. package/src-shared/vendor/yaml/dist/doc/createNode.d.ts +17 -0
  274. package/src-shared/vendor/yaml/dist/doc/createNode.js +91 -0
  275. package/src-shared/vendor/yaml/dist/doc/directives.d.ts +49 -0
  276. package/src-shared/vendor/yaml/dist/doc/directives.js +178 -0
  277. package/src-shared/vendor/yaml/dist/errors.d.ts +21 -0
  278. package/src-shared/vendor/yaml/dist/errors.js +62 -0
  279. package/src-shared/vendor/yaml/dist/index.d.ts +22 -0
  280. package/src-shared/vendor/yaml/dist/index.js +50 -0
  281. package/src-shared/vendor/yaml/dist/log.d.ts +3 -0
  282. package/src-shared/vendor/yaml/dist/log.js +17 -0
  283. package/src-shared/vendor/yaml/dist/nodes/Alias.d.ts +28 -0
  284. package/src-shared/vendor/yaml/dist/nodes/Alias.js +103 -0
  285. package/src-shared/vendor/yaml/dist/nodes/Collection.d.ts +73 -0
  286. package/src-shared/vendor/yaml/dist/nodes/Collection.js +151 -0
  287. package/src-shared/vendor/yaml/dist/nodes/Node.d.ts +47 -0
  288. package/src-shared/vendor/yaml/dist/nodes/Node.js +40 -0
  289. package/src-shared/vendor/yaml/dist/nodes/Pair.d.ts +21 -0
  290. package/src-shared/vendor/yaml/dist/nodes/Pair.js +39 -0
  291. package/src-shared/vendor/yaml/dist/nodes/Scalar.d.ts +42 -0
  292. package/src-shared/vendor/yaml/dist/nodes/Scalar.js +27 -0
  293. package/src-shared/vendor/yaml/dist/nodes/YAMLMap.d.ts +53 -0
  294. package/src-shared/vendor/yaml/dist/nodes/YAMLMap.js +147 -0
  295. package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.d.ts +60 -0
  296. package/src-shared/vendor/yaml/dist/nodes/YAMLSeq.js +115 -0
  297. package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.d.ts +4 -0
  298. package/src-shared/vendor/yaml/dist/nodes/addPairToJSMap.js +106 -0
  299. package/src-shared/vendor/yaml/dist/nodes/identity.d.ts +23 -0
  300. package/src-shared/vendor/yaml/dist/nodes/identity.js +53 -0
  301. package/src-shared/vendor/yaml/dist/nodes/toJS.d.ts +27 -0
  302. package/src-shared/vendor/yaml/dist/nodes/toJS.js +39 -0
  303. package/src-shared/vendor/yaml/dist/options.d.ts +338 -0
  304. package/src-shared/vendor/yaml/dist/parse/cst-scalar.d.ts +64 -0
  305. package/src-shared/vendor/yaml/dist/parse/cst-scalar.js +218 -0
  306. package/src-shared/vendor/yaml/dist/parse/cst-stringify.d.ts +8 -0
  307. package/src-shared/vendor/yaml/dist/parse/cst-stringify.js +63 -0
  308. package/src-shared/vendor/yaml/dist/parse/cst-visit.d.ts +39 -0
  309. package/src-shared/vendor/yaml/dist/parse/cst-visit.js +99 -0
  310. package/src-shared/vendor/yaml/dist/parse/cst.d.ts +108 -0
  311. package/src-shared/vendor/yaml/dist/parse/cst.js +112 -0
  312. package/src-shared/vendor/yaml/dist/parse/lexer.d.ts +87 -0
  313. package/src-shared/vendor/yaml/dist/parse/lexer.js +719 -0
  314. package/src-shared/vendor/yaml/dist/parse/line-counter.d.ts +22 -0
  315. package/src-shared/vendor/yaml/dist/parse/line-counter.js +41 -0
  316. package/src-shared/vendor/yaml/dist/parse/parser.d.ts +84 -0
  317. package/src-shared/vendor/yaml/dist/parse/parser.js +958 -0
  318. package/src-shared/vendor/yaml/dist/public-api.d.ts +43 -0
  319. package/src-shared/vendor/yaml/dist/public-api.js +104 -0
  320. package/src-shared/vendor/yaml/dist/schema/Schema.d.ts +18 -0
  321. package/src-shared/vendor/yaml/dist/schema/Schema.js +40 -0
  322. package/src-shared/vendor/yaml/dist/schema/common/map.d.ts +2 -0
  323. package/src-shared/vendor/yaml/dist/schema/common/map.js +19 -0
  324. package/src-shared/vendor/yaml/dist/schema/common/null.d.ts +4 -0
  325. package/src-shared/vendor/yaml/dist/schema/common/null.js +17 -0
  326. package/src-shared/vendor/yaml/dist/schema/common/seq.d.ts +2 -0
  327. package/src-shared/vendor/yaml/dist/schema/common/seq.js +19 -0
  328. package/src-shared/vendor/yaml/dist/schema/common/string.d.ts +2 -0
  329. package/src-shared/vendor/yaml/dist/schema/common/string.js +16 -0
  330. package/src-shared/vendor/yaml/dist/schema/core/bool.d.ts +4 -0
  331. package/src-shared/vendor/yaml/dist/schema/core/bool.js +21 -0
  332. package/src-shared/vendor/yaml/dist/schema/core/float.d.ts +4 -0
  333. package/src-shared/vendor/yaml/dist/schema/core/float.js +47 -0
  334. package/src-shared/vendor/yaml/dist/schema/core/int.d.ts +4 -0
  335. package/src-shared/vendor/yaml/dist/schema/core/int.js +42 -0
  336. package/src-shared/vendor/yaml/dist/schema/core/schema.d.ts +1 -0
  337. package/src-shared/vendor/yaml/dist/schema/core/schema.js +25 -0
  338. package/src-shared/vendor/yaml/dist/schema/json/schema.d.ts +2 -0
  339. package/src-shared/vendor/yaml/dist/schema/json/schema.js +64 -0
  340. package/src-shared/vendor/yaml/dist/schema/json-schema.d.ts +69 -0
  341. package/src-shared/vendor/yaml/dist/schema/tags.d.ts +40 -0
  342. package/src-shared/vendor/yaml/dist/schema/tags.js +86 -0
  343. package/src-shared/vendor/yaml/dist/schema/types.d.ts +90 -0
  344. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.d.ts +2 -0
  345. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/binary.js +68 -0
  346. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.d.ts +7 -0
  347. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/bool.js +29 -0
  348. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.d.ts +4 -0
  349. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/float.js +50 -0
  350. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.d.ts +5 -0
  351. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/int.js +76 -0
  352. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.d.ts +28 -0
  353. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/omap.js +77 -0
  354. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.d.ts +10 -0
  355. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/pairs.js +82 -0
  356. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.d.ts +1 -0
  357. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
  358. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.d.ts +28 -0
  359. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/set.js +96 -0
  360. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.d.ts +6 -0
  361. package/src-shared/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +105 -0
  362. package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.d.ts +34 -0
  363. package/src-shared/vendor/yaml/dist/stringify/foldFlowLines.js +151 -0
  364. package/src-shared/vendor/yaml/dist/stringify/stringify.d.ts +21 -0
  365. package/src-shared/vendor/yaml/dist/stringify/stringify.js +127 -0
  366. package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.d.ts +17 -0
  367. package/src-shared/vendor/yaml/dist/stringify/stringifyCollection.js +145 -0
  368. package/src-shared/vendor/yaml/dist/stringify/stringifyComment.d.ts +10 -0
  369. package/src-shared/vendor/yaml/dist/stringify/stringifyComment.js +24 -0
  370. package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.d.ts +4 -0
  371. package/src-shared/vendor/yaml/dist/stringify/stringifyDocument.js +87 -0
  372. package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.d.ts +2 -0
  373. package/src-shared/vendor/yaml/dist/stringify/stringifyNumber.js +26 -0
  374. package/src-shared/vendor/yaml/dist/stringify/stringifyPair.d.ts +3 -0
  375. package/src-shared/vendor/yaml/dist/stringify/stringifyPair.js +152 -0
  376. package/src-shared/vendor/yaml/dist/stringify/stringifyString.d.ts +9 -0
  377. package/src-shared/vendor/yaml/dist/stringify/stringifyString.js +330 -0
  378. package/src-shared/vendor/yaml/dist/test-events.d.ts +4 -0
  379. package/src-shared/vendor/yaml/dist/test-events.js +134 -0
  380. package/src-shared/vendor/yaml/dist/util.d.ts +12 -0
  381. package/src-shared/vendor/yaml/dist/util.js +28 -0
  382. package/src-shared/vendor/yaml/dist/visit.d.ts +102 -0
  383. package/src-shared/vendor/yaml/dist/visit.js +236 -0
  384. package/src-shared/vendor/yaml/package.json +96 -0
  385. package/src-shared/vendor/yaml/util.js +2 -0
@@ -0,0 +1,1257 @@
1
+ /**
2
+ * handleRequest.mjs — hermes runtime D-C2.1
3
+ *
4
+ * 把 hermes handleRequest 逻辑提取为独立模块,以便单元测试。
5
+ *
6
+ * createHermesHandleRequest({ pre, log, gwId, usage, dispatchHandlerOptions, workspaceRpc? })
7
+ * → async (request, ctx) => void
8
+ *
9
+ * 请求路由(三分支):
10
+ * 1. request.request.__relayKind === "rpc" → workspaceRpc.dispatch → publishEvent response(body=JSON) + end → 结束
11
+ * 2. request.request.__kind === "agent.run.dispatch" → unifiedDispatchHandler(返回 true)→ 结束
12
+ * 3. 否则 → 原有 streamChat 旧路径(含 appendTurn)
13
+ *
14
+ * dispatchHandlerOptions:
15
+ * relayUrl string relay 服务地址
16
+ * bridgeToken string bridge 鉴权 token
17
+ * producerFactory? Function 注入 producer(测试用)
18
+ * openclawInvoke? Function 注入 openclawInvoke(测试用;若未注入则使用默认)
19
+ * _backendOverride? Object 注入旧路径 backend(测试用)
20
+ *
21
+ * workspaceRpc:
22
+ * 可选,createHermesWorkspaceRpc 实例。默认由 handleRequest 内部实例化。
23
+ * 注入用于单测隔离文件系统。
24
+ */
25
+
26
+ import { randomBytes } from "node:crypto";
27
+ import os from "node:os";
28
+ import path from "node:path";
29
+
30
+ import * as httpBackendMod from "./httpBackend.mjs";
31
+ import * as commandBackendMod from "./commandBackend.mjs";
32
+ import * as responsesBackendMod from "./responsesBackend.mjs";
33
+ import { appendTurn, clearHistory, readHistory } from "./sessionStore.mjs";
34
+ import { createUnifiedDispatchHandler } from "../../core/unifiedDispatchHandler.mjs";
35
+ import { fetchAndBase64Encode } from "../_shared/fetchAndBase64Encode.mjs";
36
+ import { relayObjectToBlock } from "../_shared/relayObjectToBlock.mjs";
37
+ import { createHermesWorkspaceRpc } from "./workspaceRpc.mjs";
38
+ import { scanWorkspacesByRuntime } from "../../core/scanWorkspaces.mjs";
39
+ import { createWorkspace as _defaultCreateWorkspace } from "../../core/createWorkspace.mjs";
40
+ import {
41
+ classifySlashCommand,
42
+ emitAssistantText,
43
+ } from "../_shared/slashCommandRouter.mjs";
44
+ import { createInterruptGateway } from "../../core/interruptGateway.mjs";
45
+ import { waitForRunResumed } from "../../core/interruptRunListener.mjs";
46
+ import { listSessionsForRuntime, buildSessionLabel, buildSessionSubtitle } from "../_shared/sessionPicker.mjs";
47
+ import {
48
+ buildRunStarted,
49
+ buildRunCompleted,
50
+ buildRunPaused,
51
+ buildBlockStarted,
52
+ buildBlockCompleted,
53
+ newBlockId,
54
+ newRunId,
55
+ } from "../../../src-shared/envelope_builder.mjs";
56
+ import { createHermesTodoTranslator } from "./todoTranslator.mjs";
57
+ import { createActiveRunRegistry } from "../../core/activeRuns.mjs";
58
+
59
+ // /v1/responses 端点选择:hermes-agent 自家实现,**仅**它支持;切上去 bridge
60
+ // 才能拿 delegate_task 的真实返回值(chat/completions 上 _on_tool_progress
61
+ // 过滤掉 tool.completed,api_server.py:751)。
62
+ //
63
+ // 启用规则:
64
+ // - pre.useResponsesApi === true → 走 responsesBackend(强制 opt-in)
65
+ // - pre.useResponsesApi === false → 走 httpBackend(强制 opt-out)
66
+ // - 未设 + apiBaseUrl 指向 localhost/127.0.0.1 → 默认 responsesBackend
67
+ // (本地 hermes-agent,大概率支持)
68
+ // - 未设 + 其它 → httpBackend(兼容 LM Studio/Ollama/vLLM 等)
69
+ //
70
+ // 失败时不自动 fallback —— 端点选错的话报错更清楚;用户改 HERMES_BRIDGE_USE_RESPONSES_API
71
+ // env 即可显式切回 chat/completions。
72
+ function _shouldUseResponsesApi(pre) {
73
+ if (typeof pre?.useResponsesApi === "boolean") return pre.useResponsesApi;
74
+ const envFlag = String(process.env.HERMES_BRIDGE_USE_RESPONSES_API || "").trim().toLowerCase();
75
+ if (envFlag === "1" || envFlag === "true" || envFlag === "yes") return true;
76
+ if (envFlag === "0" || envFlag === "false" || envFlag === "no") return false;
77
+ const url = String(pre?.apiBaseUrl || "");
78
+ return /\b(127\.0\.0\.1|localhost|::1)\b/.test(url);
79
+ }
80
+
81
+ // hermes-agent api_server.py 只在 tool.started 时发 event:hermes.tool.progress
82
+ // 帧(line 751: `if event_type != "tool.started": return`),不发完成事件。
83
+ // bridge 端要把这个**单向**信号补齐成 unified protocol 的 tool_use{started→completed}
84
+ // + tool_result 闭环,server subagent_synth 才能给 delegate_task 之类的派生工具
85
+ // 合成 subagent_ref(synth.go:34 defaultToolNames 已含 "delegate_task")。
86
+ //
87
+ // 生成 synthetic tool_call_id —— hermes 不暴露真实 id,bridge 自造一个稳定字串
88
+ // 即可,protocol 仅要求非空(go_relay/internal/protocol/unified/block.go:312)。
89
+ function newSyntheticToolCallId() {
90
+ return "tcu_hermes_" + randomBytes(8).toString("hex");
91
+ }
92
+
93
+ const SYSTEM_PROMPT_DEFAULT =
94
+ "You are Hermes, a helpful assistant integrated with the agentlink platform.";
95
+
96
+ /**
97
+ * 从 workspace_path(可含 ~ 前缀)中解析 profile 名和 hermesHome 绝对路径。
98
+ *
99
+ * workspace_path 形如:
100
+ * /Users/alice/.hermes/profiles/deepseek32 → profileName="deepseek32"
101
+ * ~/.hermes/profiles/gemini3pro → profileName="gemini3pro"
102
+ * ~/.hermes → profileName="default"(root profile)
103
+ * /Users/alice/.hermes → profileName="default"(root profile)
104
+ *
105
+ * align-runtime-default-workspace: 当归一化路径 === root ~/.hermes 时
106
+ * 返回 profileName="default"(不取 basename=".hermes"),
107
+ * 以与 readHermesWorkspaces 的 default entry 语义对齐。
108
+ *
109
+ * 返回 { profileName, profileHome } 或 null(不符合 profiles/* 路径结构时)。
110
+ *
111
+ * @param {string} workspacePath
112
+ * @returns {{ profileName: string, profileHome: string } | null}
113
+ */
114
+ function _resolveWorkspacePathProfile(workspacePath) {
115
+ if (!workspacePath || typeof workspacePath !== "string") return null;
116
+ // expand ~ 前缀
117
+ const expanded = workspacePath.startsWith("~")
118
+ ? path.join(os.homedir(), workspacePath.slice(1))
119
+ : workspacePath;
120
+ const normalized = path.resolve(expanded);
121
+ if (!normalized) return null;
122
+
123
+ // align-runtime-default-workspace: root hermesHome(~/.hermes)→ profileName="default"
124
+ const hermesRoot = path.join(os.homedir(), ".hermes");
125
+ if (path.normalize(normalized) === path.normalize(hermesRoot)) {
126
+ return { profileName: "default", profileHome: hermesRoot };
127
+ }
128
+
129
+ const profileName = path.basename(normalized);
130
+ if (!profileName) return null;
131
+ return { profileName, profileHome: normalized };
132
+ }
133
+
134
+ /**
135
+ * @param {{
136
+ * pre: {
137
+ * backend: "http"|"command",
138
+ * model: string,
139
+ * apiKey?: string,
140
+ * apiBaseUrl?: string,
141
+ * command?: string,
142
+ * },
143
+ * log: { info, warn, error, child? },
144
+ * gwId: string,
145
+ * usage: { addCost, schedulePublish, setTokensAbsolute },
146
+ * dispatchHandlerOptions: {
147
+ * relayUrl: string,
148
+ * bridgeToken: string,
149
+ * producerFactory?: Function,
150
+ * openclawInvoke?: Function,
151
+ * _backendOverride?: Object, // 仅测试用
152
+ * },
153
+ * profileHome: string 默认 profileHome(bridge 启动时由 pair state profileName 计算),
154
+ * 无 workspace_path 时作为 fallback。不允许内部 fallback 到 process.env.HERMES_HOME。
155
+ * workspaceRpc?: { dispatch(method: string, params: object): Promise<{ok:boolean, error?:string}> }
156
+ * 可选,测试隔离用注入的 fake rpc,优先于 profileHome
157
+ * _manageProfileGateway?: Function 可选,测试隔离用注入的 manageProfileGateway(模型 b per-conv 路由)
158
+ * }} opts
159
+ * @returns {(request: any, ctx: any) => Promise<void>}
160
+ */
161
+ export function createHermesHandleRequest({ pre, log, gwId, usage, dispatchHandlerOptions, profileHome, workspaceRpc: injectedWorkspaceRpc, scanner, _manageProfileGateway, _createWorkspace }) {
162
+ // Workspace RPC handler:处理 identity.soul.set 等 gateway → bridge RPC 请求
163
+ // 优先级:injectedWorkspaceRpc(测试隔离)> profileHome(必填,来自 resolveProfileHome(gwId))
164
+ // 不允许 fallback 到 process.env.HERMES_HOME 或无参兜底(核心不变量)
165
+ const workspaceRpc = injectedWorkspaceRpc ?? createHermesWorkspaceRpc({ hermesHome: profileHome, log });
166
+
167
+ // 确定旧路径使用的 backend(测试可通过 _backendOverride 注入)
168
+ const {
169
+ relayUrl,
170
+ bridgeToken,
171
+ producerFactory,
172
+ openclawInvoke: injectedOpenclawInvoke,
173
+ _backendOverride,
174
+ } = dispatchHandlerOptions ?? {};
175
+
176
+ // manageProfileGateway 能力:注入(测试隔离)或运行时懒加载(避免 import 循环)
177
+ // _manageProfileGateway 参数在构造时注入(测试 / index.mjs 生产路径)
178
+ let _resolvedManageProfileGateway = _manageProfileGateway ?? null;
179
+ let _gwModuleCache = null;
180
+ async function _gwModule() {
181
+ // 运行时动态加载 gatewayManager(避免 circular import);缓存模块
182
+ if (!_gwModuleCache) _gwModuleCache = await import("./gatewayManager.mjs");
183
+ return _gwModuleCache;
184
+ }
185
+ async function _getManageProfileGateway() {
186
+ if (_resolvedManageProfileGateway) return _resolvedManageProfileGateway;
187
+ _resolvedManageProfileGateway = (await _gwModule()).manageProfileGateway;
188
+ return _resolvedManageProfileGateway;
189
+ }
190
+
191
+ // interrupt 闸门 — /resume picker 挂起/恢复
192
+ const interruptGateway = createInterruptGateway({ log });
193
+
194
+ // per-handler activeRuns 注册表 — 供 sessions.abort / chat.abort 分支使用
195
+ const activeRuns = createActiveRunRegistry({ log });
196
+
197
+ // 默认 openclawInvoke:调用 hermes backend.streamChat,把 chunk 翻译为统一格式
198
+ // 模型 b:每次 dispatch 按 workspace_path 解析 profile,确保对应 gateway 健康,
199
+ // 用该 profile 的 baseUrl + hermesHome(per-conversation routing)。
200
+ // Task 4.1: 加入 blocks 参数,支持 image + PDF attachment multimodal content
201
+ // run_id 由 unifiedDispatchHandler 透传 —— hermes 在 delegate_task 派生 subagent
202
+ // 时需要它作为 parent_run_id 挂到 child run.started/run.completed 上。
203
+ const defaultOpenclawInvoke = async ({ run_id: parentRunId, input, blocks = [], onChunk: _rawOnChunk, _fetchAndBase64Encode, thread_id, workspace_path }) => {
204
+ // ── 模型 b: per-conversation profile 路由 ────────────────────────────────
205
+ // 若 workspace_path 非空,按请求动态解析 profile,确保其 gateway 健康,
206
+ // 用该 profile 的 baseUrl + hermesHome。否则 fallback 到默认 profileHome。
207
+ let effectiveProfileHome = profileHome; // 默认:bridge 启动时固定的 profileHome
208
+ let effectiveApiBaseUrl = pre.apiBaseUrl; // 默认:bridge 启动时固定的 apiBaseUrl
209
+ let effectiveApiKey = pre.apiKey; // 默认:bridge 启动时固定的 apiKey
210
+
211
+ const workspaceResolved = workspace_path ? _resolveWorkspacePathProfile(workspace_path) : null;
212
+ if (workspaceResolved) {
213
+ const { profileName, profileHome: reqProfileHome } = workspaceResolved;
214
+ let gwResult;
215
+ try {
216
+ const mgr = await _getManageProfileGateway();
217
+ gwResult = await mgr(profileName, reqProfileHome, { mode: "ensure", log });
218
+ } catch (err) {
219
+ // workspace_path 明确指定了 profile,但 gateway 起不来 —— **不回退默认 profile**,
220
+ // 否则会用错身份/凭证应答(串台)。直接抛错让上层走 response.failed,报错更诚实。
221
+ throw new Error(`hermes profile '${profileName}' gateway 启动异常:${err?.message || err}`);
222
+ }
223
+ if (gwResult?.ok && gwResult.baseUrl) {
224
+ effectiveProfileHome = reqProfileHome;
225
+ effectiveApiBaseUrl = gwResult.baseUrl + "/v1";
226
+ // bridge→gateway 鉴权必须用该 profile 自己的 API_SERVER_KEY(各 profile 不同);
227
+ // 沿用默认 profile 的 key 会被目标 gateway 以 401 拒(串台 bug fix)。
228
+ const { readProfileEnvValue } = await _gwModule();
229
+ const profKey = readProfileEnvValue(reqProfileHome, "API_SERVER_KEY");
230
+ if (profKey) effectiveApiKey = profKey;
231
+ log.info?.("hermes.per_conv_profile.resolved", "per-conversation profile gateway ready", {
232
+ run_id: parentRunId, profileName, profileHome: reqProfileHome,
233
+ baseUrl: effectiveApiBaseUrl,
234
+ });
235
+ } else {
236
+ // 指定了 profile 但 gateway 未就绪 → 报错,不静默回退默认身份。
237
+ throw new Error(`hermes profile '${profileName}' gateway 未就绪:${gwResult?.error || "unknown"}`);
238
+ }
239
+ }
240
+ // ── 构造本次请求的 pre(可能是 per-profile override)────────────────────
241
+ const effectivePre = (effectiveApiBaseUrl !== pre.apiBaseUrl || effectiveApiKey !== pre.apiKey)
242
+ ? { ...pre, apiBaseUrl: effectiveApiBaseUrl, apiKey: effectiveApiKey }
243
+ : pre;
244
+ // ── 自动 attach 扫描器(fire-and-forget,不阻塞流式输出)─────────────
245
+ // scanner 由工厂注入(bootstrapAgentlinkTools.scanner)。每个 turn 一个新
246
+ // accumulator,按 `\n` 切完整行交给 scanner,扫到本机路径就异步走 attach_file。
247
+ // 包一层 onChunk 拦截 response.output_text.delta 喂 accumulator;包完所有
248
+ // text 都覆盖(包括 slash command 的 emitAssistantText 路径)。
249
+ const _acc = scanner?.createTurnAccumulator({ requestId: parentRunId });
250
+ const onChunk = _acc
251
+ ? async (event) => {
252
+ if (event && event.event === "response.output_text.delta" && typeof event.delta === "string") {
253
+ try { _acc.append(event.delta); } catch { /* swallow: 扫描器异常永远不破坏主流 */ }
254
+ }
255
+ return _rawOnChunk(event);
256
+ }
257
+ : _rawOnChunk;
258
+ try {
259
+ // ── Hermes slash command 拦截 ─────────────────────────────────────────
260
+ // hermes worker 之前直接把 /status /usage /model 等当 prompt 喂给 LLM,
261
+ // LLM 拿 cached_context 当问题, 回 "你这条消息没有实际问题" 的废话。
262
+ // 跟 codex/claude 一样, 在 bridge 层拦下来, 用 bridge 真实状态合成响应。
263
+ const slash = await classifySlashCommand(input, "hermes", {
264
+ model: pre?.model || null,
265
+ apiBaseUrl: pre?.apiBaseUrl || null,
266
+ backend: pre?.backend || null,
267
+ cwd: process.cwd(),
268
+ gwId,
269
+ threadId: thread_id || null,
270
+ version: pre?.version || null,
271
+ historyFn: async () => {
272
+ try { return await readHistory(gwId); } catch { return []; }
273
+ },
274
+ usageStats: typeof usage?.getStats === "function" ? usage.getStats() : null,
275
+ });
276
+ if (slash.kind === "client-reset") {
277
+ try { await clearHistory(gwId); } catch { /* ignore */ }
278
+ log.info?.("hermes.slash.clear",
279
+ "client-reset slash command, cleared session history",
280
+ { gwId, command: slash.command });
281
+ await emitAssistantText(onChunk, slash.ackText);
282
+ return;
283
+ }
284
+ if (slash.kind === "bridge-synth") {
285
+ log.info?.("hermes.slash.synth",
286
+ "bridge-synthesized slash response",
287
+ { gwId, command: slash.command });
288
+ await emitAssistantText(onChunk, slash.responseText);
289
+ return;
290
+ }
291
+
292
+ // ── /resume interrupt session picker (hermes) ───────────────────────────
293
+ if (slash.kind === "interrupt-session-pick") {
294
+ log.info?.("hermes.slash.resume_picker", "/resume: starting interrupt session picker", { thread_id, run_id: parentRunId });
295
+ const sessions = await listSessionsForRuntime("hermes");
296
+ if (sessions.length === 0) {
297
+ await emitAssistantText(onChunk, "暂无历史会话,直接开始新对话吧");
298
+ return;
299
+ }
300
+ const blockId = newBlockId();
301
+ const options = sessions.map((s) => ({ id: s.session_id, label: buildSessionLabel(s), description: buildSessionSubtitle(s) }));
302
+ await onChunk({
303
+ event: "__envelopes",
304
+ envelopes: [
305
+ buildRunStarted(parentRunId),
306
+ buildBlockStarted(parentRunId, {
307
+ id: blockId, type: "interrupt", subtype: "question",
308
+ prompt: "选择要续接的历史会话", options, selection_mode: "single",
309
+ }),
310
+ buildRunPaused(parentRunId, [blockId]),
311
+ ],
312
+ });
313
+ const { promise } = interruptGateway.openRequest({ runId: parentRunId, blockId, interruptId: blockId });
314
+ const listenerPromise = (relayUrl && bridgeToken && thread_id)
315
+ ? waitForRunResumed({ relayUrl, bridgeToken, threadId: thread_id, runId: parentRunId, interruptId: blockId, log })
316
+ .then(({ value }) => interruptGateway.settle(blockId, value))
317
+ .catch((err) => log.warn?.("hermes.slash.resume_picker.listener_err", err?.message))
318
+ : Promise.resolve();
319
+ let decisionValue;
320
+ try {
321
+ decisionValue = await promise;
322
+ } catch {
323
+ return;
324
+ }
325
+ await onChunk({
326
+ event: "__envelopes",
327
+ envelopes: [
328
+ buildBlockCompleted(parentRunId, { id: blockId, type: "interrupt", subtype: "question", prompt: "选择要续接的历史会话", status: "completed", value: decisionValue }),
329
+ buildRunCompleted(parentRunId, "end_turn"),
330
+ ],
331
+ });
332
+ // hermes 的 session 续接:选中的 session_id 对应 ~/.agentlink/hermes/sessions/<id>.json
333
+ // 下次 dispatch 时,hermes readHistory(sessionId) 会加载这个 session 的历史消息。
334
+ // 通过 gwId 作为 hermes 的 session key(hermes sessionStore 用 gwId 键)。
335
+ const selectedSessionId = decisionValue?.option_id ?? null;
336
+ if (selectedSessionId) {
337
+ log.info?.("hermes.slash.resume_picker.done", "hermes session selected, loading history on next turn", {
338
+ session_id: selectedSessionId,
339
+ });
340
+ // 把选中 session 的历史消息预加载到当前 gwId:实质上是把 selectedSessionId 的
341
+ // history 复制到 gwId session,以便下次 dispatch 能带上正确的上下文。
342
+ // 注意:这是 hermes-specific 的 resume 语义,不同于 claude --resume。
343
+ try {
344
+ const { readHistory: readHistoryFn, appendTurn: appendTurnFn } = await import("./sessionStore.mjs");
345
+ const history = await readHistoryFn(selectedSessionId);
346
+ // 以最后一条 user 消息为截断点,取整完整的 turn 对
347
+ if (history.length > 0) {
348
+ // 把 selectedSession 的历史写入 gwId session(替换,而非追加)
349
+ const { ensureRoot } = await import("./sessionStore.mjs");
350
+ const { promises: fsp } = await import("node:fs");
351
+ const nodePath = await import("node:path");
352
+ const nodeOs = await import("node:os");
353
+ const ROOT = nodePath.default.join(nodeOs.default.homedir(), ".agentlink", "hermes", "sessions");
354
+ await ensureRoot();
355
+ await fsp.writeFile(
356
+ nodePath.default.join(ROOT, `${gwId}.json`),
357
+ JSON.stringify({ messages: history }, null, 2),
358
+ "utf8",
359
+ );
360
+ log.info?.("hermes.slash.resume_picker.history_loaded", "history copied to current session", {
361
+ from: selectedSessionId, to: gwId, turns: Math.floor(history.length / 2),
362
+ });
363
+ }
364
+ } catch (err) {
365
+ log.warn?.("hermes.slash.resume_picker.history_err", "failed to load history for resumed session", {
366
+ session_id: selectedSessionId, err: err?.message,
367
+ });
368
+ }
369
+ }
370
+ await listenerPromise;
371
+ return;
372
+ }
373
+
374
+ // not-slash: 继续走 LLM 路径
375
+
376
+ const useResponses = _shouldUseResponsesApi(effectivePre);
377
+ let backendName = "httpBackend";
378
+ let backend;
379
+ if (_backendOverride) {
380
+ backend = _backendOverride;
381
+ backendName = "override";
382
+ } else if (effectivePre.backend === "command") {
383
+ backend = commandBackendMod;
384
+ backendName = "commandBackend";
385
+ } else if (useResponses) {
386
+ backend = responsesBackendMod;
387
+ backendName = "responsesBackend";
388
+ } else {
389
+ backend = httpBackendMod;
390
+ backendName = "httpBackend";
391
+ }
392
+ const doFetchAndBase64 = _fetchAndBase64Encode ?? fetchAndBase64Encode;
393
+ log.info("dispatch.debug.in", "hermes dispatch handling", {
394
+ run_id: parentRunId,
395
+ backend: backendName,
396
+ api_base_url: effectivePre.apiBaseUrl,
397
+ useResponsesApi_decision: useResponses,
398
+ useResponsesApi_envFlag: String(process.env.HERMES_BRIDGE_USE_RESPONSES_API || "(unset)"),
399
+ blocks_summary: (blocks ?? []).map((b) => {
400
+ const kind = b?.source?.kind ? `/${b.source.kind}` : "";
401
+ const mime = b?.mime ? `(${b.mime})` : "";
402
+ return `${b?.type ?? "?"}${kind}${mime}`;
403
+ }).join(",") || "(none)",
404
+ inputText_len: typeof input === "string" ? input.length : 0,
405
+ });
406
+
407
+ // Phase 5 (multimodal-attachment-llm-dispatch): relay_object blocks を
408
+ // relayObjectToBlock に通し、結果を OpenAI 互換形式(image_url / file)に変換する。
409
+ // hermes は ["text","image"] の caps(pdf は pdfjs fallback 経由)
410
+ const relayContentBlocks = [];
411
+ const relayBlocks = (blocks ?? []).filter(
412
+ (b) =>
413
+ (b?.type === "image" || b?.type === "attachment") &&
414
+ b?.source?.kind === "url" &&
415
+ b?.source?.url,
416
+ );
417
+ for (const block of relayBlocks) {
418
+ const result = await relayObjectToBlock(block, {
419
+ runtimeHint: "hermes",
420
+ modelCaps: ["text", "image"], // hermes: image ok, pdf → text fallback
421
+ fetchAndBase64Encode: doFetchAndBase64,
422
+ });
423
+
424
+ if (result.error) {
425
+ // caps_mismatch / fetch_failed / size_exceeded → text fallback
426
+ const name = block?.name ?? block?.source?.media_type ?? "file";
427
+ relayContentBlocks.push({
428
+ type: "text",
429
+ text: `[${block?.type === "image" ? "image" : `file: ${name}`}] (encoding failed: ${result.error.message})`,
430
+ });
431
+ continue;
432
+ }
433
+
434
+ // relayObjectToBlock 結果を OpenAI 互換 content block に変換
435
+ switch (result.type) {
436
+ case "input_image":
437
+ // OpenAI image_url format: data URI
438
+ relayContentBlocks.push({
439
+ type: "image_url",
440
+ image_url: { url: `data:${result.source.media_type};base64,${result.source.data}` },
441
+ });
442
+ break;
443
+ case "input_file":
444
+ // OpenAI file format: data URI
445
+ relayContentBlocks.push({
446
+ type: "file",
447
+ file: {
448
+ filename: result.source.filename,
449
+ file_data: `data:${result.source.media_type};base64,${result.source.data}`,
450
+ },
451
+ });
452
+ break;
453
+ case "input_text":
454
+ // text fallback(document text extraction / binary description)
455
+ relayContentBlocks.push({ type: "text", text: result.text });
456
+ break;
457
+ default:
458
+ break;
459
+ }
460
+ }
461
+
462
+ // relay block を含む場合は content array、なければ string(向后兼容)
463
+ const hasMultimodal = relayContentBlocks.length > 0;
464
+ let userContent;
465
+ if (hasMultimodal) {
466
+ // array 格式:text block(可以为空字符串,OpenAI 允许)+ relay content blocks
467
+ const contentArray = [];
468
+ if (input) {
469
+ contentArray.push({ type: "text", text: input });
470
+ }
471
+ contentArray.push(...relayContentBlocks);
472
+ userContent = contentArray;
473
+ } else {
474
+ userContent = input;
475
+ }
476
+
477
+ // 构造消息(dispatch 路径不读本地 history,直接用 input 作单条 user 消息)
478
+ const messages = [
479
+ { role: "system", content: SYSTEM_PROMPT_DEFAULT },
480
+ { role: "user", content: userContent },
481
+ ];
482
+ log.info("dispatch.debug.built", "hermes user content ready", {
483
+ hasMultimodal,
484
+ relay_block_count: relayContentBlocks.length,
485
+ relay_block_types: relayContentBlocks.map((b) => b.type).join(","),
486
+ user_content_kind: Array.isArray(userContent) ? "array" : "string",
487
+ user_content_parts: Array.isArray(userContent) ? userContent.map((p) => p.type).join(",") : null,
488
+ backend: effectivePre.backend,
489
+ model: effectivePre.model,
490
+ apiBaseUrl: effectivePre.apiBaseUrl,
491
+ });
492
+ // AbortController — 供 sessions.abort / chat.abort RPC 中断本次流式
493
+ // dispatch 路径:sessionKey / runId 均为 parentRunId(unifiedDispatch 协议)
494
+ const ac = new AbortController();
495
+ const unregisterRun = activeRuns.register({
496
+ sessionKey: String(parentRunId ?? ""),
497
+ threadId: String(thread_id ?? ""),
498
+ runId: String(parentRunId ?? ""),
499
+ kill: (reason) => ac.abort(reason),
500
+ });
501
+ const stream = backend.streamChat(messages, {
502
+ apiBaseUrl: effectivePre.apiBaseUrl,
503
+ apiKey: effectivePre.apiKey,
504
+ command: effectivePre.command,
505
+ model: effectivePre.model,
506
+ hermesHome: effectiveProfileHome, // per-conversation profile HERMES_HOME(模型 b)
507
+ signal: ac.signal,
508
+ });
509
+ log.info("dispatch.debug.stream_opened", "hermes backend.streamChat returned, awaiting chunks");
510
+ // Track open tool_use calls so we can synthesize a completed event when
511
+ // the stream ends (OpenAI tool_calls don't emit a per-call completion).
512
+ const openToolCalls = new Map(); // call_id → { name, argsAccum }
513
+
514
+ // todo translator:在 envelope_builder 之前拦截 todo 工具调用(Responses API 路径)
515
+ const todoTranslator = createHermesTodoTranslator(parentRunId);
516
+
517
+ // ── hermes subagent state machine ─────────────────────────────────────
518
+ //
519
+ // hermes-agent api_server.py 只发 `event: hermes.tool.progress` (tool.started),
520
+ // 不发 tool.finished / 子 agent 完成事件。bridge 必须补齐两件事:
521
+ //
522
+ // 1. 每个 tool 的 tool_result —— 老实现把所有 progress 攒到 stream done
523
+ // 再一次性 flush,意味着用户要等整轮 LLM 输出结束才能看到任何 tool
524
+ // 结果。新实现「立即 flush」:下一个 tool.progress 到达时(或 done
525
+ // 时)就关闭上一个 tool,让 App 实时看到进度。
526
+ //
527
+ // 2. delegate_task 的子 run 生命周期 —— hermes 不开独立 session,bridge
528
+ // 为 delegate_task 合成一条 child run(run.started/run.completed 带
529
+ // parent_run_id + extensions.openclaw.child_session_key),让
530
+ // subagent_synth.synthChildRunTerminal 走标准 §9.1.4 时序在 child
531
+ // run.completed 落地时发 subagent_ref.completed;delegate_task 之
532
+ // 后到达的工具进度(read / exec / ...)都挂在 child run 上,App 端
533
+ // SubagentRefBlockView 可以通过 view.findRun(childRunId) 展开看子
534
+ // agent 干了什么。
535
+ //
536
+ // hermes 不发嵌套信息 —— 后续 delegate_task 视为 sibling(关掉前一个
537
+ // subagent 再开新的)。
538
+ //
539
+ /** @type {null | {
540
+ * childRunId: string,
541
+ * parentCallId: string,
542
+ * agent: string,
543
+ * childSessionKey: string,
544
+ * childTools: Array<{ name: string, label: string }>,
545
+ * }} */
546
+ let currentSubagent = null;
547
+ /** @type {null | { callId: string, name: string, label: string, runId: string }} */
548
+ let inflightTool = null;
549
+
550
+ // Immediate-flush: 关闭当前 inflightTool, 发它的 tool_result。call site:
551
+ // - 下一个 hermes_tool_progress 到达时
552
+ // - stream done / error 时
553
+ async function flushInflightTool(isError = false) {
554
+ if (!inflightTool) return;
555
+ const { callId, name, label, runId } = inflightTool;
556
+ inflightTool = null;
557
+ const summaryText = label || `${name} completed`;
558
+ if (currentSubagent && runId === currentSubagent.childRunId) {
559
+ currentSubagent.childTools.push({ name, label });
560
+ }
561
+ await onChunk({
562
+ event: "response.tool_result",
563
+ call_id: callId,
564
+ run_id: runId,
565
+ output: [{ type: "text", text: summaryText }],
566
+ is_error: isError,
567
+ summary: summaryText,
568
+ });
569
+ }
570
+
571
+ // 关闭当前 subagent:
572
+ // 1) 把 child tools 的摘要拼起来,加上 childSessionKey 做成 JSON
573
+ // 写入 delegate_task 的 tool_result.output(在 parent run 上发)。
574
+ // → server synth.synthCompleted 抽 childSessionKey,挂到
575
+ // pendingByChildKey,等 child run.completed。
576
+ // 2) 发 child run.completed 带 parent_run_id + extensions.openclaw.
577
+ // child_session_key → server synth.synthChildRunTerminal 看到匹配
578
+ // 就发 subagent_ref.completed 到 parent run timeline。
579
+ async function closeCurrentSubagent(isError = false) {
580
+ if (!currentSubagent) return;
581
+ const sub = currentSubagent;
582
+ currentSubagent = null;
583
+ // 优先用 subagent_output 条目(Responses API 路径下子 agent 的实际答案)
584
+ // 否则按 childTools 拼摘要;都没有时给 fallback 文案。
585
+ const subagentOutputs = sub.childTools
586
+ .filter((t) => t.name === "subagent_output" && t.label)
587
+ .map((t) => t.label);
588
+ const summaryText = subagentOutputs.length > 0
589
+ ? subagentOutputs.join(" · ")
590
+ : (sub.childTools.length > 0
591
+ ? sub.childTools
592
+ .map((t) => (t.label ? `${t.name}: ${t.label}` : t.name))
593
+ .join(" · ")
594
+ : (isError ? "subagent failed" : "subagent completed"));
595
+ // delegate_task tool_result on PARENT run,output[0].text 是 JSON 串
596
+ // —— extractSummary 期望 JSON 格式才能抽 childSessionKey(synth.go:575)。
597
+ await onChunk({
598
+ event: "response.tool_result",
599
+ call_id: sub.parentCallId,
600
+ run_id: parentRunId,
601
+ output: [{
602
+ type: "text",
603
+ text: JSON.stringify({
604
+ summary: summaryText,
605
+ childSessionKey: sub.childSessionKey,
606
+ status: isError ? "failed" : "completed",
607
+ }),
608
+ }],
609
+ is_error: isError,
610
+ summary: summaryText,
611
+ });
612
+ // subagent_ref.completed —— 发给父 run,用同一 refBlockId(信号 2 路径)。
613
+ // 在 child run.completed 之前发出,App 可以在子 run 落地前先更新 subagent 卡片。
614
+ // 使用 sub(已赋值的局部变量),currentSubagent 已被置 null。
615
+ await onChunk({
616
+ event: "__envelopes",
617
+ envelopes: [
618
+ buildBlockCompleted(parentRunId, {
619
+ id: sub.refBlockId,
620
+ type: "subagent_ref",
621
+ child_run_id: sub.childRunId,
622
+ spawn_tool_use_id: sub.parentCallId,
623
+ agent: sub.agent,
624
+ summary: summaryText,
625
+ status: isError ? "failed" : "completed",
626
+ stop_reason: isError ? "error" : "end_turn",
627
+ }),
628
+ ],
629
+ });
630
+ // child run.completed —— 用 __envelopes passthrough 直接构造,带
631
+ // parent_run_id + extensions.openclaw.child_session_key。
632
+ await onChunk({
633
+ event: "__envelopes",
634
+ envelopes: [
635
+ buildRunCompleted(sub.childRunId, isError ? "error" : "end_turn", {
636
+ parentRunId,
637
+ extensions: { openclaw: { child_session_key: sub.childSessionKey } },
638
+ }),
639
+ ],
640
+ });
641
+ }
642
+
643
+ for await (const chunk of stream) {
644
+ if (chunk.kind === "delta") {
645
+ const text = chunk.text || "";
646
+ if (text) {
647
+ await onChunk({ event: "response.output_text.delta", delta: text });
648
+ }
649
+ } else if (chunk.kind === "thinking") {
650
+ const text = chunk.text || "";
651
+ if (text) {
652
+ await onChunk({ event: "response.reasoning_summary_text.delta", delta: text });
653
+ }
654
+ } else if (chunk.kind === "tool_use_start") {
655
+ const callId = String(chunk.call_id || "");
656
+ const name = String(chunk.name || "unknown");
657
+ if (callId) {
658
+ openToolCalls.set(callId, { name, argsAccum: "" });
659
+ await onChunk({ event: "response.tool_use.start", call_id: callId, name });
660
+ }
661
+ } else if (chunk.kind === "tool_use_args_delta") {
662
+ const callId = String(chunk.call_id || "");
663
+ const args = String(chunk.arguments || "");
664
+ if (callId && args) {
665
+ const tc = openToolCalls.get(callId);
666
+ if (tc) tc.argsAccum += args;
667
+ await onChunk({ event: "response.tool_use.arguments.delta", call_id: callId, delta: args });
668
+ }
669
+ } else if (chunk.kind === "hermes_tool_progress") {
670
+ // hermes-agent 内部 LLM 决策走 tool — 详见上面 state machine 注释。
671
+ const toolName = String(chunk.tool || "");
672
+ const label = String(chunk.label || "");
673
+ if (!toolName) continue;
674
+
675
+ // 1) 先 immediate-flush 上一个挂着的 tool(不论是父 tool 还是 child tool)
676
+ if (inflightTool) {
677
+ await flushInflightTool(false);
678
+ }
679
+
680
+ if (toolName === "delegate_task") {
681
+ // 2a) delegate_task —— 派生新 subagent
682
+ // 若已有未关闭的 subagent,先关掉(hermes 不发嵌套,按 sibling 处理)
683
+ if (currentSubagent) {
684
+ await closeCurrentSubagent(false);
685
+ }
686
+ const childRunId = newRunId();
687
+ const parentCallId = newSyntheticToolCallId();
688
+ const childSessionKey = "hermes:subagent:" + childRunId;
689
+ const inputObj = label ? { task: label } : {};
690
+ const argsJson = JSON.stringify(inputObj);
691
+ // subagent_ref.started 必须在 tool_use(delegate_task) **之前**发出。
692
+ // 原因:unifiedDispatchHandler 默认 flushPolicy size:1 来即发,每个 envelope
693
+ // 单独 POST,relay scanSDKSignals 单 batch 只看到 1 个 envelope。若 tool_use
694
+ // 先到,synth.synthStarted 立即兜底合成 subagent_ref(mint 另一个 child_run_id),
695
+ // 待我们的 subagent_ref{source=runtime} 后到时已经晚了 → 出现两张卡。
696
+ // 先发 subagent_ref → sdkModeByTool 写入持久 map → 后续 tool_use batch
697
+ // 命中 SDK 模式 → synth.synthStarted 跳过自合成。spec lifecycle 示例的
698
+ // (tool_use, run.started, subagent_ref) 顺序在 size:1 流式策略下不可用。
699
+ const refBlockId = newBlockId();
700
+ await onChunk({
701
+ event: "__envelopes",
702
+ envelopes: [
703
+ buildBlockStarted(parentRunId, {
704
+ id: refBlockId,
705
+ type: "subagent_ref",
706
+ child_run_id: childRunId,
707
+ spawn_tool_use_id: parentCallId,
708
+ agent: label || "delegate_task",
709
+ summary: "",
710
+ }),
711
+ ],
712
+ });
713
+ await onChunk({
714
+ event: "response.tool_use.start",
715
+ call_id: parentCallId, name: toolName, run_id: parentRunId,
716
+ });
717
+ if (argsJson && argsJson !== "{}") {
718
+ await onChunk({
719
+ event: "response.tool_use.arguments.delta",
720
+ call_id: parentCallId, delta: argsJson, run_id: parentRunId,
721
+ });
722
+ }
723
+ await onChunk({
724
+ event: "response.tool_use.completed",
725
+ call_id: parentCallId, name: toolName, input: inputObj, run_id: parentRunId,
726
+ });
727
+ // child run.started —— ingester 识别新 run;后续 child tool events
728
+ // 都挂在这个 run 上。spawnToolUseId 允许 relay synth SDK 模式
729
+ // 用 CLI 给的 run_id 而不自行 mint。
730
+ await onChunk({
731
+ event: "__envelopes",
732
+ envelopes: [
733
+ buildRunStarted(childRunId, {
734
+ parentRunId,
735
+ spawnToolUseId: parentCallId,
736
+ extensions: { openclaw: { child_session_key: childSessionKey } },
737
+ }),
738
+ ],
739
+ });
740
+ currentSubagent = {
741
+ childRunId,
742
+ parentCallId,
743
+ agent: label || "delegate_task",
744
+ childSessionKey,
745
+ childTools: [],
746
+ refBlockId,
747
+ };
748
+ // delegate_task 的 tool_result 等到 closeCurrentSubagent 时发;这里
749
+ // 不设 inflightTool(设了反而会被下一个 child tool 的 flush 误关)
750
+ } else {
751
+ // 2b) 普通工具(read / exec / recall / ...)—— 落在当前作用域(child
752
+ // run 若有 subagent active,否则 parent run)
753
+ const scopedRunId = currentSubagent ? currentSubagent.childRunId : parentRunId;
754
+ const callId = newSyntheticToolCallId();
755
+ const inputObj = label ? { task: label } : {};
756
+ const argsJson = JSON.stringify(inputObj);
757
+ await onChunk({
758
+ event: "response.tool_use.start",
759
+ call_id: callId, name: toolName, run_id: scopedRunId,
760
+ ...(currentSubagent ? { parent_tool_use_id: currentSubagent.parentCallId } : {}),
761
+ });
762
+ if (argsJson && argsJson !== "{}") {
763
+ await onChunk({
764
+ event: "response.tool_use.arguments.delta",
765
+ call_id: callId, delta: argsJson, run_id: scopedRunId,
766
+ });
767
+ }
768
+ await onChunk({
769
+ event: "response.tool_use.completed",
770
+ call_id: callId, name: toolName, input: inputObj, run_id: scopedRunId,
771
+ ...(currentSubagent ? { parent_tool_use_id: currentSubagent.parentCallId } : {}),
772
+ });
773
+ inflightTool = { callId, name: toolName, label, runId: scopedRunId };
774
+ }
775
+ } else if (chunk.kind === "tool_call_started") {
776
+ // Responses API 路径:tool.started 事件,arguments 完整、call_id 真实。
777
+ // 跟 hermes_tool_progress 分支的差别 ——
778
+ // 1) call_id 用 hermes-agent 真实下发的(不再 mint synthetic)
779
+ // 2) 不需要 inflightTool flush 机制(complete 会单独到达)
780
+ // 3) input 解析 arguments JSON,不仅是 label
781
+
782
+ // todo translator:在 envelope_builder 之前拦截 todo 工具调用
783
+ {
784
+ const intercepted = todoTranslator.intercept(chunk);
785
+ if (intercepted.length === 0) continue; // 静默丢弃(不应发生于 started)
786
+ if (intercepted[0] !== chunk) {
787
+ // translator 返回了 todo_list envelopes
788
+ try { await onChunk({ event: "__envelopes", envelopes: intercepted }); } catch { /* ignore */ }
789
+ continue;
790
+ }
791
+ }
792
+
793
+ const toolName = String(chunk.name || "");
794
+ const callId = String(chunk.call_id || "");
795
+ if (!toolName || !callId) continue;
796
+ let inputObj = {};
797
+ if (chunk.arguments) {
798
+ try { inputObj = JSON.parse(String(chunk.arguments)); } catch { inputObj = {}; }
799
+ }
800
+ log.info("hermes.tool_call_started", "responses tool start", {
801
+ tool_name: toolName, call_id: callId,
802
+ args_keys: Object.keys(inputObj).slice(0, 8),
803
+ is_delegate: toolName === "delegate_task",
804
+ });
805
+ if (toolName === "delegate_task") {
806
+ // 关掉前一个 subagent(sibling 处理)
807
+ if (currentSubagent) {
808
+ await closeCurrentSubagent(false);
809
+ }
810
+ const childRunId = newRunId();
811
+ const childSessionKey = "hermes:subagent:" + childRunId;
812
+ const argsJson = JSON.stringify(inputObj);
813
+ // task 描述:偏好 input.task / input.goal / input.prompt,否则截 JSON
814
+ const agentLabel = String(
815
+ inputObj?.task ?? inputObj?.goal ?? inputObj?.prompt ?? toolName,
816
+ ).slice(0, 80);
817
+ // subagent_ref.started 必须在 tool_use(delegate_task) **之前**发出。
818
+ // size:1 流式策略下 relay scanSDKSignals 单 batch 一个 envelope,
819
+ // 必须先把 SDK 信号塞进 sdkModeByTool,后续 tool_use batch 才会命中
820
+ // SDK 模式跳过自合成(同 hermes_tool_progress 分支注释)。
821
+ const refBlockIdResp = newBlockId();
822
+ await onChunk({
823
+ event: "__envelopes",
824
+ envelopes: [
825
+ buildBlockStarted(parentRunId, {
826
+ id: refBlockIdResp,
827
+ type: "subagent_ref",
828
+ child_run_id: childRunId,
829
+ spawn_tool_use_id: callId,
830
+ agent: agentLabel,
831
+ summary: "",
832
+ }),
833
+ ],
834
+ });
835
+ // tool_use(delegate_task) on PARENT run,call_id 沿用 hermes 真实下发
836
+ await onChunk({
837
+ event: "response.tool_use.start",
838
+ call_id: callId, name: toolName, run_id: parentRunId,
839
+ });
840
+ if (argsJson && argsJson !== "{}") {
841
+ await onChunk({
842
+ event: "response.tool_use.arguments.delta",
843
+ call_id: callId, delta: argsJson, run_id: parentRunId,
844
+ });
845
+ }
846
+ await onChunk({
847
+ event: "response.tool_use.completed",
848
+ call_id: callId, name: toolName, input: inputObj, run_id: parentRunId,
849
+ });
850
+ // child run.started(带 spawnToolUseId)
851
+ await onChunk({
852
+ event: "__envelopes",
853
+ envelopes: [
854
+ buildRunStarted(childRunId, {
855
+ parentRunId,
856
+ spawnToolUseId: callId,
857
+ extensions: { openclaw: { child_session_key: childSessionKey } },
858
+ }),
859
+ ],
860
+ });
861
+ currentSubagent = {
862
+ childRunId,
863
+ parentCallId: callId,
864
+ agent: agentLabel,
865
+ childSessionKey,
866
+ childTools: [],
867
+ refBlockId: refBlockIdResp,
868
+ };
869
+ } else {
870
+ // 普通工具 —— 落在当前作用域
871
+ const scopedRunId = currentSubagent ? currentSubagent.childRunId : parentRunId;
872
+ const argsJson = JSON.stringify(inputObj);
873
+ await onChunk({
874
+ event: "response.tool_use.start",
875
+ call_id: callId, name: toolName, run_id: scopedRunId,
876
+ ...(currentSubagent ? { parent_tool_use_id: currentSubagent.parentCallId } : {}),
877
+ });
878
+ if (argsJson && argsJson !== "{}") {
879
+ await onChunk({
880
+ event: "response.tool_use.arguments.delta",
881
+ call_id: callId, delta: argsJson, run_id: scopedRunId,
882
+ });
883
+ }
884
+ await onChunk({
885
+ event: "response.tool_use.completed",
886
+ call_id: callId, name: toolName, input: inputObj, run_id: scopedRunId,
887
+ ...(currentSubagent ? { parent_tool_use_id: currentSubagent.parentCallId } : {}),
888
+ });
889
+ // Responses API 配对到达 tool_call_completed —— 不用 inflightTool 攒着
890
+ }
891
+ } else if (chunk.kind === "tool_call_completed") {
892
+ // Responses API 路径:tool 真实返回值在这里到达。call_id 跟 started
893
+ // 配对。delegate_task 的 result 是 JSON 串,含 results[*].summary
894
+ // (子 agent 实际答案)—— 抽出来注入为 child run 的 text block。
895
+
896
+ // todo translator:静默丢弃 todo tool_call_completed
897
+ {
898
+ const intercepted = todoTranslator.intercept(chunk);
899
+ if (intercepted.length === 0) continue; // 静默丢弃
900
+ }
901
+
902
+ const toolName = String(chunk.name || "");
903
+ const callId = String(chunk.call_id || "");
904
+ const resultText = String(chunk.result || "");
905
+ const isError = !!chunk.is_error;
906
+ if (!callId) continue;
907
+ log.info("hermes.tool_call_completed", "responses tool complete", {
908
+ tool_name: toolName, call_id: callId,
909
+ result_len: resultText.length,
910
+ result_preview: resultText.slice(0, 200),
911
+ is_error: isError,
912
+ has_active_subagent: !!currentSubagent,
913
+ });
914
+ if (toolName === "delegate_task" && currentSubagent && currentSubagent.parentCallId === callId) {
915
+ // 解析 delegate_task 返回,把每个子任务的 summary 注入 child run
916
+ // —— 这是 chat/completions 端点看不到的"子 agent 答案"。
917
+ // 抽 child run 文本:delegate_tool.py:549-566 的 result entry 形状是
918
+ // {task_index, status, summary, error?, ...}
919
+ // - status="completed" → summary 字段是子 agent 实际答案
920
+ // - status="failed" → summary 通常为空,错误信息在 error 字段
921
+ // - status="error" → 同上
922
+ // 任何一种都要往 child run 注入 text block 让 App 看见子 agent 状态,
923
+ // 否则 status=failed 时子 run 空白用户无所适从(这是 e2e 跑通时
924
+ // 实际遇到的 bug:subagent 异常退出 → bridge 把它默默吞了)。
925
+ function _subagentEntryText(r) {
926
+ const summary = String(r?.summary || "").trim();
927
+ if (summary) return summary;
928
+ const error = String(r?.error || "").trim();
929
+ if (error) return `(subagent failed: ${error})`;
930
+ const status = String(r?.status || "unknown");
931
+ return `(subagent ended status=${status} — no summary / error)`;
932
+ }
933
+ // 直接构造 child run 上的 text block —— 不走 translator 的
934
+ // text.blockState(那是给 parent run 文本累积用的,跟 child
935
+ // run 共用会串)。
936
+ // newBlockId 返回 blk_<26 Crockford Base32>,跟 server schema
937
+ // ^blk_[0-9A-HJKMNP-TV-Z]{26}$ 对齐;自造任意前缀会被 ingester 拒。
938
+ async function _emitChildText(text) {
939
+ const textBlockId = newBlockId();
940
+ await onChunk({
941
+ event: "__envelopes",
942
+ envelopes: [
943
+ buildBlockStarted(currentSubagent.childRunId, {
944
+ id: textBlockId, type: "text",
945
+ format: "plain", text: "",
946
+ }),
947
+ buildBlockCompleted(currentSubagent.childRunId, {
948
+ id: textBlockId, type: "text",
949
+ format: "plain", text,
950
+ }),
951
+ ],
952
+ });
953
+ currentSubagent.childTools.push({
954
+ name: "subagent_output",
955
+ label: text.slice(0, 80),
956
+ });
957
+ }
958
+ try {
959
+ const parsed = JSON.parse(resultText);
960
+ const results = Array.isArray(parsed?.results) ? parsed.results : [];
961
+ log.info("hermes.delegate_task.parsed", "delegate_task result parsed", {
962
+ results_count: results.length,
963
+ summaries: results.map((r) => String(r?.summary || "").slice(0, 60)),
964
+ statuses: results.map((r) => String(r?.status || "")),
965
+ errors: results.map((r) => String(r?.error || "").slice(0, 80)),
966
+ });
967
+ if (results.length === 0) {
968
+ // results 空 —— 整个 delegate_task 调用层失败(如配额拒批 /
969
+ // depth limit 触发)。retry text 兜底,让 App 看到错误而不是
970
+ // 完全静默。
971
+ await _emitChildText(`(delegate_task returned empty results: ${resultText.slice(0, 200)})`);
972
+ } else {
973
+ for (const r of results) {
974
+ await _emitChildText(_subagentEntryText(r));
975
+ }
976
+ }
977
+ } catch (err) {
978
+ // result 不是 JSON(极少见,可能是 hermes-agent 早期版本)——
979
+ // 当纯文本整体注入。
980
+ const text = resultText.trim() || "(delegate_task returned empty body)";
981
+ await _emitChildText(text);
982
+ }
983
+ // 关闭 subagent —— 发 delegate_task tool_result on parent + child run.completed
984
+ await closeCurrentSubagent(isError);
985
+ } else {
986
+ // 普通工具完成 —— 发 tool_result 带真实结果到对应作用域
987
+ const scopedRunId = currentSubagent && currentSubagent.parentCallId !== callId
988
+ ? currentSubagent.childRunId
989
+ : (currentSubagent && currentSubagent.parentCallId === callId ? parentRunId : parentRunId);
990
+ await onChunk({
991
+ event: "response.tool_result",
992
+ call_id: callId,
993
+ run_id: scopedRunId,
994
+ output: [{ type: "text", text: resultText }],
995
+ is_error: isError,
996
+ summary: resultText.length > 200 ? resultText.slice(0, 200) + "..." : resultText,
997
+ ...(currentSubagent && scopedRunId === currentSubagent.childRunId
998
+ ? { parent_tool_use_id: currentSubagent.parentCallId }
999
+ : {}),
1000
+ });
1001
+ if (currentSubagent && scopedRunId === currentSubagent.childRunId) {
1002
+ currentSubagent.childTools.push({ name: toolName, label: resultText.slice(0, 60) });
1003
+ }
1004
+ }
1005
+ } else if (chunk.kind === "done") {
1006
+ // Synthesize tool_use.completed for any still-open tool calls before
1007
+ // signaling completion. Parse accumulated argsAccum as JSON.
1008
+ for (const [callId, tc] of openToolCalls) {
1009
+ let parsedInput = {};
1010
+ if (tc.argsAccum) {
1011
+ try { parsedInput = JSON.parse(tc.argsAccum); } catch { /* leave {} */ }
1012
+ }
1013
+ await onChunk({
1014
+ event: "response.tool_use.completed",
1015
+ call_id: callId, name: tc.name, input: parsedInput,
1016
+ });
1017
+ }
1018
+ openToolCalls.clear();
1019
+ // Subagent_synth 期望 tool_result.completed 在 run.completed 之前到达:
1020
+ // 1) 关掉最后一个挂着的 child tool(flush inflightTool)
1021
+ // 2) 关掉当前 subagent —— 发 delegate_task tool_result + child run.completed
1022
+ await flushInflightTool(false);
1023
+ await closeCurrentSubagent(false);
1024
+ await onChunk({ event: "response.completed" });
1025
+ } else if (chunk.kind === "error") {
1026
+ await flushInflightTool(true);
1027
+ await closeCurrentSubagent(true);
1028
+ await onChunk({
1029
+ event: "response.failed",
1030
+ error: { message: String(chunk.error || "hermes error") },
1031
+ });
1032
+ }
1033
+ }
1034
+ } finally {
1035
+ // turn 结束:把 accumulator buffer 里剩余的尾巴扫一次。fire-and-forget,
1036
+ // 不会阻塞 caller。早 return (slash command) 路径 buffer 为空,无副作用。
1037
+ try { _acc?.flush(); } catch { /* swallow */ }
1038
+ // 注销 activeRuns 注册,防止后续 abort RPC 命中已结束的 run
1039
+ try { unregisterRun?.(); } catch { /* swallow */ }
1040
+ }
1041
+ };
1042
+
1043
+ // 注册 unifiedDispatchHandler
1044
+ const unifiedHandler = createUnifiedDispatchHandler({
1045
+ relayUrl,
1046
+ bridgeToken,
1047
+ log,
1048
+ producerFactory,
1049
+ openclawInvoke: injectedOpenclawInvoke ?? defaultOpenclawInvoke,
1050
+ gatewayId: gwId, // 正文 agentlink-cos:// 图片引用解析(sign-get)
1051
+ });
1052
+
1053
+ // workspaces.create 实现(可注入用于测试)
1054
+ const createWorkspaceImpl = _createWorkspace ?? _defaultCreateWorkspace;
1055
+
1056
+ return async function handleRequest(request, ctx) {
1057
+ // 步骤 0(最优先):workspace RPC 分支
1058
+ // 识别字段为 request.request.__relayKind === "rpc"(不是 __kind)
1059
+ // 由服务端 RelayRuntimeRPC(legacy_types_export.go:292)设置
1060
+ if (request?.request?.__relayKind === "rpc") {
1061
+ const method = request.request.method;
1062
+ const params = request.request.params ?? {};
1063
+ let result;
1064
+ try {
1065
+ // workspaces.scan 走通用 scanWorkspacesByRuntime(hermes 第一版返空 list,
1066
+ // 见 OpenSpec change workspaces-scan-rpc 决策 2)。绕过 hermes 自己的
1067
+ // workspaceRpc.dispatch(它只认 identity.soul.set / memory.files.get)。
1068
+ if (method === "workspaces.scan") {
1069
+ result = await scanWorkspacesByRuntime({
1070
+ runtimeKind: "hermes",
1071
+ gatewayId: gwId,
1072
+ params,
1073
+ });
1074
+ } else if (method === "workspaces.create") {
1075
+ result = await createWorkspaceImpl({
1076
+ runtime: "hermes",
1077
+ name: params.name,
1078
+ });
1079
+ } else if (method === "sessions.abort" || method === "chat.abort") {
1080
+ // 调用 activeRuns 注册表中断活跃 run
1081
+ const { aborted, matched } = activeRuns.abort({
1082
+ key: params.key ?? params.sessionKey ?? "",
1083
+ runId: params.runId ?? "",
1084
+ });
1085
+ if (aborted === 0) {
1086
+ result = { ok: true, aborted: 0, matched: [], status: "no-active-run" };
1087
+ } else {
1088
+ result = { ok: true, aborted, matched };
1089
+ }
1090
+ } else {
1091
+ result = await workspaceRpc.dispatch(method, params);
1092
+ }
1093
+ } catch (err) {
1094
+ result = { ok: false, error: err?.message ?? String(err) };
1095
+ }
1096
+ // 之前只发 `rpc.result` event,但 relay 端 `waitGatewayResponse` switch
1097
+ // 里没这个 case → 落到 `end` 分支返回空 body,App 端 `_relayRpcPayload`
1098
+ // 解析空 body 直接拿不到 `file` 字段(典型场景:用户修改 AI 身份后
1099
+ // App 调 `memory.files.get` 拉新 SOUL.md,响应空 → identity 解析 null
1100
+ // → 列表标题 fallback 回 "Hermes 设备",改名不生效)。
1101
+ //
1102
+ // 改成发 `response` event 带 JSON body,relay 的 case "response" 会把
1103
+ // body 原样回给 App;workspaceRpc 同时返回 ok/error/file 等字段,App
1104
+ // 的 `_relayRpcPayload` 按 {ok:true, ...} 协议解出 file。
1105
+ const bodyJson = JSON.stringify(result);
1106
+ await ctx.publishEvent({
1107
+ event: "response",
1108
+ statusCode: 200,
1109
+ contentType: "application/json; charset=utf-8",
1110
+ body: bodyJson,
1111
+ });
1112
+ await ctx.publishEvent({ event: "end" });
1113
+ return;
1114
+ }
1115
+
1116
+ // 步骤 1:优先走 unified dispatch
1117
+ const handled = await unifiedHandler(request, ctx);
1118
+ if (handled) return;
1119
+
1120
+ // 步骤 2:旧路径 — 与 index.mjs 原有逻辑一致
1121
+ const userText = extractUserText(request);
1122
+ if (!userText) {
1123
+ log.warn("turn.skip", "no user text in request", { requestId: ctx.requestId });
1124
+ await ctx.publishEvent({ event: "error", error: "empty user input" });
1125
+ await ctx.publishEvent({ event: "end" });
1126
+ return;
1127
+ }
1128
+ const turnStartedAt = Date.now();
1129
+
1130
+ // 拼历史
1131
+ const history = await readHistory(gwId);
1132
+ const messages = [
1133
+ { role: "system", content: SYSTEM_PROMPT_DEFAULT },
1134
+ ...history,
1135
+ { role: "user", content: userText },
1136
+ ];
1137
+
1138
+ await ctx.publishEvent({ event: "start" });
1139
+
1140
+ const backend = _backendOverride ?? (pre.backend === "command" ? commandBackendMod : httpBackendMod);
1141
+ // AbortController — 旧路径同样支持 sessions.abort / chat.abort RPC 中断
1142
+ const acLegacy = new AbortController();
1143
+ const unregisterLegacy = activeRuns.register({
1144
+ sessionKey: request?.sessionKey ?? "",
1145
+ threadId: request?.request?.thread_id ?? "",
1146
+ runId: request?.sessionKey ?? "",
1147
+ kill: (reason) => acLegacy.abort(reason),
1148
+ });
1149
+ const stream = backend.streamChat(messages, {
1150
+ apiBaseUrl: pre.apiBaseUrl,
1151
+ apiKey: pre.apiKey,
1152
+ command: pre.command,
1153
+ model: pre.model,
1154
+ hermesHome: profileHome, // per-profile HERMES_HOME 透传给 command backend
1155
+ signal: acLegacy.signal,
1156
+ });
1157
+
1158
+ let fullText = "";
1159
+ let errored = false;
1160
+ let lastError = null;
1161
+ for await (const chunk of stream) {
1162
+ switch (chunk.kind) {
1163
+ case "delta": {
1164
+ const text = chunk.text || "";
1165
+ if (!text) break;
1166
+ fullText += text;
1167
+ await ctx.publishEvent({
1168
+ event: "chunk",
1169
+ data: JSON.stringify({ type: "response.output_text.delta", delta: text }),
1170
+ });
1171
+ break;
1172
+ }
1173
+ case "usage": {
1174
+ const u = chunk.usage || null;
1175
+ if (!u) break;
1176
+ const cached = Number(u?.prompt_tokens_details?.cached_tokens || 0);
1177
+ usage.setTokensAbsolute({
1178
+ input: Number(u.prompt_tokens || 0),
1179
+ cached_input: cached,
1180
+ output: Number(u.completion_tokens || 0),
1181
+ });
1182
+ break;
1183
+ }
1184
+ case "done":
1185
+ if (chunk.fullText) fullText = chunk.fullText;
1186
+ break;
1187
+ case "error":
1188
+ errored = true;
1189
+ lastError = String(chunk.error || "unknown");
1190
+ break;
1191
+ }
1192
+ }
1193
+
1194
+ if (errored) {
1195
+ await ctx.publishEvent({
1196
+ event: "error", error: lastError || "hermes turn failed",
1197
+ });
1198
+ await ctx.publishEvent({ event: "end" });
1199
+ return;
1200
+ }
1201
+
1202
+ await ctx.publishEvent({
1203
+ event: "chunk",
1204
+ data: JSON.stringify({ type: "response.output_text.done", text: fullText }),
1205
+ });
1206
+ await ctx.publishEvent({ event: "end" });
1207
+
1208
+ // 持久化历史(dispatch 路径不调此处)
1209
+ await appendTurn(gwId, userText, fullText).catch((err) => {
1210
+ log.warn("hermes.history.persist.err", "history write failed", {
1211
+ err: err?.message,
1212
+ });
1213
+ });
1214
+
1215
+ usage.addCost({ turns: 1, duration_ms: Date.now() - turnStartedAt });
1216
+ usage.schedulePublish("hermes.turn_end");
1217
+ unregisterLegacy();
1218
+ };
1219
+ }
1220
+
1221
+ // ── helpers ──────────────────────────────────────────────────────────────────
1222
+
1223
+ function extractUserText(request) {
1224
+ const input = request?.request?.input;
1225
+ if (typeof input === "string") return input.trim();
1226
+ if (Array.isArray(input)) return collectTextFromItems(input).trim();
1227
+ if (input && typeof input === "object") {
1228
+ if (typeof input.text === "string") return input.text.trim();
1229
+ if (input.content !== undefined) return collectTextFromAny(input.content).trim();
1230
+ }
1231
+ if (typeof request?.clientPreviewText === "string") {
1232
+ return request.clientPreviewText.trim();
1233
+ }
1234
+ return "";
1235
+ }
1236
+
1237
+ function collectTextFromAny(value) {
1238
+ if (typeof value === "string") return value;
1239
+ if (Array.isArray(value)) return collectTextFromItems(value);
1240
+ if (value && typeof value === "object" && typeof value.text === "string") return value.text;
1241
+ return "";
1242
+ }
1243
+
1244
+ function collectTextFromItems(items) {
1245
+ const parts = [];
1246
+ for (const item of items) {
1247
+ if (typeof item === "string") { parts.push(item); continue; }
1248
+ if (!item || typeof item !== "object") continue;
1249
+ if (item.role && String(item.role).toLowerCase() !== "user") continue;
1250
+ if (typeof item.text === "string") { parts.push(item.text); continue; }
1251
+ if (item.content !== undefined) {
1252
+ const t = collectTextFromAny(item.content);
1253
+ if (t) parts.push(t);
1254
+ }
1255
+ }
1256
+ return parts.join("\n\n");
1257
+ }