@swarmclawai/swarmclaw 0.7.7 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +12 -14
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -0,0 +1,651 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { MessageToolEvent } from '@/types'
4
+ import {
5
+ collectToolEvent,
6
+ deriveTerminalRunError,
7
+ dedupeConsecutiveToolEvents,
8
+ hasDirectLocalCodingTools,
9
+ isLikelyToolErrorOutput,
10
+ normalizeAssistantArtifactLinks,
11
+ reconcileConnectorDeliveryText,
12
+ requestedToolNamesFromMessage,
13
+ translateRequestedToolInvocation,
14
+ } from './chat-execution'
15
+ import {
16
+ buildToolDisciplineLines,
17
+ getExplicitRequiredToolNames,
18
+ looksLikeOpenEndedDeliverableTask,
19
+ resolveFinalStreamResponseText,
20
+ } from './stream-agent-chat'
21
+ import {
22
+ stripHiddenControlTokens,
23
+ shouldSuppressHiddenControlText,
24
+ } from './assistant-control'
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // collectToolEvent advanced
28
+ // ---------------------------------------------------------------------------
29
+ describe('collectToolEvent advanced', () => {
30
+ it('tracks three parallel tool calls with different toolCallIds', () => {
31
+ const bag: MessageToolEvent[] = []
32
+ collectToolEvent({ t: 'tool_call', toolName: 'web', toolInput: '{"q":"a"}', toolCallId: 'c1' }, bag)
33
+ collectToolEvent({ t: 'tool_call', toolName: 'shell', toolInput: 'ls', toolCallId: 'c2' }, bag)
34
+ collectToolEvent({ t: 'tool_call', toolName: 'files', toolInput: '{}', toolCallId: 'c3' }, bag)
35
+
36
+ assert.equal(bag.length, 3)
37
+ assert.equal(bag[0].toolCallId, 'c1')
38
+ assert.equal(bag[1].toolCallId, 'c2')
39
+ assert.equal(bag[2].toolCallId, 'c3')
40
+ })
41
+
42
+ it('matches results arriving out of order by toolCallId', () => {
43
+ const bag: MessageToolEvent[] = []
44
+ collectToolEvent({ t: 'tool_call', toolName: 'web', toolInput: '{}', toolCallId: 'c1' }, bag)
45
+ collectToolEvent({ t: 'tool_call', toolName: 'shell', toolInput: 'ls', toolCallId: 'c2' }, bag)
46
+
47
+ // Result for c2 arrives first
48
+ collectToolEvent({ t: 'tool_result', toolName: 'shell', toolOutput: 'dir listing', toolCallId: 'c2' }, bag)
49
+ collectToolEvent({ t: 'tool_result', toolName: 'web', toolOutput: 'search results', toolCallId: 'c1' }, bag)
50
+
51
+ assert.equal(bag[0].output, 'search results')
52
+ assert.equal(bag[1].output, 'dir listing')
53
+ })
54
+
55
+ it('discards orphaned tool_result with no matching call', () => {
56
+ const bag: MessageToolEvent[] = []
57
+ collectToolEvent({ t: 'tool_result', toolName: 'shell', toolOutput: 'orphan', toolCallId: 'no-match' }, bag)
58
+ assert.equal(bag.length, 0)
59
+ })
60
+
61
+ it('marks error=true for error results', () => {
62
+ const bag: MessageToolEvent[] = []
63
+ collectToolEvent({ t: 'tool_call', toolName: 'shell', toolInput: 'bad', toolCallId: 'e1' }, bag)
64
+ collectToolEvent({ t: 'tool_result', toolName: 'shell', toolOutput: 'Error (exit 1): command failed', toolCallId: 'e1' }, bag)
65
+
66
+ assert.equal(bag[0].error, true)
67
+ })
68
+
69
+ it('tracks multiple calls to the same tool with different inputs separately', () => {
70
+ const bag: MessageToolEvent[] = []
71
+ collectToolEvent({ t: 'tool_call', toolName: 'files', toolInput: '{"path":"a.txt"}', toolCallId: 'f1' }, bag)
72
+ collectToolEvent({ t: 'tool_call', toolName: 'files', toolInput: '{"path":"b.txt"}', toolCallId: 'f2' }, bag)
73
+
74
+ assert.equal(bag.length, 2)
75
+ assert.equal(bag[0].input, '{"path":"a.txt"}')
76
+ assert.equal(bag[1].input, '{"path":"b.txt"}')
77
+ })
78
+
79
+ it('marks error=true for JSON error in tool output', () => {
80
+ const bag: MessageToolEvent[] = []
81
+ collectToolEvent({ t: 'tool_call', toolName: 'web', toolInput: '{}', toolCallId: 'j1' }, bag)
82
+ collectToolEvent({ t: 'tool_result', toolName: 'web', toolOutput: '{"error":"timeout","status":"failed"}', toolCallId: 'j1' }, bag)
83
+
84
+ assert.equal(bag[0].error, true)
85
+ })
86
+
87
+ it('marks error=true for MCP validation failure', () => {
88
+ const bag: MessageToolEvent[] = []
89
+ collectToolEvent({ t: 'tool_call', toolName: 'mcp', toolInput: '{}', toolCallId: 'm1' }, bag)
90
+ collectToolEvent({ t: 'tool_result', toolName: 'mcp', toolOutput: 'invalid_type: expected string, received number (zod issue)', toolCallId: 'm1' }, bag)
91
+
92
+ assert.equal(bag[0].error, true)
93
+ })
94
+
95
+ it('leaves error undefined for normal successful output', () => {
96
+ const bag: MessageToolEvent[] = []
97
+ collectToolEvent({ t: 'tool_call', toolName: 'files', toolInput: '{}', toolCallId: 's1' }, bag)
98
+ collectToolEvent({ t: 'tool_result', toolName: 'files', toolOutput: 'File written successfully', toolCallId: 's1' }, bag)
99
+
100
+ assert.ok(!bag[0].error)
101
+ })
102
+
103
+ it('handles long sequence call-result-call-result-call-result producing 3 complete entries', () => {
104
+ const bag: MessageToolEvent[] = []
105
+ for (let i = 1; i <= 3; i++) {
106
+ collectToolEvent({ t: 'tool_call', toolName: 'shell', toolInput: `cmd${i}`, toolCallId: `seq${i}` }, bag)
107
+ collectToolEvent({ t: 'tool_result', toolName: 'shell', toolOutput: `out${i}`, toolCallId: `seq${i}` }, bag)
108
+ }
109
+
110
+ assert.equal(bag.length, 3)
111
+ assert.equal(bag[0].output, 'out1')
112
+ assert.equal(bag[1].output, 'out2')
113
+ assert.equal(bag[2].output, 'out3')
114
+ })
115
+ })
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // dedupeConsecutiveToolEvents advanced
119
+ // ---------------------------------------------------------------------------
120
+ describe('dedupeConsecutiveToolEvents advanced', () => {
121
+ const ev = (name: string, input: string, output?: string, error?: boolean): MessageToolEvent => ({
122
+ name, input, output, error,
123
+ })
124
+
125
+ it('returns empty array for empty input', () => {
126
+ assert.deepEqual(dedupeConsecutiveToolEvents([]), [])
127
+ })
128
+
129
+ it('keeps a single event', () => {
130
+ const events = [ev('shell', 'ls', 'ok')]
131
+ assert.deepEqual(dedupeConsecutiveToolEvents(events), events)
132
+ })
133
+
134
+ it('collapses 4 identical consecutive events using block dedupe', () => {
135
+ const single = ev('shell', 'ls', 'ok')
136
+ const events = Array.from({ length: 4 }, () => ({ ...single }))
137
+ const result = dedupeConsecutiveToolEvents(events)
138
+ // Block dedupe (single pass): blockSize=2 matches [A,A]==[A,A], keeps first block [A,A] = 2
139
+ assert.equal(result.length, 2)
140
+ assert.equal(result[0].name, 'shell')
141
+ })
142
+
143
+ it('collapses 6 identical consecutive events to 3', () => {
144
+ const single = ev('shell', 'ls', 'ok')
145
+ const events = Array.from({ length: 6 }, () => ({ ...single }))
146
+ const result = dedupeConsecutiveToolEvents(events)
147
+ // blockSize=3: [A,A,A]==[A,A,A], keeps first block [A,A,A] = 3
148
+ assert.equal(result.length, 3)
149
+ })
150
+
151
+ it('collapses A-B-A-B repeated block to A-B', () => {
152
+ const a = ev('shell', 'ls', 'ok')
153
+ const b = ev('files', 'read', 'data')
154
+ const events = [a, b, { ...a }, { ...b }]
155
+ const result = dedupeConsecutiveToolEvents(events)
156
+ assert.equal(result.length, 2)
157
+ assert.equal(result[0].name, 'shell')
158
+ assert.equal(result[1].name, 'files')
159
+ })
160
+
161
+ it('preserves non-consecutive duplicates: A-B-A', () => {
162
+ const a = ev('shell', 'ls', 'ok')
163
+ const b = ev('files', 'read', 'data')
164
+ const events = [a, b, { ...a }]
165
+ const result = dedupeConsecutiveToolEvents(events)
166
+ assert.equal(result.length, 3)
167
+ })
168
+
169
+ it('does not dedupe events with different outputs even if same tool', () => {
170
+ const e1 = ev('shell', 'ls', 'output1')
171
+ const e2 = ev('shell', 'ls', 'output2')
172
+ const result = dedupeConsecutiveToolEvents([e1, e2])
173
+ assert.equal(result.length, 2)
174
+ })
175
+ })
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // deriveTerminalRunError advanced
179
+ // ---------------------------------------------------------------------------
180
+ describe('deriveTerminalRunError advanced', () => {
181
+ it('uses last stream error when multiple errors and no fullResponse', () => {
182
+ const err = deriveTerminalRunError({
183
+ fullResponse: '',
184
+ streamErrors: ['rate limit exceeded', 'server timeout'],
185
+ toolEvents: [],
186
+ internal: false,
187
+ })
188
+ assert.equal(err, 'server timeout')
189
+ })
190
+
191
+ it('returns undefined when fullResponse has text, even with stream errors', () => {
192
+ const err = deriveTerminalRunError({
193
+ fullResponse: 'Here is your answer',
194
+ streamErrors: ['some error'],
195
+ toolEvents: [],
196
+ internal: false,
197
+ })
198
+ assert.equal(err, undefined)
199
+ })
200
+
201
+ it('returns undefined when tool events exist but no text', () => {
202
+ const err = deriveTerminalRunError({
203
+ fullResponse: '',
204
+ streamErrors: [],
205
+ toolEvents: [{ name: 'shell', input: 'ls', output: 'ok' }],
206
+ internal: false,
207
+ })
208
+ assert.equal(err, undefined)
209
+ })
210
+
211
+ it('returns undefined for internal run with empty response', () => {
212
+ const err = deriveTerminalRunError({
213
+ fullResponse: '',
214
+ streamErrors: [],
215
+ toolEvents: [],
216
+ internal: true,
217
+ })
218
+ assert.equal(err, undefined)
219
+ })
220
+
221
+ it('generates user-friendly error for empty everything (non-internal)', () => {
222
+ const err = deriveTerminalRunError({
223
+ fullResponse: '',
224
+ streamErrors: [],
225
+ toolEvents: [],
226
+ internal: false,
227
+ })
228
+ assert.ok(err)
229
+ assert.ok(err.includes('Check the provider configuration'))
230
+ })
231
+
232
+ it('uses errorMessage directly when provided', () => {
233
+ const err = deriveTerminalRunError({
234
+ errorMessage: 'Custom error',
235
+ fullResponse: 'some text',
236
+ streamErrors: ['other error'],
237
+ toolEvents: [],
238
+ internal: false,
239
+ })
240
+ assert.equal(err, 'Custom error')
241
+ })
242
+ })
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // requestedToolNamesFromMessage advanced
246
+ // ---------------------------------------------------------------------------
247
+ describe('requestedToolNamesFromMessage advanced', () => {
248
+ it('extracts files from "Use the files tool"', () => {
249
+ const result = requestedToolNamesFromMessage('Use the files tool')
250
+ assert.ok(result.includes('files'))
251
+ })
252
+
253
+ it('extracts web and browser from search + screenshot request', () => {
254
+ const result = requestedToolNamesFromMessage('Use `web` to search and `browser` to take a screenshot')
255
+ assert.ok(result.includes('web'))
256
+ assert.ok(result.includes('browser'))
257
+ })
258
+
259
+ it('extracts connector_message_tool for "use connector_message_tool to send WhatsApp"', () => {
260
+ const result = requestedToolNamesFromMessage('Use `connector_message_tool` to send to my WhatsApp')
261
+ assert.ok(result.includes('connector_message_tool'))
262
+ })
263
+
264
+ it('returns empty for negated tool mention', () => {
265
+ const result = requestedToolNamesFromMessage("Don't use the browser")
266
+ assert.ok(!result.includes('browser'))
267
+ })
268
+
269
+ it('extracts memory_tool from "use `memory_tool` to store"', () => {
270
+ const result = requestedToolNamesFromMessage('Use `memory_tool` to store this for later')
271
+ assert.ok(result.includes('memory_tool'))
272
+ })
273
+
274
+ it('extracts multiple tools from complex request', () => {
275
+ const result = requestedToolNamesFromMessage('Use `web` to research, `browser` to screenshot, and `connector_message_tool` to send via Slack')
276
+ assert.ok(result.includes('web'))
277
+ assert.ok(result.includes('browser'))
278
+ assert.ok(result.includes('connector_message_tool'))
279
+ })
280
+
281
+ it('returns empty array when no tool mentions', () => {
282
+ const result = requestedToolNamesFromMessage('What is the weather like today?')
283
+ assert.deepEqual(result, [])
284
+ })
285
+ })
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // translateRequestedToolInvocation advanced
289
+ // ---------------------------------------------------------------------------
290
+ describe('translateRequestedToolInvocation advanced', () => {
291
+ it('maps manage_platform with resource=tasks to manage_tasks when available', () => {
292
+ const { toolName, args } = translateRequestedToolInvocation(
293
+ 'manage_platform',
294
+ { resource: 'tasks', action: 'list' },
295
+ '',
296
+ ['manage_tasks', 'files'],
297
+ )
298
+ assert.equal(toolName, 'manage_tasks')
299
+ assert.equal(args.resource, 'tasks')
300
+ })
301
+
302
+ it('keeps manage_platform when specific tool not available', () => {
303
+ const { toolName } = translateRequestedToolInvocation(
304
+ 'manage_platform',
305
+ { resource: 'agents' },
306
+ '',
307
+ ['manage_platform'],
308
+ )
309
+ assert.equal(toolName, 'manage_platform')
310
+ })
311
+
312
+ it('uses specific tool directly when available', () => {
313
+ const { toolName } = translateRequestedToolInvocation(
314
+ 'manage_tasks',
315
+ { action: 'list' },
316
+ '',
317
+ ['manage_tasks'],
318
+ )
319
+ assert.equal(toolName, 'manage_tasks')
320
+ })
321
+
322
+ it('falls back to manage_platform when specific tool not available but umbrella is', () => {
323
+ const { toolName, args } = translateRequestedToolInvocation(
324
+ 'manage_tasks',
325
+ { action: 'list', id: '123' },
326
+ '',
327
+ ['manage_platform'],
328
+ )
329
+ assert.equal(toolName, 'manage_platform')
330
+ assert.equal(args.resource, 'tasks')
331
+ assert.equal(args.action, 'list')
332
+ })
333
+
334
+ it('maps web_search to web with action=search', () => {
335
+ const { toolName, args } = translateRequestedToolInvocation(
336
+ 'web_search',
337
+ { query: 'test query' },
338
+ '',
339
+ ['web'],
340
+ )
341
+ assert.equal(toolName, 'web')
342
+ assert.equal(args.action, 'search')
343
+ assert.equal(args.query, 'test query')
344
+ })
345
+ })
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // isLikelyToolErrorOutput advanced
349
+ // ---------------------------------------------------------------------------
350
+ describe('isLikelyToolErrorOutput advanced', () => {
351
+ it('detects "Error (exit 127): command not found"', () => {
352
+ assert.equal(isLikelyToolErrorOutput('Error (exit 127): command not found'), true)
353
+ })
354
+
355
+ it('detects JSON with error and failed status', () => {
356
+ assert.equal(isLikelyToolErrorOutput('{"error":"timeout","status":"failed"}'), true)
357
+ })
358
+
359
+ it('does not flag normal JSON response', () => {
360
+ assert.equal(isLikelyToolErrorOutput('{"status":"ok","data":[]}'), false)
361
+ })
362
+
363
+ it('does not flag success message', () => {
364
+ assert.equal(isLikelyToolErrorOutput('File written successfully'), false)
365
+ })
366
+
367
+ it('detects JSON with status=failed without error key', () => {
368
+ assert.equal(isLikelyToolErrorOutput('{"status":"failed"}'), true)
369
+ })
370
+
371
+ it('returns false for empty string', () => {
372
+ assert.equal(isLikelyToolErrorOutput(''), false)
373
+ })
374
+
375
+ it('detects MCP error keyword', () => {
376
+ assert.equal(isLikelyToolErrorOutput('MCP error: server connection refused'), true)
377
+ })
378
+
379
+ it('detects ECONNREFUSED', () => {
380
+ assert.equal(isLikelyToolErrorOutput('connect ECONNREFUSED 127.0.0.1:3000'), true)
381
+ })
382
+ })
383
+
384
+ // ---------------------------------------------------------------------------
385
+ // hasDirectLocalCodingTools
386
+ // ---------------------------------------------------------------------------
387
+ describe('hasDirectLocalCodingTools', () => {
388
+ it('returns true when shell is in plugins', () => {
389
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['shell', 'memory'] }), true)
390
+ })
391
+
392
+ it('returns false when only delegate and web', () => {
393
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['delegate', 'web'] }), false)
394
+ })
395
+
396
+ it('returns true when edit_file is in plugins', () => {
397
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['edit_file'] }), true)
398
+ })
399
+
400
+ it('returns false for empty plugins', () => {
401
+ assert.equal(hasDirectLocalCodingTools({ plugins: [] }), false)
402
+ })
403
+
404
+ it('returns true for files plugin', () => {
405
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['files'] }), true)
406
+ })
407
+
408
+ it('returns true for sandbox plugin', () => {
409
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['sandbox'] }), true)
410
+ })
411
+ })
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // reconcileConnectorDeliveryText advanced
415
+ // ---------------------------------------------------------------------------
416
+ describe('reconcileConnectorDeliveryText advanced', () => {
417
+ it('keeps original text when one connector event succeeds and another fails', () => {
418
+ const events: MessageToolEvent[] = [
419
+ { name: 'connector_message_tool', input: '{}', output: '{"error":"fail"}', error: true },
420
+ { name: 'connector_message_tool', input: '{}', output: '{"status":"sent"}' },
421
+ ]
422
+ const text = "I've sent the message to your WhatsApp."
423
+ assert.equal(reconcileConnectorDeliveryText(text, events), text)
424
+ })
425
+
426
+ it('overrides text when all connector events fail', () => {
427
+ const events: MessageToolEvent[] = [
428
+ { name: 'connector_message_tool', input: '{}', output: '{"error":"timeout"}', error: true },
429
+ ]
430
+ const text = 'I sent your message!'
431
+ const result = reconcileConnectorDeliveryText(text, events)
432
+ assert.ok(result.includes("couldn't send"))
433
+ })
434
+
435
+ it('keeps original text when no connector events exist', () => {
436
+ const events: MessageToolEvent[] = [
437
+ { name: 'shell', input: 'ls', output: 'ok' },
438
+ ]
439
+ const text = "I've sent the message to your account."
440
+ assert.equal(reconcileConnectorDeliveryText(text, events), text)
441
+ })
442
+
443
+ it('extracts error detail from nested JSON', () => {
444
+ const events: MessageToolEvent[] = [
445
+ {
446
+ name: 'connector_message_tool',
447
+ input: '{}',
448
+ output: '{"error":"failed","detail":{"message":"WhatsApp session expired"}}',
449
+ error: true,
450
+ },
451
+ ]
452
+ const text = 'I sent the message.'
453
+ const result = reconcileConnectorDeliveryText(text, events)
454
+ assert.ok(result.includes('WhatsApp session expired'))
455
+ })
456
+
457
+ it('keeps text that does not match positive delivery pattern', () => {
458
+ const events: MessageToolEvent[] = [
459
+ { name: 'connector_message_tool', input: '{}', output: '{"error":"fail"}', error: true },
460
+ ]
461
+ const text = 'The connector returned an error.'
462
+ assert.equal(reconcileConnectorDeliveryText(text, events), text)
463
+ })
464
+ })
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // buildToolDisciplineLines advanced
468
+ // ---------------------------------------------------------------------------
469
+ describe('buildToolDisciplineLines advanced', () => {
470
+ it('returns basic line only for minimal tools', () => {
471
+ const lines = buildToolDisciplineLines(['files'])
472
+ assert.ok(lines.length >= 1)
473
+ assert.ok(lines[0].includes('Enabled tools'))
474
+ })
475
+
476
+ it('includes memory guidance when memory_tool is not directly available but memory is', () => {
477
+ const lines = buildToolDisciplineLines(['memory'])
478
+ assert.ok(lines.some((line) => line.includes('Enabled tools')))
479
+ })
480
+
481
+ it('includes schedule guidance when manage_schedules is enabled', () => {
482
+ const lines = buildToolDisciplineLines(['manage_schedules'])
483
+ assert.ok(lines.some((line) => line.includes('reuse or update matching agent-created schedules')))
484
+ })
485
+
486
+ it('includes delegate local-first guidance when coding tools and delegate enabled', () => {
487
+ const lines = buildToolDisciplineLines(['delegate', 'shell', 'files'])
488
+ assert.ok(lines.some((line) => line.includes('prefer using them directly for straightforward coding')))
489
+ })
490
+ })
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // looksLikeOpenEndedDeliverableTask advanced
494
+ // ---------------------------------------------------------------------------
495
+ describe('looksLikeOpenEndedDeliverableTask advanced', () => {
496
+ it('returns true for "Write a blog post about sustainable energy practices and create a draft document"', () => {
497
+ assert.equal(looksLikeOpenEndedDeliverableTask('Write a blog post about sustainable energy practices and create a draft document'), true)
498
+ })
499
+
500
+ it('returns false for short specific fix request', () => {
501
+ assert.equal(looksLikeOpenEndedDeliverableTask('Fix the bug in line 42'), false)
502
+ })
503
+
504
+ it('returns true for "Draft a proposal for the Q3 marketing campaign including budget and timeline"', () => {
505
+ assert.equal(looksLikeOpenEndedDeliverableTask('Draft a proposal for the Q3 marketing campaign including budget and timeline'), true)
506
+ })
507
+
508
+ it('returns false for "npm install"', () => {
509
+ assert.equal(looksLikeOpenEndedDeliverableTask('npm install'), false)
510
+ })
511
+
512
+ it('returns false for empty string', () => {
513
+ assert.equal(looksLikeOpenEndedDeliverableTask(''), false)
514
+ })
515
+
516
+ it('returns true for text containing "deliverable"', () => {
517
+ assert.equal(looksLikeOpenEndedDeliverableTask('Create a final deliverable summarizing the research findings from this quarter'), true)
518
+ })
519
+ })
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Assistant control
523
+ // ---------------------------------------------------------------------------
524
+ describe('stripHiddenControlTokens', () => {
525
+ it('removes HEARTBEAT_OK', () => {
526
+ assert.equal(stripHiddenControlTokens('HEARTBEAT_OK'), '')
527
+ })
528
+
529
+ it('removes NO_MESSAGE', () => {
530
+ assert.equal(stripHiddenControlTokens('NO_MESSAGE'), '')
531
+ })
532
+
533
+ it('passes through regular text', () => {
534
+ assert.equal(stripHiddenControlTokens("Here's your answer"), "Here's your answer")
535
+ })
536
+
537
+ it('strips control tokens from mixed text, keeps the rest', () => {
538
+ const result = stripHiddenControlTokens('HEARTBEAT_OK Here is the real message')
539
+ assert.ok(!result.includes('HEARTBEAT_OK'))
540
+ assert.ok(result.includes('real message'))
541
+ })
542
+
543
+ it('handles multiple control tokens', () => {
544
+ assert.equal(stripHiddenControlTokens('NO_MESSAGE HEARTBEAT_OK'), '')
545
+ })
546
+ })
547
+
548
+ describe('shouldSuppressHiddenControlText', () => {
549
+ it('returns true for "HEARTBEAT_OK"', () => {
550
+ assert.equal(shouldSuppressHiddenControlText('HEARTBEAT_OK'), true)
551
+ })
552
+
553
+ it('returns false for normal text', () => {
554
+ assert.equal(shouldSuppressHiddenControlText("Here's your answer"), false)
555
+ })
556
+
557
+ it('returns true for "NO_MESSAGE"', () => {
558
+ assert.equal(shouldSuppressHiddenControlText('NO_MESSAGE'), true)
559
+ })
560
+
561
+ it('returns false for empty string', () => {
562
+ assert.equal(shouldSuppressHiddenControlText(''), false)
563
+ })
564
+
565
+ it('returns false for text with control token embedded in real content', () => {
566
+ assert.equal(shouldSuppressHiddenControlText('HEARTBEAT_OK and here is some real content'), false)
567
+ })
568
+ })
569
+
570
+ // ---------------------------------------------------------------------------
571
+ // resolveFinalStreamResponseText advanced
572
+ // ---------------------------------------------------------------------------
573
+ describe('resolveFinalStreamResponseText advanced', () => {
574
+ it('uses fullText when no tool calls', () => {
575
+ const result = resolveFinalStreamResponseText({
576
+ fullText: 'Full response text here',
577
+ lastSegment: 'Last segment',
578
+ lastSettledSegment: 'Settled segment',
579
+ hasToolCalls: false,
580
+ })
581
+ assert.equal(result, 'Full response text here')
582
+ })
583
+
584
+ it('prefers lastSegment when tool calls are present', () => {
585
+ const result = resolveFinalStreamResponseText({
586
+ fullText: 'Full text with lots of content',
587
+ lastSegment: 'Last segment content',
588
+ lastSettledSegment: 'Settled segment content',
589
+ hasToolCalls: true,
590
+ })
591
+ assert.equal(result, 'Last segment content')
592
+ })
593
+
594
+ it('falls back through candidates when earlier ones are empty', () => {
595
+ const result = resolveFinalStreamResponseText({
596
+ fullText: 'Full text fallback',
597
+ lastSegment: '',
598
+ lastSettledSegment: '',
599
+ hasToolCalls: true,
600
+ })
601
+ assert.equal(result, 'Full text fallback')
602
+ })
603
+
604
+ it('returns empty string when all candidates empty with tool calls', () => {
605
+ const result = resolveFinalStreamResponseText({
606
+ fullText: '',
607
+ lastSegment: '',
608
+ lastSettledSegment: '',
609
+ hasToolCalls: true,
610
+ })
611
+ assert.equal(result, '')
612
+ })
613
+ })
614
+
615
+ // ---------------------------------------------------------------------------
616
+ // normalizeAssistantArtifactLinks
617
+ // ---------------------------------------------------------------------------
618
+ describe('normalizeAssistantArtifactLinks', () => {
619
+ it('rewrites sandbox:/api/uploads/ to /api/uploads/', () => {
620
+ const text = 'See [file](sandbox:/api/uploads/abc123.png)'
621
+ const result = normalizeAssistantArtifactLinks(text, '/tmp')
622
+ assert.ok(result.includes('/api/uploads/abc123.png'))
623
+ assert.ok(!result.includes('sandbox:'))
624
+ })
625
+
626
+ it('passes through text without links unchanged', () => {
627
+ const text = 'No links here at all'
628
+ assert.equal(normalizeAssistantArtifactLinks(text, '/tmp'), text)
629
+ })
630
+ })
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // getExplicitRequiredToolNames
634
+ // ---------------------------------------------------------------------------
635
+ describe('getExplicitRequiredToolNames', () => {
636
+ it('requires web_search for research-oriented query when web_search is enabled', () => {
637
+ const result = getExplicitRequiredToolNames(
638
+ 'Search the web for the latest news about AI regulation',
639
+ ['web_search', 'web_fetch', 'browser'],
640
+ )
641
+ assert.ok(result.includes('web_search') || result.includes('web'))
642
+ })
643
+
644
+ it('returns empty when no tool matches the message', () => {
645
+ const result = getExplicitRequiredToolNames(
646
+ 'What is 2 + 2?',
647
+ ['files', 'shell'],
648
+ )
649
+ assert.deepEqual(result, [])
650
+ })
651
+ })