@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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 (262) hide show
  1. package/README.md +14 -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]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -111,4 +111,156 @@ describe('manage_tasks tool', () => {
111
111
  assert.equal(output.created.codexResumeId, 'codex-thread-1')
112
112
  assert.deepEqual(output.created.blockedBy, ['task-source'])
113
113
  })
114
+
115
+ it('auto-assigns unowned tasks to a better-fit teammate when delegation is enabled', () => {
116
+ const output = runWithTempDataDir(`
117
+ const storageMod = await import('./src/lib/server/storage')
118
+ const crudMod = await import('./src/lib/server/session-tools/crud')
119
+ const storage = storageMod.default || storageMod
120
+ const crud = crudMod.default || crudMod
121
+
122
+ const now = Date.now()
123
+ storage.saveAgents({
124
+ ceo: {
125
+ id: 'ceo',
126
+ name: 'CEO',
127
+ role: 'coordinator',
128
+ description: 'Directs specialist workers',
129
+ systemPrompt: '',
130
+ provider: 'openai',
131
+ model: 'gpt-test',
132
+ capabilities: ['coordination', 'delegation', 'operations'],
133
+ delegationEnabled: true,
134
+ delegationTargetMode: 'all',
135
+ delegationTargetAgentIds: [],
136
+ createdAt: now,
137
+ updatedAt: now,
138
+ },
139
+ builder: {
140
+ id: 'builder',
141
+ name: 'Builder',
142
+ role: 'worker',
143
+ description: 'Builds and debugs software',
144
+ systemPrompt: '',
145
+ provider: 'openai',
146
+ model: 'gpt-test',
147
+ capabilities: ['coding', 'implementation', 'debugging'],
148
+ createdAt: now,
149
+ updatedAt: now,
150
+ },
151
+ researcher: {
152
+ id: 'researcher',
153
+ name: 'Researcher',
154
+ role: 'worker',
155
+ description: 'Research and synthesis specialist',
156
+ systemPrompt: '',
157
+ provider: 'openai',
158
+ model: 'gpt-test',
159
+ capabilities: ['research', 'analysis', 'summarization'],
160
+ createdAt: now,
161
+ updatedAt: now,
162
+ },
163
+ })
164
+
165
+ const tools = crud.buildCrudTools({
166
+ cwd: process.env.WORKSPACE_DIR,
167
+ ctx: { sessionId: 'session-auto-assign', agentId: 'ceo', delegationEnabled: true, delegationTargetMode: 'all', delegationTargetAgentIds: [] },
168
+ hasExtension: (name) => name === 'manage_tasks',
169
+ })
170
+ const tool = tools.find((entry) => entry.name === 'manage_tasks')
171
+ const raw = await tool.invoke({
172
+ action: 'create',
173
+ title: 'Implement API integration',
174
+ description: 'Build the API client and fix the failing tests.',
175
+ requiredCapabilities: ['coding'],
176
+ status: 'backlog',
177
+ })
178
+
179
+ const response = JSON.parse(raw)
180
+ const stored = storage.loadTasks()[response.id]
181
+ console.log(JSON.stringify({ response, stored }))
182
+ `)
183
+
184
+ assert.equal(output.response.agentId, 'builder')
185
+ assert.equal(output.stored.agentId, 'builder')
186
+ assert.equal(output.response.delegationAdvisory.autoAssigned, true)
187
+ assert.equal(output.response.delegationAdvisory.recommendedAgentId, 'builder')
188
+ assert.deepEqual(output.response.delegationAdvisory.requiredCapabilities, ['coding'])
189
+ })
190
+
191
+ it('keeps an explicit assignee but returns delegation advisory when another teammate is a better fit', () => {
192
+ const output = runWithTempDataDir(`
193
+ const storageMod = await import('./src/lib/server/storage')
194
+ const crudMod = await import('./src/lib/server/session-tools/crud')
195
+ const storage = storageMod.default || storageMod
196
+ const crud = crudMod.default || crudMod
197
+
198
+ const now = Date.now()
199
+ storage.saveAgents({
200
+ ceo: {
201
+ id: 'ceo',
202
+ name: 'CEO',
203
+ role: 'coordinator',
204
+ description: 'Directs specialist workers',
205
+ systemPrompt: '',
206
+ provider: 'openai',
207
+ model: 'gpt-test',
208
+ capabilities: ['coordination', 'delegation', 'operations'],
209
+ delegationEnabled: true,
210
+ delegationTargetMode: 'all',
211
+ delegationTargetAgentIds: [],
212
+ createdAt: now,
213
+ updatedAt: now,
214
+ },
215
+ builder: {
216
+ id: 'builder',
217
+ name: 'Builder',
218
+ role: 'worker',
219
+ description: 'Builds and debugs software',
220
+ systemPrompt: '',
221
+ provider: 'openai',
222
+ model: 'gpt-test',
223
+ capabilities: ['coding', 'implementation', 'debugging'],
224
+ createdAt: now,
225
+ updatedAt: now,
226
+ },
227
+ researcher: {
228
+ id: 'researcher',
229
+ name: 'Researcher',
230
+ role: 'worker',
231
+ description: 'Research and synthesis specialist',
232
+ systemPrompt: '',
233
+ provider: 'openai',
234
+ model: 'gpt-test',
235
+ capabilities: ['research', 'analysis', 'summarization'],
236
+ createdAt: now,
237
+ updatedAt: now,
238
+ },
239
+ })
240
+
241
+ const tools = crud.buildCrudTools({
242
+ cwd: process.env.WORKSPACE_DIR,
243
+ ctx: { sessionId: 'session-explicit-assign', agentId: 'ceo', delegationEnabled: true, delegationTargetMode: 'all', delegationTargetAgentIds: [] },
244
+ hasExtension: (name) => name === 'manage_tasks',
245
+ })
246
+ const tool = tools.find((entry) => entry.name === 'manage_tasks')
247
+ const raw = await tool.invoke({
248
+ action: 'create',
249
+ title: 'Implement API integration',
250
+ description: 'Build the API client and fix the failing tests.',
251
+ agentId: 'researcher',
252
+ requiredCapabilities: ['coding'],
253
+ status: 'backlog',
254
+ })
255
+
256
+ const response = JSON.parse(raw)
257
+ const stored = storage.loadTasks()[response.id]
258
+ console.log(JSON.stringify({ response, stored }))
259
+ `)
260
+
261
+ assert.equal(output.response.agentId, 'researcher')
262
+ assert.equal(output.stored.agentId, 'researcher')
263
+ assert.equal(output.response.delegationAdvisory.autoAssigned, false)
264
+ assert.equal(output.response.delegationAdvisory.recommendedAgentId, 'builder')
265
+ })
114
266
  })
@@ -15,6 +15,7 @@ import { expandQuery } from '../query-expansion'
15
15
  import type { FileReference, MemoryEntry, MemoryImage, MemoryReference, Extension, ExtensionHooks, Session } from '@/types'
16
16
  import type { ToolBuildContext } from './context'
17
17
  import { registerNativeCapability } from '../native-capabilities'
18
+ import { getMessages } from '@/lib/server/messages/message-repository'
18
19
  import { normalizeToolInputArgs } from './normalize-tool-args'
19
20
  import { getMemoryTier, partitionMemoriesByTier, shouldHideFromDurableRecall } from '@/lib/server/memory/memory-tiers'
20
21
  import { syncSessionArchiveMemory } from '@/lib/server/memory/session-archive-memory'
@@ -88,13 +89,14 @@ function isSessionContext(ctx: MemoryActionContext | null | undefined): ctx is S
88
89
  return !!ctx
89
90
  && typeof ctx.id === 'string'
90
91
  && typeof ctx.name === 'string'
91
- && Array.isArray(ctx.messages)
92
+ && typeof ctx.provider === 'string'
92
93
  }
93
94
 
94
95
  function latestUserFactFromSession(session: Session | null): string {
95
- if (!session || !Array.isArray(session.messages)) return ''
96
- for (let index = session.messages.length - 1; index >= 0; index--) {
97
- const message = session.messages[index]
96
+ if (!session) return ''
97
+ const messages = getMessages(session.id)
98
+ for (let index = messages.length - 1; index >= 0; index--) {
99
+ const message = messages[index]
98
100
  if (message?.role !== 'user') continue
99
101
  const text = typeof message.text === 'string' ? message.text.replace(/\s+/g, ' ').trim() : ''
100
102
  if (text) return text
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { buildSessionIdentityPayload } from '@/lib/server/session-tools/session-info'
5
+
6
+ describe('buildSessionIdentityPayload', () => {
7
+ it('includes harness, project, provider, and delegation context for identity queries', () => {
8
+ const payload = buildSessionIdentityPayload({
9
+ context: { sessionId: 'session-child', agentId: 'agent-1' },
10
+ currentSession: {
11
+ id: 'session-child',
12
+ name: 'Builder Chat',
13
+ cwd: '/workspace/projects/northstar',
14
+ provider: 'openai',
15
+ model: 'gpt-5',
16
+ user: 'system',
17
+ createdAt: 1,
18
+ lastActiveAt: 1,
19
+ claudeSessionId: null,
20
+ parentSessionId: 'session-parent',
21
+ sessionType: 'human',
22
+ agentId: 'agent-1',
23
+ messages: [],
24
+ } as never,
25
+ currentAgent: {
26
+ name: 'Builder',
27
+ delegationTargetMode: 'selected',
28
+ delegationTargetAgentIds: ['qa-1', 'ops-1'],
29
+ } as never,
30
+ activeProjectContext: {
31
+ projectId: 'project-1',
32
+ project: { name: 'Northstar' },
33
+ } as never,
34
+ enabledExtensions: ['files', 'manage_sessions', 'codex_cli'],
35
+ toolPolicy: {
36
+ mode: 'balanced',
37
+ requestedExtensions: ['files', 'manage_sessions', 'codex_cli', 'manage_secrets'],
38
+ enabledExtensions: ['files', 'manage_sessions', 'codex_cli'],
39
+ blockedExtensions: [{ tool: 'manage_secrets', reason: 'blocked by policy', source: 'policy' }],
40
+ },
41
+ rootSessionId: 'session-root',
42
+ })
43
+
44
+ assert.equal(payload.promptMode, 'minimal')
45
+ assert.equal(payload.projectId, 'project-1')
46
+ assert.equal(payload.projectName, 'Northstar')
47
+ assert.equal(payload.provider, 'openai')
48
+ assert.equal(payload.model, 'gpt-5')
49
+ assert.equal(payload.rootSessionId, 'session-root')
50
+ assert.deepEqual(payload.enabledExtensions, ['files', 'manage_sessions', 'codex_cli'])
51
+ assert.deepEqual(payload.blockedExtensions, [{ tool: 'manage_secrets', reason: 'blocked by policy', source: 'policy' }])
52
+ assert.equal(payload.delegationEnabled, true)
53
+ assert.equal(payload.delegationTargetMode, 'selected')
54
+ assert.deepEqual(payload.delegationTargetAgentIds, ['qa-1', 'ops-1'])
55
+ })
56
+ })
@@ -3,10 +3,19 @@ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import { genId } from '@/lib/id'
4
4
  import { loadSessions, saveSessions, loadAgents } from '../storage'
5
5
  import type { ToolBuildContext } from './context'
6
- import type { Extension, ExtensionHooks } from '@/types'
6
+ import type { ActiveProjectContext } from '@/lib/server/project-context'
7
+ import type { SessionToolPolicyDecision } from '@/lib/server/tool-capability-policy'
8
+ import type { Agent, Extension, ExtensionHooks, Session } from '@/types'
7
9
  import { registerNativeCapability } from '../native-capabilities'
8
10
  import { normalizeToolInputArgs } from './normalize-tool-args'
9
- import { getEnabledCapabilitySelection } from '@/lib/capability-selection'
11
+ import { getEnabledCapabilityIds, getEnabledCapabilitySelection } from '@/lib/capability-selection'
12
+ import { getExtensionManager } from '@/lib/server/extensions'
13
+ import { canonicalizeExtensionId, expandExtensionIds } from '@/lib/server/tool-aliases'
14
+ import { resolvePromptMode } from '@/lib/server/chat-execution/prompt-mode'
15
+ import { resolveActiveProjectContext } from '@/lib/server/project-context'
16
+ import { resolveSessionLineageIds } from '@/lib/server/sessions/session-lineage'
17
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
18
+ import { resolveSessionToolPolicy } from '@/lib/server/tool-capability-policy'
10
19
 
11
20
  /**
12
21
  * Core Session Info Execution Logic
@@ -15,17 +24,111 @@ async function executeWhoAmI(context: { sessionId?: string; agentId?: string })
15
24
  try {
16
25
  const sessions = loadSessions()
17
26
  const current = context.sessionId ? sessions[context.sessionId] : null
18
- return JSON.stringify({
19
- sessionId: context.sessionId || undefined,
20
- sessionName: current?.name || undefined,
21
- sessionType: current?.sessionType || undefined,
22
- user: current?.user || undefined,
23
- agentId: context.agentId || current?.agentId || undefined,
24
- parentSessionId: current?.parentSessionId || undefined,
25
- })
27
+ const agents = loadAgents()
28
+ const agentRecord = (context.agentId ? agents[context.agentId] : null) || (current?.agentId ? agents[current.agentId] : null) || null
29
+ const { toolPolicy, enabledExtensions } = resolveSessionIdentityAccess(current)
30
+ const activeProjectContext = resolveActiveProjectContext(current || { agentId: context.agentId || null, cwd: null, projectId: null })
31
+ const { rootSessionId } = resolveSessionLineageIds(current || { id: context.sessionId || '', parentSessionId: null })
32
+ return JSON.stringify(buildSessionIdentityPayload({
33
+ context,
34
+ currentSession: current,
35
+ currentAgent: agentRecord || null,
36
+ activeProjectContext,
37
+ enabledExtensions,
38
+ toolPolicy,
39
+ rootSessionId,
40
+ }))
26
41
  } catch (err: any) { return `Error: ${err.message}` }
27
42
  }
28
43
 
44
+ function normalizeRuntimeExtensionId(extensionId: string): string {
45
+ const normalized = extensionId.trim().toLowerCase()
46
+ if (!normalized) return ''
47
+ if (normalized === 'delegate_to_claude_code' || normalized === 'claude_code') return 'claude_code'
48
+ if (normalized === 'delegate_to_codex_cli' || normalized === 'codex_cli') return 'codex_cli'
49
+ if (normalized === 'delegate_to_opencode_cli' || normalized === 'opencode_cli') return 'opencode_cli'
50
+ if (normalized === 'delegate_to_gemini_cli' || normalized === 'gemini_cli') return 'gemini_cli'
51
+ if (['session_info', 'sessions_tool', 'whoami_tool', 'search_history_tool'].includes(normalized)) return 'manage_sessions'
52
+ return canonicalizeExtensionId(normalized)
53
+ }
54
+
55
+ function canonicalizeEnabledExtensions(enabledExtensions: string[]): string[] {
56
+ const seen = new Set<string>()
57
+ const values: string[] = []
58
+ for (const extensionId of enabledExtensions) {
59
+ const normalized = normalizeRuntimeExtensionId(extensionId)
60
+ if (!normalized || seen.has(normalized)) continue
61
+ seen.add(normalized)
62
+ values.push(normalized)
63
+ }
64
+ return values
65
+ }
66
+
67
+ function resolveSessionIdentityAccess(current: Session | null): {
68
+ toolPolicy: SessionToolPolicyDecision
69
+ enabledExtensions: string[]
70
+ } {
71
+ const rawExtensions = getEnabledCapabilityIds(current)
72
+ const hasShellCapability = rawExtensions.some((toolId) => ['shell', 'execute_command'].includes(String(toolId)))
73
+ const extensionManager = getExtensionManager()
74
+ const requestedExtensions = expandExtensionIds([
75
+ ...rawExtensions,
76
+ ...(hasShellCapability ? ['process'] : []),
77
+ ]).filter((id) => !extensionManager.isExplicitlyDisabled(id))
78
+ const toolPolicy = resolveSessionToolPolicy(requestedExtensions, loadSettings())
79
+ const blockedExtensionIds = new Set(expandExtensionIds(toolPolicy.blockedExtensions.map((entry) => entry.tool)))
80
+ const enabledExtensions = canonicalizeEnabledExtensions(
81
+ expandExtensionIds(toolPolicy.enabledExtensions)
82
+ .filter((id) => !blockedExtensionIds.has(id))
83
+ .filter((id) => !extensionManager.isExplicitlyDisabled(id)),
84
+ )
85
+ return { toolPolicy, enabledExtensions }
86
+ }
87
+
88
+ export function buildSessionIdentityPayload(params: {
89
+ context: { sessionId?: string; agentId?: string }
90
+ currentSession: Session | null
91
+ currentAgent?: Agent | null
92
+ activeProjectContext?: ActiveProjectContext | null
93
+ enabledExtensions?: string[]
94
+ toolPolicy?: SessionToolPolicyDecision | null
95
+ rootSessionId?: string | null
96
+ }): Record<string, unknown> {
97
+ const current = params.currentSession
98
+ const currentAgent = params.currentAgent || null
99
+ const activeProjectContext = params.activeProjectContext || null
100
+ const enabledExtensions = Array.isArray(params.enabledExtensions) ? params.enabledExtensions : []
101
+ const toolPolicy = params.toolPolicy || null
102
+ const delegationEnabled = enabledExtensions.some((extensionId) => ['delegate', 'spawn_subagent', 'claude_code', 'codex_cli', 'opencode_cli', 'gemini_cli'].includes(extensionId))
103
+
104
+ return {
105
+ sessionId: params.context.sessionId || undefined,
106
+ sessionName: current?.name || undefined,
107
+ sessionType: current?.sessionType || undefined,
108
+ sessionKind: current?.parentSessionId ? 'delegated_child' : 'root_chat',
109
+ promptMode: current ? resolvePromptMode(current) : undefined,
110
+ user: current?.user || undefined,
111
+ agentId: params.context.agentId || current?.agentId || undefined,
112
+ agentName: typeof currentAgent?.name === 'string' ? currentAgent.name : undefined,
113
+ parentSessionId: current?.parentSessionId || undefined,
114
+ rootSessionId: params.rootSessionId || current?.id || undefined,
115
+ cwd: current?.cwd || undefined,
116
+ projectId: activeProjectContext?.projectId || undefined,
117
+ projectName: activeProjectContext?.project?.name || undefined,
118
+ provider: current?.provider || undefined,
119
+ model: current?.model || undefined,
120
+ enabledExtensions,
121
+ blockedExtensions: toolPolicy?.blockedExtensions || [],
122
+ delegationEnabled,
123
+ delegationTargetMode: delegationEnabled
124
+ ? (currentAgent?.delegationTargetMode === 'selected' ? 'selected' : 'all')
125
+ : undefined,
126
+ delegationTargetAgentIds: delegationEnabled && Array.isArray(currentAgent?.delegationTargetAgentIds)
127
+ ? currentAgent.delegationTargetAgentIds
128
+ : [],
129
+ }
130
+ }
131
+
29
132
  function inferSessionsAction(
30
133
  normalized: Record<string, unknown>,
31
134
  context: { sessionId?: string; agentId?: string },
@@ -133,8 +236,12 @@ const SessionInfoExtension: Extension = {
133
236
  name: 'Core Session Info',
134
237
  description: 'Identify current session context and manage other agent sessions.',
135
238
  hooks: {
136
- getCapabilityDescription: () => 'I can manage chat sessions (`sessions_tool`) — check my identity with action `identity`, look up past conversations, spawn sessions, and coordinate work.',
137
- getOperatingGuidance: () => 'Inspect existing chats before creating duplicates.',
239
+ getCapabilityDescription: () => 'I can manage chat sessions (`sessions_tool`) — inspect live harness/session context with action `identity`, look up past session messages with `history`, spawn sessions, and coordinate work.',
240
+ getOperatingGuidance: () => [
241
+ 'Use `sessions_tool` action `identity` when you need live session, project, lineage, or enabled-tool context.',
242
+ 'Use `sessions_tool` action `history` only when you need earlier messages from this same session that are not already visible in the current thread.',
243
+ 'Inspect existing chats before creating duplicates.',
244
+ ],
138
245
  } as ExtensionHooks,
139
246
  tools: [
140
247
  {
@@ -2,7 +2,9 @@ import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
3
  import type { Agent, Session, SessionSkillRuntimeState } from '@/types'
4
4
  import { errorMessage } from '@/lib/shared-utils'
5
- import { loadAgent, loadSkills, patchSession } from '@/lib/server/storage'
5
+ import { loadAgent } from '@/lib/server/agents/agent-repository'
6
+ import { loadSkills } from '@/lib/server/skills/skill-repository'
7
+ import { patchSession } from '@/lib/server/sessions/session-repository'
6
8
  import {
7
9
  findResolvedSkill,
8
10
  recommendRuntimeSkillsForTask,
@@ -4,11 +4,15 @@ import { dedup, errorMessage } from '@/lib/shared-utils'
4
4
  import { requestApproval } from '@/lib/server/approvals'
5
5
  import {
6
6
  loadAgent,
7
- loadApprovals,
8
- loadSkills,
9
7
  patchAgent,
10
- saveSkills,
11
- } from '@/lib/server/storage'
8
+ } from '@/lib/server/agents/agent-repository'
9
+ import { loadApproval, loadApprovals } from '@/lib/server/approvals/approval-repository'
10
+ import {
11
+ deleteSkill,
12
+ loadSkill,
13
+ loadSkills,
14
+ saveSkill,
15
+ } from '@/lib/server/skills/skill-repository'
12
16
  import { fetchSkillContent, searchClawHub } from '@/lib/server/skills/clawhub-client'
13
17
  import { clearDiscoveredSkillsCache } from '@/lib/server/skills/skill-discovery'
14
18
  import {
@@ -133,11 +137,10 @@ function upsertStoredSkill(input: {
133
137
  existingId?: string
134
138
  body: Record<string, unknown>
135
139
  }): Skill {
136
- const skills = loadSkills()
137
140
  const normalized = normalizeSkillPayload(input.body)
138
141
  const now = Date.now()
139
142
  const id = input.existingId || genId()
140
- const previous = input.existingId ? skills[input.existingId] : null
143
+ const previous = input.existingId ? loadSkill(input.existingId) : null
141
144
 
142
145
  const next: Skill = {
143
146
  id,
@@ -171,8 +174,7 @@ function upsertStoredSkill(input: {
171
174
  updatedAt: now,
172
175
  }
173
176
 
174
- skills[id] = next
175
- saveSkills(skills)
177
+ saveSkill(id, next)
176
178
  clearDiscoveredSkillsCache()
177
179
  return next
178
180
  }
@@ -241,7 +243,7 @@ function ensureApprovedInstall(approvalId: string | null | undefined): ApprovalR
241
243
  if (!normalized) {
242
244
  throw new Error('This install requires approval. Call manage_skills install first to create the approval request, then retry with approvalId after approval.')
243
245
  }
244
- const approval = loadApprovals()[normalized]
246
+ const approval = loadApproval(normalized)
245
247
  if (!approval) throw new Error(`Approval "${normalized}" not found.`)
246
248
  if (approval.status !== 'approved') {
247
249
  throw new Error(`Approval "${normalized}" is not approved yet.`)
@@ -251,7 +253,7 @@ function ensureApprovedInstall(approvalId: string | null | undefined): ApprovalR
251
253
 
252
254
  async function materializeResolvedSkill(skill: ResolvedRuntimeSkill): Promise<Skill> {
253
255
  const skills = loadSkills()
254
- const existing = skill.storageId ? skills[skill.storageId] : null
256
+ const existing = skill.storageId ? loadSkill(skill.storageId) : null
255
257
  if (existing) return existing
256
258
  const duplicate = Object.values(skills).find((entry) =>
257
259
  normalizeKey(entry.skillKey || entry.name) === normalizeKey(skill.skillKey || skill.name),
@@ -366,7 +368,7 @@ export async function executeManageSkillsAction(
366
368
  case 'update': {
367
369
  const skillId = typeof normalized.id === 'string' ? normalized.id.trim() : ''
368
370
  if (!skillId) return 'Error: "id" is required for update action.'
369
- const existing = loadSkills()[skillId]
371
+ const existing = loadSkill(skillId)
370
372
  if (!existing) return `Not found: skills "${skillId}"`
371
373
  const updated = upsertStoredSkill({
372
374
  existingId: skillId,
@@ -377,10 +379,8 @@ export async function executeManageSkillsAction(
377
379
  case 'delete': {
378
380
  const skillId = typeof normalized.id === 'string' ? normalized.id.trim() : ''
379
381
  if (!skillId) return 'Error: "id" is required for delete action.'
380
- const skills = loadSkills()
381
- if (!skills[skillId]) return `Not found: skills "${skillId}"`
382
- delete skills[skillId]
383
- saveSkills(skills)
382
+ if (!loadSkill(skillId)) return `Not found: skills "${skillId}"`
383
+ deleteSkill(skillId)
384
384
  return JSON.stringify({ deleted: skillId })
385
385
  }
386
386
  case 'status': {
@@ -1,8 +1,41 @@
1
1
  import assert from 'node:assert/strict'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
2
6
  import { describe, it } from 'node:test'
3
7
 
4
8
  import { buildSessionTools } from './index'
5
9
 
10
+ const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../../..')
11
+
12
+ function runWithTempDataDir(script: string) {
13
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-subagent-tool-'))
14
+ try {
15
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
16
+ cwd: repoRoot,
17
+ env: {
18
+ ...process.env,
19
+ DATA_DIR: path.join(tempDir, 'data'),
20
+ WORKSPACE_DIR: path.join(tempDir, 'workspace'),
21
+ SWARMCLAW_BUILD_MODE: '1',
22
+ },
23
+ encoding: 'utf-8',
24
+ timeout: 30_000,
25
+ })
26
+ assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
27
+ const lines = (result.stdout || '')
28
+ .trim()
29
+ .split('\n')
30
+ .map((line) => line.trim())
31
+ .filter(Boolean)
32
+ const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
33
+ return JSON.parse(jsonLine || '{}')
34
+ } finally {
35
+ fs.rmSync(tempDir, { recursive: true, force: true })
36
+ }
37
+ }
38
+
6
39
  describe('spawn_subagent runtime access', () => {
7
40
  it('hides spawn_subagent unless delegation is enabled', async () => {
8
41
  const built = await buildSessionTools(process.cwd(), ['spawn_subagent'], {
@@ -42,9 +75,90 @@ describe('spawn_subagent runtime access', () => {
42
75
  message: 'hello',
43
76
  })
44
77
 
45
- assert.match(String(raw), /not in the allowed delegate agent list/i)
78
+ assert.match(String(raw), /allowed delegation list/i)
46
79
  } finally {
47
80
  await built.cleanup()
48
81
  }
49
82
  })
83
+
84
+ it('resolves best_fit start requests to the best allowed delegate', () => {
85
+ const output = runWithTempDataDir(`
86
+ const storageMod = await import('./src/lib/server/storage')
87
+ const toolsMod = await import('./src/lib/server/session-tools')
88
+ const storage = storageMod.default || storageMod
89
+ const toolsApi = toolsMod.default || toolsMod
90
+
91
+ const now = Date.now()
92
+ storage.saveAgents({
93
+ ceo: {
94
+ id: 'ceo',
95
+ name: 'CEO',
96
+ role: 'coordinator',
97
+ description: 'Executes through specialists',
98
+ systemPrompt: '',
99
+ provider: 'openai',
100
+ model: 'gpt-test',
101
+ capabilities: ['coordination', 'delegation', 'operations'],
102
+ delegationEnabled: true,
103
+ delegationTargetMode: 'selected',
104
+ delegationTargetAgentIds: ['builder', 'writer'],
105
+ createdAt: now,
106
+ updatedAt: now,
107
+ },
108
+ builder: {
109
+ id: 'builder',
110
+ name: 'Builder',
111
+ role: 'worker',
112
+ description: 'Builds product changes',
113
+ systemPrompt: '',
114
+ provider: 'openai',
115
+ model: 'gpt-test',
116
+ capabilities: ['coding', 'implementation', 'debugging'],
117
+ createdAt: now,
118
+ updatedAt: now,
119
+ },
120
+ writer: {
121
+ id: 'writer',
122
+ name: 'Writer',
123
+ role: 'worker',
124
+ description: 'Writes and edits content',
125
+ systemPrompt: '',
126
+ provider: 'openai',
127
+ model: 'gpt-test',
128
+ capabilities: ['writing', 'editing'],
129
+ createdAt: now,
130
+ updatedAt: now,
131
+ },
132
+ })
133
+
134
+ const built = await toolsApi.buildSessionTools(process.env.WORKSPACE_DIR, ['spawn_subagent'], {
135
+ sessionId: 'session-best-fit',
136
+ agentId: 'ceo',
137
+ delegationEnabled: true,
138
+ delegationTargetMode: 'selected',
139
+ delegationTargetAgentIds: ['builder', 'writer'],
140
+ })
141
+
142
+ try {
143
+ const tool = built.tools.find((entry) => entry.name === 'spawn_subagent')
144
+ const raw = await tool.invoke({
145
+ action: 'start',
146
+ selectionMode: 'best_fit',
147
+ message: 'Implement the API change and fix the failing tests.',
148
+ workType: 'coding',
149
+ requiredCapabilities: ['coding', 'debugging'],
150
+ background: true,
151
+ })
152
+ console.log(JSON.stringify(JSON.parse(String(raw))))
153
+ } finally {
154
+ await built.cleanup()
155
+ }
156
+ process.exit(0)
157
+ `)
158
+
159
+ assert.equal(output.selectionMode, 'best_fit')
160
+ assert.equal(output.agentId, 'builder')
161
+ assert.equal(output.workType, 'coding')
162
+ assert.deepEqual(output.requiredCapabilities, ['coding', 'debugging'])
163
+ })
50
164
  })