@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
@@ -2,9 +2,10 @@ import { genId } from '@/lib/id'
2
2
  import type { Agent, Session } from '@/types'
3
3
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
4
4
  import { isAgentDisabled } from '@/lib/server/agents/agent-availability'
5
+ import { loadAgent, loadAgents, upsertAgent } from '@/lib/server/agents/agent-repository'
5
6
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
6
- import { loadAgents, loadSession, loadSessions, upsertAgent, upsertStoredItem } from '@/lib/server/storage'
7
7
  import { getEnabledCapabilitySelection } from '@/lib/capability-selection'
8
+ import { loadSession, loadSessions, upsertSession } from '@/lib/server/sessions/session-repository'
8
9
 
9
10
  function buildEmptyDelegateResumeIds(): NonNullable<Session['delegateResumeIds']> {
10
11
  return {
@@ -108,7 +109,7 @@ function shouldHealAgentCredentialId(agent: Agent, session: Session): boolean {
108
109
  }
109
110
 
110
111
  export function ensureAgentThreadSession(agentId: string, user = 'default', preloadedAgent?: Agent): Session | null {
111
- const agent = preloadedAgent ?? (loadAgents()[agentId] as Agent | undefined)
112
+ const agent = preloadedAgent ?? loadAgent(agentId) ?? (loadAgents()[agentId] as Agent | undefined)
112
113
  if (!agent) return null
113
114
 
114
115
  const now = Date.now()
@@ -124,7 +125,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default', prel
124
125
  agent.updatedAt = now
125
126
  upsertAgent(agentId, agent)
126
127
  }
127
- upsertStoredItem('sessions', existingId, session)
128
+ upsertSession(existingId, session)
128
129
  return session
129
130
  }
130
131
  // Session was deleted — fall through to legacy search / creation
@@ -147,7 +148,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default', prel
147
148
  }
148
149
  agent.updatedAt = now
149
150
  upsertAgent(agentId, agent)
150
- upsertStoredItem('sessions', legacySession.id, session)
151
+ upsertSession(legacySession.id, session)
151
152
  return session
152
153
  }
153
154
 
@@ -158,7 +159,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default', prel
158
159
  if (shouldHealAgentCredentialId(agent, session)) {
159
160
  agent.credentialId = session.credentialId ?? null
160
161
  }
161
- upsertStoredItem('sessions', sessionId, session)
162
+ upsertSession(sessionId, session)
162
163
 
163
164
  agent.threadSessionId = sessionId
164
165
  agent.updatedAt = now
@@ -1,4 +1,5 @@
1
1
  import type { GoalContract } from '@/types'
2
+ import { cleanText } from '@/lib/server/text-normalization'
2
3
 
3
4
  const PLAN_LINE_RE = /\[MAIN_LOOP_PLAN\]\s*(\{[^\n]*\})/i
4
5
  const REVIEW_LINE_RE = /\[MAIN_LOOP_REVIEW\]\s*(\{[^\n]*\})/i
@@ -15,10 +16,6 @@ export interface MainLoopReviewMeta {
15
16
  needs_replan?: boolean
16
17
  }
17
18
 
18
- function cleanText(value: string, max = 400): string {
19
- return (value || '').replace(/\s+/g, ' ').trim().slice(0, max)
20
- }
21
-
22
19
  function uniqueStrings(input: string[]): string[] {
23
20
  const seen = new Set<string>()
24
21
  const out: string[] = []
@@ -0,0 +1,206 @@
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 { after, before, describe, it } from 'node:test'
6
+ import type { Agent } from '@/types'
7
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
8
+
9
+ const originalEnv = {
10
+ DATA_DIR: process.env.DATA_DIR,
11
+ WORKSPACE_DIR: process.env.WORKSPACE_DIR,
12
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
13
+ }
14
+
15
+ let tempDir = ''
16
+ let advisory: typeof import('@/lib/server/agents/delegation-advisory')
17
+ let storage: typeof import('@/lib/server/storage')
18
+
19
+ before(async () => {
20
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-advisory-'))
21
+ process.env.DATA_DIR = path.join(tempDir, 'data')
22
+ process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
23
+ process.env.SWARMCLAW_BUILD_MODE = '1'
24
+ fs.mkdirSync(process.env.DATA_DIR, { recursive: true })
25
+ fs.mkdirSync(process.env.WORKSPACE_DIR, { recursive: true })
26
+
27
+ advisory = await import('@/lib/server/agents/delegation-advisory')
28
+ storage = await import('@/lib/server/storage')
29
+ })
30
+
31
+ after(() => {
32
+ if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
33
+ else process.env.DATA_DIR = originalEnv.DATA_DIR
34
+ if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
35
+ else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
36
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
37
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
38
+ fs.rmSync(tempDir, { recursive: true, force: true })
39
+ })
40
+
41
+ function makeAgent(params: Partial<Agent> & Pick<Agent, 'id' | 'name'>): Agent {
42
+ const now = Date.now()
43
+ return {
44
+ id: params.id,
45
+ name: params.name,
46
+ role: params.role || 'worker',
47
+ description: params.description || '',
48
+ systemPrompt: params.systemPrompt || '',
49
+ provider: params.provider || 'openai',
50
+ model: params.model || 'gpt-test',
51
+ capabilities: params.capabilities || [],
52
+ delegationEnabled: params.delegationEnabled ?? false,
53
+ delegationTargetMode: params.delegationTargetMode || 'all',
54
+ delegationTargetAgentIds: params.delegationTargetAgentIds || [],
55
+ createdAt: params.createdAt || now,
56
+ updatedAt: params.updatedAt || now,
57
+ } as Agent
58
+ }
59
+
60
+ function makeClassification(overrides: Partial<MessageClassification>): MessageClassification {
61
+ return {
62
+ taskIntent: 'general',
63
+ isDeliverableTask: false,
64
+ isBroadGoal: false,
65
+ walletIntent: 'none',
66
+ hasHumanSignals: false,
67
+ hasSignificantEvent: false,
68
+ isResearchSynthesis: false,
69
+ workType: 'general',
70
+ wantsScreenshots: false,
71
+ wantsOutboundDelivery: false,
72
+ wantsVoiceDelivery: false,
73
+ explicitToolRequests: [],
74
+ confidence: 0.9,
75
+ ...overrides,
76
+ }
77
+ }
78
+
79
+ function saveAgents(agents: Agent[]): Record<string, Agent> {
80
+ const record = Object.fromEntries(agents.map((agent) => [agent.id, agent]))
81
+ storage.saveAgents(record)
82
+ storage.saveTasks({})
83
+ storage.saveSessions({})
84
+ return record
85
+ }
86
+
87
+ describe('delegation-advisory', () => {
88
+ it('prefers a builder over a coordinator for coding work', () => {
89
+ const agents = saveAgents([
90
+ makeAgent({
91
+ id: 'ceo',
92
+ name: 'CEO',
93
+ role: 'coordinator',
94
+ capabilities: ['coordination', 'delegation', 'operations'],
95
+ delegationEnabled: true,
96
+ }),
97
+ makeAgent({
98
+ id: 'builder',
99
+ name: 'Builder',
100
+ role: 'worker',
101
+ capabilities: ['coding', 'implementation', 'debugging'],
102
+ }),
103
+ makeAgent({
104
+ id: 'writer',
105
+ name: 'Writer',
106
+ role: 'worker',
107
+ capabilities: ['writing', 'editing'],
108
+ }),
109
+ ])
110
+
111
+ const profile = advisory.buildDelegationTaskProfile({
112
+ classification: makeClassification({
113
+ isDeliverableTask: true,
114
+ workType: 'coding',
115
+ }),
116
+ })
117
+ const result = advisory.resolveDelegationAdvisory({
118
+ currentAgent: agents.ceo,
119
+ agents,
120
+ profile,
121
+ delegationTargetMode: 'all',
122
+ delegationTargetAgentIds: [],
123
+ })
124
+
125
+ assert.equal(result.shouldDelegate, true)
126
+ assert.equal(result.style, 'managerial')
127
+ assert.equal(result.recommended?.agentId, 'builder')
128
+ assert.match(advisory.formatDelegationRationale(result.recommended), /coding/i)
129
+ })
130
+
131
+ it('prefers a researcher for research work', () => {
132
+ const agents = saveAgents([
133
+ makeAgent({
134
+ id: 'ceo',
135
+ name: 'CEO',
136
+ role: 'coordinator',
137
+ capabilities: ['coordination', 'delegation', 'operations'],
138
+ delegationEnabled: true,
139
+ }),
140
+ makeAgent({
141
+ id: 'builder',
142
+ name: 'Builder',
143
+ role: 'worker',
144
+ capabilities: ['coding', 'implementation', 'debugging'],
145
+ }),
146
+ makeAgent({
147
+ id: 'researcher',
148
+ name: 'Researcher',
149
+ role: 'worker',
150
+ capabilities: ['research', 'analysis', 'summarization'],
151
+ }),
152
+ ])
153
+
154
+ const profile = advisory.buildDelegationTaskProfile({
155
+ classification: makeClassification({
156
+ isResearchSynthesis: true,
157
+ workType: 'research',
158
+ }),
159
+ })
160
+ const result = advisory.resolveDelegationAdvisory({
161
+ currentAgent: agents.ceo,
162
+ agents,
163
+ profile,
164
+ delegationTargetMode: 'all',
165
+ delegationTargetAgentIds: [],
166
+ })
167
+
168
+ assert.equal(result.shouldDelegate, true)
169
+ assert.equal(result.recommended?.agentId, 'researcher')
170
+ })
171
+
172
+ it('does not advise delegation when self is already as capable as peers', () => {
173
+ const agents = saveAgents([
174
+ makeAgent({
175
+ id: 'builder-a',
176
+ name: 'Builder A',
177
+ role: 'worker',
178
+ capabilities: ['coding', 'implementation', 'debugging'],
179
+ delegationEnabled: true,
180
+ }),
181
+ makeAgent({
182
+ id: 'builder-b',
183
+ name: 'Builder B',
184
+ role: 'worker',
185
+ capabilities: ['coding', 'implementation', 'debugging'],
186
+ }),
187
+ ])
188
+
189
+ const profile = advisory.buildDelegationTaskProfile({
190
+ classification: makeClassification({
191
+ isDeliverableTask: true,
192
+ workType: 'coding',
193
+ }),
194
+ })
195
+ const result = advisory.resolveDelegationAdvisory({
196
+ currentAgent: agents['builder-a'],
197
+ agents,
198
+ profile,
199
+ delegationTargetMode: 'all',
200
+ delegationTargetAgentIds: [],
201
+ })
202
+
203
+ assert.equal(result.recommended?.agentId, 'builder-b')
204
+ assert.equal(result.shouldDelegate, false)
205
+ })
206
+ })
@@ -0,0 +1,251 @@
1
+ import type { Agent } from '@/types'
2
+ import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
3
+ import { capabilityMatchScore } from '@/lib/server/agents/capability-match'
4
+ import { getAgentDirectory, type AgentDirectoryEntry } from '@/lib/server/agents/agent-registry'
5
+
6
+ export type DelegationWorkType =
7
+ | 'coding'
8
+ | 'research'
9
+ | 'writing'
10
+ | 'review'
11
+ | 'operations'
12
+ | 'general'
13
+
14
+ export interface DelegationTaskProfile {
15
+ workType: DelegationWorkType
16
+ requiredCapabilities: string[]
17
+ substantial: boolean
18
+ }
19
+
20
+ export interface DelegationCandidateFit {
21
+ agentId: string
22
+ agentName: string
23
+ score: number
24
+ availability: 'idle' | 'working' | 'unknown'
25
+ matchedCapabilities: string[]
26
+ reasons: string[]
27
+ }
28
+
29
+ export interface DelegationAdvisory {
30
+ profile: DelegationTaskProfile
31
+ current: DelegationCandidateFit | null
32
+ recommended: DelegationCandidateFit | null
33
+ shouldDelegate: boolean
34
+ style: 'managerial' | 'advisory'
35
+ }
36
+
37
+ const WORK_TYPE_CAPABILITIES: Record<DelegationWorkType, string[]> = {
38
+ coding: ['coding', 'implementation', 'debugging'],
39
+ research: ['research', 'analysis', 'summarization'],
40
+ writing: ['writing', 'messaging', 'structuring', 'editing'],
41
+ review: ['review', 'testing', 'risk assessment'],
42
+ operations: ['coordination', 'delegation', 'operations'],
43
+ general: [],
44
+ }
45
+
46
+ function normalizeCapabilityList(value: string[] | undefined | null): string[] {
47
+ if (!Array.isArray(value)) return []
48
+ const seen = new Set<string>()
49
+ const out: string[] = []
50
+ for (const entry of value) {
51
+ const trimmed = typeof entry === 'string' ? entry.trim() : ''
52
+ const key = trimmed.toLowerCase()
53
+ if (!trimmed || seen.has(key)) continue
54
+ seen.add(key)
55
+ out.push(trimmed)
56
+ }
57
+ return out
58
+ }
59
+
60
+ function normalizeWorkType(value: unknown): DelegationWorkType {
61
+ if (
62
+ value === 'coding'
63
+ || value === 'research'
64
+ || value === 'writing'
65
+ || value === 'review'
66
+ || value === 'operations'
67
+ ) {
68
+ return value
69
+ }
70
+ return 'general'
71
+ }
72
+
73
+ function matchedCapabilities(agentCapabilities: string[] | undefined, requiredCapabilities: string[]): string[] {
74
+ if (!requiredCapabilities.length || !Array.isArray(agentCapabilities) || !agentCapabilities.length) return []
75
+ const agentSet = new Set(agentCapabilities.map((entry) => entry.toLowerCase()))
76
+ return requiredCapabilities.filter((entry) => agentSet.has(entry.toLowerCase()))
77
+ }
78
+
79
+ function roleAdjustment(agent: Agent, profile: DelegationTaskProfile): number {
80
+ const role = agent.role === 'coordinator' ? 'coordinator' : 'worker'
81
+ if (profile.workType === 'operations') {
82
+ return role === 'coordinator' ? 0.28 : -0.04
83
+ }
84
+ if (profile.workType === 'general') {
85
+ return role === 'coordinator' ? -0.03 : 0
86
+ }
87
+ return role === 'coordinator' ? -0.18 : 0.16
88
+ }
89
+
90
+ function selfExecutionPenalty(agent: Agent, profile: DelegationTaskProfile, isSelf: boolean): number {
91
+ if (!isSelf) return 0
92
+ if (agent.role !== 'coordinator') return 0
93
+ if (!profile.substantial) return 0
94
+ if (profile.workType === 'operations') return 0
95
+ return -0.42
96
+ }
97
+
98
+ function availabilityAdjustment(
99
+ availability: DelegationCandidateFit['availability'],
100
+ isSelf: boolean,
101
+ directoryEntry?: AgentDirectoryEntry,
102
+ ): number {
103
+ if (availability === 'idle') return 0.08
104
+ if (availability === 'working') {
105
+ // The current live chat session should not count as a self-load penalty.
106
+ if (isSelf && !directoryEntry?.statusDetail) return 0.08
107
+ return -0.08
108
+ }
109
+ return 0
110
+ }
111
+
112
+ function buildAvailabilityMap(): Map<string, AgentDirectoryEntry> {
113
+ return new Map(getAgentDirectory().map((entry) => [entry.id, entry]))
114
+ }
115
+
116
+ function buildCandidateFit(
117
+ agent: Agent,
118
+ profile: DelegationTaskProfile,
119
+ directory: Map<string, AgentDirectoryEntry>,
120
+ isSelf = false,
121
+ ): DelegationCandidateFit {
122
+ const directoryEntry = directory.get(agent.id)
123
+ const availability = directoryEntry?.status || 'unknown'
124
+ const matched = matchedCapabilities(agent.capabilities, profile.requiredCapabilities)
125
+ const capabilityScore = profile.requiredCapabilities.length > 0
126
+ ? capabilityMatchScore(agent.capabilities, profile.requiredCapabilities) * 1.45
127
+ : 0
128
+ const score = capabilityScore
129
+ + roleAdjustment(agent, profile)
130
+ + availabilityAdjustment(availability, isSelf, directoryEntry)
131
+ + selfExecutionPenalty(agent, profile, isSelf)
132
+
133
+ const reasons: string[] = []
134
+ if (matched.length > 0) reasons.push(`capability match: ${matched.join(', ')}`)
135
+ if (profile.workType === 'operations' && agent.role === 'coordinator') reasons.push('coordinator role fits operations work')
136
+ if (profile.workType !== 'operations' && profile.workType !== 'general' && agent.role !== 'coordinator') reasons.push('worker role fits execution-heavy work')
137
+ if (availability === 'idle') reasons.push('currently idle')
138
+ if (availability === 'working' && directoryEntry?.statusDetail) reasons.push(directoryEntry.statusDetail)
139
+ if (isSelf && selfExecutionPenalty(agent, profile, true) < 0) reasons.push('coordinator should prefer orchestration over direct execution')
140
+
141
+ return {
142
+ agentId: agent.id,
143
+ agentName: agent.name,
144
+ score,
145
+ availability,
146
+ matchedCapabilities: matched,
147
+ reasons,
148
+ }
149
+ }
150
+
151
+ function isAllowedDelegateTarget(
152
+ agentId: string,
153
+ opts?: { delegationTargetMode?: 'all' | 'selected'; delegationTargetAgentIds?: string[] },
154
+ ): boolean {
155
+ if (opts?.delegationTargetMode !== 'selected') return true
156
+ const allowed = new Set(normalizeCapabilityList(opts.delegationTargetAgentIds))
157
+ return allowed.size === 0 || allowed.has(agentId)
158
+ }
159
+
160
+ export function resolveDelegationWorkType(
161
+ classification: MessageClassification | null | undefined,
162
+ ): DelegationWorkType {
163
+ return normalizeWorkType(classification?.workType)
164
+ }
165
+
166
+ export function buildDelegationTaskProfile(params: {
167
+ classification?: MessageClassification | null
168
+ workType?: DelegationWorkType | null
169
+ requiredCapabilities?: string[] | null
170
+ }): DelegationTaskProfile {
171
+ const workType = params.workType
172
+ ? normalizeWorkType(params.workType)
173
+ : resolveDelegationWorkType(params.classification)
174
+ const explicitRequirements = normalizeCapabilityList(params.requiredCapabilities)
175
+ const requiredCapabilities = explicitRequirements.length > 0
176
+ ? explicitRequirements
177
+ : WORK_TYPE_CAPABILITIES[workType]
178
+ const substantial = explicitRequirements.length > 0
179
+ || Boolean(params.classification?.isBroadGoal)
180
+ || Boolean(params.classification?.isDeliverableTask)
181
+ || Boolean(params.classification?.isResearchSynthesis)
182
+ || workType !== 'general'
183
+ return {
184
+ workType,
185
+ requiredCapabilities,
186
+ substantial,
187
+ }
188
+ }
189
+
190
+ export function resolveBestDelegateTarget(params: {
191
+ currentAgentId?: string | null
192
+ agents: Record<string, Agent>
193
+ profile: DelegationTaskProfile
194
+ delegationTargetMode?: 'all' | 'selected'
195
+ delegationTargetAgentIds?: string[]
196
+ }): DelegationCandidateFit | null {
197
+ const directory = buildAvailabilityMap()
198
+ const candidates = Object.values(params.agents)
199
+ .filter((agent) => agent.id !== params.currentAgentId)
200
+ .filter((agent) => !agent.disabled && !agent.trashedAt)
201
+ .filter((agent) => isAllowedDelegateTarget(agent.id, params))
202
+ .map((agent) => buildCandidateFit(agent, params.profile, directory))
203
+ .sort((left, right) => {
204
+ if (right.score !== left.score) return right.score - left.score
205
+ return left.agentName.localeCompare(right.agentName)
206
+ })
207
+ return candidates[0] || null
208
+ }
209
+
210
+ export function resolveDelegationAdvisory(params: {
211
+ currentAgent: Agent | null | undefined
212
+ agents: Record<string, Agent>
213
+ profile: DelegationTaskProfile
214
+ delegationTargetMode?: 'all' | 'selected'
215
+ delegationTargetAgentIds?: string[]
216
+ }): DelegationAdvisory {
217
+ const directory = buildAvailabilityMap()
218
+ const current = params.currentAgent && !params.currentAgent.disabled && !params.currentAgent.trashedAt
219
+ ? buildCandidateFit(params.currentAgent, params.profile, directory, true)
220
+ : null
221
+ const recommended = resolveBestDelegateTarget({
222
+ currentAgentId: params.currentAgent?.id || null,
223
+ agents: params.agents,
224
+ profile: params.profile,
225
+ delegationTargetMode: params.delegationTargetMode,
226
+ delegationTargetAgentIds: params.delegationTargetAgentIds,
227
+ })
228
+ const currentScore = current?.score ?? 0
229
+ const recommendedScore = recommended?.score ?? Number.NEGATIVE_INFINITY
230
+ const shouldDelegate = Boolean(
231
+ params.profile.substantial
232
+ && recommended
233
+ && recommendedScore >= currentScore + 0.3
234
+ && recommendedScore >= 0.25,
235
+ )
236
+ const style = params.currentAgent?.role === 'coordinator' && params.profile.workType !== 'operations'
237
+ ? 'managerial'
238
+ : 'advisory'
239
+ return {
240
+ profile: params.profile,
241
+ current,
242
+ recommended,
243
+ shouldDelegate,
244
+ style,
245
+ }
246
+ }
247
+
248
+ export function formatDelegationRationale(candidate: DelegationCandidateFit | null | undefined): string {
249
+ if (!candidate || candidate.reasons.length === 0) return 'better fit for this work'
250
+ return candidate.reasons.slice(0, 2).join('; ')
251
+ }