oh-my-agent-web 0.7.0 → 1.0.2

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 (249) hide show
  1. package/README.en.md +394 -248
  2. package/README.md +387 -247
  3. package/dist/server/approval/rules.js +4 -16
  4. package/dist/server/approval/scope.js +3 -3
  5. package/dist/server/cli-config.js +3 -3
  6. package/dist/server/cli-info.js +9 -9
  7. package/dist/server/cli.js +108 -23
  8. package/dist/server/engine/builtin.js +64 -1
  9. package/dist/server/engine/catalog-probe.js +34 -0
  10. package/dist/server/engine/claude-map.js +296 -0
  11. package/dist/server/engine/claude-sessions.js +581 -0
  12. package/dist/server/engine/claude-transport.js +75 -0
  13. package/dist/server/engine/claude.js +414 -0
  14. package/dist/server/engine/cli-version.js +119 -0
  15. package/dist/server/engine/codex-map.js +19 -23
  16. package/dist/server/engine/codex-sessions.js +5 -5
  17. package/dist/server/engine/codex-subagents.js +349 -0
  18. package/dist/server/engine/codex.js +215 -2
  19. package/dist/server/engine/dsh-history.js +1 -1
  20. package/dist/server/engine/dsh-log.js +47 -36
  21. package/dist/server/engine/dsh-map.js +3 -5
  22. package/dist/server/engine/dsh-prompt-patch.js +48 -0
  23. package/dist/server/engine/dsh-sessions.js +10 -11
  24. package/dist/server/engine/dsh-transport.js +2 -2
  25. package/dist/server/engine/dsh.js +45 -2
  26. package/dist/server/engine/engine-versions.js +41 -0
  27. package/dist/server/engine/fake.js +38 -7
  28. package/dist/server/engine/model-registry-reload.js +1 -1
  29. package/dist/server/engine/opencode-map.js +2 -2
  30. package/dist/server/engine/opencode-projects.js +1 -1
  31. package/dist/server/engine/opencode-sessions.js +2 -2
  32. package/dist/server/engine/opencode.js +39 -1
  33. package/dist/server/engine/pi-events.js +4 -4
  34. package/dist/server/engine/pi-plugins.js +8 -6
  35. package/dist/server/engine/pi-sdk.js +179 -5
  36. package/dist/server/engine/pi-subagent-tool.js +2 -2
  37. package/dist/server/engine/pi-subagents.js +2 -2
  38. package/dist/server/engine/pi-transcript.js +1 -1
  39. package/dist/server/engine/registry.js +12 -2
  40. package/dist/server/engine/suggestion-guidance.js +22 -9
  41. package/dist/server/engine/wiring.js +57 -3
  42. package/dist/server/files/content-search.js +187 -0
  43. package/dist/server/files/file-search.js +3 -3
  44. package/dist/server/files/file-service.js +33 -15
  45. package/dist/server/files/files-routes.js +68 -7
  46. package/dist/server/files/git-info.js +51 -11
  47. package/dist/server/files/path-resolve.js +176 -0
  48. package/dist/server/files/pick-dir-routes.js +93 -0
  49. package/dist/server/files/pick-path-routes.js +126 -0
  50. package/dist/server/http/auth-routes.js +71 -4
  51. package/dist/server/http/auth.js +48 -30
  52. package/dist/server/http/aux-routes.js +20 -4
  53. package/dist/server/http/file-headers.js +3 -3
  54. package/dist/server/http/health-and-static.js +1 -1
  55. package/dist/server/http/live-routes.js +4 -4
  56. package/dist/server/http/login-page.js +128 -1
  57. package/dist/server/http/password-store.js +31 -0
  58. package/dist/server/index.js +93 -11
  59. package/dist/server/markers/actions.js +8 -8
  60. package/dist/server/mcp/codex-mcp-store.js +190 -0
  61. package/dist/server/mcp/json-mcp-store.js +276 -0
  62. package/dist/server/mcp/mcp-auth.js +1 -1
  63. package/dist/server/mcp/mcp-command.js +0 -2
  64. package/dist/server/mcp/mcp-engine-shared.js +112 -0
  65. package/dist/server/mcp/mcp-import.js +4 -5
  66. package/dist/server/mcp/mcp-market.js +2 -3
  67. package/dist/server/mcp/mcp-routes.js +4 -3
  68. package/dist/server/mcp/none-mcp-store.js +27 -0
  69. package/dist/server/mcp/pi-mcp-config.js +1 -1
  70. package/dist/server/plugins/engine-plugin-stores.js +265 -0
  71. package/dist/server/plugins/plugin-routes.js +4 -3
  72. package/dist/server/protocol-parse.js +52 -4
  73. package/dist/server/protocol.js +0 -1
  74. package/dist/server/providers/catalog-writes.js +1 -1
  75. package/dist/server/providers/catalog.js +2 -2
  76. package/dist/server/providers/codex-models.js +1 -1
  77. package/dist/server/providers/codex-test.js +1 -1
  78. package/dist/server/providers/enabled-models.js +1 -11
  79. package/dist/server/providers/import-routes.js +2 -2
  80. package/dist/server/providers/probe-routes.js +26 -0
  81. package/dist/server/providers/provider-handlers.js +3 -3
  82. package/dist/server/providers/provider-pool.js +2 -2
  83. package/dist/server/providers/provider-store.js +5 -2
  84. package/dist/server/providers/subscription-routes.js +4 -2
  85. package/dist/server/providers/validate.js +13 -14
  86. package/dist/server/push/push-routes.js +32 -3
  87. package/dist/server/push/push.js +43 -9
  88. package/dist/server/session/client-view.js +2 -2
  89. package/dist/server/session/close-ephemeral.js +23 -0
  90. package/dist/server/session/conversation-create.js +31 -9
  91. package/dist/server/session/delete-session.js +25 -14
  92. package/dist/server/session/engine-switch.js +12 -5
  93. package/dist/server/session/fork-session.js +38 -0
  94. package/dist/server/session/lock.js +4 -4
  95. package/dist/server/session/model-controller.js +14 -2
  96. package/dist/server/session/move-session.js +40 -0
  97. package/dist/server/session/persist-ephemeral.js +48 -0
  98. package/dist/server/session/pi-opener.js +2 -2
  99. package/dist/server/session/pi-refresh.js +1 -1
  100. package/dist/server/session/pi-sessions-provider.js +2 -2
  101. package/dist/server/session/pi-sessions.js +107 -18
  102. package/dist/server/session/runtime.js +144 -9
  103. package/dist/server/session/session-stats.js +2 -2
  104. package/dist/server/session/settings.js +45 -49
  105. package/dist/server/session/subagents.js +4 -1
  106. package/dist/server/session/suggest-title.js +33 -0
  107. package/dist/server/session/title-prompt.js +126 -0
  108. package/dist/server/session/turn-finish.js +2 -2
  109. package/dist/server/session/turn-queue.js +5 -5
  110. package/dist/server/session/turn-watchdog.js +2 -2
  111. package/dist/server/skills/skill-install.js +3 -5
  112. package/dist/server/skills/skill-store.js +3 -3
  113. package/dist/server/skills/skillhub.js +3 -4
  114. package/dist/server/snapshot/messages.js +2 -8
  115. package/dist/server/snapshot/sender.js +1 -1
  116. package/dist/server/snapshot/state.js +3 -5
  117. package/dist/server/subagents/profile-store.js +11 -9
  118. package/dist/server/terminal/terminal-session.js +9 -9
  119. package/dist/server/uploads/upload-routes.js +1 -1
  120. package/dist/server/uploads/upload-store.js +4 -6
  121. package/dist/server/util/env.js +27 -21
  122. package/dist/server/util/ids.js +1 -1
  123. package/dist/server/util/json-file.js +38 -0
  124. package/dist/server/util/log-file.js +3 -3
  125. package/dist/server/util/log.js +4 -4
  126. package/dist/server/util/origin.js +5 -24
  127. package/dist/server/util/paths.js +25 -3
  128. package/dist/server/util/pricing-index.js +2 -6
  129. package/dist/server/util/pricing.js +10 -10
  130. package/dist/server/util/spawn-cli.js +2 -2
  131. package/dist/server/util/upstream-version.js +10 -12
  132. package/dist/server/util/version.js +2 -2
  133. package/dist/server/web-proxy/proxy-routes.js +167 -0
  134. package/dist/server/web-proxy/proxy-url.js +60 -0
  135. package/dist/server/web-proxy/rewrite.js +84 -0
  136. package/dist/server/ws/handle-message.js +21 -2
  137. package/package.json +13 -14
  138. package/scripts/prune-prebuilds.mjs +1 -1
  139. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-Dq-2fRb4.js +1 -0
  140. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-Dh5CEs4k.js +1 -0
  141. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_c4Diagram-XDPPPE54-Cb19QmmE.js +1 -0
  142. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-COk-IL9O.js +1 -0
  143. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-CBK3MWTC-Gp_0nfEJ.js +1 -0
  144. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-ODIL2UCW-rzPf_9IN.js +1 -0
  145. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-PFBEJUXE-B5EnLWwl.js +1 -0
  146. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_diagram-TYUNFSNR-DVpFKFwC.js +1 -0
  147. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-CDL1qiPY.js +1 -0
  148. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-CSkoONzp.js +1 -0
  149. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-B9sdVB6_.js +1 -0
  150. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-B6P7baCF.js +1 -0
  151. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-W_a8Vy2l.js +1 -0
  152. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-DvGif-RR.js +1 -0
  153. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-D0gFG_EI.js +1 -0
  154. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-BjgUBJiR.js +1 -0
  155. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DnVuWTpF.js +1 -0
  156. package/web/dist/assets/_oh-my-agent-web-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-B2vDAqpl.js +1 -0
  157. package/web/dist/assets/{arc-BTAvbR16.js → arc-Df9SI9yh.js} +1 -1
  158. package/web/dist/assets/{architecture-WOLXFQ4H-fewLD5_7.js → architecture-WOLXFQ4H-iO49E8Y1.js} +1 -1
  159. package/web/dist/assets/{blockDiagram-RJAE7QA3-ClmgyxZl.js → blockDiagram-RJAE7QA3-D8d1ukIs.js} +1 -1
  160. package/web/dist/assets/channel-C2-OT56h.js +1 -0
  161. package/web/dist/assets/{chunk-55XCU76K-DdC5EhZG.js → chunk-55XCU76K-C5wOvqDb.js} +1 -1
  162. package/web/dist/assets/{chunk-7S6XSTFO-CgToWHK6.js → chunk-7S6XSTFO-WIrWcDbG.js} +1 -1
  163. package/web/dist/assets/{chunk-AQDTSKZB-D-rQMnp7.js → chunk-AQDTSKZB-OkzYqFsw.js} +1 -1
  164. package/web/dist/assets/{chunk-DSNLSHJZ-cuZoTGMH.js → chunk-DSNLSHJZ-CHEA9kYd.js} +2 -2
  165. package/web/dist/assets/{chunk-HNE4Q36P-CiKr9iSN.js → chunk-HNE4Q36P-CPsq34CI.js} +1 -1
  166. package/web/dist/assets/{chunk-VQ5W62QI-DbTfXAAT.js → chunk-VQ5W62QI-CamPnEuF.js} +1 -1
  167. package/web/dist/assets/{chunk-WQQL4CDK-DHanHfB5.js → chunk-WQQL4CDK-gNypBVAM.js} +1 -1
  168. package/web/dist/assets/{chunk-ZAUX2LTX-SrjFXpy2.js → chunk-ZAUX2LTX--4WyRBRV.js} +1 -1
  169. package/web/dist/assets/{chunk-ZCHMQSOR-7Wy8xXQ5.js → chunk-ZCHMQSOR-umv3Lkpn.js} +1 -1
  170. package/web/dist/assets/{classDiagram-v2-4MYWR3UA-CBe5ZDvZ.js → classDiagram-v2-4MYWR3UA-C1XldpO1.js} +1 -1
  171. package/web/dist/assets/{cose-bilkent-JH36ORCC-Dljdyvhw.js → cose-bilkent-JH36ORCC-BvugZzFU.js} +1 -1
  172. package/web/dist/assets/{cynefin-EF2NZ3EQ-CcZU7GFP.js → cynefin-EF2NZ3EQ-6qXB70s4.js} +1 -1
  173. package/web/dist/assets/{dagre-NK4GIZVX-Bq_6y8qp.js → dagre-NK4GIZVX-DDdlABYw.js} +1 -1
  174. package/web/dist/assets/{diagram-I5XIIV2D-C2lnnYVG.js → diagram-I5XIIV2D-T8oWy5CY.js} +1 -1
  175. package/web/dist/assets/{diagram-RJA3I4VV-Bqf8Z6wJ.js → diagram-RJA3I4VV-CkdHTtfV.js} +1 -1
  176. package/web/dist/assets/{elk-QONTBCTQ-Cdzq_cJ5.js → elk-QONTBCTQ-BiEUbJJX.js} +1 -1
  177. package/web/dist/assets/{erDiagram-KYQIGQAA-ZJLCjPFC.js → erDiagram-KYQIGQAA-q3WwhyzH.js} +1 -1
  178. package/web/dist/assets/{eventmodeling-K75KTNOO-DHqeIpes.js → eventmodeling-K75KTNOO-BO5gW_NC.js} +1 -1
  179. package/web/dist/assets/{flowDiagram-IUGLLWBU-AVgYvXKN.js → flowDiagram-IUGLLWBU-DubjP6UK.js} +1 -1
  180. package/web/dist/assets/{ganttDiagram-R7GPNTJY-CR1SG1M7.js → ganttDiagram-R7GPNTJY-lKNRH8CI.js} +1 -1
  181. package/web/dist/assets/{gitGraph-VSP46ZUC-CIZQPK0R.js → gitGraph-VSP46ZUC-Cwlv3omT.js} +1 -1
  182. package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-BhOCWEh6.js → gitGraphDiagram-PDYL7XHY-Bo9GpFvW.js} +1 -1
  183. package/web/dist/assets/index-CeYhxqkk.js +113 -0
  184. package/web/dist/assets/index-GJgJwZ23.css +1 -0
  185. package/web/dist/assets/{info-OHQRW6UA-C2wuRxeQ.js → info-OHQRW6UA-g3MbTRmN.js} +1 -1
  186. package/web/dist/assets/{infoDiagram-MVDRKAX5-BP5doAB5.js → infoDiagram-MVDRKAX5-CmILYLoB.js} +1 -1
  187. package/web/dist/assets/{journeyDiagram-BOSHW7O7-BcIDRTPS.js → journeyDiagram-BOSHW7O7-Av8T0U1C.js} +1 -1
  188. package/web/dist/assets/{kanban-definition-FNSA4HC5-CRH_bcoT.js → kanban-definition-FNSA4HC5-B9NyWQYN.js} +1 -1
  189. package/web/dist/assets/{line-iA4GPA_S.js → line-BUrll1My.js} +1 -1
  190. package/web/dist/assets/{linear-BGUKwkTH.js → linear-DjET0phX.js} +1 -1
  191. package/web/dist/assets/{math-DgNW_bVP.js → math-BJAjkhlu.js} +1 -1
  192. package/web/dist/assets/{mermaid-parser.core-CsqmgpEv.js → mermaid-parser.core-CaTdkhkS.js} +3 -3
  193. package/web/dist/assets/{mermaid.core-C1yCGQK6.js → mermaid.core-Cktx937U.js} +4 -4
  194. package/web/dist/assets/{packet-JDAUHWVQ-2ggaoz2g.js → packet-JDAUHWVQ-DhG6l3PG.js} +1 -1
  195. package/web/dist/assets/{pie-XZMESJXO-h_yslQQs.js → pie-XZMESJXO-YPcOx7s0.js} +1 -1
  196. package/web/dist/assets/{pieDiagram-GNMPY74T-6-JZMFyk.js → pieDiagram-GNMPY74T-DesiZKUl.js} +1 -1
  197. package/web/dist/assets/{quadrantDiagram-D57K4MC3-K5r1HOgL.js → quadrantDiagram-D57K4MC3-Cn1kBVgu.js} +1 -1
  198. package/web/dist/assets/{radar-ABXABTNO-6fOJGGgq.js → radar-ABXABTNO-CtuRfYSz.js} +1 -1
  199. package/web/dist/assets/{railroad-I3PHUGI6-B_gUgQ3d.js → railroad-I3PHUGI6-D5Gnyy0R.js} +1 -1
  200. package/web/dist/assets/{railroad-abnf-LNEFI6M7-CJ_eqiXZ.js → railroad-abnf-LNEFI6M7-Kw4pmJEm.js} +1 -1
  201. package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-BTsgjLTU.js → railroad-ebnf-DWYD2UWJ-D8Mk5dIe.js} +1 -1
  202. package/web/dist/assets/{railroad-peg-JR7BVNR7-D91uiLln.js → railroad-peg-JR7BVNR7-R5Jwzs8F.js} +1 -1
  203. package/web/dist/assets/{requirementDiagram-EQYSW5RA-uapnV_OO.js → requirementDiagram-EQYSW5RA-B9RDq-cL.js} +1 -1
  204. package/web/dist/assets/{sankeyDiagram-SY24JHO5-BQsbFgUb.js → sankeyDiagram-SY24JHO5-DoEatOm6.js} +1 -1
  205. package/web/dist/assets/{sequenceDiagram-BLEHNHQU-iRoSYX_e.js → sequenceDiagram-BLEHNHQU-DGE5Z9lT.js} +1 -1
  206. package/web/dist/assets/{src-BBrKcs9r.js → src-BnEL7pZw.js} +1 -1
  207. package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-Bzj9z5KH.js → stateDiagram-v2-A6IESKWZ-_vn8W-k_.js} +1 -1
  208. package/web/dist/assets/{timeline-definition-JXYKGLGB-DLUUuJX0.js → timeline-definition-JXYKGLGB-BaSLTtgR.js} +1 -1
  209. package/web/dist/assets/{treeView-D4JQ5SDB-CphIQ3n5.js → treeView-D4JQ5SDB-DZElyFgL.js} +1 -1
  210. package/web/dist/assets/{treemap-SAJKECNS-DqU3insw.js → treemap-SAJKECNS-DWDO4K4G.js} +1 -1
  211. package/web/dist/assets/{wardley-7MLQ67FV-Ch7d8cmC.js → wardley-7MLQ67FV-DTOUsk6Q.js} +1 -1
  212. package/web/dist/assets/{xychartDiagram-4URNHDU4-DdxpWBeM.js → xychartDiagram-4URNHDU4-DiaCfgAB.js} +1 -1
  213. package/web/dist/index.html +2 -2
  214. package/docs/screenshots/01-chat.png +0 -0
  215. package/docs/screenshots/02-answer.png +0 -0
  216. package/docs/screenshots/03-files-diff.png +0 -0
  217. package/docs/screenshots/04-terminal.png +0 -0
  218. package/docs/screenshots/05-models.png +0 -0
  219. package/docs/screenshots/06-skills.png +0 -0
  220. package/docs/screenshots/07-skills-hub.png +0 -0
  221. package/docs/screenshots/08-mcp.png +0 -0
  222. package/docs/screenshots/09-subagents.png +0 -0
  223. package/docs/screenshots/10-settings.png +0 -0
  224. package/docs/screenshots/11-plugins.png +0 -0
  225. package/docs/screenshots/12-voice.png +0 -0
  226. package/docs/screenshots/13-push.png +0 -0
  227. package/docs/screenshots/README.md +0 -45
  228. package/web/dist/assets/_omaw-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-CvNbnJ75.js +0 -1
  229. package/web/dist/assets/_omaw-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-T7NZZ4ao.js +0 -1
  230. package/web/dist/assets/_omaw-mermaid-pruned_chunk_c4Diagram-XDPPPE54-ByuAPg39.js +0 -1
  231. package/web/dist/assets/_omaw-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-BY5DHttT.js +0 -1
  232. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-CBK3MWTC-D7ZYyrF7.js +0 -1
  233. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-ODIL2UCW-C6kcOZFc.js +0 -1
  234. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-PFBEJUXE-CzFPFq0D.js +0 -1
  235. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-TYUNFSNR-B4G4T8SL.js +0 -1
  236. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-BkQ4xwD0.js +0 -1
  237. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-52xqckJv.js +0 -1
  238. package/web/dist/assets/_omaw-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-7cQksbFn.js +0 -1
  239. package/web/dist/assets/_omaw-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-udgtBpwv.js +0 -1
  240. package/web/dist/assets/_omaw-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-Cd2dcQWz.js +0 -1
  241. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-p34pL1Yk.js +0 -1
  242. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-BslfPs4r.js +0 -1
  243. package/web/dist/assets/_omaw-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-DJZoN9Xk.js +0 -1
  244. package/web/dist/assets/_omaw-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DPXo67cZ.js +0 -1
  245. package/web/dist/assets/_omaw-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-BE6ukwkg.js +0 -1
  246. package/web/dist/assets/channel-DfaYIFFE.js +0 -1
  247. package/web/dist/assets/index-CZQjJbRg.js +0 -104
  248. package/web/dist/assets/index-ChDUWKZX.css +0 -1
  249. /package/web/dist/assets/{_omaw-mermaid-pruned_katex_katex-Zojedrce.js → _oh-my-agent-web-mermaid-pruned_katex_katex-Zojedrce.js} +0 -0
@@ -0,0 +1,349 @@
1
+ /** 子代理 thread 的状态(`CollabAgentStatus`)→ 面板的三态 */
2
+ function statusOfAgentState(status) {
3
+ switch (status) {
4
+ case "running":
5
+ case "pendingInit":
6
+ return "running";
7
+ case "completed":
8
+ return "completed";
9
+ case "interrupted":
10
+ case "shutdown":
11
+ return "aborted";
12
+ case "errored":
13
+ return "failed";
14
+ default:
15
+ return null;
16
+ }
17
+ }
18
+ /**
19
+ * 模板 id → codex 的 agent 类型名。
20
+ *
21
+ * codex 要求小写字母 / 数字 / 下划线(`spawn_agent` 的 `task_name` 是同一套规则),
22
+ * 我们的模板 id 里带连字符(`general-purpose`),所以映射一次。
23
+ */
24
+ function agentTypeId(id) {
25
+ const cleaned = id
26
+ .trim()
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9]+/g, "_")
29
+ .replace(/^_+|_+$/g, "");
30
+ return cleaned === "" ? "subagent" : cleaned;
31
+ }
32
+ /** `agentPath` 的最后一段就是模型给这次任务起的名字(`/root/directory_inventory`) */
33
+ function taskNameOf(agentPath, fallback) {
34
+ const last = (agentPath ?? "")
35
+ .split("/")
36
+ .filter((part) => part !== "")
37
+ .pop();
38
+ if (!last)
39
+ return fallback;
40
+ const pretty = last.replace(/[_-]+/g, " ").trim();
41
+ return pretty === "" ? fallback : pretty.charAt(0).toUpperCase() + pretty.slice(1);
42
+ }
43
+ /**
44
+ * `subAgentActivity` → 增量。
45
+ *
46
+ * 文案全从引擎给的原话里取(`agentPath` / `model`),不自己编。
47
+ */
48
+ function activityPatch(item, at) {
49
+ const threadId = typeof item.agentThreadId === "string" ? item.agentThreadId : "";
50
+ if (threadId === "")
51
+ return null;
52
+ const base = { threadId, label: taskNameOf(item.agentPath, threadId.slice(0, 8)) };
53
+ if (typeof item.model === "string" && item.model !== "")
54
+ base.model = item.model;
55
+ switch (item.kind) {
56
+ case "started":
57
+ return { ...base, status: "running" };
58
+ case "interrupted":
59
+ return { ...base, status: "aborted", endedAt: at };
60
+ case "completed":
61
+ return { ...base, status: "completed", endedAt: at };
62
+ case "interacted":
63
+ // 只是有消息往来:状态不变(running 的子代理继续保持 running)
64
+ return { ...base, status: "running" };
65
+ default:
66
+ return base;
67
+ }
68
+ }
69
+ /**
70
+ * `collabAgentToolCall` → 增量(可能一次带来多个子 thread 的状态)。
71
+ *
72
+ * 只认 `agentsStates`:那是引擎对「这个子代理现在怎么样」的**权威回答**。
73
+ * `wait` 之类的工具调用状态不等于子代理状态(wait 结束了不代表子代理跑完)。
74
+ */
75
+ function collabStatesPatch(item) {
76
+ const states = item.agentsStates ?? {};
77
+ const patches = [];
78
+ for (const [threadId, state] of Object.entries(states)) {
79
+ const status = statusOfAgentState(state?.status);
80
+ if (!status)
81
+ continue;
82
+ const message = typeof state?.message === "string" && state.message !== "" ? state.message : undefined;
83
+ patches.push({
84
+ threadId,
85
+ status,
86
+ ...(status === "failed" && message ? { error: message } : {}),
87
+ ...(status === "running" ? {} : {}),
88
+ });
89
+ }
90
+ /**
91
+ * spawn 那一刻把 prompt 记下来(面板上「让它干什么」有原文可看)。
92
+ *
93
+ * 注意:spawn item 的 `agentsStates` **不一定**已经有这个新 thread(我们实测到的是空对象),
94
+ * 所以要按 `receiverThreadIds` 补一条 patch —— 否则卡片上只剩「由 Codex 自己发起的委派」。
95
+ */
96
+ if (item.tool === "spawnAgent" && typeof item.prompt === "string" && item.prompt !== "") {
97
+ for (const threadId of item.receiverThreadIds ?? []) {
98
+ const found = patches.find((patch) => patch.threadId === threadId);
99
+ if (found) {
100
+ found.task = item.prompt;
101
+ continue;
102
+ }
103
+ patches.push({ threadId, status: "running", task: item.prompt });
104
+ }
105
+ }
106
+ return patches;
107
+ }
108
+ /**
109
+ * 模板 + 开关 → `thread/start` 的 `config`(**按会话生效**,不写用户全局配置)。
110
+ *
111
+ * `agents.<id>.description`:codex 会用这个 description 告诉模型「有哪些子代理类型可挑」;
112
+ * 具体的模型 / 上下文由 `spawn_agent` 自己的参数决定(我们不硬塞,否则模板会骗人)。
113
+ */
114
+ export function codexMultiAgentConfig(input) {
115
+ const version = input.version ?? "v1";
116
+ const agents = {
117
+ // 实测:这个数字是「同时几个子代理」;模型被告知的是 数字 + 1(含它自己)
118
+ max_concurrent_threads_per_session: Math.max(1, Math.min(16, Math.round(input.concurrency))),
119
+ };
120
+ // `agents.<id>` 的「类型」只有 v2 认(v1 里所有子代理能力相同、没有类型可挑)
121
+ if (input.enabled && version === "v2") {
122
+ for (const profile of input.profiles) {
123
+ agents[agentTypeId(profile.id)] = {
124
+ description: `${profile.label}:${profile.description}`.trim(),
125
+ };
126
+ }
127
+ }
128
+ return {
129
+ features: { multi_agent: input.enabled, multi_agent_v2: input.enabled && version === "v2" },
130
+ agents,
131
+ };
132
+ }
133
+ /**
134
+ * 注入到**父会话** developer instructions 的委派说明。
135
+ *
136
+ * 只写 codex 真有的动作(`spawn_agent` 等),不写 pi 的 `Agent` 工具;也不假装
137
+ * 「一定会主动委派」——codex 默认策略是 `explicitRequestOnly`(用户明确要求才派)。
138
+ */
139
+ export function codexSubagentGuidance(input) {
140
+ const typed = (input.version ?? "v1") === "v2";
141
+ if (!input.enabled || input.profiles.length === 0)
142
+ return null;
143
+ const types = input.profiles
144
+ .map((profile) => typed
145
+ ? `- ${agentTypeId(profile.id)}: ${profile.label} — ${profile.description}`
146
+ : `- ${profile.label}: ${profile.description}`)
147
+ .join("\n");
148
+ return [
149
+ "## Delegating to subagents",
150
+ "You can delegate with the collaboration tools: `spawn_agent` (create a sub-agent thread),",
151
+ "`send_message` / `followup_task` (talk to it again), `wait_agent`, `interrupt_agent`, `list_agents`.",
152
+ "The user watches these runs in the workspace's Agents panel and can stop them, so:",
153
+ "- Delegate only when the user (or AGENTS.md / skill instructions) explicitly asks for",
154
+ " delegation or parallel work; do not spawn sub-agents on your own initiative.",
155
+ "- Give every spawn a short `task_name` (it becomes the card title) and a self-contained",
156
+ " first message: the sub-agent cannot see this conversation unless you pass a full-history fork.",
157
+ "- When several parts are independent, spawn them one after another and then `wait_agent` once",
158
+ " with all ids — do not poll in a loop.",
159
+ typed
160
+ ? "- Available sub-agent types (pass the id as `agent_type`, and restate its rules in the message):"
161
+ : "- Available delegation briefs — pick one and restate its rules in the `message` you send:",
162
+ types,
163
+ "- Always pass the task as the spawn `message`; a spawn without a message starts an agent with nothing to do.",
164
+ "- Never claim you cannot delegate: if a collaboration call fails, report the error text.",
165
+ ].join("\n");
166
+ }
167
+ /** 内存里最多留几条运行记录(与 pi 侧同一个数) */
168
+ const MAX_RUNS = 60;
169
+ /**
170
+ * codex 版 `SubagentEnginePort`:**只观测 + 停止**,不起子会话(委派是模型自己发起的)。
171
+ *
172
+ * `start()` 因此会报一句可读错误:面板上并没有「手动起一个子代理」的入口,
173
+ * 这条路径只有 pi 的 `Agent` 工具会走(见 pi-subagent-tool.ts)。
174
+ */
175
+ export function createCodexSubagents(options) {
176
+ const now = options.now ?? (() => Date.now());
177
+ const runs = new Map();
178
+ const order = [];
179
+ const listeners = new Set();
180
+ let limit = 4;
181
+ let enabled = true;
182
+ const emit = (run) => {
183
+ for (const listener of listeners) {
184
+ try {
185
+ listener({ ...run });
186
+ }
187
+ catch {
188
+ // 宿主侧抛错不能打断引擎的事件流
189
+ }
190
+ }
191
+ };
192
+ /** 只留最近 N 条(跑一天的会话不该把内存堆满;面板本来就只显示当前会话的) */
193
+ const trim = () => {
194
+ while (order.length > MAX_RUNS) {
195
+ const oldest = order.shift();
196
+ if (!oldest)
197
+ break;
198
+ const state = runs.get(oldest);
199
+ if (state && state.view.status === "running") {
200
+ order.push(oldest); // 还在跑的不能删
201
+ break;
202
+ }
203
+ runs.delete(oldest);
204
+ }
205
+ };
206
+ const apply = (conversationId, patch) => {
207
+ const id = `codex:${patch.threadId}`;
208
+ const existing = runs.get(id);
209
+ if (!existing) {
210
+ const view = {
211
+ id,
212
+ conversationId,
213
+ // codex 不在运行视图里报告用了哪个模板:如实写「引擎自己委派的」
214
+ profile: "codex",
215
+ label: patch.label ?? patch.threadId.slice(0, 8),
216
+ description: patch.task ? patch.task.slice(0, 120) : "由 Codex 自己发起的委派",
217
+ status: patch.status ?? "running",
218
+ task: patch.task ?? "",
219
+ startedAt: now(),
220
+ ...(patch.model ? { model: patch.model } : {}),
221
+ ...(patch.status && patch.status !== "running" ? { endedAt: patch.endedAt ?? now() } : {}),
222
+ ...(patch.error ? { error: patch.error } : {}),
223
+ };
224
+ runs.set(id, { view, settled: view.status !== "running" });
225
+ order.push(id);
226
+ trim();
227
+ emit(view);
228
+ // 结束得很快的任务:补一次结果(子 thread 的最后一条 agent 消息)
229
+ if (view.status !== "running")
230
+ void fillResult(patch.threadId, view);
231
+ return;
232
+ }
233
+ const state = existing;
234
+ if (state.settled && patch.status === "running")
235
+ return; // 迟到的事件不改已结算的记录
236
+ const next = {
237
+ ...state.view,
238
+ // agentPath / prompt 是稳定的,后到的事件带了就以它为准(先到的那条常常只有 thread id)
239
+ ...(patch.label ? { label: patch.label } : {}),
240
+ ...(patch.task && state.view.task === "" ? { task: patch.task, description: patch.task.slice(0, 120) } : {}),
241
+ ...(patch.model ? { model: patch.model } : {}),
242
+ ...(patch.status ? { status: patch.status } : {}),
243
+ ...(patch.error ? { error: patch.error } : {}),
244
+ };
245
+ if (patch.status && patch.status !== "running") {
246
+ next.endedAt = patch.endedAt ?? now();
247
+ state.settled = true;
248
+ }
249
+ state.view = next;
250
+ emit(next);
251
+ if (state.settled && next.result === undefined && next.status === "completed") {
252
+ void fillResult(patch.threadId, next);
253
+ }
254
+ };
255
+ /**
256
+ * 子代理的最终答复不在 collab item 里(那是「工具调用」的元数据)→ 结束前读一次子 thread 的
257
+ * 最后一条 agent 消息。读不到就算了:卡片上没有结果正文,比编一段强。
258
+ */
259
+ async function fillResult(threadId, view) {
260
+ try {
261
+ const listed = await options.request("thread/turns/list", { threadId, limit: 4, sortDirection: "desc" }, { timeoutMs: 15_000 });
262
+ const turns = listed?.data ?? [];
263
+ // 子代理自己的第一条 user 消息就是「你让它干什么」:派活时 codex 没把 prompt 放进 item,
264
+ // 只能从子 thread 里读回来(读不到就算了,卡片上没有任务原文,比编一段强)。
265
+ let task;
266
+ for (const turn of [...turns].reverse()) {
267
+ const first = (turn.items ?? []).find((item) => item.type === "userMessage");
268
+ const text = (first?.content ?? [])
269
+ .map((part) => part.text ?? "")
270
+ .join("")
271
+ .trim();
272
+ if (text !== "") {
273
+ task = text;
274
+ break;
275
+ }
276
+ }
277
+ for (const turn of turns) {
278
+ const message = (turn.items ?? []).filter((item) => item.type === "agentMessage").pop();
279
+ if (message?.text) {
280
+ const state = runs.get(view.id);
281
+ if (state) {
282
+ state.view = {
283
+ ...state.view,
284
+ result: message.text.slice(0, 4000),
285
+ ...(task && state.view.task === "" ? { task, description: task.slice(0, 120) } : {}),
286
+ };
287
+ emit(state.view);
288
+ }
289
+ return;
290
+ }
291
+ }
292
+ }
293
+ catch (error) {
294
+ options.log?.(`codex:读子代理结果失败(${threadId.slice(0, 8)}):${error instanceof Error ? error.message : String(error)}`);
295
+ }
296
+ }
297
+ return {
298
+ concurrency: () => limit,
299
+ enabled: () => enabled,
300
+ setEnabled: (value) => {
301
+ enabled = value;
302
+ },
303
+ ingest(conversationId, item) {
304
+ if (item.type === "subAgentActivity") {
305
+ const patch = activityPatch(item, now());
306
+ if (patch)
307
+ apply(conversationId, patch);
308
+ return;
309
+ }
310
+ for (const patch of collabStatesPatch(item))
311
+ apply(conversationId, patch);
312
+ },
313
+ async start(_request) {
314
+ throw new Error("codex 的委派由引擎自己发起:在对话里明确要求它委派子代理即可(面板只负责展示与停止)");
315
+ },
316
+ async interrupt(runId) {
317
+ const state = runs.get(runId);
318
+ if (!state)
319
+ return;
320
+ const threadId = runId.replace(/^codex:/, "");
321
+ try {
322
+ // 子代理是一个 thread:找到它正在跑的那一轮,再打断
323
+ const listed = await options.request("thread/turns/list", { threadId, limit: 1, sortDirection: "desc" }, { timeoutMs: 15_000 });
324
+ const turnId = listed?.data?.[0]?.id;
325
+ if (!turnId)
326
+ return;
327
+ await options.request("turn/interrupt", { threadId, turnId });
328
+ }
329
+ catch (error) {
330
+ options.log?.(`codex:停止子代理失败:${error instanceof Error ? error.message : String(error)}`);
331
+ }
332
+ },
333
+ get(runId) {
334
+ return runs.get(runId)?.view ?? null;
335
+ },
336
+ subscribe(listener) {
337
+ listeners.add(listener);
338
+ return () => listeners.delete(listener);
339
+ },
340
+ setConcurrency(next) {
341
+ limit = Math.max(1, Math.min(16, Math.round(next)));
342
+ },
343
+ dispose() {
344
+ listeners.clear();
345
+ runs.clear();
346
+ order.length = 0;
347
+ },
348
+ };
349
+ }
@@ -1,17 +1,75 @@
1
1
  import { friendlyCodexError, itemsToTranscript, itemsToUiMessages, mapModels, thinkingLevelsOf, toolEndOf, toolStartOf, turnTs, } from "./codex-map.js";
2
2
  import { createCodexClient } from "./codex-transport.js";
3
+ import { codexMultiAgentConfig, codexSubagentGuidance, createCodexSubagents } from "./codex-subagents.js";
3
4
  import { registerCodexApprovals } from "./codex-approvals.js";
4
5
  import { sanitizeHistoricalActions } from "../markers/actions.js";
6
+ import { suggestionGuidance } from "./suggestion-guidance.js";
5
7
  import { ensureCodexProject } from "./codex-sessions.js";
8
+ import { mkdtemp, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ /**
12
+ * 探针提示词。
13
+ *
14
+ * 为什么不是「回一个字」:只出 1~3 个 token 时,生成窗口只有几十毫秒,
15
+ * 算出来的 tok/s 全是小数点后的噪声(实测 3 token / 42ms = 71 tok/s,毫无参考价值)。
16
+ * 让它出 20 个数字:输出约 20~40 token,生成窗口有几百毫秒,速度才有意义;
17
+ * 代价仍然只有几分钱量级。
18
+ */
19
+ const PROBE_PROMPT = "Print the numbers 1 to 20 separated by spaces. Nothing else.";
20
+ /** 探针墙钟上限:超了就中断并报「超时」,不能让按钮一直转 */
21
+ const PROBE_TIMEOUT_MS = 60_000;
22
+ /** 低于这个输出量不报 tok/s(样本太短,算出来只是噪声) */
23
+ const MIN_TOKENS_FOR_TPS = 5;
24
+ /**
25
+ * 生成窗口至少这么长才报「生成速度」。
26
+ *
27
+ * 踩过的坑:deepseek-flash 出 40 个 token 只用了 145ms(delta 是攒包过来的),
28
+ * 算出来 276 tok/s —— 谁都知道这不可能是真的。窗口太短就只给端到端均速。
29
+ */
30
+ const MIN_GENERATION_MS_FOR_TPS = 500;
31
+ /** 探针 thread 的 cwd:临时目录(`isScratchCwd` 判定 → 不会污染侧栏的项目列表),跑完连目录一起删 */
32
+ const PROBE_CWD_PREFIX = "agent-web-probe-";
33
+ /**
34
+ * 一次测速的收尾算术(纯函数)。
35
+ *
36
+ * 出字速度按「首字节之后到收尾」这段算:把排队 / 长思考之前的等待扣除,
37
+ * 才是模型真正的生成速度(TTFT 单独给你看)。
38
+ */
39
+ function probeMetrics(probe, endedAt) {
40
+ const totalMs = Math.max(0, endedAt - probe.startedAt);
41
+ const ttftMs = probe.firstAt === null ? null : Math.max(0, probe.firstAt - probe.startedAt);
42
+ const generationMs = probe.firstAt === null ? totalMs : Math.max(1, endedAt - probe.firstAt);
43
+ const enoughTokens = probe.outputTokens >= MIN_TOKENS_FOR_TPS;
44
+ // 生成速度:只在那段窗口够长时才算(否则攒包会把数字抬成假的几百 tok/s)
45
+ const tps = enoughTokens && generationMs >= MIN_GENERATION_MS_FOR_TPS
46
+ ? Math.round((probe.outputTokens / generationMs) * 10_000) / 10
47
+ : null;
48
+ // 端到端均速:窗口短的时候靠它兜底(含首字延迟,不美化)
49
+ const averageTps = enoughTokens && totalMs > 0 ? Math.round((probe.outputTokens / totalMs) * 10_000) / 10 : null;
50
+ return { ok: true, ttftMs, totalMs, outputTokens: probe.outputTokens, tps, averageTps };
51
+ }
6
52
  export function createCodexEngine(options = {}) {
7
53
  const client = options.client ?? createCodexClient({ bin: options.bin, home: options.home, log: options.log ?? (() => { }) });
8
54
  const bindings = new Map();
9
55
  const byThread = new Map();
10
56
  const active = new Map();
57
+ /** threadId → 探针:测速用的临时 thread 不映射到任何会话,事件走这条支线 */
58
+ const probes = new Map();
11
59
  let models = null;
12
60
  let approvalHandler = null;
13
61
  /** 最近一次可预期失败的原因(给界面用,见 EnginePort.lastError) */
14
62
  let lastError = null;
63
+ /**
64
+ * codex 的原生子代理:我们只**观测 + 停止**(委派由模型自己发起),
65
+ * 见 codex-subagents.ts 文件头(含实测依据)。
66
+ */
67
+ const subagents = createCodexSubagents({
68
+ request: (method, params, requestOptions) => client.request(method, params, requestOptions),
69
+ log: (text) => options.log?.(text),
70
+ });
71
+ /** 现读策略:面板改了模板 / 开关,**下一条**新建的会话立刻生效(老会话保持原样) */
72
+ const policy = () => options.subagentPolicy?.() ?? { enabled: false, concurrency: subagents.concurrency(), profiles: [] };
15
73
  const threadOf = (sessionId) => {
16
74
  const binding = bindings.get(sessionId);
17
75
  if (binding)
@@ -32,9 +90,72 @@ export function createCodexEngine(options = {}) {
32
90
  models = result?.data ?? [];
33
91
  return models;
34
92
  };
93
+ /**
94
+ * 把 codex 的**多代理 item** 喂给子代理观测器(面板的运行卡片就是它来的)。
95
+ *
96
+ * 只在这条 item 属于某个会话时喂:`senderThreadId` 反查宿主会话;
97
+ * 反查不到(比如历史 thread)就丢掉 —— 面板只显示当前会话的子代理。
98
+ */
99
+ const ingestSubagent = (conversationId, item) => {
100
+ const type = item.type;
101
+ if (type !== "collabAgentToolCall" && type !== "subAgentActivity")
102
+ return;
103
+ // item 自己带 senderThreadId;没有就用当前通知的 threadId 反查到的会话
104
+ const sender = item.senderThreadId;
105
+ const owner = conversationId ?? (sender ? byThread.get(sender) : undefined);
106
+ if (!owner)
107
+ return;
108
+ subagents.ingest(owner, item);
109
+ };
110
+ /**
111
+ * 测速支线:只做三件事 —— 记首字节时刻、记输出 token、等回合收尾。
112
+ * 报错一律转成一句人话(不抛,否则会把探针 Promise 挂死)。
113
+ */
114
+ const handleProbeNotification = (probe, notification) => {
115
+ const params = notification.params;
116
+ switch (notification.method) {
117
+ case "item/agentMessage/delta":
118
+ case "item/reasoning/textDelta":
119
+ case "item/reasoning/summaryTextDelta":
120
+ if (probe.firstAt === null)
121
+ probe.firstAt = Date.now();
122
+ return;
123
+ case "thread/tokenUsage/updated": {
124
+ const last = params.tokenUsage?.last;
125
+ if (typeof last?.outputTokens === "number")
126
+ probe.outputTokens = last.outputTokens;
127
+ return;
128
+ }
129
+ case "turn/completed": {
130
+ const status = params.turn?.status;
131
+ if (status === "failed") {
132
+ probe.resolve({ ok: false, message: "codex 这一轮失败了(模型不可用或额度已尽)" });
133
+ return;
134
+ }
135
+ probe.resolve(probeMetrics(probe, Date.now()));
136
+ return;
137
+ }
138
+ case "error": {
139
+ // willRetry=true 是 codex 内部的瞬时重试:不要当失败
140
+ if (params.willRetry === true)
141
+ return;
142
+ const error = params.error;
143
+ probe.resolve({ ok: false, message: friendlyCodexError(new Error(error?.message ?? "codex 报错")) });
144
+ return;
145
+ }
146
+ default:
147
+ return;
148
+ }
149
+ };
35
150
  const handleNotification = (notification) => {
36
151
  const params = notification.params;
37
152
  const threadId = typeof params.threadId === "string" ? params.threadId : "";
153
+ // 测速的临时 thread 先认领:它不参与对话,事件只用来计时,绝不能串到任何会话里
154
+ const probe = threadId ? probes.get(threadId) : undefined;
155
+ if (probe) {
156
+ handleProbeNotification(probe, notification);
157
+ return;
158
+ }
38
159
  const conversationId = byThread.get(threadId);
39
160
  const turn = conversationId ? active.get(conversationId) : undefined;
40
161
  const emit = (event) => {
@@ -50,13 +171,17 @@ export function createCodexEngine(options = {}) {
50
171
  emit({ type: "thinking_delta", text: typeof params.delta === "string" ? params.delta : "" });
51
172
  return;
52
173
  case "item/started": {
53
- const event = toolStartOf((params.item ?? {}));
174
+ const item = (params.item ?? {});
175
+ ingestSubagent(conversationId, item);
176
+ const event = toolStartOf(item);
54
177
  if (event)
55
178
  emit(event);
56
179
  return;
57
180
  }
58
181
  case "item/completed": {
59
- const event = toolEndOf((params.item ?? {}));
182
+ const item = (params.item ?? {});
183
+ ingestSubagent(conversationId, item);
184
+ const event = toolEndOf(item);
60
185
  if (event)
61
186
  emit(event);
62
187
  return;
@@ -144,13 +269,42 @@ export function createCodexEngine(options = {}) {
144
269
  };
145
270
  return {
146
271
  client,
272
+ subagents,
147
273
  version: () => client.version(),
148
274
  async createSession(sessionOptions) {
149
275
  // 对齐原生:新会话落在 codex 的 project 里(没有就建一个,幂等键按目录)
150
276
  const projectId = await ensureCodexProject(client, sessionOptions.workspace);
277
+ /**
278
+ * 子代理(多代理)**按会话**开启:
279
+ * - `config.agents.*` / `features.multi_agent*` 是 `thread/start` 的按 thread 覆盖,
280
+ * 所以模板与并发上限只对这条会话生效 —— 不写用户全局 config,Codex Desktop 不受影响;
281
+ * - `developerInstructions` 里告诉模型「有哪些子代理类型、什么时候该派活」。
282
+ * 关掉时同样只影响这条会话:模型干脆没有 spawn 工具(而不是有个调不通的)。
283
+ */
284
+ const plan = policy();
151
285
  const response = await client.request("thread/start", {
152
286
  cwd: sessionOptions.workspace,
153
287
  ...(projectId ? { projectId } : {}),
288
+ config: codexMultiAgentConfig({
289
+ enabled: plan.enabled,
290
+ concurrency: plan.concurrency,
291
+ profiles: plan.enabled
292
+ ? plan.profiles.map((profile) => ({
293
+ id: profile.id,
294
+ label: profile.label,
295
+ description: profile.description,
296
+ }))
297
+ : [],
298
+ }),
299
+ // 「下一步建议」的写法一直带(ADR-0022,否则 codex 永远出不了建议按钮);
300
+ // 开了子代理就把委派说明接在后面。两者都是 **thread 级**覆盖,
301
+ // 只对这条新会话生效 —— 不写用户的 `~/.codex/config.toml`,Codex Desktop 不受影响。
302
+ developerInstructions: [
303
+ suggestionGuidance(options.thinkCount?.() ?? null),
304
+ codexSubagentGuidance({ enabled: plan.enabled, profiles: plan.profiles }),
305
+ ]
306
+ .filter((part) => part !== null)
307
+ .join("\n\n"),
154
308
  });
155
309
  const threadId = response?.thread?.id;
156
310
  if (!threadId)
@@ -244,6 +398,7 @@ export function createCodexEngine(options = {}) {
244
398
  byThread.clear();
245
399
  active.clear();
246
400
  models = null;
401
+ subagents.dispose?.();
247
402
  client.close();
248
403
  },
249
404
  setApprovalHandler(handler) {
@@ -259,6 +414,64 @@ export function createCodexEngine(options = {}) {
259
414
  async listModels() {
260
415
  return mapModels(await loadModels());
261
416
  },
417
+ /**
418
+ * 测速 = 在临时目录里开一个一次性 thread,发一句 `ping`,量首字节与出字速度。
419
+ *
420
+ * 为什么不拿 `~/.codex/auth.json` 去发 HTTP:那是偷凭证(见 port.ts 里的注释),
421
+ * 而且 ChatGPT 订阅额度的请求路径跟 API key 不是一回事,版本一变就坏。
422
+ * 这里用的全是本工具已在用的方法(thread/start → settings/update → turn/start → delete),
423
+ * 所以 codex 升级也不会多一个新依赖。
424
+ */
425
+ async probeModel(modelId) {
426
+ let threadId = null;
427
+ let cwd = null;
428
+ let timer = null;
429
+ try {
430
+ await client.ensureStarted();
431
+ // 模型名先对一下表:不对就直接报可读错误,不去烧一次真实回合
432
+ const catalog = await loadModels().catch(() => []);
433
+ if (catalog.length > 0 && !catalog.some((model) => (model.model || model.id) === modelId)) {
434
+ return { ok: false, message: `codex 里没有这个模型:${modelId}` };
435
+ }
436
+ cwd = await mkdtemp(join(tmpdir(), PROBE_CWD_PREFIX));
437
+ const created = await client.request("thread/start", { cwd });
438
+ threadId = created?.thread?.id ?? null;
439
+ if (!threadId)
440
+ return { ok: false, message: "codex 没有返回 thread id" };
441
+ // 用我们已经在用的那条路改模型(比猜 thread/start 的参数稳)
442
+ await client.request("thread/settings/update", { threadId, model: modelId });
443
+ const probeThreadId = threadId;
444
+ const measured = new Promise((resolve) => {
445
+ probes.set(probeThreadId, { startedAt: Date.now(), firstAt: null, outputTokens: 0, resolve });
446
+ });
447
+ const timedOut = new Promise((resolve) => {
448
+ timer = setTimeout(() => resolve({ ok: false, message: `测速超时(${Math.round(PROBE_TIMEOUT_MS / 1000)}s 没跑完)` }), PROBE_TIMEOUT_MS);
449
+ });
450
+ const turn = await client.request("turn/start", { threadId, input: [{ type: "text", text: PROBE_PROMPT, text_elements: [] }] }, { timeoutMs: 30_000 });
451
+ const result = await Promise.race([measured, timedOut]);
452
+ if (!result.ok) {
453
+ // 超时 / 抛错:把回合收掉,别让 codex 在后台继续烧 token
454
+ const turnId = turn?.turn?.id;
455
+ if (turnId)
456
+ await client.request("turn/interrupt", { threadId, turnId }).catch(() => undefined);
457
+ }
458
+ return result;
459
+ }
460
+ catch (error) {
461
+ return { ok: false, message: friendlyCodexError(error) };
462
+ }
463
+ finally {
464
+ if (timer)
465
+ clearTimeout(timer);
466
+ if (threadId) {
467
+ probes.delete(threadId);
468
+ // 探针 thread 不该留在 codex 的历史里(侧栏与 Codex Desktop 都会看到它)
469
+ await client.request("thread/delete", { threadId }).catch(() => undefined);
470
+ }
471
+ if (cwd)
472
+ await rm(cwd, { recursive: true, force: true }).catch(() => undefined);
473
+ }
474
+ },
262
475
  currentModel: currentModelOf,
263
476
  sessionModel: currentModelOf,
264
477
  async switchModel(sessionId, modelId) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * DSH 会话日志 → 工作台的对话区消息 / 转录 / 统计(纯函数,单测直接覆盖)。
2
+ * DSH 会话日志 → 工作台的对话区消息 / 转录 / 统计(纯函数)。
3
3
  *
4
4
  * 与 `dsh-log.ts` 分工:那边只管**怎么把文件读出来**(多帧 zstd、定位、标题素材),
5
5
  * 这里只管**怎么映射成我们的形状**(消息块 / 工具状态 / 四桶用量)。