@swarmclawai/swarmclaw 0.7.8 → 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 (251) hide show
  1. package/README.md +12 -15
  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 +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -0,0 +1,94 @@
1
+ import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import test from 'node:test'
7
+
8
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
9
+
10
+ function runWithTempDataDir(script: string) {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chat-disabled-'))
12
+ try {
13
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
14
+ cwd: repoRoot,
15
+ env: {
16
+ ...process.env,
17
+ DATA_DIR: path.join(tempDir, 'data'),
18
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
19
+ BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
20
+ },
21
+ encoding: 'utf-8',
22
+ })
23
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
24
+ const lines = (result.stdout || '')
25
+ .trim()
26
+ .split('\n')
27
+ .map((line) => line.trim())
28
+ .filter(Boolean)
29
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
30
+ return JSON.parse(jsonLine || '{}')
31
+ } finally {
32
+ fs.rmSync(tempDir, { recursive: true, force: true })
33
+ }
34
+ }
35
+
36
+ test('executeSessionChatTurn persists a visible error for disabled agents', () => {
37
+ const output = runWithTempDataDir(`
38
+ const storageMod = await import('./src/lib/server/storage.ts')
39
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
40
+ const threadMod = await import('./src/lib/server/agent-thread-session.ts')
41
+ const ensureAgentThreadSession = threadMod.ensureAgentThreadSession
42
+ || threadMod.default?.ensureAgentThreadSession
43
+ || threadMod['module.exports']?.ensureAgentThreadSession
44
+ const execMod = await import('./src/lib/server/chat-execution.ts')
45
+ const executeSessionChatTurn = execMod.executeSessionChatTurn
46
+ || execMod.default?.executeSessionChatTurn
47
+ || execMod['module.exports']?.executeSessionChatTurn
48
+
49
+ const now = Date.now()
50
+ storage.saveAgents({
51
+ molly: {
52
+ id: 'molly',
53
+ name: 'Molly',
54
+ description: 'Temporarily disabled helper',
55
+ provider: 'openai',
56
+ model: 'gpt-test',
57
+ credentialId: null,
58
+ apiEndpoint: null,
59
+ fallbackCredentialIds: [],
60
+ disabled: false,
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ plugins: ['memory'],
64
+ },
65
+ })
66
+
67
+ const session = ensureAgentThreadSession('molly')
68
+ const agents = storage.loadAgents()
69
+ agents.molly.disabled = true
70
+ storage.saveAgents(agents)
71
+
72
+ const result = await executeSessionChatTurn({
73
+ sessionId: session.id,
74
+ message: 'hello',
75
+ runId: 'run-disabled-smoke',
76
+ })
77
+ const persisted = storage.loadSessions()[session.id]
78
+ const lastMessage = persisted.messages[persisted.messages.length - 1]
79
+
80
+ console.log(JSON.stringify({
81
+ error: result.error || null,
82
+ text: result.text || null,
83
+ persisted: result.persisted || false,
84
+ lastRole: lastMessage?.role || null,
85
+ lastText: lastMessage?.text || null,
86
+ }))
87
+ `)
88
+
89
+ assert.equal(output.persisted, true)
90
+ assert.equal(output.lastRole, 'assistant')
91
+ assert.match(String(output.error || ''), /disabled/i)
92
+ assert.match(String(output.text || ''), /disabled/i)
93
+ assert.match(String(output.lastText || ''), /disabled/i)
94
+ })
@@ -6,9 +6,12 @@ import path from 'path'
6
6
  import type { MessageToolEvent, SSEEvent } from '@/types'
7
7
  import {
8
8
  collectToolEvent,
9
+ deriveTerminalRunError,
9
10
  dedupeConsecutiveToolEvents,
11
+ hasDirectLocalCodingTools,
10
12
  isLikelyToolErrorOutput,
11
13
  normalizeAssistantArtifactLinks,
14
+ reconcileConnectorDeliveryText,
12
15
  requestedToolNamesFromMessage,
13
16
  translateRequestedToolInvocation,
14
17
  } from './chat-execution'
@@ -20,6 +23,7 @@ describe('collectToolEvent', () => {
20
23
  t: 'tool_call',
21
24
  toolName: 'files',
22
25
  toolInput: '{"path":"spec.md"}',
26
+ toolCallId: 'call-1',
23
27
  }
24
28
 
25
29
  collectToolEvent(toolCall, bag)
@@ -29,6 +33,7 @@ describe('collectToolEvent', () => {
29
33
  {
30
34
  name: 'files',
31
35
  input: '{"path":"spec.md"}',
36
+ toolCallId: 'call-1',
32
37
  },
33
38
  ])
34
39
  })
@@ -39,11 +44,13 @@ describe('collectToolEvent', () => {
39
44
  t: 'tool_call',
40
45
  toolName: 'files',
41
46
  toolInput: '{"path":"spec.md"}',
47
+ toolCallId: 'call-1',
42
48
  }
43
49
  const toolResult: SSEEvent = {
44
50
  t: 'tool_result',
45
51
  toolName: 'files',
46
52
  toolOutput: 'Written spec.md (12 bytes)',
53
+ toolCallId: 'call-1',
47
54
  }
48
55
 
49
56
  collectToolEvent(toolCall, bag)
@@ -56,10 +63,12 @@ describe('collectToolEvent', () => {
56
63
  input: '{"path":"spec.md"}',
57
64
  output: 'Written spec.md (12 bytes)',
58
65
  error: undefined,
66
+ toolCallId: 'call-1',
59
67
  },
60
68
  {
61
69
  name: 'files',
62
70
  input: '{"path":"spec.md"}',
71
+ toolCallId: 'call-1',
63
72
  },
64
73
  ])
65
74
  })
@@ -70,26 +79,61 @@ describe('collectToolEvent', () => {
70
79
  t: 'tool_call',
71
80
  toolName: 'shell',
72
81
  toolInput: '{"command":"python broken.py"}',
82
+ toolCallId: 'call-shell',
73
83
  }, bag)
74
84
  collectToolEvent({
75
85
  t: 'tool_result',
76
86
  toolName: 'shell',
77
87
  toolOutput: 'Error (exit 1): Traceback...',
88
+ toolCallId: 'call-shell',
78
89
  }, bag)
79
90
  collectToolEvent({
80
91
  t: 'tool_call',
81
92
  toolName: 'browser',
82
93
  toolInput: '{"command":"click"}',
94
+ toolCallId: 'call-browser',
83
95
  }, bag)
84
96
  collectToolEvent({
85
97
  t: 'tool_result',
86
98
  toolName: 'browser',
87
99
  toolOutput: '{"error":"MCP error -32000: invalid_type","issues":[{"code":"invalid_type"}]}',
100
+ toolCallId: 'call-browser',
88
101
  }, bag)
89
102
 
90
103
  assert.equal(bag[0].error, true)
91
104
  assert.equal(bag[1].error, true)
92
105
  })
106
+
107
+ it('matches parallel same-tool results by toolCallId instead of swapping outputs', () => {
108
+ const bag: MessageToolEvent[] = []
109
+ collectToolEvent({
110
+ t: 'tool_call',
111
+ toolName: 'wallet_tool',
112
+ toolInput: '{"action":"balance","chain":"solana"}',
113
+ toolCallId: 'call-sol',
114
+ }, bag)
115
+ collectToolEvent({
116
+ t: 'tool_call',
117
+ toolName: 'wallet_tool',
118
+ toolInput: '{"action":"balance","chain":"ethereum"}',
119
+ toolCallId: 'call-eth',
120
+ }, bag)
121
+ collectToolEvent({
122
+ t: 'tool_result',
123
+ toolName: 'wallet_tool',
124
+ toolOutput: '{"chain":"solana"}',
125
+ toolCallId: 'call-sol',
126
+ }, bag)
127
+ collectToolEvent({
128
+ t: 'tool_result',
129
+ toolName: 'wallet_tool',
130
+ toolOutput: '{"chain":"ethereum"}',
131
+ toolCallId: 'call-eth',
132
+ }, bag)
133
+
134
+ assert.equal(bag[0].output, '{"chain":"solana"}')
135
+ assert.equal(bag[1].output, '{"chain":"ethereum"}')
136
+ })
93
137
  })
94
138
 
95
139
  describe('dedupeConsecutiveToolEvents', () => {
@@ -126,6 +170,57 @@ describe('dedupeConsecutiveToolEvents', () => {
126
170
  })
127
171
  })
128
172
 
173
+ describe('deriveTerminalRunError', () => {
174
+ it('uses the streamed provider error when no text was produced', () => {
175
+ assert.equal(
176
+ deriveTerminalRunError({
177
+ errorMessage: undefined,
178
+ fullResponse: '',
179
+ streamErrors: ['Ollama error (401): invalid api key'],
180
+ toolEvents: [],
181
+ internal: false,
182
+ }),
183
+ 'Ollama error (401): invalid api key',
184
+ )
185
+ })
186
+
187
+ it('converts empty successful runs into a visible assistant error', () => {
188
+ assert.equal(
189
+ deriveTerminalRunError({
190
+ errorMessage: undefined,
191
+ fullResponse: '',
192
+ streamErrors: [],
193
+ toolEvents: [],
194
+ internal: false,
195
+ }),
196
+ 'Run completed without any response text, tool calls, or explicit error details. Check the provider configuration and try again.',
197
+ )
198
+ })
199
+
200
+ it('does not invent an error when tools ran or when the run was internal', () => {
201
+ assert.equal(
202
+ deriveTerminalRunError({
203
+ errorMessage: undefined,
204
+ fullResponse: '',
205
+ streamErrors: [],
206
+ toolEvents: [{ name: 'files', input: '{"action":"list"}', output: 'spec.md' }],
207
+ internal: false,
208
+ }),
209
+ undefined,
210
+ )
211
+ assert.equal(
212
+ deriveTerminalRunError({
213
+ errorMessage: undefined,
214
+ fullResponse: '',
215
+ streamErrors: [],
216
+ toolEvents: [],
217
+ internal: true,
218
+ }),
219
+ undefined,
220
+ )
221
+ })
222
+ })
223
+
129
224
  describe('requestedToolNamesFromMessage', () => {
130
225
  it('does not infer delegate from ordinary delegation prose', () => {
131
226
  assert.deepEqual(
@@ -140,6 +235,33 @@ describe('requestedToolNamesFromMessage', () => {
140
235
  ['delegate'],
141
236
  )
142
237
  })
238
+
239
+ it('ignores negated web mentions and ordinary browsing prose', () => {
240
+ assert.deepEqual(
241
+ requestedToolNamesFromMessage('Do not browse the web for this task.'),
242
+ [],
243
+ )
244
+ assert.deepEqual(
245
+ requestedToolNamesFromMessage('Avoid using browser for this step.'),
246
+ [],
247
+ )
248
+ })
249
+
250
+ it('detects explicit email tool requests', () => {
251
+ assert.deepEqual(
252
+ requestedToolNamesFromMessage('Use the email tool to send a welcome email after signup finishes.'),
253
+ ['email'],
254
+ )
255
+ })
256
+ })
257
+
258
+ describe('hasDirectLocalCodingTools', () => {
259
+ it('treats shell and file tooling as local coding capability', () => {
260
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['files'] }), true)
261
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['shell'] }), true)
262
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['edit_file'] }), true)
263
+ assert.equal(hasDirectLocalCodingTools({ plugins: ['delegate'] }), false)
264
+ })
143
265
  })
144
266
 
145
267
  describe('normalizeAssistantArtifactLinks', () => {
@@ -217,3 +339,38 @@ describe('translateRequestedToolInvocation', () => {
217
339
  )
218
340
  })
219
341
  })
342
+
343
+ describe('reconcileConnectorDeliveryText', () => {
344
+ it('overrides false connector success claims when no send succeeded', () => {
345
+ assert.equal(
346
+ reconcileConnectorDeliveryText(
347
+ `I've successfully sent the voice note to your WhatsApp.`,
348
+ [
349
+ {
350
+ name: 'connector_message_tool',
351
+ input: '{"action":"send_voice_note"}',
352
+ output: 'Error: {"detail":{"message":"Free users cannot use library voices via the API."}}',
353
+ error: true,
354
+ },
355
+ ],
356
+ ),
357
+ `I couldn't send that through the configured connector. Free users cannot use library voices via the API.`,
358
+ )
359
+ })
360
+
361
+ it('preserves connector success confirmations when a send completed', () => {
362
+ assert.equal(
363
+ reconcileConnectorDeliveryText(
364
+ `I've successfully sent the update to your WhatsApp.`,
365
+ [
366
+ {
367
+ name: 'connector_message_tool',
368
+ input: '{"action":"send"}',
369
+ output: '{"status":"sent","to":"447700900444@s.whatsapp.net"}',
370
+ },
371
+ ],
372
+ ),
373
+ `I've successfully sent the update to your WhatsApp.`,
374
+ )
375
+ })
376
+ })