muonroi-cli 1.7.2 → 1.8.0

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 (322) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +55 -60
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.js +43 -43
  253. package/dist/src/storage/transcript-view.js +7 -1
  254. package/dist/src/storage/transcript.d.ts +1 -0
  255. package/dist/src/storage/transcript.js +55 -0
  256. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  257. package/dist/src/storage/usage.js +14 -14
  258. package/dist/src/storage/workspaces.js +12 -12
  259. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  260. package/dist/src/tools/bash.d.ts +36 -23
  261. package/dist/src/tools/bash.js +59 -301
  262. package/dist/src/tools/bash.test.js +2 -257
  263. package/dist/src/tools/file-tracker.d.ts +5 -0
  264. package/dist/src/tools/file-tracker.js +14 -0
  265. package/dist/src/tools/file.js +2 -0
  266. package/dist/src/tools/git-safety.d.ts +38 -1
  267. package/dist/src/tools/git-safety.js +94 -4
  268. package/dist/src/tools/git-safety.test.js +45 -1
  269. package/dist/src/tools/native-tools.d.ts +1 -1
  270. package/dist/src/tools/native-tools.js +56 -0
  271. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  272. package/dist/src/tools/registry-git-safety.test.js +17 -6
  273. package/dist/src/tools/registry.d.ts +5 -0
  274. package/dist/src/tools/registry.js +185 -11
  275. package/dist/src/types/index.d.ts +7 -1
  276. package/dist/src/ui/app.d.ts +1 -0
  277. package/dist/src/ui/app.js +0 -0
  278. package/dist/src/ui/components/council-question-card.js +1 -0
  279. package/dist/src/ui/components/message-view.js +37 -3
  280. package/dist/src/ui/components/structured-response-view.js +7 -0
  281. package/dist/src/ui/components/task-list-panel.js +3 -12
  282. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  283. package/dist/src/ui/containers/chat-feed.js +66 -0
  284. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  285. package/dist/src/ui/containers/modals-layer.js +18 -0
  286. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  287. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  288. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  289. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  290. package/dist/src/ui/slash/council-inspect.js +4 -4
  291. package/dist/src/ui/slash/export.js +16 -1
  292. package/dist/src/ui/slash/menu-items.js +1 -0
  293. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  294. package/dist/src/ui/slash/ponytail.js +19 -0
  295. package/dist/src/ui/status-bar/store.js +0 -1
  296. package/dist/src/ui/status-bar/store.test.js +2 -2
  297. package/dist/src/ui/use-app-logic.d.ts +26 -0
  298. package/dist/src/ui/use-app-logic.js +0 -0
  299. package/dist/src/ui/utils/relaunch.js +1 -1
  300. package/dist/src/ui/utils/text.d.ts +11 -0
  301. package/dist/src/ui/utils/text.js +21 -1
  302. package/dist/src/ui/utils/text.test.js +25 -1
  303. package/dist/src/ui/utils/tools.js +3 -1
  304. package/dist/src/usage/cost-log.js +2 -2
  305. package/dist/src/usage/estimator.d.ts +14 -0
  306. package/dist/src/usage/estimator.js +22 -0
  307. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  308. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  309. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  311. package/dist/src/utils/clipboard-image.js +23 -23
  312. package/dist/src/utils/llm-deadline.d.ts +9 -1
  313. package/dist/src/utils/llm-deadline.js +31 -2
  314. package/dist/src/utils/permission-mode.d.ts +33 -0
  315. package/dist/src/utils/permission-mode.js +164 -3
  316. package/dist/src/utils/permission-mode.test.js +92 -1
  317. package/dist/src/utils/settings.d.ts +27 -31
  318. package/dist/src/utils/settings.js +100 -109
  319. package/dist/src/utils/side-question.js +2 -2
  320. package/dist/src/utils/skills.js +3 -3
  321. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  322. package/package.json +1 -1
@@ -34,8 +34,10 @@ import { normalizeModelId } from "../models/registry.js";
34
34
  import { cheapModelShellLine, injectCheapModelPlaybook, injectCheapModelShellDirective, shouldInjectCheapModelPlaybook, } from "../pil/cheap-model-playbook.js";
35
35
  import { injectCheapModelWorkbook, shouldInjectCheapModelWorkbook, subagentTaskType, } from "../pil/cheap-model-workbooks.js";
36
36
  import { getProviderCapabilities } from "../providers/capabilities.js";
37
+ import { getVisionGuidanceForTextOnly } from "../providers/mcp-vision-bridge.js";
37
38
  import { captureToolSchemas } from "../providers/patch-zod-schema.js";
38
39
  import { buildTurnProviderOptions, requireRuntimeProvider, resolveModelRuntime, shouldDropParam, } from "../providers/runtime.js";
40
+ import { needsVisionProxy } from "../providers/vision-proxy.js";
39
41
  import { wireDebug } from "../providers/wire-debug.js";
40
42
  import { BashTool } from "../tools/bash.js";
41
43
  import { createBuiltinTools } from "../tools/registry.js";
@@ -56,7 +58,7 @@ import { attachReminderToMessages, buildScopeReminder, cadenceForSize, shouldInj
56
58
  import { recordCompaction, recordElision } from "./session-experience.js";
57
59
  import { createStallWatchdog, STALL_ERROR_MESSAGE } from "./stall-watchdog.js";
58
60
  import { wrapToolSetWithCap } from "./sub-agent-cap.js";
59
- import { compactSubAgentMessages } from "./subagent-compactor.js";
61
+ import { compactSubAgentMessages, applyAnthropicPromptCaching } from "./subagent-compactor.js";
60
62
  import { combineAbortSignals, firstLine, formatSubagentActivity } from "./tool-utils.js";
61
63
  /**
62
64
  * StreamRunner — extracted sub-agent stream lifecycle.
@@ -133,10 +135,19 @@ export class StreamRunner {
133
135
  : topBash.getSandboxSettings(),
134
136
  shellSettings: getCurrentShellSettings(),
135
137
  });
138
+ // Resolve child model early so we can pass modelId to createBuiltinTools
139
+ // (needed for vision-proxy tools: analyze_image / ask_vision_proxy).
140
+ const childModelId = normalizeModelId(isVision
141
+ ? VISION_MODEL
142
+ : isComputer
143
+ ? COMPUTER_MODEL
144
+ : custom
145
+ ? custom.model
146
+ : this.deps.resolveModelForTask(isExplore ? "explore" : isVerify || isVerifyDetect || isVerifyManifest ? "verify" : "general"));
136
147
  // Mirror the file-local `createTools` wrapper from orchestrator.ts —
137
- // it calls createBuiltinTools(bash, mode) without provider/opts; the
138
- // provider arg in the original wrapper was unused.
139
- const childBaseToolsRaw = createBuiltinTools(childBash, childMode);
148
+ // pass modelId so registry can inject analyze_image/ask_vision_proxy for
149
+ // text-only child models (needsVisionProxy).
150
+ const childBaseToolsRaw = createBuiltinTools(childBash, childMode, { modelId: childModelId });
140
151
  // Wrap with the cumulative cap so the sub-agent's tool loop cannot
141
152
  // accumulate unbounded tool_result tokens. See sub-agent-cap.ts for the
142
153
  // tiered compression schedule. The cap is per-invocation; each sub-agent
@@ -161,13 +172,6 @@ export class StreamRunner {
161
172
  let lastActivity = initialDetail;
162
173
  let childTools = childBaseTools;
163
174
  let closeMcp;
164
- const childModelId = normalizeModelId(isVision
165
- ? VISION_MODEL
166
- : isComputer
167
- ? COMPUTER_MODEL
168
- : custom
169
- ? custom.model
170
- : this.deps.resolveModelForTask(isExplore ? "explore" : isVerify || isVerifyDetect || isVerifyManifest ? "verify" : "general"));
171
175
  const topModelId = this.deps.getModelId();
172
176
  if (childModelId !== topModelId) {
173
177
  statusBarStore.setState({ routed_from: topModelId, model: childModelId });
@@ -212,6 +216,11 @@ export class StreamRunner {
212
216
  const childSystem = shouldInjectCheapModelPlaybook(childRuntime.modelInfo)
213
217
  ? injectCheapModelShellDirective(childWithPlaybook, cheapModelShellLine(resolveShell({}).kind, process.platform))
214
218
  : childWithPlaybook;
219
+ // Inject vision proxy guidance for text-only child models (DeepSeek etc.)
220
+ // so sub-agents know to use analyze_image / ask_vision_proxy when they
221
+ // receive image context or file paths. Mirrors top-level in message-processor.
222
+ const visionGuidance = needsVisionProxy(childModelId) ? getVisionGuidanceForTextOnly(childModelId) : "";
223
+ const childSystemWithVision = visionGuidance ? `${childSystem}\n\n${visionGuidance}` : childSystem;
215
224
  onActivity?.(initialDetail);
216
225
  if (childMode === "agent" && taskCaps.supportsClientTools(childRuntime.modelInfo)) {
217
226
  const mcpBundle = await acquireMcpTools(loadMcpServers(), {
@@ -245,7 +254,8 @@ export class StreamRunner {
245
254
  // signal is intentionally unused on this branch — vision was the only consumer.
246
255
  void signal;
247
256
  const childMessages = [{ role: "user", content: childPrompt }];
248
- const maxSteps = Math.min(this.deps.getMaxToolRounds(), isExplore ? 60 : 120);
257
+ // The main agent manages its sub-agents, so don't apply an arbitrary hard limit.
258
+ const maxSteps = request.maxToolRounds ?? (this.deps.getMaxToolRounds() * 2);
249
259
  // F1 parity — derive per-turn providerOptions so the sub-agent OpenAI calls
250
260
  // carry a stable session-derived promptCacheKey (every tool round routes to
251
261
  // the same cache node, keeping the unchanging prefix cached). The top-level
@@ -265,7 +275,7 @@ export class StreamRunner {
265
275
  childMode,
266
276
  childBash,
267
277
  childRuntime,
268
- childSystem,
278
+ childSystem: childSystemWithVision,
269
279
  childMessages,
270
280
  childTools,
271
281
  childProviderOptions,
@@ -348,16 +358,16 @@ export class StreamRunner {
348
358
  // Explore sub-agents are READ-ONLY research — a codebase investigation
349
359
  // legitimately needs more grep/read steps than the tight general/medium=10
350
360
  // cell allows. Cutting it early (esp. for reasoning models that front-load
351
- // tool calls) leaves no budget to reach the synthesis turn (root cause of
352
- // empty "Task completed. Last action: grep" returns). Give explore the
353
- // analyze/large cell (15); edit-capable agents keep general/medium (10).
361
+ // Sub-agent ceiling is no longer a hard halt. Per user request:
362
+ // "cũng áp dụng với sub agent nhé không nên hardcode maxtool mà nếu có
363
+ // vấn đề sẽ main agent (khi spawn) kiểm soát đừng hard"
354
364
  const _subCeiling = isExplore ? resolveCeiling("analyze", "large") : resolveCeiling("general", "medium");
355
365
  const _subCounterKey = `subagent:${subCallId}`;
356
366
  const _subStopWhen = (async (state) => {
367
+ incSessionStep(_subCounterKey); // Keep telemetry counter ticking
357
368
  if (state.steps.length >= maxSteps)
358
369
  return true;
359
- const next = incSessionStep(_subCounterKey);
360
- return next >= _subCeiling;
370
+ return false;
361
371
  });
362
372
  // Silent-hang guard — mirror the top-level loop (message-processor.ts).
363
373
  // A sub-agent provider connection can accept the request but never send a
@@ -436,12 +446,19 @@ export class StreamRunner {
436
446
  /* fail-open */
437
447
  }
438
448
  };
449
+ // T1.1 + T1.2 — reasoning models (DeepSeek V4 Flash, R1) emit 2K-5K
450
+ // CoT tokens per turn that accumulate across the multi-step loop.
451
+ // Strip old reasoning and compact earlier (ratio 0.3 vs 0.5) to
452
+ // cut ~40-60% of cumulative input tokens.
453
+ const isReasoningModel = childRuntime.modelInfo?.reasoning === true;
439
454
  const compacted = compactSubAgentMessages(stripped, {
440
455
  thresholdChars: compactThreshold,
441
456
  keepLastTurns: compactKeepLast,
442
457
  contextWindowTokens: childCtxWindow,
458
+ contextFillRatio: isReasoningModel ? 0.3 : undefined,
443
459
  keepToolIds: subKeepToolIds.length ? subKeepToolIds : undefined,
444
460
  persistArtifact: persistSubArtifact,
461
+ stripOldReasoning: isReasoningModel,
445
462
  });
446
463
  if (compacted !== stripped)
447
464
  recordCompaction(stepNumber);
@@ -456,21 +473,26 @@ export class StreamRunner {
456
473
  const _subK = cadenceForSize(_subSize);
457
474
  const _subShouldRemind = shouldInjectReminder(stepNumber, _subK);
458
475
  const _subShouldWarn = shouldInjectSoftWarn(stepNumber, _subCeiling, _subCounterKey);
459
- if (_subShouldRemind || _subShouldWarn) {
460
- const _baseReminder = buildScopeReminder({
461
- step: stepNumber,
462
- ceiling: _subCeiling,
463
- taskType: "general",
464
- size: _subSize,
465
- originalPrompt: prepared.request.prompt,
466
- });
467
- const _reminder = _subShouldWarn ? `[approaching ceiling] ${_baseReminder}` : _baseReminder;
468
- const withReminder = attachReminderToMessages(compacted, _reminder);
469
- return { messages: withReminder };
476
+ const finalMessages = (() => {
477
+ if (_subShouldRemind || _subShouldWarn) {
478
+ const _baseReminder = buildScopeReminder({
479
+ step: stepNumber,
480
+ ceiling: _subCeiling,
481
+ taskType: "general",
482
+ size: _subSize,
483
+ originalPrompt: prepared.request.prompt,
484
+ });
485
+ const _reminder = _subShouldWarn ? `[approaching ceiling] ${_baseReminder}` : _baseReminder;
486
+ return attachReminderToMessages(compacted, _reminder);
487
+ }
488
+ return compacted;
489
+ })();
490
+ if (childRuntime.modelId.startsWith("claude")) {
491
+ return { messages: applyAnthropicPromptCaching(finalMessages, childRuntime.modelId) };
470
492
  }
471
493
  if (compacted === stripped && stripped === messages)
472
494
  return undefined;
473
- return { messages: compacted };
495
+ return { messages: finalMessages };
474
496
  },
475
497
  ...(childDropTemperature ? {} : { temperature: isExplore ? 0.2 : 0.5 }),
476
498
  ...(childDropMaxOutput ? {} : { maxOutputTokens: Math.min(this.deps.getMaxTokens(), 8_192) }),
@@ -24,7 +24,7 @@ export const TASK_TIER_PREFS = {
24
24
  // general sub-agents downgrade to balanced (was premium-first) — the top
25
25
  // model already planned the delegation; the focused execution doesn't need
26
26
  // premium. Premium remains the fallback so premium-only providers are unaffected.
27
- general: ["balanced", "premium"],
27
+ general: ["balanced", "fast", "premium"],
28
28
  // verify keeps premium-first: a downgrade here would weaken the very check it
29
29
  // exists to perform. Mirrors council `verify` role preference.
30
30
  verify: ["premium", "balanced"],
@@ -96,6 +96,14 @@ export interface SubAgentCompactorOptions {
96
96
  * source:"tool-artifact" so layer3/ee.query can later fetch "full tool result id=xxx".
97
97
  */
98
98
  persistArtifact?: (toolCallId: string, toolName: string, fullContent: string, reason: string) => void;
99
+ /**
100
+ * T1.1 — strip reasoning parts from old assistant turns (older than
101
+ * keepLastTurns). Reasoning tokens (CoT / <think>) from prior turns are
102
+ * never re-read by the model but accumulate O(N) chars per turn, costing
103
+ * full input billing on every subsequent step. Default true for reasoning
104
+ * models, false otherwise.
105
+ */
106
+ stripOldReasoning?: boolean;
99
107
  }
100
108
  /**
101
109
  * G1 — coarse char→token conversion. The real ratio is provider/tokenizer
@@ -140,3 +148,9 @@ export declare function cumulativeMessageChars(messages: ReadonlyArray<ModelMess
140
148
  * step.
141
149
  */
142
150
  export declare function compactSubAgentMessages(messages: ReadonlyArray<ModelMessage>, opts?: SubAgentCompactorOptions): ModelMessage[];
151
+ /**
152
+ * Injects Anthropic prompt caching (cacheControl) into the last message's content
153
+ * block(s) if the model is Claude (starts with 'claude').
154
+ * Creates a copy of the messages array and the last message to avoid mutating in-place.
155
+ */
156
+ export declare function applyAnthropicPromptCaching(messages: readonly ModelMessage[], modelId: string): ModelMessage[];
@@ -104,11 +104,12 @@ export function isHighValueToolResult(toolName, preview, explicitKeepIds, toolCa
104
104
  const p = preview.toLowerCase();
105
105
  if (/error|fail|todo|plan|done|✔|blocked|critical/.test(p))
106
106
  return true;
107
- if (/\.(ts|tsx|js|md|json|test|spec)\b/.test(p) || p.includes("src/") || p.includes("PLAN"))
108
- return true;
109
- if (preview.length > 1500)
110
- return true;
111
- return true; // read_file/grep etc on source are presumptively high-value
107
+ if (/\.(ts|tsx|js|md|json|test|spec)\b/.test(p) || p.includes("src/") || p.includes("PLAN")) {
108
+ // For source code/plans, only keep verbatim if it is relatively small (under 12000 chars)
109
+ // to prevent large file reads from leaking memory/tokens indefinitely in long sessions.
110
+ return preview.length <= 12000;
111
+ }
112
+ return false;
112
113
  }
113
114
  if (toolCallId && explicitKeepIds?.has(toolCallId))
114
115
  return true;
@@ -126,6 +127,7 @@ function resolveOpts(o) {
126
127
  contextFillRatio: Math.min(0.95, Math.max(0.1, o?.contextFillRatio ?? 0.5)),
127
128
  keepToolIds: keepIds,
128
129
  persistArtifact: o?.persistArtifact,
130
+ stripOldReasoning: o?.stripOldReasoning ?? false,
129
131
  };
130
132
  }
131
133
  /**
@@ -378,17 +380,43 @@ export function compactSubAgentMessages(messages, opts = {}) {
378
380
  continue;
379
381
  }
380
382
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
381
- // F1 — strip args off older assistant tool-call shells. 50+ of these
382
- // accumulate ~10-30K chars of args (file paths, bash commands) that
383
- // the model does not need once the matching result has been elided.
384
- // We keep toolCallId + toolName so pairing with tool-result is intact.
385
- out.push(stripAssistantToolCallArgs(msg));
383
+ // T1.1 — strip reasoning parts from older assistant turns. DeepSeek V4
384
+ // Flash / R1 emit 2K-5K reasoning tokens per turn that accumulate
385
+ // across the multi-step loop. These are never re-read by the model
386
+ // but cost full input billing on every subsequent round. Strip them
387
+ // from turns older than keepLastTurns to cut ~30-50% of input tokens.
388
+ // Then F1 — strip args off older assistant tool-call shells.
389
+ let processed = resolved.stripOldReasoning ? stripAssistantReasoning(msg) : msg;
390
+ processed = stripAssistantToolCallArgs(processed);
391
+ out.push(processed);
386
392
  continue;
387
393
  }
388
394
  out.push(msg);
389
395
  }
390
396
  return out;
391
397
  }
398
+ /**
399
+ * T1.1 — strip reasoning parts from an assistant message. Reasoning tokens
400
+ * (CoT / `<think>`) from older turns have zero re-read value for the model
401
+ * on subsequent steps but they accumulate O(N) chars per turn and are billed
402
+ * as full input tokens. Removing them from turns older than keepLastTurns
403
+ * cuts ~30-50% of cumulative input in multi-step loops with reasoning models
404
+ * (DeepSeek V4 Flash, R1, etc.).
405
+ *
406
+ * Preserves text + tool-call parts (the structural skeleton the model needs
407
+ * to maintain coherent tool-call↔tool-result pairing).
408
+ */
409
+ function stripAssistantReasoning(msg) {
410
+ if (!Array.isArray(msg.content))
411
+ return msg;
412
+ const parts = msg.content;
413
+ const filtered = parts.filter((part) => part.type !== "reasoning");
414
+ if (filtered.length === parts.length)
415
+ return msg; // nothing stripped
416
+ // Edge case: if ALL parts were reasoning (no text/tool-call), keep the
417
+ // message with an empty content array to preserve message-count pairing.
418
+ return { ...msg, content: filtered };
419
+ }
392
420
  function stripAssistantToolCallArgs(msg) {
393
421
  if (!Array.isArray(msg.content))
394
422
  return msg;
@@ -424,4 +452,49 @@ function stripAssistantToolCallArgs(msg) {
424
452
  return msg;
425
453
  return { ...msg, content: next };
426
454
  }
455
+ /**
456
+ * Injects Anthropic prompt caching (cacheControl) into the last message's content
457
+ * block(s) if the model is Claude (starts with 'claude').
458
+ * Creates a copy of the messages array and the last message to avoid mutating in-place.
459
+ */
460
+ export function applyAnthropicPromptCaching(messages, modelId) {
461
+ if (!modelId.startsWith("claude")) {
462
+ return messages;
463
+ }
464
+ if (messages.length === 0) {
465
+ return messages;
466
+ }
467
+ const newMessages = [...messages];
468
+ const lastIndex = newMessages.length - 1;
469
+ const originalLastMsg = newMessages[lastIndex];
470
+ if (!originalLastMsg)
471
+ return messages;
472
+ const lastMsg = { ...originalLastMsg };
473
+ if (typeof lastMsg.content === "string") {
474
+ lastMsg.content = [
475
+ {
476
+ type: "text",
477
+ text: lastMsg.content,
478
+ providerOptions: {
479
+ anthropic: { cacheControl: { type: "ephemeral" } },
480
+ },
481
+ },
482
+ ];
483
+ }
484
+ else if (Array.isArray(lastMsg.content)) {
485
+ const content = [...lastMsg.content];
486
+ const lastBlockIndex = content.length - 1;
487
+ if (lastBlockIndex >= 0) {
488
+ const lastBlock = { ...content[lastBlockIndex] };
489
+ lastBlock.providerOptions = {
490
+ ...lastBlock.providerOptions,
491
+ anthropic: { cacheControl: { type: "ephemeral" } },
492
+ };
493
+ content[lastBlockIndex] = lastBlock;
494
+ lastMsg.content = content;
495
+ }
496
+ }
497
+ newMessages[lastIndex] = lastMsg;
498
+ return newMessages;
499
+ }
427
500
  //# sourceMappingURL=subagent-compactor.js.map
@@ -384,5 +384,137 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
384
384
  }
385
385
  expect(keptExplicit).toBe(true);
386
386
  });
387
+ it("elides low-value large read_file results while keeping high-value ones", () => {
388
+ // Turn 1: large low-value read_file output (15000 chars of 'x', no ts/src/PLAN/error keywords)
389
+ const lowValueOutput = "x".repeat(15000);
390
+ // Turn 2: large high-value read_file output containing 'error'
391
+ const highValueOutputWithError = "error: failed to load module\n" + "x".repeat(15000);
392
+ // Turn 3: small source file read (5000 chars of 'x' containing 'src/index.ts')
393
+ const highValueSourceOutput = "src/index.ts\n" + "x".repeat(5000);
394
+ const msgs = [
395
+ { role: "system", content: "You are the agent." },
396
+ { role: "user", content: "perform task" },
397
+ {
398
+ role: "assistant",
399
+ content: [{ type: "tool-call", toolCallId: "call_low", toolName: "read_file", input: {} }],
400
+ },
401
+ {
402
+ role: "tool",
403
+ content: [{ type: "tool-result", toolCallId: "call_low", toolName: "read_file", output: { type: "text", value: lowValueOutput } }],
404
+ },
405
+ {
406
+ role: "assistant",
407
+ content: [{ type: "tool-call", toolCallId: "call_err", toolName: "read_file", input: {} }],
408
+ },
409
+ {
410
+ role: "tool",
411
+ content: [{ type: "tool-result", toolCallId: "call_err", toolName: "read_file", output: { type: "text", value: highValueOutputWithError } }],
412
+ },
413
+ {
414
+ role: "assistant",
415
+ content: [{ type: "tool-call", toolCallId: "call_src", toolName: "read_file", input: {} }],
416
+ },
417
+ {
418
+ role: "tool",
419
+ content: [{ type: "tool-result", toolCallId: "call_src", toolName: "read_file", output: { type: "text", value: highValueSourceOutput } }],
420
+ },
421
+ // Add a couple of low-value turns to push past threshold and keepLast turns (keepLast=1)
422
+ ...toolTurn(9, 10),
423
+ ];
424
+ // Force low value on the final kept turn
425
+ msgs[msgs.length - 1].content[0].toolName = "other_tool";
426
+ const out = compactSubAgentMessages(msgs, { thresholdChars: 10_000, keepLastTurns: 1 });
427
+ // low-value large read_file MUST be elided
428
+ const lowValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_low");
429
+ expect((lowValMsg?.content)[0].output.value).toMatch(/elided by sub-agent compactor/);
430
+ // high-value large error read_file MUST NOT be elided
431
+ const errValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_err");
432
+ expect((errValMsg?.content)[0].output.value).toBe(highValueOutputWithError);
433
+ // high-value small source read_file MUST NOT be elided
434
+ const srcValMsg = out.find(m => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_src");
435
+ expect((srcValMsg?.content)[0].output.value).toBe(highValueSourceOutput);
436
+ });
437
+ it("T1.1 — strips reasoning parts from old assistant turns when stripOldReasoning is enabled", () => {
438
+ // Build history with reasoning parts in assistant messages
439
+ const msgs = [
440
+ { role: "system", content: "You are an agent." },
441
+ { role: "user", content: "fix the bug" },
442
+ ];
443
+ // Add 6 tool turns with reasoning in assistant messages
444
+ for (let i = 1; i <= 6; i++) {
445
+ msgs.push({
446
+ role: "assistant",
447
+ content: [
448
+ { type: "reasoning", text: `Thinking about step ${i}... ${"x".repeat(3000)}` },
449
+ { type: "text", text: `Step ${i} analysis` },
450
+ { type: "tool-call", toolCallId: `call_${i}`, toolName: "bash", input: `echo ${i}` },
451
+ ],
452
+ });
453
+ msgs.push({
454
+ role: "tool",
455
+ content: [
456
+ { type: "tool-result", toolCallId: `call_${i}`, toolName: "bash", output: { type: "text", value: bigText(`R${i}`, 2) } },
457
+ ],
458
+ });
459
+ }
460
+ const out = compactSubAgentMessages(msgs, {
461
+ thresholdChars: 1000, // force compaction
462
+ keepLastTurns: 2,
463
+ stripOldReasoning: true,
464
+ });
465
+ // Old turns (1-4) should have reasoning stripped
466
+ const oldAssistants = out.filter((m, idx) => m.role === "assistant" && idx < out.length - 4);
467
+ for (const m of oldAssistants) {
468
+ const content = m.content;
469
+ if (!Array.isArray(content))
470
+ continue;
471
+ const hasReasoning = content.some((p) => p.type === "reasoning");
472
+ expect(hasReasoning).toBe(false);
473
+ }
474
+ // Recent turns (last 2) should still have reasoning
475
+ const recentAssistants = out.filter((m, idx) => m.role === "assistant" && idx >= out.length - 4);
476
+ for (const m of recentAssistants) {
477
+ const content = m.content;
478
+ if (!Array.isArray(content))
479
+ continue;
480
+ const hasReasoning = content.some((p) => p.type === "reasoning");
481
+ expect(hasReasoning).toBe(true);
482
+ }
483
+ });
484
+ it("T1.1 — does NOT strip reasoning when stripOldReasoning is false (default)", () => {
485
+ const msgs = [
486
+ { role: "system", content: "You are an agent." },
487
+ { role: "user", content: "fix the bug" },
488
+ ];
489
+ for (let i = 1; i <= 4; i++) {
490
+ msgs.push({
491
+ role: "assistant",
492
+ content: [
493
+ { type: "reasoning", text: `Thinking... ${"x".repeat(3000)}` },
494
+ { type: "tool-call", toolCallId: `call_${i}`, toolName: "bash", input: `echo ${i}` },
495
+ ],
496
+ });
497
+ msgs.push({
498
+ role: "tool",
499
+ content: [
500
+ { type: "tool-result", toolCallId: `call_${i}`, toolName: "bash", output: { type: "text", value: bigText(`R${i}`, 2) } },
501
+ ],
502
+ });
503
+ }
504
+ const out = compactSubAgentMessages(msgs, {
505
+ thresholdChars: 1000,
506
+ keepLastTurns: 1,
507
+ stripOldReasoning: false, // explicitly off
508
+ });
509
+ // All assistant messages should still have reasoning parts
510
+ const assistants = out.filter((m) => m.role === "assistant");
511
+ for (const m of assistants) {
512
+ const content = m.content;
513
+ if (!Array.isArray(content))
514
+ continue;
515
+ const hasReasoning = content.some((p) => p.type === "reasoning");
516
+ expect(hasReasoning).toBe(true);
517
+ }
518
+ });
387
519
  });
388
520
  //# sourceMappingURL=subagent-compactor.spec.js.map
@@ -42,12 +42,16 @@ export declare function detectTextEmittedToolCall(text: string): TextToolCallDet
42
42
  * Parse the DeepSeek-native DSML tool-call markup into a structured list so the
43
43
  * re-steer can restate the model's EXACT intent (much more effective than a
44
44
  * generic "use the tool interface" nudge). Pure — no execution. Recognizes:
45
- * <||DSML||invoke name="read_file">
46
- * <||DSML||parameter name="file_path" string="true">src/app/foo.ts</||DSML||parameter>
47
- * </||DSML||invoke>
45
+ * Old format: <│invoke name="read_file">
46
+ * <│parameter name="file_path" string="true">src/app/foo.ts</│parameter>
47
+ * </│invoke>
48
+ * New format: <│ DSML │invoke name="read_file">
49
+ * <│ DSML │parameter name="file_path" string="true">src/app/foo.ts</│ DSML │parameter>
50
+ * </│ DSML │invoke>
51
+ * Both U+2502 (box-drawing │) and U+FF5C (fullwidth |) bars are recognized.
48
52
  * Returns one entry per invoke block; args preserve insertion order. Tolerant of
49
- * missing close tags (cheap models truncate) captures whatever parameters are
50
- * present. Returns [] when no parseable invoke block exists.
53
+ * missing close tags (cheap models truncate). Returns [] when no parseable invoke
54
+ * block exists.
51
55
  */
52
56
  export interface ParsedDsmlCall {
53
57
  name: string;
@@ -90,16 +90,18 @@ const PARAM_ALTERNATION = PARAM_TAGS.join("|");
90
90
  // is the Anthropic XML style; `<tool_call>` / `<function_calls>` are Qwen/other.
91
91
  // These are matched directly (the wrapper itself is the signal).
92
92
  const GENERIC_WRAPPER_RE = /<\/?(?:tool_call|function_calls|tool_use)\b|<invoke\b[^>]*\bname\s*=|<function\b[^>]*\bname\s*=/i;
93
- // DeepSeek native tool-call markup leaking into text content. Signature is the
94
- // fullwidth-bar sentinel `||DSML||` (U+FF5C, NOT ASCII pipes) wrapping
95
- // invoke/tool_calls/parameter tokens, e.g. `<||DSML||invoke name="read_file">`.
93
+ // DeepSeek native tool-call markup leaking into text content. Signature is a
94
+ // vertical-bar sentinel either U+FF5C fullwidth `|` or U+2502 box-drawing `│`
95
+ // — wrapping invoke/tool_calls/parameter tokens, e.g.:
96
+ // Old format: <|invoke name="read_file"> (U+FF5C, single bar)
97
+ // New format: <│ DSML │invoke name="…"> (U+2502, with optional DSML label)
96
98
  // Live: storyflow_ui explore-A/B, deepseek T3 (session 799f0508e830) emitted a
97
- // full `<||DSML||invoke name="read_file">…` block as text and made no real tool
98
- // call → empty, silent turn. The generic `<invoke` matcher misses it because the
99
- // `<` is followed by the sentinel, not `invoke`. (BUG-A telemetry already flags
100
- // the `||DSML||` substring; this wires it into detect re-steer/surface.)
101
- const DSML_WRAPPER_RE = /||DSML||\s*(?:invoke|tool_calls?|parameter)\b/i;
102
- const DSML_INVOKE_NAME_RE = /||DSML||\s*invoke\s+name\s*=\s*"([^"]+)"/i;
99
+ // full DSML invoke block as text and made no real tool call → empty, silent turn.
100
+ // The generic `<invoke` matcher misses it because `<` is followed by the sentinel.
101
+ // Updated 2026-06-24 to cover both U+2502 and U+FF5C (tests use U+2502).
102
+ const DSML_BAR = "[\\s]*[│||]+[│||\\s]*"; // matches U+2502 (box-drawing), U+FF5C (fullwidth), ASCII pipe |, and spaces
103
+ const DSML_WRAPPER_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?(?:invoke|tool_calls?|parameter)\\b`, "i");
104
+ const DSML_INVOKE_NAME_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s+name\\s*=\\s*"([^"]+)"`, "i");
103
105
  /** Build a per-tool detector: `<tool>` then (within a small gap) a `<param>` or `</tool>`. */
104
106
  function buildToolRegexes() {
105
107
  return TOOL_TAGS.map((tag) =>
@@ -136,10 +138,12 @@ function normalizeWrapperName(raw) {
136
138
  const m = raw.match(/tool_call|function_calls|tool_use|invoke|function/i);
137
139
  return m ? m[0].toLowerCase() : "tool_call";
138
140
  }
139
- const DSML_INVOKE_BLOCK_RE = /||DSML||\s*invoke\s+name\s*=\s*"([^"]+)"([\s\S]*?)(?=||DSML||\s*invoke\s|$)/gi;
140
- const DSML_PARAM_RE = /||DSML||\s*parameter\s+name\s*=\s*"([^"]+)"[^>]*>([\s\S]*?)<\/?||DSML||\s*parameter/gi;
141
+ // Guard regex: at least one DSML-bar sentinel must exist before we bother scanning
142
+ const DSML_GUARD_RE = /[│||]/;
143
+ const DSML_INVOKE_BLOCK_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s+name\\s*=\\s*"([^"]+)"([\\s\\S]*?)(?=${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s|$)`, "gi");
144
+ const DSML_PARAM_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?parameter\\s+name\\s*=\\s*"([^"]+)"[^>]*>([\\s\\S]*?)<\\/?${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?parameter`, "gi");
141
145
  export function parseDsmlToolCalls(text) {
142
- if (!text || !text.includes("||DSML||"))
146
+ if (!text || !DSML_GUARD_RE.test(text))
143
147
  return [];
144
148
  const calls = [];
145
149
  DSML_INVOKE_BLOCK_RE.lastIndex = 0;