@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/README.md +13 -10
  2. package/dist/agent/agent-definition-registry.d.ts +3 -0
  3. package/dist/agent/agent-definition-registry.js +3 -1
  4. package/dist/agent/agent-definition.d.ts +15 -0
  5. package/dist/agent/agent-definition.js +27 -3
  6. package/dist/arena/context/context-tools.js +47 -4
  7. package/dist/arena/ledger.js +9 -1
  8. package/dist/arena/strategies/utils.d.ts +1 -4
  9. package/dist/arena/strategies/utils.js +7 -84
  10. package/dist/automation/cron-expr.d.ts +10 -0
  11. package/dist/automation/cron-expr.js +23 -5
  12. package/dist/automation/runner.d.ts +21 -4
  13. package/dist/automation/runner.js +48 -12
  14. package/dist/automation/scheduler.d.ts +38 -0
  15. package/dist/automation/scheduler.js +89 -12
  16. package/dist/automation/store.js +5 -2
  17. package/dist/capability-control/disabled-lists.d.ts +25 -0
  18. package/dist/capability-control/disabled-lists.js +57 -0
  19. package/dist/capability-control/overlay.d.ts +15 -0
  20. package/dist/capability-control/overlay.js +27 -0
  21. package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
  22. package/dist/cc-orchestrator/agent-adapter.js +133 -0
  23. package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
  24. package/dist/cc-orchestrator/cc-capability.js +53 -0
  25. package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
  26. package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
  27. package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
  28. package/dist/cc-orchestrator/codex-session-history.js +187 -0
  29. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  30. package/dist/cc-orchestrator/external-agent-changes.js +214 -0
  31. package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
  32. package/dist/cc-orchestrator/external-agent-driver.js +69 -0
  33. package/dist/cc-orchestrator/index.d.ts +8 -0
  34. package/dist/cc-orchestrator/index.js +8 -0
  35. package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
  36. package/dist/cc-orchestrator/relevance-judge.js +29 -0
  37. package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
  38. package/dist/cc-orchestrator/session-discovery.js +125 -0
  39. package/dist/cc-orchestrator/session-history.d.ts +19 -0
  40. package/dist/cc-orchestrator/session-history.js +67 -0
  41. package/dist/cli/agent-server-stdio.d.ts +15 -1
  42. package/dist/cli/agent-server-stdio.js +115 -17
  43. package/dist/cli/agent-server-tcp.js +40 -26
  44. package/dist/context/compaction.d.ts +86 -0
  45. package/dist/context/compaction.js +279 -0
  46. package/dist/context/manager.d.ts +18 -0
  47. package/dist/context/manager.js +181 -48
  48. package/dist/context/token-counter.js +13 -0
  49. package/dist/cost-tracker.js +5 -61
  50. package/dist/credentials/cipher.d.ts +49 -0
  51. package/dist/credentials/cipher.js +45 -0
  52. package/dist/credentials/cookie-jar.d.ts +24 -0
  53. package/dist/credentials/cookie-jar.js +40 -0
  54. package/dist/credentials/index.d.ts +7 -0
  55. package/dist/credentials/index.js +5 -0
  56. package/dist/credentials/inject-credential-tool.d.ts +20 -0
  57. package/dist/credentials/inject-credential-tool.js +130 -0
  58. package/dist/credentials/store.d.ts +72 -0
  59. package/dist/credentials/store.js +184 -0
  60. package/dist/credentials/types.d.ts +56 -0
  61. package/dist/credentials/use-credential-tool.d.ts +29 -0
  62. package/dist/credentials/use-credential-tool.js +205 -0
  63. package/dist/credentials/use-gate.d.ts +56 -0
  64. package/dist/credentials/use-gate.js +52 -0
  65. package/dist/data/model-metadata.d.ts +77 -0
  66. package/dist/data/model-metadata.js +56 -0
  67. package/dist/data/model-metadata.json +216 -0
  68. package/dist/data/openrouter-models.d.ts +18 -7
  69. package/dist/data/openrouter-models.js +35 -8
  70. package/dist/engine/aux-key.d.ts +10 -0
  71. package/dist/engine/aux-key.js +11 -0
  72. package/dist/engine/dynamic-tool-defs.d.ts +19 -0
  73. package/dist/engine/dynamic-tool-defs.js +36 -0
  74. package/dist/engine/engine.d.ts +280 -141
  75. package/dist/engine/engine.js +1193 -245
  76. package/dist/engine/friendly-error.d.ts +18 -0
  77. package/dist/engine/friendly-error.js +63 -0
  78. package/dist/engine/goal.d.ts +145 -0
  79. package/dist/engine/goal.js +144 -0
  80. package/dist/engine/image-policy.d.ts +13 -0
  81. package/dist/engine/image-policy.js +24 -0
  82. package/dist/engine/model-connections-pool.d.ts +17 -0
  83. package/dist/engine/model-connections-pool.js +67 -0
  84. package/dist/engine/model-facade.d.ts +10 -0
  85. package/dist/engine/model-facade.js +15 -0
  86. package/dist/engine/patch-orphaned-tools.js +3 -0
  87. package/dist/engine/query.js +2 -0
  88. package/dist/engine/resolve-llm-config.d.ts +16 -0
  89. package/dist/engine/resolve-llm-config.js +44 -0
  90. package/dist/engine/runtime.d.ts +2 -0
  91. package/dist/engine/runtime.js +27 -1
  92. package/dist/engine/sandbox-cache-key.d.ts +10 -0
  93. package/dist/engine/sandbox-cache-key.js +9 -0
  94. package/dist/engine/sandbox-config.d.ts +31 -0
  95. package/dist/engine/sandbox-config.js +38 -0
  96. package/dist/engine/session-usage.d.ts +31 -0
  97. package/dist/engine/session-usage.js +81 -0
  98. package/dist/engine/steer-queue.d.ts +33 -0
  99. package/dist/engine/steer-queue.js +26 -0
  100. package/dist/engine/streaming-tool-queue.d.ts +12 -0
  101. package/dist/engine/streaming-tool-queue.js +50 -10
  102. package/dist/engine/turn-loop.d.ts +94 -4
  103. package/dist/engine/turn-loop.js +432 -54
  104. package/dist/engine/types.d.ts +175 -0
  105. package/dist/engine/types.js +13 -0
  106. package/dist/external-agents/config.d.ts +2 -0
  107. package/dist/external-agents/config.js +15 -0
  108. package/dist/external-agents/types.d.ts +31 -0
  109. package/dist/external-agents/types.js +1 -0
  110. package/dist/git/utils.d.ts +12 -0
  111. package/dist/git/utils.js +38 -6
  112. package/dist/git/worktree.d.ts +48 -0
  113. package/dist/git/worktree.js +86 -10
  114. package/dist/hooks/goal-stop-hook.d.ts +28 -0
  115. package/dist/hooks/goal-stop-hook.js +186 -9
  116. package/dist/hooks/registry.d.ts +8 -0
  117. package/dist/hooks/registry.js +19 -0
  118. package/dist/hooks/shell-runner.js +10 -21
  119. package/dist/index.d.ts +51 -7
  120. package/dist/index.js +62 -5
  121. package/dist/llm/capabilities/param-specs.d.ts +14 -0
  122. package/dist/llm/capabilities/param-specs.js +62 -0
  123. package/dist/llm/capabilities/rules.js +5 -1
  124. package/dist/llm/capabilities/types.d.ts +10 -0
  125. package/dist/llm/client-base.d.ts +28 -1
  126. package/dist/llm/client-base.js +119 -13
  127. package/dist/llm/model-cache.js +4 -2
  128. package/dist/llm/model-pool.d.ts +21 -0
  129. package/dist/llm/model-pool.js +21 -1
  130. package/dist/llm/provider-auth.d.ts +41 -0
  131. package/dist/llm/provider-auth.js +76 -0
  132. package/dist/llm/provider-catalog.d.ts +4 -0
  133. package/dist/llm/providers/anthropic.js +64 -14
  134. package/dist/llm/providers/openai.d.ts +36 -0
  135. package/dist/llm/providers/openai.js +257 -51
  136. package/dist/llm/reasoning-setting.d.ts +3 -3
  137. package/dist/llm/reasoning-setting.js +9 -1
  138. package/dist/llm/stream-watchdog.js +5 -1
  139. package/dist/llm/token-counter.js +9 -2
  140. package/dist/llm/types.d.ts +4 -0
  141. package/dist/logging/sanitize-messages.js +22 -0
  142. package/dist/lsp/manager.d.ts +1 -1
  143. package/dist/lsp/manager.js +40 -10
  144. package/dist/model-catalog/builtin.d.ts +12 -0
  145. package/dist/model-catalog/builtin.js +412 -0
  146. package/dist/model-catalog/gen-connections.d.ts +20 -0
  147. package/dist/model-catalog/gen-connections.js +28 -0
  148. package/dist/model-catalog/index.d.ts +41 -0
  149. package/dist/model-catalog/index.js +90 -0
  150. package/dist/model-catalog/params.d.ts +20 -0
  151. package/dist/model-catalog/params.js +45 -0
  152. package/dist/model-catalog/resolve.d.ts +48 -0
  153. package/dist/model-catalog/resolve.js +33 -0
  154. package/dist/model-catalog/save-entry.d.ts +32 -0
  155. package/dist/model-catalog/save-entry.js +104 -0
  156. package/dist/model-catalog/types.d.ts +561 -0
  157. package/dist/model-catalog/types.js +93 -0
  158. package/dist/model-catalog/upsert.d.ts +9 -0
  159. package/dist/model-catalog/upsert.js +8 -0
  160. package/dist/onboarding.d.ts +12 -82
  161. package/dist/onboarding.js +61 -326
  162. package/dist/plugins/gitOps.d.ts +19 -0
  163. package/dist/plugins/gitOps.js +73 -4
  164. package/dist/plugins/installer/checkUpdate.d.ts +16 -0
  165. package/dist/plugins/installer/checkUpdate.js +32 -0
  166. package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
  167. package/dist/plugins/installer/codex/convertCommands.js +46 -0
  168. package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
  169. package/dist/plugins/installer/codex/convertMcp.js +45 -5
  170. package/dist/plugins/installer/install.js +25 -1
  171. package/dist/plugins/installer/installFromArchive.d.ts +43 -0
  172. package/dist/plugins/installer/installFromArchive.js +134 -0
  173. package/dist/plugins/installer/installFromSource.js +8 -2
  174. package/dist/plugins/installer/loadPluginAgents.js +6 -2
  175. package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
  176. package/dist/plugins/installer/loadPluginMcp.js +35 -2
  177. package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
  178. package/dist/plugins/installer/pruneDisabled.js +73 -0
  179. package/dist/plugins/installer/types.d.ts +5 -2
  180. package/dist/plugins/installer/types.js +1 -0
  181. package/dist/plugins/installer/uninstall.js +4 -0
  182. package/dist/plugins/installer/unzip.d.ts +14 -0
  183. package/dist/plugins/installer/unzip.js +82 -0
  184. package/dist/plugins/installer/update.d.ts +14 -0
  185. package/dist/plugins/installer/update.js +53 -21
  186. package/dist/plugins/loadPluginHooks.d.ts +47 -1
  187. package/dist/plugins/loadPluginHooks.js +73 -1
  188. package/dist/plugins/marketplaceManager.d.ts +7 -0
  189. package/dist/plugins/marketplaceManager.js +20 -0
  190. package/dist/plugins/pluginCommandHook.js +4 -18
  191. package/dist/plugins/pluginContent.d.ts +30 -0
  192. package/dist/plugins/pluginContent.js +83 -0
  193. package/dist/plugins/pluginInstaller.d.ts +13 -0
  194. package/dist/plugins/pluginInstaller.js +42 -8
  195. package/dist/plugins/schemas.js +1 -0
  196. package/dist/plugins/types.d.ts +6 -0
  197. package/dist/preset/index.d.ts +11 -1
  198. package/dist/preset/index.js +115 -6
  199. package/dist/product/types.d.ts +1 -1
  200. package/dist/prompt/composer.d.ts +32 -0
  201. package/dist/prompt/composer.js +75 -21
  202. package/dist/prompt/instruction-scanner.js +5 -3
  203. package/dist/prompt/section-loader.js +1 -0
  204. package/dist/prompt/sections/base.md +2 -0
  205. package/dist/prompt/sections/browser.md +10 -0
  206. package/dist/prompt/sections/coding.md +4 -0
  207. package/dist/protocol/chat-session-manager.d.ts +9 -2
  208. package/dist/protocol/chat-session-manager.js +38 -0
  209. package/dist/protocol/chat-session.d.ts +69 -1
  210. package/dist/protocol/chat-session.js +102 -4
  211. package/dist/protocol/client.d.ts +31 -0
  212. package/dist/protocol/client.js +44 -0
  213. package/dist/protocol/server.d.ts +120 -7
  214. package/dist/protocol/server.js +753 -51
  215. package/dist/protocol/transport.js +3 -2
  216. package/dist/protocol/types.d.ts +60 -0
  217. package/dist/protocol/types.js +16 -0
  218. package/dist/quota/credentials.d.ts +3 -0
  219. package/dist/quota/credentials.js +80 -0
  220. package/dist/quota/index.d.ts +36 -0
  221. package/dist/quota/index.js +155 -0
  222. package/dist/quota/types.d.ts +48 -0
  223. package/dist/quota/types.js +13 -0
  224. package/dist/review/review-prompt.d.ts +28 -0
  225. package/dist/review/review-prompt.js +81 -0
  226. package/dist/run/FileRunStore.js +8 -3
  227. package/dist/run/RunApprovalBackend.js +25 -5
  228. package/dist/run/RunManager.d.ts +12 -0
  229. package/dist/run/RunManager.js +35 -0
  230. package/dist/run/factory.d.ts +1 -1
  231. package/dist/runtime/background-shell.d.ts +139 -0
  232. package/dist/runtime/background-shell.js +509 -0
  233. package/dist/runtime/output-clean.d.ts +24 -0
  234. package/dist/runtime/output-clean.js +41 -0
  235. package/dist/runtime/ring-file.d.ts +64 -0
  236. package/dist/runtime/ring-file.js +174 -0
  237. package/dist/runtime/safe-spawn.js +98 -39
  238. package/dist/runtime/spawn-common.d.ts +159 -0
  239. package/dist/runtime/spawn-common.js +404 -0
  240. package/dist/runtime/truncate-output.d.ts +22 -0
  241. package/dist/runtime/truncate-output.js +49 -0
  242. package/dist/runtime/utf8-cut.d.ts +11 -0
  243. package/dist/runtime/utf8-cut.js +23 -0
  244. package/dist/services/auto-dream.d.ts +4 -0
  245. package/dist/services/auto-dream.js +26 -26
  246. package/dist/services/diagnostics.d.ts +1 -2
  247. package/dist/services/diagnostics.js +12 -7
  248. package/dist/services/extract-memories.d.ts +14 -1
  249. package/dist/services/extract-memories.js +45 -6
  250. package/dist/services/memory-orchestrator.d.ts +21 -0
  251. package/dist/services/memory-orchestrator.js +125 -50
  252. package/dist/services/session-memory.js +24 -12
  253. package/dist/session/file-history.d.ts +124 -1
  254. package/dist/session/file-history.js +222 -6
  255. package/dist/session/memory.d.ts +116 -2
  256. package/dist/session/memory.js +250 -28
  257. package/dist/session/session-manager.d.ts +40 -0
  258. package/dist/session/session-manager.js +119 -3
  259. package/dist/session/simple-diff.d.ts +23 -0
  260. package/dist/session/simple-diff.js +84 -0
  261. package/dist/session/transcript.d.ts +29 -1
  262. package/dist/session/transcript.js +56 -2
  263. package/dist/session/undo-target.d.ts +67 -0
  264. package/dist/session/undo-target.js +144 -0
  265. package/dist/settings/disk-defaults.d.ts +9 -2
  266. package/dist/settings/disk-defaults.js +11 -2
  267. package/dist/settings/feature-flags.d.ts +64 -0
  268. package/dist/settings/feature-flags.js +61 -0
  269. package/dist/settings/manager.d.ts +68 -1
  270. package/dist/settings/manager.js +266 -29
  271. package/dist/settings/migrate-config.d.ts +45 -0
  272. package/dist/settings/migrate-config.js +125 -0
  273. package/dist/settings/schema-export.d.ts +25 -0
  274. package/dist/settings/schema-export.js +38 -0
  275. package/dist/settings/schema.d.ts +1186 -771
  276. package/dist/settings/schema.js +268 -97
  277. package/dist/skills/scanner.d.ts +9 -0
  278. package/dist/skills/scanner.js +30 -2
  279. package/dist/stt/resolve-transcribe.d.ts +31 -0
  280. package/dist/stt/resolve-transcribe.js +108 -0
  281. package/dist/stt/transcribe.d.ts +51 -0
  282. package/dist/stt/transcribe.js +65 -0
  283. package/dist/tool-system/browser-bridge.d.ts +226 -0
  284. package/dist/tool-system/browser-bridge.js +163 -0
  285. package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
  286. package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
  287. package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
  288. package/dist/tool-system/builtin/agent-notifications.js +9 -3
  289. package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
  290. package/dist/tool-system/builtin/agent-output-file.js +72 -0
  291. package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
  292. package/dist/tool-system/builtin/agent-registry.js +8 -0
  293. package/dist/tool-system/builtin/agent.d.ts +21 -1
  294. package/dist/tool-system/builtin/agent.js +489 -42
  295. package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
  296. package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
  297. package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
  298. package/dist/tool-system/builtin/apply-patch/index.js +0 -15
  299. package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
  300. package/dist/tool-system/builtin/background-jobs.js +124 -0
  301. package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
  302. package/dist/tool-system/builtin/background-shell-tools.js +108 -0
  303. package/dist/tool-system/builtin/background-work.d.ts +67 -0
  304. package/dist/tool-system/builtin/background-work.js +86 -0
  305. package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
  306. package/dist/tool-system/builtin/bash-output-style.js +40 -0
  307. package/dist/tool-system/builtin/bash.d.ts +5 -2
  308. package/dist/tool-system/builtin/bash.js +101 -64
  309. package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
  310. package/dist/tool-system/builtin/browser-tools.js +312 -0
  311. package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
  312. package/dist/tool-system/builtin/cancel-goal.js +64 -0
  313. package/dist/tool-system/builtin/check-quota.d.ts +15 -0
  314. package/dist/tool-system/builtin/check-quota.js +34 -0
  315. package/dist/tool-system/builtin/config.js +7 -0
  316. package/dist/tool-system/builtin/cron.d.ts +7 -0
  317. package/dist/tool-system/builtin/cron.js +64 -4
  318. package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
  319. package/dist/tool-system/builtin/drive-claude-code.js +157 -0
  320. package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
  321. package/dist/tool-system/builtin/edit-model-catalog.js +104 -0
  322. package/dist/tool-system/builtin/edit.js +20 -16
  323. package/dist/tool-system/builtin/eol.d.ts +29 -0
  324. package/dist/tool-system/builtin/eol.js +37 -0
  325. package/dist/tool-system/builtin/file-cache.d.ts +6 -0
  326. package/dist/tool-system/builtin/file-cache.js +8 -0
  327. package/dist/tool-system/builtin/generate-image.d.ts +35 -0
  328. package/dist/tool-system/builtin/generate-image.js +278 -50
  329. package/dist/tool-system/builtin/generate-video.d.ts +55 -0
  330. package/dist/tool-system/builtin/generate-video.js +364 -0
  331. package/dist/tool-system/builtin/glob.js +0 -7
  332. package/dist/tool-system/builtin/grep.d.ts +9 -0
  333. package/dist/tool-system/builtin/grep.js +106 -11
  334. package/dist/tool-system/builtin/image-providers.d.ts +86 -0
  335. package/dist/tool-system/builtin/image-providers.js +190 -0
  336. package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
  337. package/dist/tool-system/builtin/image-uploader.js +74 -0
  338. package/dist/tool-system/builtin/index.d.ts +10 -2
  339. package/dist/tool-system/builtin/index.js +251 -27
  340. package/dist/tool-system/builtin/mcp-tools.js +23 -3
  341. package/dist/tool-system/builtin/memory.js +45 -7
  342. package/dist/tool-system/builtin/notebook-edit.js +0 -7
  343. package/dist/tool-system/builtin/powershell.js +8 -2
  344. package/dist/tool-system/builtin/read.js +10 -10
  345. package/dist/tool-system/builtin/repl.js +4 -1
  346. package/dist/tool-system/builtin/skill.js +9 -0
  347. package/dist/tool-system/builtin/sleep.js +8 -2
  348. package/dist/tool-system/builtin/tool-search.js +25 -7
  349. package/dist/tool-system/builtin/video-providers.d.ts +154 -0
  350. package/dist/tool-system/builtin/video-providers.js +235 -0
  351. package/dist/tool-system/builtin/web-fetch.js +12 -2
  352. package/dist/tool-system/builtin/web-search.js +21 -7
  353. package/dist/tool-system/builtin/worktree.d.ts +2 -1
  354. package/dist/tool-system/builtin/worktree.js +31 -4
  355. package/dist/tool-system/builtin/write.js +0 -6
  356. package/dist/tool-system/context.d.ts +164 -6
  357. package/dist/tool-system/executor.d.ts +3 -1
  358. package/dist/tool-system/executor.js +197 -90
  359. package/dist/tool-system/investigation-guard.js +1 -1
  360. package/dist/tool-system/mcp-manager.d.ts +42 -4
  361. package/dist/tool-system/mcp-manager.js +203 -20
  362. package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
  363. package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
  364. package/dist/tool-system/path-policy.d.ts +5 -0
  365. package/dist/tool-system/path-policy.js +307 -8
  366. package/dist/tool-system/permission.d.ts +39 -2
  367. package/dist/tool-system/permission.js +283 -70
  368. package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
  369. package/dist/tool-system/plan-mode-allowlist.js +24 -2
  370. package/dist/tool-system/registry.d.ts +1 -0
  371. package/dist/tool-system/registry.js +16 -2
  372. package/dist/tool-system/sandbox/index.d.ts +8 -0
  373. package/dist/tool-system/sandbox/index.js +7 -2
  374. package/dist/tool-system/sandbox/off.js +7 -1
  375. package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
  376. package/dist/tool-system/validate-tool-metadata.js +63 -0
  377. package/dist/types.d.ts +252 -7
  378. package/dist/updater.js +20 -9
  379. package/dist/utils/envUtils.d.ts +0 -9
  380. package/dist/utils/envUtils.js +3 -28
  381. package/dist/utils/exec.d.ts +48 -0
  382. package/dist/utils/exec.js +154 -0
  383. package/dist/utils/json.d.ts +12 -0
  384. package/dist/utils/json.js +92 -0
  385. package/dist/utils/theme.d.ts +1 -1
  386. package/dist/utils/theme.js +1 -1
  387. package/dist/utils/toolDisplay.js +0 -1
  388. package/package.json +13 -7
  389. package/dist/agent/coordinator.d.ts +0 -49
  390. package/dist/agent/coordinator.js +0 -77
  391. package/dist/settings/manager.test.js +0 -73
  392. package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
  393. package/dist/tool-system/builtin/remote-trigger.js +0 -54
  394. package/dist/tool-system/builtin/send-message.d.ts +0 -6
  395. package/dist/tool-system/builtin/send-message.js +0 -47
  396. /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
@@ -5,17 +5,20 @@
5
5
  * pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
6
6
  */
7
7
  import { initialTurnState, newTurnId } from "./turn-state.js";
8
+ import { formatFriendlyError } from "./friendly-error.js";
8
9
  import { wrapHookMessages } from "../hooks/inject.js";
9
10
  import { ContextLimitError } from "../exceptions.js";
10
11
  import { logger } from "../logging/logger.js";
11
12
  import { checkTokenBudget, createBudgetTracker } from "./token-budget.js";
12
13
  import { StreamingToolQueue } from "./streaming-tool-queue.js";
13
- import { estimateTokens } from "../context/compaction.js";
14
+ import { downgradeImagePayloadsInHistory, estimateTokens, messageHasBase64ImagePayload, } from "../context/compaction.js";
14
15
  import { isTruncatedStop } from "../llm/stop-reason.js";
15
16
  import { isAbortError } from "../llm/client-base.js";
16
17
  import { crossedReactiveThreshold } from "./reactive-threshold.js";
17
18
  import { COMPLETE_GOAL_TOOL_NAME } from "../tool-system/builtin/complete-goal.js";
18
- import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, } from "./goal.js";
19
+ import { CANCEL_GOAL_TOOL_NAME } from "../tool-system/builtin/cancel-goal.js";
20
+ import { addTokenUsage, cacheHitRateFromUsage, cumulativeCacheHitRate, } from "./session-usage.js";
21
+ import { createGoalBudgetTracker, recordGoalUsage, goalBudgetExceeded, applyGoalExtension, limitProximity, GOAL_DEFAULT_MAX_STOP_BLOCKS, } from "./goal.js";
19
22
  /**
20
23
  * 把一个 ToolResult 映射成发给 LLM 的 tool_result ContentBlock。
21
24
  * 有 contentBlocks(view_image 的图片块)就原样用作 content;否则
@@ -27,7 +30,7 @@ export function toolResultToBlock(result) {
27
30
  tool_use_id: result.id,
28
31
  content: result.error
29
32
  ? `Error: ${result.error}`
30
- : result.contentBlocks ?? (result.result ?? "(no output)"),
33
+ : (result.contentBlocks ?? result.result ?? "(no output)"),
31
34
  };
32
35
  if (result.isError || result.error)
33
36
  block.is_error = true;
@@ -48,15 +51,113 @@ export class TurnLoop {
48
51
  currentTurnLog = logger;
49
52
  /** Last emitted ctx token estimate; used to skip no-op usage_update events. */
50
53
  lastCtxEmit = -1;
54
+ currentTurnUsage = {
55
+ promptTokens: 0,
56
+ completionTokens: 0,
57
+ totalTokens: 0,
58
+ cacheReadTokens: 0,
59
+ cacheCreationTokens: 0,
60
+ };
61
+ currentCumulativeUsage;
62
+ pendingImageMessages = new Set();
51
63
  /**
52
64
  * Consecutive on_stop blocks (Goal mode kept the agent going). Reset to 0
53
65
  * on any unblocked completion. When it reaches config.maxStopBlocks the
54
66
  * loop forces a stop so a stuck goal can't loop forever.
55
67
  */
56
68
  stopBlockCount = 0;
57
- constructor(deps, config) {
69
+ /**
70
+ * Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
71
+ * (not a run() local) so extend() can bump its budgets mid-run. Null when no
72
+ * goal or between runs.
73
+ */
74
+ goalTracker = null;
75
+ /**
76
+ * Whether an approaching_limit marker was already emitted for the current
77
+ * ceiling. Reset when the run advances past the moment (a fresh extension, or
78
+ * the goal completing) so the next approach re-announces. Prevents the marker
79
+ * being re-emitted every turn while still within the approach threshold.
80
+ */
81
+ approachAnnounced = false;
82
+ /**
83
+ * Extend the in-flight run's limits (TODO 3.1 — 运行中续轮/加预算). Mutates
84
+ * the maxTurns ceiling, the maxStopBlocks cap, and/or the live goal budgets;
85
+ * the loop re-reads all of them each turn so the change takes effect on the
86
+ * next iteration. No-op for fields not supplied. Returns the resulting
87
+ * effective limits.
88
+ */
89
+ extend(opts) {
90
+ const elapsedMs = this.goalTracker ? Date.now() - this.goalTracker.startedAtMs : 0;
91
+ const next = applyGoalExtension(this.config.maxTurns, this.goalTracker?.goal, this.goalTracker?.tokensUsed ?? 0, elapsedMs, opts);
92
+ this.config = { ...this.config, maxTurns: next.maxTurns };
93
+ if (this.goalTracker) {
94
+ // Replace the goal object rather than mutating its fields: the tracker's
95
+ // goal may be a shared/frozen reference, and goalBudgetExceeded reads
96
+ // tracker.goal.{tokenBudget,timeBudgetMs} live, so a fresh object with the
97
+ // new caps takes effect on the next turn either way.
98
+ this.goalTracker.goal = {
99
+ ...this.goalTracker.goal,
100
+ tokenBudget: next.tokenBudget,
101
+ timeBudgetMs: next.timeBudgetMs,
102
+ };
103
+ }
104
+ // Raise the consecutive-stop-block cap — for a re-blocked goal this is the
105
+ // limit that actually bites, so an extend that only bumped maxTurns/budgets
106
+ // couldn't keep it going. Resolve the current cap the same way the loop does.
107
+ const curCap = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
108
+ const nextCap = typeof opts.addStopBlocks === "number" && opts.addStopBlocks > 0
109
+ ? curCap + Math.floor(opts.addStopBlocks)
110
+ : curCap;
111
+ this.config = { ...this.config, maxStopBlocks: nextCap };
112
+ // ANY extension resets the consecutive stop-block streak: the user just
113
+ // asked to keep going, so a goal that was repeatedly re-blocked shouldn't be
114
+ // immediately re-capped. (Previously only addTurns reset it, leaving a
115
+ // budget-only extension unable to un-stick a capped goal.)
116
+ const extended = (opts.addTurns ?? 0) > 0 ||
117
+ (opts.addStopBlocks ?? 0) > 0 ||
118
+ (opts.addTokenBudget ?? 0) > 0 ||
119
+ (opts.addTimeBudgetMs ?? 0) > 0;
120
+ if (extended) {
121
+ this.stopBlockCount = 0;
122
+ // Let the next approach re-announce against the raised ceilings.
123
+ this.approachAnnounced = false;
124
+ }
125
+ return { ...next, maxStopBlocks: nextCap };
126
+ }
127
+ /**
128
+ * Goal mode only: if the run is nearing EITHER stop ceiling (maxTurns or
129
+ * maxStopBlocks) and we haven't announced it yet, emit one approaching_limit
130
+ * marker so the UI can offer a "再续" button while the run is still live.
131
+ * Watches both limits because a re-blocked goal hits the stop-block cap long
132
+ * before maxTurns. Idempotent within an approach window via approachAnnounced.
133
+ */
134
+ maybeAnnounceApproachingLimit() {
135
+ if (!this.config.goal || this.approachAnnounced)
136
+ return;
137
+ const cap = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
138
+ const prox = limitProximity(this.turnCount, this.config.maxTurns, this.stopBlockCount, cap);
139
+ if (!prox.approaching)
140
+ return;
141
+ this.approachAnnounced = true;
142
+ this.config.onStream?.({
143
+ type: "goal_progress",
144
+ status: "approaching_limit",
145
+ round: this.stopBlockCount,
146
+ turnsRemaining: prox.turnsRemaining,
147
+ stopBlocksRemaining: prox.stopBlocksRemaining,
148
+ nearest: prox.nearest,
149
+ });
150
+ }
151
+ constructor(deps,
152
+ // Not readonly: extend() bumps maxTurns mid-run (TODO 3.1), and the
153
+ // constructor below rewrites onStream. The loop reads config fields fresh
154
+ // each turn so mutations take effect on the next iteration.
155
+ config) {
58
156
  this.deps = deps;
59
157
  this.config = config;
158
+ for (const msg of this.config.freshImageMessages ?? []) {
159
+ this.pendingImageMessages.add(msg);
160
+ }
60
161
  // Wrap onStream so a single throwing handler can't silently break
61
162
  // the channel for the rest of the run. A 2026-05-25 incident saw a
62
163
  // sub-agent's events stop reaching the renderer ~23s into its run —
@@ -95,11 +196,61 @@ export class TurnLoop {
95
196
  * `decision` / `stop`. Use this instead of `deps.hooks.emit` directly so
96
197
  * every emit carries the same context envelope.
97
198
  */
199
+ /**
200
+ * Persist a turn_stopped marker on a user Stop so a resume can rebuild the
201
+ * renderer's "你在 Ns 后停止了" line (the in-memory turn_end is lost on reload,
202
+ * which makes the interrupted turn fold behind the process-card header).
203
+ * Skipped for sub-agents — their signal.aborted is usually a parent abort,
204
+ * not a user Stop, and sub-agent turns aren't shown as interruptible. The
205
+ * underlying append is idempotent, so calling this at several abort returns
206
+ * for one Stop still writes a single marker.
207
+ */
208
+ markStopped() {
209
+ if (this.deps.isSubAgent === true)
210
+ return;
211
+ this.deps.transcript.appendTurnStopped();
212
+ }
213
+ prepareMessagesForModel(messages) {
214
+ const preserveMessages = this.pendingImageMessages.size > 0 ? this.pendingImageMessages : undefined;
215
+ const result = downgradeImagePayloadsInHistory(messages, { preserveMessages });
216
+ if (result.replacedCount > 0) {
217
+ this.currentTurnLog.info("context.image_payload_downgrade", {
218
+ cat: "context",
219
+ images: result.replacedCount,
220
+ pendingFresh: this.pendingImageMessages.size,
221
+ });
222
+ }
223
+ return result.messages;
224
+ }
225
+ markPendingImagesConsumed(messages) {
226
+ if (this.pendingImageMessages.size === 0)
227
+ return messages;
228
+ const consumedMessages = this.pendingImageMessages.size;
229
+ this.pendingImageMessages.clear();
230
+ const result = downgradeImagePayloadsInHistory(messages);
231
+ if (result.replacedCount > 0) {
232
+ this.currentTurnLog.info("context.image_payload_consumed", {
233
+ cat: "context",
234
+ images: result.replacedCount,
235
+ messages: consumedMessages,
236
+ });
237
+ }
238
+ return result.messages;
239
+ }
240
+ trackFreshImageMessage(message) {
241
+ if (messageHasBase64ImagePayload(message)) {
242
+ this.pendingImageMessages.add(message);
243
+ }
244
+ }
98
245
  async emitHook(event, data = {}) {
99
246
  return this.deps.hooks.emit(event, {
100
247
  ...data,
101
248
  isSubAgent: this.deps.isSubAgent === true,
102
249
  sessionId: this.deps.sessionId,
250
+ // The run's abort signal, so handlers that make their own LLM calls
251
+ // (e.g. the Goal stop-judge) can be cut short when the user hits Stop
252
+ // mid-call instead of blocking until the sub-call returns.
253
+ signal: this.config.signal,
103
254
  });
104
255
  }
105
256
  /**
@@ -132,9 +283,14 @@ export class TurnLoop {
132
283
  this.lastCtxEmit = ctx;
133
284
  this.config.onStream({ type: "usage_update", promptTokens: ctx });
134
285
  }
135
- emitCtxFromUsage(promptTokens, messages) {
286
+ recordResponseUsage(usage) {
287
+ this.currentTurnUsage = addTokenUsage(this.currentTurnUsage, usage);
288
+ this.currentCumulativeUsage = this.deps.recordCumulativeUsage?.(usage);
289
+ }
290
+ emitCtxFromUsage(usage, messages) {
136
291
  if (!this.config.onStream)
137
292
  return;
293
+ const promptTokens = usage.promptTokens;
138
294
  // Reverse-derive overhead from this authoritative reading so the next
139
295
  // estimate-based emit (post-tool-result) is calibrated.
140
296
  const msgsEstimate = estimateTokens(messages);
@@ -147,10 +303,47 @@ export class TurnLoop {
147
303
  derivedOverhead: overhead,
148
304
  prev: this.lastCtxEmit,
149
305
  });
150
- if (promptTokens === this.lastCtxEmit)
306
+ const promptChanged = promptTokens !== this.lastCtxEmit;
307
+ if (promptChanged)
308
+ this.lastCtxEmit = promptTokens;
309
+ // Forward the provider's cache counts so the UI can show a hit rate. Only
310
+ // attach fields the provider actually reported — a spread keeps them off
311
+ // the event entirely when undefined, so the renderer can tell "no cache
312
+ // info this turn" from "0 cached". Estimate-path emits don't call this and
313
+ // so carry no cache fields (correct: an estimate has no cache reading).
314
+ const singleTurnCacheHitRate = cacheHitRateFromUsage(this.currentTurnUsage);
315
+ const cumulative = this.currentCumulativeUsage;
316
+ const cumulativeHitRate = cumulative ? cumulativeCacheHitRate(cumulative) : undefined;
317
+ if (!promptChanged && singleTurnCacheHitRate === undefined && cumulativeHitRate === undefined) {
151
318
  return;
152
- this.lastCtxEmit = promptTokens;
153
- this.config.onStream({ type: "usage_update", promptTokens });
319
+ }
320
+ this.config.onStream({
321
+ type: "usage_update",
322
+ promptTokens,
323
+ ...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
324
+ ...(usage.cacheCreationTokens !== undefined
325
+ ? { cacheCreationTokens: usage.cacheCreationTokens }
326
+ : {}),
327
+ singleTurnPromptTokens: this.currentTurnUsage.promptTokens,
328
+ singleTurnCacheReadTokens: this.currentTurnUsage.cacheReadTokens ?? 0,
329
+ singleTurnCacheCreationTokens: this.currentTurnUsage.cacheCreationTokens ?? 0,
330
+ ...(singleTurnCacheHitRate !== undefined ? { singleTurnCacheHitRate } : {}),
331
+ ...(cumulative
332
+ ? {
333
+ cumulativePromptTokens: cumulative.cumulativePromptTokens,
334
+ cumulativeCacheReadTokens: cumulative.cumulativeCacheReadTokens,
335
+ cumulativeCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
336
+ ...(cumulativeHitRate !== undefined
337
+ ? { cumulativeCacheHitRate: cumulativeHitRate }
338
+ : {}),
339
+ // Legacy aliases for existing renderer builds. New UI code reads the
340
+ // cumulative* fields above.
341
+ sessionPromptTokens: cumulative.cumulativePromptTokens,
342
+ sessionCacheReadTokens: cumulative.cumulativeCacheReadTokens,
343
+ sessionCacheCreationTokens: cumulative.cumulativeCacheCreationTokens,
344
+ }
345
+ : {}),
346
+ });
154
347
  }
155
348
  /**
156
349
  * Run the multi-turn agent loop until completion.
@@ -163,9 +356,12 @@ export class TurnLoop {
163
356
  // wall-clock start now and accumulates prompt+completion tokens across
164
357
  // every turn; the guardrail below force-stops the run once any configured
165
358
  // budget is blown — the unattended-safety backstop.
166
- const goalTracker = this.config.goal
359
+ this.goalTracker = this.config.goal
167
360
  ? createGoalBudgetTracker(this.config.goal, Date.now())
168
361
  : null;
362
+ const goalTracker = this.goalTracker;
363
+ // Fresh run: re-arm the approaching-limit announcement.
364
+ this.approachAnnounced = false;
169
365
  // run() must never reject: the engine's post-run bookkeeping (saveState
170
366
  // with the terminal reason, on_session_end hook) runs AFTER this call and
171
367
  // outside the engine's try, so a throw here would leave the session
@@ -176,6 +372,13 @@ export class TurnLoop {
176
372
  try {
177
373
  while (this.turnCount < this.config.maxTurns) {
178
374
  this.turnCount++;
375
+ this.currentTurnUsage = {
376
+ promptTokens: 0,
377
+ completionTokens: 0,
378
+ totalTokens: 0,
379
+ cacheReadTokens: 0,
380
+ cacheCreationTokens: 0,
381
+ };
179
382
  // Abort fast-path: bail at the loop TOP before doing any per-turn work.
180
383
  // Without this, an aborted child (parent abort, or the 30min per-call
181
384
  // registry timeout) would run a full contextManager.manageAsync (itself
@@ -187,8 +390,15 @@ export class TurnLoop {
187
390
  // boundary between turns. (Mirrors Claude Code's query.ts, where the
188
391
  // aborted check short-circuits before re-entering the streaming loop.)
189
392
  if (this.config.signal?.aborted) {
393
+ this.markStopped();
190
394
  return { text: finalText, reason: "aborted_streaming", messages };
191
395
  }
396
+ // Step-gap steering: messages the host queued via Engine.enqueueSteer
397
+ // while the previous step was running. Splice them in as user messages so
398
+ // they join THIS step's request — no abort, no lost in-flight work. Same
399
+ // loop-top user-push pattern as turnStartInjection / turn-limit warnings
400
+ // below. Push to transcript too so they persist + survive resume.
401
+ this.consumeQueuedSteer(messages, "normal_step");
192
402
  const state = initialTurnState(this.turnCount);
193
403
  // Per-turn correlation ID. Every log written through `tlog` (or any
194
404
  // child derived from it) is stamped with `turn` + `turnId`, so
@@ -211,7 +421,10 @@ export class TurnLoop {
211
421
  if (turnStartInjection) {
212
422
  messages.push(turnStartInjection);
213
423
  }
214
- // Approaching max turns: inject a warning so the model can wrap up
424
+ // Approaching max turns: inject a warning so the model can wrap up.
425
+ // (Model-facing only — the user-facing "再续" marker is handled by
426
+ // maybeAnnounceApproachingLimit below, which also watches the stop-block
427
+ // cap, the limit a re-blocked goal actually hits first.)
215
428
  const turnsRemaining = this.config.maxTurns - this.turnCount;
216
429
  if (turnsRemaining === 2) {
217
430
  messages.push({
@@ -220,6 +433,13 @@ export class TurnLoop {
220
433
  "Start wrapping up your work and prepare a summary of what you've accomplished and what remains to be done.</system-reminder>",
221
434
  });
222
435
  }
436
+ else if (turnsRemaining === 1) {
437
+ messages.push({
438
+ role: "user",
439
+ content: "<system-reminder>Warning: you have only 1 turn remaining before the turn limit is reached. " +
440
+ "Wrap up your work now — your next turn will be your last.</system-reminder>",
441
+ });
442
+ }
223
443
  else if (turnsRemaining === 0) {
224
444
  messages.push({
225
445
  role: "user",
@@ -227,13 +447,21 @@ export class TurnLoop {
227
447
  "Do NOT call any tools. Summarize what you have accomplished and list any remaining work.</system-reminder>",
228
448
  });
229
449
  }
230
- // Pre-check: context management (async may trigger LLM summarization)
450
+ // Goal mode: announce once when nearing EITHER stop ceiling (turns or
451
+ // stop-blocks) so the UI can offer a "再续" button while still live.
452
+ this.maybeAnnounceApproachingLimit();
453
+ // Pre-check: downgrade image payloads that have already had their one
454
+ // model-consumption turn, then run context management. Fresh images in
455
+ // pendingImageMessages are preserved through this next model request.
456
+ messages = this.prepareMessagesForModel(messages);
457
+ // Context management (async — may trigger LLM summarization)
231
458
  messages = await this.deps.contextManager.manageAsync(messages);
232
459
  // manageAsync can itself issue an LLM summarization call lasting several
233
460
  // seconds; if the signal aborted during it, stop here rather than
234
461
  // proceeding into the (expensive) main model call. Belt to the loop-top
235
462
  // brace: this catches an abort that landed *inside* context management.
236
463
  if (this.config.signal?.aborted) {
464
+ this.markStopped();
237
465
  return { text: finalText, reason: "aborted_streaming", messages };
238
466
  }
239
467
  // No pre-llm ctx emit here: the messages-only estimate would be ~16k
@@ -284,7 +512,7 @@ export class TurnLoop {
284
512
  }
285
513
  catch (retryErr) {
286
514
  if (!(retryErr instanceof ContextLimitError)) {
287
- this.config.onStream?.({ type: "error", error: retryErr.message });
515
+ this.config.onStream?.({ type: "error", error: formatFriendlyError(retryErr) });
288
516
  return { text: finalText, reason: "model_error", messages };
289
517
  }
290
518
  }
@@ -298,15 +526,28 @@ export class TurnLoop {
298
526
  return { text: finalText, reason: "prompt_too_long", messages };
299
527
  }
300
528
  }
529
+ else if (isAbortError(err) || this.config.signal?.aborted) {
530
+ // User pressed Stop: the in-flight model call rejected with an
531
+ // AbortError. This is NOT a failure — the UI already shows the
532
+ // "你在 Ns 后停止了" line, so emitting an error event would stack a
533
+ // spurious red "Error:" block on top of an intentional stop.
534
+ // Persist a turn_stopped marker so a resume can rebuild that line
535
+ // (the renderer's turn_end is in-memory only; without this the
536
+ // interrupted turn folds behind the process-card header on reload).
537
+ this.patchOrphanedToolUses(messages);
538
+ this.markStopped();
539
+ return { text: finalText, reason: "aborted_streaming", messages };
540
+ }
301
541
  else {
302
542
  this.patchOrphanedToolUses(messages);
303
- this.config.onStream?.({ type: "error", error: err.message });
543
+ this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
304
544
  return { text: finalText, reason: "model_error", messages };
305
545
  }
306
546
  }
307
- // UI ctx bar: prefer the provider's authoritative promptTokens.
547
+ // Record the response once into current-turn and whole-session counters.
308
548
  if (response.usage?.promptTokens !== undefined) {
309
- this.emitCtxFromUsage(response.usage.promptTokens, messages);
549
+ this.recordResponseUsage(response.usage);
550
+ this.emitCtxFromUsage(response.usage, messages);
310
551
  }
311
552
  // Feed actual token usage back to the context manager so subsequent
312
553
  // compaction decisions use hybrid (actual + delta) estimation rather than
@@ -314,6 +555,7 @@ export class TurnLoop {
314
555
  if (response.usage?.promptTokens !== undefined) {
315
556
  this.deps.contextManager.recordActualUsage(response.usage.promptTokens, messages.length);
316
557
  }
558
+ messages = this.markPendingImagesConsumed(messages);
317
559
  // Truncation that cut off a TOOL CALL: the model overflowed
318
560
  // max_output_tokens mid tool-call, so the arg JSON is incomplete (e.g. a
319
561
  // Write whose `content` was clipped, leaving file_path unset). Executing
@@ -342,6 +584,7 @@ export class TurnLoop {
342
584
  response.toolCalls.length === 0 &&
343
585
  response.text) {
344
586
  let combinedText = response.text;
587
+ let continuedResponse = false;
345
588
  for (let retry = 0; retry < 3; retry++) {
346
589
  // Don't fire another continuation call if the user cancelled — without
347
590
  // this an abort during a truncated response could still issue up to 3
@@ -358,7 +601,11 @@ export class TurnLoop {
358
601
  },
359
602
  ];
360
603
  try {
361
- const contResponse = await this.deps.model.call(this.deps.systemPrompt, contMessages, this.deps.tools, this.config.onStream, this.config.signal);
604
+ const contResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(contMessages), this.deps.tools, this.config.onStream, this.config.signal);
605
+ if (contResponse.usage?.promptTokens !== undefined) {
606
+ this.recordResponseUsage(contResponse.usage);
607
+ continuedResponse = true;
608
+ }
362
609
  combinedText += contResponse.text;
363
610
  if (!isTruncatedStop(contResponse.stopReason) || contResponse.toolCalls.length > 0) {
364
611
  response = { ...contResponse, text: combinedText };
@@ -370,10 +617,9 @@ export class TurnLoop {
370
617
  }
371
618
  }
372
619
  response = { ...response, text: combinedText };
373
- }
374
- // After any continuation, send latest usage so ctx bar reflects real context
375
- if (response.usage?.promptTokens !== undefined) {
376
- this.emitCtxFromUsage(response.usage.promptTokens, messages);
620
+ if (continuedResponse && response.usage?.promptTokens !== undefined) {
621
+ this.emitCtxFromUsage(response.usage, messages);
622
+ }
377
623
  }
378
624
  // Goal-mode run-scoped accounting: add this turn's total token usage
379
625
  // (prompt + completion) to the running total. Done after continuation so
@@ -384,6 +630,7 @@ export class TurnLoop {
384
630
  }
385
631
  // Aborted?
386
632
  if (this.config.signal?.aborted) {
633
+ this.markStopped();
387
634
  return { text: finalText, reason: "aborted_streaming", messages };
388
635
  }
389
636
  // Accumulate text
@@ -422,12 +669,15 @@ export class TurnLoop {
422
669
  hasToolUse: false,
423
670
  });
424
671
  messages.push({ role: "assistant", content: finalText });
672
+ if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
673
+ continue;
674
+ }
425
675
  // on_stop seam: the model wants to stop. Give handlers (Goal mode)
426
676
  // a chance to BLOCK termination and keep the agent working. A
427
677
  // handler returning continueSession=true injects its messages and
428
678
  // we run another turn instead of returning. Bounded by
429
679
  // maxStopBlocks (consecutive) and the outer maxTurns ceiling.
430
- const maxStopBlocks = this.config.maxStopBlocks ?? 8;
680
+ const maxStopBlocks = this.config.maxStopBlocks ?? GOAL_DEFAULT_MAX_STOP_BLOCKS;
431
681
  const stopHook = await this.emitHook("on_stop", {
432
682
  goal: this.config.goal,
433
683
  finalText,
@@ -447,6 +697,10 @@ export class TurnLoop {
447
697
  round: this.stopBlockCount,
448
698
  gaps: goalVerdict?.gaps || undefined,
449
699
  });
700
+ // The streak just grew — we may now be nearing the stop-block cap.
701
+ // Announce here (before the next turn's top check) so the "再续"
702
+ // button shows up against the limit that's actually about to bite.
703
+ this.maybeAnnounceApproachingLimit();
450
704
  const injection = wrapHookMessages(stopHook.messages);
451
705
  if (injection) {
452
706
  messages.push(injection);
@@ -499,11 +753,23 @@ export class TurnLoop {
499
753
  });
500
754
  }
501
755
  this.stopBlockCount = 0;
756
+ if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
757
+ continue;
758
+ }
502
759
  return { text: finalText, reason: "completed", messages };
503
760
  }
504
761
  // Tool execution phase
505
- tlog.info("turn.tool_use", { cat: "turn", tools: response.toolCalls.map((t) => t.toolName) });
762
+ tlog.info("turn.tool_use", {
763
+ cat: "turn",
764
+ tools: response.toolCalls.map((t) => t.toolName),
765
+ });
506
766
  const toolCalls = response.toolCalls.slice(0, this.config.maxToolCallsPerTurn);
767
+ // Per-turn cap: any calls beyond maxToolCallsPerTurn are NOT executed and
768
+ // NOT added to the assistant message below, so the model never sees a
769
+ // result for them. Without a heads-up it assumes all ran (acting on
770
+ // tool output that never happened). Remember the dropped ones so we can
771
+ // inject a reminder after the executed batch's results. (B-3)
772
+ const droppedToolCalls = response.toolCalls.slice(this.config.maxToolCallsPerTurn);
507
773
  // Add assistant message with tool_use blocks to messages
508
774
  const assistantBlocks = [];
509
775
  if (response.text) {
@@ -537,19 +803,53 @@ export class TurnLoop {
537
803
  this.deps.transcript.appendToolResult(result.id, result.toolName, result.result, result.error);
538
804
  this.config.onStream?.({ type: "tool_result", result });
539
805
  }
540
- // Fire-and-forget tool use summary (non-blocking)
806
+ // Fire-and-forget tool use summary (non-blocking). The whole chain is
807
+ // best-effort observability — a thrown onStream handler, a failed dynamic
808
+ // import, or a rejecting summarize must never surface as an unhandled
809
+ // rejection (Node can treat those as fatal). Swallow at the tail. (B-2)
541
810
  if (this.config.onStream) {
542
- import("./tool-summary.js").then(({ generateToolUseSummary }) => {
811
+ void import("./tool-summary.js")
812
+ .then(({ generateToolUseSummary }) => {
543
813
  if (!this.deps.model.summarize)
544
814
  return;
545
- generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
815
+ return generateToolUseSummary(toolCalls, results, this.deps.model.summarize).then((summary) => {
546
816
  if (summary) {
547
817
  this.config.onStream?.({ type: "tool_summary", summary });
548
818
  }
549
819
  });
820
+ })
821
+ .catch((err) => {
822
+ this.currentTurnLog.warn("tool_summary.dispatch_failed", {
823
+ cat: "turn",
824
+ error: err.message,
825
+ });
826
+ });
827
+ }
828
+ const toolResultMessage = { role: "user", content: resultBlocks };
829
+ messages.push(toolResultMessage);
830
+ this.trackFreshImageMessage(toolResultMessage);
831
+ // B-3: tell the model which of its requested tool calls were dropped by
832
+ // the per-turn cap so it can re-issue them, instead of silently assuming
833
+ // they ran. Appended to the same user message that carries the results.
834
+ if (droppedToolCalls.length > 0) {
835
+ tlog.info("turn.tool_calls_capped", {
836
+ cat: "turn",
837
+ executed: toolCalls.length,
838
+ dropped: droppedToolCalls.length,
839
+ cap: this.config.maxToolCallsPerTurn,
840
+ });
841
+ const droppedNames = droppedToolCalls.map((t) => t.toolName).join(", ");
842
+ // Separate user message (not folded into the tool_result blocks) so the
843
+ // OpenAI converter — which lifts tool_results into standalone role:tool
844
+ // messages — keeps the reminder as a plain user turn after them.
845
+ messages.push({
846
+ role: "user",
847
+ content: `<system-reminder>Only the first ${toolCalls.length} of your ${response.toolCalls.length} ` +
848
+ `tool calls ran this turn (per-turn limit is ${this.config.maxToolCallsPerTurn}). ` +
849
+ `These were NOT executed and produced no result — do NOT assume they ran: ${droppedNames}. ` +
850
+ `Re-issue the ones you still need in the next turn.</system-reminder>`,
550
851
  });
551
852
  }
552
- messages.push({ role: "user", content: resultBlocks });
553
853
  // Tool results just pushed; recompute ctx so the bar updates *before*
554
854
  // the next model round-trip — large tool outputs can move it sharply.
555
855
  this.emitCtxFromMessages(messages);
@@ -557,10 +857,33 @@ export class TurnLoop {
557
857
  // it has DECLARED the goal done — short-circuit to "completed" WITHOUT
558
858
  // running the judge hook. The tool's result is already in `messages`
559
859
  // above so the summary lands in the transcript. Reset the stop-block
560
- // counter so a prior judge-driven block streak doesn't leak out.
860
+ // counter so a prior judge-driven block streak doesn't leak out. Also
861
+ // clear the PERSISTED goal so a later bare send doesn't re-inherit a goal
862
+ // the model just declared finished (the judge's onMet does this when IT
863
+ // decides; a self-report must do the same, else the goal outlives its
864
+ // completion and re-arms every follow-up turn).
561
865
  if (goalTracker && toolCalls.some((tc) => tc.toolName === COMPLETE_GOAL_TOOL_NAME)) {
562
866
  tlog.info("turn.goal_self_reported_complete", { cat: "goal" });
563
867
  this.stopBlockCount = 0;
868
+ this.deps.clearPersistedGoal?.();
869
+ if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
870
+ continue;
871
+ }
872
+ return { text: finalText, reason: "completed", messages };
873
+ }
874
+ // Goal mode: user-initiated cancellation. cancel_goal is the "strong
875
+ // intent" escape hatch — honor it ONLY when confirm===true (the guard the
876
+ // tool advertises). A confirmed cancel stops the run AND clears the
877
+ // persisted goal so it never re-arms; an unconfirmed call is a no-op here
878
+ // (the tool's own result string already told the model it was ignored).
879
+ const confirmedCancel = toolCalls.some((tc) => tc.toolName === CANCEL_GOAL_TOOL_NAME && tc.args?.confirm === true);
880
+ if (goalTracker && confirmedCancel) {
881
+ tlog.info("turn.goal_user_cancelled", { cat: "goal" });
882
+ this.stopBlockCount = 0;
883
+ this.deps.clearPersistedGoal?.();
884
+ if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
885
+ continue;
886
+ }
564
887
  return { text: finalText, reason: "completed", messages };
565
888
  }
566
889
  // Token budget check
@@ -577,6 +900,9 @@ export class TurnLoop {
577
900
  message: { role: "assistant", content: finalText },
578
901
  });
579
902
  messages.push({ role: "assistant", content: finalText });
903
+ if (this.consumeQueuedSteer(messages, "finalize_backfill")) {
904
+ continue;
905
+ }
580
906
  return { text: finalText, reason: "completed", messages };
581
907
  }
582
908
  if (budgetDecision === "nudge") {
@@ -632,12 +958,19 @@ export class TurnLoop {
632
958
  // engine's post-run saveState records model_error instead of leaving
633
959
  // the session stuck at "active".
634
960
  this.patchOrphanedToolUses(messages);
961
+ // A user-initiated Stop also bubbles here (an AbortError thrown from the
962
+ // per-turn scaffolding). Treat it as a clean abort, not a model_error —
963
+ // no error event, so the UI shows only the "你停止了本轮" line.
964
+ if (isAbortError(err) || this.config.signal?.aborted) {
965
+ this.markStopped();
966
+ return { text: finalText, reason: "aborted_streaming", messages };
967
+ }
635
968
  this.currentTurnLog.error("turn.unhandled_error", {
636
969
  cat: "turn",
637
970
  error: err.message,
638
971
  stack: err.stack?.split("\n").slice(0, 4).join("\n"),
639
972
  });
640
- this.config.onStream?.({ type: "error", error: err.message });
973
+ this.config.onStream?.({ type: "error", error: formatFriendlyError(err) });
641
974
  return { text: finalText, reason: "model_error", messages };
642
975
  }
643
976
  // Max turns reached — do one final summarization call (no tools)
@@ -646,6 +979,8 @@ export class TurnLoop {
646
979
  maxTurns: this.config.maxTurns,
647
980
  turnCount: this.turnCount,
648
981
  });
982
+ this.consumeQueuedSteer(messages, "finalize_backfill");
983
+ messages = this.prepareMessagesForModel(messages);
649
984
  messages = this.deps.contextManager.manage(messages);
650
985
  messages.push({
651
986
  role: "user",
@@ -653,8 +988,9 @@ export class TurnLoop {
653
988
  });
654
989
  this.emitCtxFromMessages(messages);
655
990
  try {
656
- const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, messages, [], // No tools available for summary turn
991
+ const summaryResponse = await this.deps.model.call(this.deps.systemPrompt, this.prepareMessagesForModel(messages), [], // No tools available for summary turn
657
992
  this.config.onStream, this.config.signal);
993
+ messages = this.markPendingImagesConsumed(messages);
658
994
  if (summaryResponse.text) {
659
995
  finalText = summaryResponse.text;
660
996
  }
@@ -719,7 +1055,15 @@ export class TurnLoop {
719
1055
  // non-streaming call here re-sends the whole request the user just
720
1056
  // aborted. Propagate so the run unwinds cleanly instead of doing
721
1057
  // more work after cancellation.
722
- if (isAbortError(err))
1058
+ //
1059
+ // Check the run signal too, not only isAbortError(err): when a streaming
1060
+ // request is aborted the error sometimes surfaces as a generic
1061
+ // `Error: Request was aborted.` whose `name` is NOT "AbortError"
1062
+ // (wrapping strips it), so isAbortError misses it and we'd fall back —
1063
+ // re-issuing the cancelled request, which then throws "Aborted before
1064
+ // LLM request" from withRetry and surfaces as a scary error on what was
1065
+ // really just a cancel. The signal is the authoritative cancel source.
1066
+ if (isAbortError(err) || this.config.signal?.aborted)
723
1067
  throw err;
724
1068
  // Streaming might have partially emitted — send tombstone to revoke
725
1069
  this.config.onStream?.({ type: "tombstone", messageId: `turn_${this.turnCount}` });
@@ -734,51 +1078,85 @@ export class TurnLoop {
734
1078
  get currentTurn() {
735
1079
  return this.turnCount;
736
1080
  }
1081
+ consumeQueuedSteer(messages, source) {
1082
+ const steered = this.deps.consumeSteer?.(source) ?? [];
1083
+ let consumed = false;
1084
+ for (const { id, text, clientMessageId } of steered) {
1085
+ if (!text)
1086
+ continue;
1087
+ if (clientMessageId && this.deps.claimClientMessageId?.(clientMessageId, "steer") === false) {
1088
+ logger.info("steer.submit.duplicate_ignored", {
1089
+ clientMessageId,
1090
+ steerId: id,
1091
+ sessionId: this.deps.sessionId,
1092
+ source,
1093
+ });
1094
+ continue;
1095
+ }
1096
+ consumed = true;
1097
+ messages.push({ role: "user", content: text });
1098
+ this.deps.transcript.appendMessage("user", text, { steerId: id, clientMessageId });
1099
+ this.config.onStream?.({ type: "steer_injected", text, id });
1100
+ }
1101
+ return consumed;
1102
+ }
737
1103
  /**
738
1104
  * Generate synthetic error tool_results for any dangling tool_use blocks
739
1105
  * that never received results (e.g. because the API call failed).
740
1106
  * Prevents model confusion on the next turn.
741
1107
  */
742
1108
  patchOrphanedToolUses(messages) {
743
- // Find the last assistant message with tool_use blocks
744
- for (let i = messages.length - 1; i >= 0; i--) {
745
- const msg = messages[i];
746
- if (msg.role !== "assistant" || !Array.isArray(msg.content))
1109
+ // Pre-compute every answered tool_use id across the WHOLE array, then scan
1110
+ // forward and patch EVERY assistant message with a gap not just the most
1111
+ // recent one. The old version scanned backward and returned at the first
1112
+ // all-answered assistant message, so when several earlier turns each left
1113
+ // orphaned tool_uses (multi-turn API failures) only the latest got patched
1114
+ // and the rest stayed unpaired → a 400 on the next call / on resume.
1115
+ const answeredIds = new Set();
1116
+ for (const msg of messages) {
1117
+ if (!Array.isArray(msg.content))
747
1118
  continue;
748
- const toolUseIds = [];
749
1119
  for (const block of msg.content) {
750
- if (block.type === "tool_use" && block.id)
751
- toolUseIds.push(block.id);
1120
+ if (block.type === "tool_result" && block.tool_use_id) {
1121
+ answeredIds.add(block.tool_use_id);
1122
+ }
752
1123
  }
753
- if (toolUseIds.length === 0)
1124
+ }
1125
+ let totalPatched = 0;
1126
+ for (let i = 0; i < messages.length; i++) {
1127
+ const msg = messages[i];
1128
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
754
1129
  continue;
755
- // Check if all tool_use IDs have corresponding tool_results
756
- const answeredIds = new Set();
757
- for (let j = i + 1; j < messages.length; j++) {
758
- const rm = messages[j];
759
- if (!Array.isArray(rm.content))
760
- continue;
761
- for (const block of rm.content) {
762
- if (block.type === "tool_result" && block.tool_use_id) {
763
- answeredIds.add(block.tool_use_id);
764
- }
1130
+ const orphanedIds = [];
1131
+ for (const block of msg.content) {
1132
+ if (block.type === "tool_use" && block.id && !answeredIds.has(block.id)) {
1133
+ orphanedIds.push(block.id);
765
1134
  }
766
1135
  }
767
- const orphanedIds = toolUseIds.filter((id) => !answeredIds.has(id));
768
1136
  if (orphanedIds.length === 0)
769
- return;
770
- // Inject synthetic error results
1137
+ continue;
1138
+ // Synthetic error results, spliced in RIGHT AFTER the offending assistant
1139
+ // message (not appended to the end) so each tool_use is paired in place.
1140
+ // is_error must be set: the Anthropic provider only emits is_error when
1141
+ // the flag is present, otherwise the model reads the cancellation as plain
1142
+ // text output and assumes the tool succeeded.
771
1143
  const errorBlocks = orphanedIds.map((id) => ({
772
1144
  type: "tool_result",
773
1145
  tool_use_id: id,
774
1146
  content: "Error: Tool execution was cancelled because the previous API call failed.",
1147
+ is_error: true,
775
1148
  }));
776
- messages.push({ role: "user", content: errorBlocks });
1149
+ messages.splice(i + 1, 0, { role: "user", content: errorBlocks });
1150
+ for (const id of orphanedIds)
1151
+ answeredIds.add(id);
1152
+ totalPatched += orphanedIds.length;
1153
+ i += 1; // skip the message we just spliced in
1154
+ }
1155
+ if (totalPatched > 0) {
777
1156
  this.currentTurnLog.warn("turn.patched_orphaned_tool_uses", {
778
1157
  cat: "turn",
779
- count: orphanedIds.length,
1158
+ count: totalPatched,
780
1159
  });
781
- return; // Only patch the most recent orphaned set
782
1160
  }
783
1161
  }
784
1162
  }