agentworkshop 0.7.24 → 0.7.27

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 (243) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/2jaJHWIJ.js +1 -0
  3. package/.output/public/_nuxt/B7gZokg82.js +209 -0
  4. package/.output/public/_nuxt/{Dfe8vXdG.js → B8kt898d.js} +1 -1
  5. package/.output/public/_nuxt/{BvNJUu3s.js → BFGpEo8S.js} +1 -1
  6. package/.output/public/_nuxt/{BAfp6tmu.js → BG4zduPf.js} +5 -5
  7. package/.output/public/_nuxt/{BMb80_tm.js → BGhOrgle.js} +1 -1
  8. package/.output/public/_nuxt/{Bc-_IsBP.js → BHcpzI2n.js} +1 -1
  9. package/.output/public/_nuxt/{Djir5rSD.js → BTLEvDxM.js} +1 -1
  10. package/.output/public/_nuxt/{b50gYVZl.js → BWyR3awA.js} +1 -1
  11. package/.output/public/_nuxt/Bc89zKq2.js +1 -0
  12. package/.output/public/_nuxt/{CxY6LfeN.js → BetmGSO4.js} +1 -1
  13. package/.output/public/_nuxt/{DAR70Lf52.js → BgCrZKv_2.js} +1 -1
  14. package/.output/public/_nuxt/{DUToq-k_.js → BowTbasd.js} +1 -1
  15. package/.output/public/_nuxt/{D6HDb-vN2.js → BvtknqSC2.js} +1 -1
  16. package/.output/public/_nuxt/{CLZWFhZC.js → CTfqWGdy.js} +1 -1
  17. package/.output/public/_nuxt/Chc5pfAA.js +1 -0
  18. package/.output/public/_nuxt/CobSUzQv.js +2 -0
  19. package/.output/public/_nuxt/{kZV5TQvh2.js → CucID8qg2.js} +1 -1
  20. package/.output/public/_nuxt/{CAGL7TNK.js → DHbTyoAV.js} +1 -1
  21. package/.output/public/_nuxt/{DSa2O56y.js → DfLUN8kY.js} +1 -1
  22. package/.output/public/_nuxt/{Coik560n.js → DjU9Dx5Y.js} +1 -1
  23. package/.output/public/_nuxt/DnziMlcX.js +1 -0
  24. package/.output/public/_nuxt/{Ct68Z0232.js → DosOJEmZ2.js} +1 -1
  25. package/.output/public/_nuxt/{CDLv-n51.js → Q_SJDSgx.js} +1 -1
  26. package/.output/public/_nuxt/{TownView.zu2rU5nS.css → TownView.CtskuMXS.css} +1 -1
  27. package/.output/public/_nuxt/{CuVGPuP1.js → XY2z78rj.js} +1 -1
  28. package/.output/public/_nuxt/_id_.DdbYeLqn.css +1 -0
  29. package/.output/public/_nuxt/{C1CSiSBN.js → bFnZa2yd.js} +1 -1
  30. package/.output/public/_nuxt/builds/latest.json +1 -1
  31. package/.output/public/_nuxt/builds/meta/f7a8e33d-5422-45ac-895d-27168f97e3ad.json +1 -0
  32. package/.output/public/_nuxt/daq.DGV9QS8d.css +1 -0
  33. package/.output/public/_nuxt/{entry.AZM1Culo.css → entry.BN02X8iR.css} +1 -1
  34. package/.output/public/_nuxt/{X6IraCTB.js → rkVgzg62.js} +1 -1
  35. package/.output/public/_nuxt/settings.W6qrYAui.css +1 -0
  36. package/.output/public/_nuxt/teams.BBXVkyKq.css +1 -0
  37. package/.output/public/assets/game/character/yangyang-walk.glb +0 -0
  38. package/.output/server/chunks/_/host-bindings.mjs +1 -1
  39. package/.output/server/chunks/_/nitro.mjs +1013 -844
  40. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  41. package/.output/server/chunks/build/{TownView-DxeQe1Bl.mjs → TownView-ftIylqiD.mjs} +178 -151
  42. package/.output/server/chunks/build/{TownView-DxeQe1Bl.mjs.map → TownView-ftIylqiD.mjs.map} +1 -1
  43. package/.output/server/chunks/build/{_id_-DQDqj-cZ.mjs → _id_-3Axz2-gV.mjs} +35 -35
  44. package/.output/server/chunks/build/{_id_-DQDqj-cZ.mjs.map → _id_-3Axz2-gV.mjs.map} +1 -1
  45. package/.output/server/chunks/build/{_id_-DY6v6sqB.mjs → _id_-CahfkmEt.mjs} +5 -5
  46. package/.output/server/chunks/build/{_id_-DY6v6sqB.mjs.map → _id_-CahfkmEt.mjs.map} +1 -1
  47. package/.output/server/chunks/build/{_wsId_-D38Vx6Ky.mjs → _wsId_-BymlQhzS.mjs} +7 -7
  48. package/.output/server/chunks/build/{_wsId_-D38Vx6Ky.mjs.map → _wsId_-BymlQhzS.mjs.map} +1 -1
  49. package/.output/server/chunks/build/{agents-GnmUGZir.mjs → agents-DuMiSWKp.mjs} +4 -4
  50. package/.output/server/chunks/build/{agents-GnmUGZir.mjs.map → agents-DuMiSWKp.mjs.map} +1 -1
  51. package/.output/server/chunks/build/{channel-templates-B4z6v8Z5.mjs → channel-templates-BStqJ-of.mjs} +2 -2
  52. package/.output/server/chunks/build/{channel-templates-B4z6v8Z5.mjs.map → channel-templates-BStqJ-of.mjs.map} +1 -1
  53. package/.output/server/chunks/build/{checkbox-dzcpU_Cd.mjs → checkbox-C1gABOca.mjs} +4 -4
  54. package/.output/server/chunks/build/{checkbox-dzcpU_Cd.mjs.map → checkbox-C1gABOca.mjs.map} +1 -1
  55. package/.output/server/chunks/build/{daq-B-3xCp_2.mjs → daq-ChYCABz1.mjs} +312 -426
  56. package/.output/server/chunks/build/daq-ChYCABz1.mjs.map +1 -0
  57. package/.output/server/chunks/build/{dcw-BL2dhuz7.mjs → dcw-CcQNGggh.mjs} +3 -3
  58. package/.output/server/chunks/build/{dcw-BL2dhuz7.mjs.map → dcw-CcQNGggh.mjs.map} +1 -1
  59. package/.output/server/chunks/build/{default-CLf5m0NE.mjs → default-95-zOTPt.mjs} +2 -2
  60. package/.output/server/chunks/build/{default-CLf5m0NE.mjs.map → default-95-zOTPt.mjs.map} +1 -1
  61. package/.output/server/chunks/build/{logs-Dto_umFY.mjs → logs-D-2UyGtE.mjs} +3 -3
  62. package/.output/server/chunks/build/{logs-Dto_umFY.mjs.map → logs-D-2UyGtE.mjs.map} +1 -1
  63. package/.output/server/chunks/build/{monitor-Cp4V-H2n.mjs → monitor-mfG5jBtz.mjs} +3 -3
  64. package/.output/server/chunks/build/{monitor-Cp4V-H2n.mjs.map → monitor-mfG5jBtz.mjs.map} +1 -1
  65. package/.output/server/chunks/build/{pages-Dc2H1QFu.mjs → pages-Dfosfchr.mjs} +4 -4
  66. package/.output/server/chunks/build/{pages-Dc2H1QFu.mjs.map → pages-Dfosfchr.mjs.map} +1 -1
  67. package/.output/server/chunks/build/{permissions-BTRW2CVd.mjs → permissions-B-hhZuzm.mjs} +3 -3
  68. package/.output/server/chunks/build/{permissions-BTRW2CVd.mjs.map → permissions-B-hhZuzm.mjs.map} +1 -1
  69. package/.output/server/chunks/build/{settings-UfGyDgM7.mjs → settings-BajN9TQW.mjs} +249 -20
  70. package/.output/server/chunks/build/settings-BajN9TQW.mjs.map +1 -0
  71. package/.output/server/chunks/build/{table-Cswq-l2r.mjs → table-Bf1r2JjE.mjs} +2 -2
  72. package/.output/server/chunks/build/{table-Cswq-l2r.mjs.map → table-Bf1r2JjE.mjs.map} +1 -1
  73. package/.output/server/chunks/build/{teams-Cbg27MWW.mjs → teams-t2SkRlJP.mjs} +346 -46
  74. package/.output/server/chunks/build/teams-t2SkRlJP.mjs.map +1 -0
  75. package/.output/server/chunks/build/{tokens-Bmm6rDfj.mjs → tokens-DiCojbN8.mjs} +3 -3
  76. package/.output/server/chunks/build/{tokens-Bmm6rDfj.mjs.map → tokens-DiCojbN8.mjs.map} +1 -1
  77. package/.output/server/chunks/build/{town-Dsh0jlwZ.mjs → town-C7QM8DYK.mjs} +5 -5
  78. package/.output/server/chunks/build/{town-Dsh0jlwZ.mjs.map → town-C7QM8DYK.mjs.map} +1 -1
  79. package/.output/server/chunks/build/{useDaqStream-DkRL3tEW.mjs → useDaqStream-BeA2X8KR.mjs} +26 -11
  80. package/.output/server/chunks/build/useDaqStream-BeA2X8KR.mjs.map +1 -0
  81. package/.output/server/chunks/build/{useDcwStream-DyB2ufQI.mjs → useDcwStream-BJW_MitY.mjs} +14 -6
  82. package/.output/server/chunks/build/useDcwStream-BJW_MitY.mjs.map +1 -0
  83. package/.output/server/chunks/build/{useDeviceTwins-ZbT86L9O.mjs → useDeviceTwins-Dwz_EY_i.mjs} +2 -2
  84. package/.output/server/chunks/build/{useDeviceTwins-ZbT86L9O.mjs.map → useDeviceTwins-Dwz_EY_i.mjs.map} +1 -1
  85. package/.output/server/chunks/build/{useOpsLog-8HCYFb6n.mjs → useOpsLog-Dm8QEb1I.mjs} +2 -2
  86. package/.output/server/chunks/build/{useOpsLog-8HCYFb6n.mjs.map → useOpsLog-Dm8QEb1I.mjs.map} +1 -1
  87. package/.output/server/chunks/build/{useWorkshopApi-D2gq06N2.mjs → useWorkshopApi-B-L3MmIX.mjs} +9 -2
  88. package/.output/server/chunks/build/{useWorkshopApi-D2gq06N2.mjs.map → useWorkshopApi-B-L3MmIX.mjs.map} +1 -1
  89. package/.output/server/chunks/build/{users-B50HHrfi.mjs → users-B9SAdEtH.mjs} +3 -3
  90. package/.output/server/chunks/build/{users-B50HHrfi.mjs.map → users-B9SAdEtH.mjs.map} +1 -1
  91. package/.output/server/chunks/routes/api/plugins/_name/_...path_.mjs +14 -2
  92. package/.output/server/chunks/routes/api/plugins/_name/_...path_.mjs.map +1 -1
  93. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  94. package/.output/server/chunks/routes/api/workshop/channels/_id/index.put.mjs +40 -0
  95. package/.output/server/chunks/routes/api/workshop/channels/_id/index.put.mjs.map +1 -0
  96. package/.output/server/chunks/routes/api/workshop/channels/_id/index4.get.mjs +21 -9
  97. package/.output/server/chunks/routes/api/workshop/channels/_id/index4.get.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/workshop/channels/_id/index5.get.mjs +33 -0
  99. package/.output/server/chunks/routes/api/workshop/channels/_id/index5.get.mjs.map +1 -0
  100. package/.output/server/chunks/routes/api/workshop/channels/_id/terminals.get.mjs +1 -1
  101. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +15 -1
  102. package/.output/server/chunks/routes/api/workshop/channels.post.mjs.map +1 -1
  103. package/.output/server/chunks/routes/api/workshop/daq/_id/bind.post.mjs +1 -1
  104. package/.output/server/chunks/routes/api/workshop/daq/_id/bindings.put.mjs +1 -1
  105. package/.output/server/chunks/routes/api/workshop/daq/_id/frames/content.get.mjs +1 -1
  106. package/.output/server/chunks/routes/api/workshop/daq/_id/frames.get.mjs +1 -1
  107. package/.output/server/chunks/routes/api/workshop/daq/_id/samples.get.mjs +1 -1
  108. package/.output/server/chunks/routes/api/workshop/daq/_id/test.post.mjs +1 -1
  109. package/.output/server/chunks/routes/api/workshop/daq/_id_.delete.mjs +1 -1
  110. package/.output/server/chunks/routes/api/workshop/daq/_id_.patch.mjs +1 -1
  111. package/.output/server/chunks/routes/api/workshop/daq/alarms/_id/ack.post.mjs +1 -1
  112. package/.output/server/chunks/routes/api/workshop/daq/controller.post.mjs +1 -1
  113. package/.output/server/chunks/routes/api/workshop/daq/index.post.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/daq/infra/reconnect.post.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/daq/infra.get.mjs +1 -1
  116. package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.delete.mjs +1 -1
  117. package/.output/server/chunks/routes/api/workshop/daq/templates/_key_.patch.mjs +1 -1
  118. package/.output/server/chunks/routes/api/workshop/daq/test-driver.post.mjs +1 -1
  119. package/.output/server/chunks/routes/api/workshop/dcw/_id/bind.post.mjs +1 -1
  120. package/.output/server/chunks/routes/api/workshop/dcw/_id/bindings.put.mjs +1 -1
  121. package/.output/server/chunks/routes/api/workshop/dcw/_id/param-ledger.get.mjs +1 -1
  122. package/.output/server/chunks/routes/api/workshop/dcw/_id/read.post.mjs +1 -1
  123. package/.output/server/chunks/routes/api/workshop/dcw/_id/test.post.mjs +1 -1
  124. package/.output/server/chunks/routes/api/workshop/dcw/_id/write.post.mjs +1 -1
  125. package/.output/server/chunks/routes/api/workshop/dcw/_id_.delete.mjs +1 -1
  126. package/.output/server/chunks/routes/api/workshop/dcw/_id_.patch.mjs +1 -1
  127. package/.output/server/chunks/routes/api/workshop/dcw/controller.post.mjs +1 -1
  128. package/.output/server/chunks/routes/api/workshop/dcw/index.get.mjs +1 -1
  129. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
  130. package/.output/server/chunks/routes/api/workshop/dcw/index2.get.mjs +1 -1
  131. package/.output/server/chunks/routes/api/workshop/dcw/index2.post.mjs +1 -1
  132. package/.output/server/chunks/routes/api/workshop/dcw/index3.get.mjs +1 -1
  133. package/.output/server/chunks/routes/api/workshop/dcw/index3.post.mjs +1 -1
  134. package/.output/server/chunks/routes/api/workshop/dcw/index4.get.mjs +1 -1
  135. package/.output/server/chunks/routes/api/workshop/dcw/index4.post.mjs +1 -1
  136. package/.output/server/chunks/routes/api/workshop/dcw/index5.get.mjs +1 -1
  137. package/.output/server/chunks/routes/api/workshop/dcw/journal/node/_nodeId/rollback.post.mjs +1 -1
  138. package/.output/server/chunks/routes/api/workshop/dcw/line/query.get.mjs +1 -1
  139. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
  140. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
  141. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.delete.mjs +1 -1
  142. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/judge.post.mjs +1 -1
  143. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/rollback.post.mjs +1 -1
  144. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id/series.get.mjs +1 -1
  145. package/.output/server/chunks/routes/api/workshop/dcw/optimizations/_id_.get.mjs +1 -1
  146. package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.delete.mjs +1 -1
  147. package/.output/server/chunks/routes/api/workshop/dcw/products/_id_.patch.mjs +1 -1
  148. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/apply.post.mjs +1 -1
  149. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/mark-good.post.mjs +1 -1
  150. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/rollback-good.post.mjs +1 -1
  151. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.delete.mjs +1 -1
  152. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/close.post.mjs +1 -1
  153. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/data.get.mjs +1 -1
  154. package/.output/server/chunks/routes/api/workshop/dcw/runs/_id/rollback.post.mjs +1 -1
  155. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
  156. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
  157. package/.output/server/chunks/routes/api/workshop/dcw/test-driver.post.mjs +1 -1
  158. package/.output/server/chunks/routes/api/workshop/device-twins/_id_.delete.mjs +1 -1
  159. package/.output/server/chunks/routes/api/workshop/device-twins/_id_.patch.mjs +1 -1
  160. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  161. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
  162. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  163. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  164. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  165. package/.output/server/chunks/routes/api/workshop/index3.post.mjs +1 -1
  166. package/.output/server/chunks/routes/api/workshop/index4.get.mjs +1 -1
  167. package/.output/server/chunks/routes/api/workshop/index4.post.mjs +1 -1
  168. package/.output/server/chunks/routes/api/workshop/index5.get.mjs +1 -1
  169. package/.output/server/chunks/routes/api/workshop/index5.post.mjs +1 -1
  170. package/.output/server/chunks/routes/api/workshop/index6.post.mjs +1 -1
  171. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  172. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  173. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  174. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  175. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  176. package/.output/server/chunks/routes/renderer.mjs +2 -2
  177. package/.output/server/chunks/virtual/entry.mjs +352 -433
  178. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  179. package/.output/server/package.json +1 -1
  180. package/app/components/workshop/DaqNodeRow.vue +398 -0
  181. package/app/components/workshop/town/ModelPreview3D.vue +26 -4
  182. package/app/components/workshop/town/TownScene3D.ts +60 -12
  183. package/app/components/workshop/town/TownView.vue +4 -3
  184. package/app/composables/workshop/useCharacterAssets.ts +1 -0
  185. package/app/composables/workshop/useDaqStream.ts +24 -5
  186. package/app/composables/workshop/useDcwStream.ts +13 -4
  187. package/app/composables/workshop/useWorkshopApi.ts +294 -259
  188. package/app/pages/daq/[id].vue +27 -3
  189. package/app/pages/daq/index.vue +8 -355
  190. package/app/pages/settings.vue +253 -2
  191. package/app/pages/workshop/teams.vue +212 -2
  192. package/i18n/dicts/_en.json +361 -1
  193. package/i18n/dicts/plugins.json +10 -0
  194. package/i18n/dicts/settings.json +1 -0
  195. package/i18n/dicts/teamPlugins.json +10 -0
  196. package/i18n/locales/en.ts +1860 -1900
  197. package/i18n/locales/zh-CN.ts +1788 -1829
  198. package/package.json +149 -148
  199. package/public/assets/game/character/yangyang-walk.glb +0 -0
  200. package/scripts/_dbg-cpu-profile.mjs +53 -0
  201. package/scripts/_dbg-render-perf.mjs +142 -0
  202. package/scripts/_dbg-render-regression.mjs +130 -0
  203. package/scripts/i18n-merge.mjs +152 -30
  204. package/scripts/production-closed-loop-e2e.mjs +254 -0
  205. package/scripts/team-plugins-api-smoke.mjs +149 -0
  206. package/scripts/three-system-e2e.mjs +448 -0
  207. package/server/api/plugins/[name]/[...path].ts +46 -31
  208. package/server/api/workshop/channels/[id]/plugins/index.get.ts +35 -0
  209. package/server/api/workshop/channels/[id]/plugins/index.put.ts +39 -0
  210. package/server/api/workshop/channels.post.ts +17 -0
  211. package/server/data/dcw-lines.json +7 -0
  212. package/server/data/dcw-products.json +7 -0
  213. package/server/data/dcw-recipes.json +1 -1
  214. package/server/data/dcw-rollback.json +523 -11
  215. package/server/data/dcw-writes.json +72 -0
  216. package/server/data/dcws.json +368 -368
  217. package/server/data/scene-layouts.json +66 -0
  218. package/server/plugins/aw-plugins.ts +35 -27
  219. package/server/plugins-builtin/diag-bridge/index.mjs +556 -0
  220. package/server/plugins-builtin/rag-bridge/index.mjs +364 -0
  221. package/server/services/workshop/agents/host-tool-bridge.ts +659 -642
  222. package/server/services/workshop/agents/omp-agent.ts +1178 -1178
  223. package/server/services/workshop/agents/plugin-tools.ts +10 -1
  224. package/server/services/workshop/db/channel-plugins.repo.ts +65 -0
  225. package/server/services/workshop/plugins/host.mjs +52 -4
  226. package/server/services/workshop/runtime/manager.ts +2976 -2968
  227. package/shared/config/schema.json +4 -0
  228. package/.output/public/_nuxt/BUcmySDa2.js +0 -209
  229. package/.output/public/_nuxt/CYzpZzB_.js +0 -1
  230. package/.output/public/_nuxt/CjXIpPMR.js +0 -1
  231. package/.output/public/_nuxt/DKHcFE1i.js +0 -1
  232. package/.output/public/_nuxt/DjtE7AYt.js +0 -1
  233. package/.output/public/_nuxt/JOwsShod.js +0 -2
  234. package/.output/public/_nuxt/_id_.C-YXjn8Q.css +0 -1
  235. package/.output/public/_nuxt/builds/meta/024de5f7-6a10-4f53-82ab-bc65f3ec9e35.json +0 -1
  236. package/.output/public/_nuxt/daq.3OKj58HH.css +0 -1
  237. package/.output/public/_nuxt/settings.BqMF2nfj.css +0 -1
  238. package/.output/public/_nuxt/teams.DN0pAGGu.css +0 -1
  239. package/.output/server/chunks/build/daq-B-3xCp_2.mjs.map +0 -1
  240. package/.output/server/chunks/build/settings-UfGyDgM7.mjs.map +0 -1
  241. package/.output/server/chunks/build/teams-Cbg27MWW.mjs.map +0 -1
  242. package/.output/server/chunks/build/useDaqStream-DkRL3tEW.mjs.map +0 -1
  243. package/.output/server/chunks/build/useDcwStream-DyB2ufQI.mjs.map +0 -1
@@ -1,1178 +1,1178 @@
1
- /**
2
- * OmpRpcAgentImpl — omp harness 的完整 AgentInterface 实现。
3
- *
4
- * 通过 OmpRpcClient 驱动真实 omp 子进程(--mode rpc),将 omp 的原生 agent 能力
5
- * (LLM 推理 + 内置工具 read/write/edit/bash/grep/glob 等)接入 Workshop 的
6
- * Channel 编排体系。
7
- *
8
- * 核心设计:
9
- * - 每个 Agent 实例持有一个 omp 子进程(lazy spawn,跨消息复用)
10
- * - AgentWorkspace 方法经 host-tool-bridge 注册为 omp host tools,agent 原生调用
11
- * report_progress / complete_task / dispatch_task / send_message 等,无需文本解析
12
- * - omp AgentSessionEvent → AgentEvent 五变体映射
13
- * - worker run():接收 assign → prompt omp → agent 用原生工具作业 + host tools 管理任务生命周期
14
- * - lead supervise():格式化快照 → prompt omp → agent 直接用 host tools 执行调度决策 → 返回 []
15
- * - dispose():杀子进程
16
- *
17
- * prompt 组装与工具分发已抽至共享层(prompt-builder / host-tool-bridge),
18
- * 与 codex/dsh/opencode impl 全引擎一致。协议权威:omp://rpc.md。
19
- */
20
- import { createLogger } from '../logger'
21
- import { randomUUID } from 'node:crypto'
22
- import type {
23
- AgentEvent,
24
- AgentInterface,
25
- AgentInfo,
26
- AgentRunContext,
27
- AgentRunRequest,
28
- SupervisionDecision,
29
- SupervisionSnapshot,
30
- } from './agent-interface'
31
- import type { A2AMessage } from '../types/a2a'
32
- import type { AgentContextStats } from '../types/task'
33
- import {
34
- OmpRpcClient,
35
- type AgentSessionEvent,
36
- type CompactionResult,
37
- type HostToolCallRequest,
38
- } from './adapters/omp-rpc-client'
39
- import {
40
- registerHarnessProcess,
41
- bindHarnessProcess,
42
- markHarnessProcessExit,
43
- killHarnessProcess,
44
- } from './harness-process'
45
- import {
46
- attachTerminalTap,
47
- markTerminalSessionExit,
48
- } from './harness-terminal'
49
- import {
50
- createSessionState,
51
- dispatchHostTool,
52
- hostToolsForRole,
53
- } from './host-tool-bridge'
54
- import {
55
- contextPrefix as buildContextPrefix,
56
- createRosterCache,
57
- extractJsonArray,
58
- parseSteerBanner,
59
- peerPrompt,
60
- supervisePrompt,
61
- systemManual,
62
- toolArgsPreview,
63
- workerPrompt,
64
- } from './prompt-builder'
65
- import { renderPrompt } from '../prompts/loader'
66
- import { attachOmpPluginBridge, onPluginToolsChange } from './plugin-tools'
67
- import { ompSettings } from '../settings'
68
-
69
- const log = createLogger('workshop.omp')
70
-
71
- // 兼容再导出(脚本/测试历史导入面;实现在 host-tool-bridge)
72
- export { HOST_TOOLS, hostToolsForRole } from './host-tool-bridge'
73
-
74
- // ===== 配置 =====
75
-
76
- export interface OmpAgentConfig {
77
- /** omp 可执行文件路径(默认 'omp',从 PATH 查找) */
78
- command?: string
79
- /** 额外 CLI 参数 */
80
- args?: string[]
81
- /** omp 工作目录(默认 process.cwd()) */
82
- cwd?: string
83
- /** 模型 provider(如 'anthropic'/'openai'/'zhipu');省略则用 omp 默认 */
84
- provider?: string
85
- /** 模型 ID;省略则用 omp 默认 */
86
- model?: string
87
- /** thinking level: off/minimal/low/medium/high/xhigh/max */
88
- thinkingLevel?: string
89
- /** 自定义系统 prompt 前缀(拼接到 agent 系统指令之前) */
90
- systemPromptPrefix?: string
91
- /** 限制 omp 仅使用指定内置工具(null = 全部) */
92
- toolNames?: string[]
93
- /** 每轮停滞超时(ms,默认 300000):整轮无任何 omp 事件才中止;工具内阻塞(poll_messages ≤180s)有 tool 事件刷新计时 */
94
- promptTimeoutMs?: number
95
- /** supervise 轮超时(ms,默认 150000) */
96
- superviseTimeoutMs?: number
97
- /**
98
- * omp 输出模式:'rpc-ui'(默认)= RPC 协议 + UI 上下文 —— ask 工具与
99
- * extension_ui_request 对话框可用(监控终端 HITL 通道,强制 noPty);
100
- * 'rpc' = 纯协议(无 UI,ask 不可用)。
101
- */
102
- rpcMode?: 'rpc' | 'rpc-ui'
103
- /** channel 级作业场景 prompt(manager 装配时注入;用户场景规范,全员共享) */
104
- scenarioPrompt?: string
105
- /** agent 身份(由 factory 从 AgentInfo 注入) */
106
- agentId?: string
107
- name?: string
108
- role?: 'lead' | 'worker'
109
- channelId?: string
110
- }
111
-
112
- // 插件桥接管(host.mjs ctx.omp 排队注册的工具在此回放;幂等)
113
- attachOmpPluginBridge()
114
-
115
- /** 在跑 agent 实例表(工具注册表变更 → 热重发 set_host_tools,运行时注入无需重spawn) */
116
- const gLive = globalThis as typeof globalThis & { __ompLiveAgents?: Set<OmpRpcAgentImpl> }
117
- function liveAgents(): Set<OmpRpcAgentImpl> {
118
- return gLive.__ompLiveAgents ??= new Set()
119
- }
120
-
121
- // 工具注册表变更 → 全部在跑会话热更新工具面
122
- onPluginToolsChange(() => {
123
- for (const impl of liveAgents()) {
124
- try {
125
- impl.refreshPluginTools()
126
- }
127
- catch { /* 单实例失败不影响其他 */ }
128
- }
129
- })
130
-
131
- // ===== OmpRpcAgentImpl =====
132
-
133
- export class OmpRpcAgentImpl implements AgentInterface {
134
- private readonly config: OmpAgentConfig
135
- private client: OmpRpcClient | null = null
136
- private workspace: AgentRunContext['workspace'] | null = null
137
- private agentInfo: AgentInfo | null = null
138
- private hostToolsRegistered = false
139
- /** host tool 会话态(当前任务 + 待回执上下文;与共享桥共用) */
140
- private readonly toolState = createSessionState()
141
- /** 工具桥上下文(分发唯一入口) */
142
- private readonly bridgeCtx = {
143
- identity: { agentId: '', channelId: '', role: 'worker' as 'lead' | 'worker', name: 'agent' },
144
- state: this.toolState,
145
- getWorkspace: () => this.workspace,
146
- }
147
-
148
- /**
149
- * 会话回合状态(steer 可靠注入的依据):
150
- * omp 的 steer 仅在回合 streaming 中生效——prompt 已入列但尚未开始输出时,
151
- * steer 会"成功"返回但被静默丢弃。因此注入方轮询直到回合输出开始(streamingStarted)
152
- * 才发送;若回合在等待期间结束,消息保持 pending 由消费循环处理。
153
- */
154
- private streaming = false
155
- private turnActive = false
156
- /** 当前回合产生的 assistant 文本(供诊断) */
157
- private turnText = ''
158
- /** agent 身份信息(factory 注入;无需等待 init()) */
159
- private selfAgentId = ''
160
- private agentName = 'agent'
161
- private agentRole: 'lead' | 'worker' = 'worker'
162
- private channelId = ''
163
- /** 团队名册缓存(共享 prompt-builder) */
164
- private roster = createRosterCache({
165
- selfAgentId: '',
166
- listAgents: () => this.workspace!.listAgents(),
167
- })
168
-
169
- // ===== 上下文治理(70% 无中断压缩环)=====
170
- /** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
171
- private compacting = false
172
- private lastCompactAt = 0
173
- /** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
174
- private compactLegacy = false
175
- /** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
176
- private contextWindow: number | null = null
177
- private sessionId: string | null = null
178
- /** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
179
- private lastHarvestKey = ''
180
- private lastHarvestAt = 0
181
-
182
- /** 总开关(omp.compact_enabled;env AW_OMP_COMPACT_ENABLED=0 显式关闭兼容) */
183
- private compactEnabled(): boolean {
184
- return ompSettings().compact_enabled
185
- }
186
-
187
- private compactThreshold(): number {
188
- return ompSettings().compact_threshold
189
- }
190
-
191
- private compactMinIntervalMs(): number {
192
- return ompSettings().compact_min_interval_ms
193
- }
194
-
195
- private compactWaitMs(): number {
196
- return ompSettings().compact_wait_ms
197
- }
198
-
199
- /** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
200
- getContextStats(): AgentContextStats | null {
201
- const usage = this.client?.getContextUsage() ?? null
202
- const window = this.contextWindow ?? usage?.contextWindow ?? null
203
- if (!usage && window === null) return null
204
- const usedTokens = usage?.tokens ?? 0
205
- return {
206
- usedTokens,
207
- contextWindow: window,
208
- percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
209
- compacting: this.compacting,
210
- }
211
- }
212
-
213
- /**
214
- * 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
215
- * percent 归一化到 0-1(omp 可能返回 0-100)。
216
- */
217
- private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
218
- try {
219
- const resp = await client.send({ type: 'get_session_stats' })
220
- const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
221
- | { tokens?: number, contextWindow?: number, percent?: number }
222
- | undefined
223
- if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
224
- if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
225
- this.contextWindow = cu.contextWindow
226
- client.setContextWindow(cu.contextWindow)
227
- }
228
- const window = this.contextWindow ?? cu.contextWindow ?? null
229
- const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
230
- const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
231
- return { tokens: cu.tokens, contextWindow: window, percent }
232
- }
233
- }
234
- catch { /* get_session_stats 不可用 → 被动跟踪 */ }
235
- return client.getContextUsage()
236
- }
237
-
238
- /**
239
- * 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
240
- * post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
241
- * 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
242
- * isCompacting → compacting 互斥位 + 最小间隔防振荡。
243
- */
244
- private async contextGate(reason: string): Promise<void> {
245
- if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
246
- const client = this.client
247
- if (!client || !client.alive) return
248
- try {
249
- const usage = await this.probeUsage(client)
250
- if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
251
- if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
252
- // 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
253
- const st = await client.send({ type: 'get_state' })
254
- const s = (st.data ?? {}) as Record<string, unknown>
255
- if (s.isStreaming === true || s.isCompacting === true) return
256
- if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
257
- this.compacting = true
258
- this.lastCompactAt = Date.now()
259
- try {
260
- const result = await this.runCompaction(client)
261
- if (result?.summary) {
262
- await this.harvestCompaction(
263
- result.summary,
264
- result.tokensBefore,
265
- result.tokensAfter ?? result.estimatedTokensAfter,
266
- reason,
267
- )
268
- }
269
- }
270
- finally {
271
- this.compacting = false
272
- }
273
- }
274
- catch (err) {
275
- const msg = err instanceof Error ? err.message : String(err)
276
- if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
277
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
278
- }
279
- }
280
-
281
- /**
282
- * 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
283
- * 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
284
- */
285
- private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
286
- return new Promise<CompactionResult | null>((resolve) => {
287
- let settled = false
288
- const finish = (r: CompactionResult | null): void => {
289
- if (settled) return
290
- settled = true
291
- off()
292
- clearTimeout(timer)
293
- resolve(r)
294
- }
295
- const off = client.onEvent((event) => {
296
- if (event.type === 'compaction_end') {
297
- finish((event as { result?: CompactionResult }).result ?? null)
298
- }
299
- else if (event.type === '__process_exit__') {
300
- finish(null)
301
- }
302
- })
303
- const timer = setTimeout(() => {
304
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
305
- finish(null)
306
- }, this.compactWaitMs())
307
- void client.send({ type: 'compact', customInstructions: this.compactionHints() })
308
- .then((resp) => {
309
- const result = (resp.data as { result?: CompactionResult } | undefined)?.result
310
- if (result?.summary) finish(result)
311
- // 响应无 result:等 compaction_end 事件(协议权威语义)
312
- })
313
- .catch((err) => {
314
- const msg = err instanceof Error ? err.message : String(err)
315
- if (/compact/i.test(msg)) this.compactLegacy = true
316
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
317
- finish(null)
318
- })
319
- })
320
- }
321
-
322
- /** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
323
- private compactionHints(): string | undefined {
324
- try {
325
- return renderPrompt('compaction-hints')
326
- }
327
- catch {
328
- return undefined
329
- }
330
- }
331
-
332
- /**
333
- * 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
334
- * 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
335
- * 双路去重:同摘要 60s 内只入库一次。
336
- */
337
- private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
338
- const key = summary.slice(0, 120)
339
- if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
340
- this.lastHarvestKey = key
341
- this.lastHarvestAt = Date.now()
342
- try {
343
- await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
344
- log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
345
- }
346
- catch (err) {
347
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
348
- }
349
- }
350
-
351
- /** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
352
- async onTurnSettled(): Promise<void> {
353
- await this.contextGate('post-settle')
354
- }
355
-
356
- constructor(config: Record<string, unknown> = {}) {
357
- this.config = config as OmpAgentConfig
358
- // effort 统一入口(channel 默认 LLM 注入 config.effort)→ omp thinking 级别
359
- if (!this.config.thinkingLevel && typeof config.effort === 'string' && config.effort) {
360
- this.config.thinkingLevel = config.effort
361
- }
362
- // factory 注入的 agent 身份(无需等待 init())
363
- this.selfAgentId = this.config.agentId ?? ''
364
- this.agentName = this.config.name ?? 'agent'
365
- this.agentRole = this.config.role ?? 'worker'
366
- this.channelId = this.config.channelId ?? ''
367
- this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
368
- this.roster = createRosterCache({
369
- selfAgentId: this.selfAgentId,
370
- listAgents: () => this.workspace!.listAgents(),
371
- })
372
- liveAgents().add(this) // 在跑实例表:插件工具热注入的目标集
373
- }
374
-
375
- // ===== 生命周期 =====
376
-
377
- async init(input: { agent: AgentInfo, channelId: string }): Promise<void> {
378
- this.agentInfo = input.agent
379
- this.channelId = input.channelId
380
- this.agentName = input.agent.name
381
- this.agentRole = input.agent.role
382
- this.selfAgentId = input.agent.id
383
- this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
384
- this.roster = createRosterCache({
385
- selfAgentId: this.selfAgentId,
386
- listAgents: () => this.workspace!.listAgents(),
387
- })
388
- }
389
-
390
- async dispose(): Promise<void> {
391
- liveAgents().delete(this)
392
- if (this.client) {
393
- const pid = this.client.pid
394
- await this.client.dispose()
395
- this.client = null
396
- if (pid) {
397
- markHarnessProcessExit(pid, null)
398
- markTerminalSessionExit(pid, null)
399
- }
400
- }
401
- this.hostToolsRegistered = false
402
- }
403
-
404
- /** 工具注册表变更 → 热重发 set_host_tools(在跑会话立即获得插件新工具,无需重spawn) */
405
- refreshPluginTools(): void {
406
- const client = this.client
407
- if (!client || !client.alive) return
408
- void client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) }).catch(() => {})
409
- }
410
-
411
- /** harness 进程资源信息(运行时资源监控;进程未 spawn/已回收 → null) */
412
- getProcessInfo(): { pid: number, alive: boolean, command: string } | null {
413
- const client = this.client
414
- const pid = client?.pid
415
- if (!pid || !client) return null
416
- return { pid, alive: client.alive, command: `omp --mode ${this.rpcMode}` }
417
- }
418
-
419
- /** 强制终止 harness 进程(进程树;终止后由调用方停止对应 AgentRuntime) */
420
- killProcess(): void {
421
- const pid = this.client?.pid
422
- if (pid) {
423
- markTerminalSessionExit(pid, null)
424
- killHarnessProcess(pid)
425
- }
426
- else {
427
- this.client?.kill()
428
- }
429
- }
430
-
431
- /**
432
- * harness 进程存活校准(manager sweeper 周期性调用;休眠/强杀后 exit 事件
433
- * 可能不达父进程):按 PID 探 OS 实际存在性,进程已死 → 客户端收敛为已退出,
434
- * 在途回合经 __process_exit__ 归位,下一回合 ensureClient 重生子进程。
435
- */
436
- reconcileProcess(): void {
437
- this.client?.reconcile()
438
- }
439
-
440
- /** omp 输出模式(rpc-ui = 默认,启用 HITL 对话框) */
441
- private get rpcMode(): 'rpc' | 'rpc-ui' {
442
- return this.config.rpcMode === 'rpc' ? 'rpc' : 'rpc-ui'
443
- }
444
-
445
- // ===== 工具桥(共享分发)=====
446
-
447
- /** 引擎无关工具面:REST/MCP 桥直调与 omp 内部 host_tool_call 共用同一实现 */
448
- dispatchHostTool(toolName: string, args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
449
- return dispatchHostTool(this.bridgeCtx, { toolName, arguments: args })
450
- }
451
-
452
- private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
453
- return this.dispatchHostTool(req.toolName, req.arguments ?? {})
454
- }
455
-
456
- // ===== prompt 组合 =====
457
-
458
- /** 前置上下文(场景 × 身份 × 工业简报 × 名册;共享 prompt-builder) */
459
- private async contextPrefix(): Promise<string> {
460
- return buildContextPrefix({
461
- scenarioPrompt: this.config.scenarioPrompt,
462
- systemPromptPrefix: this.config.systemPromptPrefix,
463
- agentId: this.selfAgentId,
464
- roster: await this.roster.roster(),
465
- })
466
- }
467
-
468
- private systemManual(): string {
469
- return systemManual()
470
- }
471
-
472
- /**
473
- * 实时消息注入(送达模式协议):
474
- * - 回合 streaming 中 → 立即 steer,返回 'steer'(同轮可见,唯一可标记消费的路径)
475
- * - 回合活跃但尚未 streaming(prompt 排队窗口,上限 20s)→ 等 streaming 开始后 steer
476
- * - 回合被 host 工具阻塞(如 poll_messages 等待)/空闲/发送失败 → 返回 'deferred':
477
- * 消息保持 pending,poll_messages 的 Mailbox 到信回调即时取走(毫秒级),或本回合
478
- * 结束后由消费循环按 FIFO 起回合处理。
479
- */
480
- async steer(text: string): Promise<'steer' | 'deferred'> {
481
- // 从确定性触发横幅提取回执上下文(AgentRuntime.injectSteer 生成,格式固定):
482
- // "[实时消息 from <id>]: ..." + "[系统触发器] 本消息要求回复(reply_to=<messageId>)。"
483
- const banner = parseSteerBanner(text)
484
- if (banner) {
485
- this.toolState.replyContext = banner
486
- }
487
- const client = this.client
488
- if (!client) return 'deferred'
489
- try {
490
- if (this.streaming) {
491
- await client.send({ type: 'steer', message: text })
492
- return 'steer'
493
- }
494
- if (this.turnActive) {
495
- // prompt 排队窗口:等 streaming 开始(上限 20s);工具阻塞的回合等不到 → deferred
496
- const deadline = Date.now() + 20_000
497
- while (Date.now() < deadline && this.turnActive && !this.streaming) {
498
- const { promise, resolve } = Promise.withResolvers()
499
- setTimeout(resolve, 150)
500
- await promise
501
- }
502
- if (this.streaming && this.turnActive) {
503
- await client.send({ type: 'steer', message: text })
504
- return 'steer'
505
- }
506
- return 'deferred'
507
- }
508
- // 空闲/回合已结束:deferred(消息 pending,消费循环 dequeue 即起回合)
509
- return 'deferred'
510
- }
511
- catch (err) {
512
- log.error(`[OmpRpcAgent:${this.selfAgentId}] steer 注入失败(消息保持 pending):`, err instanceof Error ? err.message : err)
513
- return 'deferred'
514
- }
515
- }
516
-
517
- async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncIterable<AgentEvent> {
518
- const kind = request.message.metadata?.['x-aw-task-kind']
519
-
520
- // worker: assign 消息 → 执行任务
521
- if (kind === 'assign' && ctx.role === 'worker') {
522
- yield* this.workerRun(request, ctx)
523
- return
524
- }
525
-
526
- // worker/lead: 同事点对点消息(含实时通信触发器)→ 按触发器语义处理并回复;
527
- // 人类经 REST 注入的消息(from 空 + x-aw-from-label)同样进入应答流
528
- if (!kind && (request.fromAgentId || request.message.metadata?.['x-aw-from-label'])) {
529
- yield* this.peerMessageRun(request, ctx)
530
- return
531
- }
532
-
533
- // 其余消息(lead 的 assign/child-completed 等):no-op(调度由 supervise() 处理)
534
- }
535
-
536
- // ===== supervise() =====
537
-
538
- /** supervise 单飞守卫:同一 client 不并发 LLM 回合(残留回合与下一 prompt 混流的根因) */
539
- private supervising = false
540
-
541
- async supervise(snapshot: SupervisionSnapshot, ctx: AgentRunContext, opts?: { signal?: AbortSignal }): Promise<SupervisionDecision[]> {
542
- await this.ensureClient(ctx)
543
- if (!this.client) return []
544
- if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
545
-
546
- // 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
547
- await this.contextGate('supervise')
548
-
549
- const prompt = supervisePrompt({
550
- snapshot,
551
- agentName: this.agentName,
552
- channelId: this.channelId,
553
- ctxPrefix: await this.contextPrefix(),
554
- manual: this.systemManual(),
555
- memory: ctx.memory,
556
- })
557
- // 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
558
- // supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
559
- // 单步可能 >60s,过紧会把正常回合掐成 "Interrupted by user"。默认 150s,
560
- // 仅拦真僵死(更紧的预算由调用方 config.superviseTimeoutMs 显式传入);真 abort 已实现。
561
- const timeoutMs = this.config.superviseTimeoutMs ?? 150_000
562
-
563
- return new Promise<SupervisionDecision[]>((resolve) => {
564
- let assistantText = ''
565
- let resolved = false
566
-
567
- const finish = (decisions: SupervisionDecision[]) => {
568
- if (resolved) return
569
- resolved = true
570
- this.supervising = false
571
- unsub()
572
- clearTimeout(timer)
573
- signalUnsub?.()
574
- resolve(decisions)
575
- }
576
-
577
- const abortTurn = (): void => {
578
- // 超时/外部取消:真正中止 omp 当前回合 —— 只 resolve 不 abort 会让残留回合
579
- // 与下一个 prompt 在同一 client 混流(决策错位 + token 空烧)
580
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] supervise 超时(${timeoutMs}ms)→ abort 当前调度回合`)
581
- void this.client?.send({ type: 'abort' }).catch(() => {})
582
- finish([])
583
- }
584
-
585
- const unsub = this.client!.onEvent((event) => {
586
- if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
587
- assistantText += event.assistantMessageEvent.delta ?? ''
588
- }
589
- if (event.type === 'agent_end' && event.isTerminal !== false) {
590
- // 尝试从文本解析 JSON 决策(备用:如果 agent 没用 host tools 而是输出 JSON)
591
- const parsed = extractJsonArray(assistantText)
592
- if (parsed && parsed.length > 0) {
593
- finish(parsed as SupervisionDecision[])
594
- }
595
- else {
596
- // agent 可能已通过 host tools 直接执行了调度,返回空(已执行)
597
- finish([])
598
- }
599
- }
600
- if (event.type === '__process_exit__' || event.type === '__error__') {
601
- finish([])
602
- }
603
- })
604
-
605
- const timer = setTimeout(abortTurn, timeoutMs)
606
-
607
- // 外部取消(调度器 cancel 路径)传导:abort 当前 LLM 回合并立即收口
608
- let signalUnsub: (() => void) | undefined
609
- if (opts?.signal) {
610
- if (opts.signal.aborted) {
611
- unsub()
612
- resolve([])
613
- return
614
- }
615
- const onAbort = (): void => abortTurn()
616
- opts.signal.addEventListener('abort', onAbort, { once: true })
617
- signalUnsub = () => opts.signal?.removeEventListener('abort', onAbort)
618
- }
619
-
620
- this.supervising = true
621
- this.client!.send({ type: 'prompt', message: prompt }).catch(() => finish([]))
622
- })
623
- }
624
-
625
- // ===== 内部:worker 执行 =====
626
-
627
- private async* workerRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
628
- const taskId = request.taskId
629
- ?? (request.message.metadata?.['x-aw-task-id'] as string | undefined)
630
- if (!taskId) return
631
-
632
- this.toolState.currentTaskId = taskId
633
-
634
- try {
635
- await this.ensureClient(ctx)
636
- }
637
- catch (err) {
638
- yield {
639
- kind: 'error',
640
- error: {
641
- code: 'OMP_SPAWN_FAILED',
642
- message: `omp 子进程启动失败: ${err instanceof Error ? err.message : String(err)}`,
643
- },
644
- }
645
- return
646
- }
647
-
648
- if (!this.client) {
649
- yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪' } }
650
- return
651
- }
652
-
653
- // 构建 prompt
654
- const taskText = request.message.parts.map((p) => {
655
- if ('text' in p) return p.text
656
- if ('data' in p) return JSON.stringify(p.data)
657
- if ('url' in p) return p.url
658
- if ('raw' in p) return p.raw
659
- return ''
660
- }).join('\n')
661
- const prompt = workerPrompt({
662
- agentName: this.agentName,
663
- channelId: this.channelId,
664
- taskId,
665
- taskText,
666
- memory: request.memory,
667
- ctxPrefix: await this.contextPrefix(),
668
- manual: this.systemManual(),
669
- })
670
-
671
- // 流式执行 + 事件映射
672
- yield* this.promptAndStream(prompt, taskId, ctx.signal)
673
- }
674
-
675
- /**
676
- * 点对点消息处理(实时通信驱动;lead 与 worker 通用)。
677
- * 触发器语义:metadata['x-aw-require-reply']='true' → 必须经 send_message_to_agent
678
- * 回给发送者:执行结果 + 对方所需内容,in_reply_to 关联原消息,并声明是否需再响应。
679
- */
680
- private async* peerMessageRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
681
- try {
682
- await this.ensureClient(ctx)
683
- }
684
- catch (err) {
685
- // spawn 失败必须显式报错:静默 return 会让 sawRunError=false → 消息被
686
- // 直接 markConsumed,实时消息一次性丢失且无任何重试;产出 error 事件
687
- // 走消息 requeue(≤2 次),与 workerRun 同口径
688
- yield {
689
- kind: 'error',
690
- error: {
691
- code: 'OMP_SPAWN_FAILED',
692
- message: `omp 子进程启动失败(peer 消息未消费,将重试): ${err instanceof Error ? err.message : String(err)}`,
693
- },
694
- }
695
- return
696
- }
697
- if (!this.client) {
698
- yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪(peer 消息未消费,将重试)' } }
699
- return
700
- }
701
-
702
- const msg = request.message
703
- // 显示名:agent 同事用 id(名册可解析);人类发送者用 fromLabel;兜底 unknown
704
- const fromId = request.fromAgentId
705
- ?? (typeof msg.metadata?.['x-aw-from-label'] === 'string' ? msg.metadata['x-aw-from-label'] : undefined)
706
- ?? 'unknown'
707
- const msgText = msg.parts.map((p) => {
708
- if ('text' in p) return p.text
709
- if ('data' in p) return JSON.stringify(p.data)
710
- if ('url' in p) return p.url
711
- if ('raw' in p) return p.raw
712
- return ''
713
- }).join('\n')
714
- const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
715
- const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
716
- // 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
717
- const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
718
- const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
719
- ? String(msg.metadata['x-aw-from-channel'])
720
- : ''
721
- // 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
722
- // 对人类的回复经事件流/时间线可见)
723
- this.toolState.replyContext = requireReply && request.fromAgentId && !crossChannel
724
- ? { fromId: request.fromAgentId, messageId: msg.messageId }
725
- : null
726
-
727
- const prompt = peerPrompt({
728
- agentName: this.agentName,
729
- role: this.agentRole,
730
- channelId: this.channelId,
731
- ctxPrefix: await this.contextPrefix(),
732
- manual: this.systemManual(),
733
- memory: request.memory,
734
- fromId,
735
- messageId: msg.messageId,
736
- requireReply,
737
- isReply,
738
- crossChannel,
739
- fromChannel,
740
- msgText,
741
- })
742
-
743
- yield* this.promptAndStream(prompt, undefined, ctx.signal)
744
- }
745
-
746
- // ===== 内部:prompt 发送 + 事件流桥接 =====
747
-
748
- private async* promptAndStream(
749
- prompt: string,
750
- taskId: string | undefined,
751
- signal: AbortSignal,
752
- ): AsyncGenerator<AgentEvent, void, unknown> {
753
- const client = this.client
754
- if (!client) {
755
- throw new Error('omp 客户端未就绪')
756
- }
757
- // 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
758
- await this.contextGate('pre-prompt')
759
- const queue: AgentEvent[] = []
760
- let resolveWait: (() => void) | null = null
761
- let isDone = false
762
- // 回合生命周期:prompt 已发出 → turnActive;首条 message_update → streaming;message_end/turn_end → 结束
763
- this.turnActive = true
764
- this.streaming = false
765
- this.turnText = ''
766
- // 正文流式差额账本:contentIndex → 已流出长度。部分 provider(reasoning 模型/
767
- // openai-completions)的 text 块不走 text_delta(只有换行),全文在 text_end.content
768
- // 落定 —— 差额兜底把未流出部分补发为 delta,保证前端流式不缺正文。
769
- const textSent = new Map<number, number>()
770
- const unsubState = client.onEvent((event) => {
771
- if (event.type === 'message_update') {
772
- const ev = event.assistantMessageEvent as
773
- | { type?: string, delta?: string, content?: string, contentIndex?: number }
774
- | undefined
775
- if (ev?.type === 'text_delta') {
776
- this.streaming = true
777
- const text = ev.delta ?? ''
778
- const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
779
- textSent.set(ci, (textSent.get(ci) ?? 0) + text.length)
780
- this.turnText += text
781
- // LLM 流式增量透出(AEP agent.delta 事件源):仅 worker/peer 转本走生成器
782
- if (text) enqueueDelta(text)
783
- }
784
- else if (ev?.type === 'text_end' && typeof ev.content === 'string') {
785
- const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
786
- const sent = textSent.get(ci) ?? 0
787
- if (ev.content.length > sent) {
788
- const chunk = ev.content.slice(sent)
789
- textSent.set(ci, ev.content.length)
790
- this.streaming = true
791
- this.turnText += chunk
792
- enqueueDelta(chunk)
793
- }
794
- }
795
- }
796
- if (event.type === 'message_end' || event.type === 'turn_end') {
797
- this.streaming = false
798
- flushDelta()
799
- }
800
- if (event.type === 'agent_end' && event.isTerminal !== false) {
801
- this.turnActive = false
802
- this.streaming = false
803
- flushDelta()
804
- }
805
- if (event.type === '__process_exit__' || event.type === '__error__') {
806
- this.turnActive = false
807
- this.streaming = false
808
- }
809
- })
810
-
811
- const enqueue = (event: AgentEvent): void => {
812
- queue.push(event)
813
- if (event.kind === 'done' || event.kind === 'error') isDone = true
814
- lastActivity = Date.now()
815
- resolveWait?.()
816
- resolveWait = null
817
- }
818
-
819
- // 停滞看门狗:整轮无任何 omp 事件(挂死的 LLM 调用/子进程僵死)时中止回合,
820
- // 否则消息永久卡 consuming、队友消息无限堆积。host 工具内阻塞
821
- // (poll_messages 最长 180s)与慢 provider(实测单步可 >5min)都有
822
- // tool/状态事件刷新计时;默认 600s 覆盖慢回合,只拦真僵死。
823
- const idleTimeoutMs = this.config.promptTimeoutMs ?? 600_000
824
- let lastActivity = Date.now()
825
- let stallTimer: ReturnType<typeof setTimeout> | null = null
826
-
827
- // LLM 流式增量缓冲:50ms 批量合并为一帧 delta(防高频 text_delta 洪泛 WS)
828
- let deltaBuf = ''
829
- let deltaTimer: ReturnType<typeof setTimeout> | null = null
830
- const flushDelta = (): void => {
831
- if (deltaTimer) {
832
- clearTimeout(deltaTimer)
833
- deltaTimer = null
834
- }
835
- if (!deltaBuf) return
836
- const text = deltaBuf
837
- deltaBuf = ''
838
- enqueue({ kind: 'delta', delta: { text } })
839
- }
840
- const enqueueDelta = (text: string): void => {
841
- deltaBuf += text
842
- deltaTimer ??= setTimeout(() => {
843
- deltaTimer = null
844
- flushDelta()
845
- }, 50)
846
- }
847
-
848
- // 订阅 omp 事件流
849
- const unsub = client.onEvent((event) => {
850
- for (const mapped of this.mapOmpEvent(event, taskId)) {
851
- enqueue(mapped)
852
- }
853
- })
854
-
855
- // abort 传导
856
- const onAbort = (): void => {
857
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] run 被 abort(signal)→ 中止 omp 回合,taskId=${taskId ?? '-'}`)
858
- client.send({ type: 'abort' }).catch(() => {})
859
- if (!isDone) {
860
- enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
861
- }
862
- }
863
- if (signal.aborted) {
864
- onAbort()
865
- }
866
- else {
867
- signal.addEventListener('abort', onAbort, { once: true })
868
- }
869
-
870
- // 发送 prompt
871
- try {
872
- await client.send({ type: 'prompt', message: prompt })
873
- }
874
- catch (err) {
875
- unsub()
876
- signal.removeEventListener('abort', onAbort)
877
- // 僵死进程回收:prompt send 失败 = stdio 断裂或 60s 命令确认超时 —— 进程
878
- // "活着但不干活"(alive-but-wedged)。若仅报错不清理,消息重试(≤2 次)会
879
- // 复用同一僵死 stdio 每次空转 60s;必须杀掉并置空,下回合 ensureClient
880
- // 全新重生(host tools/模型/终端 tap 随重建)。
881
- log.warn(`[OmpRpcAgent:${this.selfAgentId}] prompt 失败 → 回收可疑僵死进程 pid=${client.pid}`)
882
- this.killProcess()
883
- this.client = null
884
- this.hostToolsRegistered = false
885
- yield {
886
- kind: 'error',
887
- error: {
888
- code: 'PROMPT_FAILED',
889
- message: `omp prompt 失败: ${err instanceof Error ? err.message : String(err)}`,
890
- },
891
- }
892
- return
893
- }
894
-
895
- // 产出事件直到 done/error
896
- try {
897
- while (!isDone || queue.length > 0) {
898
- if (queue.length === 0 && !isDone) {
899
- const remaining = idleTimeoutMs - (Date.now() - lastActivity)
900
- if (remaining <= 0) {
901
- client.send({ type: 'abort' }).catch(() => {})
902
- // 子进程大概率已僵死:击杀并清引用,下回合 ensureClient 全新重生
903
- this.killProcess()
904
- this.client = null
905
- this.hostToolsRegistered = false
906
- enqueue({
907
- kind: 'error',
908
- error: {
909
- code: 'TURN_STALLED',
910
- message: `回合停滞 ${Math.round(idleTimeoutMs / 1000)}s 无 omp 事件,已中止重置(消息按已处理落账,后续消息继续)`,
911
- },
912
- })
913
- continue
914
- }
915
- await new Promise<void>((r) => {
916
- resolveWait = r
917
- stallTimer = setTimeout(() => {
918
- resolveWait = null
919
- r()
920
- }, remaining + 100)
921
- })
922
- if (stallTimer) {
923
- clearTimeout(stallTimer)
924
- stallTimer = null
925
- }
926
- }
927
- while (queue.length > 0) {
928
- yield queue.shift()!
929
- }
930
- }
931
- }
932
- finally {
933
- if (stallTimer) clearTimeout(stallTimer)
934
- // 事件监听必须全部解除:本 client 跨回合长期复用,残留的 mapOmpEvent
935
- // 闭包会在后续每一帧继续执行并把事件 push 进孤儿队列(内存/CPU 随
936
- // 回合数线性劣化,直接违背进程常驻的稳定性目标)
937
- unsubState()
938
- unsub()
939
- signal.removeEventListener('abort', onAbort)
940
- this.turnActive = false
941
- this.streaming = false
942
- this.toolState.currentTaskId = null
943
- }
944
- }
945
-
946
- // ===== 内部:omp 事件 → AgentEvent 映射 =====
947
-
948
- private mapOmpEvent(event: AgentSessionEvent, taskId: string | undefined): AgentEvent[] {
949
- switch (event.type) {
950
- case 'agent_start':
951
- return [{
952
- kind: 'status',
953
- status: { state: 'WORKING', timestamp: new Date().toISOString() },
954
- }]
955
- case 'message_end': {
956
- // 消息完成:如果有 message 内容,产出为 status 事件(任务历史追踪)
957
- const msg = event.message
958
- if (msg && Array.isArray(msg.content)) {
959
- const text = (msg.content as Array<{ type?: string, text?: string }>)
960
- .filter(c => c.type === 'text')
961
- .map(c => c.text ?? '')
962
- .join('')
963
- if (text) {
964
- const a2aMsg: A2AMessage = {
965
- messageId: randomUUID(),
966
- contextId: this.channelId,
967
- role: 'ROLE_AGENT',
968
- parts: [{ text }],
969
- }
970
- return [{
971
- kind: 'status',
972
- status: { state: 'WORKING', message: a2aMsg, timestamp: new Date().toISOString() },
973
- }]
974
- }
975
- }
976
- return []
977
- }
978
-
979
- case 'tool_execution_start': {
980
- const toolName = event.toolName ?? 'tool'
981
- return [{
982
- kind: 'status',
983
- status: {
984
- state: 'WORKING',
985
- message: {
986
- messageId: randomUUID(),
987
- contextId: this.channelId,
988
- role: 'ROLE_AGENT',
989
- parts: [{ text: `🔧 ${toolName}${toolArgsPreview(event.args)}` }],
990
- },
991
- timestamp: new Date().toISOString(),
992
- },
993
- }]
994
- }
995
-
996
- case 'agent_end': {
997
- if (event.isTerminal === false) return []
998
- const events: AgentEvent[] = []
999
- // 提取最终 assistant 文本作为 artifact
1000
- const messages = event.messages ?? []
1001
- const assistantText = messages
1002
- .filter(m => m.role === 'assistant')
1003
- .flatMap(m => m.content.filter(c => c.type === 'text').map(c => c.text ?? ''))
1004
- .join('')
1005
- // 回合内的 provider/API 失败(限额 429、鉴权、上游 5xx 等):omp 把错误挂在最后一条
1006
- // assistant 消息上(stopReason='error' + errorStatus/errorMessage)而非 __error__ 帧。
1007
- // 不映射出来,上层只能看到「回合结束无产出」,真实原因彻底丢失(且照常重试到耗尽)。
1008
- const failure = [...messages].reverse().find((m) => {
1009
- const mm = m as { role?: string, stopReason?: string, errorMessage?: string }
1010
- return mm.role === 'assistant' && (mm.stopReason === 'error' || typeof mm.errorMessage === 'string')
1011
- }) as { errorStatus?: number, errorMessage?: string } | undefined
1012
- if (failure) {
1013
- const code = failure.errorStatus != null ? `OMP_LLM_${failure.errorStatus}` : 'OMP_LLM_ERROR'
1014
- const detail = failure.errorMessage ?? 'harness 回合以错误结束(无错误详情)'
1015
- log.error(`[OmpRpcAgent:${this.selfAgentId}] harness 回合失败 ${code}: ${detail}`)
1016
- // error 事件即回合终点(队列侧据此收口),不再追加 done
1017
- return [{ kind: 'error', error: { code, message: detail } }]
1018
- }
1019
- if (assistantText) {
1020
- events.push({
1021
- kind: 'artifact',
1022
- artifact: {
1023
- artifactId: randomUUID(),
1024
- name: 'output',
1025
- parts: [{ text: assistantText }],
1026
- },
1027
- lastChunk: true,
1028
- totalChunks: 1,
1029
- })
1030
- }
1031
- events.push({ kind: 'done', final: taskId ? { taskId } : undefined })
1032
- return events
1033
- }
1034
-
1035
- case '__process_exit__':
1036
- return [{
1037
- kind: 'error',
1038
- error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
1039
- }]
1040
-
1041
- // ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
1042
- case 'compaction_start':
1043
- this.compacting = true
1044
- return []
1045
-
1046
- case 'compaction_end': {
1047
- this.compacting = false
1048
- const result = (event as { result?: CompactionResult }).result
1049
- if (result?.summary) {
1050
- void this.harvestCompaction(
1051
- result.summary,
1052
- result.tokensBefore,
1053
- result.tokensAfter ?? result.estimatedTokensAfter,
1054
- (event as { reason?: string }).reason,
1055
- )
1056
- }
1057
- return []
1058
- }
1059
-
1060
- // 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
1061
- case 'agent_settled':
1062
- return []
1063
-
1064
- case '__error__':
1065
- return [{
1066
- kind: 'error',
1067
- error: { code: 'OMP_ERROR', message: (event as { error?: string }).error ?? 'omp 未知错误' },
1068
- }]
1069
-
1070
- default:
1071
- return []
1072
- }
1073
- }
1074
-
1075
- // ===== 内部:omp 客户端管理 =====
1076
-
1077
- private async ensureClient(ctx: AgentRunContext): Promise<void> {
1078
- if (!this.workspace) {
1079
- this.workspace = ctx.workspace
1080
- }
1081
- if (!this.agentInfo) {
1082
- // init() 可能未调用;从 ctx 推断
1083
- this.channelId = ctx.channelId
1084
- this.agentRole = ctx.role
1085
- this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
1086
- }
1087
-
1088
- if (!this.client || !this.client.alive) {
1089
- // 旧客户端已退出(exit 事件/OS 存活校准 reconcile 触发)——必须丢弃并重生,
1090
- // 否则后续回合会一直对着死 stdio 报 PROMPT_FAILED 烧重试配额
1091
- this.client = null
1092
- this.hostToolsRegistered = false
1093
- const command = this.config.command ?? 'omp'
1094
- const client = new OmpRpcClient({
1095
- command,
1096
- mode: this.rpcMode,
1097
- args: [...(this.config.thinkingLevel ? ['--thinking', this.config.thinkingLevel] : [])],
1098
- args: this.config.args,
1099
- cwd: this.config.cwd ?? process.cwd(),
1100
- // 进程退出 → 注册表标记(供运行时资源监控);pid=-1 表示无法取得,忽略
1101
- onExit: (pid, code) => {
1102
- if (pid > 0) {
1103
- markHarnessProcessExit(pid, code)
1104
- markTerminalSessionExit(pid, code)
1105
- }
1106
- },
1107
- })
1108
- await client.start()
1109
-
1110
- // 登记 + 绑定 agent 身份(harness 进程监控的事实源)
1111
- const pid = client.pid
1112
- if (pid) {
1113
- registerHarnessProcess(pid, {
1114
- harness: 'omp',
1115
- command,
1116
- args: ['--mode', this.rpcMode, ...(this.config.args ?? [])],
1117
- })
1118
- bindHarnessProcess(pid, {
1119
- agentId: this.selfAgentId,
1120
- channelId: this.channelId,
1121
- name: this.agentName,
1122
- role: this.agentRole,
1123
- })
1124
- // 终端镜像 tap:全部 RPC 帧 → /monitor 终端(实时 TUI 渲染 + HITL)
1125
- attachTerminalTap(client, {
1126
- pid,
1127
- harness: 'omp',
1128
- agentId: this.selfAgentId,
1129
- channelId: this.channelId,
1130
- name: this.agentName,
1131
- role: this.agentRole,
1132
- })
1133
- }
1134
-
1135
- // 设置模型(如果配置了 provider/model 任一;omp 以当前已设值为缺省补全,
1136
- // 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
1137
- if (this.config.provider || this.config.model) {
1138
- try {
1139
- // 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
1140
- await client.send({
1141
- type: 'set_model',
1142
- provider: this.config.provider as string,
1143
- modelId: this.config.model as string,
1144
- })
1145
- }
1146
- catch {
1147
- // 模型设置失败不致命(用 omp 默认模型)
1148
- }
1149
- }
1150
-
1151
- // 注册 host tools(按角色差异化:lead 全量,worker 剔除调度/团队管理专属工具)
1152
- client.onHostToolCall(req => this.handleHostTool(req))
1153
- await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) })
1154
-
1155
- // 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
1156
- void this.probeContext(client).catch(() => {})
1157
- void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
1158
-
1159
- this.client = client
1160
- this.hostToolsRegistered = true
1161
- }
1162
- }
1163
-
1164
- /** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
1165
- private async probeContext(client: OmpRpcClient): Promise<void> {
1166
- try {
1167
- const resp = await client.send({ type: 'get_state' })
1168
- const data = (resp.data ?? {}) as Record<string, unknown>
1169
- const model = data.model as { contextWindow?: number } | undefined
1170
- if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
1171
- this.contextWindow = model.contextWindow
1172
- client.setContextWindow(model.contextWindow)
1173
- }
1174
- if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
1175
- }
1176
- catch { /* get_state 不可用:仅被动 usage */ }
1177
- }
1178
- }
1
+ /**
2
+ * OmpRpcAgentImpl — omp harness 的完整 AgentInterface 实现。
3
+ *
4
+ * 通过 OmpRpcClient 驱动真实 omp 子进程(--mode rpc),将 omp 的原生 agent 能力
5
+ * (LLM 推理 + 内置工具 read/write/edit/bash/grep/glob 等)接入 Workshop 的
6
+ * Channel 编排体系。
7
+ *
8
+ * 核心设计:
9
+ * - 每个 Agent 实例持有一个 omp 子进程(lazy spawn,跨消息复用)
10
+ * - AgentWorkspace 方法经 host-tool-bridge 注册为 omp host tools,agent 原生调用
11
+ * report_progress / complete_task / dispatch_task / send_message 等,无需文本解析
12
+ * - omp AgentSessionEvent → AgentEvent 五变体映射
13
+ * - worker run():接收 assign → prompt omp → agent 用原生工具作业 + host tools 管理任务生命周期
14
+ * - lead supervise():格式化快照 → prompt omp → agent 直接用 host tools 执行调度决策 → 返回 []
15
+ * - dispose():杀子进程
16
+ *
17
+ * prompt 组装与工具分发已抽至共享层(prompt-builder / host-tool-bridge),
18
+ * 与 codex/dsh/opencode impl 全引擎一致。协议权威:omp://rpc.md。
19
+ */
20
+ import { createLogger } from '../logger'
21
+ import { randomUUID } from 'node:crypto'
22
+ import type {
23
+ AgentEvent,
24
+ AgentInterface,
25
+ AgentInfo,
26
+ AgentRunContext,
27
+ AgentRunRequest,
28
+ SupervisionDecision,
29
+ SupervisionSnapshot,
30
+ } from './agent-interface'
31
+ import type { A2AMessage } from '../types/a2a'
32
+ import type { AgentContextStats } from '../types/task'
33
+ import {
34
+ OmpRpcClient,
35
+ type AgentSessionEvent,
36
+ type CompactionResult,
37
+ type HostToolCallRequest,
38
+ } from './adapters/omp-rpc-client'
39
+ import {
40
+ registerHarnessProcess,
41
+ bindHarnessProcess,
42
+ markHarnessProcessExit,
43
+ killHarnessProcess,
44
+ } from './harness-process'
45
+ import {
46
+ attachTerminalTap,
47
+ markTerminalSessionExit,
48
+ } from './harness-terminal'
49
+ import {
50
+ createSessionState,
51
+ dispatchHostTool,
52
+ hostToolsForRole,
53
+ } from './host-tool-bridge'
54
+ import {
55
+ contextPrefix as buildContextPrefix,
56
+ createRosterCache,
57
+ extractJsonArray,
58
+ parseSteerBanner,
59
+ peerPrompt,
60
+ supervisePrompt,
61
+ systemManual,
62
+ toolArgsPreview,
63
+ workerPrompt,
64
+ } from './prompt-builder'
65
+ import { renderPrompt } from '../prompts/loader'
66
+ import { attachOmpPluginBridge, onPluginToolsChange } from './plugin-tools'
67
+ import { ompSettings } from '../settings'
68
+
69
+ const log = createLogger('workshop.omp')
70
+
71
+ // 兼容再导出(脚本/测试历史导入面;实现在 host-tool-bridge)
72
+ export { HOST_TOOLS, hostToolsForRole } from './host-tool-bridge'
73
+
74
+ // ===== 配置 =====
75
+
76
+ export interface OmpAgentConfig {
77
+ /** omp 可执行文件路径(默认 'omp',从 PATH 查找) */
78
+ command?: string
79
+ /** 额外 CLI 参数 */
80
+ args?: string[]
81
+ /** omp 工作目录(默认 process.cwd()) */
82
+ cwd?: string
83
+ /** 模型 provider(如 'anthropic'/'openai'/'zhipu');省略则用 omp 默认 */
84
+ provider?: string
85
+ /** 模型 ID;省略则用 omp 默认 */
86
+ model?: string
87
+ /** thinking level: off/minimal/low/medium/high/xhigh/max */
88
+ thinkingLevel?: string
89
+ /** 自定义系统 prompt 前缀(拼接到 agent 系统指令之前) */
90
+ systemPromptPrefix?: string
91
+ /** 限制 omp 仅使用指定内置工具(null = 全部) */
92
+ toolNames?: string[]
93
+ /** 每轮停滞超时(ms,默认 300000):整轮无任何 omp 事件才中止;工具内阻塞(poll_messages ≤180s)有 tool 事件刷新计时 */
94
+ promptTimeoutMs?: number
95
+ /** supervise 轮超时(ms,默认 150000) */
96
+ superviseTimeoutMs?: number
97
+ /**
98
+ * omp 输出模式:'rpc-ui'(默认)= RPC 协议 + UI 上下文 —— ask 工具与
99
+ * extension_ui_request 对话框可用(监控终端 HITL 通道,强制 noPty);
100
+ * 'rpc' = 纯协议(无 UI,ask 不可用)。
101
+ */
102
+ rpcMode?: 'rpc' | 'rpc-ui'
103
+ /** channel 级作业场景 prompt(manager 装配时注入;用户场景规范,全员共享) */
104
+ scenarioPrompt?: string
105
+ /** agent 身份(由 factory 从 AgentInfo 注入) */
106
+ agentId?: string
107
+ name?: string
108
+ role?: 'lead' | 'worker'
109
+ channelId?: string
110
+ }
111
+
112
+ // 插件桥接管(host.mjs ctx.omp 排队注册的工具在此回放;幂等)
113
+ attachOmpPluginBridge()
114
+
115
+ /** 在跑 agent 实例表(工具注册表变更 → 热重发 set_host_tools,运行时注入无需重spawn) */
116
+ const gLive = globalThis as typeof globalThis & { __ompLiveAgents?: Set<OmpRpcAgentImpl> }
117
+ function liveAgents(): Set<OmpRpcAgentImpl> {
118
+ return gLive.__ompLiveAgents ??= new Set()
119
+ }
120
+
121
+ // 工具注册表变更 → 全部在跑会话热更新工具面
122
+ onPluginToolsChange(() => {
123
+ for (const impl of liveAgents()) {
124
+ try {
125
+ impl.refreshPluginTools()
126
+ }
127
+ catch { /* 单实例失败不影响其他 */ }
128
+ }
129
+ })
130
+
131
+ // ===== OmpRpcAgentImpl =====
132
+
133
+ export class OmpRpcAgentImpl implements AgentInterface {
134
+ private readonly config: OmpAgentConfig
135
+ private client: OmpRpcClient | null = null
136
+ private workspace: AgentRunContext['workspace'] | null = null
137
+ private agentInfo: AgentInfo | null = null
138
+ private hostToolsRegistered = false
139
+ /** host tool 会话态(当前任务 + 待回执上下文;与共享桥共用) */
140
+ private readonly toolState = createSessionState()
141
+ /** 工具桥上下文(分发唯一入口) */
142
+ private readonly bridgeCtx = {
143
+ identity: { agentId: '', channelId: '', role: 'worker' as 'lead' | 'worker', name: 'agent' },
144
+ state: this.toolState,
145
+ getWorkspace: () => this.workspace,
146
+ }
147
+
148
+ /**
149
+ * 会话回合状态(steer 可靠注入的依据):
150
+ * omp 的 steer 仅在回合 streaming 中生效——prompt 已入列但尚未开始输出时,
151
+ * steer 会"成功"返回但被静默丢弃。因此注入方轮询直到回合输出开始(streamingStarted)
152
+ * 才发送;若回合在等待期间结束,消息保持 pending 由消费循环处理。
153
+ */
154
+ private streaming = false
155
+ private turnActive = false
156
+ /** 当前回合产生的 assistant 文本(供诊断) */
157
+ private turnText = ''
158
+ /** agent 身份信息(factory 注入;无需等待 init()) */
159
+ private selfAgentId = ''
160
+ private agentName = 'agent'
161
+ private agentRole: 'lead' | 'worker' = 'worker'
162
+ private channelId = ''
163
+ /** 团队名册缓存(共享 prompt-builder) */
164
+ private roster = createRosterCache({
165
+ selfAgentId: '',
166
+ listAgents: () => this.workspace!.listAgents(),
167
+ })
168
+
169
+ // ===== 上下文治理(70% 无中断压缩环)=====
170
+ /** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
171
+ private compacting = false
172
+ private lastCompactAt = 0
173
+ /** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
174
+ private compactLegacy = false
175
+ /** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
176
+ private contextWindow: number | null = null
177
+ private sessionId: string | null = null
178
+ /** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
179
+ private lastHarvestKey = ''
180
+ private lastHarvestAt = 0
181
+
182
+ /** 总开关(omp.compact_enabled;env AW_OMP_COMPACT_ENABLED=0 显式关闭兼容) */
183
+ private compactEnabled(): boolean {
184
+ return ompSettings().compact_enabled
185
+ }
186
+
187
+ private compactThreshold(): number {
188
+ return ompSettings().compact_threshold
189
+ }
190
+
191
+ private compactMinIntervalMs(): number {
192
+ return ompSettings().compact_min_interval_ms
193
+ }
194
+
195
+ private compactWaitMs(): number {
196
+ return ompSettings().compact_wait_ms
197
+ }
198
+
199
+ /** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
200
+ getContextStats(): AgentContextStats | null {
201
+ const usage = this.client?.getContextUsage() ?? null
202
+ const window = this.contextWindow ?? usage?.contextWindow ?? null
203
+ if (!usage && window === null) return null
204
+ const usedTokens = usage?.tokens ?? 0
205
+ return {
206
+ usedTokens,
207
+ contextWindow: window,
208
+ percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
209
+ compacting: this.compacting,
210
+ }
211
+ }
212
+
213
+ /**
214
+ * 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
215
+ * percent 归一化到 0-1(omp 可能返回 0-100)。
216
+ */
217
+ private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
218
+ try {
219
+ const resp = await client.send({ type: 'get_session_stats' })
220
+ const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
221
+ | { tokens?: number, contextWindow?: number, percent?: number }
222
+ | undefined
223
+ if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
224
+ if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
225
+ this.contextWindow = cu.contextWindow
226
+ client.setContextWindow(cu.contextWindow)
227
+ }
228
+ const window = this.contextWindow ?? cu.contextWindow ?? null
229
+ const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
230
+ const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
231
+ return { tokens: cu.tokens, contextWindow: window, percent }
232
+ }
233
+ }
234
+ catch { /* get_session_stats 不可用 → 被动跟踪 */ }
235
+ return client.getContextUsage()
236
+ }
237
+
238
+ /**
239
+ * 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
240
+ * post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
241
+ * 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
242
+ * isCompacting → compacting 互斥位 + 最小间隔防振荡。
243
+ */
244
+ private async contextGate(reason: string): Promise<void> {
245
+ if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
246
+ const client = this.client
247
+ if (!client || !client.alive) return
248
+ try {
249
+ const usage = await this.probeUsage(client)
250
+ if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
251
+ if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
252
+ // 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
253
+ const st = await client.send({ type: 'get_state' })
254
+ const s = (st.data ?? {}) as Record<string, unknown>
255
+ if (s.isStreaming === true || s.isCompacting === true) return
256
+ if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
257
+ this.compacting = true
258
+ this.lastCompactAt = Date.now()
259
+ try {
260
+ const result = await this.runCompaction(client)
261
+ if (result?.summary) {
262
+ await this.harvestCompaction(
263
+ result.summary,
264
+ result.tokensBefore,
265
+ result.tokensAfter ?? result.estimatedTokensAfter,
266
+ reason,
267
+ )
268
+ }
269
+ }
270
+ finally {
271
+ this.compacting = false
272
+ }
273
+ }
274
+ catch (err) {
275
+ const msg = err instanceof Error ? err.message : String(err)
276
+ if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
277
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
278
+ }
279
+ }
280
+
281
+ /**
282
+ * 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
283
+ * 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
284
+ */
285
+ private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
286
+ return new Promise<CompactionResult | null>((resolve) => {
287
+ let settled = false
288
+ const finish = (r: CompactionResult | null): void => {
289
+ if (settled) return
290
+ settled = true
291
+ off()
292
+ clearTimeout(timer)
293
+ resolve(r)
294
+ }
295
+ const off = client.onEvent((event) => {
296
+ if (event.type === 'compaction_end') {
297
+ finish((event as { result?: CompactionResult }).result ?? null)
298
+ }
299
+ else if (event.type === '__process_exit__') {
300
+ finish(null)
301
+ }
302
+ })
303
+ const timer = setTimeout(() => {
304
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
305
+ finish(null)
306
+ }, this.compactWaitMs())
307
+ void client.send({ type: 'compact', customInstructions: this.compactionHints() })
308
+ .then((resp) => {
309
+ const result = (resp.data as { result?: CompactionResult } | undefined)?.result
310
+ if (result?.summary) finish(result)
311
+ // 响应无 result:等 compaction_end 事件(协议权威语义)
312
+ })
313
+ .catch((err) => {
314
+ const msg = err instanceof Error ? err.message : String(err)
315
+ if (/compact/i.test(msg)) this.compactLegacy = true
316
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
317
+ finish(null)
318
+ })
319
+ })
320
+ }
321
+
322
+ /** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
323
+ private compactionHints(): string | undefined {
324
+ try {
325
+ return renderPrompt('compaction-hints')
326
+ }
327
+ catch {
328
+ return undefined
329
+ }
330
+ }
331
+
332
+ /**
333
+ * 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
334
+ * 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
335
+ * 双路去重:同摘要 60s 内只入库一次。
336
+ */
337
+ private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
338
+ const key = summary.slice(0, 120)
339
+ if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
340
+ this.lastHarvestKey = key
341
+ this.lastHarvestAt = Date.now()
342
+ try {
343
+ await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
344
+ log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
345
+ }
346
+ catch (err) {
347
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
348
+ }
349
+ }
350
+
351
+ /** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
352
+ async onTurnSettled(): Promise<void> {
353
+ await this.contextGate('post-settle')
354
+ }
355
+
356
+ constructor(config: Record<string, unknown> = {}) {
357
+ this.config = config as OmpAgentConfig
358
+ // effort 统一入口(channel 默认 LLM 注入 config.effort)→ omp thinking 级别
359
+ if (!this.config.thinkingLevel && typeof config.effort === 'string' && config.effort) {
360
+ this.config.thinkingLevel = config.effort
361
+ }
362
+ // factory 注入的 agent 身份(无需等待 init())
363
+ this.selfAgentId = this.config.agentId ?? ''
364
+ this.agentName = this.config.name ?? 'agent'
365
+ this.agentRole = this.config.role ?? 'worker'
366
+ this.channelId = this.config.channelId ?? ''
367
+ this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
368
+ this.roster = createRosterCache({
369
+ selfAgentId: this.selfAgentId,
370
+ listAgents: () => this.workspace!.listAgents(),
371
+ })
372
+ liveAgents().add(this) // 在跑实例表:插件工具热注入的目标集
373
+ }
374
+
375
+ // ===== 生命周期 =====
376
+
377
+ async init(input: { agent: AgentInfo, channelId: string }): Promise<void> {
378
+ this.agentInfo = input.agent
379
+ this.channelId = input.channelId
380
+ this.agentName = input.agent.name
381
+ this.agentRole = input.agent.role
382
+ this.selfAgentId = input.agent.id
383
+ this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
384
+ this.roster = createRosterCache({
385
+ selfAgentId: this.selfAgentId,
386
+ listAgents: () => this.workspace!.listAgents(),
387
+ })
388
+ }
389
+
390
+ async dispose(): Promise<void> {
391
+ liveAgents().delete(this)
392
+ if (this.client) {
393
+ const pid = this.client.pid
394
+ await this.client.dispose()
395
+ this.client = null
396
+ if (pid) {
397
+ markHarnessProcessExit(pid, null)
398
+ markTerminalSessionExit(pid, null)
399
+ }
400
+ }
401
+ this.hostToolsRegistered = false
402
+ }
403
+
404
+ /** 工具注册表变更 → 热重发 set_host_tools(在跑会话立即获得插件新工具,无需重spawn) */
405
+ refreshPluginTools(): void {
406
+ const client = this.client
407
+ if (!client || !client.alive) return
408
+ void client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole, this.channelId) }).catch(() => {})
409
+ }
410
+
411
+ /** harness 进程资源信息(运行时资源监控;进程未 spawn/已回收 → null) */
412
+ getProcessInfo(): { pid: number, alive: boolean, command: string } | null {
413
+ const client = this.client
414
+ const pid = client?.pid
415
+ if (!pid || !client) return null
416
+ return { pid, alive: client.alive, command: `omp --mode ${this.rpcMode}` }
417
+ }
418
+
419
+ /** 强制终止 harness 进程(进程树;终止后由调用方停止对应 AgentRuntime) */
420
+ killProcess(): void {
421
+ const pid = this.client?.pid
422
+ if (pid) {
423
+ markTerminalSessionExit(pid, null)
424
+ killHarnessProcess(pid)
425
+ }
426
+ else {
427
+ this.client?.kill()
428
+ }
429
+ }
430
+
431
+ /**
432
+ * harness 进程存活校准(manager sweeper 周期性调用;休眠/强杀后 exit 事件
433
+ * 可能不达父进程):按 PID 探 OS 实际存在性,进程已死 → 客户端收敛为已退出,
434
+ * 在途回合经 __process_exit__ 归位,下一回合 ensureClient 重生子进程。
435
+ */
436
+ reconcileProcess(): void {
437
+ this.client?.reconcile()
438
+ }
439
+
440
+ /** omp 输出模式(rpc-ui = 默认,启用 HITL 对话框) */
441
+ private get rpcMode(): 'rpc' | 'rpc-ui' {
442
+ return this.config.rpcMode === 'rpc' ? 'rpc' : 'rpc-ui'
443
+ }
444
+
445
+ // ===== 工具桥(共享分发)=====
446
+
447
+ /** 引擎无关工具面:REST/MCP 桥直调与 omp 内部 host_tool_call 共用同一实现 */
448
+ dispatchHostTool(toolName: string, args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
449
+ return dispatchHostTool(this.bridgeCtx, { toolName, arguments: args })
450
+ }
451
+
452
+ private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
453
+ return this.dispatchHostTool(req.toolName, req.arguments ?? {})
454
+ }
455
+
456
+ // ===== prompt 组合 =====
457
+
458
+ /** 前置上下文(场景 × 身份 × 工业简报 × 名册;共享 prompt-builder) */
459
+ private async contextPrefix(): Promise<string> {
460
+ return buildContextPrefix({
461
+ scenarioPrompt: this.config.scenarioPrompt,
462
+ systemPromptPrefix: this.config.systemPromptPrefix,
463
+ agentId: this.selfAgentId,
464
+ roster: await this.roster.roster(),
465
+ })
466
+ }
467
+
468
+ private systemManual(): string {
469
+ return systemManual()
470
+ }
471
+
472
+ /**
473
+ * 实时消息注入(送达模式协议):
474
+ * - 回合 streaming 中 → 立即 steer,返回 'steer'(同轮可见,唯一可标记消费的路径)
475
+ * - 回合活跃但尚未 streaming(prompt 排队窗口,上限 20s)→ 等 streaming 开始后 steer
476
+ * - 回合被 host 工具阻塞(如 poll_messages 等待)/空闲/发送失败 → 返回 'deferred':
477
+ * 消息保持 pending,poll_messages 的 Mailbox 到信回调即时取走(毫秒级),或本回合
478
+ * 结束后由消费循环按 FIFO 起回合处理。
479
+ */
480
+ async steer(text: string): Promise<'steer' | 'deferred'> {
481
+ // 从确定性触发横幅提取回执上下文(AgentRuntime.injectSteer 生成,格式固定):
482
+ // "[实时消息 from <id>]: ..." + "[系统触发器] 本消息要求回复(reply_to=<messageId>)。"
483
+ const banner = parseSteerBanner(text)
484
+ if (banner) {
485
+ this.toolState.replyContext = banner
486
+ }
487
+ const client = this.client
488
+ if (!client) return 'deferred'
489
+ try {
490
+ if (this.streaming) {
491
+ await client.send({ type: 'steer', message: text })
492
+ return 'steer'
493
+ }
494
+ if (this.turnActive) {
495
+ // prompt 排队窗口:等 streaming 开始(上限 20s);工具阻塞的回合等不到 → deferred
496
+ const deadline = Date.now() + 20_000
497
+ while (Date.now() < deadline && this.turnActive && !this.streaming) {
498
+ const { promise, resolve } = Promise.withResolvers()
499
+ setTimeout(resolve, 150)
500
+ await promise
501
+ }
502
+ if (this.streaming && this.turnActive) {
503
+ await client.send({ type: 'steer', message: text })
504
+ return 'steer'
505
+ }
506
+ return 'deferred'
507
+ }
508
+ // 空闲/回合已结束:deferred(消息 pending,消费循环 dequeue 即起回合)
509
+ return 'deferred'
510
+ }
511
+ catch (err) {
512
+ log.error(`[OmpRpcAgent:${this.selfAgentId}] steer 注入失败(消息保持 pending):`, err instanceof Error ? err.message : err)
513
+ return 'deferred'
514
+ }
515
+ }
516
+
517
+ async* run(request: AgentRunRequest, ctx: AgentRunContext): AsyncIterable<AgentEvent> {
518
+ const kind = request.message.metadata?.['x-aw-task-kind']
519
+
520
+ // worker: assign 消息 → 执行任务
521
+ if (kind === 'assign' && ctx.role === 'worker') {
522
+ yield* this.workerRun(request, ctx)
523
+ return
524
+ }
525
+
526
+ // worker/lead: 同事点对点消息(含实时通信触发器)→ 按触发器语义处理并回复;
527
+ // 人类经 REST 注入的消息(from 空 + x-aw-from-label)同样进入应答流
528
+ if (!kind && (request.fromAgentId || request.message.metadata?.['x-aw-from-label'])) {
529
+ yield* this.peerMessageRun(request, ctx)
530
+ return
531
+ }
532
+
533
+ // 其余消息(lead 的 assign/child-completed 等):no-op(调度由 supervise() 处理)
534
+ }
535
+
536
+ // ===== supervise() =====
537
+
538
+ /** supervise 单飞守卫:同一 client 不并发 LLM 回合(残留回合与下一 prompt 混流的根因) */
539
+ private supervising = false
540
+
541
+ async supervise(snapshot: SupervisionSnapshot, ctx: AgentRunContext, opts?: { signal?: AbortSignal }): Promise<SupervisionDecision[]> {
542
+ await this.ensureClient(ctx)
543
+ if (!this.client) return []
544
+ if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
545
+
546
+ // 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
547
+ await this.contextGate('supervise')
548
+
549
+ const prompt = supervisePrompt({
550
+ snapshot,
551
+ agentName: this.agentName,
552
+ channelId: this.channelId,
553
+ ctxPrefix: await this.contextPrefix(),
554
+ manual: this.systemManual(),
555
+ memory: ctx.memory,
556
+ })
557
+ // 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
558
+ // supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
559
+ // 单步可能 >60s,过紧会把正常回合掐成 "Interrupted by user"。默认 150s,
560
+ // 仅拦真僵死(更紧的预算由调用方 config.superviseTimeoutMs 显式传入);真 abort 已实现。
561
+ const timeoutMs = this.config.superviseTimeoutMs ?? 150_000
562
+
563
+ return new Promise<SupervisionDecision[]>((resolve) => {
564
+ let assistantText = ''
565
+ let resolved = false
566
+
567
+ const finish = (decisions: SupervisionDecision[]) => {
568
+ if (resolved) return
569
+ resolved = true
570
+ this.supervising = false
571
+ unsub()
572
+ clearTimeout(timer)
573
+ signalUnsub?.()
574
+ resolve(decisions)
575
+ }
576
+
577
+ const abortTurn = (): void => {
578
+ // 超时/外部取消:真正中止 omp 当前回合 —— 只 resolve 不 abort 会让残留回合
579
+ // 与下一个 prompt 在同一 client 混流(决策错位 + token 空烧)
580
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] supervise 超时(${timeoutMs}ms)→ abort 当前调度回合`)
581
+ void this.client?.send({ type: 'abort' }).catch(() => {})
582
+ finish([])
583
+ }
584
+
585
+ const unsub = this.client!.onEvent((event) => {
586
+ if (event.type === 'message_update' && event.assistantMessageEvent?.type === 'text_delta') {
587
+ assistantText += event.assistantMessageEvent.delta ?? ''
588
+ }
589
+ if (event.type === 'agent_end' && event.isTerminal !== false) {
590
+ // 尝试从文本解析 JSON 决策(备用:如果 agent 没用 host tools 而是输出 JSON)
591
+ const parsed = extractJsonArray(assistantText)
592
+ if (parsed && parsed.length > 0) {
593
+ finish(parsed as SupervisionDecision[])
594
+ }
595
+ else {
596
+ // agent 可能已通过 host tools 直接执行了调度,返回空(已执行)
597
+ finish([])
598
+ }
599
+ }
600
+ if (event.type === '__process_exit__' || event.type === '__error__') {
601
+ finish([])
602
+ }
603
+ })
604
+
605
+ const timer = setTimeout(abortTurn, timeoutMs)
606
+
607
+ // 外部取消(调度器 cancel 路径)传导:abort 当前 LLM 回合并立即收口
608
+ let signalUnsub: (() => void) | undefined
609
+ if (opts?.signal) {
610
+ if (opts.signal.aborted) {
611
+ unsub()
612
+ resolve([])
613
+ return
614
+ }
615
+ const onAbort = (): void => abortTurn()
616
+ opts.signal.addEventListener('abort', onAbort, { once: true })
617
+ signalUnsub = () => opts.signal?.removeEventListener('abort', onAbort)
618
+ }
619
+
620
+ this.supervising = true
621
+ this.client!.send({ type: 'prompt', message: prompt }).catch(() => finish([]))
622
+ })
623
+ }
624
+
625
+ // ===== 内部:worker 执行 =====
626
+
627
+ private async* workerRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
628
+ const taskId = request.taskId
629
+ ?? (request.message.metadata?.['x-aw-task-id'] as string | undefined)
630
+ if (!taskId) return
631
+
632
+ this.toolState.currentTaskId = taskId
633
+
634
+ try {
635
+ await this.ensureClient(ctx)
636
+ }
637
+ catch (err) {
638
+ yield {
639
+ kind: 'error',
640
+ error: {
641
+ code: 'OMP_SPAWN_FAILED',
642
+ message: `omp 子进程启动失败: ${err instanceof Error ? err.message : String(err)}`,
643
+ },
644
+ }
645
+ return
646
+ }
647
+
648
+ if (!this.client) {
649
+ yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪' } }
650
+ return
651
+ }
652
+
653
+ // 构建 prompt
654
+ const taskText = request.message.parts.map((p) => {
655
+ if ('text' in p) return p.text
656
+ if ('data' in p) return JSON.stringify(p.data)
657
+ if ('url' in p) return p.url
658
+ if ('raw' in p) return p.raw
659
+ return ''
660
+ }).join('\n')
661
+ const prompt = workerPrompt({
662
+ agentName: this.agentName,
663
+ channelId: this.channelId,
664
+ taskId,
665
+ taskText,
666
+ memory: request.memory,
667
+ ctxPrefix: await this.contextPrefix(),
668
+ manual: this.systemManual(),
669
+ })
670
+
671
+ // 流式执行 + 事件映射
672
+ yield* this.promptAndStream(prompt, taskId, ctx.signal)
673
+ }
674
+
675
+ /**
676
+ * 点对点消息处理(实时通信驱动;lead 与 worker 通用)。
677
+ * 触发器语义:metadata['x-aw-require-reply']='true' → 必须经 send_message_to_agent
678
+ * 回给发送者:执行结果 + 对方所需内容,in_reply_to 关联原消息,并声明是否需再响应。
679
+ */
680
+ private async* peerMessageRun(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
681
+ try {
682
+ await this.ensureClient(ctx)
683
+ }
684
+ catch (err) {
685
+ // spawn 失败必须显式报错:静默 return 会让 sawRunError=false → 消息被
686
+ // 直接 markConsumed,实时消息一次性丢失且无任何重试;产出 error 事件
687
+ // 走消息 requeue(≤2 次),与 workerRun 同口径
688
+ yield {
689
+ kind: 'error',
690
+ error: {
691
+ code: 'OMP_SPAWN_FAILED',
692
+ message: `omp 子进程启动失败(peer 消息未消费,将重试): ${err instanceof Error ? err.message : String(err)}`,
693
+ },
694
+ }
695
+ return
696
+ }
697
+ if (!this.client) {
698
+ yield { kind: 'error', error: { code: 'OMP_NOT_READY', message: 'omp 客户端未就绪(peer 消息未消费,将重试)' } }
699
+ return
700
+ }
701
+
702
+ const msg = request.message
703
+ // 显示名:agent 同事用 id(名册可解析);人类发送者用 fromLabel;兜底 unknown
704
+ const fromId = request.fromAgentId
705
+ ?? (typeof msg.metadata?.['x-aw-from-label'] === 'string' ? msg.metadata['x-aw-from-label'] : undefined)
706
+ ?? 'unknown'
707
+ const msgText = msg.parts.map((p) => {
708
+ if ('text' in p) return p.text
709
+ if ('data' in p) return JSON.stringify(p.data)
710
+ if ('url' in p) return p.url
711
+ if ('raw' in p) return p.raw
712
+ return ''
713
+ }).join('\n')
714
+ const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
715
+ const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
716
+ // 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
717
+ const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
718
+ const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
719
+ ? String(msg.metadata['x-aw-from-channel'])
720
+ : ''
721
+ // 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
722
+ // 对人类的回复经事件流/时间线可见)
723
+ this.toolState.replyContext = requireReply && request.fromAgentId && !crossChannel
724
+ ? { fromId: request.fromAgentId, messageId: msg.messageId }
725
+ : null
726
+
727
+ const prompt = peerPrompt({
728
+ agentName: this.agentName,
729
+ role: this.agentRole,
730
+ channelId: this.channelId,
731
+ ctxPrefix: await this.contextPrefix(),
732
+ manual: this.systemManual(),
733
+ memory: request.memory,
734
+ fromId,
735
+ messageId: msg.messageId,
736
+ requireReply,
737
+ isReply,
738
+ crossChannel,
739
+ fromChannel,
740
+ msgText,
741
+ })
742
+
743
+ yield* this.promptAndStream(prompt, undefined, ctx.signal)
744
+ }
745
+
746
+ // ===== 内部:prompt 发送 + 事件流桥接 =====
747
+
748
+ private async* promptAndStream(
749
+ prompt: string,
750
+ taskId: string | undefined,
751
+ signal: AbortSignal,
752
+ ): AsyncGenerator<AgentEvent, void, unknown> {
753
+ const client = this.client
754
+ if (!client) {
755
+ throw new Error('omp 客户端未就绪')
756
+ }
757
+ // 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
758
+ await this.contextGate('pre-prompt')
759
+ const queue: AgentEvent[] = []
760
+ let resolveWait: (() => void) | null = null
761
+ let isDone = false
762
+ // 回合生命周期:prompt 已发出 → turnActive;首条 message_update → streaming;message_end/turn_end → 结束
763
+ this.turnActive = true
764
+ this.streaming = false
765
+ this.turnText = ''
766
+ // 正文流式差额账本:contentIndex → 已流出长度。部分 provider(reasoning 模型/
767
+ // openai-completions)的 text 块不走 text_delta(只有换行),全文在 text_end.content
768
+ // 落定 —— 差额兜底把未流出部分补发为 delta,保证前端流式不缺正文。
769
+ const textSent = new Map<number, number>()
770
+ const unsubState = client.onEvent((event) => {
771
+ if (event.type === 'message_update') {
772
+ const ev = event.assistantMessageEvent as
773
+ | { type?: string, delta?: string, content?: string, contentIndex?: number }
774
+ | undefined
775
+ if (ev?.type === 'text_delta') {
776
+ this.streaming = true
777
+ const text = ev.delta ?? ''
778
+ const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
779
+ textSent.set(ci, (textSent.get(ci) ?? 0) + text.length)
780
+ this.turnText += text
781
+ // LLM 流式增量透出(AEP agent.delta 事件源):仅 worker/peer 转本走生成器
782
+ if (text) enqueueDelta(text)
783
+ }
784
+ else if (ev?.type === 'text_end' && typeof ev.content === 'string') {
785
+ const ci = typeof ev.contentIndex === 'number' ? ev.contentIndex : 0
786
+ const sent = textSent.get(ci) ?? 0
787
+ if (ev.content.length > sent) {
788
+ const chunk = ev.content.slice(sent)
789
+ textSent.set(ci, ev.content.length)
790
+ this.streaming = true
791
+ this.turnText += chunk
792
+ enqueueDelta(chunk)
793
+ }
794
+ }
795
+ }
796
+ if (event.type === 'message_end' || event.type === 'turn_end') {
797
+ this.streaming = false
798
+ flushDelta()
799
+ }
800
+ if (event.type === 'agent_end' && event.isTerminal !== false) {
801
+ this.turnActive = false
802
+ this.streaming = false
803
+ flushDelta()
804
+ }
805
+ if (event.type === '__process_exit__' || event.type === '__error__') {
806
+ this.turnActive = false
807
+ this.streaming = false
808
+ }
809
+ })
810
+
811
+ const enqueue = (event: AgentEvent): void => {
812
+ queue.push(event)
813
+ if (event.kind === 'done' || event.kind === 'error') isDone = true
814
+ lastActivity = Date.now()
815
+ resolveWait?.()
816
+ resolveWait = null
817
+ }
818
+
819
+ // 停滞看门狗:整轮无任何 omp 事件(挂死的 LLM 调用/子进程僵死)时中止回合,
820
+ // 否则消息永久卡 consuming、队友消息无限堆积。host 工具内阻塞
821
+ // (poll_messages 最长 180s)与慢 provider(实测单步可 >5min)都有
822
+ // tool/状态事件刷新计时;默认 600s 覆盖慢回合,只拦真僵死。
823
+ const idleTimeoutMs = this.config.promptTimeoutMs ?? 600_000
824
+ let lastActivity = Date.now()
825
+ let stallTimer: ReturnType<typeof setTimeout> | null = null
826
+
827
+ // LLM 流式增量缓冲:50ms 批量合并为一帧 delta(防高频 text_delta 洪泛 WS)
828
+ let deltaBuf = ''
829
+ let deltaTimer: ReturnType<typeof setTimeout> | null = null
830
+ const flushDelta = (): void => {
831
+ if (deltaTimer) {
832
+ clearTimeout(deltaTimer)
833
+ deltaTimer = null
834
+ }
835
+ if (!deltaBuf) return
836
+ const text = deltaBuf
837
+ deltaBuf = ''
838
+ enqueue({ kind: 'delta', delta: { text } })
839
+ }
840
+ const enqueueDelta = (text: string): void => {
841
+ deltaBuf += text
842
+ deltaTimer ??= setTimeout(() => {
843
+ deltaTimer = null
844
+ flushDelta()
845
+ }, 50)
846
+ }
847
+
848
+ // 订阅 omp 事件流
849
+ const unsub = client.onEvent((event) => {
850
+ for (const mapped of this.mapOmpEvent(event, taskId)) {
851
+ enqueue(mapped)
852
+ }
853
+ })
854
+
855
+ // abort 传导
856
+ const onAbort = (): void => {
857
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] run 被 abort(signal)→ 中止 omp 回合,taskId=${taskId ?? '-'}`)
858
+ client.send({ type: 'abort' }).catch(() => {})
859
+ if (!isDone) {
860
+ enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
861
+ }
862
+ }
863
+ if (signal.aborted) {
864
+ onAbort()
865
+ }
866
+ else {
867
+ signal.addEventListener('abort', onAbort, { once: true })
868
+ }
869
+
870
+ // 发送 prompt
871
+ try {
872
+ await client.send({ type: 'prompt', message: prompt })
873
+ }
874
+ catch (err) {
875
+ unsub()
876
+ signal.removeEventListener('abort', onAbort)
877
+ // 僵死进程回收:prompt send 失败 = stdio 断裂或 60s 命令确认超时 —— 进程
878
+ // "活着但不干活"(alive-but-wedged)。若仅报错不清理,消息重试(≤2 次)会
879
+ // 复用同一僵死 stdio 每次空转 60s;必须杀掉并置空,下回合 ensureClient
880
+ // 全新重生(host tools/模型/终端 tap 随重建)。
881
+ log.warn(`[OmpRpcAgent:${this.selfAgentId}] prompt 失败 → 回收可疑僵死进程 pid=${client.pid}`)
882
+ this.killProcess()
883
+ this.client = null
884
+ this.hostToolsRegistered = false
885
+ yield {
886
+ kind: 'error',
887
+ error: {
888
+ code: 'PROMPT_FAILED',
889
+ message: `omp prompt 失败: ${err instanceof Error ? err.message : String(err)}`,
890
+ },
891
+ }
892
+ return
893
+ }
894
+
895
+ // 产出事件直到 done/error
896
+ try {
897
+ while (!isDone || queue.length > 0) {
898
+ if (queue.length === 0 && !isDone) {
899
+ const remaining = idleTimeoutMs - (Date.now() - lastActivity)
900
+ if (remaining <= 0) {
901
+ client.send({ type: 'abort' }).catch(() => {})
902
+ // 子进程大概率已僵死:击杀并清引用,下回合 ensureClient 全新重生
903
+ this.killProcess()
904
+ this.client = null
905
+ this.hostToolsRegistered = false
906
+ enqueue({
907
+ kind: 'error',
908
+ error: {
909
+ code: 'TURN_STALLED',
910
+ message: `回合停滞 ${Math.round(idleTimeoutMs / 1000)}s 无 omp 事件,已中止重置(消息按已处理落账,后续消息继续)`,
911
+ },
912
+ })
913
+ continue
914
+ }
915
+ await new Promise<void>((r) => {
916
+ resolveWait = r
917
+ stallTimer = setTimeout(() => {
918
+ resolveWait = null
919
+ r()
920
+ }, remaining + 100)
921
+ })
922
+ if (stallTimer) {
923
+ clearTimeout(stallTimer)
924
+ stallTimer = null
925
+ }
926
+ }
927
+ while (queue.length > 0) {
928
+ yield queue.shift()!
929
+ }
930
+ }
931
+ }
932
+ finally {
933
+ if (stallTimer) clearTimeout(stallTimer)
934
+ // 事件监听必须全部解除:本 client 跨回合长期复用,残留的 mapOmpEvent
935
+ // 闭包会在后续每一帧继续执行并把事件 push 进孤儿队列(内存/CPU 随
936
+ // 回合数线性劣化,直接违背进程常驻的稳定性目标)
937
+ unsubState()
938
+ unsub()
939
+ signal.removeEventListener('abort', onAbort)
940
+ this.turnActive = false
941
+ this.streaming = false
942
+ this.toolState.currentTaskId = null
943
+ }
944
+ }
945
+
946
+ // ===== 内部:omp 事件 → AgentEvent 映射 =====
947
+
948
+ private mapOmpEvent(event: AgentSessionEvent, taskId: string | undefined): AgentEvent[] {
949
+ switch (event.type) {
950
+ case 'agent_start':
951
+ return [{
952
+ kind: 'status',
953
+ status: { state: 'WORKING', timestamp: new Date().toISOString() },
954
+ }]
955
+ case 'message_end': {
956
+ // 消息完成:如果有 message 内容,产出为 status 事件(任务历史追踪)
957
+ const msg = event.message
958
+ if (msg && Array.isArray(msg.content)) {
959
+ const text = (msg.content as Array<{ type?: string, text?: string }>)
960
+ .filter(c => c.type === 'text')
961
+ .map(c => c.text ?? '')
962
+ .join('')
963
+ if (text) {
964
+ const a2aMsg: A2AMessage = {
965
+ messageId: randomUUID(),
966
+ contextId: this.channelId,
967
+ role: 'ROLE_AGENT',
968
+ parts: [{ text }],
969
+ }
970
+ return [{
971
+ kind: 'status',
972
+ status: { state: 'WORKING', message: a2aMsg, timestamp: new Date().toISOString() },
973
+ }]
974
+ }
975
+ }
976
+ return []
977
+ }
978
+
979
+ case 'tool_execution_start': {
980
+ const toolName = event.toolName ?? 'tool'
981
+ return [{
982
+ kind: 'status',
983
+ status: {
984
+ state: 'WORKING',
985
+ message: {
986
+ messageId: randomUUID(),
987
+ contextId: this.channelId,
988
+ role: 'ROLE_AGENT',
989
+ parts: [{ text: `🔧 ${toolName}${toolArgsPreview(event.args)}` }],
990
+ },
991
+ timestamp: new Date().toISOString(),
992
+ },
993
+ }]
994
+ }
995
+
996
+ case 'agent_end': {
997
+ if (event.isTerminal === false) return []
998
+ const events: AgentEvent[] = []
999
+ // 提取最终 assistant 文本作为 artifact
1000
+ const messages = event.messages ?? []
1001
+ const assistantText = messages
1002
+ .filter(m => m.role === 'assistant')
1003
+ .flatMap(m => m.content.filter(c => c.type === 'text').map(c => c.text ?? ''))
1004
+ .join('')
1005
+ // 回合内的 provider/API 失败(限额 429、鉴权、上游 5xx 等):omp 把错误挂在最后一条
1006
+ // assistant 消息上(stopReason='error' + errorStatus/errorMessage)而非 __error__ 帧。
1007
+ // 不映射出来,上层只能看到「回合结束无产出」,真实原因彻底丢失(且照常重试到耗尽)。
1008
+ const failure = [...messages].reverse().find((m) => {
1009
+ const mm = m as { role?: string, stopReason?: string, errorMessage?: string }
1010
+ return mm.role === 'assistant' && (mm.stopReason === 'error' || typeof mm.errorMessage === 'string')
1011
+ }) as { errorStatus?: number, errorMessage?: string } | undefined
1012
+ if (failure) {
1013
+ const code = failure.errorStatus != null ? `OMP_LLM_${failure.errorStatus}` : 'OMP_LLM_ERROR'
1014
+ const detail = failure.errorMessage ?? 'harness 回合以错误结束(无错误详情)'
1015
+ log.error(`[OmpRpcAgent:${this.selfAgentId}] harness 回合失败 ${code}: ${detail}`)
1016
+ // error 事件即回合终点(队列侧据此收口),不再追加 done
1017
+ return [{ kind: 'error', error: { code, message: detail } }]
1018
+ }
1019
+ if (assistantText) {
1020
+ events.push({
1021
+ kind: 'artifact',
1022
+ artifact: {
1023
+ artifactId: randomUUID(),
1024
+ name: 'output',
1025
+ parts: [{ text: assistantText }],
1026
+ },
1027
+ lastChunk: true,
1028
+ totalChunks: 1,
1029
+ })
1030
+ }
1031
+ events.push({ kind: 'done', final: taskId ? { taskId } : undefined })
1032
+ return events
1033
+ }
1034
+
1035
+ case '__process_exit__':
1036
+ return [{
1037
+ kind: 'error',
1038
+ error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
1039
+ }]
1040
+
1041
+ // ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
1042
+ case 'compaction_start':
1043
+ this.compacting = true
1044
+ return []
1045
+
1046
+ case 'compaction_end': {
1047
+ this.compacting = false
1048
+ const result = (event as { result?: CompactionResult }).result
1049
+ if (result?.summary) {
1050
+ void this.harvestCompaction(
1051
+ result.summary,
1052
+ result.tokensBefore,
1053
+ result.tokensAfter ?? result.estimatedTokensAfter,
1054
+ (event as { reason?: string }).reason,
1055
+ )
1056
+ }
1057
+ return []
1058
+ }
1059
+
1060
+ // 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
1061
+ case 'agent_settled':
1062
+ return []
1063
+
1064
+ case '__error__':
1065
+ return [{
1066
+ kind: 'error',
1067
+ error: { code: 'OMP_ERROR', message: (event as { error?: string }).error ?? 'omp 未知错误' },
1068
+ }]
1069
+
1070
+ default:
1071
+ return []
1072
+ }
1073
+ }
1074
+
1075
+ // ===== 内部:omp 客户端管理 =====
1076
+
1077
+ private async ensureClient(ctx: AgentRunContext): Promise<void> {
1078
+ if (!this.workspace) {
1079
+ this.workspace = ctx.workspace
1080
+ }
1081
+ if (!this.agentInfo) {
1082
+ // init() 可能未调用;从 ctx 推断
1083
+ this.channelId = ctx.channelId
1084
+ this.agentRole = ctx.role
1085
+ this.bridgeCtx.identity = { agentId: this.selfAgentId, channelId: this.channelId, role: this.agentRole, name: this.agentName }
1086
+ }
1087
+
1088
+ if (!this.client || !this.client.alive) {
1089
+ // 旧客户端已退出(exit 事件/OS 存活校准 reconcile 触发)——必须丢弃并重生,
1090
+ // 否则后续回合会一直对着死 stdio 报 PROMPT_FAILED 烧重试配额
1091
+ this.client = null
1092
+ this.hostToolsRegistered = false
1093
+ const command = this.config.command ?? 'omp'
1094
+ const client = new OmpRpcClient({
1095
+ command,
1096
+ mode: this.rpcMode,
1097
+ args: [...(this.config.thinkingLevel ? ['--thinking', this.config.thinkingLevel] : [])],
1098
+ args: this.config.args,
1099
+ cwd: this.config.cwd ?? process.cwd(),
1100
+ // 进程退出 → 注册表标记(供运行时资源监控);pid=-1 表示无法取得,忽略
1101
+ onExit: (pid, code) => {
1102
+ if (pid > 0) {
1103
+ markHarnessProcessExit(pid, code)
1104
+ markTerminalSessionExit(pid, code)
1105
+ }
1106
+ },
1107
+ })
1108
+ await client.start()
1109
+
1110
+ // 登记 + 绑定 agent 身份(harness 进程监控的事实源)
1111
+ const pid = client.pid
1112
+ if (pid) {
1113
+ registerHarnessProcess(pid, {
1114
+ harness: 'omp',
1115
+ command,
1116
+ args: ['--mode', this.rpcMode, ...(this.config.args ?? [])],
1117
+ })
1118
+ bindHarnessProcess(pid, {
1119
+ agentId: this.selfAgentId,
1120
+ channelId: this.channelId,
1121
+ name: this.agentName,
1122
+ role: this.agentRole,
1123
+ })
1124
+ // 终端镜像 tap:全部 RPC 帧 → /monitor 终端(实时 TUI 渲染 + HITL)
1125
+ attachTerminalTap(client, {
1126
+ pid,
1127
+ harness: 'omp',
1128
+ agentId: this.selfAgentId,
1129
+ channelId: this.channelId,
1130
+ name: this.agentName,
1131
+ role: this.agentRole,
1132
+ })
1133
+ }
1134
+
1135
+ // 设置模型(如果配置了 provider/model 任一;omp 以当前已设值为缺省补全,
1136
+ // 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
1137
+ if (this.config.provider || this.config.model) {
1138
+ try {
1139
+ // 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
1140
+ await client.send({
1141
+ type: 'set_model',
1142
+ provider: this.config.provider as string,
1143
+ modelId: this.config.model as string,
1144
+ })
1145
+ }
1146
+ catch {
1147
+ // 模型设置失败不致命(用 omp 默认模型)
1148
+ }
1149
+ }
1150
+
1151
+ // 注册 host tools(按角色差异化:lead 全量,worker 剔除调度/团队管理专属工具)
1152
+ client.onHostToolCall(req => this.handleHostTool(req))
1153
+ await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole, this.channelId) })
1154
+
1155
+ // 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
1156
+ void this.probeContext(client).catch(() => {})
1157
+ void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
1158
+
1159
+ this.client = client
1160
+ this.hostToolsRegistered = true
1161
+ }
1162
+ }
1163
+
1164
+ /** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
1165
+ private async probeContext(client: OmpRpcClient): Promise<void> {
1166
+ try {
1167
+ const resp = await client.send({ type: 'get_state' })
1168
+ const data = (resp.data ?? {}) as Record<string, unknown>
1169
+ const model = data.model as { contextWindow?: number } | undefined
1170
+ if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
1171
+ this.contextWindow = model.contextWindow
1172
+ client.setContextWindow(model.contextWindow)
1173
+ }
1174
+ if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
1175
+ }
1176
+ catch { /* get_state 不可用:仅被动 usage */ }
1177
+ }
1178
+ }