@swarmclawai/swarmclaw 0.6.8 → 0.7.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 (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod'
2
- import { tool, type StructuredToolInterface } from '@langchain/core/tools'
2
+ import { tool } from '@langchain/core/tools'
3
+ import fs from 'fs'
3
4
  import {
4
- clearManagedProcess,
5
5
  getManagedProcess,
6
6
  killManagedProcess,
7
7
  listManagedProcesses,
@@ -13,159 +13,187 @@ import {
13
13
  } from '../process-manager'
14
14
  import type { ToolBuildContext } from './context'
15
15
  import { safePath, truncate, coerceEnvMap, MAX_OUTPUT } from './context'
16
+ import type { Plugin, PluginHooks } from '@/types'
17
+ import { getPluginManager } from '../plugins'
18
+ import { normalizeToolInputArgs } from './normalize-tool-args'
16
19
 
17
- export function buildShellTools(bctx: ToolBuildContext): StructuredToolInterface[] {
18
- const tools: StructuredToolInterface[] = []
19
- const { cwd, ctx, hasTool, commandTimeoutMs } = bctx
20
-
21
- if (hasTool('shell')) {
22
- tools.push(
23
- tool(
24
- async ({ command, background, yieldMs, timeoutSec, env, workdir }) => {
25
- try {
26
- const result = await startManagedProcess({
27
- command,
28
- cwd: workdir ? safePath(cwd, workdir) : cwd,
29
- env: coerceEnvMap(env),
30
- agentId: ctx?.agentId || null,
31
- sessionId: ctx?.sessionId || null,
32
- background: !!background,
33
- yieldMs: typeof yieldMs === 'number' ? yieldMs : undefined,
34
- timeoutMs: typeof timeoutSec === 'number'
35
- ? Math.max(1, Math.trunc(timeoutSec)) * 1000
36
- : commandTimeoutMs,
37
- })
38
- if (result.status === 'completed') {
39
- return truncate(result.output || '(no output)', MAX_OUTPUT)
40
- }
41
- return JSON.stringify({
42
- status: 'running',
43
- processId: result.processId,
44
- tail: result.tail || '',
45
- }, null, 2)
46
- } catch (err: any) {
47
- return truncate(`Error: ${err.message || String(err)}`, MAX_OUTPUT)
48
- }
49
- },
50
- {
51
- name: 'execute_command',
52
- description: 'Run a shell command in my working directory. This is how I run servers, install packages, execute scripts, use git, and do anything hands-on. Use background=true for long-running processes like dev servers. Supports timeout/yield controls.',
53
- schema: z.object({
54
- command: z.string().describe('The shell command to execute'),
55
- background: z.boolean().optional().describe('If true, start command in background immediately'),
56
- yieldMs: z.number().optional().describe('If command runs longer than this, return a running process id instead of blocking'),
57
- timeoutSec: z.number().optional().describe('Per-command timeout in seconds'),
58
- workdir: z.string().optional().describe('Relative working directory override'),
59
- env: z.record(z.string(), z.string()).optional().describe('Environment variable overrides'),
60
- }),
61
- },
62
- ),
63
- )
64
- }
20
+ function resolveShellWorkdir(baseCwd: string, requestedWorkdir?: string): string {
21
+ const raw = typeof requestedWorkdir === 'string' ? requestedWorkdir.trim() : ''
22
+ if (!raw) return baseCwd
23
+ try {
24
+ const resolved = safePath(baseCwd, raw)
25
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) return resolved
26
+ } catch { /* ignore */ }
27
+ return safePath(baseCwd, raw)
28
+ }
65
29
 
66
- if (hasTool('process')) {
67
- tools.push(
68
- tool(
69
- async ({ action, processId, offset, limit, data, eof, signal }) => {
70
- try {
71
- if (action === 'list') {
72
- return JSON.stringify(listManagedProcesses(ctx?.agentId || null).map((p) => ({
73
- id: p.id,
74
- command: p.command,
75
- status: p.status,
76
- pid: p.pid,
77
- startedAt: p.startedAt,
78
- endedAt: p.endedAt,
79
- exitCode: p.exitCode,
80
- signal: p.signal,
81
- })), null, 2)
82
- }
30
+ function isLikelyServerCommand(command: string): boolean {
31
+ const cmd = command.trim()
32
+ return /\bnpm\s+run\s+(dev|start|serve)\b/.test(cmd) ||
33
+ /\bnpx\s+(serve|next|vite|nuxt|astro)\b/.test(cmd) ||
34
+ /\bpython3?\s+-m\s+http\.server\b/.test(cmd)
35
+ }
83
36
 
84
- if (!processId) return 'Error: processId is required for this action.'
37
+ function asRecord(value: unknown): Record<string, unknown> | null {
38
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
39
+ return value as Record<string, unknown>
40
+ }
85
41
 
86
- const ownerCheck = getManagedProcess(processId)
87
- if (ownerCheck && ctx?.sessionId && ownerCheck.sessionId && ownerCheck.sessionId !== ctx.sessionId) {
88
- return `Error: process ${processId} belongs to a different session.`
89
- }
42
+ function parseNestedInput(raw: unknown): Record<string, unknown> | null {
43
+ if (typeof raw === 'string') {
44
+ try {
45
+ return asRecord(JSON.parse(raw))
46
+ } catch {
47
+ return null
48
+ }
49
+ }
50
+ return asRecord(raw)
51
+ }
90
52
 
91
- if (action === 'poll') {
92
- const res = pollManagedProcess(processId)
93
- if (!res) return `Process not found: ${processId}`
94
- return JSON.stringify({
95
- id: res.process.id,
96
- status: res.process.status,
97
- exitCode: res.process.exitCode,
98
- signal: res.process.signal,
99
- chunk: res.chunk,
100
- }, null, 2)
101
- }
53
+ function pickString(...values: unknown[]): string | undefined {
54
+ for (const value of values) {
55
+ if (typeof value !== 'string') continue
56
+ const trimmed = value.trim()
57
+ if (trimmed) return trimmed
58
+ }
59
+ return undefined
60
+ }
102
61
 
103
- if (action === 'log') {
104
- const res = readManagedProcessLog(processId, offset, limit)
105
- if (!res) return `Process not found: ${processId}`
106
- return JSON.stringify({
107
- id: res.process.id,
108
- status: res.process.status,
109
- totalLines: res.totalLines,
110
- text: res.text,
111
- }, null, 2)
112
- }
62
+ function pickNumber(...values: unknown[]): number | undefined {
63
+ for (const value of values) {
64
+ if (typeof value === 'number' && Number.isFinite(value)) return value
65
+ if (typeof value === 'string' && value.trim()) {
66
+ const parsed = Number(value)
67
+ if (Number.isFinite(parsed)) return parsed
68
+ }
69
+ }
70
+ return undefined
71
+ }
113
72
 
114
- if (action === 'write') {
115
- const out = writeManagedProcessStdin(processId, data || '', !!eof)
116
- return out.ok ? `Wrote to process ${processId}` : `Error: ${out.error}`
117
- }
73
+ function pickBoolean(...values: unknown[]): boolean | undefined {
74
+ for (const value of values) {
75
+ if (typeof value === 'boolean') return value
76
+ }
77
+ return undefined
78
+ }
118
79
 
119
- if (action === 'kill') {
120
- const out = killManagedProcess(processId, (signal as NodeJS.Signals) || 'SIGTERM')
121
- return out.ok ? `Killed process ${processId}` : `Error: ${out.error}`
122
- }
80
+ export function normalizeShellArgs(rawArgs: Record<string, unknown>): Record<string, unknown> {
81
+ const base = normalizeToolInputArgs(rawArgs)
82
+ const nested = parseNestedInput(base.input)
123
83
 
124
- if (action === 'clear') {
125
- const out = clearManagedProcess(processId)
126
- return out.ok ? `Cleared process ${processId}` : `Error: ${out.error}`
127
- }
84
+ const command = pickString(
85
+ base.command,
86
+ base.cmd,
87
+ base.execute_command,
88
+ nested?.command,
89
+ nested?.cmd,
90
+ nested?.execute_command,
91
+ )
92
+ const action = pickString(base.action, nested?.action) ?? (command ? 'execute' : undefined)
128
93
 
129
- if (action === 'remove') {
130
- const out = removeManagedProcess(processId)
131
- return out.ok ? `Removed process ${processId}` : `Error: ${out.error}`
132
- }
94
+ return {
95
+ action,
96
+ command,
97
+ processId: pickString(base.processId, base.process_id, nested?.processId, nested?.process_id),
98
+ background: pickBoolean(base.background, nested?.background),
99
+ workdir: pickString(base.workdir, base.cwd, nested?.workdir, nested?.cwd),
100
+ env: asRecord(base.env) || asRecord(nested?.env),
101
+ timeoutSec: pickNumber(base.timeoutSec, base.timeout, nested?.timeoutSec, nested?.timeout),
102
+ data: pickString(base.data, base.stdin, nested?.data, nested?.stdin),
103
+ signal: pickString(base.signal, base.killSignal, nested?.signal, nested?.killSignal),
104
+ offset: pickNumber(base.offset, nested?.offset),
105
+ limit: pickNumber(base.limit, nested?.limit),
106
+ }
107
+ }
133
108
 
134
- if (action === 'status') {
135
- const p = getManagedProcess(processId)
136
- if (!p) return `Process not found: ${processId}`
137
- return JSON.stringify({
138
- id: p.id,
139
- status: p.status,
140
- pid: p.pid,
141
- startedAt: p.startedAt,
142
- endedAt: p.endedAt,
143
- exitCode: p.exitCode,
144
- signal: p.signal,
145
- }, null, 2)
146
- }
109
+ /**
110
+ * Unified Shell Execution Logic
111
+ */
112
+ async function executeShellAction(args: Record<string, unknown>, bctx: { cwd: string; agentId?: string | null; sessionId?: string | null }) {
113
+ const normalized = normalizeShellArgs(args)
114
+ const action = normalized.action as string | undefined
115
+ const command = normalized.command as string | undefined
116
+ const processId = normalized.processId as string | undefined
117
+ const background = normalized.background as boolean | undefined
118
+ const workdir = normalized.workdir as string | undefined
119
+ const env = normalized.env
120
+ const timeoutSec = normalized.timeoutSec as number | undefined
121
+ const data = normalized.data as string | undefined
122
+ const signal = normalized.signal as string | undefined
123
+ const offset = normalized.offset as number | undefined
124
+ const limit = normalized.limit as number | undefined
125
+ try {
126
+ switch (action) {
127
+ case 'execute': {
128
+ if (!command) return 'Error: command or cmd is required for execute action.'
129
+ const effectiveBackground = !!background || (typeof command === 'string' && isLikelyServerCommand(command))
130
+ const result = await startManagedProcess({
131
+ command: command,
132
+ cwd: resolveShellWorkdir(bctx.cwd, workdir),
133
+ env: coerceEnvMap(env),
134
+ agentId: bctx.agentId || null,
135
+ sessionId: bctx.sessionId || null,
136
+ background: effectiveBackground,
137
+ timeoutMs: typeof timeoutSec === 'number' ? timeoutSec * 1000 : 30000,
138
+ })
139
+ if (result.status === 'completed') return truncate(result.output || '(no output)', MAX_OUTPUT)
140
+ return JSON.stringify({ status: 'running', processId: result.processId, tail: result.tail || '' }, null, 2)
141
+ }
142
+ case 'list': return JSON.stringify(listManagedProcesses(bctx.agentId || null), null, 2)
143
+ case 'status': return JSON.stringify(getManagedProcess(processId!) || `Not found: ${processId}`, null, 2)
144
+ case 'poll': return JSON.stringify(pollManagedProcess(processId!) || `Not found: ${processId}`, null, 2)
145
+ case 'log': return JSON.stringify(readManagedProcessLog(processId!, offset, limit) || `Not found: ${processId}`, null, 2)
146
+ case 'write': return writeManagedProcessStdin(processId!, data || '', false).ok ? `Wrote to ${processId}` : `Error`
147
+ case 'kill': {
148
+ const killSignal = (typeof signal === 'string' && signal.trim() ? signal : 'SIGTERM') as NodeJS.Signals
149
+ return killManagedProcess(processId!, killSignal).ok ? `Killed ${processId}` : `Error`
150
+ }
151
+ case 'remove': return removeManagedProcess(processId!).ok ? `Removed ${processId}` : `Error`
152
+ default: return `Error: Unknown action "${action}"`
153
+ }
154
+ } catch (err: unknown) {
155
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
156
+ }
157
+ }
147
158
 
148
- return `Unknown action "${action}".`
149
- } catch (err: any) {
150
- return `Error: ${err.message || String(err)}`
151
- }
159
+ /**
160
+ * Register as a Built-in Plugin
161
+ */
162
+ const ShellPlugin: Plugin = {
163
+ name: 'Core Shell',
164
+ description: 'Execute shell commands and manage background processes.',
165
+ hooks: {} as PluginHooks,
166
+ tools: [
167
+ {
168
+ name: 'shell',
169
+ description: 'Execute commands and manage processes.',
170
+ parameters: {
171
+ type: 'object',
172
+ properties: {
173
+ action: { type: 'string', enum: ['execute', 'list', 'status', 'poll', 'log', 'write', 'kill', 'remove'] },
174
+ command: { type: 'string' },
175
+ processId: { type: 'string' },
176
+ background: { type: 'boolean' },
152
177
  },
153
- {
154
- name: 'process_tool',
155
- description: 'Manage long-running shell processes started by execute_command. Supports list, status, poll, log, write, kill, clear, and remove.',
156
- schema: z.object({
157
- action: z.enum(['list', 'status', 'poll', 'log', 'write', 'kill', 'clear', 'remove']),
158
- processId: z.string().optional(),
159
- offset: z.number().optional(),
160
- limit: z.number().optional(),
161
- data: z.string().optional(),
162
- eof: z.boolean().optional(),
163
- signal: z.string().optional().describe('Signal for kill action, e.g. SIGTERM or SIGKILL'),
164
- }),
165
- },
166
- ),
167
- )
168
- }
178
+ required: ['action']
179
+ },
180
+ execute: async (args, context) => executeShellAction(args, { ...context.session, cwd: context.session.cwd || process.cwd() })
181
+ }
182
+ ]
183
+ }
184
+
185
+ getPluginManager().registerBuiltin('shell', ShellPlugin)
169
186
 
170
- return tools
187
+ export function buildShellTools(bctx: ToolBuildContext) {
188
+ if (!bctx.hasTool('shell')) return []
189
+ return [
190
+ tool(
191
+ async (args) => executeShellAction(args, { ...bctx.ctx, cwd: bctx.cwd }),
192
+ {
193
+ name: 'shell',
194
+ description: ShellPlugin.tools![0].description,
195
+ schema: z.object({}).passthrough()
196
+ }
197
+ )
198
+ ]
171
199
  }
@@ -1,106 +1,106 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import { genId } from '@/lib/id'
4
+ import { DEFAULT_DELEGATION_MAX_DEPTH } from '@/lib/runtime-loop'
4
5
  import { loadAgents, loadSessions, saveSessions } from '../storage'
5
6
  import { executeSessionChatTurn } from '../chat-execution'
6
7
  import { log } from '../logger'
8
+ import { loadRuntimeSettings } from '../runtime-settings'
7
9
  import type { ToolBuildContext } from './context'
10
+ import type { Plugin, PluginHooks } from '@/types'
11
+ import { getPluginManager } from '../plugins'
12
+ import { normalizeToolInputArgs } from './normalize-tool-args'
8
13
 
9
- const MAX_RECURSION_DEPTH = 3
10
-
11
- function getSessionDepth(sessionId: string | undefined): number {
14
+ function getSessionDepth(sessionId: string | undefined, maxDepth: number): number {
12
15
  if (!sessionId) return 0
13
16
  const sessions = loadSessions()
14
17
  let depth = 0
15
18
  let current = sessionId
16
- while (current && depth < MAX_RECURSION_DEPTH + 1) {
19
+ while (current && depth < maxDepth + 1) {
17
20
  const session = sessions[current]
18
21
  if (!session?.parentSessionId) break
19
- current = session.parentSessionId
22
+ current = session.parentSessionId as string
20
23
  depth++
21
24
  }
22
25
  return depth
23
26
  }
24
27
 
25
- export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
26
- const { ctx, hasTool } = bctx
27
- if (!hasTool('spawn_subagent')) return []
28
+ /**
29
+ * Core Subagent Execution Logic
30
+ */
31
+ async function executeSubagentAction(args: any, context: { sessionId?: string; cwd: string }) {
32
+ const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
33
+ const agentId = (normalized.agentId ?? normalized.agent_id) as string | undefined
34
+ const message = normalized.message as string | undefined
35
+ const cwd = normalized.cwd as string | undefined
36
+ try {
37
+ const runtime = loadRuntimeSettings()
38
+ const maxDepth = runtime.delegationMaxDepth || DEFAULT_DELEGATION_MAX_DEPTH
39
+ const agents = loadAgents()
40
+ if (!agentId) return 'Error: agentId is required.'
41
+ if (!message) return 'Error: message is required.'
42
+ const agent = agents[agentId]
43
+ if (!agent) return `Error: Agent "${agentId}" not found.`
28
44
 
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(', ')}`
45
+ const depth = getSessionDepth(context.sessionId, maxDepth)
46
+ if (depth >= maxDepth) return `Error: Max subagent depth reached.`
37
47
 
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
- }
48
+ const sid = genId()
49
+ const now = Date.now()
50
+ const sessions = loadSessions()
51
+ sessions[sid] = {
52
+ id: sid, name: `subagent-${agent.name}`, cwd: cwd || context.cwd, user: 'agent',
53
+ provider: agent.provider, model: agent.model, credentialId: agent.credentialId || null,
54
+ messages: [], createdAt: now, lastActiveAt: now, sessionType: 'orchestrated',
55
+ agentId: agent.id, parentSessionId: context.sessionId || null, tools: agent.tools || [],
56
+ }
57
+ saveSessions(sessions)
43
58
 
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)
59
+ const result = await executeSessionChatTurn({ sessionId: sid, message, internal: true, source: 'subagent' })
60
+ return JSON.stringify({ agentId, agentName: agent.name, sessionId: sid, response: result.text.slice(0, 8000) })
61
+ } catch (err: any) { return `Error: ${err.message}` }
62
+ }
68
63
 
69
- log.info('subagent', `Spawning subagent "${agent.name}" (depth=${depth + 1})`, {
70
- parentSessionId: ctx?.sessionId,
71
- childSessionId: sessionId,
72
- agentId,
73
- })
64
+ /**
65
+ * Register as a Built-in Plugin
66
+ */
67
+ const SubagentPlugin: Plugin = {
68
+ name: 'Core Subagents',
69
+ description: 'Delegate tasks to other specialized agents.',
70
+ hooks: {} as PluginHooks,
71
+ tools: [
72
+ {
73
+ name: 'spawn_subagent',
74
+ description: 'Delegate a task to another agent.',
75
+ parameters: {
76
+ type: 'object',
77
+ properties: {
78
+ agentId: { type: 'string' },
79
+ message: { type: 'string' },
80
+ cwd: { type: 'string' }
81
+ },
82
+ required: ['agentId', 'message']
83
+ },
84
+ execute: async (args, context) => executeSubagentAction(args, { sessionId: context.session.id, cwd: context.session.cwd || process.cwd() })
85
+ }
86
+ ]
87
+ }
74
88
 
75
- // Execute the chat turn
76
- const result = await executeSessionChatTurn({
77
- sessionId,
78
- message,
79
- internal: true,
80
- source: 'subagent',
81
- })
89
+ getPluginManager().registerBuiltin('subagent', SubagentPlugin)
82
90
 
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
- },
91
+ /**
92
+ * Legacy Bridge
93
+ */
94
+ export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
95
+ if (!bctx.hasTool('spawn_subagent')) return []
96
+ return [
97
+ tool(
98
+ async (args) => executeSubagentAction(args, { sessionId: bctx.ctx?.sessionId || undefined, cwd: bctx.cwd }),
95
99
  {
96
100
  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
- ),
101
+ description: SubagentPlugin.tools![0].description,
102
+ schema: z.object({}).passthrough()
103
+ }
104
+ )
105
105
  ]
106
106
  }