agim-cli 1.2.107 → 1.2.109

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 (175) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/dist/cli-ui/env-file.js +1 -1
  3. package/dist/cli-ui/env-file.js.map +1 -1
  4. package/dist/core/llm/agent-loop.d.ts +41 -0
  5. package/dist/core/llm/agent-loop.d.ts.map +1 -1
  6. package/dist/core/llm/agent-loop.js +231 -142
  7. package/dist/core/llm/agent-loop.js.map +1 -1
  8. package/dist/core/llm/mcp-client.d.ts +10 -0
  9. package/dist/core/llm/mcp-client.d.ts.map +1 -1
  10. package/dist/core/llm/mcp-client.js +35 -6
  11. package/dist/core/llm/mcp-client.js.map +1 -1
  12. package/dist/core/llm/mcp-registry.d.ts +4 -0
  13. package/dist/core/llm/mcp-registry.d.ts.map +1 -1
  14. package/dist/core/llm/mcp-registry.js +36 -10
  15. package/dist/core/llm/mcp-registry.js.map +1 -1
  16. package/dist/core/llm/openai-compat-provider.d.ts +28 -5
  17. package/dist/core/llm/openai-compat-provider.d.ts.map +1 -1
  18. package/dist/core/llm/openai-compat-provider.js +72 -12
  19. package/dist/core/llm/openai-compat-provider.js.map +1 -1
  20. package/dist/plugins/agents/native/index.d.ts.map +1 -1
  21. package/dist/plugins/agents/native/index.js +30 -0
  22. package/dist/plugins/agents/native/index.js.map +1 -1
  23. package/dist/web/public/assets/{a2a-DCgHD3C7.js → a2a-DjyqPnWh.js} +2 -2
  24. package/dist/web/public/assets/{a2a-DCgHD3C7.js.map → a2a-DjyqPnWh.js.map} +1 -1
  25. package/dist/web/public/assets/{activity-CqIdqoFx.js → activity-Dy3elxiH.js} +2 -2
  26. package/dist/web/public/assets/{activity-CqIdqoFx.js.map → activity-Dy3elxiH.js.map} +1 -1
  27. package/dist/web/public/assets/{admins-Cfm5YCFY.js → admins-p5qJnsCS.js} +2 -2
  28. package/dist/web/public/assets/{admins-Cfm5YCFY.js.map → admins-p5qJnsCS.js.map} +1 -1
  29. package/dist/web/public/assets/{agents-CXy1c3ml.js → agents-BK-LNF6u.js} +2 -2
  30. package/dist/web/public/assets/{agents-CXy1c3ml.js.map → agents-BK-LNF6u.js.map} +1 -1
  31. package/dist/web/public/assets/{approvals-DzfXTuHD.js → approvals-BR1f5zmP.js} +2 -2
  32. package/dist/web/public/assets/{approvals-DzfXTuHD.js.map → approvals-BR1f5zmP.js.map} +1 -1
  33. package/dist/web/public/assets/{asks-hv8KYkMv.js → asks-1MaTIfZO.js} +2 -2
  34. package/dist/web/public/assets/{asks-hv8KYkMv.js.map → asks-1MaTIfZO.js.map} +1 -1
  35. package/dist/web/public/assets/{audit-N2_Hb8ID.js → audit-jSgqqxdG.js} +2 -2
  36. package/dist/web/public/assets/{audit-N2_Hb8ID.js.map → audit-jSgqqxdG.js.map} +1 -1
  37. package/dist/web/public/assets/{bell-B2EC2Smm.js → bell-LeCqn72n.js} +2 -2
  38. package/dist/web/public/assets/{bell-B2EC2Smm.js.map → bell-LeCqn72n.js.map} +1 -1
  39. package/dist/web/public/assets/{bgjobs-Dnik8OzW.js → bgjobs-BPyKhOuq.js} +2 -2
  40. package/dist/web/public/assets/{bgjobs-Dnik8OzW.js.map → bgjobs-BPyKhOuq.js.map} +1 -1
  41. package/dist/web/public/assets/{brain-CBk_86fd.js → brain-CPuDSbdC.js} +2 -2
  42. package/dist/web/public/assets/{brain-CBk_86fd.js.map → brain-CPuDSbdC.js.map} +1 -1
  43. package/dist/web/public/assets/{briefcase-BqvjWo9V.js → briefcase-Ck3PTo9z.js} +2 -2
  44. package/dist/web/public/assets/{briefcase-BqvjWo9V.js.map → briefcase-Ck3PTo9z.js.map} +1 -1
  45. package/dist/web/public/assets/{chevron-right-JI5zm1vU.js → chevron-right-CNLUFBr6.js} +2 -2
  46. package/dist/web/public/assets/{chevron-right-JI5zm1vU.js.map → chevron-right-CNLUFBr6.js.map} +1 -1
  47. package/dist/web/public/assets/{circle-check-BoARt50L.js → circle-check-CsSqoqeq.js} +2 -2
  48. package/dist/web/public/assets/{circle-check-BoARt50L.js.map → circle-check-CsSqoqeq.js.map} +1 -1
  49. package/dist/web/public/assets/{circle-check-big-DElVlKvH.js → circle-check-big-NZvva0KN.js} +2 -2
  50. package/dist/web/public/assets/{circle-check-big-DElVlKvH.js.map → circle-check-big-NZvva0KN.js.map} +1 -1
  51. package/dist/web/public/assets/{circle-x-DO-2tGgt.js → circle-x-CFWz6gEd.js} +2 -2
  52. package/dist/web/public/assets/{circle-x-DO-2tGgt.js.map → circle-x-CFWz6gEd.js.map} +1 -1
  53. package/dist/web/public/assets/{confirm-dialog-WAkeu7Va.js → confirm-dialog-DgO--07o.js} +2 -2
  54. package/dist/web/public/assets/{confirm-dialog-WAkeu7Va.js.map → confirm-dialog-DgO--07o.js.map} +1 -1
  55. package/dist/web/public/assets/{data-table-BXUgaJFG.js → data-table-D-Zgmc4f.js} +2 -2
  56. package/dist/web/public/assets/{data-table-BXUgaJFG.js.map → data-table-D-Zgmc4f.js.map} +1 -1
  57. package/dist/web/public/assets/{dialog-C1_Gr4a-.js → dialog-CQgpA4K2.js} +2 -2
  58. package/dist/web/public/assets/{dialog-C1_Gr4a-.js.map → dialog-CQgpA4K2.js.map} +1 -1
  59. package/dist/web/public/assets/{download-DaQMiRdc.js → download-DKzhn49i.js} +2 -2
  60. package/dist/web/public/assets/{download-DaQMiRdc.js.map → download-DKzhn49i.js.map} +1 -1
  61. package/dist/web/public/assets/{email-DaE3P0Pi.js → email-j2wp12wu.js} +2 -2
  62. package/dist/web/public/assets/{email-DaE3P0Pi.js.map → email-j2wp12wu.js.map} +1 -1
  63. package/dist/web/public/assets/{empty-state--wTr0nEY.js → empty-state-C_hR--ro.js} +2 -2
  64. package/dist/web/public/assets/{empty-state--wTr0nEY.js.map → empty-state-C_hR--ro.js.map} +1 -1
  65. package/dist/web/public/assets/{external-link-BMsOzlAO.js → external-link-CoqEoAcO.js} +2 -2
  66. package/dist/web/public/assets/{external-link-BMsOzlAO.js.map → external-link-CoqEoAcO.js.map} +1 -1
  67. package/dist/web/public/assets/{eye-DyUOG2VV.js → eye-CXOBz4Cl.js} +2 -2
  68. package/dist/web/public/assets/{eye-DyUOG2VV.js.map → eye-CXOBz4Cl.js.map} +1 -1
  69. package/dist/web/public/assets/{facts-BsU_itZp.js → facts-CJDnig7y.js} +2 -2
  70. package/dist/web/public/assets/{facts-BsU_itZp.js.map → facts-CJDnig7y.js.map} +1 -1
  71. package/dist/web/public/assets/{goals-D5MmIEKE.js → goals-D89OCyJN.js} +2 -2
  72. package/dist/web/public/assets/{goals-D5MmIEKE.js.map → goals-D89OCyJN.js.map} +1 -1
  73. package/dist/web/public/assets/{health-BHHdheFW.js → health-BQwZeXAS.js} +2 -2
  74. package/dist/web/public/assets/{health-BHHdheFW.js.map → health-BQwZeXAS.js.map} +1 -1
  75. package/dist/web/public/assets/{heart-pulse-CHnf3dk9.js → heart-pulse-BzJZEKl7.js} +2 -2
  76. package/dist/web/public/assets/{heart-pulse-CHnf3dk9.js.map → heart-pulse-BzJZEKl7.js.map} +1 -1
  77. package/dist/web/public/assets/{heartbeat-wKQjksPH.js → heartbeat-D4kJCmkw.js} +2 -2
  78. package/dist/web/public/assets/{heartbeat-wKQjksPH.js.map → heartbeat-D4kJCmkw.js.map} +1 -1
  79. package/dist/web/public/assets/{hot-BF4yOa64.js → hot-R6aOoPMY.js} +2 -2
  80. package/dist/web/public/assets/{hot-BF4yOa64.js.map → hot-R6aOoPMY.js.map} +1 -1
  81. package/dist/web/public/assets/{index-CUlfCXYW.js → index-DVf2XlVZ.js} +34 -34
  82. package/dist/web/public/assets/index-DVf2XlVZ.js.map +1 -0
  83. package/dist/web/public/assets/{installed-ChSSxLpe.js → installed-DSJ7AiMe.js} +2 -2
  84. package/dist/web/public/assets/{installed-ChSSxLpe.js.map → installed-DSJ7AiMe.js.map} +1 -1
  85. package/dist/web/public/assets/{jobs-Bl_FJXMV.js → jobs-CdVDgq8m.js} +2 -2
  86. package/dist/web/public/assets/{jobs-Bl_FJXMV.js.map → jobs-CdVDgq8m.js.map} +1 -1
  87. package/dist/web/public/assets/{layout-rdDZbKKf.js → layout-BKjENLxQ.js} +2 -2
  88. package/dist/web/public/assets/{layout-rdDZbKKf.js.map → layout-BKjENLxQ.js.map} +1 -1
  89. package/dist/web/public/assets/{layout-CTgw-R_J.js → layout-BLKq93Z4.js} +2 -2
  90. package/dist/web/public/assets/{layout-CTgw-R_J.js.map → layout-BLKq93Z4.js.map} +1 -1
  91. package/dist/web/public/assets/{layout-B4VFa896.js → layout-BqilVxGS.js} +2 -2
  92. package/dist/web/public/assets/{layout-B4VFa896.js.map → layout-BqilVxGS.js.map} +1 -1
  93. package/dist/web/public/assets/{layout-BnTMi2Vm.js → layout-CvmElCgO.js} +2 -2
  94. package/dist/web/public/assets/{layout-BnTMi2Vm.js.map → layout-CvmElCgO.js.map} +1 -1
  95. package/dist/web/public/assets/{layout-DNcGVE9i.js → layout-Dj03GsQJ.js} +2 -2
  96. package/dist/web/public/assets/{layout-DNcGVE9i.js.map → layout-Dj03GsQJ.js.map} +1 -1
  97. package/dist/web/public/assets/{llm-2BhcOCUt.js → llm-C5Uob7Ci.js} +2 -2
  98. package/dist/web/public/assets/{llm-2BhcOCUt.js.map → llm-C5Uob7Ci.js.map} +1 -1
  99. package/dist/web/public/assets/{loader-circle-BeeODmoF.js → loader-circle-DdWIiO8L.js} +2 -2
  100. package/dist/web/public/assets/{loader-circle-BeeODmoF.js.map → loader-circle-DdWIiO8L.js.map} +1 -1
  101. package/dist/web/public/assets/{map-pin-Bib-61c8.js → map-pin-kO-wD4fU.js} +2 -2
  102. package/dist/web/public/assets/{map-pin-Bib-61c8.js.map → map-pin-kO-wD4fU.js.map} +1 -1
  103. package/dist/web/public/assets/{mcp-RhWfVYQA.js → mcp-DL5oUQgm.js} +2 -2
  104. package/dist/web/public/assets/{mcp-RhWfVYQA.js.map → mcp-DL5oUQgm.js.map} +1 -1
  105. package/dist/web/public/assets/{memos-D0aauhFt.js → memos-Ab9Rp5v_.js} +2 -2
  106. package/dist/web/public/assets/{memos-D0aauhFt.js.map → memos-Ab9Rp5v_.js.map} +1 -1
  107. package/dist/web/public/assets/{messengers-BWqF9z0b.js → messengers-D0a8jlLw.js} +2 -2
  108. package/dist/web/public/assets/{messengers-BWqF9z0b.js.map → messengers-D0a8jlLw.js.map} +1 -1
  109. package/dist/web/public/assets/{native-agent-Be7FW740.js → native-agent-Bnz7Inoh.js} +2 -2
  110. package/dist/web/public/assets/{native-agent-Be7FW740.js.map → native-agent-Bnz7Inoh.js.map} +1 -1
  111. package/dist/web/public/assets/{network-CGq_PEku.js → network-BrQvlSjl.js} +2 -2
  112. package/dist/web/public/assets/{network-CGq_PEku.js.map → network-BrQvlSjl.js.map} +1 -1
  113. package/dist/web/public/assets/{outbox-B8raDIpY.js → outbox-BNDPAlVy.js} +2 -2
  114. package/dist/web/public/assets/{outbox-B8raDIpY.js.map → outbox-BNDPAlVy.js.map} +1 -1
  115. package/dist/web/public/assets/{pagination-BaOx4wBN.js → pagination-CyoXKAm9.js} +2 -2
  116. package/dist/web/public/assets/{pagination-BaOx4wBN.js.map → pagination-CyoXKAm9.js.map} +1 -1
  117. package/dist/web/public/assets/{persona-DfJOKp-G.js → persona-Bxqqj1yV.js} +2 -2
  118. package/dist/web/public/assets/{persona-DfJOKp-G.js.map → persona-Bxqqj1yV.js.map} +1 -1
  119. package/dist/web/public/assets/{play-Bn15ljVu.js → play-B7ZN5aFX.js} +2 -2
  120. package/dist/web/public/assets/{play-Bn15ljVu.js.map → play-B7ZN5aFX.js.map} +1 -1
  121. package/dist/web/public/assets/{plus-ta7et8lU.js → plus-DzvDGrh0.js} +2 -2
  122. package/dist/web/public/assets/{plus-ta7et8lU.js.map → plus-DzvDGrh0.js.map} +1 -1
  123. package/dist/web/public/assets/{policy-BIPMjtiz.js → policy-CSl5ENVs.js} +2 -2
  124. package/dist/web/public/assets/{policy-BIPMjtiz.js.map → policy-CSl5ENVs.js.map} +1 -1
  125. package/dist/web/public/assets/{refresh-ccw-CweZfxgw.js → refresh-ccw-B8Fzl2uq.js} +2 -2
  126. package/dist/web/public/assets/{refresh-ccw-CweZfxgw.js.map → refresh-ccw-B8Fzl2uq.js.map} +1 -1
  127. package/dist/web/public/assets/{reminders-uWllha6u.js → reminders-CmLd8DLt.js} +2 -2
  128. package/dist/web/public/assets/{reminders-uWllha6u.js.map → reminders-CmLd8DLt.js.map} +1 -1
  129. package/dist/web/public/assets/{save-BsDR6-4Z.js → save-JKib3YIn.js} +2 -2
  130. package/dist/web/public/assets/{save-BsDR6-4Z.js.map → save-JKib3YIn.js.map} +1 -1
  131. package/dist/web/public/assets/{schedules-COWq8_0O.js → schedules-B-770HIa.js} +2 -2
  132. package/dist/web/public/assets/{schedules-COWq8_0O.js.map → schedules-B-770HIa.js.map} +1 -1
  133. package/dist/web/public/assets/{search-COTgXLCn.js → search-Bc7OS8FB.js} +2 -2
  134. package/dist/web/public/assets/{search-COTgXLCn.js.map → search-Bc7OS8FB.js.map} +1 -1
  135. package/dist/web/public/assets/{security-BCuwv4RN.js → security-CD8INSD5.js} +2 -2
  136. package/dist/web/public/assets/{security-BCuwv4RN.js.map → security-CD8INSD5.js.map} +1 -1
  137. package/dist/web/public/assets/{service-6x5gzuKT.js → service-CFB2QfK6.js} +2 -2
  138. package/dist/web/public/assets/{service-6x5gzuKT.js.map → service-CFB2QfK6.js.map} +1 -1
  139. package/dist/web/public/assets/{status-badge-CiY-R4cR.js → status-badge---MleYSt.js} +2 -2
  140. package/dist/web/public/assets/{status-badge-CiY-R4cR.js.map → status-badge---MleYSt.js.map} +1 -1
  141. package/dist/web/public/assets/{subtasks-QTIxglPc.js → subtasks-CzToK_65.js} +2 -2
  142. package/dist/web/public/assets/{subtasks-QTIxglPc.js.map → subtasks-CzToK_65.js.map} +1 -1
  143. package/dist/web/public/assets/{table-BQd1FahX.js → table-Dg3N5NYh.js} +2 -2
  144. package/dist/web/public/assets/{table-BQd1FahX.js.map → table-Dg3N5NYh.js.map} +1 -1
  145. package/dist/web/public/assets/{topn-D7mnFIN8.js → topn-CePuMziu.js} +2 -2
  146. package/dist/web/public/assets/{topn-D7mnFIN8.js.map → topn-CePuMziu.js.map} +1 -1
  147. package/dist/web/public/assets/{trash-2-CqpK78qO.js → trash-2-LmyUghia.js} +2 -2
  148. package/dist/web/public/assets/{trash-2-CqpK78qO.js.map → trash-2-LmyUghia.js.map} +1 -1
  149. package/dist/web/public/assets/{use-background-tasks-E1AqQHuk.js → use-background-tasks-CmVigPtZ.js} +2 -2
  150. package/dist/web/public/assets/{use-background-tasks-E1AqQHuk.js.map → use-background-tasks-CmVigPtZ.js.map} +1 -1
  151. package/dist/web/public/assets/{use-llm-admin-C5j8A81P.js → use-llm-admin-D8Go_PKM.js} +2 -2
  152. package/dist/web/public/assets/{use-llm-admin-C5j8A81P.js.map → use-llm-admin-D8Go_PKM.js.map} +1 -1
  153. package/dist/web/public/assets/{use-memory-e4psRiYj.js → use-memory-BDuQTDX0.js} +2 -2
  154. package/dist/web/public/assets/{use-memory-e4psRiYj.js.map → use-memory-BDuQTDX0.js.map} +1 -1
  155. package/dist/web/public/assets/{use-observability-xdJotn0L.js → use-observability-CiO9A5Z9.js} +2 -2
  156. package/dist/web/public/assets/{use-observability-xdJotn0L.js.map → use-observability-CiO9A5Z9.js.map} +1 -1
  157. package/dist/web/public/assets/{use-settings-DbvYjLo8.js → use-settings-fG28ShvX.js} +2 -2
  158. package/dist/web/public/assets/{use-settings-DbvYjLo8.js.map → use-settings-fG28ShvX.js.map} +1 -1
  159. package/dist/web/public/assets/{use-workspace-CXkiS0Ho.js → use-workspace-CE3JTeap.js} +2 -2
  160. package/dist/web/public/assets/{use-workspace-CXkiS0Ho.js.map → use-workspace-CE3JTeap.js.map} +1 -1
  161. package/dist/web/public/assets/{useQuery-CPWNEU8n.js → useQuery-BtY7wSLM.js} +2 -2
  162. package/dist/web/public/assets/{useQuery-CPWNEU8n.js.map → useQuery-BtY7wSLM.js.map} +1 -1
  163. package/dist/web/public/assets/{vector-B52Da4cl.js → vector-CakzHMP0.js} +2 -2
  164. package/dist/web/public/assets/{vector-B52Da4cl.js.map → vector-CakzHMP0.js.map} +1 -1
  165. package/dist/web/public/assets/{viewer-Fr6DcgPb.js → viewer-DWOOXMMI.js} +2 -2
  166. package/dist/web/public/assets/{viewer-Fr6DcgPb.js.map → viewer-DWOOXMMI.js.map} +1 -1
  167. package/dist/web/public/assets/{workspace-B9SHz-uT.js → workspace-BnlWyXR1.js} +2 -2
  168. package/dist/web/public/assets/{workspace-B9SHz-uT.js.map → workspace-BnlWyXR1.js.map} +1 -1
  169. package/dist/web/public/assets/{workspaces-DNPDEzPP.js → workspaces-DTXat9Xq.js} +2 -2
  170. package/dist/web/public/assets/{workspaces-DNPDEzPP.js.map → workspaces-DTXat9Xq.js.map} +1 -1
  171. package/dist/web/public/assets/{x-DqRovlI4.js → x-BVwIUHdL.js} +2 -2
  172. package/dist/web/public/assets/{x-DqRovlI4.js.map → x-BVwIUHdL.js.map} +1 -1
  173. package/dist/web/public/index.html +1 -1
  174. package/package.json +1 -1
  175. package/dist/web/public/assets/index-CUlfCXYW.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -4,7 +4,66 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
- ## [1.2.107] - 2026-05-29
7
+ ## [1.2.109] - 2026-05-29
8
+
9
+ ### Changed (native agent stability, batch P0b — gap ② closed)
10
+
11
+ - **agent-loop 支持工具调用并行(只读类按 caller 声明的白名单)。** 此前
12
+ `for (const call of result.toolCalls) { await dispatch(...) }` 强制把模型一次返回
13
+ 的多个工具调用串行起来——5 个独立 `native_read_file` 也要排队跑,长 turn 时延被
14
+ 线性放大。现在 `AgentLoopInput.parallelSafeTools?: ReadonlySet<string>` 让 caller
15
+ 声明哪些工具名是 parallel-safe(无副作用、不共享可变状态、顺序无关);同一迭代
16
+ 内声明安全的调用走 `Promise.allSettled` 并发,非声明的工具仍串行并在自身派发前
17
+ 把累计 pending 全部 drain(serial barrier,让串行调用看到一个 settled world)。
18
+ - **history 与 toolCalls 的发射严格按原始 call 顺序**——采用 slot 数组在派发完成
19
+ 后一次性按 index 提交,避免完成顺序与 tool_call_id 对齐失序(部分 OpenAI 兼容
20
+ provider 对此敏感)。
21
+ - **stuck-loop 和 goal-critic 检查相应移到批次发射之后**:仍正确触发(last-3
22
+ 比对 + critic 新增 `lastCriticAtToolCalls` 节流,按"至少每 N 次工具调用一次"
23
+ 的语义保持,不会因为一次 iteration 多个调用而误增触发频率)。
24
+ - **native 内置声明的 parallel-safe 集合**(`NATIVE_PARALLEL_SAFE_TOOLS`):
25
+ `native_echo` / `native_now` / `native_random_uuid` / `native_read_file` /
26
+ `native_list_dir` / `native_glob` / `native_grep` / `native_web_fetch` /
27
+ `native_web_search` / `mcp__imhub__{read_skill,list_skills,search_memos,
28
+ memory_list,memory_query}`。`native_exec` / `native_write_file` / 任何写类
29
+ memo / `push_message` / `ask_user` / `call_agent` / `long_task` /
30
+ `complete_goal` / 其他 MCP 均不在内(保持串行 + barrier)。
31
+
32
+ ### Tests
33
+
34
+ 4 个集成测试覆盖:并发耗时≈max(非 sum)、缺省 parallelSafeTools 完全串行(backwards-
35
+ compat)、完成顺序 ≠ 原始顺序时 history 仍按原始顺序、serial barrier 在 mixed 序列
36
+ 正确 drain。整套 P0a+P0b 测试 45/45 全绿;typecheck 干净。
37
+
38
+ ## [1.2.108] - 2026-05-29
39
+
40
+ ### Changed (native agent stability, batch P0a — 3 of 7 gaps closed)
41
+
42
+ - **provider 重试现在尊重 `Retry-After` 头 + 指数退避 + jitter(默认 3 次)。**
43
+ 之前是固定 250ms × attempt 线性 backoff、retry=1、不读 `Retry-After`,导致
44
+ DeepSeek / Moonshot 等 OpenAI-compat 提供方返回 429 时把服务器要求的几秒等待
45
+ 完全无视,250ms 后再撞墙、跳到 fallback provider,吃 cold-start + 模型语义切换。
46
+ 现在默认 4 次尝试(base 250ms、cap 30s、jitter ±20%),`Retry-After` 作为硬下限
47
+ (秒/HTTP-date 两种 RFC 7231 格式都支持)。`IMHUB_LLM_RETRY_MAX` 覆盖默认(clamp
48
+ [0,10])。导出 `parseRetryAfter` / `computeBackoffMs` 两个纯函数 + 13 个回归测试。
49
+ - **stuck-loop 早停升级为 `(name, argsKey, errorType)` 三元组。** v1.2.95 用的是
50
+ `(name, isError, preview[0..200])`:模型只要让结果文本带个时间戳/换行/计数器就
51
+ 绕过、绕到 max-iter 才停。新版用 `stableStringify(arguments)`(递归排序键,确定性
52
+ 哈希)+ `extractErrorType`(错误首行 ≤80 字符指纹)。同样的工具同样的参数同样的
53
+ 失败 3 次 → 停。导出 `stableStringify` / `extractErrorType` / `isStuckLoop` +
54
+ 16 个回归测试(含明确的"结果抖动不再隐藏死循环"对照)。
55
+ - **MCP tools 缓存加 TTL(默认 5 min)+ 重连改指数退避(base 5s, cap 60s,
56
+ 成功重置)。** v1.2.96 的 cachedTools 永不刷新——上游 MCP server 加新工具/改
57
+ schema 必须重启 agim 才看得见;重连冷却也是固定 5s,永远 broken 的 server 被
58
+ 5s 一次永久敲。现在:`McpClient.cachedTools` 带 `fetchedAt`,
59
+ `IMHUB_MCP_TOOLS_TTL_MS` 控制 TTL(0=不缓存),新增 `refresh()` 主动清缓存;
60
+ `mcp-registry.tryReconnect` 按 `state.reconnectFailures` 指数翻倍冷却,成功
61
+ 立即 reset 为 0。导出 `computeReconnectCooldownMs` + 8 个回归测试。
62
+
63
+ ### Tests
64
+
65
+ 37 new unit tests across 3 files;41 个 P0a 相关测试全绿;typecheck 干净。
66
+
8
67
 
9
68
  ### CI
10
69
 
@@ -198,9 +198,9 @@ export function updateEnvFile(updates) {
198
198
  }
199
199
  /** Wipe a single key. No-op if missing. */
200
200
  export function unsetEnvKey(key) {
201
+ assertEnvWriteAllowed();
201
202
  if (!existsSync(ENV_FILE))
202
203
  return;
203
- assertEnvWriteAllowed();
204
204
  const current = readEnvFile();
205
205
  if (!(key in current))
206
206
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/cli-ui/env-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/cli-ui/env-file.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,24 @@
1
1
  import type { FinishReason, LlmMessage, LlmProvider, LlmUsage, ToolCallRequest, ToolChoice, ToolDef } from './provider-base.js';
2
2
  import type { ToolDispatcher } from './tool-dispatcher.js';
3
+ /**
4
+ * Deterministic JSON.stringify with sorted object keys at every level.
5
+ * Used by the stuck-loop detector to derive a stable identity key for
6
+ * tool-call arguments: identical calls hash to identical strings
7
+ * regardless of key insertion order. Tolerates circular references and
8
+ * non-JSON-able values by collapsing them to a placeholder.
9
+ */
10
+ export declare function stableStringify(value: unknown, seen?: WeakSet<object>): string;
11
+ /**
12
+ * Stable error-type fingerprint of a dispatcher's result text. Returns
13
+ * the first non-empty line trimmed and truncated to 80 chars when
14
+ * isError; empty string otherwise. Used by the stuck-loop detector so
15
+ * "fs_read failed with ENOENT three times" → stuck, but "fs_read
16
+ * returned different bytes three times" → not stuck.
17
+ */
18
+ export declare function extractErrorType(isError: boolean, text: string): string;
19
+ /** True iff the last 3 entries are the same tool called with identical
20
+ * args resulting in the identical outcome — the stuck-loop signal. */
21
+ export declare function isStuckLoop(reports: Array<Pick<ToolCallReport, 'name' | 'argsKey' | 'errorType'>>): boolean;
3
22
  /**
4
23
  * Per-tool-call gate. Return 'allow' to dispatch, 'deny' to inject a
5
24
  * "user denied" tool result (so the model can react) without running
@@ -92,6 +111,17 @@ export interface AgentLoopInput {
92
111
  goalTitle?: string;
93
112
  goalBody?: string;
94
113
  };
114
+ /** v1.2.109 — names of tools the caller declares parallel-safe (no
115
+ * shared state, no side effects, order-independent). When the model
116
+ * returns multiple tool_calls in one iteration, declared-safe calls
117
+ * run concurrently via Promise.allSettled; everything else (and any
118
+ * tool not in this set) keeps the legacy serial behaviour, with a
119
+ * barrier drain before each serial dispatch so serial calls observe
120
+ * a settled world. Emission order to history + toolCalls is always
121
+ * the original call order (tool_call_id ↔ tool_result alignment).
122
+ * Leave undefined → loop is fully serial (no behaviour change vs
123
+ * v1.2.108). */
124
+ parallelSafeTools?: ReadonlySet<string>;
95
125
  /** Lifecycle hooks for caller-side observability (heartbeats, progress
96
126
  * push, custom logging). All hooks are best-effort: thrown errors are
97
127
  * swallowed so a faulty observer cannot break the loop. */
@@ -119,6 +149,17 @@ export interface ToolCallReport {
119
149
  * `/heartbeat run-now` style introspection; longer payloads aren't
120
150
  * retained to keep memory + log size bounded. */
121
151
  preview: string;
152
+ /** v1.2.108 — stable JSON of the tool call's arguments (sorted keys),
153
+ * used by the stuck-loop detector. Identical calls hash to identical
154
+ * strings; model varying timestamps/UUIDs in the result no longer
155
+ * hides a stuck loop where the model keeps re-calling with the same
156
+ * args. See `stableStringify`. */
157
+ argsKey: string;
158
+ /** v1.2.108 — short fingerprint of the error (first non-empty line of
159
+ * the result, ≤80 chars) when isError; empty string otherwise. Lets
160
+ * the stuck-loop detector distinguish "same failure repeated" from
161
+ * "same args returning different bytes". See `extractErrorType`. */
162
+ errorType: string;
122
163
  }
123
164
  export interface AgentLoopResult {
124
165
  /** Final assistant text the caller should show the user. May be the
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/core/llm/agent-loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../../../src/core/llm/agent-loop.ts"],"names":[],"mappings":"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"}
@@ -56,6 +56,57 @@ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000; // 5 min — generous; signal usually
56
56
  const CRITIC_MIN_TOOL_CALLS = 5;
57
57
  const CRITIC_EVERY_N = 5;
58
58
  const CRITIC_MAX_CALLS = 2;
59
+ /**
60
+ * Deterministic JSON.stringify with sorted object keys at every level.
61
+ * Used by the stuck-loop detector to derive a stable identity key for
62
+ * tool-call arguments: identical calls hash to identical strings
63
+ * regardless of key insertion order. Tolerates circular references and
64
+ * non-JSON-able values by collapsing them to a placeholder.
65
+ */
66
+ export function stableStringify(value, seen = new WeakSet()) {
67
+ if (value === null)
68
+ return 'null';
69
+ if (typeof value === 'undefined')
70
+ return 'null';
71
+ if (typeof value === 'function')
72
+ return '"[fn]"';
73
+ if (typeof value !== 'object')
74
+ return JSON.stringify(value);
75
+ if (seen.has(value))
76
+ return '"[circular]"';
77
+ seen.add(value);
78
+ if (Array.isArray(value)) {
79
+ return '[' + value.map((v) => stableStringify(v, seen)).join(',') + ']';
80
+ }
81
+ const obj = value;
82
+ const keys = Object.keys(obj).sort();
83
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k], seen)).join(',') + '}';
84
+ }
85
+ /**
86
+ * Stable error-type fingerprint of a dispatcher's result text. Returns
87
+ * the first non-empty line trimmed and truncated to 80 chars when
88
+ * isError; empty string otherwise. Used by the stuck-loop detector so
89
+ * "fs_read failed with ENOENT three times" → stuck, but "fs_read
90
+ * returned different bytes three times" → not stuck.
91
+ */
92
+ export function extractErrorType(isError, text) {
93
+ if (!isError)
94
+ return '';
95
+ const firstNonEmpty = text.split('\n').find((line) => line.trim().length > 0) ?? '';
96
+ return firstNonEmpty.trim().slice(0, 80);
97
+ }
98
+ /** True iff the last 3 entries are the same tool called with identical
99
+ * args resulting in the identical outcome — the stuck-loop signal. */
100
+ export function isStuckLoop(reports) {
101
+ if (reports.length < 3)
102
+ return false;
103
+ const a = reports[reports.length - 1];
104
+ const b = reports[reports.length - 2];
105
+ const c = reports[reports.length - 3];
106
+ return a.name === b.name && b.name === c.name
107
+ && a.argsKey === b.argsKey && b.argsKey === c.argsKey
108
+ && a.errorType === b.errorType && b.errorType === c.errorType;
109
+ }
59
110
  /** Default: skip approval entirely. The agent loop is safe-by-default
60
111
  * for tests and the introspection use case; production AgentAdapters
61
112
  * should pass a real gate. */
@@ -87,6 +138,11 @@ export async function runAgentLoop(input) {
87
138
  const usageAcc = {};
88
139
  // v1.2.98 — goal-critic per-turn fire counter (capped at CRITIC_MAX_CALLS).
89
140
  let criticCallsFired = 0;
141
+ // v1.2.109 — throttle for the post-batch critic check. With parallel
142
+ // batching, toolCalls.length now jumps in chunks instead of advancing
143
+ // by 1 per per-call modulo check. Track the last fire to keep at-most-
144
+ // every-CRITIC_EVERY_N cadence regardless of batch size.
145
+ let lastCriticAtToolCalls = 0;
90
146
  let iter = 0;
91
147
  for (; iter < maxIter; iter++) {
92
148
  if (signal.signal.aborted) {
@@ -154,29 +210,50 @@ export async function runAgentLoop(input) {
154
210
  toolCalls: result.toolCalls,
155
211
  reasoningContent: result.reasoningContent,
156
212
  });
157
- // Execute each tool call (sequentially preserves order in the
158
- // conversation log and dodges races against the dispatcher's
159
- // internal state e.g. MCP session). Concurrent execution lands
160
- // when we have a real-world workload that justifies it.
161
- for (const call of result.toolCalls) {
213
+ // v1.2.109 execute tool calls with optional parallel batching.
214
+ // Tools whose names appear in input.parallelSafeTools dispatch via
215
+ // accumulating promises (Promise.allSettled); everything else (and
216
+ // any tool not declared safe) keeps the legacy serial path with a
217
+ // barrier drain before each serial dispatch so serial calls observe
218
+ // a settled world (MCP session state, file system, etc.).
219
+ // Emission order to history / toolCalls is always the original call
220
+ // order via slot arrays — some providers reject responses whose
221
+ // role:'tool' messages aren't laid out next to their assistant
222
+ // tool_calls in the expected order.
223
+ const parallelSafe = input.parallelSafeTools;
224
+ const historySlots = new Array(result.toolCalls.length).fill(null);
225
+ const reportSlots = new Array(result.toolCalls.length).fill(null);
226
+ const pendingTasks = [];
227
+ for (let idx = 0; idx < result.toolCalls.length; idx++) {
162
228
  if (signal.signal.aborted)
163
229
  break;
230
+ const call = result.toolCalls[idx];
231
+ // Stable arg key precomputed once per call so all three slot
232
+ // writers (deny / no-dispatcher / dispatched) share the same
233
+ // identity for the stuck-loop detector.
234
+ const argsKey = stableStringify(call.arguments);
164
235
  const reportBase = {
165
236
  name: call.name,
166
237
  source: 'unknown',
167
238
  preview: '',
239
+ argsKey,
240
+ errorType: '',
168
241
  };
242
+ // Approval is ALWAYS serial — it may block on user input, and we
243
+ // don't want to ask for N approvals simultaneously (the IM UI
244
+ // would get spammed).
169
245
  const decision = await safeApprove(approve, call, iter);
170
246
  if (decision === 'deny') {
171
247
  const denyText = `tool call denied by user: ${call.name}`;
172
- history.push({ role: 'tool', toolCallId: call.id, content: denyText });
173
- toolCalls.push({
248
+ historySlots[idx] = { role: 'tool', toolCallId: call.id, content: denyText };
249
+ reportSlots[idx] = {
174
250
  ...reportBase,
175
251
  decision: 'deny',
176
252
  isError: true,
177
253
  durationMs: 0,
178
254
  preview: denyText,
179
- });
255
+ errorType: extractErrorType(true, denyText),
256
+ };
180
257
  continue;
181
258
  }
182
259
  if (!dispatch) {
@@ -184,152 +261,164 @@ export async function runAgentLoop(input) {
184
261
  // theoretically a caller could pass tools+approve without a
185
262
  // dispatcher. Don't crash; tell the model the tool failed.
186
263
  const denyText = `tool call refused: no dispatcher registered`;
187
- history.push({ role: 'tool', toolCallId: call.id, content: denyText });
188
- toolCalls.push({
264
+ historySlots[idx] = { role: 'tool', toolCallId: call.id, content: denyText };
265
+ reportSlots[idx] = {
189
266
  ...reportBase,
190
267
  decision: 'allow',
191
268
  isError: true,
192
269
  durationMs: 0,
193
270
  preview: denyText,
194
- });
271
+ errorType: extractErrorType(true, denyText),
272
+ };
195
273
  continue;
196
274
  }
197
- try {
198
- input.hooks?.onToolStart?.(call);
199
- }
200
- catch { /* observer must not break loop */ }
201
- const dt0 = Date.now();
202
- let dispatched;
203
- try {
204
- dispatched = await dispatch(call, signal.signal);
205
- }
206
- catch (err) {
207
- const msg = err instanceof Error ? err.message : String(err);
208
- dispatched = { text: `tool threw: ${msg}`, isError: true, source: 'error' };
209
- log.warn({
210
- event: 'agent-loop.dispatch_threw',
211
- tool: call.name, err: msg,
212
- });
213
- }
214
- const durationMs = Date.now() - dt0;
215
- try {
216
- input.hooks?.onToolEnd?.(call, { isError: !!dispatched.isError, durationMs });
217
- }
218
- catch { /* observer must not break loop */ }
219
- history.push({
220
- role: 'tool',
221
- toolCallId: call.id,
222
- content: dispatched.text,
223
- });
224
- toolCalls.push({
225
- name: call.name,
226
- decision: 'allow',
227
- isError: dispatched.isError,
228
- source: dispatched.source ?? 'unknown',
229
- durationMs,
230
- preview: dispatched.text.slice(0, 200),
231
- });
232
- // v1.2.95 — stuck-loop detector. If the last three tool reports
233
- // share name + error-flag + preview content, the model is calling
234
- // the SAME tool with the SAME outcome — almost always a sign it's
235
- // re-running a failing SQL / fetch / command without changing
236
- // strategy. Break out early so we don't waste the full
237
- // maxIterations budget on a stuck model, and so the caller can
238
- // surface a much more focused "you're stuck on X" recap.
239
- //
240
- // Three consecutive matches is the smallest threshold that
241
- // unambiguously says "loop" without being triggered by a
242
- // legitimate "two reads on the same file" sequence. The preview
243
- // is already truncated to 200 chars at push time, so this
244
- // comparison is bounded.
245
- if (toolCalls.length >= 3) {
246
- const a = toolCalls[toolCalls.length - 1];
247
- const b = toolCalls[toolCalls.length - 2];
248
- const c = toolCalls[toolCalls.length - 3];
249
- if (a.name === b.name && b.name === c.name
250
- && a.isError === b.isError && b.isError === c.isError
251
- && a.preview === b.preview && b.preview === c.preview) {
275
+ // Capture dispatch into an async closure so it can be awaited
276
+ // either immediately (serial path) or later via Promise.allSettled
277
+ // (parallel batch path). Captures idx by closure so the writer
278
+ // hits the correct slot regardless of completion order.
279
+ const dispatchCall = dispatch;
280
+ const runDispatch = async () => {
281
+ try {
282
+ input.hooks?.onToolStart?.(call);
283
+ }
284
+ catch { /* observer must not break loop */ }
285
+ const t0 = Date.now();
286
+ let dispatched;
287
+ try {
288
+ dispatched = await dispatchCall(call, signal.signal);
289
+ }
290
+ catch (err) {
291
+ const msg = err instanceof Error ? err.message : String(err);
292
+ dispatched = { text: `tool threw: ${msg}`, isError: true, source: 'error' };
252
293
  log.warn({
253
- event: 'agent-loop.stuck_loop_detected',
254
- tool: a.name,
255
- isError: a.isError,
256
- iter,
257
- totalToolCalls: toolCalls.length,
258
- previewSnippet: (a.preview ?? '').slice(0, 80),
259
- }, `stuck loop: 3× ${a.name} with identical outcome — early stop at iter ${iter}`);
260
- signal.cleanup();
261
- return {
262
- text: '',
263
- iterations: iter + 1,
264
- toolCalls,
265
- usage: usageAcc,
266
- finishReason: 'stuck_loop',
267
- };
294
+ event: 'agent-loop.dispatch_threw',
295
+ tool: call.name, err: msg,
296
+ });
268
297
  }
298
+ const durationMs = Date.now() - t0;
299
+ try {
300
+ input.hooks?.onToolEnd?.(call, { isError: !!dispatched.isError, durationMs });
301
+ }
302
+ catch { /* observer must not break loop */ }
303
+ historySlots[idx] = {
304
+ role: 'tool',
305
+ toolCallId: call.id,
306
+ content: dispatched.text,
307
+ };
308
+ reportSlots[idx] = {
309
+ name: call.name,
310
+ decision: 'allow',
311
+ isError: dispatched.isError,
312
+ source: dispatched.source ?? 'unknown',
313
+ durationMs,
314
+ preview: dispatched.text.slice(0, 200),
315
+ argsKey,
316
+ errorType: extractErrorType(dispatched.isError, dispatched.text),
317
+ };
318
+ };
319
+ const isParallel = !!parallelSafe?.has(call.name);
320
+ if (isParallel) {
321
+ pendingTasks.push(runDispatch());
322
+ }
323
+ else {
324
+ // Serial barrier: drain accumulated parallel work first.
325
+ if (pendingTasks.length > 0) {
326
+ await Promise.allSettled(pendingTasks.splice(0));
327
+ }
328
+ await runDispatch();
269
329
  }
270
- // v1.2.98 / v1.2.99 — goal-critic check (semantic off-track).
271
- // Fires when ALL of:
272
- // · caller supplied a criticAnchor
273
- // · critic mode is 'always' OR ('goal-only' AND anchor has a
274
- // long-task goal title). Per-turn prompts alone won't
275
- // trigger in goal-only mode this is the v1.2.99 fix for
276
- // CR finding A2 (default ON in v1.2.98 surprised the cost
277
- // budget). Set IMHUB_NATIVE_CRITIC=always to re-enable for
278
- // every multi-step turn.
279
- // · CRITIC_MIN_TOOL_CALLS tool calls this turn
280
- // · haven't already fired CRITIC_MAX_CALLS times
281
- // · on a trigger mark (every CRITIC_EVERY_N tool calls)
282
- // The critic itself decides whether to break. It returns
283
- // onTrack=true on errors so provider hiccups never accidentally
284
- // kill a working turn.
285
- const criticMode = resolveCriticMode();
286
- const criticGateOpen = criticMode === 'always'
287
- || (criticMode === 'goal-only' && !!input.criticAnchor?.goalTitle?.trim());
288
- if (input.criticAnchor
289
- && criticGateOpen
290
- && toolCalls.length >= CRITIC_MIN_TOOL_CALLS
291
- && criticCallsFired < CRITIC_MAX_CALLS
292
- && (toolCalls.length - CRITIC_MIN_TOOL_CALLS) % CRITIC_EVERY_N === 0) {
293
- criticCallsFired += 1;
294
- const recent = toolCalls.slice(-10);
295
- const verdict = await runGoalCritic({
296
- prompt: input.criticAnchor.prompt,
297
- goalTitle: input.criticAnchor.goalTitle,
298
- goalBody: input.criticAnchor.goalBody,
299
- recentToolCalls: recent,
300
- }, { signal: signal.signal });
301
- log.info({
302
- event: 'agent-loop.goal_critic_verdict',
330
+ }
331
+ // End-of-iteration drain for any tail parallel tasks.
332
+ if (pendingTasks.length > 0) {
333
+ await Promise.allSettled(pendingTasks.splice(0));
334
+ }
335
+ // Commit slots in original call order so tool_call_id tool_result
336
+ // alignment is preserved regardless of dispatch completion order.
337
+ // Slots may be null if the loop aborted mid-iteration — skip those.
338
+ for (let idx = 0; idx < result.toolCalls.length; idx++) {
339
+ const h = historySlots[idx];
340
+ const r = reportSlots[idx];
341
+ if (h)
342
+ history.push(h);
343
+ if (r)
344
+ toolCalls.push(r);
345
+ }
346
+ // v1.2.108 stuck-loop detector (now post-batch). Keyed on
347
+ // (name, argsKey, errorType) of the last 3 entries; with parallel
348
+ // batching this still fires correctly when 3 identical calls land
349
+ // in a row (whether in one batch or across batches).
350
+ if (isStuckLoop(toolCalls)) {
351
+ const a = toolCalls[toolCalls.length - 1];
352
+ log.warn({
353
+ event: 'agent-loop.stuck_loop_detected',
354
+ tool: a.name,
355
+ isError: a.isError,
356
+ argsKeySnippet: a.argsKey.slice(0, 200),
357
+ errorType: a.errorType,
358
+ iter,
359
+ totalToolCalls: toolCalls.length,
360
+ }, `stuck loop: 3× ${a.name} with identical args + outcome — early stop at iter ${iter}`);
361
+ signal.cleanup();
362
+ return {
363
+ text: '',
364
+ iterations: iter + 1,
365
+ toolCalls,
366
+ usage: usageAcc,
367
+ finishReason: 'stuck_loop',
368
+ };
369
+ }
370
+ // v1.2.98 / v1.2.99 / v1.2.109 — goal-critic check (semantic
371
+ // off-track), now post-batch with a since-last-fire throttle so
372
+ // cadence stays at-most-every-CRITIC_EVERY_N tool calls regardless
373
+ // of how many calls landed in this iteration's batch.
374
+ const criticMode = resolveCriticMode();
375
+ const criticGateOpen = criticMode === 'always'
376
+ || (criticMode === 'goal-only' && !!input.criticAnchor?.goalTitle?.trim());
377
+ if (input.criticAnchor
378
+ && criticGateOpen
379
+ && toolCalls.length >= CRITIC_MIN_TOOL_CALLS
380
+ && criticCallsFired < CRITIC_MAX_CALLS
381
+ && toolCalls.length - lastCriticAtToolCalls >= CRITIC_EVERY_N) {
382
+ criticCallsFired += 1;
383
+ lastCriticAtToolCalls = toolCalls.length;
384
+ const recent = toolCalls.slice(-10);
385
+ const verdict = await runGoalCritic({
386
+ prompt: input.criticAnchor.prompt,
387
+ goalTitle: input.criticAnchor.goalTitle,
388
+ goalBody: input.criticAnchor.goalBody,
389
+ recentToolCalls: recent,
390
+ }, { signal: signal.signal });
391
+ log.info({
392
+ event: 'agent-loop.goal_critic_verdict',
393
+ iter,
394
+ totalToolCalls: toolCalls.length,
395
+ onTrack: verdict.onTrack,
396
+ reason: verdict.reason,
397
+ modelUsed: verdict.modelUsed,
398
+ });
399
+ if (!verdict.onTrack) {
400
+ log.warn({
401
+ event: 'agent-loop.off_track_detected',
303
402
  iter,
304
- totalToolCalls: toolCalls.length,
305
- onTrack: verdict.onTrack,
306
403
  reason: verdict.reason,
307
- modelUsed: verdict.modelUsed,
308
- });
309
- if (!verdict.onTrack) {
310
- log.warn({
311
- event: 'agent-loop.off_track_detected',
312
- iter,
313
- reason: verdict.reason,
314
- redirect: verdict.redirect,
315
- }, `goal-critic flagged off-track at iter ${iter}: ${verdict.reason}`);
316
- signal.cleanup();
317
- return {
318
- text: '',
319
- iterations: iter + 1,
320
- toolCalls,
321
- usage: usageAcc,
322
- finishReason: 'off_track',
323
- // Smuggle the critic's reason + redirect into `error` so
324
- // the native adapter can surface it verbatim in the recap
325
- // without re-importing goal-critic. Yes, abusing `error`
326
- // for non-error info is mildly ugly; alternative is adding
327
- // a new field to AgentLoopResult — overkill for one knob.
328
- error: verdict.redirect
329
- ? `${verdict.reason} || redirect: ${verdict.redirect}`
330
- : verdict.reason,
331
- };
332
- }
404
+ redirect: verdict.redirect,
405
+ }, `goal-critic flagged off-track at iter ${iter}: ${verdict.reason}`);
406
+ signal.cleanup();
407
+ return {
408
+ text: '',
409
+ iterations: iter + 1,
410
+ toolCalls,
411
+ usage: usageAcc,
412
+ finishReason: 'off_track',
413
+ // Smuggle the critic's reason + redirect into `error` so
414
+ // the native adapter can surface it verbatim in the recap
415
+ // without re-importing goal-critic. Yes, abusing `error`
416
+ // for non-error info is mildly ugly; alternative is adding
417
+ // a new field to AgentLoopResult — overkill for one knob.
418
+ error: verdict.redirect
419
+ ? `${verdict.reason} || redirect: ${verdict.redirect}`
420
+ : verdict.reason,
421
+ };
333
422
  }
334
423
  }
335
424
  // Loop tail → next iteration calls provider with appended history.