@swarmclawai/swarmclaw 0.7.3 → 0.7.4

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 (147) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +3 -1
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
package/src/cli/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Command } from 'commander'
4
4
  import { pathToFileURL } from 'node:url'
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
5
7
  import {
6
8
  SETUP_PROVIDERS,
7
9
  DEFAULT_AGENTS,
@@ -33,7 +35,21 @@ const DEFAULT_BASE_URL =
33
35
  || process.env.SWARMCLAW_BASE_URL
34
36
  || 'http://localhost:3456'
35
37
 
36
- const DEFAULT_ACCESS_KEY = process.env.SWARMCLAW_ACCESS_KEY || ''
38
+ function resolveDefaultAccessKey(cwd: string = process.cwd()): string {
39
+ const envKey = (
40
+ process.env.SWARMCLAW_ACCESS_KEY
41
+ || process.env.SWARMCLAW_API_KEY
42
+ || process.env.SC_ACCESS_KEY
43
+ || ''
44
+ ).trim()
45
+ if (envKey) return envKey
46
+
47
+ const keyFile = path.join(cwd, 'platform-api-key.txt')
48
+ if (!fs.existsSync(keyFile)) return ''
49
+ return fs.readFileSync(keyFile, 'utf8').trim()
50
+ }
51
+
52
+ const DEFAULT_ACCESS_KEY = resolveDefaultAccessKey()
37
53
 
38
54
  function normalizeBaseUrl(value: string): string {
39
55
  const trimmed = value.trim()
@@ -210,10 +226,10 @@ async function resolveSetupAccessKey(ctx: CliContext): Promise<{
210
226
  const firstTime = status?.firstTime === true
211
227
 
212
228
  if (firstTime) {
213
- throw new Error('No access key provided. Read the generated key from the launch terminal or .env.local, then pass --key (or SWARMCLAW_ACCESS_KEY).')
229
+ throw new Error('No access key provided. Read the generated key from the launch terminal or .env.local, then pass --key, set SWARMCLAW_ACCESS_KEY / SWARMCLAW_API_KEY, or use platform-api-key.txt.')
214
230
  }
215
231
 
216
- throw new Error('No access key provided. Pass --key (or SWARMCLAW_ACCESS_KEY).')
232
+ throw new Error('No access key provided. Pass --key, set SWARMCLAW_ACCESS_KEY / SWARMCLAW_API_KEY, or use platform-api-key.txt.')
217
233
  }
218
234
 
219
235
  function printResult(value: unknown, rawOutput: boolean): void {
@@ -1357,7 +1373,7 @@ export async function runCli(argv: string[] = process.argv.slice(2)): Promise<nu
1357
1373
  ? null
1358
1374
  : checkForUpdate(
1359
1375
  normalizeBaseUrl(process.env.SWARMCLAW_URL || process.env.SWARMCLAW_BASE_URL || 'http://localhost:3456'),
1360
- (process.env.SWARMCLAW_ACCESS_KEY || '').trim(),
1376
+ resolveDefaultAccessKey(),
1361
1377
  )
1362
1378
 
1363
1379
  await program.parseAsync(['node', 'swarmclaw', ...argv])
@@ -0,0 +1,59 @@
1
+ 'use strict'
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+
4
+ const test = require('node:test')
5
+ const assert = require('node:assert/strict')
6
+ const fs = require('node:fs')
7
+ const os = require('node:os')
8
+ const path = require('node:path')
9
+
10
+ function loadServerCmdForHome(homeDir) {
11
+ const modPath = require.resolve('../../bin/server-cmd.js')
12
+ const previousHome = process.env.SWARMCLAW_HOME
13
+ process.env.SWARMCLAW_HOME = homeDir
14
+ delete require.cache[modPath]
15
+ const loaded = require(modPath)
16
+ if (previousHome === undefined) delete process.env.SWARMCLAW_HOME
17
+ else process.env.SWARMCLAW_HOME = previousHome
18
+ delete require.cache[modPath]
19
+ return loaded
20
+ }
21
+
22
+ test('needsBuild returns true when no build marker exists', () => {
23
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
24
+ const serverCmd = loadServerCmdForHome(homeDir)
25
+ assert.equal(serverCmd.needsBuild(false), true)
26
+ fs.rmSync(homeDir, { recursive: true, force: true })
27
+ })
28
+
29
+ test('needsBuild returns false when build marker version matches and standalone server exists', () => {
30
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
31
+ const serverCmd = loadServerCmdForHome(homeDir)
32
+
33
+ fs.mkdirSync(path.join(homeDir, '.next', 'standalone'), { recursive: true })
34
+ fs.writeFileSync(path.join(homeDir, '.next', 'standalone', 'server.js'), 'console.log("ok")\n', 'utf8')
35
+ fs.writeFileSync(
36
+ path.join(homeDir, '.built'),
37
+ JSON.stringify({ builtAt: new Date().toISOString(), version: serverCmd.getVersion() }),
38
+ 'utf8',
39
+ )
40
+
41
+ assert.equal(serverCmd.needsBuild(false), false)
42
+ fs.rmSync(homeDir, { recursive: true, force: true })
43
+ })
44
+
45
+ test('needsBuild returns true when build marker version is stale', () => {
46
+ const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
47
+ const serverCmd = loadServerCmdForHome(homeDir)
48
+
49
+ fs.mkdirSync(path.join(homeDir, '.next', 'standalone'), { recursive: true })
50
+ fs.writeFileSync(path.join(homeDir, '.next', 'standalone', 'server.js'), 'console.log("ok")\n', 'utf8')
51
+ fs.writeFileSync(
52
+ path.join(homeDir, '.built'),
53
+ JSON.stringify({ builtAt: new Date().toISOString(), version: '0.0.0-test' }),
54
+ 'utf8',
55
+ )
56
+
57
+ assert.equal(serverCmd.needsBuild(false), true)
58
+ fs.rmSync(homeDir, { recursive: true, force: true })
59
+ })
package/src/cli/spec.js CHANGED
@@ -127,6 +127,16 @@ const COMMAND_GROUPS = {
127
127
  },
128
128
  },
129
129
  },
130
+ 'external-agents': {
131
+ description: 'Manage external agent runtimes',
132
+ commands: {
133
+ list: { description: 'List external agent runtimes', method: 'GET', path: '/external-agents' },
134
+ create: { description: 'Register an external agent runtime', method: 'POST', path: '/external-agents' },
135
+ update: { description: 'Update an external agent runtime', method: 'PUT', path: '/external-agents/:id', params: ['id'] },
136
+ delete: { description: 'Delete an external agent runtime', method: 'DELETE', path: '/external-agents/:id', params: ['id'] },
137
+ heartbeat: { description: 'Record an external agent heartbeat', method: 'POST', path: '/external-agents/:id/heartbeat', params: ['id'] },
138
+ },
139
+ },
130
140
  uploads: {
131
141
  description: 'Manage uploaded artifacts',
132
142
  commands: {
@@ -143,6 +153,16 @@ const COMMAND_GROUPS = {
143
153
  open: { description: 'Open a local file path via host default app/browser', method: 'POST', path: '/files/open' },
144
154
  },
145
155
  },
156
+ gateways: {
157
+ description: 'Manage named OpenClaw gateway profiles',
158
+ commands: {
159
+ list: { description: 'List configured gateway profiles', method: 'GET', path: '/gateways' },
160
+ create: { description: 'Create a gateway profile', method: 'POST', path: '/gateways' },
161
+ update: { description: 'Update a gateway profile', method: 'PUT', path: '/gateways/:id', params: ['id'] },
162
+ delete: { description: 'Delete a gateway profile', method: 'DELETE', path: '/gateways/:id', params: ['id'] },
163
+ health: { description: 'Run a gateway health check', method: 'GET', path: '/gateways/:id/health', params: ['id'] },
164
+ },
165
+ },
146
166
  logs: {
147
167
  description: 'Application logs',
148
168
  commands: {
@@ -291,8 +311,6 @@ const COMMAND_GROUPS = {
291
311
  'messages-delete': { description: 'Delete a message from a chat', method: 'DELETE', path: '/chats/:id/messages', params: ['id'] },
292
312
  fork: { description: 'Fork chat from a specific message index', method: 'POST', path: '/chats/:id/fork', params: ['id'] },
293
313
  'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/chats/:id/edit-resend', params: ['id'] },
294
- 'main-loop': { description: 'Get main mission loop state for a chat', method: 'GET', path: '/chats/:id/main-loop', params: ['id'] },
295
- 'main-loop-action': { description: 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', method: 'POST', path: '/chats/:id/main-loop', params: ['id'] },
296
314
  chat: { description: 'Send chat message (SSE stream)', method: 'POST', path: '/chats/:id/chat', params: ['id'], stream: true, waitable: true },
297
315
  stop: { description: 'Cancel active/running chat work', method: 'POST', path: '/chats/:id/stop', params: ['id'] },
298
316
  clear: { description: 'Clear chat history', method: 'POST', path: '/chats/:id/clear', params: ['id'] },
@@ -7,7 +7,7 @@ import { api } from '@/lib/api-client'
7
7
  import { BottomSheet } from '@/components/shared/bottom-sheet'
8
8
  import { toast } from 'sonner'
9
9
  import { ModelCombobox } from '@/components/shared/model-combobox'
10
- import type { ProviderType, ClaudeSkill, AgentWallet } from '@/types'
10
+ import type { ProviderType, ClaudeSkill, AgentWallet, AgentPackManifest, AgentRoutingStrategy, AgentRoutingTarget } from '@/types'
11
11
  import { WalletSection } from '@/components/wallets/wallet-section'
12
12
  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
13
13
  import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
@@ -83,6 +83,8 @@ export function AgentSheet() {
83
83
  const loadProjects = useAppStore((s) => s.loadProjects)
84
84
  const providers = useAppStore((s) => s.providers)
85
85
  const loadProviders = useAppStore((s) => s.loadProviders)
86
+ const gatewayProfiles = useAppStore((s) => s.gatewayProfiles)
87
+ const loadGatewayProfiles = useAppStore((s) => s.loadGatewayProfiles)
86
88
  const credentials = useAppStore((s) => s.credentials)
87
89
  const loadCredentials = useAppStore((s) => s.loadCredentials)
88
90
  const appSettings = useAppStore((s) => s.appSettings)
@@ -110,6 +112,9 @@ export function AgentSheet() {
110
112
  const [model, setModel] = useState('')
111
113
  const [credentialId, setCredentialId] = useState<string | null>(null)
112
114
  const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
115
+ const [gatewayProfileId, setGatewayProfileId] = useState<string | null>(null)
116
+ const [routingStrategy, setRoutingStrategy] = useState<AgentRoutingStrategy>('single')
117
+ const [routingTargets, setRoutingTargets] = useState<AgentRoutingTarget[]>([])
113
118
  const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
114
119
  const [subAgentIds, setAgentAgentIds] = useState<string[]>([])
115
120
  const [tools, setTools] = useState<string[]>([])
@@ -129,6 +134,8 @@ export function AgentSheet() {
129
134
  const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
130
135
  const [uploading, setUploading] = useState(false)
131
136
  const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
137
+ const [memoryScopeMode, setMemoryScopeMode] = useState<'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'>('auto')
138
+ const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
132
139
  const [autoRecovery, setAutoRecovery] = useState(false)
133
140
  const [voiceId, setVoiceId] = useState('')
134
141
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
@@ -182,6 +189,7 @@ export function AgentSheet() {
182
189
  const currentProvider = providers.find((p) => p.id === provider)
183
190
  const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
184
191
  const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
192
+ const openclawGatewayProfiles = gatewayProfiles.filter((item) => item.provider === 'openclaw')
185
193
  const editing = editingId ? agents[editingId] : null
186
194
  const hasNativeCapabilities = NATIVE_CAPABILITY_PROVIDER_IDS.has(provider)
187
195
 
@@ -193,6 +201,7 @@ export function AgentSheet() {
193
201
  useEffect(() => {
194
202
  if (open) {
195
203
  loadProviders()
204
+ loadGatewayProfiles()
196
205
  loadCredentials()
197
206
  loadSkills()
198
207
  loadProjects()
@@ -210,6 +219,9 @@ export function AgentSheet() {
210
219
  setModel(editing.model)
211
220
  setCredentialId(editing.credentialId || null)
212
221
  setApiEndpoint(editing.apiEndpoint || null)
222
+ setGatewayProfileId(editing.gatewayProfileId || null)
223
+ setRoutingStrategy(editing.routingStrategy || 'single')
224
+ setRoutingTargets(editing.routingTargets || [])
213
225
  setPlatformAssignScope(editing.platformAssignScope || 'self')
214
226
  setAgentAgentIds(editing.subAgentIds || [])
215
227
  setTools(editing.plugins || [])
@@ -226,6 +238,8 @@ export function AgentSheet() {
226
238
  setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
227
239
  setAvatarUrl(editing.avatarUrl || null)
228
240
  setThinkingLevel(editing.thinkingLevel || '')
241
+ setMemoryScopeMode(editing.memoryScopeMode || 'auto')
242
+ setMemoryTierPreference(editing.memoryTierPreference || 'blended')
229
243
  setAutoRecovery(editing.autoRecovery || false)
230
244
  setVoiceId(editing.elevenLabsVoiceId || '')
231
245
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
@@ -272,6 +286,9 @@ export function AgentSheet() {
272
286
  setModel('')
273
287
  setCredentialId(null)
274
288
  setApiEndpoint(null)
289
+ setGatewayProfileId(null)
290
+ setRoutingStrategy('single')
291
+ setRoutingTargets([])
275
292
  setPlatformAssignScope('self')
276
293
  setAgentAgentIds([])
277
294
  setTools([])
@@ -286,6 +303,8 @@ export function AgentSheet() {
286
303
  setProjectId(undefined)
287
304
  setAvatarSeed('')
288
305
  setThinkingLevel('')
306
+ setMemoryScopeMode('auto')
307
+ setMemoryTierPreference('blended')
289
308
  setAutoRecovery(false)
290
309
  setVoiceId('')
291
310
  setHeartbeatEnabled(false)
@@ -369,6 +388,45 @@ export function AgentSheet() {
369
388
  setEditingId(null)
370
389
  }
371
390
 
391
+ const applyGatewayProfileSelection = (nextGatewayProfileId: string | null) => {
392
+ setGatewayProfileId(nextGatewayProfileId)
393
+ const gateway = openclawGatewayProfiles.find((item) => item.id === nextGatewayProfileId)
394
+ if (!gateway) return
395
+ setProvider('openclaw')
396
+ setOpenclawEnabled(true)
397
+ setApiEndpoint(gateway.endpoint)
398
+ if (gateway.credentialId) setCredentialId(gateway.credentialId)
399
+ if (!model) setModel('default')
400
+ }
401
+
402
+ const updateRoutingTarget = (targetId: string, patch: Partial<AgentRoutingTarget>) => {
403
+ setRoutingTargets((current) => current.map((target) => (
404
+ target.id === targetId
405
+ ? { ...target, ...patch }
406
+ : target
407
+ )))
408
+ }
409
+
410
+ const removeRoutingTarget = (targetId: string) => {
411
+ setRoutingTargets((current) => current.filter((target) => target.id !== targetId))
412
+ }
413
+
414
+ const addRoutingTargetFromCurrent = () => {
415
+ const nextTarget: AgentRoutingTarget = {
416
+ id: crypto.randomUUID(),
417
+ label: routingTargets.length === 0 ? 'Primary route' : `Route ${routingTargets.length + 1}`,
418
+ role: routingTargets.length === 0 ? 'primary' : 'backup',
419
+ provider,
420
+ model,
421
+ credentialId,
422
+ fallbackCredentialIds,
423
+ apiEndpoint,
424
+ gatewayProfileId,
425
+ priority: routingTargets.length + 1,
426
+ }
427
+ setRoutingTargets((current) => [...current, nextTarget])
428
+ }
429
+
372
430
  const handleSave = async () => {
373
431
  // For any endpoint, just ensure bare host:port gets a protocol prepended
374
432
  let normalizedEndpoint = apiEndpoint
@@ -404,6 +462,12 @@ export function AgentSheet() {
404
462
  model,
405
463
  credentialId,
406
464
  apiEndpoint: normalizedEndpoint,
465
+ gatewayProfileId,
466
+ routingStrategy,
467
+ routingTargets: routingTargets.map((target, index) => ({
468
+ ...target,
469
+ priority: typeof target.priority === 'number' ? target.priority : index + 1,
470
+ })),
407
471
  subAgentIds: canDelegateToAgents ? subAgentIds : [],
408
472
  tools,
409
473
  skills,
@@ -417,6 +481,8 @@ export function AgentSheet() {
417
481
  avatarSeed: avatarSeed.trim() || undefined,
418
482
  avatarUrl: avatarUrl || null,
419
483
  thinkingLevel: thinkingLevel || undefined,
484
+ memoryScopeMode,
485
+ memoryTierPreference,
420
486
  autoRecovery,
421
487
  elevenLabsVoiceId: voiceId.trim() || null,
422
488
  heartbeatEnabled,
@@ -460,15 +526,40 @@ export function AgentSheet() {
460
526
 
461
527
  const handleExport = () => {
462
528
  if (!editing) return
463
- const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...exportData } = editing
464
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
529
+ const pack: AgentPackManifest = {
530
+ schemaVersion: 1,
531
+ kind: 'swarmclaw-agent-pack',
532
+ name: `${editing.name} Pack`,
533
+ description: editing.description || undefined,
534
+ exportedAt: Date.now(),
535
+ recommendedProviders: [editing.provider],
536
+ agents: [{
537
+ id: editing.name.replace(/\s+/g, '-').toLowerCase(),
538
+ name: editing.name,
539
+ description: editing.description || undefined,
540
+ provider: editing.provider,
541
+ model: editing.model,
542
+ credentialId: editing.credentialId || null,
543
+ fallbackCredentialIds: editing.fallbackCredentialIds || [],
544
+ apiEndpoint: editing.apiEndpoint || null,
545
+ gatewayProfileId: editing.gatewayProfileId || null,
546
+ routingStrategy: editing.routingStrategy || null,
547
+ routingTargets: editing.routingTargets || [],
548
+ tools: editing.tools,
549
+ plugins: editing.plugins,
550
+ capabilities: editing.capabilities,
551
+ soul: editing.soul,
552
+ systemPrompt: editing.systemPrompt,
553
+ }],
554
+ }
555
+ const blob = new Blob([JSON.stringify(pack, null, 2)], { type: 'application/json' })
465
556
  const url = URL.createObjectURL(blob)
466
557
  const a = document.createElement('a')
467
558
  a.href = url
468
- a.download = `${editing.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.agent.json`
559
+ a.download = `${editing.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.agent-pack.json`
469
560
  a.click()
470
561
  URL.revokeObjectURL(url)
471
- toast.success('Agent exported')
562
+ toast.success('Agent pack exported')
472
563
  }
473
564
 
474
565
  const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -478,11 +569,15 @@ export function AgentSheet() {
478
569
  reader.onload = async (ev) => {
479
570
  try {
480
571
  const data = JSON.parse(ev.target?.result as string)
572
+ const importedAgent = data?.kind === 'swarmclaw-agent-pack'
573
+ ? data?.agents?.[0]
574
+ : data
575
+ if (!importedAgent || typeof importedAgent !== 'object') throw new Error('Invalid agent pack')
481
576
  // Strip IDs and timestamps
482
- const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = data
577
+ const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = importedAgent
483
578
  await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
484
579
  await loadAgents()
485
- toast.success('Agent imported')
580
+ toast.success(data?.kind === 'swarmclaw-agent-pack' ? 'Agent pack imported' : 'Agent imported')
486
581
  onClose()
487
582
  } catch (err) {
488
583
  toast.error('Invalid agent JSON file')
@@ -582,6 +677,7 @@ export function AgentSheet() {
582
677
  setModel('')
583
678
  setApiEndpoint(null)
584
679
  setCredentialId(null)
680
+ setGatewayProfileId(null)
585
681
  setTestStatus('idle')
586
682
  setTestMessage('')
587
683
  setTestErrorCode(null)
@@ -771,6 +867,29 @@ export function AgentSheet() {
771
867
  <p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
772
868
  </div>
773
869
 
870
+ <div className="mb-8">
871
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
872
+ Memory Defaults <HintTip text="Controls where this agent should look first and which memory tier it should favor when writing or recalling context." />
873
+ </label>
874
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
875
+ <select value={memoryScopeMode} onChange={(e) => setMemoryScopeMode(e.target.value as typeof memoryScopeMode)} className={inputClass}>
876
+ <option value="auto">Scope: Auto</option>
877
+ <option value="all">Scope: All memories</option>
878
+ <option value="global">Scope: Global only</option>
879
+ <option value="agent">Scope: Agent memories</option>
880
+ <option value="session">Scope: Session memories</option>
881
+ <option value="project">Scope: Project memories</option>
882
+ </select>
883
+ <select value={memoryTierPreference} onChange={(e) => setMemoryTierPreference(e.target.value as typeof memoryTierPreference)} className={inputClass}>
884
+ <option value="blended">Tier: Blended</option>
885
+ <option value="working">Tier: Working</option>
886
+ <option value="durable">Tier: Durable</option>
887
+ <option value="archive">Tier: Archive</option>
888
+ </select>
889
+ </div>
890
+ <p className="text-[11px] text-text-3/70 mt-1.5">Use working for fast recent context, durable for facts/preferences, and archive for long-lived history.</p>
891
+ </div>
892
+
774
893
  {/* Auto-Recovery */}
775
894
  <div className="mb-8">
776
895
  <div className="flex items-center justify-between mb-1.5">
@@ -1177,6 +1296,23 @@ export function AgentSheet() {
1177
1296
  {/* OpenClaw Gateway Fields */}
1178
1297
  {openclawEnabled && (
1179
1298
  <div className="mb-8 space-y-5">
1299
+ {openclawGatewayProfiles.length > 0 && (
1300
+ <div>
1301
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Gateway Profile</label>
1302
+ <select
1303
+ value={gatewayProfileId || ''}
1304
+ onChange={(e) => applyGatewayProfileSelection(e.target.value || null)}
1305
+ className={inputClass}
1306
+ >
1307
+ <option value="">Custom endpoint</option>
1308
+ {openclawGatewayProfiles.map((gateway) => (
1309
+ <option key={gateway.id} value={gateway.id}>
1310
+ {gateway.name}{gateway.isDefault ? ' (default)' : ''}
1311
+ </option>
1312
+ ))}
1313
+ </select>
1314
+ </div>
1315
+ )}
1180
1316
  {/* Connection fields */}
1181
1317
  <div className="space-y-4">
1182
1318
  <div>
@@ -1378,6 +1514,7 @@ export function AgentSheet() {
1378
1514
  key={p.id}
1379
1515
  onClick={() => {
1380
1516
  setProvider(p.id)
1517
+ setGatewayProfileId(null)
1381
1518
  }}
1382
1519
  className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
1383
1520
  active:scale-[0.97] text-[14px] font-600 border
@@ -1405,6 +1542,9 @@ export function AgentSheet() {
1405
1542
  onChange={setModel}
1406
1543
  models={currentProvider.models}
1407
1544
  defaultModels={currentProvider.defaultModels}
1545
+ credentialId={credentialId}
1546
+ apiEndpoint={apiEndpoint}
1547
+ supportsDiscovery={currentProvider.supportsModelDiscovery}
1408
1548
  className={`${inputClass} cursor-pointer`}
1409
1549
  />
1410
1550
  </div>
@@ -1558,6 +1698,108 @@ export function AgentSheet() {
1558
1698
  </div>
1559
1699
  )}
1560
1700
 
1701
+ <div className="mb-8">
1702
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
1703
+ Model Routing <HintTip text="Route this agent through a provider/model pool instead of a single fixed model. The base provider remains the default when no route matches." />
1704
+ </label>
1705
+ <div className="flex items-center gap-3 mb-3">
1706
+ <select value={routingStrategy} onChange={(e) => setRoutingStrategy(e.target.value as AgentRoutingStrategy)} className={inputClass}>
1707
+ <option value="single">Single route</option>
1708
+ <option value="balanced">Balanced</option>
1709
+ <option value="economy">Economy</option>
1710
+ <option value="premium">Premium</option>
1711
+ <option value="reasoning">Reasoning</option>
1712
+ </select>
1713
+ <button
1714
+ type="button"
1715
+ onClick={addRoutingTargetFromCurrent}
1716
+ className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-700 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
1717
+ >
1718
+ + Add Current Route
1719
+ </button>
1720
+ </div>
1721
+ <div className="space-y-3">
1722
+ {routingTargets.map((target, index) => {
1723
+ const targetCredentials = Object.values(credentials).filter((item) => item.provider === target.provider)
1724
+ return (
1725
+ <div key={target.id} className="p-4 rounded-[12px] border border-white/[0.08] bg-white/[0.02] space-y-3">
1726
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1727
+ <input
1728
+ value={target.label || ''}
1729
+ onChange={(e) => updateRoutingTarget(target.id, { label: e.target.value })}
1730
+ placeholder={`Route ${index + 1} label`}
1731
+ className={inputClass}
1732
+ />
1733
+ <select value={target.role || 'backup'} onChange={(e) => updateRoutingTarget(target.id, { role: e.target.value as AgentRoutingTarget['role'] })} className={inputClass}>
1734
+ <option value="primary">Primary</option>
1735
+ <option value="economy">Economy</option>
1736
+ <option value="premium">Premium</option>
1737
+ <option value="reasoning">Reasoning</option>
1738
+ <option value="backup">Backup</option>
1739
+ </select>
1740
+ </div>
1741
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
1742
+ <select value={target.provider} onChange={(e) => updateRoutingTarget(target.id, { provider: e.target.value as ProviderType, gatewayProfileId: e.target.value === 'openclaw' ? target.gatewayProfileId : null })} className={inputClass}>
1743
+ {providers.map((item) => (
1744
+ <option key={item.id} value={item.id}>{item.name}</option>
1745
+ ))}
1746
+ </select>
1747
+ <input
1748
+ value={target.model}
1749
+ onChange={(e) => updateRoutingTarget(target.id, { model: e.target.value })}
1750
+ placeholder="Model"
1751
+ className={inputClass}
1752
+ />
1753
+ </div>
1754
+ {target.provider === 'openclaw' && openclawGatewayProfiles.length > 0 && (
1755
+ <select
1756
+ value={target.gatewayProfileId || ''}
1757
+ onChange={(e) => {
1758
+ const nextId = e.target.value || null
1759
+ const gateway = openclawGatewayProfiles.find((item) => item.id === nextId)
1760
+ updateRoutingTarget(target.id, {
1761
+ gatewayProfileId: nextId,
1762
+ apiEndpoint: gateway?.endpoint || target.apiEndpoint || null,
1763
+ credentialId: gateway?.credentialId || target.credentialId || null,
1764
+ model: target.model || 'default',
1765
+ })
1766
+ }}
1767
+ className={inputClass}
1768
+ >
1769
+ <option value="">Custom OpenClaw endpoint</option>
1770
+ {openclawGatewayProfiles.map((gateway) => (
1771
+ <option key={gateway.id} value={gateway.id}>{gateway.name}</option>
1772
+ ))}
1773
+ </select>
1774
+ )}
1775
+ <div className="grid grid-cols-1 md:grid-cols-[1fr_auto] gap-3">
1776
+ <input
1777
+ value={target.apiEndpoint || ''}
1778
+ onChange={(e) => updateRoutingTarget(target.id, { apiEndpoint: e.target.value || null })}
1779
+ placeholder="Endpoint (optional)"
1780
+ className={`${inputClass} font-mono text-[14px]`}
1781
+ />
1782
+ <select value={target.credentialId || ''} onChange={(e) => updateRoutingTarget(target.id, { credentialId: e.target.value || null })} className={inputClass}>
1783
+ <option value="">No key</option>
1784
+ {targetCredentials.map((item) => (
1785
+ <option key={item.id} value={item.id}>{item.name}</option>
1786
+ ))}
1787
+ </select>
1788
+ </div>
1789
+ <div className="flex justify-end">
1790
+ <button type="button" onClick={() => removeRoutingTarget(target.id)} className="px-3 py-1.5 rounded-[8px] border border-red-400/20 bg-red-400/[0.06] text-[12px] font-700 text-red-300 hover:bg-red-400/[0.1] transition-all cursor-pointer">
1791
+ Remove Route
1792
+ </button>
1793
+ </div>
1794
+ </div>
1795
+ )
1796
+ })}
1797
+ </div>
1798
+ {routingTargets.length === 0 && (
1799
+ <p className="text-[11px] text-text-3/70 mt-2">No route pool yet. Add one if this agent should switch between cheaper, stronger, or gateway-specific models.</p>
1800
+ )}
1801
+ </div>
1802
+
1561
1803
  {/* Plugins — hidden for providers that manage capabilities outside LangGraph */}
1562
1804
  {!hasNativeCapabilities && (
1563
1805
  <div className="mb-8">
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
3
4
  import { useCallback, useEffect, useState, type ReactNode } from 'react'
4
5
  import type { Agent } from '@/types'
5
6
  import { useAppStore } from '@/stores/use-app-store'
@@ -192,7 +193,7 @@ function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDele
192
193
  { label: 'Provider', value: PROVIDER_LABELS[agent.provider] || agent.provider.replace(/-/g, ' ') },
193
194
  { label: 'Model', value: agent.model || 'Default' },
194
195
  { label: 'Plugins', value: String(agent.plugins?.length ?? 0) },
195
- { label: 'Heartbeat', value: agent.heartbeatEnabled ? `Every ${agent.heartbeatIntervalSec ?? 120}s` : 'Off' },
196
+ { label: 'Heartbeat', value: agent.heartbeatEnabled ? `Every ${agent.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC}s` : 'Off' },
196
197
  ]
197
198
 
198
199
  return (
@@ -417,7 +418,7 @@ function AdvancedTab({ agent }: { agent: Agent }) {
417
418
  <div className={panelCardClass('p-4')}>
418
419
  <SectionLabel>Heartbeat</SectionLabel>
419
420
  <p className="text-[13px] text-text-2">
420
- Every {agent.heartbeatIntervalSec ?? 120}s
421
+ Every {agent.heartbeatIntervalSec ?? DEFAULT_HEARTBEAT_INTERVAL_SEC}s
421
422
  {agent.heartbeatModel && ` (${agent.heartbeatModel})`}
422
423
  </p>
423
424
  </div>
@@ -51,7 +51,10 @@ export function SandboxEnvPanel() {
51
51
 
52
52
  return (
53
53
  <div className="flex flex-col gap-2">
54
- <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">Sandbox Env Allowlist</label>
54
+ <label className="block text-[11px] font-600 uppercase tracking-wider text-text-3/50">OpenClaw Sandbox Env Allowlist</label>
55
+ <p className="text-[12px] text-text-3/50">
56
+ Applies to OpenClaw gateway Docker sandboxes only. It does not affect SwarmClaw&apos;s local <code className="font-mono">sandbox_exec</code> tool.
57
+ </p>
55
58
  <div className="flex flex-col gap-1">
56
59
  {available.map((key) => (
57
60
  <label key={key} className="flex items-center gap-2 py-1 px-2 rounded-[8px] hover:bg-white/[0.02] cursor-pointer transition-colors">