@swarmclawai/swarmclaw 0.7.3 → 0.7.5
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.
- package/README.md +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +4 -87
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -8
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -28,6 +28,7 @@ import { getPluginManager } from './plugins'
|
|
|
28
28
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
29
29
|
import { routeTaskIntent } from './capability-router'
|
|
30
30
|
import { notify } from './ws-hub'
|
|
31
|
+
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
31
32
|
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
32
33
|
import { pluginIdMatches } from './tool-aliases'
|
|
33
34
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
@@ -132,14 +133,10 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
132
133
|
const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
|
|
133
134
|
if (idx === -1) return
|
|
134
135
|
const output = ev.toolOutput || ''
|
|
135
|
-
const isError = /^(Error:|error:)/i.test(output.trim())
|
|
136
|
-
|| output.includes('ECONNREFUSED')
|
|
137
|
-
|| output.includes('ETIMEDOUT')
|
|
138
|
-
|| output.includes('Error:')
|
|
139
136
|
bag[idx] = {
|
|
140
137
|
...bag[idx],
|
|
141
138
|
output,
|
|
142
|
-
error:
|
|
139
|
+
error: isLikelyToolErrorOutput(output) || undefined,
|
|
143
140
|
}
|
|
144
141
|
}
|
|
145
142
|
}
|
|
@@ -191,15 +188,124 @@ function extractDelegateResponse(outputText: string): string | null {
|
|
|
191
188
|
}
|
|
192
189
|
}
|
|
193
190
|
|
|
191
|
+
const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
|
|
192
|
+
agent: 'manage_agents',
|
|
193
|
+
agents: 'manage_agents',
|
|
194
|
+
task: 'manage_tasks',
|
|
195
|
+
tasks: 'manage_tasks',
|
|
196
|
+
schedule: 'manage_schedules',
|
|
197
|
+
schedules: 'manage_schedules',
|
|
198
|
+
skill: 'manage_skills',
|
|
199
|
+
skills: 'manage_skills',
|
|
200
|
+
document: 'manage_documents',
|
|
201
|
+
documents: 'manage_documents',
|
|
202
|
+
secret: 'manage_secrets',
|
|
203
|
+
secrets: 'manage_secrets',
|
|
204
|
+
connector: 'manage_connectors',
|
|
205
|
+
connectors: 'manage_connectors',
|
|
206
|
+
session: 'manage_sessions',
|
|
207
|
+
sessions: 'manage_sessions',
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function translateRequestedToolInvocation(
|
|
211
|
+
requestedName: string,
|
|
212
|
+
rawArgs: Record<string, unknown>,
|
|
213
|
+
messageFallback: string,
|
|
214
|
+
availableToolNames?: Iterable<string>,
|
|
215
|
+
): { toolName: string; args: Record<string, unknown> } {
|
|
216
|
+
const available = new Set(availableToolNames || [])
|
|
217
|
+
|
|
218
|
+
if (requestedName === 'web_search') {
|
|
219
|
+
return {
|
|
220
|
+
toolName: 'web',
|
|
221
|
+
args: {
|
|
222
|
+
action: 'search',
|
|
223
|
+
query: typeof rawArgs.query === 'string' ? rawArgs.query : messageFallback.trim(),
|
|
224
|
+
maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (requestedName === 'web_fetch') {
|
|
229
|
+
return {
|
|
230
|
+
toolName: 'web',
|
|
231
|
+
args: {
|
|
232
|
+
action: 'fetch',
|
|
233
|
+
url: rawArgs.url,
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (requestedName === 'delegate_to_claude_code') {
|
|
238
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
239
|
+
}
|
|
240
|
+
if (requestedName === 'delegate_to_codex_cli') {
|
|
241
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
|
|
242
|
+
}
|
|
243
|
+
if (requestedName === 'delegate_to_opencode_cli') {
|
|
244
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
|
|
245
|
+
}
|
|
246
|
+
if (requestedName === 'delegate_to_gemini_cli') {
|
|
247
|
+
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const managePrefix = 'manage_'
|
|
251
|
+
if (requestedName === 'manage_platform') {
|
|
252
|
+
const resource = typeof rawArgs.resource === 'string'
|
|
253
|
+
? rawArgs.resource.trim().toLowerCase()
|
|
254
|
+
: ''
|
|
255
|
+
const specificTool = MANAGE_PLATFORM_RESOURCE_TO_TOOL[resource]
|
|
256
|
+
if (specificTool && available.has(specificTool) && !available.has('manage_platform')) {
|
|
257
|
+
return { toolName: specificTool, args: rawArgs }
|
|
258
|
+
}
|
|
259
|
+
return { toolName: requestedName, args: rawArgs }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
|
|
263
|
+
if (!available.has(requestedName) && available.has('manage_platform')) {
|
|
264
|
+
const resource = requestedName.slice(managePrefix.length)
|
|
265
|
+
if (resource) {
|
|
266
|
+
const { action, id, data, ...rest } = rawArgs
|
|
267
|
+
const nextArgs: Record<string, unknown> = { resource, ...rest }
|
|
268
|
+
if (action !== undefined) nextArgs.action = action
|
|
269
|
+
if (id !== undefined) nextArgs.id = id
|
|
270
|
+
if (data !== undefined) nextArgs.data = data
|
|
271
|
+
return {
|
|
272
|
+
toolName: 'manage_platform',
|
|
273
|
+
args: nextArgs,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { toolName: requestedName, args: rawArgs }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { toolName: requestedName, args: rawArgs }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function isLikelyToolErrorOutput(output: string): boolean {
|
|
284
|
+
const trimmed = String(output || '').trim()
|
|
285
|
+
if (!trimmed) return false
|
|
286
|
+
if (/^(Error(?::|\s*\(exit\b[^)]*\):?)|error:)/i.test(trimmed)) return true
|
|
287
|
+
if (/\b(MCP error|ECONNREFUSED|ETIMEDOUT|ERR_CONNECTION_REFUSED|ENOENT|EACCES)\b/i.test(trimmed)) return true
|
|
288
|
+
if (/\binvalid_type\b/i.test(trimmed) && /\b(issue|issues|expected|required|received|zod)\b/i.test(trimmed)) return true
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(trimmed) as Record<string, unknown>
|
|
291
|
+
const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
|
|
292
|
+
if (status === 'error' || status === 'failed') return true
|
|
293
|
+
if (typeof parsed.error === 'string' && parsed.error.trim()) return true
|
|
294
|
+
} catch {
|
|
295
|
+
// Ignore non-JSON tool output.
|
|
296
|
+
}
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
|
|
194
300
|
function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
|
|
195
|
-
return text.replace(
|
|
301
|
+
return text.replace(/\[([^\]]+)\]\(sandbox:\/workspace\/([^)]+)\)/g, (raw, label: string, relativePath: string) => {
|
|
196
302
|
const normalized = String(relativePath || '').replace(/^\/+/, '')
|
|
197
303
|
if (!normalized) return raw
|
|
198
304
|
const resolvedCwd = path.resolve(cwd)
|
|
199
305
|
const resolved = path.resolve(resolvedCwd, normalized)
|
|
200
306
|
if (!resolved.startsWith(resolvedCwd)) return raw
|
|
201
307
|
if (!fs.existsSync(resolved)) return raw
|
|
202
|
-
return
|
|
308
|
+
return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
|
|
203
309
|
})
|
|
204
310
|
}
|
|
205
311
|
|
|
@@ -520,18 +626,41 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
520
626
|
if (!agent) return
|
|
521
627
|
|
|
522
628
|
let changed = false
|
|
629
|
+
const route = resolvePrimaryAgentRoute(agent)
|
|
523
630
|
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
524
631
|
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
525
632
|
session.model = agent.model
|
|
526
633
|
changed = true
|
|
527
634
|
}
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
changed = true
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
635
|
+
if (route) {
|
|
636
|
+
const resolved = applyResolvedRoute({ ...session }, route)
|
|
637
|
+
if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
|
|
638
|
+
if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
|
|
639
|
+
if ((session.credentialId || null) !== (resolved.credentialId || null)) {
|
|
640
|
+
session.credentialId = resolved.credentialId ?? null
|
|
641
|
+
changed = true
|
|
642
|
+
}
|
|
643
|
+
if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
|
|
644
|
+
session.fallbackCredentialIds = [...resolved.fallbackCredentialIds]
|
|
645
|
+
changed = true
|
|
646
|
+
}
|
|
647
|
+
if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
|
|
648
|
+
session.apiEndpoint = resolved.apiEndpoint ?? null
|
|
649
|
+
changed = true
|
|
650
|
+
}
|
|
651
|
+
if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
|
|
652
|
+
session.gatewayProfileId = resolved.gatewayProfileId ?? null
|
|
653
|
+
changed = true
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
657
|
+
session.credentialId = agent.credentialId ?? null
|
|
658
|
+
changed = true
|
|
659
|
+
}
|
|
660
|
+
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
661
|
+
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
662
|
+
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
663
|
+
}
|
|
535
664
|
}
|
|
536
665
|
if (!Array.isArray(session.plugins)) {
|
|
537
666
|
session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
@@ -719,6 +848,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
719
848
|
let sessionForRun = pluginsForRun === session.plugins
|
|
720
849
|
? session
|
|
721
850
|
: { ...session, plugins: pluginsForRun }
|
|
851
|
+
if (agentForSession) {
|
|
852
|
+
const preferredRoute = resolvePrimaryAgentRoute(agentForSession)
|
|
853
|
+
if (preferredRoute) {
|
|
854
|
+
sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
722
857
|
let effectiveMessage = message
|
|
723
858
|
|
|
724
859
|
if (pluginsForRun.length > 0) {
|
|
@@ -825,14 +960,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
825
960
|
detail: {
|
|
826
961
|
source,
|
|
827
962
|
internal,
|
|
828
|
-
provider:
|
|
829
|
-
model:
|
|
963
|
+
provider: sessionForRun.provider,
|
|
964
|
+
model: sessionForRun.model,
|
|
830
965
|
messagePreview: effectiveMessage.slice(0, 200),
|
|
831
966
|
hasImage: !!(imagePath || imageUrl),
|
|
832
967
|
},
|
|
833
968
|
})
|
|
834
969
|
|
|
835
|
-
const providerType =
|
|
970
|
+
const providerType = sessionForRun.provider || 'claude-cli'
|
|
836
971
|
const provider = getProvider(providerType)
|
|
837
972
|
if (!provider) throw new Error(`Unknown provider: ${providerType}`)
|
|
838
973
|
|
|
@@ -840,7 +975,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
840
975
|
throw new Error(`Directory not found: ${session.cwd}`)
|
|
841
976
|
}
|
|
842
977
|
|
|
843
|
-
const apiKey = resolveApiKeyForSession(
|
|
978
|
+
const apiKey = resolveApiKeyForSession(sessionForRun, provider)
|
|
844
979
|
|
|
845
980
|
if (!internal) {
|
|
846
981
|
const linkAnalysis = await runLinkUnderstanding(message)
|
|
@@ -1105,57 +1240,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1105
1240
|
: null
|
|
1106
1241
|
const calledNames = new Set((toolEvents || []).map((t) => t.name))
|
|
1107
1242
|
|
|
1108
|
-
const translateToolInvocation = (
|
|
1109
|
-
requestedName: string,
|
|
1110
|
-
rawArgs: Record<string, unknown>,
|
|
1111
|
-
): { toolName: string; args: Record<string, unknown> } => {
|
|
1112
|
-
if (requestedName === 'web_search') {
|
|
1113
|
-
return {
|
|
1114
|
-
toolName: 'web',
|
|
1115
|
-
args: {
|
|
1116
|
-
action: 'search',
|
|
1117
|
-
query: typeof rawArgs.query === 'string' ? rawArgs.query : message.trim(),
|
|
1118
|
-
maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
|
|
1119
|
-
},
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
if (requestedName === 'web_fetch') {
|
|
1123
|
-
return {
|
|
1124
|
-
toolName: 'web',
|
|
1125
|
-
args: {
|
|
1126
|
-
action: 'fetch',
|
|
1127
|
-
url: rawArgs.url,
|
|
1128
|
-
},
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
if (requestedName === 'delegate_to_claude_code') {
|
|
1132
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
|
|
1133
|
-
}
|
|
1134
|
-
if (requestedName === 'delegate_to_codex_cli') {
|
|
1135
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
|
|
1136
|
-
}
|
|
1137
|
-
if (requestedName === 'delegate_to_opencode_cli') {
|
|
1138
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
|
|
1139
|
-
}
|
|
1140
|
-
if (requestedName === 'delegate_to_gemini_cli') {
|
|
1141
|
-
return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const managePrefix = 'manage_'
|
|
1145
|
-
if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
|
|
1146
|
-
const resource = requestedName.slice(managePrefix.length)
|
|
1147
|
-
if (resource) {
|
|
1148
|
-
const { action, id, data, ...rest } = rawArgs
|
|
1149
|
-
return {
|
|
1150
|
-
toolName: 'manage_platform',
|
|
1151
|
-
args: { resource, action, id, data, ...rest },
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
return { toolName: requestedName, args: rawArgs }
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
1243
|
const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
|
|
1160
1244
|
const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
|
|
1161
1245
|
if (blockedReason) {
|
|
@@ -1179,8 +1263,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1179
1263
|
mcpDisabledTools: agent?.mcpDisabledTools,
|
|
1180
1264
|
})
|
|
1181
1265
|
try {
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1266
|
+
const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
|
|
1267
|
+
const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
|
|
1268
|
+
const translated = directTool
|
|
1269
|
+
? { toolName, args }
|
|
1270
|
+
: translateRequestedToolInvocation(toolName, args, message, availableToolNames)
|
|
1271
|
+
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
1184
1272
|
if (!selectedTool?.invoke) return false
|
|
1185
1273
|
const toolInput = JSON.stringify(translated.args)
|
|
1186
1274
|
emit({ t: 'tool_call', toolName, toolInput })
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
compactChatroomMessages,
|
|
7
7
|
buildHistoryForAgent,
|
|
8
8
|
buildSyntheticSession,
|
|
9
|
+
resolveChatroomWorkspaceDir,
|
|
9
10
|
resolveAgentApiEndpoint,
|
|
10
11
|
resolveReplyTargetAgentId,
|
|
11
12
|
} from './chatroom-helpers'
|
|
@@ -156,4 +157,10 @@ describe('chatroom-helpers', () => {
|
|
|
156
157
|
assert.equal(resolveAgentApiEndpoint(agent), 'http://localhost:11434')
|
|
157
158
|
assert.equal(buildSyntheticSession(agent, 'room-1').apiEndpoint, 'http://localhost:11434')
|
|
158
159
|
})
|
|
160
|
+
|
|
161
|
+
it('keeps chatroom execution inside the workspace instead of the repo root', () => {
|
|
162
|
+
const cwd = buildSyntheticSession(makeAgents().default, 'room-safe').cwd
|
|
163
|
+
assert.equal(cwd, resolveChatroomWorkspaceDir('room-safe'))
|
|
164
|
+
assert.match(cwd, /chatrooms[\/\\]room-safe$/)
|
|
165
|
+
})
|
|
159
166
|
})
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
1
2
|
import os from 'os'
|
|
2
|
-
import
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from './storage'
|
|
3
5
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
4
6
|
import { buildIdentityContinuityContext } from './identity-continuity'
|
|
5
7
|
import { genId } from '@/lib/id'
|
|
6
8
|
import { getProvider } from '@/lib/providers'
|
|
7
9
|
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
10
|
+
import { WORKSPACE_DIR } from './data-dir'
|
|
11
|
+
import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
|
|
8
12
|
import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
|
|
9
13
|
|
|
10
14
|
/** Resolve API key from an agent's credentialId */
|
|
@@ -210,11 +214,31 @@ export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<str
|
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
/** Build a synthetic session object for an agent in a chatroom */
|
|
213
|
-
export function
|
|
217
|
+
export function resolveChatroomWorkspaceDir(chatroomId: string): string {
|
|
218
|
+
return path.join(WORKSPACE_DIR, 'chatrooms', chatroomId)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveSyntheticSessionId(chatroomId: string, agentId: string): string {
|
|
222
|
+
return `chatroom-${chatroomId}-${agentId}`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
|
|
214
226
|
return {
|
|
215
|
-
|
|
227
|
+
claudeCode: null,
|
|
228
|
+
codex: null,
|
|
229
|
+
opencode: null,
|
|
230
|
+
gemini: null,
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
235
|
+
const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
|
|
236
|
+
fs.mkdirSync(roomWorkspace, { recursive: true })
|
|
237
|
+
const now = Date.now()
|
|
238
|
+
return applyResolvedRoute({
|
|
239
|
+
id: resolveSyntheticSessionId(chatroomId, agent.id),
|
|
216
240
|
name: `Chatroom session for ${agent.name}`,
|
|
217
|
-
cwd:
|
|
241
|
+
cwd: roomWorkspace,
|
|
218
242
|
user: 'chatroom',
|
|
219
243
|
provider: agent.provider,
|
|
220
244
|
model: agent.model,
|
|
@@ -222,12 +246,81 @@ export function buildSyntheticSession(agent: Agent, chatroomId: string): Session
|
|
|
222
246
|
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
223
247
|
apiEndpoint: resolveAgentApiEndpoint(agent),
|
|
224
248
|
claudeSessionId: null,
|
|
249
|
+
codexThreadId: null,
|
|
250
|
+
opencodeSessionId: null,
|
|
251
|
+
delegateResumeIds: buildEmptyDelegateResumeIds(),
|
|
225
252
|
messages: [],
|
|
226
|
-
createdAt:
|
|
227
|
-
lastActiveAt:
|
|
253
|
+
createdAt: now,
|
|
254
|
+
lastActiveAt: now,
|
|
255
|
+
sessionType: 'human',
|
|
228
256
|
plugins: agent.plugins || agent.tools || [],
|
|
229
257
|
agentId: agent.id,
|
|
258
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function ensureSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
262
|
+
const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
|
|
263
|
+
fs.mkdirSync(roomWorkspace, { recursive: true })
|
|
264
|
+
const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
|
|
265
|
+
const sessions = loadSessions()
|
|
266
|
+
const now = Date.now()
|
|
267
|
+
const existing = sessions[sessionId]
|
|
268
|
+
const session: Session = existing
|
|
269
|
+
? applyResolvedRoute({
|
|
270
|
+
...existing,
|
|
271
|
+
id: sessionId,
|
|
272
|
+
name: `Chatroom session for ${agent.name}`,
|
|
273
|
+
cwd: roomWorkspace,
|
|
274
|
+
user: 'chatroom',
|
|
275
|
+
provider: agent.provider,
|
|
276
|
+
model: agent.model,
|
|
277
|
+
credentialId: agent.credentialId ?? null,
|
|
278
|
+
fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
|
|
279
|
+
apiEndpoint: resolveAgentApiEndpoint(agent),
|
|
280
|
+
sessionType: existing.sessionType || 'human',
|
|
281
|
+
agentId: agent.id,
|
|
282
|
+
plugins: agent.plugins || agent.tools || [],
|
|
283
|
+
tools: agent.plugins || agent.tools || [],
|
|
284
|
+
createdAt: existing.createdAt || now,
|
|
285
|
+
lastActiveAt: now,
|
|
286
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
287
|
+
: applyResolvedRoute({
|
|
288
|
+
...buildSyntheticSession(agent, chatroomId),
|
|
289
|
+
fallbackCredentialIds: Array.isArray(agent.fallbackCredentialIds) ? [...agent.fallbackCredentialIds] : [],
|
|
290
|
+
lastActiveAt: now,
|
|
291
|
+
tools: agent.plugins || agent.tools || [],
|
|
292
|
+
}, resolvePrimaryAgentRoute(agent))
|
|
293
|
+
|
|
294
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
295
|
+
if (!session.delegateResumeIds || typeof session.delegateResumeIds !== 'object') {
|
|
296
|
+
session.delegateResumeIds = buildEmptyDelegateResumeIds()
|
|
230
297
|
}
|
|
298
|
+
if (session.codexThreadId === undefined) session.codexThreadId = null
|
|
299
|
+
if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
|
|
300
|
+
sessions[sessionId] = session
|
|
301
|
+
saveSessions(sessions)
|
|
302
|
+
return session
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function appendSyntheticSessionMessage(
|
|
306
|
+
sessionId: string,
|
|
307
|
+
role: 'user' | 'assistant',
|
|
308
|
+
text: string,
|
|
309
|
+
): void {
|
|
310
|
+
const trimmed = String(text || '').trim()
|
|
311
|
+
if (!trimmed) return
|
|
312
|
+
const sessions = loadSessions()
|
|
313
|
+
const session = sessions[sessionId]
|
|
314
|
+
if (!session) return
|
|
315
|
+
if (!Array.isArray(session.messages)) session.messages = []
|
|
316
|
+
session.messages.push({
|
|
317
|
+
role,
|
|
318
|
+
text: trimmed,
|
|
319
|
+
time: Date.now(),
|
|
320
|
+
})
|
|
321
|
+
session.lastActiveAt = Date.now()
|
|
322
|
+
sessions[sessionId] = session
|
|
323
|
+
saveSessions(sessions)
|
|
231
324
|
}
|
|
232
325
|
|
|
233
326
|
/** Build agent's system prompt including skills and identity context */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { describe, it } from 'node:test'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
|
|
9
|
+
|
|
10
|
+
function runWithTempDataDir(script: string) {
|
|
11
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-chatroom-session-'))
|
|
12
|
+
try {
|
|
13
|
+
const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
|
|
14
|
+
cwd: repoRoot,
|
|
15
|
+
env: {
|
|
16
|
+
...process.env,
|
|
17
|
+
DATA_DIR: path.join(tempDir, 'data'),
|
|
18
|
+
WORKSPACE_DIR: path.join(tempDir, 'workspace'),
|
|
19
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
})
|
|
22
|
+
assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
|
|
23
|
+
const lines = (result.stdout || '')
|
|
24
|
+
.trim()
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((line) => line.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
|
|
29
|
+
return JSON.parse(jsonLine || '{}')
|
|
30
|
+
} finally {
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('chatroom synthetic session persistence', () => {
|
|
36
|
+
it('reuses stored synthetic sessions and preserves delegate resume state', () => {
|
|
37
|
+
const output = runWithTempDataDir(`
|
|
38
|
+
const helpersMod = await import('./src/lib/server/chatroom-helpers.ts')
|
|
39
|
+
const helpers = helpersMod.default || helpersMod
|
|
40
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
41
|
+
const storage = storageMod.default || storageMod
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
const agent = {
|
|
44
|
+
id: 'default',
|
|
45
|
+
name: 'Molly',
|
|
46
|
+
description: '',
|
|
47
|
+
systemPrompt: '',
|
|
48
|
+
provider: 'openai',
|
|
49
|
+
model: 'gpt-4o',
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
plugins: ['delegate'],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const first = helpers.ensureSyntheticSession(agent, 'room-1')
|
|
56
|
+
helpers.appendSyntheticSessionMessage(first.id, 'user', 'first prompt')
|
|
57
|
+
|
|
58
|
+
const sessions = storage.loadSessions()
|
|
59
|
+
sessions[first.id].delegateResumeIds = {
|
|
60
|
+
claudeCode: null,
|
|
61
|
+
codex: 'resume-123',
|
|
62
|
+
opencode: null,
|
|
63
|
+
gemini: null,
|
|
64
|
+
}
|
|
65
|
+
storage.saveSessions(sessions)
|
|
66
|
+
|
|
67
|
+
const second = helpers.ensureSyntheticSession({ ...agent, model: 'gpt-4.1' }, 'room-1')
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
sessionId: second.id,
|
|
70
|
+
cwd: second.cwd,
|
|
71
|
+
model: second.model,
|
|
72
|
+
messageCount: second.messages.length,
|
|
73
|
+
firstMessage: second.messages[0]?.text || '',
|
|
74
|
+
delegateResumeIds: second.delegateResumeIds,
|
|
75
|
+
plugins: second.plugins || [],
|
|
76
|
+
}))
|
|
77
|
+
`)
|
|
78
|
+
|
|
79
|
+
assert.equal(output.sessionId, 'chatroom-room-1-default')
|
|
80
|
+
assert.match(String(output.cwd), /chatrooms[\/\\]room-1$/)
|
|
81
|
+
assert.equal(output.model, 'gpt-4.1')
|
|
82
|
+
assert.equal(output.messageCount, 1)
|
|
83
|
+
assert.equal(output.firstMessage, 'first prompt')
|
|
84
|
+
assert.equal(output.delegateResumeIds?.codex, 'resume-123')
|
|
85
|
+
assert.deepEqual(output.plugins, ['delegate'])
|
|
86
|
+
})
|
|
87
|
+
})
|