@swarmclawai/swarmclaw 0.8.4 → 0.8.7

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 (394) hide show
  1. package/README.md +9 -9
  2. package/bin/swarmclaw.js +5 -1
  3. package/bin/worker-cmd.js +73 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +17 -7
  6. package/src/app/api/agents/route.ts +21 -8
  7. package/src/app/api/approvals/route.test.ts +6 -6
  8. package/src/app/api/approvals/route.ts +2 -1
  9. package/src/app/api/auth/route.ts +2 -3
  10. package/src/app/api/chatrooms/[id]/chat/route.test.ts +299 -0
  11. package/src/app/api/chatrooms/[id]/chat/route.ts +3 -2
  12. package/src/app/api/chatrooms/[id]/route.ts +7 -6
  13. package/src/app/api/chats/[id]/chat/route.test.ts +496 -0
  14. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  15. package/src/app/api/chats/[id]/clear/route.ts +9 -9
  16. package/src/app/api/chats/[id]/devserver/route.ts +2 -1
  17. package/src/app/api/chats/[id]/edit-resend/route.ts +3 -4
  18. package/src/app/api/chats/[id]/fork/route.ts +3 -5
  19. package/src/app/api/chats/[id]/restore/route.ts +6 -7
  20. package/src/app/api/chats/[id]/retry/route.ts +3 -4
  21. package/src/app/api/chats/[id]/route.ts +61 -62
  22. package/src/app/api/chats/route.ts +7 -1
  23. package/src/app/api/connectors/[id]/route.ts +7 -8
  24. package/src/app/api/connectors/route.ts +5 -4
  25. package/src/app/api/eval/run/route.ts +2 -1
  26. package/src/app/api/eval/suite/route.ts +2 -1
  27. package/src/app/api/external-agents/route.test.ts +1 -1
  28. package/src/app/api/external-agents/route.ts +2 -2
  29. package/src/app/api/files/serve/route.ts +1 -1
  30. package/src/app/api/gateways/[id]/route.ts +7 -5
  31. package/src/app/api/gateways/route.ts +1 -1
  32. package/src/app/api/knowledge/upload/route.ts +1 -1
  33. package/src/app/api/logs/route.ts +5 -7
  34. package/src/app/api/memory-images/[filename]/route.ts +2 -3
  35. package/src/app/api/openclaw/agent-files/route.ts +4 -3
  36. package/src/app/api/openclaw/approvals/route.ts +3 -4
  37. package/src/app/api/openclaw/config-sync/route.ts +3 -2
  38. package/src/app/api/openclaw/cron/route.ts +3 -2
  39. package/src/app/api/openclaw/dotenv-keys/route.ts +2 -1
  40. package/src/app/api/openclaw/exec-config/route.ts +3 -2
  41. package/src/app/api/openclaw/gateway/route.ts +5 -4
  42. package/src/app/api/openclaw/history/route.ts +3 -2
  43. package/src/app/api/openclaw/media/route.ts +2 -1
  44. package/src/app/api/openclaw/permissions/route.ts +3 -2
  45. package/src/app/api/openclaw/sandbox-env/route.ts +3 -2
  46. package/src/app/api/openclaw/skills/install/route.ts +2 -1
  47. package/src/app/api/openclaw/skills/remove/route.ts +2 -1
  48. package/src/app/api/openclaw/skills/route.ts +3 -2
  49. package/src/app/api/orchestrator/run/route.ts +5 -14
  50. package/src/app/api/perf/route.ts +43 -0
  51. package/src/app/api/plugins/dependencies/route.ts +2 -1
  52. package/src/app/api/plugins/install/route.ts +2 -1
  53. package/src/app/api/plugins/marketplace/route.ts +3 -2
  54. package/src/app/api/plugins/settings/route.ts +2 -1
  55. package/src/app/api/preview-server/route.ts +11 -10
  56. package/src/app/api/projects/[id]/route.ts +1 -1
  57. package/src/app/api/schedules/[id]/route.test.ts +128 -0
  58. package/src/app/api/schedules/[id]/route.ts +43 -43
  59. package/src/app/api/schedules/[id]/run/route.ts +11 -62
  60. package/src/app/api/schedules/route.ts +21 -87
  61. package/src/app/api/settings/route.ts +2 -0
  62. package/src/app/api/setup/doctor/route.ts +9 -8
  63. package/src/app/api/tasks/[id]/approve/route.ts +33 -30
  64. package/src/app/api/tasks/[id]/route.ts +12 -35
  65. package/src/app/api/tasks/import/github/route.ts +2 -1
  66. package/src/app/api/tasks/route.ts +79 -91
  67. package/src/app/api/wallets/[id]/approve/route.ts +2 -1
  68. package/src/app/api/wallets/[id]/route.ts +13 -19
  69. package/src/app/api/wallets/[id]/send/route.ts +2 -1
  70. package/src/app/api/wallets/route.ts +2 -1
  71. package/src/app/api/webhooks/[id]/route.ts +2 -1
  72. package/src/app/api/webhooks/route.test.ts +3 -1
  73. package/src/app/page.tsx +23 -331
  74. package/src/cli/index.js +19 -0
  75. package/src/cli/index.ts +38 -7
  76. package/src/cli/spec.js +9 -0
  77. package/src/components/activity/activity-feed.tsx +7 -4
  78. package/src/components/agents/agent-card.tsx +32 -6
  79. package/src/components/agents/agent-chat-list.tsx +55 -22
  80. package/src/components/agents/agent-files-editor.tsx +3 -2
  81. package/src/components/agents/agent-sheet.tsx +123 -22
  82. package/src/components/agents/inspector-panel.tsx +1 -1
  83. package/src/components/agents/openclaw-skills-panel.tsx +2 -1
  84. package/src/components/agents/trash-list.tsx +1 -1
  85. package/src/components/auth/access-key-gate.tsx +8 -2
  86. package/src/components/auth/setup-wizard.tsx +10 -9
  87. package/src/components/auth/user-picker.tsx +3 -2
  88. package/src/components/chat/chat-area.tsx +20 -1
  89. package/src/components/chat/chat-card.tsx +18 -3
  90. package/src/components/chat/chat-header.tsx +24 -4
  91. package/src/components/chat/chat-list.tsx +2 -11
  92. package/src/components/chat/heartbeat-history-panel.tsx +2 -1
  93. package/src/components/chat/message-bubble.tsx +45 -6
  94. package/src/components/chat/message-list.tsx +280 -145
  95. package/src/components/chat/streaming-bubble.tsx +217 -60
  96. package/src/components/chat/swarm-panel.test.ts +274 -0
  97. package/src/components/chat/swarm-panel.tsx +410 -0
  98. package/src/components/chat/swarm-status-card.tsx +346 -0
  99. package/src/components/chat/tool-call-bubble.tsx +48 -23
  100. package/src/components/chatrooms/chatroom-list.tsx +8 -5
  101. package/src/components/chatrooms/chatroom-message.tsx +10 -7
  102. package/src/components/chatrooms/chatroom-view.tsx +12 -9
  103. package/src/components/connectors/connector-health.tsx +6 -4
  104. package/src/components/connectors/connector-list.tsx +16 -11
  105. package/src/components/connectors/connector-sheet.tsx +12 -6
  106. package/src/components/home/home-view.tsx +38 -24
  107. package/src/components/input/chat-input.tsx +10 -1
  108. package/src/components/layout/app-layout.tsx +2 -38
  109. package/src/components/layout/sheet-layer.tsx +50 -0
  110. package/src/components/mcp-servers/mcp-server-list.tsx +37 -5
  111. package/src/components/mcp-servers/mcp-server-sheet.tsx +12 -2
  112. package/src/components/plugins/plugin-list.tsx +8 -4
  113. package/src/components/plugins/plugin-sheet.tsx +2 -1
  114. package/src/components/providers/provider-list.tsx +3 -2
  115. package/src/components/providers/provider-sheet.tsx +2 -1
  116. package/src/components/runs/run-list.tsx +11 -7
  117. package/src/components/schedules/schedule-card.tsx +5 -3
  118. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  119. package/src/components/shared/attachment-chip.tsx +19 -3
  120. package/src/components/shared/notification-center.tsx +6 -3
  121. package/src/components/shared/settings/plugin-manager.tsx +3 -2
  122. package/src/components/shared/settings/section-embedding.tsx +2 -1
  123. package/src/components/shared/settings/section-orchestrator.tsx +2 -1
  124. package/src/components/shared/settings/section-user-preferences.tsx +107 -0
  125. package/src/components/shared/settings/settings-page.tsx +13 -9
  126. package/src/components/skills/clawhub-browser.tsx +15 -4
  127. package/src/components/skills/skill-list.tsx +15 -4
  128. package/src/components/tasks/approvals-panel.tsx +2 -1
  129. package/src/components/tasks/task-board.tsx +35 -37
  130. package/src/components/tasks/task-sheet.tsx +4 -3
  131. package/src/components/ui/full-screen-loader.tsx +164 -0
  132. package/src/components/wallets/wallet-approval-dialog.tsx +2 -1
  133. package/src/components/wallets/wallet-panel.tsx +6 -5
  134. package/src/components/wallets/wallet-section.tsx +3 -2
  135. package/src/components/webhooks/webhook-list.tsx +4 -5
  136. package/src/components/webhooks/webhook-sheet.tsx +6 -6
  137. package/src/hooks/use-app-bootstrap.ts +202 -0
  138. package/src/hooks/use-mounted-ref.ts +14 -0
  139. package/src/hooks/use-now.ts +31 -0
  140. package/src/hooks/use-openclaw-gateway.ts +2 -1
  141. package/src/instrumentation.ts +20 -8
  142. package/src/lib/agent-default-tools.test.ts +52 -0
  143. package/src/lib/agent-default-tools.ts +40 -0
  144. package/src/lib/api-client.test.ts +21 -0
  145. package/src/lib/api-client.ts +6 -11
  146. package/src/lib/canvas-content.test.ts +360 -0
  147. package/src/lib/chat-streaming-state.test.ts +49 -2
  148. package/src/lib/chat-streaming-state.ts +26 -10
  149. package/src/lib/fetch-timeout.test.ts +54 -0
  150. package/src/lib/fetch-timeout.ts +60 -3
  151. package/src/lib/live-tool-events.test.ts +77 -0
  152. package/src/lib/live-tool-events.ts +73 -0
  153. package/src/lib/local-observability.test.ts +2 -2
  154. package/src/lib/openclaw-endpoint.test.ts +1 -1
  155. package/src/lib/providers/anthropic.ts +12 -16
  156. package/src/lib/providers/index.ts +4 -2
  157. package/src/lib/providers/ollama.ts +9 -6
  158. package/src/lib/providers/openai.ts +11 -14
  159. package/src/lib/runtime-env.test.ts +8 -8
  160. package/src/lib/schedule-dedupe-advanced.test.ts +2 -2
  161. package/src/lib/schedule-dedupe.test.ts +1 -1
  162. package/src/lib/schedule-dedupe.ts +3 -2
  163. package/src/lib/server/agent-thread-session.test.ts +6 -6
  164. package/src/lib/server/agent-thread-session.ts +6 -9
  165. package/src/lib/server/alert-dispatch.ts +2 -1
  166. package/src/lib/server/api-routes.test.ts +6 -6
  167. package/src/lib/server/approval-connector-notify.test.ts +4 -4
  168. package/src/lib/server/approvals-auto-approve.test.ts +29 -29
  169. package/src/lib/server/approvals.test.ts +317 -0
  170. package/src/lib/server/approvals.ts +5 -4
  171. package/src/lib/server/autonomy-runtime.test.ts +11 -11
  172. package/src/lib/server/browser-state.ts +2 -2
  173. package/src/lib/server/capability-router.test.ts +1 -1
  174. package/src/lib/server/capability-router.ts +3 -2
  175. package/src/lib/server/chat-execution-advanced.test.ts +15 -2
  176. package/src/lib/server/chat-execution-connector-delivery.ts +67 -0
  177. package/src/lib/server/chat-execution-disabled.test.ts +3 -3
  178. package/src/lib/server/chat-execution-eval-history.test.ts +3 -3
  179. package/src/lib/server/chat-execution-heartbeat.test.ts +42 -1
  180. package/src/lib/server/chat-execution-session-sync.test.ts +119 -0
  181. package/src/lib/server/chat-execution-tool-events.ts +116 -0
  182. package/src/lib/server/chat-execution-utils.test.ts +479 -0
  183. package/src/lib/server/chat-execution-utils.ts +533 -0
  184. package/src/lib/server/chat-execution.ts +153 -748
  185. package/src/lib/server/chat-streaming-utils.ts +174 -0
  186. package/src/lib/server/chat-turn-tool-routing.ts +310 -0
  187. package/src/lib/server/chatroom-session-persistence.test.ts +2 -2
  188. package/src/lib/server/clawhub-client.ts +2 -1
  189. package/src/lib/server/collection-helpers.test.ts +92 -0
  190. package/src/lib/server/collection-helpers.ts +25 -3
  191. package/src/lib/server/connectors/access.ts +146 -0
  192. package/src/lib/server/connectors/bluebubbles.test.ts +1 -1
  193. package/src/lib/server/connectors/bluebubbles.ts +4 -4
  194. package/src/lib/server/connectors/commands.ts +367 -0
  195. package/src/lib/server/connectors/connector-routing.test.ts +4 -4
  196. package/src/lib/server/connectors/delivery.ts +142 -0
  197. package/src/lib/server/connectors/discord.ts +37 -40
  198. package/src/lib/server/connectors/email.ts +11 -10
  199. package/src/lib/server/connectors/googlechat.ts +4 -4
  200. package/src/lib/server/connectors/inbound-audio-transcription.ts +2 -1
  201. package/src/lib/server/connectors/ingress-delivery.ts +23 -0
  202. package/src/lib/server/connectors/manager-roundtrip.test.ts +300 -0
  203. package/src/lib/server/connectors/manager.test.ts +352 -77
  204. package/src/lib/server/connectors/manager.ts +134 -673
  205. package/src/lib/server/connectors/matrix.ts +4 -4
  206. package/src/lib/server/connectors/message-sentinel.ts +7 -0
  207. package/src/lib/server/connectors/openclaw.test.ts +1 -1
  208. package/src/lib/server/connectors/openclaw.ts +8 -10
  209. package/src/lib/server/connectors/outbox.test.ts +192 -0
  210. package/src/lib/server/connectors/outbox.ts +369 -0
  211. package/src/lib/server/connectors/pairing.test.ts +18 -1
  212. package/src/lib/server/connectors/pairing.ts +49 -4
  213. package/src/lib/server/connectors/policy.ts +9 -3
  214. package/src/lib/server/connectors/reconnect-state.ts +71 -0
  215. package/src/lib/server/connectors/response-media.ts +256 -0
  216. package/src/lib/server/connectors/runtime-state.ts +67 -0
  217. package/src/lib/server/connectors/session.test.ts +357 -0
  218. package/src/lib/server/connectors/session.ts +422 -0
  219. package/src/lib/server/connectors/signal.ts +7 -7
  220. package/src/lib/server/connectors/slack.ts +43 -43
  221. package/src/lib/server/connectors/teams.ts +4 -4
  222. package/src/lib/server/connectors/telegram.ts +37 -43
  223. package/src/lib/server/connectors/types.ts +31 -1
  224. package/src/lib/server/connectors/whatsapp.test.ts +108 -0
  225. package/src/lib/server/connectors/whatsapp.ts +106 -34
  226. package/src/lib/server/context-manager.test.ts +409 -0
  227. package/src/lib/server/cost.test.ts +1 -1
  228. package/src/lib/server/daemon-policy.ts +78 -0
  229. package/src/lib/server/daemon-state-connectors.test.ts +167 -0
  230. package/src/lib/server/daemon-state.test.ts +283 -55
  231. package/src/lib/server/daemon-state.ts +106 -109
  232. package/src/lib/server/data-dir.test.ts +5 -5
  233. package/src/lib/server/data-dir.ts +4 -0
  234. package/src/lib/server/delegation-jobs-advanced.test.ts +1 -1
  235. package/src/lib/server/delegation-jobs.test.ts +87 -0
  236. package/src/lib/server/delegation-jobs.ts +42 -48
  237. package/src/lib/server/devserver-launch.ts +1 -1
  238. package/src/lib/server/document-utils.ts +7 -9
  239. package/src/lib/server/elevenlabs.ts +2 -1
  240. package/src/lib/server/embeddings.test.ts +105 -0
  241. package/src/lib/server/ethereum.ts +3 -2
  242. package/src/lib/server/eval/agent-regression.ts +3 -2
  243. package/src/lib/server/eval/runner.ts +2 -1
  244. package/src/lib/server/eval/scorer.ts +2 -1
  245. package/src/lib/server/evm-swap.ts +2 -1
  246. package/src/lib/server/gateway/protocol.test.ts +1 -1
  247. package/src/lib/server/guardian.ts +2 -1
  248. package/src/lib/server/heartbeat-blocked-suppression.test.ts +151 -0
  249. package/src/lib/server/heartbeat-service-timer.test.ts +6 -6
  250. package/src/lib/server/heartbeat-service.test.ts +406 -0
  251. package/src/lib/server/heartbeat-service.ts +54 -7
  252. package/src/lib/server/heartbeat-wake.test.ts +19 -0
  253. package/src/lib/server/heartbeat-wake.ts +17 -16
  254. package/src/lib/server/integrity-monitor.test.ts +149 -0
  255. package/src/lib/server/json-utils.ts +22 -0
  256. package/src/lib/server/knowledge-db.test.ts +13 -13
  257. package/src/lib/server/link-understanding.ts +2 -1
  258. package/src/lib/server/llm-response-cache.test.ts +1 -1
  259. package/src/lib/server/main-agent-loop-advanced.test.ts +65 -3
  260. package/src/lib/server/main-agent-loop.test.ts +6 -6
  261. package/src/lib/server/main-agent-loop.ts +21 -7
  262. package/src/lib/server/mcp-client.test.ts +1 -1
  263. package/src/lib/server/mcp-conformance.test.ts +1 -1
  264. package/src/lib/server/mcp-conformance.ts +3 -2
  265. package/src/lib/server/memory-consolidation.ts +2 -1
  266. package/src/lib/server/memory-db.test.ts +485 -0
  267. package/src/lib/server/memory-db.ts +39 -26
  268. package/src/lib/server/memory-graph.test.ts +2 -2
  269. package/src/lib/server/memory-policy.test.ts +7 -7
  270. package/src/lib/server/memory-retrieval.test.ts +1 -1
  271. package/src/lib/server/openclaw-config-sync.ts +2 -1
  272. package/src/lib/server/openclaw-deploy.test.ts +1 -1
  273. package/src/lib/server/openclaw-deploy.ts +8 -12
  274. package/src/lib/server/openclaw-exec-config.ts +2 -1
  275. package/src/lib/server/openclaw-gateway.ts +6 -7
  276. package/src/lib/server/openclaw-skills-normalize.ts +2 -1
  277. package/src/lib/server/openclaw-sync.ts +7 -5
  278. package/src/lib/server/orchestrator-lg-structure.test.ts +17 -0
  279. package/src/lib/server/orchestrator-lg.ts +199 -327
  280. package/src/lib/server/path-utils.ts +31 -0
  281. package/src/lib/server/perf.ts +161 -0
  282. package/src/lib/server/plugins-approval-guidance.ts +115 -0
  283. package/src/lib/server/plugins.test.ts +1 -1
  284. package/src/lib/server/plugins.ts +22 -132
  285. package/src/lib/server/process-manager.ts +5 -8
  286. package/src/lib/server/provider-health.test.ts +137 -0
  287. package/src/lib/server/provider-health.ts +3 -3
  288. package/src/lib/server/provider-model-discovery.ts +3 -12
  289. package/src/lib/server/queue-followups.test.ts +9 -9
  290. package/src/lib/server/queue-reconcile.test.ts +2 -2
  291. package/src/lib/server/queue-recovery.test.ts +269 -0
  292. package/src/lib/server/queue.test.ts +570 -0
  293. package/src/lib/server/queue.ts +62 -455
  294. package/src/lib/server/resolve-image.ts +30 -0
  295. package/src/lib/server/runtime-settings.test.ts +4 -4
  296. package/src/lib/server/runtime-storage-write-paths.test.ts +60 -0
  297. package/src/lib/server/schedule-normalization.test.ts +279 -0
  298. package/src/lib/server/schedule-service.ts +263 -0
  299. package/src/lib/server/scheduler.ts +17 -74
  300. package/src/lib/server/session-mailbox.test.ts +191 -0
  301. package/src/lib/server/session-run-manager.test.ts +640 -0
  302. package/src/lib/server/session-run-manager.ts +59 -15
  303. package/src/lib/server/session-tools/autonomy-tools.test.ts +20 -20
  304. package/src/lib/server/session-tools/calendar.ts +2 -1
  305. package/src/lib/server/session-tools/canvas.ts +2 -1
  306. package/src/lib/server/session-tools/chatroom.ts +2 -1
  307. package/src/lib/server/session-tools/connector.ts +26 -28
  308. package/src/lib/server/session-tools/context-mgmt.ts +3 -2
  309. package/src/lib/server/session-tools/crawl.ts +4 -3
  310. package/src/lib/server/session-tools/crud.ts +105 -324
  311. package/src/lib/server/session-tools/delegate-fallback.test.ts +9 -9
  312. package/src/lib/server/session-tools/delegate.ts +6 -8
  313. package/src/lib/server/session-tools/discovery-approvals.test.ts +15 -15
  314. package/src/lib/server/session-tools/discovery.ts +4 -3
  315. package/src/lib/server/session-tools/document.ts +2 -1
  316. package/src/lib/server/session-tools/email.ts +2 -1
  317. package/src/lib/server/session-tools/extract.ts +2 -1
  318. package/src/lib/server/session-tools/file.ts +4 -3
  319. package/src/lib/server/session-tools/http.ts +2 -1
  320. package/src/lib/server/session-tools/human-loop.ts +2 -1
  321. package/src/lib/server/session-tools/image-gen.ts +4 -3
  322. package/src/lib/server/session-tools/index.ts +26 -30
  323. package/src/lib/server/session-tools/mailbox.ts +2 -1
  324. package/src/lib/server/session-tools/manage-connectors.test.ts +4 -4
  325. package/src/lib/server/session-tools/manage-schedules.test.ts +12 -12
  326. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +5 -5
  327. package/src/lib/server/session-tools/manage-tasks.test.ts +2 -2
  328. package/src/lib/server/session-tools/monitor.ts +2 -1
  329. package/src/lib/server/session-tools/platform.ts +2 -1
  330. package/src/lib/server/session-tools/plugin-creator.ts +2 -1
  331. package/src/lib/server/session-tools/replicate.ts +3 -2
  332. package/src/lib/server/session-tools/session-tools-wiring.test.ts +6 -6
  333. package/src/lib/server/session-tools/shell.ts +4 -9
  334. package/src/lib/server/session-tools/subagent.ts +322 -170
  335. package/src/lib/server/session-tools/table.ts +6 -5
  336. package/src/lib/server/session-tools/wallet-tool.test.ts +3 -3
  337. package/src/lib/server/session-tools/wallet.ts +7 -6
  338. package/src/lib/server/session-tools/web-browser-config.test.ts +1 -0
  339. package/src/lib/server/session-tools/web-utils.ts +317 -0
  340. package/src/lib/server/session-tools/web.ts +62 -328
  341. package/src/lib/server/skill-prompt-budget.test.ts +1 -1
  342. package/src/lib/server/skills-normalize.ts +2 -1
  343. package/src/lib/server/storage-item-access.test.ts +302 -0
  344. package/src/lib/server/storage.ts +366 -314
  345. package/src/lib/server/stream-agent-chat.test.ts +82 -3
  346. package/src/lib/server/stream-agent-chat.ts +146 -510
  347. package/src/lib/server/stream-continuation.ts +412 -0
  348. package/src/lib/server/subagent-lineage.test.ts +647 -0
  349. package/src/lib/server/subagent-lineage.ts +435 -0
  350. package/src/lib/server/subagent-runtime.test.ts +484 -0
  351. package/src/lib/server/subagent-runtime.ts +419 -0
  352. package/src/lib/server/subagent-swarm.test.ts +391 -0
  353. package/src/lib/server/subagent-swarm.ts +564 -0
  354. package/src/lib/server/system-events.ts +3 -3
  355. package/src/lib/server/task-followups.test.ts +491 -0
  356. package/src/lib/server/task-followups.ts +391 -0
  357. package/src/lib/server/task-lifecycle.test.ts +205 -0
  358. package/src/lib/server/task-lifecycle.ts +200 -0
  359. package/src/lib/server/task-quality-gate.test.ts +1 -1
  360. package/src/lib/server/task-resume.ts +208 -0
  361. package/src/lib/server/task-service.test.ts +108 -0
  362. package/src/lib/server/task-service.ts +264 -0
  363. package/src/lib/server/task-validation.test.ts +1 -1
  364. package/src/lib/server/test-utils/run-with-temp-data-dir.ts +42 -0
  365. package/src/lib/server/tool-capability-policy.test.ts +2 -2
  366. package/src/lib/server/tool-capability-policy.ts +3 -2
  367. package/src/lib/server/tool-planning.ts +2 -1
  368. package/src/lib/server/tool-retry.ts +2 -3
  369. package/src/lib/server/wake-dispatcher.test.ts +303 -0
  370. package/src/lib/server/wake-dispatcher.ts +318 -0
  371. package/src/lib/server/wake-mode.test.ts +161 -0
  372. package/src/lib/server/wake-mode.ts +174 -0
  373. package/src/lib/server/wallet-service.ts +8 -9
  374. package/src/lib/server/watch-jobs.ts +2 -1
  375. package/src/lib/server/workspace-context.ts +2 -2
  376. package/src/lib/shared-utils.test.ts +142 -0
  377. package/src/lib/shared-utils.ts +62 -0
  378. package/src/lib/tool-event-summary.ts +2 -1
  379. package/src/lib/view-routes.test.ts +100 -0
  380. package/src/lib/wallet.test.ts +322 -6
  381. package/src/proxy.test.ts +4 -4
  382. package/src/proxy.ts +2 -3
  383. package/src/stores/set-if-changed.ts +40 -0
  384. package/src/stores/slices/agent-slice.ts +111 -0
  385. package/src/stores/slices/auth-slice.ts +25 -0
  386. package/src/stores/slices/data-slice.ts +301 -0
  387. package/src/stores/slices/index.ts +7 -0
  388. package/src/stores/slices/session-slice.ts +112 -0
  389. package/src/stores/slices/task-slice.ts +63 -0
  390. package/src/stores/slices/ui-slice.ts +192 -0
  391. package/src/stores/use-app-store.ts +17 -822
  392. package/src/stores/use-approval-store.ts +2 -1
  393. package/src/stores/use-chat-store.ts +8 -1
  394. package/src/types/index.ts +10 -0
@@ -12,12 +12,14 @@ import { buildSkillPromptText } from './skill-prompt-budget'
12
12
 
13
13
  import { logExecution } from './execution-log'
14
14
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
15
- import { canonicalizePluginId, expandPluginIds } from './tool-aliases'
15
+ import { canonicalizePluginId, expandPluginIds, pluginIdMatches } from './tool-aliases'
16
16
  import type { Session, Message, UsageRecord, PluginInvocationRecord, MessageToolEvent } from '@/types'
17
17
  import { extractSuggestions } from './suggestions'
18
18
  import { buildIdentityContinuityContext } from './identity-continuity'
19
19
  import { enqueueSystemEvent } from './system-events'
20
20
  import { resolveActiveProjectContext } from './project-context'
21
+ import { resolveImagePath } from './resolve-image'
22
+ import { routeTaskIntent } from './capability-router'
21
23
  import {
22
24
  getEnabledToolPlanningView,
23
25
  getFirstToolForCapability,
@@ -26,7 +28,54 @@ import {
26
28
  } from './tool-planning'
27
29
  import { ToolLoopTracker } from './tool-loop-detection'
28
30
  import type { LoopDetectionResult } from './tool-loop-detection'
29
- import { isCurrentThreadRecallRequest, isDirectMemoryWriteRequest } from './memory-policy'
31
+ import { isCurrentThreadRecallRequest } from './memory-policy'
32
+ import {
33
+ isBroadGoal,
34
+ looksLikeExternalWalletTask,
35
+ looksLikeBoundedExternalExecutionTask,
36
+ looksLikeOpenEndedDeliverableTask,
37
+ shouldForceExternalExecutionFollowthrough,
38
+ shouldForceDeliverableFollowthrough,
39
+ hasStateChangingWalletEvidence,
40
+ countExternalExecutionResearchSteps,
41
+ countDistinctExternalResearchHosts,
42
+ renderToolEvidence,
43
+ resolveFinalStreamResponseText,
44
+ resolveContinuationAssistantText,
45
+ buildContinuationPrompt,
46
+ } from './stream-continuation'
47
+ import type { ContinuationType } from './stream-continuation'
48
+ import { errorMessage, sleep } from '@/lib/shared-utils'
49
+ import { perf } from './perf'
50
+ import {
51
+ compactThreadRecallText,
52
+ getExplicitRequiredToolNames,
53
+ getWalletApprovalBoundaryAction,
54
+ isNarrowDirectMemoryWriteTurn,
55
+ isWalletSimulationResult,
56
+ resolveToolAction,
57
+ shouldAllowToolForDirectMemoryWrite,
58
+ shouldAllowToolForCurrentThreadRecall,
59
+ shouldForceExternalServiceSummary,
60
+ shouldTerminateOnSuccessfulMemoryMutation,
61
+ updateStreamedToolEvents,
62
+ } from './chat-streaming-utils'
63
+
64
+ // Re-export continuation functions so existing consumers don't need to change imports
65
+ export {
66
+ getExplicitRequiredToolNames,
67
+ isNarrowDirectMemoryWriteTurn,
68
+ isWalletSimulationResult,
69
+ looksLikeOpenEndedDeliverableTask,
70
+ shouldAllowToolForDirectMemoryWrite,
71
+ shouldAllowToolForCurrentThreadRecall,
72
+ shouldForceExternalExecutionFollowthrough,
73
+ shouldForceDeliverableFollowthrough,
74
+ shouldForceExternalServiceSummary,
75
+ shouldTerminateOnSuccessfulMemoryMutation,
76
+ resolveFinalStreamResponseText,
77
+ resolveContinuationAssistantText,
78
+ }
30
79
 
31
80
  /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
32
81
  interface StreamAgentChatOpts {
@@ -66,6 +115,9 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
66
115
  const lines = [
67
116
  `Enabled tools in this session: ${uniqueTools.map((toolId) => `\`${toolId}\``).join(', ')}.`,
68
117
  'Only call tools from this enabled list or tools explicitly returned by the runtime.',
118
+ 'Treat enabled tools as available now. Do not ask the user for permission before routine use of an enabled tool.',
119
+ 'If the request clearly maps to an enabled tool, try that tool before telling the user to do it themselves.',
120
+ 'Only talk about approvals when a tool result explicitly returns an approval boundary for a concrete state-changing action.',
69
121
  ]
70
122
 
71
123
  const directPlatformTools = uniqueTools.filter((toolId) => toolId.startsWith('manage_') && toolId !== 'manage_platform')
@@ -91,6 +143,16 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
91
143
  lines.push(`For current events, live conflicts, or “keep watching for updates” requests, use \`${researchSearchTools[0]}\` before answering. Do not rely on memory or unstated background knowledge for fresh developments.`)
92
144
  }
93
145
 
146
+ const alternateResearchTools = Array.from(new Set([
147
+ ...(researchSearchTools.length || researchFetchTools.length ? [...researchSearchTools, ...researchFetchTools] : []),
148
+ ...httpTools,
149
+ ...(uniqueTools.includes('shell') ? ['shell'] : []),
150
+ ...(uniqueTools.includes('browser') ? ['browser'] : []),
151
+ ]))
152
+ if (alternateResearchTools.length >= 2) {
153
+ lines.push(`If one enabled research path is blocked by a site-specific error or access challenge, try one other enabled acquisition path (${alternateResearchTools.map((toolName) => `\`${toolName}\``).join(', ')}) before telling the user to fetch the data manually.`)
154
+ }
155
+
94
156
  if (browserCaptureTools.length && deliveryMediaTools.length) {
95
157
  lines.push(`When the user asks you to send screenshots or other media, capture the artifact first with \`${browserCaptureTools[0]}\`, then deliver that exact file or upload URL through \`${deliveryMediaTools[0]}\` instead of saying the capability is unavailable.`)
96
158
  }
@@ -145,62 +207,6 @@ export function buildToolDisciplineLines(enabledPlugins: string[]): string[] {
145
207
  return lines
146
208
  }
147
209
 
148
- export function looksLikeOpenEndedDeliverableTask(text: string): boolean {
149
- const normalized = text.toLowerCase()
150
- if (!normalized.trim()) return false
151
- if (/```|package\.json|tsconfig|\btsx?\b|\bjsx?\b|pytest|vitest|npm run|src\/|components\/|api\//.test(normalized)) return false
152
- if (/\b(revise|revision|iterate|iteration|draft|deliverable|deliverables|offer|brief|copy|proposal|landing|outreach|plan|strategy|report|memo|document|docs?)\b/.test(normalized)) return true
153
- // Explicit file-save instructions (e.g. "create X and save it to /tmp/foo.html")
154
- if (
155
- /\b(create|build|generate|make|write|produce)\b/.test(normalized)
156
- && /\b(save|write|output|export)\b[^.!?\n]{0,60}\b(to|as|in)\b[^.!?\n]{0,40}(\/|~\/|\.\/|\.[a-z]{2,5}\b)/.test(normalized)
157
- ) {
158
- return true
159
- }
160
- if (
161
- isBroadGoal(text)
162
- && /\b(create|build|generate|make|write|research|capture|take|start|produce)\b/.test(normalized)
163
- && /\b(screenshot|screenshots|image|images|markdown|\.md\b|md\b|md files?|pdf|pdf files?|html|html\s+(?:page|file)|dashboard|site|sites|website|web page|webpage|dev server|dev servers|artifact|artifacts|topic|topics)\b/.test(normalized)
164
- ) {
165
- return true
166
- }
167
- return isBroadGoal(text) && /(\.md\b|\.txt\b|\.html\b|\.json\b|copy|brief|proposal|plan|report|draft|document|dashboard)/.test(normalized)
168
- }
169
-
170
- /**
171
- * Returns tool names that the user explicitly referenced by name in their message.
172
- *
173
- * Previously this used regex-based capability matching (matchToolCapabilitiesForMessage)
174
- * to infer required tools from keywords like "send", "search", "screenshot". This caused
175
- * false positives ("sends an HTTP request" forced connector_message_tool, "create a file"
176
- * forced delivery tools) and extra continuation loops.
177
- *
178
- * OpenClaw's approach: trust the LLM to select the right tools based on prompt engineering
179
- * (tool discipline lines, skill adherence header, system prompt). No regex-based forced
180
- * tool requirements. The deliverable/execution followthrough mechanisms handle cases where
181
- * the agent stops early.
182
- *
183
- * We now only force tools when the user explicitly names them (ask_human, email) — these
184
- * are cases where the LLM has a known tendency to skip the tool and respond in prose.
185
- */
186
- export function getExplicitRequiredToolNames(userMessage: string, enabledPlugins: string[]): string[] {
187
- const normalized = userMessage.toLowerCase()
188
- const required: string[] = []
189
-
190
- // Only force tools that the user explicitly names and the LLM tends to skip
191
- if (enabledPlugins.includes('ask_human')
192
- && (/\bask_human\b/.test(normalized) || /ask the human/.test(normalized) || /request_input/.test(normalized))) {
193
- required.push('ask_human')
194
- }
195
-
196
- if (enabledPlugins.includes('email')
197
- && (/\bemail\b/.test(normalized) || /send a welcome email/.test(normalized) || /send an email/.test(normalized))) {
198
- required.push('email')
199
- }
200
-
201
- return required
202
- }
203
-
204
210
  const OPEN_ENDED_REVISION_BLOCK = [
205
211
  '## Revision Loop',
206
212
  'For open-ended deliverable work, do a real two-pass loop before declaring success: create the draft artifacts, critique them against the objective, then modify at least one artifact based on that critique.',
@@ -209,22 +215,23 @@ const OPEN_ENDED_REVISION_BLOCK = [
209
215
  'If `files` is available, use it with explicit actions and paths to inspect and revise the artifacts.',
210
216
  ].join('\n')
211
217
 
212
- function looksLikeExternalWalletTask(text: string): boolean {
213
- const normalized = text.toLowerCase()
214
- if (!normalized.trim()) return false
215
- return /\b(wallet|wallet connect|walletconnect|trade|trading|exchange|dex|bridge|swap|deposit|withdraw|onchain|token|gas|hyperliquid|arbitrum|ethereum|solana|base|usdc|eth|sol)\b/.test(normalized)
216
- }
217
-
218
- function looksLikeBoundedExternalExecutionTask(text: string): boolean {
219
- const normalized = text.toLowerCase()
220
- if (!looksLikeExternalWalletTask(text)) return false
221
- return /\b(live|swap|trade|buy|purchase|sell|mint|claim|execute|transact|transaction|approve|broadcast)\b/.test(normalized)
222
- }
223
-
224
218
  function getEnabledDisplayTool(enabledPlugins: string[], canonicalPluginId: string): string | null {
225
219
  return getEnabledToolPlanningView(enabledPlugins).displayToolIds.find((toolId) => toolId === canonicalPluginId) || null
226
220
  }
227
221
 
222
+ export function shouldForceAttachmentFollowthrough(params: {
223
+ userMessage: string
224
+ enabledPlugins: string[]
225
+ hasToolCalls: boolean
226
+ hasAttachmentContext: boolean
227
+ }): boolean {
228
+ if (!params.hasAttachmentContext) return false
229
+ if (params.hasToolCalls) return false
230
+ const decision = routeTaskIntent(params.userMessage, params.enabledPlugins, null)
231
+ if (decision.intent !== 'research' && decision.intent !== 'browsing') return false
232
+ return decision.preferredTools.some((toolName) => pluginIdMatches(params.enabledPlugins, toolName))
233
+ }
234
+
228
235
  export function buildExternalWalletExecutionBlock(enabledPlugins: string[]): string {
229
236
  const hasExecutionContext = Boolean(
230
237
  getFirstToolForCapability(enabledPlugins, TOOL_CAPABILITY.walletInspect)
@@ -244,213 +251,6 @@ export function buildExternalWalletExecutionBlock(enabledPlugins: string[]): str
244
251
  return lines.join('\n')
245
252
  }
246
253
 
247
- export function shouldForceExternalServiceSummary(params: {
248
- userMessage: string
249
- finalResponse: string
250
- hasToolCalls: boolean
251
- toolEventCount: number
252
- }): boolean {
253
- if (!looksLikeExternalWalletTask(params.userMessage)) return false
254
- if (!params.hasToolCalls || params.toolEventCount === 0) return false
255
- const trimmed = params.finalResponse.trim()
256
- if (!trimmed) return true
257
- if (/\b(blocker|blocked|cannot|can't|requires|need|missing|last reversible step|next step)\b/i.test(trimmed)) return false
258
- if (trimmed.length >= 240 && !/(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed)) return false
259
- return /:$/.test(trimmed) || /(let me|i'll|i will|checking|verify|promising|look into|explore|access their interface)/i.test(trimmed) || trimmed.length < 240
260
- }
261
-
262
- function resolveToolAction(input: unknown): string {
263
- if (input && typeof input === 'object' && !Array.isArray(input)) {
264
- const action = (input as Record<string, unknown>).action
265
- return typeof action === 'string' ? action.trim().toLowerCase() : ''
266
- }
267
- if (typeof input !== 'string') return ''
268
- const trimmed = input.trim()
269
- if (!trimmed.startsWith('{')) return ''
270
- try {
271
- const parsed = JSON.parse(trimmed) as Record<string, unknown>
272
- return typeof parsed.action === 'string' ? parsed.action.trim().toLowerCase() : ''
273
- } catch {
274
- return ''
275
- }
276
- }
277
-
278
- export function shouldTerminateOnSuccessfulMemoryMutation(params: {
279
- toolName: string
280
- toolInput: unknown
281
- toolOutput: string
282
- }): boolean {
283
- const canonicalToolName = canonicalizePluginId(params.toolName) || params.toolName
284
- if (canonicalToolName !== 'memory') return false
285
- const exactToolName = String(params.toolName || '').trim().toLowerCase()
286
- const action = exactToolName === 'memory_store'
287
- ? 'store'
288
- : exactToolName === 'memory_update'
289
- ? 'update'
290
- : resolveToolAction(params.toolInput)
291
- if (action !== 'store' && action !== 'update') return false
292
- const output = extractSuggestions(params.toolOutput || '').clean.trim()
293
- if (!output || /^error[:\s]/i.test(output)) return false
294
- if (!/^(stored|updated) memory\b/i.test(output)) return false
295
- return /no further memory lookup is needed unless the user asked you to verify/i.test(output)
296
- }
297
-
298
- function hasStateChangingWalletEvidence(toolEvents: MessageToolEvent[]): boolean {
299
- return toolEvents.some((event) => {
300
- const input = `${event.input || ''}\n${event.output || ''}`
301
- return event.name === 'wallet_tool' && (
302
- /"action":"send_transaction"/.test(input)
303
- || /"action":"send"/.test(input)
304
- || /"action":"sign_transaction"/.test(input)
305
- || /"type":"plugin_wallet_action_request"/.test(input)
306
- || /"type":"plugin_wallet_transfer_request"/.test(input)
307
- || /"status":"broadcast"/.test(input)
308
- )
309
- })
310
- }
311
-
312
- function countExternalExecutionResearchSteps(toolEvents: MessageToolEvent[]): number {
313
- return toolEvents.filter((event) => {
314
- if (['http_request', 'web', 'web_search', 'web_fetch', 'browser'].includes(event.name)) return true
315
- if (event.name !== 'wallet_tool') return false
316
- return /"action":"(balance|address|transactions|call_contract|encode_contract_call)"/.test(event.input || '')
317
- }).length
318
- }
319
-
320
- function countDistinctExternalResearchHosts(toolEvents: MessageToolEvent[]): number {
321
- const hosts = new Set<string>()
322
- for (const event of toolEvents) {
323
- const candidates = [event.input || '', event.output || '']
324
- for (const candidate of candidates) {
325
- const matches = candidate.match(/https?:\/\/[^"'\\\s)]+/g) || []
326
- for (const match of matches) {
327
- try {
328
- hosts.add(new URL(match).host.toLowerCase())
329
- } catch {
330
- // Ignore malformed URLs in model/tool text.
331
- }
332
- }
333
- }
334
- }
335
- return hosts.size
336
- }
337
-
338
- function getWalletApprovalBoundaryAction(output: string): string | null {
339
- if (!output.includes('plugin_wallet_')) return null
340
- if (/"type":"plugin_wallet_transfer_request"/.test(output)) return 'send'
341
- const actionMatch = output.match(/"action":"([^"]+)"/)
342
- const action = actionMatch?.[1] || ''
343
- if (!action) return null
344
- const readOnlyActions = new Set([
345
- 'balance',
346
- 'address',
347
- 'transactions',
348
- 'encode_contract_call',
349
- 'simulate_transaction',
350
- ])
351
- return readOnlyActions.has(action) ? null : action
352
- }
353
-
354
- export function isWalletSimulationResult(toolName: string, output: string): boolean {
355
- return toolName === 'wallet_tool' && /"status":"simulated"/.test(output)
356
- }
357
-
358
- export function shouldForceExternalExecutionFollowthrough(params: {
359
- userMessage: string
360
- finalResponse: string
361
- hasToolCalls: boolean
362
- toolEvents: MessageToolEvent[]
363
- }): boolean {
364
- if (!looksLikeBoundedExternalExecutionTask(params.userMessage)) return false
365
- if (!params.hasToolCalls || params.toolEvents.length < 4) return false
366
- if (hasStateChangingWalletEvidence(params.toolEvents)) return false
367
- const distinctHosts = countDistinctExternalResearchHosts(params.toolEvents)
368
- const trimmed = params.finalResponse.trim()
369
- if (!trimmed) return countExternalExecutionResearchSteps(params.toolEvents) >= 4 || distinctHosts >= 3
370
- if (/\b(last reversible step|exact blocker|safest next action|blocked|cannot|can't|missing capability|no-key route unavailable)\b/i.test(trimmed)) {
371
- return false
372
- }
373
- if (countExternalExecutionResearchSteps(params.toolEvents) < 4 && distinctHosts < 3) return false
374
- return /(let me|i'll|i will|trying|research|query|check|look|promising|now let me|good -|good,)/i.test(trimmed) || trimmed.length < 500
375
- }
376
-
377
- function looksLikeIncompleteDeliverableResponse(text: string): boolean {
378
- const trimmed = text.trim()
379
- if (!trimmed) return true
380
- if (trimmed.endsWith(':') || trimmed.endsWith('...') || trimmed.endsWith('…')) return true
381
- const lastChunk = trimmed.slice(-400).toLowerCase()
382
- return /\b(?:next|now|then|after that|moving on to|proceeding to)\b[^.!?\n]{0,120}\b(?:i(?:'ll| will)|create|build|write|capture|take|start|finish|generate)\b/.test(lastChunk)
383
- || /\b(?:i(?:'ll| will)|let me)\s+(?:now|next)?\s*(?:create|build|write|capture|take|start|finish|generate|continue)\b/.test(lastChunk)
384
- }
385
-
386
- export function shouldForceDeliverableFollowthrough(params: {
387
- userMessage: string
388
- finalResponse: string
389
- hasToolCalls: boolean
390
- toolEvents: MessageToolEvent[]
391
- }): boolean {
392
- if (!looksLikeOpenEndedDeliverableTask(params.userMessage)) return false
393
- if (!params.hasToolCalls || params.toolEvents.length === 0) return false
394
- const trimmed = params.finalResponse.trim()
395
- if (!trimmed) return params.toolEvents.length >= 2
396
- if (
397
- /\b(task complete|completed|finished|done|delivered|shared|sent|uploaded|attached)\b/i.test(trimmed)
398
- && /(?:\/api\/uploads\/|https?:\/\/|`[^`\n]+\.(?:md|pdf|png|jpe?g|webp|gif|html|txt|zip)`)/i.test(trimmed)
399
- ) {
400
- return false
401
- }
402
- // If the user asked for file output but no file-write tool was used, force continuation
403
- const userNormalized = params.userMessage.toLowerCase()
404
- if (/\b(save|write|output)\b[^.!?\n]{0,60}\b(to|as)\b[^.!?\n]{0,40}(\/|~\/|\.[a-z]{2,5}\b)/.test(userNormalized)) {
405
- // Check if a file-writing tool was actually used (not just file-reading).
406
- // The `files` tool with action: 'read' or 'list' doesn't count as writing.
407
- const usedFileWriteTools = params.toolEvents.some((e) => {
408
- if (!e.name) return false
409
- if (['write_file', 'edit_file'].includes(e.name)) return true
410
- if (e.name === 'shell' || e.name === 'execute_command') return true
411
- if (e.name === 'files') {
412
- // Only count as a write if the tool input specifies action: "write"
413
- const input = e.input || ''
414
- return /"action"\s*:\s*"write"/i.test(input)
415
- }
416
- return false
417
- })
418
- if (!usedFileWriteTools) return true
419
- }
420
- if (looksLikeIncompleteDeliverableResponse(trimmed)) return true
421
- return trimmed.length < 120 && params.toolEvents.length >= 3
422
- }
423
-
424
- function updateStreamedToolEvents(events: MessageToolEvent[], event: { type: 'call' | 'result'; name: string; input?: string; output?: string; toolCallId?: string }) {
425
- if (event.type === 'call') {
426
- events.push({
427
- name: event.name,
428
- input: event.input || '',
429
- toolCallId: event.toolCallId,
430
- })
431
- return
432
- }
433
- const index = event.toolCallId
434
- ? events.findLastIndex((entry) => entry.toolCallId === event.toolCallId && !entry.output)
435
- : events.findLastIndex((entry) => entry.name === event.name && !entry.output)
436
- if (index === -1) return
437
- events[index] = {
438
- ...events[index],
439
- output: event.output || '',
440
- }
441
- }
442
-
443
- function renderToolEvidence(events: MessageToolEvent[]): string {
444
- return events
445
- .slice(-10)
446
- .map((event, index) => [
447
- `Tool ${index + 1}: ${event.name}`,
448
- event.input ? `Input: ${event.input}` : '',
449
- event.output ? `Output: ${event.output.slice(0, 1200)}` : '',
450
- ].filter(Boolean).join('\n'))
451
- .join('\n\n')
452
- }
453
-
454
254
  async function buildForcedExternalServiceSummary(params: {
455
255
  llm: { invoke: (messages: HumanMessage[]) => Promise<{ content: unknown }> }
456
256
  userMessage: string
@@ -491,73 +291,6 @@ async function buildForcedExternalServiceSummary(params: {
491
291
  }
492
292
  }
493
293
 
494
- function buildExternalExecutionFollowthroughPrompt(params: {
495
- userMessage: string
496
- fullText: string
497
- toolEvents: MessageToolEvent[]
498
- }): string {
499
- return [
500
- 'You are in a bounded external execution task and have already done enough research.',
501
- 'Do not restart broad discovery. Do not ask the user for another prompt.',
502
- 'Do not spend this continuation on more venue shopping. Use the already confirmed route unless one last fetch is strictly required to prepare execution.',
503
- 'If several venue or aggregator APIs already failed, stop searching for more venues. Either use a direct onchain read path with the available wallet tools, or state the blocker.',
504
- 'A prose approval request does not count as completion. If the next step is a sign/send/approve action, call the real wallet tool action so the runtime can create the approval request.',
505
- 'Do not mutate already confirmed token addresses, router addresses, spender addresses, or network identifiers unless newer tool evidence proves the earlier value was wrong.',
506
- 'Within this continuation, do exactly one of the following:',
507
- '1. Take the next concrete execution step now using the existing tools and stop at the first approval boundary for a state-changing action.',
508
- '2. If no safe executable step exists with the current tools, state the exact blocker with evidence.',
509
- 'A successful continuation ends with one of these outcomes only: an approval request, a broadcast transaction, or a final blocker summary.',
510
- 'Prefer the route sources and facts already confirmed in the tool evidence below. Do not keep shopping for new venues unless the current options are clearly unusable.',
511
- 'If the tool evidence already includes enough information to prepare a contract call, approval, quote read, or transaction simulation, do that now instead of making another search or HTTP request.',
512
- '',
513
- `Objective:\n${params.userMessage}`,
514
- '',
515
- `Current partial response:\n${params.fullText || '(none)'}`,
516
- '',
517
- `Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
518
- ].join('\n')
519
- }
520
-
521
- function buildDeliverableFollowthroughPrompt(params: {
522
- userMessage: string
523
- fullText: string
524
- toolEvents: MessageToolEvent[]
525
- }): string {
526
- const lines = [
527
- 'You are in the middle of a multi-step deliverable and stopped after only a partial batch of work.',
528
- 'Continue from the existing workspace and artifacts. Do not restart from scratch and do not ask the user to restate the request.',
529
- 'Do not stop after one partial batch. Finish every requested deliverable that is still outstanding before concluding.',
530
- 'If a requested artifact cannot be produced, say exactly which artifact is missing, what blocked it, and what you already completed.',
531
- 'Use the existing files, screenshots, and generated outputs first. Inspect them if needed, then complete the remaining work.',
532
- 'Preserve hard structural constraints from the original request: exact counts stay exact, required titled sections stay present, and source coverage gaps should be filled instead of skipped.',
533
- 'End with a concise grouped completion summary that lists exact file paths, upload URLs, localhost URLs/ports, and screenshots you produced.',
534
- ]
535
-
536
- // If the user explicitly asked for file output, remind the model to use file tools
537
- const userNormalized = params.userMessage.toLowerCase()
538
- const fileOutputMatch = userNormalized.match(/\b(?:save|write|output|export)\b[^.!?\n]{0,80}\b(?:to|as|at|in)\b[^.!?\n]{0,60}(\/[^\s,'"]+|~\/[^\s,'"]+|\.\/[^\s,'"]+)/i)
539
- if (fileOutputMatch) {
540
- const fileToolNames = ['write_file', 'edit_file', 'files', 'shell', 'execute_command']
541
- const usedFileTools = params.toolEvents.some((e) => e.name && fileToolNames.includes(e.name))
542
- if (!usedFileTools) {
543
- lines.push(
544
- '',
545
- `CRITICAL: The user asked you to save output to a file path (${fileOutputMatch[1] || 'see objective'}). You have NOT used any file-writing tool yet.`,
546
- 'You MUST use the `files` or `write_file` tool to write the content to the requested path. Do not just include the content in your text response — actually write the file.',
547
- )
548
- }
549
- }
550
-
551
- lines.push(
552
- '',
553
- `Objective:\n${params.userMessage}`,
554
- '',
555
- `Current partial response:\n${params.fullText || '(none)'}`,
556
- '',
557
- `Recent tool evidence:\n${renderToolEvidence(params.toolEvents) || '(none)'}`,
558
- )
559
- return lines.join('\n')
560
- }
561
294
 
562
295
  function buildExactStructureBlock(userMessage: string): string {
563
296
  const exactBulletMatch = userMessage.match(/\bexactly\s+(\d+)\s+bullet points?\b/i)
@@ -571,18 +304,6 @@ function buildExactStructureBlock(userMessage: string): string {
571
304
  ].join('\n')
572
305
  }
573
306
 
574
- /** Detect whether a user message is a broad, high-level goal that benefits from decomposition. */
575
- function isBroadGoal(text: string): boolean {
576
- if (text.length < 50) return false
577
- // Messages with code fences, file paths, or numbered steps are already structured
578
- if (/```/.test(text)) return false
579
- if (/\/(src|lib|app|pages|components|api)\//.test(text)) return false
580
- if (/^\s*\d+[.)]\s/m.test(text)) return false
581
- // Short direct questions aren't broad goals
582
- if (text.length < 80 && text.endsWith('?')) return false
583
- return true
584
- }
585
-
586
307
  const GOAL_DECOMPOSITION_BLOCK = [
587
308
  '## Goal Decomposition',
588
309
  'When you receive a broad, open-ended goal:',
@@ -602,6 +323,7 @@ function buildAgenticExecutionPolicy(opts: {
602
323
  platformAssignScope?: 'self' | 'all'
603
324
  userMessage?: string
604
325
  history?: Message[]
326
+ hasAttachmentContext?: boolean
605
327
  responseStyle?: 'concise' | 'normal' | 'detailed' | null
606
328
  responseMaxChars?: number | null
607
329
  }) {
@@ -642,6 +364,16 @@ function buildAgenticExecutionPolicy(opts: {
642
364
  parts.push(buildDirectMemoryWriteBlock())
643
365
  }
644
366
 
367
+ if (opts.hasAttachmentContext) {
368
+ parts.push(
369
+ '## Attachments',
370
+ 'User attachments in this thread are part of the available context. Image attachments are directly visible to you, and readable files/PDFs are inlined when available.',
371
+ 'If the user asks you to identify, read, transcribe, compare, or look something up from an attachment, inspect the attachment content first instead of claiming the image/file is unavailable.',
372
+ 'When the task depends on details from an attachment plus outside lookup, extract the identifier from the attachment and then use the enabled tools to continue.',
373
+ 'Do not claim you cannot use images, attachments, or external tools when those capabilities are available in this session. Only report a blocker after a real attempt or when the attachment is genuinely unreadable.',
374
+ )
375
+ }
376
+
645
377
  // Plugin-specific operating guidance (collected dynamically from plugins)
646
378
  const guidanceLines = getPluginManager().collectOperatingGuidance(opts.enabledPlugins)
647
379
  if (guidanceLines.length) parts.push(...guidanceLines)
@@ -683,12 +415,6 @@ function buildAgenticExecutionPolicy(opts: {
683
415
  return parts.filter(Boolean).join('\n')
684
416
  }
685
417
 
686
- function compactThreadRecallText(text: string, maxChars = 180): string {
687
- const compact = extractSuggestions(text || '').clean.replace(/\s+/g, ' ').trim()
688
- if (!compact) return ''
689
- return compact.length > maxChars ? `${compact.slice(0, maxChars - 3)}...` : compact
690
- }
691
-
692
418
  function buildCurrentThreadRecallBlock(history: Message[]): string {
693
419
  const recentUserFacts = history
694
420
  .filter((entry) => entry.role === 'user' && typeof entry.text === 'string' && entry.text.trim())
@@ -735,41 +461,6 @@ function buildDirectMemoryWriteBlock(): string {
735
461
  ].join('\n')
736
462
  }
737
463
 
738
- const DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE = /\b(?:then|and then|after that)?\s*(?:confirm|recap|repeat|summarize|tell me|say)\b[\s\S]{0,120}\b(?:stored|saved|updated|remembered|wrote|write)\b/i
739
- const DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE = /\b(?:then|and then|after that|also)\b[\s\S]{0,160}\b(?:write|create|send|email|message|delegate|research|search|browse|open|edit|build|schedule|plan|review|analy[sz]e)\b/i
740
-
741
- export function isNarrowDirectMemoryWriteTurn(message: string): boolean {
742
- const trimmed = String(message || '').trim()
743
- if (!trimmed || !isDirectMemoryWriteRequest(trimmed)) return false
744
- if (looksLikeOpenEndedDeliverableTask(trimmed)) return false
745
- if (DIRECT_MEMORY_WRITE_EXTRA_ACTION_RE.test(trimmed) && !DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed)) {
746
- return false
747
- }
748
- return !isBroadGoal(trimmed) || DIRECT_MEMORY_WRITE_CONFIRMATION_ONLY_RE.test(trimmed) || !/[?]$/.test(trimmed)
749
- }
750
-
751
- const CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS = new Set([
752
- 'memory',
753
- 'manage_sessions',
754
- 'web',
755
- 'context_mgmt',
756
- ])
757
-
758
- export function shouldAllowToolForCurrentThreadRecall(toolName: string): boolean {
759
- const canonicalToolName = canonicalizePluginId(toolName) || toolName.trim().toLowerCase()
760
- return !CURRENT_THREAD_RECALL_BLOCKED_TOOL_IDS.has(canonicalToolName)
761
- }
762
-
763
- const DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS = new Set([
764
- 'memory_store',
765
- 'memory_update',
766
- ])
767
-
768
- export function shouldAllowToolForDirectMemoryWrite(toolName: string): boolean {
769
- const rawToolName = toolName.trim().toLowerCase()
770
- return DIRECT_MEMORY_WRITE_ALLOWED_TOOL_IDS.has(rawToolName)
771
- }
772
-
773
464
  export interface StreamAgentChatResult {
774
465
  /** All text accumulated across every LLM turn (for SSE / web UI history). */
775
466
  fullText: string
@@ -778,53 +469,20 @@ export interface StreamAgentChatResult {
778
469
  finalResponse: string
779
470
  }
780
471
 
781
- function resolveToolOnlyFinalResponse(toolEvents: MessageToolEvent[] | undefined): string {
782
- const events = Array.isArray(toolEvents) ? toolEvents : []
783
- for (let index = events.length - 1; index >= 0; index--) {
784
- const event = events[index]
785
- const output = typeof event?.output === 'string'
786
- ? extractSuggestions(event.output).clean.trim()
787
- : ''
788
- if (!output) continue
789
- if (/^error[:\s]/i.test(output)) continue
790
- if (output.startsWith('{') || output.startsWith('[')) continue
791
- return output
792
- }
793
- return ''
794
- }
472
+ type StreamAgentChatHandler = (opts: StreamAgentChatOpts) => Promise<StreamAgentChatResult>
795
473
 
796
- export function resolveFinalStreamResponseText(params: {
797
- fullText: string
798
- lastSegment: string
799
- lastSettledSegment: string
800
- hasToolCalls: boolean
801
- toolEvents?: MessageToolEvent[]
802
- }): string {
803
- const fullText = params.fullText || ''
804
- if (!params.hasToolCalls) return fullText
805
-
806
- const candidates = [
807
- extractSuggestions(params.lastSegment || '').clean.trim(),
808
- extractSuggestions(params.lastSettledSegment || '').clean.trim(),
809
- extractSuggestions(fullText).clean.trim(),
810
- resolveToolOnlyFinalResponse(params.toolEvents),
811
- ]
474
+ let streamAgentChatOverride: StreamAgentChatHandler | null = null
812
475
 
813
- return candidates.find((candidate) => candidate.length > 0) || ''
476
+ export function setStreamAgentChatForTest(handler: StreamAgentChatHandler | null): void {
477
+ streamAgentChatOverride = handler
814
478
  }
815
479
 
816
- export function resolveContinuationAssistantText(params: {
817
- iterationText: string
818
- lastSegment: string
819
- }): string {
820
- const candidates = [
821
- extractSuggestions(params.iterationText || '').clean.trim(),
822
- extractSuggestions(params.lastSegment || '').clean.trim(),
823
- ]
824
- return candidates.find((candidate) => candidate.length > 0) || ''
480
+ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
481
+ if (streamAgentChatOverride) return streamAgentChatOverride(opts)
482
+ return streamAgentChatCore(opts)
825
483
  }
826
484
 
827
- export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
485
+ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAgentChatResult> {
828
486
  const startTs = Date.now()
829
487
  const { session, message, imagePath, attachedFiles, apiKey, systemPrompt, write, history, fallbackCredentialIds, signal } = opts
830
488
  const rawPlugins = Array.isArray(session.plugins) ? session.plugins : []
@@ -875,6 +533,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
875
533
  const hasProvidedSystemPrompt = typeof systemPrompt === 'string' && systemPrompt.trim().length > 0
876
534
  const directMemoryWriteOnlyTurn = isNarrowDirectMemoryWriteTurn(message)
877
535
  const currentThreadRecallRequest = !directMemoryWriteOnlyTurn && isCurrentThreadRecallRequest(message)
536
+ const hasAttachmentContext = Boolean(
537
+ imagePath
538
+ || attachedFiles?.length
539
+ || history.some((entry) => entry.imagePath || entry.imageUrl || (Array.isArray(entry.attachedFiles) && entry.attachedFiles.length > 0)),
540
+ )
878
541
 
879
542
  if (hasProvidedSystemPrompt) {
880
543
  stateModifierParts.push(systemPrompt!.trim())
@@ -1069,6 +732,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1069
732
  platformAssignScope: agentPlatformAssignScope,
1070
733
  userMessage: message,
1071
734
  history,
735
+ hasAttachmentContext,
1072
736
  responseStyle: agentResponseStyle,
1073
737
  responseMaxChars: agentResponseMaxChars,
1074
738
  }),
@@ -1076,6 +740,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1076
740
 
1077
741
  let stateModifier = stateModifierParts.join('\n\n')
1078
742
 
743
+ const endToolBuildPerf = perf.start('stream-agent-chat', 'buildSessionTools', { sessionId: session.id })
1079
744
  const { tools, cleanup, toolToPluginMap } = await buildSessionTools(session.cwd, sessionPlugins, {
1080
745
  agentId: session.agentId,
1081
746
  sessionId: session.id,
@@ -1088,6 +753,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1088
753
  projectDescription: activeProjectContext.project?.description || null,
1089
754
  memoryScopeMode: agentMemoryScopeMode,
1090
755
  })
756
+ endToolBuildPerf({ toolCount: tools.length })
1091
757
  const toolsForTurn = currentThreadRecallRequest
1092
758
  ? tools.filter((tool) => {
1093
759
  const toolName = typeof (tool as { name?: unknown }).name === 'string'
@@ -1253,7 +919,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1253
919
  const langchainMessages: Array<HumanMessage | AIMessage> = []
1254
920
  for (const m of effectiveHistory) {
1255
921
  if (m.role === 'user') {
1256
- langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
922
+ const resolvedImg = resolveImagePath(m.imagePath, m.imageUrl)
923
+ langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, resolvedImg ?? undefined, m.attachedFiles) }))
1257
924
  } else {
1258
925
  langchainMessages.push(new AIMessage({ content: m.text }))
1259
926
  }
@@ -1298,12 +965,14 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1298
965
  const MAX_TRANSIENT_RETRIES = 2
1299
966
  const MAX_REQUIRED_TOOL_CONTINUES = 2
1300
967
  const MAX_EXECUTION_FOLLOWTHROUGHS = 1
968
+ const MAX_ATTACHMENT_FOLLOWTHROUGHS = 1
1301
969
  const MAX_DELIVERABLE_FOLLOWTHROUGHS = 2
1302
970
  const MAX_TOOL_SUMMARY_RETRIES = 2
1303
971
  let autoContinueCount = 0
1304
972
  let transientRetryCount = 0
1305
973
  let requiredToolContinueCount = 0
1306
974
  let executionFollowthroughCount = 0
975
+ let attachmentFollowthroughCount = 0
1307
976
  let deliverableFollowthroughCount = 0
1308
977
  let toolSummaryRetryCount = 0
1309
978
  const explicitRequiredToolNames = getExplicitRequiredToolNames(message, sessionPlugins)
@@ -1315,7 +984,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1315
984
  try {
1316
985
  const maxIterations = MAX_AUTO_CONTINUES + MAX_TRANSIENT_RETRIES + MAX_REQUIRED_TOOL_CONTINUES + MAX_EXECUTION_FOLLOWTHROUGHS + MAX_DELIVERABLE_FOLLOWTHROUGHS + MAX_TOOL_SUMMARY_RETRIES
1317
986
  for (let iteration = 0; iteration <= maxIterations; iteration++) {
1318
- let shouldContinue: 'recursion' | 'transient' | 'required_tool' | 'execution_followthrough' | 'deliverable_followthrough' | 'tool_summary' | false = false
987
+ let shouldContinue: ContinuationType = false
1319
988
  let requiredToolReminderNames: string[] = []
1320
989
  let waitingForToolResult = false
1321
990
  let idleTimedOut = false
@@ -1370,6 +1039,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1370
1039
  // to suppress the duplicate nested events.
1371
1040
  const acceptedToolRunIds = new Set<string>()
1372
1041
  const seenToolInputKeys = new Set<string>()
1042
+ const toolPerfEnds = new Map<string, (extra?: Record<string, unknown>) => number>()
1373
1043
 
1374
1044
  try {
1375
1045
  armIdleWatchdog()
@@ -1453,6 +1123,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1453
1123
  }
1454
1124
  seenToolInputKeys.add(toolDedupeKey)
1455
1125
  acceptedToolRunIds.add(event.run_id)
1126
+ toolPerfEnds.set(event.run_id, perf.start('tool-call', toolName, { sessionId: session.id }))
1456
1127
 
1457
1128
  clearIdleWatchdog()
1458
1129
  waitingForToolResult = true
@@ -1490,6 +1161,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1490
1161
  // Dedup: skip on_tool_end for run_ids we didn't accept in on_tool_start
1491
1162
  if (!acceptedToolRunIds.has(event.run_id)) continue
1492
1163
  acceptedToolRunIds.delete(event.run_id)
1164
+ const endToolPerf = toolPerfEnds.get(event.run_id)
1165
+ toolPerfEnds.delete(event.run_id)
1493
1166
 
1494
1167
  waitingForToolResult = false
1495
1168
  armIdleWatchdog()
@@ -1550,6 +1223,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1550
1223
  }
1551
1224
  }
1552
1225
 
1226
+ endToolPerf?.({ outputLen: outputStr?.length || 0 })
1553
1227
  write(`data: ${JSON.stringify({
1554
1228
  t: 'tool_result',
1555
1229
  toolName,
@@ -1622,7 +1296,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1622
1296
  const errName = innerErr instanceof Error ? innerErr.constructor.name : ''
1623
1297
  const errMsg = idleTimedOut
1624
1298
  ? 'Model stream stalled without emitting text or tool results for 90 seconds.'
1625
- : innerErr instanceof Error ? innerErr.message : String(innerErr)
1299
+ : errorMessage(innerErr)
1626
1300
  const errStack = innerErr instanceof Error ? innerErr.stack?.slice(0, 500) : undefined
1627
1301
 
1628
1302
  // Classify the error:
@@ -1735,6 +1409,25 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1735
1409
  }
1736
1410
  }
1737
1411
 
1412
+ if (!shouldContinue
1413
+ && attachmentFollowthroughCount < MAX_ATTACHMENT_FOLLOWTHROUGHS
1414
+ && shouldForceAttachmentFollowthrough({
1415
+ userMessage: message,
1416
+ enabledPlugins: sessionPlugins,
1417
+ hasToolCalls,
1418
+ hasAttachmentContext,
1419
+ })) {
1420
+ shouldContinue = 'attachment_followthrough'
1421
+ attachmentFollowthroughCount++
1422
+ write(`data: ${JSON.stringify({
1423
+ t: 'status',
1424
+ text: JSON.stringify({
1425
+ attachmentFollowthrough: attachmentFollowthroughCount,
1426
+ maxFollowthroughs: MAX_ATTACHMENT_FOLLOWTHROUGHS,
1427
+ }),
1428
+ })}\n\n`)
1429
+ }
1430
+
1738
1431
  if (!shouldContinue
1739
1432
  && executionFollowthroughCount < MAX_EXECUTION_FOLLOWTHROUGHS
1740
1433
  && shouldForceExternalExecutionFollowthrough({
@@ -1817,88 +1510,31 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
1817
1510
  lastSegment,
1818
1511
  })
1819
1512
 
1820
- if (shouldContinue === 'recursion') {
1821
- // Continue with only the newly produced assistant text from this
1822
- // iteration, not the cumulative full transcript, or the model tends to
1823
- // restart from earlier paragraphs on later followthrough turns.
1824
- if (continuationAssistantText) {
1825
- langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
1826
- }
1827
- const settledSegment = extractSuggestions(lastSegment).clean.trim()
1828
- if (settledSegment) lastSettledSegment = settledSegment
1829
- langchainMessages.push(new HumanMessage({ content: 'Continue where you left off. Complete the remaining steps of the objective.' }))
1830
- lastSegment = ''
1831
- } else if (shouldContinue === 'required_tool') {
1832
- if (continuationAssistantText) {
1833
- langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
1834
- }
1835
- const settledSegment = extractSuggestions(lastSegment).clean.trim()
1836
- if (settledSegment) lastSettledSegment = settledSegment
1837
- langchainMessages.push(new HumanMessage({
1838
- content: `You have not yet completed the required explicit tool step(s): ${requiredToolReminderNames.join(', ')}. Use those enabled tools now before declaring success. Do not replace ask_human with a plain-text request, do not replace outbound delivery tools with prose, and do not replace screenshot requests with text-only summaries.`,
1839
- }))
1840
- lastSegment = ''
1841
- } else if (shouldContinue === 'execution_followthrough') {
1842
- if (continuationAssistantText) {
1843
- langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
1844
- }
1845
- const settledSegment = extractSuggestions(lastSegment).clean.trim()
1846
- if (settledSegment) lastSettledSegment = settledSegment
1847
- langchainMessages.push(new HumanMessage({
1848
- content: buildExternalExecutionFollowthroughPrompt({
1849
- userMessage: message,
1850
- fullText,
1851
- toolEvents: streamedToolEvents,
1852
- }),
1853
- }))
1854
- lastSegment = ''
1855
- } else if (shouldContinue === 'deliverable_followthrough') {
1513
+ const continuationPrompt = buildContinuationPrompt({
1514
+ type: shouldContinue,
1515
+ message,
1516
+ fullText,
1517
+ toolEvents: streamedToolEvents,
1518
+ requiredToolReminderNames,
1519
+ })
1520
+
1521
+ if (continuationPrompt) {
1856
1522
  if (continuationAssistantText) {
1857
1523
  langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
1858
1524
  }
1859
1525
  const settledSegment = extractSuggestions(lastSegment).clean.trim()
1860
1526
  if (settledSegment) lastSettledSegment = settledSegment
1861
- langchainMessages.push(new HumanMessage({
1862
- content: buildDeliverableFollowthroughPrompt({
1863
- userMessage: message,
1864
- fullText,
1865
- toolEvents: streamedToolEvents,
1866
- }),
1867
- }))
1868
- lastSegment = ''
1869
- } else if (shouldContinue === 'tool_summary') {
1870
- // Model called tools but produced no/trivial text — prompt it to synthesize results.
1871
- if (continuationAssistantText) {
1872
- langchainMessages.push(new AIMessage({ content: continuationAssistantText }))
1873
- }
1874
- const toolSummaryLines = streamedToolEvents
1875
- .filter((e) => e.output)
1876
- .map((e) => `[${e.name}]: ${(e.output || '').slice(0, 500)}`)
1877
- .slice(0, 6)
1878
- const preambleNote = fullText.trim()
1879
- ? `You started with "${fullText.trim().slice(0, 100)}..." but did not follow through with actual results.`
1880
- : 'Your tool calls completed but you did not provide a response.'
1881
- langchainMessages.push(new HumanMessage({
1882
- content: [
1883
- preambleNote,
1884
- 'Here are the tool results:',
1885
- ...toolSummaryLines,
1886
- '',
1887
- `Original request: ${message.slice(0, 500)}`,
1888
- '',
1889
- 'Now answer the original request using these tool results. Be concise and direct. Present the findings clearly.',
1890
- ].join('\n'),
1891
- }))
1527
+ langchainMessages.push(new HumanMessage({ content: continuationPrompt }))
1892
1528
  lastSegment = ''
1893
1529
  } else if (shouldContinue === 'transient') {
1894
1530
  // Short delay before retrying transient errors (API timeout, rate limit, etc.)
1895
- await new Promise((r) => setTimeout(r, 2000 * transientRetryCount))
1531
+ await sleep(2000 * transientRetryCount)
1896
1532
  }
1897
1533
  }
1898
1534
  } catch (err: unknown) {
1899
1535
  const errMsg = timedOut
1900
1536
  ? 'Ongoing loop stopped after reaching the configured runtime limit.'
1901
- : err instanceof Error ? err.message : String(err)
1537
+ : errorMessage(err)
1902
1538
  const heartbeatEligible = runtime.loopMode === 'ongoing' || session.heartbeatEnabled === true || agentHeartbeatEnabled
1903
1539
  const budgetLimited = timedOut || /recursion limit|maximum recursion/i.test(errMsg)
1904
1540
  if (heartbeatEligible && budgetLimited) {