@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.
- package/README.md +16 -85
- package/bin/server-cmd.js +64 -1
- package/package.json +2 -2
- package/skills/coding-agent/SKILL.md +111 -0
- package/skills/github/SKILL.md +140 -0
- package/skills/nano-banana-pro/SKILL.md +62 -0
- package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
- package/skills/nano-pdf/SKILL.md +53 -0
- package/skills/openai-image-gen/SKILL.md +78 -0
- package/skills/openai-image-gen/scripts/gen.py +328 -0
- package/skills/resourceful-problem-solving/SKILL.md +49 -0
- package/skills/skill-creator/SKILL.md +147 -0
- package/skills/skill-creator/scripts/init_skill.py +378 -0
- package/skills/skill-creator/scripts/quick_validate.py +159 -0
- package/skills/summarize/SKILL.md +77 -0
- package/src/app/api/auth/route.ts +20 -5
- package/src/app/api/chats/[id]/devserver/route.ts +13 -19
- package/src/app/api/chats/[id]/messages/route.ts +13 -15
- package/src/app/api/chats/[id]/route.ts +9 -10
- package/src/app/api/chats/[id]/stop/route.ts +5 -7
- package/src/app/api/chats/messages-route.test.ts +8 -6
- package/src/app/api/chats/route.ts +9 -10
- package/src/app/api/ip/route.ts +2 -2
- package/src/app/api/preview-server/route.ts +1 -1
- package/src/app/api/projects/[id]/route.ts +7 -46
- package/src/cli/server-cmd.test.js +74 -0
- package/src/components/chat/chat-area.tsx +45 -23
- package/src/components/chat/message-bubble.test.ts +35 -0
- package/src/components/chat/message-bubble.tsx +19 -9
- package/src/components/chat/message-list.tsx +37 -3
- package/src/components/input/chat-input.tsx +34 -14
- package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
- package/src/instrumentation.ts +1 -1
- package/src/lib/chat/assistant-render-id.ts +3 -0
- package/src/lib/chat/chat-streaming-state.test.ts +42 -3
- package/src/lib/chat/chat-streaming-state.ts +20 -8
- package/src/lib/chat/queued-message-queue.test.ts +23 -1
- package/src/lib/chat/queued-message-queue.ts +11 -2
- package/src/lib/providers/cli-utils.test.ts +124 -0
- package/src/lib/server/activity/activity-log.ts +21 -0
- package/src/lib/server/agents/agent-availability.test.ts +10 -5
- package/src/lib/server/agents/agent-cascade.ts +79 -59
- package/src/lib/server/agents/agent-registry.ts +3 -1
- package/src/lib/server/agents/agent-repository.ts +90 -0
- package/src/lib/server/agents/delegation-job-repository.ts +53 -0
- package/src/lib/server/agents/delegation-jobs.ts +11 -4
- package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
- package/src/lib/server/agents/guardian.ts +2 -2
- package/src/lib/server/agents/main-agent-loop.ts +10 -3
- package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
- package/src/lib/server/agents/subagent-runtime.ts +9 -6
- package/src/lib/server/agents/subagent-swarm.ts +3 -2
- package/src/lib/server/agents/task-session.ts +3 -4
- package/src/lib/server/approvals/approval-repository.ts +30 -0
- package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
- package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
- package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
- package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
- package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
- package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
- package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
- package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
- package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
- package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
- package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
- package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
- package/src/lib/server/connectors/connector-repository.ts +58 -0
- package/src/lib/server/connectors/runtime-state.test.ts +117 -0
- package/src/lib/server/credentials/credential-repository.ts +7 -0
- package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
- package/src/lib/server/memory/memory-abstract.test.ts +59 -0
- package/src/lib/server/missions/mission-repository.ts +74 -0
- package/src/lib/server/missions/mission-service/actions.ts +6 -0
- package/src/lib/server/missions/mission-service/bindings.ts +9 -0
- package/src/lib/server/missions/mission-service/context.ts +4 -0
- package/src/lib/server/missions/mission-service/core.ts +2269 -0
- package/src/lib/server/missions/mission-service/queries.ts +12 -0
- package/src/lib/server/missions/mission-service/recovery.ts +5 -0
- package/src/lib/server/missions/mission-service/ticks.ts +9 -0
- package/src/lib/server/missions/mission-service.test.ts +9 -2
- package/src/lib/server/missions/mission-service.ts +6 -2266
- package/src/lib/server/openclaw/deploy.test.ts +42 -3
- package/src/lib/server/openclaw/deploy.ts +26 -12
- package/src/lib/server/persistence/repository-utils.ts +154 -0
- package/src/lib/server/persistence/storage-context.ts +51 -0
- package/src/lib/server/persistence/transaction.ts +1 -0
- package/src/lib/server/projects/project-repository.ts +36 -0
- package/src/lib/server/projects/project-service.ts +79 -0
- package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
- package/src/lib/server/runtime/alert-dispatch.ts +1 -1
- package/src/lib/server/runtime/daemon-policy.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
- package/src/lib/server/runtime/daemon-state/health.ts +6 -0
- package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
- package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
- package/src/lib/server/runtime/daemon-state.test.ts +48 -0
- package/src/lib/server/runtime/daemon-state.ts +3 -1470
- package/src/lib/server/runtime/estop-repository.ts +4 -0
- package/src/lib/server/runtime/estop.ts +3 -1
- package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +55 -34
- package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
- package/src/lib/server/runtime/idle-window.ts +2 -2
- package/src/lib/server/runtime/network.ts +11 -0
- package/src/lib/server/runtime/orchestrator-events.ts +2 -2
- package/src/lib/server/runtime/queue/claims.ts +4 -0
- package/src/lib/server/runtime/queue/core.ts +2079 -0
- package/src/lib/server/runtime/queue/execution.ts +7 -0
- package/src/lib/server/runtime/queue/followups.ts +4 -0
- package/src/lib/server/runtime/queue/queries.ts +12 -0
- package/src/lib/server/runtime/queue/recovery.ts +7 -0
- package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
- package/src/lib/server/runtime/queue-repository.ts +17 -0
- package/src/lib/server/runtime/queue.ts +5 -2061
- package/src/lib/server/runtime/run-ledger.ts +6 -5
- package/src/lib/server/runtime/run-repository.ts +73 -0
- package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
- package/src/lib/server/runtime/runtime-settings.ts +1 -1
- package/src/lib/server/runtime/runtime-state.ts +99 -0
- package/src/lib/server/runtime/scheduler.ts +4 -2
- package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
- package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
- package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
- package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
- package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
- package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
- package/src/lib/server/runtime/session-run-manager.ts +72 -1377
- package/src/lib/server/runtime/watch-job-repository.ts +35 -0
- package/src/lib/server/runtime/watch-jobs.ts +3 -1
- package/src/lib/server/schedules/schedule-repository.ts +42 -0
- package/src/lib/server/sessions/session-repository.ts +85 -0
- package/src/lib/server/settings/settings-repository.ts +25 -0
- package/src/lib/server/skills/skill-discovery.test.ts +2 -2
- package/src/lib/server/skills/skill-discovery.ts +2 -2
- package/src/lib/server/skills/skill-repository.ts +14 -0
- package/src/lib/server/storage.ts +13 -24
- package/src/lib/server/tasks/task-repository.ts +54 -0
- package/src/lib/server/usage/usage-repository.ts +30 -0
- package/src/lib/server/webhooks/webhook-repository.ts +10 -0
- package/src/lib/strip-internal-metadata.test.ts +42 -41
- package/src/stores/use-chat-store.test.ts +54 -0
- package/src/stores/use-chat-store.ts +21 -5
- /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 {
|
|
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 ?
|
|
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
|
-
&&
|
|
365
|
-
&& agent
|
|
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 ?
|
|
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/
|
|
16
|
+
import { appendUsage } from '@/lib/server/usage/usage-repository'
|
|
17
17
|
import { runCapabilityHook } from '@/lib/server/native-capabilities'
|
|
18
18
|
import {
|
|
19
19
|
shouldForceExternalServiceSummary,
|