@swarmclawai/swarmclaw 0.7.2 → 0.7.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 +81 -22
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +36 -7
- package/src/app/api/agents/route.ts +12 -1
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +18 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/route.ts +16 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +4 -0
- package/src/cli/index.ts +3 -10
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +207 -16
- package/src/components/agents/inspector-panel.tsx +108 -48
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/chat/chat-area.tsx +29 -13
- package/src/components/chat/chat-card.tsx +4 -20
- package/src/components/chat/chat-header.tsx +255 -353
- package/src/components/chat/chat-list.tsx +7 -9
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +3 -1
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/layout/app-layout.tsx +383 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +245 -46
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +250 -61
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +45 -5
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +946 -110
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +59 -1
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +13 -39
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +27 -967
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +17 -6
- package/src/lib/server/orchestrator.ts +2 -2
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +822 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/queue.ts +3 -20
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +70 -32
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery.ts +22 -4
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +237 -24
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +56 -1
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +150 -7
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +86 -23
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +36 -3
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/subagent.ts +193 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +896 -100
- package/src/lib/server/storage.ts +226 -7
- package/src/lib/server/stream-agent-chat.ts +46 -21
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +44 -7
- package/src/lib/server/tool-capability-policy.ts +6 -0
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +7 -0
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +0 -6
- package/src/stores/use-chat-store.ts +31 -2
- package/src/types/index.ts +287 -44
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
3
4
|
import {
|
|
4
5
|
loadSessions,
|
|
5
6
|
saveSessions,
|
|
@@ -38,7 +39,12 @@ import {
|
|
|
38
39
|
} from './llm-response-cache'
|
|
39
40
|
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
40
41
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
42
|
+
import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
|
|
41
43
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
44
|
+
import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
|
|
45
|
+
import { syncSessionArchiveMemory } from './session-archive-memory'
|
|
46
|
+
import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
|
|
47
|
+
import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
|
|
42
48
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
43
49
|
|
|
44
50
|
/** Slice history from the most recent context-clear marker forward */
|
|
@@ -92,6 +98,10 @@ export interface ExecuteChatTurnResult {
|
|
|
92
98
|
estimatedCost?: number
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
export function shouldApplySessionFreshnessReset(source: string): boolean {
|
|
102
|
+
return source !== 'eval'
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
function extractEventJson(line: string): SSEEvent | null {
|
|
96
106
|
if (!line.startsWith('data: ')) return null
|
|
97
107
|
try {
|
|
@@ -101,8 +111,17 @@ function extractEventJson(line: string): SSEEvent | null {
|
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
114
|
+
export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
105
115
|
if (ev.t === 'tool_call') {
|
|
116
|
+
const previous = bag[bag.length - 1]
|
|
117
|
+
if (
|
|
118
|
+
previous
|
|
119
|
+
&& previous.name === (ev.toolName || 'unknown')
|
|
120
|
+
&& previous.input === (ev.toolInput || '')
|
|
121
|
+
&& !previous.output
|
|
122
|
+
) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
106
125
|
bag.push({
|
|
107
126
|
name: ev.toolName || 'unknown',
|
|
108
127
|
input: ev.toolInput || '',
|
|
@@ -125,6 +144,96 @@ function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
125
144
|
}
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
148
|
+
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
149
|
+
left.name === right.name
|
|
150
|
+
&& left.input === right.input
|
|
151
|
+
&& (left.output || '') === (right.output || '')
|
|
152
|
+
&& (left.error === true) === (right.error === true)
|
|
153
|
+
)
|
|
154
|
+
const sameBlock = (startA: number, startB: number, size: number): boolean => {
|
|
155
|
+
for (let offset = 0; offset < size; offset += 1) {
|
|
156
|
+
if (!sameEvent(events[startA + offset], events[startB + offset])) return false
|
|
157
|
+
}
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const deduped: MessageToolEvent[] = []
|
|
162
|
+
for (let index = 0; index < events.length;) {
|
|
163
|
+
const remaining = events.length - index
|
|
164
|
+
let collapsed = false
|
|
165
|
+
for (let blockSize = Math.floor(remaining / 2); blockSize >= 1; blockSize -= 1) {
|
|
166
|
+
if (!sameBlock(index, index + blockSize, blockSize)) continue
|
|
167
|
+
for (let offset = 0; offset < blockSize; offset += 1) deduped.push(events[index + offset])
|
|
168
|
+
const blockStart = index
|
|
169
|
+
index += blockSize
|
|
170
|
+
while (index + blockSize <= events.length && sameBlock(blockStart, index, blockSize)) {
|
|
171
|
+
index += blockSize
|
|
172
|
+
}
|
|
173
|
+
collapsed = true
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
if (collapsed) continue
|
|
177
|
+
deduped.push(events[index])
|
|
178
|
+
index += 1
|
|
179
|
+
}
|
|
180
|
+
return deduped
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractDelegateResponse(outputText: string): string | null {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
186
|
+
if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
|
|
187
|
+
if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
|
|
188
|
+
return null
|
|
189
|
+
} catch {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
|
|
195
|
+
return text.replace(/sandbox:\/workspace\/([^\s)"'\]`]+)/g, (raw, relativePath: string) => {
|
|
196
|
+
const normalized = String(relativePath || '').replace(/^\/+/, '')
|
|
197
|
+
if (!normalized) return raw
|
|
198
|
+
const resolvedCwd = path.resolve(cwd)
|
|
199
|
+
const resolved = path.resolve(resolvedCwd, normalized)
|
|
200
|
+
if (!resolved.startsWith(resolvedCwd)) return raw
|
|
201
|
+
if (!fs.existsSync(resolved)) return raw
|
|
202
|
+
return `/api/files/serve?path=${encodeURIComponent(resolved)}`
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeAbsoluteFileMarkdownLinks(text: string): string {
|
|
207
|
+
return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (raw, label: string, target: string) => {
|
|
208
|
+
if (!path.isAbsolute(target)) return raw
|
|
209
|
+
const resolved = path.resolve(target)
|
|
210
|
+
if (!fs.existsSync(resolved)) return raw
|
|
211
|
+
return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function normalizeAssistantArtifactLinks(text: string, cwd: string): string {
|
|
216
|
+
const uploadsNormalized = text.replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
|
|
217
|
+
const workspaceNormalized = normalizeWorkspaceSandboxLinks(uploadsNormalized, cwd)
|
|
218
|
+
return normalizeAbsoluteFileMarkdownLinks(workspaceNormalized)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractHeartbeatStatus(text: string): { goal?: string; status?: string; summary?: string; nextAction?: string } | null {
|
|
222
|
+
const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
|
|
223
|
+
if (!match) return null
|
|
224
|
+
try {
|
|
225
|
+
const meta = JSON.parse(match[1]) as Record<string, unknown>
|
|
226
|
+
const payload: { goal?: string; status?: string; summary?: string; nextAction?: string } = {}
|
|
227
|
+
if (typeof meta.goal === 'string' && meta.goal.trim()) payload.goal = meta.goal.trim()
|
|
228
|
+
if (typeof meta.status === 'string' && meta.status.trim()) payload.status = meta.status.trim()
|
|
229
|
+
if (typeof meta.summary === 'string' && meta.summary.trim()) payload.summary = meta.summary.trim()
|
|
230
|
+
if (typeof meta.next_action === 'string' && meta.next_action.trim()) payload.nextAction = meta.next_action.trim()
|
|
231
|
+
return Object.keys(payload).length > 0 ? payload : null
|
|
232
|
+
} catch {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
128
237
|
function shouldReplaceRecentAssistantMessage(params: {
|
|
129
238
|
previous: Message | null | undefined
|
|
130
239
|
nextToolEvents: MessageToolEvent[]
|
|
@@ -140,7 +249,11 @@ function shouldReplaceRecentAssistantMessage(params: {
|
|
|
140
249
|
return prevTools === 0
|
|
141
250
|
}
|
|
142
251
|
|
|
143
|
-
function
|
|
252
|
+
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
253
|
+
return pruneStreamingAssistantArtifacts(messages)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function requestedToolNamesFromMessage(message: string): string[] {
|
|
144
257
|
const lower = message.toLowerCase()
|
|
145
258
|
const candidates = [
|
|
146
259
|
'delegate_to_claude_code',
|
|
@@ -179,15 +292,24 @@ function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
179
292
|
'sandbox_list_runtimes',
|
|
180
293
|
'git',
|
|
181
294
|
'canvas',
|
|
182
|
-
'delegate',
|
|
183
295
|
'schedule_wake',
|
|
184
296
|
'spawn_subagent',
|
|
297
|
+
'mailbox',
|
|
298
|
+
'ask_human',
|
|
299
|
+
'document',
|
|
300
|
+
'extract',
|
|
301
|
+
'table',
|
|
302
|
+
'crawl',
|
|
185
303
|
'context_status',
|
|
186
304
|
'context_summarize',
|
|
187
305
|
'openclaw_nodes',
|
|
188
306
|
'openclaw_workspace',
|
|
189
307
|
]
|
|
190
|
-
|
|
308
|
+
const requested = candidates.filter((name) => lower.includes(name.toLowerCase()))
|
|
309
|
+
if (/(^|[\s(])`delegate`([\s).,!?]|$)|\bdelegate tool\b|\buse delegate\b/.test(lower)) {
|
|
310
|
+
requested.push('delegate')
|
|
311
|
+
}
|
|
312
|
+
return Array.from(new Set(requested))
|
|
191
313
|
}
|
|
192
314
|
|
|
193
315
|
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
@@ -398,10 +520,16 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
398
520
|
if (!agent) return
|
|
399
521
|
|
|
400
522
|
let changed = false
|
|
401
|
-
if (
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
523
|
+
if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
|
|
524
|
+
if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
|
|
525
|
+
session.model = agent.model
|
|
526
|
+
changed = true
|
|
527
|
+
}
|
|
528
|
+
if (session.credentialId === undefined && agent.credentialId !== undefined) {
|
|
529
|
+
session.credentialId = agent.credentialId ?? null
|
|
530
|
+
changed = true
|
|
531
|
+
}
|
|
532
|
+
if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
|
|
405
533
|
const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
|
|
406
534
|
if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
|
|
407
535
|
}
|
|
@@ -409,6 +537,11 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
409
537
|
session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
|
|
410
538
|
changed = true
|
|
411
539
|
}
|
|
540
|
+
const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
|
|
541
|
+
if (isShortcutChat) {
|
|
542
|
+
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
543
|
+
if (session.name !== agent.name) { session.name = agent.name; changed = true }
|
|
544
|
+
}
|
|
412
545
|
|
|
413
546
|
if (changed) {
|
|
414
547
|
sessions[sessionId] = session
|
|
@@ -435,6 +568,8 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
435
568
|
if (agent.description) identityLines.push(`Description: ${agent.description}`)
|
|
436
569
|
identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
|
|
437
570
|
parts.push(identityLines.join('\n'))
|
|
571
|
+
const continuityBlock = buildIdentityContinuityContext(session, agent)
|
|
572
|
+
if (continuityBlock) parts.push(continuityBlock)
|
|
438
573
|
|
|
439
574
|
// 2. Runtime & Capabilities (OpenClaw Style)
|
|
440
575
|
const runtimeLines = [
|
|
@@ -553,20 +688,50 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
553
688
|
const sessions = loadSessions()
|
|
554
689
|
const session = sessions[sessionId]
|
|
555
690
|
if (!session) throw new Error(`Session not found: ${sessionId}`)
|
|
691
|
+
session.messages = Array.isArray(session.messages) ? session.messages : []
|
|
692
|
+
const runStartedAt = Date.now()
|
|
693
|
+
const runMessageStartIndex = session.messages.length
|
|
556
694
|
|
|
557
695
|
const appSettings = loadSettings()
|
|
696
|
+
const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
|
|
558
697
|
const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
|
|
559
|
-
const isHeartbeatRun = internal
|
|
560
|
-
const isAutoRunNoHistory = isHeartbeatRun
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
698
|
+
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
699
|
+
const isAutoRunNoHistory = isHeartbeatRun
|
|
700
|
+
const heartbeatStatusOnly = false
|
|
701
|
+
if (shouldApplySessionFreshnessReset(source)) {
|
|
702
|
+
const freshness = evaluateSessionFreshness({
|
|
703
|
+
session,
|
|
704
|
+
policy: resolveSessionResetPolicy({
|
|
705
|
+
session,
|
|
706
|
+
agent: agentForSession,
|
|
707
|
+
settings: appSettings,
|
|
708
|
+
}),
|
|
709
|
+
})
|
|
710
|
+
if (!freshness.fresh) {
|
|
711
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
712
|
+
resetSessionRuntime(session, freshness.reason || 'session_reset')
|
|
713
|
+
onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
|
|
714
|
+
sessions[sessionId] = session
|
|
715
|
+
saveSessions(sessions)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
566
718
|
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
567
719
|
let sessionForRun = pluginsForRun === session.plugins
|
|
568
720
|
? session
|
|
569
721
|
: { ...session, plugins: pluginsForRun }
|
|
722
|
+
let effectiveMessage = message
|
|
723
|
+
|
|
724
|
+
if (pluginsForRun.length > 0) {
|
|
725
|
+
try {
|
|
726
|
+
effectiveMessage = await getPluginManager().transformText(
|
|
727
|
+
'transformInboundMessage',
|
|
728
|
+
{ session: sessionForRun, text: message },
|
|
729
|
+
{ enabledIds: pluginsForRun },
|
|
730
|
+
)
|
|
731
|
+
} catch {
|
|
732
|
+
effectiveMessage = message
|
|
733
|
+
}
|
|
734
|
+
}
|
|
570
735
|
|
|
571
736
|
// Apply model override for heartbeat runs (cheaper model)
|
|
572
737
|
if (isHeartbeatRun && input.modelOverride) {
|
|
@@ -662,7 +827,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
662
827
|
internal,
|
|
663
828
|
provider: session.provider,
|
|
664
829
|
model: session.model,
|
|
665
|
-
messagePreview:
|
|
830
|
+
messagePreview: effectiveMessage.slice(0, 200),
|
|
666
831
|
hasImage: !!(imagePath || imageUrl),
|
|
667
832
|
},
|
|
668
833
|
})
|
|
@@ -679,7 +844,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
679
844
|
|
|
680
845
|
if (!internal) {
|
|
681
846
|
const linkAnalysis = await runLinkUnderstanding(message)
|
|
682
|
-
|
|
847
|
+
const nextUserMessage: Message = {
|
|
683
848
|
role: 'user',
|
|
684
849
|
text: message,
|
|
685
850
|
time: Date.now(),
|
|
@@ -687,7 +852,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
687
852
|
imageUrl: imageUrl || undefined,
|
|
688
853
|
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
689
854
|
replyToId: input.replyToId || undefined,
|
|
690
|
-
}
|
|
855
|
+
}
|
|
856
|
+
session.messages.push(nextUserMessage)
|
|
691
857
|
if (linkAnalysis.length > 0) {
|
|
692
858
|
session.messages.push({
|
|
693
859
|
role: 'assistant',
|
|
@@ -698,6 +864,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
698
864
|
}
|
|
699
865
|
session.lastActiveAt = Date.now()
|
|
700
866
|
saveSessions(sessions)
|
|
867
|
+
try {
|
|
868
|
+
await getPluginManager().runHook('onMessage', { session, message: nextUserMessage }, { enabledIds: pluginsForRun })
|
|
869
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
701
870
|
}
|
|
702
871
|
|
|
703
872
|
const systemPrompt = buildAgentSystemPrompt(session)
|
|
@@ -746,19 +915,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
746
915
|
const fresh = loadSessions()
|
|
747
916
|
const current = fresh[sessionId]
|
|
748
917
|
if (!current) return
|
|
918
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
749
919
|
const partialMsg: Message = {
|
|
750
920
|
role: 'assistant',
|
|
751
921
|
text: streamingPartialText,
|
|
752
922
|
time: Date.now(),
|
|
753
923
|
streaming: true,
|
|
754
|
-
toolEvents: toolEvents.length ? [...toolEvents] : undefined,
|
|
755
|
-
}
|
|
756
|
-
const lastMsg = current.messages.at(-1)
|
|
757
|
-
if (lastMsg?.streaming) {
|
|
758
|
-
current.messages[current.messages.length - 1] = partialMsg
|
|
759
|
-
} else {
|
|
760
|
-
current.messages.push(partialMsg)
|
|
924
|
+
toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
|
|
761
925
|
}
|
|
926
|
+
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
927
|
+
minIndex: runMessageStartIndex,
|
|
928
|
+
minTime: runStartedAt,
|
|
929
|
+
})
|
|
762
930
|
fresh[sessionId] = current
|
|
763
931
|
saveSessions(fresh)
|
|
764
932
|
notify(`messages:${sessionId}`)
|
|
@@ -812,7 +980,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
812
980
|
if (hasPlugins) {
|
|
813
981
|
fullResponse = (await streamAgentChat({
|
|
814
982
|
session: sessionForRun,
|
|
815
|
-
message:
|
|
983
|
+
message: effectiveMessage,
|
|
816
984
|
imagePath,
|
|
817
985
|
attachedFiles,
|
|
818
986
|
apiKey,
|
|
@@ -830,7 +998,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
830
998
|
model: sessionForRun.model,
|
|
831
999
|
apiEndpoint: sessionForRun.apiEndpoint || '',
|
|
832
1000
|
systemPrompt,
|
|
833
|
-
message:
|
|
1001
|
+
message: effectiveMessage,
|
|
834
1002
|
imagePath,
|
|
835
1003
|
imageUrl,
|
|
836
1004
|
attachedFiles,
|
|
@@ -858,7 +1026,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
858
1026
|
} else {
|
|
859
1027
|
fullResponse = await provider.handler.streamChat({
|
|
860
1028
|
session: sessionForRun,
|
|
861
|
-
message:
|
|
1029
|
+
message: effectiveMessage,
|
|
862
1030
|
imagePath,
|
|
863
1031
|
apiKey,
|
|
864
1032
|
systemPrompt,
|
|
@@ -1019,10 +1187,16 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1019
1187
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
1020
1188
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
1021
1189
|
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1190
|
+
const delegateResponse = (
|
|
1191
|
+
toolName === 'delegate'
|
|
1192
|
+
|| toolName.startsWith('delegate_to_')
|
|
1193
|
+
) ? extractDelegateResponse(outputText) : null
|
|
1194
|
+
if (delegateResponse) {
|
|
1195
|
+
fullResponse = delegateResponse
|
|
1196
|
+
} else if (!fullResponse.trim() && outputText?.trim()) {
|
|
1197
|
+
// Don't overwrite fullResponse with raw tool output — it's already captured
|
|
1198
|
+
// in toolEvents. Only set a brief notice when the LLM produced no text,
|
|
1199
|
+
// so the message bubble isn't empty.
|
|
1026
1200
|
const label = toolName.replace(/_/g, ' ')
|
|
1027
1201
|
fullResponse = `Used **${label}** — see tool output above for details.`
|
|
1028
1202
|
}
|
|
@@ -1075,7 +1249,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1075
1249
|
const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
|
|
1076
1250
|
.filter((tool) => enabledDelegateTools.includes(tool))
|
|
1077
1251
|
for (const delegateTool of delegationOrder) {
|
|
1078
|
-
const invoked = await invokeSessionTool(delegateTool, { task:
|
|
1252
|
+
const invoked = await invokeSessionTool(delegateTool, { task: effectiveMessage.trim() }, 'Auto-delegation failed')
|
|
1079
1253
|
if (invoked) break
|
|
1080
1254
|
}
|
|
1081
1255
|
}
|
|
@@ -1095,7 +1269,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1095
1269
|
for (const delegateTool of fallbackOrder) {
|
|
1096
1270
|
const invoked = await invokeSessionTool(
|
|
1097
1271
|
delegateTool,
|
|
1098
|
-
{ task:
|
|
1272
|
+
{ task: effectiveMessage.trim() },
|
|
1099
1273
|
`Provider failover via ${delegateTool} failed`,
|
|
1100
1274
|
)
|
|
1101
1275
|
if (invoked) {
|
|
@@ -1113,7 +1287,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1113
1287
|
if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
|
|
1114
1288
|
await invokeSessionTool(
|
|
1115
1289
|
'browser',
|
|
1116
|
-
{ action: '
|
|
1290
|
+
{ action: 'read_page', url: routingDecision.primaryUrl },
|
|
1117
1291
|
'Auto browser routing failed',
|
|
1118
1292
|
)
|
|
1119
1293
|
}
|
|
@@ -1123,7 +1297,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1123
1297
|
if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
|
|
1124
1298
|
await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
|
|
1125
1299
|
} else if (hasToolEnabled(sessionForRun, 'web_search')) {
|
|
1126
|
-
await invokeSessionTool('web_search', { query:
|
|
1300
|
+
await invokeSessionTool('web_search', { query: effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed')
|
|
1127
1301
|
}
|
|
1128
1302
|
}
|
|
1129
1303
|
|
|
@@ -1158,27 +1332,23 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1158
1332
|
errorMessage = streamErrors[streamErrors.length - 1]
|
|
1159
1333
|
}
|
|
1160
1334
|
|
|
1161
|
-
|
|
1335
|
+
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1336
|
+
if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1337
|
+
try {
|
|
1338
|
+
finalText = await getPluginManager().transformText(
|
|
1339
|
+
'transformOutboundMessage',
|
|
1340
|
+
{ session: sessionForRun, text: finalText },
|
|
1341
|
+
{ enabledIds: pluginsForRun },
|
|
1342
|
+
)
|
|
1343
|
+
} catch { /* outbound transforms are non-critical */ }
|
|
1344
|
+
}
|
|
1345
|
+
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1162
1346
|
const textForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1347
|
+
const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
|
|
1163
1348
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (metaMatch) {
|
|
1168
|
-
try {
|
|
1169
|
-
const meta = JSON.parse(metaMatch[1])
|
|
1170
|
-
const statusPayload: Record<string, string | undefined> = {}
|
|
1171
|
-
if (meta.goal) statusPayload.goal = String(meta.goal)
|
|
1172
|
-
if (meta.status) statusPayload.status = String(meta.status)
|
|
1173
|
-
if (meta.summary) statusPayload.summary = String(meta.summary)
|
|
1174
|
-
if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
|
|
1175
|
-
if (Object.keys(statusPayload).length > 0) {
|
|
1176
|
-
emit({ t: 'status', text: JSON.stringify(statusPayload) })
|
|
1177
|
-
}
|
|
1178
|
-
} catch {
|
|
1179
|
-
// ignore malformed meta JSON
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1349
|
+
if (isHeartbeatRun && finalText) {
|
|
1350
|
+
const heartbeatStatus = extractHeartbeatStatus(finalText)
|
|
1351
|
+
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1182
1352
|
}
|
|
1183
1353
|
|
|
1184
1354
|
// HEARTBEAT_OK suppression
|
|
@@ -1214,7 +1384,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1214
1384
|
const fresh = loadSessions()
|
|
1215
1385
|
const current = fresh[sessionId]
|
|
1216
1386
|
if (current) {
|
|
1387
|
+
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1388
|
+
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1217
1389
|
let changed = false
|
|
1390
|
+
changed = pruneStreamingAssistantArtifacts(current.messages, {
|
|
1391
|
+
minIndex: runMessageStartIndex,
|
|
1392
|
+
minTime: runStartedAt,
|
|
1393
|
+
}) || changed
|
|
1218
1394
|
const persistField = (key: string, value: unknown) => {
|
|
1219
1395
|
const normalized = normalizeResumeId(value)
|
|
1220
1396
|
if ((current as Record<string, unknown>)[key] !== normalized) {
|
|
@@ -1246,7 +1422,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1246
1422
|
}
|
|
1247
1423
|
|
|
1248
1424
|
if (shouldPersistAssistant) {
|
|
1249
|
-
const persistedKind =
|
|
1425
|
+
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1250
1426
|
const persistedText = heartbeatClassification === 'strip'
|
|
1251
1427
|
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1252
1428
|
: textForPersistence
|
|
@@ -1256,13 +1432,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1256
1432
|
text: persistedText,
|
|
1257
1433
|
time: nowTs,
|
|
1258
1434
|
thinking: thinkingText || undefined,
|
|
1259
|
-
toolEvents:
|
|
1435
|
+
toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
|
|
1260
1436
|
kind: persistedKind,
|
|
1261
1437
|
}
|
|
1262
1438
|
const previous = current.messages.at(-1)
|
|
1263
1439
|
if (previous?.streaming || shouldReplaceRecentAssistantMessage({
|
|
1264
1440
|
previous,
|
|
1265
|
-
nextToolEvents:
|
|
1441
|
+
nextToolEvents: persistedToolEvents,
|
|
1266
1442
|
nextKind: persistedKind,
|
|
1267
1443
|
now: nowTs,
|
|
1268
1444
|
})) {
|
|
@@ -1275,6 +1451,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1275
1451
|
current.lastHeartbeatSentAt = nowTs
|
|
1276
1452
|
}
|
|
1277
1453
|
changed = true
|
|
1454
|
+
try {
|
|
1455
|
+
await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1456
|
+
} catch { /* onMessage hooks are non-critical */ }
|
|
1278
1457
|
|
|
1279
1458
|
// Conversation tone detection
|
|
1280
1459
|
if (!internal) {
|
|
@@ -1329,6 +1508,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1329
1508
|
}
|
|
1330
1509
|
}
|
|
1331
1510
|
}
|
|
1511
|
+
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1512
|
+
changed = pruneSuppressedHeartbeatStreamMessage(current.messages) || changed
|
|
1513
|
+
}
|
|
1332
1514
|
|
|
1333
1515
|
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
1334
1516
|
try {
|
|
@@ -1338,13 +1520,20 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1338
1520
|
response: textForPersistence,
|
|
1339
1521
|
source,
|
|
1340
1522
|
internal,
|
|
1341
|
-
})
|
|
1523
|
+
}, { enabledIds: pluginsForRun })
|
|
1342
1524
|
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1343
1525
|
|
|
1344
1526
|
// Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
|
|
1345
|
-
if (source
|
|
1527
|
+
if (!isHeartbeatSource(source)) {
|
|
1346
1528
|
current.lastActiveAt = Date.now()
|
|
1347
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
refreshSessionIdentityState(current, currentAgent)
|
|
1532
|
+
changed = true
|
|
1533
|
+
try {
|
|
1534
|
+
const archiveSync = syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1535
|
+
if (archiveSync.stored) changed = true
|
|
1536
|
+
} catch { /* archive sync is best-effort */ }
|
|
1348
1537
|
fresh[sessionId] = current
|
|
1349
1538
|
saveSessions(fresh)
|
|
1350
1539
|
notify(`messages:${sessionId}`)
|
|
@@ -1355,7 +1544,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1355
1544
|
sessionId,
|
|
1356
1545
|
text: finalText,
|
|
1357
1546
|
persisted: shouldPersistAssistant,
|
|
1358
|
-
toolEvents,
|
|
1547
|
+
toolEvents: persistedToolEvents,
|
|
1359
1548
|
error: errorMessage,
|
|
1360
1549
|
inputTokens: accumulatedUsage.inputTokens || undefined,
|
|
1361
1550
|
outputTokens: accumulatedUsage.outputTokens || undefined,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import type { Agent } from '@/types'
|
|
4
|
+
import { filterHealthyChatroomAgents } from './chatroom-health'
|
|
5
|
+
|
|
6
|
+
describe('filterHealthyChatroomAgents', () => {
|
|
7
|
+
it('treats providers with default endpoints as healthy without explicit agent endpoints', () => {
|
|
8
|
+
const now = Date.now()
|
|
9
|
+
const agents: Record<string, Agent> = {
|
|
10
|
+
agent_writer: {
|
|
11
|
+
id: 'agent_writer',
|
|
12
|
+
name: 'Writer',
|
|
13
|
+
description: '',
|
|
14
|
+
systemPrompt: '',
|
|
15
|
+
provider: 'ollama',
|
|
16
|
+
model: 'glm-5:cloud',
|
|
17
|
+
createdAt: now,
|
|
18
|
+
updatedAt: now,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = filterHealthyChatroomAgents(['agent_writer'], agents)
|
|
23
|
+
assert.deepEqual(result.healthyAgentIds, ['agent_writer'])
|
|
24
|
+
assert.deepEqual(result.skipped, [])
|
|
25
|
+
})
|
|
26
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProvider } from '@/lib/providers'
|
|
2
2
|
import type { Agent } from '@/types'
|
|
3
|
-
import { resolveApiKey } from './chatroom-helpers'
|
|
3
|
+
import { resolveAgentApiEndpoint, resolveApiKey } from './chatroom-helpers'
|
|
4
4
|
import { isProviderCoolingDown } from './provider-health'
|
|
5
5
|
|
|
6
6
|
export interface ChatroomAgentHealthSkip {
|
|
@@ -47,7 +47,7 @@ export function filterHealthyChatroomAgents(
|
|
|
47
47
|
skipped.push({ agentId, reason: 'missing_api_credentials' })
|
|
48
48
|
continue
|
|
49
49
|
}
|
|
50
|
-
if (providerInfo.requiresEndpoint && !agent
|
|
50
|
+
if (providerInfo.requiresEndpoint && !resolveAgentApiEndpoint(agent)) {
|
|
51
51
|
skipped.push({ agentId, reason: 'missing_api_endpoint' })
|
|
52
52
|
continue
|
|
53
53
|
}
|
|
@@ -57,4 +57,3 @@ export function filterHealthyChatroomAgents(
|
|
|
57
57
|
|
|
58
58
|
return { healthyAgentIds, skipped }
|
|
59
59
|
}
|
|
60
|
-
|