@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
@@ -1,21 +1,135 @@
1
1
  'use client'
2
2
 
3
- import { useMemo, useState } from 'react'
3
+ import { memo, useMemo, useState } from 'react'
4
4
  import { AiAvatar } from '@/components/shared/avatar'
5
5
  import { AgentAvatar } from '@/components/agents/agent-avatar'
6
- import { ToolCallBubble, extractMedia } from './tool-call-bubble'
6
+ import { ToolCallBubble, extractMedia, getInputPreview, getToolLabel } from './tool-call-bubble'
7
7
  import { ActivityMoment, isNotableTool } from './activity-moment'
8
8
  import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
9
9
  import { isStructuredMarkdown } from './markdown-utils'
10
10
 
11
- function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
11
+ function summarizeToolResult(event: ToolEvent): string | null {
12
+ if (!event.output) return null
13
+
14
+ const media = extractMedia(event.output)
15
+ const parts: string[] = []
16
+
17
+ if (media.images.length > 0) parts.push(`${media.images.length} image${media.images.length === 1 ? '' : 's'}`)
18
+ if (media.videos.length > 0) parts.push(`${media.videos.length} video${media.videos.length === 1 ? '' : 's'}`)
19
+ if (media.pdfs.length > 0) parts.push(`${media.pdfs.length} PDF${media.pdfs.length === 1 ? '' : 's'}`)
20
+ if (media.files.length > 0) parts.push(`${media.files.length} file${media.files.length === 1 ? '' : 's'}`)
21
+ if (parts.length > 0) return parts.join(' · ')
22
+
23
+ const clean = media.cleanText.replace(/\s+/g, ' ').trim()
24
+ if (!clean) return null
25
+ return clean.length > 120 ? `${clean.slice(0, 120)}...` : clean
26
+ }
27
+
28
+ const ToolStatusPill = memo(function ToolStatusPill({ status }: { status: ToolEvent['status'] }) {
29
+ const tone = status === 'running'
30
+ ? 'border-white/[0.08] bg-white/[0.05] text-text-3'
31
+ : status === 'error'
32
+ ? 'border-rose-500/25 bg-rose-500/10 text-rose-300'
33
+ : 'border-emerald-500/25 bg-emerald-500/10 text-emerald-300'
34
+
35
+ const label = status === 'running' ? 'Running' : status === 'error' ? 'Failed' : 'Done'
36
+
37
+ return (
38
+ <span className={`rounded-full border px-2 py-0.5 text-[10px] font-600 uppercase tracking-[0.08em] ${tone}`}>
39
+ {label}
40
+ </span>
41
+ )
42
+ })
43
+
44
+ const ToolSummaryRow = memo(function ToolSummaryRow({ event, caption }: { event: ToolEvent; caption: string }) {
45
+ const isRunning = event.status === 'running'
46
+ const isError = event.status === 'error'
47
+ const label = useMemo(() => getToolLabel(event.name, event.input), [event.input, event.name])
48
+ const inputPreview = useMemo(() => getInputPreview(event.name, event.input), [event.input, event.name])
49
+ const resultPreview = useMemo(() => summarizeToolResult(event), [event])
50
+ const color = isError ? '#F43F5E' : isRunning ? '#F59E0B' : '#22C55E'
51
+
52
+ return (
53
+ <div
54
+ className={`rounded-[14px] border px-3.5 py-3 ${
55
+ isRunning
56
+ ? 'border-amber-500/20 bg-amber-500/[0.06]'
57
+ : isError
58
+ ? 'border-rose-500/18 bg-rose-500/[0.05]'
59
+ : 'border-white/[0.06] bg-white/[0.03]'
60
+ }`}
61
+ data-testid="tool-call-row"
62
+ data-tool-name={event.name}
63
+ data-tool-status={event.status}
64
+ >
65
+ <div className="flex items-start gap-3">
66
+ <div className="mt-0.5 shrink-0">
67
+ {isRunning ? (
68
+ <span className="block w-3.5 h-3.5 rounded-full border-2 border-current animate-spin" style={{ color, borderTopColor: 'transparent' }} />
69
+ ) : isError ? (
70
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
71
+ <line x1="18" y1="6" x2="6" y2="18" />
72
+ <line x1="6" y1="6" x2="18" y2="18" />
73
+ </svg>
74
+ ) : (
75
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round">
76
+ <polyline points="20 6 9 17 4 12" />
77
+ </svg>
78
+ )}
79
+ </div>
80
+ <div className="min-w-0 flex-1">
81
+ <div className="flex flex-wrap items-center gap-2">
82
+ <span className="text-[10px] font-600 uppercase tracking-[0.08em] text-text-3/55">{caption}</span>
83
+ <span className="text-[13px] font-600 text-text-2">{label}</span>
84
+ <ToolStatusPill status={event.status} />
85
+ </div>
86
+ {inputPreview && (
87
+ <div className="mt-1 text-[12px] font-mono text-text-2 truncate">
88
+ {inputPreview}
89
+ </div>
90
+ )}
91
+ {resultPreview && !isRunning && (
92
+ <div className={`mt-1 text-[12px] ${isError ? 'text-rose-200/80' : 'text-text-3/70'}`}>
93
+ {resultPreview}
94
+ </div>
95
+ )}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ )
100
+ })
101
+
102
+ const ToolEventsSection = memo(function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
12
103
  const [expanded, setExpanded] = useState(false)
13
- const shouldCollapse = toolEvents.length > 2
14
- const latestTool = toolEvents[toolEvents.length - 1]
104
+ const summary = useMemo(() => {
105
+ let running = 0
106
+ let done = 0
107
+ let error = 0
108
+ for (const event of toolEvents) {
109
+ if (event.status === 'running') running += 1
110
+ else if (event.status === 'error') error += 1
111
+ else done += 1
112
+ }
113
+ return { total: toolEvents.length, running, done, error }
114
+ }, [toolEvents])
115
+
116
+ const spotlightEvent = useMemo(() => {
117
+ for (let i = toolEvents.length - 1; i >= 0; i--) {
118
+ if (toolEvents[i].status === 'running') return toolEvents[i]
119
+ }
120
+ return toolEvents[toolEvents.length - 1] || null
121
+ }, [toolEvents])
122
+
123
+ const secondaryEvents = useMemo(() => {
124
+ if (!spotlightEvent) return []
125
+ return toolEvents
126
+ .filter((event) => event.id !== spotlightEvent.id)
127
+ .slice(-2)
128
+ .reverse()
129
+ }, [spotlightEvent, toolEvents])
15
130
 
16
- // When collapsed, collect deduplicated media from all tool events so files remain visible
17
131
  const collapsedMedia = useMemo(() => {
18
- if (!shouldCollapse || expanded) return null
132
+ if (expanded) return null
19
133
  const seen = new Set<string>()
20
134
  const images: string[] = []
21
135
  const videos: string[] = []
@@ -23,41 +137,97 @@ function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
23
137
  const files: { name: string; url: string }[] = []
24
138
  for (const ev of toolEvents) {
25
139
  if (!ev.output) continue
26
- const m = extractMedia(ev.output)
27
- for (const url of m.images) { if (!seen.has(url)) { seen.add(url); images.push(url) } }
28
- for (const url of m.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
29
- for (const p of m.pdfs) { if (!seen.has(p.url)) { seen.add(p.url); pdfs.push(p) } }
30
- for (const f of m.files) { if (!seen.has(f.url)) { seen.add(f.url); files.push(f) } }
140
+ const media = extractMedia(ev.output)
141
+ for (const url of media.images) { if (!seen.has(url)) { seen.add(url); images.push(url) } }
142
+ for (const url of media.videos) { if (!seen.has(url)) { seen.add(url); videos.push(url) } }
143
+ for (const pdf of media.pdfs) { if (!seen.has(pdf.url)) { seen.add(pdf.url); pdfs.push(pdf) } }
144
+ for (const file of media.files) { if (!seen.has(file.url)) { seen.add(file.url); files.push(file) } }
31
145
  }
32
146
  if (!images.length && !videos.length && !pdfs.length && !files.length) return null
33
147
  return { images, videos, pdfs, files }
34
- }, [toolEvents, shouldCollapse, expanded])
35
-
36
- if (shouldCollapse && !expanded) {
37
- return (
38
- <div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
39
- <button
40
- type="button"
41
- onClick={() => setExpanded(true)}
42
- className="self-start flex items-center gap-2 px-3 py-1.5 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] border border-white/[0.06] cursor-pointer transition-colors"
43
- >
44
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/60">
45
- <polyline points="6 9 12 15 18 9" />
46
- </svg>
47
- <span className="text-[11px] text-text-3 font-mono">
48
- {toolEvents.length} tool calls
49
- </span>
50
- <span className="text-[10px] text-text-3/50">
51
- latest: {latestTool?.name || 'unknown'}
52
- </span>
53
- </button>
148
+ }, [expanded, toolEvents])
149
+
150
+ return (
151
+ <div className="max-w-[85%] md:max-w-[72%] mb-2" data-testid="tool-activity">
152
+ <div className="rounded-[16px] border border-white/[0.08] bg-surface/72 backdrop-blur-sm overflow-hidden">
153
+ <div className="px-4 py-3.5">
154
+ <div className="flex flex-wrap items-start gap-3 justify-between">
155
+ <div>
156
+ <div className="text-[11px] font-600 uppercase tracking-[0.08em] text-text-3/55">
157
+ Tool Activity
158
+ </div>
159
+ <div className="mt-1 text-[13px] text-text-2">
160
+ {summary.total} call{summary.total === 1 ? '' : 's'} in this response
161
+ </div>
162
+ </div>
163
+ <div className="flex flex-wrap items-center gap-1.5">
164
+ {summary.running > 0 && (
165
+ <span className="rounded-full border border-amber-500/25 bg-amber-500/10 px-2 py-0.5 text-[10px] font-600 uppercase tracking-[0.08em] text-amber-300">
166
+ {summary.running} running
167
+ </span>
168
+ )}
169
+ {summary.done > 0 && (
170
+ <span className="rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-600 uppercase tracking-[0.08em] text-emerald-300">
171
+ {summary.done} done
172
+ </span>
173
+ )}
174
+ {summary.error > 0 && (
175
+ <span className="rounded-full border border-rose-500/25 bg-rose-500/10 px-2 py-0.5 text-[10px] font-600 uppercase tracking-[0.08em] text-rose-300">
176
+ {summary.error} failed
177
+ </span>
178
+ )}
179
+ </div>
180
+ </div>
181
+
182
+ {spotlightEvent && (
183
+ <div className="mt-3">
184
+ <ToolSummaryRow
185
+ event={spotlightEvent}
186
+ caption={spotlightEvent.status === 'running' ? 'Current step' : 'Latest step'}
187
+ />
188
+ </div>
189
+ )}
190
+
191
+ {secondaryEvents.length > 0 && (
192
+ <div className="mt-2 flex flex-col gap-2">
193
+ {secondaryEvents.map((event) => (
194
+ <ToolSummaryRow key={`summary-${event.id}`} event={event} caption="Recent step" />
195
+ ))}
196
+ </div>
197
+ )}
198
+
199
+ <div className="mt-3 flex flex-wrap items-center gap-2">
200
+ <button
201
+ type="button"
202
+ onClick={() => setExpanded((value) => !value)}
203
+ data-testid="tool-activity-toggle"
204
+ className="inline-flex items-center gap-2 rounded-[10px] border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-[11px] font-600 text-text-2 hover:bg-white/[0.06] cursor-pointer transition-colors"
205
+ >
206
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={`transition-transform ${expanded ? 'rotate-180' : ''}`}>
207
+ <polyline points="6 9 12 15 18 9" />
208
+ </svg>
209
+ {expanded ? 'Hide call details' : `View ${summary.total === 1 ? 'call' : `${summary.total} calls`} details`}
210
+ </button>
211
+ {summary.running > 0 && (
212
+ <span className="text-[11px] text-text-3/55">
213
+ Updates stream here without reflowing the whole thread
214
+ </span>
215
+ )}
216
+ </div>
217
+ </div>
218
+
54
219
  {collapsedMedia && (
55
- <>
220
+ <div className="px-4 pb-4 flex flex-col gap-2">
56
221
  {collapsedMedia.images.map((src, i) => (
57
222
  // eslint-disable-next-line @next/next/no-img-element
58
- <img key={`ci-${i}`} src={src} alt={`Screenshot ${i + 1}`} loading="lazy"
223
+ <img
224
+ key={`ci-${i}`}
225
+ src={src}
226
+ alt={`Screenshot ${i + 1}`}
227
+ loading="lazy"
59
228
  className="max-w-[400px] rounded-[10px] border border-white/10"
60
- onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
229
+ onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
230
+ />
61
231
  ))}
62
232
  {collapsedMedia.videos.map((src, i) => (
63
233
  <video key={`cv-${i}`} src={src} controls playsInline preload="none" className="max-w-full rounded-[10px] border border-white/10" />
@@ -76,29 +246,20 @@ function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
76
246
  {file.name}
77
247
  </a>
78
248
  ))}
79
- </>
249
+ </div>
80
250
  )}
81
- </div>
82
- )
83
- }
84
251
 
85
- return (
86
- <div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mb-2">
87
- {shouldCollapse && (
88
- <button
89
- type="button"
90
- onClick={() => setExpanded(false)}
91
- className="self-start px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-[11px] text-text-3 border border-white/[0.06] cursor-pointer transition-colors"
92
- >
93
- Collapse tool calls
94
- </button>
95
- )}
96
- {toolEvents.map((event) => (
97
- <ToolCallBubble key={event.id} event={event} />
98
- ))}
252
+ {expanded && (
253
+ <div className="border-t border-white/[0.06] px-3.5 pb-3 pt-3 flex flex-col gap-2">
254
+ {toolEvents.map((event) => (
255
+ <ToolCallBubble key={event.id} event={event} />
256
+ ))}
257
+ </div>
258
+ )}
259
+ </div>
99
260
  </div>
100
261
  )
101
- }
262
+ })
102
263
 
103
264
  interface Props {
104
265
  text: string
@@ -108,17 +269,15 @@ interface Props {
108
269
  agentName?: string
109
270
  }
110
271
 
111
- export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
272
+ export const StreamingBubble = memo(function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAvatarUrl, agentName }: Props) {
112
273
  const toolEvents = useChatStore((s) => s.toolEvents)
113
274
  const streamPhase = useChatStore((s) => s.streamPhase)
114
275
  const streamToolName = useChatStore((s) => s.streamToolName)
115
276
  const thinkingText = useChatStore((s) => s.thinkingText)
116
277
  const wide = useMemo(() => isStructuredMarkdown(text), [text])
117
278
 
118
- // Track which activity moments have been dismissed
119
279
  const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set())
120
280
 
121
- // Find the latest completed notable tool event that hasn't been dismissed
122
281
  let currentMoment: { id: string; name: string; input: string } | null = null
123
282
  for (let i = toolEvents.length - 1; i >= 0; i--) {
124
283
  const event = toolEvents[i]
@@ -156,7 +315,6 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAva
156
315
  )}
157
316
  </div>
158
317
 
159
- {/* Collapsed thinking section (shown when text has started but thinking exists) */}
160
318
  {text && thinkingText && (
161
319
  <div className="max-w-[85%] md:max-w-[72%] mb-2">
162
320
  <details className="group rounded-[12px] border border-purple-500/15 bg-purple-500/[0.04]">
@@ -176,7 +334,6 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAva
176
334
  </div>
177
335
  )}
178
336
 
179
- {/* Tool call events (collapsible when > 2) */}
180
337
  {toolEvents.length > 0 && (
181
338
  <ToolEventsSection toolEvents={toolEvents} />
182
339
  )}
@@ -190,4 +347,4 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAva
190
347
  )}
191
348
  </div>
192
349
  )
193
- }
350
+ })
@@ -0,0 +1,274 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+ import { parseSwarmOutput, parseSwarmStatusOutput } from './swarm-panel'
4
+
5
+ describe('parseSwarmOutput', () => {
6
+ it('returns null for non-spawn_subagent tools', () => {
7
+ assert.equal(parseSwarmOutput('execute_command', '{}'), null)
8
+ assert.equal(parseSwarmOutput('delegate_to_agent', '{}'), null)
9
+ })
10
+
11
+ it('returns null for invalid JSON', () => {
12
+ assert.equal(parseSwarmOutput('spawn_subagent', 'not json'), null)
13
+ })
14
+
15
+ it('returns null for unrecognized output shapes', () => {
16
+ assert.equal(parseSwarmOutput('spawn_subagent', '{"foo":"bar"}'), null)
17
+ })
18
+
19
+ it('parses batch running output', () => {
20
+ const output = JSON.stringify({
21
+ action: 'batch',
22
+ status: 'running',
23
+ jobIds: ['job-1', 'job-2', 'job-3'],
24
+ taskCount: 3,
25
+ })
26
+
27
+ const result = parseSwarmOutput('spawn_subagent', output)
28
+ assert.ok(result)
29
+ assert.equal(result.kind, 'batch')
30
+ assert.equal(result.status, 'running')
31
+ assert.equal(result.agents.length, 3)
32
+ assert.deepEqual(result.jobIds, ['job-1', 'job-2', 'job-3'])
33
+ assert.equal(result.agents[0].status, 'running')
34
+ assert.equal(result.agents[0].agentName, 'Agent 1')
35
+ })
36
+
37
+ it('parses batch completed output', () => {
38
+ const output = JSON.stringify({
39
+ action: 'batch',
40
+ status: 'completed',
41
+ jobIds: ['job-1', 'job-2'],
42
+ completed: 1,
43
+ failed: 1,
44
+ cancelled: 0,
45
+ timedOut: 0,
46
+ totalDurationMs: 5000,
47
+ results: [
48
+ { jobId: 'job-1', agentName: 'Research Agent', status: 'completed', response: 'Found results' },
49
+ { jobId: 'job-2', agentName: 'Code Agent', status: 'failed', error: 'Compilation error' },
50
+ ],
51
+ })
52
+
53
+ const result = parseSwarmOutput('spawn_subagent', output)
54
+ assert.ok(result)
55
+ assert.equal(result.kind, 'batch')
56
+ assert.equal(result.status, 'partial') // has failures
57
+ assert.equal(result.agents.length, 2)
58
+ assert.equal(result.completed, 1)
59
+ assert.equal(result.failed, 1)
60
+ assert.equal(result.totalDurationMs, 5000)
61
+
62
+ assert.equal(result.agents[0].agentName, 'Research Agent')
63
+ assert.equal(result.agents[0].status, 'completed')
64
+ assert.equal(result.agents[0].response, 'Found results')
65
+
66
+ assert.equal(result.agents[1].agentName, 'Code Agent')
67
+ assert.equal(result.agents[1].status, 'failed')
68
+ assert.equal(result.agents[1].error, 'Compilation error')
69
+ })
70
+
71
+ it('parses batch completed all-success output', () => {
72
+ const output = JSON.stringify({
73
+ action: 'batch',
74
+ status: 'completed',
75
+ jobIds: ['job-1'],
76
+ completed: 1,
77
+ failed: 0,
78
+ cancelled: 0,
79
+ timedOut: 0,
80
+ totalDurationMs: 2000,
81
+ results: [
82
+ { jobId: 'job-1', agentName: 'Solo Agent', status: 'completed', response: 'Done' },
83
+ ],
84
+ })
85
+
86
+ const result = parseSwarmOutput('spawn_subagent', output)
87
+ assert.ok(result)
88
+ assert.equal(result.status, 'completed') // no failures
89
+ })
90
+
91
+ it('parses single spawn running output', () => {
92
+ const output = JSON.stringify({
93
+ jobId: 'job-abc',
94
+ status: 'running',
95
+ agentId: 'research-agent',
96
+ agentName: 'Research Agent',
97
+ sessionId: 'sess-123',
98
+ lineageId: 'lin-456',
99
+ lifecycleState: 'running',
100
+ })
101
+
102
+ const result = parseSwarmOutput('spawn_subagent', output)
103
+ assert.ok(result)
104
+ assert.equal(result.kind, 'single')
105
+ assert.equal(result.status, 'running')
106
+ assert.equal(result.agents.length, 1)
107
+ assert.equal(result.agents[0].agentName, 'Research Agent')
108
+ assert.equal(result.agents[0].status, 'running')
109
+ assert.equal(result.agents[0].lineageId, 'lin-456')
110
+ })
111
+
112
+ it('parses single spawn completed output', () => {
113
+ const output = JSON.stringify({
114
+ jobId: 'job-xyz',
115
+ status: 'completed',
116
+ agentId: 'code-agent',
117
+ agentName: 'Code Agent',
118
+ sessionId: 'sess-789',
119
+ lineageId: 'lin-012',
120
+ response: 'All tests passing',
121
+ depth: 1,
122
+ childCount: 0,
123
+ durationMs: 15000,
124
+ stateHistory: [],
125
+ })
126
+
127
+ const result = parseSwarmOutput('spawn_subagent', output)
128
+ assert.ok(result)
129
+ assert.equal(result.kind, 'single')
130
+ assert.equal(result.status, 'completed')
131
+ assert.equal(result.agents[0].agentName, 'Code Agent')
132
+ assert.equal(result.agents[0].response, 'All tests passing')
133
+ assert.equal(result.agents[0].durationMs, 15000)
134
+ assert.equal(result.completed, 1)
135
+ assert.equal(result.totalDurationMs, 15000)
136
+ })
137
+
138
+ it('parses single spawn failed output', () => {
139
+ const output = JSON.stringify({
140
+ jobId: 'job-fail',
141
+ status: 'failed',
142
+ agentId: 'bad-agent',
143
+ agentName: 'Bad Agent',
144
+ sessionId: 'sess-bad',
145
+ error: 'Connection timeout',
146
+ })
147
+
148
+ const result = parseSwarmOutput('spawn_subagent', output)
149
+ assert.ok(result)
150
+ assert.equal(result.kind, 'single')
151
+ assert.equal(result.status, 'failed')
152
+ assert.equal(result.agents[0].error, 'Connection timeout')
153
+ assert.equal(result.failed, 1)
154
+ })
155
+
156
+ it('parses swarm completed output with snapshot', () => {
157
+ const output = JSON.stringify({
158
+ action: 'swarm',
159
+ status: 'completed',
160
+ swarmId: 'swarm-1',
161
+ snapshot: {
162
+ swarmId: 'swarm-1',
163
+ parentSessionId: 'parent-1',
164
+ status: 'completed',
165
+ createdAt: 1000,
166
+ completedAt: 5000,
167
+ memberCount: 2,
168
+ completedCount: 2,
169
+ failedCount: 0,
170
+ members: [
171
+ { index: 0, agentId: 'a1', agentName: 'Agent A', jobId: 'j1', sessionId: 's1', task: 'Do X', status: 'completed', resultPreview: 'Done X', error: null, durationMs: 2000 },
172
+ { index: 1, agentId: 'a2', agentName: 'Agent B', jobId: 'j2', sessionId: 's2', task: 'Do Y', status: 'completed', resultPreview: 'Done Y', error: null, durationMs: 3000 },
173
+ ],
174
+ },
175
+ })
176
+
177
+ const result = parseSwarmOutput('spawn_subagent', output)
178
+ assert.ok(result)
179
+ assert.equal(result.kind, 'batch')
180
+ assert.equal(result.status, 'completed')
181
+ assert.equal(result.agents.length, 2)
182
+ assert.equal(result.agents[0].agentName, 'Agent A')
183
+ assert.equal(result.agents[0].response, 'Done X')
184
+ assert.equal(result.agents[1].agentName, 'Agent B')
185
+ assert.equal(result.completed, 2)
186
+ assert.equal(result.failed, 0)
187
+ })
188
+
189
+ it('parses swarm running output without snapshot', () => {
190
+ const output = JSON.stringify({
191
+ action: 'swarm',
192
+ status: 'running',
193
+ swarmId: 'swarm-2',
194
+ memberCount: 3,
195
+ })
196
+
197
+ const result = parseSwarmOutput('spawn_subagent', output)
198
+ assert.ok(result)
199
+ assert.equal(result.kind, 'batch')
200
+ assert.equal(result.status, 'running')
201
+ assert.equal(result.agents.length, 3)
202
+ assert.equal(result.agents[0].status, 'running')
203
+ })
204
+
205
+ it('parses swarm with spawn errors as failed', () => {
206
+ const output = JSON.stringify({
207
+ action: 'swarm',
208
+ status: 'partial',
209
+ swarmId: 'swarm-3',
210
+ snapshot: {
211
+ swarmId: 'swarm-3',
212
+ status: 'partial',
213
+ memberCount: 2,
214
+ completedCount: 1,
215
+ failedCount: 1,
216
+ members: [
217
+ { index: 0, agentId: 'a1', agentName: 'OK Agent', status: 'completed', resultPreview: 'Done', error: null, durationMs: 1000 },
218
+ { index: 1, agentId: '', agentName: '', status: 'spawn_error', resultPreview: null, error: 'Agent not found', durationMs: 0 },
219
+ ],
220
+ },
221
+ })
222
+
223
+ const result = parseSwarmOutput('spawn_subagent', output)
224
+ assert.ok(result)
225
+ assert.equal(result.status, 'partial')
226
+ assert.equal(result.agents[1].status, 'failed') // spawn_error mapped to failed
227
+ assert.equal(result.agents[1].error, 'Agent not found')
228
+ })
229
+ })
230
+
231
+ describe('parseSwarmStatusOutput', () => {
232
+ it('returns null for non-swarm output', () => {
233
+ assert.equal(parseSwarmStatusOutput('spawn_subagent', JSON.stringify({ action: 'batch', results: [] })), null)
234
+ assert.equal(parseSwarmStatusOutput('other_tool', '{}'), null)
235
+ assert.equal(parseSwarmStatusOutput('spawn_subagent', 'invalid'), null)
236
+ })
237
+
238
+ it('returns null when no snapshot is present', () => {
239
+ assert.equal(parseSwarmStatusOutput('spawn_subagent', JSON.stringify({ action: 'swarm', status: 'running' })), null)
240
+ })
241
+
242
+ it('parses swarm output into SwarmStatusData', () => {
243
+ const output = JSON.stringify({
244
+ action: 'swarm',
245
+ swarmId: 'swarm-rich',
246
+ snapshot: {
247
+ swarmId: 'swarm-rich',
248
+ parentSessionId: 'parent-1',
249
+ status: 'completed',
250
+ createdAt: 1000,
251
+ completedAt: 5000,
252
+ memberCount: 2,
253
+ completedCount: 2,
254
+ failedCount: 0,
255
+ members: [
256
+ { index: 0, agentId: 'researcher', agentName: 'Researcher', jobId: 'j1', sessionId: 's1', task: 'Research APIs', status: 'completed', resultPreview: 'Found 3 APIs', error: null, durationMs: 2500 },
257
+ { index: 1, agentId: 'coder', agentName: 'Coder', jobId: 'j2', sessionId: 's2', task: 'Write code', status: 'completed', resultPreview: 'Module ready', error: null, durationMs: 4000 },
258
+ ],
259
+ },
260
+ })
261
+
262
+ const result = parseSwarmStatusOutput('spawn_subagent', output)
263
+ assert.ok(result)
264
+ assert.equal(result.swarmId, 'swarm-rich')
265
+ assert.equal(result.status, 'completed')
266
+ assert.equal(result.memberCount, 2)
267
+ assert.equal(result.completedCount, 2)
268
+ assert.equal(result.members.length, 2)
269
+ assert.equal(result.members[0].agentName, 'Researcher')
270
+ assert.equal(result.members[0].task, 'Research APIs')
271
+ assert.equal(result.members[1].resultPreview, 'Module ready')
272
+ assert.equal(result.parentAgentName, 'Orchestrator')
273
+ })
274
+ })