@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
@@ -219,7 +219,10 @@ export function cancelAllHeartbeatRuns(reason = 'Heartbeat disabled globally'):
219
219
  async function drainExecution(executionKey: string): Promise<void> {
220
220
  if (state.runningByExecution.has(executionKey)) return
221
221
  const q = queueForExecution(executionKey)
222
- const next = q.shift()
222
+ // Priority: user (non-heartbeat) runs go first. If a heartbeat is queued
223
+ // behind a user run, the user run takes priority.
224
+ const userIdx = q.findIndex(e => !isInternalHeartbeatRun(e.run.internal, e.run.source))
225
+ const next = userIdx >= 0 ? q.splice(userIdx, 1)[0] : q.shift()
223
226
  if (!next) return
224
227
 
225
228
  state.runningByExecution.set(executionKey, next)
@@ -416,6 +419,18 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
416
419
  cancelPendingForSession(input.sessionId, 'Cancelled by steer mode')
417
420
  }
418
421
 
422
+ // Heartbeat preemption: if a user chat arrives while a heartbeat is running,
423
+ // abort the heartbeat so the user doesn't wait. The heartbeat will retry
424
+ // on the next tick.
425
+ if (!internal && source === 'chat') {
426
+ const running = state.runningByExecution.get(executionKey)
427
+ if (running && isInternalHeartbeatRun(running.run.internal, running.run.source)) {
428
+ log.info('session-run', `Preempting heartbeat ${running.run.id} for user chat on ${input.sessionId}`)
429
+ abortSessionRuntime(running, 'Preempted by user chat')
430
+ state.runningByExecution.delete(executionKey)
431
+ }
432
+ }
433
+
419
434
  const running = state.runningByExecution.get(executionKey)
420
435
  const q = queueForExecution(executionKey)
421
436
  if (mode === 'collect' && !input.imagePath && !input.imageUrl && !input.attachedFiles?.length) {
@@ -506,15 +521,48 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
506
521
  export function getSessionRunState(sessionId: string): {
507
522
  runningRunId?: string
508
523
  queueLength: number
524
+ } {
525
+ const summary = getSessionExecutionState(sessionId)
526
+ return {
527
+ runningRunId: summary.runningRunId,
528
+ queueLength: summary.queueLength,
529
+ }
530
+ }
531
+
532
+ export function getSessionExecutionState(sessionId: string): {
533
+ runningRunId?: string
534
+ queueLength: number
535
+ hasRunning: boolean
536
+ hasQueued: boolean
537
+ hasRunningHeartbeat: boolean
538
+ hasQueuedHeartbeat: boolean
539
+ hasRunningNonHeartbeat: boolean
540
+ hasQueuedNonHeartbeat: boolean
509
541
  } {
510
542
  const executionKey = executionKeyForSession(sessionId)
511
543
  const running = state.runningByExecution.get(executionKey)
512
- const queued = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId).length
544
+ const runningMatchesSession = running?.run.sessionId === sessionId
545
+ const runningHeartbeat = Boolean(
546
+ runningMatchesSession
547
+ && isInternalHeartbeatRun(running.run.internal, running.run.source),
548
+ )
549
+ const runningNonHeartbeat = Boolean(runningMatchesSession && !runningHeartbeat)
550
+ const queuedEntries = queueForExecution(executionKey).filter((entry) => entry.run.sessionId === sessionId)
551
+ const queuedHeartbeat = queuedEntries.filter((entry) =>
552
+ isInternalHeartbeatRun(entry.run.internal, entry.run.source),
553
+ ).length
554
+ const queuedNonHeartbeat = queuedEntries.length - queuedHeartbeat
513
555
  return {
514
- runningRunId: (running?.run.sessionId === sessionId && running.run.status === 'running')
556
+ runningRunId: (runningMatchesSession && running?.run.status === 'running')
515
557
  ? running.run.id
516
558
  : undefined,
517
- queueLength: queued,
559
+ queueLength: queuedEntries.length,
560
+ hasRunning: Boolean(runningMatchesSession),
561
+ hasQueued: queuedEntries.length > 0,
562
+ hasRunningHeartbeat: runningHeartbeat,
563
+ hasQueuedHeartbeat: queuedHeartbeat > 0,
564
+ hasRunningNonHeartbeat: runningNonHeartbeat,
565
+ hasQueuedNonHeartbeat: queuedNonHeartbeat > 0,
518
566
  }
519
567
  }
520
568
 
@@ -6,6 +6,7 @@ import type { ToolBuildContext } from './context'
6
6
  import type { Plugin, PluginHooks } from '@/types'
7
7
  import { getPluginManager } from '../plugins'
8
8
  import { normalizeToolInputArgs } from './normalize-tool-args'
9
+ import { normalizeCanvasContent, summarizeCanvasContent } from '@/lib/canvas-content'
9
10
 
10
11
  /**
11
12
  * Core Canvas Execution Logic
@@ -14,6 +15,7 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
14
15
  const normalized = normalizeToolInputArgs(args)
15
16
  const action = normalized.action as string
16
17
  const content = normalized.content as string | undefined
18
+ const document = normalized.document
17
19
  try {
18
20
  const sessionId = context.sessionId
19
21
  if (!sessionId) return 'Error: no active session for canvas.'
@@ -23,13 +25,18 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
23
25
  if (!session) return 'Error: session not found.'
24
26
 
25
27
  if (action === 'present') {
26
- if (!content) return 'Error: content is required for present action.'
27
- ;(session as Record<string, unknown>).canvasContent = content
28
+ const nextContent = normalizeCanvasContent(document ?? content)
29
+ if (!nextContent) return 'Error: content or document is required for present action.'
30
+ ;(session as Record<string, unknown>).canvasContent = nextContent
28
31
  session.lastActiveAt = Date.now()
29
32
  sessions[sessionId] = session
30
33
  saveSessions(sessions)
31
34
  notify(`canvas:${sessionId}`)
32
- return JSON.stringify({ ok: true, action: 'present', contentLength: content.length })
35
+ return JSON.stringify({
36
+ ok: true,
37
+ action: 'present',
38
+ ...summarizeCanvasContent(nextContent),
39
+ })
33
40
  }
34
41
 
35
42
  if (action === 'hide') {
@@ -42,14 +49,8 @@ async function executeCanvasAction(args: Record<string, unknown>, context: { ses
42
49
  }
43
50
 
44
51
  if (action === 'snapshot') {
45
- const current = (session as Record<string, unknown>).canvasContent
46
- return JSON.stringify({
47
- ok: true,
48
- action: 'snapshot',
49
- hasContent: !!current,
50
- contentLength: typeof current === 'string' ? current.length : 0,
51
- preview: typeof current === 'string' ? current.slice(0, 500) : null,
52
- })
52
+ const current = normalizeCanvasContent((session as Record<string, unknown>).canvasContent)
53
+ return JSON.stringify({ ok: true, action: 'snapshot', ...summarizeCanvasContent(current) })
53
54
  }
54
55
 
55
56
  return `Unknown canvas action "${action}".`
@@ -73,7 +74,8 @@ const CanvasPlugin: Plugin = {
73
74
  type: 'object',
74
75
  properties: {
75
76
  action: { type: 'string', enum: ['present', 'hide', 'snapshot'] },
76
- content: { type: 'string' }
77
+ content: { type: 'string' },
78
+ document: { type: 'object', additionalProperties: true },
77
79
  },
78
80
  required: ['action']
79
81
  },
@@ -0,0 +1,138 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import {
5
+ CONNECTOR_MESSAGE_TOOL_ACTIONS,
6
+ CONNECTOR_MESSAGE_TOOL_PARAMETERS,
7
+ inferConnectorActionName,
8
+ normalizeConnectorActionInputAliases,
9
+ normalizeConnectorActionName,
10
+ } from './connector'
11
+ import { getPluginManager } from '../plugins'
12
+ import { buildSessionTools } from './index'
13
+
14
+ describe('connector_message_tool contract', () => {
15
+ it('exposes the connector actions and voice-note fields through the plugin schema', () => {
16
+ const entry = getPluginManager()
17
+ .getTools(['manage_connectors'])
18
+ .find((tool) => tool.tool.name === 'connector_message_tool')
19
+
20
+ assert.ok(entry, 'connector_message_tool should be registered for manage_connectors')
21
+
22
+ const props = (entry!.tool.parameters?.properties ?? {}) as Record<string, { type?: string; enum?: string[] }>
23
+ assert.deepEqual(props.action?.enum, [...CONNECTOR_MESSAGE_TOOL_ACTIONS])
24
+ assert.equal(props.approved?.type, 'boolean')
25
+ assert.equal(props.ptt?.type, 'boolean')
26
+ assert.equal(props.voiceText?.type, 'string')
27
+ assert.equal(props.recipientId?.type, 'string')
28
+ assert.equal(props.channel?.type, 'string')
29
+ assert.equal(Array.isArray(entry!.tool.parameters?.required), false)
30
+ assert.equal(Array.isArray((CONNECTOR_MESSAGE_TOOL_PARAMETERS as { required?: unknown }).required), false)
31
+ })
32
+
33
+ it('normalizes legacy rich-message aliases to the current connector actions', () => {
34
+ assert.equal(normalizeConnectorActionName('message_react'), 'react')
35
+ assert.equal(normalizeConnectorActionName('message_edit'), 'edit')
36
+ assert.equal(normalizeConnectorActionName('message_delete'), 'delete')
37
+ assert.equal(normalizeConnectorActionName('message_pin'), 'pin')
38
+ assert.equal(normalizeConnectorActionName('send_voice_note'), 'send_voice_note')
39
+ })
40
+
41
+ it('infers send-style actions from partial connector payloads', () => {
42
+ assert.equal(inferConnectorActionName({ voiceText: 'hello there' }), 'send_voice_note')
43
+ assert.equal(inferConnectorActionName({ followUpMessage: 'check back later', delaySec: 60 }), 'schedule_followup')
44
+ assert.equal(inferConnectorActionName({ message: 'plain text message' }), 'send')
45
+ assert.equal(inferConnectorActionName({}), null)
46
+ })
47
+
48
+ it('normalizes connector and target aliases from model-generated delivery calls', () => {
49
+ const running = [{ id: 'd81cd63b', name: 'Main Whatsapp connection' }]
50
+
51
+ assert.deepEqual(
52
+ normalizeConnectorActionInputAliases({
53
+ action: 'send_voice_note',
54
+ channel: 'Main Whatsapp connection',
55
+ recipientId: '07958148127',
56
+ }, running),
57
+ {
58
+ action: 'send_voice_note',
59
+ channel: 'Main Whatsapp connection',
60
+ recipientId: '07958148127',
61
+ connectorId: 'd81cd63b',
62
+ to: '07958148127',
63
+ },
64
+ )
65
+
66
+ assert.deepEqual(
67
+ normalizeConnectorActionInputAliases({
68
+ action: 'send_voice_note',
69
+ id: 'd81cd63b',
70
+ target: '199900000001@lid',
71
+ }, running),
72
+ {
73
+ action: 'send_voice_note',
74
+ id: 'd81cd63b',
75
+ target: '199900000001@lid',
76
+ connectorId: 'd81cd63b',
77
+ to: '199900000001@lid',
78
+ },
79
+ )
80
+ })
81
+
82
+ it('treats raw id as messageId for message actions instead of as a target alias', () => {
83
+ assert.deepEqual(
84
+ normalizeConnectorActionInputAliases({
85
+ action: 'react',
86
+ id: 'msg-123',
87
+ emoji: '👍',
88
+ }, [{ id: 'conn-1', name: 'Primary connector' }]),
89
+ {
90
+ action: 'react',
91
+ id: 'msg-123',
92
+ emoji: '👍',
93
+ messageId: 'msg-123',
94
+ },
95
+ )
96
+ })
97
+
98
+ it('buildSessionTools exposes the native connector schema instead of the legacy passthrough bridge', async () => {
99
+ const built = await buildSessionTools(process.cwd(), ['manage_connectors'], {
100
+ sessionId: 'connector-native-schema-test',
101
+ agentId: 'default',
102
+ platformAssignScope: 'self',
103
+ })
104
+
105
+ try {
106
+ const connectorTool = built.tools.find((tool) => tool.name === 'connector_message_tool')
107
+ assert.ok(connectorTool, 'connector_message_tool should be available when manage_connectors is enabled')
108
+
109
+ const schema = (connectorTool as { schema?: { safeParse: (value: unknown) => { success: boolean } } }).schema
110
+ assert.ok(schema, 'connector_message_tool should expose a validation schema')
111
+ assert.equal(schema.safeParse({ action: 'send_voice_note', approved: true, ptt: true }).success, true)
112
+ assert.equal(schema.safeParse({ voiceText: 'hello', recipientId: '07958148127', channel: 'Main Whatsapp connection' }).success, true)
113
+ assert.equal(schema.safeParse({ action: 'message_react' }).success, true)
114
+ assert.equal(schema.safeParse({}).success, true)
115
+ assert.equal(schema.safeParse({ action: 'bogus_action' }).success, false)
116
+ } finally {
117
+ await built.cleanup()
118
+ }
119
+ })
120
+
121
+ it('loads connector_message_tool when a session only has the tool-level grant alias', async () => {
122
+ const built = await buildSessionTools(process.cwd(), ['connector_message_tool'], {
123
+ sessionId: 'connector-tool-alias-test',
124
+ agentId: 'default',
125
+ platformAssignScope: 'self',
126
+ })
127
+
128
+ try {
129
+ assert.equal(
130
+ built.tools.some((tool) => tool.name === 'connector_message_tool'),
131
+ true,
132
+ 'connector_message_tool should load from its persisted approval alias',
133
+ )
134
+ } finally {
135
+ await built.cleanup()
136
+ }
137
+ })
138
+ })