@swarmclawai/swarmclaw 1.2.3 → 1.2.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.
Files changed (273) hide show
  1. package/README.md +20 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/models/route.test.ts +60 -0
  49. package/src/app/api/providers/[id]/models/route.ts +33 -1
  50. package/src/app/api/providers/[id]/route.test.ts +49 -0
  51. package/src/app/api/providers/[id]/route.ts +30 -1
  52. package/src/app/api/providers/ollama/route.ts +6 -5
  53. package/src/app/api/schedules/[id]/route.ts +14 -108
  54. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  55. package/src/app/api/schedules/route.ts +9 -51
  56. package/src/app/api/settings/route.ts +4 -3
  57. package/src/app/api/setup/check-provider/route.ts +15 -1
  58. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  59. package/src/app/api/system/status/route.ts +2 -2
  60. package/src/app/api/tasks/[id]/route.ts +16 -202
  61. package/src/app/api/tasks/bulk/route.ts +5 -86
  62. package/src/app/api/tasks/metrics/route.ts +2 -1
  63. package/src/app/api/tasks/route.ts +11 -171
  64. package/src/app/api/upload/route.ts +1 -1
  65. package/src/app/api/uploads/[filename]/route.ts +1 -1
  66. package/src/app/api/uploads/route.ts +1 -1
  67. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  68. package/src/app/layout.tsx +9 -6
  69. package/src/app/protocols/page.tsx +71 -89
  70. package/src/app/tasks/page.tsx +32 -32
  71. package/src/cli/index.js +1 -0
  72. package/src/cli/spec.js +1 -0
  73. package/src/components/agents/agent-sheet.tsx +51 -25
  74. package/src/components/agents/inspector-panel.tsx +15 -4
  75. package/src/components/auth/setup-wizard/index.tsx +27 -18
  76. package/src/components/auth/setup-wizard/shared.tsx +2 -2
  77. package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
  78. package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
  79. package/src/components/auth/setup-wizard/types.ts +6 -4
  80. package/src/components/auth/setup-wizard/utils.test.ts +38 -8
  81. package/src/components/auth/setup-wizard/utils.ts +14 -8
  82. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  83. package/src/components/connectors/connector-list.tsx +26 -40
  84. package/src/components/connectors/connector-sheet.tsx +95 -149
  85. package/src/components/gateways/gateway-sheet.tsx +61 -110
  86. package/src/components/layout/live-query-sync.tsx +121 -0
  87. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  88. package/src/components/providers/app-query-provider.tsx +17 -0
  89. package/src/components/providers/provider-list.tsx +150 -77
  90. package/src/components/providers/provider-sheet.tsx +102 -77
  91. package/src/components/shared/model-combobox.tsx +5 -4
  92. package/src/components/skills/skill-list.tsx +5 -18
  93. package/src/components/skills/skill-sheet.tsx +21 -20
  94. package/src/components/skills/skills-workspace.tsx +48 -87
  95. package/src/components/tasks/task-card.tsx +20 -13
  96. package/src/components/tasks/task-column.tsx +22 -7
  97. package/src/components/tasks/task-list.tsx +8 -11
  98. package/src/components/tasks/task-sheet.tsx +111 -103
  99. package/src/features/agents/queries.ts +20 -0
  100. package/src/features/chatrooms/queries.ts +20 -0
  101. package/src/features/chats/queries.ts +27 -0
  102. package/src/features/connectors/queries.ts +145 -0
  103. package/src/features/credentials/queries.ts +37 -0
  104. package/src/features/extensions/queries.ts +26 -0
  105. package/src/features/external-agents/queries.ts +36 -0
  106. package/src/features/gateways/queries.ts +274 -0
  107. package/src/features/missions/queries.ts +23 -0
  108. package/src/features/projects/queries.ts +20 -0
  109. package/src/features/protocols/queries.ts +149 -0
  110. package/src/features/providers/queries.ts +142 -0
  111. package/src/features/settings/queries.ts +20 -0
  112. package/src/features/skills/queries.ts +182 -0
  113. package/src/features/tasks/queries.ts +189 -0
  114. package/src/hooks/use-ws.ts +3 -2
  115. package/src/lib/agent-provider-options.test.ts +152 -0
  116. package/src/lib/agent-provider-options.ts +84 -0
  117. package/src/lib/app/api-client.ts +2 -2
  118. package/src/lib/providers/index.test.ts +78 -0
  119. package/src/lib/providers/index.ts +13 -10
  120. package/src/lib/query/client.ts +17 -0
  121. package/src/lib/server/agents/agent-runtime-config.ts +6 -6
  122. package/src/lib/server/agents/agent-service.ts +429 -0
  123. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  124. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  125. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  126. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  127. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  128. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  129. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  130. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  131. package/src/lib/server/build-llm.ts +7 -15
  132. package/src/lib/server/capability-router.test.ts +70 -1
  133. package/src/lib/server/capability-router.ts +24 -99
  134. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  135. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  136. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  137. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  138. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  139. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  140. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  141. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  142. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  143. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  144. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  145. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  146. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  147. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  148. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  149. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  150. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  151. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  152. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  153. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  154. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  155. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  156. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  157. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  158. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  159. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  160. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  161. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  162. package/src/lib/server/chats/chat-session-service.ts +410 -0
  163. package/src/lib/server/connectors/access.ts +1 -1
  164. package/src/lib/server/connectors/commands.ts +7 -6
  165. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  166. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  167. package/src/lib/server/connectors/connector-service.ts +453 -0
  168. package/src/lib/server/connectors/delivery.ts +17 -12
  169. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  170. package/src/lib/server/connectors/media.ts +1 -1
  171. package/src/lib/server/connectors/response-media.ts +1 -1
  172. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  173. package/src/lib/server/connectors/session.ts +9 -7
  174. package/src/lib/server/connectors/voice-note.ts +2 -1
  175. package/src/lib/server/context-manager.ts +20 -1
  176. package/src/lib/server/cost.ts +2 -3
  177. package/src/lib/server/credentials/credential-repository.ts +43 -4
  178. package/src/lib/server/credentials/credential-service.ts +112 -0
  179. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  180. package/src/lib/server/daemon/controller.ts +577 -0
  181. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  182. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  183. package/src/lib/server/daemon/types.ts +101 -0
  184. package/src/lib/server/embeddings.ts +3 -9
  185. package/src/lib/server/eval/agent-regression.ts +3 -2
  186. package/src/lib/server/eval/runner.ts +2 -2
  187. package/src/lib/server/execution-brief.test.ts +167 -0
  188. package/src/lib/server/execution-brief.ts +295 -0
  189. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  190. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  191. package/src/lib/server/execution-engine/index.ts +35 -0
  192. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  193. package/src/lib/server/execution-engine/types.ts +33 -0
  194. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  195. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  196. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  197. package/src/lib/server/messages/message-repository.ts +330 -0
  198. package/src/lib/server/missions/mission-service/core.ts +8 -6
  199. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  200. package/src/lib/server/openclaw/doctor.ts +1 -1
  201. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  202. package/src/lib/server/openclaw/gateway.ts +5 -14
  203. package/src/lib/server/openclaw/health.ts +3 -11
  204. package/src/lib/server/openclaw/sync.ts +8 -6
  205. package/src/lib/server/persistence/storage-context.ts +3 -0
  206. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  207. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  208. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  209. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  210. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  211. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  212. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  213. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  214. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  215. package/src/lib/server/protocols/protocol-types.ts +10 -7
  216. package/src/lib/server/provider-endpoint.ts +7 -12
  217. package/src/lib/server/provider-model-discovery.ts +2 -11
  218. package/src/lib/server/query-expansion.ts +5 -6
  219. package/src/lib/server/run-context.test.ts +365 -0
  220. package/src/lib/server/run-context.ts +367 -0
  221. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  222. package/src/lib/server/runtime/queue/core.ts +61 -190
  223. package/src/lib/server/runtime/run-ledger.ts +8 -0
  224. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  225. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  226. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  227. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  228. package/src/lib/server/service-result.ts +16 -0
  229. package/src/lib/server/session-note.ts +2 -3
  230. package/src/lib/server/session-reset-policy.ts +4 -3
  231. package/src/lib/server/session-tools/connector.ts +9 -6
  232. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  233. package/src/lib/server/session-tools/crud.ts +162 -10
  234. package/src/lib/server/session-tools/delegate.ts +1 -1
  235. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  236. package/src/lib/server/session-tools/memory.ts +6 -4
  237. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  238. package/src/lib/server/session-tools/session-info.ts +119 -12
  239. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  240. package/src/lib/server/session-tools/skills.ts +15 -15
  241. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  242. package/src/lib/server/session-tools/subagent.ts +125 -7
  243. package/src/lib/server/session-tools/team-context.ts +4 -3
  244. package/src/lib/server/session-tools/wallet.ts +0 -58
  245. package/src/lib/server/sessions/session-lineage.ts +55 -0
  246. package/src/lib/server/sessions/session-repository.ts +2 -2
  247. package/src/lib/server/skills/learned-skills.ts +24 -23
  248. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  249. package/src/lib/server/skills/skill-repository.ts +136 -13
  250. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  251. package/src/lib/server/storage-normalization.test.ts +42 -215
  252. package/src/lib/server/storage-normalization.ts +98 -0
  253. package/src/lib/server/storage.ts +19 -0
  254. package/src/lib/server/structured-extract.ts +3 -14
  255. package/src/lib/server/tasks/task-followups.ts +16 -11
  256. package/src/lib/server/tasks/task-result.test.ts +25 -29
  257. package/src/lib/server/tasks/task-result.ts +5 -9
  258. package/src/lib/server/tasks/task-route-service.ts +449 -0
  259. package/src/lib/server/text-normalization.ts +41 -0
  260. package/src/lib/server/tool-planning.ts +6 -42
  261. package/src/lib/server/upload-path.ts +5 -0
  262. package/src/lib/server/working-state/extraction.ts +614 -0
  263. package/src/lib/server/working-state/normalization.ts +866 -0
  264. package/src/lib/server/working-state/prompt.ts +60 -0
  265. package/src/lib/server/working-state/repository.ts +38 -0
  266. package/src/lib/server/working-state/service.test.ts +253 -0
  267. package/src/lib/server/working-state/service.ts +293 -0
  268. package/src/lib/validation/schemas.ts +1 -0
  269. package/src/lib/ws-client.ts +3 -3
  270. package/src/stores/slices/task-slice.ts +1 -4
  271. package/src/stores/use-chatroom-store.ts +2 -2
  272. package/src/types/index.ts +288 -22
  273. package/src/views/settings/section-providers.tsx +2 -2
@@ -0,0 +1,78 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
+
6
+ test('custom providers resolve from saved provider configs', () => {
7
+ const output = runWithTempDataDir<{
8
+ providerIds: string[]
9
+ supportsModelDiscovery: boolean | null
10
+ resolvedProviderName: string | null
11
+ hasHandler: boolean
12
+ }>(`
13
+ const storageModule = await import('@/lib/server/storage')
14
+ const storage = storageModule.default || storageModule
15
+ storage.saveProviderConfigs({
16
+ 'custom-llama': {
17
+ id: 'custom-llama',
18
+ name: 'Llama.cpp',
19
+ type: 'custom',
20
+ baseUrl: 'http://127.0.0.1:8080/v1',
21
+ models: ['llama-3.1-8b'],
22
+ requiresApiKey: false,
23
+ credentialId: null,
24
+ isEnabled: true,
25
+ createdAt: 1,
26
+ updatedAt: 1,
27
+ },
28
+ })
29
+
30
+ const providersModule = await import('@/lib/providers/index')
31
+ const providers = providersModule.default || providersModule
32
+ const providerList = providers.getProviderList()
33
+ const resolvedProvider = providers.getProvider('custom-llama')
34
+
35
+ console.log(JSON.stringify({
36
+ providerIds: providerList.map((provider) => provider.id),
37
+ supportsModelDiscovery: providerList.find((provider) => provider.id === 'custom-llama')?.supportsModelDiscovery ?? null,
38
+ resolvedProviderName: resolvedProvider?.name ?? null,
39
+ hasHandler: typeof resolvedProvider?.handler?.streamChat === 'function',
40
+ }))
41
+ `)
42
+
43
+ assert.equal(output.providerIds.includes('custom-llama'), true)
44
+ assert.equal(output.supportsModelDiscovery, false)
45
+ assert.equal(output.resolvedProviderName, 'Llama.cpp')
46
+ assert.equal(output.hasHandler, true)
47
+ })
48
+
49
+ test('builtin provider override records do not surface as custom providers', () => {
50
+ const output = runWithTempDataDir<{ openAiCount: number }>(`
51
+ const storageModule = await import('@/lib/server/storage')
52
+ const storage = storageModule.default || storageModule
53
+ storage.saveProviderConfigs({
54
+ openai: {
55
+ id: 'openai',
56
+ name: 'OpenAI',
57
+ type: 'builtin',
58
+ baseUrl: '',
59
+ models: [],
60
+ requiresApiKey: true,
61
+ credentialId: null,
62
+ isEnabled: false,
63
+ createdAt: 1,
64
+ updatedAt: 1,
65
+ },
66
+ })
67
+
68
+ const providersModule = await import('@/lib/providers/index')
69
+ const providers = providersModule.default || providersModule
70
+ const providerList = providers.getProviderList()
71
+
72
+ console.log(JSON.stringify({
73
+ openAiCount: providerList.filter((provider) => provider.id === 'openai').length,
74
+ }))
75
+ `)
76
+
77
+ assert.equal(output.openAiCount, 1)
78
+ })
@@ -9,7 +9,7 @@ import { streamOpenClawChat } from './openclaw'
9
9
  import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
10
10
  import { classifyProviderError } from './error-classification'
11
11
  import { log } from '@/lib/server/logger'
12
- import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
12
+ import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType, ProviderId } from '../../types'
13
13
 
14
14
  const TAG = 'providers'
15
15
 
@@ -281,8 +281,11 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
281
281
  function getCustomProviders(): Record<string, CustomProviderConfig> {
282
282
  try {
283
283
  // eslint-disable-next-line @typescript-eslint/no-require-imports
284
- const { loadProviderConfigs } = require('../server/storage')
285
- return loadProviderConfigs() as Record<string, CustomProviderConfig>
284
+ const { loadProviderConfigs } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
285
+ const configs = loadProviderConfigs() as Record<string, CustomProviderConfig>
286
+ return Object.fromEntries(
287
+ Object.entries(configs).filter(([, config]) => config?.type === 'custom'),
288
+ )
286
289
  } catch {
287
290
  return {}
288
291
  }
@@ -291,7 +294,7 @@ function getCustomProviders(): Record<string, CustomProviderConfig> {
291
294
  function getModelOverrides(): Record<string, string[]> {
292
295
  try {
293
296
  // eslint-disable-next-line @typescript-eslint/no-require-imports
294
- const { loadModelOverrides } = require('../server/storage')
297
+ const { loadModelOverrides } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
295
298
  return loadModelOverrides()
296
299
  } catch {
297
300
  return {}
@@ -316,11 +319,11 @@ export function getProviderList(): ProviderInfo[] {
316
319
  const customs: ProviderInfo[] = Object.values(getCustomProviders())
317
320
  .filter((c) => c.isEnabled)
318
321
  .map((c) => ({
319
- id: c.id as ProviderType,
322
+ id: c.id as ProviderId,
320
323
  name: c.name,
321
324
  models: c.models,
322
325
  defaultModels: c.models,
323
- supportsModelDiscovery: !!(c.baseUrl && c.baseUrl.trim()),
326
+ supportsModelDiscovery: false,
324
327
  requiresApiKey: c.requiresApiKey,
325
328
  requiresEndpoint: false as boolean,
326
329
  defaultEndpoint: c.baseUrl,
@@ -331,7 +334,7 @@ export function getProviderList(): ProviderInfo[] {
331
334
  // eslint-disable-next-line @typescript-eslint/no-require-imports
332
335
  const { getExtensionManager } = require('../server/extensions')
333
336
  extensionProviders = getExtensionManager().getProviders().map((p: Record<string, unknown>) => ({
334
- id: String(p.id) as ProviderType,
337
+ id: String(p.id) as ProviderId,
335
338
  name: String(p.name),
336
339
  models: p.models as string[],
337
340
  defaultModels: p.models as string[],
@@ -353,7 +356,7 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
353
356
  const custom = customs[id]
354
357
  if (custom?.isEnabled) {
355
358
  return {
356
- id: custom.id as ProviderType,
359
+ id: custom.id as ProviderId,
357
360
  name: custom.name,
358
361
  models: custom.models,
359
362
  requiresApiKey: custom.requiresApiKey,
@@ -376,7 +379,7 @@ export function getProvider(id: string): BuiltinProviderConfig | null {
376
379
  const found = extensionProviders.find((p: Record<string, unknown>) => p.id === id)
377
380
  if (found) {
378
381
  return {
379
- id: found.id as ProviderType,
382
+ id: found.id as ProviderId,
380
383
  name: found.name,
381
384
  models: found.models,
382
385
  requiresApiKey: found.requiresApiKey,
@@ -421,7 +424,7 @@ export async function streamChatWithFailover(
421
424
  if (credId && i > 0) {
422
425
  // Need to decrypt fallback credential
423
426
  // eslint-disable-next-line @typescript-eslint/no-require-imports
424
- const { loadCredentials, decryptKey } = require('../server/storage')
427
+ const { loadCredentials, decryptKey } = require('@/lib/server/storage') as typeof import('@/lib/server/storage')
425
428
  const creds = loadCredentials()
426
429
  const cred = creds[credId]
427
430
  if (cred?.encryptedKey) {
@@ -0,0 +1,17 @@
1
+ import { QueryClient } from '@tanstack/react-query'
2
+
3
+ export function createAppQueryClient(): QueryClient {
4
+ return new QueryClient({
5
+ defaultOptions: {
6
+ queries: {
7
+ staleTime: 15_000,
8
+ gcTime: 5 * 60_000,
9
+ retry: 0,
10
+ refetchOnWindowFocus: false,
11
+ },
12
+ mutations: {
13
+ retry: 0,
14
+ },
15
+ },
16
+ })
17
+ }
@@ -3,11 +3,11 @@ import type {
3
3
  AgentRoutingStrategy,
4
4
  AgentRoutingTarget,
5
5
  GatewayProfile,
6
- ProviderType,
6
+ ProviderId,
7
7
  } from '@/types'
8
8
  import { deriveOpenClawWsUrl, normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
9
9
  import { resolveProviderApiEndpoint, resolveProviderCredentialId } from '@/lib/server/provider-endpoint'
10
- import { loadGatewayProfiles } from '@/lib/server/storage'
10
+ import { loadGatewayProfiles } from '@/lib/server/gateways/gateway-profile-repository'
11
11
  import { isProviderCoolingDown } from '@/lib/server/provider-health'
12
12
 
13
13
  const DEFAULT_OPENCLAW_ENDPOINT = 'http://localhost:18789/v1'
@@ -16,7 +16,7 @@ const DEFAULT_OPENCLAW_MODEL = 'default'
16
16
  export interface ResolvedAgentRoute {
17
17
  id: string
18
18
  label: string
19
- provider: ProviderType
19
+ provider: ProviderId
20
20
  model: string
21
21
  ollamaMode?: Agent['ollamaMode']
22
22
  credentialId?: string | null
@@ -36,7 +36,7 @@ interface GatewayRoutePreferences {
36
36
  interface RouteSeed {
37
37
  id: string
38
38
  label?: string
39
- provider?: ProviderType | null
39
+ provider?: ProviderId | null
40
40
  model?: string | null
41
41
  ollamaMode?: Agent['ollamaMode']
42
42
  credentialId?: string | null
@@ -258,7 +258,7 @@ function buildRouteFromSeed(
258
258
  routePreferences?: GatewayRoutePreferences | null,
259
259
  agentGatewayProfileId?: string | null,
260
260
  ): ResolvedAgentRoute | null {
261
- const provider = (seed.provider || 'claude-cli') as ProviderType
261
+ const provider: ProviderId = seed.provider || 'claude-cli'
262
262
  const mergedPreferences = normalizeRoutePreferences({
263
263
  preferredGatewayTags: seed.preferredGatewayTags ?? routePreferences?.preferredGatewayTags,
264
264
  preferredGatewayUseCase: seed.preferredGatewayUseCase ?? routePreferences?.preferredGatewayUseCase,
@@ -406,7 +406,7 @@ export function resolvePrimaryAgentRoute(
406
406
  }
407
407
 
408
408
  export function applyResolvedRoute<T extends {
409
- provider: ProviderType
409
+ provider: ProviderId
410
410
  model: string
411
411
  ollamaMode?: Agent['ollamaMode']
412
412
  credentialId?: string | null
@@ -0,0 +1,429 @@
1
+ import { genId } from '@/lib/id'
2
+ import { resolveAgentToolSelection } from '@/lib/agent-default-tools'
3
+ import { normalizeAgentSandboxConfig } from '@/lib/agent-sandbox-defaults'
4
+ import { normalizeCapabilitySelection } from '@/lib/capability-selection'
5
+ import { normalizeProviderEndpoint } from '@/lib/openclaw/openclaw-endpoint'
6
+ import { normalizeOrchestratorConfig } from '@/lib/orchestrator-config'
7
+ import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agents/agent-availability'
8
+ import { suspendAgentReferences, purgeAgentReferences, restoreAgentSchedules } from '@/lib/server/agents/agent-cascade'
9
+ import { ensureAgentThreadSession } from '@/lib/server/agents/agent-thread-session'
10
+ import {
11
+ deleteAgent,
12
+ loadAgents,
13
+ loadTrashedAgents,
14
+ patchAgent,
15
+ saveAgent,
16
+ } from '@/lib/server/agents/agent-repository'
17
+ import { logActivity } from '@/lib/server/activity/activity-log'
18
+ import { getAgentSpendWindows } from '@/lib/server/cost'
19
+ import { serviceFail, serviceOk } from '@/lib/server/service-result'
20
+ import { listSessions, saveSession } from '@/lib/server/sessions/session-repository'
21
+ import { loadUsage } from '@/lib/server/usage/usage-repository'
22
+ import { notify } from '@/lib/server/ws-hub'
23
+ import type { Agent, Session } from '@/types'
24
+ import type { ServiceResult } from '@/lib/server/service-result'
25
+
26
+ function normalizeStringList(value: unknown): string[] {
27
+ return Array.isArray(value)
28
+ ? value.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
29
+ : []
30
+ }
31
+
32
+ function normalizeOllamaMode(value: unknown): Agent['ollamaMode'] {
33
+ if (value === 'cloud') return 'cloud'
34
+ if (value === 'local') return 'local'
35
+ return null
36
+ }
37
+
38
+ function updateThreadShortcutSession(agentId: string, agent: Agent): void {
39
+ if (!agent.threadSessionId) return
40
+ const shortcut = listSessions()[agent.threadSessionId]
41
+ if (!shortcut) return
42
+ let changed = false
43
+ if (shortcut.name !== agent.name) {
44
+ shortcut.name = agent.name
45
+ changed = true
46
+ }
47
+ if (shortcut.shortcutForAgentId !== agentId) {
48
+ shortcut.shortcutForAgentId = agentId
49
+ changed = true
50
+ }
51
+ if (changed) saveSession(shortcut.id, shortcut)
52
+ }
53
+
54
+ function detachAgentSessions(agentId: string): number {
55
+ let detached = 0
56
+ for (const session of Object.values(listSessions())) {
57
+ if (!session || session.agentId !== agentId) continue
58
+ session.agentId = null
59
+ session.heartbeatEnabled = false
60
+ saveSession(session.id, session)
61
+ detached += 1
62
+ }
63
+ return detached
64
+ }
65
+
66
+ export function listAgentsForApi(): Record<string, Agent> {
67
+ const agents = loadAgents()
68
+ const sessions = listSessions()
69
+ const usage = loadUsage()
70
+ const now = Date.now()
71
+ for (const agent of Object.values(agents)) {
72
+ if (
73
+ (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0)
74
+ || (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0)
75
+ || (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0)
76
+ ) {
77
+ const spend = getAgentSpendWindows(agent.id, now, {
78
+ sessions,
79
+ usage,
80
+ })
81
+ if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = spend.monthly
82
+ if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = spend.daily
83
+ if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = spend.hourly
84
+ }
85
+ }
86
+ return agents
87
+ }
88
+
89
+ export function createAgent(input: {
90
+ body: Record<string, unknown>
91
+ rawRecord?: Record<string, unknown> | null
92
+ }): Agent {
93
+ const body = input.body as Record<string, unknown>
94
+ const rawRecord = input.rawRecord || null
95
+ const orchestratorConfig = normalizeOrchestratorConfig({
96
+ provider: body.provider as string,
97
+ orchestratorEnabled: body.orchestratorEnabled,
98
+ orchestratorMission: body.orchestratorMission,
99
+ orchestratorWakeInterval: body.orchestratorWakeInterval,
100
+ orchestratorGovernance: body.orchestratorGovernance,
101
+ orchestratorMaxCyclesPerDay: body.orchestratorMaxCyclesPerDay,
102
+ })
103
+ const capabilitySelection = resolveAgentToolSelection({
104
+ hasExplicitTools: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'tools')),
105
+ hasExplicitExtensions: Boolean(rawRecord && Object.prototype.hasOwnProperty.call(rawRecord, 'extensions')),
106
+ tools: Array.isArray(body.tools) ? normalizeStringList(body.tools) : undefined,
107
+ extensions: Array.isArray(body.extensions) ? normalizeStringList(body.extensions) : undefined,
108
+ })
109
+ const id = genId()
110
+ const now = Date.now()
111
+ const agent: Agent = {
112
+ id,
113
+ name: String(body.name || ''),
114
+ description: String(body.description || ''),
115
+ soul: typeof body.soul === 'string' && body.soul ? body.soul : undefined,
116
+ systemPrompt: String(body.systemPrompt || ''),
117
+ provider: String(body.provider || ''),
118
+ model: String(body.model || ''),
119
+ ollamaMode: body.provider === 'ollama' ? (normalizeOllamaMode(body.ollamaMode) || 'local') : null,
120
+ credentialId: (body.credentialId as string | null | undefined) || null,
121
+ fallbackCredentialIds: normalizeStringList(body.fallbackCredentialIds),
122
+ apiEndpoint: normalizeProviderEndpoint(String(body.provider || ''), (body.apiEndpoint as string | null | undefined) || null),
123
+ gatewayProfileId: (body.gatewayProfileId as string | null | undefined) || null,
124
+ preferredGatewayTags: normalizeStringList(body.preferredGatewayTags),
125
+ preferredGatewayUseCase: typeof body.preferredGatewayUseCase === 'string' && body.preferredGatewayUseCase.trim()
126
+ ? body.preferredGatewayUseCase.trim()
127
+ : null,
128
+ routingStrategy: body.routingStrategy as Agent['routingStrategy'],
129
+ routingTargets: Array.isArray(body.routingTargets)
130
+ ? body.routingTargets.map((target) => {
131
+ const row = target as Record<string, unknown>
132
+ const provider = typeof row.provider === 'string' ? row.provider : String(body.provider || '')
133
+ return {
134
+ ...row,
135
+ provider,
136
+ ollamaMode: provider === 'ollama' ? (normalizeOllamaMode(row.ollamaMode) || 'local') : null,
137
+ apiEndpoint: normalizeProviderEndpoint(provider, (row.apiEndpoint as string | null | undefined) || null),
138
+ }
139
+ }) as Agent['routingTargets']
140
+ : undefined,
141
+ delegationEnabled: body.delegationEnabled === true,
142
+ delegationTargetMode: body.delegationTargetMode === 'selected' ? 'selected' : 'all',
143
+ delegationTargetAgentIds: (body.delegationTargetMode === 'selected' ? normalizeStringList(body.delegationTargetAgentIds) : []),
144
+ tools: capabilitySelection.tools,
145
+ extensions: capabilitySelection.extensions,
146
+ skills: Array.isArray(body.skills) ? body.skills as Agent['skills'] : undefined,
147
+ skillIds: normalizeStringList(body.skillIds),
148
+ mcpServerIds: normalizeStringList(body.mcpServerIds),
149
+ mcpDisabledTools: normalizeStringList(body.mcpDisabledTools).length ? normalizeStringList(body.mcpDisabledTools) : undefined,
150
+ capabilities: Array.isArray(body.capabilities) ? body.capabilities as string[] : undefined,
151
+ thinkingLevel: (body.thinkingLevel as Agent['thinkingLevel']) || undefined,
152
+ autoRecovery: body.autoRecovery === true,
153
+ disabled: body.disabled === true,
154
+ heartbeatEnabled: body.heartbeatEnabled !== false,
155
+ heartbeatInterval: body.heartbeatInterval as Agent['heartbeatInterval'],
156
+ heartbeatIntervalSec: typeof body.heartbeatIntervalSec === 'number' ? body.heartbeatIntervalSec : null,
157
+ heartbeatModel: typeof body.heartbeatModel === 'string' ? body.heartbeatModel : undefined,
158
+ heartbeatPrompt: typeof body.heartbeatPrompt === 'string' ? body.heartbeatPrompt : undefined,
159
+ orchestratorEnabled: orchestratorConfig.orchestratorEnabled,
160
+ orchestratorMission: orchestratorConfig.orchestratorMission,
161
+ orchestratorWakeInterval: orchestratorConfig.orchestratorWakeInterval,
162
+ orchestratorGovernance: orchestratorConfig.orchestratorGovernance,
163
+ orchestratorMaxCyclesPerDay: orchestratorConfig.orchestratorMaxCyclesPerDay,
164
+ elevenLabsVoiceId: typeof body.elevenLabsVoiceId === 'string' ? body.elevenLabsVoiceId : undefined,
165
+ monthlyBudget: typeof body.monthlyBudget === 'number' ? body.monthlyBudget : null,
166
+ dailyBudget: typeof body.dailyBudget === 'number' ? body.dailyBudget : null,
167
+ hourlyBudget: typeof body.hourlyBudget === 'number' ? body.hourlyBudget : null,
168
+ budgetAction: (body.budgetAction as Agent['budgetAction']) || 'warn',
169
+ identityState: (body.identityState as Agent['identityState']) ?? null,
170
+ memoryScopeMode: (body.memoryScopeMode as Agent['memoryScopeMode']) || undefined,
171
+ memoryTierPreference: (body.memoryTierPreference as Agent['memoryTierPreference']) || undefined,
172
+ proactiveMemory: body.proactiveMemory !== false,
173
+ autoDraftSkillSuggestions: body.autoDraftSkillSuggestions as Agent['autoDraftSkillSuggestions'],
174
+ projectId: typeof body.projectId === 'string' && body.projectId.trim() ? body.projectId.trim() : undefined,
175
+ avatarSeed: typeof body.avatarSeed === 'string' ? body.avatarSeed : undefined,
176
+ avatarUrl: typeof body.avatarUrl === 'string' ? body.avatarUrl : undefined,
177
+ sessionResetMode: (body.sessionResetMode as Agent['sessionResetMode']) ?? null,
178
+ sessionIdleTimeoutSec: typeof body.sessionIdleTimeoutSec === 'number' ? body.sessionIdleTimeoutSec : null,
179
+ sessionMaxAgeSec: typeof body.sessionMaxAgeSec === 'number' ? body.sessionMaxAgeSec : null,
180
+ sessionDailyResetAt: typeof body.sessionDailyResetAt === 'string' ? body.sessionDailyResetAt : null,
181
+ sessionResetTimezone: typeof body.sessionResetTimezone === 'string' ? body.sessionResetTimezone : null,
182
+ sandboxConfig: normalizeAgentSandboxConfig(body.sandboxConfig),
183
+ createdAt: now,
184
+ updatedAt: now,
185
+ }
186
+ saveAgent(id, agent)
187
+ logActivity({ entityType: 'agent', entityId: id, action: 'created', actor: 'user', summary: `Agent created: "${agent.name}"` })
188
+ notify('agents')
189
+ return agent
190
+ }
191
+
192
+ export function updateAgent(agentId: string, body: Record<string, unknown>): Agent | null {
193
+ const updated = patchAgent(agentId, (current) => {
194
+ if (!current) return null
195
+ const agent = { ...current, ...body, updatedAt: Date.now() }
196
+ if (body.tools !== undefined || body.extensions !== undefined) {
197
+ const nextSelection = normalizeCapabilitySelection({
198
+ tools: Array.isArray(body.tools) ? body.tools : agent.tools,
199
+ extensions: Array.isArray(body.extensions) ? body.extensions : agent.extensions,
200
+ })
201
+ agent.tools = nextSelection.tools
202
+ agent.extensions = nextSelection.extensions
203
+ }
204
+ if (body.delegationEnabled !== undefined) {
205
+ agent.delegationEnabled = body.delegationEnabled === true
206
+ }
207
+ if (body.delegationTargetMode === 'all' || body.delegationTargetMode === 'selected') {
208
+ agent.delegationTargetMode = body.delegationTargetMode
209
+ }
210
+ if (body.delegationTargetAgentIds !== undefined) {
211
+ agent.delegationTargetAgentIds = normalizeStringList(body.delegationTargetAgentIds)
212
+ }
213
+ if (agent.delegationTargetMode !== 'selected') {
214
+ agent.delegationTargetAgentIds = []
215
+ }
216
+ if (body.apiEndpoint !== undefined) {
217
+ agent.apiEndpoint = normalizeProviderEndpoint(
218
+ (body.provider as string) || agent.provider,
219
+ body.apiEndpoint as string | null | undefined,
220
+ )
221
+ }
222
+ if (body.provider !== undefined && body.provider !== 'ollama' && body.ollamaMode === undefined) {
223
+ agent.ollamaMode = null
224
+ }
225
+ if (body.sandboxConfig !== undefined) {
226
+ agent.sandboxConfig = normalizeAgentSandboxConfig(body.sandboxConfig)
227
+ }
228
+ if (
229
+ body.provider !== undefined
230
+ || body.orchestratorEnabled !== undefined
231
+ || body.orchestratorMission !== undefined
232
+ || body.orchestratorWakeInterval !== undefined
233
+ || body.orchestratorGovernance !== undefined
234
+ || body.orchestratorMaxCyclesPerDay !== undefined
235
+ ) {
236
+ const orchestratorConfig = normalizeOrchestratorConfig({
237
+ provider: typeof body.provider === 'string' ? body.provider : agent.provider,
238
+ orchestratorEnabled: body.orchestratorEnabled ?? agent.orchestratorEnabled,
239
+ orchestratorMission: body.orchestratorMission ?? agent.orchestratorMission,
240
+ orchestratorWakeInterval: body.orchestratorWakeInterval ?? agent.orchestratorWakeInterval,
241
+ orchestratorGovernance: body.orchestratorGovernance ?? agent.orchestratorGovernance,
242
+ orchestratorMaxCyclesPerDay: body.orchestratorMaxCyclesPerDay ?? agent.orchestratorMaxCyclesPerDay,
243
+ })
244
+ agent.orchestratorEnabled = orchestratorConfig.orchestratorEnabled
245
+ agent.orchestratorMission = orchestratorConfig.orchestratorMission
246
+ agent.orchestratorWakeInterval = orchestratorConfig.orchestratorWakeInterval
247
+ agent.orchestratorGovernance = orchestratorConfig.orchestratorGovernance
248
+ agent.orchestratorMaxCyclesPerDay = orchestratorConfig.orchestratorMaxCyclesPerDay
249
+ }
250
+ if (body.preferredGatewayTags !== undefined) {
251
+ agent.preferredGatewayTags = normalizeStringList(body.preferredGatewayTags)
252
+ }
253
+ if (body.preferredGatewayUseCase !== undefined) {
254
+ agent.preferredGatewayUseCase = typeof body.preferredGatewayUseCase === 'string' && body.preferredGatewayUseCase.trim()
255
+ ? body.preferredGatewayUseCase.trim()
256
+ : null
257
+ }
258
+ if (body.routingTargets !== undefined && Array.isArray(body.routingTargets)) {
259
+ agent.routingTargets = body.routingTargets.map((target, index) => {
260
+ const row = target as Record<string, unknown>
261
+ const provider = typeof row.provider === 'string' && row.provider.trim() ? row.provider : agent.provider
262
+ return {
263
+ id: typeof row.id === 'string' && row.id.trim() ? row.id.trim() : `route-${index + 1}`,
264
+ label: typeof row.label === 'string' ? row.label : undefined,
265
+ role: row.role,
266
+ provider,
267
+ model: typeof row.model === 'string' ? row.model : '',
268
+ ollamaMode: provider === 'ollama'
269
+ ? (row.ollamaMode === 'cloud' ? 'cloud' : 'local')
270
+ : null,
271
+ credentialId: row.credentialId ?? null,
272
+ fallbackCredentialIds: Array.isArray(row.fallbackCredentialIds) ? row.fallbackCredentialIds : [],
273
+ apiEndpoint: normalizeProviderEndpoint(
274
+ provider,
275
+ typeof row.apiEndpoint === 'string' ? row.apiEndpoint : null,
276
+ ),
277
+ gatewayProfileId: row.gatewayProfileId ?? null,
278
+ preferredGatewayTags: normalizeStringList(row.preferredGatewayTags),
279
+ preferredGatewayUseCase: typeof row.preferredGatewayUseCase === 'string' && row.preferredGatewayUseCase.trim()
280
+ ? row.preferredGatewayUseCase.trim()
281
+ : null,
282
+ priority: typeof row.priority === 'number' ? row.priority : index + 1,
283
+ }
284
+ }) as Agent['routingTargets']
285
+ }
286
+ delete (agent as Record<string, unknown>).platformAssignScope
287
+ delete (agent as Record<string, unknown>).subAgentIds
288
+ delete (agent as Record<string, unknown>).id
289
+ agent.id = agentId
290
+ return agent as Agent
291
+ })
292
+ if (!updated) return null
293
+
294
+ if (updated.threadSessionId) {
295
+ ensureAgentThreadSession(agentId)
296
+ updateThreadShortcutSession(agentId, updated)
297
+ }
298
+
299
+ logActivity({ entityType: 'agent', entityId: agentId, action: 'updated', actor: 'user', summary: `Agent updated: "${updated.name}"` })
300
+ return updated
301
+ }
302
+
303
+ export function trashAgent(agentId: string): { ok: false } | { ok: true; detachedSessions: number; cascade: ReturnType<typeof suspendAgentReferences> } {
304
+ const trashed = patchAgent(agentId, (current) => {
305
+ if (!current) return null
306
+ return { ...current, trashedAt: Date.now() }
307
+ })
308
+ if (!trashed) return { ok: false }
309
+
310
+ logActivity({ entityType: 'agent', entityId: agentId, action: 'deleted', actor: 'user', summary: `Agent trashed: "${trashed.name}"` })
311
+ const detachedSessions = detachAgentSessions(agentId)
312
+ const cascade = suspendAgentReferences(agentId)
313
+ return { ok: true, detachedSessions, cascade }
314
+ }
315
+
316
+ export function restoreTrashedAgent(agentId: string): Agent | null {
317
+ const agent = patchAgent(agentId, (current) => {
318
+ if (!current || !current.trashedAt) return null
319
+ const next = { ...current }
320
+ delete next.trashedAt
321
+ next.updatedAt = Date.now()
322
+ return next
323
+ })
324
+ if (!agent) return null
325
+ notify('agents')
326
+ const restoredSchedules = restoreAgentSchedules(agentId)
327
+ if (restoredSchedules) notify('schedules')
328
+ return agent
329
+ }
330
+
331
+ export function permanentlyDeleteTrashedAgent(agentId: string): { ok: false; reason: 'not_found' | 'not_trashed' } | { ok: true; purged: ReturnType<typeof purgeAgentReferences> } {
332
+ const agent = loadAgents({ includeTrashed: true })[agentId]
333
+ if (!agent) return { ok: false, reason: 'not_found' }
334
+ if (!agent.trashedAt) return { ok: false, reason: 'not_trashed' }
335
+
336
+ const purged = purgeAgentReferences(agentId)
337
+ deleteAgent(agentId)
338
+ notify('agents')
339
+ return { ok: true, purged }
340
+ }
341
+
342
+ export function cloneAgent(agentId: string): Agent | null {
343
+ const source = loadAgents({ includeTrashed: true })[agentId]
344
+ if (!source) return null
345
+ const newId = crypto.randomUUID()
346
+ const now = Date.now()
347
+ const cloned = JSON.parse(JSON.stringify(source)) as Agent
348
+ cloned.id = newId
349
+ cloned.name = `${source.name} (Copy)`
350
+ cloned.createdAt = now
351
+ cloned.updatedAt = now
352
+ cloned.totalCost = 0
353
+ cloned.lastUsedAt = undefined
354
+ cloned.threadSessionId = null
355
+ cloned.pinned = false
356
+ cloned.trashedAt = undefined
357
+
358
+ saveAgent(newId, cloned)
359
+ logActivity({
360
+ entityType: 'agent',
361
+ entityId: newId,
362
+ action: 'created',
363
+ actor: 'user',
364
+ summary: `Agent cloned from "${source.name}": "${cloned.name}"`,
365
+ })
366
+ notify('agents')
367
+ return cloned
368
+ }
369
+
370
+ export function bulkPatchAgents(patches: unknown): { updated: number; errors: string[] } {
371
+ if (!Array.isArray(patches) || patches.length === 0) {
372
+ return { updated: 0, errors: ['patches must be a non-empty array'] }
373
+ }
374
+ let updated = 0
375
+ const errors: string[] = []
376
+ for (const entry of patches) {
377
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
378
+ errors.push('Invalid patch entry (not an object)')
379
+ continue
380
+ }
381
+ const { id, patch } = entry as { id?: unknown; patch?: unknown }
382
+ if (typeof id !== 'string' || !id.trim()) {
383
+ errors.push('Patch entry missing valid id')
384
+ continue
385
+ }
386
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
387
+ errors.push(`Patch for ${id} is not a valid object`)
388
+ continue
389
+ }
390
+ const result = patchAgent(id, (current) => current ? { ...current, ...(patch as Record<string, unknown>), updatedAt: Date.now() } : null)
391
+ if (!result) {
392
+ errors.push(`Agent ${id} not found`)
393
+ continue
394
+ }
395
+ updated += 1
396
+ logActivity({
397
+ entityType: 'agent',
398
+ entityId: id,
399
+ action: 'updated',
400
+ actor: 'user',
401
+ summary: `Bulk patch: updated agent "${result.name || id}"`,
402
+ })
403
+ }
404
+ if (updated > 0) notify('agents')
405
+ return { updated, errors }
406
+ }
407
+
408
+ export function getAgentThreadSession(agentId: string, user = 'default'): ServiceResult<Session> {
409
+ const agent = loadAgents()[agentId]
410
+ if (!agent) {
411
+ return serviceFail(404, 'Agent not found')
412
+ }
413
+ const session = ensureAgentThreadSession(agentId, user, agent)
414
+ if (!session) {
415
+ if (isAgentDisabled(agent)) {
416
+ return serviceFail(409, buildAgentDisabledMessage(agent, 'start new chats'))
417
+ }
418
+ return serviceFail(404, 'Agent not found')
419
+ }
420
+ return serviceOk(session)
421
+ }
422
+
423
+ export function getAgentStatus(agentId: string): Agent | null {
424
+ return loadAgents()[agentId] || null
425
+ }
426
+
427
+ export function listTrashedAgentsForApi(): Record<string, Agent> {
428
+ return loadTrashedAgents()
429
+ }