@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +53 -1
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -1,6 +1,9 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { loadSessions, saveSessions } from '../storage'
3
+ import { HumanMessage } from '@langchain/core/messages'
4
+ import { loadSessions, saveSessions, loadCredentials, decryptKey } from '../storage'
5
+ import { buildChatModel } from '../build-llm'
6
+ import { getProvider } from '@/lib/providers'
4
7
  import type { ToolBuildContext } from './context'
5
8
 
6
9
  export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterface[] {
@@ -19,8 +22,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
19
22
  const systemPromptTokens = 2000
20
23
  const status = getContextStatus(messages, systemPromptTokens, session.provider, session.model)
21
24
  return JSON.stringify(status)
22
- } catch (err: any) {
23
- return `Error: ${err.message || String(err)}`
25
+ } catch (err: unknown) {
26
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
24
27
  }
25
28
  },
26
29
  {
@@ -46,20 +49,33 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
46
49
  return JSON.stringify({ status: 'no_action', reason: 'Not enough messages to compact', messageCount: messages.length })
47
50
  }
48
51
 
49
- const generateSummary = async (text: string): Promise<string> => {
50
- const lines = text.split('\n\n').filter(Boolean)
51
- const keyLines: string[] = []
52
- for (const line of lines) {
53
- if (line.length > 20) {
54
- keyLines.push(line.slice(0, 200))
55
- }
56
- }
57
- let summary = ''
58
- for (const line of keyLines) {
59
- if (summary.length + line.length > 2000) break
60
- summary += line + '\n'
52
+ // Resolve API key for the session's provider
53
+ let apiKey: string | null = null
54
+ const providerInfo = getProvider(session.provider)
55
+ if ((providerInfo?.requiresApiKey || providerInfo?.optionalApiKey) && session.credentialId) {
56
+ try {
57
+ const creds = loadCredentials()
58
+ const cred = creds[session.credentialId]
59
+ if (cred) apiKey = decryptKey(cred.encryptedKey)
60
+ } catch { /* continue without key */ }
61
+ }
62
+
63
+ // Build LLM summarizer using the session's provider/model
64
+ const generateSummary = async (prompt: string): Promise<string> => {
65
+ const llm = buildChatModel({
66
+ provider: session.provider,
67
+ model: session.model,
68
+ apiKey,
69
+ apiEndpoint: session.apiEndpoint,
70
+ })
71
+ const response = await llm.invoke([new HumanMessage(prompt)])
72
+ if (typeof response.content === 'string') return response.content
73
+ if (Array.isArray(response.content)) {
74
+ return response.content
75
+ .map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
76
+ .join('')
61
77
  }
62
- return summary.trim() || 'Previous conversation context was pruned.'
78
+ return ''
63
79
  }
64
80
 
65
81
  const result = await summarizeAndCompact({
@@ -67,6 +83,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
67
83
  keepLastN: keep,
68
84
  agentId: ctx?.agentId || session.agentId || null,
69
85
  sessionId: ctx.sessionId,
86
+ provider: session.provider,
87
+ model: session.model,
70
88
  generateSummary,
71
89
  })
72
90
 
@@ -84,8 +102,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
84
102
  summaryAdded: result.summaryAdded,
85
103
  remainingMessages: result.messages.length,
86
104
  })
87
- } catch (err: any) {
88
- return `Error: ${err.message || String(err)}`
105
+ } catch (err: unknown) {
106
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
89
107
  }
90
108
  },
91
109
  {
@@ -20,6 +20,8 @@ import {
20
20
  } from '../storage'
21
21
  import { resolveScheduleName } from '@/lib/schedule-name'
22
22
  import { findDuplicateSchedule, type ScheduleLike } from '@/lib/schedule-dedupe'
23
+ import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
24
+ import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
23
25
  import type { ToolBuildContext } from './context'
24
26
  import { safePath, findBinaryOnPath } from './context'
25
27
 
@@ -115,6 +117,7 @@ const RESOURCE_DEFAULTS: Record<string, (parsed: any) => any> = {
115
117
  queuedAt: null,
116
118
  startedAt: null,
117
119
  completedAt: null,
120
+ priority: ['low', 'medium', 'high', 'critical'].includes(p.priority) ? p.priority : undefined,
118
121
  ...p,
119
122
  }),
120
123
  manage_schedules: (p) => {
@@ -342,6 +345,24 @@ export function buildCrudTools(bctx: ToolBuildContext): StructuredToolInterface[
342
345
  })
343
346
  }
344
347
  }
348
+ // @mention agent resolution for tasks
349
+ if (toolKey === 'manage_tasks' && parsed.description) {
350
+ const agents = loadAgents()
351
+ parsed.agentId = resolveTaskAgentFromDescription(
352
+ parsed.description,
353
+ parsed.agentId || ctx?.agentId || '',
354
+ agents,
355
+ )
356
+ }
357
+ // Task dedup
358
+ if (toolKey === 'manage_tasks') {
359
+ const fp = computeTaskFingerprint(parsed.title || 'Untitled Task', parsed.agentId || ctx?.agentId || '')
360
+ parsed.fingerprint = fp
361
+ const dupe = findDuplicateTask(all as Record<string, import('@/types').BoardTask>, { fingerprint: fp })
362
+ if (dupe) {
363
+ return JSON.stringify({ ...dupe, deduplicated: true })
364
+ }
365
+ }
345
366
  const newId = genId()
346
367
  const entry = {
347
368
  id: newId,
@@ -634,6 +634,69 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
634
634
  }
635
635
  }
636
636
 
637
+ // check_delegation_status: lets agents check on tasks they delegated
638
+ if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
639
+ tools.push(
640
+ tool(
641
+ async ({ taskId }) => {
642
+ try {
643
+ const tasks = loadTasks()
644
+ const task = tasks[taskId] as Record<string, unknown> | undefined
645
+ if (!task) return `Error: Task "${taskId}" not found.`
646
+
647
+ const status = task.status as string || 'unknown'
648
+ const result = typeof task.result === 'string' ? task.result : null
649
+ const error = typeof task.error === 'string' ? task.error : null
650
+ const agentId = task.agentId as string || ''
651
+ const agents = loadAgents()
652
+ const agent = agents[agentId]
653
+ const startedAt = typeof task.startedAt === 'number' ? task.startedAt : null
654
+ const completedAt = typeof task.completedAt === 'number' ? task.completedAt : null
655
+
656
+ const info: Record<string, unknown> = {
657
+ taskId,
658
+ status,
659
+ agentId,
660
+ agentName: agent?.name || agentId,
661
+ agentAvatarSeed: agent?.avatarSeed || null,
662
+ title: task.title || '',
663
+ }
664
+
665
+ if (startedAt) info.startedAt = new Date(startedAt).toISOString()
666
+ if (completedAt) info.completedAt = new Date(completedAt).toISOString()
667
+ if (startedAt && !completedAt && status === 'running') {
668
+ info.runningForSeconds = Math.round((Date.now() - startedAt) / 1000)
669
+ }
670
+ if (result) info.result = result.slice(0, 4000)
671
+ if (error) info.error = error.slice(0, 1000)
672
+
673
+ // Include latest comments for context
674
+ const comments = Array.isArray(task.comments) ? task.comments as Array<{ text: string; author: string; createdAt: number }> : []
675
+ if (comments.length > 0) {
676
+ const latest = comments.slice(-3).map((c) => ({
677
+ author: c.author,
678
+ text: (c.text || '').slice(0, 500),
679
+ time: new Date(c.createdAt).toISOString(),
680
+ }))
681
+ info.latestComments = latest
682
+ }
683
+
684
+ return JSON.stringify(info)
685
+ } catch (err: unknown) {
686
+ return `Error checking task: ${err instanceof Error ? err.message : String(err)}`
687
+ }
688
+ },
689
+ {
690
+ name: 'check_delegation_status',
691
+ description: 'Check the status and result of a delegated task. Use this after delegate_to_agent to monitor progress. Returns status (todo/queued/running/completed/failed), result if completed, and latest comments.',
692
+ schema: z.object({
693
+ taskId: z.string().describe('The task ID returned by delegate_to_agent'),
694
+ }),
695
+ },
696
+ ),
697
+ )
698
+ }
699
+
637
700
  // delegate_to_agent: requires "Assign to Other Agents" (platformAssignScope: 'all')
638
701
  if (ctx?.platformAssignScope === 'all' && ctx?.agentId) {
639
702
  tools.push(
@@ -698,9 +761,10 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
698
761
  taskId,
699
762
  agentId: resolvedId,
700
763
  agentName: target.name,
764
+ agentAvatarSeed: target.avatarSeed || null,
701
765
  message: startImmediately
702
- ? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}.`
703
- : `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo. Ask the user if they want to start it now — call again with startImmediately: true to queue it.`,
766
+ ? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}. Use check_delegation_status to monitor progress.`
767
+ : `Task delegated to ${target.name}. Task ID: ${taskId}. Status: todo (not auto-started). Use delegate_to_agent with startImmediately: true to queue it.`,
704
768
  })
705
769
  } catch (err: unknown) {
706
770
  return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`
@@ -708,12 +772,12 @@ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterf
708
772
  },
709
773
  {
710
774
  name: 'delegate_to_agent',
711
- description: 'Delegate a task to another agent. Creates a task on the task board. By default the task goes to "todo" status. Set startImmediately=true to queue it for execution right away. Ask the user to confirm before starting immediately.',
775
+ description: 'Delegate a task to another agent. Creates a task on the task board and queues it for immediate execution by default. Set startImmediately=false if you want the task to go to "todo" status instead.',
712
776
  schema: z.object({
713
777
  agentId: z.string().describe('ID or name of the target agent to delegate to'),
714
778
  task: z.string().describe('What the target agent should do'),
715
779
  description: z.string().optional().describe('Optional longer description of the task'),
716
- startImmediately: z.boolean().optional().default(false).describe('If true, queue the task for immediate execution instead of putting it in todo'),
780
+ startImmediately: z.boolean().optional().default(true).describe('If true (default), queue the task for immediate execution. Set false to put in todo for manual start.'),
717
781
  }),
718
782
  },
719
783
  ),
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { execFile } from 'child_process'
4
+ import { promisify } from 'util'
5
+ import type { ToolBuildContext } from './context'
6
+ import { findBinaryOnPath, safePath, truncate, MAX_OUTPUT } from './context'
7
+
8
+ const execFileAsync = promisify(execFile)
9
+
10
+ const GIT_ACTIONS = [
11
+ 'status', 'log', 'diff', 'commit', 'add', 'push', 'pull',
12
+ 'branch', 'checkout', 'stash', 'merge', 'clone', 'remote',
13
+ 'tag', 'reset', 'show',
14
+ ] as const
15
+
16
+ export function buildGitTools(bctx: ToolBuildContext): StructuredToolInterface[] {
17
+ if (!bctx.hasTool('git')) return []
18
+
19
+ const gitPath = findBinaryOnPath('git')
20
+ if (!gitPath) return []
21
+
22
+ return [
23
+ tool(
24
+ async ({ action, args, repoPath, timeoutSec }) => {
25
+ try {
26
+ const cwd = repoPath ? safePath(bctx.cwd, repoPath) : bctx.cwd
27
+ const timeout = Math.max(5, Math.min(timeoutSec ?? 60, 300)) * 1000
28
+
29
+ // Verify we're in a git repo (except for clone)
30
+ if (action !== 'clone') {
31
+ try {
32
+ await execFileAsync(gitPath, ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 })
33
+ } catch {
34
+ return JSON.stringify({ error: `Not a git repository: ${cwd}` })
35
+ }
36
+ }
37
+
38
+ const cmdArgs = [action, ...(args ?? [])]
39
+ const result = await execFileAsync(gitPath, cmdArgs, {
40
+ cwd,
41
+ timeout,
42
+ maxBuffer: MAX_OUTPUT,
43
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
44
+ })
45
+ return JSON.stringify({
46
+ exitCode: 0,
47
+ stdout: truncate(result.stdout ?? '', MAX_OUTPUT),
48
+ stderr: truncate(result.stderr ?? '', MAX_OUTPUT),
49
+ })
50
+ } catch (err: unknown) {
51
+ const execErr = err as { code?: number; stdout?: string; stderr?: string; message?: string }
52
+ return JSON.stringify({
53
+ exitCode: execErr.code ?? 1,
54
+ stdout: truncate(execErr.stdout ?? '', MAX_OUTPUT),
55
+ stderr: truncate(execErr.stderr ?? execErr.message ?? String(err), MAX_OUTPUT),
56
+ })
57
+ }
58
+ },
59
+ {
60
+ name: 'git',
61
+ description: 'Run git operations. Verify the repo exists before committing or pushing. Use args for subcommand flags (e.g. args: ["-m", "message"] for commit).',
62
+ schema: z.object({
63
+ action: z.enum(GIT_ACTIONS).describe('Git subcommand to run'),
64
+ args: z.array(z.string()).optional().describe('Additional arguments (e.g. ["-m", "fix: typo"], ["--oneline", "-n", "5"])'),
65
+ repoPath: z.string().optional().describe('Relative path to git repo (defaults to working directory)'),
66
+ timeoutSec: z.number().optional().describe('Timeout in seconds (default 60, max 300)'),
67
+ }),
68
+ },
69
+ ),
70
+ ]
71
+ }
@@ -0,0 +1,57 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import type { ToolBuildContext } from './context'
4
+ import { truncate, MAX_OUTPUT } from './context'
5
+
6
+ export function buildHttpTools(bctx: ToolBuildContext): StructuredToolInterface[] {
7
+ if (!bctx.hasTool('http_request')) return []
8
+
9
+ return [
10
+ tool(
11
+ async ({ method, url, headers, body, timeoutSec, followRedirects }) => {
12
+ try {
13
+ const timeout = Math.max(1, Math.min(timeoutSec ?? 30, 120)) * 1000
14
+ const init: RequestInit = {
15
+ method,
16
+ headers: (headers ?? undefined) as Record<string, string> | undefined,
17
+ signal: AbortSignal.timeout(timeout),
18
+ }
19
+ if (body && method !== 'GET' && method !== 'HEAD') {
20
+ init.body = body
21
+ }
22
+ if (followRedirects === false) {
23
+ init.redirect = 'manual'
24
+ }
25
+ const res = await fetch(url, init)
26
+ const resHeaders: Record<string, string> = {}
27
+ for (const key of ['content-type', 'location', 'x-request-id', 'retry-after', 'content-length']) {
28
+ const val = res.headers.get(key)
29
+ if (val) resHeaders[key] = val
30
+ }
31
+ let resBody: string
32
+ const ct = res.headers.get('content-type') ?? ''
33
+ if (ct.includes('image/') || ct.includes('audio/') || ct.includes('video/') || ct.includes('application/octet-stream')) {
34
+ resBody = `[binary content, ${res.headers.get('content-length') ?? 'unknown'} bytes]`
35
+ } else {
36
+ resBody = truncate(await res.text(), MAX_OUTPUT)
37
+ }
38
+ return JSON.stringify({ status: res.status, statusText: res.statusText, headers: resHeaders, body: resBody })
39
+ } catch (err: unknown) {
40
+ return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
41
+ }
42
+ },
43
+ {
44
+ name: 'http_request',
45
+ description: 'Make an HTTP API request. Supports all methods, custom headers, and request bodies. Returns status, headers, and body.',
46
+ schema: z.object({
47
+ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).describe('HTTP method'),
48
+ url: z.string().describe('Full URL to request'),
49
+ headers: z.record(z.string(), z.string()).optional().describe('Request headers as key-value pairs'),
50
+ body: z.string().optional().describe('Request body (JSON string, form data, or plain text). Ignored for GET/HEAD.'),
51
+ timeoutSec: z.number().optional().describe('Timeout in seconds (default 30, max 120)'),
52
+ followRedirects: z.boolean().optional().describe('Follow redirects (default true). Set false to inspect redirect responses.'),
53
+ }),
54
+ },
55
+ ),
56
+ ]
57
+ }
@@ -16,6 +16,11 @@ import { buildConnectorTools } from './connector'
16
16
  import { buildContextTools } from './context-mgmt'
17
17
  import { buildSandboxTools } from './sandbox'
18
18
  import { buildOpenClawNodeTools } from './openclaw-nodes'
19
+ import { buildChatroomTools } from './chatroom'
20
+ import { buildSubagentTools } from './subagent'
21
+ import { buildCanvasTools } from './canvas'
22
+ import { buildHttpTools } from './http'
23
+ import { buildGitTools } from './git'
19
24
 
20
25
  export type { ToolContext, SessionToolsResult }
21
26
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
@@ -97,6 +102,11 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
97
102
  ...buildContextTools(bctx),
98
103
  ...buildSandboxTools(bctx),
99
104
  ...buildOpenClawNodeTools(bctx),
105
+ ...buildChatroomTools(bctx),
106
+ ...buildSubagentTools(bctx),
107
+ ...buildCanvasTools(bctx),
108
+ ...buildHttpTools(bctx),
109
+ ...buildGitTools(bctx),
100
110
  )
101
111
 
102
112
  // ---------------------------------------------------------------------------
@@ -15,7 +15,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
15
15
 
16
16
  tools.push(
17
17
  tool(
18
- async ({ action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags }) => {
18
+ async (input) => {
19
+ const { action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags, pinned, sharedWith } = input as Record<string, any>
19
20
  try {
20
21
  const scopeMode = scope || 'auto'
21
22
  const currentAgentId = ctx?.agentId || null
@@ -59,6 +60,7 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
59
60
 
60
61
  const formatEntry = (m: any) => {
61
62
  let line = `[${m.id}] (${m.agentId ? `agent:${m.agentId}` : 'shared'}) ${m.category}/${m.title}: ${m.content}`
63
+ if (m.reinforcementCount) line += ` (reinforced ×${m.reinforcementCount})`
62
64
  if (m.references?.length) {
63
65
  line += `\n refs: ${m.references.map((r: any) => {
64
66
  const core = r.path || r.title || r.type
@@ -98,6 +100,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
98
100
  image: storedImage,
99
101
  imagePath: storedImage?.path || undefined,
100
102
  linkedMemoryIds,
103
+ pinned: pinned === true,
104
+ sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
101
105
  })
102
106
  const memoryScope = entry.agentId ? 'agent' : 'shared'
103
107
  let result = `Stored ${memoryScope} memory "${key}" (id: ${entry.id})`
@@ -220,6 +224,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
220
224
  linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
221
225
  targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
222
226
  tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
227
+ pinned: z.boolean().optional().describe('Mark memory as pinned (always preloaded in agent context). For store action.'),
228
+ sharedWith: z.array(z.string()).optional().describe('Agent IDs to share this memory with (for store action). They can read it in their context.'),
223
229
  }),
224
230
  },
225
231
  ),
@@ -260,16 +260,24 @@ export async function getSearchProvider(settings: Partial<AppSettings>): Promise
260
260
  return new SearXNGProvider(url)
261
261
  }
262
262
  case 'tavily': {
263
- const { getSecret } = await import('../storage')
264
- const secret = await getSecret('tavily')
265
- if (!secret?.value) throw new Error('Tavily requires an API key. Add a secret named "tavily" in Secrets.')
266
- return new TavilyProvider(secret.value)
263
+ let apiKey = settings.tavilyApiKey
264
+ if (!apiKey) {
265
+ const { getSecret } = await import('../storage')
266
+ const secret = await getSecret('tavily')
267
+ apiKey = secret?.value ?? null
268
+ }
269
+ if (!apiKey) throw new Error('Tavily requires an API key. Set one in Settings > Web Search.')
270
+ return new TavilyProvider(apiKey)
267
271
  }
268
272
  case 'brave': {
269
- const { getSecret } = await import('../storage')
270
- const secret = await getSecret('brave')
271
- if (!secret?.value) throw new Error('Brave Search requires an API key. Add a secret named "brave" in Secrets.')
272
- return new BraveProvider(secret.value)
273
+ let apiKey = settings.braveApiKey
274
+ if (!apiKey) {
275
+ const { getSecret } = await import('../storage')
276
+ const secret = await getSecret('brave')
277
+ apiKey = secret?.value ?? null
278
+ }
279
+ if (!apiKey) throw new Error('Brave Search requires an API key. Set one in Settings > Web Search.')
280
+ return new BraveProvider(apiKey)
273
281
  }
274
282
  default:
275
283
  return new DuckDuckGoProvider()
@@ -0,0 +1,106 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { genId } from '@/lib/id'
4
+ import { loadAgents, loadSessions, saveSessions } from '../storage'
5
+ import { executeSessionChatTurn } from '../chat-execution'
6
+ import { log } from '../logger'
7
+ import type { ToolBuildContext } from './context'
8
+
9
+ const MAX_RECURSION_DEPTH = 3
10
+
11
+ function getSessionDepth(sessionId: string | undefined): number {
12
+ if (!sessionId) return 0
13
+ const sessions = loadSessions()
14
+ let depth = 0
15
+ let current = sessionId
16
+ while (current && depth < MAX_RECURSION_DEPTH + 1) {
17
+ const session = sessions[current]
18
+ if (!session?.parentSessionId) break
19
+ current = session.parentSessionId
20
+ depth++
21
+ }
22
+ return depth
23
+ }
24
+
25
+ export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
26
+ const { ctx, hasTool } = bctx
27
+ if (!hasTool('spawn_subagent')) return []
28
+
29
+ return [
30
+ tool(
31
+ async ({ agentId, message, cwd }) => {
32
+ try {
33
+ // Validate agent exists
34
+ const agents = loadAgents()
35
+ const agent = agents[agentId]
36
+ if (!agent) return `Error: Agent "${agentId}" not found. Available agents: ${Object.values(agents).map((a) => `"${a.id}" (${a.name})`).join(', ')}`
37
+
38
+ // Check recursion depth
39
+ const depth = getSessionDepth(ctx?.sessionId ?? undefined)
40
+ if (depth >= MAX_RECURSION_DEPTH) {
41
+ return `Error: Maximum subagent recursion depth (${MAX_RECURSION_DEPTH}) reached. Cannot spawn further subagents.`
42
+ }
43
+
44
+ // Create ephemeral session
45
+ const sessionId = genId()
46
+ const now = Date.now()
47
+ const sessions = loadSessions()
48
+ sessions[sessionId] = {
49
+ id: sessionId,
50
+ name: `subagent-${agent.name}-${sessionId.slice(0, 6)}`,
51
+ cwd: cwd || bctx.cwd,
52
+ user: 'agent',
53
+ provider: agent.provider,
54
+ model: agent.model,
55
+ credentialId: agent.credentialId || null,
56
+ fallbackCredentialIds: agent.fallbackCredentialIds || [],
57
+ apiEndpoint: agent.apiEndpoint || null,
58
+ claudeSessionId: null,
59
+ messages: [],
60
+ createdAt: now,
61
+ lastActiveAt: now,
62
+ sessionType: 'orchestrated',
63
+ agentId: agent.id,
64
+ parentSessionId: ctx?.sessionId || null,
65
+ tools: agent.tools || [],
66
+ }
67
+ saveSessions(sessions)
68
+
69
+ log.info('subagent', `Spawning subagent "${agent.name}" (depth=${depth + 1})`, {
70
+ parentSessionId: ctx?.sessionId,
71
+ childSessionId: sessionId,
72
+ agentId,
73
+ })
74
+
75
+ // Execute the chat turn
76
+ const result = await executeSessionChatTurn({
77
+ sessionId,
78
+ message,
79
+ internal: true,
80
+ source: 'subagent',
81
+ })
82
+
83
+ return JSON.stringify({
84
+ agentId,
85
+ agentName: agent.name,
86
+ sessionId,
87
+ response: result.text.slice(0, 8000),
88
+ toolEvents: result.toolEvents?.length || 0,
89
+ error: result.error || null,
90
+ })
91
+ } catch (err: unknown) {
92
+ return `Error spawning subagent: ${err instanceof Error ? err.message : String(err)}`
93
+ }
94
+ },
95
+ {
96
+ name: 'spawn_subagent',
97
+ description: `Delegate a task to another agent. The subagent runs independently and returns its response. Use this to leverage specialized agents for subtasks. Max recursion depth: ${MAX_RECURSION_DEPTH}.`,
98
+ schema: z.object({
99
+ agentId: z.string().describe('ID of the agent to delegate to'),
100
+ message: z.string().describe('The message/task to send to the subagent'),
101
+ cwd: z.string().optional().describe('Optional working directory for the subagent (defaults to current)'),
102
+ }),
103
+ },
104
+ ),
105
+ ]
106
+ }