@swarmclawai/swarmclaw 0.5.3 → 0.6.2
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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/route.ts +4 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +53 -1
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +111 -73
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +194 -97
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +31 -15
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +59 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
|
@@ -43,9 +43,9 @@ async function fileToContentParts(filePath: string): Promise<any[]> {
|
|
|
43
43
|
return [{ type: 'text', text: `[Attached file: ${name}]` }]
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
46
|
+
export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey, systemPrompt, write, active, loadHistory, onUsage }: StreamChatOptions): Promise<string> {
|
|
47
47
|
return new Promise(async (resolve) => {
|
|
48
|
-
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory)
|
|
48
|
+
const messages = await buildMessages(session, message, imagePath, systemPrompt, loadHistory, imageUrl)
|
|
49
49
|
const model = session.model || 'gpt-4o'
|
|
50
50
|
|
|
51
51
|
const payload = JSON.stringify({
|
|
@@ -163,7 +163,11 @@ export function streamOpenAiChat({ session, message, imagePath, apiKey, systemPr
|
|
|
163
163
|
})
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
function urlToImagePart(url: string): { type: string; image_url: { url: string; detail: string } } {
|
|
167
|
+
return { type: 'image_url', image_url: { url, detail: 'auto' } }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function buildMessages(session: any, message: string, imagePath: string | undefined, systemPrompt: string | undefined, loadHistory: (id: string) => any[], imageUrl?: string) {
|
|
167
171
|
const msgs: Array<{ role: string; content: any }> = []
|
|
168
172
|
|
|
169
173
|
if (systemPrompt) {
|
|
@@ -173,8 +177,9 @@ async function buildMessages(session: any, message: string, imagePath: string |
|
|
|
173
177
|
if (loadHistory) {
|
|
174
178
|
const history = loadHistory(session.id).slice(-40)
|
|
175
179
|
for (const m of history) {
|
|
176
|
-
if (m.role === 'user' && m.imagePath) {
|
|
177
|
-
const parts = await fileToContentParts(m.imagePath)
|
|
180
|
+
if (m.role === 'user' && (m.imagePath || m.imageUrl)) {
|
|
181
|
+
const parts = m.imagePath ? await fileToContentParts(m.imagePath) : []
|
|
182
|
+
if (m.imageUrl) parts.push(urlToImagePart(m.imageUrl))
|
|
178
183
|
msgs.push({ role: 'user', content: [...parts, { type: 'text', text: m.text }] })
|
|
179
184
|
} else {
|
|
180
185
|
msgs.push({ role: m.role, content: m.text })
|
|
@@ -183,8 +188,9 @@ async function buildMessages(session: any, message: string, imagePath: string |
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
// Current message with optional attachment
|
|
186
|
-
if (imagePath) {
|
|
187
|
-
const parts = await fileToContentParts(imagePath)
|
|
191
|
+
if (imagePath || imageUrl) {
|
|
192
|
+
const parts = imagePath ? await fileToContentParts(imagePath) : []
|
|
193
|
+
if (imageUrl) parts.push(urlToImagePart(imageUrl))
|
|
188
194
|
msgs.push({ role: 'user', content: [...parts, { type: 'text', text: message }] })
|
|
189
195
|
} else {
|
|
190
196
|
msgs.push({ role: 'user', content: message })
|
|
@@ -24,11 +24,20 @@ import { getMemoryDb } from './memory-db'
|
|
|
24
24
|
import { routeTaskIntent } from './capability-router'
|
|
25
25
|
import { notify } from './ws-hub'
|
|
26
26
|
import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
|
|
27
|
-
import
|
|
27
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
28
|
+
import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
|
|
28
29
|
import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
|
|
29
30
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
30
31
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli'
|
|
31
32
|
|
|
33
|
+
/** Slice history from the most recent context-clear marker forward */
|
|
34
|
+
function applyContextClearBoundary(messages: Message[]): Message[] {
|
|
35
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
36
|
+
if (messages[i].kind === 'context-clear') return messages.slice(i + 1)
|
|
37
|
+
}
|
|
38
|
+
return messages
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
interface SessionWithTools {
|
|
33
42
|
tools?: string[] | null
|
|
34
43
|
}
|
|
@@ -55,6 +64,7 @@ export interface ExecuteChatTurnInput {
|
|
|
55
64
|
onEvent?: (event: SSEEvent) => void
|
|
56
65
|
modelOverride?: string
|
|
57
66
|
heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
|
|
67
|
+
replyToId?: string
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
export interface ExecuteChatTurnResult {
|
|
@@ -306,11 +316,11 @@ function buildAgentSystemPrompt(session: any): string | undefined {
|
|
|
306
316
|
if (!session.agentId) return undefined
|
|
307
317
|
const agents = loadAgents()
|
|
308
318
|
const agent = agents[session.agentId]
|
|
309
|
-
if (!agent?.systemPrompt && !agent?.soul) return undefined
|
|
310
319
|
|
|
311
320
|
const settings = loadSettings()
|
|
312
321
|
const parts: string[] = []
|
|
313
322
|
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
323
|
+
parts.push(buildCurrentDateTimePromptContext())
|
|
314
324
|
if (agent.soul) parts.push(agent.soul)
|
|
315
325
|
if (agent.systemPrompt) parts.push(agent.systemPrompt)
|
|
316
326
|
if (agent.skillIds?.length) {
|
|
@@ -320,6 +330,7 @@ function buildAgentSystemPrompt(session: any): string | undefined {
|
|
|
320
330
|
if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
321
331
|
}
|
|
322
332
|
}
|
|
333
|
+
if (!parts.length) return undefined
|
|
323
334
|
return parts.join('\n\n')
|
|
324
335
|
}
|
|
325
336
|
|
|
@@ -341,13 +352,26 @@ function resolveApiKeyForSession(session: SessionWithCredentials, provider: Prov
|
|
|
341
352
|
return null
|
|
342
353
|
}
|
|
343
354
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
355
|
+
function stripMarkupForHeartbeat(text: string): string {
|
|
356
|
+
return text
|
|
357
|
+
.replace(/<[^>]*>/g, ' ') // strip HTML tags
|
|
358
|
+
.replace(/ /gi, ' ') // decode nbsp
|
|
359
|
+
.replace(/^[*`~_]+/, '') // strip leading markdown
|
|
360
|
+
.replace(/[*`~_]+$/, '') // strip trailing markdown
|
|
361
|
+
.trim()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const HEARTBEAT_OK_RE = /HEARTBEAT_OK[^\w]{0,4}$/
|
|
365
|
+
const NO_MESSAGE_RE = /NO_MESSAGE[^\w]{0,4}$/
|
|
366
|
+
|
|
367
|
+
function classifyHeartbeatResponse(text: string, ackMaxChars: number, hadToolCalls: boolean): 'suppress' | 'strip' | 'keep' {
|
|
368
|
+
const cleaned = stripMarkupForHeartbeat(text)
|
|
369
|
+
if (cleaned === 'HEARTBEAT_OK' || cleaned === 'NO_MESSAGE') return 'suppress'
|
|
370
|
+
if (HEARTBEAT_OK_RE.test(cleaned) || NO_MESSAGE_RE.test(cleaned)) return 'suppress'
|
|
371
|
+
const stripped = cleaned.replace(/HEARTBEAT_OK/gi, '').replace(/NO_MESSAGE/gi, '').trim()
|
|
348
372
|
if (!stripped) return 'suppress'
|
|
349
|
-
if (stripped.length <= ackMaxChars) return 'suppress'
|
|
350
|
-
return stripped.length <
|
|
373
|
+
if (!hadToolCalls && stripped.length <= ackMaxChars) return 'suppress'
|
|
374
|
+
return stripped.length < cleaned.length ? 'strip' : 'keep'
|
|
351
375
|
}
|
|
352
376
|
|
|
353
377
|
function estimateConversationTone(text: string): string {
|
|
@@ -542,6 +566,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
542
566
|
imagePath: imagePath || undefined,
|
|
543
567
|
imageUrl: imageUrl || undefined,
|
|
544
568
|
attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
|
|
569
|
+
replyToId: input.replyToId || undefined,
|
|
545
570
|
})
|
|
546
571
|
session.lastActiveAt = Date.now()
|
|
547
572
|
saveSessions(sessions)
|
|
@@ -551,6 +576,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
551
576
|
const toolEvents: MessageToolEvent[] = []
|
|
552
577
|
const streamErrors: string[] = []
|
|
553
578
|
|
|
579
|
+
let thinkingText = ''
|
|
554
580
|
const emit = (ev: SSEEvent) => {
|
|
555
581
|
if (ev.t === 'err' && typeof ev.text === 'string') {
|
|
556
582
|
const trimmed = ev.text.trim()
|
|
@@ -559,6 +585,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
559
585
|
if (streamErrors.length > 8) streamErrors.shift()
|
|
560
586
|
}
|
|
561
587
|
}
|
|
588
|
+
if (ev.t === 'thinking' && ev.text) {
|
|
589
|
+
thinkingText += ev.text
|
|
590
|
+
}
|
|
562
591
|
collectToolEvent(ev, toolEvents)
|
|
563
592
|
onEvent?.(ev)
|
|
564
593
|
}
|
|
@@ -593,9 +622,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
593
622
|
const hasTools = !!sessionForRun.tools?.length && !NON_LANGGRAPH_PROVIDER_IDS.has(providerType)
|
|
594
623
|
|
|
595
624
|
try {
|
|
596
|
-
// Heartbeat runs
|
|
597
|
-
//
|
|
598
|
-
|
|
625
|
+
// Heartbeat runs get a small tail of recent messages so the agent can see
|
|
626
|
+
// prior findings and avoid repeating the same searches. Full history is
|
|
627
|
+
// skipped to avoid blowing the context window on long-lived sessions.
|
|
628
|
+
const heartbeatHistory = isAutoRunNoHistory
|
|
629
|
+
? getSessionMessages(sessionId).slice(-6)
|
|
630
|
+
: undefined
|
|
599
631
|
|
|
600
632
|
console.log(`[chat-execution] provider=${providerType}, hasTools=${hasTools}, imagePath=${imagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, tools=${(sessionForRun.tools || []).length}`)
|
|
601
633
|
|
|
@@ -608,7 +640,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
608
640
|
apiKey,
|
|
609
641
|
systemPrompt,
|
|
610
642
|
write: (raw) => parseAndEmit(raw),
|
|
611
|
-
history: heartbeatHistory ?? getSessionMessages(sessionId),
|
|
643
|
+
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
612
644
|
signal: abortController.signal,
|
|
613
645
|
})).fullText
|
|
614
646
|
: await provider.handler.streamChat({
|
|
@@ -619,7 +651,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
619
651
|
systemPrompt,
|
|
620
652
|
write: (raw: string) => parseAndEmit(raw),
|
|
621
653
|
active,
|
|
622
|
-
loadHistory: isAutoRunNoHistory ? () =>
|
|
654
|
+
loadHistory: isAutoRunNoHistory ? () => getSessionMessages(sessionId).slice(-6) : (sid: string) => applyContextClearBoundary(getSessionMessages(sid)),
|
|
623
655
|
onUsage: (u) => { directUsage.inputTokens = u.inputTokens; directUsage.outputTokens = u.outputTokens; directUsage.received = true },
|
|
624
656
|
})
|
|
625
657
|
} catch (err: any) {
|
|
@@ -708,7 +740,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
708
740
|
const toolOutput = await selectedTool.invoke(args)
|
|
709
741
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
710
742
|
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
711
|
-
|
|
743
|
+
// Don't overwrite fullResponse with raw tool output — it's already captured
|
|
744
|
+
// in toolEvents. Only set a brief notice when the LLM produced no text,
|
|
745
|
+
// so the message bubble isn't empty.
|
|
746
|
+
if (!fullResponse.trim() && outputText?.trim()) {
|
|
747
|
+
const label = toolName.replace(/_/g, ' ')
|
|
748
|
+
fullResponse = `Used **${label}** — see tool output above for details.`
|
|
749
|
+
}
|
|
712
750
|
calledNames.add(toolName)
|
|
713
751
|
return true
|
|
714
752
|
} catch (forceErr: any) {
|
|
@@ -854,7 +892,12 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
854
892
|
const heartbeatConfig = input.heartbeatConfig
|
|
855
893
|
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
856
894
|
if (isHeartbeatRun && textForPersistence.length > 0) {
|
|
857
|
-
heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300)
|
|
895
|
+
heartbeatClassification = classifyHeartbeatResponse(textForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Emit WS notification for every heartbeat completion so UI can show pulse
|
|
899
|
+
if (isHeartbeatRun && session.agentId) {
|
|
900
|
+
notify(`heartbeat:agent:${session.agentId}`)
|
|
858
901
|
}
|
|
859
902
|
|
|
860
903
|
const shouldPersistAssistant = textForPersistence.length > 0
|
|
@@ -904,6 +947,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
904
947
|
role: 'assistant',
|
|
905
948
|
text: persistedText,
|
|
906
949
|
time: Date.now(),
|
|
950
|
+
thinking: thinkingText || undefined,
|
|
907
951
|
toolEvents: toolEvents.length ? toolEvents : undefined,
|
|
908
952
|
kind: persistedKind,
|
|
909
953
|
})
|
|
@@ -944,6 +988,22 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
944
988
|
// Best effort — connector manager may not be loaded
|
|
945
989
|
}
|
|
946
990
|
}
|
|
991
|
+
|
|
992
|
+
// Auto-discover connectors linked to this agent when no explicit target is set
|
|
993
|
+
if (isHeartbeatRun && !heartbeatConfig?.target && heartbeatConfig?.showAlerts !== false && session.agentId) {
|
|
994
|
+
try {
|
|
995
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
996
|
+
const { listRunningConnectors: listRunning, sendConnectorMessage: sendMsg } = require('./connectors/manager')
|
|
997
|
+
const agentConnectors = listRunning().filter((c: { agentId: string | null; recentChannelId: string | null; supportsSend: boolean }) =>
|
|
998
|
+
c.agentId === session.agentId && c.recentChannelId && c.supportsSend
|
|
999
|
+
)
|
|
1000
|
+
for (const conn of agentConnectors) {
|
|
1001
|
+
sendMsg({ connectorId: conn.id, channelId: conn.recentChannelId, text: persistedText }).catch(() => {})
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
// Best effort — connector manager may not be loaded
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
947
1007
|
}
|
|
948
1008
|
|
|
949
1009
|
const autoMemoryEligible = shouldStoreAutoMemoryNote({
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { loadSettings, loadSkills, loadCredentials, decryptKey } from './storage'
|
|
2
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
3
|
+
import type { Chatroom, Agent, Session, Message } from '@/types'
|
|
4
|
+
|
|
5
|
+
/** Resolve API key from an agent's credentialId */
|
|
6
|
+
export function resolveApiKey(credentialId: string | null | undefined): string | null {
|
|
7
|
+
if (!credentialId) return null
|
|
8
|
+
const creds = loadCredentials()
|
|
9
|
+
const cred = creds[credentialId]
|
|
10
|
+
if (!cred?.encryptedKey) return null
|
|
11
|
+
try { return decryptKey(cred.encryptedKey) } catch { return null }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Parse @mentions from message text, returns matching agentIds */
|
|
15
|
+
export function parseMentions(text: string, agents: Record<string, Agent>, memberIds: string[]): string[] {
|
|
16
|
+
if (/@all\b/i.test(text)) return [...memberIds]
|
|
17
|
+
const mentionPattern = /@(\S+)/g
|
|
18
|
+
const mentioned: string[] = []
|
|
19
|
+
let match: RegExpExecArray | null
|
|
20
|
+
while ((match = mentionPattern.exec(text)) !== null) {
|
|
21
|
+
const name = match[1].toLowerCase()
|
|
22
|
+
for (const id of memberIds) {
|
|
23
|
+
const agent = agents[id]
|
|
24
|
+
if (agent && agent.name.toLowerCase().replace(/\s+/g, '') === name) {
|
|
25
|
+
if (!mentioned.includes(id)) mentioned.push(id)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return mentioned
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Build chatroom context as a system prompt addendum with agent profiles and collaboration guidelines */
|
|
33
|
+
export function buildChatroomSystemPrompt(chatroom: Chatroom, agents: Record<string, Agent>, agentId: string): string {
|
|
34
|
+
const selfAgent = agents[agentId]
|
|
35
|
+
const selfName = selfAgent?.name || agentId
|
|
36
|
+
|
|
37
|
+
// Build team profiles with capabilities
|
|
38
|
+
const teamProfiles = chatroom.agentIds
|
|
39
|
+
.filter((id) => id !== agentId)
|
|
40
|
+
.map((id) => {
|
|
41
|
+
const a = agents[id]
|
|
42
|
+
if (!a) return null
|
|
43
|
+
const tools = a.tools?.length ? `Tools: ${a.tools.join(', ')}` : 'No specialized tools'
|
|
44
|
+
const desc = a.description || a.soul || 'No description'
|
|
45
|
+
return `- **${a.name}**: ${desc}\n ${tools}`
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join('\n')
|
|
49
|
+
|
|
50
|
+
const recentMessages = chatroom.messages.slice(-30).map((m) => {
|
|
51
|
+
return `[${m.senderName}]: ${m.text}`
|
|
52
|
+
}).join('\n')
|
|
53
|
+
|
|
54
|
+
const memberCount = chatroom.agentIds.length
|
|
55
|
+
const otherNames = chatroom.agentIds
|
|
56
|
+
.filter((id) => id !== agentId)
|
|
57
|
+
.map((id) => agents[id]?.name)
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
`## Chatroom Context`,
|
|
62
|
+
`You are **${selfName}** in a group chatroom called "${chatroom.name}" with ${memberCount} participants (you, ${otherNames.join(', ') || 'others'}, and the user).`,
|
|
63
|
+
selfAgent?.description ? `Your role: ${selfAgent.description}` : '',
|
|
64
|
+
selfAgent?.tools?.length ? `Your available tools: ${selfAgent.tools.join(', ')}` : '',
|
|
65
|
+
'',
|
|
66
|
+
'## Team Members',
|
|
67
|
+
teamProfiles || '(no other agents)',
|
|
68
|
+
'',
|
|
69
|
+
'## How to Behave in This Chatroom',
|
|
70
|
+
'- **You are in a group chat.** Talk like you are in a real-time conversation with teammates — be direct, casual, and concise.',
|
|
71
|
+
'- **Be yourself.** Respond with personality. Don\'t give generic "let me know if you need anything" responses. Actually engage with what was said.',
|
|
72
|
+
'- **Answer the question or react to the message.** If someone says "how are you doing?" just answer naturally. If someone asks a question you can help with, help directly.',
|
|
73
|
+
'- **Keep responses short** unless depth is needed. A few sentences is usually enough. This is a chat, not an essay.',
|
|
74
|
+
'- **@mention teammates** only when you genuinely need their specific expertise. Don\'t tag people just to be polite.',
|
|
75
|
+
'- **Don\'t narrate your capabilities** unless asked. Just demonstrate them by doing things.',
|
|
76
|
+
'- **Read the room.** Look at recent messages to understand context. Don\'t repeat what others already said.',
|
|
77
|
+
'',
|
|
78
|
+
'## Recent Messages',
|
|
79
|
+
recentMessages || '(no messages yet)',
|
|
80
|
+
].filter((line) => line !== undefined).join('\n')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Build a synthetic session object for an agent in a chatroom */
|
|
84
|
+
export function buildSyntheticSession(agent: Agent, chatroomId: string): Session {
|
|
85
|
+
return {
|
|
86
|
+
id: `chatroom-${chatroomId}-${agent.id}`,
|
|
87
|
+
name: `Chatroom session for ${agent.name}`,
|
|
88
|
+
cwd: process.cwd(),
|
|
89
|
+
user: 'chatroom',
|
|
90
|
+
provider: agent.provider,
|
|
91
|
+
model: agent.model,
|
|
92
|
+
credentialId: agent.credentialId ?? null,
|
|
93
|
+
fallbackCredentialIds: agent.fallbackCredentialIds,
|
|
94
|
+
apiEndpoint: agent.apiEndpoint ?? null,
|
|
95
|
+
claudeSessionId: null,
|
|
96
|
+
messages: [],
|
|
97
|
+
createdAt: Date.now(),
|
|
98
|
+
lastActiveAt: Date.now(),
|
|
99
|
+
tools: agent.tools || [],
|
|
100
|
+
agentId: agent.id,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Build agent's system prompt including skills */
|
|
105
|
+
export function buildAgentSystemPromptForChatroom(agent: Agent): string {
|
|
106
|
+
const settings = loadSettings()
|
|
107
|
+
const parts: string[] = []
|
|
108
|
+
if (settings.userPrompt) parts.push(settings.userPrompt)
|
|
109
|
+
parts.push(buildCurrentDateTimePromptContext())
|
|
110
|
+
if (agent.soul) parts.push(agent.soul)
|
|
111
|
+
if (agent.systemPrompt) parts.push(agent.systemPrompt)
|
|
112
|
+
if (agent.skillIds?.length) {
|
|
113
|
+
const allSkills = loadSkills()
|
|
114
|
+
for (const skillId of agent.skillIds) {
|
|
115
|
+
const skill = allSkills[skillId]
|
|
116
|
+
if (skill?.content) parts.push(`## Skill: ${skill.name}\n${skill.content}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return parts.join('\n\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Convert chatroom messages to Message history format for LLM */
|
|
123
|
+
export function buildHistoryForAgent(chatroom: Chatroom, agentId: string, imagePath?: string, attachedFiles?: string[]): Message[] {
|
|
124
|
+
const history = chatroom.messages.slice(-50).map((m) => {
|
|
125
|
+
let msgText = `[${m.senderName}]: ${m.text}`
|
|
126
|
+
// Include attachment info in history
|
|
127
|
+
if (m.attachedFiles?.length) {
|
|
128
|
+
const names = m.attachedFiles.map((f) => f.split('/').pop()).join(', ')
|
|
129
|
+
msgText += `\n[Attached: ${names}]`
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
role: m.senderId === agentId ? 'assistant' as const : 'user' as const,
|
|
133
|
+
text: msgText,
|
|
134
|
+
time: m.time,
|
|
135
|
+
...(m.imagePath ? { imagePath: m.imagePath } : {}),
|
|
136
|
+
...(m.attachedFiles ? { attachedFiles: m.attachedFiles } : {}),
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
// Pass through imagePath/attachedFiles from the current message to the last history entry
|
|
140
|
+
if (history.length > 0 && (imagePath || attachedFiles)) {
|
|
141
|
+
const last = history[history.length - 1]
|
|
142
|
+
if (imagePath && !last.imagePath) last.imagePath = imagePath
|
|
143
|
+
if (attachedFiles && !last.attachedFiles) last.attachedFiles = attachedFiles
|
|
144
|
+
}
|
|
145
|
+
return history
|
|
146
|
+
}
|