@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -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/credentials/route.ts +2 -3
- 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/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -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 +155 -0
- 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/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 +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- 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/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- 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 +66 -70
- 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 +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -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-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- 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 +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- 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 +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- 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/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/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- 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/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- 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 +296 -0
- 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-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- 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 +46 -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 +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -15,7 +15,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
15
15
|
|
|
16
16
|
tools.push(
|
|
17
17
|
tool(
|
|
18
|
-
async (
|
|
18
|
+
async (input) => {
|
|
19
|
+
const { action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags, pinned, sharedWith } = input as Record<string, any>
|
|
19
20
|
try {
|
|
20
21
|
const scopeMode = scope || 'auto'
|
|
21
22
|
const currentAgentId = ctx?.agentId || null
|
|
@@ -98,6 +99,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
98
99
|
image: storedImage,
|
|
99
100
|
imagePath: storedImage?.path || undefined,
|
|
100
101
|
linkedMemoryIds,
|
|
102
|
+
pinned: pinned === true,
|
|
103
|
+
sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
|
|
101
104
|
})
|
|
102
105
|
const memoryScope = entry.agentId ? 'agent' : 'shared'
|
|
103
106
|
let result = `Stored ${memoryScope} memory "${key}" (id: ${entry.id})`
|
|
@@ -220,6 +223,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
|
|
|
220
223
|
linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
|
|
221
224
|
targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
|
|
222
225
|
tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
|
|
226
|
+
pinned: z.boolean().optional().describe('Mark memory as pinned (always preloaded in agent context). For store action.'),
|
|
227
|
+
sharedWith: z.array(z.string()).optional().describe('Agent IDs to share this memory with (for store action). They can read it in their context.'),
|
|
223
228
|
}),
|
|
224
229
|
},
|
|
225
230
|
),
|
|
@@ -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
|
|
|
@@ -51,6 +52,8 @@ const COLLECTIONS = [
|
|
|
51
52
|
'projects',
|
|
52
53
|
'activity',
|
|
53
54
|
'webhook_retry_queue',
|
|
55
|
+
'notifications',
|
|
56
|
+
'chatrooms',
|
|
54
57
|
] as const
|
|
55
58
|
|
|
56
59
|
for (const table of COLLECTIONS) {
|
|
@@ -283,35 +286,37 @@ if (!IS_BUILD_BOOTSTRAP) {
|
|
|
283
286
|
description: 'A general-purpose AI assistant',
|
|
284
287
|
provider: 'claude-cli',
|
|
285
288
|
model: '',
|
|
286
|
-
systemPrompt: `You are the
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
- **
|
|
291
|
-
- **Providers
|
|
292
|
-
- **Tasks
|
|
293
|
-
- **Schedules
|
|
294
|
-
- **Skills
|
|
295
|
-
- **Connectors
|
|
296
|
-
- **Secrets
|
|
297
|
-
|
|
298
|
-
##
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
- **manage_agents**: List, create, update, or delete agents.
|
|
303
|
-
- **manage_tasks**: Create and manage task board items. Set
|
|
304
|
-
- **manage_schedules**: Create recurring or one-time scheduled jobs
|
|
305
|
-
- **manage_skills**:
|
|
306
|
-
- **manage_documents**: Upload
|
|
307
|
-
- **manage_webhooks**: Register
|
|
308
|
-
- **manage_connectors**: Manage chat platform bridges
|
|
309
|
-
- **manage_sessions**:
|
|
310
|
-
- **manage_secrets**: Store and retrieve encrypted
|
|
311
|
-
- **memory_tool**: Store and retrieve long-term
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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.`,
|
|
315
320
|
isOrchestrator: false,
|
|
316
321
|
tools: defaultStarterTools,
|
|
317
322
|
heartbeatEnabled: true,
|
|
@@ -690,6 +695,15 @@ export function saveConnectors(c: Record<string, any>) {
|
|
|
690
695
|
saveCollection('connectors', c)
|
|
691
696
|
}
|
|
692
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
|
+
|
|
693
707
|
// --- Documents ---
|
|
694
708
|
export function loadDocuments(): Record<string, any> {
|
|
695
709
|
return loadCollection('documents')
|
|
@@ -775,6 +789,43 @@ export function deleteWebhookRetry(id: string) {
|
|
|
775
789
|
deleteCollectionItem('webhook_retry_queue', id)
|
|
776
790
|
}
|
|
777
791
|
|
|
792
|
+
// --- Notifications ---
|
|
793
|
+
export function loadNotifications(): Record<string, unknown> {
|
|
794
|
+
return loadCollection('notifications')
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function saveNotification(id: string, data: unknown) {
|
|
798
|
+
upsertCollectionItem('notifications', id, data)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export function deleteNotification(id: string) {
|
|
802
|
+
deleteCollectionItem('notifications', id)
|
|
803
|
+
}
|
|
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
|
+
|
|
816
|
+
export function markNotificationRead(id: string) {
|
|
817
|
+
const raw = getCollectionRawCache('notifications')
|
|
818
|
+
const json = raw.get(id)
|
|
819
|
+
if (!json) return
|
|
820
|
+
try {
|
|
821
|
+
const notification = JSON.parse(json) as Record<string, unknown>
|
|
822
|
+
notification.read = true
|
|
823
|
+
upsertCollectionItem('notifications', id, notification)
|
|
824
|
+
} catch {
|
|
825
|
+
// ignore malformed
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
778
829
|
export function getSessionMessages(sessionId: string): Message[] {
|
|
779
830
|
const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
|
|
780
831
|
const row = stmt.get(sessionId) as { data: string } | undefined
|
|
@@ -12,6 +12,20 @@ import { logExecution } from './execution-log'
|
|
|
12
12
|
import type { Session, Message, UsageRecord } from '@/types'
|
|
13
13
|
import { extractSuggestions } from './suggestions'
|
|
14
14
|
|
|
15
|
+
/** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
|
|
16
|
+
function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
|
|
17
|
+
if (!input || typeof input !== 'object') return null
|
|
18
|
+
const inp = input as Record<string, unknown>
|
|
19
|
+
const action = typeof inp.action === 'string' ? inp.action : ''
|
|
20
|
+
if (toolName === 'manage_tasks') {
|
|
21
|
+
if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
|
|
22
|
+
if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
|
|
23
|
+
}
|
|
24
|
+
if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
|
|
25
|
+
if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
interface StreamAgentChatOpts {
|
|
16
30
|
session: Session
|
|
17
31
|
message: string
|
|
@@ -25,7 +39,7 @@ interface StreamAgentChatOpts {
|
|
|
25
39
|
signal?: AbortSignal
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
42
|
+
function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
|
|
29
43
|
const lines: string[] = []
|
|
30
44
|
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
45
|
if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
|
|
@@ -52,9 +66,12 @@ function buildToolCapabilityLines(enabledTools: string[]): string[] {
|
|
|
52
66
|
// Context tools are available to any session with tools (not just manage_sessions)
|
|
53
67
|
if (enabledTools.length > 0) {
|
|
54
68
|
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
|
-
|
|
69
|
+
if (opts?.platformAssignScope === 'all') {
|
|
70
|
+
lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
|
|
71
|
+
}
|
|
56
72
|
}
|
|
57
73
|
if (enabledTools.includes('manage_secrets')) lines.push('- Secret management is available (`manage_secrets`) for durable encrypted credentials and API tokens.')
|
|
74
|
+
if (enabledTools.includes('manage_chatrooms')) lines.push('- Chatroom management is available (`manage_chatrooms`) for multi-agent collaborative chatrooms with @mention-based interactions.')
|
|
58
75
|
return lines
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -63,9 +80,10 @@ function buildAgenticExecutionPolicy(opts: {
|
|
|
63
80
|
loopMode: 'bounded' | 'ongoing'
|
|
64
81
|
heartbeatPrompt: string
|
|
65
82
|
heartbeatIntervalSec: number
|
|
83
|
+
platformAssignScope?: 'self' | 'all'
|
|
66
84
|
}) {
|
|
67
85
|
const hasTooling = opts.enabledTools.length > 0
|
|
68
|
-
const toolLines = buildToolCapabilityLines(opts.enabledTools)
|
|
86
|
+
const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
|
|
69
87
|
const delegationOrder = [
|
|
70
88
|
opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
|
|
71
89
|
opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
|
|
@@ -260,57 +278,92 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
260
278
|
.map((h) => h.text),
|
|
261
279
|
].join('\n')
|
|
262
280
|
|
|
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
281
|
const seen = new Set<string>()
|
|
268
|
-
const formatMemoryLine = (m:
|
|
282
|
+
const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
|
|
269
283
|
const category = String(m.category || 'note')
|
|
270
284
|
const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
|
|
271
285
|
const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
|
|
272
|
-
|
|
286
|
+
const pin = m.pinned ? ' [pinned]' : ''
|
|
287
|
+
return `- [${category}]${pin} ${title}: ${snippet}`
|
|
273
288
|
}
|
|
274
289
|
|
|
290
|
+
// Pinned memories always appear first
|
|
291
|
+
const pinned = memDb.listPinned(session.agentId, 5)
|
|
292
|
+
const pinnedLines = pinned
|
|
293
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
294
|
+
.map(formatMemoryLine)
|
|
295
|
+
|
|
296
|
+
// Reduce relevant slice by pinned count to keep total context bounded
|
|
297
|
+
const relevantSlice = Math.max(2, 6 - pinnedLines.length)
|
|
298
|
+
const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
|
|
299
|
+
const relevant = relevantLookup.entries.slice(0, relevantSlice)
|
|
300
|
+
const recent = memDb.list(session.agentId, 12).slice(0, 6)
|
|
301
|
+
|
|
275
302
|
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
|
-
})
|
|
303
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
281
304
|
.map(formatMemoryLine)
|
|
282
305
|
|
|
283
306
|
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
|
-
})
|
|
307
|
+
.filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
|
|
289
308
|
.map(formatMemoryLine)
|
|
290
309
|
|
|
291
310
|
const memorySections: string[] = []
|
|
311
|
+
if (pinnedLines.length) {
|
|
312
|
+
memorySections.push(
|
|
313
|
+
['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
|
|
314
|
+
)
|
|
315
|
+
}
|
|
292
316
|
if (relevantLines.length) {
|
|
293
317
|
memorySections.push(
|
|
294
|
-
[
|
|
295
|
-
'## Relevant Memory Hits',
|
|
296
|
-
'These memories were retrieved by relevance for the current objective.',
|
|
297
|
-
...relevantLines,
|
|
298
|
-
].join('\n'),
|
|
318
|
+
['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
|
|
299
319
|
)
|
|
300
320
|
}
|
|
301
321
|
if (recentLines.length) {
|
|
302
322
|
memorySections.push(
|
|
303
|
-
[
|
|
304
|
-
'## Recent Memory Notes',
|
|
305
|
-
'Recent durable notes that may still apply.',
|
|
306
|
-
...recentLines,
|
|
307
|
-
].join('\n'),
|
|
323
|
+
['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
|
|
308
324
|
)
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
if (memorySections.length) {
|
|
312
328
|
stateModifierParts.push(memorySections.join('\n\n'))
|
|
313
329
|
}
|
|
330
|
+
|
|
331
|
+
// Memory Policy — always injected when memory tool is available
|
|
332
|
+
stateModifierParts.push([
|
|
333
|
+
'## Memory Policy',
|
|
334
|
+
'You have long-term memory. Use it proactively — do not wait to be asked.',
|
|
335
|
+
'',
|
|
336
|
+
'**Store memories for:**',
|
|
337
|
+
'- User preferences, corrections, or explicit "remember this" requests',
|
|
338
|
+
'- Key decisions or outcomes from complex tasks',
|
|
339
|
+
'- Discovered facts about projects, codebases, or environments',
|
|
340
|
+
'- Errors encountered and their solutions',
|
|
341
|
+
'- Relationship context (who is who, team dynamics)',
|
|
342
|
+
'- Important configuration details or environment specifics',
|
|
343
|
+
'',
|
|
344
|
+
'**Do NOT store:**',
|
|
345
|
+
'- Trivial acknowledgments or small talk',
|
|
346
|
+
'- Temporary in-progress work (use category "working" for ephemeral notes)',
|
|
347
|
+
'- Information already in your system prompt',
|
|
348
|
+
'- Exact duplicates of memories you already have',
|
|
349
|
+
'',
|
|
350
|
+
'**Best practices:**',
|
|
351
|
+
'- Use descriptive titles ("User prefers dark mode" not "Note 1")',
|
|
352
|
+
'- Use categories: preference, fact, learning, project, identity, decision',
|
|
353
|
+
'- Search memory before storing to avoid duplicates',
|
|
354
|
+
'- When correcting old knowledge, update or delete the old memory',
|
|
355
|
+
].join('\n'))
|
|
356
|
+
|
|
357
|
+
// Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
|
|
358
|
+
const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
|
|
359
|
+
if (msgCount > 20) {
|
|
360
|
+
stateModifierParts.push([
|
|
361
|
+
'## Memory Flush Reminder',
|
|
362
|
+
'This conversation is getting long. Before context is trimmed, store any important',
|
|
363
|
+
'learnings, decisions, or facts as memories now. Only store what is significant and durable —',
|
|
364
|
+
'skip trivial details. If nothing needs storing, continue normally.',
|
|
365
|
+
].join('\n'))
|
|
366
|
+
}
|
|
314
367
|
} catch {
|
|
315
368
|
// If memory context fails to load, continue without blocking the run.
|
|
316
369
|
}
|
|
@@ -363,6 +416,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
363
416
|
loopMode: runtime.loopMode,
|
|
364
417
|
heartbeatPrompt,
|
|
365
418
|
heartbeatIntervalSec,
|
|
419
|
+
platformAssignScope: agentPlatformAssignScope,
|
|
366
420
|
}),
|
|
367
421
|
)
|
|
368
422
|
|
|
@@ -463,20 +517,35 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
463
517
|
// Auto-compaction: prune old history if approaching context window limit
|
|
464
518
|
let effectiveHistory = history
|
|
465
519
|
try {
|
|
466
|
-
const { shouldAutoCompact,
|
|
520
|
+
const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
|
|
467
521
|
const systemPromptTokens = estimateTokens(stateModifier)
|
|
468
522
|
if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
523
|
+
const summarize = async (prompt: string): Promise<string> => {
|
|
524
|
+
const response = await llm.invoke([new HumanMessage(prompt)])
|
|
525
|
+
if (typeof response.content === 'string') return response.content
|
|
526
|
+
if (Array.isArray(response.content)) {
|
|
527
|
+
return response.content
|
|
528
|
+
.map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
|
|
529
|
+
.join('')
|
|
530
|
+
}
|
|
531
|
+
return ''
|
|
473
532
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
533
|
+
const result = await llmCompact({
|
|
534
|
+
messages: history,
|
|
535
|
+
provider: session.provider,
|
|
536
|
+
model: session.model,
|
|
537
|
+
agentId: session.agentId || null,
|
|
538
|
+
sessionId: session.id,
|
|
539
|
+
summarize,
|
|
540
|
+
})
|
|
541
|
+
effectiveHistory = result.messages
|
|
542
|
+
console.log(
|
|
543
|
+
`[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
|
|
544
|
+
(result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
|
|
545
|
+
)
|
|
477
546
|
}
|
|
478
547
|
} catch {
|
|
479
|
-
//
|
|
548
|
+
// Context manager failure — continue with full history
|
|
480
549
|
}
|
|
481
550
|
|
|
482
551
|
const langchainMessages: Array<HumanMessage | AIMessage> = []
|
|
@@ -497,6 +566,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
497
566
|
let hasToolCalls = false
|
|
498
567
|
let totalInputTokens = 0
|
|
499
568
|
let totalOutputTokens = 0
|
|
569
|
+
let lastToolInput: unknown = null
|
|
500
570
|
|
|
501
571
|
// Plugin hooks: beforeAgentStart
|
|
502
572
|
const pluginMgr = getPluginManager()
|
|
@@ -529,15 +599,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
529
599
|
const chunk = event.data?.chunk
|
|
530
600
|
if (chunk?.content) {
|
|
531
601
|
// content can be string or array of content blocks
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
602
|
+
if (Array.isArray(chunk.content)) {
|
|
603
|
+
for (const block of chunk.content) {
|
|
604
|
+
// Anthropic extended thinking blocks
|
|
605
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
606
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
|
|
607
|
+
// OpenClaw [[thinking]] prefix convention
|
|
608
|
+
} else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
|
|
609
|
+
write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
|
|
610
|
+
} else if (block.text) {
|
|
611
|
+
fullText += block.text
|
|
612
|
+
lastSegment += block.text
|
|
613
|
+
write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const text = typeof chunk.content === 'string' ? chunk.content : ''
|
|
618
|
+
if (text) {
|
|
619
|
+
fullText += text
|
|
620
|
+
lastSegment += text
|
|
621
|
+
write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
|
|
622
|
+
}
|
|
541
623
|
}
|
|
542
624
|
}
|
|
543
625
|
} else if (kind === 'on_llm_end') {
|
|
@@ -554,6 +636,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
554
636
|
lastSegment = ''
|
|
555
637
|
const toolName = event.name || 'unknown'
|
|
556
638
|
const input = event.data?.input
|
|
639
|
+
lastToolInput = input
|
|
557
640
|
// Plugin hooks: beforeToolExec
|
|
558
641
|
await pluginMgr.runHook('beforeToolExec', { toolName, input })
|
|
559
642
|
const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
|
|
@@ -576,6 +659,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
576
659
|
: JSON.stringify(output)
|
|
577
660
|
// Plugin hooks: afterToolExec
|
|
578
661
|
await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
|
|
662
|
+
// Event-driven memory breadcrumbs
|
|
663
|
+
if (session.agentId && (session.tools || []).includes('memory')) {
|
|
664
|
+
try {
|
|
665
|
+
const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
|
|
666
|
+
if (breadcrumbTitle) {
|
|
667
|
+
const memDb = getMemoryDb()
|
|
668
|
+
memDb.add({
|
|
669
|
+
agentId: session.agentId,
|
|
670
|
+
sessionId: session.id,
|
|
671
|
+
category: 'breadcrumb',
|
|
672
|
+
title: breadcrumbTitle,
|
|
673
|
+
content: '',
|
|
674
|
+
})
|
|
675
|
+
}
|
|
676
|
+
} catch { /* breadcrumbs are best-effort */ }
|
|
677
|
+
}
|
|
678
|
+
lastToolInput = null
|
|
579
679
|
logExecution(session.id, 'tool_result', `${toolName} returned`, {
|
|
580
680
|
agentId: session.agentId,
|
|
581
681
|
detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
|
|
@@ -617,6 +717,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
|
|
|
617
717
|
if (signal) signal.removeEventListener('abort', abortFromSignal)
|
|
618
718
|
}
|
|
619
719
|
|
|
720
|
+
// Skip post-stream work if the client disconnected mid-stream
|
|
721
|
+
if (signal?.aborted) {
|
|
722
|
+
await cleanup()
|
|
723
|
+
return { fullText, finalResponse: fullText }
|
|
724
|
+
}
|
|
725
|
+
|
|
620
726
|
// Extract LLM-generated suggestions from the response and strip the tag
|
|
621
727
|
const extracted = extractSuggestions(fullText)
|
|
622
728
|
fullText = extracted.clean
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory event queue for heartbeat context injection.
|
|
3
|
+
* Events are accumulated between heartbeat ticks and drained into heartbeat prompts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface SystemEvent {
|
|
7
|
+
text: string
|
|
8
|
+
timestamp: number
|
|
9
|
+
contextKey?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MAX_EVENTS_PER_SESSION = 20
|
|
13
|
+
|
|
14
|
+
const globalKey = '__swarmclaw_system_events__' as const
|
|
15
|
+
const globalScope = globalThis as typeof globalThis & { [globalKey]?: Map<string, SystemEvent[]> }
|
|
16
|
+
const queues: Map<string, SystemEvent[]> = globalScope[globalKey] ?? (globalScope[globalKey] = new Map())
|
|
17
|
+
|
|
18
|
+
/** Push an event for a session. Deduplicates consecutive identical text, caps at MAX_EVENTS_PER_SESSION. */
|
|
19
|
+
export function enqueueSystemEvent(sessionId: string, text: string, contextKey?: string): void {
|
|
20
|
+
let queue = queues.get(sessionId)
|
|
21
|
+
if (!queue) {
|
|
22
|
+
queue = []
|
|
23
|
+
queues.set(sessionId, queue)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Deduplicate consecutive identical text
|
|
27
|
+
const last = queue[queue.length - 1]
|
|
28
|
+
if (last && last.text === text) return
|
|
29
|
+
|
|
30
|
+
queue.push({ text, timestamp: Date.now(), contextKey })
|
|
31
|
+
|
|
32
|
+
// Cap at max
|
|
33
|
+
if (queue.length > MAX_EVENTS_PER_SESSION) {
|
|
34
|
+
queue.splice(0, queue.length - MAX_EVENTS_PER_SESSION)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Destructive read — returns and clears all events for a session. */
|
|
39
|
+
export function drainSystemEvents(sessionId: string): SystemEvent[] {
|
|
40
|
+
const queue = queues.get(sessionId)
|
|
41
|
+
if (!queue || queue.length === 0) return []
|
|
42
|
+
queues.delete(sessionId)
|
|
43
|
+
return queue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Non-destructive read — returns current events without clearing. */
|
|
47
|
+
export function peekSystemEvents(sessionId: string): SystemEvent[] {
|
|
48
|
+
return queues.get(sessionId) || []
|
|
49
|
+
}
|
package/src/lib/server/ws-hub.ts
CHANGED
|
@@ -71,6 +71,17 @@ export function initWsServer() {
|
|
|
71
71
|
console.log(`[ws-hub] WebSocket server listening on port ${port}`)
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function closeWsServer(): Promise<void> {
|
|
75
|
+
const hub = getHub()
|
|
76
|
+
if (!hub) return Promise.resolve()
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
for (const client of hub.clients) {
|
|
79
|
+
client.ws.close(1001, 'Server shutting down')
|
|
80
|
+
}
|
|
81
|
+
hub.wss.close(() => resolve())
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
export function notify(topic: string, action = 'update', id?: string) {
|
|
75
86
|
const hub = getHub()
|
|
76
87
|
if (!hub) return
|