@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
@@ -8,7 +8,7 @@
8
8
  import fs from 'node:fs'
9
9
  import os from 'node:os'
10
10
  import path from 'node:path'
11
- import type { MessageToolEvent } from '@/types'
11
+ import type { Message, MessageToolEvent } from '@/types'
12
12
  import { extractSuggestions } from '@/lib/server/suggestions'
13
13
  import { isSuccessfulMemoryMutationToolEvent } from '@/lib/server/chat-execution/memory-mutation-tools'
14
14
  import type { MessageClassification } from '@/lib/server/chat-execution/message-classifier'
@@ -108,7 +108,7 @@ function looksLikeIncompleteDeliverableResponse(text: string): boolean {
108
108
  }
109
109
 
110
110
  function hasRecentDeliverableContext(
111
- history: Array<{ role?: string; text?: string }> | undefined,
111
+ history: Message[] | undefined,
112
112
  userMessage: string,
113
113
  ): boolean {
114
114
  if (!Array.isArray(history) || history.length === 0) return false
@@ -116,7 +116,7 @@ function hasRecentDeliverableContext(
116
116
  if (!trimmed || trimmed.length > 160) return false
117
117
  return history
118
118
  .slice(-8)
119
- .some((entry) => entry.role === 'user' && looksLikeOpenEndedDeliverableTask((entry.text || '').trim()))
119
+ .some((entry) => entry.role === 'user' && entry.semantics?.isDeliverableTask === true)
120
120
  }
121
121
 
122
122
  const ARTIFACT_PATH_EXT_RE = /\.(?:md|txt|html?|json|csv|ya?ml|xml|pdf|png|jpe?g|webp|gif|svg|zip|ts|tsx|js|jsx|mjs|cjs|py|sql|sh)$/i
@@ -245,7 +245,7 @@ export function shouldForceExternalExecutionFollowthrough(params: {
245
245
  toolEvents: MessageToolEvent[]
246
246
  classification?: MessageClassification | null
247
247
  }): boolean {
248
- const isTransactional = params.classification ? params.classification.walletIntent === 'transactional' : looksLikeBoundedExternalExecutionTask(params.userMessage)
248
+ const isTransactional = params.classification?.walletIntent === 'transactional'
249
249
  if (!isTransactional) return false
250
250
  if (!params.hasToolCalls || params.toolEvents.length < 4) return false
251
251
  if (hasStateChangingWalletEvidence(params.toolEvents)) return false
@@ -266,7 +266,7 @@ export function shouldForceExternalExecutionKickoffFollowthrough(params: {
266
266
  toolEvents: MessageToolEvent[]
267
267
  classification?: MessageClassification | null
268
268
  }): boolean {
269
- const isTransactional = params.classification ? params.classification.walletIntent === 'transactional' : looksLikeBoundedExternalExecutionTask(params.userMessage)
269
+ const isTransactional = params.classification?.walletIntent === 'transactional'
270
270
  if (!isTransactional) return false
271
271
  if (params.hasToolCalls || params.toolEvents.length > 0) return false
272
272
 
@@ -289,11 +289,11 @@ export function shouldForceDeliverableFollowthrough(params: {
289
289
  hasToolCalls: boolean
290
290
  toolEvents: MessageToolEvent[]
291
291
  cwd?: string
292
- history?: Array<{ role?: string; text?: string }>
292
+ history?: Message[]
293
293
  classification?: MessageClassification | null
294
294
  }): boolean {
295
295
  const recentDeliverableContext = hasRecentDeliverableContext(params.history, params.userMessage)
296
- const isDeliverable = params.classification ? params.classification.isDeliverableTask : looksLikeOpenEndedDeliverableTask(params.userMessage)
296
+ const isDeliverable = params.classification?.isDeliverableTask === true
297
297
  const deliverableIntent = isDeliverable || recentDeliverableContext
298
298
  const requestedArtifacts = getRequestedArtifactStatus({
299
299
  userMessage: params.userMessage,
@@ -744,6 +744,9 @@ export function buildContinuationPrompt(params: {
744
744
  cwd?: string
745
745
  frequencyLimitedToolName?: string
746
746
  sessionExtensions?: string[]
747
+ isCoordinatorAgent?: boolean
748
+ recommendedDelegateName?: string | null
749
+ delegationRationale?: string | null
747
750
  }): string | null {
748
751
  switch (params.type) {
749
752
  case 'memory_write_followthrough':
@@ -819,12 +822,24 @@ export function buildContinuationPrompt(params: {
819
822
  ].filter(Boolean).join('\n')
820
823
 
821
824
  case 'coordinator_delegation_nudge':
822
- return [
823
- 'IMPORTANT: You have specialist workers available but you have been doing substantial work directly with tools.',
824
- 'You MUST delegate the remaining work via `spawn_subagent` to the appropriate specialist worker NOW.',
825
- 'As a coordinator, your job is to orchestrate — not to do the work yourself. Direct tool use is only for quick lookups and validation.',
826
- 'Review the workers listed in your system prompt and delegate immediately.',
827
- ].join('\n')
825
+ return params.isCoordinatorAgent
826
+ ? [
827
+ 'IMPORTANT: You have specialist workers available but you have been doing substantial work directly with tools.',
828
+ params.recommendedDelegateName
829
+ ? `Delegate the remaining execution via \`spawn_subagent\` to ${params.recommendedDelegateName} now.`
830
+ : 'You MUST delegate the remaining work via `spawn_subagent` to the appropriate specialist worker NOW.',
831
+ 'As a coordinator, your job is to orchestrate — not to do the work yourself. Direct tool use is only for quick lookups and validation.',
832
+ params.delegationRationale ? `Reason: ${params.delegationRationale}.` : '',
833
+ 'Review the workers listed in your system prompt and delegate immediately.',
834
+ ].filter(Boolean).join('\n')
835
+ : [
836
+ 'You have delegation available and a teammate is a materially better fit for the remaining work.',
837
+ params.recommendedDelegateName
838
+ ? `Use \`spawn_subagent\` to hand the execution to ${params.recommendedDelegateName} now.`
839
+ : 'Use `spawn_subagent` to hand the execution to the best-fit teammate now.',
840
+ params.delegationRationale ? `Reason: ${params.delegationRationale}.` : '',
841
+ 'Keep your direct tool use to reconnaissance, validation, or synthesis unless delegation is blocked.',
842
+ ].filter(Boolean).join('\n')
828
843
 
829
844
  case 'loop_recovery': {
830
845
  const freqTool = params.frequencyLimitedToolName
@@ -1,5 +1,5 @@
1
1
  import type { Chatroom, Agent } from '@/types'
2
- import { loadChatrooms, saveChatrooms } from '@/lib/server/storage'
2
+ import { patchChatroom } from '@/lib/server/chatrooms/chatroom-repository'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
 
5
5
  /**
@@ -35,25 +35,33 @@ export function isImplicitlyMentioned(text: string, agent: Agent): boolean {
35
35
  * Useful for acknowledging tasks or agreeing with teammates.
36
36
  */
37
37
  export function addAgentReaction(chatroomId: string, messageId: string, agentId: string, emoji: string) {
38
- const chatrooms = loadChatrooms()
39
- const chatroom = chatrooms[chatroomId] as Chatroom | undefined
40
- if (!chatroom) return
38
+ const updated = patchChatroom(chatroomId, (current) => {
39
+ const chatroom = current as Chatroom | null
40
+ if (!chatroom) return null
41
+ const message = chatroom.messages.find(m => m.id === messageId)
42
+ if (!message) return chatroom
43
+ if (message.reactions.some(r => r.reactorId === agentId && r.emoji === emoji)) return chatroom
41
44
 
42
- const message = chatroom.messages.find(m => m.id === messageId)
43
- if (!message) return
44
-
45
- // Prevent duplicate reactions from the same agent
46
- if (message.reactions.some(r => r.reactorId === agentId && r.emoji === emoji)) return
47
-
48
- message.reactions.push({
49
- emoji,
50
- reactorId: agentId,
51
- time: Date.now()
45
+ return {
46
+ ...chatroom,
47
+ messages: chatroom.messages.map((entry) => (
48
+ entry.id !== messageId
49
+ ? entry
50
+ : {
51
+ ...entry,
52
+ reactions: [
53
+ ...entry.reactions,
54
+ {
55
+ emoji,
56
+ reactorId: agentId,
57
+ time: Date.now(),
58
+ },
59
+ ],
60
+ }
61
+ )),
62
+ }
52
63
  })
53
-
54
- chatrooms[chatroomId] = chatroom
55
- saveChatrooms(chatrooms)
56
- notify(`chatroom:${chatroomId}`)
64
+ if (updated) notify(`chatroom:${chatroomId}`)
57
65
  }
58
66
 
59
67
  /**
@@ -1,14 +1,18 @@
1
1
  import fs from 'fs'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
- import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from '@/lib/server/storage'
5
4
  import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
6
5
  import { buildIdentityContinuityContext } from '@/lib/server/identity-continuity'
7
6
  import { genId } from '@/lib/id'
8
7
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
9
8
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
9
+ import { loadCredential, decryptKey } from '@/lib/server/credentials/credential-repository'
10
10
  import { resolveProviderApiEndpoint, resolveProviderCredentialId } from '@/lib/server/provider-endpoint'
11
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
11
12
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
13
+ import { loadSkills } from '@/lib/server/skills/skill-repository'
14
+ import { loadSession, patchSession, saveSession } from '@/lib/server/sessions/session-repository'
15
+ import { appendMessage } from '@/lib/server/messages/message-repository'
12
16
  import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
13
17
  import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
14
18
 
@@ -16,8 +20,7 @@ import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-sel
16
20
  export function resolveApiKey(credentialId: string | null | undefined): string | null {
17
21
  const resolvedCredentialId = resolveProviderCredentialId({ credentialId })
18
22
  if (!resolvedCredentialId) return null
19
- const creds = loadCredentials()
20
- const cred = creds[resolvedCredentialId]
23
+ const cred = loadCredential(resolvedCredentialId)
21
24
  if (!cred?.encryptedKey) return null
22
25
  try { return decryptKey(cred.encryptedKey) } catch { return null }
23
26
  }
@@ -136,6 +139,7 @@ export function parseMentions(
136
139
 
137
140
  // Check if the only explicit matches are the sender — if so, treat as "no explicit mentions"
138
141
  const senderId = opts?.senderId
142
+ const explicitSelfMentioned = senderId ? mentioned.includes(senderId) : false
139
143
  const explicitNonSelf = senderId ? mentioned.filter((id) => id !== senderId) : mentioned
140
144
 
141
145
  // 2. Reply-based implicit mention
@@ -158,8 +162,9 @@ export function parseMentions(
158
162
  }
159
163
  }
160
164
 
161
- // Remove self from final list when senderId is provided
162
- return senderId ? mentioned.filter((mid) => mid !== senderId) : mentioned
165
+ // Preserve explicit self-mentions so agents can intentionally address themselves.
166
+ if (!senderId || explicitSelfMentioned) return mentioned
167
+ return mentioned.filter((mid) => mid !== senderId)
163
168
  }
164
169
 
165
170
  export function resolveReplyTargetAgentId(
@@ -312,9 +317,8 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
312
317
  const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
313
318
  fs.mkdirSync(roomWorkspace, { recursive: true })
314
319
  const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
315
- const sessions = loadSessions()
316
320
  const now = Date.now()
317
- const existing = sessions[sessionId]
321
+ const existing = loadSession(sessionId)
318
322
  const session: Session = existing
319
323
  ? applyResolvedRoute({
320
324
  ...existing,
@@ -348,8 +352,7 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
348
352
  }
349
353
  if (session.codexThreadId === undefined) session.codexThreadId = null
350
354
  if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
351
- sessions[sessionId] = session
352
- saveSessions(sessions)
355
+ saveSession(sessionId, session)
353
356
  return session
354
357
  }
355
358
 
@@ -360,18 +363,16 @@ export function appendSyntheticSessionMessage(
360
363
  ): void {
361
364
  const trimmed = String(text || '').trim()
362
365
  if (!trimmed) return
363
- const sessions = loadSessions()
364
- const session = sessions[sessionId]
365
- if (!session) return
366
- if (!Array.isArray(session.messages)) session.messages = []
367
- session.messages.push({
366
+ const timestamp = Date.now()
367
+ appendMessage(sessionId, {
368
368
  role,
369
369
  text: trimmed,
370
- time: Date.now(),
370
+ time: timestamp,
371
+ })
372
+ patchSession(sessionId, (current) => {
373
+ if (!current) return null
374
+ return { ...current, lastActiveAt: timestamp }
371
375
  })
372
- session.lastActiveAt = Date.now()
373
- sessions[sessionId] = session
374
- saveSessions(sessions)
375
376
  }
376
377
 
377
378
  /** Build agent's system prompt including skills and identity context */
@@ -1,10 +1,13 @@
1
1
  import type { Chatroom } from '@/types'
2
2
 
3
3
  import {
4
+ deleteStoredItem,
4
5
  loadChatroom as loadStoredChatroom,
5
6
  loadChatrooms as loadStoredChatrooms,
7
+ patchStoredItem,
6
8
  saveChatrooms as saveStoredChatrooms,
7
9
  upsertChatroom as upsertStoredChatroom,
10
+ upsertStoredItems,
8
11
  } from '@/lib/server/storage'
9
12
  import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
13
 
@@ -20,13 +23,26 @@ export const chatroomRepository = createRecordRepository<Chatroom>(
20
23
  upsert(id, value) {
21
24
  upsertStoredChatroom(id, value as Chatroom)
22
25
  },
26
+ upsertMany(entries) {
27
+ upsertStoredItems('chatrooms', entries as Array<[string, Chatroom]>)
28
+ },
29
+ patch(id, updater) {
30
+ return patchStoredItem('chatrooms', id, updater as (current: Chatroom | null) => Chatroom | null) as Chatroom | null
31
+ },
23
32
  replace(data) {
24
33
  saveStoredChatrooms(data)
25
34
  },
35
+ delete(id) {
36
+ deleteStoredItem('chatrooms', id)
37
+ },
26
38
  },
27
39
  )
28
40
 
29
41
  export const loadChatrooms = () => chatroomRepository.list()
30
42
  export const loadChatroom = (id: string) => chatroomRepository.get(id)
43
+ export const loadChatroomMany = (ids: string[]) => chatroomRepository.getMany(ids)
31
44
  export const saveChatrooms = (items: Record<string, Chatroom | Record<string, unknown>>) => chatroomRepository.replace(items as Record<string, Chatroom>)
32
45
  export const upsertChatroom = (id: string, value: Chatroom | Record<string, unknown>) => chatroomRepository.upsert(id, value as Chatroom)
46
+ export const upsertChatrooms = (entries: Array<[string, Chatroom | Record<string, unknown>]>) => chatroomRepository.upsertMany(entries as Array<[string, Chatroom]>)
47
+ export const patchChatroom = (id: string, updater: (current: Chatroom | null) => Chatroom | null) => chatroomRepository.patch(id, updater)
48
+ export const deleteChatroom = (id: string) => chatroomRepository.delete(id)
@@ -0,0 +1,96 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import type { Agent, Chatroom } from '@/types'
4
+ import {
5
+ ensureChatroomRoutingGuidance,
6
+ selectChatroomRecipients,
7
+ synthesizeRoutingGuidanceFromRules,
8
+ } from './chatroom-routing'
9
+
10
+ const agents: Record<string, Agent> = {
11
+ ops: {
12
+ id: 'ops',
13
+ name: 'Ops',
14
+ description: 'Handles deploys and infrastructure',
15
+ provider: 'openai',
16
+ model: 'gpt-test',
17
+ systemPrompt: '',
18
+ capabilities: ['deploy', 'infrastructure'],
19
+ },
20
+ design: {
21
+ id: 'design',
22
+ name: 'Design',
23
+ description: 'Handles design critique and UI polish',
24
+ provider: 'openai',
25
+ model: 'gpt-test',
26
+ systemPrompt: '',
27
+ capabilities: ['design', 'ui'],
28
+ },
29
+ }
30
+
31
+ function makeChatroom(overrides: Partial<Chatroom> = {}): Chatroom {
32
+ return {
33
+ id: 'room-1',
34
+ name: 'Test Room',
35
+ description: 'General routing test room',
36
+ agentIds: ['ops', 'design'],
37
+ messages: [],
38
+ createdAt: 1,
39
+ updatedAt: 1,
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ test('synthesizes guidance from legacy routing rules and migrates the chatroom', () => {
45
+ const chatroom = makeChatroom({
46
+ routingRules: [
47
+ { id: 'rule-1', type: 'keyword', keywords: ['deploy', 'release'], agentId: 'ops', priority: 1 },
48
+ { id: 'rule-2', type: 'capability', pattern: 'design review', agentId: 'design', priority: 2 },
49
+ ],
50
+ })
51
+
52
+ const guidance = synthesizeRoutingGuidanceFromRules(chatroom.routingRules, agents)
53
+ assert.match(String(guidance || ''), /deploy/i)
54
+ assert.match(String(guidance || ''), /Design/i)
55
+
56
+ const changed = ensureChatroomRoutingGuidance(chatroom, agents)
57
+ assert.equal(changed, true)
58
+ assert.equal(typeof chatroom.routingGuidance, 'string')
59
+ assert.equal(chatroom.routingRules, undefined)
60
+ })
61
+
62
+ test('selects only member ids returned by the selector model', async () => {
63
+ const chatroom = makeChatroom({
64
+ routingGuidance: 'Route deployment incidents to Ops. Prefer Design for UI critique.',
65
+ })
66
+
67
+ const selected = await selectChatroomRecipients({
68
+ text: 'Please diagnose the failed deployment.',
69
+ chatroom,
70
+ agentsById: agents,
71
+ }, {
72
+ generateText: async () => '{"agentIds":["ops","non-member","ops"]}',
73
+ })
74
+
75
+ assert.deepEqual(selected, ['ops'])
76
+ })
77
+
78
+ test('fails open to no inferred mentions when there is no guidance or the selector output is invalid', async () => {
79
+ const unguided = await selectChatroomRecipients({
80
+ text: 'Anyone here?',
81
+ chatroom: makeChatroom(),
82
+ agentsById: agents,
83
+ }, {
84
+ generateText: async () => '{"agentIds":["ops"]}',
85
+ })
86
+ assert.deepEqual(unguided, [])
87
+
88
+ const invalid = await selectChatroomRecipients({
89
+ text: 'Please review the new layout.',
90
+ chatroom: makeChatroom({ routingGuidance: 'Prefer Design for UI review.' }),
91
+ agentsById: agents,
92
+ }, {
93
+ generateText: async () => 'not-json',
94
+ })
95
+ assert.deepEqual(invalid, [])
96
+ })
@@ -1,66 +1,220 @@
1
- import type { ChatroomRoutingRule, Agent } from '@/types'
2
- import { matchesCapabilities } from '@/lib/server/agents/capability-match'
1
+ import { HumanMessage } from '@langchain/core/messages'
2
+ import type { Agent, Chatroom, ChatroomRoutingRule } from '@/types'
3
+ import { buildLLM } from '@/lib/server/build-llm'
4
+ import { log } from '@/lib/server/logger'
3
5
 
4
- /**
5
- * Evaluate routing rules against inbound message text.
6
- *
7
- * Rules are evaluated in priority order (lower number = higher priority).
8
- * First match wins — returns the matched agentIds.
9
- *
10
- * - 'keyword' rules: case-insensitive substring match against `keywords[]`,
11
- * or regex match against `pattern`.
12
- * - 'capability' rules: match `pattern` against each agent's `capabilities[]`.
13
- */
14
- export function evaluateRoutingRules(
15
- text: string,
16
- rules: ChatroomRoutingRule[],
17
- agents: Agent[],
18
- ): string[] {
19
- if (!rules.length) return []
6
+ const TAG = 'chatroom-routing'
7
+ const SELECTOR_TIMEOUT_MS = 4_000
20
8
 
21
- const sorted = [...rules].sort((a, b) => a.priority - b.priority)
22
- const lowerText = text.toLowerCase()
9
+ interface ChatroomRecipientSelection {
10
+ agentIds: string[]
11
+ }
23
12
 
24
- for (const rule of sorted) {
25
- if (rule.type === 'keyword') {
26
- let matched = false
13
+ function normalizeGuidance(value: string | null | undefined): string | null {
14
+ const trimmed = typeof value === 'string' ? value.trim() : ''
15
+ return trimmed || null
16
+ }
27
17
 
28
- // Check keywords (case-insensitive substring)
29
- if (rule.keywords?.length) {
30
- matched = rule.keywords.some((kw) => lowerText.includes(kw.toLowerCase()))
18
+ function extractFirstJsonObject(text: string): string | null {
19
+ const source = String(text || '').trim()
20
+ if (!source) return null
21
+ let start = -1
22
+ let depth = 0
23
+ let inString = false
24
+ let escaped = false
25
+ for (let index = 0; index < source.length; index += 1) {
26
+ const char = source[index]
27
+ if (start === -1) {
28
+ if (char === '{') {
29
+ start = index
30
+ depth = 1
31
31
  }
32
+ continue
33
+ }
34
+ if (inString) {
35
+ if (escaped) escaped = false
36
+ else if (char === '\\') escaped = true
37
+ else if (char === '"') inString = false
38
+ continue
39
+ }
40
+ if (char === '"') {
41
+ inString = true
42
+ continue
43
+ }
44
+ if (char === '{') depth += 1
45
+ else if (char === '}') depth -= 1
46
+ if (depth === 0) return source.slice(start, index + 1)
47
+ }
48
+ return null
49
+ }
32
50
 
33
- // Check pattern (regex)
34
- if (!matched && rule.pattern) {
35
- try {
36
- const re = new RegExp(rule.pattern, 'i')
37
- matched = re.test(text)
38
- } catch {
39
- // Invalid regex — skip
40
- }
41
- }
51
+ function extractModelText(content: unknown): string {
52
+ if (typeof content === 'string') return content
53
+ if (!Array.isArray(content)) return ''
54
+ return content
55
+ .map((part) => (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') ? part.text : '')
56
+ .join('')
57
+ }
42
58
 
43
- if (matched) return [rule.agentId]
59
+ function parseRecipientSelection(text: string, allowedAgentIds: Set<string>): string[] {
60
+ const jsonText = extractFirstJsonObject(text)
61
+ if (!jsonText) return []
62
+ try {
63
+ const parsed = JSON.parse(jsonText) as Partial<ChatroomRecipientSelection>
64
+ if (!Array.isArray(parsed.agentIds)) return []
65
+ const seen = new Set<string>()
66
+ const selected: string[] = []
67
+ for (const value of parsed.agentIds) {
68
+ if (typeof value !== 'string') continue
69
+ const agentId = value.trim()
70
+ if (!agentId || !allowedAgentIds.has(agentId) || seen.has(agentId)) continue
71
+ seen.add(agentId)
72
+ selected.push(agentId)
44
73
  }
74
+ return selected
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ function formatLegacyRule(rule: ChatroomRoutingRule, agentsById: Record<string, Agent | undefined>): string | null {
81
+ const agentName = agentsById[rule.agentId]?.name || rule.agentId
82
+ if (rule.type === 'keyword') {
83
+ const parts = [
84
+ Array.isArray(rule.keywords) && rule.keywords.length > 0
85
+ ? `topics or phrases like ${rule.keywords.map((keyword) => `"${keyword}"`).join(', ')}`
86
+ : null,
87
+ rule.pattern ? `messages matching ${JSON.stringify(rule.pattern)}` : null,
88
+ ].filter(Boolean)
89
+ if (parts.length === 0) return null
90
+ return `Priority ${rule.priority}: route ${parts.join(' or ')} to ${agentName}.`
91
+ }
92
+ if (!rule.pattern) return null
93
+ return `Priority ${rule.priority}: prefer ${agentName} when the request best fits capability area ${JSON.stringify(rule.pattern)}.`
94
+ }
95
+
96
+ export function synthesizeRoutingGuidanceFromRules(
97
+ rules: ChatroomRoutingRule[] | null | undefined,
98
+ agentsById: Record<string, Agent | undefined>,
99
+ ): string | null {
100
+ if (!Array.isArray(rules) || rules.length === 0) return null
101
+ const lines = rules
102
+ .slice()
103
+ .sort((a, b) => a.priority - b.priority)
104
+ .map((rule) => formatLegacyRule(rule, agentsById))
105
+ .filter((line): line is string => typeof line === 'string' && line.trim().length > 0)
106
+ if (lines.length === 0) return null
107
+ return [
108
+ 'Legacy routing guidance synthesized from older routing rules. Earlier priorities take precedence when multiple agents could fit.',
109
+ ...lines,
110
+ ].join('\n')
111
+ }
45
112
 
46
- if (rule.type === 'capability') {
47
- if (!rule.pattern) continue
48
- const patternLower = rule.pattern.toLowerCase()
113
+ export function resolveChatroomRoutingGuidance(
114
+ chatroom: Chatroom,
115
+ agentsById: Record<string, Agent | undefined>,
116
+ ): string | null {
117
+ return normalizeGuidance(chatroom.routingGuidance)
118
+ || synthesizeRoutingGuidanceFromRules(chatroom.routingRules, agentsById)
119
+ }
49
120
 
50
- // Check if the specific agent has a matching capability
51
- const agent = agents.find((a) => a.id === rule.agentId)
52
- if (agent && matchesCapabilities(agent.capabilities, [rule.pattern])) {
53
- // Only match if the message text is relevant to the capability
54
- // Use the pattern as a keyword match against the message text too
55
- try {
56
- const re = new RegExp(rule.pattern, 'i')
57
- if (re.test(text)) return [rule.agentId]
58
- } catch {
59
- if (lowerText.includes(patternLower)) return [rule.agentId]
60
- }
121
+ export function ensureChatroomRoutingGuidance(
122
+ chatroom: Chatroom,
123
+ agentsById: Record<string, Agent | undefined>,
124
+ ): boolean {
125
+ const guidance = resolveChatroomRoutingGuidance(chatroom, agentsById)
126
+ const nextGuidance = normalizeGuidance(guidance)
127
+ const hadRules = Array.isArray(chatroom.routingRules) && chatroom.routingRules.length > 0
128
+ const guidanceChanged = nextGuidance !== normalizeGuidance(chatroom.routingGuidance)
129
+ if (!guidanceChanged && !hadRules) return false
130
+ chatroom.routingGuidance = nextGuidance
131
+ delete chatroom.routingRules
132
+ return guidanceChanged || hadRules
133
+ }
134
+
135
+ function buildRecipientSelectionPrompt(params: {
136
+ text: string
137
+ chatroom: Chatroom
138
+ guidance: string
139
+ members: Array<{
140
+ id: string
141
+ name: string
142
+ description: string
143
+ capabilities: string[]
144
+ }>
145
+ }): string {
146
+ return [
147
+ 'Choose which chatroom members should receive the latest message.',
148
+ 'Return JSON only.',
149
+ 'Use only agent IDs from the provided member list.',
150
+ 'Prefer the smallest relevant set. Return an empty array when no routing guidance clearly applies.',
151
+ 'Respect explicit routing guidance over generic capability overlap.',
152
+ '',
153
+ 'Output shape:',
154
+ '{"agentIds":["agent-id-1","agent-id-2"]}',
155
+ '',
156
+ `Chatroom description: ${JSON.stringify(params.chatroom.description || '')}`,
157
+ `Routing guidance: ${JSON.stringify(params.guidance)}`,
158
+ `Latest message: ${JSON.stringify(params.text)}`,
159
+ 'Members:',
160
+ JSON.stringify(params.members),
161
+ ].join('\n')
162
+ }
163
+
164
+ export async function selectChatroomRecipients(
165
+ params: {
166
+ text: string
167
+ chatroom: Chatroom
168
+ agentsById: Record<string, Agent | undefined>
169
+ },
170
+ hooks?: {
171
+ generateText?: (prompt: string) => Promise<string>
172
+ },
173
+ ): Promise<string[]> {
174
+ const guidance = resolveChatroomRoutingGuidance(params.chatroom, params.agentsById)
175
+ if (!guidance) return []
176
+
177
+ const members = params.chatroom.agentIds
178
+ .map((agentId) => {
179
+ const agent = params.agentsById[agentId]
180
+ if (!agent) return null
181
+ return {
182
+ id: agent.id,
183
+ name: agent.name,
184
+ description: agent.description || '',
185
+ capabilities: Array.isArray(agent.capabilities) ? agent.capabilities.slice(0, 12) : [],
61
186
  }
62
- }
63
- }
187
+ })
188
+ .filter((member): member is NonNullable<typeof member> => member !== null)
189
+ if (members.length === 0) return []
190
+
191
+ const prompt = buildRecipientSelectionPrompt({
192
+ text: params.text,
193
+ chatroom: params.chatroom,
194
+ guidance,
195
+ members,
196
+ })
197
+ const allowedAgentIds = new Set(members.map((member) => member.id))
64
198
 
65
- return []
199
+ try {
200
+ const responseText = await Promise.race([
201
+ hooks?.generateText
202
+ ? hooks.generateText(prompt)
203
+ : (async () => {
204
+ const { llm } = await buildLLM()
205
+ const response = await llm.invoke([new HumanMessage(prompt)])
206
+ return extractModelText(response.content)
207
+ })(),
208
+ new Promise<never>((_, reject) => {
209
+ setTimeout(() => reject(new Error('chatroom-recipient-selector-timeout')), SELECTOR_TIMEOUT_MS)
210
+ }),
211
+ ])
212
+ return parseRecipientSelection(responseText, allowedAgentIds)
213
+ } catch (error: unknown) {
214
+ log.warn(TAG, 'Failed to select chatroom recipients from routing guidance', {
215
+ error: error instanceof Error ? error.message : 'unknown',
216
+ chatroomId: params.chatroom.id,
217
+ })
218
+ return []
219
+ }
66
220
  }