@swarmclawai/swarmclaw 0.2.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 (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,708 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import crypto from 'crypto'
4
+ import { spawn, spawnSync } from 'child_process'
5
+ import { loadAgents, loadTasks, upsertTask } from '../storage'
6
+ import { log } from '../logger'
7
+ import type { ToolBuildContext } from './context'
8
+ import { truncate, tail, extractResumeIdentifier, findBinaryOnPath, MAX_OUTPUT } from './context'
9
+
10
+ export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
11
+ const tools: StructuredToolInterface[] = []
12
+ const { cwd, ctx, claudeTimeoutMs, cliProcessTimeoutMs, persistDelegateResumeId, readStoredDelegateResumeId } = bctx
13
+
14
+ const wantsClaudeDelegate = bctx.hasTool('claude_code')
15
+ const wantsCodexDelegate = bctx.hasTool('codex_cli')
16
+ const wantsOpenCodeDelegate = bctx.hasTool('opencode_cli')
17
+
18
+ if (wantsClaudeDelegate || wantsCodexDelegate || wantsOpenCodeDelegate) {
19
+ const claudeBinary = findBinaryOnPath('claude')
20
+ const codexBinary = findBinaryOnPath('codex')
21
+ const opencodeBinary = findBinaryOnPath('opencode')
22
+
23
+ if (wantsClaudeDelegate && !claudeBinary) {
24
+ log.warn('session-tools', 'Claude delegation enabled but claude binary not found', {
25
+ sessionId: ctx?.sessionId || null,
26
+ agentId: ctx?.agentId || null,
27
+ })
28
+ }
29
+ if (wantsCodexDelegate && !codexBinary) {
30
+ log.warn('session-tools', 'Codex delegation enabled but codex binary not found', {
31
+ sessionId: ctx?.sessionId || null,
32
+ agentId: ctx?.agentId || null,
33
+ })
34
+ }
35
+ if (wantsOpenCodeDelegate && !opencodeBinary) {
36
+ log.warn('session-tools', 'OpenCode delegation enabled but opencode binary not found', {
37
+ sessionId: ctx?.sessionId || null,
38
+ agentId: ctx?.agentId || null,
39
+ })
40
+ }
41
+
42
+ if (claudeBinary && wantsClaudeDelegate) {
43
+ tools.push(
44
+ tool(
45
+ async ({ task, resume, resumeId }) => {
46
+ try {
47
+ const env: NodeJS.ProcessEnv = { ...process.env }
48
+ // Running inside Claude environments can block nested `claude` launches.
49
+ // Strip all CLAUDE* vars so delegation can run as an independent subprocess.
50
+ const removedClaudeEnvKeys: string[] = []
51
+ for (const key of Object.keys(env)) {
52
+ if (key.toUpperCase().startsWith('CLAUDE')) {
53
+ removedClaudeEnvKeys.push(key)
54
+ delete env[key]
55
+ }
56
+ }
57
+
58
+ // Fast preflight: when Claude isn't authenticated, surface a clear error immediately.
59
+ const authProbe = spawnSync(claudeBinary, ['auth', 'status'], {
60
+ cwd,
61
+ env,
62
+ encoding: 'utf-8',
63
+ timeout: 8000,
64
+ })
65
+ if ((authProbe.status ?? 1) !== 0) {
66
+ let loggedIn = false
67
+ try {
68
+ const parsed = JSON.parse(authProbe.stdout || '{}') as { loggedIn?: boolean }
69
+ loggedIn = parsed.loggedIn === true
70
+ } catch {
71
+ // ignore parse issues and fall back to a generic auth guidance
72
+ }
73
+ if (!loggedIn) {
74
+ return 'Error: Claude Code CLI is not authenticated. Run `claude auth login` (or `claude setup-token`) on this machine, then retry.'
75
+ }
76
+ }
77
+
78
+ const storedResumeId = readStoredDelegateResumeId('claudeCode')
79
+ const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
80
+ ? resumeId.trim()
81
+ : (resume ? storedResumeId : null)
82
+
83
+ log.info('session-tools', 'delegate_to_claude_code start', {
84
+ sessionId: ctx?.sessionId || null,
85
+ agentId: ctx?.agentId || null,
86
+ cwd,
87
+ timeoutMs: claudeTimeoutMs,
88
+ removedClaudeEnvKeys,
89
+ resumeRequested: !!resume || !!resumeId,
90
+ resumeId: resumeIdToUse || null,
91
+ taskPreview: (task || '').slice(0, 200),
92
+ })
93
+
94
+ return new Promise<string>((resolve) => {
95
+ const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
96
+ if (resumeIdToUse) args.push('--resume', resumeIdToUse)
97
+ const child = spawn(claudeBinary, args, {
98
+ cwd,
99
+ env,
100
+ stdio: ['pipe', 'pipe', 'pipe'],
101
+ })
102
+ let stdout = ''
103
+ let stderr = ''
104
+ let stdoutBuf = ''
105
+ let assistantText = ''
106
+ let discoveredSessionId: string | null = null
107
+ let settled = false
108
+ let timedOut = false
109
+ const startedAt = Date.now()
110
+
111
+ const finish = (result: string) => {
112
+ if (settled) return
113
+ settled = true
114
+ resolve(truncate(result, MAX_OUTPUT))
115
+ }
116
+
117
+ const timeoutHandle = setTimeout(() => {
118
+ timedOut = true
119
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
120
+ setTimeout(() => {
121
+ try { child.kill('SIGKILL') } catch { /* ignore */ }
122
+ }, 5000)
123
+ }, claudeTimeoutMs)
124
+
125
+ log.info('session-tools', 'delegate_to_claude_code spawned', {
126
+ sessionId: ctx?.sessionId || null,
127
+ pid: child.pid || null,
128
+ args,
129
+ })
130
+ child.stdout?.on('data', (chunk: Buffer) => {
131
+ const text = chunk.toString()
132
+ stdout += text
133
+ if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
134
+ stdoutBuf += text
135
+ const lines = stdoutBuf.split('\n')
136
+ stdoutBuf = lines.pop() || ''
137
+ for (const line of lines) {
138
+ if (!line.trim()) continue
139
+ try {
140
+ const ev = JSON.parse(line)
141
+ if (typeof ev?.session_id === 'string' && ev.session_id.trim()) {
142
+ discoveredSessionId = ev.session_id.trim()
143
+ }
144
+ if (ev?.type === 'result' && typeof ev?.result === 'string') {
145
+ assistantText = ev.result
146
+ } else if (ev?.type === 'assistant' && Array.isArray(ev?.message?.content)) {
147
+ const textBlocks = ev.message.content
148
+ .filter((block: any) => block?.type === 'text' && typeof block?.text === 'string')
149
+ .map((block: any) => block.text)
150
+ .join('')
151
+ if (textBlocks) assistantText = textBlocks
152
+ } else if (ev?.type === 'content_block_delta' && typeof ev?.delta?.text === 'string') {
153
+ assistantText += ev.delta.text
154
+ }
155
+ } catch {
156
+ // keep raw stdout fallback when parsing fails
157
+ }
158
+ }
159
+ })
160
+ child.stderr?.on('data', (chunk: Buffer) => {
161
+ stderr += chunk.toString()
162
+ if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
163
+ })
164
+ child.on('error', (err) => {
165
+ clearTimeout(timeoutHandle)
166
+ log.error('session-tools', 'delegate_to_claude_code child error', {
167
+ sessionId: ctx?.sessionId || null,
168
+ error: err?.message || String(err),
169
+ })
170
+ finish(`Error: failed to start Claude Code CLI: ${err?.message || String(err)}`)
171
+ })
172
+ child.on('close', (code, signal) => {
173
+ clearTimeout(timeoutHandle)
174
+ const durationMs = Date.now() - startedAt
175
+ if (!discoveredSessionId) {
176
+ const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
177
+ if (guessed) discoveredSessionId = guessed
178
+ }
179
+ if (discoveredSessionId) persistDelegateResumeId('claudeCode', discoveredSessionId)
180
+ log.info('session-tools', 'delegate_to_claude_code child close', {
181
+ sessionId: ctx?.sessionId || null,
182
+ code,
183
+ signal: signal || null,
184
+ timedOut,
185
+ durationMs,
186
+ stdoutLen: stdout.length,
187
+ stderrLen: stderr.length,
188
+ discoveredSessionId,
189
+ stderrPreview: tail(stderr, 240),
190
+ })
191
+ if (timedOut) {
192
+ const msg = [
193
+ `Error: Claude Code CLI timed out after ${Math.round(claudeTimeoutMs / 1000)}s.`,
194
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
195
+ stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
196
+ 'Try increasing "Claude Code Timeout (sec)" in Settings.',
197
+ ].filter(Boolean).join('\n\n')
198
+ finish(msg)
199
+ return
200
+ }
201
+
202
+ const successText = assistantText.trim() || stdout.trim() || stderr.trim()
203
+ if (code === 0 && successText) {
204
+ const out = discoveredSessionId
205
+ ? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
206
+ : successText
207
+ finish(out)
208
+ return
209
+ }
210
+
211
+ const msg = [
212
+ `Error: Claude Code CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
213
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
214
+ stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
215
+ ].filter(Boolean).join('\n\n')
216
+ finish(msg || 'Error: Claude Code CLI returned no output.')
217
+ })
218
+
219
+ try {
220
+ child.stdin?.write(task)
221
+ child.stdin?.end()
222
+ } catch (err: any) {
223
+ clearTimeout(timeoutHandle)
224
+ finish(`Error: failed to send task to Claude Code CLI: ${err?.message || String(err)}`)
225
+ }
226
+ })
227
+ } catch (err: any) {
228
+ return `Error delegating to Claude Code: ${err.message}`
229
+ }
230
+ },
231
+ {
232
+ name: 'delegate_to_claude_code',
233
+ description: 'Delegate a complex task to Claude Code CLI. Use for tasks that need deep code understanding, multi-file refactoring, or running tests. The task runs in the session working directory.',
234
+ schema: z.object({
235
+ task: z.string().describe('Detailed description of the task for Claude Code'),
236
+ resume: z.boolean().optional().describe('If true, try to resume the last saved Claude delegation session for this SwarmClaw session'),
237
+ resumeId: z.string().optional().describe('Explicit Claude session id to resume (overrides resume=true memory)'),
238
+ }),
239
+ },
240
+ ),
241
+ )
242
+ }
243
+
244
+ if (codexBinary && wantsCodexDelegate) {
245
+ tools.push(
246
+ tool(
247
+ async ({ task, resume, resumeId }) => {
248
+ try {
249
+ const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
250
+ const removedCodexEnvKeys: string[] = []
251
+ for (const key of Object.keys(env)) {
252
+ if (key.toUpperCase().startsWith('CODEX')) {
253
+ removedCodexEnvKeys.push(key)
254
+ delete env[key]
255
+ }
256
+ }
257
+
258
+ const hasApiKey = typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim().length > 0
259
+ if (!hasApiKey) {
260
+ const loginProbe = spawnSync(codexBinary, ['login', 'status'], {
261
+ cwd,
262
+ env,
263
+ encoding: 'utf-8',
264
+ timeout: 8000,
265
+ })
266
+ const probeText = `${loginProbe.stdout || ''}\n${loginProbe.stderr || ''}`.toLowerCase()
267
+ const loggedIn = probeText.includes('logged in')
268
+ if ((loginProbe.status ?? 1) !== 0 || !loggedIn) {
269
+ return 'Error: Codex CLI is not authenticated. Run `codex login` (or set OPENAI_API_KEY), then retry.'
270
+ }
271
+ }
272
+
273
+ const storedResumeId = readStoredDelegateResumeId('codex')
274
+ const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
275
+ ? resumeId.trim()
276
+ : (resume ? storedResumeId : null)
277
+
278
+ log.info('session-tools', 'delegate_to_codex_cli start', {
279
+ sessionId: ctx?.sessionId || null,
280
+ agentId: ctx?.agentId || null,
281
+ cwd,
282
+ timeoutMs: cliProcessTimeoutMs,
283
+ removedCodexEnvKeys,
284
+ resumeRequested: !!resume || !!resumeId,
285
+ resumeId: resumeIdToUse || null,
286
+ taskPreview: (task || '').slice(0, 200),
287
+ })
288
+
289
+ return new Promise<string>((resolve) => {
290
+ const args = ['exec']
291
+ if (resumeIdToUse) args.push('resume', resumeIdToUse)
292
+ args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
293
+ const child = spawn(codexBinary, args, {
294
+ cwd,
295
+ env,
296
+ stdio: ['pipe', 'pipe', 'pipe'],
297
+ })
298
+ let stdout = ''
299
+ let stderr = ''
300
+ let settled = false
301
+ let timedOut = false
302
+ const startedAt = Date.now()
303
+ let agentText = ''
304
+ let discoveredThreadId: string | null = null
305
+ const eventErrors: string[] = []
306
+ let stdoutBuf = ''
307
+
308
+ const finish = (result: string) => {
309
+ if (settled) return
310
+ settled = true
311
+ resolve(truncate(result, MAX_OUTPUT))
312
+ }
313
+
314
+ const timeoutHandle = setTimeout(() => {
315
+ timedOut = true
316
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
317
+ setTimeout(() => {
318
+ try { child.kill('SIGKILL') } catch { /* ignore */ }
319
+ }, 5000)
320
+ }, cliProcessTimeoutMs)
321
+
322
+ log.info('session-tools', 'delegate_to_codex_cli spawned', {
323
+ sessionId: ctx?.sessionId || null,
324
+ pid: child.pid || null,
325
+ args,
326
+ })
327
+
328
+ child.stdout?.on('data', (chunk: Buffer) => {
329
+ const text = chunk.toString()
330
+ stdout += text
331
+ if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
332
+
333
+ stdoutBuf += text
334
+ const lines = stdoutBuf.split('\n')
335
+ stdoutBuf = lines.pop() || ''
336
+ for (const line of lines) {
337
+ if (!line.trim()) continue
338
+ try {
339
+ const ev = JSON.parse(line)
340
+ if (typeof ev?.thread_id === 'string' && ev.thread_id.trim()) {
341
+ discoveredThreadId = ev.thread_id.trim()
342
+ }
343
+ if (ev.type === 'item.completed' && ev.item?.type === 'agent_message' && typeof ev.item?.text === 'string') {
344
+ agentText = ev.item.text
345
+ } else if (ev.type === 'item.completed' && ev.item?.type === 'message' && ev.item?.role === 'assistant') {
346
+ const content = ev.item.content
347
+ if (Array.isArray(content)) {
348
+ const txt = content
349
+ .filter((c: any) => c?.type === 'output_text' && typeof c?.text === 'string')
350
+ .map((c: any) => c.text)
351
+ .join('')
352
+ if (txt) agentText = txt
353
+ } else if (typeof content === 'string') {
354
+ agentText = content
355
+ }
356
+ } else if (ev.type === 'error' && ev.message) {
357
+ eventErrors.push(String(ev.message))
358
+ } else if (ev.type === 'turn.failed' && ev.error?.message) {
359
+ eventErrors.push(String(ev.error.message))
360
+ }
361
+ } catch {
362
+ // Ignore non-JSON lines in parser path; raw stdout still captured above.
363
+ }
364
+ }
365
+ })
366
+ child.stderr?.on('data', (chunk: Buffer) => {
367
+ stderr += chunk.toString()
368
+ if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
369
+ })
370
+ child.on('error', (err) => {
371
+ clearTimeout(timeoutHandle)
372
+ log.error('session-tools', 'delegate_to_codex_cli child error', {
373
+ sessionId: ctx?.sessionId || null,
374
+ error: err?.message || String(err),
375
+ })
376
+ finish(`Error: failed to start Codex CLI: ${err?.message || String(err)}`)
377
+ })
378
+ child.on('close', (code, signal) => {
379
+ clearTimeout(timeoutHandle)
380
+ const durationMs = Date.now() - startedAt
381
+ if (!discoveredThreadId) {
382
+ const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
383
+ if (guessed) discoveredThreadId = guessed
384
+ }
385
+ if (discoveredThreadId) persistDelegateResumeId('codex', discoveredThreadId)
386
+ log.info('session-tools', 'delegate_to_codex_cli child close', {
387
+ sessionId: ctx?.sessionId || null,
388
+ code,
389
+ signal: signal || null,
390
+ timedOut,
391
+ durationMs,
392
+ stdoutLen: stdout.length,
393
+ stderrLen: stderr.length,
394
+ eventErrorCount: eventErrors.length,
395
+ discoveredThreadId,
396
+ stderrPreview: tail(stderr, 240),
397
+ })
398
+ if (timedOut) {
399
+ const msg = [
400
+ `Error: Codex CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
401
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
402
+ eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
403
+ 'Try increasing "CLI Process Timeout (sec)" in Settings.',
404
+ ].filter(Boolean).join('\n\n')
405
+ finish(msg)
406
+ return
407
+ }
408
+ if (code === 0 && agentText.trim()) {
409
+ const out = discoveredThreadId
410
+ ? `${agentText.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
411
+ : agentText.trim()
412
+ finish(out)
413
+ return
414
+ }
415
+ if (code === 0 && stdout.trim() && !eventErrors.length) {
416
+ const out = discoveredThreadId
417
+ ? `${stdout.trim()}\n\n[delegate_meta]\nresume_id=${discoveredThreadId}`
418
+ : stdout.trim()
419
+ finish(out)
420
+ return
421
+ }
422
+ const msg = [
423
+ `Error: Codex CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
424
+ eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
425
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
426
+ stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
427
+ ].filter(Boolean).join('\n\n')
428
+ finish(msg || 'Error: Codex CLI returned no output.')
429
+ })
430
+
431
+ try {
432
+ child.stdin?.write(task)
433
+ child.stdin?.end()
434
+ } catch (err: any) {
435
+ clearTimeout(timeoutHandle)
436
+ finish(`Error: failed to send task to Codex CLI: ${err?.message || String(err)}`)
437
+ }
438
+ })
439
+ } catch (err: any) {
440
+ return `Error delegating to Codex CLI: ${err.message}`
441
+ }
442
+ },
443
+ {
444
+ name: 'delegate_to_codex_cli',
445
+ description: 'Delegate a complex task to Codex CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
446
+ schema: z.object({
447
+ task: z.string().describe('Detailed description of the task for Codex CLI'),
448
+ resume: z.boolean().optional().describe('If true, try to resume the last saved Codex delegation thread for this SwarmClaw session'),
449
+ resumeId: z.string().optional().describe('Explicit Codex thread id to resume (overrides resume=true memory)'),
450
+ }),
451
+ },
452
+ ),
453
+ )
454
+ }
455
+
456
+ if (opencodeBinary && wantsOpenCodeDelegate) {
457
+ tools.push(
458
+ tool(
459
+ async ({ task, resume, resumeId }) => {
460
+ try {
461
+ const env: NodeJS.ProcessEnv = { ...process.env, TERM: 'dumb', NO_COLOR: '1' }
462
+ const storedResumeId = readStoredDelegateResumeId('opencode')
463
+ const resumeIdToUse = typeof resumeId === 'string' && resumeId.trim()
464
+ ? resumeId.trim()
465
+ : (resume ? storedResumeId : null)
466
+
467
+ log.info('session-tools', 'delegate_to_opencode_cli start', {
468
+ sessionId: ctx?.sessionId || null,
469
+ agentId: ctx?.agentId || null,
470
+ cwd,
471
+ timeoutMs: cliProcessTimeoutMs,
472
+ resumeRequested: !!resume || !!resumeId,
473
+ resumeId: resumeIdToUse || null,
474
+ taskPreview: (task || '').slice(0, 200),
475
+ })
476
+
477
+ return new Promise<string>((resolve) => {
478
+ const args = ['run', task, '--format', 'json']
479
+ if (resumeIdToUse) args.push('--session', resumeIdToUse)
480
+ const child = spawn(opencodeBinary, args, {
481
+ cwd,
482
+ env,
483
+ stdio: ['pipe', 'pipe', 'pipe'],
484
+ })
485
+ let stdout = ''
486
+ let stderr = ''
487
+ let discoveredSessionId: string | null = null
488
+ let parsedText = ''
489
+ const eventErrors: string[] = []
490
+ let stdoutBuf = ''
491
+ let settled = false
492
+ let timedOut = false
493
+ const startedAt = Date.now()
494
+
495
+ const finish = (result: string) => {
496
+ if (settled) return
497
+ settled = true
498
+ resolve(truncate(result, MAX_OUTPUT))
499
+ }
500
+
501
+ const timeoutHandle = setTimeout(() => {
502
+ timedOut = true
503
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
504
+ setTimeout(() => {
505
+ try { child.kill('SIGKILL') } catch { /* ignore */ }
506
+ }, 5000)
507
+ }, cliProcessTimeoutMs)
508
+
509
+ log.info('session-tools', 'delegate_to_opencode_cli spawned', {
510
+ sessionId: ctx?.sessionId || null,
511
+ pid: child.pid || null,
512
+ args: resumeIdToUse
513
+ ? ['run', '(task hidden)', '--format', 'json', '--session', resumeIdToUse]
514
+ : ['run', '(task hidden)', '--format', 'json'],
515
+ })
516
+ child.stdout?.on('data', (chunk: Buffer) => {
517
+ const text = chunk.toString()
518
+ stdout += text
519
+ if (stdout.length > MAX_OUTPUT * 8) stdout = tail(stdout, MAX_OUTPUT * 8)
520
+ stdoutBuf += text
521
+ const lines = stdoutBuf.split('\n')
522
+ stdoutBuf = lines.pop() || ''
523
+ for (const line of lines) {
524
+ if (!line.trim()) continue
525
+ try {
526
+ const ev = JSON.parse(line)
527
+ if (typeof ev?.sessionID === 'string' && ev.sessionID.trim()) {
528
+ discoveredSessionId = ev.sessionID.trim()
529
+ }
530
+ if (ev?.type === 'text' && typeof ev?.part?.text === 'string') {
531
+ parsedText += ev.part.text
532
+ } else if (ev?.type === 'error') {
533
+ const msg = typeof ev?.error === 'string'
534
+ ? ev.error
535
+ : typeof ev?.message === 'string'
536
+ ? ev.message
537
+ : 'Unknown OpenCode event error'
538
+ eventErrors.push(msg)
539
+ }
540
+ } catch {
541
+ // keep raw stdout fallback
542
+ }
543
+ }
544
+ })
545
+ child.stderr?.on('data', (chunk: Buffer) => {
546
+ stderr += chunk.toString()
547
+ if (stderr.length > MAX_OUTPUT * 8) stderr = tail(stderr, MAX_OUTPUT * 8)
548
+ })
549
+ child.on('error', (err) => {
550
+ clearTimeout(timeoutHandle)
551
+ log.error('session-tools', 'delegate_to_opencode_cli child error', {
552
+ sessionId: ctx?.sessionId || null,
553
+ error: err?.message || String(err),
554
+ })
555
+ finish(`Error: failed to start OpenCode CLI: ${err?.message || String(err)}`)
556
+ })
557
+ child.on('close', (code, signal) => {
558
+ clearTimeout(timeoutHandle)
559
+ const durationMs = Date.now() - startedAt
560
+ const guessed = extractResumeIdentifier(`${stdout}\n${stderr}`)
561
+ if (guessed) discoveredSessionId = guessed
562
+ if (discoveredSessionId) persistDelegateResumeId('opencode', discoveredSessionId)
563
+ log.info('session-tools', 'delegate_to_opencode_cli child close', {
564
+ sessionId: ctx?.sessionId || null,
565
+ code,
566
+ signal: signal || null,
567
+ timedOut,
568
+ durationMs,
569
+ stdoutLen: stdout.length,
570
+ stderrLen: stderr.length,
571
+ parsedTextLen: parsedText.length,
572
+ eventErrorCount: eventErrors.length,
573
+ discoveredSessionId,
574
+ stderrPreview: tail(stderr, 240),
575
+ })
576
+ if (timedOut) {
577
+ const msg = [
578
+ `Error: OpenCode CLI timed out after ${Math.round(cliProcessTimeoutMs / 1000)}s.`,
579
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
580
+ eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
581
+ stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
582
+ 'Try increasing "CLI Process Timeout (sec)" in Settings.',
583
+ ].filter(Boolean).join('\n\n')
584
+ finish(msg)
585
+ return
586
+ }
587
+ const successText = parsedText.trim() || stdout.trim() || stderr.trim()
588
+ if (code === 0 && successText) {
589
+ const out = discoveredSessionId
590
+ ? `${successText}\n\n[delegate_meta]\nresume_id=${discoveredSessionId}`
591
+ : successText
592
+ finish(out)
593
+ return
594
+ }
595
+ const msg = [
596
+ `Error: OpenCode CLI exited with code ${code ?? 'unknown'}${signal ? ` (signal ${signal})` : ''}.`,
597
+ eventErrors.length ? `event errors:\n${tail(eventErrors.join('\n'), 1200)}` : '',
598
+ stderr.trim() ? `stderr:\n${tail(stderr, 1500)}` : '',
599
+ stdout.trim() ? `stdout:\n${tail(stdout, 1500)}` : '',
600
+ ].filter(Boolean).join('\n\n')
601
+ finish(msg || 'Error: OpenCode CLI returned no output.')
602
+ })
603
+ })
604
+ } catch (err: any) {
605
+ return `Error delegating to OpenCode CLI: ${err.message}`
606
+ }
607
+ },
608
+ {
609
+ name: 'delegate_to_opencode_cli',
610
+ description: 'Delegate a complex task to OpenCode CLI. Use for deep coding/refactor tasks and shell-driven implementation work.',
611
+ schema: z.object({
612
+ task: z.string().describe('Detailed description of the task for OpenCode CLI'),
613
+ resume: z.boolean().optional().describe('If true, try to resume the last saved OpenCode delegation session for this SwarmClaw session'),
614
+ resumeId: z.string().optional().describe('Explicit OpenCode session id to resume (overrides resume=true memory)'),
615
+ }),
616
+ },
617
+ ),
618
+ )
619
+ }
620
+ }
621
+
622
+ // delegate_to_agent: requires orchestrator capability to be enabled
623
+ if (bctx.activeTools.includes('orchestrator') && ctx?.agentId) {
624
+ tools.push(
625
+ tool(
626
+ async ({ agentId: targetAgentId, task: taskPrompt, description: taskDesc, startImmediately }) => {
627
+ try {
628
+ const agents = loadAgents()
629
+ let target = agents[targetAgentId]
630
+ let resolvedId = targetAgentId
631
+ // Fallback: resolve by name if the ID doesn't match directly
632
+ if (!target) {
633
+ const byName = Object.values(agents).find(
634
+ (a) => a.name.toLowerCase() === targetAgentId.toLowerCase(),
635
+ )
636
+ if (byName) {
637
+ target = byName
638
+ resolvedId = byName.id
639
+ }
640
+ }
641
+ if (!target) return `Error: Agent "${targetAgentId}" not found. Use the agent directory in your system prompt to find valid agent IDs.`
642
+
643
+ const taskId = crypto.randomBytes(4).toString('hex')
644
+ const now = Date.now()
645
+ const newTask = {
646
+ id: taskId,
647
+ title: taskPrompt.slice(0, 100),
648
+ description: taskDesc || taskPrompt,
649
+ status: 'todo',
650
+ agentId: resolvedId,
651
+ sourceType: 'delegation' as const,
652
+ delegatedByAgentId: ctx.agentId!,
653
+ createdAt: now,
654
+ updatedAt: now,
655
+ comments: [{
656
+ id: crypto.randomBytes(4).toString('hex'),
657
+ author: agents[ctx.agentId!]?.name || 'Agent',
658
+ agentId: ctx.agentId!,
659
+ text: `Delegated from ${agents[ctx.agentId!]?.name || ctx.agentId}`,
660
+ createdAt: now,
661
+ }],
662
+ }
663
+ // Atomic upsert to avoid race with concurrent queue processing
664
+ upsertTask(taskId, newTask)
665
+ console.log(`[delegate] Created task ${taskId} for agent ${resolvedId}, startImmediately=${startImmediately}`)
666
+
667
+ // Verify it persisted
668
+ const verify = loadTasks()
669
+ if (!verify[taskId]) {
670
+ console.error(`[delegate] RACE: task ${taskId} not found after upsert!`)
671
+ }
672
+
673
+ if (startImmediately) {
674
+ // Lazy import to avoid circular: session-tools → queue → chat-execution → session-tools
675
+ const { enqueueTask } = await import('../queue')
676
+ enqueueTask(taskId)
677
+ console.log(`[delegate] Enqueued task ${taskId}`)
678
+ }
679
+
680
+ return JSON.stringify({
681
+ ok: true,
682
+ taskId,
683
+ agentId: resolvedId,
684
+ agentName: target.name,
685
+ message: startImmediately
686
+ ? `Task delegated to ${target.name} and queued for immediate execution. Task ID: ${taskId}.`
687
+ : `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.`,
688
+ })
689
+ } catch (err: unknown) {
690
+ return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`
691
+ }
692
+ },
693
+ {
694
+ name: 'delegate_to_agent',
695
+ 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.',
696
+ schema: z.object({
697
+ agentId: z.string().describe('ID or name of the target agent to delegate to'),
698
+ task: z.string().describe('What the target agent should do'),
699
+ description: z.string().optional().describe('Optional longer description of the task'),
700
+ startImmediately: z.boolean().optional().default(false).describe('If true, queue the task for immediate execution instead of putting it in todo'),
701
+ }),
702
+ },
703
+ ),
704
+ )
705
+ }
706
+
707
+ return tools
708
+ }