agentworkshop 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/.AgentWorkShop/prompts/host-tools.json +123 -0
  2. package/.output/.AgentWorkShop/prompts/host-tools.json +123 -0
  3. package/.output/nitro.json +1 -1
  4. package/.output/public/_nuxt/{DJJDigim2.js → -FhgRqJl2.js} +1 -1
  5. package/.output/public/_nuxt/{CGk_VUhO2.js → 0qvcO_KM2.js} +1 -1
  6. package/.output/public/_nuxt/26OL4Ao82.js +1 -0
  7. package/.output/public/_nuxt/{sBDZymdy2.js → 2Kwo_j9G2.js} +1 -1
  8. package/.output/public/_nuxt/{BqYVLAcm.js → 2v5GdspM.js} +1 -1
  9. package/.output/public/_nuxt/{DkQBMVTs2.js → 8M7wFbEN2.js} +1 -1
  10. package/.output/public/_nuxt/{DtiY69xi2.js → 8SZLbrI42.js} +1 -1
  11. package/.output/public/_nuxt/{DF32f5EL2.js → B2NbzUau2.js} +1 -1
  12. package/.output/public/_nuxt/{CwZW1ZCe2.js → B4JlRHBB2.js} +1 -1
  13. package/.output/public/_nuxt/{BQOn0q6Y.js → B6C4WofF.js} +1 -1
  14. package/.output/public/_nuxt/{BC7W6lNm.js → B6UwtJ1a.js} +1 -1
  15. package/.output/public/_nuxt/{BfQ-ptH0.js → B8HFUsMH.js} +1 -1
  16. package/.output/public/_nuxt/{DzBbYWrw2.js → BBB_j2LU2.js} +1 -1
  17. package/.output/public/_nuxt/BCLUwdP52.js +1 -0
  18. package/.output/public/_nuxt/{CLxmY3Rh.js → BK4wbfGS.js} +1 -1
  19. package/.output/public/_nuxt/{BRMVDfPl2.js → BKidykmV2.js} +1 -1
  20. package/.output/public/_nuxt/BPUQZ9Lq2.js +8 -0
  21. package/.output/public/_nuxt/BRYBzVMJ.js +1 -0
  22. package/.output/public/_nuxt/{Bj5sjA5O.js → BRci9D_l.js} +1 -1
  23. package/.output/public/_nuxt/{Dmgqz2np2.js → BVGzwAxr2.js} +1 -1
  24. package/.output/public/_nuxt/BVg2R2VB2.js +1 -0
  25. package/.output/public/_nuxt/{C3aUN4Vp2.js → BW2uTUx52.js} +1 -1
  26. package/.output/public/_nuxt/{B5FKaMM02.js → BY3Lbz3h2.js} +1 -1
  27. package/.output/public/_nuxt/{DIwmMa_7.js → BYzim_Bn.js} +1 -1
  28. package/.output/public/_nuxt/{D8xUsQGt.js → B_Obcbiu.js} +1 -1
  29. package/.output/public/_nuxt/BaYDgyaR.js +3 -0
  30. package/.output/public/_nuxt/{DZJ11BTI.js → BbACaREb.js} +1 -1
  31. package/.output/public/_nuxt/{BJrj5JVe2.js → BcosvNXb2.js} +1 -1
  32. package/.output/public/_nuxt/{B_-vAvzb.js → BfeXib9G.js} +1 -1
  33. package/.output/public/_nuxt/{CeLqojCJ.js → BfgTar9L.js} +1 -1
  34. package/.output/public/_nuxt/BfvuWqSa.js +1 -0
  35. package/.output/public/_nuxt/{D8r8PNQa2.js → BiZWwdcD2.js} +1 -1
  36. package/.output/public/_nuxt/{DIFkTkdS.js → BkEokGX5.js} +1 -1
  37. package/.output/public/_nuxt/{CJSohaqk2.js → BklGqEto2.js} +2 -2
  38. package/.output/public/_nuxt/{DkpmSnHX.js → Bko5adbu.js} +1 -1
  39. package/.output/public/_nuxt/{DTQTGA6v2.js → BmBlIIJo2.js} +1 -1
  40. package/.output/public/_nuxt/{D_NjL-Hz.js → Bmvt9ydE.js} +1 -1
  41. package/.output/public/_nuxt/{Cp4r6Eiz.js → BoyuIoSm.js} +1 -1
  42. package/.output/public/_nuxt/{62J4vljm.js → Bq-tmkst.js} +1 -1
  43. package/.output/public/_nuxt/{BMMoqi94.js → BqFaZ4kS.js} +1 -1
  44. package/.output/public/_nuxt/BsJYieAL2.js +1 -0
  45. package/.output/public/_nuxt/{DD4J2Otc.js → BtZKe9ZV.js} +1 -1
  46. package/.output/public/_nuxt/{BnpY_zNx.js → BtumQhZK.js} +1 -1
  47. package/.output/public/_nuxt/{BjLzqWJH2.js → BwWs9EdO2.js} +1 -1
  48. package/.output/public/_nuxt/{Bx2a9cuY2.js → BxxpJxZf2.js} +1 -1
  49. package/.output/public/_nuxt/{qRXa-PM22.js → ByTnUqFw2.js} +1 -1
  50. package/.output/public/_nuxt/{DvOETuoT2.js → Byiiiwc72.js} +1 -1
  51. package/.output/public/_nuxt/{Car2lkbb.js → C1-SuwUN.js} +1 -1
  52. package/.output/public/_nuxt/{Dg81dOEO.js → C14pM5G4.js} +1 -1
  53. package/.output/public/_nuxt/{MDhaRQq_.js → C2Yp-B8m.js} +1 -1
  54. package/.output/public/_nuxt/{BJ2nJNlK2.js → C4e6_nom2.js} +1 -1
  55. package/.output/public/_nuxt/{CiCEprsp2.js → C5OStZJ62.js} +1 -1
  56. package/.output/public/_nuxt/{ByMj6e7s.js → C6lssmvl.js} +2 -2
  57. package/.output/public/_nuxt/{C6uBP0Xa.js → CC-GEjfN.js} +1 -1
  58. package/.output/public/_nuxt/CC2Pv8VL2.js +1 -0
  59. package/.output/public/_nuxt/{DKiqdYnj.js → CCcMgxV9.js} +1 -1
  60. package/.output/public/_nuxt/{5S26I34j2.js → CDL30A2z2.js} +1 -1
  61. package/.output/public/_nuxt/{B8C-QJB5.js → CDYLasP8.js} +1 -1
  62. package/.output/public/_nuxt/CElzwyXQ.js +1 -0
  63. package/.output/public/_nuxt/{2KE8xGVm.js → CEvJu-xI.js} +1 -1
  64. package/.output/public/_nuxt/{CR612R752.js → CFQRFctQ2.js} +1 -1
  65. package/.output/public/_nuxt/{DnzjxC0u.js → CHk9XxNi.js} +2 -2
  66. package/.output/public/_nuxt/{Bx4MR0jd.js → CLFmtXU7.js} +1 -1
  67. package/.output/public/_nuxt/{DW8k_0dQ.js → CMJi6yYS.js} +1 -1
  68. package/.output/public/_nuxt/{DD-r0S1p.js → CPvFGbr8.js} +1 -1
  69. package/.output/public/_nuxt/{CCgbgkid.js → CQSxS_Ek.js} +2 -2
  70. package/.output/public/_nuxt/{jGxHnjW6.js → CQuo31jF.js} +1 -1
  71. package/.output/public/_nuxt/{SoO2K-di.js → CRCrWy-6.js} +1 -1
  72. package/.output/public/_nuxt/CUCFFL9m.js +84 -0
  73. package/.output/public/_nuxt/CZYNy60j2.js +1 -0
  74. package/.output/public/_nuxt/{WPwdj9pg.js → CZsFf7ck.js} +1 -1
  75. package/.output/public/_nuxt/CfFUK6932.js +2 -0
  76. package/.output/public/_nuxt/{DAsjfdZD.js → CfIbFnJz.js} +1 -1
  77. package/.output/public/_nuxt/{Tk6TQZjU.js → CjF4QY9h.js} +1 -1
  78. package/.output/public/_nuxt/{DlUQa0wn.js → Cjo1t2IF.js} +1 -1
  79. package/.output/public/_nuxt/{CDn0eDn8.js → CmuGMxDP.js} +1 -1
  80. package/.output/public/_nuxt/Cn6EojFe.js +1 -0
  81. package/.output/public/_nuxt/{DLqSQbGv.js → CnYazk8d.js} +1 -1
  82. package/.output/public/_nuxt/{DQfy6R_2.js → CnrGVJIr.js} +1 -1
  83. package/.output/public/_nuxt/{C0YL0HKK.js → CtcBhkci.js} +1 -1
  84. package/.output/public/_nuxt/{h5gsh8E92.js → Cv-Mv7_h2.js} +1 -1
  85. package/.output/public/_nuxt/{vPcnBJsa2.js → CzlZrfq82.js} +1 -1
  86. package/.output/public/_nuxt/{C2YYOgMe.js → CznnUTZx.js} +2 -2
  87. package/.output/public/_nuxt/{csitHKnE2.js → D-80EfyA2.js} +1 -1
  88. package/.output/public/_nuxt/{BRdoISnJ2.js → D1C6qtS12.js} +1 -1
  89. package/.output/public/_nuxt/{Cnrmlixo.js → D1MkndMZ.js} +2 -2
  90. package/.output/public/_nuxt/{BLwDhlaa.js → D37gODe0.js} +1 -1
  91. package/.output/public/_nuxt/{NXY37IO62.js → D6FhDrYe2.js} +1 -1
  92. package/.output/public/_nuxt/{Btx8wL4-.js → D7TZLu4F.js} +1 -1
  93. package/.output/public/_nuxt/{qoUdVDnC.js → D7tEwf-o.js} +3 -3
  94. package/.output/public/_nuxt/D8HQPtm62.js +1 -0
  95. package/.output/public/_nuxt/D9O4L4jK.js +1 -0
  96. package/.output/public/_nuxt/{aQ1B7BZa2.js → D9lL3fJM2.js} +2 -2
  97. package/.output/public/_nuxt/{CT-vTjcG2.js → DC-2eKo62.js} +1 -1
  98. package/.output/public/_nuxt/DF9ETYJ32.js +1 -0
  99. package/.output/public/_nuxt/DGGBrc0q2.js +1 -0
  100. package/.output/public/_nuxt/DGu_fna7.js +1 -0
  101. package/.output/public/_nuxt/{BupBjt6B.js → DKOvbitt.js} +1 -1
  102. package/.output/public/_nuxt/{B1PzYsqN.js → DNxudyND.js} +1 -1
  103. package/.output/public/_nuxt/{BfX6_AjY.js → DOT-XQyi.js} +1 -1
  104. package/.output/public/_nuxt/{BsTZnkVK.js → DPPC9Teq.js} +1 -1
  105. package/.output/public/_nuxt/{HxohXQ_r.js → DRskyE7l.js} +1 -1
  106. package/.output/public/_nuxt/{B-U7E_Mc2.js → DW4ZgInh2.js} +1 -1
  107. package/.output/public/_nuxt/{BErG4kzc2.js → DWe6BMlp2.js} +1 -1
  108. package/.output/public/_nuxt/{BH8RvpVy.js → DWy0AjON.js} +1 -1
  109. package/.output/public/_nuxt/{Bc_k6AVA.js → DXgw1dVL.js} +4 -4
  110. package/.output/public/_nuxt/{C3sWJ8SD2.js → DZsKIzKD2.js} +1 -1
  111. package/.output/public/_nuxt/D_59BNP1.js +14 -0
  112. package/.output/public/_nuxt/{BQIO4MkA2.js → D_FZ3KZI2.js} +1 -1
  113. package/.output/public/_nuxt/{n_Njdwii2.js → D__vJGHF2.js} +1 -1
  114. package/.output/public/_nuxt/{C9gErLQc2.js → DaizNGUm2.js} +1 -1
  115. package/.output/public/_nuxt/{C-MY-JQE.js → DdyE0ylS.js} +1 -1
  116. package/.output/public/_nuxt/DhLlTxQh2.js +1 -0
  117. package/.output/public/_nuxt/{CxVhG0Xx.js → DhW7cFOa.js} +1 -1
  118. package/.output/public/_nuxt/{Rt2i6k7C2.js → DhjgWrB62.js} +1 -1
  119. package/.output/public/_nuxt/{KwT4sN_d.js → DiWcX4gj.js} +1 -1
  120. package/.output/public/_nuxt/DiloA8vK.js +1 -0
  121. package/.output/public/_nuxt/{DX-c5laM2.js → DlS-SkQ42.js} +1 -1
  122. package/.output/public/_nuxt/{D_Zspo1y.js → DmHp9yQ3.js} +1 -1
  123. package/.output/public/_nuxt/{dq1HKoGD.js → DoL9u6wu.js} +1 -1
  124. package/.output/public/_nuxt/{CIvMT5Ic.js → DqxBCeDe.js} +1 -1
  125. package/.output/public/_nuxt/{BnNmkGJS2.js → DrlnrdvY2.js} +3 -3
  126. package/.output/public/_nuxt/{BAcjVcku2.js → Dt8TL8_X2.js} +1 -1
  127. package/.output/public/_nuxt/{BtfwbNC0.js → Du7gS8RU.js} +1 -1
  128. package/.output/public/_nuxt/DuUc4bbY2.js +1 -0
  129. package/.output/public/_nuxt/{DPUXbTg9.js → DudoisLM.js} +1 -1
  130. package/.output/public/_nuxt/{Ce5R-ZGf2.js → DwgVlehj2.js} +1 -1
  131. package/.output/public/_nuxt/Dyxf2xZw.js +1 -0
  132. package/.output/public/_nuxt/Dz9C2pTa.js +1 -0
  133. package/.output/public/_nuxt/{BYxG9Hb2.js → EMbqTHfT.js} +1 -1
  134. package/.output/public/_nuxt/{B1SzppY12.js → FsRVXpyF2.js} +1 -1
  135. package/.output/public/_nuxt/{0MLy6ogg2.js → GQrEW8nk2.js} +1 -1
  136. package/.output/public/_nuxt/{Tuz5LcLM.js → IXR1BWP5.js} +1 -1
  137. package/.output/public/_nuxt/{-jpUnM1I2.js → Iub61dX92.js} +1 -1
  138. package/.output/public/_nuxt/{CQgOzpvI.js → JegDslce.js} +2 -2
  139. package/.output/public/_nuxt/{DrF3TWoj.js → KzK37yIr.js} +1 -1
  140. package/.output/public/_nuxt/{Br3MggOL.js → N-CCmCsX.js} +1 -1
  141. package/.output/public/_nuxt/{DHhmq7s_.js → NfvZ5Lzv.js} +1 -1
  142. package/.output/public/_nuxt/{CzX_kaPL.js → POTcePF_.js} +1 -1
  143. package/.output/public/_nuxt/{D7s96J3r2.js → Q-JpBT032.js} +1 -1
  144. package/.output/public/_nuxt/{DzwIZ81f2.js → R1g64DNi2.js} +1 -1
  145. package/.output/public/_nuxt/{JgheoP3e.js → R5Pz7BqB.js} +1 -1
  146. package/.output/public/_nuxt/{yLyz0-Yz.js → SvjzWbn8.js} +1 -1
  147. package/.output/public/_nuxt/{Dd7MZc_Q.js → XqXoaoDs.js} +1 -1
  148. package/.output/public/_nuxt/{B6jtLy6z.js → YAA98rWJ.js} +11 -11
  149. package/.output/public/_nuxt/{HX4_xnak2.js → YLYSczSq2.js} +1 -1
  150. package/.output/public/_nuxt/{BBw9ylUY2.js → YNhAoK-R2.js} +1 -1
  151. package/.output/public/_nuxt/Zkbt8Uk5.js +1 -0
  152. package/.output/public/_nuxt/aml.PalMp-5n.css +1 -0
  153. package/.output/public/_nuxt/{C41CcsiD.js → b7UcqLKi.js} +1 -1
  154. package/.output/public/_nuxt/builds/latest.json +1 -1
  155. package/.output/public/_nuxt/builds/meta/fa0af8b5-3929-428d-a6de-3b5cc6b8248f.json +1 -0
  156. package/.output/public/_nuxt/{DVkFSOZi.js → cKj4vSo5.js} +1 -1
  157. package/.output/public/_nuxt/{r1CpGsFp.js → dHwWyr6b.js} +1 -1
  158. package/.output/public/_nuxt/{DlcA9Gxl.js → eePbiWEh.js} +1 -1
  159. package/.output/public/_nuxt/{cjRzSvAg2.js → eymRov9Y2.js} +1 -1
  160. package/.output/public/_nuxt/{B8cViLJT.js → geMze5qw.js} +1 -1
  161. package/.output/public/_nuxt/{uu1wfdfR2.js → gh-WMxV_2.js} +1 -1
  162. package/.output/public/_nuxt/{Ce6e1mPk2.js → hCN555XP2.js} +1 -1
  163. package/.output/public/_nuxt/{rdUE8Khf.js → hZG5MLjK.js} +3 -3
  164. package/.output/public/_nuxt/{CcX_jP-d.js → hlfahYxK.js} +1 -1
  165. package/.output/public/_nuxt/{cGSl2GZp2.js → ilJHF1nA2.js} +1 -1
  166. package/.output/public/_nuxt/{D6ZcgHPz.js → jDuiDtGb.js} +1 -1
  167. package/.output/public/_nuxt/{DbeNMehP.js → k9b520K-.js} +1 -1
  168. package/.output/public/_nuxt/ontology.B_3Wn907.css +1 -0
  169. package/.output/public/_nuxt/optimizer.CLA8I1sy.css +1 -0
  170. package/.output/public/_nuxt/{DSORcpTJ.js → rDvMijKA.js} +2 -2
  171. package/.output/public/_nuxt/{CAiHhX8P2.js → rF7mggl72.js} +2 -2
  172. package/.output/public/_nuxt/ri9UF8rk.js +2 -0
  173. package/.output/public/_nuxt/{KgnxOsMr.js → toG_qAMH.js} +1 -1
  174. package/.output/public/_nuxt/{KQZmHR_K.js → u3h4pGpm.js} +2 -2
  175. package/.output/public/_nuxt/{dJ7gK7C2.js → uomnHjhM.js} +1 -1
  176. package/.output/public/_nuxt/{BUTg4W_x2.js → v77qozuJ2.js} +1 -1
  177. package/.output/public/_nuxt/{BcEr7tIB.js → v7cV7hJa.js} +1 -1
  178. package/.output/public/_nuxt/{D25dVZ822.js → vkQoYNQR2.js} +2 -2
  179. package/.output/public/_nuxt/{wR_w07322.js → vp3LtnTz2.js} +1 -1
  180. package/.output/public/_nuxt/{Bnc8HELj2.js → we7-sMSo2.js} +1 -1
  181. package/.output/public/_nuxt/{kN3ZzR1-.js → wxISMS1i.js} +1 -1
  182. package/.output/public/_nuxt/{DGrrucbG.js → zy7efYKR.js} +1 -1
  183. package/.output/server/chunks/_/data-export.mjs +5 -5
  184. package/.output/server/chunks/_/data-query.mjs +2 -2
  185. package/.output/server/chunks/_/eda-run.repo.mjs +2 -2
  186. package/.output/server/chunks/_/features.mjs +4 -4
  187. package/.output/server/chunks/_/golden.mjs +2 -2
  188. package/.output/server/chunks/_/index.mjs +1 -1
  189. package/.output/server/chunks/_/mes-console.mjs +1 -1
  190. package/.output/server/chunks/_/nitro.mjs +2149 -1520
  191. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  192. package/.output/server/chunks/_/spc.mjs +1 -1
  193. package/.output/server/chunks/build/TownView-BWMDbosY.mjs +1 -1
  194. package/.output/server/chunks/build/{_wsId_-BNKrrV1H.mjs → _wsId_-BPy-Aj9L.mjs} +3 -3
  195. package/.output/server/chunks/build/{_wsId_-BNKrrV1H.mjs.map → _wsId_-BPy-Aj9L.mjs.map} +1 -1
  196. package/.output/server/chunks/build/{agents-8tV9xL97.mjs → agents-CpNLdI8E.mjs} +6 -6
  197. package/.output/server/chunks/build/{agents-8tV9xL97.mjs.map → agents-CpNLdI8E.mjs.map} +1 -1
  198. package/.output/server/chunks/build/{aml-BNTEDOsl.mjs → aml-DRAFRcGU.mjs} +15 -12
  199. package/.output/server/chunks/build/{aml-BNTEDOsl.mjs.map → aml-DRAFRcGU.mjs.map} +1 -1
  200. package/.output/server/chunks/build/{channel-templates-Iot9Y3vV.mjs → channel-templates-ByHOQQkO.mjs} +2 -2
  201. package/.output/server/chunks/build/{channel-templates-Iot9Y3vV.mjs.map → channel-templates-ByHOQQkO.mjs.map} +1 -1
  202. package/.output/server/chunks/build/daq-CdxcfY_G.mjs +1 -1
  203. package/.output/server/chunks/build/dcw-BlDQtS4q.mjs +1 -1
  204. package/.output/server/chunks/build/{default-CJkOcmrm.mjs → default-CNWS6NUw.mjs} +4 -4
  205. package/.output/server/chunks/build/{default-CJkOcmrm.mjs.map → default-CNWS6NUw.mjs.map} +1 -1
  206. package/.output/server/chunks/build/{dropdown-CjwsxU39.mjs → dropdown-BNwTVGde.mjs} +2 -2
  207. package/.output/server/chunks/build/{dropdown-CjwsxU39.mjs.map → dropdown-BNwTVGde.mjs.map} +1 -1
  208. package/.output/server/chunks/build/{dropdown-C6YH_kSO.mjs → dropdown-Cd-xPhR6.mjs} +2 -2
  209. package/.output/server/chunks/build/{dropdown-C6YH_kSO.mjs.map → dropdown-Cd-xPhR6.mjs.map} +1 -1
  210. package/.output/server/chunks/build/input-number-BNlNDK7H.mjs +1 -1
  211. package/.output/server/chunks/build/{logs-BTfJQ6vQ.mjs → logs-pRb-TMWa.mjs} +3 -3
  212. package/.output/server/chunks/build/{logs-BTfJQ6vQ.mjs.map → logs-pRb-TMWa.mjs.map} +1 -1
  213. package/.output/server/chunks/build/{monitor-DvaR3yo8.mjs → monitor-Cq6hRFeQ.mjs} +6 -6
  214. package/.output/server/chunks/build/{monitor-DvaR3yo8.mjs.map → monitor-Cq6hRFeQ.mjs.map} +1 -1
  215. package/.output/server/chunks/build/{ontology-C3HS5pMo.mjs → ontology-DK0Mf0zT.mjs} +510 -81
  216. package/.output/server/chunks/build/ontology-DK0Mf0zT.mjs.map +1 -0
  217. package/.output/server/chunks/build/{operations-Cl8nVm9D.mjs → operations-SMQrRAcr.mjs} +3 -3
  218. package/.output/server/chunks/build/{operations-Cl8nVm9D.mjs.map → operations-SMQrRAcr.mjs.map} +1 -1
  219. package/.output/server/chunks/build/optimizer-BlWTsM-W.mjs +202 -0
  220. package/.output/server/chunks/build/optimizer-BlWTsM-W.mjs.map +1 -0
  221. package/.output/server/chunks/build/{pagination-KaIvHBzi.mjs → pagination-DTAconUo.mjs} +2 -2
  222. package/.output/server/chunks/build/{pagination-KaIvHBzi.mjs.map → pagination-DTAconUo.mjs.map} +1 -1
  223. package/.output/server/chunks/build/{permissions-CVTfixZj.mjs → permissions-DgXQZSyx.mjs} +6 -6
  224. package/.output/server/chunks/build/{permissions-CVTfixZj.mjs.map → permissions-DgXQZSyx.mjs.map} +1 -1
  225. package/.output/server/chunks/build/{schedules-BLhn2CLn.mjs → schedules-dSnr8url.mjs} +6 -6
  226. package/.output/server/chunks/build/{schedules-BLhn2CLn.mjs.map → schedules-dSnr8url.mjs.map} +1 -1
  227. package/.output/server/chunks/build/{select--XFl7CtY.mjs → select-B3Dsb9yq.mjs} +3 -3
  228. package/.output/server/chunks/build/{select--XFl7CtY.mjs.map → select-B3Dsb9yq.mjs.map} +1 -1
  229. package/.output/server/chunks/build/{settings-DIf_iYdu.mjs → settings-BrIhnS2Z.mjs} +2 -2
  230. package/.output/server/chunks/build/{settings-DIf_iYdu.mjs.map → settings-BrIhnS2Z.mjs.map} +1 -1
  231. package/.output/server/chunks/build/{table-DIu_YCJN.mjs → table-CmVD-cA9.mjs} +4 -4
  232. package/.output/server/chunks/build/{table-DIu_YCJN.mjs.map → table-CmVD-cA9.mjs.map} +1 -1
  233. package/.output/server/chunks/build/{teams-B2J6ouRY.mjs → teams-B1d32eHK.mjs} +4 -4
  234. package/.output/server/chunks/build/{teams-B2J6ouRY.mjs.map → teams-B1d32eHK.mjs.map} +1 -1
  235. package/.output/server/chunks/build/{tokens-DFQGaN2n.mjs → tokens-GYaGJ35Z.mjs} +6 -6
  236. package/.output/server/chunks/build/{tokens-DFQGaN2n.mjs.map → tokens-GYaGJ35Z.mjs.map} +1 -1
  237. package/.output/server/chunks/build/{tools-DWJFnU_t.mjs → tools-s_9U1Djj.mjs} +6 -6
  238. package/.output/server/chunks/build/{tools-DWJFnU_t.mjs.map → tools-s_9U1Djj.mjs.map} +1 -1
  239. package/.output/server/chunks/build/types-D6J7slEa.mjs +1 -1
  240. package/.output/server/chunks/build/useDaqAddNode-D_Rkju0f.mjs +1 -1
  241. package/.output/server/chunks/build/useDaqDetailNode-DeOsDxK5.mjs +1 -1
  242. package/.output/server/chunks/build/useDaqDetailParams-Cm9a9r5G.mjs +1 -1
  243. package/.output/server/chunks/build/useDaqFilters-CctmO03p.mjs +1 -1
  244. package/.output/server/chunks/build/useDaqStream-B9ZBVQrI.mjs +1 -1
  245. package/.output/server/chunks/build/useDaqTemplates-CK37sMfi.mjs +1 -1
  246. package/.output/server/chunks/build/useDcwAddNode-DACVcEHs.mjs +1 -1
  247. package/.output/server/chunks/build/useDcwStream-Dir5ufAU.mjs +1 -1
  248. package/.output/server/chunks/build/{users-BWw0VCr9.mjs → users-DjiVC_tS.mjs} +6 -6
  249. package/.output/server/chunks/build/{users-BWw0VCr9.mjs.map → users-DjiVC_tS.mjs.map} +1 -1
  250. package/.output/server/chunks/routes/api/mcp/workshop.mjs +1 -1
  251. package/.output/server/chunks/routes/api/system/backup/run.post.mjs +2 -2
  252. package/.output/server/chunks/routes/api/workshop/aml/models/_id_.patch.mjs +7 -1
  253. package/.output/server/chunks/routes/api/workshop/aml/models/_id_.patch.mjs.map +1 -1
  254. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  255. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +6 -6
  256. package/.output/server/chunks/routes/api/workshop/daq/_id/ingest.post.mjs +2 -2
  257. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  258. package/.output/server/chunks/routes/api/workshop/index.mjs +1 -1
  259. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  260. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/autofix.mjs +52 -0
  261. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/autofix.mjs.map +1 -0
  262. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/compile.mjs +1 -1
  263. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/evidence.mjs +1 -1
  264. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/publish.mjs +1 -1
  265. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval-decision.mjs +1 -1
  266. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/approval.mjs +1 -1
  267. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/rules/_ruleId/transition.mjs +1 -1
  268. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/subgraph.mjs +38 -0
  269. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/subgraph.mjs.map +1 -0
  270. package/.output/server/chunks/routes/api/workshop/ontology/drafts/_draftId/validate.mjs +1 -1
  271. package/.output/server/chunks/routes/api/workshop/ontology/export/_ontologyId_.mjs +1 -1
  272. package/.output/server/chunks/routes/api/workshop/ontology/index.mjs +1 -1
  273. package/.output/server/chunks/routes/api/workshop/ontology/scene.mjs +8 -1
  274. package/.output/server/chunks/routes/api/workshop/ontology/scene.mjs.map +1 -1
  275. package/.output/server/chunks/routes/api/workshop/ontology/traces/_ontologyId_.mjs +1 -1
  276. package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId/compare.mjs +1 -1
  277. package/.output/server/chunks/routes/api/workshop/ontology/versions/_ontologyId_.mjs +1 -1
  278. package/.output/server/chunks/routes/api/workshop/optimization/capabilities.get.mjs +1 -1
  279. package/.output/server/chunks/routes/api/workshop/optimization/index.mjs +1 -1
  280. package/.output/server/chunks/routes/api/workshop/optimization/index2.mjs +1 -1
  281. package/.output/server/chunks/routes/api/workshop/optimization/profiles/_id/publish.post.mjs +1 -1
  282. package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/crosscheck.post.mjs +1 -1
  283. package/.output/server/chunks/routes/api/workshop/optimization/recommendations/_id/dispatch.post.mjs +1 -1
  284. package/.output/server/chunks/routes/api/workshop/optimization/solve.post.mjs +1 -1
  285. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  286. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  287. package/.output/server/chunks/routes/api/workshop/process-model/datasets.post.mjs +1 -1
  288. package/.output/server/chunks/routes/api/workshop/process-model/eligibility.mjs +1 -1
  289. package/.output/server/chunks/routes/api/workshop/process-model/index.mjs +1 -1
  290. package/.output/server/chunks/routes/api/workshop/process-model/specs/_id/publish.post.mjs +1 -1
  291. package/.output/server/chunks/routes/api/workshop/process-model/train.post.mjs +1 -1
  292. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  293. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  294. package/.output/server/chunks/routes/api/workshop/tools/sync.post.mjs +1 -1
  295. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  296. package/.output/server/chunks/routes/renderer.mjs +2 -2
  297. package/.output/server/chunks/virtual/entry.mjs +92 -18
  298. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  299. package/.output/server/package.json +1 -1
  300. package/README-zh.md +5 -4
  301. package/README.md +6 -5
  302. package/app/components/aml/AmlModelsTable.vue +5 -0
  303. package/app/components/ontology/OntologyGraph.vue +356 -0
  304. package/app/pages/aml/styles.css +1 -0
  305. package/app/pages/ontology/index.vue +318 -35
  306. package/app/pages/ontology/types.ts +42 -0
  307. package/app/pages/optimizer/index.vue +21 -4
  308. package/config.yml +1 -1
  309. package/docs/cli.en.md +2 -2
  310. package/docs/cli.md +2 -2
  311. package/docs/plugins.en.md +1 -1
  312. package/docs/plugins.md +1 -1
  313. package/docs/sdk.en.md +1 -1
  314. package/docs/sdk.md +1 -1
  315. package/i18n/locales/en.ts +26 -0
  316. package/i18n/locales/zh-CN.ts +26 -0
  317. package/mcp/aw-mcp-server.mjs +20 -2
  318. package/package.json +1 -1
  319. package/scripts/e2e-direct-rest-simulator-paper.mjs +624 -0
  320. package/scripts/e2e-idd-results.mjs +177 -0
  321. package/scripts/e2e-model-loop-live.mjs +58 -4
  322. package/scripts/e2e-ontology-live.mjs +138 -3
  323. package/scripts/e2e-ontology.mjs +393 -328
  324. package/scripts/e2e-pi-simulator-paper.mjs +2316 -0
  325. package/scripts/e2e-registered-harness-simulator-paper.mjs +1926 -0
  326. package/scripts/e2e-simulator-optimizer-paper.mjs +264 -0
  327. package/scripts/fixtures/omp-paper-harness.mjs +163 -0
  328. package/sdk/context.mjs +6 -0
  329. package/sdk/index.d.mts +2 -0
  330. package/server/api/workshop/aml/models/[id].patch.ts +36 -30
  331. package/server/api/workshop/ontology/drafts/[draftId]/autofix.ts +31 -0
  332. package/server/api/workshop/ontology/drafts/[draftId]/subgraph.ts +24 -0
  333. package/server/api/workshop/ontology/scene.ts +14 -1
  334. package/server/plugins-builtin/diag-bridge/client.mjs +390 -239
  335. package/server/plugins-builtin/diag-bridge/host/helpers.mjs +80 -0
  336. package/server/plugins-builtin/diag-bridge/host/plugin.mjs +264 -2
  337. package/server/plugins-builtin/diag-bridge/i18n.json +84 -4
  338. package/server/services/workshop/agents/adapters/omp-rpc-client/frames.ts +129 -111
  339. package/server/services/workshop/agents/host-tool-bridge/tools/industrial.ts +6 -3
  340. package/server/services/workshop/agents/industrial/aml-job-tools.ts +602 -569
  341. package/server/services/workshop/agents/industrial/grant-guard.ts +15 -1
  342. package/server/services/workshop/agents/industrial/index.ts +2 -2
  343. package/server/services/workshop/agents/industrial/ontology-tools.ts +66 -6
  344. package/server/services/workshop/agents/industrial/ops-tools.ts +54 -16
  345. package/server/services/workshop/agents/industrial/optimizer-tools.ts +2 -0
  346. package/server/services/workshop/agents/industrial/recipe-gate.ts +19 -6
  347. package/server/services/workshop/agents/industrial/recipe-propose-tools.ts +1 -1
  348. package/server/services/workshop/agents/omp-agent/supervision-history.ts +38 -0
  349. package/server/services/workshop/agents/omp-agent/tools-steer.ts +18 -1
  350. package/server/services/workshop/aml/aml.repo.ts +16 -0
  351. package/server/services/workshop/daq/bus/index.ts +12 -5
  352. package/server/services/workshop/db/database/open.ts +97 -97
  353. package/server/services/workshop/db/database/schema.ts +1 -0
  354. package/server/services/workshop/ontology/autofix.ts +198 -0
  355. package/server/services/workshop/ontology/contracts.ts +3 -1
  356. package/server/services/workshop/ontology/index.ts +2 -0
  357. package/server/services/workshop/ontology/ontology.service.ts +46 -0
  358. package/server/services/workshop/ontology/subgraph.core.ts +300 -0
  359. package/server/services/workshop/ontology/subgraph.service.ts +113 -0
  360. package/server/services/workshop/optimization/contracts.ts +2 -0
  361. package/server/services/workshop/optimization/profile-adopt.service.ts +5 -0
  362. package/server/services/workshop/optimization/profile.repo.ts +6 -4
  363. package/.output/public/_nuxt/B3Y5bCrM.js +0 -1
  364. package/.output/public/_nuxt/B8N8nsad2.js +0 -2
  365. package/.output/public/_nuxt/BO0BJx3w2.js +0 -8
  366. package/.output/public/_nuxt/BWn-lfCp2.js +0 -1
  367. package/.output/public/_nuxt/Bcs9zzO62.js +0 -1
  368. package/.output/public/_nuxt/BwO2Y5FH2.js +0 -1
  369. package/.output/public/_nuxt/Byf4IxU2.js +0 -14
  370. package/.output/public/_nuxt/C-LU6xv_.js +0 -1
  371. package/.output/public/_nuxt/CJ0bjZ6v2.js +0 -1
  372. package/.output/public/_nuxt/CJ3Vul2u.js +0 -1
  373. package/.output/public/_nuxt/CNwx04ln.js +0 -84
  374. package/.output/public/_nuxt/CVCscYzb2.js +0 -1
  375. package/.output/public/_nuxt/C_A0MCol2.js +0 -1
  376. package/.output/public/_nuxt/CnP6snIz.js +0 -3
  377. package/.output/public/_nuxt/D0mzKkZr.js +0 -1
  378. package/.output/public/_nuxt/D3buuIkE.js +0 -1
  379. package/.output/public/_nuxt/D4_INWgq.js +0 -1
  380. package/.output/public/_nuxt/D5kCimyw2.js +0 -1
  381. package/.output/public/_nuxt/DBtXLkyA.js +0 -1
  382. package/.output/public/_nuxt/DHURbGkn2.js +0 -1
  383. package/.output/public/_nuxt/DaBzS-iu.js +0 -1
  384. package/.output/public/_nuxt/DnlhV7Xe.js +0 -1
  385. package/.output/public/_nuxt/GtI6PUI32.js +0 -1
  386. package/.output/public/_nuxt/Nw_6OpQZ.js +0 -1
  387. package/.output/public/_nuxt/Y4sMSPrS2.js +0 -1
  388. package/.output/public/_nuxt/aml.CeLxRzvI.css +0 -1
  389. package/.output/public/_nuxt/builds/meta/7a79a7e4-d691-4cf5-ae3a-3b01814479fb.json +0 -1
  390. package/.output/public/_nuxt/goF_oZHF.js +0 -2
  391. package/.output/public/_nuxt/ontology.QMGeh66l.css +0 -1
  392. package/.output/public/_nuxt/optimizer.B8gCbU1b.css +0 -1
  393. package/.output/public/_nuxt/rJ7To74v2.js +0 -1
  394. package/.output/server/chunks/build/ontology-C3HS5pMo.mjs.map +0 -1
  395. package/.output/server/chunks/build/optimizer-BOtGWEtc.mjs +0 -200
  396. package/.output/server/chunks/build/optimizer-BOtGWEtc.mjs.map +0 -1
@@ -1,569 +1,602 @@
1
- /**
2
- * AML 作业与模型(aml_job_* / aml_leaderboard / aml_model_*)
3
- * (由 server/services/workshop/agents/industrial-tools.ts 按职责拆出;内容逐行原文搬运)
4
- */
5
- import { AML_AUDIT_KIND, amlErr, amlJobCard, amlNodeName, amlNodeUnit, fmtAmlNum } from './aml-dataset-tools'
6
- import { agentBadgeLabel } from '../agent-badge'
7
- import { alignToGrid } from '../../aml/clean'
8
- import { cancelJob, jobLogsTail, runtimeStatus, submitJob } from '../../aml/job-orchestrator'
9
- import { getActiveLineRun } from '../../dcw/line-run'
10
- import { getAgentNodeBindingRepo } from '../node-bindings.repo'
11
- import { getAmlRuntime } from '../../aml/runtime'
12
- import { getDaqNodeRepo } from '../../daq/daq-node.repo'
13
- import { createTrainingPlan, listTrainingPlans, runPlanTraining } from '../../aml/twin/training-plans'
14
- import { getDcwProductRepo } from '../../dcw/dcw-product.repo'
15
- import { getDcwRecipeRepo } from '../../dcw/dcw-recipe.repo'
16
- import { getToolApprovals } from '../tool-approvals'
17
- import { claimToolResult } from '../../runtime/platform-notice'
18
- import { predictProduction, readIoSpecFor } from '../../aml/predictor'
19
- import { recordOps } from '../../ops/ops'
20
- import { transitionModel } from '../../aml/model-registry'
21
- import { resolveRefFaces } from './tool-verify'
22
-
23
- /** 工具:aml_job_submit —— 提交训练作业(train.py 全文内联;归因 agentId;队列 FIFO) */
24
- export async function toolAmlJobSubmit(agentId: string, args: {
25
- dataset_id?: string
26
- code?: string
27
- change_note?: string
28
- params?: Record<string, unknown>
29
- seed?: number | string
30
- purpose?: string
31
- parent_experiment_id?: string
32
- scene_id?: string
33
- scene_version?: string
34
- objective_id?: string
35
- job_kind?: 'supervised' | 'physics_calibration' | 'hybrid_residual' | 'uncertainty_calibration'
36
- physics_manifest?: Record<string, unknown>
37
- physics_spec?: Record<string, unknown>
38
- twin_snapshot?: Record<string, unknown>
39
- objective_profile?: Record<string, unknown>
40
- provider_id?: string
41
- provider_version?: string
42
- provider_hash?: string
43
- provider_generation?: number | string
44
- model_name?: string
45
- model_description?: string
46
- }): Promise<{ text: string, isError?: boolean }> {
47
- const datasetId = String(args.dataset_id ?? '').trim()
48
- const code = typeof args.code === 'string' ? args.code : ''
49
- if (!datasetId) return amlErr('dataset_id 必填(aml_dataset_build 返回的 id)。')
50
- const templateFallback = args.job_kind === 'hybrid_residual' && !code.trim()
51
- if (!templateFallback && !code.trim()) return amlErr('code 必填:train.py 全文(单文件;契约见工具说明)。例外:job_kind=hybrid_residual 可省略 code,平台用内置参考训练器(物理参数校准→残差集成→UQ)。')
52
- if (code.length > 512_000) return amlErr(`code 过大(${Math.round(code.length / 1024)}KB),上限 500KB;精简训练代码(模型定义/训练循环拆薄)后重试。`)
53
- if (!templateFallback && !/\bimport\s+amlkit\b/.test(code)) {
54
- return amlErr('code 必须 import amlkit(平台数据/进度/导出契约):amlkit.load_bundle(datasetPath) 取数、amlkit.report_progress(pct, note) 报进度、amlkit.export_torch_onnx(model, manifest) 导出。缺契约的作业必然失败。')
55
- }
56
- const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : args.purpose === 'mpc_surrogate' ? 'mpc_surrogate' : undefined
57
- const changeNote = String(args.change_note ?? '').trim()
58
- if (!changeNote) return amlErr('change_note 必填:单组件改动纪律 —— 每次实验只改一个组件(网络结构/特征/超参之一),写清改了什么与预期,实验谱系靠它归因。')
59
- try {
60
- const job = submitJob({
61
- datasetId,
62
- purpose,
63
- changeNote,
64
- params: args.params && typeof args.params === 'object' && !Array.isArray(args.params) ? args.params : undefined,
65
- seed: Number.isInteger(Number(args.seed)) ? Number(args.seed) : undefined,
66
- parentExperimentId: String(args.parent_experiment_id ?? '').trim() || undefined,
67
- code,
68
- sceneId: String(args.scene_id ?? '').trim() || undefined,
69
- sceneVersion: String(args.scene_version ?? '').trim() || undefined,
70
- objectiveId: String(args.objective_id ?? '').trim() || undefined,
71
- jobKind: args.job_kind,
72
- physicsManifest: args.physics_manifest,
73
- physicsSpec: args.physics_spec,
74
- twinSnapshot: args.twin_snapshot,
75
- objectiveProfile: args.objective_profile,
76
- providerId: String(args.provider_id ?? '').trim() || undefined,
77
- providerVersion: String(args.provider_version ?? '').trim() || undefined,
78
- providerHash: String(args.provider_hash ?? '').trim() || undefined,
79
- providerGeneration: Number.isInteger(Number(args.provider_generation)) ? Number(args.provider_generation) : undefined,
80
- modelName: String(args.model_name ?? '').trim() || undefined,
81
- modelDescription: String(args.model_description ?? '').trim() || undefined,
82
- agent: { id: agentId },
83
- })
84
- const st = await runtimeStatus()
85
- return {
86
- text: [
87
- `训练作业已提交:`,
88
- ` job_id: ${job.id}`,
89
- ` dataset: ${job.datasetId} | purpose=${job.purpose} | seed=${fmtAmlNum(Number(job.budget.seed ?? 42), 0)} | change_note: ${changeNote}`,
90
- args.model_name || args.model_description ? ` 模型标识: name=${String(args.model_name ?? '(缺省派生)')} | description=${String(args.model_description ?? '(缺省派生)').slice(0, 80) || '(缺省派生)'}` : ` 模型标识: 注册时自动派生「产线·配方·优化目标」label(可传 model_name/model_description 自拟)`,
91
- ` 队列: 排队 ${st.queued} 个(含本作业)/ 在跑 ${st.running} 个 | python ${st.python.ok ? (st.python.version ?? 'ok') : `不可用(${st.python.reason ?? '?'};作业会失败,请联系用户修复环境)`}`,
92
- `用 aml_job_status { job_id: "${job.id}" } 看状态/阶段/门禁,aml_job_logs { job_id: "${job.id}" } 看日志尾。作业完成后平台会自动回执结果到你的信箱并唤醒你(无需持续轮询);期间可继续其他任务。`,
93
- ].join('\n'),
94
- }
95
- }
96
- catch (err) {
97
- return { text: `提交失败:${err instanceof Error ? err.message : String(err)}`, isError: true }
98
- }
99
- }
100
-
101
- /** 工具:aml_job_status —— 作业状态(单作业详情 / 缺省列最近 10 个) */
102
- export async function toolAmlJobStatus(agentId: string, args: { job_id?: string }): Promise<{ text: string, isError?: boolean }> {
103
- const rt = getAmlRuntime()
104
- const jobId = String(args.job_id ?? '').trim()
105
- if (jobId) {
106
- const job = rt.repo.job.get(jobId)
107
- if (!job) return amlErr(`作业 ${jobId} 不存在(aml_job_status 不带参数可列出最近 10 个)。`)
108
- // 终态经轮询面亲自看到 → 认领结果所有权,取消平台的事后补送(先到先得,省一跳无谓回合)
109
- if (['done', 'failed', 'cancelled'].includes(job.status)) claimToolResult('aml_job', jobId)
110
- return { text: `${amlJobCard(job)}\n\n轮询建议:training 阶段 30~60s 查一次;终态(done/failed)后看门禁与错误,失败原因可 in aml_job_logs 定位。训练完成后平台也会自动回执到你的信箱,无需持续轮询。` }
111
- }
112
- // 无 job_id 的清单必须按调用者可读面过滤(2026-10-18):此前返回全平台最近 10 个,
113
- // 任意零绑定 Agent 可读到其他产线/频道的作业 id 与数据集谱系。
114
- const readableLines = resolveRefFaces(agentId).readableLines
115
- const visible = (job: { datasetId: string, agentId?: string }): boolean => {
116
- if (job.agentId === agentId) return true
117
- const ds = rt.repo.dataset.get(job.datasetId)
118
- return !!ds && readableLines.has(ds.lineId)
119
- }
120
- const jobs = rt.repo.job.list({ limit: 10 }).filter(visible)
121
- if (jobs.length === 0) {
122
- return {
123
- text: `你的可读范围内没有训练作业(可读产线:${readableLines.size ? [...readableLines].join(', ') : '(无)'})。路径:aml_node_catalog 确认节点 → aml_dataset_build 组数据集 → aml_job_submit 提交训练;如需查看他线作业,请让用户授予对应产线权限。`,
124
- }
125
- }
126
- for (const j of jobs) {
127
- if (['done', 'failed', 'cancelled'].includes(j.status)) claimToolResult('aml_job', j.id)
128
- }
129
- const rows = jobs.map(j => `- ${j.id} [${j.status}] ${j.stage || '-'} ${j.progress}% dataset=${j.datasetId}${j.error ? ` | 错误: ${j.error.slice(0, 100)}` : ''}`)
130
- return { text: `最近 ${jobs.length} 个作业(新→旧):\n${rows.join('\n')}\n\n传 job_id 查看单作业详情(指标/门禁逐项/错误)。训练完成后平台会自动回执到你的信箱。` }
131
- }
132
-
133
- /** 工具:aml_job_logs —— 作业日志尾随(平台 ##AML 协议行 + 训练输出) */
134
- export async function toolAmlJobLogs(_agentId: string, args: { job_id?: string, lines?: number | string }): Promise<{ text: string, isError?: boolean }> {
135
- const jobId = String(args.job_id ?? '').trim()
136
- if (!jobId) return amlErr('job_id 必填(aml_job_status 不带参数可列出最近作业)。')
137
- const job = getAmlRuntime().repo.job.get(jobId)
138
- if (!job) return amlErr(`作业 ${jobId} 不存在。`)
139
- const n = Math.min(Math.max(Number(args.lines) || 80, 5), 400)
140
- const rows = jobLogsTail(jobId, n)
141
- if (rows.length === 0) {
142
- return { text: `作业 ${jobId} 暂无日志(${job.status === 'queued' ? '仍在排队,开跑后产生日志' : '日志未落盘或已被清理'});稍后重试或用 aml_job_status 看状态。` }
143
- }
144
- return { text: `作业 ${jobId} 日志尾(最近 ${rows.length} 行,时间正序;##AML 前缀行 = 平台协议事件):\n${rows.join('\n')}` }
145
- }
146
-
147
- /** 工具:aml_job_cancel —— 取消排队/运行中的作业(归因入册) */
148
- export async function toolAmlJobCancel(agentId: string, args: { job_id?: string }): Promise<{ text: string, isError?: boolean }> {
149
- const jobId = String(args.job_id ?? '').trim()
150
- if (!jobId) return amlErr('job_id 必填。')
151
- const rt = getAmlRuntime()
152
- const job = rt.repo.job.get(jobId)
153
- if (!job) return amlErr(`作业 ${jobId} 不存在。`)
154
- if (['done', 'failed', 'cancelled', 'timeout'].includes(job.status)) {
155
- return amlErr(`作业 ${jobId} 已是终态(${job.status}),无需取消;aml_job_status 复核。`)
156
- }
157
- const ok = cancelJob(jobId, { id: agentId, kind: 'agent' })
158
- if (!ok) return amlErr(`取消失败:作业 ${jobId} 不在排队/运行中(可能恰好结束);aml_job_status 复核。`)
159
- const dataset = rt.repo.dataset.get(job.datasetId)
160
- recordOps({
161
- actor: agentId,
162
- actorName: agentBadgeLabel(agentId),
163
- actorKind: 'agent',
164
- action: 'aml.job.cancel',
165
- kind: AML_AUDIT_KIND,
166
- summary: `取消训练作业 ${jobId}(原状态 ${job.status})`,
167
- targetKind: 'aml_job',
168
- targetId: jobId,
169
- lineId: dataset?.lineId,
170
- productId: dataset?.productId,
171
- recipeId: dataset?.recipeId,
172
- })
173
- return { text: `作业 ${jobId} 已取消(运行中的会终止进程树)。数据集 ${job.datasetId} 仍可复用;需要重训直接 aml_job_submit。` }
174
- }
175
-
176
- /** 工具:aml_leaderboard —— 数据集的实验谱系与门禁对照(标当前最优) */
177
- export async function toolAmlLeaderboard(_agentId: string, args: { dataset_id?: string }): Promise<{ text: string, isError?: boolean }> {
178
- const datasetId = String(args.dataset_id ?? '').trim()
179
- if (!datasetId) return amlErr('dataset_id 必填(aml_dataset_build 返回的 id)。')
180
- const rt = getAmlRuntime()
181
- const dataset = rt.repo.dataset.get(datasetId)
182
- if (!dataset) return amlErr(`数据集 ${datasetId} 不存在。`)
183
- const exps = rt.repo.experiment.listByDataset(datasetId, 50)
184
- if (exps.length === 0) {
185
- return { text: `数据集 ${datasetId}(${dataset.rowCount} 行)尚无实验:aml_job_submit 提交训练后,此处给出谱系与门禁对照。` }
186
- }
187
- const parsed = exps.map((e) => {
188
- let metrics: { oneStepTest?: { nrmse?: number }, rolloutTest?: { nrmse?: number } } = {}
189
- let gates: { passed?: boolean } = {}
190
- try {
191
- metrics = JSON.parse(e.metricsJson)
192
- }
193
- catch { /* 无指标 */ }
194
- try {
195
- gates = JSON.parse(e.gatesJson)
196
- }
197
- catch { /* 无门禁 */ }
198
- return { e, g1: metrics.oneStepTest?.nrmse ?? null, g2: metrics.rolloutTest?.nrmse ?? null, passed: gates.passed === true || e.status === 'gates_passed' }
199
- })
200
- const passers = parsed.filter(p => p.passed && p.g1 != null)
201
- const bestId = passers.length > 0 ? passers.reduce((a, b) => ((a.g1 ?? Infinity) <= (b.g1 ?? Infinity) ? a : b)).e.id : null
202
- const rows = parsed.map(p =>
203
- `- ${p.e.id} [${p.e.status}] ${p.e.changeNote ? `「${p.e.changeNote.slice(0, 60)}」` : '(无备注)'}${p.e.parentExperimentId ? ` ← 父 ${p.e.parentExperimentId}` : ''} | G1=${fmtAmlNum(p.g1)}${p.g2 != null ? ` G2=${fmtAmlNum(p.g2)}` : ''} | 门禁 ${p.passed ? '通过' : '未通过'}${p.e.id === bestId ? ' ★当前最优' : ''}`)
204
- const models = rt.repo.model.list({ limit: 100 }).filter(m => m.datasetId === datasetId)
205
- const modelLines = models.map(m => `- ${m.id} [${m.stage}] 「${m.label || '(未打标)'}」 purpose=${m.purpose}${m.objectiveId ? ` objective=${m.objectiveId}` : ''}(实验 ${m.experimentId})`)
206
- return {
207
- text: [
208
- `实验谱系(数据集 ${datasetId} · ${dataset.productId}/${dataset.recipeId} · ${exps.length} 条,新→旧):`,
209
- ...rows,
210
- modelLines.length > 0 ? `\n已登记模型(晋升对象;aml_leaderboard 最优 = 门禁通过且 G1 最小):` : null,
211
- ...modelLines,
212
- `\nG1=单步测试 NRMSE,G2=多步滚动 NRMSE(越小越好)。优化纪律:每次只改一个组件,change_note 写明;把最优模型推上生产用 aml_model_promote(lead 专属,需人工审批)。`,
213
- ].filter(x => x !== null).join('\n'),
214
- }
215
- }
216
-
217
- /** 工具:aml_model_promote —— 模型晋升 shadow/production(lead 专属;HITL 人工审批;流转入册由 transitionModel 内部完成) */
218
- export async function toolAmlModelPromote(agentId: string, args: { model_id?: string, to_stage?: string }): Promise<{ text: string, isError?: boolean }> {
219
- // 治理硬守卫:晋升仅限 lead 实例(注入层过滤 + 分发层双重校验,HTTP 直调不可绕过)
220
- const roleRow = getAmlRuntime().db.prepare('SELECT role FROM channel_agents WHERE id = ? AND enabled = 1').get(agentId) as { role?: string } | undefined
221
- if (roleRow?.role !== 'lead') {
222
- return amlErr(`aml_model_promote 为 lead 专属工具:实例 ${agentId}(role=${roleRow?.role ?? '未知'})无权晋升模型。请由团队 lead 发起,晋升需人工 HITL 批准。`)
223
- }
224
- const modelId = String(args.model_id ?? '').trim()
225
- const toStage = String(args.to_stage ?? '').trim()
226
- if (!modelId) return amlErr('model_id 必填(aml_leaderboard 的「已登记模型」段可查)。')
227
- if (toStage !== 'shadow' && toStage !== 'production') return amlErr('to_stage 必须为 \'shadow\'(影子观测)或 \'production\'(生产生效)。')
228
- const rt = getAmlRuntime()
229
- const model = rt.repo.model.get(modelId)
230
- if (!model) return amlErr(`模型 ${modelId} 不存在。`)
231
- if (model.stage === toStage) return amlErr(`模型 ${modelId} 已是 ${toStage},无需流转。`)
232
- // production 预检:同组 (product, recipe, purpose) 旧生产将被退役
233
- let oldProdId: string | undefined
234
- if (toStage === 'production') {
235
- oldProdId = rt.repo.model
236
- .list({ stage: 'production', productId: model.productId, recipeId: model.recipeId, limit: 50 })
237
- .find(m => m.id !== modelId && m.purpose === model.purpose)?.id
238
- }
239
- // 同模型挂起审批去重(防审批面板堆积,与 dcw_control 同纪律)
240
- const approvals = getToolApprovals()
241
- if (approvals.hasPendingFor(agentId, modelId)) {
242
- return amlErr(`模型 ${modelId} 已有一条待审批的晋升请求,请等待用户处理后再发起新请求。`)
243
- }
244
- const detail = `AML 模型晋升:${modelId} ${model.stage} → ${toStage}「${model.label || '(未打标)'}」(产线 ${model.lineId || rt.repo.dataset.get(model.datasetId)?.lineId || '?'} / 产品 ${model.productId} / 配方 ${model.recipeId} / purpose=${model.purpose}${model.objectiveId ? ` / objective=${model.objectiveId}` : ''}${oldProdId ? `;批准后原生产模型 ${oldProdId} 将自动退役` : ''})`
245
- const ap = await approvals.request(agentId, modelId, 'dcw', detail, { title: 'AML 模型晋升审批' })
246
- if (!ap.approved) {
247
- return { text: `人工未批准:模型 ${modelId} 保持 ${model.stage} 阶段。用户备注:${ap.comment || '(无)'}。可回炉重训(aml_job_submit)或换候选模型后再申请。` }
248
- }
249
- try {
250
- const r = transitionModel(modelId, toStage, agentBadgeLabel(agentId), 'agent')
251
- // 归属审计已由 transitionModel 内部 recordOps(aml.model.promote)完成,此处不重复记录
252
- return {
253
- text: `晋升完成:${modelId} ${r.from} → ${toStage}。${r.retiredId ? `原生产模型 ${r.retiredId} 已自动退役。` : ''}${toStage === 'shadow'
254
- ? '影子阶段:用 aml_model_reference 观测其预测与实际偏差,积累证据后再申请 production。'
255
- : '该模型已作为生产模型生效:aml_model_reference 的影子参考即来自它;调控 Agent 调参前请先查它。'}`,
256
- }
257
- }
258
- catch (err) {
259
- return { text: `晋升失败:${err instanceof Error ? err.message : String(err)}(审批已通过;若为并发竞态可刷新状态重试)。`, isError: true }
260
- }
261
- }
262
-
263
- /** 工具:aml_model_find —— 模型发现与选型(按产线/产品/配方/目标/阶段过滤;复用 vs 新建决策指引) */
264
- export async function toolAmlModelFind(agentId: string, args: {
265
- line_id?: string
266
- product_id?: string
267
- recipe_id?: string
268
- purpose?: string
269
- objective_id?: string
270
- stage?: string
271
- }): Promise<{ text: string, isError?: boolean }> {
272
- // 0. 绑定面:与 aml_model_reference 同纪律 —— 至少一个 daq 绑定;line_id 只缩不放
273
- const repo = getAgentNodeBindingRepo()
274
- const daqBindings = repo.byAgent(agentId).filter(b => b.kind === 'daq')
275
- if (daqBindings.length === 0) {
276
- return amlErr('你尚未绑定任何数采节点,无法确定可查的产线范围;请先绑定数采节点。')
277
- }
278
- const lineOf = (id: string): string => getDaqNodeRepo().byId(id)?.lineId ?? ''
279
- const scopeLines = [...new Set(daqBindings.map(b => lineOf(b.nodeId)).filter(Boolean))]
280
- const lineFilter = String(args.line_id ?? '').trim()
281
- if (lineFilter && !scopeLines.includes(lineFilter)) {
282
- return amlErr(`产线 ${lineFilter} 不在你的负责范围(你绑定节点覆盖:${scopeLines.join(', ') || '无'};绑定只缩不放)。`)
283
- }
284
- const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : args.purpose === 'mpc_surrogate' ? 'mpc_surrogate' : undefined
285
- const stage = ['candidate', 'shadow', 'production', 'retired'].includes(String(args.stage ?? '')) ? String(args.stage) : undefined
286
- const productFilter = String(args.product_id ?? '').trim() || undefined
287
- const recipeFilter = String(args.recipe_id ?? '').trim() || undefined
288
- const objectiveFilter = String(args.objective_id ?? '').trim() || undefined
289
-
290
- // 1. 拉全量后在代码层过滤(模型行 line_id 对历史行为空,经数据集回补;绑定只缩不放)
291
- const rt = getAmlRuntime()
292
- const models = rt.repo.model.list({ stage, purpose, limit: 200 }).filter((m) => {
293
- const ds = rt.repo.dataset.get(m.datasetId)
294
- const modelLine = m.lineId || ds?.lineId || ''
295
- if (lineFilter ? modelLine !== lineFilter : (modelLine && !scopeLines.includes(modelLine))) return false
296
- if (productFilter && m.productId !== productFilter) return false
297
- if (recipeFilter && m.recipeId !== recipeFilter) return false
298
- if (objectiveFilter && m.objectiveId !== objectiveFilter) return false
299
- return true
300
- })
301
- if (models.length === 0) {
302
- return {
303
- text: [
304
- `过滤条件下(${[lineFilter || `产线∈{${scopeLines.join(', ')}}`, productFilter, recipeFilter, purpose, objectiveFilter && `objective=${objectiveFilter}`, stage].filter(Boolean).join(', ')})没有匹配模型。`,
305
- `新建路径:aml_node_catalog → aml_dataset_build(绑定产线的配方批次组数据集)→ aml_job_submit 提交训练(建议带 model_name/model_description 打标)。`,
306
- `注意:模型按 (产品, 配方, purpose) 隔离 —— 换配方必须为该配方另建模型,不可跨配方复用。`,
307
- ].join('\n'),
308
- }
309
- }
310
-
311
- // 2. 分组呈现:production 置顶(可直接复用)→ shadow/candidate(晋升对象)→ retired 计数
312
- const fmtG = (metricsJson: string): string => {
313
- try {
314
- const m = JSON.parse(metricsJson) as { oneStepTest?: { nrmse?: number }, rolloutTest?: { nrmse?: number } }
315
- return `G1=${fmtAmlNum(m.oneStepTest?.nrmse ?? null)}${m.rolloutTest?.nrmse != null ? ` G2=${fmtAmlNum(m.rolloutTest.nrmse)}` : ''}`
316
- }
317
- catch { return 'G1=- G2=-' }
318
- }
319
- const prods = models.filter(m => m.stage === 'production')
320
- const cands = models.filter(m => m.stage === 'shadow' || m.stage === 'candidate')
321
- const retiredCount = models.filter(m => m.stage === 'retired').length
322
- const rowOf = (m: typeof models[number]): string => {
323
- const lineId = m.lineId || rt.repo.dataset.get(m.datasetId)?.lineId || '?'
324
- const desc = m.description ? ` | ${m.description.slice(0, 120)}` : ''
325
- return `- ${m.id} [${m.stage}] 「${m.label || '(未打标)'}」 purpose=${m.purpose}${m.objectiveId ? ` objective=${m.objectiveId}` : ''} | 产线 ${lineId} | ${fmtG(m.metricsJson)}${desc}`
326
- }
327
- const sections: string[] = []
328
- sections.push(`匹配 ${models.length} 个模型(过滤:${[lineFilter || `产线∈{${scopeLines.join(', ')}}`, productFilter, recipeFilter, purpose, objectiveFilter && `objective=${objectiveFilter}`, stage].filter(Boolean).join(', ')})`)
329
- if (prods.length > 0) {
330
- sections.push(`\n生产模型(可直接复用):`)
331
- sections.push(...prods.map(rowOf))
332
- }
333
- if (cands.length > 0) {
334
- sections.push(`\n候选/影子(晋升对象;aml_model_promote 为 lead 专属,需人工审批):`)
335
- sections.push(...cands.map(rowOf))
336
- }
337
- if (retiredCount > 0) sections.push(`\n另有 ${retiredCount} 个已退役模型(stage 过滤可查)。`)
338
-
339
- // 3. 决策指引:按本次优化目标给出 复用 / 晋升 / 新建 三分支
340
- const tips: string[] = []
341
- if (prods.some(m => (!productFilter || m.productId === productFilter) && (!recipeFilter || m.recipeId === recipeFilter) && (!purpose || m.purpose === purpose))) {
342
- tips.push(`已有匹配生产模型 → 调参前先 aml_model_reference 问影子参考;预测与实际偏差大再考虑重训(换配方/换目标才需要新模型)。`)
343
- }
344
- else if (cands.length > 0) {
345
- tips.push(`无生产模型但有候选 → 评估候选指标后由 lead 走 aml_model_promote(shadow 先行,production 需人工审批)。`)
346
- }
347
- else {
348
- tips.push(`无可用模型 → 走新建:aml_dataset_build 组该配方数据集 → aml_job_submit(带 model_name/model_description)→ 门禁通过后晋升。`)
349
- }
350
- tips.push(`模型身份规则:每个模型的 label/description 强制携带 产线·配方·优化目标(注册时平台派生,aml_job_submit 可用 model_name/model_description 补充);不同 recipe 的模型互不通用,选型先对齐配方与目标。`)
351
- return { text: [...sections, `\n选型指引:`, ...tips.map(t => `- ${t}`)].join('\n') }
352
- }
353
-
354
- /** 工具:aml_model_reference —— 生产模型影子参考:拉最近历史组装模型输入 → 预测未来目标轨迹(调参前先问影子模型) */
355
- export async function toolAmlModelReference(agentId: string, args: {
356
- line_id?: string
357
- product_id?: string
358
- recipe_id?: string
359
- purpose?: string
360
- controls?: Record<string, number | string>
361
- steps?: number | string
362
- }): Promise<{ text: string, isError?: boolean }> {
363
- // 0. 绑定面:至少一个 daq 绑定;line_id 只缩不放
364
- const repo = getAgentNodeBindingRepo()
365
- const daqBindings = repo.byAgent(agentId).filter(b => b.kind === 'daq')
366
- if (daqBindings.length === 0) {
367
- return amlErr('你尚未绑定任何数采节点,无法组装影子参考所需的历史输入;请先绑定数采节点。')
368
- }
369
- const lineOf = (id: string): string => getDaqNodeRepo().byId(id)?.lineId ?? ''
370
- const scopeLines = [...new Set(daqBindings.map(b => lineOf(b.nodeId)).filter(Boolean))]
371
- const lineFilter = String(args.line_id ?? '').trim()
372
- if (lineFilter && !scopeLines.includes(lineFilter)) {
373
- return amlErr(`产线 ${lineFilter} 不在你的负责范围(你绑定节点覆盖:${scopeLines.join(', ') || '无'};绑定只缩不放)。`)
374
- }
375
- const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : 'mpc_surrogate'
376
-
377
- // 1. 解析 (productId, recipeId):参数给全用参数;缺省回退到候选产线的唯一活动批次
378
- let productId = String(args.product_id ?? '').trim()
379
- let recipeId = String(args.recipe_id ?? '').trim()
380
- let resolveHow = '参数指定'
381
- if (!productId || !recipeId) {
382
- const candidateLines = lineFilter ? [lineFilter] : scopeLines
383
- const runs = candidateLines
384
- .map(lid => getActiveLineRun(lid))
385
- .filter((r): r is NonNullable<ReturnType<typeof getActiveLineRun>> => !!r && !!r.productId && !!r.recipeId)
386
- if (runs.length === 1) {
387
- const run = runs[0]!
388
- if (!productId) productId = run.productId
389
- if (!recipeId) recipeId = run.recipeId
390
- resolveHow = `产线 ${run.lineId} 唯一活动批次 ${run.runId.slice(0, 8)}(产品「${run.productName}」/配方「${run.recipeName}」)`
391
- }
392
- else if (runs.length === 0) {
393
- return { text: `无法确定产品/配方:${candidateLines.length > 0 ? `产线 ${candidateLines.join(', ')}` : '你绑定节点所属产线'}当前无活动批次,且 product_id/recipe_id 未给全。请开跑后重试,或显式传 product_id 与 recipe_id(line_context 可查)。`, isError: true }
394
- }
395
- else {
396
- return { text: `无法确定产品/配方:候选产线存在多个活动批次(${runs.map(r => `${r.lineId} → ${r.productName}/${r.recipeName}`).join('; ')})。请传 line_id 缩小范围,或显式传 product_id 与 recipe_id。`, isError: true }
397
- }
398
- }
399
-
400
- // 2. 生产模型(无则指引建模范式)
401
- const rt = getAmlRuntime()
402
- const model = rt.repo.model.productionOf(productId, recipeId, purpose)
403
- if (!model) {
404
- const cands = rt.repo.model.list({ productId, recipeId, limit: 20 })
405
- return { text: `尚无生产模型:(${productId}, ${recipeId}, ${purpose}) 下没有 stage=production 的模型。${cands.length > 0 ? `现有候选:${cands.map(m => `${m.id}「${m.label || '(未打标)'}」(${m.stage})`).join(', ')} —— 请 lead 用 aml_model_promote 晋升(需人工审批)。` : '该三元组下还没有任何模型:aml_dataset_build → aml_job_submit 训练,门禁通过后申请晋升。在此之前调参请依据 dcw_journal 历史经验与 daq_query 证据。'}` }
406
- }
407
- // 3. 产线归属鉴权(绑定只缩不放;模型归属线经数据集追溯)
408
- const ds = rt.repo.dataset.get(model.datasetId)
409
- const modelLine = ds?.lineId ?? ''
410
- if (modelLine && !scopeLines.includes(modelLine)) {
411
- return amlErr(`模型 ${model.id} 归属产线 ${modelLine},不在你绑定节点覆盖的产线(${scopeLines.join(', ')})内;绑定只缩不放。`)
412
- }
413
-
414
- // 4. io 契约 → 拉最近历史 → 网格对齐 → 组 [H][nAll](原始物理量,顺序 = allNodes)
415
- const io = readIoSpecFor(model.id)
416
- const H = io.historySteps
417
- const beatMs = io.beatMs
418
- const now = Date.now()
419
- const fromMs = now - H * beatMs - 5 * beatMs
420
- const { getTsdb, tsdbReady } = await import('../../daq/storage')
421
- await tsdbReady
422
- const tsdb = getTsdb()
423
- const columns = new Map<string, number[]>()
424
- const shortage: string[] = []
425
- for (const nodeId of io.allNodes) {
426
- try {
427
- const pts = await tsdb.query(nodeId, { fromMs, toMs: now, limit: Math.min(Math.max(H * 10, 600), 5000) })
428
- const { grid } = alignToGrid(
429
- pts.map(p => ({ at: p.at, value: p.value ?? p.avg ?? 0 })).filter(p => Number.isFinite(p.value)),
430
- { beatMs, maxInterpMs: beatMs * 2 },
431
- )
432
- const tail = grid.slice(-H).map(g => g.value)
433
- if (tail.length < H) {
434
- shortage.push(`${amlNodeName(nodeId)}(${nodeId})仅 ${tail.length}/${H} 格点`)
435
- continue
436
- }
437
- columns.set(nodeId, tail)
438
- }
439
- catch (err) {
440
- shortage.push(`${amlNodeName(nodeId)}(${nodeId})查询失败:${err instanceof Error ? err.message : String(err)}`)
441
- }
442
- }
443
- if (shortage.length > 0) {
444
- return { text: `历史数据不足,无法组装模型输入:${shortage.join(';')}。需要从 ${new Date(fromMs).toISOString().slice(0, 16)} 起连续约 ${(H * beatMs / 60_000).toFixed(0)} 分钟的运行数据(产线须在跑);等数据积累后重试,或缩短 steps/换更短 history 的模型。`, isError: true }
445
- }
446
- const history: number[][] = []
447
- for (let i = 0; i < H; i++) history.push(io.allNodes.map(n => columns.get(n)![i]!))
448
-
449
- // 5. 控制轨迹:controls 映射(节点 id → 值,常数外推 steps 行);缺省行持最后观测
450
- const steps = Math.max(1, Math.min(Number(args.steps) || io.horizonSteps, io.horizonSteps))
451
- const lastRow = history[history.length - 1]!
452
- const unknownKeys = args.controls ? Object.keys(args.controls).filter(k => !io.controlNodes.includes(k)) : []
453
- const controlRow = io.controlNodes.map((c) => {
454
- const provided = args.controls?.[c]
455
- const v = provided != null ? Number(provided) : Number.NaN
456
- return Number.isFinite(v) ? v : lastRow[io.allNodes.indexOf(c)]!
457
- })
458
- const controls: number[][] = Array.from({ length: steps }, () => [...controlRow])
459
-
460
- // 6. 预测 + 人话报告
461
- try {
462
- const r = await predictProduction(productId, recipeId, purpose, { history, controls, steps })
463
- const header = `步 | ${r.targetNodes.map(t => `${amlNodeName(t)}(${amlNodeUnit(t)})`).join(' | ')}`
464
- const forecastLines = r.forecast.map((row, i) =>
465
- ` ${i + 1}(+${fmtAmlNum(((i + 1) * r.beatMs) / 1000, 0)}s) | ${row.map(v => fmtAmlNum(v, 3)).join(' | ')}`)
466
- const ctrlTxt = io.controlNodes.length > 0
467
- ? io.controlNodes.map((c) => {
468
- const provided = args.controls?.[c]
469
- return `${amlNodeName(c)}=${Number.isFinite(Number(provided)) ? Number(provided) : `${fmtAmlNum(lastRow[io.allNodes.indexOf(c)], 3)}(持最后观测)`}${amlNodeUnit(c)}`
470
- }).join(', ')
471
- : '无控制节点'
472
- return {
473
- text: [
474
- `影子模型参考(调参前先问影子模型;预测≠承诺,真实下发仍走 dcw_control 安全联锁):`,
475
- ` 模型 ${r.modelId}「${model.label || '(未打标)'}」(stage=${r.stage},purpose=${purpose}${model.objectiveId ? `,objective=${model.objectiveId}` : ''})| 三元组:产品 ${getDcwProductRepo().byId(productId)?.name ?? productId} / 配方 ${getDcwRecipeRepo().byId(recipeId)?.name ?? recipeId}(解析:${resolveHow})`,
476
- model.description ? ` 模型说明: ${model.description}` : null,
477
- ` 输入:最近 ${H} 拍 × ${io.allNodes.length} 节点(beat=${beatMs}ms),截至 ${new Date(now).toISOString().slice(11, 19)};控制轨迹(共 ${steps} 步):${ctrlTxt}${unknownKeys.length > 0 ? `(已忽略非控制节点的键:${unknownKeys.join(', ')})` : ''}`,
478
- ` 预测(steps=${steps},每拍 ${beatMs / 1000}s):`,
479
- ` ${header}`,
480
- ...forecastLines,
481
- ` 假设: ${r.assumptions}`,
482
- `\n用法:把候选设定值作为 controls 传入(键 = 控制节点 id)对比不同方案的预测轨迹;选定后 dcw_control 下发并 daq_query 复测。`,
483
- ].filter(x => x !== null).join('\n'),
484
- }
485
- }
486
- catch (err) {
487
- return { text: `预测失败:${err instanceof Error ? err.message : String(err)}(模型 ${model.id};若为工件缺失可重训同规格数据集)。`, isError: true }
488
- }
489
- }
490
-
491
- /* ═══ 建模任务(aml_training_plan_*;2026-09-26 解耦计划:训练 Channel 的编排入口) ═══ */
492
-
493
- /** 工具:aml_training_plan_list —— 建模任务清单(状态/最新模型/新批次检测基数) */
494
- export async function toolAmlTrainingPlanList(agentId: string, _args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
495
- // 按调用者可读面过滤(2026-10-18):此前 listTrainingPlans() 全表返回,
496
- // 任意 Agent 可枚举其他产线的计划/模型/作业 id。
497
- const readableLines = resolveRefFaces(agentId).readableLines
498
- const plans = listTrainingPlans().filter(p => p.createdBy === agentId || readableLines.has(p.lineId))
499
- if (plans.length === 0) {
500
- return { text: `你的可读范围内没有建模任务(可读产线:${readableLines.size ? [...readableLines].join(', ') : '(无)'})。创建路径:AML 界面「建模任务」表单,或 aml_training_plan_create(绑定产线/产品/配方 + 数据集 IO 契约);auto 策略的任务在新批次数据到达时自动修正训练。` }
501
- }
502
- const lines = plans.map((p) => {
503
- const specFixed = p.trainingSpecJson ? 'spec已固化' : 'spec未固化(需先完成 draft 流程后 PATCH 固化)'
504
- return `- ${p.id}「${p.name}」[${p.enabled ? '启用' : '停用'}/${p.strategy}] 产线 ${p.lineId}·配方 ${p.recipeId}${p.objectiveId ? `·目标 ${p.objectiveId}` : ''} | 状态 ${p.lastStatus}${p.lastModelId ? ` → 模型 ${p.lastModelId}` : ''}${p.lastJobId ? `(作业 ${p.lastJobId})` : ''} | ${specFixed} | 批次基数 ${p.lastRunCount}`
505
- })
506
- return { text: ['建模任务清单:', ...lines].join('\n') }
507
- }
508
-
509
- /** 工具:aml_training_plan_train —— 立即触发一次修正训练(重建数据集含新批次 → 平台训练器) */
510
- export async function toolAmlTrainingPlanTrain(agentId: string, args: { plan_id?: string }): Promise<{ text: string, isError?: boolean }> {
511
- const planId = String(args.plan_id ?? '').trim()
512
- if (!planId) {
513
- const plans = listTrainingPlans()
514
- if (plans.length === 0) return amlErr('尚无建模任务可触发。')
515
- return amlErr(`plan_id 必填。可选:${plans.map(p => `${p.id}「${p.name}」`).join('; ')}`)
516
- }
517
- try {
518
- const r = await runPlanTraining(planId, { id: agentId, kind: 'agent' })
519
- return { text: `建模任务 ${planId} 修正训练已提交:\n 作业: ${r.jobId}\n 数据集: ${r.datasetId}(${r.rowCount} 行,含历史+新批次)\n用 aml_job_status 轮询;完成后模型自动入册,plan 状态回写,可在 AML 界面绑定到工艺优化 Channel。` }
520
- }
521
- catch (err) {
522
- return amlErr(`触发失败:${err instanceof Error ? err.message : String(err)}`)
523
- }
524
- }
525
-
526
- /** 工具:aml_training_plan_create —— 创建建模任务(绑定 recipe + 数据集 IO 契约 + 固化训练 spec) */
527
- export async function toolAmlTrainingPlanCreate(agentId: string, args: {
528
- name?: string
529
- line_id?: string
530
- product_id?: string
531
- recipe_id?: string
532
- scene_id?: string
533
- scene_version?: string
534
- objective_id?: string
535
- strategy?: string
536
- dataset_spec?: Record<string, unknown>
537
- training_spec?: Record<string, unknown>
538
- model_name_prefix?: string
539
- params?: Record<string, unknown>
540
- seed?: number | string
541
- min_interval_sec?: number | string
542
- }): Promise<{ text: string, isError?: boolean }> {
543
- const lineId = String(args.line_id ?? '').trim()
544
- const productId = String(args.product_id ?? '').trim()
545
- const recipeId = String(args.recipe_id ?? '').trim()
546
- if (!lineId || !productId || !recipeId) return amlErr('line_id/product_id/recipe_id 必填(隔离三元组;防配方串味)。')
547
- if (!args.dataset_spec || typeof args.dataset_spec !== 'object') return amlErr('dataset_spec 必填(AmlDatasetSpec:nodes 角色 control=DCW 输入,target=DAQ 输出;window/beatMs/split)。')
548
- try {
549
- const plan = createTrainingPlan({
550
- name: String(args.name ?? '').trim() || `建模任务-${Date.now().toString(36)}`,
551
- lineId, productId, recipeId,
552
- sceneId: String(args.scene_id ?? '').trim() || undefined,
553
- sceneVersion: String(args.scene_version ?? '').trim() || undefined,
554
- objectiveId: String(args.objective_id ?? '').trim() || undefined,
555
- strategy: args.strategy === 'manual' ? 'manual' : 'auto',
556
- datasetSpec: args.dataset_spec as never,
557
- trainingSpec: args.training_spec as never,
558
- modelNamePrefix: String(args.model_name_prefix ?? '').trim() || undefined,
559
- params: args.params,
560
- seed: Number.isFinite(Number(args.seed)) ? Number(args.seed) : undefined,
561
- minIntervalSec: Number.isFinite(Number(args.min_interval_sec)) ? Number(args.min_interval_sec) : undefined,
562
- createdBy: agentId,
563
- })
564
- return { text: `建模任务已创建:\n id: ${plan.id}\n 名称: ${plan.name}(${plan.strategy})\n 谱系: 产线 ${plan.lineId}/产品 ${plan.productId}/配方 ${plan.recipeId}\n 训练: ${plan.trainingSpecJson ? 'spec 已固化,新批次到达将自动修正训练' : 'spec 未固化 —— 先完成 twin_physics_spec_draft/compile 流程后固化(REST PATCH training_spec);再 aml_training_plan_train 触发'}\n立即触发:aml_training_plan_train { plan_id: "${plan.id}" }。` }
565
- }
566
- catch (err) {
567
- return amlErr(`创建失败:${err instanceof Error ? err.message : String(err)}`)
568
- }
569
- }
1
+ /**
2
+ * AML 作业与模型(aml_job_* / aml_leaderboard / aml_model_*)
3
+ * (由 server/services/workshop/agents/industrial-tools.ts 按职责拆出;内容逐行原文搬运)
4
+ */
5
+ import { AML_AUDIT_KIND, amlErr, amlJobCard, amlNodeName, amlNodeUnit, fmtAmlNum } from './aml-dataset-tools'
6
+ import { agentBadgeLabel } from '../agent-badge'
7
+ import { alignToGrid } from '../../aml/clean'
8
+ import { cancelJob, jobLogsTail, runtimeStatus, submitJob } from '../../aml/job-orchestrator'
9
+ import { getActiveLineRun } from '../../dcw/line-run'
10
+ import { getAgentNodeBindingRepo } from '../node-bindings.repo'
11
+ import { getAmlRuntime } from '../../aml/runtime'
12
+ import { getDaqNodeRepo } from '../../daq/daq-node.repo'
13
+ import { createTrainingPlan, listTrainingPlans, runPlanTraining } from '../../aml/twin/training-plans'
14
+ import { getDcwProductRepo } from '../../dcw/dcw-product.repo'
15
+ import { getDcwRecipeRepo } from '../../dcw/dcw-recipe.repo'
16
+ import { getToolApprovals } from '../tool-approvals'
17
+ import { claimToolResult } from '../../runtime/platform-notice'
18
+ import { predictProduction, readIoSpecFor } from '../../aml/predictor'
19
+ import { recordOps } from '../../ops/ops'
20
+ import { transitionModel } from '../../aml/model-registry'
21
+ import { resolveRefFaces } from './tool-verify'
22
+
23
+ /** 工具:aml_job_submit —— 提交训练作业(train.py 全文内联;归因 agentId;队列 FIFO) */
24
+ export async function toolAmlJobSubmit(agentId: string, args: {
25
+ dataset_id?: string
26
+ code?: string
27
+ change_note?: string
28
+ params?: Record<string, unknown>
29
+ seed?: number | string
30
+ purpose?: string
31
+ parent_experiment_id?: string
32
+ scene_id?: string
33
+ scene_version?: string
34
+ objective_id?: string
35
+ job_kind?: 'supervised' | 'physics_calibration' | 'hybrid_residual' | 'uncertainty_calibration'
36
+ physics_manifest?: Record<string, unknown>
37
+ physics_spec?: Record<string, unknown>
38
+ twin_snapshot?: Record<string, unknown>
39
+ objective_profile?: Record<string, unknown>
40
+ provider_id?: string
41
+ provider_version?: string
42
+ provider_hash?: string
43
+ provider_generation?: number | string
44
+ model_name?: string
45
+ model_description?: string
46
+ }): Promise<{ text: string, isError?: boolean }> {
47
+ const datasetId = String(args.dataset_id ?? '').trim()
48
+ const code = typeof args.code === 'string' ? args.code : ''
49
+ if (!datasetId) return amlErr('dataset_id 必填(aml_dataset_build 返回的 id)。')
50
+ const templateFallback = args.job_kind === 'hybrid_residual' && !code.trim()
51
+ if (!templateFallback && !code.trim()) return amlErr('code 必填:train.py 全文(单文件;契约见工具说明)。例外:job_kind=hybrid_residual 可省略 code,平台用内置参考训练器(物理参数校准→残差集成→UQ)。')
52
+ if (code.length > 512_000) return amlErr(`code 过大(${Math.round(code.length / 1024)}KB),上限 500KB;精简训练代码(模型定义/训练循环拆薄)后重试。`)
53
+ if (!templateFallback && !/\bimport\s+amlkit\b/.test(code)) {
54
+ return amlErr('code 必须 import amlkit(平台数据/进度/导出契约):amlkit.load_bundle(datasetPath) 取数、amlkit.report_progress(pct, note) 报进度、amlkit.export_torch_onnx(model, manifest) 导出。缺契约的作业必然失败。')
55
+ }
56
+ const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : args.purpose === 'mpc_surrogate' ? 'mpc_surrogate' : undefined
57
+ const changeNote = String(args.change_note ?? '').trim()
58
+ if (!changeNote) return amlErr('change_note 必填:单组件改动纪律 —— 每次实验只改一个组件(网络结构/特征/超参之一),写清改了什么与预期,实验谱系靠它归因。')
59
+ try {
60
+ const job = submitJob({
61
+ datasetId,
62
+ purpose,
63
+ changeNote,
64
+ params: args.params && typeof args.params === 'object' && !Array.isArray(args.params) ? args.params : undefined,
65
+ seed: Number.isInteger(Number(args.seed)) ? Number(args.seed) : undefined,
66
+ parentExperimentId: String(args.parent_experiment_id ?? '').trim() || undefined,
67
+ code,
68
+ sceneId: String(args.scene_id ?? '').trim() || undefined,
69
+ sceneVersion: String(args.scene_version ?? '').trim() || undefined,
70
+ objectiveId: String(args.objective_id ?? '').trim() || undefined,
71
+ jobKind: args.job_kind,
72
+ physicsManifest: args.physics_manifest,
73
+ physicsSpec: args.physics_spec,
74
+ twinSnapshot: args.twin_snapshot,
75
+ objectiveProfile: args.objective_profile,
76
+ providerId: String(args.provider_id ?? '').trim() || undefined,
77
+ providerVersion: String(args.provider_version ?? '').trim() || undefined,
78
+ providerHash: String(args.provider_hash ?? '').trim() || undefined,
79
+ providerGeneration: Number.isInteger(Number(args.provider_generation)) ? Number(args.provider_generation) : undefined,
80
+ modelName: String(args.model_name ?? '').trim() || undefined,
81
+ modelDescription: String(args.model_description ?? '').trim() || undefined,
82
+ agent: { id: agentId },
83
+ })
84
+ const st = await runtimeStatus()
85
+ return {
86
+ text: [
87
+ `训练作业已提交:`,
88
+ ` job_id: ${job.id}`,
89
+ ` dataset: ${job.datasetId} | purpose=${job.purpose} | seed=${fmtAmlNum(Number(job.budget.seed ?? 42), 0)} | change_note: ${changeNote}`,
90
+ args.model_name || args.model_description ? ` 模型标识: name=${String(args.model_name ?? '(缺省派生)')} | description=${String(args.model_description ?? '(缺省派生)').slice(0, 80) || '(缺省派生)'}` : ` 模型标识: 注册时自动派生「产线·配方·优化目标」label(可传 model_name/model_description 自拟)`,
91
+ ` 队列: 排队 ${st.queued} 个(含本作业)/ 在跑 ${st.running} 个 | python ${st.python.ok ? (st.python.version ?? 'ok') : `不可用(${st.python.reason ?? '?'};作业会失败,请联系用户修复环境)`}`,
92
+ `用 aml_job_status { job_id: "${job.id}" } 看状态/阶段/门禁,aml_job_logs { job_id: "${job.id}" } 看日志尾。作业完成后平台会自动回执结果到你的信箱并唤醒你(无需持续轮询);期间可继续其他任务。`,
93
+ ].join('\n'),
94
+ }
95
+ }
96
+ catch (err) {
97
+ return { text: `提交失败:${err instanceof Error ? err.message : String(err)}`, isError: true }
98
+ }
99
+ }
100
+
101
+ /** 工具:aml_job_status —— 作业状态(单作业详情 / 缺省列最近 10 个) */
102
+ export async function toolAmlJobStatus(agentId: string, args: { job_id?: string }): Promise<{ text: string, isError?: boolean }> {
103
+ const rt = getAmlRuntime()
104
+ const jobId = String(args.job_id ?? '').trim()
105
+ if (jobId) {
106
+ const job = rt.repo.job.get(jobId)
107
+ if (!job) return amlErr(`作业 ${jobId} 不存在(aml_job_status 不带参数可列出最近 10 个)。`)
108
+ // 终态经轮询面亲自看到 → 认领结果所有权,取消平台的事后补送(先到先得,省一跳无谓回合)
109
+ if (['done', 'failed', 'cancelled'].includes(job.status)) claimToolResult('aml_job', jobId)
110
+ return { text: `${amlJobCard(job)}\n\n轮询建议:training 阶段 30~60s 查一次;终态(done/failed)后看门禁与错误,失败原因可 in aml_job_logs 定位。训练完成后平台也会自动回执到你的信箱,无需持续轮询。` }
111
+ }
112
+ // 无 job_id 的清单必须按调用者可读面过滤(2026-10-18):此前返回全平台最近 10 个,
113
+ // 任意零绑定 Agent 可读到其他产线/频道的作业 id 与数据集谱系。
114
+ const readableLines = resolveRefFaces(agentId).readableLines
115
+ const visible = (job: { datasetId: string, agentId?: string }): boolean => {
116
+ if (job.agentId === agentId) return true
117
+ const ds = rt.repo.dataset.get(job.datasetId)
118
+ return !!ds && readableLines.has(ds.lineId)
119
+ }
120
+ const jobs = rt.repo.job.list({ limit: 10 }).filter(visible)
121
+ if (jobs.length === 0) {
122
+ return {
123
+ text: `你的可读范围内没有训练作业(可读产线:${readableLines.size ? [...readableLines].join(', ') : '(无)'})。路径:aml_node_catalog 确认节点 → aml_dataset_build 组数据集 → aml_job_submit 提交训练;如需查看他线作业,请让用户授予对应产线权限。`,
124
+ }
125
+ }
126
+ for (const j of jobs) {
127
+ if (['done', 'failed', 'cancelled'].includes(j.status)) claimToolResult('aml_job', j.id)
128
+ }
129
+ const rows = jobs.map(j => `- ${j.id} [${j.status}] ${j.stage || '-'} ${j.progress}% dataset=${j.datasetId}${j.error ? ` | 错误: ${j.error.slice(0, 100)}` : ''}`)
130
+ return { text: `最近 ${jobs.length} 个作业(新→旧):\n${rows.join('\n')}\n\n传 job_id 查看单作业详情(指标/门禁逐项/错误)。训练完成后平台会自动回执到你的信箱。` }
131
+ }
132
+
133
+ /** 工具:aml_job_logs —— 作业日志尾随(平台 ##AML 协议行 + 训练输出) */
134
+ export async function toolAmlJobLogs(_agentId: string, args: { job_id?: string, lines?: number | string }): Promise<{ text: string, isError?: boolean }> {
135
+ const jobId = String(args.job_id ?? '').trim()
136
+ if (!jobId) return amlErr('job_id 必填(aml_job_status 不带参数可列出最近作业)。')
137
+ const job = getAmlRuntime().repo.job.get(jobId)
138
+ if (!job) return amlErr(`作业 ${jobId} 不存在。`)
139
+ const n = Math.min(Math.max(Number(args.lines) || 80, 5), 400)
140
+ const rows = jobLogsTail(jobId, n)
141
+ if (rows.length === 0) {
142
+ return { text: `作业 ${jobId} 暂无日志(${job.status === 'queued' ? '仍在排队,开跑后产生日志' : '日志未落盘或已被清理'});稍后重试或用 aml_job_status 看状态。` }
143
+ }
144
+ return { text: `作业 ${jobId} 日志尾(最近 ${rows.length} 行,时间正序;##AML 前缀行 = 平台协议事件):\n${rows.join('\n')}` }
145
+ }
146
+
147
+ /** 工具:aml_job_cancel —— 取消排队/运行中的作业(归因入册) */
148
+ export async function toolAmlJobCancel(agentId: string, args: { job_id?: string }): Promise<{ text: string, isError?: boolean }> {
149
+ const jobId = String(args.job_id ?? '').trim()
150
+ if (!jobId) return amlErr('job_id 必填。')
151
+ const rt = getAmlRuntime()
152
+ const job = rt.repo.job.get(jobId)
153
+ if (!job) return amlErr(`作业 ${jobId} 不存在。`)
154
+ if (['done', 'failed', 'cancelled', 'timeout'].includes(job.status)) {
155
+ return amlErr(`作业 ${jobId} 已是终态(${job.status}),无需取消;aml_job_status 复核。`)
156
+ }
157
+ const ok = cancelJob(jobId, { id: agentId, kind: 'agent' })
158
+ if (!ok) return amlErr(`取消失败:作业 ${jobId} 不在排队/运行中(可能恰好结束);aml_job_status 复核。`)
159
+ const dataset = rt.repo.dataset.get(job.datasetId)
160
+ recordOps({
161
+ actor: agentId,
162
+ actorName: agentBadgeLabel(agentId),
163
+ actorKind: 'agent',
164
+ action: 'aml.job.cancel',
165
+ kind: AML_AUDIT_KIND,
166
+ summary: `取消训练作业 ${jobId}(原状态 ${job.status})`,
167
+ targetKind: 'aml_job',
168
+ targetId: jobId,
169
+ lineId: dataset?.lineId,
170
+ productId: dataset?.productId,
171
+ recipeId: dataset?.recipeId,
172
+ })
173
+ return { text: `作业 ${jobId} 已取消(运行中的会终止进程树)。数据集 ${job.datasetId} 仍可复用;需要重训直接 aml_job_submit。` }
174
+ }
175
+
176
+ /** 工具:aml_leaderboard —— 数据集的实验谱系与门禁对照(标当前最优) */
177
+ export async function toolAmlLeaderboard(_agentId: string, args: { dataset_id?: string }): Promise<{ text: string, isError?: boolean }> {
178
+ const datasetId = String(args.dataset_id ?? '').trim()
179
+ if (!datasetId) return amlErr('dataset_id 必填(aml_dataset_build 返回的 id)。')
180
+ const rt = getAmlRuntime()
181
+ const dataset = rt.repo.dataset.get(datasetId)
182
+ if (!dataset) return amlErr(`数据集 ${datasetId} 不存在。`)
183
+ const exps = rt.repo.experiment.listByDataset(datasetId, 50)
184
+ if (exps.length === 0) {
185
+ return { text: `数据集 ${datasetId}(${dataset.rowCount} 行)尚无实验:aml_job_submit 提交训练后,此处给出谱系与门禁对照。` }
186
+ }
187
+ const parsed = exps.map((e) => {
188
+ let metrics: { oneStepTest?: { nrmse?: number }, rolloutTest?: { nrmse?: number } } = {}
189
+ let gates: { passed?: boolean } = {}
190
+ try {
191
+ metrics = JSON.parse(e.metricsJson)
192
+ }
193
+ catch { /* 无指标 */ }
194
+ try {
195
+ gates = JSON.parse(e.gatesJson)
196
+ }
197
+ catch { /* 无门禁 */ }
198
+ return { e, g1: metrics.oneStepTest?.nrmse ?? null, g2: metrics.rolloutTest?.nrmse ?? null, passed: gates.passed === true || e.status === 'gates_passed' }
199
+ })
200
+ const passers = parsed.filter(p => p.passed && p.g1 != null)
201
+ const bestId = passers.length > 0 ? passers.reduce((a, b) => ((a.g1 ?? Infinity) <= (b.g1 ?? Infinity) ? a : b)).e.id : null
202
+ const rows = parsed.map(p =>
203
+ `- ${p.e.id} [${p.e.status}] ${p.e.changeNote ? `「${p.e.changeNote.slice(0, 60)}」` : '(无备注)'}${p.e.parentExperimentId ? ` ← 父 ${p.e.parentExperimentId}` : ''} | G1=${fmtAmlNum(p.g1)}${p.g2 != null ? ` G2=${fmtAmlNum(p.g2)}` : ''} | 门禁 ${p.passed ? '通过' : '未通过'}${p.e.id === bestId ? ' ★当前最优' : ''}`)
204
+ const models = rt.repo.model.list({ limit: 100 }).filter(m => m.datasetId === datasetId)
205
+ const modelLines = models.map(m => `- ${m.id} [${m.stage}] 「${m.label || '(未打标)'}」 purpose=${m.purpose}${m.objectiveId ? ` objective=${m.objectiveId}` : ''}(实验 ${m.experimentId})`)
206
+ return {
207
+ text: [
208
+ `实验谱系(数据集 ${datasetId} · ${dataset.productId}/${dataset.recipeId} · ${exps.length} 条,新→旧):`,
209
+ ...rows,
210
+ modelLines.length > 0 ? `\n已登记模型(晋升对象;aml_leaderboard 最优 = 门禁通过且 G1 最小):` : null,
211
+ ...modelLines,
212
+ `\nG1=单步测试 NRMSE,G2=多步滚动 NRMSE(越小越好)。优化纪律:每次只改一个组件,change_note 写明;把最优模型推上生产用 aml_model_promote(lead 专属,需人工审批)。`,
213
+ ].filter(x => x !== null).join('\n'),
214
+ }
215
+ }
216
+
217
+ /** 工具:aml_model_promote —— 模型晋升 shadow/production(lead 专属;HITL 人工审批;流转入册由 transitionModel 内部完成) */
218
+ export async function toolAmlModelPromote(agentId: string, args: { model_id?: string, to_stage?: string }): Promise<{ text: string, isError?: boolean }> {
219
+ // 治理硬守卫:晋升仅限 lead 实例(注入层过滤 + 分发层双重校验,HTTP 直调不可绕过)
220
+ const roleRow = getAmlRuntime().db.prepare('SELECT role FROM channel_agents WHERE id = ? AND enabled = 1').get(agentId) as { role?: string } | undefined
221
+ if (roleRow?.role !== 'lead') {
222
+ return amlErr(`aml_model_promote 为 lead 专属工具:实例 ${agentId}(role=${roleRow?.role ?? '未知'})无权晋升模型。请由团队 lead 发起,晋升需人工 HITL 批准。`)
223
+ }
224
+ const modelId = String(args.model_id ?? '').trim()
225
+ const toStage = String(args.to_stage ?? '').trim()
226
+ if (!modelId) return amlErr('model_id 必填(aml_leaderboard 的「已登记模型」段可查)。')
227
+ if (toStage !== 'shadow' && toStage !== 'production') return amlErr('to_stage 必须为 \'shadow\'(影子观测)或 \'production\'(生产生效)。')
228
+ const rt = getAmlRuntime()
229
+ const model = rt.repo.model.get(modelId)
230
+ if (!model) return amlErr(`模型 ${modelId} 不存在。`)
231
+ if (model.stage === toStage) return amlErr(`模型 ${modelId} 已是 ${toStage},无需流转。`)
232
+ // production 预检:同组 (product, recipe, purpose) 旧生产将被退役
233
+ let oldProdId: string | undefined
234
+ if (toStage === 'production') {
235
+ oldProdId = rt.repo.model
236
+ .list({ stage: 'production', productId: model.productId, recipeId: model.recipeId, limit: 50 })
237
+ .find(m => m.id !== modelId && m.purpose === model.purpose)?.id
238
+ }
239
+ // 同模型挂起审批去重(防审批面板堆积,与 dcw_control 同纪律)
240
+ const approvals = getToolApprovals()
241
+ if (approvals.hasPendingFor(agentId, modelId)) {
242
+ return amlErr(`模型 ${modelId} 已有一条待审批的晋升请求,请等待用户处理后再发起新请求。`)
243
+ }
244
+ const detail = `AML 模型晋升:${modelId} ${model.stage} → ${toStage}「${model.label || '(未打标)'}」(产线 ${model.lineId || rt.repo.dataset.get(model.datasetId)?.lineId || '?'} / 产品 ${model.productId} / 配方 ${model.recipeId} / purpose=${model.purpose}${model.objectiveId ? ` / objective=${model.objectiveId}` : ''}${oldProdId ? `;批准后原生产模型 ${oldProdId} 将自动退役` : ''})`
245
+ const ap = await approvals.request(agentId, modelId, 'dcw', detail, { title: 'AML 模型晋升审批' })
246
+ if (!ap.approved) {
247
+ return { text: `人工未批准:模型 ${modelId} 保持 ${model.stage} 阶段。用户备注:${ap.comment || '(无)'}。可回炉重训(aml_job_submit)或换候选模型后再申请。` }
248
+ }
249
+ try {
250
+ const r = transitionModel(modelId, toStage, agentBadgeLabel(agentId), 'agent')
251
+ // 归属审计已由 transitionModel 内部 recordOps(aml.model.promote)完成,此处不重复记录
252
+ return {
253
+ text: `晋升完成:${modelId} ${r.from} → ${toStage}。${r.retiredId ? `原生产模型 ${r.retiredId} 已自动退役。` : ''}${toStage === 'shadow'
254
+ ? '影子阶段:用 aml_model_reference 观测其预测与实际偏差,积累证据后再申请 production。'
255
+ : '该模型已作为生产模型生效:aml_model_reference 的影子参考即来自它;调控 Agent 调参前请先查它。'}`,
256
+ }
257
+ }
258
+ catch (err) {
259
+ return { text: `晋升失败:${err instanceof Error ? err.message : String(err)}(审批已通过;若为并发竞态可刷新状态重试)。`, isError: true }
260
+ }
261
+ }
262
+
263
+ /** 工具:aml_model_find —— 模型发现与选型(按产线/产品/配方/目标/阶段过滤;复用 vs 新建决策指引) */
264
+ export async function toolAmlModelFind(agentId: string, args: {
265
+ line_id?: string
266
+ product_id?: string
267
+ recipe_id?: string
268
+ purpose?: string
269
+ objective_id?: string
270
+ stage?: string
271
+ }): Promise<{ text: string, isError?: boolean }> {
272
+ // 0. 绑定面:与 aml_model_reference 同纪律 —— 至少一个 daq 绑定;line_id 只缩不放
273
+ const repo = getAgentNodeBindingRepo()
274
+ const daqBindings = repo.byAgent(agentId).filter(b => b.kind === 'daq')
275
+ if (daqBindings.length === 0) {
276
+ return amlErr('你尚未绑定任何数采节点,无法确定可查的产线范围;请先绑定数采节点。')
277
+ }
278
+ const lineOf = (id: string): string => getDaqNodeRepo().byId(id)?.lineId ?? ''
279
+ const scopeLines = [...new Set(daqBindings.map(b => lineOf(b.nodeId)).filter(Boolean))]
280
+ const lineFilter = String(args.line_id ?? '').trim()
281
+ if (lineFilter && !scopeLines.includes(lineFilter)) {
282
+ return amlErr(`产线 ${lineFilter} 不在你的负责范围(你绑定节点覆盖:${scopeLines.join(', ') || '无'};绑定只缩不放)。`)
283
+ }
284
+ const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : args.purpose === 'mpc_surrogate' ? 'mpc_surrogate' : undefined
285
+ const stage = ['candidate', 'shadow', 'production', 'retired'].includes(String(args.stage ?? '')) ? String(args.stage) : undefined
286
+ const productFilter = String(args.product_id ?? '').trim() || undefined
287
+ const recipeFilter = String(args.recipe_id ?? '').trim() || undefined
288
+ const objectiveFilter = String(args.objective_id ?? '').trim() || undefined
289
+
290
+ // 1. 拉全量后在代码层过滤(模型行 line_id 对历史行为空,经数据集回补;绑定只缩不放)
291
+ const rt = getAmlRuntime()
292
+ const models = rt.repo.model.list({ stage, purpose, limit: 200 }).filter((m) => {
293
+ const ds = rt.repo.dataset.get(m.datasetId)
294
+ const modelLine = m.lineId || ds?.lineId || ''
295
+ if (lineFilter ? modelLine !== lineFilter : (modelLine && !scopeLines.includes(modelLine))) return false
296
+ if (productFilter && m.productId !== productFilter) return false
297
+ if (recipeFilter && m.recipeId !== recipeFilter) return false
298
+ if (objectiveFilter && m.objectiveId !== objectiveFilter) return false
299
+ return true
300
+ })
301
+ if (models.length === 0) {
302
+ return {
303
+ text: [
304
+ `过滤条件下(${[lineFilter || `产线∈{${scopeLines.join(', ')}}`, productFilter, recipeFilter, purpose, objectiveFilter && `objective=${objectiveFilter}`, stage].filter(Boolean).join(', ')})没有匹配模型。`,
305
+ `新建路径:aml_node_catalog → aml_dataset_build(绑定产线的配方批次组数据集)→ aml_job_submit 提交训练(建议带 model_name/model_description 打标)。`,
306
+ `注意:模型按 (产品, 配方, purpose) 隔离 —— 换配方必须为该配方另建模型,不可跨配方复用。`,
307
+ ].join('\n'),
308
+ }
309
+ }
310
+
311
+ // 2. 分组呈现:production 置顶(可直接复用)→ shadow/candidate(晋升对象)→ retired 计数
312
+ const fmtG = (metricsJson: string): string => {
313
+ try {
314
+ const m = JSON.parse(metricsJson) as { oneStepTest?: { nrmse?: number }, rolloutTest?: { nrmse?: number } }
315
+ return `G1=${fmtAmlNum(m.oneStepTest?.nrmse ?? null)}${m.rolloutTest?.nrmse != null ? ` G2=${fmtAmlNum(m.rolloutTest.nrmse)}` : ''}`
316
+ }
317
+ catch { return 'G1=- G2=-' }
318
+ }
319
+ const prods = models.filter(m => m.stage === 'production')
320
+ const cands = models.filter(m => m.stage === 'shadow' || m.stage === 'candidate')
321
+ const retiredCount = models.filter(m => m.stage === 'retired').length
322
+ const rowOf = (m: typeof models[number]): string => {
323
+ const lineId = m.lineId || rt.repo.dataset.get(m.datasetId)?.lineId || '?'
324
+ const desc = m.description ? ` | ${m.description.slice(0, 120)}` : ''
325
+ return `- ${m.id} [${m.stage}] 「${m.label || '(未打标)'}」 purpose=${m.purpose}${m.objectiveId ? ` objective=${m.objectiveId}` : ''} | 产线 ${lineId} | ${fmtG(m.metricsJson)}${desc}`
326
+ }
327
+ const sections: string[] = []
328
+ sections.push(`匹配 ${models.length} 个模型(过滤:${[lineFilter || `产线∈{${scopeLines.join(', ')}}`, productFilter, recipeFilter, purpose, objectiveFilter && `objective=${objectiveFilter}`, stage].filter(Boolean).join(', ')})`)
329
+ if (prods.length > 0) {
330
+ sections.push(`\n生产模型(可直接复用):`)
331
+ sections.push(...prods.map(rowOf))
332
+ }
333
+ if (cands.length > 0) {
334
+ sections.push(`\n候选/影子(晋升对象;aml_model_promote 为 lead 专属,需人工审批):`)
335
+ sections.push(...cands.map(rowOf))
336
+ }
337
+ if (retiredCount > 0) sections.push(`\n另有 ${retiredCount} 个已退役模型(stage 过滤可查)。`)
338
+
339
+ // 3. 决策指引:按本次优化目标给出 复用 / 晋升 / 新建 三分支
340
+ const tips: string[] = []
341
+ if (prods.some(m => (!productFilter || m.productId === productFilter) && (!recipeFilter || m.recipeId === recipeFilter) && (!purpose || m.purpose === purpose))) {
342
+ tips.push(`已有匹配生产模型 → 调参前先 aml_model_reference 问影子参考;预测与实际偏差大再考虑重训(换配方/换目标才需要新模型)。`)
343
+ }
344
+ else if (cands.length > 0) {
345
+ tips.push(`无生产模型但有候选 → 评估候选指标后由 lead 走 aml_model_promote(shadow 先行,production 需人工审批)。`)
346
+ }
347
+ else {
348
+ tips.push(`无可用模型 → 走新建:aml_dataset_build 组该配方数据集 → aml_job_submit(带 model_name/model_description)→ 门禁通过后晋升。`)
349
+ }
350
+ tips.push(`模型身份规则:每个模型的 label/description 强制携带 产线·配方·优化目标(注册时平台派生,aml_job_submit 可用 model_name/model_description 补充);不同 recipe 的模型互不通用,选型先对齐配方与目标。`)
351
+ return { text: [...sections, `\n选型指引:`, ...tips.map(t => `- ${t}`)].join('\n') }
352
+ }
353
+
354
+ /** 工具:aml_model_reference —— 生产模型影子参考:拉最近历史组装模型输入 → 预测未来目标轨迹(调参前先问影子模型) */
355
+ export async function toolAmlModelReference(agentId: string, args: {
356
+ line_id?: string
357
+ product_id?: string
358
+ recipe_id?: string
359
+ purpose?: string
360
+ controls?: Record<string, number | string>
361
+ steps?: number | string
362
+ }): Promise<{ text: string, isError?: boolean }> {
363
+ // 0. 绑定面:至少一个 daq 绑定;line_id 只缩不放
364
+ const repo = getAgentNodeBindingRepo()
365
+ const daqBindings = repo.byAgent(agentId).filter(b => b.kind === 'daq')
366
+ if (daqBindings.length === 0) {
367
+ return amlErr('你尚未绑定任何数采节点,无法组装影子参考所需的历史输入;请先绑定数采节点。')
368
+ }
369
+ const lineOf = (id: string): string => getDaqNodeRepo().byId(id)?.lineId ?? ''
370
+ const scopeLines = [...new Set(daqBindings.map(b => lineOf(b.nodeId)).filter(Boolean))]
371
+ const lineFilter = String(args.line_id ?? '').trim()
372
+ if (lineFilter && !scopeLines.includes(lineFilter)) {
373
+ return amlErr(`产线 ${lineFilter} 不在你的负责范围(你绑定节点覆盖:${scopeLines.join(', ') || '无'};绑定只缩不放)。`)
374
+ }
375
+ const purpose = args.purpose === 'quality_predict' ? 'quality_predict' : 'mpc_surrogate'
376
+
377
+ // 1. 解析 (productId, recipeId):参数给全用参数;缺省回退到候选产线的唯一活动批次
378
+ let productId = String(args.product_id ?? '').trim()
379
+ let recipeId = String(args.recipe_id ?? '').trim()
380
+ let resolveHow = '参数指定'
381
+ if (!productId || !recipeId) {
382
+ const candidateLines = lineFilter ? [lineFilter] : scopeLines
383
+ const runs = candidateLines
384
+ .map(lid => getActiveLineRun(lid))
385
+ .filter((r): r is NonNullable<ReturnType<typeof getActiveLineRun>> => !!r && !!r.productId && !!r.recipeId)
386
+ if (runs.length === 1) {
387
+ const run = runs[0]!
388
+ if (!productId) productId = run.productId
389
+ if (!recipeId) recipeId = run.recipeId
390
+ resolveHow = `产线 ${run.lineId} 唯一活动批次 ${run.runId.slice(0, 8)}(产品「${run.productName}」/配方「${run.recipeName}」)`
391
+ }
392
+ else if (runs.length === 0) {
393
+ return { text: `无法确定产品/配方:${candidateLines.length > 0 ? `产线 ${candidateLines.join(', ')}` : '你绑定节点所属产线'}当前无活动批次,且 product_id/recipe_id 未给全。请开跑后重试,或显式传 product_id 与 recipe_id(line_context 可查)。`, isError: true }
394
+ }
395
+ else {
396
+ return { text: `无法确定产品/配方:候选产线存在多个活动批次(${runs.map(r => `${r.lineId} → ${r.productName}/${r.recipeName}`).join('; ')})。请传 line_id 缩小范围,或显式传 product_id 与 recipe_id。`, isError: true }
397
+ }
398
+ }
399
+
400
+ // 2. 生产模型(无则指引建模范式)
401
+ const rt = getAmlRuntime()
402
+ const model = rt.repo.model.productionOf(productId, recipeId, purpose)
403
+ if (!model) {
404
+ const cands = rt.repo.model.list({ productId, recipeId, limit: 20 })
405
+ return { text: `尚无生产模型:(${productId}, ${recipeId}, ${purpose}) 下没有 stage=production 的模型。${cands.length > 0 ? `现有候选:${cands.map(m => `${m.id}「${m.label || '(未打标)'}」(${m.stage})`).join(', ')} —— 请 lead 用 aml_model_promote 晋升(需人工审批)。` : '该三元组下还没有任何模型:aml_dataset_build → aml_job_submit 训练,门禁通过后申请晋升。在此之前调参请依据 dcw_journal 历史经验与 daq_query 证据。'}` }
406
+ }
407
+ // 3. 产线归属鉴权(绑定只缩不放;模型归属线经数据集追溯)
408
+ const ds = rt.repo.dataset.get(model.datasetId)
409
+ const modelLine = ds?.lineId ?? ''
410
+ if (modelLine && !scopeLines.includes(modelLine)) {
411
+ return amlErr(`模型 ${model.id} 归属产线 ${modelLine},不在你绑定节点覆盖的产线(${scopeLines.join(', ')})内;绑定只缩不放。`)
412
+ }
413
+
414
+ // 4. io 契约 → 拉最近历史 → 网格对齐 → 组 [H][nAll](原始物理量,顺序 = allNodes)
415
+ const io = readIoSpecFor(model.id)
416
+ const H = io.historySteps
417
+ const beatMs = io.beatMs
418
+ const now = Date.now()
419
+ const fromMs = now - H * beatMs - 5 * beatMs
420
+ const { getTsdb, tsdbReady } = await import('../../daq/storage')
421
+ await tsdbReady
422
+ const tsdb = getTsdb()
423
+ const columns = new Map<string, number[]>()
424
+ const shortage: string[] = []
425
+ for (const nodeId of io.allNodes) {
426
+ try {
427
+ const pts = await tsdb.query(nodeId, { fromMs, toMs: now, limit: Math.min(Math.max(H * 10, 600), 5000) })
428
+ const { grid } = alignToGrid(
429
+ pts.map(p => ({ at: p.at, value: p.value ?? p.avg ?? 0 })).filter(p => Number.isFinite(p.value)),
430
+ { beatMs, maxInterpMs: beatMs * 2 },
431
+ )
432
+ const tail = grid.slice(-H).map(g => g.value)
433
+ if (tail.length < H) {
434
+ shortage.push(`${amlNodeName(nodeId)}(${nodeId})仅 ${tail.length}/${H} 格点`)
435
+ continue
436
+ }
437
+ columns.set(nodeId, tail)
438
+ }
439
+ catch (err) {
440
+ shortage.push(`${amlNodeName(nodeId)}(${nodeId})查询失败:${err instanceof Error ? err.message : String(err)}`)
441
+ }
442
+ }
443
+ if (shortage.length > 0) {
444
+ return { text: `历史数据不足,无法组装模型输入:${shortage.join(';')}。需要从 ${new Date(fromMs).toISOString().slice(0, 16)} 起连续约 ${(H * beatMs / 60_000).toFixed(0)} 分钟的运行数据(产线须在跑);等数据积累后重试,或缩短 steps/换更短 history 的模型。`, isError: true }
445
+ }
446
+ const history: number[][] = []
447
+ for (let i = 0; i < H; i++) history.push(io.allNodes.map(n => columns.get(n)![i]!))
448
+
449
+ // 5. 控制轨迹:controls 映射(节点 id → 值,常数外推 steps 行);缺省行持最后观测
450
+ const steps = Math.max(1, Math.min(Number(args.steps) || io.horizonSteps, io.horizonSteps))
451
+ const lastRow = history[history.length - 1]!
452
+ const unknownKeys = args.controls ? Object.keys(args.controls).filter(k => !io.controlNodes.includes(k)) : []
453
+ const controlRow = io.controlNodes.map((c) => {
454
+ const provided = args.controls?.[c]
455
+ const v = provided != null ? Number(provided) : Number.NaN
456
+ return Number.isFinite(v) ? v : lastRow[io.allNodes.indexOf(c)]!
457
+ })
458
+ const controls: number[][] = Array.from({ length: steps }, () => [...controlRow])
459
+
460
+ // 6. 预测 + 人话报告
461
+ try {
462
+ const r = await predictProduction(productId, recipeId, purpose, { history, controls, steps })
463
+ const header = `步 | ${r.targetNodes.map(t => `${amlNodeName(t)}(${amlNodeUnit(t)})`).join(' | ')}`
464
+ const forecastLines = r.forecast.map((row, i) =>
465
+ ` ${i + 1}(+${fmtAmlNum(((i + 1) * r.beatMs) / 1000, 0)}s) | ${row.map(v => fmtAmlNum(v, 3)).join(' | ')}`)
466
+ const ctrlTxt = io.controlNodes.length > 0
467
+ ? io.controlNodes.map((c) => {
468
+ const provided = args.controls?.[c]
469
+ return `${amlNodeName(c)}=${Number.isFinite(Number(provided)) ? Number(provided) : `${fmtAmlNum(lastRow[io.allNodes.indexOf(c)], 3)}(持最后观测)`}${amlNodeUnit(c)}`
470
+ }).join(', ')
471
+ : '无控制节点'
472
+ return {
473
+ text: [
474
+ `影子模型参考(调参前先问影子模型;预测≠承诺,真实下发仍走 dcw_control 安全联锁):`,
475
+ ` 模型 ${r.modelId}「${model.label || '(未打标)'}」(stage=${r.stage},purpose=${purpose}${model.objectiveId ? `,objective=${model.objectiveId}` : ''})| 三元组:产品 ${getDcwProductRepo().byId(productId)?.name ?? productId} / 配方 ${getDcwRecipeRepo().byId(recipeId)?.name ?? recipeId}(解析:${resolveHow})`,
476
+ model.description ? ` 模型说明: ${model.description}` : null,
477
+ ` 输入:最近 ${H} 拍 × ${io.allNodes.length} 节点(beat=${beatMs}ms),截至 ${new Date(now).toISOString().slice(11, 19)};控制轨迹(共 ${steps} 步):${ctrlTxt}${unknownKeys.length > 0 ? `(已忽略非控制节点的键:${unknownKeys.join(', ')})` : ''}`,
478
+ ` 预测(steps=${steps},每拍 ${beatMs / 1000}s):`,
479
+ ` ${header}`,
480
+ ...forecastLines,
481
+ ` 假设: ${r.assumptions}`,
482
+ `\n用法:把候选设定值作为 controls 传入(键 = 控制节点 id)对比不同方案的预测轨迹;选定后 dcw_control 下发并 daq_query 复测。`,
483
+ ].filter(x => x !== null).join('\n'),
484
+ }
485
+ }
486
+ catch (err) {
487
+ return { text: `预测失败:${err instanceof Error ? err.message : String(err)}(模型 ${model.id};若为工件缺失可重训同规格数据集)。`, isError: true }
488
+ }
489
+ }
490
+
491
+ /* ═══ 建模任务(aml_training_plan_*;2026-09-26 解耦计划:训练 Channel 的编排入口) ═══ */
492
+
493
+ /** 工具:aml_training_plan_list —— 建模任务清单(状态/最新模型/新批次检测基数) */
494
+ export async function toolAmlTrainingPlanList(agentId: string, _args: Record<string, unknown>): Promise<{ text: string, isError?: boolean }> {
495
+ // 按调用者可读面过滤(2026-10-18):此前 listTrainingPlans() 全表返回,
496
+ // 任意 Agent 可枚举其他产线的计划/模型/作业 id。
497
+ const readableLines = resolveRefFaces(agentId).readableLines
498
+ const plans = listTrainingPlans().filter(p => p.createdBy === agentId || readableLines.has(p.lineId))
499
+ if (plans.length === 0) {
500
+ return { text: `你的可读范围内没有建模任务(可读产线:${readableLines.size ? [...readableLines].join(', ') : '(无)'})。创建路径:AML 界面「建模任务」表单,或 aml_training_plan_create(绑定产线/产品/配方 + 数据集 IO 契约);auto 策略的任务在新批次数据到达时自动修正训练。` }
501
+ }
502
+ const lines = plans.map((p) => {
503
+ const specFixed = p.trainingSpecJson ? 'spec已固化' : 'spec未固化(需先完成 draft 流程后 PATCH 固化)'
504
+ return `- ${p.id}「${p.name}」[${p.enabled ? '启用' : '停用'}/${p.strategy}] 产线 ${p.lineId}·配方 ${p.recipeId}${p.objectiveId ? `·目标 ${p.objectiveId}` : ''} | 状态 ${p.lastStatus}${p.lastModelId ? ` → 模型 ${p.lastModelId}` : ''}${p.lastJobId ? `(作业 ${p.lastJobId})` : ''} | ${specFixed} | 批次基数 ${p.lastRunCount}`
505
+ })
506
+ return { text: ['建模任务清单:', ...lines].join('\n') }
507
+ }
508
+
509
+ /** 工具:aml_training_plan_train —— 立即触发一次修正训练(重建数据集含新批次 → 平台训练器) */
510
+ export async function toolAmlTrainingPlanTrain(agentId: string, args: { plan_id?: string }): Promise<{ text: string, isError?: boolean }> {
511
+ const planId = String(args.plan_id ?? '').trim()
512
+ if (!planId) {
513
+ const plans = listTrainingPlans()
514
+ if (plans.length === 0) return amlErr('尚无建模任务可触发。')
515
+ return amlErr(`plan_id 必填。可选:${plans.map(p => `${p.id}「${p.name}」`).join('; ')}`)
516
+ }
517
+ try {
518
+ const r = await runPlanTraining(planId, { id: agentId, kind: 'agent' })
519
+ return { text: `建模任务 ${planId} 修正训练已提交:\n 作业: ${r.jobId}\n 数据集: ${r.datasetId}(${r.rowCount} 行,含历史+新批次)\n用 aml_job_status 轮询;完成后模型自动入册,plan 状态回写,可在 AML 界面绑定到工艺优化 Channel。` }
520
+ }
521
+ catch (err) {
522
+ return amlErr(`触发失败:${err instanceof Error ? err.message : String(err)}`)
523
+ }
524
+ }
525
+
526
+ /** 工具:aml_training_plan_create —— 创建建模任务(绑定 recipe + 数据集 IO 契约 + 固化训练 spec) */
527
+ export async function toolAmlTrainingPlanCreate(agentId: string, args: {
528
+ name?: string
529
+ line_id?: string
530
+ product_id?: string
531
+ recipe_id?: string
532
+ scene_id?: string
533
+ scene_version?: string
534
+ objective_id?: string
535
+ strategy?: string
536
+ dataset_spec?: Record<string, unknown>
537
+ training_spec?: Record<string, unknown>
538
+ model_name_prefix?: string
539
+ params?: Record<string, unknown>
540
+ seed?: number | string
541
+ min_interval_sec?: number | string
542
+ }): Promise<{ text: string, isError?: boolean }> {
543
+ const lineId = String(args.line_id ?? '').trim()
544
+ const productId = String(args.product_id ?? '').trim()
545
+ const recipeId = String(args.recipe_id ?? '').trim()
546
+ if (!lineId || !productId || !recipeId) return amlErr('line_id/product_id/recipe_id 必填(隔离三元组;防配方串味)。')
547
+ if (!args.dataset_spec || typeof args.dataset_spec !== 'object') return amlErr('dataset_spec 必填(AmlDatasetSpec:nodes 角色 control=DCW 输入,target=DAQ 输出;window/beatMs/split)。')
548
+ try {
549
+ const plan = createTrainingPlan({
550
+ name: String(args.name ?? '').trim() || `建模任务-${Date.now().toString(36)}`,
551
+ lineId, productId, recipeId,
552
+ sceneId: String(args.scene_id ?? '').trim() || undefined,
553
+ sceneVersion: String(args.scene_version ?? '').trim() || undefined,
554
+ objectiveId: String(args.objective_id ?? '').trim() || undefined,
555
+ strategy: args.strategy === 'manual' ? 'manual' : 'auto',
556
+ datasetSpec: args.dataset_spec as never,
557
+ trainingSpec: args.training_spec as never,
558
+ modelNamePrefix: String(args.model_name_prefix ?? '').trim() || undefined,
559
+ params: args.params,
560
+ seed: Number.isFinite(Number(args.seed)) ? Number(args.seed) : undefined,
561
+ minIntervalSec: Number.isFinite(Number(args.min_interval_sec)) ? Number(args.min_interval_sec) : undefined,
562
+ createdBy: agentId,
563
+ })
564
+ return { text: `建模任务已创建:\n id: ${plan.id}\n 名称: ${plan.name}(${plan.strategy})\n 谱系: 产线 ${plan.lineId}/产品 ${plan.productId}/配方 ${plan.recipeId}\n 训练: ${plan.trainingSpecJson ? 'spec 已固化,新批次到达将自动修正训练' : 'spec 未固化 —— 先完成 twin_physics_spec_draft/compile 流程后固化(REST PATCH training_spec);再 aml_training_plan_train 触发'}\n立即触发:aml_training_plan_train { plan_id: "${plan.id}" }。` }
565
+ }
566
+ catch (err) {
567
+ return amlErr(`创建失败:${err instanceof Error ? err.message : String(err)}`)
568
+ }
569
+ }
570
+
571
+ /** 工具:aml_model_annotate —— Agent 维护模型人读身份(名称/描述) */
572
+ export async function toolAmlModelAnnotate(agentId: string, args: {
573
+ model_id?: string
574
+ label?: string
575
+ description?: string
576
+ }): Promise<{ text: string, isError?: boolean }> {
577
+ const modelId = String(args.model_id ?? '').trim()
578
+ if (!modelId) return amlErr('model_id 必填(aml_leaderboard / aml_model_find 可查)。')
579
+ const label = typeof args.label === 'string' && args.label.trim() ? args.label.trim().slice(0, 200) : undefined
580
+ const description = typeof args.description === 'string' && args.description.trim() ? args.description.trim().slice(0, 500) : undefined
581
+ if (!label && !description) return amlErr('label / description 至少提供一个:它们是用户理解这个模型代表什么的主要入口。')
582
+ const rt = getAmlRuntime()
583
+ const model = rt.repo.model.get(modelId)
584
+ if (!model) return amlErr(`模型 ${modelId} 不存在(训练完成后才登记;aml_leaderboard 可查)。`)
585
+ rt.repo.model.updateIdentity(modelId, { label, description })
586
+ recordOps({
587
+ actor: agentId, actorName: agentId, actorKind: 'agent',
588
+ action: 'aml.model.annotate', kind: 'system',
589
+ summary: `模型 ${modelId} 人读身份更新(${[label ? 'label' : '', description ? 'description' : ''].filter(Boolean).join('+')})`,
590
+ targetKind: 'aml_model', targetId: modelId,
591
+ detail: { label, description },
592
+ })
593
+ const after = rt.repo.model.get(modelId)
594
+ return {
595
+ text: [
596
+ `模型 ${modelId} 人读身份已更新:`,
597
+ ` label: ${after?.label || '-'}`,
598
+ ` description: ${after?.description || '-'}`,
599
+ '用户与 Agent 都会在 /aml 模型台账看到这两项;阶段/工件/指标不受影响(不能借此绕过门禁)。',
600
+ ].join('\n'),
601
+ }
602
+ }