@swarmclawai/swarmclaw 0.7.1 → 0.7.3

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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,24 +1,49 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { spawn, spawnSync } from 'child_process'
3
+ import { spawn, spawnSync, type ChildProcess } from 'child_process'
4
4
  import type { ToolBuildContext } from './context'
5
5
  import { truncate, findBinaryOnPath, MAX_OUTPUT } 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 {
10
+ appendDelegationCheckpoint,
11
+ cancelDelegationJob,
12
+ completeDelegationJob,
13
+ createDelegationJob,
14
+ failDelegationJob,
15
+ getDelegationJob,
16
+ listDelegationJobs,
17
+ recoverStaleDelegationJobs,
18
+ registerDelegationRuntime,
19
+ startDelegationJob,
20
+ } from '../delegation-jobs'
21
+ import { markProviderFailure, markProviderSuccess } from '../provider-health'
9
22
 
10
23
  const MAX_DELEGATION_CHAIN_HOPS = 128
24
+ const DELEGATE_BACKEND_ORDER: DelegateBackend[] = ['claude', 'codex', 'opencode', 'gemini']
11
25
 
12
26
  interface DelegateContext {
27
+ id?: string
28
+ sessionId?: string | null
29
+ agentId?: string | null
30
+ jobId?: string | null
13
31
  cwd?: string
14
32
  claudeTimeoutMs?: number
15
- readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode') => string | null
16
- persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode', id: string) => void
17
- ctx?: { platformAssignScope?: string; agentId?: string | null }
33
+ readStoredDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini') => string | null
34
+ persistDelegateResumeId?: (key: 'claudeCode' | 'codex' | 'opencode' | 'gemini', id: string | null | undefined) => void
35
+ ctx?: { platformAssignScope?: string; agentId?: string | null; sessionId?: string | null }
36
+ hasPlugin?: (name: string) => boolean
37
+ /** @deprecated Use hasPlugin */
18
38
  hasTool?: (name: string) => boolean
19
39
  }
20
40
 
21
- type DelegateBackend = 'claude' | 'codex' | 'opencode'
41
+ type DelegateBackend = 'claude' | 'codex' | 'opencode' | 'gemini'
42
+
43
+ interface DelegateRuntimeState {
44
+ child?: ChildProcess | null
45
+ cancel?: () => void
46
+ }
22
47
 
23
48
  function asTaskRecord(value: unknown): Record<string, unknown> | null {
24
49
  return value && typeof value === 'object' ? value as Record<string, unknown> : null
@@ -61,11 +86,169 @@ function _computeDelegationDepth(
61
86
  return depth
62
87
  }
63
88
 
64
- /**
65
- * Core Delegate Execution Logic
66
- */
67
- async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
68
- const normalized = normalizeToolInputArgs(args)
89
+ function sleep(ms: number) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms))
91
+ }
92
+
93
+ function buildDelegateContextFromSessionish(session: unknown): DelegateContext {
94
+ const record = session && typeof session === 'object' ? session as Record<string, unknown> : {}
95
+ const sessionId = typeof record.id === 'string'
96
+ ? record.id
97
+ : typeof record.sessionId === 'string'
98
+ ? record.sessionId
99
+ : null
100
+ const agentId = typeof record.agentId === 'string' ? record.agentId : null
101
+ const platformAssignScope = typeof record.platformAssignScope === 'string' ? record.platformAssignScope : undefined
102
+ const storedResumeIds = record.delegateResumeIds && typeof record.delegateResumeIds === 'object'
103
+ ? record.delegateResumeIds as Record<string, unknown>
104
+ : null
105
+
106
+ return {
107
+ cwd: typeof record.cwd === 'string' ? record.cwd : process.cwd(),
108
+ claudeTimeoutMs: typeof record.claudeTimeoutMs === 'number' ? record.claudeTimeoutMs : undefined,
109
+ readStoredDelegateResumeId: typeof record.readStoredDelegateResumeId === 'function'
110
+ ? record.readStoredDelegateResumeId as DelegateContext['readStoredDelegateResumeId']
111
+ : (key) => {
112
+ const raw = storedResumeIds?.[key]
113
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : null
114
+ },
115
+ persistDelegateResumeId: typeof record.persistDelegateResumeId === 'function'
116
+ ? record.persistDelegateResumeId as DelegateContext['persistDelegateResumeId']
117
+ : undefined,
118
+ id: typeof record.id === 'string' ? record.id : undefined,
119
+ sessionId,
120
+ agentId,
121
+ ctx: {
122
+ sessionId,
123
+ agentId,
124
+ platformAssignScope,
125
+ },
126
+ }
127
+ }
128
+
129
+ function buildDelegateResumePatch(bctx: DelegateContext) {
130
+ const resumeIds = {
131
+ claudeCode: bctx.readStoredDelegateResumeId?.('claudeCode') || null,
132
+ codex: bctx.readStoredDelegateResumeId?.('codex') || null,
133
+ opencode: bctx.readStoredDelegateResumeId?.('opencode') || null,
134
+ gemini: bctx.readStoredDelegateResumeId?.('gemini') || null,
135
+ }
136
+ const resumeId = resumeIds.claudeCode || resumeIds.codex || resumeIds.opencode || resumeIds.gemini || null
137
+ return { resumeIds, resumeId }
138
+ }
139
+
140
+ function coerceDelegateBackend(value: unknown): DelegateBackend | null {
141
+ const normalized = String(value || '').trim().toLowerCase()
142
+ if (!normalized) return null
143
+ if (['claude', 'claude code', 'claude-code', 'claude_code'].includes(normalized)) return 'claude'
144
+ if (['codex', 'codex cli', 'codex-cli', 'codex_cli'].includes(normalized)) return 'codex'
145
+ if (['opencode', 'open code', 'open-code', 'open_code'].includes(normalized)) return 'opencode'
146
+ if (['gemini', 'gemini cli', 'gemini-cli', 'gemini_cli'].includes(normalized)) return 'gemini'
147
+ return null
148
+ }
149
+
150
+ function buildDelegateTaskFromPayload(normalized: Record<string, unknown>): string | null {
151
+ const action = String(normalized.action || '').trim().toLowerCase()
152
+ const target = [
153
+ normalized.target,
154
+ normalized.path,
155
+ normalized.filePath,
156
+ normalized.filename,
157
+ normalized.name,
158
+ ].find((value) => typeof value === 'string' && value.trim()) as string | undefined
159
+ const content = typeof normalized.content === 'string' ? normalized.content.trim() : ''
160
+ const taskName = typeof normalized.name === 'string' ? normalized.name.trim() : ''
161
+ const files = Array.isArray(normalized.files) ? normalized.files : []
162
+ const fileInstructions = files
163
+ .filter((entry): entry is Record<string, unknown> => !!entry && typeof entry === 'object' && !Array.isArray(entry))
164
+ .map((entry) => {
165
+ const filePath = typeof entry.path === 'string'
166
+ ? entry.path.trim()
167
+ : typeof entry.filePath === 'string'
168
+ ? entry.filePath.trim()
169
+ : typeof entry.filename === 'string'
170
+ ? entry.filename.trim()
171
+ : ''
172
+ const fileContent = typeof entry.content === 'string' ? entry.content.trim() : ''
173
+ if (!filePath && !fileContent) return ''
174
+ if (filePath && fileContent) {
175
+ return `Create or update "${filePath}" with this content:\n\n${fileContent}`
176
+ }
177
+ if (filePath) return `Create or update "${filePath}".`
178
+ return `Create or update a file with this content:\n\n${fileContent}`
179
+ })
180
+ .filter(Boolean)
181
+
182
+ if (['write', 'create', 'create_file', 'create-file', 'createfile'].includes(action)) {
183
+ if (target && content) return `Create or overwrite the file "${target}" with this content:\n\n${content}`
184
+ if (target) return `Create the file "${target}".`
185
+ }
186
+ if (['edit', 'update', 'modify'].includes(action)) {
187
+ if (target && content) return `Update the file "${target}" with this content:\n\n${content}`
188
+ if (target) return `Update the file "${target}".`
189
+ }
190
+ if (target && content) return `Perform the "${action || 'requested'}" task against "${target}" using this content:\n\n${content}`
191
+ if (target) return `Perform the "${action || 'requested'}" task against "${target}".`
192
+ if (fileInstructions.length > 0) {
193
+ const intro = taskName || 'Perform the delegated file task.'
194
+ return `${intro}\n\n${fileInstructions.join('\n\n')}`
195
+ }
196
+ if (content) return `Perform the delegated task with this content:\n\n${content}`
197
+ if (taskName) return taskName
198
+ return null
199
+ }
200
+
201
+ function normalizeDelegateArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
202
+ const normalized = normalizeToolInputArgs(rawArgs)
203
+ const backend = coerceDelegateBackend(
204
+ normalized.backend
205
+ ?? normalized.tool_name
206
+ ?? normalized.toolName
207
+ ?? normalized.delegate
208
+ ?? normalized.provider,
209
+ )
210
+ if (backend && !normalized.backend) normalized.backend = backend
211
+ if (typeof normalized.task !== 'string' && typeof normalized.prompt === 'string') normalized.task = normalized.prompt
212
+ const action = String(normalized.action || '').trim().toLowerCase()
213
+ const isLifecycleAction = ['status', 'list', 'wait', 'cancel'].includes(action)
214
+ if (!isLifecycleAction) {
215
+ if (typeof normalized.task !== 'string' || !normalized.task.trim()) {
216
+ const synthesized = buildDelegateTaskFromPayload(normalized)
217
+ if (synthesized) normalized.task = synthesized
218
+ }
219
+ normalized.action = 'start'
220
+ }
221
+ return normalized
222
+ }
223
+
224
+ function resolveDelegateSessionId(bctx: DelegateContext): string | null {
225
+ const nested = typeof bctx.ctx?.sessionId === 'string' ? bctx.ctx.sessionId.trim() : ''
226
+ if (nested) return nested
227
+ const direct = typeof bctx.sessionId === 'string' ? bctx.sessionId.trim() : ''
228
+ if (direct) return direct
229
+ const legacy = typeof bctx.id === 'string' ? bctx.id.trim() : ''
230
+ return legacy || null
231
+ }
232
+
233
+ function bindDelegateRuntime(runtime: DelegateRuntimeState | undefined, child: ChildProcess) {
234
+ if (!runtime) return
235
+ runtime.child = child
236
+ runtime.cancel = () => {
237
+ try {
238
+ child.kill('SIGTERM')
239
+ } catch {
240
+ // best-effort cancel
241
+ }
242
+ }
243
+ const clear = () => {
244
+ if (runtime.child === child) runtime.child = null
245
+ }
246
+ child.once('close', clear)
247
+ child.once('error', clear)
248
+ }
249
+
250
+ async function runDelegateBackend(args: Record<string, unknown>, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
251
+ const normalized = normalizeDelegateArgs(args)
69
252
  const task = normalized.task as string
70
253
  const backend = ((normalized.backend as string) || 'claude') as DelegateBackend
71
254
  const resume = normalized.resume as boolean
@@ -74,16 +257,214 @@ async function executeDelegateAction(args: Record<string, unknown>, bctx: Delega
74
257
  claude: findBinaryOnPath('claude'),
75
258
  codex: findBinaryOnPath('codex'),
76
259
  opencode: findBinaryOnPath('opencode'),
260
+ gemini: findBinaryOnPath('gemini'),
77
261
  }
78
262
  const binary = backends[backend as keyof typeof backends]
79
263
  if (!binary) return `Error: Backend "${backend}" unavailable.`
80
264
 
81
- if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx)
82
- if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx)
83
- if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx)
265
+ if (backend === 'claude') return runClaudeDelegate(binary, task, resume, resumeId, bctx, runtime)
266
+ if (backend === 'codex') return runCodexDelegate(binary, task, resume, resumeId, bctx, runtime)
267
+ if (backend === 'opencode') return runOpenCodeDelegate(binary, task, resume, resumeId, bctx, runtime)
268
+ if (backend === 'gemini') return runGeminiDelegate(binary, task, resume, resumeId, bctx, runtime)
84
269
  return `Error: Unsupported backend "${backend}".`
85
270
  }
86
271
 
272
+ function providerIdForBackend(backend: DelegateBackend): string {
273
+ if (backend === 'claude') return 'claude-cli'
274
+ if (backend === 'codex') return 'codex-cli'
275
+ if (backend === 'opencode') return 'opencode-cli'
276
+ return 'gemini-cli'
277
+ }
278
+
279
+ function fallbackOrderForBackend(requested: DelegateBackend): DelegateBackend[] {
280
+ return [requested, ...DELEGATE_BACKEND_ORDER.filter((backend) => backend !== requested)]
281
+ }
282
+
283
+ function isRecoverableDelegateFailure(result: string): boolean {
284
+ const normalized = String(result || '').trim().toLowerCase()
285
+ if (!normalized.startsWith('error:')) return false
286
+ return [
287
+ 'not authenticated',
288
+ 'backend "',
289
+ 'unavailable',
290
+ 'enoent',
291
+ 'not found',
292
+ 'command not found',
293
+ 'spawn ',
294
+ 'eacces',
295
+ 'permission denied',
296
+ ].some((needle) => normalized.includes(needle))
297
+ }
298
+
299
+ function summarizeDelegateAttempts(
300
+ requested: DelegateBackend,
301
+ attempts: Array<{ backend: DelegateBackend; result: string }>,
302
+ ): string {
303
+ const summary = attempts
304
+ .map(({ backend, result }) => `${backend}: ${result.replace(/^Error:\s*/i, '').trim() || result.trim()}`)
305
+ .join(' | ')
306
+ return `Error: Delegate backend "${requested}" could not complete the task. ${summary}. Continue with another available tool instead of stopping.`
307
+ }
308
+
309
+ async function runDelegateBackendWithFallback(
310
+ args: Record<string, unknown>,
311
+ bctx: DelegateContext,
312
+ runtime?: DelegateRuntimeState,
313
+ opts?: { onAttempt?: (backend: DelegateBackend, attemptIndex: number) => void; onFallback?: (from: DelegateBackend, to: DelegateBackend, reason: string) => void },
314
+ ): Promise<{ backend: DelegateBackend; result: string; attempts: Array<{ backend: DelegateBackend; result: string }> }> {
315
+ const normalized = normalizeDelegateArgs(args)
316
+ const requested = ((normalized.backend as string) || 'claude') as DelegateBackend
317
+ const orderedBackends = fallbackOrderForBackend(requested)
318
+ const attempts: Array<{ backend: DelegateBackend; result: string }> = []
319
+
320
+ for (const [index, backend] of orderedBackends.entries()) {
321
+ opts?.onAttempt?.(backend, index)
322
+ const result = await runDelegateBackend({ ...normalized, backend }, bctx, runtime)
323
+ attempts.push({ backend, result })
324
+ if (/^Error:/i.test(result.trim())) {
325
+ markProviderFailure(providerIdForBackend(backend), result)
326
+ } else {
327
+ markProviderSuccess(providerIdForBackend(backend))
328
+ return { backend, result, attempts }
329
+ }
330
+
331
+ const nextBackend = orderedBackends[index + 1]
332
+ if (nextBackend && isRecoverableDelegateFailure(result)) {
333
+ opts?.onFallback?.(backend, nextBackend, result)
334
+ continue
335
+ }
336
+ return {
337
+ backend,
338
+ result: attempts.length > 1 ? summarizeDelegateAttempts(requested, attempts) : result,
339
+ attempts,
340
+ }
341
+ }
342
+
343
+ return {
344
+ backend: requested,
345
+ result: summarizeDelegateAttempts(requested, attempts),
346
+ attempts,
347
+ }
348
+ }
349
+
350
+ async function waitForDelegateJob(jobId: string, timeoutSec = 30): Promise<string> {
351
+ const timeoutAt = Date.now() + Math.max(1, timeoutSec) * 1000
352
+ while (Date.now() < timeoutAt) {
353
+ const job = getDelegationJob(jobId)
354
+ if (!job) return `Error: delegation job "${jobId}" not found.`
355
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
356
+ return JSON.stringify(job)
357
+ }
358
+ await sleep(1000)
359
+ }
360
+ const latest = getDelegationJob(jobId)
361
+ return latest ? JSON.stringify(latest) : `Error: delegation job "${jobId}" not found.`
362
+ }
363
+
364
+ /**
365
+ * Core Delegate Execution Logic
366
+ */
367
+ async function executeDelegateAction(args: Record<string, unknown>, bctx: DelegateContext) {
368
+ const normalized = normalizeDelegateArgs(args)
369
+ const action = String(normalized.action || '').trim().toLowerCase()
370
+ const task = normalized.task as string
371
+ const requestedBackend = ((normalized.backend as string) || 'claude') as DelegateBackend
372
+ const jobId = typeof normalized.jobId === 'string' ? normalized.jobId.trim() : ''
373
+ const waitForCompletion = normalized.waitForCompletion !== false && normalized.background !== true
374
+ const parentSessionId = resolveDelegateSessionId(bctx)
375
+
376
+ recoverStaleDelegationJobs()
377
+
378
+ if (action === 'status') {
379
+ if (!jobId) return 'Error: jobId is required.'
380
+ const job = getDelegationJob(jobId)
381
+ return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
382
+ }
383
+ if (action === 'list') {
384
+ const jobs = listDelegationJobs({ parentSessionId: parentSessionId || null })
385
+ .filter((job) => job.kind === 'delegate')
386
+ return JSON.stringify(jobs)
387
+ }
388
+ if (action === 'cancel') {
389
+ if (!jobId) return 'Error: jobId is required.'
390
+ const job = cancelDelegationJob(jobId)
391
+ return job ? JSON.stringify(job) : `Error: delegation job "${jobId}" not found.`
392
+ }
393
+ if (action === 'wait') {
394
+ if (!jobId) return 'Error: jobId is required.'
395
+ const timeoutSec = typeof normalized.timeoutSec === 'number' ? normalized.timeoutSec : 30
396
+ return waitForDelegateJob(jobId, timeoutSec)
397
+ }
398
+
399
+ if (!task) return 'Error: task is required.'
400
+
401
+ const job = createDelegationJob({
402
+ kind: 'delegate',
403
+ parentSessionId,
404
+ backend: requestedBackend,
405
+ task,
406
+ cwd: bctx.cwd || null,
407
+ })
408
+ appendDelegationCheckpoint(job.id, `Dispatching to ${requestedBackend}`, 'queued')
409
+ startDelegationJob(job.id, { backend: requestedBackend, cwd: bctx.cwd || null })
410
+ const runtimeHandle: DelegateRuntimeState = {}
411
+ registerDelegationRuntime(job.id, runtimeHandle)
412
+
413
+ const runner = runDelegateBackendWithFallback(args, bctx, runtimeHandle, {
414
+ onAttempt: (backend, index) => {
415
+ if (index === 0) return
416
+ appendDelegationCheckpoint(job.id, `Retrying delegate with ${backend}`, 'running')
417
+ startDelegationJob(job.id, { backend, cwd: bctx.cwd || null })
418
+ },
419
+ onFallback: (from, to, reason) => {
420
+ appendDelegationCheckpoint(
421
+ job.id,
422
+ `Delegate ${from} failed: ${reason.replace(/^Error:\s*/i, '').trim()}. Falling back to ${to}.`,
423
+ 'running',
424
+ )
425
+ },
426
+ })
427
+ .then(({ backend, result }) => {
428
+ const latest = getDelegationJob(job.id)
429
+ if (latest?.status === 'cancelled') return { backend, result }
430
+ const resumePatch = buildDelegateResumePatch(bctx)
431
+ if (/^Error:/i.test(result.trim())) {
432
+ appendDelegationCheckpoint(job.id, `Delegate failed on ${backend}`, 'failed')
433
+ failDelegationJob(job.id, result.replace(/^Error:\s*/i, '').trim() || result, { ...resumePatch, backend })
434
+ } else {
435
+ appendDelegationCheckpoint(job.id, `Delegate completed on ${backend}`, 'completed')
436
+ completeDelegationJob(job.id, result, { ...resumePatch, backend })
437
+ }
438
+ return { backend, result }
439
+ })
440
+ .catch((err: unknown) => {
441
+ const message = err instanceof Error ? err.message : String(err)
442
+ const latest = getDelegationJob(job.id)
443
+ if (latest?.status === 'cancelled') return { backend: requestedBackend, result: `Error: ${message}` }
444
+ appendDelegationCheckpoint(job.id, `Delegate crashed on ${requestedBackend}: ${message}`, 'failed')
445
+ failDelegationJob(job.id, message, { ...buildDelegateResumePatch(bctx), backend: requestedBackend })
446
+ return { backend: requestedBackend, result: `Error: ${message}` }
447
+ })
448
+
449
+ if (!waitForCompletion) {
450
+ void runner
451
+ return JSON.stringify({
452
+ jobId: job.id,
453
+ status: 'running',
454
+ backend: requestedBackend,
455
+ })
456
+ }
457
+
458
+ const { backend, result } = await runner
459
+ const latest = getDelegationJob(job.id)
460
+ return JSON.stringify({
461
+ jobId: job.id,
462
+ status: latest?.status || (/^Error:/i.test(result.trim()) ? 'failed' : 'completed'),
463
+ backend: latest?.backend || backend,
464
+ response: result,
465
+ })
466
+ }
467
+
87
468
  function stripEnvPrefixes(input: NodeJS.ProcessEnv, prefixes: string[]): NodeJS.ProcessEnv {
88
469
  const out: NodeJS.ProcessEnv = { ...input }
89
470
  for (const key of Object.keys(out)) {
@@ -116,7 +497,7 @@ function parseCodexOutputText(ev: Record<string, unknown>): string | null {
116
497
  return null
117
498
  }
118
499
 
119
- async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
500
+ async function runCodexDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
120
501
  try {
121
502
  const env = stripEnvPrefixes({ ...process.env, TERM: 'dumb', NO_COLOR: '1' }, ['CODEX'])
122
503
  const authProbe = spawnSync(binary, ['login', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
@@ -135,6 +516,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
135
516
  args.push('--json', '--full-auto', '--skip-git-repo-check', '-')
136
517
 
137
518
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
519
+ bindDelegateRuntime(runtime, child)
138
520
  let stdoutBuf = ''
139
521
  let stderrBuf = ''
140
522
  let responseText = ''
@@ -197,7 +579,7 @@ async function runCodexDelegate(binary: string, task: string, resume: boolean, r
197
579
  }
198
580
  }
199
581
 
200
- async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
582
+ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
201
583
  try {
202
584
  const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
203
585
  const storedResumeId = bctx.readStoredDelegateResumeId?.('opencode')
@@ -208,6 +590,7 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
208
590
  if (resumeIdToUse) args.push('--session', resumeIdToUse)
209
591
 
210
592
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
593
+ bindDelegateRuntime(runtime, child)
211
594
  let stdoutBuf = ''
212
595
  let stderrBuf = ''
213
596
  let responseText = ''
@@ -273,7 +656,88 @@ async function runOpenCodeDelegate(binary: string, task: string, resume: boolean
273
656
  }
274
657
  }
275
658
 
276
- async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext): Promise<string> {
659
+ async function runGeminiDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
660
+ try {
661
+ const env = { ...process.env, TERM: 'dumb', NO_COLOR: '1' } as NodeJS.ProcessEnv
662
+ const storedResumeId = bctx.readStoredDelegateResumeId?.('gemini')
663
+ const resumeIdToUse = resumeId?.trim() || (resume ? storedResumeId : null)
664
+
665
+ return await new Promise<string>((resolve) => {
666
+ const args = ['--prompt', task, '--output-format', 'stream-json', '--yolo']
667
+ if (resumeIdToUse) args.push('--resume', resumeIdToUse)
668
+
669
+ const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['ignore', 'pipe', 'pipe'] })
670
+ bindDelegateRuntime(runtime, child)
671
+ let stdoutBuf = ''
672
+ let stderrBuf = ''
673
+ let responseText = ''
674
+ let discoveredId: string | null = null
675
+ let settled = false
676
+
677
+ const finish = (text: string) => {
678
+ if (settled) return
679
+ settled = true
680
+ resolve(truncate(text, MAX_OUTPUT))
681
+ }
682
+
683
+ const timeoutHandle = setTimeout(() => {
684
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
685
+ }, bctx.claudeTimeoutMs || 300000)
686
+
687
+ child.stdout?.on('data', (chunk) => {
688
+ stdoutBuf += chunk.toString()
689
+ const lines = stdoutBuf.split('\n')
690
+ stdoutBuf = lines.pop() || ''
691
+ for (const line of lines) {
692
+ const trimmed = line.trim()
693
+ if (!trimmed) continue
694
+ try {
695
+ const ev = JSON.parse(trimmed) as Record<string, unknown>
696
+ // Capture session ID from init event
697
+ if (ev.type === 'init' && typeof ev.session_id === 'string') {
698
+ discoveredId = ev.session_id
699
+ }
700
+ // Capture assistant text from message events
701
+ if (ev.type === 'message' && ev.role === 'assistant' && typeof ev.content === 'string') {
702
+ responseText += ev.content
703
+ }
704
+ // Capture final result
705
+ if (ev.type === 'result' && ev.status === 'error') {
706
+ const errMsg = typeof ev.error === 'string' ? ev.error : 'Gemini error'
707
+ stderrBuf += `${errMsg}\n`
708
+ }
709
+ } catch {
710
+ responseText += `${line}\n`
711
+ }
712
+ }
713
+ })
714
+
715
+ child.stderr?.on('data', (chunk) => {
716
+ stderrBuf += chunk.toString()
717
+ if (stderrBuf.length > 16_000) stderrBuf = stderrBuf.slice(-16_000)
718
+ })
719
+
720
+ child.on('close', (code, signal) => {
721
+ clearTimeout(timeoutHandle)
722
+ if (discoveredId) bctx.persistDelegateResumeId?.('gemini', discoveredId)
723
+ const output = responseText.trim()
724
+ if (output) return finish(output)
725
+ const stderr = stderrBuf.trim()
726
+ if (stderr) return finish(`Error: ${stderr}`)
727
+ return finish(`Error: Gemini exited with code ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}.`)
728
+ })
729
+
730
+ child.on('error', (err) => {
731
+ clearTimeout(timeoutHandle)
732
+ finish(`Error: ${err.message}`)
733
+ })
734
+ })
735
+ } catch (err: unknown) {
736
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
737
+ }
738
+ }
739
+
740
+ async function runClaudeDelegate(binary: string, task: string, resume: boolean, resumeId: string, bctx: DelegateContext, runtime?: DelegateRuntimeState): Promise<string> {
277
741
  try {
278
742
  const env: NodeJS.ProcessEnv = stripEnvPrefixes({ ...process.env }, ['CLAUDE'])
279
743
  const authProbe = spawnSync(binary, ['auth', 'status'], { cwd: bctx.cwd, env, encoding: 'utf-8', timeout: 8000 })
@@ -286,6 +750,7 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
286
750
  const args = ['--print', '--output-format', 'stream-json', '--verbose', '--dangerously-skip-permissions']
287
751
  if (resumeIdToUse) args.push('--resume', resumeIdToUse)
288
752
  const child = spawn(binary, args, { cwd: bctx.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
753
+ bindDelegateRuntime(runtime, child)
289
754
  let stderr = ''
290
755
  let assistantText = ''
291
756
  let discoveredId: string | null = null
@@ -335,22 +800,30 @@ async function runClaudeDelegate(binary: string, task: string, resume: boolean,
335
800
  const DelegatePlugin: Plugin = {
336
801
  name: 'Core Delegate',
337
802
  description: 'Delegate complex multi-file tasks to specialized CLI backends or other agents.',
338
- hooks: {} as PluginHooks,
803
+ hooks: {
804
+ getCapabilityDescription: () => 'I can hand off deep coding work to Claude Code, Codex, or Gemini CLI (`delegate`) for complex multi-file refactors and code generation. Resume IDs may come back via `[delegate_meta]`.',
805
+ getOperatingGuidance: () => ['CRITICAL: `execute_command` (not delegation) for running servers, installs, scripts. Delegation sessions end and kill processes.', 'Delegate only for deep multi-file code work: refactors, debugging, generation, test suites.'],
806
+ } as PluginHooks,
339
807
  tools: [
340
808
  {
341
809
  name: 'delegate',
342
- description: 'Delegate to a specialized backend (Claude, Codex, OpenCode).',
810
+ description: 'Delegate to a specialized backend (Claude, Codex, OpenCode, Gemini). Supports background jobs with action=status|list|wait|cancel.',
343
811
  parameters: {
344
812
  type: 'object',
345
813
  properties: {
814
+ action: { type: 'string', enum: ['start', 'status', 'list', 'wait', 'cancel'] },
346
815
  task: { type: 'string' },
347
- backend: { type: 'string', enum: ['claude', 'codex', 'opencode'] },
816
+ backend: { type: 'string', enum: ['claude', 'codex', 'opencode', 'gemini'] },
348
817
  resume: { type: 'boolean' },
349
- resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' }
818
+ resumeId: { type: 'string', description: 'Optional explicit session/thread ID to resume' },
819
+ jobId: { type: 'string' },
820
+ waitForCompletion: { type: 'boolean' },
821
+ background: { type: 'boolean' },
822
+ timeoutSec: { type: 'number' },
350
823
  },
351
- required: ['task']
824
+ required: []
352
825
  },
353
- execute: async (args, context) => executeDelegateAction(args, { ...context.session, cwd: context.session.cwd || process.cwd() })
826
+ execute: async (args, context) => executeDelegateAction(args, buildDelegateContextFromSessionish(context.session))
354
827
  }
355
828
  ]
356
829
  }
@@ -362,9 +835,9 @@ getPluginManager().registerBuiltin('delegate', DelegatePlugin)
362
835
  */
363
836
  export function buildDelegateTools(bctx: ToolBuildContext): StructuredToolInterface[] {
364
837
  const tools: StructuredToolInterface[] = []
365
- const { hasTool } = bctx
838
+ const { hasPlugin } = bctx
366
839
 
367
- if (hasTool('delegate')) {
840
+ if (hasPlugin('delegate')) {
368
841
  tools.push(
369
842
  tool(
370
843
  async (args) => executeDelegateAction(args, bctx),