@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
|
@@ -9,6 +9,58 @@ import { spawnSync } from 'child_process'
|
|
|
9
9
|
import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
|
|
10
10
|
import { getSearchProvider } from './search-providers'
|
|
11
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Search result compression — summarize verbose results before injecting into context
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
async function compressSearchResults(
|
|
17
|
+
results: Array<{ title?: string; url?: string; snippet?: string }>,
|
|
18
|
+
query: string,
|
|
19
|
+
bctx: ToolBuildContext,
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
const session = bctx.resolveCurrentSession?.()
|
|
22
|
+
if (!session?.provider || !session?.model) return null
|
|
23
|
+
|
|
24
|
+
const { getProvider } = await import('@/lib/providers')
|
|
25
|
+
const { loadCredentials, decryptKey } = await import('../storage')
|
|
26
|
+
const providerEntry = getProvider(session.provider)
|
|
27
|
+
if (!providerEntry?.handler?.streamChat) return null
|
|
28
|
+
|
|
29
|
+
// Resolve API key
|
|
30
|
+
let apiKey: string | undefined
|
|
31
|
+
if (session.credentialId) {
|
|
32
|
+
const creds = loadCredentials()
|
|
33
|
+
const cred = creds[session.credentialId]
|
|
34
|
+
if (cred) apiKey = decryptKey(cred)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const systemPrompt = 'You are a search result summarizer. Condense search results into a concise reference. Keep key facts, URLs, and data points. Remove filler and redundancy. Output plain text, not JSON.'
|
|
38
|
+
const message = `Query: "${query}"\n\nResults:\n${JSON.stringify(results, null, 1)}\n\nSummarize these results concisely.`
|
|
39
|
+
|
|
40
|
+
let compressed = ''
|
|
41
|
+
await providerEntry.handler.streamChat({
|
|
42
|
+
session: { ...session, messages: [] },
|
|
43
|
+
message,
|
|
44
|
+
apiKey,
|
|
45
|
+
systemPrompt,
|
|
46
|
+
write: (raw: string) => {
|
|
47
|
+
// Extract text data from SSE lines
|
|
48
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!line.startsWith('data: ')) continue
|
|
51
|
+
try {
|
|
52
|
+
const ev = JSON.parse(line.slice(6))
|
|
53
|
+
if (ev.t === 'd' && ev.text) compressed += ev.text
|
|
54
|
+
} catch { /* skip */ }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
active: new Map(),
|
|
58
|
+
loadHistory: () => [],
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return compressed.trim() || null
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
// ---------------------------------------------------------------------------
|
|
13
65
|
// Global registry of active browser instances for cleanup sweeps
|
|
14
66
|
// ---------------------------------------------------------------------------
|
|
@@ -70,9 +122,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
70
122
|
const settings = loadSettings()
|
|
71
123
|
const provider = await getSearchProvider(settings)
|
|
72
124
|
const results = await provider.search(query, limit)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
if (results.length === 0) return 'No results found.'
|
|
126
|
+
const raw = JSON.stringify(results, null, 2)
|
|
127
|
+
// Compress search results if they exceed 2000 chars
|
|
128
|
+
if (raw.length > 2000) {
|
|
129
|
+
try {
|
|
130
|
+
const compressed = await compressSearchResults(results, query, bctx)
|
|
131
|
+
if (compressed) return compressed
|
|
132
|
+
} catch {
|
|
133
|
+
// Compression failed — fall through to raw results
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return raw
|
|
76
137
|
} catch (err: unknown) {
|
|
77
138
|
return `Error searching web: ${err instanceof Error ? err.message : String(err)}`
|
|
78
139
|
}
|
|
@@ -284,6 +345,49 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
284
345
|
return JSON.stringify(result)
|
|
285
346
|
}
|
|
286
347
|
|
|
348
|
+
// Best-effort cookie/consent banner dismissal after navigation
|
|
349
|
+
const dismissCookieBanners = async (
|
|
350
|
+
mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>,
|
|
351
|
+
) => {
|
|
352
|
+
// Wait briefly for consent overlays to appear
|
|
353
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
354
|
+
const js = `
|
|
355
|
+
(() => {
|
|
356
|
+
const sel = [
|
|
357
|
+
// Common "Reject" / "Reject all" / "Decline" buttons
|
|
358
|
+
'button[id*="reject" i]', 'button[class*="reject" i]',
|
|
359
|
+
'a[id*="reject" i]', 'a[class*="reject" i]',
|
|
360
|
+
'[data-testid*="reject" i]', '[data-action="reject"]',
|
|
361
|
+
// OneTrust
|
|
362
|
+
'#onetrust-reject-all-handler',
|
|
363
|
+
// Cookiebot
|
|
364
|
+
'#CybotCookiebotDialogBodyButtonDecline',
|
|
365
|
+
// Didomi
|
|
366
|
+
'#didomi-notice-disagree-button',
|
|
367
|
+
// Quantcast / IAB TCF
|
|
368
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
369
|
+
'button.sp_choice_type_12',
|
|
370
|
+
// Generic patterns
|
|
371
|
+
'button[aria-label*="reject" i]', 'button[aria-label*="decline" i]',
|
|
372
|
+
'button[aria-label*="deny" i]', 'button[aria-label*="refuse" i]',
|
|
373
|
+
];
|
|
374
|
+
for (const s of sel) {
|
|
375
|
+
const el = document.querySelector(s);
|
|
376
|
+
if (el && el.offsetParent !== null) { el.click(); return 'dismissed:' + s; }
|
|
377
|
+
}
|
|
378
|
+
// Fallback: find buttons by visible text
|
|
379
|
+
const btns = [...document.querySelectorAll('button, a[role="button"], [class*="cookie"] button, [class*="consent"] button, [id*="cookie"] button')];
|
|
380
|
+
const rejectRe = /^(reject|reject all|decline|deny|refuse|no,? thanks|only necessary|necessary only)$/i;
|
|
381
|
+
for (const b of btns) {
|
|
382
|
+
const txt = (b.textContent || '').trim();
|
|
383
|
+
if (rejectRe.test(txt) && b.offsetParent !== null) { b.click(); return 'dismissed:text=' + txt; }
|
|
384
|
+
}
|
|
385
|
+
return 'none';
|
|
386
|
+
})()
|
|
387
|
+
`
|
|
388
|
+
await mcpCall('browser_evaluate', { expression: js })
|
|
389
|
+
}
|
|
390
|
+
|
|
287
391
|
// Action-to-MCP tool mapping
|
|
288
392
|
const MCP_TOOL_MAP: Record<string, string> = {
|
|
289
393
|
navigate: 'browser_navigate',
|
|
@@ -315,7 +419,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
315
419
|
const saveTo = typeof params.saveTo === 'string' && params.saveTo.trim()
|
|
316
420
|
? params.saveTo.trim()
|
|
317
421
|
: undefined
|
|
318
|
-
|
|
422
|
+
const result = await callMcpTool(mcpTool, args, { saveTo })
|
|
423
|
+
|
|
424
|
+
// After navigation, attempt to dismiss cookie consent banners
|
|
425
|
+
if (action === 'navigate') {
|
|
426
|
+
try { await dismissCookieBanners(callMcpTool) } catch { /* best-effort */ }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return result
|
|
319
430
|
} catch (err: unknown) {
|
|
320
431
|
return `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
321
432
|
}
|
|
@@ -20,6 +20,7 @@ const DB_PATH = IS_BUILD_BOOTSTRAP ? ':memory:' : path.join(DATA_DIR, 'swarmclaw
|
|
|
20
20
|
const db = new Database(DB_PATH)
|
|
21
21
|
if (!IS_BUILD_BOOTSTRAP) {
|
|
22
22
|
db.pragma('journal_mode = WAL')
|
|
23
|
+
db.pragma('busy_timeout = 5000')
|
|
23
24
|
}
|
|
24
25
|
db.pragma('foreign_keys = ON')
|
|
25
26
|
|
|
@@ -52,6 +53,7 @@ const COLLECTIONS = [
|
|
|
52
53
|
'activity',
|
|
53
54
|
'webhook_retry_queue',
|
|
54
55
|
'notifications',
|
|
56
|
+
'chatrooms',
|
|
55
57
|
] as const
|
|
56
58
|
|
|
57
59
|
for (const table of COLLECTIONS) {
|
|
@@ -284,35 +286,37 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
284
286
|
description: 'A general-purpose AI assistant',
|
|
285
287
|
provider: 'claude-cli',
|
|
286
288
|
model: '',
|
|
287
|
-
systemPrompt: `You are the
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
- **
|
|
292
|
-
- **Providers
|
|
293
|
-
- **Tasks
|
|
294
|
-
- **Schedules
|
|
295
|
-
- **Skills
|
|
296
|
-
- **Connectors
|
|
297
|
-
- **Secrets
|
|
298
|
-
|
|
299
|
-
##
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- **manage_agents**: List, create, update, or delete agents.
|
|
304
|
-
- **manage_tasks**: Create and manage task board items. Set
|
|
305
|
-
- **manage_schedules**: Create recurring or one-time scheduled jobs
|
|
306
|
-
- **manage_skills**:
|
|
307
|
-
- **manage_documents**: Upload
|
|
308
|
-
- **manage_webhooks**: Register
|
|
309
|
-
- **manage_connectors**: Manage chat platform bridges
|
|
310
|
-
- **manage_sessions**:
|
|
311
|
-
- **manage_secrets**: Store and retrieve encrypted
|
|
312
|
-
- **memory_tool**: Store and retrieve long-term
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
289
|
+
systemPrompt: `You are the SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
|
|
290
|
+
|
|
291
|
+
## Platform
|
|
292
|
+
|
|
293
|
+
- **Agents** — Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
|
|
294
|
+
- **Providers** — Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
|
|
295
|
+
- **Tasks** — The Task Board tracks work items. Assign agents and they'll execute autonomously.
|
|
296
|
+
- **Schedules** — Cron-based recurring jobs that run agents or tasks automatically.
|
|
297
|
+
- **Skills** — Reusable markdown instruction files you attach to agents to specialize them.
|
|
298
|
+
- **Connectors** — Bridge agents to Discord, Slack, Telegram, or WhatsApp.
|
|
299
|
+
- **Secrets** — Encrypted vault for API keys (Settings → Secrets).
|
|
300
|
+
|
|
301
|
+
## Tools
|
|
302
|
+
|
|
303
|
+
Use your platform management tools proactively:
|
|
304
|
+
|
|
305
|
+
- **manage_agents**: List, create, update, or delete agents.
|
|
306
|
+
- **manage_tasks**: Create and manage task board items. Set status (backlog → queued → running → completed/failed) and assign agents.
|
|
307
|
+
- **manage_schedules**: Create recurring or one-time scheduled jobs with cron expressions or intervals.
|
|
308
|
+
- **manage_skills**: Manage reusable skill definitions.
|
|
309
|
+
- **manage_documents**: Upload, index, and search long-lived documents.
|
|
310
|
+
- **manage_webhooks**: Register webhook endpoints that trigger agent runs.
|
|
311
|
+
- **manage_connectors**: Manage chat platform bridges.
|
|
312
|
+
- **manage_sessions**: List chats, send inter-chat messages, spawn new agent chats.
|
|
313
|
+
- **manage_secrets**: Store and retrieve encrypted credentials.
|
|
314
|
+
- **memory_tool**: Store and retrieve long-term knowledge.`,
|
|
315
|
+
soul: `You're a knowledgeable, friendly guide who's genuinely enthusiastic about helping people build agent workflows. You adapt your tone to match the conversation — casual when exploring, precise when debugging, encouraging when learning.
|
|
316
|
+
|
|
317
|
+
You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
|
|
318
|
+
|
|
319
|
+
Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
|
|
316
320
|
isOrchestrator: false,
|
|
317
321
|
tools: defaultStarterTools,
|
|
318
322
|
heartbeatEnabled: true,
|
|
@@ -691,6 +695,15 @@ export function saveConnectors(c: Record<string, any>) {
|
|
|
691
695
|
saveCollection('connectors', c)
|
|
692
696
|
}
|
|
693
697
|
|
|
698
|
+
// --- Chatrooms ---
|
|
699
|
+
export function loadChatrooms(): Record<string, any> {
|
|
700
|
+
return loadCollection('chatrooms')
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function saveChatrooms(c: Record<string, any>) {
|
|
704
|
+
saveCollection('chatrooms', c)
|
|
705
|
+
}
|
|
706
|
+
|
|
694
707
|
// --- Documents ---
|
|
695
708
|
export function loadDocuments(): Record<string, any> {
|
|
696
709
|
return loadCollection('documents')
|
|
@@ -789,6 +802,17 @@ export function deleteNotification(id: string) {
|
|
|
789
802
|
deleteCollectionItem('notifications', id)
|
|
790
803
|
}
|
|
791
804
|
|
|
805
|
+
export function hasUnreadNotificationWithKey(dedupKey: string): boolean {
|
|
806
|
+
const raw = getCollectionRawCache('notifications')
|
|
807
|
+
for (const json of raw.values()) {
|
|
808
|
+
try {
|
|
809
|
+
const n = JSON.parse(json) as Record<string, unknown>
|
|
810
|
+
if (n.dedupKey === dedupKey && n.read !== true) return true
|
|
811
|
+
} catch { /* skip malformed */ }
|
|
812
|
+
}
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
815
|
+
|
|
792
816
|
export function markNotificationRead(id: string) {
|
|
793
817
|
const raw = getCollectionRawCache('notifications')
|
|
794
818
|
const json = raw.get(id)
|
|
@@ -9,9 +9,24 @@ import { getPluginManager } from './plugins'
|
|
|
9
9
|
import { loadRuntimeSettings, getAgentLoopRecursionLimit } from './runtime-settings'
|
|
10
10
|
import { getMemoryDb } from './memory-db'
|
|
11
11
|
import { logExecution } from './execution-log'
|
|
12
|
+
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
12
13
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
14
|
import { extractSuggestions } from './suggestions'
|
|
14
15
|
|
|
16
|
+
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
17
|
+
function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
|
|
18
|
+
if (!input || typeof input !== 'object') return null
|
|
19
|
+
const inp = input as Record<string, unknown>
|
|
20
|
+
const action = typeof inp.action === 'string' ? inp.action : ''
|
|
21
|
+
if (toolName === 'manage_tasks') {
|
|
22
|
+
if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
|
|
23
|
+
if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
|
|
24
|
+
}
|
|
25
|
+
if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
|
|
26
|
+
if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
interface StreamAgentChatOpts {
|
|
16
31
|
session: Session
|
|
17
32
|
message: string
|
|
@@ -25,7 +40,7 @@ interface StreamAgentChatOpts {
|
|
|
25
40
|
signal?: AbortSignal
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
43
|
+
function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
|
|
29
44
|
const lines: string[] = []
|
|
30
45
|
if (enabledTools.includes('shell')) lines.push('- Shell execution is available (`execute_command`). Use it for running servers, installing deps, running scripts, git commands, build/test steps, and any single or chained shell commands. Supports background mode for long-running processes like dev servers.')
|
|
31
46
|
if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
|
|
@@ -52,9 +67,12 @@ function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
|
52
67
|
// Context tools are available to any session with tools (not just manage_sessions)
|
|
53
68
|
if (enabledTools.length > 0) {
|
|
54
69
|
lines.push('- Context management is available (`context_status`, `context_summarize`). Use `context_status` to check token usage and `context_summarize` to compact conversation history when approaching limits.')
|
|
55
|
-
|
|
70
|
+
if (opts?.platformAssignScope === 'all') {
|
|
71
|
+
lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
|
|
72
|
+
}
|
|
56
73
|
}
|
|
57
74
|
if (enabledTools.includes('manage_secrets')) lines.push('- Secret management is available (`manage_secrets`) for durable encrypted credentials and API tokens.')
|
|
75
|
+
if (enabledTools.includes('manage_chatrooms')) lines.push('- Chatroom management is available (`manage_chatrooms`) for multi-agent collaborative chatrooms with @mention-based interactions.')
|
|
58
76
|
return lines
|
|
59
77
|
}
|
|
60
78
|
|
|
@@ -63,9 +81,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
63
81
|
loopMode: 'bounded' | 'ongoing'
|
|
64
82
|
heartbeatPrompt: string
|
|
65
83
|
heartbeatIntervalSec: number
|
|
84
|
+
platformAssignScope?: 'self' | 'all'
|
|
66
85
|
}) {
|
|
67
86
|
const hasTooling = opts.enabledTools.length > 0
|
|
68
|
-
const toolLines = buildToolCapabilityLines(opts.enabledTools)
|
|
87
|
+
const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
|
|
69
88
|
const delegationOrder = [
|
|
70
89
|
opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
|
|
71
90
|
opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
|
|
@@ -209,6 +228,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
209
228
|
stateModifierParts.push(systemPrompt!.trim())
|
|
210
229
|
} else {
|
|
211
230
|
if (settings.userPrompt) stateModifierParts.push(settings.userPrompt)
|
|
231
|
+
stateModifierParts.push(buildCurrentDateTimePromptContext())
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
// Load agent context when a full prompt was not already composed by the route layer.
|
|
@@ -260,57 +280,92 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
260
280
|
.map((h) => h.text),
|
|
261
281
|
].join('\n')
|
|
262
282
|
|
|
263
|
-
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
264
|
-
const relevant = relevantLookup.entries.slice(0, 6)
|
|
265
|
-
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
266
|
-
|
|
267
283
|
const seen = new Set<string>()
|
|
268
|
-
const formatMemoryLine = (m:
|
|
284
|
+
const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
|
|
269
285
|
const category = String(m.category || 'note')
|
|
270
286
|
const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
|
|
271
287
|
const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
272
|
-
|
|
288
|
+
const pin = m.pinned ? ' [pinned]' : ''
|
|
289
|
+
return `- [${category}]${pin} ${title}: ${snippet}`
|
|
273
290
|
}
|
|
274
291
|
|
|
292
|
+
// Pinned memories always appear first
|
|
293
|
+
const pinned = memDb.listPinned(session.agentId, 5)
|
|
294
|
+
const pinnedLines = pinned
|
|
295
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
296
|
+
.map(formatMemoryLine)
|
|
297
|
+
|
|
298
|
+
// Reduce relevant slice by pinned count to keep total context bounded
|
|
299
|
+
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
300
|
+
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
301
|
+
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
302
|
+
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
303
|
+
|
|
275
304
|
const relevantLines = relevant
|
|
276
|
-
.filter((m) => {
|
|
277
|
-
if (!m?.id || seen.has(m.id)) return false
|
|
278
|
-
seen.add(m.id)
|
|
279
|
-
return true
|
|
280
|
-
})
|
|
305
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
281
306
|
.map(formatMemoryLine)
|
|
282
307
|
|
|
283
308
|
const recentLines = recent
|
|
284
|
-
.filter((m) => {
|
|
285
|
-
if (!m?.id || seen.has(m.id)) return false
|
|
286
|
-
seen.add(m.id)
|
|
287
|
-
return true
|
|
288
|
-
})
|
|
309
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
289
310
|
.map(formatMemoryLine)
|
|
290
311
|
|
|
291
312
|
const memorySections: string[] = []
|
|
313
|
+
if (pinnedLines.length) {
|
|
314
|
+
memorySections.push(
|
|
315
|
+
['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
|
|
316
|
+
)
|
|
317
|
+
}
|
|
292
318
|
if (relevantLines.length) {
|
|
293
319
|
memorySections.push(
|
|
294
|
-
[
|
|
295
|
-
'## Relevant Memory Hits',
|
|
296
|
-
'These memories were retrieved by relevance for the current objective.',
|
|
297
|
-
...relevantLines,
|
|
298
|
-
].join('\n'),
|
|
320
|
+
['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
|
|
299
321
|
)
|
|
300
322
|
}
|
|
301
323
|
if (recentLines.length) {
|
|
302
324
|
memorySections.push(
|
|
303
|
-
[
|
|
304
|
-
'## Recent Memory Notes',
|
|
305
|
-
'Recent durable notes that may still apply.',
|
|
306
|
-
...recentLines,
|
|
307
|
-
].join('\n'),
|
|
325
|
+
['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
|
|
308
326
|
)
|
|
309
327
|
}
|
|
310
328
|
|
|
311
329
|
if (memorySections.length) {
|
|
312
330
|
stateModifierParts.push(memorySections.join('\n\n'))
|
|
313
331
|
}
|
|
332
|
+
|
|
333
|
+
// Memory Policy — always injected when memory tool is available
|
|
334
|
+
stateModifierParts.push([
|
|
335
|
+
'## Memory Policy',
|
|
336
|
+
'You have long-term memory. Use it proactively — do not wait to be asked.',
|
|
337
|
+
'',
|
|
338
|
+
'**Store memories for:**',
|
|
339
|
+
'- User preferences, corrections, or explicit "remember this" requests',
|
|
340
|
+
'- Key decisions or outcomes from complex tasks',
|
|
341
|
+
'- Discovered facts about projects, codebases, or environments',
|
|
342
|
+
'- Errors encountered and their solutions',
|
|
343
|
+
'- Relationship context (who is who, team dynamics)',
|
|
344
|
+
'- Important configuration details or environment specifics',
|
|
345
|
+
'',
|
|
346
|
+
'**Do NOT store:**',
|
|
347
|
+
'- Trivial acknowledgments or small talk',
|
|
348
|
+
'- Temporary in-progress work (use category "working" for ephemeral notes)',
|
|
349
|
+
'- Information already in your system prompt',
|
|
350
|
+
'- Exact duplicates of memories you already have',
|
|
351
|
+
'',
|
|
352
|
+
'**Best practices:**',
|
|
353
|
+
'- Use descriptive titles ("User prefers dark mode" not "Note 1")',
|
|
354
|
+
'- Use categories: preference, fact, learning, project, identity, decision',
|
|
355
|
+
'- Search memory before storing to avoid duplicates',
|
|
356
|
+
'- When correcting old knowledge, update or delete the old memory',
|
|
357
|
+
].join('\n'))
|
|
358
|
+
|
|
359
|
+
// Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
|
|
360
|
+
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
361
|
+
if (msgCount > 20) {
|
|
362
|
+
stateModifierParts.push([
|
|
363
|
+
'## Memory Flush Reminder',
|
|
364
|
+
'This conversation is getting long. Before context is trimmed, store any important',
|
|
365
|
+
'learnings, decisions, or facts as memories now. Only store what is significant and durable —',
|
|
366
|
+
'skip trivial details. If nothing needs storing, continue normally.',
|
|
367
|
+
].join('\n'))
|
|
368
|
+
}
|
|
314
369
|
} catch {
|
|
315
370
|
// If memory context fails to load, continue without blocking the run.
|
|
316
371
|
}
|
|
@@ -347,15 +402,17 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
347
402
|
}
|
|
348
403
|
}
|
|
349
404
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
405
|
+
if (settings.suggestionsEnabled !== false) {
|
|
406
|
+
stateModifierParts.push(
|
|
407
|
+
[
|
|
408
|
+
'## Follow-up Suggestions',
|
|
409
|
+
'At the end of every response, include a <suggestions> block with exactly 3 short',
|
|
410
|
+
'follow-up prompts the user might want to send next, as a JSON array. Keep each under 60 chars.',
|
|
411
|
+
'Make them contextual to what you just said. Example:',
|
|
412
|
+
'<suggestions>["Set up a Discord connector", "Create a research agent", "Show the task board"]</suggestions>',
|
|
413
|
+
].join('\n'),
|
|
414
|
+
)
|
|
415
|
+
}
|
|
359
416
|
|
|
360
417
|
stateModifierParts.push(
|
|
361
418
|
buildAgenticExecutionPolicy({
|
|
@@ -363,6 +420,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
363
420
|
loopMode: runtime.loopMode,
|
|
364
421
|
heartbeatPrompt,
|
|
365
422
|
heartbeatIntervalSec,
|
|
423
|
+
platformAssignScope: agentPlatformAssignScope,
|
|
366
424
|
}),
|
|
367
425
|
)
|
|
368
426
|
|
|
@@ -463,24 +521,49 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
463
521
|
// Auto-compaction: prune old history if approaching context window limit
|
|
464
522
|
let effectiveHistory = history
|
|
465
523
|
try {
|
|
466
|
-
const { shouldAutoCompact,
|
|
524
|
+
const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
|
|
467
525
|
const systemPromptTokens = estimateTokens(stateModifier)
|
|
468
526
|
if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
527
|
+
const summarize = async (prompt: string): Promise<string> => {
|
|
528
|
+
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
529
|
+
if (typeof response.content === 'string') return response.content
|
|
530
|
+
if (Array.isArray(response.content)) {
|
|
531
|
+
return response.content
|
|
532
|
+
.map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
|
|
533
|
+
.join('')
|
|
534
|
+
}
|
|
535
|
+
return ''
|
|
473
536
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
537
|
+
const result = await llmCompact({
|
|
538
|
+
messages: history,
|
|
539
|
+
provider: session.provider,
|
|
540
|
+
model: session.model,
|
|
541
|
+
agentId: session.agentId || null,
|
|
542
|
+
sessionId: session.id,
|
|
543
|
+
summarize,
|
|
544
|
+
})
|
|
545
|
+
effectiveHistory = result.messages
|
|
546
|
+
console.log(
|
|
547
|
+
`[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
|
|
548
|
+
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
549
|
+
)
|
|
477
550
|
}
|
|
478
551
|
} catch {
|
|
479
|
-
//
|
|
552
|
+
// Context manager failure — continue with full history
|
|
480
553
|
}
|
|
481
554
|
|
|
555
|
+
// Apply context-clear boundary: slice from most recent context-clear marker
|
|
556
|
+
let contextStart = 0
|
|
557
|
+
for (let i = effectiveHistory.length - 1; i >= 0; i--) {
|
|
558
|
+
if (effectiveHistory[i].kind === 'context-clear') {
|
|
559
|
+
contextStart = i + 1
|
|
560
|
+
break
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const postClearHistory = effectiveHistory.slice(contextStart)
|
|
564
|
+
|
|
482
565
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
483
|
-
for (const m of
|
|
566
|
+
for (const m of postClearHistory.slice(-20)) {
|
|
484
567
|
if (m.role === 'user') {
|
|
485
568
|
langchainMessages.push(new HumanMessage({ content: await buildLangChainContent(m.text, m.imagePath, m.attachedFiles) }))
|
|
486
569
|
} else {
|
|
@@ -497,6 +580,8 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
497
580
|
let hasToolCalls = false
|
|
498
581
|
let totalInputTokens = 0
|
|
499
582
|
let totalOutputTokens = 0
|
|
583
|
+
let lastToolInput: unknown = null
|
|
584
|
+
let accumulatedThinking = ''
|
|
500
585
|
|
|
501
586
|
// Plugin hooks: beforeAgentStart
|
|
502
587
|
const pluginMgr = getPluginManager()
|
|
@@ -529,15 +614,29 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
529
614
|
const chunk = event.data?.chunk
|
|
530
615
|
if (chunk?.content) {
|
|
531
616
|
// content can be string or array of content blocks
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
617
|
+
if (Array.isArray(chunk.content)) {
|
|
618
|
+
for (const block of chunk.content) {
|
|
619
|
+
// Anthropic extended thinking blocks
|
|
620
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
621
|
+
accumulatedThinking += block.thinking
|
|
622
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
623
|
+
// OpenClaw [[thinking]] prefix convention
|
|
624
|
+
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
625
|
+
accumulatedThinking += block.text.slice(12)
|
|
626
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
627
|
+
} else if (block.text) {
|
|
628
|
+
fullText += block.text
|
|
629
|
+
lastSegment += block.text
|
|
630
|
+
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
const text = typeof chunk.content === 'string' ? chunk.content : ''
|
|
635
|
+
if (text) {
|
|
636
|
+
fullText += text
|
|
637
|
+
lastSegment += text
|
|
638
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
639
|
+
}
|
|
541
640
|
}
|
|
542
641
|
}
|
|
543
642
|
} else if (kind === 'on_llm_end') {
|
|
@@ -554,6 +653,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
554
653
|
lastSegment = ''
|
|
555
654
|
const toolName = event.name || 'unknown'
|
|
556
655
|
const input = event.data?.input
|
|
656
|
+
lastToolInput = input
|
|
557
657
|
// Plugin hooks: beforeToolExec
|
|
558
658
|
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
559
659
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
@@ -576,6 +676,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
576
676
|
: JSON.stringify(output)
|
|
577
677
|
// Plugin hooks: afterToolExec
|
|
578
678
|
await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
|
|
679
|
+
// Event-driven memory breadcrumbs
|
|
680
|
+
if (session.agentId && (session.tools || []).includes('memory')) {
|
|
681
|
+
try {
|
|
682
|
+
const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
|
|
683
|
+
if (breadcrumbTitle) {
|
|
684
|
+
const memDb = getMemoryDb()
|
|
685
|
+
memDb.add({
|
|
686
|
+
agentId: session.agentId,
|
|
687
|
+
sessionId: session.id,
|
|
688
|
+
category: 'breadcrumb',
|
|
689
|
+
title: breadcrumbTitle,
|
|
690
|
+
content: '',
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
} catch { /* breadcrumbs are best-effort */ }
|
|
694
|
+
}
|
|
695
|
+
lastToolInput = null
|
|
579
696
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
580
697
|
agentId: session.agentId,
|
|
581
698
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -617,6 +734,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
617
734
|
if (signal) signal.removeEventListener('abort', abortFromSignal)
|
|
618
735
|
}
|
|
619
736
|
|
|
737
|
+
// Skip post-stream work if the client disconnected mid-stream
|
|
738
|
+
if (signal?.aborted) {
|
|
739
|
+
await cleanup()
|
|
740
|
+
return { fullText, finalResponse: fullText }
|
|
741
|
+
}
|
|
742
|
+
|
|
620
743
|
// Extract LLM-generated suggestions from the response and strip the tag
|
|
621
744
|
const extracted = extractSuggestions(fullText)
|
|
622
745
|
fullText = extracted.clean
|
|
@@ -624,6 +747,11 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
624
747
|
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ suggestions: extracted.suggestions }) })}\n\n`)
|
|
625
748
|
}
|
|
626
749
|
|
|
750
|
+
// Emit full thinking text as metadata so the client can persist it
|
|
751
|
+
if (accumulatedThinking) {
|
|
752
|
+
write(`data: ${JSON.stringify({ t: 'md', text: JSON.stringify({ thinking: accumulatedThinking }) })}\n\n`)
|
|
753
|
+
}
|
|
754
|
+
|
|
627
755
|
// Track cost
|
|
628
756
|
const totalTokens = totalInputTokens + totalOutputTokens
|
|
629
757
|
if (totalTokens > 0) {
|