@swarmclawai/swarmclaw 0.6.7 → 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 (203) hide show
  1. package/README.md +82 -39
  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 +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -3,420 +3,114 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import { genId } from '@/lib/id'
4
4
  import { loadSessions, saveSessions, loadAgents } from '../storage'
5
5
  import type { ToolBuildContext } from './context'
6
+ import type { Plugin, PluginHooks } from '@/types'
7
+ import { getPluginManager } from '../plugins'
8
+ import { normalizeToolInputArgs } from './normalize-tool-args'
9
+
10
+ /**
11
+ * Core Session Info Execution Logic
12
+ */
13
+ async function executeWhoAmI(context: { sessionId?: string; agentId?: string }) {
14
+ try {
15
+ const sessions = loadSessions()
16
+ const current = context.sessionId ? sessions[context.sessionId] : null
17
+ return JSON.stringify({
18
+ sessionId: context.sessionId || undefined,
19
+ sessionName: current?.name || undefined,
20
+ sessionType: current?.sessionType || undefined,
21
+ user: current?.user || undefined,
22
+ agentId: context.agentId || current?.agentId || undefined,
23
+ parentSessionId: current?.parentSessionId || undefined,
24
+ })
25
+ } catch (err: any) { return `Error: ${err.message}` }
26
+ }
6
27
 
7
- export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInterface[] {
8
- const tools: StructuredToolInterface[] = []
9
- const { cwd, ctx } = bctx
10
-
11
- if (bctx.hasTool('manage_sessions')) {
12
- tools.push(
13
- tool(
14
- async () => {
15
- try {
16
- const sessions = loadSessions()
17
- const current = ctx?.sessionId ? sessions[ctx.sessionId] : null
18
- return JSON.stringify({
19
- sessionId: ctx?.sessionId || null,
20
- sessionName: current?.name || null,
21
- sessionType: current?.sessionType || null,
22
- user: current?.user || null,
23
- agentId: ctx?.agentId || current?.agentId || null,
24
- parentSessionId: current?.parentSessionId || null,
25
- heartbeatEnabled: typeof current?.heartbeatEnabled === 'boolean'
26
- ? current.heartbeatEnabled
27
- : null,
28
- })
29
- } catch (err: any) {
30
- return `Error: ${err.message || String(err)}`
31
- }
32
- },
33
- {
34
- name: 'whoami_tool',
35
- description: 'Return identity/runtime context for this agent execution (current session id, agent id, session owner, and parent session).',
36
- schema: z.object({}),
37
- },
38
- ),
39
- )
40
-
41
- tools.push(
42
- tool(
43
- async ({ action, sessionId, message, limit, agentId, name, waitForReply, timeoutSec, queueMode, heartbeatEnabled, heartbeatIntervalSec, heartbeatIntervalMs, finalStatus, envelopeId, type, correlationId, ttlSec }) => {
44
- try {
45
- const sessions = loadSessions()
46
- if (action === 'list') {
47
- const { getSessionRunState } = await import('../session-run-manager')
48
- const items = Object.values(sessions)
49
- .sort((a: any, b: any) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0))
50
- .slice(0, Math.max(1, Math.min(limit || 50, 200)))
51
- .map((s: any) => {
52
- const runState = getSessionRunState(s.id)
53
- return {
54
- id: s.id,
55
- name: s.name,
56
- sessionType: s.sessionType || 'human',
57
- agentId: s.agentId || null,
58
- provider: s.provider,
59
- model: s.model,
60
- parentSessionId: s.parentSessionId || null,
61
- active: !!runState.runningRunId,
62
- queuedCount: runState.queueLength,
63
- heartbeatEnabled: s.heartbeatEnabled !== false,
64
- lastActiveAt: s.lastActiveAt,
65
- createdAt: s.createdAt,
66
- }
67
- })
68
- return JSON.stringify(items)
69
- }
70
-
71
- if (action === 'history') {
72
- const targetSessionId = sessionId || ctx?.sessionId || null
73
- if (!targetSessionId) return 'Error: sessionId is required for history when no current session context exists.'
74
- const target = sessions[targetSessionId]
75
- if (!target) return `Not found: session "${targetSessionId}"`
76
- const max = Math.max(1, Math.min(limit || 20, 100))
77
- const history = (target.messages || []).slice(-max).map((m: any) => ({
78
- role: m.role,
79
- text: m.text,
80
- time: m.time,
81
- kind: m.kind || 'chat',
82
- }))
83
- return JSON.stringify({ sessionId: target.id, name: target.name, history, currentSessionDefaulted: !sessionId })
84
- }
85
-
86
- if (action === 'status') {
87
- if (!sessionId) return 'Error: sessionId is required for status.'
88
- const target = sessions[sessionId]
89
- if (!target) return `Not found: session "${sessionId}"`
90
- const { getSessionRunState } = await import('../session-run-manager')
91
- const run = getSessionRunState(sessionId)
92
- return JSON.stringify({
93
- id: target.id,
94
- name: target.name,
95
- runningRunId: run.runningRunId || null,
96
- queuedCount: run.queueLength,
97
- heartbeatEnabled: target.heartbeatEnabled !== false,
98
- lastActiveAt: target.lastActiveAt,
99
- messageCount: (target.messages || []).length,
100
- })
101
- }
102
-
103
- if (action === 'stop') {
104
- if (!sessionId) return 'Error: sessionId is required for stop.'
105
- if (!sessions[sessionId]) return `Not found: session "${sessionId}"`
106
- const { cancelSessionRuns } = await import('../session-run-manager')
107
- const out = cancelSessionRuns(sessionId, 'Stopped by manage_sessions')
108
- return JSON.stringify({ sessionId, ...out })
109
- }
110
-
111
- if (action === 'send') {
112
- if (!sessionId) return 'Error: sessionId is required for send.'
113
- if (!message?.trim()) return 'Error: message is required for send.'
114
- if (!sessions[sessionId]) return `Not found: session "${sessionId}"`
115
- if (ctx?.sessionId && sessionId === ctx.sessionId) return 'Error: cannot send to the current session itself.'
116
-
117
- const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
118
- const sourceLabel = sourceSession
119
- ? `${sourceSession.name} (${sourceSession.id})`
120
- : (ctx?.agentId ? `agent:${ctx.agentId}` : 'platform')
121
- const bridgedMessage = `[Session message from ${sourceLabel}]\n${message.trim()}`
122
-
123
- const { enqueueSessionRun } = await import('../session-run-manager')
124
- const mode = queueMode === 'steer' || queueMode === 'collect' || queueMode === 'followup'
125
- ? queueMode
126
- : 'followup'
127
- const run = enqueueSessionRun({
128
- sessionId,
129
- message: bridgedMessage,
130
- source: 'session-send',
131
- internal: false,
132
- mode,
133
- })
134
-
135
- if (waitForReply === false) {
136
- return JSON.stringify({
137
- sessionId,
138
- runId: run.runId,
139
- status: 'queued',
140
- mode,
141
- })
142
- }
143
-
144
- const timeoutMs = Math.max(5, Math.min(timeoutSec || 120, 900)) * 1000
145
- const result = await Promise.race([
146
- run.promise,
147
- new Promise<never>((_, reject) =>
148
- setTimeout(() => reject(new Error(`Timed out waiting for session reply after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs),
149
- ),
150
- ])
151
- return JSON.stringify({
152
- sessionId,
153
- runId: run.runId,
154
- status: result.error ? 'failed' : 'completed',
155
- reply: result.text || '',
156
- error: result.error || null,
157
- })
158
- }
159
-
160
- if (action === 'spawn') {
161
- if (!agentId) return 'Error: agentId is required for spawn.'
162
- const agents = loadAgents()
163
- const agent = agents[agentId]
164
- if (!agent) return `Not found: agent "${agentId}"`
165
- const sourceSession = ctx?.sessionId ? sessions[ctx.sessionId] : null
166
- const ownerUser = sourceSession?.user || 'system'
167
-
168
- const id = genId()
169
- const now = Date.now()
170
- const entry = {
171
- id,
172
- name: (name || `${agent.name} Session`).trim(),
173
- cwd,
174
- user: ownerUser,
175
- provider: agent.provider || 'claude-cli',
176
- model: agent.model || '',
177
- credentialId: agent.credentialId || null,
178
- apiEndpoint: agent.apiEndpoint || null,
179
- claudeSessionId: null,
180
- codexThreadId: null,
181
- opencodeSessionId: null,
182
- delegateResumeIds: {
183
- claudeCode: null,
184
- codex: null,
185
- opencode: null,
186
- },
187
- messages: [],
188
- createdAt: now,
189
- lastActiveAt: now,
190
- sessionType: 'orchestrated',
191
- agentId: agent.id,
192
- parentSessionId: ctx?.sessionId || null,
193
- tools: agent.tools || [],
194
- heartbeatEnabled: agent.heartbeatEnabled ?? true,
195
- heartbeatIntervalSec: agent.heartbeatIntervalSec ?? null,
196
- }
197
- sessions[id] = entry as any
198
- saveSessions(sessions)
199
-
200
- let runId: string | null = null
201
- if (message?.trim()) {
202
- const { enqueueSessionRun } = await import('../session-run-manager')
203
- const run = enqueueSessionRun({
204
- sessionId: id,
205
- message: message.trim(),
206
- source: 'session-spawn',
207
- internal: false,
208
- mode: 'followup',
209
- })
210
- runId = run.runId
211
- }
212
-
213
- return JSON.stringify({
214
- sessionId: id,
215
- name: entry.name,
216
- agentId: agent.id,
217
- queuedRunId: runId,
218
- })
219
- }
220
-
221
- if (action === 'set_heartbeat') {
222
- const targetSessionId = sessionId || ctx?.sessionId || null
223
- if (!targetSessionId) return 'Error: sessionId is required when no current session context exists.'
224
- const target = sessions[targetSessionId]
225
- if (!target) return `Not found: session "${targetSessionId}"`
226
- const intervalFromMs = typeof heartbeatIntervalMs === 'number'
227
- ? Math.max(0, Math.round(heartbeatIntervalMs / 1000))
228
- : undefined
229
- const nextIntervalSecRaw = typeof heartbeatIntervalSec === 'number'
230
- ? heartbeatIntervalSec
231
- : intervalFromMs
232
- const nextIntervalSec = typeof nextIntervalSecRaw === 'number'
233
- ? Math.max(0, Math.min(3600, Math.round(nextIntervalSecRaw)))
234
- : undefined
235
-
236
- if (typeof heartbeatEnabled !== 'boolean' && typeof nextIntervalSec !== 'number') {
237
- return 'Error: set_heartbeat requires heartbeatEnabled and/or heartbeatIntervalSec/heartbeatIntervalMs.'
238
- }
239
-
240
- if (typeof heartbeatEnabled === 'boolean') target.heartbeatEnabled = heartbeatEnabled
241
- if (typeof nextIntervalSec === 'number') target.heartbeatIntervalSec = nextIntervalSec
242
- target.lastActiveAt = Date.now()
243
-
244
- let statusMessageAdded = false
245
- if (target.heartbeatEnabled === false && finalStatus?.trim()) {
246
- if (!Array.isArray(target.messages)) target.messages = []
247
- target.messages.push({
248
- role: 'assistant',
249
- text: finalStatus.trim(),
250
- time: Date.now(),
251
- kind: 'heartbeat',
252
- })
253
- statusMessageAdded = true
254
- }
255
-
256
- saveSessions(sessions)
257
- return JSON.stringify({
258
- sessionId: targetSessionId,
259
- heartbeatEnabled: target.heartbeatEnabled !== false,
260
- heartbeatIntervalSec: target.heartbeatIntervalSec ?? null,
261
- heartbeatIntervalMs: typeof target.heartbeatIntervalSec === 'number' ? target.heartbeatIntervalSec * 1000 : null,
262
- statusMessageAdded,
263
- })
264
- }
265
-
266
- if (action === 'mailbox_send') {
267
- if (!sessionId) return 'Error: sessionId (target session) is required for mailbox_send.'
268
- if (!message?.trim()) return 'Error: message is required for mailbox_send.'
269
- const { sendMailboxEnvelope } = await import('../session-mailbox')
270
- const envelope = sendMailboxEnvelope({
271
- toSessionId: sessionId,
272
- type: type?.trim() || 'message',
273
- payload: message.trim(),
274
- fromSessionId: ctx?.sessionId || null,
275
- fromAgentId: ctx?.agentId || null,
276
- correlationId: correlationId?.trim() || null,
277
- ttlSec: typeof ttlSec === 'number' ? ttlSec : null,
278
- })
279
- return JSON.stringify({ ok: true, envelope })
280
- }
281
-
282
- if (action === 'mailbox_inbox') {
283
- const targetSessionId = sessionId || ctx?.sessionId || null
284
- if (!targetSessionId) return 'Error: sessionId is required for mailbox_inbox when no current session context exists.'
285
- const { listMailbox } = await import('../session-mailbox')
286
- const envelopes = listMailbox(targetSessionId, { limit, includeAcked: false })
287
- return JSON.stringify({
288
- sessionId: targetSessionId,
289
- count: envelopes.length,
290
- envelopes,
291
- currentSessionDefaulted: !sessionId,
292
- })
293
- }
294
-
295
- if (action === 'mailbox_ack') {
296
- const targetSessionId = sessionId || ctx?.sessionId || null
297
- if (!targetSessionId) return 'Error: sessionId is required for mailbox_ack when no current session context exists.'
298
- if (!envelopeId?.trim()) return 'Error: envelopeId is required for mailbox_ack.'
299
- const { ackMailboxEnvelope } = await import('../session-mailbox')
300
- const envelope = ackMailboxEnvelope(targetSessionId, envelopeId.trim())
301
- if (!envelope) return `Not found: envelope "${envelopeId.trim()}"`
302
- return JSON.stringify({ ok: true, envelope })
303
- }
304
-
305
- if (action === 'mailbox_clear') {
306
- const targetSessionId = sessionId || ctx?.sessionId || null
307
- if (!targetSessionId) return 'Error: sessionId is required for mailbox_clear when no current session context exists.'
308
- const { clearMailbox } = await import('../session-mailbox')
309
- const cleared = clearMailbox(targetSessionId, true)
310
- return JSON.stringify({ ok: true, ...cleared })
311
- }
28
+ async function executeSessionsAction(args: any, context: { sessionId?: string; agentId?: string; cwd: string }) {
29
+ const normalized = normalizeToolInputArgs((args ?? {}) as Record<string, unknown>)
30
+ const action = normalized.action as string | undefined
31
+ const sessionId = (normalized.sessionId ?? normalized.session_id) as string | undefined
32
+ const message = normalized.message as string | undefined
33
+ const limit = normalized.limit as number | undefined
34
+ const agentId = (normalized.agentId ?? normalized.agent_id) as string | undefined
35
+ const name = normalized.name as string | undefined
36
+ try {
37
+ const sessions = loadSessions()
38
+ if (action === 'list') {
39
+ return JSON.stringify(Object.values(sessions).slice(0, limit || 50).map((s: any) => ({ id: s.id, name: s.name })))
40
+ }
41
+ if (action === 'history') {
42
+ const target = sessions[sessionId || context.sessionId || '']
43
+ if (!target) return 'Not found.'
44
+ return JSON.stringify((target.messages || []).slice(-(limit || 20)))
45
+ }
46
+ if (action === 'spawn') {
47
+ if (!agentId) return 'agentId required.'
48
+ const agents = loadAgents()
49
+ const agent = agents[agentId]
50
+ if (!agent) return 'Agent not found.'
51
+ const id = genId()
52
+ const now = Date.now()
53
+ sessions[id] = {
54
+ id, name: (name || `${agent.name} Session`).trim(), cwd: context.cwd, user: 'system',
55
+ provider: agent.provider, model: agent.model, credentialId: agent.credentialId || null,
56
+ messages: [], createdAt: now, lastActiveAt: now, sessionType: 'orchestrated',
57
+ agentId: agent.id, parentSessionId: context.sessionId || undefined, tools: agent.tools || [],
58
+ }
59
+ saveSessions(sessions)
60
+ return JSON.stringify({ sessionId: id, name: agent.name })
61
+ }
62
+ return `Unknown action "${action}".`
63
+ } catch (err: any) { return `Error: ${err.message}` }
64
+ }
312
65
 
313
- return 'Unknown action. Use list, history, status, send, spawn, stop, set_heartbeat, mailbox_send, mailbox_inbox, mailbox_ack, or mailbox_clear.'
314
- } catch (err: any) {
315
- return `Error: ${err.message || String(err)}`
316
- }
66
+ /**
67
+ * Register as a Built-in Plugin
68
+ */
69
+ const SessionInfoPlugin: Plugin = {
70
+ name: 'Core Session Info',
71
+ description: 'Identify current session context and manage other agent sessions.',
72
+ hooks: {} as PluginHooks,
73
+ tools: [
74
+ {
75
+ name: 'whoami_tool',
76
+ description: 'Return identity/runtime context for this agent execution.',
77
+ parameters: { type: 'object', properties: {} },
78
+ execute: async (args, context) => executeWhoAmI({ sessionId: context.session.id, agentId: context.session.agentId ?? undefined })
79
+ },
80
+ {
81
+ name: 'sessions_tool',
82
+ description: 'Manage and interact with other sessions.',
83
+ parameters: {
84
+ type: 'object',
85
+ properties: {
86
+ action: { type: 'string', enum: ['list', 'history', 'spawn', 'status', 'stop'] },
87
+ sessionId: { type: 'string' },
88
+ agentId: { type: 'string' },
89
+ message: { type: 'string' },
90
+ limit: { type: 'number' }
317
91
  },
318
- {
319
- name: 'sessions_tool',
320
- description: 'Session-to-session operations: list/status/history sessions, send messages to other sessions, spawn new agent sessions, stop active runs, control per-session heartbeat, and exchange protocol envelopes via mailbox_* actions.',
321
- schema: z.object({
322
- action: z.enum(['list', 'history', 'status', 'send', 'spawn', 'stop', 'set_heartbeat', 'mailbox_send', 'mailbox_inbox', 'mailbox_ack', 'mailbox_clear']).describe('Session action'),
323
- sessionId: z.string().optional().describe('Target session id (history defaults to current session when omitted; status/send/stop still require explicit sessionId)'),
324
- message: z.string().optional().describe('Message body (required for send, optional initial task for spawn)'),
325
- limit: z.number().optional().describe('Max items/messages for list/history'),
326
- agentId: z.string().optional().describe('Agent id to spawn (required for spawn)'),
327
- name: z.string().optional().describe('Optional session name for spawn'),
328
- waitForReply: z.boolean().optional().describe('For send: if false, queue and return immediately'),
329
- timeoutSec: z.number().optional().describe('For send with waitForReply=true, max wait time in seconds (default 120)'),
330
- queueMode: z.enum(['followup', 'steer', 'collect']).optional().describe('Queue mode for send'),
331
- heartbeatEnabled: z.boolean().optional().describe('For set_heartbeat: true to enable heartbeat, false to disable'),
332
- heartbeatIntervalSec: z.number().optional().describe('For set_heartbeat: optional heartbeat interval in seconds (0-3600).'),
333
- heartbeatIntervalMs: z.number().optional().describe('For set_heartbeat: optional heartbeat interval in milliseconds (alias of heartbeatIntervalSec).'),
334
- finalStatus: z.string().optional().describe('For set_heartbeat when disabling: optional final status update to append in the session'),
335
- envelopeId: z.string().optional().describe('For mailbox_ack: envelope id to acknowledge.'),
336
- type: z.string().optional().describe('For mailbox_send: protocol message type (default "message").'),
337
- correlationId: z.string().optional().describe('For mailbox_send: optional request/response correlation id.'),
338
- ttlSec: z.number().optional().describe('For mailbox_send: optional envelope TTL in seconds.'),
339
- }),
340
- },
341
- ),
342
- )
343
-
344
- tools.push(
345
- tool(
346
- async ({ query, sessionId, limit, dateRange }) => {
347
- try {
348
- const sessions = loadSessions()
349
- const targetSessionId = sessionId || ctx?.sessionId || null
350
- if (!targetSessionId) return 'Error: sessionId is required when no current session context exists.'
351
- const target = sessions[targetSessionId]
352
- if (!target) return `Not found: session "${targetSessionId}"`
353
-
354
- const from = typeof dateRange?.from === 'number' ? dateRange.from : Number.NEGATIVE_INFINITY
355
- const to = typeof dateRange?.to === 'number' ? dateRange.to : Number.POSITIVE_INFINITY
356
- const max = Math.max(1, Math.min(limit || 20, 200))
357
- const q = (query || '').trim().toLowerCase()
358
- const terms = q ? q.split(/\s+/).filter(Boolean) : []
92
+ required: ['action']
93
+ },
94
+ execute: async (args, context) => executeSessionsAction(args, { sessionId: context.session.id, agentId: context.session.agentId ?? undefined, cwd: context.session.cwd || process.cwd() })
95
+ }
96
+ ]
97
+ }
359
98
 
360
- const scoredAll = (target.messages || [])
361
- .map((m: any, idx: number) => ({ ...m, _idx: idx }))
362
- .filter((m: any) => {
363
- const t = typeof m.time === 'number' ? m.time : 0
364
- if (t < from || t > to) return false
365
- if (!terms.length) return true
366
- const hay = `${m.role || ''}\n${m.kind || ''}\n${m.text || ''}`.toLowerCase()
367
- return terms.every((term) => hay.includes(term))
368
- })
369
- .map((m: any) => {
370
- const hay = `${m.text || ''}`.toLowerCase()
371
- let score = 0
372
- if (q && hay.includes(q)) score += 5
373
- for (const term of terms) {
374
- if (hay.includes(term)) score += 1
375
- }
376
- const ageBoost = Math.max(0, (m.time || 0) / 1e13)
377
- score += ageBoost
378
- return { ...m, _score: score }
379
- })
380
- .sort((a: any, b: any) => b._score - a._score)
381
- const scored = scoredAll
382
- .slice(0, max)
383
- .map((m: any) => ({
384
- index: m._idx,
385
- role: m.role,
386
- kind: m.kind || 'chat',
387
- time: m.time,
388
- text: typeof m.text === 'string' && m.text.length > 1200 ? `${m.text.slice(0, 1200)}...` : (m.text || ''),
389
- }))
99
+ getPluginManager().registerBuiltin('session_info', SessionInfoPlugin)
390
100
 
391
- return JSON.stringify({
392
- sessionId: target.id,
393
- name: target.name,
394
- query: query || '',
395
- limit: max,
396
- matches: scored,
397
- totalMatches: scoredAll.length,
398
- currentSessionDefaulted: !sessionId,
399
- })
400
- } catch (err: any) {
401
- return `Error: ${err.message || String(err)}`
402
- }
403
- },
404
- {
405
- name: 'search_history_tool',
406
- description: 'Search message history for the current session by default, or another session if sessionId is provided. Useful for recalling prior commitments, decisions, and details.',
407
- schema: z.object({
408
- query: z.string().describe('Search query text (keywords, phrase, or topic).'),
409
- sessionId: z.string().optional().describe('Optional target session id; defaults to current session.'),
410
- limit: z.number().optional().describe('Maximum number of matches to return (default 20, max 200).'),
411
- dateRange: z.object({
412
- from: z.number().optional().describe('Unix epoch ms lower bound (inclusive).'),
413
- to: z.number().optional().describe('Unix epoch ms upper bound (inclusive).'),
414
- }).optional().describe('Optional time filter for message timestamps.'),
415
- }),
416
- },
417
- ),
101
+ /**
102
+ * Legacy Bridge
103
+ */
104
+ export function buildSessionInfoTools(bctx: ToolBuildContext): StructuredToolInterface[] {
105
+ if (!bctx.hasTool('manage_sessions')) return []
106
+ return [
107
+ tool(
108
+ async () => executeWhoAmI({ sessionId: bctx.ctx?.sessionId || undefined, agentId: bctx.ctx?.agentId || undefined }),
109
+ { name: 'whoami_tool', description: SessionInfoPlugin.tools![0].description, schema: z.object({}).passthrough() }
110
+ ),
111
+ tool(
112
+ async (args) => executeSessionsAction(args, { sessionId: bctx.ctx?.sessionId || undefined, agentId: bctx.ctx?.agentId || undefined, cwd: bctx.cwd }),
113
+ { name: 'sessions_tool', description: SessionInfoPlugin.tools![1].description, schema: z.object({}).passthrough() }
418
114
  )
419
- }
420
-
421
- return tools
115
+ ]
422
116
  }
@@ -0,0 +1,43 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { normalizeShellArgs } from './shell'
4
+
5
+ describe('normalizeShellArgs', () => {
6
+ it('keeps explicit action + command', () => {
7
+ const out = normalizeShellArgs({ action: 'execute', command: 'pwd' })
8
+ assert.equal(out.action, 'execute')
9
+ assert.equal(out.command, 'pwd')
10
+ })
11
+
12
+ it('maps top-level execute_command to execute action', () => {
13
+ const out = normalizeShellArgs({ execute_command: 'ls -la' })
14
+ assert.equal(out.action, 'execute')
15
+ assert.equal(out.command, 'ls -la')
16
+ })
17
+
18
+ it('maps nested input.execute_command payload', () => {
19
+ const out = normalizeShellArgs({
20
+ input: {
21
+ execute_command: 'cd openclaw/site && ls -la',
22
+ },
23
+ })
24
+ assert.equal(out.action, 'execute')
25
+ assert.equal(out.command, 'cd openclaw/site && ls -la')
26
+ })
27
+
28
+ it('maps stringified input payload', () => {
29
+ const out = normalizeShellArgs({
30
+ input: JSON.stringify({ execute_command: 'echo hello' }),
31
+ })
32
+ assert.equal(out.action, 'execute')
33
+ assert.equal(out.command, 'echo hello')
34
+ })
35
+
36
+ it('maps args wrapper payload', () => {
37
+ const out = normalizeShellArgs({
38
+ args: { execute_command: 'pwd' },
39
+ })
40
+ assert.equal(out.action, 'execute')
41
+ assert.equal(out.command, 'pwd')
42
+ })
43
+ })