@swarmclawai/swarmclaw 1.2.1 → 1.2.3

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 (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -0,0 +1,296 @@
1
+ import { CONTEXT_OVERFLOW_RE } from '@/lib/providers/error-classification'
2
+ import type { ProviderType } from '@/types'
3
+ import { getEnabledCapabilityIds } from '@/lib/capability-selection'
4
+ import { isLocalOpenClawEndpoint } from '@/lib/openclaw/openclaw-endpoint'
5
+ import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
6
+ import { applyContextClearBoundary } from '@/lib/server/chat-execution/chat-execution-utils'
7
+ import { estimateCost } from '@/lib/server/cost'
8
+ import { log } from '@/lib/server/logger'
9
+ import { runCapabilityHook } from '@/lib/server/native-capabilities'
10
+ import { markProviderFailure } from '@/lib/server/provider-health'
11
+ import {
12
+ getCachedLlmResponse,
13
+ resolveLlmResponseCacheConfig,
14
+ setCachedLlmResponse,
15
+ type LlmResponseCacheKeyInput,
16
+ } from '@/lib/server/llm-response-cache'
17
+ import {
18
+ activeSessionProcesses,
19
+ clearActiveSessionProcess,
20
+ registerActiveSessionProcess,
21
+ } from '@/lib/server/runtime/runtime-state'
22
+ import { perf } from '@/lib/server/runtime/perf'
23
+ import { getSessionMessages } from '@/lib/server/sessions/session-repository'
24
+ import { notify } from '@/lib/server/ws-hub'
25
+ import { errorMessage as toErrorMessage } from '@/lib/shared-utils'
26
+
27
+ import type { ExecuteChatTurnInput } from './chat-execution-types'
28
+ import type { PartialAssistantPersistence } from '@/lib/server/chat-execution/chat-turn-partial-persistence'
29
+ import type { PreparedExecutableChatTurn } from '@/lib/server/chat-execution/chat-turn-preparation'
30
+ import type { ToolRoutingResult } from '@/lib/server/chat-execution/chat-turn-tool-routing'
31
+
32
+ const TAG = 'chat-execution'
33
+
34
+ export interface ExecutedPreparedChatTurn {
35
+ fullResponse: string
36
+ errorMessage?: string
37
+ toolRoutingResult: ToolRoutingResult | null
38
+ responseCacheHit: boolean
39
+ durationMs: number
40
+ directUsage: {
41
+ inputTokens: number
42
+ outputTokens: number
43
+ received: boolean
44
+ }
45
+ }
46
+
47
+ export async function executePreparedChatTurn(params: {
48
+ input: ExecuteChatTurnInput
49
+ prepared: PreparedExecutableChatTurn
50
+ partialPersistence: PartialAssistantPersistence
51
+ preflightToolRoutingResult?: ToolRoutingResult | null
52
+ }): Promise<ExecutedPreparedChatTurn> {
53
+ const { input, prepared, partialPersistence, preflightToolRoutingResult = null } = params
54
+ const {
55
+ sessionId,
56
+ imageUrl,
57
+ attachedFiles,
58
+ internal = false,
59
+ runId,
60
+ source = 'chat',
61
+ signal,
62
+ } = input
63
+ const {
64
+ sessionForRun,
65
+ appSettings,
66
+ lifecycleRunId,
67
+ extensionsForRun,
68
+ effectiveMessage,
69
+ providerType,
70
+ provider,
71
+ apiKey,
72
+ hasExtensions,
73
+ systemPrompt,
74
+ resolvedImagePath,
75
+ heartbeatLightContext,
76
+ isAutoRunNoHistory,
77
+ missionContextBlock,
78
+ } = prepared
79
+
80
+ const emit = partialPersistence.emit
81
+ const parseAndEmit = partialPersistence.parseAndEmit
82
+ let fullResponse = ''
83
+ let errorMessage: string | undefined
84
+
85
+ const directUsage = { inputTokens: 0, outputTokens: 0, received: false }
86
+ const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
87
+ let responseCacheHit = false
88
+ let responseCacheInput: LlmResponseCacheKeyInput | null = null
89
+ let durationMs = 0
90
+ const startTs = Date.now()
91
+ const endLlmPerf = perf.start('chat-execution', 'llm-round-trip', {
92
+ sessionId,
93
+ provider: providerType,
94
+ hasExtensions,
95
+ extensionCount: getEnabledCapabilityIds(sessionForRun).length,
96
+ })
97
+
98
+ if (preflightToolRoutingResult) {
99
+ fullResponse = preflightToolRoutingResult.fullResponse
100
+ errorMessage = preflightToolRoutingResult.errorMessage
101
+ if (fullResponse) emit({ t: 'd', text: fullResponse })
102
+ partialPersistence.stop()
103
+ endLlmPerf({ durationMs: 0, cacheHit: false })
104
+ return {
105
+ fullResponse,
106
+ errorMessage,
107
+ toolRoutingResult: preflightToolRoutingResult,
108
+ responseCacheHit,
109
+ durationMs,
110
+ directUsage,
111
+ }
112
+ }
113
+
114
+ const abortController = new AbortController()
115
+ const abortFromOutside = () => abortController.abort()
116
+ if (signal) {
117
+ if (signal.aborted) abortController.abort()
118
+ else signal.addEventListener('abort', abortFromOutside)
119
+ }
120
+
121
+ registerActiveSessionProcess(sessionId, {
122
+ runId: runId || null,
123
+ source,
124
+ kill: () => abortController.abort(),
125
+ })
126
+
127
+ try {
128
+ const heartbeatHistory = isAutoRunNoHistory
129
+ ? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
130
+ : undefined
131
+
132
+ const useLocalOpenClawNativeRuntime = providerType === 'openclaw' && isLocalOpenClawEndpoint(sessionForRun.apiEndpoint)
133
+ log.info(
134
+ TAG,
135
+ `provider=${providerType}, hasExtensions=${hasExtensions}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, extensions=${getEnabledCapabilityIds(sessionForRun).length}`,
136
+ )
137
+
138
+ if (hasExtensions) {
139
+ const result = await streamAgentChat({
140
+ session: sessionForRun,
141
+ message: effectiveMessage,
142
+ imagePath: resolvedImagePath,
143
+ imageUrl,
144
+ attachedFiles,
145
+ apiKey,
146
+ systemPrompt,
147
+ extraSystemContext: missionContextBlock ? [missionContextBlock] : undefined,
148
+ write: (raw) => parseAndEmit(raw),
149
+ history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
150
+ signal: abortController.signal,
151
+ source,
152
+ })
153
+ fullResponse = result.finalResponse || result.fullText
154
+ } else {
155
+ let directHistorySnapshot = isAutoRunNoHistory
156
+ ? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
157
+ : applyContextClearBoundary(getSessionMessages(sessionId))
158
+ responseCacheInput = {
159
+ provider: providerType,
160
+ model: sessionForRun.model,
161
+ apiEndpoint: sessionForRun.apiEndpoint || '',
162
+ systemPrompt,
163
+ message: effectiveMessage,
164
+ imagePath: input.imagePath,
165
+ imageUrl,
166
+ attachedFiles,
167
+ history: directHistorySnapshot,
168
+ }
169
+ const canUseResponseCache = !internal && responseCacheConfig.enabled
170
+ const cached = canUseResponseCache
171
+ ? getCachedLlmResponse(responseCacheInput, responseCacheConfig)
172
+ : null
173
+ if (cached) {
174
+ responseCacheHit = true
175
+ fullResponse = cached.text
176
+ emit({
177
+ t: 'md',
178
+ text: JSON.stringify({
179
+ cache: {
180
+ hit: true,
181
+ ageMs: cached.ageMs,
182
+ provider: cached.provider,
183
+ model: cached.model,
184
+ },
185
+ }),
186
+ })
187
+ emit({ t: 'd', text: cached.text })
188
+ } else {
189
+ await runCapabilityHook(
190
+ 'llmInput',
191
+ {
192
+ session: sessionForRun,
193
+ runId: lifecycleRunId,
194
+ provider: providerType as ProviderType,
195
+ model: sessionForRun.model,
196
+ systemPrompt,
197
+ prompt: effectiveMessage,
198
+ historyMessages: directHistorySnapshot,
199
+ imagesCount: resolvedImagePath ? 1 : 0,
200
+ },
201
+ { enabledIds: extensionsForRun },
202
+ )
203
+ const doStreamChat = () => provider.handler.streamChat({
204
+ session: sessionForRun,
205
+ message: effectiveMessage,
206
+ imagePath: resolvedImagePath,
207
+ apiKey,
208
+ systemPrompt,
209
+ write: (raw: string) => parseAndEmit(raw),
210
+ active: activeSessionProcesses,
211
+ loadHistory: (sid: string) => {
212
+ if (sid === sessionId) return directHistorySnapshot
213
+ return isAutoRunNoHistory
214
+ ? getSessionMessages(sid).slice(-6)
215
+ : applyContextClearBoundary(getSessionMessages(sid))
216
+ },
217
+ onUsage: (usage) => {
218
+ directUsage.inputTokens = usage.inputTokens
219
+ directUsage.outputTokens = usage.outputTokens
220
+ directUsage.received = true
221
+ },
222
+ signal: abortController.signal,
223
+ })
224
+ try {
225
+ fullResponse = await doStreamChat()
226
+ } catch (streamErr: unknown) {
227
+ const streamErrMsg = toErrorMessage(streamErr)
228
+ const streamStatus = (streamErr as Record<string, unknown>)?.status
229
+ if (typeof streamStatus === 'number' && streamStatus === 400 && CONTEXT_OVERFLOW_RE.test(streamErrMsg)) {
230
+ log.warn('chat-run', 'Context overflow in direct path, reducing history and retrying', {
231
+ sessionId,
232
+ error: streamErrMsg,
233
+ historyLen: directHistorySnapshot.length,
234
+ })
235
+ directHistorySnapshot = directHistorySnapshot.slice(-10)
236
+ fullResponse = await doStreamChat()
237
+ } else {
238
+ throw streamErr
239
+ }
240
+ }
241
+ await runCapabilityHook(
242
+ 'llmOutput',
243
+ {
244
+ session: sessionForRun,
245
+ runId: lifecycleRunId,
246
+ provider: providerType as ProviderType,
247
+ model: sessionForRun.model,
248
+ assistantTexts: fullResponse ? [fullResponse] : [],
249
+ response: fullResponse,
250
+ usage: directUsage.received
251
+ ? {
252
+ input: directUsage.inputTokens,
253
+ output: directUsage.outputTokens,
254
+ total: directUsage.inputTokens + directUsage.outputTokens,
255
+ estimatedCost: estimateCost(sessionForRun.model, directUsage.inputTokens, directUsage.outputTokens),
256
+ }
257
+ : undefined,
258
+ },
259
+ { enabledIds: extensionsForRun },
260
+ )
261
+ if (canUseResponseCache && responseCacheInput && fullResponse) {
262
+ setCachedLlmResponse(responseCacheInput, fullResponse, responseCacheConfig)
263
+ }
264
+ }
265
+ }
266
+ durationMs = Date.now() - startTs
267
+ endLlmPerf({ durationMs, cacheHit: responseCacheHit })
268
+ } catch (err: unknown) {
269
+ endLlmPerf({ error: true })
270
+ errorMessage = toErrorMessage(err)
271
+ const failureText = errorMessage || 'Run failed.'
272
+ markProviderFailure(providerType, failureText, sessionForRun.credentialId)
273
+ emit({ t: 'err', text: failureText })
274
+ log.error('chat-run', `Run failed for session ${sessionId}`, {
275
+ runId,
276
+ source,
277
+ internal,
278
+ error: failureText,
279
+ stack: err instanceof Error ? err.stack?.split('\n').slice(0, 6).join('\n') : undefined,
280
+ })
281
+ } finally {
282
+ partialPersistence.stop()
283
+ clearActiveSessionProcess(sessionId)
284
+ notify('sessions')
285
+ if (signal) signal.removeEventListener('abort', abortFromOutside)
286
+ }
287
+
288
+ return {
289
+ fullResponse,
290
+ errorMessage,
291
+ toolRoutingResult: null,
292
+ responseCacheHit,
293
+ durationMs,
294
+ directUsage,
295
+ }
296
+ }
@@ -12,7 +12,7 @@
12
12
  import path from 'node:path'
13
13
  import type { StructuredToolInterface } from '@langchain/core/tools'
14
14
  import type { AppSettings, MessageToolEvent, SSEEvent } from '@/types'
15
- import { loadAgents } from '@/lib/server/storage'
15
+ import { getAgent } from '@/lib/server/agents/agent-repository'
16
16
  import { buildSessionTools } from '@/lib/server/session-tools'
17
17
  import { resolveConcreteToolPolicyBlock, type ExtensionPolicyDecision } from '@/lib/server/tool-capability-policy'
18
18
  import { resolveActiveProjectContext } from '@/lib/server/project-context'
@@ -350,7 +350,7 @@ export function resolveRequestedToolPreflightResponse(params: {
350
350
  const requestedToolNames = requestedToolNamesFromMessage(params.message)
351
351
  if (requestedToolNames.length === 0) return null
352
352
 
353
- const agent = params.session?.agentId ? loadAgents()[params.session.agentId] : null
353
+ const agent = params.session?.agentId ? getAgent(params.session.agentId) : null
354
354
  const blockedResponses: string[] = []
355
355
  const unavailableResponses: string[] = []
356
356
  for (const toolName of requestedToolNames) {
@@ -361,8 +361,8 @@ export function resolveRequestedToolPreflightResponse(params: {
361
361
  }
362
362
  if (
363
363
  (toolName === 'delegate' || toolName.startsWith('delegate_to_'))
364
- && agent
365
- && agent.delegationEnabled !== true
364
+ && params.session?.agentId
365
+ && agent?.delegationEnabled !== true
366
366
  ) {
367
367
  unavailableResponses.push(buildToolUnavailableResponse(toolName, 'delegation is not enabled for this agent right now'))
368
368
  continue
@@ -407,7 +407,7 @@ async function invokeSessionTool(
407
407
  return { invoked: false, responseOverride: null }
408
408
  }
409
409
 
410
- const agent = ctx.session.agentId ? loadAgents()[ctx.session.agentId] : null
410
+ const agent = ctx.session.agentId ? getAgent(ctx.session.agentId) : null
411
411
  const agentRecord = agent as Record<string, unknown> | null
412
412
  const activeProjectContext = resolveActiveProjectContext(ctx.session as unknown as { agentId?: string | null; cwd?: string | null; projectId?: string | null })
413
413
  const { tools, cleanup } = await buildSessionTools(ctx.session.cwd, ctx.enabledExtensions, {
@@ -0,0 +1,329 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it, before, after } from 'node:test'
3
+
4
+ const originalEnv = {
5
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
6
+ }
7
+
8
+ let mod: typeof import('@/lib/server/chat-execution/message-classifier')
9
+
10
+ before(async () => {
11
+ process.env.SWARMCLAW_BUILD_MODE = '1'
12
+ mod = await import('@/lib/server/chat-execution/message-classifier')
13
+ })
14
+
15
+ after(() => {
16
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
17
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
18
+ })
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // parseClassificationResponse
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('parseClassificationResponse', () => {
25
+ const validJson = JSON.stringify({
26
+ isDeliverableTask: true,
27
+ isBroadGoal: false,
28
+ walletIntent: 'none',
29
+ hasHumanSignals: false,
30
+ hasSignificantEvent: false,
31
+ isResearchSynthesis: false,
32
+ explicitToolRequests: [],
33
+ confidence: 0.9,
34
+ })
35
+
36
+ it('parses valid JSON with all schema fields', () => {
37
+ const result = mod.parseClassificationResponse(validJson)
38
+ assert.ok(result)
39
+ assert.equal(result!.isDeliverableTask, true)
40
+ assert.equal(result!.isBroadGoal, false)
41
+ assert.equal(result!.walletIntent, 'none')
42
+ assert.equal(result!.confidence, 0.9)
43
+ assert.deepEqual(result!.explicitToolRequests, [])
44
+ })
45
+
46
+ it('returns null for malformed JSON', () => {
47
+ assert.equal(mod.parseClassificationResponse('not json at all'), null)
48
+ assert.equal(mod.parseClassificationResponse('{broken'), null)
49
+ })
50
+
51
+ it('returns null for JSON missing required keys', () => {
52
+ const partial = JSON.stringify({ isDeliverableTask: true })
53
+ assert.equal(mod.parseClassificationResponse(partial), null)
54
+ })
55
+
56
+ it('tolerates extra keys in JSON', () => {
57
+ const withExtra = JSON.stringify({
58
+ isDeliverableTask: true,
59
+ isBroadGoal: false,
60
+ walletIntent: 'none',
61
+ hasHumanSignals: false,
62
+ hasSignificantEvent: false,
63
+ isResearchSynthesis: false,
64
+ explicitToolRequests: ['shell'],
65
+ confidence: 0.85,
66
+ extraKey: 'should be ignored',
67
+ })
68
+ const result = mod.parseClassificationResponse(withExtra)
69
+ assert.ok(result)
70
+ assert.equal(result!.isDeliverableTask, true)
71
+ })
72
+
73
+ it('extracts embedded JSON from prose text', () => {
74
+ const prose = `Here is my classification:\n${validJson}\nEnd of classification.`
75
+ const result = mod.parseClassificationResponse(prose)
76
+ assert.ok(result)
77
+ assert.equal(result!.isDeliverableTask, true)
78
+ })
79
+
80
+ it('returns null for empty text', () => {
81
+ assert.equal(mod.parseClassificationResponse(''), null)
82
+ assert.equal(mod.parseClassificationResponse(' '), null)
83
+ })
84
+ })
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // isDeliverableTask
88
+ // ---------------------------------------------------------------------------
89
+
90
+ describe('isDeliverableTask', () => {
91
+ it('uses classification value when provided', () => {
92
+ const cls = makeClassification({ isDeliverableTask: true })
93
+ assert.equal(mod.isDeliverableTask(cls, 'anything'), true)
94
+
95
+ const cls2 = makeClassification({ isDeliverableTask: false })
96
+ assert.equal(mod.isDeliverableTask(cls2, 'build me a landing page'), false)
97
+ })
98
+
99
+ it('falls back to regex when classification is null', () => {
100
+ // A message that looks like a deliverable task
101
+ const result = mod.isDeliverableTask(null, 'Create a detailed marketing report with competitor analysis and market sizing. Include charts and recommendations for Q3 strategy across all regions.')
102
+ assert.equal(typeof result, 'boolean')
103
+ })
104
+ })
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // isBroadGoal
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe('isBroadGoal', () => {
111
+ it('uses classification value when provided', () => {
112
+ assert.equal(mod.isBroadGoal(makeClassification({ isBroadGoal: true }), ''), true)
113
+ assert.equal(mod.isBroadGoal(makeClassification({ isBroadGoal: false }), ''), false)
114
+ })
115
+
116
+ it('falls back to regex when classification is null', () => {
117
+ const result = mod.isBroadGoal(null, 'I want to build a complete e-commerce platform with user authentication, product catalog, shopping cart, and payment processing')
118
+ assert.equal(typeof result, 'boolean')
119
+ })
120
+ })
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // hasWalletIntent
124
+ // ---------------------------------------------------------------------------
125
+
126
+ describe('hasWalletIntent', () => {
127
+ it('walletIntent none returns false', () => {
128
+ assert.equal(mod.hasWalletIntent(makeClassification({ walletIntent: 'none' }), ''), false)
129
+ })
130
+
131
+ it('walletIntent read_only returns true', () => {
132
+ assert.equal(mod.hasWalletIntent(makeClassification({ walletIntent: 'read_only' }), ''), true)
133
+ })
134
+
135
+ it('walletIntent transactional returns true', () => {
136
+ assert.equal(mod.hasWalletIntent(makeClassification({ walletIntent: 'transactional' }), ''), true)
137
+ })
138
+
139
+ it('falls back to regex when classification is null', () => {
140
+ const result = mod.hasWalletIntent(null, 'check my wallet balance')
141
+ assert.equal(typeof result, 'boolean')
142
+ })
143
+ })
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // hasTransactionalWalletIntent
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe('hasTransactionalWalletIntent', () => {
150
+ it('only transactional returns true', () => {
151
+ assert.equal(mod.hasTransactionalWalletIntent(makeClassification({ walletIntent: 'transactional' }), ''), true)
152
+ assert.equal(mod.hasTransactionalWalletIntent(makeClassification({ walletIntent: 'read_only' }), ''), false)
153
+ assert.equal(mod.hasTransactionalWalletIntent(makeClassification({ walletIntent: 'none' }), ''), false)
154
+ })
155
+
156
+ it('falls back to regex when classification is null', () => {
157
+ const result = mod.hasTransactionalWalletIntent(null, 'swap 1 ETH for USDC')
158
+ assert.equal(typeof result, 'boolean')
159
+ })
160
+ })
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // hasHumanSignals
164
+ // ---------------------------------------------------------------------------
165
+
166
+ describe('hasHumanSignals', () => {
167
+ it('uses classification value when provided', () => {
168
+ assert.equal(mod.hasHumanSignals(makeClassification({ hasHumanSignals: true }), ''), true)
169
+ assert.equal(mod.hasHumanSignals(makeClassification({ hasHumanSignals: false }), ''), false)
170
+ })
171
+
172
+ it('regex detects personal text', () => {
173
+ assert.equal(mod.hasHumanSignals(null, 'my birthday is next week'), true)
174
+ })
175
+
176
+ it('regex returns false for task-only text', () => {
177
+ assert.equal(mod.hasHumanSignals(null, 'deploy the app'), false)
178
+ })
179
+ })
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // hasSignificantEvent
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe('hasSignificantEvent', () => {
186
+ it('uses classification value when provided', () => {
187
+ assert.equal(mod.hasSignificantEvent(makeClassification({ hasSignificantEvent: true }), ''), true)
188
+ assert.equal(mod.hasSignificantEvent(makeClassification({ hasSignificantEvent: false }), ''), false)
189
+ })
190
+
191
+ it('regex detects significant events', () => {
192
+ assert.equal(mod.hasSignificantEvent(null, 'I just got promoted at work'), true)
193
+ assert.equal(mod.hasSignificantEvent(null, 'my graduation ceremony is on Friday'), true)
194
+ })
195
+
196
+ it('regex returns false for non-event text', () => {
197
+ assert.equal(mod.hasSignificantEvent(null, 'fix the login bug'), false)
198
+ })
199
+ })
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // isResearchSynthesis
203
+ // ---------------------------------------------------------------------------
204
+
205
+ describe('isResearchSynthesis', () => {
206
+ it('uses classification value when provided', () => {
207
+ assert.equal(mod.isResearchSynthesis(makeClassification({ isResearchSynthesis: true }), null), true)
208
+ assert.equal(mod.isResearchSynthesis(makeClassification({ isResearchSynthesis: false }), null), false)
209
+ })
210
+
211
+ it('falls back to routingIntent when classification is null', () => {
212
+ assert.equal(mod.isResearchSynthesis(null, 'research'), true)
213
+ assert.equal(mod.isResearchSynthesis(null, 'browsing'), true)
214
+ assert.equal(mod.isResearchSynthesis(null, 'coding'), false)
215
+ assert.equal(mod.isResearchSynthesis(null, null), false)
216
+ })
217
+ })
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // classifyMessage — with generateText override
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe('classifyMessage', () => {
224
+ it('returns valid classification from mock generateText', async () => {
225
+ const mockResponse = JSON.stringify({
226
+ isDeliverableTask: true,
227
+ isBroadGoal: false,
228
+ walletIntent: 'none',
229
+ hasHumanSignals: false,
230
+ hasSignificantEvent: false,
231
+ isResearchSynthesis: false,
232
+ explicitToolRequests: ['shell'],
233
+ confidence: 0.95,
234
+ })
235
+
236
+ const result = await mod.classifyMessage(
237
+ { sessionId: 'test-session', message: 'Build me a dashboard' },
238
+ { generateText: async () => mockResponse },
239
+ )
240
+
241
+ assert.ok(result)
242
+ assert.equal(result!.isDeliverableTask, true)
243
+ assert.equal(result!.walletIntent, 'none')
244
+ assert.deepEqual(result!.explicitToolRequests, ['shell'])
245
+ })
246
+
247
+ it('returns null for empty message', async () => {
248
+ const result = await mod.classifyMessage(
249
+ { sessionId: 'test-session', message: '' },
250
+ { generateText: async () => '{}' },
251
+ )
252
+ assert.equal(result, null)
253
+ })
254
+
255
+ it('returns null for whitespace-only message', async () => {
256
+ const result = await mod.classifyMessage(
257
+ { sessionId: 'test-session', message: ' ' },
258
+ { generateText: async () => '{}' },
259
+ )
260
+ assert.equal(result, null)
261
+ })
262
+
263
+ it('returns null when generateText times out', async () => {
264
+ const result = await mod.classifyMessage(
265
+ { sessionId: 'test-session', message: 'A message that will timeout for classification purposes' },
266
+ {
267
+ generateText: () => new Promise((resolve) => {
268
+ // Never resolves within 2s timeout
269
+ setTimeout(() => resolve('{}'), 10_000)
270
+ }),
271
+ },
272
+ )
273
+ assert.equal(result, null)
274
+ })
275
+
276
+ it('caches results for the same message', async () => {
277
+ let callCount = 0
278
+ const mockResponse = JSON.stringify({
279
+ isDeliverableTask: false,
280
+ isBroadGoal: false,
281
+ walletIntent: 'none',
282
+ hasHumanSignals: false,
283
+ hasSignificantEvent: false,
284
+ isResearchSynthesis: false,
285
+ explicitToolRequests: [],
286
+ confidence: 0.8,
287
+ })
288
+
289
+ const generateText = async () => {
290
+ callCount++
291
+ return mockResponse
292
+ }
293
+
294
+ // Use a unique message to avoid cache from other tests
295
+ const uniqueMsg = `cache-test-${Date.now()}-${Math.random()}`
296
+
297
+ const first = await mod.classifyMessage(
298
+ { sessionId: 'test-session', message: uniqueMsg },
299
+ { generateText },
300
+ )
301
+ const second = await mod.classifyMessage(
302
+ { sessionId: 'test-session', message: uniqueMsg },
303
+ { generateText },
304
+ )
305
+
306
+ assert.ok(first)
307
+ assert.ok(second)
308
+ assert.deepEqual(first, second)
309
+ assert.equal(callCount, 1, 'generateText should only be called once due to cache')
310
+ })
311
+ })
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Helper
315
+ // ---------------------------------------------------------------------------
316
+
317
+ function makeClassification(overrides: Partial<import('@/lib/server/chat-execution/message-classifier').MessageClassification>): import('@/lib/server/chat-execution/message-classifier').MessageClassification {
318
+ return {
319
+ isDeliverableTask: false,
320
+ isBroadGoal: false,
321
+ walletIntent: 'none',
322
+ hasHumanSignals: false,
323
+ hasSignificantEvent: false,
324
+ isResearchSynthesis: false,
325
+ explicitToolRequests: [],
326
+ confidence: 0.9,
327
+ ...overrides,
328
+ }
329
+ }
@@ -13,7 +13,7 @@ const TAG = 'post-stream'
13
13
  import { extractSuggestions } from '@/lib/server/suggestions'
14
14
  import type { StructuredToolInterface } from '@langchain/core/tools'
15
15
  import { estimateCost, buildExtensionDefinitionCosts } from '@/lib/server/cost'
16
- import { appendUsage } from '@/lib/server/storage'
16
+ import { appendUsage } from '@/lib/server/usage/usage-repository'
17
17
  import { runCapabilityHook } from '@/lib/server/native-capabilities'
18
18
  import {
19
19
  shouldForceExternalServiceSummary,